301 lines
13 KiB
TypeScript
301 lines
13 KiB
TypeScript
import React, { useState } from 'react';
|
||
import { StatCard, Badge, MaturityBadge } from '../../shared/ui';
|
||
import { formatCurrency, formatNumber } from '../../shared/utils/formatters';
|
||
import {
|
||
XAxis,
|
||
YAxis,
|
||
CartesianGrid,
|
||
Tooltip,
|
||
ResponsiveContainer,
|
||
PieChart,
|
||
Pie,
|
||
Cell,
|
||
AreaChart,
|
||
Area,
|
||
} from 'recharts';
|
||
import { BarChart3, TrendingUp, DollarSign, Users } from 'lucide-react';
|
||
|
||
const PERIOD_METRICS = {
|
||
today: {
|
||
revenue: 184500,
|
||
revChange: '+12.4%',
|
||
orders: 58,
|
||
ordersChange: '+6',
|
||
avgCheck: 3180,
|
||
avgCheckChange: '+180 ₽',
|
||
retention: '68.4%',
|
||
retentionChange: '+2.1%',
|
||
salesTrend: [
|
||
{ label: '10:00', revenue: 14200, orders: 4 },
|
||
{ label: '12:00', revenue: 38400, orders: 12 },
|
||
{ label: '14:00', revenue: 76000, orders: 24 },
|
||
{ label: '16:00', revenue: 104500, orders: 32 },
|
||
{ label: '18:00', revenue: 149000, orders: 45 },
|
||
{ label: '20:00', revenue: 184500, orders: 58 },
|
||
],
|
||
},
|
||
week: {
|
||
revenue: 1428000,
|
||
revChange: '+15.8%',
|
||
orders: 432,
|
||
ordersChange: '+42',
|
||
avgCheck: 3305,
|
||
avgCheckChange: '+210 ₽',
|
||
retention: '70.2%',
|
||
retentionChange: '+3.5%',
|
||
salesTrend: [
|
||
{ label: 'Пн', revenue: 142000, orders: 48 },
|
||
{ label: 'Вт', revenue: 156000, orders: 52 },
|
||
{ label: 'Ср', revenue: 168000, orders: 56 },
|
||
{ label: 'Чт', revenue: 174000, orders: 59 },
|
||
{ label: 'Пт', revenue: 245000, orders: 84 },
|
||
{ label: 'Сб', revenue: 290000, orders: 98 },
|
||
{ label: 'Вс', revenue: 260000, orders: 89 },
|
||
],
|
||
},
|
||
month: {
|
||
revenue: 4850000,
|
||
revChange: '+18.5%',
|
||
orders: 1490,
|
||
ordersChange: '+180',
|
||
avgCheck: 3255,
|
||
avgCheckChange: '+315 ₽',
|
||
retention: '72.4%',
|
||
retentionChange: '+4.0%',
|
||
salesTrend: [
|
||
{ label: '1-я нед', revenue: 1120000, orders: 340 },
|
||
{ label: '2-я нед', revenue: 1240000, orders: 380 },
|
||
{ label: '3-я нед', revenue: 1390000, orders: 420 },
|
||
{ label: '4-я нед', revenue: 1428000, orders: 432 },
|
||
],
|
||
},
|
||
year: {
|
||
revenue: 52400000,
|
||
revChange: '+34.2%',
|
||
orders: 16800,
|
||
ordersChange: '+2400',
|
||
avgCheck: 3120,
|
||
avgCheckChange: '+420 ₽',
|
||
retention: '75.8%',
|
||
retentionChange: '+6.2%',
|
||
salesTrend: [
|
||
{ label: 'Q1', revenue: 11200000, orders: 3600 },
|
||
{ label: 'Q2', revenue: 12800000, orders: 4100 },
|
||
{ label: 'Q3', revenue: 14100000, orders: 4600 },
|
||
{ label: 'Q4 (прогноз)', revenue: 14300000, orders: 4500 },
|
||
],
|
||
},
|
||
};
|
||
|
||
const SALES_CHANNEL_DATA = [
|
||
{ name: 'Зал ресторана', value: 48, color: '#2F684C' },
|
||
{ name: 'Собственная доставка', value: 26, color: '#B78335' },
|
||
{ name: 'Telegram-бот', value: 14, color: '#527286' },
|
||
{ name: 'Яндекс Еда / Агрегаторы', value: 12, color: '#B16D50' },
|
||
];
|
||
|
||
const ABC_ITEMS_DATA = [
|
||
{ name: 'Филадельфия Премиум', revenue: 420000, qty: 442, margin: '71.2%', category: 'A' },
|
||
{ name: 'Том Ям с морепродуктами', revenue: 310000, qty: 455, margin: '68.5%', category: 'A' },
|
||
{ name: 'Калифорния краб', revenue: 265000, qty: 335, margin: '69.0%', category: 'A' },
|
||
{ name: 'Рамен Тонкоцу', revenue: 195000, qty: 330, margin: '64.5%', category: 'B' },
|
||
{ name: 'Гёдза со свининой', revenue: 160000, qty: 380, margin: '74.0%', category: 'B' },
|
||
{ name: 'Эби Темпура', revenue: 84000, qty: 155, margin: '58.0%', category: 'C' },
|
||
];
|
||
|
||
export const AnalyticsModule: React.FC = () => {
|
||
const [selectedPeriod, setSelectedPeriod] = useState<'today' | 'week' | 'month' | 'year'>('month');
|
||
|
||
const currentData = PERIOD_METRICS[selectedPeriod];
|
||
|
||
return (
|
||
<div className="space-y-4 animate-fade-in">
|
||
{/* Header with MaturityBadge and Period Switcher */}
|
||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 bg-krona-surface p-3.5 rounded-xl border border-krona-border shadow-krona-soft">
|
||
<div>
|
||
<div className="flex items-center gap-2">
|
||
<h2 className="text-sm font-bold text-krona-primary">
|
||
Сквозная бизнес-аналитика, ABC-матрица & BI
|
||
</h2>
|
||
<MaturityBadge level="target" size="sm" />
|
||
</div>
|
||
<p className="text-xs text-krona-secondary mt-0.5">
|
||
Анализ маржинальности, выручки по каналам, структуры чеков, LTV и Food Cost
|
||
</p>
|
||
</div>
|
||
|
||
{/* Date range selector */}
|
||
<div className="flex items-center gap-1 bg-krona-workspace p-1 rounded-lg border border-krona-border text-xs">
|
||
{[
|
||
{ id: 'today', label: 'Сегодня' },
|
||
{ id: 'week', label: '7 дней' },
|
||
{ id: 'month', label: '30 дней' },
|
||
{ id: 'year', label: 'Год' },
|
||
].map((p) => (
|
||
<button
|
||
key={p.id}
|
||
onClick={() => setSelectedPeriod(p.id as any)}
|
||
className={`px-3 py-1 rounded font-medium transition-colors ${
|
||
selectedPeriod === p.id
|
||
? 'bg-[#2F684C] text-white shadow-sm'
|
||
: 'text-krona-secondary hover:text-krona-primary'
|
||
}`}
|
||
>
|
||
{p.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* KPI Cards */}
|
||
<div className="grid grid-cols-1 sm:grid-cols-4 gap-3.5">
|
||
<StatCard
|
||
title="Выручка за период"
|
||
value={formatCurrency(currentData.revenue)}
|
||
change={{ value: currentData.revChange, trend: 'up' }}
|
||
subtitle={`Заказов: ${formatNumber(currentData.orders)}`}
|
||
icon={<DollarSign className="w-4 h-4 text-krona-green" />}
|
||
/>
|
||
<StatCard
|
||
title="Средний чек"
|
||
value={formatCurrency(currentData.avgCheck)}
|
||
change={{ value: currentData.avgCheckChange, trend: 'up' }}
|
||
subtitle="По всем каналам"
|
||
icon={<BarChart3 className="w-4 h-4 text-krona-gold" />}
|
||
/>
|
||
<StatCard
|
||
title="Удержание гостей (CRR)"
|
||
value={currentData.retention}
|
||
change={{ value: currentData.retentionChange, trend: 'up' }}
|
||
subtitle="Повторные покупки"
|
||
icon={<Users className="w-4 h-4 text-sky-600" />}
|
||
/>
|
||
<StatCard
|
||
title="Маржинальность меню"
|
||
value="70.6%"
|
||
change={{ value: '+1.2%', trend: 'up' }}
|
||
subtitle="Food cost: 29.4%"
|
||
icon={<TrendingUp className="w-4 h-4 text-krona-green" />}
|
||
/>
|
||
</div>
|
||
|
||
{/* Main Charts Grid */}
|
||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||
{/* Revenue Trend Area Chart */}
|
||
<div className="lg:col-span-2 bg-krona-surface border border-krona-border rounded-xl p-4 shadow-krona-soft space-y-3">
|
||
<div className="flex items-center justify-between">
|
||
<h4 className="text-xs font-bold text-krona-primary">
|
||
Динамика выручки за выбранный период
|
||
</h4>
|
||
<Badge variant="green" size="sm">План выполнен на 106%</Badge>
|
||
</div>
|
||
|
||
<div className="h-64 w-full">
|
||
<ResponsiveContainer width="100%" height="100%">
|
||
<AreaChart data={currentData.salesTrend} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
|
||
<defs>
|
||
<linearGradient id="colorRev" x1="0" y1="0" x2="0" y2="1">
|
||
<stop offset="5%" stopColor="#2F684C" stopOpacity={0.4} />
|
||
<stop offset="95%" stopColor="#2F684C" stopOpacity={0.0} />
|
||
</linearGradient>
|
||
</defs>
|
||
<CartesianGrid strokeDasharray="3 3" stroke="#DDDAD1" opacity={0.5} />
|
||
<XAxis dataKey="label" stroke="#71766F" fontSize={11} />
|
||
<YAxis stroke="#71766F" fontSize={11} tickFormatter={(val) => `${val / 1000}k`} />
|
||
<Tooltip
|
||
formatter={(val: any) => [formatCurrency(val), 'Выручка']}
|
||
contentStyle={{ backgroundColor: '#FCFAF5', borderColor: '#DDDAD1', borderRadius: '8px', fontSize: '11px' }}
|
||
/>
|
||
<Area type="monotone" dataKey="revenue" stroke="#2F684C" strokeWidth={2} fillOpacity={1} fill="url(#colorRev)" />
|
||
</AreaChart>
|
||
</ResponsiveContainer>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Sales by channel pie chart */}
|
||
<div className="bg-krona-surface border border-krona-border rounded-xl p-4 shadow-krona-soft space-y-3 flex flex-col justify-between">
|
||
<div>
|
||
<h4 className="text-xs font-bold text-krona-primary">Структура выручки по каналам продаж</h4>
|
||
<p className="text-[11px] text-krona-secondary">Доля в общем обороте заведений</p>
|
||
</div>
|
||
|
||
<div className="h-44 w-full flex items-center justify-center">
|
||
<ResponsiveContainer width="100%" height="100%">
|
||
<PieChart>
|
||
<Pie
|
||
data={SALES_CHANNEL_DATA}
|
||
innerRadius={45}
|
||
outerRadius={70}
|
||
paddingAngle={4}
|
||
dataKey="value"
|
||
>
|
||
{SALES_CHANNEL_DATA.map((entry, index) => (
|
||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||
))}
|
||
</Pie>
|
||
<Tooltip formatter={(val: any) => [`${val}%`, 'Доля']} />
|
||
</PieChart>
|
||
</ResponsiveContainer>
|
||
</div>
|
||
|
||
<div className="space-y-1 text-xs border-t border-krona-border/60 pt-2">
|
||
{SALES_CHANNEL_DATA.map((ch) => (
|
||
<div key={ch.name} className="flex items-center justify-between text-[11px]">
|
||
<div className="flex items-center gap-1.5">
|
||
<span className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: ch.color }}></span>
|
||
<span className="text-krona-secondary">{ch.name}</span>
|
||
</div>
|
||
<span className="font-bold font-mono text-krona-primary">{ch.value}%</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* ABC Menu Matrix Table */}
|
||
<div className="bg-krona-surface border border-krona-border rounded-xl shadow-krona-soft overflow-hidden">
|
||
<div className="p-3.5 border-b border-krona-border flex items-center justify-between">
|
||
<div>
|
||
<h4 className="text-xs font-bold text-krona-primary">ABC-анализ меню и блюд</h4>
|
||
<p className="text-[11px] text-krona-secondary">
|
||
Классификация позиций по выручке и маржинальности (Группа A формирует 80% выручки)
|
||
</p>
|
||
</div>
|
||
<Badge variant="gold" size="sm">ABC Matrix v2.1</Badge>
|
||
</div>
|
||
|
||
<table className="w-full text-left text-xs border-collapse">
|
||
<thead className="bg-krona-workspace/80 text-krona-secondary border-b border-krona-border">
|
||
<tr>
|
||
<th className="p-3 font-semibold">Блюдо / Позиция</th>
|
||
<th className="p-3 font-semibold text-center">Класс</th>
|
||
<th className="p-3 font-semibold text-right">Продано порций</th>
|
||
<th className="p-3 font-semibold text-right">Выручка</th>
|
||
<th className="p-3 font-semibold text-right">Маржинальность</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-krona-border/50">
|
||
{ABC_ITEMS_DATA.map((it) => (
|
||
<tr key={it.name} className="hover:bg-krona-workspace/40 transition-colors">
|
||
<td className="p-3 font-medium text-krona-primary">{it.name}</td>
|
||
<td className="p-3 text-center">
|
||
<Badge
|
||
variant={it.category === 'A' ? 'green' : it.category === 'B' ? 'gold' : 'neutral'}
|
||
size="sm"
|
||
>
|
||
Группа {it.category}
|
||
</Badge>
|
||
</td>
|
||
<td className="p-3 text-right font-mono text-krona-secondary">{it.qty} шт</td>
|
||
<td className="p-3 text-right font-mono font-bold text-krona-primary">
|
||
{formatCurrency(it.revenue)}
|
||
</td>
|
||
<td className="p-3 text-right font-mono text-emerald-700 font-semibold">{it.margin}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|