/** * 仪表盘纯逻辑工具函数(与 UI 分离,便于单测)。 * * 所有函数均为纯函数:相同输入 → 相同输出,无副作用。 */ import type { HomeworkAssignmentListItem, HomeworkSubmissionListItem, StudentHomeworkAssignmentListItem, StudentHomeworkProgressStatus, TeacherGradeTrendItem, } from "@/modules/homework/types" import type { ClassScheduleItem, TeacherClass } from "@/modules/classes/types" import type { StudentTodayScheduleItem, TeacherTodayScheduleItem, TeacherDashboardData, } from "@/modules/dashboard/types" /** 周一=1 ... 周日=7 */ export type Weekday = 1 | 2 | 3 | 4 | 5 | 6 | 7 /** * 将 Date 转换为 1-7 周几表示(周一=1,周日=7)。 * getDay() 返回 0(周日)-6(周六),需映射为 1-7。 */ export function toWeekday(d: Date): Weekday { const day = d.getDay() if (day < 0 || day > 6) { throw new Error(`Invalid day from getDay(): ${day}`) } const WEEKDAY_MAP: readonly Weekday[] = [7, 1, 2, 3, 4, 5, 6] return WEEKDAY_MAP[day] } /** 学生作业统计结果 */ export interface StudentAssignmentStats { dueSoonCount: number overdueCount: number gradedCount: number } /** * 单次遍历统计学生作业状态:即将到期 / 已逾期 / 已批改。 */ export function countStudentAssignments( assignments: readonly StudentHomeworkAssignmentListItem[], now: Date, dueSoonWindowDays = 7, ): StudentAssignmentStats { const in7Days = new Date(now) in7Days.setDate(in7Days.getDate() + dueSoonWindowDays) let dueSoonCount = 0 let overdueCount = 0 let gradedCount = 0 for (const a of assignments) { const status: StudentHomeworkProgressStatus = a.progressStatus if (status === "graded") { gradedCount++ continue } if (!a.dueAt) continue const due = new Date(a.dueAt) if (due >= now && due <= in7Days) { dueSoonCount++ } else if (due < now) { overdueCount++ } } return { dueSoonCount, overdueCount, gradedCount } } /** * 按截止日期升序排序作业,取前 N 条作为「即将到期」列表。 * 无截止日期的作业排到最后。 */ export function sortUpcomingAssignments( assignments: readonly StudentHomeworkAssignmentListItem[], limit = 6, ): StudentHomeworkAssignmentListItem[] { return [...assignments] .sort((a, b) => { const aDue = a.dueAt ? new Date(a.dueAt).getTime() : Number.POSITIVE_INFINITY const bDue = b.dueAt ? new Date(b.dueAt).getTime() : Number.POSITIVE_INFINITY return aDue - bDue }) .slice(0, limit) } /** * 从课表中筛选指定周几的课程,按开始时间升序排序。 */ export function filterTodaySchedule( schedule: readonly ClassScheduleItem[], weekday: Weekday, classNameById?: ReadonlyMap, ): StudentTodayScheduleItem[] | TeacherTodayScheduleItem[] { return schedule .filter((s) => s.weekday === weekday) .sort((a, b) => a.startTime.localeCompare(b.startTime)) .map((s) => ({ id: s.id, classId: s.classId, className: classNameById?.get(s.classId) ?? "Class", course: s.course, startTime: s.startTime, endTime: s.endTime, location: s.location ?? null, })) as StudentTodayScheduleItem[] | TeacherTodayScheduleItem[] } /** 教师仪表盘派生指标 */ export interface TeacherDashboardMetrics { todayScheduleItems: TeacherTodayScheduleItem[] toGradeCount: number submissionsToGrade: HomeworkSubmissionListItem[] activeAssignmentsCount: number averageScore: number submissionRate: number } /** * 计算教师仪表盘派生指标:待批改数、进行中作业数、平均分、提交率等。 */ export function computeTeacherMetrics( classes: readonly TeacherClass[], schedule: readonly ClassScheduleItem[], assignments: readonly HomeworkAssignmentListItem[], submissions: readonly HomeworkSubmissionListItem[], gradeTrends: readonly TeacherGradeTrendItem[], now: Date, ): TeacherDashboardMetrics { const todayWeekday = toWeekday(now) const classNameById = new Map(classes.map((c) => [c.id, c.name] as const)) const todayScheduleItems = filterTodaySchedule( schedule, todayWeekday, classNameById, ) as TeacherTodayScheduleItem[] const submittedSubmissions = submissions.filter((s) => Boolean(s.submittedAt)) const toGradeCount = submittedSubmissions.filter((s) => s.status === "submitted").length const submissionsToGrade = submittedSubmissions .filter((s) => s.status === "submitted") .sort( (a, b) => (a.submittedAt ? new Date(a.submittedAt).getTime() : 0) - (b.submittedAt ? new Date(b.submittedAt).getTime() : 0), ) .slice(0, 6) const activeAssignmentsCount = assignments.filter((a) => a.status === "published").length const totalTrendScore = gradeTrends.reduce((acc, curr) => acc + curr.averageScore, 0) const averageScore = gradeTrends.length > 0 ? totalTrendScore / gradeTrends.length : 0 const totalSubmissions = gradeTrends.reduce((acc, curr) => acc + curr.submissionCount, 0) const totalPotentialSubmissions = gradeTrends.reduce((acc, curr) => acc + curr.totalStudents, 0) const submissionRate = totalPotentialSubmissions > 0 ? (totalSubmissions / totalPotentialSubmissions) * 100 : 0 return { todayScheduleItems, toGradeCount, submissionsToGrade, activeAssignmentsCount, averageScore, submissionRate, } } /** * 根据当前小时返回问候语时段 key(morning / afternoon / evening)。 */ export function getGreetingKey(now: Date): "morning" | "afternoon" | "evening" { const hour = now.getHours() if (hour < 12) return "morning" if (hour < 18) return "afternoon" return "evening" } /** 重导出 TeacherDashboardData 便于 actions 使用 */ export type { TeacherDashboardData }