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, @InjectRepository(DingAttendanceRaw) private dingRawRepo: Repository, ) {} // ── 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); } }