/** * @file SmsCampaignStats.jsx * @description SMS Campaign statistics with charts: * - KPI cards (sent, delivered, in transit, errors) * - Daily trend (sent vs delivered, last 14 days) * - Status donut chart * - Delivery rate */ import React, { useState, useEffect, useCallback } from "react"; import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, LineChart, Line, CartesianGrid, Legend, } from "recharts"; import { Panel } from "../UI/Panel"; import { supabase } from "../../supabaseClient"; const STATUS_COLORS = { delivered: "#22c55e", sent: "#3b82f6", checking: "#94a3b8", expired: "#eab308", send_failed: "#ef4444", error: "#ef4444", limit_exceeded: "#f97316", }; const STATUS_LABELS = { delivered: "Доставлено", sent: "Отправлено", checking: "Проверяется", expired: "Истёк срок", send_failed: "Ошибка отправки", error: "Ошибка доставки", limit_exceeded: "Лимит", }; export const SmsCampaignStats = ({ campaignType }) => { const [stats, setStats] = useState(null); const [dailyData, setDailyData] = useState([]); const [statusData, setStatusData] = useState([]); const [isLoading, setIsLoading] = useState(true); const [period, setPeriod] = useState("14d"); const loadStats = useCallback(async () => { setIsLoading(true); try { const days = period === "7d" ? 7 : period === "30d" ? 30 : period === "all" ? 90 : 14; const startDate = new Date(); startDate.setDate(startDate.getDate() - days); const startDateISO = startDate.toISOString(); const { data, error } = await supabase .from("sms_campaign_log") .select("status, sms_code, attempts, created_at, campaign_type") .eq("campaign_type", campaignType) .gte("created_at", startDateISO) .order("created_at", { ascending: true }); if (error) throw error; // KPI const total = data.length; const delivered = data.filter(d => d.status === "delivered").length; const inTransit = data.filter(d => d.status === "sent" || d.status === "checking").length; const errors = data.filter(d => ["send_failed", "error", "limit_exceeded"].includes(d.status)).length; const expired = data.filter(d => d.status === "expired").length; const deliveryRate = total > 0 ? Math.round((delivered / total) * 100) : 0; setStats({ total, delivered, inTransit, errors, expired, deliveryRate }); // Daily data const byDay = {}; data.forEach(d => { const day = new Date(d.created_at).toLocaleDateString("ru-RU", { day: "2-digit", month: "2-digit" }); if (!byDay[day]) byDay[day] = { date: day, sent: 0, delivered: 0, errors: 0 }; byDay[day].sent++; if (d.status === "delivered") byDay[day].delivered++; if (["send_failed", "error", "limit_exceeded"].includes(d.status)) byDay[day].errors++; }); setDailyData(Object.values(byDay)); // Status pie const byStatus = {}; data.forEach(d => { byStatus[d.status] = (byStatus[d.status] || 0) + 1; }); setStatusData(Object.entries(byStatus).map(([name, value]) => ({ name: STATUS_LABELS[name] || name, value, color: STATUS_COLORS[name] || "#94a3b8", }))); } catch (e) { console.error("Stats error:", e); } finally { setIsLoading(false); } }, [campaignType, period]); useEffect(() => { loadStats(); }, [loadStats]); if (isLoading) { return (
Загрузка статистики…
); } if (!stats || stats.total === 0) { return (
Нет данных для статистики
); } return (
{/* ── Period selector ──────────────────────────────────────────────── */}
{[ { key: "7d", label: "7 дней" }, { key: "14d", label: "14 дней" }, { key: "30d", label: "30 дней" }, { key: "all", label: "Всё время" }, ].map(p => ( ))}
{/* ── KPI cards ────────────────────────────────────────────────────── */}
{/* ── Charts ───────────────────────────────────────────────────────── */}
{/* Daily trend */}
📈 Отправки по дням
{/* Status donut */}
🍩 По статусам
{statusData.map((entry, i) => ( ))}
{/* ── Delivery rate trend ──────────────────────────────────────────── */} {dailyData.length > 1 && (
📊 Конверсия доставки по дням
({ date: d.date, rate: d.sent > 0 ? Math.round((d.delivered / d.sent) * 100) : 0, }))} margin={{ top: 5, right: 10, left: -20, bottom: 5 }} > [`${v}%`, "Конверсия"]} />
)}
); }; // ── KPI Card ───────────────────────────────────────────────────────────────── const KpiCard = ({ label, value, color }) => (
{label}
{value}
);