geocrop-platform./apps/nextgen/.harness/plans/stubs-sprint-1b/B.md

5.7 KiB

Track B — Fees & Hardcoded Copy

Owner: Person B Branch: fix/stubs-fees-and-copy (worktree at .worktrees/stubs-fees-and-copy/) Files in scope: client/src/pages/Fees.tsx, client/src/pages/teacher/MyCourses.tsx, client/src/pages/dashboard/AdminDashboard.tsx (limited slices per task). No backend changes expected.

Conventions (from AGENTS.md)

  • Work in a worktree branched from dev. Do not edit the main checkout.
  • Use the shared axios instance at client/src/store/api.ts.
  • Drop unused lucide-react imports when removing a button.
  • PWA service worker is only built by npm run build.
  • Do not commit or push without an explicit "go" from the reviewer.

Tasks

B1. Fees — Fee-group "More actions" button

  • File: client/src/pages/Fees.tsx:124-127
  • Current: The <MoreVertical /> icon on each fee group has onClick={() => { console.warn('MoreVertical on fee group list — not yet implemented'); }}. The TODO comment in source confirms it.
  • Desired: A real dropdown menu with Edit, Archive, and Duplicate (matches what Classes.tsx already offers on class subjects). Edit opens a modal pre-filled with the row's name, description, amount, due_date. Archive calls DELETE /fees/groups/:id (or PATCH with is_deleted=1 — match what backend exposes) and reloads.
  • Acceptance: Clicking the icon opens a menu. Edit → modal saves. Archive → row disappears after reload. No console.warn from this code path.

B2. Fees — Fee ledger status filter

  • File: client/src/pages/Fees.tsx:179-187
  • Current: Filter button does window.prompt('Filter by status …'). The free-text answer is logged but never applied. loadData() re-fetches the full set regardless.
  • Desired: Replace prompt with a <select> (all / paid / partial / unpaid) bound to feeStatusFilter state, applied client-side to studentFees. Use the same visual treatment as the existing status pills in the table headers.
  • Acceptance: Picking a status narrows the table live. No window.prompt.

B3. Fees — Hardcoded stat copy and "Grade 10-B" pill

  • File: client/src/pages/Fees.tsx:95, 105, 115, 238
  • Current (all literal strings lying to the user):
    • :95 <span>+12% from last month</span> — should derive from comparing this period's collected to last month's.
    • :105 <span>82% Target achieved</span> — should derive from totalCollected / (totalCollected + totalOutstanding), or drop "Target achieved" entirely if there is no configured target.
    • :115 <span>Review required</span> — remove if no real review queue exists, or replace with partialCases count: \{partialCases} {partialCases === 1 ? 'case' : 'cases'} awaiting review``.
    • :238 <p ...>Grade 10-B</p> — this is the per-row class label; it ignores sf.class_name. Render sf.class_name || '—'.
  • Acceptance: Every one of the four sentences matches the actual data on screen. No literal class name "Grade 10-B" appears in the row.

B4. teacher/MyCourses — Mock class fallback on API failure

  • File: client/src/pages/teacher/MyCourses.tsx:88-92
  • Current: catch block seeds setClasses([{Grade 10-B}, {Grade 11-A}, {Advanced Placement}]). The mock leaks into the "Create New Course" modal's <select>.
  • Desired: Replace the mock with setClasses([]) (or whatever the canonical empty-state in the codebase looks like — check Events.tsx:57, Students.tsx:74, Attendance.tsx:90 for the pattern). The Create-Course modal's <select> should render a single disabled <option>No classes available</option> when empty.
  • Acceptance: Kill the API server mid-test (docker-compose stop server or just point api.get at a dead host). The Create modal does not list fake classes; it shows an empty/disabled state.

B5. dashboard/AdminDashboard — Fake avatar pile

  • File: client/src/pages/dashboard/AdminDashboard.tsx:153-156
  • Current: [1, 2, 3, 4].map(i => <Avatar firstName="Student" />) — every avatar labelled "Student". Decorative.
  • Desired: Pull the top N actual students by outstanding balance. If a backend sort isn't available, fetch users?role=student&limit=4 and render in stats.pendingFees-descending order using the existing Avatar component + initials. If fewer than 4 students exist, render only those that do, then the +N chip. If zero, only the +N chip (or hide the row).
  • Acceptance: Avatars show real student names/initials. No "Student" hardcoded label.

Cross-cutting checks

  • grep -n "82% Target achieved\|+12% from last month\|Review required\|Grade 10-B" client/src/pages/Fees.tsx returns no hardcoded marketing-copy hits (real data values still render).
  • grep -n "Grade 10-B\|General Section" client/src/pages/teacher/MyCourses.tsx — only the legitimate fallback 'General Section' at line 65 remains if you keep it as the subject.class_name || 'General Section' default; remove the mock-array literal.
  • npm run build is green in your worktree.

Manual test

  1. As bursar@school.com (or any fee admin), open /fees. Filter the table by status (B2). Edit a fee group (B1). Verify the four stat cards and the row labels match the data (B3).
  2. As a teacher, open /teacher/courses and click "Create New Course". Kill or block the backend first if you can — see the modal show an empty class list, not fake classes (B4).
  3. As school_admin, open /admin/dashboard. The pending-fees widget shows real students in the avatar pile (B5).

Out of scope here (other tracks)

  • Login, Profile, password resets — see Track A.
  • ExamEditor.tsx and Settings.tsx alert-soundness work — see Track C.
  • General "swap all alert() to toast" sweep across the rest of the app — Track C's C3 lays the foundation.