Files
gongxue-base/apps/server/src/attendance/attendance.service.ts

163 lines
4.8 KiB
TypeScript

import {
Injectable,
NotFoundException,
BadRequestException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, In, Between } from 'typeorm';
import { AttendanceRecord, DingAttendanceRaw } from '../entities';
import {
BatchCreateAttendanceDto,
AttendanceSummaryQueryDto,
AttendanceCalendarQueryDto,
QueryDingRawDto,
MatchDingRecordDto,
} from './dto/attendance.dto';
@Injectable()
export class AttendanceService {
constructor(
@InjectRepository(AttendanceRecord)
private attendanceRepo: Repository<AttendanceRecord>,
@InjectRepository(DingAttendanceRaw)
private dingRawRepo: Repository<DingAttendanceRaw>,
) {}
// ── Batch create attendance records ──
async batchCreate(dto: BatchCreateAttendanceDto) {
if (!dto.records || dto.records.length === 0) {
throw new BadRequestException('records array must not be empty');
}
const entities = dto.records.map((r) =>
this.attendanceRepo.create({
studentId: r.studentId,
classId: r.classId ?? undefined,
attendanceDate: r.attendanceDate,
session: r.session,
status: r.status,
remark: r.remark,
}),
);
const saved = await this.attendanceRepo.save(entities);
return { count: saved.length, records: saved };
}
// ── Attendance summary ──
async getSummary(query: AttendanceSummaryQueryDto) {
const qb = this.attendanceRepo.createQueryBuilder('ar');
if (query.classId) {
qb.andWhere('ar.classId = :classId', { classId: query.classId });
}
if (query.dateFrom) {
qb.andWhere('ar.attendanceDate >= :dateFrom', { dateFrom: query.dateFrom });
}
if (query.dateTo) {
qb.andWhere('ar.attendanceDate <= :dateTo', { dateTo: query.dateTo });
}
const rows = await qb.getMany();
const total = rows.length;
const present = rows.filter((r) => r.status === 'present').length;
const late = rows.filter((r) => r.status === 'late').length;
const absent = rows.filter((r) => r.status === 'absent').length;
const leave = rows.filter((r) => r.status === 'leave').length;
const presentRate = total > 0 ? Number(((present / total) * 100).toFixed(1)) : 0;
return { total, present, late, absent, leave, presentRate };
}
// ── Attendance calendar ──
async getCalendar(query: AttendanceCalendarQueryDto) {
const { classId, weekStart } = query;
if (!weekStart) {
// Default to the Monday of the current week
const now = new Date();
const day = now.getDay();
const diff = day === 0 ? -6 : 1 - day; // Monday offset
const monday = new Date(now);
monday.setDate(now.getDate() + diff);
const mondayStr = monday.toISOString().slice(0, 10);
return this.buildCalendar(classId, mondayStr);
}
return this.buildCalendar(classId, weekStart);
}
private async buildCalendar(classId: number, weekStart: string) {
// Compute weekEnd (Sunday = weekStart + 6 days)
const start = new Date(weekStart);
const end = new Date(start);
end.setDate(start.getDate() + 6);
const endStr = end.toISOString().slice(0, 10);
// Fetch attendance records for the week
const records = await this.attendanceRepo.find({
where: {
classId,
attendanceDate: Between(weekStart, endStr),
},
relations: ['student'],
order: { attendanceDate: 'ASC', session: 'ASC' },
});
// Group by studentId
const studentMap = new Map<
number,
{
studentId: number;
studentName: string;
days: Array<{ date: string; session: string; status: string }>;
}
>();
for (const r of records) {
if (!studentMap.has(r.studentId)) {
studentMap.set(r.studentId, {
studentId: r.studentId,
studentName: r.student?.name ?? `Student#${r.studentId}`,
days: [],
});
}
studentMap.get(r.studentId)!.days.push({
date: r.attendanceDate,
session: r.session,
status: r.status,
});
}
return Array.from(studentMap.values());
}
// ── DingAttendance raw records ──
async getDingRaw(query: QueryDingRawDto) {
const where: any = {};
if (query.matchStatus) {
where.matchStatus = query.matchStatus;
}
return this.dingRawRepo.find({
where,
relations: ['matchedStudent'],
order: { attendanceDate: 'DESC', checkInTime: 'ASC' },
});
}
// ── Match a dingtalk record to a student ──
async matchDingRecord(id: number, dto: MatchDingRecordDto) {
const record = await this.dingRawRepo.findOne({ where: { id } });
if (!record) {
throw new NotFoundException(`DingAttendanceRaw ${id} not found`);
}
record.matchedStudentId = dto.studentId;
record.matchStatus = '已匹配';
return this.dingRawRepo.save(record);
}
}