diff --git a/src/App.jsx b/src/App.jsx
index 40ba2d4..3923a47 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -15,6 +15,7 @@ const ProtectedRoute = React.lazy(() => import('./admin/auth/ProtectedRoute'));
const AdminLayout = React.lazy(() => import('./admin/layout/AdminLayout'));
const KanbanBoard = React.lazy(() => import('./admin/orders/KanbanBoard'));
const AnalyticsDashboard = React.lazy(() => import('./admin/analytics/AnalyticsDashboard'));
+const AnalyticsPage = React.lazy(() => import('./admin/analytics/AnalyticsPage'));
const CompletedOrdersTable = React.lazy(() => import('./admin/analytics/CompletedOrdersTable'));
const UserManagementPage = React.lazy(() => import('./admin/users/UserManagementPage'));
const SettingsPage = React.lazy(() => import('./admin/settings/SettingsPage'));
@@ -52,7 +53,8 @@ function App() {
}>
}>
} />
- } />
+ } />
+ } />
} />
} />
} />
diff --git a/src/admin/analytics/AnalyticsPage.jsx b/src/admin/analytics/AnalyticsPage.jsx
new file mode 100644
index 0000000..996cbd2
--- /dev/null
+++ b/src/admin/analytics/AnalyticsPage.jsx
@@ -0,0 +1,343 @@
+import { useMemo } from 'react';
+import { TrendingUp } from 'lucide-react';
+import { useAnalyticsPage } from './useAnalyticsPage';
+
+const MONTHS_RU = ['Янв', 'Фев', 'Мар', 'Апр', 'Май', 'Июн', 'Июл', 'Авг', 'Сен', 'Окт', 'Ноя', 'Дек'];
+
+const STATUS_COLORS = {
+ 'новая': '#3b82f6',
+ 'в работе': '#f59e0b',
+ 'завершена': '#10b981',
+ 'отменена': '#6b7280',
+};
+
+const STATUS_LIST = ['новая', 'в работе', 'завершена', 'отменена'];
+
+function formatRub(value) {
+ if (!value || value === 0) return '₽ 0';
+ return '₽ ' + Math.round(value).toLocaleString('ru-RU');
+}
+
+function getLast6Months() {
+ const months = [];
+ const now = new Date();
+ for (let i = 5; i >= 0; i--) {
+ const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
+ months.push({ key: `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`, label: MONTHS_RU[d.getMonth()] });
+ }
+ return months;
+}
+
+const SummaryCard = ({ label, value }) => (
+
+
+ {label}
+
+
+ {value}
+
+
+);
+
+const AnalyticsPage = () => {
+ const { orders, loading } = useAnalyticsPage();
+
+ const metrics = useMemo(() => {
+ const total = orders.length;
+ const completed = orders.filter((o) => o.status === 'завершена');
+ const completedCount = completed.length;
+ const revenue = completed.reduce((sum, o) => sum + (Number(o.final_cost) || 0), 0);
+ const avg = completedCount > 0 ? revenue / completedCount : 0;
+ return { total, completedCount, revenue, avg };
+ }, [orders]);
+
+ const statusCounts = useMemo(() => {
+ const counts = {};
+ STATUS_LIST.forEach((s) => { counts[s] = 0; });
+ orders.forEach((o) => {
+ if (counts[o.status] !== undefined) counts[o.status]++;
+ });
+ return counts;
+ }, [orders]);
+
+ const maxStatusCount = useMemo(() => Math.max(...Object.values(statusCounts), 1), [statusCounts]);
+
+ const revenueByMonth = useMemo(() => {
+ const months = getLast6Months();
+ const map = {};
+ months.forEach((m) => { map[m.key] = 0; });
+ orders
+ .filter((o) => o.status === 'завершена')
+ .forEach((o) => {
+ if (!o.created_at) return;
+ const d = new Date(o.created_at);
+ const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`;
+ if (map[key] !== undefined) {
+ map[key] += Number(o.final_cost) || 0;
+ }
+ });
+ return months.map((m) => ({ ...m, revenue: map[m.key] }));
+ }, [orders]);
+
+ const maxMonthRevenue = useMemo(() => Math.max(...revenueByMonth.map((m) => m.revenue), 1), [revenueByMonth]);
+
+ const managerStats = useMemo(() => {
+ const map = {};
+ orders.forEach((o) => {
+ const name = o.assigned_to || 'Не назначен';
+ if (!map[name]) {
+ map[name] = { name, total: 0, completed: 0, revenue: 0 };
+ }
+ map[name].total++;
+ if (o.status === 'завершена') {
+ map[name].completed++;
+ map[name].revenue += Number(o.final_cost) || 0;
+ }
+ });
+ return Object.values(map).sort((a, b) => b.revenue - a.revenue);
+ }, [orders]);
+
+ if (loading) {
+ return (
+
+ Загрузка...
+
+ );
+ }
+
+ const chartPaddingTop = 30;
+ const chartPaddingBottom = 28;
+ const chartHeight = 200;
+ const innerHeight = chartHeight - chartPaddingTop - chartPaddingBottom;
+
+ return (
+
+ {/* Page title */}
+
+
+
+ Аналитика
+
+
+
+ {/* Section A — Summary cards */}
+
+
+ Сводка
+
+
+
+
+
+ 0 ? formatRub(metrics.avg) : '—'} />
+
+
+
+ {/* Section B — Orders by status */}
+
+
+ Заявки по статусам
+
+
+ {STATUS_LIST.map((status) => {
+ const count = statusCounts[status];
+ const pct = (count / maxStatusCount) * 100;
+ return (
+
+
+
+ {status}
+
+
+ {count}
+
+
+
+ );
+ })}
+
+
+
+ {/* Section C — Revenue trend (last 6 months) */}
+
+
+ Выручка за последние 6 месяцев
+
+
+
+
+ {/* Section D — Manager breakdown */}
+
+
+ По менеджерам
+
+ {managerStats.length === 0 ? (
+
Нет данных
+ ) : (
+
+
+
+
+ {['Менеджер', 'Заявок', 'Завершено', 'Выручка', 'Конверсия'].map((col) => (
+ |
+ {col}
+ |
+ ))}
+
+
+
+ {managerStats.map((m, idx) => {
+ const conversion = m.total > 0 ? Math.round((m.completed / m.total) * 100) : 0;
+ return (
+
+ |
+ {m.name}
+ |
+
+ {m.total}
+ |
+
+ {m.completed}
+ |
+
+ {formatRub(m.revenue)}
+ |
+
+ = 50 ? 'rgba(16,185,129,0.12)' : 'rgba(107,114,128,0.1)',
+ color: conversion >= 50 ? '#10b981' : '#6b7280',
+ }}>
+ {conversion}%
+
+ |
+
+ );
+ })}
+
+
+
+ )}
+
+
+ );
+};
+
+export default AnalyticsPage;
diff --git a/src/admin/analytics/useAnalyticsPage.js b/src/admin/analytics/useAnalyticsPage.js
new file mode 100644
index 0000000..83c7602
--- /dev/null
+++ b/src/admin/analytics/useAnalyticsPage.js
@@ -0,0 +1,25 @@
+import { useState, useEffect } from 'react';
+import { supabase } from '../../lib/supabase';
+
+export function useAnalyticsPage() {
+ const [orders, setOrders] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ let isMounted = true;
+ supabase
+ .from('orders')
+ .select('id, status, final_cost, created_at, assigned_to, film_thickness')
+ .then(({ data }) => {
+ if (isMounted) {
+ setOrders(data ?? []);
+ setLoading(false);
+ }
+ });
+ return () => {
+ isMounted = false;
+ };
+ }, []);
+
+ return { orders, loading };
+}