6.7 KiB
Staff Attendance Backend
Purpose
staff_attendance_records stores staff-level attendance entries per organization. This slice exposes
a filtered record list, an aggregated summary used by the attendance snapshot, and a scoped upsert
endpoint for manual office/staff attendance entry.
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,PUT /records/:userId/:date). - 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.usersinside 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,hasFeaturePermission,campusDimensionScope,getRoleScope,getSchoolId),services/shared/validate.ts(clampLimit,optionalIsoDate),shared/constants/staff-attendance.ts(STAFF_ATTENDANCE_STATUSES,STAFF_ATTENDANCE_DEFAULT_LIMIT,STAFF_ATTENDANCE_MAX_LIMIT).
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.PUT /api/staff_attendance/records/:userId/:date->200record DTO. Body:{ data: { status, note } }, wherestatusispresent,late, orabsent, andnoteis nullable/optional text. The route requiresFILL_ATTENDANCE.
Invalid startDate/endDate (non-ISO) or a non-positive / non-integer limit raises
ValidationError.
Access Rules
Enforced by the service:
- A user who does NOT have
READ_STAFF_ATTENDANCE_REPORTSsees only their own records fromGET /records, scoped byuserId(requireUserId). - A user with
READ_STAFF_ATTENDANCE_REPORTSsees scope-filtered records: organization-wide for owner/superintendent, school campuses plus users directly assigned to that school for principal/registrar, and a single campus for director/campus scope. GET /summaryuses scope-filtered record counts for users with eitherREAD_STAFF_ATTENDANCE_REPORTSorFILL_ATTENDANCE; users without either permission receive only their own summary counts. This lets attendance fillers and report readers share the same completion signal without exposing the report row list.- A user with
FILL_ATTENDANCEcan upsert staff attendance only for staff users inside their effective scope: organization office users at organization scope, school office users at school scope, or campus users at campus/class scope. - Role-seeded permissions are only the baseline grants.
custom_permissionscan grant permissions andcustom_permissions_filtercan remove them for non-global users.
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, their school, or the
whole tenant per the access rules above. The summary's
staffCountquery applies the same scope over internal-role users. For school scope, it includes users withschoolIdequal to the active school as well as users assigned to campuses under that school; for organization scope, it includes organization office, school, and campus staff.
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 (internal-role users in scope), 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
src/services/staff_attendance.test.tsverifies school report scope includes both school-owned users and campuses under the school, summary completion counts for fill-only users, school-scope office attendance upserts, and campus/class staff upserts that store the resolved campus.src/db/seeders/user-roles.test.tsverifies the seeded attendance filler/report-reader roles, including notification eligibility throughFILL_ATTENDANCEorREAD_STAFF_ATTENDANCE_REPORTS.
Related
- Frontend:
frontend/docs/staff-attendance-integration.md. - Related slices:
campus-attendance(campus daily aggregates), the student-levelattendance_sessions/attendance_recordsslices, andusers(active employee count, campus resolution).