198 lines
10 KiB
Markdown
198 lines
10 KiB
Markdown
# 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` |
|