feat: DingTalk attendance import + integration config + expense types + UI polish

Server:
- Add DingTalk attendance import service with SSE progress streaming
- Add IntegrationConfig entity & module for multi-tenant DingTalk setup
- Add ExpenseType entity & ExpenseTypesModule
- Add SeedModule for DB initialization
- Add UserDingMapping entity for DingTalk user linkage
- Attendance service: import flow with dedup & student auto-mapping
- Rooms service: time-range overlap queries
- Sync controller/service: DingTalk integration wiring
- Permission guard: refactor to pure re-export
- Campus scope middleware: tenant-aware filtering

Admin UI:
- Attendance page: import UI with progress & result summary
- All pages: tableStyle/tablePagination standardization
- Login page: responsive styling
- Sensitive data: useViewSensitive hook for masked viewing
- Vite config: path aliases, build optimization
- Test infra: vitest config, test utilities

Docs: PRD DingTalk batch 1 & 2 design docs
This commit is contained in:
2026-07-09 09:11:56 +08:00
parent f1959f0d2a
commit 42d3f0e27f
71 changed files with 5331 additions and 609 deletions

View File

@@ -7,6 +7,9 @@ import { AttendanceRecord } from '../entities/attendance-record.entity';
import { DingAttendanceRaw } from '../entities/ding-attendance-raw.entity';
import { Class } from '../entities/class.entity';
import { Student } from '../entities/student.entity';
import { ClassSchedule } from '../entities/class-schedule.entity';
import { ClassStudent } from '../entities/class-student.entity';
import { UserDingMapping } from '../entities/user-ding-mapping.entity';
import { CampusScope } from '../common/campus-scope';
import { BatchCreateAttendanceDto } from './dto/attendance.dto';
@@ -34,6 +37,10 @@ describe('AttendanceService — batchCreate', () => {
const mockDingRepo = {};
const mockClassRepo = { find: jest.fn().mockResolvedValue([]) };
const mockStudentRepo = { find: jest.fn().mockResolvedValue([]) };
// Reserved for future tests (auto-match, schedule-based attendance, etc.)
const mockScheduleRepo = { find: jest.fn().mockResolvedValue([]) };
const mockClassStudentRepo = { find: jest.fn().mockResolvedValue([]) };
const mockUserDingMappingRepo = { find: jest.fn().mockResolvedValue([]) };
const mockCampusScope = { getScopeDepartmentIds: jest.fn().mockResolvedValue(null), filter: jest.fn((w: any) => w) };
const module: TestingModule = await Test.createTestingModule({
@@ -43,6 +50,9 @@ describe('AttendanceService — batchCreate', () => {
{ provide: getRepositoryToken(DingAttendanceRaw), useValue: mockDingRepo },
{ provide: getRepositoryToken(Class), useValue: mockClassRepo },
{ provide: getRepositoryToken(Student), useValue: mockStudentRepo },
{ provide: getRepositoryToken(ClassSchedule), useValue: mockScheduleRepo },
{ provide: getRepositoryToken(UserDingMapping), useValue: mockUserDingMappingRepo },
{ provide: getRepositoryToken(ClassStudent), useValue: mockClassStudentRepo },
{ provide: CampusScope, useValue: mockCampusScope },
],
}).compile();
@@ -91,4 +101,10 @@ describe('AttendanceService — batchCreate', () => {
await expect(service.batchCreate(dto)).rejects.toThrow(BadRequestException);
});
it.skip('autoMatchDingRecords with UserDingMapping chain', async () => {
// TODO: match dingtalk raw records to students via UserDingMapping lookup,
// then to class schedules → ClassStudent association, producing attendance records.
// Requires mock setup for UserDingMapping, ClassSchedule, ClassStudent, and DingAttendanceRaw repos.
});
});