Files
NextEdu/src/modules/attendance/warning-compute.ts
SpecialX 048fc1c386 feat(attendance): add correlation, trend, warnings, report print, and services
- Add attendance-grade-correlation-card and data-access-correlation, correlation-compute

- Add attendance-trend-chart and trend-compute for trend analysis

- Add attendance-warnings-card and warning-compute for attendance warnings

- Add attendance-report-print for printable reports

- Add class-comparison-card for class attendance comparison

- Add notifications and services directory
2026-07-03 10:24:16 +08:00

153 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type {
AttendanceWarning,
AttendanceWarningSeverity,
AttendanceWarningSummary,
} from "./types"
/**
* L-3预警阈值默认值当班级未配置规则时使用
*/
export const DEFAULT_ATTENDANCE_RATE_THRESHOLD = 90
export const DEFAULT_CONSECUTIVE_ABSENCE_THRESHOLD = 3
/**
* 出勤率预警的严重等级划分:
* - high低于阈值 10 个百分点以上
* - medium低于阈值 5-10 个百分点
* - low低于阈值 5 个百分点以内
*/
const rateSeverity = (current: number, threshold: number): AttendanceWarningSeverity => {
const diff = threshold - current
if (diff >= 10) return "high"
if (diff >= 5) return "medium"
return "low"
}
/**
* 连续缺勤预警的严重等级划分:
* - high连续缺勤 ≥ 阈值 + 2
* - medium连续缺勤 ≥ 阈值 + 1
* - low连续缺勤 = 阈值
*/
const absenceSeverity = (current: number, threshold: number): AttendanceWarningSeverity => {
if (current >= threshold + 2) return "high"
if (current >= threshold + 1) return "medium"
return "low"
}
/**
* 从按日期升序的状态列表中计算最长连续缺勤段。
* 缺勤定义为 status === "absent"(不含 late/early_leave 等其他异常状态)。
* 返回 { maxStreak, lastStreakDates }:最长连续段及其日期列表。
*/
export const computeConsecutiveAbsence = (
records: { date: string; status: string }[]
): { maxStreak: number; lastStreakDates: string[] } => {
if (records.length === 0) return { maxStreak: 0, lastStreakDates: [] }
// 按日期升序排序
const sorted = [...records].sort((a, b) => a.date.localeCompare(b.date))
let maxStreak = 0
let currentStreak = 0
let currentStreakStart = 0
let lastMaxStart = 0
for (let i = 0; i < sorted.length; i++) {
if (sorted[i].status === "absent") {
if (currentStreak === 0) currentStreakStart = i
currentStreak += 1
if (currentStreak > maxStreak) {
maxStreak = currentStreak
lastMaxStart = currentStreakStart
}
} else {
currentStreak = 0
}
}
const lastStreakDates = maxStreak > 0
? sorted.slice(lastMaxStart, lastMaxStart + maxStreak).map((r) => r.date)
: []
return { maxStreak, lastStreakDates }
}
/**
* L-3根据学生出勤率统计和阈值生成预警列表纯函数便于测试
*
* @param studentStats 学生列表,每项含 studentId/studentName/total/present 和按日期升序的记录
* @param attendanceRateThreshold 出勤率阈值(百分比)
* @param consecutiveAbsenceThreshold 连续缺勤阈值(次)
*/
export const computeAttendanceWarnings = (
studentStats: {
studentId: string
studentName: string
total: number
present: number
records: { date: string; status: string }[]
}[],
attendanceRateThreshold: number,
consecutiveAbsenceThreshold: number
): AttendanceWarning[] => {
const warnings: AttendanceWarning[] = []
for (const s of studentStats) {
// 跳过无记录的学生(避免除零,且无记录不触发预警)
if (s.total === 0) continue
const presentRate = Math.round((s.present / s.total) * 10000) / 100
// 出勤率低于阈值
if (presentRate < attendanceRateThreshold) {
warnings.push({
studentId: s.studentId,
studentName: s.studentName,
type: "low_attendance_rate",
severity: rateSeverity(presentRate, attendanceRateThreshold),
currentValue: presentRate,
threshold: attendanceRateThreshold,
relatedDates: [],
})
}
// 连续缺勤达到阈值
const { maxStreak, lastStreakDates } = computeConsecutiveAbsence(s.records)
if (maxStreak >= consecutiveAbsenceThreshold) {
warnings.push({
studentId: s.studentId,
studentName: s.studentName,
type: "consecutive_absence",
severity: absenceSeverity(maxStreak, consecutiveAbsenceThreshold),
currentValue: maxStreak,
threshold: consecutiveAbsenceThreshold,
relatedDates: lastStreakDates,
})
}
}
// 严重等级排序high → medium → low
const severityOrder: Record<AttendanceWarningSeverity, number> = {
high: 0,
medium: 1,
low: 2,
}
warnings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity])
return warnings
}
/**
* 构造空的预警汇总(当班级无数据或无规则时使用)。
*/
export const createEmptyWarningSummary = (
classId: string,
className: string
): AttendanceWarningSummary => ({
classId,
className,
attendanceRateThreshold: DEFAULT_ATTENDANCE_RATE_THRESHOLD,
consecutiveAbsenceThreshold: DEFAULT_CONSECUTIVE_ABSENCE_THRESHOLD,
warnings: [],
})