From 693ba5d3b894a9bb41238fff7cfe8fdb86ed9b27 Mon Sep 17 00:00:00 2001 From: wangziqi Date: Mon, 6 Jul 2026 18:10:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20complete=20remaining=20PRD=20tasks=20?= =?UTF-8?q?=E2=80=94=20RBAC=20nodes=20and=20staff=20split,=20schedule=20mo?= =?UTF-8?q?nth=20view,=20auto-generate=20attendance=20from=20schedules,=20?= =?UTF-8?q?plus=20fix=20TypeORM=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/admin/src/pages/Schedules/index.tsx | 268 +++++++++++++++--- .../src/archive/archive-report.service.ts | 6 +- .../src/attendance/attendance.controller.ts | 23 ++ .../src/attendance/attendance.module.ts | 4 +- .../src/attendance/attendance.service.spec.ts | 2 +- .../src/attendance/attendance.service.ts | 118 +++++++- .../src/attendance/dto/attendance.dto.ts | 30 ++ apps/server/src/common/campus-scope.ts | 4 +- .../src/integration/dingtalk.service.ts | 2 +- apps/server/src/integration/wecom.service.ts | 2 +- apps/server/src/rbac/rbac.service.ts | 32 +++ apps/server/src/students/students.service.ts | 4 +- 12 files changed, 447 insertions(+), 48 deletions(-) diff --git a/apps/admin/src/pages/Schedules/index.tsx b/apps/admin/src/pages/Schedules/index.tsx index 86606da..7c66dae 100644 --- a/apps/admin/src/pages/Schedules/index.tsx +++ b/apps/admin/src/pages/Schedules/index.tsx @@ -1,7 +1,8 @@ import React, { useEffect, useState, useMemo, useCallback } from 'react'; import { Card, Button, Select, Modal, Form, Input, DatePicker, TimePicker, - Popconfirm, message, Space, Spin, Empty, Tag, Tooltip, + Popconfirm, message, Space, Spin, Empty, Tag, Tooltip, Segmented, + Badge, } from 'antd'; import { CalendarOutlined, @@ -61,8 +62,12 @@ const WEEKDAY_NUMBERS = [1, 2, 3, 4, 5, 6, 7]; // ---- Component ---- const SchedulesPage: React.FC = () => { - // Week navigation - const [weekStart, setWeekStart] = useState(() => dayjs().weekday(1).startOf('day')); + // View mode and navigation + const [viewMode, setViewMode] = useState<'week' | 'month'>('week'); + const [viewDate, setViewDate] = useState(() => dayjs().weekday(1).startOf('day')); + + // Modal date selection (month view) + const [selectedDate, setSelectedDate] = useState(null); // Data const [classrooms, setClassrooms] = useState([]); @@ -85,12 +90,48 @@ const SchedulesPage: React.FC = () => { const [submitting, setSubmitting] = useState(false); const [form] = Form.useForm(); - // Derived week info + // Derived week/month info + const weekStart = useMemo(() => viewDate.weekday(1).startOf('day'), [viewDate]); + const monthStart = useMemo(() => viewDate.startOf('month'), [viewDate]); const weekEnd = useMemo(() => weekStart.add(6, 'day'), [weekStart]); const weekNum = useMemo(() => weekStart.week(), [weekStart]); const weekYear = useMemo(() => weekStart.year(), [weekStart]); - const startDateStr = useMemo(() => weekStart.format('YYYY-MM-DD'), [weekStart]); - const endDateStr = useMemo(() => weekEnd.format('YYYY-MM-DD'), [weekEnd]); + const calendarDays = useMemo(() => { + const monthEnd = monthStart.endOf('month'); + const startOffset = (monthStart.day() + 6) % 7; + const endOffset = (7 - monthEnd.day()) % 7; + const start = monthStart.subtract(startOffset, 'day'); + const end = monthEnd.add(endOffset, 'day'); + const totalDays = end.diff(start, 'day') + 1; + const days: Dayjs[] = []; + for (let i = 0; i < totalDays; i++) { + days.push(start.add(i, 'day')); + } + return days; + }, [monthStart]); + + const weeks = useMemo(() => { + const result: Dayjs[][] = []; + for (let i = 0; i < calendarDays.length; i += 7) { + result.push(calendarDays.slice(i, i + 7)); + } + return result; + }, [calendarDays]); + + const startDateStr = useMemo(() => { + if (viewMode === 'month') { + return calendarDays[0].format('YYYY-MM-DD'); + } + return weekStart.format('YYYY-MM-DD'); + }, [viewMode, weekStart, calendarDays]); + + const endDateStr = useMemo(() => { + if (viewMode === 'month') { + return calendarDays[calendarDays.length - 1].format('YYYY-MM-DD'); + } + return weekEnd.format('YYYY-MM-DD'); + }, [viewMode, weekEnd, calendarDays]); + // ---- Data fetching ---- @@ -159,10 +200,30 @@ const SchedulesPage: React.FC = () => { return filtered; }, [matrix, filterClassId]); + const monthScheduleMap = useMemo(() => { + const map: Record = {}; + for (const day of calendarDays) { + const wd = day.day() === 0 ? 7 : day.day(); + const dateStr = day.format('YYYY-MM-DD'); + const result: ClassScheduleItem[] = []; + for (const classroom of filteredClassrooms) { + const daySchedules = displayMatrix[classroom.id]?.[wd] || []; + for (const s of daySchedules) { + if (dateStr >= s.startDate && dateStr <= s.endDate) { + result.push(s); + } + } + } + map[dateStr] = result; + } + return map; + }, [calendarDays, displayMatrix, filteredClassrooms]); + // ---- Cell click handlers ---- const handleCellClick = (classroomId: number, weekDay: number) => { const schedules = displayMatrix[classroomId]?.[weekDay] || []; setSelectedCell({ classroomId, weekDay }); + setSelectedDate(null); if (schedules.length > 0) { setSelectedSchedules(schedules); @@ -175,6 +236,31 @@ const SchedulesPage: React.FC = () => { } }; + const getSchedulesForDate = (date: Dayjs): ClassScheduleItem[] => { + const wd = date.day() === 0 ? 7 : date.day(); + const dateStr = date.format('YYYY-MM-DD'); + const result: ClassScheduleItem[] = []; + for (const classroom of filteredClassrooms) { + const daySchedules = displayMatrix[classroom.id]?.[wd] || []; + for (const s of daySchedules) { + if (dateStr >= s.startDate && dateStr <= s.endDate) { + result.push(s); + } + } + } + return result; + }; + + const handleDateClick = (date: Dayjs) => { + const dateKey = date.format('YYYY-MM-DD'); + const schedules = monthScheduleMap[dateKey] || getSchedulesForDate(date); + setSelectedDate(date); + setSelectedCell(null); + setSelectedSchedules(schedules); + setModalMode('detail'); + setModalOpen(true); + }; + // ---- Create schedule ---- const handleSubmit = async () => { @@ -214,13 +300,11 @@ const SchedulesPage: React.FC = () => { try { await api.delete(`/class-schedules/${id}`); message.success('排课已删除'); - // Refresh the cell's schedules - if (selectedCell) { - const remaining = selectedSchedules.filter((s) => s.id !== id); - setSelectedSchedules(remaining); - if (remaining.length === 0) { - setModalOpen(false); - } + // Refresh the displayed schedules + const remaining = selectedSchedules.filter((s) => s.id !== id); + setSelectedSchedules(remaining); + if (remaining.length === 0) { + setModalOpen(false); } fetchData(); } catch (e: unknown) { @@ -273,24 +357,57 @@ const SchedulesPage: React.FC = () => {

排课管理

- - - {weekYear} W{weekNum} - - ({startDateStr} ~ {endDateStr}) - - - + { + setViewMode(v as 'week' | 'month'); + setSelectedDate(null); + }} + options={[ + { label: '周视图', value: 'week' }, + { label: '月视图', value: 'month' }, + ]} + /> + {viewMode === 'week' ? ( + <> + + + {weekYear} W{weekNum} + + ({startDateStr} ~ {endDateStr}) + + + + + ) : ( + <> + + + {monthStart.format('YYYY年 M月')} + + + + )} @@ -322,7 +439,7 @@ const SchedulesPage: React.FC = () => { {classrooms.length === 0 ? ( - ) : ( + ) : (viewMode === 'week' ? (
{
- )} + ) : ( +
+ + + + {['周一', '周二', '周三', '周四', '周五', '周六', '周日'].map((d) => ( + + ))} + + + + {weeks.map((week, wi) => ( + + {week.map((day, di) => { + const isCurrentMonth = day.month() === monthStart.month(); + const dateKey = day.format('YYYY-MM-DD'); + const daySchedules = monthScheduleMap[dateKey] || []; + const count = daySchedules.length; + return ( + + ); + })} + + ))} + +
+ {d} +
handleDateClick(day)} + style={{ + padding: '6px 8px', + border: '1px solid #f0f0f0', + verticalAlign: 'top', + cursor: 'pointer', + height: 90, + background: isCurrentMonth ? '#fff' : '#fafafa', + transition: 'background 0.15s', + }} + onMouseEnter={(e) => { + (e.currentTarget as HTMLElement).style.background = isCurrentMonth ? '#f0f5ff' : '#f0f0f0'; + }} + onMouseLeave={(e) => { + (e.currentTarget as HTMLElement).style.background = isCurrentMonth ? '' : '#fafafa'; + }} + > +
+ {day.date()} +
+ {count > 0 && ( + + )} +
+
+ ))}
{/* Modal */} @@ -463,7 +661,9 @@ const SchedulesPage: React.FC = () => { title={ modalMode === 'create' ? `新增排课 — ${selectedClassroom?.name || ''} · ${selectedCell ? WEEKDAYS[selectedCell.weekDay - 1] : ''}` - : `排课详情 — ${selectedClassroom?.name || ''} · ${selectedCell ? WEEKDAYS[selectedCell.weekDay - 1] : ''}` + : selectedDate + ? `排课详情 — ${selectedDate.format('YYYY-MM-DD')} ${WEEKDAYS[selectedDate.day() === 0 ? 6 : selectedDate.day() - 1]}` + : `排课详情 — ${selectedClassroom?.name || ''} · ${selectedCell ? WEEKDAYS[selectedCell.weekDay - 1] : ''}` } open={modalOpen} onCancel={() => setModalOpen(false)} diff --git a/apps/server/src/archive/archive-report.service.ts b/apps/server/src/archive/archive-report.service.ts index a5721fb..6702639 100644 --- a/apps/server/src/archive/archive-report.service.ts +++ b/apps/server/src/archive/archive-report.service.ts @@ -442,8 +442,8 @@ ${this.buildLearningAndResult(learnings, result, now)} const sortedExams = [...cultureExams].filter((e) => e.score != null); let improvement = '—'; if (sortedExams.length >= 2) { - const first = sortedExams[0].score!; - const last = sortedExams[sortedExams.length - 1].score!; + const first = sortedExams[0].score; + const last = sortedExams[sortedExams.length - 1].score; improvement = (last - first).toFixed(1); } @@ -538,7 +538,7 @@ ${this.buildLearningAndResult(learnings, result, now)} const cultureExams = exams.filter((e) => e.score != null); if (cultureExams.length === 0) return ''; - const scores = cultureExams.map((e) => e.score!); + const scores = cultureExams.map((e) => e.score); const labels = cultureExams.map((e) => { const d = e.examDate || '-'; return d.length > 7 ? d.slice(5) : d; diff --git a/apps/server/src/attendance/attendance.controller.ts b/apps/server/src/attendance/attendance.controller.ts index eda8fe2..cae0244 100644 --- a/apps/server/src/attendance/attendance.controller.ts +++ b/apps/server/src/attendance/attendance.controller.ts @@ -22,6 +22,7 @@ import { MatchDingRecordDto, AttendanceReportQueryDto, UpdateAttendanceRecordDto, + GenerateFromSchedulesDto, } from './dto/attendance.dto'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { OperationLogsService } from '../operation-logs/operation-logs.service'; @@ -58,6 +59,28 @@ export class AttendanceController { return result; } + // ── Generate attendance records from schedules (with optional date range) ── + @Post('attendance-records/generate-from-schedules') + @RequirePermission('attendance:create') + async generateFromSchedules( + @Body() dto: GenerateFromSchedulesDto, + @Request() req: any, + ) { + const { ipAddress, userAgent } = extractRequestInfo(req); + const result = await this.service.generateFromSchedules(dto); + await this.logService.log({ + userId: req.user?.id, + username: req.user?.username, + module: '考勤管理', + action: '按课表生成考勤', + detail: `班级 ${dto.classId}, 共 ${result.count} 条`, + ipAddress, + userAgent, + }); + return result; + } + + // ── Export attendance records ── @Get('attendance-records/export') @RequirePermission('attendance:export') diff --git a/apps/server/src/attendance/attendance.module.ts b/apps/server/src/attendance/attendance.module.ts index 199bcfa..d47d1a1 100644 --- a/apps/server/src/attendance/attendance.module.ts +++ b/apps/server/src/attendance/attendance.module.ts @@ -1,13 +1,13 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { AttendanceRecord, DingAttendanceRaw, Student, Class } from '../entities'; +import { AttendanceRecord, DingAttendanceRaw, Student, Class, ClassSchedule, ClassStudent } from '../entities'; import { AttendanceService } from './attendance.service'; import { AttendanceController } from './attendance.controller'; import { OperationLogsModule } from '../operation-logs/operation-logs.module'; import { CommonModule } from '../common/common.module'; @Module({ imports: [ - TypeOrmModule.forFeature([AttendanceRecord, DingAttendanceRaw, Student, Class]), + TypeOrmModule.forFeature([AttendanceRecord, DingAttendanceRaw, Student, Class, ClassSchedule, ClassStudent]), OperationLogsModule, CommonModule, ], diff --git a/apps/server/src/attendance/attendance.service.spec.ts b/apps/server/src/attendance/attendance.service.spec.ts index db0f496..a624023 100644 --- a/apps/server/src/attendance/attendance.service.spec.ts +++ b/apps/server/src/attendance/attendance.service.spec.ts @@ -25,7 +25,7 @@ describe('AttendanceService — batchCreate', () => { save: jest .fn() .mockImplementation((entities: AttendanceRecord[]) => { - const result = entities.map((e, i) => ({ ...e, id: i + 1 } as AttendanceRecord)); + const result = entities.map((e, i) => ({ ...e, id: i + 1 })); savedRecords.push(...result); return Promise.resolve(result); }), diff --git a/apps/server/src/attendance/attendance.service.ts b/apps/server/src/attendance/attendance.service.ts index 9c4075c..0e646c9 100644 --- a/apps/server/src/attendance/attendance.service.ts +++ b/apps/server/src/attendance/attendance.service.ts @@ -4,8 +4,8 @@ import { BadRequestException, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, In, Between } from 'typeorm'; -import { AttendanceRecord, DingAttendanceRaw, Class, Student } from '../entities'; +import { Repository, In, Between, LessThanOrEqual, MoreThanOrEqual } from 'typeorm'; +import { AttendanceRecord, DingAttendanceRaw, Class, Student, ClassSchedule, ClassStudent, ScheduleType } from '../entities'; import { CampusScope } from '../common/campus-scope'; import { BatchCreateAttendanceDto, @@ -15,6 +15,8 @@ import { MatchDingRecordDto, AttendanceReportQueryDto, UpdateAttendanceRecordDto, + GenerateAttendanceFromSchedulesDto, + GenerateFromSchedulesDto, } from './dto/attendance.dto'; @Injectable() @@ -28,6 +30,10 @@ export class AttendanceService { private classRepo: Repository, @InjectRepository(Student) private studentRepo: Repository, + @InjectRepository(ClassSchedule) + private scheduleRepo: Repository, + @InjectRepository(ClassStudent) + private classStudentRepo: Repository, private readonly scope: CampusScope, ) {} @@ -60,6 +66,114 @@ export class AttendanceService { return { count: saved.length, records: saved }; } + // ── Generate attendance records from class schedules ── + async generateAttendanceFromSchedules(dto: GenerateAttendanceFromSchedulesDto) { + const { classId, dateFrom, dateTo } = dto; + + if (dateFrom > dateTo) { + throw new BadRequestException('dateFrom must not be later than dateTo'); + } + + const cls = await this.classRepo.findOne({ where: { id: classId } }); + if (!cls) { + throw new NotFoundException(`Class ${classId} not found`); + } + + const schedules = await this.scheduleRepo.find({ + where: { + classId, + scheduleType: ScheduleType.INTERNAL, + status: 'active', + startDate: LessThanOrEqual(dateTo), + endDate: MoreThanOrEqual(dateFrom), + }, + }); + + const classStudents = await this.classStudentRepo.find({ + where: { classId, status: 'active' }, + relations: ['student'], + }); + + if (schedules.length === 0 || classStudents.length === 0) { + return { count: 0, records: [] }; + } + + const existingRecords = await this.attendanceRepo.find({ + where: { classId, attendanceDate: Between(dateFrom, dateTo) }, + }); + const existingKeys = new Set( + existingRecords.map((r) => `${r.studentId}|${r.attendanceDate}|${r.session}`), + ); + + const entities: AttendanceRecord[] = []; + const end = new Date(dateTo); + for (let d = new Date(dateFrom); d <= end; d.setDate(d.getDate() + 1)) { + const dateStr = d.toISOString().slice(0, 10); + const weekDay = d.getDay() === 0 ? 7 : d.getDay(); + + for (const sched of schedules) { + if (sched.weekDay !== weekDay) continue; + if (dateStr < sched.startDate || dateStr > sched.endDate) continue; + + const session = this.mapScheduleTimeToSession(sched.startTime); + for (const cs of classStudents) { + const key = `${cs.studentId}|${dateStr}|${session}`; + if (existingKeys.has(key)) continue; + + const entity = this.attendanceRepo.create({ + studentId: cs.studentId, + classId, + attendanceDate: dateStr, + session, + status: 'pending', + source: 'schedule', + }); + entity.departmentId = cs.student?.departmentId ?? cls.departmentId ?? undefined; + entities.push(entity); + existingKeys.add(key); + } + } + } + + const saved = await this.attendanceRepo.save(entities); + return { count: saved.length, records: saved }; + } + + // ── Generate attendance records from schedules (optional date range, defaults to current week) ── + async generateFromSchedules(dto: GenerateFromSchedulesDto): Promise<{ count: number; records: AttendanceRecord[] }> { + const { classId, startDate, endDate } = dto; + + // Default to current week (Monday–Sunday) + const now = new Date(); + const dayOfWeek = now.getDay(); + const mondayOffset = dayOfWeek === 0 ? -6 : 1 - dayOfWeek; + const monday = new Date(now); + monday.setDate(now.getDate() + mondayOffset); + monday.setHours(0, 0, 0, 0); + const sunday = new Date(monday); + sunday.setDate(monday.getDate() + 6); + sunday.setHours(23, 59, 59, 999); + + const dateFrom = startDate ?? monday.toISOString().slice(0, 10); + const dateTo = endDate ?? sunday.toISOString().slice(0, 10); + + return this.generateAttendanceFromSchedules({ + classId, + dateFrom, + dateTo, + }); + } + + + private mapScheduleTimeToSession(startTime: string): string { + const hour = parseInt(startTime.slice(0, 2), 10); + if (hour < 8) return 'morning_reading'; + if (hour < 12) return 'morning'; + if (hour < 17) return 'afternoon'; + if (hour < 20) return 'evening_study'; + return 'night_check'; + } + // ── Attendance summary ── async getSummary(query: AttendanceSummaryQueryDto) { const qb = this.attendanceRepo.createQueryBuilder('ar'); diff --git a/apps/server/src/attendance/dto/attendance.dto.ts b/apps/server/src/attendance/dto/attendance.dto.ts index db62fdd..d4fb758 100644 --- a/apps/server/src/attendance/dto/attendance.dto.ts +++ b/apps/server/src/attendance/dto/attendance.dto.ts @@ -151,3 +151,33 @@ export class AttendanceReportQueryDto { @IsDateString() dateTo?: string; } + +export class GenerateAttendanceFromSchedulesDto { + @IsInt() + @Type(() => Number) + @IsNotEmpty() + classId: number; + + @IsDateString() + @IsNotEmpty() + dateFrom: string; + + @IsDateString() + @IsNotEmpty() + dateTo: string; +} + +export class GenerateFromSchedulesDto { + @IsInt() + @Type(() => Number) + @IsNotEmpty() + classId: number; + + @IsOptional() + @IsDateString() + startDate?: string; + + @IsOptional() + @IsDateString() + endDate?: string; +} diff --git a/apps/server/src/common/campus-scope.ts b/apps/server/src/common/campus-scope.ts index 684f600..0fc918a 100644 --- a/apps/server/src/common/campus-scope.ts +++ b/apps/server/src/common/campus-scope.ts @@ -47,12 +47,12 @@ export class CampusScope { if (ids.length === 0) { // Non-super-admin with no scoping → match nothing, never leak unfiltered data if (!this.isSuperAdmin) { - return { ...where, departmentId: In([]) } as unknown as T; + return { ...where, departmentId: In([]) }; } return where; } - return { ...where, departmentId: In(ids) } as unknown as T; + return { ...where, departmentId: In(ids) }; } /** Returns department IDs for QueryBuilder .andWhere() usage. null = no filtering needed. */ diff --git a/apps/server/src/integration/dingtalk.service.ts b/apps/server/src/integration/dingtalk.service.ts index 8574fff..86e78e3 100644 --- a/apps/server/src/integration/dingtalk.service.ts +++ b/apps/server/src/integration/dingtalk.service.ts @@ -123,7 +123,7 @@ export class DingTalkService { name: dd.name, source: 'dingtalk', sourceId, - parentSourceId: (dd.parent_id ? String(dd.parent_id) : undefined) as any, + parentSourceId: (dd.parent_id ? String(dd.parent_id) : undefined), type: 'department', }); deptCount++; diff --git a/apps/server/src/integration/wecom.service.ts b/apps/server/src/integration/wecom.service.ts index 2ff7299..e8b61ec 100644 --- a/apps/server/src/integration/wecom.service.ts +++ b/apps/server/src/integration/wecom.service.ts @@ -117,7 +117,7 @@ export class WeComService { name: wd.name, source: 'wecom', sourceId, - parentSourceId: (wd.parentid ? String(wd.parentid) : undefined) as any, + parentSourceId: (wd.parentid ? String(wd.parentid) : undefined), type: 'department', }); deptCount++; diff --git a/apps/server/src/rbac/rbac.service.ts b/apps/server/src/rbac/rbac.service.ts index d864239..e4dc713 100644 --- a/apps/server/src/rbac/rbac.service.ts +++ b/apps/server/src/rbac/rbac.service.ts @@ -69,6 +69,17 @@ const PRESET_PERMISSIONS: Array<{ code: string; name: string; group: string }> = { code: 'attendance:create', name: '新增考勤', group: 'attendance' }, { code: 'attendance:edit', name: '编辑考勤', group: 'attendance' }, { code: 'attendance:export', name: '导出考勤', group: 'attendance' }, + { code: 'attendance:generate', name: '按课表生成考勤', group: 'attendance' }, + { code: 'learning:create', name: '创建学习任务', group: 'learning' }, + { code: 'learning:edit', name: '编辑学习任务', group: 'learning' }, + { code: 'learning:delete', name: '删除学习任务', group: 'learning' }, + { code: 'exam:create', name: '创建考试', group: 'exam' }, + { code: 'exam:edit', name: '编辑考试', group: 'exam' }, + { code: 'exam:delete', name: '删除考试', group: 'exam' }, + { code: 'sync:trigger', name: '触发数据同步', group: 'sync' }, + { code: 'sync:read', name: '查看同步状态', group: 'sync' }, + { code: 'integration:trigger', name: '触发集成', group: 'integration' }, + { code: 'integration:read', name: '查看集成状态', group: 'integration' }, ]; const PRESET_ROLES: Array<{ @@ -120,6 +131,27 @@ const PRESET_ROLES: Array<{ isSystem: true, permissionGroups: ['classroom', 'rental', 'tenant'], }, + { + name: '财务', + code: 'finance', + description: '管理费用、账单与押金', + isSystem: true, + permissionGroups: ['expense', 'bill', 'deposit', 'dashboard'], + }, + { + name: '宿管', + code: 'dorm_manager', + description: '管理宿舍入住与宿舍信息', + isSystem: true, + permissionGroups: ['student', 'room', 'occupancy', 'dashboard'], + }, + { + name: '教务', + code: 'academic', + description: '管理班级、排课、考勤、学习与考试', + isSystem: true, + permissionGroups: ['class', 'schedule', 'attendance', 'classroom', 'learning', 'exam', 'dashboard'], + }, ]; @Injectable() diff --git a/apps/server/src/students/students.service.ts b/apps/server/src/students/students.service.ts index 554b2a6..6ff9d73 100644 --- a/apps/server/src/students/students.service.ts +++ b/apps/server/src/students/students.service.ts @@ -177,9 +177,9 @@ export class StudentsService { // Group attendance by class const attendanceByClass = new Map(); for (const r of attendanceRecords) { - const list = attendanceByClass.get(r.classId!) || []; + const list = attendanceByClass.get(r.classId) || []; list.push(r); - attendanceByClass.set(r.classId!, list); + attendanceByClass.set(r.classId, list); } const comparison = enrollments.map((e) => {