10 KiB
10 KiB
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 forphone_call_logs+postal_dispatch. ExposesfetchPhoneCalls / createPhoneCall / updatePhoneCall / deletePhoneCall / markFollowUpDoneandfetchPostal / createPostalItem / updatePostalItem / markSent / markDelivered / deletePostalItem. Wraps the sharedapiaxios instance.attendance.ts— Zustand slice for all 4 attendance contexts ('class' | 'hostel' | 'transport' | 'club'). ExposesfetchRoster(context, params),submitBulkAttendance(context, payload),fetchHistory(context, studentId, dateRange), plus afetchListhelper for richer views. Internally routes to the right endpoint family based oncontext. Context-prefix rules:'class'→/attendance*'hostel'→/hostel-attendance*'transport'→/transport-attendance*'club'→/club-attendance*
Utils (client/src/utils/)
csv.ts— minimal CSV-export helper.buildCsvreturns the string,exportToCsvwraps 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/rosterso 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/rosterendpoint. Visible to: school_admin, systems_admin, principal, teacher.teacher/AttendanceHistory.tsx— thin wrapper aroundcomponents/AttendanceHistoryViewthat loads the full student list via/api/users?role=student. Visible to: school_admin, systems_admin, principal, teacher.parent/AttendanceHistory.tsx— read-only wrapper aroundAttendanceHistoryView. Loads the parent's linked children via/api/users/children(the same endpointparent/Attendance.tsxalready uses). Shows an empty state if the parent has noparent_studentsrows. 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/historyendpoints which return{ student_id, summary, records }.
Modified files
client/src/App.tsx- New imports:
PhoneCalls,PostalDispatch,AttendanceTake,TeacherAttendanceHistory,ParentAttendanceHistory. school_admincase: +4 routes (/front-office/phone-calls,/front-office/postal,/attendance/take,/attendance/history).systems_admincase: +4 routes (same set).principalcase: +4 routes (same set).teachercase: +4 routes (same set).parentcase: +1 route (/attendance/history→ParentAttendanceHistory).
- New imports:
client/src/components/Nav.tsx- New lucide imports:
PhoneCall as PhoneCallIcon,Send,ClipboardCheck(used the existingBarChart3for 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/attendancepage used byclient/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).
- New lucide imports:
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
apifromclient/src/store/api.ts(or one of the two new stores that wraps it). No new auth wiring. - No new dependencies.
package.jsonunchanged.npm run buildsucceeds (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.tsxandComplaints.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.mdand the controller sources on branchfeature/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 viagit diff dev -- client/src/pages/dashboard/AdminDashboard.tsx→ no changes.npm run devstarted 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}/studentsis 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
adminOnlymiddleware. If a principal role needs postal write, that has to be relaxed on both sides. - The hostel/transport/club
/rosterendpoints 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 |