feat: update analitycs
This commit is contained in:
391
components/analytics/chart-config-panel.tsx
Normal file
391
components/analytics/chart-config-panel.tsx
Normal file
@@ -0,0 +1,391 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
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 type {
|
||||
TargetChannel,
|
||||
AnalyticsMetric,
|
||||
DateGrouping,
|
||||
} from "@/lib/types/api";
|
||||
import { AnalyticsChart } from "./analytics-chart";
|
||||
import type { SpendingAnalytics } from "@/lib/types/api";
|
||||
|
||||
interface ChartConfigPanelProps {
|
||||
channels: TargetChannel[];
|
||||
data: SpendingAnalytics | null;
|
||||
loading: boolean;
|
||||
onBuild: (config: {
|
||||
targetChannelIds: string[];
|
||||
metrics: AnalyticsMetric[];
|
||||
dateFrom: Date | null;
|
||||
dateTo: Date | null;
|
||||
grouping: DateGrouping;
|
||||
}) => void;
|
||||
onRemove?: () => void;
|
||||
showRemoveButton?: boolean;
|
||||
chartNumber?: number;
|
||||
}
|
||||
|
||||
const METRICS: { value: AnalyticsMetric; label: string }[] = [
|
||||
{ value: "cost", label: "Расходы" },
|
||||
{ value: "subscriptions", label: "Подписки" },
|
||||
{ value: "views", label: "Просмотры" },
|
||||
{ value: "cpf", label: "CPF (Cost Per Follow)" },
|
||||
{ value: "cpm", label: "CPM (Cost Per Mille)" },
|
||||
];
|
||||
|
||||
const GROUPINGS: { value: DateGrouping; label: string }[] = [
|
||||
{ value: "day", label: "По дням" },
|
||||
{ value: "week", label: "По неделям" },
|
||||
{ value: "month", label: "По месяцам" },
|
||||
{ value: "quarter", label: "По кварталам" },
|
||||
{ value: "year", label: "По годам" },
|
||||
];
|
||||
|
||||
export const ChartConfigPanel: React.FC<ChartConfigPanelProps> = ({
|
||||
channels,
|
||||
data,
|
||||
loading,
|
||||
onBuild,
|
||||
onRemove,
|
||||
showRemoveButton = false,
|
||||
chartNumber = 1,
|
||||
}) => {
|
||||
const [selectedChannelIds, setSelectedChannelIds] = useState<string[]>([]);
|
||||
const [selectedMetrics, setSelectedMetrics] = useState<AnalyticsMetric[]>([]);
|
||||
const [dateFrom, setDateFrom] = useState<Date | undefined>();
|
||||
const [dateTo, setDateTo] = useState<Date | undefined>();
|
||||
const [grouping, setGrouping] = useState<DateGrouping>("day");
|
||||
const [chartType, setChartType] = useState<"line" | "bar">("line");
|
||||
const [showDataLabels, setShowDataLabels] = useState(false);
|
||||
|
||||
// Отслеживание изменений настроек
|
||||
const [lastBuiltConfig, setLastBuiltConfig] = useState<string | null>(null);
|
||||
const [isBuilt, setIsBuilt] = useState(false);
|
||||
|
||||
// Текущая конфигурация как строка для сравнения
|
||||
const currentConfigString = JSON.stringify({
|
||||
selectedChannelIds,
|
||||
selectedMetrics,
|
||||
dateFrom: dateFrom?.toISOString(),
|
||||
dateTo: dateTo?.toISOString(),
|
||||
grouping,
|
||||
});
|
||||
|
||||
// Проверяем, изменились ли настройки
|
||||
const hasChanges = isBuilt && lastBuiltConfig !== currentConfigString;
|
||||
|
||||
const toggleChannel = (channelId: string) => {
|
||||
setSelectedChannelIds((prev) =>
|
||||
prev.includes(channelId)
|
||||
? prev.filter((id) => id !== channelId)
|
||||
: [...prev, channelId]
|
||||
);
|
||||
};
|
||||
|
||||
const toggleMetric = (metric: AnalyticsMetric) => {
|
||||
setSelectedMetrics((prev) =>
|
||||
prev.includes(metric)
|
||||
? prev.filter((m) => m !== metric)
|
||||
: [...prev, metric]
|
||||
);
|
||||
};
|
||||
|
||||
const handleBuild = () => {
|
||||
if (selectedChannelIds.length === 0 || selectedMetrics.length === 0) {
|
||||
alert("Выберите хотя бы один канал и одну метрику");
|
||||
return;
|
||||
}
|
||||
|
||||
onBuild({
|
||||
targetChannelIds: selectedChannelIds,
|
||||
metrics: selectedMetrics,
|
||||
dateFrom: dateFrom || null,
|
||||
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 (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>График #{chartNumber}</CardTitle>
|
||||
<CardDescription className="mt-1">
|
||||
Настройте параметры и постройте график
|
||||
</CardDescription>
|
||||
</div>
|
||||
{showRemoveButton && onRemove && (
|
||||
<Button variant="ghost" size="sm" onClick={onRemove}>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Настройки */}
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
{/* Выбор каналов */}
|
||||
<div className="space-y-2">
|
||||
<Label>Целевые каналы</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-start"
|
||||
>
|
||||
{selectedChannelIds.length === 0
|
||||
? "Выберите каналы"
|
||||
: `Выбрано: ${selectedChannelIds.length}`}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-64 p-3" align="start">
|
||||
<div className="space-y-2">
|
||||
{channels.map((channel) => (
|
||||
<div
|
||||
key={channel.id}
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<Checkbox
|
||||
id={`channel-${channel.id}`}
|
||||
checked={selectedChannelIds.includes(channel.id)}
|
||||
onCheckedChange={() => toggleChannel(channel.id)}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`channel-${channel.id}`}
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
|
||||
>
|
||||
{channel.title}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
{/* Выбор метрик */}
|
||||
<div className="space-y-2">
|
||||
<Label>Метрики</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-start"
|
||||
>
|
||||
{selectedMetrics.length === 0
|
||||
? "Выберите метрики"
|
||||
: `Выбрано: ${selectedMetrics.length}`}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-64 p-3" align="start">
|
||||
<div className="space-y-2">
|
||||
{METRICS.map((metric) => (
|
||||
<div
|
||||
key={metric.value}
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<Checkbox
|
||||
id={`metric-${metric.value}`}
|
||||
checked={selectedMetrics.includes(metric.value)}
|
||||
onCheckedChange={() => toggleMetric(metric.value)}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`metric-${metric.value}`}
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
|
||||
>
|
||||
{metric.label}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
{/* Период от */}
|
||||
<div className="space-y-2">
|
||||
<Label>Дата от</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-start text-left font-normal",
|
||||
!dateFrom && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||
{dateFrom ? (
|
||||
format(dateFrom, "d MMM yyyy", { locale: ru })
|
||||
) : (
|
||||
<span>Выберите</span>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={dateFrom}
|
||||
onSelect={setDateFrom}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
{/* Период до */}
|
||||
<div className="space-y-2">
|
||||
<Label>Дата до</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-start text-left font-normal",
|
||||
!dateTo && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||
{dateTo ? (
|
||||
format(dateTo, "d MMM yyyy", { locale: ru })
|
||||
) : (
|
||||
<span>Выберите</span>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={dateTo}
|
||||
onSelect={setDateTo}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Группировка */}
|
||||
<div className="flex-1 space-y-2">
|
||||
<Label>Группировка</Label>
|
||||
<Select
|
||||
value={grouping}
|
||||
onValueChange={(v) => setGrouping(v as DateGrouping)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{GROUPINGS.map((g) => (
|
||||
<SelectItem key={g.value} value={g.value}>
|
||||
{g.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Тип графика */}
|
||||
<div className="space-y-2">
|
||||
<Label>Тип</Label>
|
||||
<div className="flex gap-1 border rounded-md">
|
||||
<Button
|
||||
variant={chartType === "line" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => setChartType("line")}
|
||||
className="rounded-r-none"
|
||||
>
|
||||
<LineChart className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={chartType === "bar" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => setChartType("bar")}
|
||||
className="rounded-l-none"
|
||||
>
|
||||
<BarChart3 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Подписи значений */}
|
||||
<div className="space-y-2">
|
||||
<Label>Подписи</Label>
|
||||
<div className="flex items-center space-x-2 h-9">
|
||||
<Switch
|
||||
id="show-data-labels"
|
||||
checked={showDataLabels}
|
||||
onCheckedChange={setShowDataLabels}
|
||||
/>
|
||||
<Label htmlFor="show-data-labels" className="cursor-pointer">
|
||||
{showDataLabels ? "Вкл" : "Выкл"}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Кнопка построения */}
|
||||
<div className="space-y-2">
|
||||
<Label className="invisible">Построить</Label>
|
||||
<Button onClick={handleBuild} disabled={isButtonDisabled}>
|
||||
<TrendingUp className={cn("mr-2 h-4 w-4", loading && "animate-pulse")} />
|
||||
{getButtonText()}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* График */}
|
||||
{data && (
|
||||
<AnalyticsChart
|
||||
data={data.chart_data}
|
||||
metrics={selectedMetrics}
|
||||
chartType={chartType}
|
||||
showDataLabels={showDataLabels}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user