diff --git a/app/(dashboard)/analytics/target-channels/page.tsx b/app/(dashboard)/analytics/target-channels/page.tsx index 118c923..c8db270 100644 --- a/app/(dashboard)/analytics/target-channels/page.tsx +++ b/app/(dashboard)/analytics/target-channels/page.tsx @@ -5,6 +5,23 @@ import { DashboardHeader } from "@/components/layout/dashboard-header"; import { Button } from "@/components/ui/button"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { ChartConfigPanel } from "@/components/analytics/chart-config-panel"; +import { + DndContext, + closestCenter, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, + DragEndEvent, +} from "@dnd-kit/core"; +import { + arrayMove, + SortableContext, + sortableKeyboardCoordinates, + useSortable, + verticalListSortingStrategy, +} from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; import { Table, TableBody, @@ -37,8 +54,62 @@ interface ChartData { }; data: SpendingAnalytics | null; loading: boolean; + width: "full" | "half"; } +// Компонент для sortable item +interface SortableChartItemProps { + chart: ChartData; + index: number; + channels: TargetChannel[]; + chartsLength: number; + onBuild: (chartId: string, config: ChartData["config"]) => void; + onRemove: (chartId: string) => void; + onWidthChange: (chartId: string, width: "full" | "half") => void; +} + +const SortableChartItem: React.FC = ({ + chart, + index, + channels, + chartsLength, + onBuild, + onRemove, + onWidthChange, +}) => { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: chart.id }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + }; + + return ( +
+ onBuild(chart.id, config)} + onRemove={chartsLength > 1 ? () => onRemove(chart.id) : undefined} + onWidthChange={(width) => onWidthChange(chart.id, width)} + showRemoveButton={chartsLength > 1} + chartNumber={index + 1} + dragHandleProps={{ ...attributes, ...listeners }} + /> +
+ ); +}; + export default function TargetChannelsAnalyticsPage() { const [channels, setChannels] = useState([]); const [charts, setCharts] = useState([ @@ -53,8 +124,16 @@ export default function TargetChannelsAnalyticsPage() { }, data: null, loading: false, + width: "full", }, ]); + + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -191,6 +270,7 @@ export default function TargetChannelsAnalyticsPage() { }, data: null, loading: false, + width: "full", }, ]); }; @@ -203,6 +283,27 @@ export default function TargetChannelsAnalyticsPage() { setCharts(charts.filter((c) => c.id !== chartId)); }; + const handleWidthChange = (chartId: string, width: "full" | "half") => { + setCharts( + charts.map((chart) => + chart.id === chartId ? { ...chart, width } : chart + ) + ); + }; + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + + if (over && active.id !== over.id) { + setCharts((items) => { + const oldIndex = items.findIndex((item) => item.id === active.id); + const newIndex = items.findIndex((item) => item.id === over.id); + + return arrayMove(items, oldIndex, newIndex); + }); + } + }; + // Собираем все данные для таблицы const getAllTableData = (): SpendingDataPoint[] => { const allData: SpendingDataPoint[] = []; @@ -294,24 +395,31 @@ export default function TargetChannelsAnalyticsPage() { ) : ( <> {/* Графики */} -
- {charts.map((chart, index) => ( - handleBuildChart(chart.id, config)} - onRemove={ - charts.length > 1 - ? () => handleRemoveChart(chart.id) - : undefined - } - showRemoveButton={charts.length > 1} - chartNumber={index + 1} - /> - ))} -
+ + c.id)} + strategy={verticalListSortingStrategy} + > +
+ {charts.map((chart, index) => ( + + ))} +
+
+
{/* Таблица */} {tableData.length > 0 && ( diff --git a/components/analytics/chart-config-panel.tsx b/components/analytics/chart-config-panel.tsx index 71cc40c..ad6717e 100644 --- a/components/analytics/chart-config-panel.tsx +++ b/components/analytics/chart-config-panel.tsx @@ -28,7 +28,16 @@ import { Switch } from "@/components/ui/switch"; import { cn } from "@/lib/utils"; import { format } from "date-fns"; import { ru } from "date-fns/locale"; -import { CalendarIcon, X, TrendingUp, BarChart3, LineChart } from "lucide-react"; +import { + CalendarIcon, + X, + TrendingUp, + BarChart3, + LineChart, + GripVertical, + Maximize2, + Columns2, +} from "lucide-react"; import type { TargetChannel, AnalyticsMetric, @@ -41,6 +50,7 @@ interface ChartConfigPanelProps { channels: TargetChannel[]; data: SpendingAnalytics | null; loading: boolean; + width: "full" | "half"; onBuild: (config: { targetChannelIds: string[]; metrics: AnalyticsMetric[]; @@ -49,8 +59,10 @@ interface ChartConfigPanelProps { grouping: DateGrouping; }) => void; onRemove?: () => void; + onWidthChange?: (width: "full" | "half") => void; showRemoveButton?: boolean; chartNumber?: number; + dragHandleProps?: any; } const METRICS: { value: AnalyticsMetric; label: string }[] = [ @@ -73,10 +85,13 @@ export const ChartConfigPanel: React.FC = ({ channels, data, loading, + width, onBuild, onRemove, + onWidthChange, showRemoveButton = false, chartNumber = 1, + dragHandleProps, }) => { const [selectedChannelIds, setSelectedChannelIds] = useState([]); const [selectedMetrics, setSelectedMetrics] = useState([]); @@ -85,11 +100,11 @@ export const ChartConfigPanel: React.FC = ({ const [grouping, setGrouping] = useState("day"); const [chartType, setChartType] = useState<"line" | "bar">("line"); const [showDataLabels, setShowDataLabels] = useState(false); - + // Отслеживание изменений настроек const [lastBuiltConfig, setLastBuiltConfig] = useState(null); const [isBuilt, setIsBuilt] = useState(false); - + // Текущая конфигурация как строка для сравнения const currentConfigString = JSON.stringify({ selectedChannelIds, @@ -98,7 +113,7 @@ export const ChartConfigPanel: React.FC = ({ dateTo: dateTo?.toISOString(), grouping, }); - + // Проверяем, изменились ли настройки const hasChanges = isBuilt && lastBuiltConfig !== currentConfigString; @@ -131,35 +146,72 @@ export const ChartConfigPanel: React.FC = ({ dateTo: dateTo || null, grouping, }); - + setLastBuiltConfig(currentConfigString); setIsBuilt(true); }; - + const getButtonText = () => { if (loading) return "Загрузка..."; if (!isBuilt) return "Построить график"; if (hasChanges) return "Обновить график"; return "График построен"; }; - + const isButtonDisabled = loading || (isBuilt && !hasChanges); return ( -
-
- График #{chartNumber} - - Настройте параметры и постройте график - +
+
+ {/* Drag handle */} +
+ +
+
+ График #{chartNumber} + + Настройте параметры и постройте график + +
+
+ +
+ {/* Width toggle */} + {onWidthChange && ( +
+ + +
+ )} + + {/* Remove button */} + {showRemoveButton && onRemove && ( + + )}
- {showRemoveButton && onRemove && ( - - )}
@@ -170,10 +222,7 @@ export const ChartConfigPanel: React.FC = ({ -
@@ -388,4 +436,3 @@ export const ChartConfigPanel: React.FC = ({ ); }; -