feat: period filter in analytics — month/last month/3 months/all time

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-12 11:36:57 +03:00
parent b69ea40f35
commit 12cca64f61
+62 -13
View File
@@ -1,4 +1,4 @@
import { useMemo } from 'react';
import { useMemo, useState } from 'react';
import { TrendingUp, Download } from 'lucide-react';
import { useAnalyticsPage } from './useAnalyticsPage';
@@ -45,26 +45,55 @@ const SummaryCard = ({ label, value }) => (
</div>
);
const PERIODS = [
{ value: 'month', label: 'Этот месяц' },
{ value: 'last_month', label: 'Прошлый месяц' },
{ value: '3months', label: '3 месяца' },
{ value: 'all', label: 'Всё время' },
];
const AnalyticsPage = () => {
const { orders, loading } = useAnalyticsPage();
const [period, setPeriod] = useState('month');
const filteredOrders = useMemo(() => {
const now = new Date();
if (period === 'month') {
const start = new Date(now.getFullYear(), now.getMonth(), 1);
return orders.filter((o) => new Date(o.created_at) >= start);
}
if (period === 'last_month') {
const start = new Date(now.getFullYear(), now.getMonth() - 1, 1);
const end = new Date(now.getFullYear(), now.getMonth(), 1);
return orders.filter((o) => {
const d = new Date(o.created_at);
return d >= start && d < end;
});
}
if (period === '3months') {
const start = new Date(now.getFullYear(), now.getMonth() - 2, 1);
return orders.filter((o) => new Date(o.created_at) >= start);
}
return orders; // 'all'
}, [orders, period]);
const metrics = useMemo(() => {
const total = orders.length;
const completed = orders.filter((o) => o.status === 'завершена');
const total = filteredOrders.length;
const completed = filteredOrders.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]);
}, [filteredOrders]);
const statusCounts = useMemo(() => {
const counts = {};
STATUS_LIST.forEach((s) => { counts[s] = 0; });
orders.forEach((o) => {
filteredOrders.forEach((o) => {
if (counts[o.status] !== undefined) counts[o.status]++;
});
return counts;
}, [orders]);
}, [filteredOrders]);
const maxStatusCount = useMemo(() => Math.max(...Object.values(statusCounts), 1), [statusCounts]);
@@ -72,7 +101,7 @@ const AnalyticsPage = () => {
const months = getLast6Months();
const map = {};
months.forEach((m) => { map[m.key] = 0; });
orders
filteredOrders
.filter((o) => o.status === 'завершена')
.forEach((o) => {
if (!o.created_at) return;
@@ -83,13 +112,13 @@ const AnalyticsPage = () => {
}
});
return months.map((m) => ({ ...m, revenue: map[m.key] }));
}, [orders]);
}, [filteredOrders]);
const maxMonthRevenue = useMemo(() => Math.max(...revenueByMonth.map((m) => m.revenue), 1), [revenueByMonth]);
const managerStats = useMemo(() => {
const map = {};
orders.forEach((o) => {
filteredOrders.forEach((o) => {
const name = o.assigned_to || 'Не назначен';
if (!map[name]) {
map[name] = { name, total: 0, completed: 0, revenue: 0 };
@@ -101,12 +130,12 @@ const AnalyticsPage = () => {
}
});
return Object.values(map).sort((a, b) => b.revenue - a.revenue);
}, [orders]);
}, [filteredOrders]);
const handleExportCSV = () => {
const BOM = '';
const headers = ['ID', 'Статус', 'Сумма', 'Дата создания', 'Менеджер', 'Плёнка'];
const rows = orders.map((o) => [
const rows = filteredOrders.map((o) => [
o.id,
o.status ?? '',
o.final_cost ?? '',
@@ -148,14 +177,14 @@ const AnalyticsPage = () => {
</h1>
<button
onClick={handleExportCSV}
disabled={loading || orders.length === 0}
disabled={loading || filteredOrders.length === 0}
style={{
display: 'inline-flex', alignItems: 'center', gap: '6px',
padding: '8px 16px', borderRadius: '8px', cursor: 'pointer',
background: 'transparent', border: '1.5px solid #E9ECEF',
fontSize: '0.88rem', fontWeight: 600, color: '#1E1E2D',
fontFamily: 'var(--font-heading)',
opacity: (loading || orders.length === 0) ? 0.5 : 1,
opacity: (loading || filteredOrders.length === 0) ? 0.5 : 1,
}}
onMouseEnter={(e) => e.currentTarget.style.borderColor = '#4361EE'}
onMouseLeave={(e) => e.currentTarget.style.borderColor = '#E9ECEF'}
@@ -165,6 +194,26 @@ const AnalyticsPage = () => {
</button>
</div>
{/* Period selector */}
<div style={{ display: 'flex', gap: '6px', marginBottom: '24px', flexWrap: 'wrap' }}>
{PERIODS.map((p) => (
<button
key={p.value}
onClick={() => setPeriod(p.value)}
style={{
padding: '7px 16px', borderRadius: '20px', fontSize: '0.85rem', fontWeight: 600,
border: period === p.value ? '1.5px solid #4361EE' : '1.5px solid #E9ECEF',
background: period === p.value ? '#4361EE' : '#fff',
color: period === p.value ? '#fff' : '#8C9097',
cursor: 'pointer', fontFamily: 'var(--font-heading)',
transition: 'all 0.15s',
}}
>
{p.label}
</button>
))}
</div>
{/* Section A — Summary cards */}
<div
className="glass-panel"