Files
baya-monorepo/dev/post-phase/ui/ui-phase-11-admin-and-partner-console.md
T
2026-07-17 13:22:04 +03:30

22 KiB
Raw Blame History

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 02 · Unlocks: the ops desk gets speed and safety, partners get a professional portal

Before you start, read ../../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:36const 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 — the full evidence: 20 problems, 11 opportunities, the keep-list §5 restates.
  • ../../../.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 — 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 searchAdminVerificationQueueFilters 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 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.mdREQ-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:

  • 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 StatusChips 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.