import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, Between } from 'typeorm'; import { AttendanceRecord, ClassSchedule } from '../entities'; import type { AttendanceCalendarQueryDto } from './dto/attendance.dto'; @Injectable() export class AttendanceCalendarService { constructor( @InjectRepository(AttendanceRecord) private attendanceRepo: Repository, @InjectRepository(ClassSchedule) private scheduleRepo: Repository, ) {} 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 getWeekDayForDate(date: string): number { const day = new Date(`${date}T00:00:00+08:00`).getUTCDay(); return day === 0 ? 7 : day; } async getScheduleOptionsForAttendance(classId: number, date: string) { const weekDay = this.getWeekDayForDate(date); const { entities, raw } = await this.scheduleRepo .createQueryBuilder('cs') .leftJoin('cs.teacher', 'teacher') .addSelect('cs.id', 'scheduleIdForTeacherMap') .addSelect('teacher.username', 'teacherUsername') .addSelect('teacher.name', 'teacherName') .where('cs.classId = :classId', { classId }) .andWhere('cs.weekDay = :weekDay', { weekDay }) .andWhere('cs.startDate <= :date', { date }) .andWhere('cs.endDate >= :date', { date }) .andWhere('cs.status = :status', { status: 'active' }) .orderBy('cs.startTime', 'ASC') .addOrderBy('cs.subject', 'ASC') .getRawAndEntities(); const teacherByScheduleId = new Map( raw.map((row: { scheduleIdForTeacherMap: string; teacherName: string | null; teacherUsername: string | null }) => [ Number(row.scheduleIdForTeacherMap), { teacherName: row.teacherName || null, teacherUsername: row.teacherUsername || null, }, ]), ); return entities.map((schedule) => { const teacher = teacherByScheduleId.get(schedule.id) ?? { teacherName: null, teacherUsername: null, }; return { ...schedule, ...teacher }; }); } 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); 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()); } }