feat: 考勤模块重构与钉钉考勤同步
This commit is contained in:
199
apps/admin/src/pages/Attendance/AttendanceAdmin.helpers.tsx
Normal file
199
apps/admin/src/pages/Attendance/AttendanceAdmin.helpers.tsx
Normal file
@@ -0,0 +1,199 @@
|
||||
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,
|
||||
};
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user