200 lines
5.7 KiB
TypeScript
200 lines
5.7 KiB
TypeScript
import dayjs from 'dayjs';
|
|
import type { AttendanceSummary } from './attendance-workspace';
|
|
import type { LessonAttendanceRecord } from './types';
|
|
|
|
export type { AttendanceSummary } from './attendance-workspace';
|
|
|
|
export const DEFAULT_ATTENDANCE_PERIODS = [
|
|
{ periodKey: 'morning_reading', label: '早自习', startTime: '07:30', endTime: '08:30', sortOrder: 1, enabled: true },
|
|
{ periodKey: 'morning', label: '早课', startTime: '09:00', endTime: '12:00', sortOrder: 2, enabled: true },
|
|
{ periodKey: 'afternoon', label: '晚课', startTime: '14:00', endTime: '17:00', sortOrder: 3, enabled: true },
|
|
{ periodKey: 'evening_study', label: '晚自习', startTime: '18:30', endTime: '21:00', sortOrder: 4, enabled: true },
|
|
];
|
|
|
|
export const STATUS_META: Record<
|
|
string,
|
|
{ label: string; color: string; className: string; short: string }
|
|
> = {
|
|
present: { label: '出勤', color: 'success', className: 'is-present', short: '勤' },
|
|
absent: { label: '缺勤', color: 'error', className: 'is-absent', short: '缺' },
|
|
leave: { label: '请假', color: 'processing', className: 'is-leave', short: '假' },
|
|
pending: { label: '待确认', color: 'default', className: 'is-pending', short: '待' },
|
|
};
|
|
|
|
export const ADMIN_CORRECTION_OPTIONS = [
|
|
{ value: 'present', label: '正常' },
|
|
{ value: 'leave', label: '请假' },
|
|
{ value: 'absent', label: '缺勤' },
|
|
];
|
|
|
|
export interface ClassTeacherOption {
|
|
userId: number;
|
|
username: string | null;
|
|
name: string | null;
|
|
roleType: string;
|
|
subject: string | null;
|
|
}
|
|
|
|
export interface ClassOption {
|
|
classId: number;
|
|
className: string;
|
|
teachers?: ClassTeacherOption[];
|
|
}
|
|
|
|
export type AttendanceRecordItem = LessonAttendanceRecord;
|
|
|
|
export interface HistoryScheduleOption {
|
|
id: number;
|
|
classId: number;
|
|
weekDay: number;
|
|
startTime: string;
|
|
endTime: string;
|
|
startDate: string;
|
|
endDate: string;
|
|
subject: string;
|
|
teacherId: number | null;
|
|
teacherName?: string | null;
|
|
teacherUsername?: string | null;
|
|
status: string;
|
|
}
|
|
|
|
export interface AlertItem {
|
|
studentId: number;
|
|
studentName: string;
|
|
studentNo: string;
|
|
className: string;
|
|
type: string;
|
|
count: number;
|
|
lastDate: string;
|
|
}
|
|
|
|
export interface AttendancePeriodConfigItem {
|
|
id?: number;
|
|
periodKey: string;
|
|
label: string;
|
|
startTime: string;
|
|
endTime: string;
|
|
sortOrder: number;
|
|
enabled: boolean;
|
|
}
|
|
|
|
export interface DingTalkSyncStatus {
|
|
lastPulledAt: string | null;
|
|
action: string | null;
|
|
username: string | null;
|
|
detail: string | null;
|
|
}
|
|
|
|
export const EMPTY_SUMMARY: AttendanceSummary = {
|
|
total: 0,
|
|
present: 0,
|
|
late: 0,
|
|
absent: 0,
|
|
leave: 0,
|
|
pending: 0,
|
|
};
|
|
|
|
export function displayAttendanceStatus(status?: string | null): string {
|
|
return status === 'pending' || !status ? 'absent' : status;
|
|
}
|
|
|
|
export function getTeacherDisplayName(teacher?: {
|
|
name?: string | null;
|
|
username?: string | null;
|
|
}): string {
|
|
const name = teacher?.name?.trim();
|
|
if (name) return name;
|
|
return teacher?.username?.trim() || '未设置';
|
|
}
|
|
|
|
export function formatTeacherNames(
|
|
teachers: readonly { name?: string | null; username?: string | null }[],
|
|
): string {
|
|
const names = [
|
|
...new Set(
|
|
teachers
|
|
.map((teacher) => getTeacherDisplayName(teacher))
|
|
.filter((name) => name && name !== '未设置'),
|
|
),
|
|
];
|
|
return names.length > 0 ? names.join('、') : '未设置';
|
|
}
|
|
|
|
export function AttendanceStatusTag({ status }: { status: string }) {
|
|
const displayStatus = displayAttendanceStatus(status);
|
|
const meta = STATUS_META[displayStatus] ?? {
|
|
label: displayStatus,
|
|
color: 'default',
|
|
className: 'is-absent',
|
|
short: '?',
|
|
};
|
|
return (
|
|
<span className={`attendance-status ${meta.className}`}>
|
|
<span className="attendance-status__dot" />
|
|
{meta.label}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
export interface AdminStudentPanel {
|
|
key: string;
|
|
studentId: number;
|
|
studentName: string;
|
|
studentNo: string;
|
|
className: string;
|
|
records: AttendanceRecordItem[];
|
|
statusBySession: Partial<Record<string, AttendanceRecordItem>>;
|
|
primaryStatus: string;
|
|
rate: number;
|
|
latestDate: string;
|
|
}
|
|
|
|
export const ADMIN_METRIC_META = [
|
|
{ key: 'all', label: '出勤率', short: '率' },
|
|
{ key: 'present', label: '正常', short: '正常' },
|
|
{ key: 'leave', label: '请假', short: '请假' },
|
|
{ key: 'absent', label: '缺勤', short: '缺勤' },
|
|
];
|
|
|
|
function pickPrimaryStatus(records: AttendanceRecordItem[]) {
|
|
const priority = ['absent', 'leave', 'present'];
|
|
return (
|
|
priority.find((item) =>
|
|
records.some((record) => displayAttendanceStatus(record.status) === item),
|
|
) || 'absent'
|
|
);
|
|
}
|
|
|
|
export function buildAdminStudentPanels(records: AttendanceRecordItem[]): AdminStudentPanel[] {
|
|
const map = new Map<number, AdminStudentPanel>();
|
|
for (const record of records) {
|
|
const current = map.get(record.studentId) ?? {
|
|
key: String(record.studentId),
|
|
studentId: record.studentId,
|
|
studentName: record.student?.name || '未知学生',
|
|
studentNo: record.student?.studentNo?.trim() || '',
|
|
className: record.class?.name || '未关联班级',
|
|
records: [],
|
|
statusBySession: {},
|
|
primaryStatus: 'absent',
|
|
rate: 0,
|
|
latestDate: record.attendanceDate,
|
|
};
|
|
current.records.push(record);
|
|
if (!current.statusBySession[record.session]) current.statusBySession[record.session] = record;
|
|
if (dayjs(record.attendanceDate).isAfter(dayjs(current.latestDate))) {
|
|
current.latestDate = record.attendanceDate;
|
|
}
|
|
map.set(record.studentId, current);
|
|
}
|
|
|
|
return Array.from(map.values()).map((item) => {
|
|
const checked = item.records.filter((record) => record.status === 'present').length;
|
|
return {
|
|
...item,
|
|
primaryStatus: pickPrimaryStatus(item.records),
|
|
rate: item.records.length > 0 ? Math.round((checked / item.records.length) * 100) : 0,
|
|
};
|
|
});
|
|
}
|