250 lines
8.4 KiB
TypeScript
250 lines
8.4 KiB
TypeScript
/**
|
||
* Секция «Ближайшие занятия» для дашборда ментора (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" data-tour="mentor-upcoming">
|
||
<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>
|
||
}
|
||
/>
|
||
);
|
||
};
|