diff --git a/src/App.jsx b/src/App.jsx index 9a2b647..282c07a 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -18,6 +18,7 @@ const AnalyticsDashboard = React.lazy(() => import('./admin/analytics/AnalyticsD const CompletedOrdersTable = React.lazy(() => import('./admin/analytics/CompletedOrdersTable')); const UserManagementPage = React.lazy(() => import('./admin/users/UserManagementPage')); const SettingsPage = React.lazy(() => import('./admin/settings/SettingsPage')); +const ClientsPage = React.lazy(() => import('./admin/clients/ClientsPage')); function PublicSite() { return ( @@ -52,6 +53,7 @@ function App() { } /> } /> } /> + } /> diff --git a/src/admin/clients/ClientsPage.jsx b/src/admin/clients/ClientsPage.jsx new file mode 100644 index 0000000..1b01f19 --- /dev/null +++ b/src/admin/clients/ClientsPage.jsx @@ -0,0 +1,240 @@ +import { useState } from 'react'; +import { Users, ChevronDown, ChevronRight, BookUser } from 'lucide-react'; +import { useClients } from './useClients'; + +const statusColor = (status) => { + switch (status) { + case 'закрыт': return '#22c55e'; + case 'монтаж': return '#3b82f6'; + case 'новая': return 'var(--text-muted)'; + default: return 'var(--text-muted)'; + } +}; + +const formatMoney = (value) => { + if (!value) return '—'; + const num = Number(value); + if (!num) return '—'; + return num.toLocaleString('ru-RU') + ' ₽'; +}; + +const formatDate = (date) => { + if (!date) return '—'; + return new Date(date).toLocaleDateString('ru-RU'); +}; + +const thStyle = { + padding: '10px 14px', + textAlign: 'left', + fontSize: '0.72rem', + fontWeight: 700, + color: 'var(--text-muted)', + textTransform: 'uppercase', + letterSpacing: '0.4px', + borderBottom: '1px solid var(--border-light)', + whiteSpace: 'nowrap', +}; + +const tdStyle = { + padding: '12px 14px', + fontSize: '0.88rem', + color: 'var(--text-main)', + borderBottom: '1px solid rgba(255,255,255,0.04)', + verticalAlign: 'middle', +}; + +const innerThStyle = { + padding: '8px 12px', + textAlign: 'left', + fontSize: '0.70rem', + fontWeight: 700, + color: 'var(--text-muted)', + textTransform: 'uppercase', + letterSpacing: '0.4px', + borderBottom: '1px solid rgba(255,255,255,0.06)', + whiteSpace: 'nowrap', +}; + +const innerTdStyle = { + padding: '9px 12px', + fontSize: '0.83rem', + color: 'var(--text-main)', + borderBottom: '1px solid rgba(255,255,255,0.03)', + verticalAlign: 'middle', +}; + +const ClientsPage = () => { + const { clients, loading, error } = useClients(); + const [expandedPhone, setExpandedPhone] = useState(null); + + const toggleExpand = (phone) => { + setExpandedPhone((prev) => (prev === phone ? null : phone)); + }; + + if (loading) { + return
Загрузка...
; + } + + if (error) { + return
Ошибка: {error}
; + } + + const totalClients = clients.length; + const totalRevenue = clients.reduce((sum, c) => sum + c.totalSpent, 0); + + return ( +
+ {/* Page header */} +

+ + Клиенты +

+ + {/* Summary cards */} +
+
+

+ Всего клиентов +

+

+ {totalClients} +

+
+
+

+ Выручка (закрытые) +

+

+ {totalRevenue ? totalRevenue.toLocaleString('ru-RU') + ' ₽' : '—'} +

+
+
+ + {/* Clients table */} +
+ {clients.length === 0 ? ( +

Нет клиентов

+ ) : ( + + + + + + + + + + + + + {clients.map((client) => { + const isExpanded = expandedPhone === client.phone; + const lastOrder = client.orders[0]; + return ( + <> + toggleExpand(client.phone)} + style={{ + cursor: 'pointer', + background: isExpanded ? 'rgba(14, 165, 233, 0.06)' : 'transparent', + transition: 'background 0.15s', + }} + onMouseEnter={(e) => { + if (!isExpanded) e.currentTarget.style.background = 'rgba(255,255,255,0.03)'; + }} + onMouseLeave={(e) => { + if (!isExpanded) e.currentTarget.style.background = 'transparent'; + }} + > + + + + + + + + + {/* Expanded order history */} + {isExpanded && ( + + + + )} + + ); + })} + +
КлиентТелефонЗаказовПотраченоПоследний заказ
+ {isExpanded + ? + : } + + {client.name || '—'} + + {client.phone !== 'unknown' ? client.phone : '—'} + + {client.orders.length} + + {client.totalSpent ? client.totalSpent.toLocaleString('ru-RU') + ' ₽' : '—'} + + {lastOrder ? formatDate(lastOrder.created_at) : '—'} +
+ + + + + + + + + + + + {client.orders.map((order) => ( + + + + + + + + ))} + +
ДатаПлёнкаПлощадь (м²)СуммаСтатус
+ {formatDate(order.created_at)} + + {order.film_thickness ? order.film_thickness + ' мкм' : '—'} + + {order.area_sqm ?? '—'} + + {formatMoney(order.final_cost)} + + + {order.status || '—'} + +
+
+ )} +
+
+ ); +}; + +export default ClientsPage; diff --git a/src/admin/clients/useClients.js b/src/admin/clients/useClients.js new file mode 100644 index 0000000..a5e1011 --- /dev/null +++ b/src/admin/clients/useClients.js @@ -0,0 +1,38 @@ +import { useState, useEffect } from 'react'; +import { supabase } from '../../lib/supabase'; + +export function useClients() { + const [clients, setClients] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + let isMounted = true; + supabase + .from('orders') + .select('id, name, phone, film_thickness, area_sqm, final_cost, status, created_at, address') + .order('created_at', { ascending: false }) + .then(({ data, error: err }) => { + if (!isMounted) return; + if (err) { setError(err.message); setLoading(false); return; } + const map = {}; + (data || []).forEach((order) => { + const key = order.phone || 'unknown'; + if (!map[key]) { + map[key] = { phone: key, name: order.name, orders: [], totalSpent: 0 }; + } + map[key].orders.push(order); + if (order.status === 'закрыт' && order.final_cost) { + map[key].totalSpent += Number(order.final_cost); + } + }); + // Sort clients by most recent order (orders are already newest-first) + const list = Object.values(map); + setClients(list); + setLoading(false); + }); + return () => { isMounted = false; }; + }, []); + + return { clients, loading, error }; +} diff --git a/src/admin/layout/Sidebar.jsx b/src/admin/layout/Sidebar.jsx index ab8f0c7..9e2601d 100644 --- a/src/admin/layout/Sidebar.jsx +++ b/src/admin/layout/Sidebar.jsx @@ -1,5 +1,5 @@ import { NavLink, useNavigate } from 'react-router-dom'; -import { LayoutDashboard, BarChart2, CheckSquare, Shield, LogOut, Users, Settings } from 'lucide-react'; +import { LayoutDashboard, BarChart2, CheckSquare, Shield, LogOut, Users, Settings, BookUser } from 'lucide-react'; import { useAuth } from '../auth/useAuth'; const NAV_ITEMS = [ @@ -7,6 +7,7 @@ const NAV_ITEMS = [ { to: '/admin/analytics', icon: BarChart2, label: 'Аналитика' }, { to: '/admin/completed', icon: CheckSquare, label: 'Завершённые' }, { to: '/admin/users', icon: Users, label: 'Пользователи' }, + { to: '/admin/clients', icon: BookUser, label: 'Клиенты' }, { to: '/admin/settings', icon: Settings, label: 'Настройки' }, ];