# 2026-07-17 — P1-4 modal migration: finance/* (Track A continuation)

Branch: `feat/p1-4-modals-finance`
Worktree: `.worktrees/feat-p1-4-modals-finance`

## Why

P1-4 in the readiness audit calls for migrating every inline modal
to the shared `<Modal>` component Track A introduced (in
`client/src/components/ui/Modal.tsx`). BankingAccounts.tsx already
demonstrates the pattern; this PR applies the same shape across the
`finance/*` slice.

## Changes

### Migrated modals (9 of 22 in `finance/*`)

| File | Modals migrated |
|---|---|
| `client/src/pages/finance/Trips.tsx` | 2 — `TripEditor` (8-field form), `confirmDelete` confirmation |
| `client/src/pages/finance/TripDetail.tsx` | 2 — `Record payment` form, `Assign students` form |
| `client/src/pages/finance/ChartOfAccounts.tsx` | 2 — `confirmDelete` confirmation, `ChartEditor` form |
| `client/src/pages/finance/Discounts.tsx` | 3 — `confirmRevoke`, `DiscountEditor`, `ApplyDiscountModal` |

Each migration follows the Track A pattern:

```tsx
<Modal
  open={isOpen}
  onClose={onClose}
  title="…"
  icon={Icon}
  maxWidth="max-w-md"
  footer={<><button>Cancel</button><button>Save</button></>}
>
  <ModalBody>
    {/* was the inline form */}
  </ModalBody>
</Modal>
```

`<Modal>` already provides the backdrop-blur, click-outside-to-close,
Escape-to-close, scroll-lock, and rounded-2xl premium styling
(see `modals.md` §1). Forms retain `<form id=…>` semantics with the
submit button using `form="…"` so the layout can keep the action row
in the `footer` slot.

### `scripts/lint-no-inline-modals.js` (new)

Tiny Node script that walks `client/src/pages/**`, counts every
`fixed inset-0 … backdrop-blur-sm …` pattern, and exits non-zero if
any remain. Run via `node scripts/lint-no-inline-modals.js`. Used as a
regression net so future PRs that re-introduce an inline modal show
up here.

### Out of scope for this slice

The remaining 13 inline modals in `finance/*` (per lint) live in:

- `Expenses.tsx` (2)
- `FeePlans.tsx` (2)
- `Suppliers.tsx` (2)
- `StudentGroups.tsx` (3)
- `Invoices.tsx` (1 — a right-side detail drawer, not a centered
  modal; would change UX if ported as-is)
- `PayrollRuns.tsx` (1)
- `Payslips.tsx` (1)
- `StudentGroupDetail.tsx` (1)

These land in the **next** PR that picks up the `finance/*` slack.
The other 5 file-owner PRs from the original P1-4 list
(`Fees.tsx`, `HRManagement.tsx`, `Inventory.tsx`, `Library.tsx`,
`parent/AcademicProgress.tsx`) land separately.

## Verification

- `node scripts/lint-no-inline-modals.js` → before: 22 in `finance/*`,
  after: 13 in `finance/*`. (9 migrated.)
- Manual diff: every inline `<div className="fixed inset-0 …">` in
  the 4 migrated files now wraps `<Modal>` + `<ModalBody>`. No
  controller-level or store-level changes touched.
