"use client"; /** * Homework domain API(ARCHITECTURE.md §5.1 / §5.3 / §9.1 教师域作业模块) * * 三类操作: * 1. useHomework(按 id 单查):✅ 真实查询 homework(id: ID!),schema 已就绪 * 2. 列表/提交查询/mutation:❌ schema 无对应字段 → MSW 兜底(@contract-pending) * * 契约工单:docs/architecture/issues/contracts/core-edu_contract.md * 后端补齐后:重跑 normalize + codegen → 关闭 skipDocumentsValidation → 切换 fetcher → 删 mock * * 关联:ARCHITECTURE.md §5.3 契约纪律 / §5.4 MSW 兜底 / §9.1 / §11.4 契约工单 */ import type { FetchPolicy } from "@apollo/client"; import { useWidgetMutation } from "../useWidgetMutation"; import { useWidgetQuery } from "../useWidgetQuery"; import { ApiError } from "./errors"; import { ASSIGN_HOMEWORK_DOC, GET_AI_BATCH_GRADING_DOC, GET_ASSIGNMENT_SUBMISSIONS_DOC, GET_HOMEWORK_DOC, GET_HOMEWORK_LIST_DOC, GET_HOMEWORK_SUBMISSIONS_DOC, GET_SUBMISSION_DETAIL_DOC, GRADE_SUBMISSION_DOC, RECORD_GRADE_DOC, SAVE_SCAN_GRADING_DOC, } from "./operations/homework.graphql"; import type { UseQueryResult } from "./types"; // ===== 数据类型(对齐 schema Homework 类型)===== /** * 作业实体(对齐 combined-schema.graphql Homework 类型,core-edu 子图) * * 字段命名 camelCase(与 schema 一致)。 */ export interface Homework { id: string; classId: string; subjectId: string; title: string; description: string | null; dueDate: string; gracePeriod: number; status: string; schoolId: string; createdBy: string; createdAt: string; updatedAt: string; } /** 作业列表项(轻量字段集,用于列表渲染) */ export interface HomeworkListItem { id: string; classId: string; subjectId: string; title: string; description: string | null; dueDate: string; gracePeriod: number; status: string; createdAt: string; } /** 列表查询响应(@contract-pending 假契约形状,MSW 返回此结构) */ interface HomeworkListResponse { homeworks: { items: HomeworkListItem[]; total: number; }; } /** 单查响应(真实 schema) */ interface HomeworkResponse { homework: Homework | null; } /** 布置作业输入 */ export interface AssignHomeworkInput { classId: string; subjectId: string; title: string; description?: string; dueDate: string; gracePeriod?: number; } /** 布置作业 mutation 响应(@contract-pending) */ interface AssignHomeworkResponse { assignHomework: { id: string } | null; } // ===== 提交相关类型(@contract-pending 全 MSW)===== /** 提交状态枚举 */ export type SubmissionStatus = "SUBMITTED" | "GRADING" | "GRADED" | "RETURNED" | "LATE"; /** 作业提交列表项 */ export interface HomeworkSubmissionItem { id: string; homeworkId: string; homeworkTitle: string; studentId: string; studentName: string; studentNo: string; classId: string; className: string; status: string; submittedAt: string | null; gradedAt: string | null; gradedBy: string | null; totalScore: number | null; maxScore: number; } /** 提交列表筛选 */ export interface SubmissionsFilter { classId?: string; homeworkId?: string; status?: string; limit?: number; offset?: number; } /** 提交列表响应 */ interface HomeworkSubmissionsResponse { homeworkSubmissions: { items: HomeworkSubmissionItem[]; total: number }; } /** 单题作答 */ export interface SubmissionAnswer { id: string; submissionId: string; questionId: string; questionTitle: string; questionType: string; maxScore: number; answer: string; score: number | null; teacherComment: string | null; isCorrect: boolean | null; aiSuggestion: string | null; } /** 批改页导航信息 */ export interface SubmissionNavigation { prevId: string | null; nextId: string | null; currentIndex: number; totalCount: number; } /** 单份提交详情 */ export interface SubmissionDetail { submission: HomeworkSubmissionItem & { feedback: string | null }; answers: SubmissionAnswer[]; navigation: SubmissionNavigation; } /** 单份提交详情响应 */ interface SubmissionDetailResponse { submissionDetail: SubmissionDetail | null; } /** 批改提交输入 */ export interface GradeSubmissionInput { submissionId: string; answers: Array<{ questionId: string; score: number; teacherComment?: string; }>; feedback?: string; } /** 批改提交响应 */ interface GradeSubmissionResponse { gradeSubmission: { submissionId: string } | null; } /** 保存扫描批改输入 */ export interface SaveScanGradingInput { submissionId: string; answers: Array<{ questionId: string; score: number; teacherComment?: string; }>; feedback?: string; } /** 保存扫描批改响应 */ interface SaveScanGradingResponse { saveScanGrading: { submissionId: string } | null; } /** AI 批量评分建议项 */ export interface AiGradingSuggestion { submissionId: string; studentName: string; suggestedScore: number; confidence: number; reasoning: string; } /** AI 批量评分汇总 */ export interface AiBatchGradingSummary { totalSubmissions: number; processed: number; avgConfidence: number; } /** AI 批量评分数据 */ export interface AiBatchGrading { homeworkId: string; suggestions: AiGradingSuggestion[]; summary: AiBatchGradingSummary; } /** AI 批量评分响应 */ interface AiBatchGradingResponse { aiBatchGrading: AiBatchGrading | null; } /** 按作业汇总统计 */ export interface AssignmentStats { totalStudents: number; submittedCount: number; gradedCount: number; pendingCount: number; avgScore: number; submissionRate: number; } /** 按作业的作业摘要 */ export interface AssignmentHomeworkSummary { id: string; title: string; classId: string; className: string; dueDate: string; maxScore: number; } /** 按作业的提交列表数据 */ export interface AssignmentSubmissions { homework: AssignmentHomeworkSummary; stats: AssignmentStats; submissions: HomeworkSubmissionItem[]; } /** 按作业提交列表响应 */ interface AssignmentSubmissionsResponse { assignmentSubmissions: AssignmentSubmissions | null; } /** 详情页内联批改输入 */ export interface RecordGradeInput { homeworkId: string; studentId: string; score: number; feedback?: string; } /** 详情页内联批改响应 */ interface RecordGradeResponse { recordGrade: { gradeId: string } | null; } // ===== 查询选项 ===== export interface HomeworkQueryOptions { enabled?: boolean; pollInterval?: number; fetchPolicy?: FetchPolicy; } // ===== Hooks ===== /** * 按 id 查询作业详情(真实 schema,✅ 契约已就绪)。 * * 关联:ARCHITECTURE.md §5.5 后端已就绪查询 / §9.1 详情页 */ export function useHomework( id: string, options?: HomeworkQueryOptions, ): UseQueryResult { const result = useWidgetQuery( GET_HOMEWORK_DOC, { id }, { ...options, enabled: options?.enabled ?? id.length > 0, }, ); return { data: result.data?.homework ?? null, loading: result.loading, error: result.error, refetch: result.refetch, }; } /** * 查询班级下的作业列表(@contract-pending,MSW 兜底)。 * * schema 无 homeworks(classId) 根字段,由 MSW handlers 返回 mock 数据。 * 后端补齐列表查询后切换到真实 fetcher,页面无需改动。 * * 关联:ARCHITECTURE.md §5.4 / §9.1 列表页 / §11.4 契约工单 */ export function useHomeworkList( classId: string, options?: HomeworkQueryOptions & { status?: string; limit?: number; offset?: number; }, ): UseQueryResult<{ items: HomeworkListItem[]; total: number }> { const result = useWidgetQuery< HomeworkListResponse, { classId: string; status?: string; limit?: number; offset?: number; } >( GET_HOMEWORK_LIST_DOC, { classId, status: options?.status, limit: options?.limit, offset: options?.offset, }, { enabled: options?.enabled ?? classId.length > 0, fetchPolicy: options?.fetchPolicy, pollInterval: options?.pollInterval, }, ); return { data: result.data?.homeworks, loading: result.loading, error: result.error, refetch: result.refetch, }; } /** * 布置作业 mutation(@contract-pending,MSW 兜底)。 * * schema 无 Mutation 类型,由 MSW handlers 返回 mock 数据。 * 后端补齐 mutation 后切换到真实 fetcher。 * * 关联:ARCHITECTURE.md §5.4 / §9.1 表单页 / §11.4 契约工单 */ export function useAssignHomework(): { run: (input: AssignHomeworkInput) => Promise<{ id: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation( ASSIGN_HOMEWORK_DOC, ); const run = async (input: AssignHomeworkInput): Promise<{ id: string }> => { const data = await rawRun({ input }); if (!data?.assignHomework) { throw new ApiError("Failed to assign homework", "INTERNAL_ERROR"); } return data.assignHomework; }; return { run, loading, error }; } /** * 查询跨作业提交列表(@contract-pending,MSW 兜底)。 * * schema 无 homeworkSubmissions 根字段,由 MSW handlers 返回 mock 数据。 * 用于 /shell/teacher/homework/submissions 列表页。 * * 关联:ARCHITECTURE.md §5.4 / §9.1 submissions 列表页 / §11.4 契约工单 */ export function useHomeworkSubmissions( filter: SubmissionsFilter, options?: HomeworkQueryOptions, ): UseQueryResult<{ items: HomeworkSubmissionItem[]; total: number }> { const result = useWidgetQuery< HomeworkSubmissionsResponse, { classId?: string; homeworkId?: string; status?: string; limit?: number; offset?: number; } >(GET_HOMEWORK_SUBMISSIONS_DOC, filter, { enabled: options?.enabled ?? true, fetchPolicy: options?.fetchPolicy, pollInterval: options?.pollInterval, }); return { data: result.data?.homeworkSubmissions, loading: result.loading, error: result.error, refetch: result.refetch, }; } /** * 查询单份提交详情(@contract-pending,MSW 兜底)。 * * schema 无 submissionDetail 根字段,由 MSW handlers 返回 mock 数据。 * 用于 /shell/teacher/homework/submissions/[submissionId] 批改页。 * * 关联:ARCHITECTURE.md §5.4 / §9.1 批改页 / §11.4 契约工单 */ export function useSubmissionDetail( submissionId: string, options?: HomeworkQueryOptions, ): UseQueryResult { const result = useWidgetQuery< SubmissionDetailResponse, { submissionId: string } >( GET_SUBMISSION_DETAIL_DOC, { submissionId }, { ...options, enabled: options?.enabled ?? submissionId.length > 0, }, ); return { data: result.data?.submissionDetail ?? null, loading: result.loading, error: result.error, refetch: result.refetch, }; } /** * 批改提交 mutation(@contract-pending,MSW 兜底)。 * * schema 无 Mutation 类型,由 MSW handlers 返回 mock 数据。 * 用于单份提交批改页保存评分。 * * 关联:ARCHITECTURE.md §5.4 / §9.1 批改页 / §11.4 契约工单 */ export function useGradeSubmission(): { run: (input: GradeSubmissionInput) => Promise<{ submissionId: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< GradeSubmissionResponse, { input: GradeSubmissionInput } >(GRADE_SUBMISSION_DOC); const run = async ( input: GradeSubmissionInput, ): Promise<{ submissionId: string }> => { const data = await rawRun({ input }); if (!data?.gradeSubmission) { throw new ApiError("Failed to grade submission", "INTERNAL_ERROR"); } return data.gradeSubmission; }; return { run, loading, error }; } /** * 保存扫描批改 mutation(@contract-pending,MSW 兜底)。 * * schema 无 Mutation 类型,由 MSW handlers 返回 mock 数据。 * 用于扫描批改页保存。 * * 关联:ARCHITECTURE.md §5.4 / §9.1 扫描批改页 / §11.4 契约工单 */ export function useSaveScanGrading(): { run: (input: SaveScanGradingInput) => Promise<{ submissionId: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< SaveScanGradingResponse, { input: SaveScanGradingInput } >(SAVE_SCAN_GRADING_DOC); const run = async ( input: SaveScanGradingInput, ): Promise<{ submissionId: string }> => { const data = await rawRun({ input }); if (!data?.saveScanGrading) { throw new ApiError("Failed to save scan grading", "INTERNAL_ERROR"); } return data.saveScanGrading; }; return { run, loading, error }; } /** * 查询 AI 批量评分建议(@contract-pending,MSW 兜底)。 * * schema 无 aiBatchGrading 根字段,由 MSW handlers 返回 mock 数据。 * 用于按作业批量批改页 AI 评分建议。 * * 关联:ARCHITECTURE.md §5.4 / §9.1 批量批改页 / §11.4 契约工单 */ export function useAiBatchGrading( homeworkId: string, options?: HomeworkQueryOptions, ): UseQueryResult { const result = useWidgetQuery( GET_AI_BATCH_GRADING_DOC, { homeworkId }, { ...options, enabled: options?.enabled ?? homeworkId.length > 0, }, ); return { data: result.data?.aiBatchGrading ?? null, loading: result.loading, error: result.error, refetch: result.refetch, }; } /** * 查询按作业拉所有提交(@contract-pending,MSW 兜底)。 * * schema 无 assignmentSubmissions 根字段,由 MSW handlers 返回 mock 数据。 * 用于 /shell/teacher/homework/assignments/[id]/submissions 批量批改视图。 * * 关联:ARCHITECTURE.md §5.4 / §9.1 批量批改页 / §11.4 契约工单 */ export function useAssignmentSubmissions( homeworkId: string, options?: HomeworkQueryOptions, ): UseQueryResult { const result = useWidgetQuery< AssignmentSubmissionsResponse, { homeworkId: string } >( GET_ASSIGNMENT_SUBMISSIONS_DOC, { homeworkId }, { ...options, enabled: options?.enabled ?? homeworkId.length > 0, }, ); return { data: result.data?.assignmentSubmissions ?? null, loading: result.loading, error: result.error, refetch: result.refetch, }; } /** * 详情页内联批改 mutation(@contract-pending,MSW 兜底)。 * * schema 无 Mutation 类型,由 MSW handlers 返回 mock 数据。 * 用于作业详情页内联录入单份提交评分。 * * 关联:ARCHITECTURE.md §5.4 / §9.1 详情页 / §11.4 契约工单 */ export function useRecordGrade(): { run: (input: RecordGradeInput) => Promise<{ gradeId: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation( RECORD_GRADE_DOC, ); const run = async (input: RecordGradeInput): Promise<{ gradeId: string }> => { const data = await rawRun({ input }); if (!data?.recordGrade) { throw new ApiError("Failed to record grade", "INTERNAL_ERROR"); } return data.recordGrade; }; return { run, loading, error }; }