/** * Клиентский компонент графика доходов (без SSR) */ 'use client'; import React from 'react'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Area, AreaChart, Legend } from 'recharts'; import { IncomeChartData } from '@/api/dashboard'; interface RevenueChartClientProps { data: IncomeChartData[]; loading?: boolean; period?: 'day' | 'week' | 'month'; } export const RevenueChartClient: React.FC = ({ data, loading, period = 'month' }) => { if (loading || !data || data.length === 0) { return (
{loading ? 'Загрузка...' : 'Нет данных'}
); } // Форматируем данные для графика в зависимости от периода let chartData: Array<{ date: string; income: number; lessons: number; fullDate?: string; rawDate: Date | null; }> = []; try { chartData = (Array.isArray(data) ? data : []).map((item) => { const rawDateString = item?.date ?? ''; let dateLabel = rawDateString; // По умолчанию используем исходную строку let date: Date | null = null; // Парсим дату в зависимости от формата, который возвращает backend if (period === 'day') { // Для дня формат: "00:00", "01:00" и т.д. - просто используем как есть dateLabel = rawDateString || ''; } else if (period === 'week') { // Для недели формат: "26.01", "27.01" и т.д. (день.месяц) if (rawDateString && /^\d{2}\.\d{2}$/.test(rawDateString)) { const [day, month] = rawDateString.split('.').map(Number); const now = new Date(); date = new Date(now.getFullYear(), (month || 1) - 1, day || 1); if (!isNaN(date.getTime())) { dateLabel = date.toLocaleDateString('ru-RU', { weekday: 'short', day: 'numeric', }); } } } else { // Для месяца формат: "01.01", "02.01" и т.д. (день.месяц) if (rawDateString && /^\d{2}\.\d{2}$/.test(rawDateString)) { const [day, month] = rawDateString.split('.').map(Number); const now = new Date(); date = new Date(now.getFullYear(), (month || 1) - 1, day || 1); if (!isNaN(date.getTime())) { dateLabel = date.toLocaleDateString('ru-RU', { day: 'numeric', month: 'short', }); } } } return { date: dateLabel || '', income: Math.round(Number(item?.income ?? 0)) || 0, lessons: Number(item?.lessons ?? 0) || 0, fullDate: rawDateString, rawDate: date, }; }); } catch (e) { console.error('[RevenueChartClient] Ошибка при подготовке данных графика', e); // Если что-то пошло не так с данными, показываем безопасный fallback, // чтобы не было бесконечных перезагрузок страницы return (
Ошибка загрузки графика
); } // Кастомный tooltip в стиле Material Design 3 const CustomTooltip = ({ active, payload }: any) => { if (active && payload && payload.length) { return (

{payload[0]?.payload?.fullDate ? (() => { let date: Date; // Используем rawDate если он есть (уже распарсен) if (payload[0].payload.rawDate && !isNaN(new Date(payload[0].payload.rawDate).getTime())) { date = new Date(payload[0].payload.rawDate); } else if ( period === 'day' && typeof payload[0].payload.fullDate === 'string' && /^\d{2}:\d{2}$/.test(payload[0].payload.fullDate) ) { // Формат "HH:MM" const [hours, minutes] = payload[0].payload.fullDate.split(':').map(Number); date = new Date(); date.setHours(hours, minutes || 0, 0, 0); } else if ( (period === 'week' || period === 'month') && typeof payload[0].payload.fullDate === 'string' && /^\d{2}\.\d{2}$/.test(payload[0].payload.fullDate) ) { // Формат "DD.MM" const [day, month] = payload[0].payload.fullDate.split('.').map(Number); date = new Date(); date.setMonth(month - 1, day); date.setFullYear(new Date().getFullYear()); } else { // Пробуем ISO формат date = new Date(payload[0].payload.fullDate); } if (isNaN(date.getTime())) { return payload[0].payload.date || payload[0].payload.fullDate; // Возвращаем отформатированную дату или исходную строку } if (period === 'day') { return date.toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' }) + ' ' + date.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }); } else if (period === 'week') { return date.toLocaleDateString('ru-RU', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }); } else { return date.toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' }); } })() : ''}

Доход: {payload[0].value.toLocaleString('ru-RU')} ₽
{payload[1] && (
Занятий: {payload[1].value}
)}
); } return null; }; return ( `${value} ₽`} label={{ value: 'Доход (₽)', angle: -90, position: 'insideLeft', style: { textAnchor: 'middle', fill: 'var(--md-sys-color-on-surface-variant)', fontSize: '12px' } }} /> } /> { if (value === 'Доход') { return Доход; } return Занятий; }} /> ); };