"use client"; /** * Admin domain API(语义化 Hook) * * 为 widgets/admin/* 提供语义化查询/变更 Hook,封装 GraphQL DOC 与类型映射。 * widget 通过 `import { useUsers } from "@/lib/api/admin"` 调用。 * * 设计: * - 查询 Hook 返回 UseQueryResult,data 已归一化(剥离外层 query 字段) * - 变更 Hook 返回 { run, loading, error },run 抛 ApiError 表示业务失败 * - 分页查询返回 UseQueryResult>,保留 items + total * * 关联:spec §2.2、§5.6 */ import { useWidgetQuery } from "@/lib/useWidgetQuery"; import { useWidgetMutation } from "@/lib/useWidgetMutation"; import { ApiError } from "./errors"; import type { PaginatedResult, Pagination, UseQueryResult } from "./types"; import { GET_USERS_DOC, UPDATE_USER_STATUS_DOC, UPDATE_USER_ROLE_DOC, GET_ROLES_DOC, GET_PERMISSIONS_DOC, UPDATE_ROLE_PERMISSIONS_DOC, GET_AUDIT_LOGS_DOC, GET_INVITATION_CODES_DOC, CREATE_INVITATION_CODE_DOC, REVOKE_INVITATION_CODE_DOC, GET_SCHOOL_DOC, UPDATE_SCHOOL_DOC, GET_PLUGIN_REGISTRY_DOC, GET_ROLE_PLUGIN_MAPPING_DOC, GET_LAYOUT_TEMPLATES_DOC, GET_ROLE_LAYOUT_DEFAULT_DOC, UPDATE_PLUGIN_REGISTRY_DOC, UPDATE_ROLE_PLUGIN_MAPPING_DOC, UPDATE_ROLE_LAYOUT_DEFAULT_DOC, RESET_USER_LAYOUT_OVERRIDE_DOC, } from "./operations/admin.graphql"; // ============================================================ // Types: User management // ============================================================ export interface User { id: string; name: string; email: string; role: string; status: string; createdAt: string; } export type UserQueryVars = { role: string | null; limit: number; offset: number; }; // ============================================================ // Types: RBAC // ============================================================ export interface Permission { id: string; name: string; resource: string; action: string; description: string; } export interface RolePermission { id: string; name: string; resource: string; action: string; } export interface Role { id: string; name: string; permissions: RolePermission[]; } // ============================================================ // Types: Audit logs // ============================================================ export interface AuditLog { id: string; userId: string; userName: string; action: string; resource: string; resourceId: string; ip: string; timestamp: string; details: string; } export interface AuditLogFilter { userId?: string | null; action?: string | null; resource?: string | null; } // ============================================================ // Types: Invitation codes // ============================================================ export interface InvitationCode { id: string; code: string; role: string; status: string; usedCount: number; maxUses: number; expiresAt: string; createdAt: string; createdBy: string; } export interface CreateInvitationCodeInput { role: string; maxUses: number; ttlHours: number; } export type CreatedInvitationCode = Pick< InvitationCode, "id" | "code" | "role" | "maxUses" | "expiresAt" >; // ============================================================ // Types: School // ============================================================ export interface School { id: string; name: string; address: string; phone: string; email: string; currentAcademicYear: string; currentTerm: string; semesterStart: string; semesterEnd: string; } export interface SchoolInput { name?: string; address?: string; phone?: string; email?: string; currentAcademicYear?: string; currentTerm?: string; semesterStart?: string; semesterEnd?: string; } // ============================================================ // Types: Plugin manager // ============================================================ export interface RegistryItem { pluginId: string; category: string; version: string; displayName: string; description: string; requiredRoles: string[]; isBuiltin: boolean; isActive: boolean; defaultSlot: string; defaultSize: { colSpan: number; rowSpan: number }; defaultProps: Record; propsSchema: Record; } export interface RoleMapping { role: string; pluginId: string; slot: string; sortOrder: number; isEnabled: boolean; widgetProps: Record; } export interface RolePluginMappingInput { pluginId: string; slot: string; sortOrder: number; isEnabled: boolean; widgetProps: Record; } export interface LayoutTemplate { layoutId: string; displayName: string; description: string; availableSlots: string[]; } export interface RoleLayoutDefault { role: string; layoutId: string; slotOverrides: Record; } export interface PluginRegistryInput { isActive?: boolean; defaultProps?: Record; } export interface UpdatedPluginRegistry { pluginId: string; isActive: boolean; defaultProps: Record; } export interface UpdatedRoleMapping { role: string; pluginId: string; isEnabled: boolean; } // ============================================================ // Hooks: User management // ============================================================ export function useUsers( vars: UserQueryVars, ): UseQueryResult> { const result = useWidgetQuery< { users: PaginatedResult }, UserQueryVars >(GET_USERS_DOC, vars); return { ...result, data: result.data?.users, }; } export function useUpdateUserStatus(): { run: (id: string, status: string) => Promise<{ id: string; status: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { updateUserStatus: { id: string; status: string } }, { id: string; status: string } >(UPDATE_USER_STATUS_DOC); const run = async ( id: string, status: string, ): Promise<{ id: string; status: string }> => { const data = await rawRun({ id, status }); if (!data?.updateUserStatus) { throw new ApiError("Failed to update user status", "INTERNAL_ERROR"); } return data.updateUserStatus; }; return { run, loading, error }; } export function useUpdateUserRole(): { run: (id: string, role: string) => Promise<{ id: string; role: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { updateUserRole: { id: string; role: string } }, { id: string; role: string } >(UPDATE_USER_ROLE_DOC); const run = async ( id: string, role: string, ): Promise<{ id: string; role: string }> => { const data = await rawRun({ id, role }); if (!data?.updateUserRole) { throw new ApiError("Failed to update user role", "INTERNAL_ERROR"); } return data.updateUserRole; }; return { run, loading, error }; } // ============================================================ // Hooks: RBAC // ============================================================ export function useRoles(): UseQueryResult { const result = useWidgetQuery<{ roles: Role[] }, Record>( GET_ROLES_DOC, {}, ); return { ...result, data: result.data?.roles, }; } export function usePermissions(): UseQueryResult { const result = useWidgetQuery< { permissions: Permission[] }, Record >(GET_PERMISSIONS_DOC, {}); return { ...result, data: result.data?.permissions, }; } export function useUpdateRolePermissions(): { run: (roleId: string, permissionIds: string[]) => Promise<{ id: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { updateRolePermissions: { id: string } }, { roleId: string; permissionIds: string[] } >(UPDATE_ROLE_PERMISSIONS_DOC); const run = async ( roleId: string, permissionIds: string[], ): Promise<{ id: string }> => { const data = await rawRun({ roleId, permissionIds }); if (!data?.updateRolePermissions) { throw new ApiError("Failed to update role permissions", "INTERNAL_ERROR"); } return data.updateRolePermissions; }; return { run, loading, error }; } // ============================================================ // Hooks: Audit logs // ============================================================ export function useAuditLogs( filter: AuditLogFilter, pagination: Pagination, ): UseQueryResult> { const result = useWidgetQuery< { auditLogs: PaginatedResult }, { filter: AuditLogFilter; limit: number; offset: number } >(GET_AUDIT_LOGS_DOC, { filter, limit: pagination.limit, offset: pagination.offset, }); return { ...result, data: result.data?.auditLogs, }; } // ============================================================ // Hooks: Invitation codes // ============================================================ export function useInvitationCodes( status: string | null, ): UseQueryResult { const result = useWidgetQuery< { invitationCodes: InvitationCode[] }, { status: string | null } >(GET_INVITATION_CODES_DOC, { status }); return { ...result, data: result.data?.invitationCodes, }; } export function useCreateInvitationCode(): { run: (input: CreateInvitationCodeInput) => Promise; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { createInvitationCode: CreatedInvitationCode }, { input: CreateInvitationCodeInput } >(CREATE_INVITATION_CODE_DOC); const run = async ( input: CreateInvitationCodeInput, ): Promise => { const data = await rawRun({ input }); if (!data?.createInvitationCode) { throw new ApiError("Failed to create invitation code", "INTERNAL_ERROR"); } return data.createInvitationCode; }; return { run, loading, error }; } export function useRevokeInvitationCode(): { run: (id: string) => Promise<{ id: string; status: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { revokeInvitationCode: { id: string; status: string } }, { id: string } >(REVOKE_INVITATION_CODE_DOC); const run = async (id: string): Promise<{ id: string; status: string }> => { const data = await rawRun({ id }); if (!data?.revokeInvitationCode) { throw new ApiError("Failed to revoke invitation code", "INTERNAL_ERROR"); } return data.revokeInvitationCode; }; return { run, loading, error }; } // ============================================================ // Hooks: School settings // ============================================================ export function useSchool(): UseQueryResult { const result = useWidgetQuery< { school: School | null }, Record >(GET_SCHOOL_DOC, {}); return { ...result, data: result.data?.school ?? null, }; } export function useUpdateSchool(): { run: (input: SchoolInput) => Promise; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation<{ updateSchool: School }, { input: SchoolInput }>( UPDATE_SCHOOL_DOC, ); const run = async (input: SchoolInput): Promise => { const data = await rawRun({ input }); if (!data?.updateSchool) { throw new ApiError("Failed to update school", "INTERNAL_ERROR"); } return data.updateSchool; }; return { run, loading, error }; } // ============================================================ // Hooks: Plugin manager // ============================================================ export function usePluginRegistry(): UseQueryResult { const result = useWidgetQuery< { pluginRegistry: RegistryItem[] }, Record >(GET_PLUGIN_REGISTRY_DOC, {}); return { ...result, data: result.data?.pluginRegistry, }; } export function useRolePluginMapping( role: string, ): UseQueryResult { const result = useWidgetQuery< { rolePluginMapping: RoleMapping[] }, { role: string | null } >(GET_ROLE_PLUGIN_MAPPING_DOC, { role }); return { ...result, data: result.data?.rolePluginMapping, }; } export function useLayoutTemplates(): UseQueryResult { const result = useWidgetQuery< { layoutTemplates: LayoutTemplate[] }, Record >(GET_LAYOUT_TEMPLATES_DOC, {}); return { ...result, data: result.data?.layoutTemplates, }; } export function useRoleLayoutDefault( role: string, ): UseQueryResult { const result = useWidgetQuery< { roleLayoutDefault: RoleLayoutDefault | null }, { role: string | null } >(GET_ROLE_LAYOUT_DEFAULT_DOC, { role }); return { ...result, data: result.data?.roleLayoutDefault ?? null, }; } export function useUpdatePluginRegistry(): { run: ( pluginId: string, input: PluginRegistryInput, ) => Promise; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { updatePluginRegistry: UpdatedPluginRegistry }, { pluginId: string; input: PluginRegistryInput } >(UPDATE_PLUGIN_REGISTRY_DOC); const run = async ( pluginId: string, input: PluginRegistryInput, ): Promise => { const data = await rawRun({ pluginId, input }); if (!data?.updatePluginRegistry) { throw new ApiError("Failed to update plugin registry", "INTERNAL_ERROR"); } return data.updatePluginRegistry; }; return { run, loading, error }; } export function useUpdateRolePluginMapping(): { run: ( role: string, mappings: RolePluginMappingInput[], ) => Promise; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { updateRolePluginMapping: UpdatedRoleMapping[] }, { role: string; mappings: RolePluginMappingInput[] } >(UPDATE_ROLE_PLUGIN_MAPPING_DOC); const run = async ( role: string, mappings: RolePluginMappingInput[], ): Promise => { const data = await rawRun({ role, mappings }); if (!data?.updateRolePluginMapping) { throw new ApiError( "Failed to update role plugin mapping", "INTERNAL_ERROR", ); } return data.updateRolePluginMapping; }; return { run, loading, error }; } export function useUpdateRoleLayoutDefault(): { run: ( role: string, layoutId: string, ) => Promise<{ role: string; layoutId: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { updateRoleLayoutDefault: { role: string; layoutId: string } }, { role: string; layoutId: string } >(UPDATE_ROLE_LAYOUT_DEFAULT_DOC); const run = async ( role: string, layoutId: string, ): Promise<{ role: string; layoutId: string }> => { const data = await rawRun({ role, layoutId }); if (!data?.updateRoleLayoutDefault) { throw new ApiError( "Failed to update role layout default", "INTERNAL_ERROR", ); } return data.updateRoleLayoutDefault; }; return { run, loading, error }; } export function useResetUserLayoutOverride(): { run: (userId: string) => Promise<{ userId: string }>; loading: boolean; error: unknown; } { const { run: rawRun, loading, error, } = useWidgetMutation< { resetUserLayoutOverride: { userId: string } }, { userId: string } >(RESET_USER_LAYOUT_OVERRIDE_DOC); const run = async (userId: string): Promise<{ userId: string }> => { const data = await rawRun({ userId }); if (!data?.resetUserLayoutOverride) { throw new ApiError( "Failed to reset user layout override", "INTERNAL_ERROR", ); } return data.resetUserLayoutOverride; }; return { run, loading, error }; }