import { Injectable, NotFoundException, ConflictException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { ClassSchedule } from '../entities'; import { CampusScope } from '../common/campus-scope'; import { CreateScheduleDto, UpdateScheduleDto, QueryScheduleDto, WeeklyViewQueryDto, } from './dto/schedule.dto'; @Injectable() export class SchedulesService { constructor( @InjectRepository(ClassSchedule) private readonly scheduleRepo: Repository, private readonly scope: CampusScope, ) {} async findAll(query: QueryScheduleDto) { const qb = this.scheduleRepo.createQueryBuilder('cs'); const scopeIds = await this.scope.getScopeDepartmentIds(); if (scopeIds) { qb.andWhere('cs.departmentId IN (:...scopeIds)', { scopeIds }); } if (query.classroomId) qb.andWhere('cs.classroomId = :classroomId', { classroomId: query.classroomId }); if (query.classId) qb.andWhere('cs.classId = :classId', { classId: query.classId }); if (query.weekDay) qb.andWhere('cs.weekDay = :weekDay', { weekDay: query.weekDay }); if (query.startDate) qb.andWhere('cs.startDate >= :startDate', { startDate: query.startDate }); if (query.endDate) qb.andWhere('cs.endDate <= :endDate', { endDate: query.endDate }); qb.orderBy('cs.weekDay', 'ASC') .addOrderBy('cs.startTime', 'ASC'); return qb.getMany(); } async findOne(id: number) { const schedule = await this.scheduleRepo.findOne({ where: { id } }); if (!schedule) throw new NotFoundException('排课记录不存在'); return schedule; } async create(dto: CreateScheduleDto) { await this.checkConflict(dto.classroomId, dto.weekDay, dto.startTime, dto.endTime, dto.startDate, dto.endDate); const schedule = this.scheduleRepo.create(dto); const saved = await this.scheduleRepo.save(schedule); return this.findOne(saved.id); } async update(id: number, dto: UpdateScheduleDto) { const existing = await this.scheduleRepo.findOne({ where: { id } }); if (!existing) throw new NotFoundException('排课记录不存在'); // If classroom, weekDay, or times are changing, check conflicts excluding self const classroomId = dto.classroomId ?? existing.classroomId; const weekDay = dto.weekDay ?? existing.weekDay; const startTime = dto.startTime ?? existing.startTime; const endTime = dto.endTime ?? existing.endTime; const startDate = dto.startDate ?? existing.startDate; const endDate = dto.endDate ?? existing.endDate; await this.checkConflict(classroomId, weekDay, startTime, endTime, startDate, endDate, id); await this.scheduleRepo.update(id, dto as Record); return this.findOne(id); } async remove(id: number) { const schedule = await this.scheduleRepo.findOne({ where: { id } }); if (!schedule) throw new NotFoundException('排课记录不存在'); await this.scheduleRepo.remove(schedule); return { success: true }; } async checkConflict( classroomId: number, weekDay: number, startTime: string, endTime: string, startDate: string, endDate: string, excludeId?: number, ) { const qb = this.scheduleRepo .createQueryBuilder('cs') .where('cs.classroomId = :classroomId', { classroomId }) .andWhere('cs.weekDay = :weekDay', { weekDay }) .andWhere('cs.status = :status', { status: 'active' }) .andWhere('cs.startTime < :endTime', { endTime }) .andWhere('cs.endTime > :startTime', { startTime }) .andWhere('cs.startDate <= :endDate', { endDate }) .andWhere('cs.endDate >= :startDate', { startDate }); if (excludeId) qb.andWhere('cs.id != :excludeId', { excludeId }); const conflicts = await qb.getMany(); if (conflicts.length > 0) { throw new ConflictException( `该时间段与已有排课冲突: ${conflicts.map((c) => `${c.subject}(${c.startTime}-${c.endTime})`).join(', ')}`, ); } return conflicts; } async getWeeklyView(query: WeeklyViewQueryDto) { const qb = this.scheduleRepo.createQueryBuilder('cs'); const scopeIds = await this.scope.getScopeDepartmentIds(); if (scopeIds) { qb.andWhere('cs.departmentId IN (:...scopeIds)', { scopeIds }); } if (query.classroomId) { qb.andWhere('cs.classroomId = :classroomId', { classroomId: query.classroomId }); } if (query.startDate) { qb.andWhere('cs.endDate >= :startDate', { startDate: query.startDate }); } if (query.endDate) { qb.andWhere('cs.startDate <= :endDate', { endDate: query.endDate }); } const schedules = await qb .andWhere('cs.status = :status', { status: 'active' }) .orderBy('cs.weekDay', 'ASC') .addOrderBy('cs.startTime', 'ASC') .getMany(); // Group by classroomId → weekDay const matrix: Record> = {}; for (const s of schedules) { if (!matrix[s.classroomId]) matrix[s.classroomId] = {}; if (!matrix[s.classroomId][s.weekDay]) matrix[s.classroomId][s.weekDay] = []; matrix[s.classroomId][s.weekDay].push(s); } return matrix; } async getClassroomOccupancy(classroomId: number, date?: string) { const qb = this.scheduleRepo .createQueryBuilder('cs') .where('cs.classroomId = :classroomId', { classroomId }) .andWhere('cs.status = :status', { status: 'active' }); if (date) { qb.andWhere('cs.startDate <= :date', { date }) .andWhere('cs.endDate >= :date', { date }); } return qb .orderBy('cs.weekDay', 'ASC') .addOrderBy('cs.startTime', 'ASC') .getMany(); } }