Files
Edu/apps/portal-shell/src/lib/api/admin-p5.ts
SpecialX f991bf0446 feat(portal-shell): 管理域全模块功能补齐与差异修复
按 ARCHITECTURE.md 与 admin-NeedTodo.md 要求补齐所有管理页面缺失功能:
- users/roles/permissions:权限矩阵搜索/折叠、zod 校验、value 字段
- audit-logs:行内详情对话框、分页页码、ChartCardShell
- school:CRUD 对话框、GradeOverviewCards、academic-year 侧栏
- announcements/invitation-codes/ai-settings:发布按钮、分页、zod 校验
- course-plans/elective:Select 导入、undefined 处理
- error-book/scheduling/questions/lesson-plans/attendance:统计卡片

验证:typecheck 0 错误、arch:scan 已更新
2026-07-30 17:49:49 +08:00

3382 lines
91 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
/**
* Admin domain P5 扩展 HookARCHITECTURE.md §9.4 B5 + admin-NeedTodo §四)
*
* 覆盖:
* - users/import、roles detail + CRUD、permissions role counts
* - audit-logsoverview / login-logs / data-changes / module-options / stats
* - schoolschools CRUD / departments / academic-year / grades / admin-classes / teacher/staff options
* - announcementslist / detail / CRUD / archive / pin
* - fileslist / stats
* - ai-settingsproviders CRUD / usage dashboard
* - system settings、viewports
* - students / teachers / organizationARCH 新建)
* - §四补充批次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-pendingschema 未就绪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 已定义 RolePermissionid/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-pendingMSW 兜底) */
code?: string;
/** 更新时间 ISO 字符串(@contract-pendingMSW 兜底) */
updatedAt?: string;
}
export interface Department {
id: string;
name: string;
schoolId: string;
headId: string;
headName: string;
memberCount: number;
/** 部门描述(@contract-pendingMSW 兜底) */
description?: string;
/** 更新时间 ISO 字符串(@contract-pendingMSW 兜底) */
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-pendingMSW 兜底) */
order?: number;
/** 教学主任 ID@contract-pendingMSW 兜底) */
teachingHead?: string;
/** 教学主任姓名(@contract-pendingMSW 兜底) */
teachingHeadName?: string;
/** 更新时间 ISO 字符串(@contract-pendingMSW 兜底) */
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-pendingMSW 兜底) */
homeroomLabel?: string;
/** 教室(@contract-pendingMSW 兜底) */
room?: string;
/** 班主任 ID@contract-pendingMSW 兜底) */
homeroom?: string;
/** 班主任姓名(@contract-pendingMSW 兜底) */
homeroomName?: string;
/** 任课教师列表(@contract-pendingMSW 兜底) */
subjectTeachers?: string;
/** 更新时间 ISO 字符串(@contract-pendingMSW 兜底) */
updatedAt?: string;
}
/** 班级 CRUD 输入类型(@contract-pendingMSW 兜底) */
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<string, number>;
}
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<string, unknown>;
createdAt: string;
}
export interface AiProviderInput {
name: string;
type: string;
scope: string;
model: string;
apiBase?: string;
isActive?: boolean;
isDefault?: boolean;
visibility?: string;
config?: Record<string, unknown>;
}
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-pendingMSW 兜底,对齐 CICD 列表"学期"列) */
semester?: string;
/** 总课时(@contract-pendingMSW 兜底,用于进度条 totalHours */
totalHours?: number;
/** 已完成课时(@contract-pendingMSW 兜底,用于进度条 completedHours */
completedHours?: number;
/** 更新时间 ISO 字符串(@contract-pendingMSW 兜底) */
updatedAt?: string;
}
/** 管理端课程计划周计划项(@contract-pendingMSW 兜底) */
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-pendingMSW 兜底,对齐 CICD 详情"教学大纲"区) */
syllabus?: string;
/** 教学目标(@contract-pendingMSW 兜底,对齐 CICD 详情"教学目标"区) */
objectives?: string;
/** 周课时(@contract-pendingMSW 兜底) */
weeklyHours?: number;
/** 计划开始日期(@contract-pendingMSW 兜底) */
startDate?: string;
/** 计划结束日期(@contract-pendingMSW 兜底) */
endDate?: string;
/** 教材链接(@contract-pendingMSW 兜底) */
textbooksHref?: string;
/** 作业链接(@contract-pendingMSW 兜底) */
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-pendingMSW 兜底)。
* 与 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-pendingMSW 兜底,对齐 CICD 列表"教室"列) */
classroom?: string;
/** 上课时间(@contract-pendingMSW 兜底,对齐 CICD 列表"时间"列) */
schedule?: string;
/** 学分(@contract-pendingMSW 兜底,对齐 CICD 列表"学分"列) */
credit?: number;
/** 选课模式(@contract-pendingMSW 兜底FIRST_COME | LOTTERY */
selectionMode?: string;
/** 选课开始时间 ISO@contract-pendingMSW 兜底) */
selectionStartAt?: string;
/** 选课结束时间 ISO@contract-pendingMSW 兜底) */
selectionEndAt?: string;
/** 退课截止时间 ISO@contract-pendingMSW 兜底) */
dropDeadline?: string;
/** 描述(@contract-pendingMSW 兜底,列表展示用 line-clamp */
description?: string;
/** 开始日期(@contract-pendingMSW 兜底) */
startDate?: string;
/** 结束日期(@contract-pendingMSW 兜底) */
endDate?: string;
/** 创建时间 ISO@contract-pendingMSW 兜底) */
createdAt?: string;
/** 更新时间 ISO@contract-pendingMSW 兜底) */
updatedAt?: string;
}
export interface AdminElective extends AdminElectiveListItem {
description: string;
selections: Array<{
studentId: string;
studentName: string;
selectedAt: string;
/** 选课状态(@contract-pendingMSW 兜底confirmed | pending | cancelled */
status?: string;
/** 优先级(@contract-pendingMSW 兜底,对齐 CICD 详情"优先级"列) */
priority?: number;
/** 选课时间别名(@contract-pendingMSW 兜底,与 selectedAt 同义) */
enrolledAt?: string;
}>;
}
/** 创建课程计划输入(@contract-pendingMSW 兜底,扩展 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-pendingMSW 兜底) */
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-pendingMSW 兜底,扩展 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-pendingMSW 兜底) */
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-pendingMSW 兜底) */
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-pendingMSW 兜底)。
*
* 与 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-pendingMSW 兜底)。
* 用于"按班级分组"卡片展示,与 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<string, unknown>;
}
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-pendingMSW 兜底,用于年级筛选) */
gradeId: string;
date: string;
status: string;
recordedBy: string;
note: string;
}
export interface AttendanceFilter {
classId?: string | null;
status?: string | null;
date?: string | null;
/** 年级筛选(@contract-pendingMSW 兜底)。与 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<ImportResult>;
loading: boolean;
error: unknown;
} {
const {
run: rawRun,
loading,
error,
} = useWidgetMutation<{ importUsers: ImportResult }, { file: File }>(
IMPORT_USERS_DOC,
);
const run = async (file: File): Promise<ImportResult> => {
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<RoleDetail | null> {
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<RolePermissionMatrixItem[]> {
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<string, never>
>(GET_PERMISSION_ROLE_COUNTS_DOC, {});
return { ...result, data: result.data?.permissionRoleCounts };
}
// ============================================================
// Hooks: Audit logs
// ============================================================
export function useAuditOverviewStats(): UseQueryResult<AuditOverviewStats | null> {
const result = useWidgetQuery<
{ auditOverviewStats: AuditOverviewStats | null },
Record<string, never>
>(GET_AUDIT_OVERVIEW_STATS_DOC, {});
return { ...result, data: result.data?.auditOverviewStats ?? null };
}
export function useAuditTrend(days: number): UseQueryResult<AuditTrendPoint[]> {
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<string, never>
>(GET_DATA_CHANGE_ACTION_STATS_DOC, {});
return { ...result, data: result.data?.dataChangeActionStats };
}
export function useLoginLogs(
filter: LoginLogFilter,
pagination: Pagination,
): UseQueryResult<PaginatedResult<LoginLog>> {
const result = useWidgetQuery<
{ loginLogs: PaginatedResult<LoginLog> },
{ 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<PaginatedResult<DataChangeLog>> {
const result = useWidgetQuery<
{ dataChangeLogs: PaginatedResult<DataChangeLog> },
{ 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<string[]> {
const result = useWidgetQuery<
{ dataChangeTableOptions: string[] },
Record<string, never>
>(GET_DATA_CHANGE_TABLE_OPTIONS_DOC, {});
return { ...result, data: result.data?.dataChangeTableOptions };
}
export function useDataChangeStats(): UseQueryResult<DataChangeStat[]> {
const result = useWidgetQuery<
{ dataChangeStats: DataChangeStat[] },
Record<string, never>
>(GET_DATA_CHANGE_STATS_DOC, {});
return { ...result, data: result.data?.dataChangeStats };
}
export function useAuditModuleOptions(): UseQueryResult<string[]> {
const result = useWidgetQuery<
{ auditModuleOptions: string[] },
Record<string, never>
>(GET_AUDIT_MODULE_OPTIONS_DOC, {});
return { ...result, data: result.data?.auditModuleOptions };
}
// ============================================================
// Hooks: Audit / Login / DataChange - export (CSV)
// ============================================================
export function useExportAuditLogs(): {
run: (filter: AuditLogFilter) => Promise<ExportResult>;
loading: boolean;
error: unknown;
} {
const {
run: rawRun,
loading,
error,
} = useWidgetMutation<
{ exportAuditLogs: ExportResult },
{ filter: AuditLogFilter }
>(EXPORT_AUDIT_LOGS_DOC);
const run = async (filter: AuditLogFilter): Promise<ExportResult> => {
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<ExportResult>;
loading: boolean;
error: unknown;
} {
const {
run: rawRun,
loading,
error,
} = useWidgetMutation<
{ exportLoginLogs: ExportResult },
{ filter: LoginLogFilter }
>(EXPORT_LOGIN_LOGS_DOC);
const run = async (filter: LoginLogFilter): Promise<ExportResult> => {
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<ExportResult>;
loading: boolean;
error: unknown;
} {
const {
run: rawRun,
loading,
error,
} = useWidgetMutation<
{ exportDataChanges: ExportResult },
{ filter: DataChangeLogFilter }
>(EXPORT_DATA_CHANGES_DOC);
const run = async (filter: DataChangeLogFilter): Promise<ExportResult> => {
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<SchoolListItem[]> {
const result = useWidgetQuery<
{ schools: SchoolListItem[] },
Record<string, never>
>(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<Department[]> {
const result = useWidgetQuery<
{ departments: Department[] },
Record<string, never>
>(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<AcademicYear[]> {
const result = useWidgetQuery<
{ academicYears: AcademicYear[] },
Record<string, never>
>(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<AdminGradeListItem[]> {
const result = useWidgetQuery<
{ grades: AdminGradeListItem[] },
Record<string, never>
>(GET_GRADES_DOC, {});
return { ...result, data: result.data?.grades };
}
export function useGradeOverviewStats(): UseQueryResult<GradeOverviewStat[]> {
const result = useWidgetQuery<
{ gradeOverviewStats: GradeOverviewStat[] },
Record<string, never>
>(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<AdminClassListItem[]> {
const result = useWidgetQuery<
{ adminClasses: AdminClassListItem[] },
Record<string, never>
>(GET_ADMIN_CLASSES_DOC, {});
return { ...result, data: result.data?.adminClasses };
}
export function useTeacherOptions(): UseQueryResult<OptionItem[]> {
const result = useWidgetQuery<
{ teacherOptions: OptionItem[] },
Record<string, never>
>(GET_TEACHER_OPTIONS_DOC, {});
return { ...result, data: result.data?.teacherOptions };
}
export function useStaffOptions(): UseQueryResult<OptionItem[]> {
const result = useWidgetQuery<
{ staffOptions: OptionItem[] },
Record<string, never>
>(GET_STAFF_OPTIONS_DOC, {});
return { ...result, data: result.data?.staffOptions };
}
/**
* 创建班级(@contract-pendingMSW 兜底)。
* 用于 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-pendingMSW 兜底)。
* 用于 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-pendingMSW 兜底)。
* 用于 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<PaginatedResult<AnnouncementListItem>> {
const result = useWidgetQuery<
{ adminAnnouncements: PaginatedResult<AnnouncementListItem> },
{ status: string | null }
>(GET_ADMIN_ANNOUNCEMENTS_DOC, { status });
return { ...result, data: result.data?.adminAnnouncements };
}
export function useAdminAnnouncement(
id: string,
): UseQueryResult<AnnouncementDetail | null> {
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<PaginatedResult<FileAttachment>> {
const result = useWidgetQuery<
{ fileAttachments: PaginatedResult<FileAttachment> },
{ filter: FileFilter }
>(GET_FILE_ATTACHMENTS_DOC, { filter });
return { ...result, data: result.data?.fileAttachments };
}
export function useFileStats(): UseQueryResult<FileStats | null> {
const result = useWidgetQuery<
{ fileStats: FileStats | null },
Record<string, never>
>(GET_FILE_STATS_DOC, {});
return { ...result, data: result.data?.fileStats ?? null };
}
export function useUploadFile(): {
run: (input: UploadFileInput) => Promise<UploadFileResult>;
loading: boolean;
error: unknown;
} {
const {
run: rawRun,
loading,
error,
} = useWidgetMutation<
{ uploadFile: UploadFileResult },
{ input: UploadFileInput }
>(UPLOAD_FILE_DOC);
const run = async (input: UploadFileInput): Promise<UploadFileResult> => {
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<BatchDeleteFilesResult>;
loading: boolean;
error: unknown;
} {
const {
run: rawRun,
loading,
error,
} = useWidgetMutation<
{ batchDeleteFiles: BatchDeleteFilesResult },
{ fileIds: string[] }
>(BATCH_DELETE_FILES_DOC);
const run = async (fileIds: string[]): Promise<BatchDeleteFilesResult> => {
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<AiProvider[]> {
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<AiUsageDashboard | null> {
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<SystemSetting[]> {
const result = useWidgetQuery<
{ systemSettings: SystemSetting[] },
Record<string, never>
>(GET_SYSTEM_SETTINGS_DOC, {});
return { ...result, data: result.data?.systemSettings };
}
export function useUpdateSystemSettings(): {
run: (input: SystemSettingInput[]) => Promise<SystemSetting[]>;
loading: boolean;
error: unknown;
} {
const {
run: rawRun,
loading,
error,
} = useWidgetMutation<
{ updateSystemSettings: SystemSetting[] },
{ input: SystemSettingInput[] }
>(UPDATE_SYSTEM_SETTINGS_DOC);
const run = async (input: SystemSettingInput[]): Promise<SystemSetting[]> => {
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<Viewport[]> {
const result = useWidgetQuery<
{ viewports: Viewport[] },
Record<string, never>
>(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<PaginatedResult<AdminStudent>> {
const result = useWidgetQuery<
{ adminStudents: PaginatedResult<AdminStudent> },
{ 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<PaginatedResult<AdminTeacher>> {
const result = useWidgetQuery<
{ adminTeachers: PaginatedResult<AdminTeacher> },
{ 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<OrgNode[]> {
const result = useWidgetQuery<
{ organizationTree: OrgNode[] },
Record<string, never>
>(GET_ORGANIZATION_TREE_DOC, {});
return { ...result, data: result.data?.organizationTree };
}
// ============================================================
// Hooks: §四补充批次 - Course plans
// ============================================================
export function useAdminCoursePlans(
status: string | null,
): UseQueryResult<PaginatedResult<AdminCoursePlanListItem>> {
const result = useWidgetQuery<
{ adminCoursePlans: PaginatedResult<AdminCoursePlanListItem> },
{ status: string | null }
>(GET_ADMIN_COURSE_PLANS_DOC, { status });
return { ...result, data: result.data?.adminCoursePlans };
}
export function useAdminCoursePlan(
id: string,
): UseQueryResult<AdminCoursePlan | null> {
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<string, never>
>(GET_STANDARDS_COVERAGE_HEATMAP_DOC, {});
return { ...result, data: result.data?.standardsCoverageHeatmap };
}
export function useGlobalLessonPlanStats(): UseQueryResult<GlobalLessonPlanStats | null> {
const result = useWidgetQuery<
{ globalLessonPlanStats: GlobalLessonPlanStats | null },
Record<string, never>
>(GET_GLOBAL_LESSON_PLAN_STATS_DOC, {});
return { ...result, data: result.data?.globalLessonPlanStats ?? null };
}
// Electives
export function useAdminElectives(): UseQueryResult<
PaginatedResult<AdminElectiveListItem>
> {
const result = useWidgetQuery<
{ adminElectives: PaginatedResult<AdminElectiveListItem> },
Record<string, never>
>(GET_ADMIN_ELECTIVES_DOC, {});
return { ...result, data: result.data?.adminElectives };
}
export function useAdminElective(
id: string,
): UseQueryResult<AdminElective | null> {
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<PaginatedResult<AdminQuestionListItem>> {
const result = useWidgetQuery<
{ adminQuestions: PaginatedResult<AdminQuestionListItem> },
{ filter: QuestionFilter }
>(GET_ADMIN_QUESTIONS_DOC, { filter });
return { ...result, data: result.data?.adminQuestions };
}
/**
* 题库单条详情 hook@contract-pendingMSW 兜底)。
*
* 用于详情对话框按 id 拉取题目明细answer/explanation/knowledgePointTitle 等)。
* id 为空时不发请求(对话框关闭时跳过)。
*/
export function useAdminQuestion(
id: string,
options?: { enabled?: boolean },
): UseQueryResult<AdminQuestionDetail | null> {
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<AdminLessonPlanListItem>
> {
const result = useWidgetQuery<
{ adminLessonPlans: PaginatedResult<AdminLessonPlanListItem> },
Record<string, never>
>(GET_ADMIN_LESSON_PLANS_DOC, {});
return { ...result, data: result.data?.adminLessonPlans };
}
export function useAdminLessonPlan(
id: string,
): UseQueryResult<AdminLessonPlan | null> {
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-pendingMSW 兜底)。
*
* 对齐 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<SoftDeleteLessonPlanResponse, { planId: string }>(
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<AdminErrorBookStats | null> {
const result = useWidgetQuery<
{ adminErrorBookStats: AdminErrorBookStats | null },
Record<string, never>
>(GET_ADMIN_ERROR_BOOK_STATS_DOC, {});
return { ...result, data: result.data?.adminErrorBookStats ?? null };
}
/**
* 导出错题本 CSV mutation@contract-pendingMSW 兜底)。
*
* 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<ScheduleChange>
> {
const result = useWidgetQuery<
{ adminScheduleChanges: PaginatedResult<ScheduleChange> },
Record<string, never>
>(GET_ADMIN_SCHEDULE_CHANGES_DOC, {});
return { ...result, data: result.data?.adminScheduleChanges };
}
export function useAdminScheduleEntries(): UseQueryResult<ScheduleEntry[]> {
const result = useWidgetQuery<
{ adminScheduleEntries: ScheduleEntry[] },
Record<string, never>
>(GET_ADMIN_SCHEDULE_ENTRIES_DOC, {});
return { ...result, data: result.data?.adminScheduleEntries };
}
export function useAdminSchedulingRules(): UseQueryResult<
AdminSchedulingRule[]
> {
const result = useWidgetQuery<
{ schedulingRules: AdminSchedulingRule[] },
Record<string, never>
>(ADMIN_GET_SCHEDULING_RULES_DOC, {});
return { ...result, data: result.data?.schedulingRules };
}
export function useUpdateSchedulingRules(): {
run: (
input: Array<{ id: string; isEnabled: boolean }>,
) => Promise<Array<{ id: string; isEnabled: boolean }>>;
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<Array<{ id: string; isEnabled: boolean }>> => {
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<AdminAttendanceStats | null> {
const result = useWidgetQuery<
{ adminAttendanceStats: AdminAttendanceStats | null },
Record<string, never>
>(GET_ADMIN_ATTENDANCE_STATS_DOC, {});
return { ...result, data: result.data?.adminAttendanceStats ?? null };
}
export function useAdminAttendanceRecords(
filter: AttendanceFilter,
pagination: Pagination,
): UseQueryResult<PaginatedResult<AdminAttendanceRecord>> {
const result = useWidgetQuery<
{ adminAttendanceRecords: PaginatedResult<AdminAttendanceRecord> },
{ 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<string, never>
>(GET_ATTENDANCE_GRADE_CORRELATION_DOC, {});
return { ...result, data: result.data?.attendanceGradeCorrelation };
}
// ============================================================
// Types & Hooks: Audit retention config + purge@contract-pendingMSW 兜底)
// ============================================================
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<AuditRetentionConfig | null> {
const result = useWidgetQuery<
{ auditRetentionConfig: AuditRetentionConfig | null },
Record<string, never>
>(GET_AUDIT_RETENTION_CONFIG_DOC, {});
return { ...result, data: result.data?.auditRetentionConfig ?? null };
}
export function useSaveAuditRetentionConfig(): {
run: (input: AuditRetentionConfigInput) => Promise<AuditRetentionConfig>;
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<AuditRetentionConfig> => {
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<PurgeResult>;
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<PurgeResult> => {
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-pendingMSW 兜底)
// ============================================================
export function useCreateCoursePlanItem(): {
run: (input: AdminCoursePlanItemInput) => Promise<AdminCoursePlanItem>;
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<AdminCoursePlanItem> => {
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<AdminCoursePlanItem>;
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<AdminCoursePlanItem> => {
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<Array<{ id: string; week: number; updatedAt: string }>>;
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<Array<{ id: string; week: number; updatedAt: string }>> => {
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-pendingMSW 兜底)
// ============================================================
/** 删除课程计划 mutation 响应(@contract-pending */
interface DeleteCoursePlanResponse {
deleteCoursePlan: { id: string; success: boolean } | null;
}
/**
* 删除课程计划(@contract-pendingMSW 兜底)。
* 用于详情页/列表页删除按钮,调用后建议 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<DeleteCoursePlanResponse, { id: string }>(
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-pendingMSW 兜底)。
* 用于详情页批量"标记完成/取消完成"操作。
*
* 关联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-pendingMSW 兜底)
// ============================================================
/** 创建选修课 mutation 响应(@contract-pending */
interface AdminCreateElectiveResponse {
createElective: { id: string } | null;
}
/**
* 创建选修课(@contract-pendingMSW 兜底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-pendingMSW 兜底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-pendingMSW 兜底)。
*
* 关联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<DeleteElectiveResponse, { id: string }>(
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-pendingMSW 兜底)。
* 将选修课状态从 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<OpenElectiveSelectionResponse, { id: string }>(
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-pendingMSW 兜底)。
* 将选修课状态从 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<CloseElectiveSelectionResponse, { id: string }>(
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-pendingMSW 兜底)。
* 对 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<RunElectiveLotteryResponse, { id: string }>(
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-pendingMSW 兜底)。
* 用于列表页 StatsGrid 顶部统计卡片(总数 / 容量 / 已选 / 草稿 / 报名中)。
*
* 关联ARCHITECTURE.md §5.4 / §9.4 / §11.4 契约工单
*/
export function useGetElectiveOverviewStats(): UseQueryResult<ElectiveOverviewStats | null> {
const result = useWidgetQuery<
ElectiveOverviewStatsResponse,
Record<string, never>
>(GET_ELECTIVE_OVERVIEW_STATS_DOC, {});
return {
...result,
data: result.data?.electiveOverviewStats ?? null,
};
}