40227-vm/backend/docs/staff-attendance.md
2026-06-10 18:27:19 +02:00

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_records and db.staff inside the service (no separate db/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): startDate and endDate (ISO YYYY-MM-DD, validated via optionalIsoDate), and limit (positive integer, defaulting to STAFF_ATTENDANCE_DEFAULT_LIMIT = 90 and capped at STAFF_ATTENDANCE_MAX_LIMIT = 366 via clampLimit). Each row is the record DTO below. Rows are ordered by attendance_date descending, then user_name ascending.
  • GET /api/staff_attendance/summary -> 200 { staffCount, recordsCount, present, late, absent }. Accepts the same startDate, endDate, and limit query parameters; limit is read from the filter type but the summary counts are computed with SQL COUNT aggregates, 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 by userId (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 with globalAccess see all organization records; other report-role users are restricted to their own campus (campusId from 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.
  • globalAccess on 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 staffCount query applies the same campusScope over the staff table (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_STATUSESpresent, 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 COUNT queries (run concurrently with the active-staff count via Promise.all) rather than fetching and counting rows in JS, so totals are not truncated by limit.
  • dateFilter builds an attendance_date range using Op.gte / Op.lte only for the provided bound(s); with neither bound it adds no date condition.
  • The list limit defaults and caps come from the staff-attendance constants, not the shared pagination helper.

Tests

None yet (no staff_attendance unit/e2e test in src/).

  • Frontend: frontend/docs/staff-attendance-integration.md.
  • Related slices: campus-attendance (campus daily aggregates), the student-level attendance_sessions / attendance_records slices, and staff (active staff count, campus resolution).