# 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((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` |