"use client"; /** * 自动排课页 - 客户端组件(ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5 / admin-NeedTodo §四) * * 数据契约: * - adminClasses():❌ schema 无 → MSW 兜底(@contract-pending) * - autoSchedule(classId) mutation:❌ schema 无 → MSW 兜底 * * URL 状态:无(操作型页面,状态本地维护) * * 三态规范(§11.3 DoD):loading(骨架)/ error(局部降级)/ empty(EmptyState + 行动按钮) * * 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4 */ import { CalendarClock } from "lucide-react"; import Link from "next/link"; import { useState } from "react"; import { useTranslations } from "next-intl"; import { useAdminClasses, useAutoSchedule } from "@/lib/api"; import { notify } from "@/shared/lib/notify"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/shared/components/ui/alert-dialog"; import { Button } from "@/shared/components/ui/button"; import { EmptyState } from "@/shared/components/ui/empty-state"; import { ListPageShell, ListPageSkeleton, } from "@/shared/components/page-templates"; import { formatGeneratedEntries, hasConflicts, scheduleStatusToBadgeClass, scheduleStatusToKey, type ScheduleStatus, } from "@/features/admin/scheduling/transformations"; /** 班级排课结果本地状态 */ interface ClassScheduleResult { status: ScheduleStatus; generatedEntries?: number; conflicts?: string[]; } /** * 自动排课客户端主体。需由 server page 包裹在 中。 */ export function AutoScheduleClient(): React.ReactElement { const t = useTranslations("admin.scheduling.auto"); const tCommon = useTranslations("common"); // @contract-pending:MSW 兜底 const { data, loading, error } = useAdminClasses(); const { run: autoSchedule, loading: running } = useAutoSchedule(); const [results, setResults] = useState>( {}, ); const classes = data ?? []; const handleStart = async (classId: string): Promise => { // 标记为 pending(覆盖之前结果) setResults((prev) => ({ ...prev, [classId]: { status: "pending" }, })); try { const result = await autoSchedule(classId); const nextStatus: ScheduleStatus = hasConflicts(result.conflicts) ? "failed" : "scheduled"; setResults((prev) => ({ ...prev, [classId]: { status: nextStatus, generatedEntries: result.generatedEntries, conflicts: result.conflicts, }, })); if (nextStatus === "scheduled") { notify.success(t("startSuccess", { count: result.generatedEntries })); } else { notify.error(t("startError")); } } catch (err) { setResults((prev) => ({ ...prev, [classId]: { status: "failed", conflicts: [String(err)] }, })); notify.error(`${t("startError")}: ${String(err)}`); } }; const errorNode = error ? (

{tCommon("error.loadFailed", { message: String(error) })}

{t("mswNotice")}

) : undefined; const emptyNode = ( ); return ( } actions={ } loading={loading} loadingNode={} empty={classes.length === 0 && !loading} emptyNode={emptyNode} errorNode={errorNode} >

{t("mswNotice")}

); } /** * 班级自动排课表格。 */ function AutoScheduleTable({ classes, results, running, onStart, }: { classes: NonNullable["data"]>; results: Record; running: boolean; onStart: (classId: string) => Promise; }): React.ReactElement { const t = useTranslations("admin.scheduling.auto"); const tCommon = useTranslations("common"); const [confirmId, setConfirmId] = useState(null); return (
{classes.map((cls) => { const result = results[cls.id]; const statusKey = scheduleStatusToKey(result?.status ?? "pending"); const statusLabel = t( `status${statusKey.charAt(0).toUpperCase()}${statusKey.slice(1)}` as "statusPending" | "statusScheduled" | "statusFailed", ); return ( ); })}
{t("colClassName")} {t("colGrade")} {t("colTeacher")} {t("colSubjectCount")} {t("colStatus")} {t("startButton")}
{cls.name} {cls.gradeName} {cls.headTeacherName || "--"} {cls.subjectCount} {result?.generatedEntries != null ? ( ({formatGeneratedEntries(result.generatedEntries)}) ) : null}
{ if (!open) setConfirmId(null); }} > {t("startButton")} {t("startConfirm")} {tCommon("cancel")} { if (confirmId) void onStart(confirmId); setConfirmId(null); }} > {t("startButton")}
); } /** * 排课状态徽章。 */ function StatusBadge({ status, label, }: { status: ScheduleStatus; label: string; }): React.ReactElement { const cls = scheduleStatusToBadgeClass(status); return ( {label} ); }