115 lines
6.1 KiB
Markdown
115 lines
6.1 KiB
Markdown
# 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_records` and `db.users` 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`, `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):
|
|
`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.
|
|
- `PUT /api/staff_attendance/records/:userId/:date` -> `200` record DTO. Body:
|
|
`{ data: { status, note } }`, where `status` is `present`, `late`, or `absent`, and `note` is
|
|
nullable/optional text. The route requires `FILL_ATTENDANCE`.
|
|
|
|
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 have `READ_STAFF_ATTENDANCE_REPORTS` sees only their own
|
|
records, scoped by `userId` (`requireUserId`).
|
|
- A user with `READ_STAFF_ATTENDANCE_REPORTS` sees 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.
|
|
- A user with `FILL_ATTENDANCE` can 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_permissions` can grant permissions
|
|
and `custom_permissions_filter` can 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 `staffCount` query applies the same scope
|
|
over internal-role users. For school scope, it includes users with `schoolId` equal 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 `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
|
|
|
|
- `src/services/staff_attendance.test.ts` verifies school report scope includes both school-owned
|
|
users and campuses under the school, and that school-scope office attendance upserts are scoped to
|
|
school office users.
|
|
|
|
## Related
|
|
|
|
- Frontend: `frontend/docs/staff-attendance-integration.md`.
|
|
- Related slices: `campus-attendance` (campus daily aggregates), the student-level
|
|
`attendance_sessions` / `attendance_records` slices, and `users` (active employee count, campus
|
|
resolution).
|