Files
gongxue-base/apps/admin/src/pages/Attendance/attendance-workspace.ts

162 lines
4.4 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.

export type AttendanceExperience = 'teacher' | 'admin';
export type SchedulePhase = 'upcoming' | 'ongoing' | 'ended';
import { getRoleDomains } from '../../auth/menu-policy';
export function getAttendanceExperience(
permissions: readonly string[],
roles: readonly string[],
): AttendanceExperience {
const domains = getRoleDomains(roles, permissions);
if (
permissions.includes('attendance:edit') ||
domains.has('academic') ||
domains.has('super')
) {
return 'admin';
}
return 'teacher';
}
function toMinuteOfDay(time: string): number {
const [hour = 0, minute = 0] = time.split(':').map(Number);
return hour * 60 + minute;
}
export function getSchedulePhase(
startTime: string,
endTime: string,
now = new Date(),
): SchedulePhase {
const current = now.getHours() * 60 + now.getMinutes();
if (current < toMinuteOfDay(startTime)) return 'upcoming';
if (current <= toMinuteOfDay(endTime)) return 'ongoing';
return 'ended';
}
export function canPullAttendance(phase: SchedulePhase): boolean {
return phase !== 'upcoming';
}
export interface AttendanceSummary {
total: number;
present: number;
late: number;
absent: number;
leave: number;
pending: number;
}
export function summarizeAttendance(records: readonly { status: string }[]): AttendanceSummary {
const summary: AttendanceSummary = {
total: records.length,
present: 0,
late: 0,
absent: 0,
leave: 0,
pending: 0,
};
for (const record of records) {
if (record.status in summary && record.status !== 'total') {
summary[record.status as Exclude<keyof AttendanceSummary, 'total'>] += 1;
}
}
return summary;
}
export interface LessonCheckinSummary {
total: number;
checkedIn: number;
notCheckedIn: number;
}
export function summarizeLessonCheckins(
records: readonly { status: string }[],
): LessonCheckinSummary {
const checkedIn = records.filter(
(record) => record.status === 'present' || record.status === 'late',
).length;
return {
total: records.length,
checkedIn,
notCheckedIn: records.length - checkedIn,
};
}
export type LessonAttendanceFilter = 'all' | 'checked_in' | 'not_checked_in';
export interface LessonAttendanceFilterRecord {
student: { name: string };
status: string;
}
export function filterLessonAttendanceRecords<T extends LessonAttendanceFilterRecord>(
records: readonly T[],
keyword: string,
filter: LessonAttendanceFilter,
): T[] {
const normalizedKeyword = keyword.trim().toLocaleLowerCase('zh-CN');
return records.filter((record) => {
const matchesKeyword =
!normalizedKeyword ||
record.student.name.toLocaleLowerCase('zh-CN').includes(normalizedKeyword);
if (!matchesKeyword || filter === 'all') return matchesKeyword;
const checkedIn = record.status === 'present' || record.status === 'late';
return filter === 'checked_in' ? checkedIn : !checkedIn;
});
}
export interface PunchDisplayRecord {
status: string;
source?: string;
punchTime?: string | null;
punchSource?: string | null;
punchDeviceName?: string | null;
punchDeviceId?: string | null;
}
export interface PunchDisplayInfo {
label: string;
machine: boolean;
detail?: string;
time?: string;
}
export function getPunchDisplayInfo(record: PunchDisplayRecord): PunchDisplayInfo | null {
if (record.status !== 'present' && record.status !== 'late') return null;
if (record.source === 'manual') return { label: '老师手动标记', machine: false };
const source = (record.punchSource || '').trim().toUpperCase();
const machine = ['ATM', 'ATTENDANCE_MACHINE', 'MACHINE', 'DEVICE'].some(
(value) => source === value || source.includes(value),
);
const label = machine
? '考勤机打卡'
: source === 'USER'
? '手机打卡'
: source.includes('BEACON') || source.includes('BLE')
? '蓝牙打卡'
: source.includes('WIFI')
? 'Wi-Fi 打卡'
: source.includes('APPROVE')
? '审批补卡'
: source
? `其他打卡(${record.punchSource}`
: '打卡来源未知';
const device = record.punchDeviceName?.trim();
const deviceId = record.punchDeviceId?.trim();
const detail = device
? deviceId && deviceId !== device
? `${device}${deviceId}`
: device
: deviceId || undefined;
return {
label,
machine,
detail,
time: record.punchTime || undefined,
};
}