'use client'; /** * Отдельный экран (breakpoint) завершения занятия — как в старом frontend. * 3 шага: Оценки → Комментарий → Домашнее задание. */ import React, { useEffect, useState, useRef } from 'react'; import { getLesson, completeLesson, getLessonFiles, createLessonFile, deleteLessonFile, } from '@/api/schedule'; import type { Lesson, LessonFile } from '@/api/schedule'; import { getMyMaterials } from '@/api/materials'; import type { Material } from '@/api/materials'; const MAX_LESSON_FILES = 10; const MAX_FILE_SIZE_MB = 10; export interface CompleteLessonModalProps { isOpen: boolean; lessonId: number | null; onClose: () => void; onSuccess: () => void; } type Step = 1 | 2 | 3; export function CompleteLessonModal({ isOpen, lessonId, onClose, onSuccess, }: CompleteLessonModalProps) { const [lesson, setLesson] = useState(null); const [loadingLesson, setLoadingLesson] = useState(false); const [step, setStep] = useState(1); const [formData, setFormData] = useState({ mentor_grade: '', school_grade: '', mentor_notes: '', homework_assigned: false, homework_text: '', }); const [lessonFiles, setLessonFiles] = useState([]); const [materials, setMaterials] = useState([]); const [filesLoading, setFilesLoading] = useState(false); const [materialsLoading, setMaterialsLoading] = useState(false); const [selectedMaterialIds, setSelectedMaterialIds] = useState([]); const [selectedFileIds, setSelectedFileIds] = useState>(new Set()); const [newFiles, setNewFiles] = useState([]); const [uploadingFiles, setUploadingFiles] = useState>(new Set()); const [materialsSearch, setMaterialsSearch] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const step3JustRenderedRef = useRef(false); useEffect(() => { if (!isOpen || lessonId == null) return; setStep(1); setFormData({ mentor_grade: '', school_grade: '', mentor_notes: '', homework_assigned: false, homework_text: '', }); setError(null); setLoadingLesson(true); getLesson(String(lessonId)) .then((l) => setLesson(l)) .catch(() => setLesson(null)) .finally(() => setLoadingLesson(false)); }, [isOpen, lessonId]); useEffect(() => { if (!isOpen || !lessonId) return; setFilesLoading(true); getLessonFiles(String(lessonId)) .then((files) => setLessonFiles(files ?? [])) .catch(() => setLessonFiles([])) .finally(() => setFilesLoading(false)); }, [isOpen, lessonId]); useEffect(() => { if (!isOpen) return; setMaterialsLoading(true); getMyMaterials() .then((list) => setMaterials(Array.isArray(list) ? list : [])) .catch(() => setMaterials([])) .finally(() => setMaterialsLoading(false)); }, [isOpen]); useEffect(() => { if (step === 3) { step3JustRenderedRef.current = true; const t = setTimeout(() => { step3JustRenderedRef.current = false; }, 300); return () => clearTimeout(t); } }, [step]); if (!isOpen) return null; const handleClose = () => { if (!loading) { onClose(); } }; const handleNext = () => { if (step < 3) setStep((s) => (s + 1) as Step); }; const handleBack = () => { if (step > 1) setStep((s) => (s - 1) as Step); }; const handleFileChange = async (e: React.ChangeEvent) => { const files = Array.from(e.target.files || []); if (!files.length || !lessonId) return; for (let i = 0; i < files.length; i++) { const file = files[i]; if (file.size > MAX_FILE_SIZE_MB * 1024 * 1024) continue; const fileId = `${file.name}-${file.size}-${i}-${Date.now()}`; setUploadingFiles((prev) => new Set(prev).add(fileId)); try { const created = await createLessonFile({ lesson: String(lessonId), file, filename: file.name, }); setLessonFiles((prev) => [created, ...prev]); setSelectedFileIds((prev) => new Set(prev).add(String(created.id))); } catch (err) { console.error('Ошибка загрузки файла урока', file.name, err); } finally { setUploadingFiles((prev) => { const next = new Set(prev); next.delete(fileId); return next; }); } } e.target.value = ''; }; const handleMaterialToggle = (materialId: string, attach: boolean) => { setSelectedMaterialIds((prev) => attach ? [...prev, materialId] : prev.filter((id) => id !== materialId) ); }; const handleDeleteFile = async (fileId: string) => { if (!window.confirm('Удалить этот файл из урока?')) return; try { await deleteLessonFile(fileId); setLessonFiles((prev) => prev.filter((f) => String(f.id) !== fileId)); setSelectedFileIds((prev) => { const next = new Set(prev); next.delete(fileId); return next; }); } catch (err) { console.error('Ошибка удаления файла урока', err); } }; const getFileUrl = (file: LessonFile): string | null => { const base = typeof window !== 'undefined' ? `${window.location.origin}` : ''; const path = file.file_url || file.file; if (!path) return null; return path.startsWith('http') ? path : `${base}${path}`; }; const handleSubmit = async () => { if (step !== 3 || !lessonId || step3JustRenderedRef.current) return; setError(null); setLoading(true); try { const currentAttachedMaterialIds = lessonFiles .map((f) => (f.material ? String(f.material) : null)) .filter((id): id is string => !!id); const materialsToAttach = selectedMaterialIds.filter( (id) => !currentAttachedMaterialIds.includes(id) ); const materialsToDetach = currentAttachedMaterialIds.filter( (id) => !selectedMaterialIds.includes(id) ); for (const materialId of materialsToAttach) { try { await createLessonFile({ lesson: String(lessonId), material: materialId, }); } catch (err) { console.error('Ошибка прикрепления материала', materialId, err); } } for (const materialId of materialsToDetach) { const fileToDelete = lessonFiles.find( (f) => f.material != null && String(f.material) === materialId ); if (fileToDelete) { try { await deleteLessonFile(String(fileToDelete.id)); } catch (err) { if (err && typeof err === 'object' && 'status' in err && err.status === 404) { // уже удалено } else { console.error('Ошибка открепления материала', materialId, err); } } } } const notes = (formData.mentor_notes || '').trim(); const mentorGrade = formData.mentor_grade ? parseInt(formData.mentor_grade, 10) : undefined; const schoolGrade = formData.school_grade ? parseInt(formData.school_grade, 10) : undefined; const homeworkText = formData.homework_assigned ? (formData.homework_text || '').trim() : undefined; const uploadedFiles = lessonFiles.filter( (f) => f.file || (f as { source?: string }).source === 'uploaded' ); const hasHomeworkFiles = uploadedFiles.some((f) => selectedFileIds.has(String(f.id))) || selectedMaterialIds.length > 0; const result = await completeLesson( String(lessonId), notes, mentorGrade, schoolGrade, homeworkText || undefined, hasHomeworkFiles ); if (!result?.success) { setError(result?.message || 'Не удалось завершить занятие.'); return; } onSuccess(); onClose(); } catch (err) { setError(err instanceof Error ? err.message : 'Ошибка завершения занятия'); } finally { setLoading(false); } }; if (loadingLesson || !lesson) { return (
Загрузка…
); } return (

Завершить занятие

{lesson.title} {lesson.subject ? `— ${lesson.subject}` : ''}

{[1, 2, 3].map((s) => (
= s ? 'var(--md-sys-color-primary)' : 'var(--md-sys-color-surface-variant)', color: step > s ? 'var(--md-sys-color-on-primary)' : 'var(--md-sys-color-on-surface-variant)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 13, fontWeight: 600, }} > {step > s ? '✓' : s}
{s < 3 && (
= 2 && s === 1 ? 'var(--md-sys-color-primary)' : step >= 3 && s === 2 ? 'var(--md-sys-color-primary)' : 'var(--md-sys-color-outline-variant)', }} /> )} ))} {step === 1 && 'Оценки'} {step === 2 && 'Комментарий'} {step === 3 && 'ДЗ'}
{step === 1 && (

Оценки (необязательно)

setFormData({ ...formData, mentor_grade: e.target.value })} style={{ width: '100%', padding: '12px 16px', borderRadius: 12, border: '1px solid var(--md-sys-color-outline)', background: 'var(--md-sys-color-surface)', fontSize: 15, color: 'var(--md-sys-color-on-surface)', }} />
setFormData({ ...formData, school_grade: e.target.value })} style={{ width: '100%', padding: '12px 16px', borderRadius: 12, border: '1px solid var(--md-sys-color-outline)', background: 'var(--md-sys-color-surface)', fontSize: 15, color: 'var(--md-sys-color-on-surface)', }} />
)} {step === 2 && (

Комментарий к занятию (необязательно)