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
This commit is contained in:
2026-07-06 17:39:16 +08:00
parent 85872d6564
commit ba34c09be4
3 changed files with 100 additions and 0 deletions

View File

@@ -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')

View File

@@ -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');

View File

@@ -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()