feat: refine admin forms, attendance and finance workflows
Squash merge PR #23. Included changes: - complete occupancy check-in required fields/default payload - improve responsive admin management pages - fix attendance edge cases and attendance period config - refine wallet/finance-related workflow handling Checks: - npm run typecheck -w apps/admin - npm run typecheck -w apps/server
This commit is contained in:
@@ -25,6 +25,7 @@ import {
|
||||
AttendanceSummaryQueryDto,
|
||||
AttendanceCalendarQueryDto,
|
||||
QueryAttendanceRecordsDto,
|
||||
AttendanceScheduleOptionsQueryDto,
|
||||
QueryDingRawDto,
|
||||
MatchDingRecordDto,
|
||||
AttendanceReportQueryDto,
|
||||
@@ -33,6 +34,8 @@ import {
|
||||
GenerateFromSchedulesDto,
|
||||
LessonAttendanceQueryDto,
|
||||
StartLessonAttendanceDto,
|
||||
SaveAttendancePeriodConfigsDto,
|
||||
RefreshDingTalkAttendanceDto,
|
||||
} from './dto/attendance.dto';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { OperationLogsService } from '../operation-logs/operation-logs.service';
|
||||
@@ -92,6 +95,45 @@ export class AttendanceController {
|
||||
return this.service.assertClassAccess(req.user.id, classId, this.canManageAllAttendance(req));
|
||||
}
|
||||
|
||||
|
||||
@Get('attendance-period-configs')
|
||||
@RequirePermission('attendance:view')
|
||||
getAttendancePeriodConfigs() {
|
||||
return this.service.getAttendancePeriodConfigs();
|
||||
}
|
||||
|
||||
@Put('attendance-period-configs')
|
||||
@RequirePermission('attendance:edit')
|
||||
async saveAttendancePeriodConfigs(
|
||||
@Body() dto: SaveAttendancePeriodConfigsDto,
|
||||
@Request() req: { user: RequestUser },
|
||||
) {
|
||||
const result = await this.service.saveAttendancePeriodConfigs(dto);
|
||||
await this.logService.log({
|
||||
userId: req.user.id,
|
||||
username: req.user.username,
|
||||
module: '考勤管理',
|
||||
action: '保存考勤时段配置',
|
||||
targetType: 'attendancePeriodConfig',
|
||||
detail: dto.periods.map((item) => `${item.label}:${item.startTime}-${item.endTime}`).join(';'),
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@Post('attendance-period-configs/reset')
|
||||
@RequirePermission('attendance:edit')
|
||||
async resetAttendancePeriodConfigs(@Request() req: { user: RequestUser }) {
|
||||
const result = await this.service.resetAttendancePeriodConfigs();
|
||||
await this.logService.log({
|
||||
userId: req.user.id,
|
||||
username: req.user.username,
|
||||
module: '考勤管理',
|
||||
action: '重置考勤时段配置',
|
||||
targetType: 'attendancePeriodConfig',
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@Get('attendance-lessons/schedules/:scheduleId')
|
||||
@RequirePermission('attendance:view')
|
||||
async getLessonAttendance(
|
||||
@@ -117,6 +159,7 @@ export class AttendanceController {
|
||||
req.user.id,
|
||||
schedule.schedule.classId!,
|
||||
this.canManageAllAttendance(req),
|
||||
dto.date,
|
||||
);
|
||||
const importRange = this.service.getLessonAttendanceImportDateRange(
|
||||
schedule.schedule,
|
||||
@@ -128,6 +171,9 @@ export class AttendanceController {
|
||||
autoMatch: true,
|
||||
userId: req.user.id,
|
||||
});
|
||||
if (!importResult.success || importResult.errors.length > 0) {
|
||||
throw new BadRequestException(importResult.errors.join('; ') || '钉钉考勤拉取失败');
|
||||
}
|
||||
const result = await this.service.createLessonAttendanceFromDingTalk(
|
||||
scheduleId,
|
||||
dto.date,
|
||||
@@ -166,6 +212,84 @@ export class AttendanceController {
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@Get('attendance-records/dingtalk-sync-status')
|
||||
@RequirePermission('attendance:view')
|
||||
async getDingTalkSyncStatus() {
|
||||
const latest = await this.logService.findLatestDingTalkAttendancePull();
|
||||
return {
|
||||
lastPulledAt: latest?.createdAt ?? null,
|
||||
action: latest?.action ?? null,
|
||||
username: latest?.username ?? null,
|
||||
detail: latest?.detail ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
@Post('attendance-records/refresh-dingtalk')
|
||||
@RequirePermission('attendance:create')
|
||||
async refreshDingTalkAttendance(
|
||||
@Body() dto: RefreshDingTalkAttendanceDto,
|
||||
@Request() req: { user: RequestUser },
|
||||
) {
|
||||
if (dto.date > this.getTodayDateOnly()) {
|
||||
throw new BadRequestException('不能查看或刷新未来日期的考勤');
|
||||
}
|
||||
if (dto.classId) await this.assertClassAccess(req, dto.classId);
|
||||
const schedules = await this.service.getRefreshableSchedules(
|
||||
dto.date,
|
||||
dto.classId,
|
||||
dto.session,
|
||||
await this.getAccessibleClassIds(req),
|
||||
);
|
||||
let refreshed = 0;
|
||||
let imported = 0;
|
||||
let matched = 0;
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const schedule of schedules) {
|
||||
try {
|
||||
const importClassIds = await this.service.getTeacherClassDingUserIds(
|
||||
req.user.id,
|
||||
schedule.classId!,
|
||||
this.canManageAllAttendance(req),
|
||||
dto.date,
|
||||
);
|
||||
const importRange = this.service.getLessonAttendanceImportDateRange(schedule, dto.date);
|
||||
const importResult = await this.importService.importFromDingTalk({
|
||||
...importRange,
|
||||
userIds: importClassIds,
|
||||
autoMatch: true,
|
||||
userId: req.user.id,
|
||||
});
|
||||
if (!importResult.success || importResult.errors.length > 0) {
|
||||
errors.push(...importResult.errors);
|
||||
continue;
|
||||
}
|
||||
await this.service.createLessonAttendanceFromDingTalk(schedule.id, dto.date, req.user.id);
|
||||
refreshed += 1;
|
||||
imported += importResult.imported;
|
||||
matched += importResult.matched;
|
||||
} catch (error: unknown) {
|
||||
errors.push((error as { message?: string })?.message || `排课 ${schedule.id} 刷新失败`);
|
||||
}
|
||||
}
|
||||
|
||||
await this.logService.log({
|
||||
userId: req.user.id,
|
||||
username: req.user.username,
|
||||
module: '考勤管理',
|
||||
action: '刷新钉钉考勤',
|
||||
targetType: 'attendanceRecord',
|
||||
detail: `日期${dto.date},排课${schedules.length}节,刷新${refreshed}节,钉钉新增${imported}条,匹配${matched}条${errors.length ? `,错误${errors.length}条` : ''}`,
|
||||
status: errors.length > 0 && refreshed === 0 ? 'failure' : 'success',
|
||||
});
|
||||
|
||||
if (schedules.length === 0) {
|
||||
return { refreshed, imported, matched, errors: ['当前条件下没有可刷新的课程'] };
|
||||
}
|
||||
return { refreshed, imported, matched, errors };
|
||||
}
|
||||
|
||||
// ── Batch create attendance records ──
|
||||
@Post('attendance-records/batch')
|
||||
@RequirePermission('attendance:create')
|
||||
@@ -277,6 +401,16 @@ export class AttendanceController {
|
||||
res.end();
|
||||
}
|
||||
|
||||
@Get('attendance-records/schedules')
|
||||
@RequirePermission('attendance:view')
|
||||
async getAttendanceScheduleOptions(
|
||||
@Query() query: AttendanceScheduleOptionsQueryDto,
|
||||
@Request() req: { user: RequestUser },
|
||||
) {
|
||||
await this.assertClassAccess(req, query.classId);
|
||||
return this.service.getScheduleOptionsForAttendance(query.classId, query.date);
|
||||
}
|
||||
|
||||
// ── List attendance records with filters ──
|
||||
@Get('attendance-records')
|
||||
@RequirePermission('attendance:view')
|
||||
@@ -522,6 +656,7 @@ export class AttendanceController {
|
||||
req.user.id,
|
||||
dto.classId,
|
||||
canManageAll,
|
||||
dto.start,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user