import { clampLimit, optionalIsoDate } from '@/services/shared/validate'; import { Op } from 'sequelize'; import db from '@/db/models'; import { STAFF_ATTENDANCE_DEFAULT_LIMIT, STAFF_ATTENDANCE_MAX_LIMIT, STAFF_ATTENDANCE_REPORT_ROLE_NAMES, STAFF_ATTENDANCE_STATUSES, } from '@/shared/constants/staff-attendance'; import { STAFF_STATUSES } from '@/shared/constants/staff'; import { assertAuthenticatedTenantUser, campusDimensionScope, hasRoleAccess, requireOrganizationId, requireUserId, } from '@/services/shared/access'; import type { StaffAttendanceRecords } from '@/db/models/staff_attendance_records'; import type { CurrentUser } from '@/db/api/types'; interface StaffAttendanceFilter { startDate?: unknown; endDate?: unknown; limit?: unknown; } /** * Restricts records to the staff member, or (for report roles) to the records * their scope allows: org-wide for owner/superintendent, the school's campuses * for principal/registrar, a single campus for director. */ function visibilityScope(currentUser?: CurrentUser) { if (!hasRoleAccess(currentUser, STAFF_ATTENDANCE_REPORT_ROLE_NAMES)) { return { userId: requireUserId(currentUser) }; } return campusDimensionScope(currentUser); } function dateFilter(filter: StaffAttendanceFilter): { attendance_date?: { [Op.gte]?: string; [Op.lte]?: string }; } { const startDate = optionalIsoDate(filter.startDate); const endDate = optionalIsoDate(filter.endDate); if (!startDate && !endDate) { return {}; } return { attendance_date: { ...(startDate ? { [Op.gte]: startDate } : {}), ...(endDate ? { [Op.lte]: endDate } : {}), }, }; } function toRecordDto(record: StaffAttendanceRecords) { const plain = record.get({ plain: true }); return { id: plain.id, date: plain.attendance_date, status: plain.status, note: plain.note, user_name: plain.user_name, user_role: plain.user_role, organizationId: plain.organizationId, campusId: plain.campusId, userId: plain.userId, createdAt: plain.createdAt, updatedAt: plain.updatedAt, }; } class StaffAttendanceService { static async listRecords( filter: StaffAttendanceFilter, currentUser?: CurrentUser, ) { assertAuthenticatedTenantUser(currentUser); const result = await db.staff_attendance_records.findAndCountAll({ where: { organizationId: requireOrganizationId(currentUser), ...visibilityScope(currentUser), ...dateFilter(filter), }, limit: clampLimit(filter.limit, STAFF_ATTENDANCE_DEFAULT_LIMIT, STAFF_ATTENDANCE_MAX_LIMIT), order: [ ['attendance_date', 'desc'], ['user_name', 'asc'], ], }); return { rows: result.rows.map(toRecordDto), count: result.count, }; } static async summary( filter: StaffAttendanceFilter, currentUser?: CurrentUser, ) { assertAuthenticatedTenantUser(currentUser); // Aggregate in SQL (COUNT per status) instead of fetching every record and // counting in JS — avoids the limit-truncated, incorrect totals and the // large row transfer. const recordsWhere = { organizationId: requireOrganizationId(currentUser), ...visibilityScope(currentUser), ...dateFilter(filter), }; const [present, late, absent, staffCount] = await Promise.all([ db.staff_attendance_records.count({ where: { ...recordsWhere, status: STAFF_ATTENDANCE_STATUSES.PRESENT }, }), db.staff_attendance_records.count({ where: { ...recordsWhere, status: STAFF_ATTENDANCE_STATUSES.LATE }, }), db.staff_attendance_records.count({ where: { ...recordsWhere, status: STAFF_ATTENDANCE_STATUSES.ABSENT }, }), db.staff.count({ where: { organizationId: requireOrganizationId(currentUser), status: STAFF_STATUSES.ACTIVE, ...campusDimensionScope(currentUser), }, }), ]); return { staffCount, recordsCount: present + late + absent, present, late, absent, }; } } export default StaffAttendanceService;