/** * API модуль для домашних заданий */ import apiClient from '@/lib/api-client'; export interface HomeworkMentor { id: number; email: string; first_name: string; last_name: string; } /** Файл задания/решения (ментор прикрепляет к заданию, ученик видит и скачивает). */ export interface HomeworkFileItem { id: number; file_type: 'assignment' | 'submission' | 'feedback'; file: string; filename: string; file_size: number; /** Признак изображения по расширению (с бэкенда) — показывать превью и открывать в модалке. */ is_image?: boolean; uploaded_by?: { id: number; first_name: string; last_name: string } | null; created_at: string; } export interface Homework { id: number; title: string; description?: string; mentor: HomeworkMentor; lesson: number | null; deadline: string | null; max_score: number; passing_score: number; status: 'draft' | 'published' | 'archived'; /** Черновик «заполнить позже» — создан при завершении урока, нужно дописать задание. */ fill_later?: boolean; total_submissions: number; checked_submissions: number; returned_submissions: number; average_score: number; is_overdue: boolean; created_at: string; published_at: string | null; /** Файл задания (один), URL для скачивания. */ attachment?: string | null; /** Ссылка на материал (внешняя). */ attachment_url?: string | null; /** Дополнительные файлы задания (ментор прикрепляет несколько). */ files?: HomeworkFileItem[] | null; students?: { id: number; first_name: string; last_name: string; score: number | null; status: string }[] | null; student_score?: { score: number | null; max_score: number; status: string } | null; /** Только для ментора: количество решений с черновиком от ИИ (status=pending, ai_checked_at задано). */ ai_draft_count?: number; } export interface HomeworkSubmission { id: number; homework: { id: number; title: string; description?: string; max_score: number }; student: { id: number; first_name: string; last_name: string; email: string }; status: string; content?: string; /** Основной файл решения (URL для скачивания). */ attachment?: string | null; attachment_url?: string | null; /** Доп. файлы решения (студент прикрепляет несколько). */ files?: HomeworkFileItem[] | null; score?: number | null; feedback?: string; /** HTML комментария проверки (markdown → HTML). */ feedback_html?: string; submitted_at: string; checked_at?: string | null; ai_score?: number | null; ai_feedback?: string; /** HTML превью черновика ИИ (markdown → HTML). */ ai_feedback_html?: string; ai_checked_at?: string | null; /** True, если оценка опубликована автоматически через ИИ. */ graded_by_ai?: boolean; checked_by?: { id: number; first_name: string; last_name: string } | null; } export async function getHomework(params?: { status?: string; page_size?: number; child_id?: string; }): Promise<{ results: Homework[]; count: number }> { const q = new URLSearchParams(); if (params?.status) q.append('status', params.status); if (params?.page_size) q.append('page_size', String(params.page_size || 1000)); if (params?.child_id) q.append('child_id', params.child_id); const query = q.toString(); const url = `/homework/homeworks/${query ? `?${query}` : ''}`; const res = await apiClient.get<{ results: Homework[]; count: number } | Homework[]>(url); const data = res.data; if (Array.isArray(data)) { return { results: data, count: data.length }; } return { results: data?.results ?? [], count: data?.count ?? 0, }; } export async function getHomeworkById(id: string | number): Promise { const res = await apiClient.get(`/homework/homeworks/${id}/`); return res.data; } /** Создать домашнее задание (в т.ч. черновик для «заполнить позже»). По умолчанию макс. балл 5, проходной 1 (не учитывается). */ export async function createHomework(data: { title: string; description?: string; lesson_id?: number; status?: 'draft' | 'published'; /** Пометить как «заполнить позже» — отображается в колонке «Ожидают заполнения» у ментора. */ fill_later?: boolean; /** Максимальный балл (1–5 по умолчанию). По умолчанию 5. */ max_score?: number; /** Проходной балл (по умолчанию 1, не учитывается). */ passing_score?: number; }): Promise { const payload = { ...data, max_score: data.max_score ?? 5, passing_score: data.passing_score ?? 1, }; const res = await apiClient.post('/homework/homeworks/', payload); return res.data; } /** Опции запроса списка решений (например, отключить кэш для актуальных данных). */ export interface GetHomeworkSubmissionsOptions { cache?: boolean; /** Для родителя: user_id ребёнка — вернуть решения этого ребёнка. */ child_id?: string | null; } export async function getHomeworkSubmissions( homeworkId: string | number, options?: GetHomeworkSubmissionsOptions ): Promise { const params = new URLSearchParams({ homework_id: String(homeworkId) }); if (options?.child_id) params.append('child_id', options.child_id); const res = await apiClient.get<{ results: HomeworkSubmission[] } | HomeworkSubmission[]>( `/homework/submissions/?${params.toString()}`, { cache: options?.cache ?? false } ); const data = res.data; if (Array.isArray(data)) return data; return data?.results ?? []; } export async function getMySubmission( homeworkId: string | number, options?: GetHomeworkSubmissionsOptions ): Promise { const list = await getHomeworkSubmissions(homeworkId, options); return list.length > 0 ? list[0] : null; } /** Получить одно решение по ID (для детального просмотра). */ export async function getHomeworkSubmission( submissionId: string | number ): Promise { const res = await apiClient.get( `/homework/submissions/${submissionId}/` ); return res.data; } /** * ДЗ с оценками по предмету для графика прогресса. * GET /api/homework/submissions/by_subject/ */ export async function getHomeworkSubmissionsBySubject(params: { subject: string; start_date?: string; end_date?: string; child_id?: string; }): Promise<{ count: number; results: HomeworkSubmission[] }> { const q = new URLSearchParams(); q.append('subject', params.subject); if (params.start_date) q.append('start_date', params.start_date); if (params.end_date) q.append('end_date', params.end_date); if (params.child_id) q.append('child_id', params.child_id); const res = await apiClient.get<{ count: number; results: HomeworkSubmission[] }>( `/homework/submissions/by_subject/?${q}` ); return res.data; } export async function gradeSubmission( submissionId: string | number, data: { score: number; feedback?: string } ): Promise { const res = await apiClient.post(`/homework/submissions/${submissionId}/grade/`, data); return res.data; } /** Использование токенов за один запрос (если API вернул). */ export interface TokenUsage { prompt_tokens: number; completion_tokens: number; total_tokens: number; } export interface CheckWithAiResponse { success: boolean; ai_score: number; ai_feedback: string; /** HTML для отображения комментария ИИ (markdown + LaTeX → HTML). */ ai_feedback_html?: string; ai_checked_at?: string; message?: string; /** Токены за эту проверку (потрачено). Остаток лимита — в кабинете Timeweb. */ usage?: TokenUsage; } /** Проверить решение через ИИ. Ментор: задание + решение → комментарий и оценка 1–5. */ export async function checkSubmissionWithAi( submissionId: string | number ): Promise { const res = await apiClient.post( `/homework/submissions/${submissionId}/check_with_ai/` ); return res.data; } export async function returnSubmissionForRevision( submissionId: string | number, feedback: string ): Promise { const res = await apiClient.post(`/homework/submissions/${submissionId}/return_for_revision/`, { feedback }); return res.data; } /** Удалить своё решение (студент). Задание снова переходит в ожидание загрузки. */ export async function deleteSubmission(submissionId: string | number): Promise { await apiClient.delete(`/homework/submissions/${submissionId}/`); } const MAX_HOMEWORK_FILE_SIZE = 50 * 1024 * 1024; // 50 МБ const MAX_HOMEWORK_FILES = 10; export function validateHomeworkFiles(files: File[]): { valid: boolean; error?: string } { if (files.length === 0) return { valid: true }; if (files.length > MAX_HOMEWORK_FILES) { return { valid: false, error: `Максимум ${MAX_HOMEWORK_FILES} файлов` }; } for (const f of files) { if (f.size > MAX_HOMEWORK_FILE_SIZE) { return { valid: false, error: `Файл "${f.name}" больше 50 МБ` }; } } return { valid: true }; } /** * Обновить домашнее задание (для черновиков fill_later). * PATCH /api/homework/homeworks/{id}/ */ export async function updateHomework( homeworkId: string | number, data: { title?: string; description?: string; deadline?: string | null; status?: 'draft' | 'published'; fill_later?: boolean; } ): Promise { const res = await apiClient.patch(`/homework/homeworks/${homeworkId}/`, data); return res.data; } /** * Опубликовать домашнее задание (из черновика в published). * POST /api/homework/homeworks/{id}/publish/ */ export async function publishHomework(homeworkId: string | number): Promise { const res = await apiClient.post(`/homework/homeworks/${homeworkId}/publish/`); return res.data; } export async function submitHomework( homeworkId: string | number, data: { content?: string; text?: string; files?: File[] }, onUploadProgress?: (percent: number) => void ): Promise { const hasFiles = data.files && data.files.length > 0; if (hasFiles) { const formData = new FormData(); formData.append('homework_id', String(homeworkId)); if (data.content) formData.append('content', data.content); if (data.text) formData.append('content', data.text); data.files!.forEach((f) => formData.append('attachment', f)); const res = await apiClient.post(`/homework/submissions/`, formData, { onUploadProgress: onUploadProgress && (function (event: { loaded: number; total?: number }) { if (event.total && event.total > 0) { const percent = Math.round((event.loaded / event.total) * 100); onUploadProgress(Math.min(percent, 100)); } }), }); return res.data; } const res = await apiClient.post(`/homework/submissions/`, { homework_id: homeworkId, content: data.content || data.text || '', }); return res.data; }