Files
glass/src/admin/clients/ClientsPage.jsx
T
houseassassin 8dae7f5d3b feat: add Clients page with order history
Groups orders by phone into a client list with expandable rows showing per-order history, summary cards for total clients and closed revenue, and a Clients nav item in the sidebar.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 16:26:50 +03:00

241 lines
9.8 KiB
React

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 <div style={{ padding: '30px', color: 'var(--text-muted)' }}>Загрузка...</div>;
}
if (error) {
return <div style={{ padding: '30px', color: '#ef4444' }}>Ошибка: {error}</div>;
}
const totalClients = clients.length;
const totalRevenue = clients.reduce((sum, c) => sum + c.totalSpent, 0);
return (
<div style={{ padding: '30px', maxWidth: '1000px' }}>
{/* Page header */}
<h1 style={{
fontFamily: 'var(--font-heading)',
fontSize: '1.5rem',
fontWeight: 800,
marginBottom: '24px',
display: 'flex',
alignItems: 'center',
gap: '10px',
}}>
<Users size={22} color="var(--accent-blue)" />
Клиенты
</h1>
{/* Summary cards */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', marginBottom: '28px', maxWidth: '480px' }}>
<div className="glass-panel" style={{ padding: '20px 24px' }}>
<p style={{ fontSize: '0.72rem', fontWeight: 700, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.4px', marginBottom: '6px' }}>
Всего клиентов
</p>
<p style={{ fontSize: '1.8rem', fontWeight: 800, fontFamily: 'var(--font-heading)', color: 'var(--text-main)' }}>
{totalClients}
</p>
</div>
<div className="glass-panel" style={{ padding: '20px 24px' }}>
<p style={{ fontSize: '0.72rem', fontWeight: 700, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.4px', marginBottom: '6px' }}>
Выручка (закрытые)
</p>
<p style={{ fontSize: '1.8rem', fontWeight: 800, fontFamily: 'var(--font-heading)', color: 'var(--accent-blue)' }}>
{totalRevenue ? totalRevenue.toLocaleString('ru-RU') + ' ₽' : '—'}
</p>
</div>
</div>
{/* Clients table */}
<div className="glass-panel" style={{ padding: 0, overflow: 'hidden' }}>
{clients.length === 0 ? (
<p style={{ padding: '30px', color: 'var(--text-muted)', textAlign: 'center' }}>Нет клиентов</p>
) : (
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr>
<th style={{ ...thStyle, width: '32px', padding: '10px 8px 10px 16px' }}></th>
<th style={thStyle}>Клиент</th>
<th style={thStyle}>Телефон</th>
<th style={{ ...thStyle, textAlign: 'right' }}>Заказов</th>
<th style={{ ...thStyle, textAlign: 'right' }}>Потрачено</th>
<th style={{ ...thStyle, textAlign: 'right' }}>Последний заказ</th>
</tr>
</thead>
<tbody>
{clients.map((client) => {
const isExpanded = expandedPhone === client.phone;
const lastOrder = client.orders[0];
return (
<>
<tr
key={client.phone}
onClick={() => 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';
}}
>
<td style={{ ...tdStyle, padding: '12px 8px 12px 16px', color: 'var(--text-muted)' }}>
{isExpanded
? <ChevronDown size={15} />
: <ChevronRight size={15} />}
</td>
<td style={tdStyle}>
<span style={{ fontWeight: 600 }}>{client.name || '—'}</span>
</td>
<td style={{ ...tdStyle, fontFamily: 'var(--font-body)', color: 'var(--text-muted)' }}>
{client.phone !== 'unknown' ? client.phone : '—'}
</td>
<td style={{ ...tdStyle, textAlign: 'right' }}>
{client.orders.length}
</td>
<td style={{ ...tdStyle, textAlign: 'right', fontWeight: client.totalSpent ? 600 : 400 }}>
{client.totalSpent ? client.totalSpent.toLocaleString('ru-RU') + ' ₽' : '—'}
</td>
<td style={{ ...tdStyle, textAlign: 'right', color: 'var(--text-muted)', fontSize: '0.83rem' }}>
{lastOrder ? formatDate(lastOrder.created_at) : '—'}
</td>
</tr>
{/* Expanded order history */}
{isExpanded && (
<tr key={client.phone + '-expanded'}>
<td colSpan={6} style={{ padding: '0 0 0 48px', background: 'rgba(14, 165, 233, 0.04)' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', borderLeft: '2px solid rgba(14, 165, 233, 0.25)' }}>
<thead>
<tr>
<th style={innerThStyle}>Дата</th>
<th style={innerThStyle}>Плёнка</th>
<th style={{ ...innerThStyle, textAlign: 'right' }}>Площадь (м²)</th>
<th style={{ ...innerThStyle, textAlign: 'right' }}>Сумма</th>
<th style={innerThStyle}>Статус</th>
</tr>
</thead>
<tbody>
{client.orders.map((order) => (
<tr key={order.id}>
<td style={{ ...innerTdStyle, color: 'var(--text-muted)', fontSize: '0.80rem' }}>
{formatDate(order.created_at)}
</td>
<td style={innerTdStyle}>
{order.film_thickness ? order.film_thickness + ' мкм' : '—'}
</td>
<td style={{ ...innerTdStyle, textAlign: 'right' }}>
{order.area_sqm ?? '—'}
</td>
<td style={{ ...innerTdStyle, textAlign: 'right', fontWeight: order.final_cost ? 600 : 400 }}>
{formatMoney(order.final_cost)}
</td>
<td style={innerTdStyle}>
<span style={{
display: 'inline-block',
padding: '2px 8px',
borderRadius: '4px',
fontSize: '0.75rem',
fontWeight: 700,
color: statusColor(order.status),
background: statusColor(order.status) + '22',
}}>
{order.status || '—'}
</span>
</td>
</tr>
))}
</tbody>
</table>
</td>
</tr>
)}
</>
);
})}
</tbody>
</table>
)}
</div>
</div>
);
};
export default ClientsPage;