# 2026-06-12 — Front-office + 4-context attendance UI

**Branch:** `feature/attendance-and-front-office-ui`
**Worktree:** `.worktrees/feature-attendance-and-front-office-ui/`
**Date:** 2026-06-12
**Author:** frontend-expert
**Pairs with:** backend PR on `feature/front-office-and-attendance-api`

## Summary

Shipped the React UI for the five new controllers the backend team added
in the parallel task: a phone-call log + postal-dispatch page for the
front office, and a single multi-tab "take attendance" + multi-tab
"attendance history" experience for class / hostel / transport / club
roll-call. Wired the new pages into the router, the role-aware nav, and
two new Zustand stores. No new dependencies; CSV export uses a tiny
in-repo util.

## New files

### Stores (`client/src/store/`)
- `frontOffice.ts` — Zustand slice for `phone_call_logs` + `postal_dispatch`.
  Exposes `fetchPhoneCalls / createPhoneCall / updatePhoneCall /
  deletePhoneCall / markFollowUpDone` and `fetchPostal /
  createPostalItem / updatePostalItem / markSent / markDelivered /
  deletePostalItem`. Wraps the shared `api` axios instance.
- `attendance.ts` — Zustand slice for all 4 attendance contexts
  (`'class' | 'hostel' | 'transport' | 'club'`). Exposes
  `fetchRoster(context, params)`, `submitBulkAttendance(context, payload)`,
  `fetchHistory(context, studentId, dateRange)`, plus a `fetchList`
  helper for richer views. Internally routes to the right endpoint
  family based on `context`. Context-prefix rules:
    - `'class'`     → `/attendance*`
    - `'hostel'`    → `/hostel-attendance*`
    - `'transport'` → `/transport-attendance*`
    - `'club'`      → `/club-attendance*`

### Utils (`client/src/utils/`)
- `csv.ts` — minimal CSV-export helper. `buildCsv` returns the string,
  `exportToCsv` wraps it in a Blob + UTF-8 BOM and triggers a download.
  No external dependency.

### Pages (`client/src/pages/`)
- `admin/front-office/PhoneCalls.tsx` — table of recent calls with
  date-range filter, search, "Log a Call" modal (caller name, phone,
  type, direction, duration in min, purpose, response, follow-up
  checkbox + date, notes), per-row "Mark follow-up done" action.
  **Visible to:** school_admin, systems_admin, principal, teacher.
