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
This commit is contained in:
SpecialX
2026-07-03 10:24:16 +08:00
parent 7567f317e1
commit 048fc1c386
25 changed files with 3220 additions and 256 deletions

View File

@@ -0,0 +1,45 @@
"use client"
import { createContext, useContext, type ReactNode } from "react"
import type { AttendanceDataService } from "./types"
/**
* 考勤数据服务 Context。
*
* 客户端组件通过 `<AttendanceProvider service={...}>` 注入服务实现,
* 内部用 `useAttendanceService()` 读取,实现"消费方依赖接口而非具体实现"。
*
* 注意Server Component 无法使用 Context应直接以接口类型消费
* `createAttendanceDataService()` 返回的实现。
*/
const AttendanceServiceContext = createContext<AttendanceDataService | null>(null)
export function AttendanceProvider({
service,
children,
}: {
service: AttendanceDataService
children: ReactNode
}) {
return (
<AttendanceServiceContext.Provider value={service}>
{children}
</AttendanceServiceContext.Provider>
)
}
/**
* 读取注入的考勤数据服务。
* 若未在 `<AttendanceProvider>` 内调用,抛出明确错误以便定位。
*/
export function useAttendanceService(): AttendanceDataService {
const service = useContext(AttendanceServiceContext)
if (!service) {
throw new Error(
"useAttendanceService 必须在 <AttendanceProvider> 内调用;" +
"请在页面根节点注入 AttendanceProvider。",
)
}
return service
}

View File

