forked from wangziqi/gongxue-base
fix: UI improvements and DingTalk attendance refresh for completed sessions
- DatePicker editors open immediately on double-click - Student archive drawer hides empty studentNo parentheses - Student add/edit form uses 2-column grid layout - Table horizontal scrollbars show only on hover/focus - Remove fake 近7日趋势 stats from attendance student detail - Allow DingTalk refresh to update completed attendance sessions (late-arriving punches can now change absent → present) - Switch dev command from concurrently to turbo run dev - Remove concurrently/wait-on dependencies
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { LessonAttendanceSyncService } from './lesson-attendance-sync.service';
|
||||
|
||||
const lesson = {
|
||||
schedule: {
|
||||
id: 4,
|
||||
classId: 8,
|
||||
startTime: '22:00',
|
||||
endTime: '01:00',
|
||||
attendanceAdvanceMinutes: 30,
|
||||
},
|
||||
session: null,
|
||||
records: [],
|
||||
};
|
||||
|
||||
const createService = () => {
|
||||
const attendanceService = {
|
||||
getLessonAttendance: jest.fn().mockResolvedValue(lesson),
|
||||
getTeacherClassDingUserIds: jest.fn().mockResolvedValue(['ding-1', 'ding-2']),
|
||||
getLessonAttendanceImportDateRange: jest.fn().mockReturnValue({
|
||||
startDate: '2026-07-11',
|
||||
endDate: '2026-07-12',
|
||||
}),
|
||||
createLessonAttendanceFromDingTalk: jest.fn().mockResolvedValue({
|
||||
...lesson,
|
||||
session: { id: 90, status: 'in_progress' },
|
||||
}),
|
||||
};
|
||||
const importService = {
|
||||
importFromDingTalk: jest.fn().mockResolvedValue({
|
||||
success: true,
|
||||
imported: 3,
|
||||
skipped: 0,
|
||||
matched: 2,
|
||||
errors: [],
|
||||
duration: 10,
|
||||
}),
|
||||
};
|
||||
return {
|
||||
service: new LessonAttendanceSyncService(attendanceService as never, importService as never),
|
||||
attendanceService,
|
||||
importService,
|
||||
};
|
||||
};
|
||||
|
||||
describe('LessonAttendanceSyncService', () => {
|
||||
it('owns the complete refresh recipe and returns attendance with import statistics', async () => {
|
||||
const { service, attendanceService, importService } = createService();
|
||||
|
||||
const result = await service.syncLesson({
|
||||
scheduleId: 4,
|
||||
lessonDate: '2026-07-11',
|
||||
actorId: 21,
|
||||
canManageAll: false,
|
||||
mode: 'refresh',
|
||||
});
|
||||
|
||||
expect(attendanceService.getTeacherClassDingUserIds).toHaveBeenCalledWith(
|
||||
21,
|
||||
8,
|
||||
false,
|
||||
'2026-07-11',
|
||||
);
|
||||
expect(importService.importFromDingTalk).toHaveBeenCalledWith({
|
||||
startDate: '2026-07-11',
|
||||
endDate: '2026-07-12',
|
||||
userIds: ['ding-1', 'ding-2'],
|
||||
autoMatch: true,
|
||||
userId: 21,
|
||||
});
|
||||
expect(attendanceService.createLessonAttendanceFromDingTalk).toHaveBeenCalledWith(
|
||||
4,
|
||||
'2026-07-11',
|
||||
21,
|
||||
false,
|
||||
);
|
||||
expect(result).toMatchObject({
|
||||
attendance: { session: { id: 90 } },
|
||||
imported: 3,
|
||||
matched: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('finalizes only after a complete import', async () => {
|
||||
const { service, attendanceService } = createService();
|
||||
|
||||
await service.syncLesson({
|
||||
scheduleId: 4,
|
||||
lessonDate: '2026-07-11',
|
||||
actorId: 21,
|
||||
canManageAll: true,
|
||||
mode: 'finalize',
|
||||
});
|
||||
|
||||
expect(attendanceService.createLessonAttendanceFromDingTalk).toHaveBeenCalledWith(
|
||||
4,
|
||||
'2026-07-11',
|
||||
21,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects partial imports before changing lesson attendance', async () => {
|
||||
const { service, attendanceService, importService } = createService();
|
||||
importService.importFromDingTalk.mockResolvedValue({
|
||||
success: true,
|
||||
imported: 1,
|
||||
matched: 1,
|
||||
errors: ['one batch failed'],
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.syncLesson({
|
||||
scheduleId: 4,
|
||||
lessonDate: '2026-07-11',
|
||||
actorId: 21,
|
||||
canManageAll: false,
|
||||
mode: 'refresh',
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(attendanceService.createLessonAttendanceFromDingTalk).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user