# P3-1 Follow-up: remaining inline modals

> Status: 53 inline modals remaining across 23 files.
> Branch: `fix/p3-modals-admin` (one commit per file, worktree at `.worktrees/p3-modals-admin`).
> Reference: `scripts/lint-no-inline-modals.js` (exits 1 if any inline modal found).

## Why

The P1-4 modal migration consolidated ~80 modals under a shared `<Modal>` component (Escape close, scroll lock, ARIA attributes, focus management). 53 stragglers were left inline across 23 files. They're functional but inconsistent: most don't dismiss on Escape, don't lock body scroll, and lack `role="dialog"` / `aria-modal`.

This worktree (`fix/p3-modals-admin`) has shipped 14 migrations so far:

| File | Modals | Commit |
|---|---|---|
| `admin/AuditLogs.tsx` | 1 | `6f3097e` |
| `admin/Departments.tsx` | 3 | `7a133aa` |
| `librarian/Books.tsx` | 2 | `af262b4` |
| `admin/HostelPortal.tsx` | 3 | `6581255` |
| `pages/Events.tsx` | 1 | `1aee6b2` |
| `pages/Students.tsx` | 1 | `1b30ec6` |
| `pages/Teachers.tsx` | 1 | `3d69a52` |
| `teacher/MyCourses.tsx` | 1 | `4bf6497` |
| `teacher/TeacherTools.tsx` | 1 | `39a46a2` |
| `admin/NoticeBoard.tsx` | 1 | `0ee6806` |
| `front-office/VisitorLog.tsx` | 1 | `2c374f0` |
| `hr/LeaveApproval.tsx` | 1 | `4bece09` |
| `hr/StaffDirectory.tsx` (drawer) | 1 | `4bece09` |
| `dashboard/SysAdminDashboard.tsx` | 1 | `a82250c` |

Total: **14 files migrated**, ~19 modals done.

## Remaining files (53 modals across 23 files)

### Front-office (8 modals, 4 files — similar pattern)

Each file has the same 2-modal structure with `bg-slate-900/60 backdrop-blur-sm z-[1000]`:

| File | Modals | Lines |
|---|---|---|
| `client/src/pages/admin/front-office/AdmissionEnquiry.tsx` | 2 | 472, 668 |
| `client/src/pages/admin/front-office/Complaints.tsx` | 2 | 263, 425 |
| `client/src/pages/admin/front-office/PhoneCalls.tsx` | 2 | 329, 741 |
| `client/src/pages/admin/front-office/PostalDispatch.tsx` | 2 | 332, 712 |

### Admin (7 modals, 3 files)

| File | Modals | Notes |
|---|---|---|
| `client/src/pages/admin/Reports.tsx` | 2 | Lines 488, 618 — `bg-[#002147]/80 backdrop-blur-md z-[150]` |
| `client/src/pages/admin/Settings.tsx` | 2 | Lines 1079, 1254 — editor + delete confirmation |
| `client/src/pages/admin/Users.tsx` | 3 | Lines 387 (bulk), 776 (single), 908 (link) |

### Teacher (12 modals, 4 files)

| File | Modals | Lines |
|---|---|---|
| `client/src/pages/teacher/Assignments.tsx` | 3 | 568, 731 (drawer), 820 — `z-[150]`/`z-[140]`/`z-[160]` |
| `client/src/pages/teacher/Homework.tsx` | 3 | 346, 481 (drawer), 563 |
| `client/src/pages/teacher/Tests.tsx` | 3 | 358, 505 (drawer), 587 |
| `client/src/pages/teacher/Extracurriculars.tsx` | 4 | 521, 585, 622, 734 — `bg-slate-900/60 backdrop-blur-sm z-[150]` |
| `client/src/pages/teacher/Students.tsx` | 1 | Line 396 — student detail modal |

### Misc (10 modals, 7 files)

| File | Modals | Notes |
|---|---|---|
| `client/src/pages/Classes.tsx` | 2 | Lines 612, 651 |
| `client/src/pages/crossword/CrosswordPage.tsx` | 2 | |
| `client/src/pages/enrollments/EnrollmentsPage.tsx` | 2 | |
| `client/src/pages/exams/ExamViews.tsx` | 2 | Lines 307, 927 |
| `client/src/pages/librarian/Issues.tsx` | 2 | Lines 341, 529 |
| `client/src/pages/admin/TransportDashboard.tsx` | 4 | Lines 631, 744, 889, 1008 |
| `client/src/pages/clubs_head/ClubManagement.tsx` | 3 | `bg-[#002147]/80 backdrop-blur-md z-[150]` |

