uchill/front_material/components/dashboard/mentor/ExtraStatsSection.tsx

168 lines
6.3 KiB
TypeScript
Raw Permalink 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.

/**
* Секция «Статистика» (список) для дашборда ментора (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" data-tour="mentor-extrastats">
<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>
);
};