"use client"; /** * Admin domain P5 扩展 Hook(ARCHITECTURE.md §9.4 B5 + admin-NeedTodo §四) * * 覆盖: * - users/import、roles detail + CRUD、permissions role counts * - audit-logs(overview / login-logs / data-changes / module-options / stats) * - school(schools CRUD / departments / academic-year / grades / admin-classes / teacher/staff options) * - announcements(list / detail / CRUD / archive / pin) * - files(list / stats) * - ai-settings(providers CRUD / usage dashboard) * - system settings、viewports * - students / teachers / organization(ARCH 新建) * - §四补充批次(course-plans / curriculum-map / elective / questions / lesson-plans / error-book / scheduling / attendance) * * 契约状态:除 pluginRegistry/config-service 外均 ❌ schema 未就绪 → MSW 兜底(@contract-pending) * * 关联:ARCHITECTURE.md §5.4 / §9.4 / §11.3 / §11.4 */ import { useWidgetQuery } from "@/lib/useWidgetQuery"; import { useWidgetMutation } from "@/lib/useWidgetMutation"; import { ApiError } from "./errors"; import type { PaginatedResult, Pagination, UseQueryResult } from "./types"; import type { AuditLogFilter, Role, SchoolInput } from "./admin"; import { IMPORT_USERS_DOC, GET_ROLE_DOC, CREATE_ROLE_DOC, UPDATE_ROLE_DOC, DELETE_ROLE_DOC, TOGGLE_ROLE_ENABLED_DOC, GET_PERMISSION_ROLE_COUNTS_DOC, GET_ROLE_PERMISSIONS_DOC, UPDATE_ROLE_PERMISSION_ACTIONS_DOC, GET_AUDIT_OVERVIEW_STATS_DOC, GET_AUDIT_TREND_DOC, GET_DATA_CHANGE_ACTION_STATS_DOC, GET_LOGIN_LOGS_DOC, GET_DATA_CHANGE_LOGS_DOC, GET_DATA_CHANGE_TABLE_OPTIONS_DOC, GET_DATA_CHANGE_STATS_DOC, GET_AUDIT_MODULE_OPTIONS_DOC, EXPORT_AUDIT_LOGS_DOC, EXPORT_LOGIN_LOGS_DOC, EXPORT_DATA_CHANGES_DOC, GET_SCHOOLS_DOC, CREATE_SCHOOL_DOC, ADMIN_UPDATE_SCHOOL_DOC, DELETE_SCHOOL_DOC, GET_DEPARTMENTS_DOC, CREATE_DEPARTMENT_DOC, UPDATE_DEPARTMENT_DOC, DELETE_DEPARTMENT_DOC, GET_ACADEMIC_YEARS_DOC, CREATE_ACADEMIC_YEAR_DOC, UPDATE_ACADEMIC_YEAR_DOC, DELETE_ACADEMIC_YEAR_DOC, GET_GRADES_DOC, GET_GRADE_OVERVIEW_STATS_DOC, ADMIN_CREATE_GRADE_DOC, UPDATE_GRADE_DOC, DELETE_GRADE_DOC, GET_ADMIN_CLASSES_DOC, CREATE_ADMIN_CLASS_DOC, UPDATE_ADMIN_CLASS_DOC, DELETE_ADMIN_CLASS_DOC, GET_TEACHER_OPTIONS_DOC, GET_STAFF_OPTIONS_DOC, GET_ADMIN_ANNOUNCEMENTS_DOC, GET_ADMIN_ANNOUNCEMENT_DETAIL_DOC, CREATE_ANNOUNCEMENT_DOC, UPDATE_ANNOUNCEMENT_DOC, DELETE_ANNOUNCEMENT_DOC, ARCHIVE_ANNOUNCEMENT_DOC, PIN_ANNOUNCEMENT_DOC, PUBLISH_ANNOUNCEMENT_DOC, GET_FILE_ATTACHMENTS_DOC, GET_FILE_STATS_DOC, UPLOAD_FILE_DOC, BATCH_DELETE_FILES_DOC, GET_AI_PROVIDERS_DOC, CREATE_AI_PROVIDER_DOC, UPDATE_AI_PROVIDER_DOC, DELETE_AI_PROVIDER_DOC, TEST_AI_PROVIDER_DOC, GET_AI_USAGE_DASHBOARD_DOC, GET_SYSTEM_SETTINGS_DOC, UPDATE_SYSTEM_SETTINGS_DOC, GET_VIEWPORTS_DOC, UPDATE_VIEWPORT_DOC, GET_ADMIN_STUDENTS_DOC, GET_ADMIN_TEACHERS_DOC, GET_ORGANIZATION_TREE_DOC, GET_ADMIN_COURSE_PLANS_DOC, GET_ADMIN_COURSE_PLAN_DOC, GET_STANDARDS_COVERAGE_HEATMAP_DOC, GET_GLOBAL_LESSON_PLAN_STATS_DOC, GET_ADMIN_ELECTIVES_DOC, GET_ADMIN_ELECTIVE_DOC, GET_ADMIN_QUESTIONS_DOC, GET_ADMIN_QUESTION_DOC, GET_ADMIN_LESSON_PLANS_DOC, GET_ADMIN_LESSON_PLAN_DOC, GET_ADMIN_ERROR_BOOK_STATS_DOC, EXPORT_ERROR_BOOK_CSV_DOC, GET_ADMIN_SCHEDULE_CHANGES_DOC, GET_ADMIN_SCHEDULE_ENTRIES_DOC, ADMIN_GET_SCHEDULING_RULES_DOC, UPDATE_SCHEDULING_RULES_DOC, APPROVE_SCHEDULE_CHANGE_DOC, REJECT_SCHEDULE_CHANGE_DOC, AUTO_SCHEDULE_DOC, GET_ADMIN_ATTENDANCE_STATS_DOC, GET_ADMIN_ATTENDANCE_RECORDS_DOC, GET_ATTENDANCE_GRADE_CORRELATION_DOC, GET_AUDIT_RETENTION_CONFIG_DOC, SAVE_AUDIT_RETENTION_CONFIG_DOC, PURGE_EXPIRED_AUDIT_LOGS_DOC, CREATE_COURSE_PLAN_ITEM_DOC, UPDATE_COURSE_PLAN_ITEM_DOC, DELETE_COURSE_PLAN_ITEM_DOC, TOGGLE_COURSE_PLAN_ITEM_COMPLETED_DOC, REORDER_COURSE_PLAN_ITEMS_DOC, DELETE_COURSE_PLAN_DOC, BULK_TOGGLE_COURSE_PLAN_ITEMS_DOC, DELETE_ELECTIVE_DOC, OPEN_ELECTIVE_SELECTION_DOC, CLOSE_ELECTIVE_SELECTION_DOC, RUN_ELECTIVE_LOTTERY_DOC, GET_ELECTIVE_OVERVIEW_STATS_DOC, } from "./operations/admin.graphql"; import { CREATE_ELECTIVE_DOC, UPDATE_ELECTIVE_DOC, } from "./operations/elective.graphql"; import { SOFT_DELETE_LESSON_PLAN_DOC } from "./operations/lesson-plans.graphql"; // ============================================================ // Types: Users import // ============================================================ export interface ImportResult { total: number; success: number; failed: number; errors: string[]; } // ============================================================ // Types: RBAC role detail + CRUD // ============================================================ export interface RoleDetail extends Role { description: string; isLocked: boolean; } export interface RoleInput { name: string; description?: string; permissionIds: string[]; /** * 角色值(如 `role:teacher`,@contract-pending:schema 未就绪,MSW 兜底)。 * 缺省视为未知,UI 用 `-` 占位。 */ value?: string; } // ============================================================ // Types: RBAC role permission matrix (CRUD actions per permission point) // ============================================================ export interface PermissionActions { read: boolean; create: boolean; update: boolean; delete: boolean; } /** * 权限矩阵中的一个权限点条目(按模块分组,每行一个权限点,列为 CRUD 动作)。 * * 命名说明:admin.ts 已定义 RolePermission(id/name/resource/action, * 用于 Role.permissions 列表)。本类型为矩阵视图的独立结构,故显式命名为 * RolePermissionMatrixItem 以避免 barrel 导出冲突。 */ export interface RolePermissionMatrixItem { permissionId: string; label: string; module: string; actions: PermissionActions; } // Input for UpdateRolePermissionActions mutation (one entry per permission point) export interface PermissionActionUpdate { permissionId: string; actions: PermissionActions; } // Role/RolePermission/Permission 类型由 ./admin.ts 提供(barrel 统一导出) export interface PermissionRoleCount { permissionId: string; permissionName: string; resource: string; action: string; roleCount: number; } // ============================================================ // Types: Audit logs // ============================================================ export interface AuditOverviewStats { totalLogs: number; totalToday: number; totalErrors: number; totalUsers: number; /** @contract-pending 今日审计事件数(与 totalToday 含义区分,强调事件维度) */ auditEventsToday: number; /** @contract-pending 今日失败登录次数(用于失败登录卡片高亮) */ failedLoginsToday: number; /** @contract-pending 今日数据变更次数 */ dataChangesToday: number; } export interface AuditTrendPoint { date: string; count: number; } export interface DataChangeActionStat { action: string; count: number; } export interface LoginLog { id: string; userId: string; userName: string; action: string; status: string; ip: string; userAgent: string; timestamp: string; /** @contract-pending 失败原因(仅 status=failure/error 时有值),MSW 兜底 */ errorMessage?: string | null; } export interface LoginLogFilter { action?: string | null; status?: string | null; userId?: string | null; startDate?: string | null; endDate?: string | null; } export interface DataChangeLog { id: string; tableName: string; recordId: string; action: string; userId: string; userName: string; changes: string; timestamp: string; /** @contract-pending 变更前快照(JSON 字符串或对象),MSW 兜底,用于行内展开对比 */ oldValue?: string | null; /** @contract-pending 变更后快照(JSON 字符串或对象),MSW 兜底,用于行内展开对比 */ newValue?: string | null; } export interface DataChangeLogFilter { tableName?: string | null; action?: string | null; userId?: string | null; startDate?: string | null; endDate?: string | null; } export interface DataChangeStat { action: string; count: number; lastChangeAt: string; } /** * 导出结果(CSV 字符串由 MSW 用 transformations 纯函数构造)。 * @contract-pending schema 未就绪,MSW 兜底返回 csv 字符串。 */ export interface ExportResult { success: boolean; count: number; filename: string; csv: string; } // ============================================================ // Types: School CRUD // ============================================================ export interface SchoolListItem { id: string; name: string; address: string; phone: string; email: string; currentAcademicYear: string; currentTerm: string; /** 学校编码(@contract-pending,MSW 兜底) */ code?: string; /** 更新时间 ISO 字符串(@contract-pending,MSW 兜底) */ updatedAt?: string; } export interface Department { id: string; name: string; schoolId: string; headId: string; headName: string; memberCount: number; /** 部门描述(@contract-pending,MSW 兜底) */ description?: string; /** 更新时间 ISO 字符串(@contract-pending,MSW 兜底) */ updatedAt?: string; } export interface DepartmentInput { name: string; schoolId: string; headId?: string; description?: string; } export interface AcademicYear { id: string; name: string; schoolId: string; startDate: string; endDate: string; isActive: boolean; } export interface AcademicYearInput { name: string; schoolId: string; startDate: string; endDate: string; isActive?: boolean; } export interface AdminGradeListItem { id: string; name: string; schoolId: string; schoolName: string; headStaffId: string; headStaffName: string; classCount: number; studentCount: number; /** 年级序号(@contract-pending,MSW 兜底) */ order?: number; /** 教学主任 ID(@contract-pending,MSW 兜底) */ teachingHead?: string; /** 教学主任姓名(@contract-pending,MSW 兜底) */ teachingHeadName?: string; /** 更新时间 ISO 字符串(@contract-pending,MSW 兜底) */ updatedAt?: string; } export interface GradeInput { name: string; schoolId: string; headStaffId?: string; } export interface GradeOverviewStat { gradeId: string; gradeName: string; classCount: number; studentCount: number; avgScore: number; } export interface AdminClassListItem { id: string; name: string; gradeId: string; gradeName: string; schoolId: string; schoolName: string; headTeacherId: string; headTeacherName: string; studentCount: number; subjectCount: number; /** 班号(@contract-pending,MSW 兜底) */ homeroomLabel?: string; /** 教室(@contract-pending,MSW 兜底) */ room?: string; /** 班主任 ID(@contract-pending,MSW 兜底) */ homeroom?: string; /** 班主任姓名(@contract-pending,MSW 兜底) */ homeroomName?: string; /** 任课教师列表(@contract-pending,MSW 兜底) */ subjectTeachers?: string; /** 更新时间 ISO 字符串(@contract-pending,MSW 兜底) */ updatedAt?: string; } /** 班级 CRUD 输入类型(@contract-pending,MSW 兜底) */ export interface AdminClassInput { name: string; gradeId: string; schoolId: string; headTeacherId?: string; homeroomLabel?: string; room?: string; homeroom?: string; } export interface OptionItem { id: string; name: string; } // ============================================================ // Types: Announcements // ============================================================ export interface AnnouncementListItem { id: string; title: string; content: string; status: string; audience: string; pinnedAt: string | null; publishedAt: string | null; createdAt: string; updatedAt: string; authorName: string; readCount: number; } export interface AnnouncementDetail extends AnnouncementListItem { grades: string[]; classes: string[]; } export interface AnnouncementInput { title: string; content: string; status: string; audience: string; grades?: string[]; classes?: string[]; } // ============================================================ // Types: Files // ============================================================ export interface FileAttachment { id: string; name: string; size: number; mimeType: string; url: string; uploadedBy: string; uploadedAt: string; } export interface FileFilter { limit?: number; mimeType?: string; } export interface FileStats { totalFiles: number; totalSize: number; byType: Record; } export interface UploadFileInput { filename: string; mimeType: string; size: number; } export interface UploadFileResult { id: string; url: string; filename: string; } export interface BatchDeleteFilesResult { success: boolean; deletedCount: number; } // ============================================================ // Types: AI settings // ============================================================ export interface AiProvider { id: string; name: string; type: string; scope: string; ownerId: string; isActive: boolean; isDefault: boolean; visibility: string; model: string; apiBase: string; config: Record; createdAt: string; } export interface AiProviderInput { name: string; type: string; scope: string; model: string; apiBase?: string; isActive?: boolean; isDefault?: boolean; visibility?: string; config?: Record; } export interface AiUsageDashboard { totalRequests: number; totalTokens: number; totalCostCents: number; byProvider: Array<{ providerId: string; providerName: string; requests: number; tokens: number; costCents: number; }>; byDay: Array<{ date: string; requests: number; tokens: number }>; } // ============================================================ // Types: System settings // ============================================================ export interface SystemSetting { key: string; value: string; description: string; updatedAt: string; } export interface SystemSettingInput { key: string; value: string; } // ============================================================ // Types: Viewports // ============================================================ export interface ViewportPlugin { pluginId: string; slot: string; sortOrder: number; isEnabled: boolean; } export interface Viewport { id: string; name: string; role: string; layoutId: string; plugins: ViewportPlugin[]; isDefault: boolean; } export interface ViewportInput { name: string; layoutId: string; plugins: ViewportPlugin[]; } // ============================================================ // Types: Students / Teachers / Organization // ============================================================ export interface AdminStudent { id: string; name: string; studentNo: string; gradeId: string; gradeName: string; classId: string; className: string; status: string; enrolledAt: string; } export interface StudentFilter { gradeId?: string | null; classId?: string | null; status?: string | null; q?: string | null; } export interface AdminTeacher { id: string; name: string; email: string; staffNo: string; department: string; title: string; classCount: number; subjectCount: number; status: string; } export interface TeacherFilter { department?: string | null; q?: string | null; status?: string | null; } export interface OrgNode { id: string; name: string; type: string; parentId: string | null; memberCount: number; children: OrgNode[]; } // ============================================================ // Types: §四补充批次 // ============================================================ export interface AdminCoursePlanListItem { id: string; name: string; classId: string; className: string; subjectId: string; subjectName: string; teacherId: string; teacherName: string; academicYearId: string; academicYearName: string; status: string; createdAt: string; /** 学期(@contract-pending,MSW 兜底,对齐 CICD 列表"学期"列) */ semester?: string; /** 总课时(@contract-pending,MSW 兜底,用于进度条 totalHours) */ totalHours?: number; /** 已完成课时(@contract-pending,MSW 兜底,用于进度条 completedHours) */ completedHours?: number; /** 更新时间 ISO 字符串(@contract-pending,MSW 兜底) */ updatedAt?: string; } /** 管理端课程计划周计划项(@contract-pending,MSW 兜底) */ export interface AdminCoursePlanItem { id: string; planId: string; week: number; topic: string; content: string | null; hours: number; textbookChapter: string | null; notes: string | null; isCompleted: boolean; completedAt: string | null; createdAt: string; updatedAt: string; } export interface AdminCoursePlan extends AdminCoursePlanListItem { content: string; items: AdminCoursePlanItem[]; updatedAt: string; /** 教学大纲(@contract-pending,MSW 兜底,对齐 CICD 详情"教学大纲"区) */ syllabus?: string; /** 教学目标(@contract-pending,MSW 兜底,对齐 CICD 详情"教学目标"区) */ objectives?: string; /** 周课时(@contract-pending,MSW 兜底) */ weeklyHours?: number; /** 计划开始日期(@contract-pending,MSW 兜底) */ startDate?: string; /** 计划结束日期(@contract-pending,MSW 兜底) */ endDate?: string; /** 教材链接(@contract-pending,MSW 兜底) */ textbooksHref?: string; /** 作业链接(@contract-pending,MSW 兜底) */ homeworkHref?: string; } /** 创建/更新周计划项的输入类型(@contract-pending) */ export interface AdminCoursePlanItemInput { planId: string; week: number; topic: string; content?: string | null; hours?: number; textbookChapter?: string | null; notes?: string | null; completedAt?: string | null; } /** 更新周计划项的输入类型(不含 planId,@contract-pending) */ export interface AdminCoursePlanItemUpdateInput { week?: number; topic?: string; content?: string | null; hours?: number; textbookChapter?: string | null; notes?: string | null; completedAt?: string | null; } /** 重排序周计划项的输入条目(@contract-pending) */ export interface ReorderCoursePlanItemInput { id: string; week: number; } export interface StandardsCoverageCell { standardId: string; standardName: string; gradeId: string; gradeName: string; coverageRate: number; lessonPlanCount: number; /** * 该标准 × 年级下已关联教案的课时数(@contract-pending,MSW 兜底)。 * 与 lessonPlanCount 区分:lessonPlanCount 为教案条目数,total 为应覆盖总课时数。 */ total: number; /** 已关联教案数别名(与 lessonPlanCount 同义,对齐 CICD linked/total 口径) */ linked?: number; } export interface GlobalLessonPlanStats { totalTeachers: number; totalLessonPlans: number; publishedCount: number; submittedCount: number; standardsLinked: number; } export interface AdminElectiveListItem { id: string; name: string; subjectId: string; subjectName: string; gradeId: string; gradeName: string; teacherId: string; teacherName: string; capacity: number; selectedCount: number; status: string; /** 教室(@contract-pending,MSW 兜底,对齐 CICD 列表"教室"列) */ classroom?: string; /** 上课时间(@contract-pending,MSW 兜底,对齐 CICD 列表"时间"列) */ schedule?: string; /** 学分(@contract-pending,MSW 兜底,对齐 CICD 列表"学分"列) */ credit?: number; /** 选课模式(@contract-pending,MSW 兜底:FIRST_COME | LOTTERY) */ selectionMode?: string; /** 选课开始时间 ISO(@contract-pending,MSW 兜底) */ selectionStartAt?: string; /** 选课结束时间 ISO(@contract-pending,MSW 兜底) */ selectionEndAt?: string; /** 退课截止时间 ISO(@contract-pending,MSW 兜底) */ dropDeadline?: string; /** 描述(@contract-pending,MSW 兜底,列表展示用 line-clamp) */ description?: string; /** 开始日期(@contract-pending,MSW 兜底) */ startDate?: string; /** 结束日期(@contract-pending,MSW 兜底) */ endDate?: string; /** 创建时间 ISO(@contract-pending,MSW 兜底) */ createdAt?: string; /** 更新时间 ISO(@contract-pending,MSW 兜底) */ updatedAt?: string; } export interface AdminElective extends AdminElectiveListItem { description: string; selections: Array<{ studentId: string; studentName: string; selectedAt: string; /** 选课状态(@contract-pending,MSW 兜底:confirmed | pending | cancelled) */ status?: string; /** 优先级(@contract-pending,MSW 兜底,对齐 CICD 详情"优先级"列) */ priority?: number; /** 选课时间别名(@contract-pending,MSW 兜底,与 selectedAt 同义) */ enrolledAt?: string; }>; } /** 创建课程计划输入(@contract-pending,MSW 兜底,扩展 course-plans.ts 的基础输入) */ export interface AdminCreateCoursePlanInput { name: string; gradeId: string; classId?: string; subjectId: string; teacherId?: string; academicYearId?: string; semester?: string; description?: string; objectives?: string; syllabus?: string; totalHours?: number; weeklyHours?: number; startDate?: string; endDate?: string; status?: string; } /** 更新课程计划输入(@contract-pending,MSW 兜底) */ export interface AdminUpdateCoursePlanInput { id: string; name?: string; gradeId?: string; classId?: string; subjectId?: string; teacherId?: string; academicYearId?: string; semester?: string; description?: string; objectives?: string; syllabus?: string; totalHours?: number; weeklyHours?: number; startDate?: string; endDate?: string; status?: string; } /** 创建选修课输入(@contract-pending,MSW 兜底,扩展 elective.ts 的基础输入) */ export interface AdminCreateElectiveInput { name: string; description?: string; subjectId: string; gradeId: string; teacherId?: string; capacity: number; credit?: number; classroom?: string; schedule?: string; selectionMode?: string; startDate?: string; endDate?: string; selectionStartAt?: string; selectionEndAt?: string; dropDeadline?: string; status?: string; } /** 更新选修课输入(@contract-pending,MSW 兜底) */ export interface AdminUpdateElectiveInput { id: string; name?: string; description?: string; subjectId?: string; gradeId?: string; teacherId?: string; capacity?: number; credit?: number; classroom?: string; schedule?: string; selectionMode?: string; startDate?: string; endDate?: string; selectionStartAt?: string; selectionEndAt?: string; dropDeadline?: string; status?: string; } /** 选修课总览统计(@contract-pending,MSW 兜底) */ export interface ElectiveOverviewStats { totalCourses: number; totalCapacity: number; totalEnrolled: number; totalDraft: number; totalOpen: number; } export interface AdminQuestionListItem { id: string; type: string; content: string; difficulty: string; subjectId: string; subjectName: string; textbookId: string; status: string; createdAt: string; createdBy: string; } /** * 题目详情(管理域视角,@contract-pending,MSW 兜底)。 * * 与 AdminQuestionListItem 的差异:包含 answer / explanation / knowledgePointId / * knowledgePointTitle / updatedAt 等详情字段,供详情对话框展示。 */ export interface AdminQuestionDetail { id: string; type: string; content: string; difficulty: string; subjectId: string; subjectName: string; textbookId: string; textbookTitle: string; knowledgePointId: string; knowledgePointTitle: string; status: string; answer: string; explanation: string | null; source: string | null; createdAt: string; updatedAt: string; createdBy: string; } export interface QuestionFilter { type?: string | null; difficulty?: string | null; subjectId?: string | null; q?: string | null; } export interface AdminLessonPlanListItem { id: string; title: string; subjectId: string; subjectName: string; teacherId: string; teacherName: string; classId: string; className: string; status: string; createdAt: string; updatedAt: string; } export interface AdminLessonPlan extends AdminLessonPlanListItem { textbookId: string; textbookTitle: string; chapterId: string; chapterTitle: string; content: string; } export interface AdminErrorBookStats { totalStudents: number; totalErrorQuestions: number; totalErrorCount: number; avgErrorRate: number; bySubject: Array<{ subjectId: string; subjectName: string; errorCount: number; questionCount: number; errorRate: number; }>; /** * 按班级分组的错题统计(@contract-pending,MSW 兜底)。 * 用于"按班级分组"卡片展示,与 bySubject 互补。 */ byClass: Array<{ classId: string; className: string; errorCount: number; questionCount: number; errorRate: number; }>; topStudents: Array<{ studentId: string; studentName: string; className: string; errorCount: number; errorRate: number; }>; topWrongQuestions: Array<{ questionId: string; content: string; errorCount: number; errorRate: number; }>; } export interface ScheduleChange { id: string; classId: string; className: string; teacherId: string; teacherName: string; originalSlot: string; requestedSlot: string; reason: string; status: string; submittedAt: string; } export interface ScheduleEntry { classId: string; className: string; entries: Array<{ slot: string; subjectId: string; subjectName: string; teacherId: string; teacherName: string; }>; } export interface AdminSchedulingRule { id: string; name: string; type: string; priority: number; isEnabled: boolean; config: Record; } export interface AdminAttendanceStats { totalRecords: number; presentRate: number; absentRate: number; lateRate: number; earlyLeaveRate: number; byClass: Array<{ classId: string; className: string; presentRate: number; absentRate: number; }>; } export interface AdminAttendanceRecord { id: string; studentId: string; studentName: string; classId: string; className: string; /** 年级 ID(@contract-pending,MSW 兜底,用于年级筛选) */ gradeId: string; date: string; status: string; recordedBy: string; note: string; } export interface AttendanceFilter { classId?: string | null; status?: string | null; date?: string | null; /** 年级筛选(@contract-pending,MSW 兜底)。与 classId 互不冲突,可同时设置。 */ gradeId?: string | null; } export interface AttendanceGradeCorrelation { classId: string; className: string; presentRate: number; avgScore: number; correlation: number; } // ============================================================ // Hooks: Users import // ============================================================ export function useImportUsers(): { run: (file: File) => Promise; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation<{ importUsers: ImportResult }, { file: File }>( IMPORT_USERS_DOC, ); const run = async (file: File): Promise => { const data = await rawRun({ file }); if (!data?.importUsers) { throw new ApiError("Failed to import users", "INTERNAL_ERROR"); } return data.importUsers; }; return { run, loading, error }; } // ============================================================ // Hooks: RBAC role detail + CRUD // ============================================================ export function useRole(id: string): UseQueryResult { const result = useWidgetQuery<{ role: RoleDetail | null }, { id: string }>( GET_ROLE_DOC, { id }, ); return { ...result, data: result.data?.role ?? null }; } export function useCreateRole(): { run: (input: RoleInput) => Promise<{ id: string; name: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { createRole: { id: string; name: string } }, { input: RoleInput } >(CREATE_ROLE_DOC); const run = async ( input: RoleInput, ): Promise<{ id: string; name: string }> => { const data = await rawRun({ input }); if (!data?.createRole) { throw new ApiError("Failed to create role", "INTERNAL_ERROR"); } return data.createRole; }; return { run, loading, error }; } export function useUpdateRole(): { run: (id: string, input: RoleInput) => Promise<{ id: string; name: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { updateRole: { id: string; name: string } }, { id: string; input: RoleInput } >(UPDATE_ROLE_DOC); const run = async ( id: string, input: RoleInput, ): Promise<{ id: string; name: string }> => { const data = await rawRun({ id, input }); if (!data?.updateRole) { throw new ApiError("Failed to update role", "INTERNAL_ERROR"); } return data.updateRole; }; return { run, loading, error }; } export function useDeleteRole(): { run: (id: string) => Promise<{ id: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation<{ deleteRole: { id: string } }, { id: string }>( DELETE_ROLE_DOC, ); const run = async (id: string): Promise<{ id: string }> => { const data = await rawRun({ id }); if (!data?.deleteRole) { throw new ApiError("Failed to delete role", "INTERNAL_ERROR"); } return data.deleteRole; }; return { run, loading, error }; } /** * 启用/停用角色(@contract-pending MSW 兜底)。 * 系统锁定角色(isLocked=true)禁止切换。 */ export function useToggleRoleEnabled(): { run: ( id: string, isEnabled: boolean, ) => Promise<{ id: string; isEnabled: boolean }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { toggleRoleEnabled: { id: string; isEnabled: boolean } }, { id: string; isEnabled: boolean } >(TOGGLE_ROLE_ENABLED_DOC); const run = async ( id: string, isEnabled: boolean, ): Promise<{ id: string; isEnabled: boolean }> => { const data = await rawRun({ id, isEnabled }); if (!data?.toggleRoleEnabled) { throw new ApiError("Failed to toggle role enabled", "INTERNAL_ERROR"); } return data.toggleRoleEnabled; }; return { run, loading, error }; } // ============================================================ // Hooks: RBAC role permission matrix (CRUD actions per permission point) // ============================================================ export function useRolePermissions( roleId: string, ): UseQueryResult { const result = useWidgetQuery< { rolePermissions: RolePermissionMatrixItem[] }, { roleId: string } >(GET_ROLE_PERMISSIONS_DOC, { roleId }); return { ...result, data: result.data?.rolePermissions ?? [] }; } export function useUpdateRolePermissionActions(): { run: ( roleId: string, permissionUpdates: PermissionActionUpdate[], ) => Promise<{ id: string; updatedCount: number }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { updateRolePermissionActions: { id: string; updatedCount: number } }, { roleId: string; permissionUpdates: PermissionActionUpdate[]; } >(UPDATE_ROLE_PERMISSION_ACTIONS_DOC); const run = async ( roleId: string, permissionUpdates: PermissionActionUpdate[], ): Promise<{ id: string; updatedCount: number }> => { const data = await rawRun({ roleId, permissionUpdates }); if (!data?.updateRolePermissionActions) { throw new ApiError( "Failed to update role permission actions", "INTERNAL_ERROR", ); } return data.updateRolePermissionActions; }; return { run, loading, error }; } export function usePermissionRoleCounts(): UseQueryResult< PermissionRoleCount[] > { const result = useWidgetQuery< { permissionRoleCounts: PermissionRoleCount[] }, Record >(GET_PERMISSION_ROLE_COUNTS_DOC, {}); return { ...result, data: result.data?.permissionRoleCounts }; } // ============================================================ // Hooks: Audit logs // ============================================================ export function useAuditOverviewStats(): UseQueryResult { const result = useWidgetQuery< { auditOverviewStats: AuditOverviewStats | null }, Record >(GET_AUDIT_OVERVIEW_STATS_DOC, {}); return { ...result, data: result.data?.auditOverviewStats ?? null }; } export function useAuditTrend(days: number): UseQueryResult { const result = useWidgetQuery< { auditTrend: AuditTrendPoint[] }, { days: number } >(GET_AUDIT_TREND_DOC, { days }); return { ...result, data: result.data?.auditTrend }; } export function useDataChangeActionStats(): UseQueryResult< DataChangeActionStat[] > { const result = useWidgetQuery< { dataChangeActionStats: DataChangeActionStat[] }, Record >(GET_DATA_CHANGE_ACTION_STATS_DOC, {}); return { ...result, data: result.data?.dataChangeActionStats }; } export function useLoginLogs( filter: LoginLogFilter, pagination: Pagination, ): UseQueryResult> { const result = useWidgetQuery< { loginLogs: PaginatedResult }, { filter: LoginLogFilter; limit: number; offset: number } >(GET_LOGIN_LOGS_DOC, { filter, limit: pagination.limit, offset: pagination.offset, }); return { ...result, data: result.data?.loginLogs }; } export function useDataChangeLogs( filter: DataChangeLogFilter, pagination: Pagination, ): UseQueryResult> { const result = useWidgetQuery< { dataChangeLogs: PaginatedResult }, { filter: DataChangeLogFilter; limit: number; offset: number } >(GET_DATA_CHANGE_LOGS_DOC, { filter, limit: pagination.limit, offset: pagination.offset, }); return { ...result, data: result.data?.dataChangeLogs }; } export function useDataChangeTableOptions(): UseQueryResult { const result = useWidgetQuery< { dataChangeTableOptions: string[] }, Record >(GET_DATA_CHANGE_TABLE_OPTIONS_DOC, {}); return { ...result, data: result.data?.dataChangeTableOptions }; } export function useDataChangeStats(): UseQueryResult { const result = useWidgetQuery< { dataChangeStats: DataChangeStat[] }, Record >(GET_DATA_CHANGE_STATS_DOC, {}); return { ...result, data: result.data?.dataChangeStats }; } export function useAuditModuleOptions(): UseQueryResult { const result = useWidgetQuery< { auditModuleOptions: string[] }, Record >(GET_AUDIT_MODULE_OPTIONS_DOC, {}); return { ...result, data: result.data?.auditModuleOptions }; } // ============================================================ // Hooks: Audit / Login / DataChange - export (CSV) // ============================================================ export function useExportAuditLogs(): { run: (filter: AuditLogFilter) => Promise; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { exportAuditLogs: ExportResult }, { filter: AuditLogFilter } >(EXPORT_AUDIT_LOGS_DOC); const run = async (filter: AuditLogFilter): Promise => { const data = await rawRun({ filter }); if (!data?.exportAuditLogs) { throw new ApiError("Failed to export audit logs", "INTERNAL_ERROR"); } return data.exportAuditLogs; }; return { run, loading, error }; } export function useExportLoginLogs(): { run: (filter: LoginLogFilter) => Promise; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { exportLoginLogs: ExportResult }, { filter: LoginLogFilter } >(EXPORT_LOGIN_LOGS_DOC); const run = async (filter: LoginLogFilter): Promise => { const data = await rawRun({ filter }); if (!data?.exportLoginLogs) { throw new ApiError("Failed to export login logs", "INTERNAL_ERROR"); } return data.exportLoginLogs; }; return { run, loading, error }; } export function useExportDataChanges(): { run: (filter: DataChangeLogFilter) => Promise; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { exportDataChanges: ExportResult }, { filter: DataChangeLogFilter } >(EXPORT_DATA_CHANGES_DOC); const run = async (filter: DataChangeLogFilter): Promise => { const data = await rawRun({ filter }); if (!data?.exportDataChanges) { throw new ApiError("Failed to export data changes", "INTERNAL_ERROR"); } return data.exportDataChanges; }; return { run, loading, error }; } // ============================================================ // Hooks: School CRUD // ============================================================ export function useSchools(): UseQueryResult { const result = useWidgetQuery< { schools: SchoolListItem[] }, Record >(GET_SCHOOLS_DOC, {}); return { ...result, data: result.data?.schools }; } export function useCreateSchool(): { run: (input: SchoolInput) => Promise<{ id: string; name: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { createSchool: { id: string; name: string } }, { input: SchoolInput } >(CREATE_SCHOOL_DOC); const run = async ( input: SchoolInput, ): Promise<{ id: string; name: string }> => { const data = await rawRun({ input }); if (!data?.createSchool) { throw new ApiError("Failed to create school", "INTERNAL_ERROR"); } return data.createSchool; }; return { run, loading, error }; } export function useAdminUpdateSchool(): { run: ( id: string, input: SchoolInput, ) => Promise<{ id: string; name: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { updateSchool: { id: string; name: string } }, { id: string; input: SchoolInput } >(ADMIN_UPDATE_SCHOOL_DOC); const run = async ( id: string, input: SchoolInput, ): Promise<{ id: string; name: string }> => { const data = await rawRun({ id, input }); if (!data?.updateSchool) { throw new ApiError("Failed to update school", "INTERNAL_ERROR"); } return data.updateSchool; }; return { run, loading, error }; } export function useDeleteSchool(): { run: (id: string) => Promise<{ id: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation<{ deleteSchool: { id: string } }, { id: string }>( DELETE_SCHOOL_DOC, ); const run = async (id: string): Promise<{ id: string }> => { const data = await rawRun({ id }); if (!data?.deleteSchool) { throw new ApiError("Failed to delete school", "INTERNAL_ERROR"); } return data.deleteSchool; }; return { run, loading, error }; } // Departments export function useDepartments(): UseQueryResult { const result = useWidgetQuery< { departments: Department[] }, Record >(GET_DEPARTMENTS_DOC, {}); return { ...result, data: result.data?.departments }; } export function useCreateDepartment(): { run: (input: DepartmentInput) => Promise<{ id: string; name: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { createDepartment: { id: string; name: string } }, { input: DepartmentInput } >(CREATE_DEPARTMENT_DOC); const run = async ( input: DepartmentInput, ): Promise<{ id: string; name: string }> => { const data = await rawRun({ input }); if (!data?.createDepartment) { throw new ApiError("Failed to create department", "INTERNAL_ERROR"); } return data.createDepartment; }; return { run, loading, error }; } export function useUpdateDepartment(): { run: ( id: string, input: DepartmentInput, ) => Promise<{ id: string; name: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { updateDepartment: { id: string; name: string } }, { id: string; input: DepartmentInput } >(UPDATE_DEPARTMENT_DOC); const run = async ( id: string, input: DepartmentInput, ): Promise<{ id: string; name: string }> => { const data = await rawRun({ id, input }); if (!data?.updateDepartment) { throw new ApiError("Failed to update department", "INTERNAL_ERROR"); } return data.updateDepartment; }; return { run, loading, error }; } export function useDeleteDepartment(): { run: (id: string) => Promise<{ id: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation<{ deleteDepartment: { id: string } }, { id: string }>( DELETE_DEPARTMENT_DOC, ); const run = async (id: string): Promise<{ id: string }> => { const data = await rawRun({ id }); if (!data?.deleteDepartment) { throw new ApiError("Failed to delete department", "INTERNAL_ERROR"); } return data.deleteDepartment; }; return { run, loading, error }; } // Academic years export function useAcademicYears(): UseQueryResult { const result = useWidgetQuery< { academicYears: AcademicYear[] }, Record >(GET_ACADEMIC_YEARS_DOC, {}); return { ...result, data: result.data?.academicYears }; } export function useCreateAcademicYear(): { run: (input: AcademicYearInput) => Promise<{ id: string; name: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { createAcademicYear: { id: string; name: string } }, { input: AcademicYearInput } >(CREATE_ACADEMIC_YEAR_DOC); const run = async ( input: AcademicYearInput, ): Promise<{ id: string; name: string }> => { const data = await rawRun({ input }); if (!data?.createAcademicYear) { throw new ApiError("Failed to create academic year", "INTERNAL_ERROR"); } return data.createAcademicYear; }; return { run, loading, error }; } export function useUpdateAcademicYear(): { run: ( id: string, input: AcademicYearInput, ) => Promise<{ id: string; name: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { updateAcademicYear: { id: string; name: string } }, { id: string; input: AcademicYearInput } >(UPDATE_ACADEMIC_YEAR_DOC); const run = async ( id: string, input: AcademicYearInput, ): Promise<{ id: string; name: string }> => { const data = await rawRun({ id, input }); if (!data?.updateAcademicYear) { throw new ApiError("Failed to update academic year", "INTERNAL_ERROR"); } return data.updateAcademicYear; }; return { run, loading, error }; } export function useDeleteAcademicYear(): { run: (id: string) => Promise<{ id: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation<{ deleteAcademicYear: { id: string } }, { id: string }>( DELETE_ACADEMIC_YEAR_DOC, ); const run = async (id: string): Promise<{ id: string }> => { const data = await rawRun({ id }); if (!data?.deleteAcademicYear) { throw new ApiError("Failed to delete academic year", "INTERNAL_ERROR"); } return data.deleteAcademicYear; }; return { run, loading, error }; } // Grades export function useGrades(): UseQueryResult { const result = useWidgetQuery< { grades: AdminGradeListItem[] }, Record >(GET_GRADES_DOC, {}); return { ...result, data: result.data?.grades }; } export function useGradeOverviewStats(): UseQueryResult { const result = useWidgetQuery< { gradeOverviewStats: GradeOverviewStat[] }, Record >(GET_GRADE_OVERVIEW_STATS_DOC, {}); return { ...result, data: result.data?.gradeOverviewStats }; } export function useAdminCreateGrade(): { run: (input: GradeInput) => Promise<{ id: string; name: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { createGrade: { id: string; name: string } }, { input: GradeInput } >(ADMIN_CREATE_GRADE_DOC); const run = async ( input: GradeInput, ): Promise<{ id: string; name: string }> => { const data = await rawRun({ input }); if (!data?.createGrade) { throw new ApiError("Failed to create grade", "INTERNAL_ERROR"); } return data.createGrade; }; return { run, loading, error }; } export function useUpdateGrade(): { run: (id: string, input: GradeInput) => Promise<{ id: string; name: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { updateGrade: { id: string; name: string } }, { id: string; input: GradeInput } >(UPDATE_GRADE_DOC); const run = async ( id: string, input: GradeInput, ): Promise<{ id: string; name: string }> => { const data = await rawRun({ id, input }); if (!data?.updateGrade) { throw new ApiError("Failed to update grade", "INTERNAL_ERROR"); } return data.updateGrade; }; return { run, loading, error }; } export function useDeleteGrade(): { run: (id: string) => Promise<{ id: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation<{ deleteGrade: { id: string } }, { id: string }>( DELETE_GRADE_DOC, ); const run = async (id: string): Promise<{ id: string }> => { const data = await rawRun({ id }); if (!data?.deleteGrade) { throw new ApiError("Failed to delete grade", "INTERNAL_ERROR"); } return data.deleteGrade; }; return { run, loading, error }; } // Admin classes + options export function useAdminClasses(): UseQueryResult { const result = useWidgetQuery< { adminClasses: AdminClassListItem[] }, Record >(GET_ADMIN_CLASSES_DOC, {}); return { ...result, data: result.data?.adminClasses }; } export function useTeacherOptions(): UseQueryResult { const result = useWidgetQuery< { teacherOptions: OptionItem[] }, Record >(GET_TEACHER_OPTIONS_DOC, {}); return { ...result, data: result.data?.teacherOptions }; } export function useStaffOptions(): UseQueryResult { const result = useWidgetQuery< { staffOptions: OptionItem[] }, Record >(GET_STAFF_OPTIONS_DOC, {}); return { ...result, data: result.data?.staffOptions }; } /** * 创建班级(@contract-pending,MSW 兜底)。 * 用于 admin/school/classes 列表页 ClassFormDialog 新建模式。 */ export function useCreateAdminClass(): { run: (input: AdminClassInput) => Promise<{ id: string; name: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { createAdminClass: { id: string; name: string } }, { input: AdminClassInput } >(CREATE_ADMIN_CLASS_DOC); const run = async ( input: AdminClassInput, ): Promise<{ id: string; name: string }> => { const data = await rawRun({ input }); if (!data?.createAdminClass) { throw new ApiError("Failed to create admin class", "INTERNAL_ERROR"); } return data.createAdminClass; }; return { run, loading, error }; } /** * 更新班级(@contract-pending,MSW 兜底)。 * 用于 admin/school/classes 列表页 ClassFormDialog 编辑模式。 */ export function useUpdateAdminClass(): { run: ( id: string, input: AdminClassInput, ) => Promise<{ id: string; name: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { updateAdminClass: { id: string; name: string } }, { id: string; input: AdminClassInput } >(UPDATE_ADMIN_CLASS_DOC); const run = async ( id: string, input: AdminClassInput, ): Promise<{ id: string; name: string }> => { const data = await rawRun({ id, input }); if (!data?.updateAdminClass) { throw new ApiError("Failed to update admin class", "INTERNAL_ERROR"); } return data.updateAdminClass; }; return { run, loading, error }; } /** * 删除班级(@contract-pending,MSW 兜底)。 * 用于 admin/school/classes 列表页 ClassDeleteDialog。 */ export function useDeleteAdminClass(): { run: (id: string) => Promise<{ id: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation<{ deleteAdminClass: { id: string } }, { id: string }>( DELETE_ADMIN_CLASS_DOC, ); const run = async (id: string): Promise<{ id: string }> => { const data = await rawRun({ id }); if (!data?.deleteAdminClass) { throw new ApiError("Failed to delete admin class", "INTERNAL_ERROR"); } return data.deleteAdminClass; }; return { run, loading, error }; } // ============================================================ // Hooks: Announcements // ============================================================ export function useAdminAnnouncements( status: string | null, ): UseQueryResult> { const result = useWidgetQuery< { adminAnnouncements: PaginatedResult }, { status: string | null } >(GET_ADMIN_ANNOUNCEMENTS_DOC, { status }); return { ...result, data: result.data?.adminAnnouncements }; } export function useAdminAnnouncement( id: string, ): UseQueryResult { const result = useWidgetQuery< { adminAnnouncement: AnnouncementDetail | null }, { id: string } >(GET_ADMIN_ANNOUNCEMENT_DETAIL_DOC, { id }); return { ...result, data: result.data?.adminAnnouncement ?? null }; } export function useCreateAnnouncement(): { run: (input: AnnouncementInput) => Promise<{ id: string; title: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { createAnnouncement: { id: string; title: string } }, { input: AnnouncementInput } >(CREATE_ANNOUNCEMENT_DOC); const run = async ( input: AnnouncementInput, ): Promise<{ id: string; title: string }> => { const data = await rawRun({ input }); if (!data?.createAnnouncement) { throw new ApiError("Failed to create announcement", "INTERNAL_ERROR"); } return data.createAnnouncement; }; return { run, loading, error }; } export function useUpdateAnnouncement(): { run: ( id: string, input: AnnouncementInput, ) => Promise<{ id: string; title: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { updateAnnouncement: { id: string; title: string } }, { id: string; input: AnnouncementInput } >(UPDATE_ANNOUNCEMENT_DOC); const run = async ( id: string, input: AnnouncementInput, ): Promise<{ id: string; title: string }> => { const data = await rawRun({ id, input }); if (!data?.updateAnnouncement) { throw new ApiError("Failed to update announcement", "INTERNAL_ERROR"); } return data.updateAnnouncement; }; return { run, loading, error }; } export function useDeleteAnnouncement(): { run: (id: string) => Promise<{ id: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation<{ deleteAnnouncement: { id: string } }, { id: string }>( DELETE_ANNOUNCEMENT_DOC, ); const run = async (id: string): Promise<{ id: string }> => { const data = await rawRun({ id }); if (!data?.deleteAnnouncement) { throw new ApiError("Failed to delete announcement", "INTERNAL_ERROR"); } return data.deleteAnnouncement; }; return { run, loading, error }; } export function useArchiveAnnouncement(): { run: (id: string) => Promise<{ id: string; status: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { archiveAnnouncement: { id: string; status: string } }, { id: string } >(ARCHIVE_ANNOUNCEMENT_DOC); const run = async (id: string): Promise<{ id: string; status: string }> => { const data = await rawRun({ id }); if (!data?.archiveAnnouncement) { throw new ApiError("Failed to archive announcement", "INTERNAL_ERROR"); } return data.archiveAnnouncement; }; return { run, loading, error }; } export function usePinAnnouncement(): { run: (id: string) => Promise<{ id: string; pinnedAt: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { pinAnnouncement: { id: string; pinnedAt: string } }, { id: string } >(PIN_ANNOUNCEMENT_DOC); const run = async (id: string): Promise<{ id: string; pinnedAt: string }> => { const data = await rawRun({ id }); if (!data?.pinAnnouncement) { throw new ApiError("Failed to pin announcement", "INTERNAL_ERROR"); } return data.pinAnnouncement; }; return { run, loading, error }; } export function usePublishAnnouncement(): { run: ( id: string, ) => Promise<{ id: string; status: string; publishedAt: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { publishAnnouncement: { id: string; status: string; publishedAt: string }; }, { id: string } >(PUBLISH_ANNOUNCEMENT_DOC); const run = async ( id: string, ): Promise<{ id: string; status: string; publishedAt: string }> => { const data = await rawRun({ id }); if (!data?.publishAnnouncement) { throw new ApiError("Failed to publish announcement", "INTERNAL_ERROR"); } return data.publishAnnouncement; }; return { run, loading, error }; } // ============================================================ // Hooks: Files // ============================================================ export function useFileAttachments( filter: FileFilter, ): UseQueryResult> { const result = useWidgetQuery< { fileAttachments: PaginatedResult }, { filter: FileFilter } >(GET_FILE_ATTACHMENTS_DOC, { filter }); return { ...result, data: result.data?.fileAttachments }; } export function useFileStats(): UseQueryResult { const result = useWidgetQuery< { fileStats: FileStats | null }, Record >(GET_FILE_STATS_DOC, {}); return { ...result, data: result.data?.fileStats ?? null }; } export function useUploadFile(): { run: (input: UploadFileInput) => Promise; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { uploadFile: UploadFileResult }, { input: UploadFileInput } >(UPLOAD_FILE_DOC); const run = async (input: UploadFileInput): Promise => { const data = await rawRun({ input }); if (!data?.uploadFile) { throw new ApiError("Failed to upload file", "INTERNAL_ERROR"); } return data.uploadFile; }; return { run, loading, error }; } export function useBatchDeleteFiles(): { run: (fileIds: string[]) => Promise; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { batchDeleteFiles: BatchDeleteFilesResult }, { fileIds: string[] } >(BATCH_DELETE_FILES_DOC); const run = async (fileIds: string[]): Promise => { const data = await rawRun({ fileIds }); if (!data?.batchDeleteFiles) { throw new ApiError("Failed to batch delete files", "INTERNAL_ERROR"); } return data.batchDeleteFiles; }; return { run, loading, error }; } // ============================================================ // Hooks: AI settings // ============================================================ export function useAiProviders( scope: string | null, ): UseQueryResult { const result = useWidgetQuery< { aiProviders: AiProvider[] }, { scope: string | null } >(GET_AI_PROVIDERS_DOC, { scope }); return { ...result, data: result.data?.aiProviders }; } export function useCreateAiProvider(): { run: (input: AiProviderInput) => Promise<{ id: string; name: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { createAiProvider: { id: string; name: string } }, { input: AiProviderInput } >(CREATE_AI_PROVIDER_DOC); const run = async ( input: AiProviderInput, ): Promise<{ id: string; name: string }> => { const data = await rawRun({ input }); if (!data?.createAiProvider) { throw new ApiError("Failed to create AI provider", "INTERNAL_ERROR"); } return data.createAiProvider; }; return { run, loading, error }; } export function useUpdateAiProvider(): { run: ( id: string, input: AiProviderInput, ) => Promise<{ id: string; name: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { updateAiProvider: { id: string; name: string } }, { id: string; input: AiProviderInput } >(UPDATE_AI_PROVIDER_DOC); const run = async ( id: string, input: AiProviderInput, ): Promise<{ id: string; name: string }> => { const data = await rawRun({ id, input }); if (!data?.updateAiProvider) { throw new ApiError("Failed to update AI provider", "INTERNAL_ERROR"); } return data.updateAiProvider; }; return { run, loading, error }; } export function useDeleteAiProvider(): { run: (id: string) => Promise<{ id: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation<{ deleteAiProvider: { id: string } }, { id: string }>( DELETE_AI_PROVIDER_DOC, ); const run = async (id: string): Promise<{ id: string }> => { const data = await rawRun({ id }); if (!data?.deleteAiProvider) { throw new ApiError("Failed to delete AI provider", "INTERNAL_ERROR"); } return data.deleteAiProvider; }; return { run, loading, error }; } export function useTestAiProvider(): { run: ( id: string, ) => Promise<{ ok: boolean; latencyMs: number; message: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { testAiProvider: { ok: boolean; latencyMs: number; message: string } }, { id: string } >(TEST_AI_PROVIDER_DOC); const run = async ( id: string, ): Promise<{ ok: boolean; latencyMs: number; message: string }> => { const data = await rawRun({ id }); if (!data?.testAiProvider) { throw new ApiError("Failed to test AI provider", "INTERNAL_ERROR"); } return data.testAiProvider; }; return { run, loading, error }; } export function useAiUsageDashboard( range: string, ): UseQueryResult { const result = useWidgetQuery< { aiUsageDashboard: AiUsageDashboard | null }, { range: string } >(GET_AI_USAGE_DASHBOARD_DOC, { range }); return { ...result, data: result.data?.aiUsageDashboard ?? null }; } // ============================================================ // Hooks: System settings // ============================================================ export function useSystemSettings(): UseQueryResult { const result = useWidgetQuery< { systemSettings: SystemSetting[] }, Record >(GET_SYSTEM_SETTINGS_DOC, {}); return { ...result, data: result.data?.systemSettings }; } export function useUpdateSystemSettings(): { run: (input: SystemSettingInput[]) => Promise; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { updateSystemSettings: SystemSetting[] }, { input: SystemSettingInput[] } >(UPDATE_SYSTEM_SETTINGS_DOC); const run = async (input: SystemSettingInput[]): Promise => { const data = await rawRun({ input }); if (!data?.updateSystemSettings) { throw new ApiError("Failed to update system settings", "INTERNAL_ERROR"); } return data.updateSystemSettings; }; return { run, loading, error }; } // ============================================================ // Hooks: Viewports // ============================================================ export function useViewports(): UseQueryResult { const result = useWidgetQuery< { viewports: Viewport[] }, Record >(GET_VIEWPORTS_DOC, {}); return { ...result, data: result.data?.viewports }; } export function useUpdateViewport(): { run: ( id: string, input: ViewportInput, ) => Promise<{ id: string; name: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { updateViewport: { id: string; name: string } }, { id: string; input: ViewportInput } >(UPDATE_VIEWPORT_DOC); const run = async ( id: string, input: ViewportInput, ): Promise<{ id: string; name: string }> => { const data = await rawRun({ id, input }); if (!data?.updateViewport) { throw new ApiError("Failed to update viewport", "INTERNAL_ERROR"); } return data.updateViewport; }; return { run, loading, error }; } // ============================================================ // Hooks: Students / Teachers / Organization // ============================================================ export function useAdminStudents( filter: StudentFilter, pagination: Pagination, ): UseQueryResult> { const result = useWidgetQuery< { adminStudents: PaginatedResult }, { filter: StudentFilter; limit: number; offset: number } >(GET_ADMIN_STUDENTS_DOC, { filter, limit: pagination.limit, offset: pagination.offset, }); return { ...result, data: result.data?.adminStudents }; } export function useAdminTeachers( filter: TeacherFilter, pagination: Pagination, ): UseQueryResult> { const result = useWidgetQuery< { adminTeachers: PaginatedResult }, { filter: TeacherFilter; limit: number; offset: number } >(GET_ADMIN_TEACHERS_DOC, { filter, limit: pagination.limit, offset: pagination.offset, }); return { ...result, data: result.data?.adminTeachers }; } export function useOrganizationTree(): UseQueryResult { const result = useWidgetQuery< { organizationTree: OrgNode[] }, Record >(GET_ORGANIZATION_TREE_DOC, {}); return { ...result, data: result.data?.organizationTree }; } // ============================================================ // Hooks: §四补充批次 - Course plans // ============================================================ export function useAdminCoursePlans( status: string | null, ): UseQueryResult> { const result = useWidgetQuery< { adminCoursePlans: PaginatedResult }, { status: string | null } >(GET_ADMIN_COURSE_PLANS_DOC, { status }); return { ...result, data: result.data?.adminCoursePlans }; } export function useAdminCoursePlan( id: string, ): UseQueryResult { const result = useWidgetQuery< { adminCoursePlan: AdminCoursePlan | null }, { id: string } >(GET_ADMIN_COURSE_PLAN_DOC, { id }); return { ...result, data: result.data?.adminCoursePlan ?? null }; } // Curriculum map export function useStandardsCoverageHeatmap(): UseQueryResult< StandardsCoverageCell[] > { const result = useWidgetQuery< { standardsCoverageHeatmap: StandardsCoverageCell[] }, Record >(GET_STANDARDS_COVERAGE_HEATMAP_DOC, {}); return { ...result, data: result.data?.standardsCoverageHeatmap }; } export function useGlobalLessonPlanStats(): UseQueryResult { const result = useWidgetQuery< { globalLessonPlanStats: GlobalLessonPlanStats | null }, Record >(GET_GLOBAL_LESSON_PLAN_STATS_DOC, {}); return { ...result, data: result.data?.globalLessonPlanStats ?? null }; } // Electives export function useAdminElectives(): UseQueryResult< PaginatedResult > { const result = useWidgetQuery< { adminElectives: PaginatedResult }, Record >(GET_ADMIN_ELECTIVES_DOC, {}); return { ...result, data: result.data?.adminElectives }; } export function useAdminElective( id: string, ): UseQueryResult { const result = useWidgetQuery< { adminElective: AdminElective | null }, { id: string } >(GET_ADMIN_ELECTIVE_DOC, { id }); return { ...result, data: result.data?.adminElective ?? null }; } // Questions export function useAdminQuestions( filter: QuestionFilter, ): UseQueryResult> { const result = useWidgetQuery< { adminQuestions: PaginatedResult }, { filter: QuestionFilter } >(GET_ADMIN_QUESTIONS_DOC, { filter }); return { ...result, data: result.data?.adminQuestions }; } /** * 题库单条详情 hook(@contract-pending,MSW 兜底)。 * * 用于详情对话框按 id 拉取题目明细(answer/explanation/knowledgePointTitle 等)。 * id 为空时不发请求(对话框关闭时跳过)。 */ export function useAdminQuestion( id: string, options?: { enabled?: boolean }, ): UseQueryResult { const enabled = options?.enabled ?? true; const result = useWidgetQuery< { adminQuestion: AdminQuestionDetail | null }, { id: string } >(GET_ADMIN_QUESTION_DOC, { id }, { enabled }); return { ...result, data: result.data?.adminQuestion ?? null }; } // Lesson plans export function useAdminLessonPlans(): UseQueryResult< PaginatedResult > { const result = useWidgetQuery< { adminLessonPlans: PaginatedResult }, Record >(GET_ADMIN_LESSON_PLANS_DOC, {}); return { ...result, data: result.data?.adminLessonPlans }; } export function useAdminLessonPlan( id: string, ): UseQueryResult { const result = useWidgetQuery< { adminLessonPlan: AdminLessonPlan | null }, { id: string } >(GET_ADMIN_LESSON_PLAN_DOC, { id }); return { ...result, data: result.data?.adminLessonPlan ?? null }; } /** 软删除教案 mutation 响应(@contract-pending) */ interface SoftDeleteLessonPlanResponse { softDeleteLessonPlan: { success: boolean; deletedAt: string } | null; } /** * 软删除教案 mutation(@contract-pending,MSW 兜底)。 * * 对齐 CICD softDeleteLessonPlan:将 status 置为 archived,返回 { success, deletedAt }。 * 用于管理端 admin/lesson-plans 列表/详情页删除按钮。 * * 关联:ARCHITECTURE.md §5.4 / §9.4 / §11.4 契约工单 */ export function useSoftDeleteLessonPlan(): { run: (planId: string) => Promise<{ success: boolean; deletedAt: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation( SOFT_DELETE_LESSON_PLAN_DOC, ); const run = async ( planId: string, ): Promise<{ success: boolean; deletedAt: string }> => { const data = await rawRun({ planId }); if (!data?.softDeleteLessonPlan) { throw new ApiError("Failed to soft delete lesson plan", "INTERNAL_ERROR"); } return data.softDeleteLessonPlan; }; return { run, loading, error }; } // Error book export function useAdminErrorBookStats(): UseQueryResult { const result = useWidgetQuery< { adminErrorBookStats: AdminErrorBookStats | null }, Record >(GET_ADMIN_ERROR_BOOK_STATS_DOC, {}); return { ...result, data: result.data?.adminErrorBookStats ?? null }; } /** * 导出错题本 CSV mutation(@contract-pending,MSW 兜底)。 * * CSV 由 MSW 用纯函数构造,返回 { success, count, filename, csv } 字符串。 * 前端拿到 csv 字符串后直接触发下载(与 EXPORT_AUDIT_LOGS_DOC 模式一致)。 */ export function useExportErrorBookCsv(): { run: (filter: { subjectId?: string | null; classId?: string | null; }) => Promise<{ success: boolean; count: number; filename: string; csv: string; }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { exportErrorBookCsv: { success: boolean; count: number; filename: string; csv: string; }; }, { filter: { subjectId?: string | null; classId?: string | null } } >(EXPORT_ERROR_BOOK_CSV_DOC); const run = async (filter: { subjectId?: string | null; classId?: string | null; }): Promise<{ success: boolean; count: number; filename: string; csv: string; }> => { const data = await rawRun({ filter }); if (!data?.exportErrorBookCsv) { throw new ApiError("Failed to export error book CSV", "INTERNAL_ERROR"); } return data.exportErrorBookCsv; }; return { run, loading, error }; } // Scheduling export function useAdminScheduleChanges(): UseQueryResult< PaginatedResult > { const result = useWidgetQuery< { adminScheduleChanges: PaginatedResult }, Record >(GET_ADMIN_SCHEDULE_CHANGES_DOC, {}); return { ...result, data: result.data?.adminScheduleChanges }; } export function useAdminScheduleEntries(): UseQueryResult { const result = useWidgetQuery< { adminScheduleEntries: ScheduleEntry[] }, Record >(GET_ADMIN_SCHEDULE_ENTRIES_DOC, {}); return { ...result, data: result.data?.adminScheduleEntries }; } export function useAdminSchedulingRules(): UseQueryResult< AdminSchedulingRule[] > { const result = useWidgetQuery< { schedulingRules: AdminSchedulingRule[] }, Record >(ADMIN_GET_SCHEDULING_RULES_DOC, {}); return { ...result, data: result.data?.schedulingRules }; } export function useUpdateSchedulingRules(): { run: ( input: Array<{ id: string; isEnabled: boolean }>, ) => Promise>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { updateSchedulingRules: Array<{ id: string; isEnabled: boolean }> }, { input: Array<{ id: string; isEnabled: boolean }> } >(UPDATE_SCHEDULING_RULES_DOC); const run = async ( input: Array<{ id: string; isEnabled: boolean }>, ): Promise> => { const data = await rawRun({ input }); if (!data?.updateSchedulingRules) { throw new ApiError("Failed to update scheduling rules", "INTERNAL_ERROR"); } return data.updateSchedulingRules; }; return { run, loading, error }; } export function useApproveScheduleChange(): { run: (id: string) => Promise<{ id: string; status: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { approveScheduleChange: { id: string; status: string } }, { id: string } >(APPROVE_SCHEDULE_CHANGE_DOC); const run = async (id: string): Promise<{ id: string; status: string }> => { const data = await rawRun({ id }); if (!data?.approveScheduleChange) { throw new ApiError("Failed to approve schedule change", "INTERNAL_ERROR"); } return data.approveScheduleChange; }; return { run, loading, error }; } export function useRejectScheduleChange(): { run: (id: string, reason: string) => Promise<{ id: string; status: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { rejectScheduleChange: { id: string; status: string } }, { id: string; reason: string } >(REJECT_SCHEDULE_CHANGE_DOC); const run = async ( id: string, reason: string, ): Promise<{ id: string; status: string }> => { const data = await rawRun({ id, reason }); if (!data?.rejectScheduleChange) { throw new ApiError("Failed to reject schedule change", "INTERNAL_ERROR"); } return data.rejectScheduleChange; }; return { run, loading, error }; } export function useAutoSchedule(): { run: (classId: string) => Promise<{ classId: string; generatedEntries: number; conflicts: string[]; }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { autoSchedule: { classId: string; generatedEntries: number; conflicts: string[]; }; }, { classId: string } >(AUTO_SCHEDULE_DOC); const run = async ( classId: string, ): Promise<{ classId: string; generatedEntries: number; conflicts: string[]; }> => { const data = await rawRun({ classId }); if (!data?.autoSchedule) { throw new ApiError("Failed to auto schedule", "INTERNAL_ERROR"); } return data.autoSchedule; }; return { run, loading, error }; } // Attendance export function useAdminAttendanceStats(): UseQueryResult { const result = useWidgetQuery< { adminAttendanceStats: AdminAttendanceStats | null }, Record >(GET_ADMIN_ATTENDANCE_STATS_DOC, {}); return { ...result, data: result.data?.adminAttendanceStats ?? null }; } export function useAdminAttendanceRecords( filter: AttendanceFilter, pagination: Pagination, ): UseQueryResult> { const result = useWidgetQuery< { adminAttendanceRecords: PaginatedResult }, { filter: AttendanceFilter; limit: number; offset: number } >(GET_ADMIN_ATTENDANCE_RECORDS_DOC, { filter, limit: pagination.limit, offset: pagination.offset, }); return { ...result, data: result.data?.adminAttendanceRecords }; } export function useAttendanceGradeCorrelation(): UseQueryResult< AttendanceGradeCorrelation[] > { const result = useWidgetQuery< { attendanceGradeCorrelation: AttendanceGradeCorrelation[] }, Record >(GET_ATTENDANCE_GRADE_CORRELATION_DOC, {}); return { ...result, data: result.data?.attendanceGradeCorrelation }; } // ============================================================ // Types & Hooks: Audit retention config + purge(@contract-pending,MSW 兜底) // ============================================================ export interface AuditRetentionConfig { retentionDays: number; loginLogRetentionDays: number; autoCleanupEnabled: boolean; } export interface PurgeResult { auditLogsDeleted: number; loginLogsDeleted: number; dataChangeLogsDeleted: number; } export interface AuditRetentionConfigInput { retentionDays: number; loginLogRetentionDays: number; autoCleanupEnabled: boolean; } export function useAuditRetentionConfig(): UseQueryResult { const result = useWidgetQuery< { auditRetentionConfig: AuditRetentionConfig | null }, Record >(GET_AUDIT_RETENTION_CONFIG_DOC, {}); return { ...result, data: result.data?.auditRetentionConfig ?? null }; } export function useSaveAuditRetentionConfig(): { run: (input: AuditRetentionConfigInput) => Promise; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { saveAuditRetentionConfig: AuditRetentionConfig }, { input: AuditRetentionConfigInput } >(SAVE_AUDIT_RETENTION_CONFIG_DOC); const run = async ( input: AuditRetentionConfigInput, ): Promise => { const data = await rawRun({ input }); if (!data?.saveAuditRetentionConfig) { throw new ApiError( "Failed to save audit retention config", "INTERNAL_ERROR", ); } return data.saveAuditRetentionConfig; }; return { run, loading, error }; } export function usePurgeExpiredAuditLogs(): { run: ( retentionDays: number, loginLogRetentionDays?: number, ) => Promise; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { purgeExpiredAuditLogs: PurgeResult }, { retentionDays: number; loginLogRetentionDays?: number | null } >(PURGE_EXPIRED_AUDIT_LOGS_DOC); const run = async ( retentionDays: number, loginLogRetentionDays?: number, ): Promise => { const data = await rawRun( loginLogRetentionDays !== undefined ? { retentionDays, loginLogRetentionDays } : { retentionDays, loginLogRetentionDays: null }, ); if (!data?.purgeExpiredAuditLogs) { throw new ApiError( "Failed to purge expired audit logs", "INTERNAL_ERROR", ); } return data.purgeExpiredAuditLogs; }; return { run, loading, error }; } // ============================================================ // Hooks: Course plan items CRUD(@contract-pending,MSW 兜底) // ============================================================ export function useCreateCoursePlanItem(): { run: (input: AdminCoursePlanItemInput) => Promise; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { createCoursePlanItem: AdminCoursePlanItem }, { input: AdminCoursePlanItemInput } >(CREATE_COURSE_PLAN_ITEM_DOC); const run = async ( input: AdminCoursePlanItemInput, ): Promise => { const data = await rawRun({ input }); if (!data?.createCoursePlanItem) { throw new ApiError("Failed to create course plan item", "INTERNAL_ERROR"); } return data.createCoursePlanItem; }; return { run, loading, error }; } export function useUpdateCoursePlanItem(): { run: ( id: string, input: AdminCoursePlanItemUpdateInput, ) => Promise; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { updateCoursePlanItem: AdminCoursePlanItem }, { id: string; input: AdminCoursePlanItemUpdateInput } >(UPDATE_COURSE_PLAN_ITEM_DOC); const run = async ( id: string, input: AdminCoursePlanItemUpdateInput, ): Promise => { const data = await rawRun({ id, input }); if (!data?.updateCoursePlanItem) { throw new ApiError("Failed to update course plan item", "INTERNAL_ERROR"); } return data.updateCoursePlanItem; }; return { run, loading, error }; } export function useDeleteCoursePlanItem(): { run: (id: string) => Promise<{ id: string; success: boolean }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { deleteCoursePlanItem: { id: string; success: boolean } }, { id: string } >(DELETE_COURSE_PLAN_ITEM_DOC); const run = async (id: string): Promise<{ id: string; success: boolean }> => { const data = await rawRun({ id }); if (!data?.deleteCoursePlanItem) { throw new ApiError("Failed to delete course plan item", "INTERNAL_ERROR"); } return data.deleteCoursePlanItem; }; return { run, loading, error }; } export function useToggleCoursePlanItemCompleted(): { run: ( id: string, isCompleted: boolean, ) => Promise<{ id: string; isCompleted: boolean; completedAt: string | null; updatedAt: string; }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { toggleCoursePlanItemCompleted: { id: string; isCompleted: boolean; completedAt: string | null; updatedAt: string; }; }, { id: string; isCompleted: boolean } >(TOGGLE_COURSE_PLAN_ITEM_COMPLETED_DOC); const run = async ( id: string, isCompleted: boolean, ): Promise<{ id: string; isCompleted: boolean; completedAt: string | null; updatedAt: string; }> => { const data = await rawRun({ id, isCompleted }); if (!data?.toggleCoursePlanItemCompleted) { throw new ApiError( "Failed to toggle course plan item completed", "INTERNAL_ERROR", ); } return data.toggleCoursePlanItemCompleted; }; return { run, loading, error }; } export function useReorderCoursePlanItems(): { run: ( planId: string, items: ReorderCoursePlanItemInput[], ) => Promise>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { reorderCoursePlanItems: Array<{ id: string; week: number; updatedAt: string; }>; }, { planId: string; items: ReorderCoursePlanItemInput[] } >(REORDER_COURSE_PLAN_ITEMS_DOC); const run = async ( planId: string, items: ReorderCoursePlanItemInput[], ): Promise> => { const data = await rawRun({ planId, items }); if (!data?.reorderCoursePlanItems) { throw new ApiError( "Failed to reorder course plan items", "INTERNAL_ERROR", ); } return data.reorderCoursePlanItems; }; return { run, loading, error }; } // ============================================================ // Hooks: 课程计划 CRUD + 批量操作(@contract-pending,MSW 兜底) // ============================================================ /** 删除课程计划 mutation 响应(@contract-pending) */ interface DeleteCoursePlanResponse { deleteCoursePlan: { id: string; success: boolean } | null; } /** * 删除课程计划(@contract-pending,MSW 兜底)。 * 用于详情页/列表页删除按钮,调用后建议 refetch 列表。 * * 关联:ARCHITECTURE.md §5.4 / §9.4 / §11.4 契约工单 */ export function useDeleteCoursePlan(): { run: (id: string) => Promise<{ id: string; success: boolean }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation( DELETE_COURSE_PLAN_DOC, ); const run = async (id: string): Promise<{ id: string; success: boolean }> => { const data = await rawRun({ id }); if (!data?.deleteCoursePlan) { throw new ApiError("Failed to delete course plan", "INTERNAL_ERROR"); } return data.deleteCoursePlan; }; return { run, loading, error }; } /** 批量切换周计划项完成状态 mutation 响应(@contract-pending) */ interface BulkToggleCoursePlanItemsResponse { bulkToggleCoursePlanItems: Array<{ id: string; isCompleted: boolean; completedAt: string | null; updatedAt: string; }>; } /** * 批量切换周计划项完成状态(@contract-pending,MSW 兜底)。 * 用于详情页批量"标记完成/取消完成"操作。 * * 关联:ARCHITECTURE.md §5.4 / §9.4 / §11.4 契约工单 */ export function useBulkToggleCoursePlanItems(): { run: ( planId: string, itemIds: string[], isCompleted: boolean, ) => Promise< Array<{ id: string; isCompleted: boolean; completedAt: string | null; updatedAt: string; }> >; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< BulkToggleCoursePlanItemsResponse, { planId: string; itemIds: string[]; isCompleted: boolean } >(BULK_TOGGLE_COURSE_PLAN_ITEMS_DOC); const run = async ( planId: string, itemIds: string[], isCompleted: boolean, ): Promise< Array<{ id: string; isCompleted: boolean; completedAt: string | null; updatedAt: string; }> > => { const data = await rawRun({ planId, itemIds, isCompleted }); if (!data?.bulkToggleCoursePlanItems) { throw new ApiError( "Failed to bulk toggle course plan items", "INTERNAL_ERROR", ); } return data.bulkToggleCoursePlanItems; }; return { run, loading, error }; } // ============================================================ // Hooks: 选修课 CRUD + 业务动作(@contract-pending,MSW 兜底) // ============================================================ /** 创建选修课 mutation 响应(@contract-pending) */ interface AdminCreateElectiveResponse { createElective: { id: string } | null; } /** * 创建选修课(@contract-pending,MSW 兜底,admin scope)。 * 与 elective.ts 的 useCreateElective 区别:本 hook 走 admin scope, * 支持扩展字段(classroom / schedule / credit / selectionMode 等)。 * * 关联:ARCHITECTURE.md §5.4 / §9.4 / §11.4 契约工单 */ export function useAdminCreateElective(): { run: (input: AdminCreateElectiveInput) => Promise<{ id: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< AdminCreateElectiveResponse, { input: AdminCreateElectiveInput } >(CREATE_ELECTIVE_DOC); const run = async ( input: AdminCreateElectiveInput, ): Promise<{ id: string }> => { const data = await rawRun({ input }); if (!data?.createElective) { throw new ApiError("Failed to create elective", "INTERNAL_ERROR"); } return data.createElective; }; return { run, loading, error }; } /** 更新选修课 mutation 响应(@contract-pending) */ interface AdminUpdateElectiveResponse { updateElective: { id: string } | null; } /** * 更新选修课(@contract-pending,MSW 兜底,admin scope)。 * 与 elective.ts 的 useUpdateElective 区别:本 hook 走 admin scope, * 支持扩展字段。 * * 关联:ARCHITECTURE.md §5.4 / §9.4 / §11.4 契约工单 */ export function useAdminUpdateElective(): { run: (input: AdminUpdateElectiveInput) => Promise<{ id: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< AdminUpdateElectiveResponse, { input: AdminUpdateElectiveInput } >(UPDATE_ELECTIVE_DOC); const run = async ( input: AdminUpdateElectiveInput, ): Promise<{ id: string }> => { const data = await rawRun({ input }); if (!data?.updateElective) { throw new ApiError("Failed to update elective", "INTERNAL_ERROR"); } return data.updateElective; }; return { run, loading, error }; } /** 删除选修课 mutation 响应(@contract-pending) */ interface DeleteElectiveResponse { deleteElective: { id: string; success: boolean } | null; } /** * 删除选修课(@contract-pending,MSW 兜底)。 * * 关联:ARCHITECTURE.md §5.4 / §9.4 / §11.4 契约工单 */ export function useDeleteElective(): { run: (id: string) => Promise<{ id: string; success: boolean }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation( DELETE_ELECTIVE_DOC, ); const run = async (id: string): Promise<{ id: string; success: boolean }> => { const data = await rawRun({ id }); if (!data?.deleteElective) { throw new ApiError("Failed to delete elective", "INTERNAL_ERROR"); } return data.deleteElective; }; return { run, loading, error }; } /** 开放选课 mutation 响应(@contract-pending) */ interface OpenElectiveSelectionResponse { openElectiveSelection: { id: string; status: string; updatedAt: string; } | null; } /** * 开放选课(@contract-pending,MSW 兜底)。 * 将选修课状态从 DRAFT 切换为 OPEN。 * * 关联:ARCHITECTURE.md §5.4 / §9.4 / §11.4 契约工单 */ export function useOpenElectiveSelection(): { run: ( id: string, ) => Promise<{ id: string; status: string; updatedAt: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation( OPEN_ELECTIVE_SELECTION_DOC, ); const run = async ( id: string, ): Promise<{ id: string; status: string; updatedAt: string }> => { const data = await rawRun({ id }); if (!data?.openElectiveSelection) { throw new ApiError("Failed to open elective selection", "INTERNAL_ERROR"); } return data.openElectiveSelection; }; return { run, loading, error }; } /** 关闭选课 mutation 响应(@contract-pending) */ interface CloseElectiveSelectionResponse { closeElectiveSelection: { id: string; status: string; updatedAt: string; } | null; } /** * 关闭选课(@contract-pending,MSW 兜底)。 * 将选修课状态从 OPEN 切换为 CLOSED。 * * 关联:ARCHITECTURE.md §5.4 / §9.4 / §11.4 契约工单 */ export function useCloseElectiveSelection(): { run: ( id: string, ) => Promise<{ id: string; status: string; updatedAt: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation( CLOSE_ELECTIVE_SELECTION_DOC, ); const run = async ( id: string, ): Promise<{ id: string; status: string; updatedAt: string }> => { const data = await rawRun({ id }); if (!data?.closeElectiveSelection) { throw new ApiError( "Failed to close elective selection", "INTERNAL_ERROR", ); } return data.closeElectiveSelection; }; return { run, loading, error }; } /** 运行选课抽签 mutation 响应(@contract-pending) */ interface RunElectiveLotteryResponse { runElectiveLottery: { id: string; status: string; selectedCount: number; updatedAt: string; } | null; } /** * 运行选课抽签(@contract-pending,MSW 兜底)。 * 对 LOTTERY 模式的选修课执行抽签,返回最终中选人数。 * * 关联:ARCHITECTURE.md §5.4 / §9.4 / §11.4 契约工单 */ export function useRunElectiveLottery(): { run: (id: string) => Promise<{ id: string; status: string; selectedCount: number; updatedAt: string; }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation( RUN_ELECTIVE_LOTTERY_DOC, ); const run = async ( id: string, ): Promise<{ id: string; status: string; selectedCount: number; updatedAt: string; }> => { const data = await rawRun({ id }); if (!data?.runElectiveLottery) { throw new ApiError("Failed to run elective lottery", "INTERNAL_ERROR"); } return data.runElectiveLottery; }; return { run, loading, error }; } /** 选修课总览统计查询响应(@contract-pending) */ interface ElectiveOverviewStatsResponse { electiveOverviewStats: ElectiveOverviewStats | null; } /** * 查询选修课总览统计(@contract-pending,MSW 兜底)。 * 用于列表页 StatsGrid 顶部统计卡片(总数 / 容量 / 已选 / 草稿 / 报名中)。 * * 关联:ARCHITECTURE.md §5.4 / §9.4 / §11.4 契约工单 */ export function useGetElectiveOverviewStats(): UseQueryResult { const result = useWidgetQuery< ElectiveOverviewStatsResponse, Record >(GET_ELECTIVE_OVERVIEW_STATS_DOC, {}); return { ...result, data: result.data?.electiveOverviewStats ?? null, }; }