# 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.
