"use client"; /** * 考勤管理页 - 客户端组件(ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5 / admin-NeedTodo §四) * * 数据契约: * - adminAttendanceStats():❌ schema 无 → MSW 兜底(@contract-pending) * - attendanceGradeCorrelation():❌ schema 无 → MSW 兜底 * - adminClasses():❌ schema 无 → MSW 兜底(用于班级筛选下拉) * - grades():✅ 真实 schema(用于年级筛选下拉) * - classComparison():❌ schema 无 → MSW 兜底(由 ClassComparisonCard 内部调用) * * URL 状态:?classId=xxx&status=xxx&date=xxx&gradeId=xxx&page=xxx * * 三态规范(§11.3 DoD):loading(骨架)/ error(局部降级)/ empty(EmptyState) * * 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4 */ import { CalendarCheck } from "lucide-react"; import { useRouter, useSearchParams } from "next/navigation"; import { useMemo, useTransition } from "react"; import { useTranslations } from "next-intl"; import { useAdminAttendanceStats, useAdminClasses, useAttendanceGradeCorrelation, useGrades, } from "@/lib/api"; import { EmptyState } from "@/shared/components/ui/empty-state"; import { StatCard } from "@/shared/components/ui/stat-card"; import { ListPageShell, ListPageSkeleton, } from "@/shared/components/page-templates"; import { computeAbnormalRate, computeAvgCorrelation, formatAbnormalRate, formatCorrelation, formatRate, formatRecordDate, hasAttendanceData, presentRateToColorClass, truncateNote, type AttendanceStatus, } from "@/features/admin/attendance/transformations"; import { ClassComparisonCard } from "@/features/admin/attendance/class-comparison-card"; import { AttendanceGradeCorrelationCard } from "@/features/admin/attendance/attendance-grade-correlation-card"; import { AttendanceRecordsList } from "@/features/admin/attendance/attendance-records-list"; /** 考勤状态选项(用于筛选下拉) */ const STATUS_OPTIONS: readonly AttendanceStatus[] = [ "present", "absent", "late", "leave", ] as const; /** * 考勤管理客户端主体。需由 server page 包裹在 中 * (useSearchParams 要求 Suspense 边界,Next.js 15 强制)。 */ export function AdminAttendanceClient(): React.ReactElement { const t = useTranslations("admin.attendance.list"); const tCommon = useTranslations("common"); const router = useRouter(); const searchParams = useSearchParams(); const [, startTransition] = useTransition(); const classId = searchParams.get("classId") ?? ""; const status = searchParams.get("status") ?? ""; const date = searchParams.get("date") ?? ""; const gradeId = searchParams.get("gradeId") ?? ""; // @contract-pending:MSW 兜底 const { data: stats, loading, error } = useAdminAttendanceStats(); const { data: correlations } = useAttendanceGradeCorrelation(); const { data: classes } = useAdminClasses(); const { data: grades } = useGrades(); const avgCorrelation = useMemo( () => computeAvgCorrelation(correlations ?? []), [correlations], ); const updateFilter = (key: string, next: string): void => { const params = new URLSearchParams(searchParams.toString()); if (next) { params.set(key, next); } else { params.delete(key); } // 切换筛选时重置页码 if (key !== "page") { params.delete("page"); } startTransition(() => { router.push(`/shell/admin/attendance?${params.toString()}`); }); }; const hasData = hasAttendanceData(stats); const errorNode = error ? (

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

{t("mswNotice")}

) : undefined; const emptyNode = ( ); return ( } filters={ updateFilter("classId", v)} onStatusChange={(v) => updateFilter("status", v)} onDateChange={(v) => updateFilter("date", v)} onGradeChange={(v) => updateFilter("gradeId", v)} /> } loading={loading} loadingNode={} empty={!hasData && !loading} emptyNode={emptyNode} errorNode={errorNode} > {stats ? ( ) : null} {/* 班级对比卡 + 考勤-成绩关联分析卡(并排布局,lg 以上双列) */}
{/* 考勤记录列表:按 classId/status/date/gradeId 筛选 + URL 分页 */}

{t("mswNotice")}

); } /** * 考勤筛选栏(年级 + 班级 + 状态 + 日期)。 */ function AttendanceFilters({ classId, status, date, gradeId, classes, grades, onClassChange, onStatusChange, onDateChange, onGradeChange, }: { classId: string; status: string; date: string; gradeId: string; classes: NonNullable["data"]>; grades: NonNullable["data"]>; onClassChange: (v: string) => void; onStatusChange: (v: string) => void; onDateChange: (v: string) => void; onGradeChange: (v: string) => void; }): React.ReactElement { const t = useTranslations("admin.attendance.list"); return (
onDateChange(e.target.value)} className="h-9 rounded-md border border-input bg-transparent px-3 text-sm shadow-xs outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]" />
); } /** * 考勤主体内容(统计卡片)。 * 班级对比与考勤-成绩关联分析由独立卡片组件渲染(ClassComparisonCard / * AttendanceGradeCorrelationCard),各自管理数据获取与三态。 */ function AttendanceContent({ stats, avgCorrelation, }: { stats: NonNullable["data"]>; avgCorrelation: number; }): React.ReactElement { const t = useTranslations("admin.attendance.list"); const abnormalRate = computeAbnormalRate( stats.absentRate, stats.lateRate, stats.earlyLeaveRate, ); return (
{/* 统计卡片 */}
0.1 ? "text-destructive" : undefined} />
{/* 次级统计卡片 */}
); } /** * 备注/日期格式化辅助导出(供页面其他部分复用,对齐 DoD 纯函数)。 */ export { formatRecordDate, truncateNote };