'use client';
import { useEffect, useState, useMemo } from 'react';
import { getLessons, type Lesson } from '@/api/schedule';
import { getHomeworkSubmissionsBySubject, type HomeworkSubmission } from '@/api/homework';
import { format, subMonths, startOfDay, endOfDay, addDays } from 'date-fns';
import dayjs from 'dayjs';
import dynamic from 'next/dynamic';
import { useSelectedChild } from '@/contexts/SelectedChildContext';
import { DashboardLayout, Panel, SectionHeader } from '@/components/dashboard/ui';
import { DateRangePicker } from '@/components/common/DateRangePicker';
import { LoadingSpinner } from '@/components/common/LoadingSpinner';
const Chart = dynamic(() => import('react-apexcharts').then((mod) => mod.default), {
ssr: false,
loading: () => (
),
});
const CHART_COLORS = ['#6750A4', '#7D5260'];
const defaultRange = {
start_date: dayjs().subtract(7, 'day').format('YYYY-MM-DD'),
end_date: dayjs().format('YYYY-MM-DD'),
};
function getSubjectFromLesson(lesson: Lesson): string {
if (typeof lesson.subject === 'string' && lesson.subject?.trim()) return lesson.subject.trim();
return '';
}
/** Все даты в диапазоне [startStr, endStr] включительно (формат YYYY-MM-DD) */
function getDatesInRange(startStr: string, endStr: string): string[] {
const dates: string[] = [];
let d = new Date(startStr);
const end = new Date(endStr);
while (d <= end) {
dates.push(format(d, 'yyyy-MM-dd'));
d = addDays(d, 1);
}
return dates;
}
export default function MyProgressPage() {
const { selectedChild } = useSelectedChild();
const [dateRangeValue, setDateRangeValue] = useState<{ start_date: string; end_date: string }>(() => defaultRange);
const [selectedSubject, setSelectedSubject] = useState('');
const dateRange = useMemo(() => ({
start: startOfDay(new Date(dateRangeValue.start_date)),
end: endOfDay(new Date(dateRangeValue.end_date)),
}), [dateRangeValue.start_date, dateRangeValue.end_date]);
const startStr = format(dateRange.start, 'yyyy-MM-dd');
const endStr = format(dateRange.end, 'yyyy-MM-dd');
const [subjectsFromLessons, setSubjectsFromLessons] = useState([]);
const [subjectsLoading, setSubjectsLoading] = useState(true);
useEffect(() => {
let cancelled = false;
setSubjectsLoading(true);
getLessons({
start_date: format(subMonths(new Date(), 24), 'yyyy-MM-dd'),
end_date: format(new Date(), 'yyyy-MM-dd'),
...(selectedChild?.id && { child_id: selectedChild.id }),
})
.then((res) => {
if (cancelled) return;
const set = new Set();
(res.results || []).forEach((l: Lesson) => {
const sub = getSubjectFromLesson(l);
if (sub) set.add(sub);
});
const list = Array.from(set).sort();
setSubjectsFromLessons(list);
if (list.length > 0 && !selectedSubject) setSelectedSubject(list[0]);
})
.catch(() => { if (!cancelled) setSubjectsFromLessons([]); })
.finally(() => { if (!cancelled) setSubjectsLoading(false); });
return () => { cancelled = true; };
}, []);
useEffect(() => {
if (subjectsFromLessons.length > 0 && !selectedSubject) setSelectedSubject(subjectsFromLessons[0]);
}, [subjectsFromLessons, selectedSubject]);
const [lessons, setLessons] = useState([]);
const [lessonsLoading, setLessonsLoading] = useState(false);
useEffect(() => {
if (!startStr || !endStr) return;
let cancelled = false;
setLessonsLoading(true);
getLessons({
start_date: startStr,
end_date: endStr,
...(selectedChild?.id && { child_id: selectedChild.id }),
})
.then((res) => {
if (cancelled) return;
const list = (res.results || []).filter((l: Lesson) => {
const sub = getSubjectFromLesson(l);
return selectedSubject ? sub === selectedSubject : true;
});
list.sort((a: Lesson, b: Lesson) => new Date(a.start_time).getTime() - new Date(b.start_time).getTime());
setLessons(list);
})
.catch(() => { if (!cancelled) setLessons([]); })
.finally(() => { if (!cancelled) setLessonsLoading(false); });
return () => { cancelled = true; };
}, [startStr, endStr, selectedSubject, selectedChild?.id]);
const [homeworkSubmissions, setHomeworkSubmissions] = useState([]);
const [homeworkLoading, setHomeworkLoading] = useState(false);
useEffect(() => {
if (!selectedSubject) {
setHomeworkSubmissions([]);
return;
}
let cancelled = false;
setHomeworkLoading(true);
getHomeworkSubmissionsBySubject({
subject: selectedSubject,
start_date: startStr,
end_date: endStr,
...(selectedChild?.id && { child_id: selectedChild.id }),
})
.then((res) => {
if (cancelled) return;
setHomeworkSubmissions(res.results || []);
})
.catch(() => { if (!cancelled) setHomeworkSubmissions([]); })
.finally(() => { if (!cancelled) setHomeworkLoading(false); });
return () => { cancelled = true; };
}, [selectedSubject, startStr, endStr, selectedChild?.id]);
const periodStats = useMemo(() => {
const completed = lessons.filter((l) => l.status === 'completed').length;
const total = lessons.length;
const cancelled = lessons.filter((l) => l.status === 'cancelled').length;
const attendanceRate = total > 0 ? Math.round((completed / total) * 100) : 0;
const withGrades = lessons.filter((l) => l.status === 'completed' && (l.mentor_grade != null || l.school_grade != null));
let sum = 0;
let count = 0;
withGrades.forEach((l) => {
if (l.mentor_grade != null) { sum += l.mentor_grade; count++; }
if (l.school_grade != null) { sum += l.school_grade; count++; }
});
const avgGrade = count > 0 ? Math.round((sum / count) * 10) / 10 : 0;
const hwGraded = homeworkSubmissions.filter((s) => s.score != null && s.checked_at).length;
return {
completedLessons: completed,
totalLessons: total,
attendanceRate,
cancelled,
avgGrade,
hwGraded,
};
}, [lessons, homeworkSubmissions]);
const gradesChart = useMemo(() => {
const allDates = getDatesInRange(startStr, endStr);
const categories = allDates.map((d) => {
const [, m, day] = d.split('-');
return `${day}.${m}`;
});
const byDate: Record = {};
lessons
.filter((l) => l.status === 'completed' && (l.mentor_grade != null || l.school_grade != null))
.forEach((l) => {
const key = l.start_time?.slice(0, 10) || '';
if (!key) return;
byDate[key] = { mentor: l.mentor_grade ?? null, school: l.school_grade ?? null };
});
const mentorGrades = allDates.map((d) => byDate[d]?.mentor ?? null);
const schoolGrades = allDates.map((d) => byDate[d]?.school ?? null);
return {
series: [
{ name: 'Оценка репетитора', data: mentorGrades },
{ name: 'Оценка в школе', data: schoolGrades },
],
categories,
};
}, [lessons, startStr, endStr]);
const homeworkChart = useMemo(() => {
const allDates = getDatesInRange(startStr, endStr);
const categories = allDates.map((d) => {
const [, m, day] = d.split('-');
return `${day}.${m}`;
});
const byDate: Record = {};
homeworkSubmissions
.filter((s) => s.checked_at && s.score != null)
.forEach((s) => {
const key = format(new Date(s.checked_at!), 'yyyy-MM-dd');
byDate[key] = s.score ?? null;
});
const scores = allDates.map((d) => byDate[d] ?? null);
return {
series: [{ name: 'Оценка за ДЗ', data: scores }],
categories,
};
}, [homeworkSubmissions, startStr, endStr]);
// Посещаемость: по датам — сколько занятий было проведено; ось X = все даты периода
const attendanceChart = useMemo(() => {
const allDates = getDatesInRange(startStr, endStr);
const categories = allDates.map((d) => {
const [, m, day] = d.split('-');
return `${day}.${m}`;
});
const byDate: Record = {};
lessons.forEach((l) => {
if (l.status !== 'completed') return;
const key = l.start_time?.slice(0, 10) || '';
if (!key) return;
byDate[key] = (byDate[key] ?? 0) + 1;
});
const data = allDates.map((d) => byDate[d] ?? 0);
return {
series: [{ name: 'Занятия проведены', data }],
categories,
};
}, [lessons, startStr, endStr]);
const subjects = subjectsFromLessons;
const loading = lessonsLoading && lessons.length === 0;
const chartOptionsBase = useMemo(
() => ({
chart: {
toolbar: {
show: true,
tools: {
download: true,
zoom: true,
zoomin: true,
zoomout: true,
pan: true,
reset: true,
},
},
zoom: { enabled: true, type: 'x' as const, allowMouseWheelZoom: true },
pan: { enabled: true, type: 'x' as const },
selection: { enabled: true, type: 'x' as const },
},
stroke: { curve: 'smooth' as const, width: 2 },
colors: CHART_COLORS,
dataLabels: { enabled: false },
xaxis: {
axisBorder: { show: false },
axisTicks: { show: false },
labels: { style: { colors: 'var(--md-sys-color-on-surface-variant)', fontSize: '12px' } },
},
yaxis: {
labels: { style: { colors: 'var(--md-sys-color-on-surface-variant)', fontSize: '12px' } },
},
legend: {
position: 'bottom' as const,
horizontalAlign: 'center' as const,
labels: { colors: 'var(--md-sys-color-on-surface-variant)' },
},
grid: { borderColor: 'var(--ios26-list-divider)', strokeDashArray: 4 },
}),
[],
);
const selectStyle = {
padding: '8px 12px',
borderRadius: 12,
border: '1px solid var(--ios26-list-divider, rgba(0,0,0,0.12))',
background: 'var(--md-sys-color-surface-container-low)',
color: 'var(--md-sys-color-on-surface)',
minWidth: 180,
fontSize: 14,
cursor: 'pointer' as const,
outline: 'none' as const,
};
return (
{/* Ячейка 1: Общая статистика за период + выбор предмета и даты */}
setSelectedSubject(e.target.value)}
disabled={!subjects.length}
style={{
...selectStyle,
opacity: subjects.length ? 1 : 0.7,
}}
>
{subjects.length === 0 ? (
Нет предметов
) : (
subjects.map((s) => (
{s}
))
)}
setDateRangeValue({ start_date: v.start_date, end_date: v.end_date })}
disabled={subjectsLoading}
/>
}
/>
{loading ? (
) : (
Занятий проведено
{periodStats.completedLessons}
из {periodStats.totalLessons}
Посещаемость
{periodStats.attendanceRate}%
Средняя оценка
{periodStats.avgGrade || '—'}
ДЗ с оценкой
{periodStats.hwGraded}
)}
{/* Ячейка 2: Успеваемость (оценки репетитора и школы) */}
{gradesChart.categories.length === 0 ? (
Нет оценок за период
) : (
)}
{/* Ячейка 3: Успеваемость по ДЗ */}
{homeworkChart.categories.length === 0 ? (
Нет оценок за ДЗ за период
) : (
)}
{/* Ячейка 4: Посещаемость — проведённые занятия по датам */}
{attendanceChart.categories.length === 0 ? (
Нет проведённых занятий за период
) : (
String(Math.round(val)),
},
},
}}
/>
)}
);
}