From ba34c09be41b3eb12f4b4d0353877e1899831cf6 Mon Sep 17 00:00:00 2001 From: wangziqi Date: Mon, 6 Jul 2026 17:39:16 +0800 Subject: [PATCH] feat(attendance): add single-record edit and delete APIs - Add PUT /attendance-records/:id to update status and remark - Add DELETE /attendance-records/:id to remove a record - Enforce campus scope on both operations - Log edit/delete actions via OperationLogsService --- .../src/attendance/attendance.controller.ts | 50 +++++++++++++++++++ .../src/attendance/attendance.service.ts | 39 +++++++++++++++ .../src/attendance/dto/attendance.dto.ts | 11 ++++ 3 files changed, 100 insertions(+) diff --git a/apps/server/src/attendance/attendance.controller.ts b/apps/server/src/attendance/attendance.controller.ts index b0c976d..eda8fe2 100644 --- a/apps/server/src/attendance/attendance.controller.ts +++ b/apps/server/src/attendance/attendance.controller.ts @@ -2,6 +2,8 @@ import { Controller, Get, Post, + Put, + Delete, Body, Param, Query, @@ -19,6 +21,7 @@ import { QueryDingRawDto, MatchDingRecordDto, AttendanceReportQueryDto, + UpdateAttendanceRecordDto, } from './dto/attendance.dto'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { OperationLogsService } from '../operation-logs/operation-logs.service'; @@ -117,6 +120,53 @@ export class AttendanceController { return this.service.findAll(query); } + // ── Update a single attendance record ── + @Put('attendance-records/:id') + @RequirePermission('attendance:edit') + async update( + @Param('id') id: string, + @Body() dto: UpdateAttendanceRecordDto, + @Request() req: any, + ) { + const { ipAddress, userAgent } = extractRequestInfo(req); + const result = await this.service.update(+id, dto); + await this.logService.log({ + userId: req.user?.id, + username: req.user?.username, + module: '考勤管理', + action: '编辑考勤记录', + targetId: +id, + targetType: 'attendanceRecord', + detail: `状态=${result.status}, 备注=${result.remark || ''}`, + ipAddress, + userAgent, + }); + return result; + } + + // ── Delete a single attendance record ── + @Delete('attendance-records/:id') + @RequirePermission('attendance:edit') + async remove( + @Param('id') id: string, + @Request() req: any, + ) { + const { ipAddress, userAgent } = extractRequestInfo(req); + const result = await this.service.remove(+id); + await this.logService.log({ + userId: req.user?.id, + username: req.user?.username, + module: '考勤管理', + action: '删除考勤记录', + targetId: +id, + targetType: 'attendanceRecord', + detail: `删除考勤记录 ${id}`, + ipAddress, + userAgent, + }); + return result; + } + // ── Get distinct classes with attendance records ── @Get('attendance-records/classes') @RequirePermission('attendance:view') diff --git a/apps/server/src/attendance/attendance.service.ts b/apps/server/src/attendance/attendance.service.ts index 1592437..9c4075c 100644 --- a/apps/server/src/attendance/attendance.service.ts +++ b/apps/server/src/attendance/attendance.service.ts @@ -14,6 +14,7 @@ import { QueryDingRawDto, MatchDingRecordDto, AttendanceReportQueryDto, + UpdateAttendanceRecordDto, } from './dto/attendance.dto'; @Injectable() @@ -314,6 +315,44 @@ export class AttendanceService { return qb.getMany(); } + // ── Update a single attendance record ── + async update(id: number, dto: UpdateAttendanceRecordDto) { + const record = await this.attendanceRepo.findOne({ where: { id } }); + if (!record) { + throw new NotFoundException(`AttendanceRecord ${id} not found`); + } + + const scopeIds = await this.scope.getScopeDepartmentIds(); + if (scopeIds && !scopeIds.includes(record.departmentId)) { + throw new NotFoundException(`AttendanceRecord ${id} not found`); + } + + if (dto.status !== undefined) { + record.status = dto.status; + } + if (dto.remark !== undefined) { + record.remark = dto.remark; + } + + return this.attendanceRepo.save(record); + } + + // ── Delete a single attendance record ── + async remove(id: number) { + const record = await this.attendanceRepo.findOne({ where: { id } }); + if (!record) { + throw new NotFoundException(`AttendanceRecord ${id} not found`); + } + + const scopeIds = await this.scope.getScopeDepartmentIds(); + if (scopeIds && !scopeIds.includes(record.departmentId)) { + throw new NotFoundException(`AttendanceRecord ${id} not found`); + } + + await this.attendanceRepo.remove(record); + return { deleted: true }; + } + // ── Class-based attendance report ── async getReport(query: AttendanceReportQueryDto) { 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 631da3b..db62fdd 100644 --- a/apps/server/src/attendance/dto/attendance.dto.ts +++ b/apps/server/src/attendance/dto/attendance.dto.ts @@ -126,6 +126,17 @@ export class MatchDingRecordDto { studentId: number; } +export class UpdateAttendanceRecordDto { + @IsOptional() + @IsString() + @IsIn(['present', 'late', 'absent', 'leave']) + status?: string; + + @IsOptional() + @IsString() + remark?: string; +} + export class AttendanceReportQueryDto { @IsOptional() @IsInt()