5.7 KiB
Staff Attendance Backend
Purpose
staff_attendance_records stores staff-level attendance entries per organization. This slice is a
read-only reporting surface: it exposes a filtered record list and an aggregated summary used by the
attendance snapshot and the director dashboard. It does not write, import, or generate records.
This is distinct from the student-level attendance models (attendance_sessions,
attendance_records) and from campus daily aggregates (campus_attendance_summaries).
Slice Files (by layer)
- Route:
src/routes/staff_attendance.ts(thin wiring;GET /records,GET /summary). - Controller:
src/api/controllers/staff_attendance.controller.ts(custom — not the CRUD factory). - Service (BLL):
src/services/staff_attendance.ts. - Repository (DAL): queries run through
db.staff_attendance_recordsanddb.staffinside the service (no separatedb/api/staff_attendance.ts). - Model:
src/db/models/staff_attendance_records.ts. - Shared used:
services/shared/access.ts(assertAuthenticatedTenantUser,requireOrganizationId,requireUserId,hasRoleAccess,campusScope),services/shared/validate.ts(clampLimit,optionalIsoDate),shared/constants/staff-attendance.ts(STAFF_ATTENDANCE_STATUSES,STAFF_ATTENDANCE_REPORT_ROLE_NAMES,STAFF_ATTENDANCE_TENANT_WIDE_ROLE_NAMES,STAFF_ATTENDANCE_DEFAULT_LIMIT,STAFF_ATTENDANCE_MAX_LIMIT),shared/constants/staff.ts(STAFF_STATUSES).
API
The slice is mounted at /api/staff_attendance; all routes require JWT authentication (the mount in
src/index.ts applies the jwt passport guard).
GET /api/staff_attendance/records->200{ rows, count }. Query parameters (all optional):startDateandendDate(ISOYYYY-MM-DD, validated viaoptionalIsoDate), andlimit(positive integer, defaulting toSTAFF_ATTENDANCE_DEFAULT_LIMIT= 90 and capped atSTAFF_ATTENDANCE_MAX_LIMIT= 366 viaclampLimit). Each row is the record DTO below. Rows are ordered byattendance_datedescending, thenuser_nameascending.GET /api/staff_attendance/summary->200{ staffCount, recordsCount, present, late, absent }. Accepts the samestartDate,endDate, andlimitquery parameters;limitis read from the filter type but the summary counts are computed with SQLCOUNTaggregates, not by limiting rows.
Invalid startDate/endDate (non-ISO) or a non-positive / non-integer limit raises
ValidationError.
Access Rules
Enforced by visibilityScope in the service:
- A user who does NOT hold a report role (
STAFF_ATTENDANCE_REPORT_ROLE_NAMES) sees only their own records, scoped byuserId(requireUserId). - A user who holds a report role sees campus-scoped records via
campusScope: tenant-wide roles (STAFF_ATTENDANCE_TENANT_WIDE_ROLE_NAMES) or users withglobalAccesssee all organization records; other report-role users are restricted to their own campus (campusIdfrom their staff profile, else unrestricted if no campus resolves). STAFF_ATTENDANCE_REPORT_ROLE_NAMES= the generated roles Super Administrator, Administrator, Platform Owner, Tenant Director, Campus Manager.STAFF_ATTENDANCE_TENANT_WIDE_ROLE_NAMES= Super Administrator, Administrator, Platform Owner.globalAccesson the user's app role grants access in any role check (hasRoleAccess).
Both endpoints call assertAuthenticatedTenantUser first; a request without an authenticated user or
resolvable organization raises ForbiddenError.
Tenant Scope
- Every query is bound to the current user's organization (
requireOrganizationId). - Within the organization, visibility is narrowed to the user, their campus, or the whole tenant per
the access rules above. The summary's
staffCountquery applies the samecampusScopeover thestafftable (active staff only).
Data Contract
Record DTO returned by GET /records (toRecordDto): id, date (from attendance_date),
status, note, user_name, user_role, organizationId, campusId, userId, createdAt,
updatedAt.
Summary DTO returned by GET /summary: staffCount (active staff in scope, from the staff table
filtered by STAFF_STATUSES.ACTIVE), recordsCount (= present + late + absent), present, late,
absent.
Model staff_attendance_records fields: id (UUID PK), attendance_date (DATEONLY, not null),
status (ENUM of STAFF_ATTENDANCE_STATUSES — present, late, absent; not null), note
(TEXT, nullable), user_name (TEXT, not null), user_role (TEXT, nullable), importHash
(unique, nullable), organizationId (UUID, not null), campusId (UUID, nullable), userId
(UUID, not null), createdById (UUID, not null), updatedById (UUID, nullable), plus
createdAt/updatedAt/deletedAt. The model is paranoid (soft delete) with freezeTableName.
Associations (belongsTo): organization, campus, user, createdBy, updatedBy.
Behavior / Notes
- The summary aggregates each status with separate SQL
COUNTqueries (run concurrently with the active-staff count viaPromise.all) rather than fetching and counting rows in JS, so totals are not truncated bylimit. dateFilterbuilds anattendance_daterange usingOp.gte/Op.lteonly for the provided bound(s); with neither bound it adds no date condition.- The list
limitdefaults and caps come from the staff-attendance constants, not the shared pagination helper.
Tests
None yet (no staff_attendance unit/e2e test in src/).
Related
- Frontend:
frontend/docs/staff-attendance-integration.md. - Related slices:
campus-attendance(campus daily aggregates), the student-levelattendance_sessions/attendance_recordsslices, andstaff(active staff count, campus resolution).