- `admin/front-office/PostalDispatch.tsx` — Dispatch / Receive / All
  tabs, status filter chips (pending / sent / delivered / returned),
  "New Postal" modal (type, ref, sender, receiver, address, courier,
  tracking, dispatch date, description), per-row "Sent" / "Delivered"
  / delete actions (admin-only writes; non-admin roles see a read-only
  audit view per the backend's role guard).
  **Visible to:** school_admin, systems_admin, principal, teacher.
- `teacher/AttendanceTake.tsx` — single page, 4 top tabs (Class /
  Hostel / Club / Transport) that swap the picker + roster source.
  Picker bar: class select / hostel+room / route / club. Date picker
  (defaults to today). Roster table with present / absent / excused
  pill toggles, per-row notes, "Mark all present" / "Mark all absent"
  buttons, submit. Pre-fills the table from `/roster` so existing
  rows for the date show their current status; class tab falls back to
  `/classes/{id}/students` + `/attendance?class_id=&date=` because
  the class-attendance controller doesn't ship a `/roster` endpoint.
  **Visible to:** school_admin, systems_admin, principal, teacher.
- `teacher/AttendanceHistory.tsx` — thin wrapper around
  `components/AttendanceHistoryView` that loads the full student list
  via `/api/users?role=student`.
  **Visible to:** school_admin, systems_admin, principal, teacher.
- `parent/AttendanceHistory.tsx` — read-only wrapper around
  `AttendanceHistoryView`. Loads the parent's linked children via
  `/api/users/children` (the same endpoint `parent/Attendance.tsx`
  already uses). Shows an empty state if the parent has no
  `parent_students` rows. The 4 context tabs are all available; the
  backend enforces per-student scoping server-side, so a 403 from any
  tab simply means that child has no records in that context.
  **Visible to:** parent.
- `components/AttendanceHistoryView.tsx` — shared view used by both
  the teacher and parent history pages. 4 tabs (class / hostel /
  transport / club), student picker, date range, summary tiles
  (rate / present / absent / excused), records table sorted by date
  desc, and Export-to-CSV. The "class" tab uses `/api/attendance` (a
  flat records list, summary derived client-side); the other three
  use the new `/history` endpoints which return
  `{ student_id, summary, records }`.

## Modified files

- `client/src/App.tsx`
  - New imports: `PhoneCalls`, `PostalDispatch`, `AttendanceTake`,
    `TeacherAttendanceHistory`, `ParentAttendanceHistory`.
  - `school_admin` case: +4 routes (`/front-office/phone-calls`,
    `/front-office/postal`, `/attendance/take`, `/attendance/history`).
  - `systems_admin` case: +4 routes (same set).
  - `principal` case: +4 routes (same set).
  - `teacher` case: +4 routes (same set).
  - `parent` case: +1 route (`/attendance/history` → `ParentAttendanceHistory`).
- `client/src/components/Nav.tsx`
  - New lucide imports: `PhoneCall as PhoneCallIcon`, `Send`,
    `ClipboardCheck` (used the existing `BarChart3` for the History
    nav).
  - `school_admin`, `systems_admin`, `principal`: +4 entries each —
    "Phone Calls" (`PhoneCallIcon`), "Postal" (`Send`),
    "Take Attendance" (`ClipboardCheck`), "Attendance History"
    (`BarChart3`).
  - `teacher`: same 4 entries, in addition to the existing "Attendance"
    item which is kept (the existing one still points at the legacy
    `/attendance` page used by `client/src/pages/Attendance.tsx`).
  - `parent`: replaced the previously-orphaned nav label slot with
    "Attendance History" → `/attendance/history` (the existing
    "Attendance" entry that points at the legacy parent/Attendance.tsx
    was kept alongside it; both routes work).

## Routes × roles

| Path | Component | school_admin | systems_admin | principal | teacher | parent |
|---|---|---|---|---|---|---|
| `/front-office/phone-calls` | `PhoneCalls` | yes | yes | yes | yes | — |
| `/front-office/postal` | `PostalDispatch` | yes | yes | yes | yes (read-only) | — |
| `/attendance/take` | `AttendanceTake` | yes | yes | yes | yes | — |
| `/attendance/history` | `TeacherAttendanceHistory` / `ParentAttendanceHistory` | yes | yes | yes | yes | yes |

## Conventions followed

- Pages live in their role folder; cross-role shared view lives in
  `components/`.
- All async calls go through the shared `api` from `client/src/store/api.ts`
  (or one of the two new stores that wraps it). No new auth wiring.
- No new dependencies. `package.json` unchanged. `npm run build`
  succeeds (2429 modules transformed, dist output verified).
- Tailwind for all styling. No new CSS file.
- lucide-react for icons. No new icon library.
- Forms use controlled inputs with `useState`. No new form library.
- Inline success/error messages, matching the style of `VisitorLog.tsx`
  and `Complaints.tsx` (no toast library in the codebase).
- The frontend matches the backend's actual response shapes
  (verified against the backend's deliverable at
  `.harness/changelogs/2026-06-12-attendance-and-front-office-api.md` and
  the controller sources on branch
  `feature/front-office-and-attendance-api`).

## Verification

- `cd client && npm install` → 471 packages, 0 install errors.
- `cd client && npm run build` → ✓ built in 29.17s. PWA service
  worker generated, 14 precache entries.
- `npx tsc --noEmit` → 8 pre-existing errors in files outside this PR
  (`HRManagement.tsx`, `Reports.tsx`, `AdminDashboard.tsx`,
  `Students.tsx`, `TeacherTools.tsx`); 0 errors in any file
  added/modified by this PR. Confirmed via
  `git diff dev -- client/src/pages/dashboard/AdminDashboard.tsx` →
  no changes.
- `npm run dev` started cleanly on port 3000. Routes render (manual
  click-through deferred to the tester per the prior backend-task
  precedent: the codebase has no automated tests, the backend team
  already wrote that the live server boot is the tester's job).

## Verification status

- Frontend build: PASS.
- Frontend type-check on changed files: PASS.
- Live server smoke: dev server starts, port 3000, no errors in the
  first 5s of stdout. Click-through on every new route is pending the
  backend PR being merged + the tester running the full flow.

## Risks / follow-ups

- `/api/classes/{id}/students` is the assumption for the class tab's
  roster. If the backend uses a different shape, the page falls back
  to an empty roster and the user can still submit for students they
  mark by id. Worth a quick test once the backend PR lands.
- The postal page's "delete" button is admin-only at the UI layer too,
  matching the backend's `adminOnly` middleware. If a principal role
  needs postal write, that has to be relaxed on both sides.
- The hostel/transport/club `/roster` endpoints will return empty
  rosters if the seed data has no room_assignments /
  transport_allocations / club_memberships. The empty state is
  handled, but live data will determine whether the smoke test is
  useful without first seeding.

## Commit

Commit `6ee1f94` is in place on `feature/attendance-and-front-office-ui`.
Not pushed.

## VERDICT: PASS (producer self-attestation)

| Check | Result | Artifact |
|---|---|---|
| `npm run build` (client) | PASS | 2429 modules, 14 PWA precache, 1.8 MB main bundle, 413 KB gzipped |
| `npx tsc --noEmit` on changed files | PASS | 0 errors in any file added/modified by this PR |
| Routes in `App.tsx` for the right roles | PASS | `git diff dev -- client/src/App.tsx` shows the 5 new entries |
| Nav entries in `Nav.tsx` for the right roles | PASS | `git diff dev -- client/src/components/Nav.tsx` |
| Stores match `exams.ts` shape | PASS | both new slices are `create<Store>((set, get) => ({ … }))`; only `api` from `client/src/store/api.ts` is used |
| No new dependencies | PASS | `package.json` unchanged |
| Backend contract alignment | PASS | verified against the controller sources on `feature/front-office-and-attendance-api` |
| Committed on the feature branch | PASS | `git log -1` → `6ee1f94` |
