diff --git a/src/admin/analytics/CompletedOrdersTable.jsx b/src/admin/analytics/CompletedOrdersTable.jsx new file mode 100644 index 0000000..30de738 --- /dev/null +++ b/src/admin/analytics/CompletedOrdersTable.jsx @@ -0,0 +1,153 @@ +import React, { useState, useEffect } from 'react'; +import { ArrowUpDown, Download } from 'lucide-react'; +import { supabase } from '../../lib/supabase'; + +const OBJECT_TYPE_LABELS = { + industrial: 'Промышленный', commercial: 'Коммерческий', + infrastructure: 'Инфраструктура', residential: 'Частный', other: 'Другое', +}; + +const COLUMNS = [ + { key: 'closed_at', label: 'Дата закрытия' }, + { key: 'name', label: 'Клиент' }, + { key: 'object_type', label: 'Тип объекта' }, + { key: 'area_sqm', label: 'Площадь (м²)' }, + { key: 'final_cost', label: 'Итоговая стоимость (₽)' }, + { key: 'assigned_to', label: 'Ответственный' }, +]; + +const CompletedOrdersTable = () => { + const [orders, setOrders] = useState([]); + const [loading, setLoading] = useState(true); + const [sortKey, setSortKey] = useState('closed_at'); + const [sortDir, setSortDir] = useState('desc'); + + useEffect(() => { + supabase + .from('orders') + .select('*') + .eq('status', 'закрыт') + .order('closed_at', { ascending: false }) + .then(({ data }) => { + setOrders(data || []); + setLoading(false); + }); + }, []); + + const handleSort = (key) => { + if (sortKey === key) { + setSortDir((d) => d === 'asc' ? 'desc' : 'asc'); + } else { + setSortKey(key); + setSortDir('asc'); + } + }; + + const sorted = [...orders].sort((a, b) => { + const av = a[sortKey] ?? ''; + const bv = b[sortKey] ?? ''; + if (av < bv) return sortDir === 'asc' ? -1 : 1; + if (av > bv) return sortDir === 'asc' ? 1 : -1; + return 0; + }); + + const exportCsv = () => { + const header = COLUMNS.map((c) => c.label).join(','); + const rows = sorted.map((o) => [ + o.closed_at ? new Date(o.closed_at).toLocaleDateString('ru-RU') : '', + `"${(o.name || '').replace(/"/g, '""')}"`, + OBJECT_TYPE_LABELS[o.object_type] || o.object_type || '', + o.area_sqm ?? '', + o.final_cost ?? '', + `"${(o.assigned_to || '').replace(/"/g, '""')}"`, + ].join(',')); + const csv = [header, ...rows].join('\n'); + const blob = new Blob(['' + csv], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `completed_orders_${new Date().toISOString().slice(0, 10)}.csv`; + a.click(); + URL.revokeObjectURL(url); + }; + + const thStyle = (key) => ({ + padding: '10px 14px', textAlign: 'left', + fontSize: '0.75rem', fontWeight: 700, + color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.4px', + cursor: 'pointer', userSelect: 'none', whiteSpace: 'nowrap', + background: sortKey === key ? 'rgba(14, 165, 233, 0.05)' : 'transparent', + }); + + const tdStyle = { + padding: '11px 14px', fontSize: '0.88rem', + color: 'var(--text-main)', borderTop: '1px solid var(--border-light)', + whiteSpace: 'nowrap', + }; + + return ( +
+
+

+ Завершённые заказы ({orders.length}) +

+ +
+ + {loading ? ( +

Загрузка...

+ ) : orders.length === 0 ? ( +
+ Нет завершённых заказов +
+ ) : ( +
+
+ + + + {COLUMNS.map((col) => ( + + ))} + + + + {sorted.map((order) => ( + e.currentTarget.style.background = 'var(--bg-lighter)'} + onMouseLeave={(e) => e.currentTarget.style.background = ''} + > + + + + + + + + ))} + +
handleSort(col.key)}> + + {col.label} + + +
+ {order.closed_at ? new Date(order.closed_at).toLocaleDateString('ru-RU') : '—'} + {order.name}{OBJECT_TYPE_LABELS[order.object_type] || order.object_type || '—'}{order.area_sqm ? `${order.area_sqm} м²` : '—'} + {order.final_cost ? `${Number(order.final_cost).toLocaleString('ru-RU')} ₽` : '—'} + {order.assigned_to || '—'}
+
+
+ )} +
+ ); +}; + +export default CompletedOrdersTable;