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

493 lines
18 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 { createDateTimeInUserTimezone, parseISOToUserTimezone } from '@/utils/timezone';
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 { getErrorMessage } from '@/lib/error-utils';
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);
const [editingLessonStatus, setEditingLessonStatus] = 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),
mentor_name: lesson.mentor_name ?? (lesson.mentor?.first_name || lesson.mentor?.last_name
? `${lesson.mentor?.first_name || ''} ${lesson.mentor?.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) => {
// Парсим дату в timezone пользователя для правильной фильтрации
const parsed = parseISOToUserTimezone(lesson.start_time, user?.timezone);
const lessonDate = startOfDay(parsed.dateObj);
return lessonDate.getTime() === selectedDate.getTime();
})
.sort((a, b) => {
// Сортируем по времени начала (раньше → первые)
return new Date(a.start_time).getTime() - new Date(b.start_time).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));
// Парсим время в timezone пользователя
const startParsed = parseISOToUserTimezone(details.start_time, user?.timezone);
const safeStart = startOfDay(startParsed.dateObj);
// синхронизируем правую панель с датой урока
setSelectedDate(safeStart);
setDisplayDate(safeStart);
const duration = (() => {
const start = new Date(details.start_time);
const end = new Date(details.end_time);
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: startParsed.date,
start_time: startParsed.time,
duration,
price: typeof details.price === 'number' ? details.price : undefined,
is_recurring: !!(details as any).is_recurring,
status: (details as any).status ?? 'completed',
});
setEditingLessonStatus((details as any).status ?? null);
// пробуем выставить предмет по названию
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 {
const isCompleted = editingLessonStatus === 'completed';
if (!isCompleted) {
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 = !isCompleted
? createDateTimeInUserTimezone(formData.start_date, formData.start_time, user?.timezone)
: '';
const title = generateTitle();
if (isEditingMode && editingLessonId) {
if (editingLessonStatus === 'completed') {
await updateLesson(editingLessonId, {
price: formData.price,
status: formData.status ?? 'completed',
});
} else {
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);
setEditingLessonStatus(null);
loadLessons();
} catch (err: any) {
setFormError(getErrorMessage(err, 'Не удалось сохранить занятие. Проверьте данные.'));
} finally {
setFormLoading(false);
}
};
const handleDelete = async (deleteAllFuture: boolean) => {
if (!editingLessonId) return;
setFormLoading(true);
setFormError(null);
try {
await deleteLesson(editingLessonId, deleteAllFuture);
setIsFormVisible(false);
setEditingLessonId(null);
setEditingLessonStatus(null);
loadLessons();
} catch (err: any) {
setFormError(getErrorMessage(err, 'Не удалось удалить занятие.'));
} finally {
setFormLoading(false);
}
};
const handleCancel = () => {
setIsFormVisible(false);
setIsEditingMode(false);
setFormError(null);
setEditingLessonId(null);
setEditingLessonStatus(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: 'min(calc(100vh - 160px), 600px)',
}}>
<div className="ios26-schedule-calendar-wrap">
<Calendar
lessons={lessons}
lessonsLoading={lessonsLoading}
selectedDate={selectedDate}
onSelectSlot={handleSelectSlot}
onSelectEvent={handleSelectEvent}
onMonthChange={handleMonthChange}
isMentor={isMentor}
userTimezone={user?.timezone}
/>
</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}
isCompletedLesson={editingLessonStatus === 'completed'}
/>
</div>
</div>
</div>
);
}