@@ -0,0 +1,160 @@
import "server-only"
import type { DataScope } from "@/shared/types/permissions"
import {
batchCreateAttendanceRecords,
createAttendanceRecord,
deleteAttendanceRecord,
getAttendanceRecords,
getAttendanceRules,
getAttendanceStats,
getClassAttendanceForDate,
upsertAttendanceRules,
updateAttendanceRecord,
} from "../data-access"
import {
getClassAttendanceStats,
getStudentAttendanceSummary,
} from "../data-access-stats"
import type {
AttendanceDataService,
AttendanceReadService,
AttendanceWriteService,
DateRange,
} from "./types"
/**
* 基于真实 data-access 的考勤数据服务实现(读操作)。
* 各角色共用此实现,差异由调用方传入的 `scope` 隔离。
*/
class AttendanceReadServiceImpl implements AttendanceReadService {
async getStudentSummary(
studentId: string,
range?: DateRange,
recentLimit?: number,
) {
return getStudentAttendanceSummary(
studentId,
range?.startDate,
range?.endDate,
recentLimit,
)
}
async getRecentRecords(studentId: string, limit: number) {
const result = await getStudentAttendanceSummary(studentId, undefined, undefined, limit)
return result?.recentRecords ?? []
}
async getClassRecordsForDate(classId: string, date: string) {
return getClassAttendanceForDate(classId, date)
}
async getClassStats(classId: string, range?: DateRange) {
return getClassAttendanceStats(classId, range?.startDate, range?.endDate)
}
async queryRecords(params: Parameters<AttendanceReadService["queryRecords"]>[0]) {
return getAttendanceRecords(params)
}
async getOverviewStats(params: Parameters<AttendanceReadService["getOverviewStats"]>[0]) {
return getAttendanceStats(params)
}
async getRules(classId?: string) {
return getAttendanceRules(classId)
}
}
/**
* 基于真实 data-access 的考勤数据服务实现(写操作)。
* 仅教师/管理员角色使用;学生/家长不应调用。
*/
class AttendanceWriteServiceImpl implements AttendanceWriteService {
async recordAttendance(data: Parameters<AttendanceWriteService["recordAttendance"]>[0], recordedBy: string) {
return createAttendanceRecord(data, recordedBy)
}
async batchRecordAttendance(data: Parameters<AttendanceWriteService["batchRecordAttendance"]>[0], recordedBy: string) {
return batchCreateAttendanceRecords(data, recordedBy)
}
async updateAttendance(id: string, data: Parameters<AttendanceWriteService["updateAttendance"]>[1]) {
return updateAttendanceRecord(id, data)
}
async deleteAttendance(id: string) {
return deleteAttendanceRecord(id)
}
async saveRules(data: Parameters<AttendanceWriteService["saveRules"]>[0]) {
return upsertAttendanceRules(data)
}
}
/**
* 完整的考勤数据服务实现(读 + 写)。
* data-access 层函数的薄封装,统一接口契约。
*/
class AttendanceDataServiceImpl
implements AttendanceDataService
{
private readonly reader = new AttendanceReadServiceImpl()
private readonly writer = new AttendanceWriteServiceImpl()
getStudentSummary = this.reader.getStudentSummary.bind(this.reader)
getRecentRecords = this.reader.getRecentRecords.bind(this.reader)
getClassRecordsForDate = this.reader.getClassRecordsForDate.bind(this.reader)
getClassStats = this.reader.getClassStats.bind(this.reader)
queryRecords = this.reader.queryRecords.bind(this.reader)
getOverviewStats = this.reader.getOverviewStats.bind(this.reader)
getRules = this.reader.getRules.bind(this.reader)
recordAttendance = this.writer.recordAttendance.bind(this.writer)
batchRecordAttendance = this.writer.batchRecordAttendance.bind(this.writer)
updateAttendance = this.writer.updateAttendance.bind(this.writer)
deleteAttendance = this.writer.deleteAttendance.bind(this.writer)
saveRules = this.writer.saveRules.bind(this.writer)
}
/** 单例实例(无状态,可安全共享)。 */
const sharedService = new AttendanceDataServiceImpl()
/**
* 获取考勤数据服务实例。
*
* Server Component 调用此工厂以接口类型 `AttendanceDataService` 消费,
* 避免直接 import data-access 函数P1-12 修复)。
*
* @param _scope 当前用户的数据范围(用于文档化角色差异;实际过滤由 data-access 内 buildScopeFilter 执行)
*/
export function createAttendanceDataService(
_scope?: DataScope,
): AttendanceDataService {
// 当前实现无状态且共享scope 在具体 data-access 调用时传入。
// 未来若需按角色返回不同实现,可在此分支。
return sharedService
}
/**
* 获取只读考勤数据服务(学生/家长角色使用)。
* 返回的接口不包含写操作,编译期即阻止误用。
*/
export function createAttendanceReadService(
_scope?: DataScope,
): AttendanceReadService {
return sharedService
}
// 重新导出常用类型,便于消费方单点导入
export type {
AttendanceDataService,
AttendanceReadService,
AttendanceWriteService,
DateRange,
} from "./types"
// 暴露 getAttendanceRecordClassId归属校验所需不属于读写服务契约
export { getAttendanceRecordClassId } from "../data-access"

View File

