151 lines
4.4 KiB
TypeScript
151 lines
4.4 KiB
TypeScript
/**
|
||
* Секция «Динамика доходов» для дашборда ментора (iOS 26).
|
||
* Использует переиспользуемые Panel, SectionHeader, SegmentedControl.
|
||
*/
|
||
|
||
'use client';
|
||
|
||
import React from 'react';
|
||
import { MentorIncomeResponse } from '@/api/dashboard';
|
||
import { Panel, SectionHeader, SegmentedControl } from '../ui';
|
||
import { RevenueChart } from '../RevenueChart';
|
||
import { LoadingSpinner } from '@/components/common/LoadingSpinner';
|
||
|
||
export type IncomePeriod = 'day' | 'week' | 'month';
|
||
|
||
export interface IncomeSectionProps {
|
||
data: MentorIncomeResponse | null;
|
||
period: IncomePeriod;
|
||
onPeriodChange: (p: IncomePeriod) => void;
|
||
loading: boolean;
|
||
}
|
||
|
||
const PERIOD_OPTIONS = [
|
||
{ value: 'day' as const, label: 'День' },
|
||
{ value: 'week' as const, label: 'Неделя' },
|
||
{ value: 'month' as const, label: 'Месяц' },
|
||
];
|
||
|
||
export const IncomeSection: React.FC<IncomeSectionProps> = ({
|
||
data,
|
||
period,
|
||
onPeriodChange,
|
||
loading,
|
||
}) => {
|
||
const totalIncome = Number(data?.summary?.total_income ?? 0);
|
||
const totalLessons = Number(data?.summary?.total_lessons ?? 0);
|
||
const averageLessonPrice = Number(data?.summary?.average_lesson_price ?? 0);
|
||
|
||
return (
|
||
<Panel padding="md" data-tour="mentor-income">
|
||
<SectionHeader
|
||
title="Динамика доходов"
|
||
trailing={
|
||
<SegmentedControl
|
||
options={PERIOD_OPTIONS}
|
||
value={period}
|
||
onChange={onPeriodChange}
|
||
disabled={loading}
|
||
/>
|
||
}
|
||
/>
|
||
{loading && !data ? (
|
||
<LoadingSpinner size="medium" />
|
||
) : (
|
||
<>
|
||
<RevenueChart
|
||
data={data?.chart_data ?? []}
|
||
loading={loading}
|
||
period={period}
|
||
/>
|
||
<div
|
||
className="income-stats-grid"
|
||
style={{
|
||
display: 'grid',
|
||
gridTemplateColumns: 'repeat(3, 1fr)',
|
||
gap: 'var(--ios26-spacing)',
|
||
paddingTop: 'var(--ios26-spacing-md)',
|
||
borderTop: '1px solid var(--ios26-list-divider)',
|
||
minHeight: 72,
|
||
}}
|
||
>
|
||
<div>
|
||
<p
|
||
style={{
|
||
fontSize: 16,
|
||
color: 'var(--md-sys-color-on-surface-variant)',
|
||
margin: '0 0 4px 0',
|
||
}}
|
||
>
|
||
Всего доход
|
||
</p>
|
||
<p
|
||
style={{
|
||
fontSize: 21,
|
||
fontWeight: 600,
|
||
color: 'var(--md-sys-color-primary)',
|
||
margin: 0,
|
||
letterSpacing: '-0.02em',
|
||
opacity: data?.summary ? 1 : 0.6,
|
||
}}
|
||
>
|
||
{data?.summary
|
||
? `${Math.round(totalIncome).toLocaleString('ru-RU')} ₽`
|
||
: '—'}
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<p
|
||
style={{
|
||
fontSize: 16,
|
||
color: 'var(--md-sys-color-on-surface-variant)',
|
||
margin: '0 0 4px 0',
|
||
}}
|
||
>
|
||
Занятий
|
||
</p>
|
||
<p
|
||
style={{
|
||
fontSize: 21,
|
||
fontWeight: 600,
|
||
color: 'var(--md-sys-color-primary)',
|
||
margin: 0,
|
||
letterSpacing: '-0.02em',
|
||
opacity: data?.summary ? 1 : 0.6,
|
||
}}
|
||
>
|
||
{data?.summary ? totalLessons : '—'}
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<p
|
||
style={{
|
||
fontSize: 16,
|
||
color: 'var(--md-sys-color-on-surface-variant)',
|
||
margin: '0 0 4px 0',
|
||
}}
|
||
>
|
||
Средняя цена
|
||
</p>
|
||
<p
|
||
style={{
|
||
fontSize: 21,
|
||
fontWeight: 600,
|
||
color: 'var(--md-sys-color-primary)',
|
||
margin: 0,
|
||
letterSpacing: '-0.02em',
|
||
opacity: data?.summary ? 1 : 0.6,
|
||
}}
|
||
>
|
||
{data?.summary
|
||
? `${Math.round(averageLessonPrice).toLocaleString('ru-RU')} ₽`
|
||
: '—'}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</>
|
||
)}
|
||
</Panel>
|
||
);
|
||
};
|