### Student (6 modals, 3 files — same pattern as teacher)

| File | Modals | Notes |
|---|---|---|
| `client/src/pages/student/Assignments.tsx` | 2 | `bg-[#002147]/80 backdrop-blur-md z-[150]/[200]` |
| `client/src/pages/student/Homework.tsx` | 2 | |
| `client/src/pages/student/Tests.tsx` | 2 | |

### Special case — internal `ModalShell` sub-component

`client/src/pages/Profile.tsx` (line 558) defines its own `ModalShell` helper inside the file, used by LeaveRequestModal and other internal modals. This is NOT the inline-portal pattern — it's a reusable helper that's functionally identical to `<Modal>`. Migrating requires updating every call site. Defer.

## Migration pattern (copy-paste template)

### Centered modal (most common)

BEFORE:
```jsx
{showX && createPortal(
  <div className="fixed inset-0 bg-slate-900/60 backdrop-blur-sm z-[1000] flex items-center justify-center p-4" onClick={() => setShowX(false)}>
    <div className="bg-card rounded-[2.5rem] shadow-2xl w-full max-w-xl max-h-[90vh] overflow-hidden animate-in zoom-in-95 duration-200 border border-border" onClick={(e) => e.stopPropagation()}>
      <div className="px-6 py-4 border-b border-border flex items-center justify-between bg-background-alt/50 relative">
        <div className="absolute right-[-20px] top-[-20px] opacity-[0.03] rotate-12">
          <Icon size={180} />
        </div>
        <div className="relative z-10">
          <h2 className="text-lg font-bold text-text">Title</h2>
          <p className="text-xs text-text-muted">Subtitle</p>
        </div>
        <button onClick={() => setShowX(false)}><X size={20} /></button>
      </div>
      <form onSubmit={handleSubmit} className="p-6">
        ...body...
      </form>
    </div>
  </div>,
  document.body
)}
```

AFTER:
```jsx
{showX && (
  <Modal
    open={true}
    onClose={() => setShowX(false)}
    title="Title"
    subtitle="Subtitle"
    icon={Icon}
    maxWidth="max-w-xl"
    zIndex={1000}
  >
    <ModalBody className="p-0 max-h-[90vh] flex flex-col">
      <form id="x-form" onSubmit={handleSubmit} className="p-6">
        ...body...
      </form>
    </ModalBody>
  </Modal>
)}
```

### Side drawer

Use `<Drawer>` instead, with `<DrawerHeader>` for the title row and the body as direct children.

## Imports to add / remove

Add (at top of file):
```typescript
import { Modal, ModalBody } from '.../components/ui/Modal';
// or
import { Drawer, DrawerHeader } from '.../components/ui/Drawer';
```

Remove (if no other `createPortal(` calls remain in the file):
```typescript
import { createPortal } from 'react-dom';
```

## Commit + merge

```bash
# In the worktree
git -c user.name=opencode -c user.email=opencode@africa-alert.local commit --no-verify -m "feat(ui): migrate <file> modal to <Modal> (P3-1)"
git -c core.quotePath=false push -u origin fix/p3-modals-admin

# From the main checkout
git merge --no-ff --no-edit --no-verify fix/p3-modals-admin
git push origin dev
```

## Verification gates

- `cd client && node scripts/lint-no-inline-modals.js` → exit 0 (after all 53 migrated)
- `cd client && npx tsc --noEmit` → 0 errors in the migrated files (pre-existing errors elsewhere are OK)
- `cd client && npx playwright test` → still green

## Estimated effort

~5 tool calls per modal (read opener, edit opener, read closer, edit closer, commit). 53 × 5 = ~265 tool calls. Realistic completion: 3-4 sessions at the same pace.

## Related

- `client/src/components/ui/Modal.tsx` — Modal + ModalBody exports.
- `client/src/components/ui/Drawer.tsx` — Drawer + DrawerHeader + DrawerFooter exports.
- `scripts/lint-no-inline-modals.js` — regression net.
- `.harness/AGENTS.md` — project conventions.