uchill/front_material/app/(protected)/schedule/page.tsx

465 lines
17 KiB
TypeScript
Raw 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.

'use client';
import { useState, useEffect, useCallback, useRef } from 'react';
import {
startOfDay,
format,
addDays,
subDays,
subMonths,
addMonths,
startOfMonth,
endOfMonth,
differenceInMinutes,
} from 'date-fns';
import { ru } from 'date-fns/locale';
import { Calendar } from '@/components/calendar/calendar';
import { CheckLesson } from '@/components/checklesson/checklesson';
import { getLessonsCalendar, getLesson, createLesson, updateLesson, deleteLesson } from '@/api/schedule';
import { getStudents } from '@/api/students';
import { useAuth } from '@/contexts/AuthContext';
import { useSelectedChild } from '@/contexts/SelectedChildContext';
import { getSubjects, getMentorSubjects } from '@/api/subjects';
import { loadComponent } from '@/lib/material-components';
import { LoadingSpinner } from '@/components/common/LoadingSpinner';
import { ErrorDisplay } from '@/components/common/ErrorDisplay';
import type { CalendarLesson } from '@/components/calendar/calendar';
import type { CheckLessonFormData, CheckLessonProps } from '@/components/checklesson/checklesson';
import type { LessonPreview } from '@/api/dashboard';
import type { Student } from '@/api/students';
import type { Subject, MentorSubject } from '@/api/subjects';
export default function SchedulePage() {
const { user } = useAuth();
const { selectedChild } = useSelectedChild();
const isMentor = user?.role === 'mentor';
const [selectedDate, setSelectedDate] = useState<Date>(startOfDay(new Date()));
const [displayDate, setDisplayDate] = useState<Date>(startOfDay(new Date()));
const [visibleMonth, setVisibleMonth] = useState<Date>(() => startOfMonth(new Date()));
const [lessons, setLessons] = useState<CalendarLesson[]>([]);
const [lessonsLoading, setLessonsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const formDataLoadedRef = useRef(false);
const hasLoadedLessonsOnceRef = useRef(false);
// Форма
const [isFormVisible, setIsFormVisible] = useState(false);
const [isEditingMode, setIsEditingMode] = useState(false);
const [formLoading, setFormLoading] = useState(false);
const [formError, setFormError] = useState<string | null>(null);
const [formData, setFormData] = useState<CheckLessonFormData>({
client: '',
title: '',
description: '',
start_date: format(selectedDate, 'yyyy-MM-dd'),
start_time: '14:00',
duration: 60,
price: undefined,
is_recurring: false,
});
const [selectedSubjectId, setSelectedSubjectId] = useState<number | null>(null);
const [selectedMentorSubjectId, setSelectedMentorSubjectId] = useState<number | null>(null);
const [editingLessonId, setEditingLessonId] = useState<string | null>(null);
// Компоненты Material Web
const [buttonComponentsLoaded, setButtonComponentsLoaded] = useState(false);
const [formComponentsLoaded, setFormComponentsLoaded] = useState(false);
// Данные для формы
const [students, setStudents] = useState<Student[]>([]);
const [subjects, setSubjects] = useState<Subject[]>([]);
const [mentorSubjects, setMentorSubjects] = useState<MentorSubject[]>([]);
const [lessonEditLoading, setLessonEditLoading] = useState(false);
useEffect(() => {
Promise.all([
loadComponent('elevated-card'),
loadComponent('filled-button'),
loadComponent('icon'),
]).then(() => {
setButtonComponentsLoaded(true);
});
Promise.all([
loadComponent('filled-button'),
loadComponent('outlined-button'),
loadComponent('text-field'),
loadComponent('select'),
loadComponent('switch'),
]).then(() => {
setFormComponentsLoaded(true);
});
}, []);
useEffect(() => {
if (!isFormVisible || formDataLoadedRef.current) return;
(async () => {
try {
const [studentsResp, subjectsResp, mentorSubjectsResp] = await Promise.all([
getStudents({ page: 1, page_size: 200 }),
getSubjects(),
getMentorSubjects(),
]);
setStudents(studentsResp.results || []);
setSubjects(subjectsResp || []);
setMentorSubjects(mentorSubjectsResp || []);
formDataLoadedRef.current = true;
} catch (err) {
console.error('Error loading form data:', err);
}
})();
}, [isFormVisible]);
const loadLessons = useCallback(async () => {
const start = startOfMonth(subMonths(visibleMonth, 1));
const end = endOfMonth(addMonths(visibleMonth, 1));
const isInitial = !hasLoadedLessonsOnceRef.current;
try {
if (isInitial) setLessonsLoading(true);
setError(null);
const { lessons: lessonsData } = await getLessonsCalendar({
start_date: format(start, 'yyyy-MM-dd'),
end_date: format(end, 'yyyy-MM-dd'),
...(selectedChild?.id && { child_id: selectedChild.id }),
});
const mappedLessons: CalendarLesson[] = (lessonsData || []).map((lesson: any) => ({
id: lesson.id,
title: lesson.title,
start_time: lesson.start_time,
end_time: lesson.end_time,
status: lesson.status,
client: lesson.client?.id,
client_name: lesson.client_name ?? (lesson.client?.user
? `${lesson.client.user.first_name || ''} ${lesson.client.user.last_name || ''}`.trim()
: undefined),
subject: lesson.subject ?? lesson.subject_name ?? '',
}));
setLessons(mappedLessons);
hasLoadedLessonsOnceRef.current = true;
} catch (err: any) {
console.error('Error loading lessons:', err);
setError(err?.message || 'Ошибка загрузки занятий');
} finally {
if (isInitial) setLessonsLoading(false);
}
}, [visibleMonth, selectedChild?.id]);
useEffect(() => {
loadLessons();
}, [loadLessons]);
const handleMonthChange = useCallback((start: Date, _end: Date) => {
const key = format(start, 'yyyy-MM');
if (key === format(visibleMonth, 'yyyy-MM')) return;
setVisibleMonth(startOfMonth(start));
}, [visibleMonth]);
const lessonsForSelectedDate: LessonPreview[] = lessons
.filter((lesson) => {
const lessonDate = startOfDay(new Date(lesson.start_time));
return lessonDate.getTime() === selectedDate.getTime();
})
.map((lesson) => ({
id: String(lesson.id),
title: lesson.title || 'Занятие',
subject: lesson.subject ?? '',
start_time: lesson.start_time,
end_time: lesson.end_time,
status: (lesson.status || 'scheduled') as 'scheduled' | 'in_progress' | 'completed' | 'cancelled',
client: lesson.client_name
? {
id: String(lesson.client || ''),
name: lesson.client_name,
first_name: lesson.client_name.split(' ')[0] || lesson.client_name,
last_name: lesson.client_name.split(' ').slice(1).join(' ') || '',
}
: undefined,
}));
const handleSelectSlot = (date: Date) => {
const dayStart = startOfDay(date);
setSelectedDate(dayStart);
setDisplayDate(dayStart);
setIsFormVisible(false);
};
const handleSelectEvent = (lesson: { id: string }) => {
if (isMentor) handleLessonClick(lesson);
};
const handlePrevDay = () => {
const prev = subDays(displayDate, 1);
setDisplayDate(prev);
setSelectedDate(startOfDay(prev));
setIsFormVisible(false);
};
const handleNextDay = () => {
const next = addDays(displayDate, 1);
setDisplayDate(next);
setSelectedDate(startOfDay(next));
setIsFormVisible(false);
};
const handleAddLesson = () => {
setIsEditingMode(false);
setFormData({
client: '',
title: '',
description: '',
start_date: format(selectedDate, 'yyyy-MM-dd'),
start_time: '14:00',
duration: 60,
price: undefined,
is_recurring: false,
});
setSelectedSubjectId(null);
setSelectedMentorSubjectId(null);
setIsFormVisible(true);
};
const handleLessonClick = (lesson: { id: string }) => {
if (!isMentor) return; // Добавить/редактировать/просмотр — только для ментора
setIsEditingMode(true);
setIsFormVisible(true);
setLessonEditLoading(true);
setFormError(null);
setEditingLessonId(lesson.id);
(async () => {
try {
const details = await getLesson(String(lesson.id));
const start = new Date(details.start_time);
const end = new Date(details.end_time);
const safeStart = startOfDay(start);
// синхронизируем правую панель с датой урока
setSelectedDate(safeStart);
setDisplayDate(safeStart);
const duration = (() => {
const mins = differenceInMinutes(end, start);
return Number.isFinite(mins) && mins > 0 ? mins : 60;
})();
setFormData({
client: details.client?.id ? String(details.client.id) : '',
title: details.title ?? '',
description: details.description ?? '',
start_date: format(start, 'yyyy-MM-dd'),
start_time: format(start, 'HH:mm'),
duration,
price: typeof details.price === 'number' ? details.price : undefined,
is_recurring: !!(details as any).is_recurring,
});
// пробуем выставить предмет по названию
const subjName = (details as any).subject_name || (details as any).subject || '';
if (subjName) {
const foundSubject = subjects.find((s) => s.name === subjName);
const foundMentorSubject = mentorSubjects.find((s) => s.name === subjName);
if (foundMentorSubject) {
setSelectedSubjectId(null);
setSelectedMentorSubjectId(foundMentorSubject.id);
} else if (foundSubject) {
setSelectedSubjectId(foundSubject.id);
setSelectedMentorSubjectId(null);
} else {
setSelectedSubjectId(null);
setSelectedMentorSubjectId(null);
}
} else {
setSelectedSubjectId(null);
setSelectedMentorSubjectId(null);
}
} catch (err: any) {
console.error('Error loading lesson:', err);
setFormError(err?.message || 'Не удалось загрузить данные занятия');
} finally {
setLessonEditLoading(false);
}
})();
};
const handleSubjectChange = (subjectId: number | null, mentorSubjectId: number | null) => {
setSelectedSubjectId(subjectId);
setSelectedMentorSubjectId(mentorSubjectId);
};
const getSubjectName = () => {
if (selectedSubjectId) {
const s = subjects.find((x) => x.id === selectedSubjectId);
return s?.name ?? '';
}
if (selectedMentorSubjectId) {
const s = mentorSubjects.find((x) => x.id === selectedMentorSubjectId);
return s?.name ?? '';
}
return '';
};
const generateTitle = () => {
const student = students.find((s) => String(s.id) === formData.client);
const studentName = student
? `${student.user?.first_name || ''} ${student.user?.last_name || ''}`.trim() || student.user?.email
: '';
const subjectName = getSubjectName();
if (studentName && subjectName) return `${subjectName}${studentName}`;
if (studentName) return studentName;
if (subjectName) return subjectName;
return formData.title || 'Занятие';
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setFormLoading(true);
setFormError(null);
try {
if (!formData.client) {
setFormError('Выберите ученика');
setFormLoading(false);
return;
}
if (!selectedSubjectId && !selectedMentorSubjectId) {
setFormError('Выберите предмет');
setFormLoading(false);
return;
}
if (!formData.start_date || !formData.start_time) {
setFormError('Укажите дату и время');
setFormLoading(false);
return;
}
if (formData.price == null || formData.price < 0) {
setFormError('Укажите стоимость занятия');
setFormLoading(false);
return;
}
const startUtc = new Date(`${formData.start_date}T${formData.start_time}`).toISOString();
const title = generateTitle();
if (isEditingMode && editingLessonId) {
await updateLesson(editingLessonId, {
title,
description: formData.description,
start_time: startUtc,
duration: formData.duration,
price: formData.price,
});
} else {
const payload: any = {
client: formData.client,
title,
description: formData.description,
start_time: startUtc,
duration: formData.duration,
price: formData.price,
is_recurring: formData.is_recurring,
};
if (selectedSubjectId) payload.subject_id = selectedSubjectId;
else if (selectedMentorSubjectId) payload.mentor_subject_id = selectedMentorSubjectId;
await createLesson(payload);
}
setIsFormVisible(false);
setEditingLessonId(null);
loadLessons();
} catch (err: any) {
const msg = err?.response?.data
? typeof err.response.data === 'object'
? Object.entries(err.response.data)
.map(([k, v]) => `${k}: ${Array.isArray(v) ? v.join(', ') : v}`)
.join('\n')
: String(err.response.data)
: err?.message || 'Ошибка сохранения занятия';
setFormError(msg);
} finally {
setFormLoading(false);
}
};
const handleDelete = async (deleteAllFuture: boolean) => {
if (!editingLessonId) return;
setFormLoading(true);
setFormError(null);
try {
await deleteLesson(editingLessonId, deleteAllFuture);
setIsFormVisible(false);
setEditingLessonId(null);
loadLessons();
} catch (err: any) {
setFormError(err?.message || 'Ошибка удаления занятия');
} finally {
setFormLoading(false);
}
};
const handleCancel = () => {
setIsFormVisible(false);
setIsEditingMode(false);
setFormError(null);
setEditingLessonId(null);
};
return (
<div className="ios26-dashboard ios26-schedule-page" style={{ padding: '16px' }}>
{error && <ErrorDisplay error={error} onRetry={loadLessons} />}
<div className="ios26-schedule-layout" style={{
display: 'grid',
gridTemplateColumns: '5fr 2fr',
gap: 'var(--ios26-spacing)',
alignItems: 'stretch',
// стабилизируем высоту секции (без фиксированных px),
// чтобы календарь не “схлопывался” на месяцах с меньшим количеством контента
minHeight: 'calc(100vh - 160px)',
}}>
<div className="ios26-schedule-calendar-wrap">
<Calendar
lessons={lessons}
lessonsLoading={lessonsLoading}
selectedDate={selectedDate}
onSelectSlot={handleSelectSlot}
onSelectEvent={handleSelectEvent}
onMonthChange={handleMonthChange}
/>
</div>
<div className="ios26-schedule-right-wrap">
<CheckLesson
selectedDate={selectedDate}
displayDate={displayDate}
lessonsLoading={lessonsLoading}
lessonsForSelectedDate={lessonsForSelectedDate}
isFormVisible={isFormVisible}
isMentor={isMentor}
onPrevDay={handlePrevDay}
onNextDay={handleNextDay}
onAddLesson={handleAddLesson}
onLessonClick={handleLessonClick}
buttonComponentsLoaded={buttonComponentsLoaded}
formComponentsLoaded={formComponentsLoaded}
lessonEditLoading={lessonEditLoading}
isEditingMode={isEditingMode}
formLoading={formLoading}
formError={formError}
formData={formData}
setFormData={setFormData}
selectedSubjectId={selectedSubjectId}
selectedMentorSubjectId={selectedMentorSubjectId}
onSubjectChange={handleSubjectChange}
students={students}
subjects={subjects}
mentorSubjects={mentorSubjects}
onSubmit={handleSubmit}
onCancel={handleCancel}
onDelete={isEditingMode ? handleDelete : undefined}
/>
</div>
</div>
</div>
);
}