'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: Общая статистика за период + выбор предмета и даты */} 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)), }, }, }} /> )}
); }