geocrop-platform./apps/nextgen/.harness/changelogs/2026-06-12-attendance-and-f...

230 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Changelog — 2026-06-12 — Attendance & Front-Office API
Branch: `feature/front-office-and-attendance-api`
Worktree: `.worktrees/feature-front-office-and-attendance-api/`
Owner: backend-expert
Date: 2026-06-12
This change activates two previously-dead front-office tables (`phone_call_logs`,
`postal_dispatch`) and adds proper controllers for the three extended roll-call
tables (`hostel_attendance`, `transport_attendance`, `club_attendance`) that
had no HTTP surface. It also adds `GET /api/attendance/today` to the
class-attendance controller.
No schema changes. No new tables. No new SyncEngine entries (all five tables
were already in `tablesToSync` — verified at `server/src/services/SyncEngine.js`
lines 89-99).
---
## Routes added
### `phone-call-logs` (mounted under existing `/api/front-office`)
Mount: `front-officeController` at `app.use('/api/front-office', ...)` in
`server/src/index.js:79` (unchanged; new routes added to the same router).
| Method | Path | Auth | Role guard (writes) | Notes |
|---|---|---|---|---|
| GET | `/api/front-office/phone-calls?from=&to=&handled_by=` | yes | read — all roles | Filters by call_time range and handler. |
| POST | `/api/front-office/phone-calls` | yes | `staffWrite` (school_admin / principal / teacher) | Required: `caller_name`. Sets `sync_status='pending'`. |
| PUT | `/api/front-office/phone-calls/:id` | yes | `staffWrite` | Partial update; `sync_status='pending'`. |
| DELETE | `/api/front-office/phone-calls/:id` | yes | `staffWrite` | Soft delete (`is_deleted=1`). |
| PUT | `/api/front-office/phone-calls/:id/follow-up` | yes | `staffWrite` | Sets `follow_up_required=0`, optional `notes`. |
`caller_type` CHECK: `parent | student | vendor | government | other`.
`direction` CHECK: `incoming | outgoing`.
### `postal-dispatch` (mounted under existing `/api/front-office`)
| Method | Path | Auth | Role guard (writes) | Notes |
|---|---|---|---|---|
| GET | `/api/front-office/postal?type=&status=` | yes | read — all roles | `type`: `dispatch | receive`. `status`: `pending | sent | delivered | returned`. |
| POST | `/api/front-office/postal` | yes | `adminOnly` (school_admin) | Required: `sender`, `receiver`. |
| PUT | `/api/front-office/postal/:id` | yes | `adminOnly` | Partial update. |
| PUT | `/api/front-office/postal/:id/mark-sent` | yes | `adminOnly` | Sets `status='sent'`. |
| PUT | `/api/front-office/postal/:id/mark-delivered` | yes | `adminOnly` | Sets `status='delivered'`, `received_date=NOW()`. |
| DELETE | `/api/front-office/postal/:id` | yes | `adminOnly` | Soft delete. |
### `hostel-attendance` (new controller, new mount)
Mount added at `server/src/index.js`: `app.use('/api/hostel-attendance', hostelAttendanceController);`
| Method | Path | Auth | Role guard (writes) | Notes |
|---|---|---|---|---|
| GET | `/api/hostel-attendance?hostel_id=&room_id=&date=&from=&to=` | yes | read — scoped | Student → own rows; parent → linked students via `parent_students`; others → all. |
| GET | `/api/hostel-attendance/roster?hostel_id=&room_id=&date=` | yes | read — all roles | Default `date` = today. Roster from active `room_assignments` left-joined with `hostel_attendance` for the date. |
| POST | `/api/hostel-attendance/bulk` | yes | `staffWrite` | Body `{ hostel_id, room_id?, date, records: [{student_id, status}] }`. Single `db.transaction`. Upsert pattern (SELECT then UPDATE/INSERT) — mirrors `clubs.controller.js:218`. |
| GET | `/api/hostel-attendance/history?student_id=&from=&to=` | yes | read — scoped | Per-student history with `{ summary, records }`. |
Valid statuses: `present | absent | excused`.
### `transport-attendance` (new controller, new mount)
Mount added at `server/src/index.js`: `app.use('/api/transport-attendance', transportAttendanceController);`
| Method | Path | Auth | Role guard (writes) | Notes |
|---|---|---|---|---|
| GET | `/api/transport-attendance?route_id=&date=&from=&to=` | yes | read — scoped | Same scoping rule as hostel-attendance. |
| GET | `/api/transport-attendance/roster?route_id=&date=` | yes | read — all roles | Roster from `transport_allocations` (active) joined with attendance for the date. |
| POST | `/api/transport-attendance/bulk` | yes | `staffWrite` | Body `{ route_id, date, records: [{student_id, status}] }`. Single `db.transaction`, upsert. |
| GET | `/api/transport-attendance/history?student_id=&from=&to=` | yes | read — scoped | Per-student history with summary. |
### `club-attendance` (new controller, new mount)
Mount added at `server/src/index.js`: `app.use('/api/club-attendance', clubAttendanceController);`
**Note:** The original `clubs.controller.js` already exposes a minimal
`GET /api/clubs/attendance` and `POST /api/clubs/attendance/bulk` — those
endpoints are preserved (no deletions). This new controller provides a
consistent module URL (`/api/club-attendance/...`) with the full
list / roster / bulk / history shape used by hostel and transport.
| Method | Path | Auth | Role guard (writes) | Notes |
|---|---|---|---|---|
| GET | `/api/club-attendance?club_id=&date=&from=&to=` | yes | read — scoped | |
| GET | `/api/club-attendance/roster?club_id=&date=` | yes | read — all roles | Roster from active `club_memberships` joined with attendance. |
| POST | `/api/club-attendance/bulk` | yes | `staffWrite` | Body `{ club_id, date, records: [...] }`. |
| GET | `/api/club-attendance/history?student_id=&from=&to=` | yes | read — scoped | |
### Class attendance — `GET /today` added
`POST /api/attendance/bulk` already existed (in `attendance.controller.js`
line 265 of the previous file, no inline duplicate in `index.js`). We only
added `GET /today` per the task spec.
| Method | Path | Auth | Role guard | Notes |
|---|---|---|---|---|
| GET | `/api/attendance/today?class_id=` | yes | read — scoped | Returns `{ date, total, records: [...] }` for today. Student → own; parent → linked; teacher → own classes/subjects; admin/principal → all. |
---
## Files changed
| File | Change |
|---|---|
| `server/src/controllers/front-office.controller.js` | + 2 new resource blocks (`phone-calls` × 5 routes, `postal` × 6 routes). New `staffWrite` middleware. |
| `server/src/controllers/hostel-attendance.controller.js` | **NEW** — 4 routes, joins `hostels`/`rooms`/`room_assignments`/`users`. |
| `server/src/controllers/transport-attendance.controller.js` | **NEW** — 4 routes, joins `routes`/`transport_allocations`/`users`. |
| `server/src/controllers/club-attendance.controller.js` | **NEW** — 4 routes, joins `clubs`/`club_memberships`/`users`. |
| `server/src/controllers/attendance.controller.js` | + `GET /today` route. No other changes. |
| `server/src/index.js` | + 3 `require(...)` lines and + 3 `app.use(...)` lines. |
---
## Conventions enforced
- All writes set `sync_status = 'pending'`. Verified across all new routes.
- All reads include `WHERE … is_deleted = 0` unless explicitly looking at the trash.
- Every prepared statement uses `?` placeholders. No string-concatenated SQL.
- Auth middleware is inline per-controller (matches the `front-office.controller.js` canonical pattern).
- Role checks: writes gated by `staffWrite` (admin / school_admin / principal / teacher) for the 3 attendance controllers and the phone-calls block. Postal-dispatch writes are tighter (admin / school_admin only) because it's an audit trail.
- Students see only their own attendance records. Parents see only rows for students linked via `parent_students` (returns 403 if `student_id` is requested but not linked).
- Soft delete: `UPDATE … SET is_deleted = 1, updated_at = CURRENT_TIMESTAMP, sync_status = 'pending' WHERE id = ? AND is_deleted = 0`. Returns `{ success: true }`.
- All bulk endpoints wrap multi-write upserts in a single `db.transaction(() => { … })()`, matching the `clubs.controller.js:218` pattern.
---
## Schema sanity check (no schema change required)
All five tables already existed and already had the full SQL contract
(`uid`, `sync_status`, `last_synced_at`, `is_deleted`, `created_at`,
`updated_at`):
- `phone_call_logs``server/src/database/init.js:1150`
- `postal_dispatch``server/src/database/init.js:1175`
- `hostel_attendance``server/src/database/init.js:1595`
- `transport_attendance``server/src/database/init.js:1611`
- `club_attendance``server/src/database/init.js:1577`
All five are already in `SyncEngine.tablesToSync` at
`server/src/services/SyncEngine.js` lines 89-99. No `sync-expert` handoff
required.
---
## Verify (run yourself)
The server was not booted in this session (the previous attempt timed out
hanging on a long-lived dev process; the verifier will boot the server
on their own). All module-load smoke tests passed:
```bash
cd .worktrees/feature-front-office-and-attendance-api/server
npm run db:init # idempotent — re-runs cleanly
# Module-load smoke test (passes):
node -e "
const m1 = require('./src/controllers/hostel-attendance.controller');
const m2 = require('./src/controllers/transport-attendance.controller');
const m3 = require('./src/controllers/club-attendance.controller');
const m4 = require('./src/controllers/attendance.controller');
const m5 = require('./src/controllers/front-office.controller');
console.log({ hostel: m1.stack.length, transport: m2.stack.length,
club: m3.stack.length, attendance: m4.stack.length,
frontOffice: m5.stack.length });
"
# → { hostel: 4, transport: 4, club: 4, attendance: 10, frontOffice: 20 }
```
Curl recipes for the verifier (boot with `cd server && JWT_SECRET=africa-alert-secret-key-2024 PORT=3001 node src/index.js` then):
```bash
TOKEN=$(curl -s -X POST http://localhost:3001/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"admin@school.com","password":"admin123"}' \
| node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>console.log(JSON.parse(d).token))")
# Sanity
curl -s http://localhost:3001/api/dashboard/stats -H "Authorization: Bearer $TOKEN" | head -c 200; echo
# Front-office additions
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3001/api/front-office/phone-calls -H "Authorization: Bearer $TOKEN"
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3001/api/front-office/postal -H "Authorization: Bearer $TOKEN"
# Attendance additions
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3001/api/hostel-attendance -H "Authorization: Bearer $TOKEN"
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3001/api/hostel-attendance/roster -H "Authorization: Bearer $TOKEN"
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3001/api/transport-attendance -H "Authorization: Bearer $TOKEN"
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3001/api/transport-attendance/roster -H "Authorization: Bearer $TOKEN"
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3001/api/club-attendance -H "Authorization: Bearer $TOKEN"
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3001/api/club-attendance/roster -H "Authorization: Bearer $TOKEN"
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3001/api/attendance/today -H "Authorization: Bearer $TOKEN"
# Regression checks
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3001/api/users -H "Authorization: Bearer $TOKEN"
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3001/api/classes -H "Authorization: Bearer $TOKEN"
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3001/api/attendance -H "Authorization: Bearer $TOKEN"
```
All endpoints should return `200`. The two POST `/bulk` and two POST
`/api/front-office/phone-calls|postal` require a JSON body and will be
covered by the integration tests run by `tester` / `code-reviewer`.
---
## Notes for the verifier
- The repo has inconsistent role naming (`admin` vs `school_admin` in
different files). I accepted **both** in `staffWrite` for the three
new attendance controllers; the front-office additions keep the
existing `school_admin`-only `adminOnly` plus a new `staffWrite` that
also accepts `principal` and `teacher` (the front-desk staff).
- The 4 `student`-scoped list endpoints in the 3 attendance controllers
all return a 200 with the student's own rows (no 403). The
history endpoint accepts a missing `student_id` and forces it to
`req.user.id` for students; parents get a 403 if they ask for an
unlinked student.
- The `hostel_attendance` and `transport_attendance` tables are
populated indirectly: rosters are derived from `room_assignments` and
`transport_allocations` respectively. No seed data was added — the
verifier should expect empty `roster` and empty list results until
upstream tables are populated.
- All endpoints are 7-day-JWT authenticated with the dev default secret
`africa-alert-secret-key-2024` (per `conventions.md`). Set
`JWT_SECRET` in production.
- This change touches the auth layer indirectly (inline middleware
repeated in each new controller). Per the §13.2 audit
recommendation, an Architect review is appropriate for the
shared-middleware extraction; out of scope for this PR.