feat: P2-12 Teacher profile/workspace + P2-13 Multi-class comparison
P2-12 Teacher Management: - Add profile JSON field (subjects, joinedAt, qualifications) to User entity - Add GET/PUT /rbac/users/:id/profile endpoints with UpdateProfileDto - Add GET /rbac/teacher-workspace endpoint returning assignedClasses, todaySchedules, and myStudents - Create /teacher-workspace page with tabs (My Classes, Today's Schedule, My Students) - Add route with PermissionRoute and menu item in MainLayout P2-13 Student Archive Multi-class: - Add GET /students/:id/compare-classes endpoint returning side-by-side enrollment data with per-class attendance statistics - Students module now includes ClassStudent and AttendanceRecord entities - No 2-enrollment cap existed in the codebase; student enrollments already return all records via class-student table
This commit is contained in:
@@ -280,4 +280,10 @@ export class StudentsController {
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@Get(':id/compare-classes')
|
||||
@RequirePermission('student:view')
|
||||
compareClasses(@Param('id') id: string) {
|
||||
return this.service.compareClasses(+id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Student } from '../entities/student.entity';
|
||||
import { ClassStudent } from '../entities/class-student.entity';
|
||||
import { AttendanceRecord } from '../entities/attendance-record.entity';
|
||||
import { StudentsService } from './students.service';
|
||||
import { StudentsController } from './students.controller';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Student])],
|
||||
imports: [TypeOrmModule.forFeature([Student, ClassStudent, AttendanceRecord])],
|
||||
controllers: [StudentsController],
|
||||
providers: [StudentsService],
|
||||
exports: [StudentsService],
|
||||
|
||||
@@ -2,11 +2,18 @@ import { Injectable, NotFoundException, BadRequestException } from '@nestjs/comm
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, Like, Not, In } from 'typeorm';
|
||||
import { Student } from '../entities/student.entity';
|
||||
import { ClassStudent } from '../entities/class-student.entity';
|
||||
import { AttendanceRecord } from '../entities/attendance-record.entity';
|
||||
import { CreateStudentDto, UpdateStudentDto } from './dto/student.dto';
|
||||
|
||||
@Injectable()
|
||||
export class StudentsService {
|
||||
constructor(@InjectRepository(Student) private repo: Repository<Student>) {}
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Student) private repo: Repository<Student>,
|
||||
@InjectRepository(ClassStudent) private classStudentRepo: Repository<ClassStudent>,
|
||||
@InjectRepository(AttendanceRecord) private attendanceRepo: Repository<AttendanceRecord>,
|
||||
) {}
|
||||
|
||||
async findAll(query?: { name?: string; status?: string; includeArchived?: boolean }) {
|
||||
const where: any = {};
|
||||
@@ -129,4 +136,61 @@ export class StudentsService {
|
||||
skipped,
|
||||
};
|
||||
}
|
||||
|
||||
async compareClasses(studentId: number) {
|
||||
const student = await this.repo.findOne({ where: { id: studentId } });
|
||||
if (!student) throw new NotFoundException('学生不存在');
|
||||
|
||||
// Get all class enrollments for this student
|
||||
const enrollments = await this.classStudentRepo.find({
|
||||
where: { studentId },
|
||||
relations: ['class'],
|
||||
});
|
||||
|
||||
if (enrollments.length === 0) {
|
||||
return { student, enrollments: [] };
|
||||
}
|
||||
|
||||
// For each enrollment, compute attendance stats
|
||||
const classIds = enrollments.map((e) => e.classId);
|
||||
const attendanceRecords = await this.attendanceRepo.find({
|
||||
where: { studentId, classId: In(classIds) },
|
||||
});
|
||||
|
||||
// Group attendance by class
|
||||
const attendanceByClass = new Map<number, AttendanceRecord[]>();
|
||||
for (const r of attendanceRecords) {
|
||||
const list = attendanceByClass.get(r.classId!) || [];
|
||||
list.push(r);
|
||||
attendanceByClass.set(r.classId!, list);
|
||||
}
|
||||
|
||||
const comparison = enrollments.map((e) => {
|
||||
const records = attendanceByClass.get(e.classId) || [];
|
||||
const total = records.length;
|
||||
const present = records.filter((r) => r.status === 'present' || r.status === '正常').length;
|
||||
const absent = records.filter((r) => r.status === 'absent' || r.status === '缺勤').length;
|
||||
const late = records.filter((r) => r.status === 'late' || r.status === '迟到').length;
|
||||
const leave = records.filter((r) => r.status === 'leave' || r.status === '请假').length;
|
||||
|
||||
return {
|
||||
classId: e.classId,
|
||||
className: e.class?.name || '',
|
||||
classType: e.class?.classType || '',
|
||||
joinDate: e.joinDate,
|
||||
leaveDate: e.leaveDate,
|
||||
status: e.status,
|
||||
attendanceStats: {
|
||||
total,
|
||||
present,
|
||||
absent,
|
||||
late,
|
||||
leave,
|
||||
rate: total > 0 ? Math.round((present / total) * 100) : 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return { student, enrollments: comparison };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user