168 lines
6.4 KiB
TypeScript
168 lines
6.4 KiB
TypeScript
/**
|
||
* Секция «Статистика» (список) для дашборда ментора (iOS 26).
|
||
*/
|
||
|
||
'use client';
|
||
|
||
import React from 'react';
|
||
import { MentorDashboardResponse } from '@/api/dashboard';
|
||
import { Panel, SectionHeader } from '../ui';
|
||
import type { StatsListRow } from '../ui';
|
||
|
||
export interface ExtraStatsSectionProps {
|
||
stats: MentorDashboardResponse | null;
|
||
loading: boolean;
|
||
}
|
||
|
||
const IconUsers = (
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
||
<circle cx="9" cy="7" r="4" />
|
||
</svg>
|
||
);
|
||
|
||
const IconCheck = (
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||
<polyline points="20 6 9 17 4 12" />
|
||
</svg>
|
||
);
|
||
|
||
const IconCalendar = (
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
|
||
<line x1="16" y1="2" x2="16" y2="6" />
|
||
<line x1="8" y1="2" x2="8" y2="6" />
|
||
<line x1="3" y1="10" x2="21" y2="10" />
|
||
</svg>
|
||
);
|
||
|
||
const IconRevenue = (
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||
<line x1="12" y1="1" x2="12" y2="23" />
|
||
<path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
|
||
</svg>
|
||
);
|
||
|
||
const IconFile = (
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||
<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z" />
|
||
<polyline points="13 2 13 9 20 9" />
|
||
</svg>
|
||
);
|
||
|
||
const IconClipboard = (
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||
<path d="M9 11l3 3L22 4" />
|
||
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11" />
|
||
</svg>
|
||
);
|
||
|
||
const IconTrendingUp = (
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||
<polyline points="23 6 13.5 15.5 8.5 10.5 1 18" />
|
||
<polyline points="17 6 23 6 23 12" />
|
||
</svg>
|
||
);
|
||
|
||
function buildRows(stats: MentorDashboardResponse | null, loading: boolean): StatsListRow[] {
|
||
const s = stats?.summary;
|
||
if (loading || !s) {
|
||
return [
|
||
{ label: 'Всего учеников:', value: '—', icon: IconUsers },
|
||
{ label: 'Завершённых занятий всего:', value: '—', icon: IconCheck },
|
||
{ label: 'Занятий на этой неделе:', value: '—', icon: IconCalendar },
|
||
{ label: 'Всего занятий (месяц / всего):', value: '—', icon: IconCalendar },
|
||
{ label: 'Доход (месяц / всё время):', value: '—', icon: IconRevenue, highlight: 'tertiary' },
|
||
{ label: 'Средняя цена занятия:', value: '—', icon: IconTrendingUp },
|
||
{ label: 'Процент завершённых:', value: '—', icon: IconTrendingUp },
|
||
{ label: 'ДЗ на проверке:', value: '—', icon: IconClipboard },
|
||
{ label: 'Всего материалов:', value: '—', icon: IconFile },
|
||
];
|
||
}
|
||
|
||
// Вычисляем среднюю цену занятия
|
||
const averagePrice =
|
||
s.completed_lessons && s.completed_lessons > 0 && s.total_revenue
|
||
? Math.round(s.total_revenue / s.completed_lessons)
|
||
: null;
|
||
|
||
// Вычисляем процент завершенных занятий
|
||
const completionRate =
|
||
s.total_lessons && s.total_lessons > 0 && s.completed_lessons
|
||
? Math.round((s.completed_lessons / s.total_lessons) * 100)
|
||
: null;
|
||
|
||
return [
|
||
{ label: 'Всего учеников:', value: s.total_clients ?? '—', icon: IconUsers },
|
||
{ label: 'Завершённых занятий всего:', value: s.completed_lessons ?? '—', icon: IconCheck },
|
||
{ label: 'Занятий на этой неделе:', value: s.lessons_this_week ?? '—', icon: IconCalendar },
|
||
{
|
||
label: 'Всего занятий (месяц / всего):',
|
||
value: `${s.lessons_this_month ?? 0}/${s.total_lessons ?? 0}`,
|
||
icon: IconCalendar,
|
||
},
|
||
{
|
||
label: 'Доход (месяц / всё время):',
|
||
value:
|
||
s.revenue_this_month != null || s.total_revenue != null
|
||
? `${s.revenue_this_month != null ? `${Math.round(s.revenue_this_month).toLocaleString('ru-RU')} ₽` : '0 ₽'} / ${
|
||
s.total_revenue != null ? `${Math.round(s.total_revenue).toLocaleString('ru-RU')} ₽` : '0 ₽'
|
||
}`
|
||
: '—',
|
||
icon: IconRevenue,
|
||
highlight: 'tertiary',
|
||
},
|
||
{
|
||
label: 'Средняя цена занятия:',
|
||
value: averagePrice ? `${averagePrice.toLocaleString('ru-RU')} ₽` : '—',
|
||
icon: IconTrendingUp,
|
||
},
|
||
{
|
||
label: 'Процент завершённых:',
|
||
value: completionRate != null ? `${completionRate}%` : '—',
|
||
icon: IconTrendingUp,
|
||
},
|
||
{
|
||
label: 'ДЗ на проверке:',
|
||
value: s.pending_submissions ?? '—',
|
||
icon: IconClipboard,
|
||
highlight: s.pending_submissions && s.pending_submissions > 0 ? 'error' : undefined,
|
||
},
|
||
{
|
||
label: 'Всего материалов:',
|
||
value: s.total_materials ?? '—',
|
||
icon: IconFile,
|
||
},
|
||
];
|
||
}
|
||
|
||
export const ExtraStatsSection: React.FC<ExtraStatsSectionProps> = ({ stats, loading }) => {
|
||
const rows = buildRows(stats, loading).slice(0, 9);
|
||
|
||
return (
|
||
<Panel padding="md">
|
||
<SectionHeader title="Статистика" />
|
||
<div className="ios26-stat-grid">
|
||
{rows.map((row, index) => {
|
||
const highlightClass =
|
||
row.highlight === 'tertiary'
|
||
? ' ios26-stat-value--tertiary'
|
||
: row.highlight === 'error'
|
||
? ' ios26-stat-value--error'
|
||
: row.highlight === 'primary' || row.highlight === true
|
||
? ' ios26-stat-value--primary'
|
||
: '';
|
||
|
||
return (
|
||
<div key={index} className="ios26-stat-tile">
|
||
{row.icon && <div className="ios26-stat-icon">{row.icon}</div>}
|
||
<div className="ios26-stat-label">{row.label.replace(/:$/, '')}</div>
|
||
<div className={`ios26-stat-value${highlightClass}`}>{row.value}</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</Panel>
|
||
);
|
||
};
|