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:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user