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:
45
src/modules/attendance/services/attendance-context.tsx
Normal file
45
src/modules/attendance/services/attendance-context.tsx
Normal 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
|
||||
}
|
||||
160
src/modules/attendance/services/attendance-data-service.ts
Normal file
160
src/modules/attendance/services/attendance-data-service.ts
Normal 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"
|
||||
157
src/modules/attendance/services/types.ts
Normal file
157
src/modules/attendance/services/types.ts
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user