cleanup phases 6
This commit is contained in:
@@ -0,0 +1,284 @@
|
||||
# UI Phase 11 — Admin & partner console
|
||||
|
||||
> **Mission:** the backoffice has a real primitive layer — `AdminDataTable`, `ConfirmDialog`, `AdminPager`,
|
||||
> the four-state list pattern — under daily-use ergonomics gaps: no URL-synced state (back/refresh loses
|
||||
> the queue), audited actions targeting people by hand-typed numeric ID, a trust queue you can't search,
|
||||
> a ticket console that can never close a case, Gregorian date inputs in a Shamsi product, and a partner
|
||||
> portal showing raw English `snake_case` statuses to Persian center staff. Make the console **fast and
|
||||
> safe** for the ops desk and the portal **professional** — while keeping the density of a work tool.
|
||||
>
|
||||
> **Track:** frontend · **Depends on:** [Phases 0–2](ui-phase-2-shells-and-navigation.md) ·
|
||||
> **Unlocks:** the ops desk gets speed and safety, partners get a professional portal
|
||||
>
|
||||
> **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md) and invoke the frontend-designer skill — both are mandatory.**
|
||||
|
||||
## 1. Context — where this sits
|
||||
|
||||
Trust, money, and dispute resolution all converge on `/admin`; the partner portal is the only surface an
|
||||
external business ever sees. The audit found the composite layer unusually good (one table, one pager, one
|
||||
confirm pattern, zero hard-coded hexes, Shamsi/Toman formatting throughout) and everything *around* it
|
||||
thin. Phase 2 replaced the starter shell (dense chrome, working sidebar active-state, locale-aware nav);
|
||||
phase 1 shipped PageHeader, JalaliDatePicker, StatusChip v2. This phase is the **workflow layer**. All
|
||||
defects below were re-verified in code on 2026-07-16:
|
||||
|
||||
- **No URL-synced list state anywhere.** `admin/tickets/page.tsx:39-41` holds applied filters + page
|
||||
in `useState` (every queue page does); detail pages hand-roll `router.push` "back" buttons that land
|
||||
on page 1 (`tickets/[id]/page.tsx:90`).
|
||||
- **Rows beyond page 1 are unreachable on config and holidays.** `admin/config/page.tsx:63`
|
||||
(`usePlatformConfigs(1)`), `:182` (history drawer), and `admin/holidays/page.tsx:38`
|
||||
(`useHolidays({}, 1)`) are hard-wired to page 1 with no pager — later rows are invisible/uneditable.
|
||||
- **Raw-ID targeting on audited actions.** `admin/roles/page.tsx:176-182` grants roles via a bare
|
||||
`type="number"` TextField; same pattern for partner-center admin and sponsored-nurse assignment
|
||||
(`partners/page.tsx`, `partners/[id]/page.tsx`). One mistyped digit grants `super_admin` to a
|
||||
stranger — and `admin/users/page.tsx` is a `PlaceholderScreen`, so there's nowhere to look an ID up.
|
||||
Related: alert assign-to-self silently falls back to user #1 (`admin/alerts/page.tsx:36` —
|
||||
`const meId = authState.currentUser?.id ?? 1;`).
|
||||
- **The flagship trust queue can't search or prioritize.** `AdminVerificationQueueFilters` is
|
||||
`{ status?: 'pending' | 'in_review' }` (`services/verification/types.ts:177-179`) — no name/phone
|
||||
search, no counts, no age signal; `AdminDataTable.tsx` has no sort affordance at all.
|
||||
- **A resolved ticket can never leave the queue.** `services/tickets/hooks/` has **no
|
||||
close/reopen/assign mutation**; the thread header (`tickets/[id]/page.tsx:118-127`) shows read-only
|
||||
chips. The message list (`:150`) opens scrolled to the oldest message; internal-note mode (`:163-190`)
|
||||
is only a small toggle — composer and send button look identical in both modes.
|
||||
- **UTC off-by-one on payout window defaults.** `admin/payouts/page.tsx:56` uses
|
||||
`d.toISOString().slice(0, 10)` — near Tehran midnight the prefilled window is yesterday. The final
|
||||
run confirm (`:365-371`) is generic copy with no money-movement summary.
|
||||
- **Partners see raw wire codes.** `partner/bookings/page.tsx:17,45,66-69` renders
|
||||
`pending_payment`/`in_progress` literally in the filter menu and table chip — untranslated,
|
||||
un-StatusChip'd, shown to external Persian-speaking staff.
|
||||
- Small verified cleanups: dead ternary `config/page.tsx:152` (both branches `'text'`); misleading
|
||||
`holidays/page.tsx:106` (`TODAY_ISO = ''` claims to be seeded — the field starts blank); static
|
||||
chevron, no `aria-expanded` on `AuditLogRow.tsx`; pager indicator has no total (callers pass only
|
||||
`{ page }`, e.g. `partner/bookings/page.tsx:101`, though every caller computes `pageCount`).
|
||||
|
||||
**What already exists (do not rebuild):**
|
||||
|
||||
- The composite layer in `client/src/components/admin/` — `AdminDataTable`, `AdminPageHeader`,
|
||||
`AdminPager`, `AdminEmptyState`, `AdminErrorState`, `ConfirmDialog` — used by every console and
|
||||
unit-tested. **Restyle/extend these; never fork per-page markup.**
|
||||
- The domain composites: `RefundPanel`, `DocumentViewer` (signed URLs + expired→re-request),
|
||||
`AdminMessageBubble`, `SupportAlertCard`, `ConfigRow`, `AuditLogRow`, `PartnerSettlementRow`.
|
||||
- The draft-vs-applied filter pattern on tickets/audit (typing never refetches; Apply commits the key)
|
||||
and the four-state list pattern (skeleton → error-with-retry → empty → table) on every list page.
|
||||
- Phase 1's `PageHeader`, `JalaliDatePicker`, `StatusChip`; phase 2's admin chrome + locale-aware nav.
|
||||
- The URL-as-filter-carrier pattern proven in `client/src/services/search/filterParams.ts` (C1 writes
|
||||
the URL, C2 reads it back into the query-key object) — the model for 3.1.
|
||||
|
||||
## 2. Required reading (do this first)
|
||||
|
||||
- [audit/admin-partner.md](audit/admin-partner.md) — the full evidence: 20 problems, 11 opportunities,
|
||||
the keep-list §5 restates.
|
||||
- [../../../.claude/skills/frontend-designer/SKILL.md](../../../.claude/skills/frontend-designer/SKILL.md) —
|
||||
invoke the skill; its token/wrapper/icon/RTL rules bind every deliverable below.
|
||||
- Code, in this order: `client/src/components/admin/` (all of it); `admin/tickets/page.tsx` +
|
||||
`tickets/[id]/page.tsx` (draft-vs-applied + the thread); `admin/verification/page.tsx` +
|
||||
`verification/[nurseId]/page.tsx` (the case-view bones: StepCard, DocumentViewer, credential form);
|
||||
`admin/{payouts,roles,partners,alerts,config,holidays,audit}/…`; the three `partner/*` pages;
|
||||
`services/search/filterParams.ts`.
|
||||
- Service seams you'll touch: `services/{tickets,verification,payouts,admin,partnerCenter}` — note
|
||||
which are mock-primary (`USE_*_MOCK` in each domain's `constants.ts`) before wiring anything.
|
||||
- `client/CLAUDE.md` "Golden rules" + Project Structure; `product/business/` for verification/payout
|
||||
business rules (never infer them from code).
|
||||
- [../../shared-working-context/frontend/requests/for-backend.md](../../shared-working-context/frontend/requests/for-backend.md)
|
||||
— REQ-028's delivery note matters for 3.4 (admin queue unread is explicitly served as `0`).
|
||||
|
||||
## 3. Scope — build this
|
||||
|
||||
### 3.1 `useAdminListState` — URL-synced worklist state, adopted everywhere
|
||||
|
||||
Build `client/src/hooks/useAdminListState.ts` (+ co-located test): a small generic hook that mirrors
|
||||
**applied** filters + page into `searchParams` (read on mount → initial state; `router.replace` on
|
||||
apply/page-change) the way `services/search/filterParams.ts` already proves. Draft filter state stays
|
||||
local — only *applied* state hits the URL. Adopt it on **all** admin queue pages — tickets, audit,
|
||||
verification, reviews, payouts, partners, alerts, holidays, config — and the three partner lists.
|
||||
Detail-page "back" becomes a real `router.back()` (falling back to the list route), so queue position
|
||||
survives back/refresh/share-a-link. While you're in there, **fix the hard-wired page-1 reads**: give
|
||||
config, its history drawer, and holidays real page state + `AdminPager` (the hooks already accept a
|
||||
page argument — callers pass `1`).
|
||||
|
||||
### 3.2 Kill raw-ID targeting — `UserPicker` / `NursePicker`
|
||||
|
||||
Build `client/src/components/admin/UserPicker/` (and a thin `NursePicker` variant), an async MUI
|
||||
`Autocomplete` that searches by name/phone and renders **name + masked phone + `#id`** per option. Drop
|
||||
it into role grants (`roles/page.tsx`), partner-center admin assignment (`partners/page.tsx`),
|
||||
sponsored-nurse assignment (`partners/[id]/page.tsx`), and alert assignment. The resolved **name** is
|
||||
echoed into the `ConfirmDialog` body (e.g. «اعطای نقش مدیر مالی به مریم احمدی (۰۹۱۲***۴۵۶۷)؟») — never
|
||||
just `#42`. `services/admin` has **no user-search endpoint** (verified) — file the REQ (§4) and back
|
||||
the picker with a `mockApi` implementation behind the existing seam.
|
||||
|
||||
**Fix the alert assign-to-self fallback:** `alerts/page.tsx:36` must never default to user `1`. Disable
|
||||
"assign to me" until `authState.currentUser?.id` is hydrated (tooltip: «در حال بارگذاری حساب شما…»).
|
||||
|
||||
### 3.3 Verification desk — the flagship trust queue
|
||||
|
||||
- **Status tabs with counts** («در انتظار (۱۲)» / «در حال بررسی (۴)») replacing the lone select — counts
|
||||
need the queue read extended (REQ, §4); render the tabs without counts until it lands.
|
||||
- **Name/phone search** — `AdminVerificationQueueFilters` has only `status`; add the search field behind
|
||||
the draft-vs-applied pattern and file the REQ for the query param.
|
||||
- **Waiting-time column** — client-computed age from the served `submittedAt` (display-only): relative
|
||||
Shamsi age with SLA coloring (`--bal-warning` past 48h, `--bal-error` past 96h; named constants).
|
||||
- **Keyboard-friendly next-case flow** — from `verification/[nurseId]`, «پرونده بعدی»/«پرونده قبلی»
|
||||
affordances (+ arrow-key bindings) walking the current queue order via the 3.1 URL state — a reviewer
|
||||
never round-trips to the list between cases. A full split-pane case view is **(DEFERRED —
|
||||
post-chain)**: next/prev delivers the throughput win at a fraction of the layout risk.
|
||||
- `DocumentViewer`'s signed-URL flow (on-demand fetch, expired→re-request) is **do-not-regress**.
|
||||
|
||||
### 3.4 Ticket console — lifecycle + a safe composer
|
||||
|
||||
- **Close/reopen + assign-to-me on the thread header.** The b15 contract exposes no close mutation
|
||||
(verified: `services/tickets/hooks/` has none). Add `useCloseTicket`/`useReopenTicket`/
|
||||
`useAssignTicket` hooks with full `mockApi` implementations and `clientApi` mapped to the proposed
|
||||
routes; file the REQ (§4). On the real path, gate the controls behind a `TICKET_LIFECYCLE_ENABLED`
|
||||
constant in `services/tickets/constants.ts` — never show a button that can only 404. Close is
|
||||
`ConfirmDialog`-guarded.
|
||||
- **Queue columns: unread + last-activity.** REQ-028 delivered `unreadCount`/`lastMessageAt` for the
|
||||
*user* list, but the admin queue is explicitly served `unreadCount = 0` (tracker delivery note).
|
||||
[Phase 10](ui-phase-10-messaging-and-notifications.md) owns filing the admin-side extension —
|
||||
**reference its REQ, don't double-file**; render the columns when present, fall back to `createdAt`.
|
||||
- **Scroll-to-latest on open** — anchor the `:150` message Box to the newest message on thread load.
|
||||
- **Internal-note mode made unmistakable:** when `internal` is active, the composer Paper gets an
|
||||
amber surface (`--bal-warning` soft, both schemes) and the send button label swaps to
|
||||
«ثبت یادداشت داخلی» — the safety cue lives *on the action*, not only in the toggle above.
|
||||
|
||||
### 3.5 Money-desk safety
|
||||
|
||||
- **Payout run confirm shows the movement summary:** batch total (Toman via the shared formatter),
|
||||
nurse count, and processing date — all from the server preview the dialog already fetched — plus a
|
||||
**typed confirmation** (type «تایید» or the exact amount) enabling the final-run confirm button: the
|
||||
standard guard for an irreversible money movement.
|
||||
- **Fix the UTC off-by-one:** replace `payouts/page.tsx:56`'s `toISOString().slice(0,10)` with
|
||||
local-date formatting (and adopt phase 1's `JalaliDatePicker` for the window inputs — 3.6).
|
||||
- **Reconcile/retry polish:** transfer-reference entry stays `dir="ltr"`; failed payout rows get a
|
||||
visible failure reason and a retry affordance through `useRetryPayout`; localize skipped-reason
|
||||
strings if served as codes (raw free-text reasons stay `dir="ltr"` as today).
|
||||
|
||||
### 3.6 Primitives v2 (extend, never fork)
|
||||
|
||||
- **`AdminDataTable`:** optional per-column server-param sort (a `sort` callback + direction chevron —
|
||||
the filter object is already the query key, so sort is just another applied param through 3.1);
|
||||
sticky header for long pages; per-column `minWidth`; a footer line «نمایش ۱–۲۰ از ۱۲۴» — callers
|
||||
already hold `total`. Keep `align: 'inherit'` (RTL-safe) and the horizontal-scroll container.
|
||||
- **`AdminPager`:** take `total`, restore «صفحه {page} از {total}» in the admin i18n namespace (the
|
||||
non-admin namespace kept it), locale digits via the existing formatting utils. Update both tests.
|
||||
- **Detail headers:** unify the four divergent patterns (verification case = `AdminPageHeader` + back;
|
||||
ticket thread = h6-in-Paper; payout batch = raw h5; partner-center detail = h5 + a back button
|
||||
misusing the `partners` icon) onto phase 1's shared `PageHeader` (title, back affordance, chips
|
||||
slot). If `PageHeader` lacks a slot, extend it minimally and note it (ownership rule — no fork).
|
||||
- **Jalali date inputs everywhere:** adopt phase 1's `JalaliDatePicker` (Shamsi display, ISO-Gregorian
|
||||
wire value) for audit from/to, payout windows, holiday date, and credential issued/expires
|
||||
(`verification/[nurseId]/page.tsx`). No native `type="date"` remains under `/admin`.
|
||||
- **`AuditLogRow`:** rotate the chevron on expand, add `aria-expanded` + button semantics; resolve
|
||||
actor IDs to names via 3.2's batch id→label lookup (same REQ), falling back to `#id` until it lands.
|
||||
|
||||
### 3.7 Partner portal — professional, light-touch
|
||||
|
||||
- **Localize booking statuses** (the worst partner-facing defect, verified at
|
||||
`partner/bookings/page.tsx:17,45,66-69`): map the seven codes to `StatusChip` kinds + fa/en labels in
|
||||
both the filter menu and the table chip. No raw wire code ever reaches a partner's screen again.
|
||||
- **Scoped read-only booking detail:** link each sponsored-booking row to a summary detail (dates,
|
||||
status timeline, patient display name — no clinical data). `services/partnerCenter` has only the
|
||||
list read — file the REQ; build the detail mock-tolerant behind the seam.
|
||||
- **CSV export on settlement** — client-side, current result set, UTF-8 **with BOM** (so Excel renders
|
||||
Persian correctly) + CRLF; a small `toCsv` util with a co-located test — for the center's accountant.
|
||||
- **Portal identity in the chrome:** carry the center's name + MoR state persistently across portal
|
||||
pages (home already shows the MoR `StatusChip`; lift a compact identity block via `useMyPartnerCenter`).
|
||||
If this touches `layout/`, extend phase 2's shell minimally and note it.
|
||||
|
||||
### 3.8 Dead ends & honest nav
|
||||
|
||||
- **Users console:** replace `admin/users/page.tsx`'s placeholder with a **read-first directory**
|
||||
(search by phone/name via the 3.2 endpoint, role chips, links into audit/tickets) **only if** the
|
||||
user-search REQ is granted during this phase. Otherwise **hide the nav entry** (same rule phase 10
|
||||
applies to admin notifications) and say so honestly in the report — a production nav ships no
|
||||
placeholder dead-ends.
|
||||
- Small verified cleanups: delete the dead ternary at `config/page.tsx:152`; fix the misleading
|
||||
`TODAY_ISO` constant/comment at `holidays/page.tsx:106` (seed today's local date or drop the lie).
|
||||
|
||||
## 4. Mocks & seams in this phase
|
||||
|
||||
No new seams. Every backend gap becomes a REQ appended to
|
||||
[../../shared-working-context/frontend/requests/for-backend.md](../../shared-working-context/frontend/requests/for-backend.md)
|
||||
— **REQ-001…038 are taken; check the tracker's current tail (other UI phases may have appended) and
|
||||
number onward.** File, at minimum: **(1) admin user lookup** — search by name/phone →
|
||||
`{ id, displayName, maskedPhone, roles }` + a batch id→label endpoint (powers 3.2 and 3.6);
|
||||
**(2) verification queue enrichment** — name/phone search + per-status counts (3.3), optionally a
|
||||
`submittedAt` sort param; **(3) ticket lifecycle mutations** — close/reopen + assign (3.4);
|
||||
**(4) partner scoped booking detail** — read-only summary (3.7). For each: build the UI complete against
|
||||
the domain's `mockApi`, map `clientApi` to the proposed route, and gate real-path controls that would
|
||||
otherwise 404. Do **not** re-file phase 10's admin ticket-queue enrichment — reference it.
|
||||
|
||||
## 5. Critical rules you must not get wrong
|
||||
|
||||
- **Server-authority posture stays.** `useAdminCapabilities` flags only *hide* controls; money is never
|
||||
recomputed client-side (payout eligibility and the holiday shift come from the server; the 3.5
|
||||
summary re-renders served preview numbers, never sums).
|
||||
- **`ConfirmDialog` on every audited/irreversible action** — approve/reject, moderate, revoke,
|
||||
run/retry payout, resolve alert, close ticket — with required-reason gating where it exists today,
|
||||
loading that disables both buttons, destructive color. The 3.2 pickers make its body *human*.
|
||||
- **`is_internal` never enters user-facing types** — the admin-only typing boundary from f14/f15 stays;
|
||||
3.4's composer work touches presentation only.
|
||||
- **Draft-vs-applied filtering stays** — typing never refetches; Apply commits the query key (and now
|
||||
the URL). 3.1 syncs *applied* state only.
|
||||
- **Density is a feature.** Keep `size="small"`, dense tables, tight vertical rhythm — no consumer-app
|
||||
whitespace or hero moments in a worklist tool.
|
||||
- **PII discipline stays:** write-then-masked settlement IBAN, never-echoed credential numbers, signed
|
||||
document URLs, non-leaking partner access-denied state, masked phones in the new pickers.
|
||||
- Design contract: i18n in **both** catalogs; tokens not hexes (`--bal-*` in both scheme blocks); RTL
|
||||
logical props (`borderInlineStart`, `align: 'inherit'`, `dir="ltr"` islands for IBANs/references);
|
||||
dark mode by construction; MUI v9 API only; co-located tests for every shared component/hook touched;
|
||||
`clientFetch`/cookie rules untouched.
|
||||
|
||||
## 6. Definition of Done
|
||||
|
||||
On top of the shared [definition-of-done.md](../../phases/_shared/definition-of-done.md):
|
||||
|
||||
- [ ] `npm run check` green; `npm run test:ci` green including updated tests for `AdminDataTable`,
|
||||
`AdminPager`, `UserPicker`, `useAdminListState`, and the CSV util; `en.json`/`fa.json` in sync.
|
||||
- [ ] Filter + page state on every admin/partner queue survives refresh, browser back from a detail
|
||||
page, and a pasted URL; config and holidays can page past page 1.
|
||||
- [ ] No `type="number"` user-ID input remains on any audited action; every confirm body names the
|
||||
resolved person; alert assign-to-me can never target user #1.
|
||||
- [ ] Verification queue: search field + SLA-colored waiting-time column render; next/prev case
|
||||
navigation works. Ticket thread: opens scrolled to the latest message; internal mode shows the
|
||||
amber composer and «ثبت یادداشت داخلی» send label; close/reopen works on the mock path.
|
||||
- [ ] Payout run confirm shows batch total/count/date and requires the typed confirmation; window
|
||||
defaults are correct at local midnight (no UTC drift).
|
||||
- [ ] Partner bookings show localized `StatusChip` statuses in filter and table; settlement exports a
|
||||
CSV that opens correctly in Excel with Persian text.
|
||||
- [ ] No placeholder screen is reachable from the admin nav; REQs filed with correct sequential
|
||||
numbers, no duplicate of phase 10's REQ.
|
||||
- [ ] Visual verification on all four axes (`/fa` + `/en` × light + dark), desktop-first (this is a
|
||||
desk tool) with a mobile sanity pass on the partner portal.
|
||||
|
||||
## 7. How to test (what a human can verify after this phase)
|
||||
|
||||
1. `/fa/admin/tickets`: apply a status filter + go to page 2 → open a ticket → browser back → filter
|
||||
and page intact; paste the list URL into a new tab → same view.
|
||||
2. `/fa/admin/config`: page past page 1 (mock or seeded rows) → edit a row from page 2 → saves; the
|
||||
history drawer pages too.
|
||||
3. `/fa/admin/roles` → «اعطای نقش»: type a partial name → options show name + masked phone + id →
|
||||
pick one → the ConfirmDialog names the person, not a number.
|
||||
4. `/fa/admin/alerts` before `/me` hydrates (throttle the network): "assign to me" is disabled.
|
||||
5. `/fa/admin/verification`: search a seeded nurse by name → row found; waiting-time column shows
|
||||
amber/red for old cases; open a case → «پرونده بعدی» walks the queue without returning to the list.
|
||||
6. Open a long ticket thread → scrolled to the newest message; internal mode → composer turns amber,
|
||||
send button reads «ثبت یادداشت داخلی»; close (mock path) → it leaves the open queue; reopen restores.
|
||||
7. `/fa/admin/payouts`: window defaults match today's local date; preview → run → the confirm shows
|
||||
total/count/date and stays disabled until «تایید» (or the amount) is typed.
|
||||
8. `/fa/admin/audit`: pick from/to with the Jalali picker (no Gregorian native input); expand a row →
|
||||
chevron rotates, `aria-expanded` toggles, actor shows a name (or `#id` fallback).
|
||||
9. `/fa/partner/bookings`: statuses render as Persian `StatusChip`s in table and filter;
|
||||
`/fa/partner/settlement` → «خروجی CSV» opens in Excel with correct Persian.
|
||||
10. Every pager/footer reads «صفحه ۲ از ۷» / «نمایش ۱–۲۰ از ۱۲۴» with locale digits; verify the four
|
||||
axes; no admin nav item leads to a placeholder.
|
||||
|
||||
## 8. Hand off & document (close the phase)
|
||||
|
||||
- Update `client/CLAUDE.md` (Project Structure) for new files: `useAdminListState`, `UserPicker`/
|
||||
`NursePicker`, the CSV util, any `PageHeader` extension, hidden/added routes.
|
||||
- Write the report at `dev/shared-working-context/reports/ui-phase-11-report.md`: what shipped, the
|
||||
exact REQ numbers filed, which controls are gated awaiting delivery, any minimal foundation
|
||||
extensions to phase 1/2 files, and the users-console decision (built vs nav hidden).
|
||||
- Save a memory note per operating-rules §8: URL-synced admin list state, the picker pattern replacing
|
||||
raw IDs, the ticket-lifecycle gating constant, the REQ numbers — phase 12 (copy/motion) sweeps these
|
||||
surfaces.
|
||||
Reference in New Issue
Block a user