292 lines
10 KiB
TypeScript
292 lines
10 KiB
TypeScript
'use client';
|
||
|
||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||
import { useAuth } from '@/contexts/AuthContext';
|
||
import { useSelectedChild } from '@/contexts/SelectedChildContext';
|
||
import {
|
||
getHomework,
|
||
getHomeworkById,
|
||
type Homework,
|
||
} from '@/api/homework';
|
||
import { HomeworkDetailsModal } from './HomeworkDetailsModal';
|
||
import { SubmitHomeworkModal } from './SubmitHomeworkModal';
|
||
import { DashboardLayout } from '@/components/dashboard/ui';
|
||
import { LoadingSpinner } from '@/components/common/LoadingSpinner';
|
||
|
||
function formatDate(s: string | null): string {
|
||
if (!s) return '—';
|
||
const d = new Date(s);
|
||
return isNaN(d.getTime()) ? '—' : d.toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' });
|
||
}
|
||
|
||
function formatTime(s: string | null): string {
|
||
if (!s) return '—';
|
||
const d = new Date(s);
|
||
return isNaN(d.getTime()) ? '—' : d.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
|
||
}
|
||
|
||
function getHomeworkStatus(hw: Homework): 'pending' | 'submitted' | 'returned' | 'reviewed' {
|
||
if (hw.status !== 'published') return 'pending';
|
||
if (hw.checked_submissions > 0 && hw.checked_submissions === hw.total_submissions) return 'reviewed';
|
||
if (hw.returned_submissions > 0 && hw.returned_submissions === hw.total_submissions) return 'returned';
|
||
if (hw.total_submissions > 0) return 'submitted';
|
||
return 'pending';
|
||
}
|
||
|
||
export function HomeworkPageContent() {
|
||
const { user } = useAuth();
|
||
const { selectedChild } = useSelectedChild();
|
||
const [homework, setHomework] = useState<Homework[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [selectedHomework, setSelectedHomework] = useState<Homework | null>(null);
|
||
const [detailsOpen, setDetailsOpen] = useState(false);
|
||
const [detailsLoading, setDetailsLoading] = useState(false);
|
||
const [submitId, setSubmitId] = useState<number | null>(null);
|
||
const [submitOpen, setSubmitOpen] = useState(false);
|
||
|
||
const loadHomework = useCallback(async () => {
|
||
try {
|
||
setLoading(true);
|
||
const res = await getHomework({
|
||
page_size: 1000,
|
||
...(user?.role === 'parent' && selectedChild?.id && { child_id: selectedChild.id }),
|
||
});
|
||
setHomework(res.results);
|
||
} catch (e) {
|
||
setError(e instanceof Error ? e.message : 'Ошибка загрузки');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, [user?.role, selectedChild?.id]);
|
||
|
||
useEffect(() => {
|
||
loadHomework();
|
||
}, [loadHomework]);
|
||
|
||
const userRole = user?.role ?? '';
|
||
|
||
const pending = useMemo(() => homework.filter((hw) => getHomeworkStatus(hw) === 'pending' && hw.status === 'published'), [homework]);
|
||
const submitted = useMemo(() => homework.filter((hw) => getHomeworkStatus(hw) === 'submitted'), [homework]);
|
||
const returned = useMemo(() => homework.filter((hw) => getHomeworkStatus(hw) === 'returned'), [homework]);
|
||
const reviewed = useMemo(() => homework.filter((hw) => getHomeworkStatus(hw) === 'reviewed'), [homework]);
|
||
/** Только для ментора: черновики «заполнить позже» — ожидают заполнения задания. */
|
||
const fillLater = useMemo(
|
||
() => (userRole === 'mentor' ? homework.filter((hw) => hw.fill_later === true) : []),
|
||
[homework, userRole]
|
||
);
|
||
/** Только для ментора: задания, у которых есть хотя бы одно решение с черновиком от ИИ. */
|
||
const aiDraft = useMemo(
|
||
() => (userRole === 'mentor' ? homework.filter((hw) => (hw.ai_draft_count ?? 0) > 0) : []),
|
||
[homework, userRole]
|
||
);
|
||
|
||
const handleViewDetails = useCallback(async (hw: Homework) => {
|
||
try {
|
||
setDetailsLoading(true);
|
||
const full = await getHomeworkById(hw.id);
|
||
setSelectedHomework(full);
|
||
setDetailsOpen(true);
|
||
} catch {
|
||
setSelectedHomework(hw);
|
||
setDetailsOpen(true);
|
||
} finally {
|
||
setDetailsLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
const handleSubmit = useCallback((hw: Homework) => {
|
||
setSubmitId(hw.id);
|
||
setSubmitOpen(true);
|
||
}, []);
|
||
|
||
const HomeworkCard = ({
|
||
hw,
|
||
badge,
|
||
onView,
|
||
onSubmit,
|
||
}: {
|
||
hw: Homework;
|
||
badge: string;
|
||
onView: () => void;
|
||
onSubmit?: () => void;
|
||
}) => (
|
||
<div
|
||
className="ios26-panel"
|
||
style={{ padding: 16, cursor: 'pointer' }}
|
||
onClick={onView}
|
||
>
|
||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 }}>
|
||
<span
|
||
style={{
|
||
display: 'inline-block',
|
||
fontSize: 11,
|
||
fontWeight: 600,
|
||
padding: '4px 10px',
|
||
borderRadius: 8,
|
||
background: hw.is_overdue ? 'rgba(186,26,26,0.15)' : 'var(--md-sys-color-primary-container)',
|
||
color: hw.is_overdue ? 'var(--md-sys-color-error)' : 'var(--md-sys-color-primary)',
|
||
}}
|
||
>
|
||
{badge}
|
||
</span>
|
||
</div>
|
||
<h4 style={{ fontSize: 15, fontWeight: 600, margin: '0 0 8px 0', color: 'var(--md-sys-color-on-surface)' }}>
|
||
{hw.title}
|
||
</h4>
|
||
<div style={{ fontSize: 13, color: 'var(--md-sys-color-on-surface-variant)' }}>
|
||
{userRole === 'client' && (
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
|
||
<span className="material-symbols-outlined" style={{ fontSize: 16 }}>person</span>
|
||
{hw.mentor.first_name} {hw.mentor.last_name}
|
||
</div>
|
||
)}
|
||
{userRole === 'mentor' && hw.total_submissions > 0 && (
|
||
<div style={{ marginBottom: 4 }}>Решений: {hw.total_submissions}</div>
|
||
)}
|
||
{hw.deadline && (
|
||
<>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
|
||
<span className="material-symbols-outlined" style={{ fontSize: 16 }}>calendar_today</span>
|
||
{formatDate(hw.deadline)} {formatTime(hw.deadline)}
|
||
</div>
|
||
</>
|
||
)}
|
||
{hw.student_score?.score != null && (
|
||
<div style={{ marginTop: 8, fontWeight: 600, color: 'var(--md-sys-color-primary)' }}>
|
||
Оценка: {hw.student_score.score} / 5
|
||
</div>
|
||
)}
|
||
</div>
|
||
{userRole === 'client' && onSubmit && getHomeworkStatus(hw) === 'pending' && (
|
||
<button
|
||
type="button"
|
||
style={{
|
||
marginTop: 12,
|
||
width: '100%',
|
||
padding: '10px 16px',
|
||
borderRadius: 12,
|
||
border: 'none',
|
||
background: 'var(--md-sys-color-primary)',
|
||
color: 'var(--md-sys-color-on-primary)',
|
||
fontSize: 14,
|
||
fontWeight: 600,
|
||
cursor: 'pointer',
|
||
}}
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
onSubmit();
|
||
}}
|
||
>
|
||
Сдать ДЗ
|
||
</button>
|
||
)}
|
||
</div>
|
||
);
|
||
|
||
const Column = ({
|
||
title,
|
||
count,
|
||
items,
|
||
getBadge,
|
||
}: {
|
||
title: string;
|
||
count: number;
|
||
items: Homework[];
|
||
getBadge: (hw: Homework) => string;
|
||
}) => (
|
||
<div className="ios26-feedback-column">
|
||
<h3 className="ios26-feedback-column__title">
|
||
{title} {count > 0 ? `(${count})` : ''}
|
||
</h3>
|
||
<div className="ios26-feedback-column__cards">
|
||
{items.map((hw) => (
|
||
<HomeworkCard
|
||
key={hw.id}
|
||
hw={hw}
|
||
badge={getBadge(hw)}
|
||
onView={() => handleViewDetails(hw)}
|
||
onSubmit={userRole === 'client' ? () => handleSubmit(hw) : undefined}
|
||
/>
|
||
))}
|
||
{items.length === 0 && (
|
||
<p style={{ fontSize: 14, color: 'var(--md-sys-color-on-surface-variant)', padding: 16 }}>
|
||
Нет заданий
|
||
</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
|
||
return (
|
||
<DashboardLayout className="ios26-dashboard ios26-feedback-page" data-tour="homework-root">
|
||
{error && (
|
||
<div
|
||
style={{
|
||
padding: 16,
|
||
marginBottom: 16,
|
||
background: 'rgba(186,26,26,0.1)',
|
||
borderRadius: 12,
|
||
color: 'var(--md-sys-color-error)',
|
||
}}
|
||
>
|
||
{error}
|
||
</div>
|
||
)}
|
||
|
||
{loading ? (
|
||
<div style={{ display: 'flex', justifyContent: 'center', padding: 48 }}>
|
||
<LoadingSpinner size="medium" />
|
||
</div>
|
||
) : homework.length === 0 ? (
|
||
<p style={{ fontSize: 16, color: 'var(--md-sys-color-on-surface-variant)', padding: 24 }}>
|
||
Нет заданий
|
||
</p>
|
||
) : (
|
||
<div className="ios26-feedback-kanban ios26-homework-kanban">
|
||
{userRole === 'mentor' && fillLater.length > 0 && (
|
||
<Column title="Ожидают заполнения" count={fillLater.length} items={fillLater} getBadge={() => 'Заполнить позже'} />
|
||
)}
|
||
{pending.length > 0 && (
|
||
<Column title="Ожидают" count={pending.length} items={pending} getBadge={(hw) => (hw.is_overdue ? 'Просрочено' : 'Домашнее задание')} />
|
||
)}
|
||
{submitted.length > 0 && (
|
||
<Column title="На проверке" count={submitted.length} items={submitted} getBadge={() => 'На проверке'} />
|
||
)}
|
||
{userRole === 'mentor' && aiDraft.length > 0 && (
|
||
<Column title="Черновик от ИИ" count={aiDraft.length} items={aiDraft} getBadge={() => 'Черновик от ИИ'} />
|
||
)}
|
||
{returned.length > 0 && (
|
||
<Column title="На доработке" count={returned.length} items={returned} getBadge={() => 'На доработке'} />
|
||
)}
|
||
{reviewed.length > 0 && (
|
||
<Column title="Проверено" count={reviewed.length} items={reviewed} getBadge={() => 'Проверено'} />
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
<HomeworkDetailsModal
|
||
isOpen={detailsOpen}
|
||
homework={selectedHomework}
|
||
userRole={userRole}
|
||
childId={userRole === 'parent' ? selectedChild?.id ?? null : null}
|
||
onClose={() => {
|
||
setDetailsOpen(false);
|
||
setSelectedHomework(null);
|
||
}}
|
||
onSuccess={loadHomework}
|
||
/>
|
||
|
||
<SubmitHomeworkModal
|
||
isOpen={submitOpen}
|
||
homeworkId={submitId ?? 0}
|
||
onClose={() => {
|
||
setSubmitOpen(false);
|
||
setSubmitId(null);
|
||
}}
|
||
onSuccess={loadHomework}
|
||
/>
|
||
</DashboardLayout>
|
||
);
|
||
}
|