# 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.