krona-front/src/features/dashboard/DynamicDashboard.tsx

722 lines
30 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState } from 'react';
import { useDemoStore } from '../../mock/store';
import { StatCard, Button, Badge, MaturityBadge } from '../../shared/ui';
import { formatCurrency } from '../../shared/utils/formatters';
import {
TrendingUp,
AlertTriangle,
AlertCircle,
Clock,
CheckCircle2,
ArrowRight,
Utensils,
Car,
ShoppingBag,
Users,
Truck,
DollarSign,
Package,
Layers,
} from 'lucide-react';
import {
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from 'recharts';
import { useNavigate } from 'react-router-dom';
const REVENUE_DATA_TODAY = [
{ time: '10:00', revenue: 14200, orders: 4 },
{ time: '12:00', revenue: 38400, orders: 12 },
{ time: '14:00', revenue: 76000, orders: 24 },
{ time: '16:00', revenue: 104500, orders: 32 },
{ time: '18:00', revenue: 149000, orders: 45 },
{ time: '20:00', revenue: 184500, orders: 58 },
];
const REVENUE_DATA_WEEK = [
{ time: 'Пн', revenue: 142000, orders: 48 },
{ time: 'Вт', revenue: 156000, orders: 52 },
{ time: 'Ср', revenue: 168000, orders: 56 },
{ time: 'Чт', revenue: 174000, orders: 59 },
{ time: 'Пт', revenue: 245000, orders: 84 },
{ time: 'Сб', revenue: 290000, orders: 98 },
{ time: 'Вс', revenue: 260000, orders: 89 },
];
export const DynamicDashboard: React.FC = () => {
const {
getActiveProfile,
getActiveCapabilities,
activeLocation,
setIsQuickActionsOpen,
kdsTickets,
deliveries,
workOrders,
} = useDemoStore();
const profile = getActiveProfile();
const capabilities = getActiveCapabilities();
const navigate = useNavigate();
const [chartPeriod, setChartPeriod] = useState<'today' | 'week'>('today');
// Dynamic KPIs based on Profile
const renderKpis = () => {
switch (profile.id) {
case 'car_wash':
return (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3.5">
<StatCard
title="Машин в работе"
value={`${workOrders.filter((w) => w.status === 'in_progress').length || 4} авто`}
change={{ value: '+2', trend: 'up' }}
subtitle="2 бокса на въезде"
icon={<Car className="w-4 h-4 text-krona-info" />}
/>
<StatCard
title="Загрузка боксов"
value="83%"
change={{ value: '+5%', trend: 'up' }}
subtitle="5 из 6 боксов заняты"
icon={<Layers className="w-4 h-4 text-krona-gold" />}
/>
<StatCard
title="Средний чек мойки"
value={formatCurrency(1850)}
change={{ value: '+12%', trend: 'up' }}
subtitle="Комплекс + воск"
icon={<TrendingUp className="w-4 h-4 text-krona-green" />}
/>
<StatCard
title="Время на пост"
value="28 минут"
change={{ value: '-3 мин', trend: 'up' }}
subtitle="Норматив: 30 мин"
icon={<Clock className="w-4 h-4 text-krona-green" />}
/>
</div>
);
case 'auto_service':
return (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3.5">
<StatCard
title="Авто на постах"
value={`${workOrders.length} авто`}
change={{ value: '+3', trend: 'up' }}
subtitle="В работе: 3 • Ожидают: 1"
icon={<Car className="w-4 h-4 text-krona-info" />}
/>
<StatCard
title="Подъемники"
value="4 из 4 заняты"
change={{ value: '100%', trend: 'up' }}
subtitle="Очередь: 2 авто"
icon={<Layers className="w-4 h-4 text-krona-gold" />}
/>
<StatCard
title="Нормо-часы"
value="42.5 н-ч"
change={{ value: '+6.5', trend: 'up' }}
subtitle="Ставка: 2 100 ₽/н-ч"
icon={<Clock className="w-4 h-4 text-krona-green" />}
/>
<StatCard
title="Запчасти в нарядах"
value={formatCurrency(186400)}
change={{ value: '+18%', trend: 'up' }}
subtitle="На складе: 94% позиций"
icon={<DollarSign className="w-4 h-4 text-krona-green" />}
/>
</div>
);
case 'restaurant_delivery_only':
return (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3.5">
<StatCard
title="Выручка доставки"
value={formatCurrency(142800)}
change={{ value: '+22%', trend: 'up' }}
subtitle="План: 130 000 ₽"
icon={<DollarSign className="w-4 h-4 text-krona-green" />}
/>
<StatCard
title="Курьеров на линии"
value={`${deliveries.filter((d) => d.status === 'in_transit').length + 3} курьера`}
change={{ value: '+2', trend: 'up' }}
subtitle="3 на авто, 2 самокат"
icon={<Truck className="w-4 h-4 text-krona-gold" />}
/>
<StatCard
title="Среднее время доставки"
value="34 мин"
change={{ value: '-4 мин', trend: 'up' }}
subtitle="Готовка 14 мин + путь 20 мин"
icon={<Clock className="w-4 h-4 text-krona-green" />}
/>
<StatCard
title="Онлайн-чеков"
value={`${deliveries.length + 38} заказов`}
change={{ value: '+14', trend: 'up' }}
subtitle="Сайт: 60% • Бот: 40%"
icon={<ShoppingBag className="w-4 h-4 text-krona-info" />}
/>
</div>
);
case 'fitness':
return (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3.5">
<StatCard
title="Активных абонементов"
value="840 карт"
change={{ value: '+32', trend: 'up' }}
subtitle="Продлений в мес: 78%"
icon={<Users className="w-4 h-4 text-krona-green" />}
/>
<StatCard
title="Загрузка залов"
value="72%"
change={{ value: '+8%', trend: 'up' }}
subtitle="Пик: 18:00 - 21:00"
icon={<Layers className="w-4 h-4 text-krona-gold" />}
/>
<StatCard
title="Тренировок сегодня"
value="18 занятий"
subtitle="Персональных: 12"
icon={<Clock className="w-4 h-4 text-krona-info" />}
/>
<StatCard
title="Выручка сегодня"
value={formatCurrency(92000)}
change={{ value: '+15%', trend: 'up' }}
subtitle="Карты + спортбар"
icon={<DollarSign className="w-4 h-4 text-krona-green" />}
/>
</div>
);
case 'clinic':
return (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3.5">
<StatCard
title="Приёмов сегодня"
value="36 пациентов"
change={{ value: '+4', trend: 'up' }}
subtitle="Подтверждено: 34"
icon={<Users className="w-4 h-4 text-krona-green" />}
/>
<StatCard
title="Занятость кабинетов"
value="90%"
change={{ value: '+6%', trend: 'up' }}
subtitle="9 из 10 кабинетов"
icon={<Layers className="w-4 h-4 text-krona-gold" />}
/>
<StatCard
title="Средний чек приема"
value={formatCurrency(3400)}
change={{ value: '+300 ₽', trend: 'up' }}
subtitle="Первичный + анализы"
icon={<TrendingUp className="w-4 h-4 text-krona-gold" />}
/>
<StatCard
title="Выручка сегодня"
value={formatCurrency(122400)}
change={{ value: '+11%', trend: 'up' }}
subtitle="Оплата картами: 82%"
icon={<DollarSign className="w-4 h-4 text-krona-green" />}
/>
</div>
);
case 'coworking':
return (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3.5">
<StatCard
title="Занятость рабочих мест"
value="88% (44 из 50)"
change={{ value: '+6%', trend: 'up' }}
subtitle="Свободно 6 мест"
icon={<Layers className="w-4 h-4 text-krona-gold" />}
/>
<StatCard
title="Брони переговорных"
value="9 броней"
change={{ value: '+2', trend: 'up' }}
subtitle="Зал А, Б и Скайп-румы"
icon={<Clock className="w-4 h-4 text-krona-info" />}
/>
<StatCard
title="Активных резидентов"
value="64 чел"
subtitle="Командных офисов: 6"
icon={<Users className="w-4 h-4 text-krona-green" />}
/>
<StatCard
title="Выручка сегодня"
value={formatCurrency(45000)}
change={{ value: '+8%', trend: 'up' }}
subtitle="Дневные пропуски + кофе"
icon={<DollarSign className="w-4 h-4 text-krona-green" />}
/>
</div>
);
case 'logistics':
return (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3.5">
<StatCard
title="Заказов в пути"
value="18 отправлений"
change={{ value: '+5', trend: 'up' }}
subtitle="В доставке прямо сейчас"
icon={<Truck className="w-4 h-4 text-krona-info" />}
/>
<StatCard
title="Доставлено вовремя %"
value="97.8%"
change={{ value: '+0.5%', trend: 'up' }}
subtitle="SLA: 95%"
icon={<CheckCircle2 className="w-4 h-4 text-krona-green" />}
/>
<StatCard
title="Курьеров на маршрутах"
value="8 курьеров"
subtitle="Авто: 5, Мото: 3"
icon={<Users className="w-4 h-4 text-krona-gold" />}
/>
<StatCard
title="Среднее время рейса"
value="26 минут"
change={{ value: '-2 мин', trend: 'up' }}
subtitle="Городской экспресс"
icon={<Clock className="w-4 h-4 text-krona-green" />}
/>
</div>
);
case 'retail':
return (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3.5">
<StatCard
title="Выручка магазина"
value={formatCurrency(248000)}
change={{ value: '+19%', trend: 'up' }}
subtitle="Чеков сегодня: 142"
icon={<ShoppingBag className="w-4 h-4 text-krona-green" />}
/>
<StatCard
title="Средний чек"
value={formatCurrency(1746)}
change={{ value: '+4%', trend: 'up' }}
subtitle="Товаров в чеке: 3.2"
icon={<TrendingUp className="w-4 h-4 text-krona-gold" />}
/>
<StatCard
title="Товарных позиций на складе"
value="1 420 SKU"
subtitle="Критический остаток: 4 SKU"
icon={<Package className="w-4 h-4 text-krona-info" />}
/>
<StatCard
title="Оборачиваемость склада"
value="14.2 дня"
change={{ value: '-1.5 дн', trend: 'up' }}
subtitle="Маржинальность: 42.4%"
icon={<DollarSign className="w-4 h-4 text-krona-green" />}
/>
</div>
);
case 'beauty_services':
return (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3.5">
<StatCard
title="Выручка салона"
value={formatCurrency(124000)}
change={{ value: '+12%', trend: 'up' }}
subtitle="18 визитов сегодня"
icon={<DollarSign className="w-4 h-4 text-krona-green" />}
/>
<StatCard
title="Загрузка мастеров"
value="86%"
change={{ value: '+5%', trend: 'up' }}
subtitle="5 мастеров на смене"
icon={<Users className="w-4 h-4 text-krona-gold" />}
/>
<StatCard
title="Записей на сегодня"
value="22 клиента"
subtitle="Подтверждено: 19"
icon={<Clock className="w-4 h-4 text-krona-info" />}
/>
<StatCard
title="Средний чек визита"
value={formatCurrency(4850)}
change={{ value: '+350 ₽', trend: 'up' }}
subtitle="Услуги + уход дома"
icon={<TrendingUp className="w-4 h-4 text-krona-green" />}
/>
</div>
);
case 'wholesale_b2b':
return (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3.5">
<StatCard
title="Объем отгрузок сегодня"
value={formatCurrency(1450000)}
change={{ value: '+24%', trend: 'up' }}
subtitle="9 партий сформировано"
icon={<DollarSign className="w-4 h-4 text-krona-green" />}
/>
<StatCard
title="Активные B2B контракты"
value="42 контрагента"
subtitle="В согласовании: 3"
icon={<Users className="w-4 h-4 text-krona-info" />}
/>
<StatCard
title="Дебиторская задолженность"
value={formatCurrency(840000)}
change={{ value: '-8%', trend: 'up' }}
subtitle="Просрочено: 95 000 ₽"
icon={<TrendingUp className="w-4 h-4 text-krona-gold" />}
/>
<StatCard
title="Загрузка распредцентра"
value="84%"
subtitle="Свободно 320 паллетомест"
icon={<Package className="w-4 h-4 text-krona-green" />}
/>
</div>
);
case 'restaurant_hall_delivery':
default:
return (
<div className="grid grid-cols-2 lg:grid-cols-5 gap-3.5">
<StatCard
title="Выручка сегодня"
value={formatCurrency(184500)}
change={{ value: '+14%', trend: 'up' }}
subtitle="План: 170 000 ₽ (108%)"
icon={<DollarSign className="w-4 h-4 text-krona-green" />}
/>
<StatCard
title="Заказы"
value="58 чеков"
change={{ value: '+8', trend: 'up' }}
subtitle="Зал: 34 • Доставка: 24"
icon={<Utensils className="w-4 h-4 text-krona-gold" />}
/>
<StatCard
title="Средний чек"
value={formatCurrency(3180)}
change={{ value: '+6.2%', trend: 'up' }}
subtitle="Зал: 3 650 ₽ • Онлайн: 2 450 ₽"
icon={<TrendingUp className="w-4 h-4 text-krona-green" />}
/>
<StatCard
title="Загрузка зала"
value="82%"
change={{ value: '+12%', trend: 'up' }}
subtitle="11 из 14 столов заняты"
icon={<Users className="w-4 h-4 text-krona-info" />}
/>
<StatCard
title="Food Cost / Маржа"
value="29.4%"
change={{ value: '-0.8%', trend: 'up' }}
subtitle="Маржинальность: 70.6%"
icon={<Package className="w-4 h-4 text-krona-green" />}
/>
</div>
);
}
};
return (
<div className="space-y-5 animate-fade-in">
{/* Top Welcome Bar */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 bg-krona-surface border border-krona-border rounded-xl p-4 shadow-krona-soft">
<div>
<div className="flex items-center gap-2">
<h1 className="text-lg font-bold text-krona-primary tracking-tight">
Операционный центр • {profile.companyName}
</h1>
<Badge variant="gold" size="sm">{profile.category}</Badge>
<MaturityBadge level="pilot1" size="sm" />
</div>
<p className="text-xs text-krona-secondary mt-1">
Локация: <span className="font-semibold text-krona-primary">{activeLocation}</span> • Контур реального времени синхронизирован
</p>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={() => navigate('/operations')}>
Открыть рабочий стол
</Button>
<Button variant="primary" size="sm" onClick={() => setIsQuickActionsOpen(true)}>
+ Быстрое действие
</Button>
</div>
</div>
{/* 1. ЧТО ПРОИСХОДИТ? — KPI МЕТРИКИ */}
{renderKpis()}
{/* 2. БОЛЬШОЙ ГРАФИК ВЫРУЧКИ И ЧЕКОВ */}
<div className="bg-krona-surface border border-krona-border rounded-xl p-4 shadow-krona-soft">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-sm font-semibold text-krona-primary">
Динамика выручки и среднего чека
</h3>
<p className="text-xs text-krona-secondary">
Почасовая разбивка с кассовых узлов и онлайн-заказов
</p>
</div>
<div className="flex items-center gap-1 bg-krona-workspace p-1 rounded-lg border border-krona-border text-xs">
<button
onClick={() => setChartPeriod('today')}
className={`px-3 py-1 rounded font-medium transition-colors ${
chartPeriod === 'today' ? 'bg-[#2F684C] text-white' : 'text-krona-secondary hover:text-krona-primary'
}`}
>
Сегодня
</button>
<button
onClick={() => setChartPeriod('week')}
className={`px-3 py-1 rounded font-medium transition-colors ${
chartPeriod === 'week' ? 'bg-[#2F684C] text-white' : 'text-krona-secondary hover:text-krona-primary'
}`}
>
Неделя
</button>
</div>
</div>
<div className="h-64 w-full">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={chartPeriod === 'today' ? REVENUE_DATA_TODAY : REVENUE_DATA_WEEK}>
<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="#D8D5CB" opacity={0.4} />
<XAxis dataKey="time" stroke="#686C65" fontSize={11} />
<YAxis stroke="#686C65" fontSize={11} tickFormatter={(val) => `${val / 1000}k`} />
<Tooltip
formatter={(value: any) => [formatCurrency(Number(value)), 'Выручка']}
contentStyle={{
backgroundColor: '#F8F6F0',
borderColor: '#D8D5CB',
borderRadius: 8,
fontSize: 12,
}}
/>
<Area
type="monotone"
dataKey="revenue"
stroke="#2F684C"
strokeWidth={2.5}
fillOpacity={1}
fill="url(#colorRev)"
/>
</AreaChart>
</ResponsiveContainer>
</div>
</div>
{/* 3. ЧТО ТРЕБУЕТ ВНИМАНИЯ? (Alerts with direct actions) */}
<div className="bg-krona-surface border border-krona-border rounded-xl p-4 shadow-krona-soft">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<AlertTriangle className="w-4 h-4 text-krona-warning" />
<h3 className="text-sm font-semibold text-krona-primary">Требует внимания управляющего</h3>
</div>
<span className="text-xs text-krona-secondary">3 критических инцидента</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<div className="p-3 rounded-lg border border-krona-danger/40 bg-krona-danger/10 flex flex-col justify-between">
<div>
<div className="flex items-center gap-1.5 text-xs font-semibold text-krona-danger">
<AlertCircle className="w-4 h-4" />
<span>Критический остаток</span>
</div>
<p className="text-xs text-krona-primary mt-1.5 leading-relaxed">
Лосось охлажденный: осталось <strong>2.4 кг</strong> (порог 6 кг). Хватит на ~2.5 часа работы кухни.
</p>
</div>
<Button
variant="outline"
size="sm"
className="mt-3 bg-krona-surface text-xs justify-between"
onClick={() => navigate('/operations/inventory')}
>
<span>Заявка поставщику</span>
<ArrowRight className="w-3.5 h-3.5" />
</Button>
</div>
<div className="p-3 rounded-lg border border-krona-warning/40 bg-krona-warning/10 flex flex-col justify-between">
<div>
<div className="flex items-center gap-1.5 text-xs font-semibold text-krona-warning">
<Clock className="w-4 h-4" />
<span>Задержка на кухне (KDS)</span>
</div>
<p className="text-xs text-krona-primary mt-1.5 leading-relaxed">
Заказ #104 на <strong>Стол 4</strong> находится в работе уже 18 минут при норме 12 минут.
</p>
</div>
<Button
variant="outline"
size="sm"
className="mt-3 bg-krona-surface text-xs justify-between"
onClick={() => navigate('/operations/kds')}
>
<span>Открыть KDS кухни</span>
<ArrowRight className="w-3.5 h-3.5" />
</Button>
</div>
<div className="p-3 rounded-lg border border-krona-info/40 bg-krona-info/10 flex flex-col justify-between">
<div>
<div className="flex items-center gap-1.5 text-xs font-semibold text-krona-info">
<Truck className="w-4 h-4" />
<span>Наличные у курьера</span>
</div>
<p className="text-xs text-krona-primary mt-1.5 leading-relaxed">
Курьер Алексей Смирнов держит <strong>24 500 ₽</strong> наличными после 7 доставок.
</p>
</div>
<Button
variant="outline"
size="sm"
className="mt-3 bg-krona-surface text-xs justify-between"
onClick={() => navigate('/operations/delivery')}
>
<span>Принять инкассацию</span>
<ArrowRight className="w-3.5 h-3.5" />
</Button>
</div>
</div>
</div>
{/* 4. СОСТОЯНИЕ БИЗНЕСА СЕЙЧАС (Live Pulse Grid) */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3.5">
{capabilities.dining && (
<div className="bg-krona-surface border border-krona-border rounded-lg p-3.5 shadow-krona-soft">
<div className="flex items-center justify-between text-xs mb-2">
<span className="font-semibold text-krona-primary">Зал и столы</span>
<Badge variant="green" size="sm">82% занято</Badge>
</div>
<div className="text-xs text-krona-secondary space-y-1">
<div className="flex justify-between">
<span>Открытых чеков:</span>
<span className="font-medium text-krona-primary">11 столов</span>
</div>
<div className="flex justify-between">
<span>Среднее время посадки:</span>
<span className="font-medium text-krona-primary">48 минут</span>
</div>
</div>
<button
onClick={() => navigate('/operations/dining')}
className="mt-2.5 text-xs text-krona-green font-medium hover:underline block"
>
Схема зала →
</button>
</div>
)}
{capabilities.kds && (
<div className="bg-krona-surface border border-krona-border rounded-lg p-3.5 shadow-krona-soft">
<div className="flex items-center justify-between text-xs mb-2">
<span className="font-semibold text-krona-primary">Кухня (KDS)</span>
<Badge variant="gold" size="sm">{kdsTickets.length} в работе</Badge>
</div>
<div className="text-xs text-krona-secondary space-y-1">
<div className="flex justify-between">
<span>Суши-цех:</span>
<span className="font-medium text-krona-primary">4 блюда</span>
</div>
<div className="flex justify-between">
<span>Горячий цех:</span>
<span className="font-medium text-krona-primary">2 блюда</span>
</div>
</div>
<button
onClick={() => navigate('/operations/kds')}
className="mt-2.5 text-xs text-krona-green font-medium hover:underline block"
>
Кухонный экран →
</button>
</div>
)}
{capabilities.delivery && (
<div className="bg-krona-surface border border-krona-border rounded-lg p-3.5 shadow-krona-soft">
<div className="flex items-center justify-between text-xs mb-2">
<span className="font-semibold text-krona-primary">Доставка & Курьеры</span>
<Badge variant="info" size="sm">3 в пути</Badge>
</div>
<div className="text-xs text-krona-secondary space-y-1">
<div className="flex justify-between">
<span>Среднее время доставки:</span>
<span className="font-medium text-krona-primary">34 мин</span>
</div>
<div className="flex justify-between">
<span>Курьеров на линии:</span>
<span className="font-medium text-krona-primary">4 сотрудника</span>
</div>
</div>
<button
onClick={() => navigate('/operations/delivery')}
className="mt-2.5 text-xs text-krona-green font-medium hover:underline block"
>
Управление доставкой →
</button>
</div>
)}
<div className="bg-krona-surface border border-krona-border rounded-lg p-3.5 shadow-krona-soft">
<div className="flex items-center justify-between text-xs mb-2">
<span className="font-semibold text-krona-primary">Финансы & Кассы</span>
<Badge variant="green" size="sm">Смены открыты</Badge>
</div>
<div className="text-xs text-krona-secondary space-y-1">
<div className="flex justify-between">
<span>Наличные в кассе:</span>
<span className="font-medium text-krona-primary">42 800 ₽</span>
</div>
<div className="flex justify-between">
<span>Безнал / Эквайринг:</span>
<span className="font-medium text-krona-primary">141 700 ₽</span>
</div>
</div>
<button
onClick={() => navigate('/finance')}
className="mt-2.5 text-xs text-krona-green font-medium hover:underline block"
>
ДДС & Сальдо →
</button>
</div>
</div>
</div>
);
};