@@ -0,0 +1,157 @@
/**
* 考勤模块服务接口抽象。
*
* 设计目标P1-12 / P2-6
* - 通过 TypeScript 接口抽象数据依赖,使用 React Context 注入数据服务。
* - 模块内部组件绝不直接 import 其他业务模块的 actions 或 data-access
* 只能通过注入的接口调用。
* - 不同角色的差异通过接口的不同实现来隔离。
* - 导出清晰的接口类型以便 mock可测试性
*
* 使用方式:
* - Server Component通过 `createAttendanceDataService(scope)` 获取实现,
* 以接口类型 `AttendanceDataService` 消费。
* - Client Component通过 `<AttendanceProvider service={...}>` 注入,
* 内部用 `useAttendanceService()` 读取。
*/
import type { DataScope } from "@/shared/types/permissions"
import type {
AttendanceListItem,
AttendanceQueryParams,
AttendanceRule,
AttendanceStats,
ClassAttendanceSummary,
PaginatedAttendanceResult,
StudentAttendanceSummary,
} from "../types"
import type {
AttendanceRuleInput,
BatchRecordAttendanceInput,
RecordAttendanceInput,
UpdateAttendanceInput,
} from "../schema"
/** 日期范围参数。 */
export interface DateRange {
startDate?: string
endDate?: string
}
/**
* 考勤数据读取服务接口(查询契约)。
* 消费方parent / student / 其他模块)依赖此接口,不依赖具体 data-access 实现。
*/
export interface AttendanceReadService {
/** 获取学生考勤汇总(统计 + 最近记录)。 */
getStudentSummary(
studentId: string,
range?: DateRange,
recentLimit?: number,
): Promise<StudentAttendanceSummary | null>
/** 获取学生最近考勤记录。 */
getRecentRecords(
studentId: string,
limit: number,
): Promise<AttendanceListItem[]>
/** 按日期获取班级考勤记录(点名场景)。 */
getClassRecordsForDate(
classId: string,
date: string,
): Promise<AttendanceListItem[]>
/** 获取班级考勤统计。 */
getClassStats(
classId: string,
range?: DateRange,
): Promise<ClassAttendanceSummary | null>
/** 分页查询考勤记录admin/teacher 列表场景)。 */
queryRecords(
params: AttendanceQueryParams & { scope: DataScope; currentUserId?: string },
): Promise<PaginatedAttendanceResult>
/** 获取考勤总览统计admin/teacher 仪表盘)。 */
getOverviewStats(params: {
scope: DataScope
currentUserId: string
classId?: string
date?: string
}): Promise<AttendanceStats>
/** 获取班级考勤规则。 */
getRules(classId?: string): Promise<AttendanceRule[]>
}
/**
* 考勤数据写入服务接口(命令契约)。
* 仅教师/管理员角色实现;学生/家长角色不实现此接口(无写权限)。
*/
export interface AttendanceWriteService {
/** 创建单条考勤记录。 */
recordAttendance(
data: RecordAttendanceInput,
recordedBy: string,
): Promise<string>
/** 批量创建考勤记录(点名)。 */
batchRecordAttendance(
data: BatchRecordAttendanceInput,
recordedBy: string,
): Promise<number>
/** 更新考勤记录。 */
updateAttendance(id: string, data: UpdateAttendanceInput): Promise<void>
/** 删除考勤记录。 */
deleteAttendance(id: string): Promise<void>
/** 保存upsert考勤规则。 */
saveRules(data: AttendanceRuleInput): Promise<string>
}
/**
* 完整的考勤数据服务接口(读 + 写)。
* 教师与管理员角色使用此接口;学生/家长仅使用 `AttendanceReadService`。
*/
export interface AttendanceDataService
extends AttendanceReadService,
AttendanceWriteService {}
/**
* 考勤 Repository 接口P2-6
* 仅供 data-access 层实现与测试 mock 使用,定义底层 CRUD 契约。
*/
export interface AttendanceRepository {
findRecords(
params: AttendanceQueryParams & { scope: DataScope; currentUserId?: string },
): Promise<PaginatedAttendanceResult>
findClassRecordsForDate(
classId: string,
date: string,
): Promise<AttendanceListItem[]>
findRecordClassId(id: string): Promise<string | null>
createRecord(
data: RecordAttendanceInput,
recordedBy: string,
): Promise<string>
batchCreateRecords(
data: BatchRecordAttendanceInput,
recordedBy: string,
): Promise<number>
updateRecord(id: string, data: UpdateAttendanceInput): Promise<void>
deleteRecord(id: string): Promise<void>
findRules(classId?: string): Promise<AttendanceRule[]>
upsertRules(data: AttendanceRuleInput): Promise<string>
}
/**
* 考勤纯计算服务接口(可测试性:纯逻辑与 UI 分离)。
* 实现见 `data-access-stats.ts` 的 `computeStats`。
*/
export interface AttendanceStatsCalculator {
/** 根据记录行计算统计。 */
computeStats(rows: { status: string }[]): AttendanceStats
}