import React, { useState, useEffect } from 'react'; import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, Legend, LineChart, Line, CartesianGrid, } from 'recharts'; import { Panel } from '../UI/Panel'; import { Badge } from '../UI/Badge'; import { SegmentedTabs } from '../UI/SegmentedTabs'; import { useAdminStats } from '../../hooks/useAdminStats'; const useIsMobile = () => { const [mobile, setMobile] = useState(false); useEffect(() => { const mq = window.matchMedia('(max-width: 640px)'); setMobile(mq.matches); const handler = (e) => setMobile(e.matches); mq.addEventListener('change', handler); return () => mq.removeEventListener('change', handler); }, []); return mobile; }; const STATUS_COLORS = { pending_confirmation: '#94a3b8', manual_confirmation_required: '#eab308', agreed: '#22c55e', driver_assigned: '#3b82f6', loaded: '#6366f1', on_route: '#8b5cf6', delivered: '#10b981', paid_storage: '#06b6d4', problem: '#ef4444', cancelled: '#64748b', }; const STATUS_LABELS = { pending_confirmation: 'Ожидает подтверждения', manual_confirmation_required: 'Ручное подтверждение', agreed: 'Согласовано', driver_assigned: 'Водитель назначен', loaded: 'Загружено', on_route: 'В пути', delivered: 'Доставлено', paid_storage: 'Оплаченное хранение', problem: 'Проблема', cancelled: 'Отменено', }; const PERIOD_OPTIONS = [ { key: '1d', label: 'Сегодня' }, { key: '7d', label: '7 дней' }, { key: '30d', label: '30 дней' }, { key: 'all', label: 'Все' }, ]; const CustomTooltip = ({ active, payload, label: tooltipLabel }) => { if (!active || !payload?.length) return null; return (
{tooltipLabel &&
{tooltipLabel}
} {payload.map((p, i) => (
{p.name}: {p.value}
))}
); }; export const AdminDashboard = () => { const [period, setPeriod] = useState('7d'); const mobile = useIsMobile(); const { stats, statusDist, dailyTrend, driverStats, economics, isLoading, error, refetch } = useAdminStats(period); if (isLoading) { return (
Загрузка...
); } if (error) { return (
Ошибка: {error}
); } const sv = stats || {}; const totalGroups = sv.total || 0; const econ = economics || {}; const statusPieData = (statusDist || []).map(s => ({ name: STATUS_LABELS[s.delivery_status] || s.delivery_status, value: s.count, status: s.delivery_status, })).filter(d => d.value > 0); const trendData = (dailyTrend || []).map(d => ({ date: d.date ? new Date(d.date).toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit' }) : '', delivered: d.delivered || 0, total: d.total || 0, problems: d.problems || 0, })); const driverData = (driverStats || []).map(d => ({ name: d.driver_name || 'Неизвестный', total: d.total || 0, delivered: d.delivered || 0, problems: d.problems || 0, })); // Funnel: ALWAYS show all steps, even with 0 values const funnelSteps = [ { label: 'Согласовано после 1-й SMS', value: econ.confirmed_after_sms1 || 0, color: '#22c55e' }, { label: 'Согласовано после 2-й SMS', value: econ.confirmed_after_sms2 || 0, color: '#14b8a6' }, { label: 'Согласовано вручную', value: econ.confirmed_via_manual || 0, color: '#eab308' }, { label: 'Ручное назначение даты', value: econ.manual_date_set_count || 0, color: '#f97316' }, { label: 'Платное хранение', value: econ.paid_storage_count || 0, color: '#06b6d4' }, { label: 'Отмена', value: econ.cancelled_count || 0, color: '#ef4444' }, ]; // Responsive values const chartHeight = mobile ? 200 : 240; const kpiMin = mobile ? '80px' : '110px'; const chartGridCols = mobile ? '1fr' : '1fr 2fr'; const driverLabelWidth = mobile ? 80 : 120; const fontSize = mobile ? { xs: '0.6rem', s: '0.7rem', m: '0.78rem', l: '0.85rem', xl: '1rem' } : { xs: '0.65rem', s: '0.68rem', m: '0.78rem', l: '0.85rem', xl: '1.1rem' }; return (
{/* Period selector */}

Аналитика

Статистика по доставкам

{/* KPI — centered on mobile */}
{[ { label: 'Всего', val: totalGroups }, { label: 'Ожидает', val: sv.pending }, { label: 'В работе', val: sv.in_progress }, { label: 'Доставлено', val: sv.delivered }, { label: 'Проблемы', val: sv.problem }, { label: '% доставки', val: sv.delivery_rate != null ? sv.delivery_rate + '%' : '—' }, ].map((kpi, i) => (
{kpi.label}
{kpi.val ?? '—'}
))}
{/* Pie + Line — stacked on mobile, side-by-side on desktop */}

По статусам

{statusPieData.length === 0 ? (
Нет данных
) : ( {statusPieData.map(entry => ( ))} } /> )}

Тренд по дням

{trendData.length === 0 ? (
Нет данных
) : ( } /> )}
{/* Status table */}

Все статусы

{statusPieData.length === 0 ? (
Нет данных
) : (
Статус
Кол-во
Доля
{statusPieData.map(s => { const pct = totalGroups > 0 ? ((s.value / totalGroups) * 100).toFixed(1) : 0; return (
{s.name}
{s.value}
{pct}%
); })}
)} {/* Воронка согласования — ALL steps always visible */}

Воронка согласования

{totalGroups === 0 ? (
Нет данных
) : (
{funnelSteps.map((step, i) => { const pct = totalGroups > 0 ? Math.round((step.value / totalGroups) * 100) : 0; const widthPct = step.value > 0 ? Math.max(15, (step.value / totalGroups) * 100) : 15; return (
{step.value}
0 ? step.color : 'var(--color-border, #334155)', borderRadius: '4px', display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'width 0.4s ease', minWidth: '40px', maxWidth: '100%', opacity: step.value > 0 ? 1 : 0.5, }}> 0 ? '#fff' : 'var(--color-text-muted)', textShadow: step.value > 0 ? '0 1px 2px rgba(0,0,0,0.3)' : 'none' }}> {pct}%
{step.label}
{i < funnelSteps.length - 1 && (
)}
); })}
Автосогласование
{econ.auto_confirm_pct ?? 0}%
Ручное вмешательство
{econ.manual_intervention_pct ?? 0}%
Всего согласовано
{econ.confirmed_auto_total ?? 0}
)} {/* SMS */}

SMS

{[ { label: 'SMS 1', val: econ.sms1_sent_count ?? 0 }, { label: 'SMS 2', val: econ.sms2_sent_count ?? 0 }, { label: 'Всего', val: (econ.sms1_sent_count || 0) + (econ.sms2_sent_count || 0) }, ].map((item, i) => (
{item.label}
{item.val}
))}
{/* Drivers */}

По водителям

{driverData.length === 0 ? (
Нет данных
) : ( } /> )}
); };