diff --git a/src/components/admin/AdminDashboard.jsx b/src/components/admin/AdminDashboard.jsx index bb703c2..ca0bd26 100644 --- a/src/components/admin/AdminDashboard.jsx +++ b/src/components/admin/AdminDashboard.jsx @@ -7,6 +7,8 @@ import { Panel } from '../UI/Panel'; import { Badge } from '../UI/Badge'; import { SegmentedTabs } from '../UI/SegmentedTabs'; import { useAdminStats } from '../../hooks/useAdminStats'; +import { usePickupStats } from '../../hooks/usePickupStats'; +import { PickupStatsPanel } from './PickupStatsPanel'; const useIsMobile = () => { const [mobile, setMobile] = useState(false); @@ -31,6 +33,7 @@ const STATUS_COLORS = { paid_storage: '#06b6d4', problem: '#ef4444', cancelled: '#64748b', + pickup: '#f59e0b', }; const STATUS_LABELS = { @@ -44,6 +47,7 @@ const STATUS_LABELS = { paid_storage: 'Оплаченное хранение', problem: 'Проблема', cancelled: 'Отменено', + pickup: 'Самовывоз', }; const PERIOD_OPTIONS = [ @@ -74,6 +78,7 @@ export const AdminDashboard = () => { const [period, setPeriod] = useState('7d'); const mobile = useIsMobile(); const { stats, statusDist, dailyTrend, driverStats, economics, isLoading, error, refetch } = useAdminStats(period); + const { stats: pickupStats, isLoading: pickupLoading } = usePickupStats(period); if (isLoading) { return ( @@ -309,6 +314,9 @@ export const AdminDashboard = () => { + {/* Pickup Stats */} + + {/* Drivers */}

По водителям

diff --git a/src/components/admin/PickupStatsPanel.jsx b/src/components/admin/PickupStatsPanel.jsx new file mode 100644 index 0000000..a25ff2a --- /dev/null +++ b/src/components/admin/PickupStatsPanel.jsx @@ -0,0 +1,167 @@ +import React from 'react'; +import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer } from 'recharts'; +import { Panel } from '../UI/Panel'; + +const PICKUP_COLORS = { + today: '#22c55e', + tomorrow: '#3b82f6', + dayAfter: '#8b5cf6', + firstHalf: '#f59e0b', + secondHalf: '#ef4444', + saturday: '#06b6d4', + pickup: '#f59e0b', + delivery: '#6366f1', +}; + +const CustomTooltip = ({ active, payload }) => { + if (!active || !payload?.length) return null; + return ( +
+ {payload.map((p, i) => ( +
+ {p.name}: {p.value} +
+ ))} +
+ ); +}; + +const ProgressBar = ({ label, value, max, color, fontSize, mobile }) => { + const pct = max > 0 ? Math.max(0, (value / max) * 100) : 0; + return ( +
+
{label}
+
+
0 ? '4px' : '0' }} /> +
+
{value}
+
+ ); +}; + +export const PickupStatsPanel = ({ stats, isLoading, mobile, fontSize }) => { + if (isLoading) { + return ( + +
+ Загрузка статистики самовывоза... +
+
+ ); + } + + if (!stats) { + return ( + +
+ Нет данных по самовывозу +
+
+ ); + } + + const fs = fontSize || { xs: '0.65rem', s: '0.68rem', m: '0.78rem', l: '0.85rem', xl: '1.1rem' }; + const totalPickups = Number(stats.total_pickups) || 0; + const pickupRate = Number(stats.pickup_rate) || 0; + const avgDays = stats.avg_days_until_pickup != null ? Number(stats.avg_days_until_pickup) : null; + const dist = stats.delivery_type_dist || {}; + + const maxDay = Math.max( + Number(stats.pickup_today) || 0, + Number(stats.pickup_tomorrow) || 0, + Number(stats.pickup_day_after) || 0, + 1 + ); + + const maxHalf = Math.max( + Number(stats.pickup_first_half) || 0, + Number(stats.pickup_second_half) || 0, + 1 + ); + + const pieData = [ + { name: 'Самовывоз', value: Number(dist.pickup) || 0, fill: PICKUP_COLORS.pickup }, + { name: 'Доставка', value: Number(dist.delivery) || 0, fill: PICKUP_COLORS.delivery }, + ].filter(d => d.value > 0); + + return ( + +

+ 📦 Самовывоз +

+ + {/* KPI row */} +
+ {[ + { label: 'Всего самовывоз', val: totalPickups, color: '#f59e0b' }, + { label: 'Доля самовывоза', val: pickupRate + '%', color: '#f59e0b' }, + { label: 'Ср. дней до выдачи', val: avgDays !== null ? avgDays : '—', color: '#3b82f6' }, + ].map((kpi, i) => ( +
+
{kpi.label}
+
{kpi.val}
+
+ ))} +
+ + {/* Distribution by day */} +
+
По дням
+
+ + + +
+
+ + {/* Half-day split */} +
+
По времени
+
+ + +
+
+ + {/* Saturday */} +
+ Самовывоз в субботу + {Number(stats.pickup_on_saturday) || 0} +
+ + {/* Delivery vs Pickup donut */} + {pieData.length > 0 && ( +
+
Доставка vs Самовывоз
+ + + + {pieData.map((entry, i) => ( + + ))} + + } /> + + +
+ {pieData.map((d, i) => ( +
+
+ {d.name}: {d.value} +
+ ))} +
+
+ )} + + ); +}; diff --git a/src/hooks/usePickupStats.js b/src/hooks/usePickupStats.js new file mode 100644 index 0000000..5cb31a1 --- /dev/null +++ b/src/hooks/usePickupStats.js @@ -0,0 +1,31 @@ +import { useState, useEffect, useCallback } from "react"; +import { supabase, hasSupabaseConfig } from "../supabaseClient"; + +const PERIOD_DAYS = { today: 1, "7d": 7, "30d": 30, all: 0 }; + +export const usePickupStats = (period = "7d") => { + const [stats, setStats] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + const days = PERIOD_DAYS[period] ?? 7; + + const fetchStats = useCallback(async () => { + setIsLoading(true); + setError(null); + try { + if (!hasSupabaseConfig || !supabase) throw new Error("Supabase не сконфигурирован"); + const { data, error: rpcError } = await supabase.rpc("admin_pickup_stats", { p_days: days, p_date_from: null, p_date_to: null }); + if (rpcError) throw rpcError; + setStats(data?.[0] || null); + } catch (e) { + setError(e.message || "Ошибка загрузки статистики самовывоза"); + } finally { + setIsLoading(false); + } + }, [days]); + + useEffect(() => { fetchStats(); }, [fetchStats]); + + return { stats, isLoading, error, refetch: fetchStats }; +};