feat(classes): add schedule and attendance-summary endpoints + frontend tabs

- Add GET /classes/:id/schedule returning ClassSchedule list with classroom name
- Add GET /classes/:id/attendance-summary returning attendance/absence/late rates
- Add 课表 and 出勤汇总 tabs to Classes detail page
- Register ClassSchedule and AttendanceRecord in ClassesModule
This commit is contained in:
2026-07-06 17:48:19 +08:00
parent b837e9b145
commit 5ea49d0a9e
5 changed files with 250 additions and 6 deletions

View File

@@ -19,6 +19,8 @@ import {
QueryClassDto,
AddStudentsDto,
AddTeacherDto,
QueryClassScheduleDto,
QueryClassAttendanceSummaryDto,
} from './dto/class.dto';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { OperationLogsService } from '../operation-logs/operation-logs.service';
@@ -49,6 +51,21 @@ export class ClassesController {
return this.service.findOne(+id);
}
@Get(':id/schedule')
@RequirePermission('class:view')
getSchedule(@Param('id') id: string, @Query() query: QueryClassScheduleDto) {
return this.service.getSchedule(+id, query);
}
@Get(':id/attendance-summary')
@RequirePermission('class:view')
getAttendanceSummary(
@Param('id') id: string,
@Query() query: QueryClassAttendanceSummaryDto,
) {
return this.service.getAttendanceSummary(+id, query);
}
@Post()
@RequirePermission('class:create')
async create(@Body() dto: CreateClassDto, @Request() req: any) {

View File

@@ -1,14 +1,14 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CommonModule } from '../common/common.module';
import { Class, ClassStudent, ClassTeacher } from '../entities';
import { Class, ClassStudent, ClassTeacher, ClassSchedule, AttendanceRecord } from '../entities';
import { ClassesService } from './classes.service';
import { ClassesController } from './classes.controller';
import { OperationLogsModule } from '../operation-logs/operation-logs.module';
import { NotificationsModule } from '../notifications/notifications.module';
@Module({
imports: [TypeOrmModule.forFeature([Class, ClassStudent, ClassTeacher]), OperationLogsModule, NotificationsModule, CommonModule],
imports: [TypeOrmModule.forFeature([Class, ClassStudent, ClassTeacher, ClassSchedule, AttendanceRecord]), OperationLogsModule, NotificationsModule, CommonModule],
controllers: [ClassesController],
providers: [ClassesService],
exports: [ClassesService],

View File

@@ -1,8 +1,8 @@
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, In, Like } from 'typeorm';
import { Class, ClassStudent, ClassTeacher } from '../entities';
import { CreateClassDto, UpdateClassDto, QueryClassDto, AddTeacherDto } from './dto/class.dto';
import { Class, ClassStudent, ClassTeacher, ClassSchedule, AttendanceRecord, Classroom } from '../entities';
import { CreateClassDto, UpdateClassDto, QueryClassDto, AddTeacherDto, QueryClassScheduleDto, QueryClassAttendanceSummaryDto } from './dto/class.dto';
import { CampusScope } from '../common/campus-scope';
interface RawStudentCount {
@@ -19,6 +19,10 @@ export class ClassesService {
private classStudentRepo: Repository<ClassStudent>,
@InjectRepository(ClassTeacher)
private classTeacherRepo: Repository<ClassTeacher>,
@InjectRepository(ClassSchedule)
private scheduleRepo: Repository<ClassSchedule>,
@InjectRepository(AttendanceRecord)
private attendanceRepo: Repository<AttendanceRecord>,
private readonly scope: CampusScope,
) {}
@@ -118,7 +122,7 @@ export class ClassesService {
async update(id: number, dto: UpdateClassDto) {
const cls = await this.classRepo.findOne({ where: { id } });
if (!cls) throw new NotFoundException('班级不存在');
await this.classRepo.update(id, dto as Record<string, unknown>);
await this.classRepo.update(id, dto);
return this.findOne(id);
}
@@ -196,4 +200,61 @@ export class ClassesService {
await this.classRepo.update(classId, updates);
}
}
async getSchedule(classId: number, query: QueryClassScheduleDto) {
const qb = this.scheduleRepo
.createQueryBuilder('cs')
.leftJoinAndSelect('cs.classroom', 'classroom')
.where('cs.classId = :classId', { classId });
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
.orderBy('cs.weekDay', 'ASC')
.addOrderBy('cs.startTime', 'ASC')
.getMany();
return schedules.map((s) => ({
...s,
classroomName: (s.classroom as Classroom | undefined)?.name || null,
}));
}
async getAttendanceSummary(classId: number, query: QueryClassAttendanceSummaryDto) {
const qb = this.attendanceRepo
.createQueryBuilder('ar')
.where('ar.classId = :classId', { classId });
if (query.startDate) {
qb.andWhere('ar.attendanceDate >= :startDate', { startDate: query.startDate });
}
if (query.endDate) {
qb.andWhere('ar.attendanceDate <= :endDate', { endDate: query.endDate });
}
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;
return {
total,
present,
late,
absent,
leave,
presentRate: total > 0 ? Number(((present / total) * 100).toFixed(1)) : 0,
absentRate: total > 0 ? Number(((absent / total) * 100).toFixed(1)) : 0,
lateRate: total > 0 ? Number(((late / total) * 100).toFixed(1)) : 0,
leaveRate: total > 0 ? Number(((leave / total) * 100).toFixed(1)) : 0,
};
}
}

View File

@@ -115,3 +115,19 @@ export class AddTeacherDto {
@IsOptional() @IsString()
subject?: string;
}
export class QueryClassScheduleDto {
@IsOptional() @IsDateString()
startDate?: string;
@IsOptional() @IsDateString()
endDate?: string;
}
export class QueryClassAttendanceSummaryDto {
@IsOptional() @IsDateString()
startDate?: string;
@IsOptional() @IsDateString()
endDate?: string;
}