- 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
153 lines
4.6 KiB
TypeScript
153 lines
4.6 KiB
TypeScript
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: [],
|
||
})
|