uchill/front_material/components/dashboard/mentor/UpcomingLessonsSection.tsx

250 lines
8.4 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.

/**
* Секция «Ближайшие занятия» для дашборда ментора (iOS 26).
*/
'use client';
import React, { useMemo, useState } from 'react';
import { MentorDashboardResponse } from '@/api/dashboard';
import { Panel, SectionHeader, FlipCard } from '../ui';
import { LoadingSpinner } from '@/components/common/LoadingSpinner';
import { getLesson, type Lesson } from '@/api/schedule';
export interface UpcomingLessonsSectionProps {
data: MentorDashboardResponse | null;
loading: boolean;
}
const formatDateTime = (dateTimeStr: string | null): string => {
if (!dateTimeStr) return '—';
try {
const date = new Date(dateTimeStr);
if (isNaN(date.getTime())) return '—';
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
const isToday = date.toDateString() === today.toDateString();
const isTomorrow = date.toDateString() === tomorrow.toDateString();
const timeStr = date.toLocaleTimeString('ru-RU', {
hour: '2-digit',
minute: '2-digit',
});
if (isToday) {
return `Сегодня, ${timeStr}`;
} else if (isTomorrow) {
return `Завтра, ${timeStr}`;
} else {
return date.toLocaleDateString('ru-RU', {
day: 'numeric',
month: 'short',
hour: '2-digit',
minute: '2-digit',
});
}
} catch {
return '—';
}
};
export const UpcomingLessonsSection: React.FC<UpcomingLessonsSectionProps> = ({
data,
loading,
}) => {
const lessons = data?.upcoming_lessons?.slice(0, 4) || [];
const [flipped, setFlipped] = useState(false);
const [selectedLessonId, setSelectedLessonId] = useState<string | null>(null);
const [details, setDetails] = useState<Lesson | null>(null);
const [loadingDetails, setLoadingDetails] = useState(false);
const selectedPreview = useMemo(
() => lessons.find((l) => l.id === selectedLessonId) || null,
[lessons, selectedLessonId],
);
const formatFullDateTime = (dateTimeStr: string | null): string => {
if (!dateTimeStr) return '—';
try {
const date = new Date(dateTimeStr);
if (isNaN(date.getTime())) return '—';
return date.toLocaleString('ru-RU', {
day: 'numeric',
month: 'long',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
} catch {
return '—';
}
};
const openLessonDetails = async (lessonId: string) => {
setSelectedLessonId(lessonId);
setFlipped(true);
setLoadingDetails(true);
setDetails(null);
try {
const full = await getLesson(lessonId);
setDetails(full);
} catch (error) {
console.error('Ошибка загрузки занятия:', error);
} finally {
setLoadingDetails(false);
}
};
return (
<FlipCard
flipped={flipped}
onFlippedChange={(v) => {
// если пользователь кликнул по "пустому месту" в карточке — не хотим случайно закрывать,
// но оставим стандартное поведение: переворот по клику на сам контейнер FlipCard.
setFlipped(v);
}}
front={
<Panel padding="md">
<SectionHeader title="Ближайшие занятия" />
{loading && !data ? (
<LoadingSpinner size="medium" />
) : lessons.length === 0 ? (
<div
style={{
padding: 'var(--ios26-spacing-md) 0',
textAlign: 'center',
color: 'var(--md-sys-color-on-surface-variant)',
fontSize: 14,
}}
>
Нет запланированных занятий
</div>
) : (
<div
className="upcoming-lessons-grid"
style={{
display: 'grid',
gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
gap: 'var(--ios26-spacing-sm)',
}}
onClick={(e) => e.stopPropagation()}
>
{lessons.map((lesson) => {
const client = lesson.client as any;
const fullName =
client?.first_name || client?.last_name
? `${client.first_name ?? ''} ${client.last_name ?? ''}`.trim()
: client?.name || 'Студент';
return (
<button
key={lesson.id}
type="button"
onClick={() => openLessonDetails(lesson.id)}
className="ios26-lesson-preview"
>
<div className="ios26-lesson-avatar">
{client?.avatar ? (
<img src={client.avatar} alt={fullName} />
) : (
<span>
{(fullName || 'С')[0]?.toUpperCase()}
</span>
)}
</div>
<div className="ios26-lesson-text">
<p className="ios26-lesson-subject">
{lesson.subject || 'Предмет не указан'}
</p>
<p className="ios26-lesson-student">
{fullName}
</p>
<p className="ios26-lesson-datetime">
{formatDateTime(lesson.start_time)}
</p>
</div>
</button>
);
})}
</div>
)}
</Panel>
}
back={
<Panel padding="md">
<SectionHeader
title="Детали занятия"
trailing={
<button
type="button"
onClick={(e) => {
e.stopPropagation();
setFlipped(false);
}}
className="ios26-back-button"
>
Назад
</button>
}
/>
<div onClick={(e) => e.stopPropagation()}>
{loadingDetails ? (
<div
style={{
padding: 'var(--ios26-spacing-md) 0',
textAlign: 'center',
color: 'var(--md-sys-color-on-surface-variant)',
fontSize: 14,
}}
>
Загрузка...
</div>
) : details ? (
<div>
<p style={{ fontSize: 18, fontWeight: 700, margin: '0 0 8px 0' }}>
{details.title || selectedPreview?.title || 'Занятие'}
</p>
{details.subject && (
<p style={{ fontSize: 14, margin: '0 0 8px 0', color: 'var(--md-sys-color-on-surface-variant)' }}>
Предмет: {details.subject}
</p>
)}
<p style={{ fontSize: 14, margin: '0 0 4px 0', color: 'var(--md-sys-color-on-surface-variant)' }}>
<strong>Начало:</strong> {formatFullDateTime(details.start_time)}
</p>
<p style={{ fontSize: 14, margin: '0 0 4px 0', color: 'var(--md-sys-color-on-surface-variant)' }}>
<strong>Окончание:</strong> {formatFullDateTime(details.end_time)}
</p>
{details.description && (
<p style={{ fontSize: 14, margin: '10px 0 0 0', lineHeight: 1.5 }}>
{details.description}
</p>
)}
{details.mentor_notes && (
<p style={{ fontSize: 13, margin: '10px 0 0 0', lineHeight: 1.5, fontStyle: 'italic', color: 'var(--md-sys-color-on-surface-variant)' }}>
<strong>Заметки:</strong> {details.mentor_notes}
</p>
)}
</div>
) : (
<div
style={{
padding: 'var(--ios26-spacing-md) 0',
textAlign: 'center',
color: 'var(--md-sys-color-on-surface-variant)',
fontSize: 14,
}}
>
Не удалось загрузить детали
</div>
)}
</div>
</Panel>
}
/>
);
};