按 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 已更新
3382 lines
91 KiB
TypeScript
3382 lines
91 KiB
TypeScript
"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<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-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<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-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<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-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<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-pending,MSW 兜底)。
|
||
*
|
||
* 用于详情对话框按 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-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<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-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<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-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<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-pending,MSW 兜底)
|
||
// ============================================================
|
||
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-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<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-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<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-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<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-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<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-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<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-pending,MSW 兜底)。
|
||
* 用于列表页 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,
|
||
};
|
||
}
|