147 lines
4.0 KiB
TypeScript
147 lines
4.0 KiB
TypeScript
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;
|