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

258 lines
9.2 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 { getHomeworkSubmission, type HomeworkSubmission } from '@/api/homework';
export interface RecentSubmissionsSectionProps {
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 now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffDays = Math.floor(diffHours / 24);
if (diffHours < 1) {
const diffMins = Math.floor(diffMs / (1000 * 60));
return diffMins <= 1 ? 'только что' : `${diffMins} мин назад`;
} else if (diffHours < 24) {
return `${diffHours} ч назад`;
} else if (diffDays === 1) {
return 'Вчера';
} else if (diffDays < 7) {
return `${diffDays} дн назад`;
} else {
return date.toLocaleDateString('ru-RU', {
day: 'numeric',
month: 'short',
});
}
} catch {
return '—';
}
};
const getStatusColor = (status: string): string => {
switch (status) {
case 'graded':
return 'var(--md-sys-color-tertiary)';
case 'returned':
return 'var(--md-sys-color-error)';
case 'submitted':
return 'var(--md-sys-color-on-surface-variant)';
default:
return 'var(--md-sys-color-on-surface-variant)';
}
};
const getStatusLabel = (status: string): string => {
switch (status) {
case 'graded':
return 'Проверено';
case 'returned':
return 'На доработке';
case 'submitted':
return 'Сдано';
default:
return status;
}
};
export const RecentSubmissionsSection: React.FC<RecentSubmissionsSectionProps> = ({
data,
loading,
}) => {
const submissions = data?.recent_submissions?.slice(0, 4) || [];
const [flipped, setFlipped] = useState(false);
const [selectedSubmissionId, setSelectedSubmissionId] = useState<string | null>(null);
const [details, setDetails] = useState<HomeworkSubmission | null>(null);
const [loadingDetails, setLoadingDetails] = useState(false);
const selectedPreview = useMemo(
() => submissions.find((s) => s.id === selectedSubmissionId) || null,
[submissions, selectedSubmissionId],
);
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 openSubmissionDetails = async (submissionId: string) => {
setSelectedSubmissionId(submissionId);
setFlipped(true);
setLoadingDetails(true);
setDetails(null);
try {
const full = await getHomeworkSubmission(submissionId);
setDetails(full);
} catch (error) {
console.error('Ошибка загрузки решения ДЗ:', error);
} finally {
setLoadingDetails(false);
}
};
return (
<FlipCard
flipped={flipped}
onFlippedChange={setFlipped}
front={
<Panel padding="md">
<SectionHeader title="Последние сданные ДЗ" />
{loading && !data ? (
<LoadingSpinner size="medium" />
) : submissions.length === 0 ? (
<div
style={{
padding: 'var(--ios26-spacing-md) 0',
textAlign: 'center',
color: 'var(--md-sys-color-on-surface-variant)',
fontSize: 14,
}}
>
Нет сданных ДЗ
</div>
) : (
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
gap: 'var(--ios26-spacing-sm)',
}}
onClick={(e) => e.stopPropagation()}
>
{submissions.map((submission) => {
const studentName = submission.student?.first_name && submission.student?.last_name
? `${submission.student.first_name} ${submission.student.last_name}`.trim()
: submission.student?.name || 'Студент';
return (
<button
key={submission.id}
type="button"
onClick={() => openSubmissionDetails(submission.id)}
className="ios26-lesson-preview"
>
<div className="ios26-lesson-avatar">
{submission.student?.avatar ? (
<img src={submission.student.avatar} alt={studentName} />
) : (
<span>
{(studentName || 'С')[0]?.toUpperCase()}
</span>
)}
</div>
<div className="ios26-lesson-text">
<p className="ios26-lesson-subject">
{submission.subject || 'Предмет не указан'}
</p>
<p className="ios26-lesson-student">
{studentName}
</p>
{submission.score != null && (
<p className="ios26-lesson-datetime">
{submission.score}/5 {getStatusLabel(submission.status)}
</p>
)}
{submission.score == null && (
<p className="ios26-lesson-datetime">
{getStatusLabel(submission.status)}
</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.homework?.title || selectedPreview?.homework?.title || 'ДЗ'}
</p>
{details.homework?.description && (
<p style={{ fontSize: 14, margin: '0 0 10px 0', lineHeight: 1.5, color: 'var(--md-sys-color-on-surface-variant)' }}>
{details.homework.description}
</p>
)}
<p style={{ fontSize: 14, margin: '0 0 6px 0', color: 'var(--md-sys-color-on-surface-variant)' }}>
<strong>Студент:</strong> {details.student?.first_name} {details.student?.last_name}
</p>
<p style={{ fontSize: 14, margin: '0 0 6px 0', color: 'var(--md-sys-color-on-surface-variant)' }}>
<strong>Сдано:</strong> {formatFullDateTime(details.submitted_at)}
</p>
{details.score != null && (
<p style={{ fontSize: 14, fontWeight: 600, margin: '0 0 6px 0', color: getStatusColor(details.status) }}>
Оценка: {details.score}/5
</p>
)}
{details.feedback && (
<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.feedback}
</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>
}
/>
);
};