22 KiB
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 Englishsnake_casestatuses 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 · 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-41holds applied filters + page inuseState(every queue page does); detail pages hand-rollrouter.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), andadmin/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-182grants roles via a baretype="number"TextField; same pattern for partner-center admin and sponsored-nurse assignment (partners/page.tsx,partners/[id]/page.tsx). One mistyped digit grantssuper_adminto a stranger — andadmin/users/page.tsxis aPlaceholderScreen, 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.
AdminVerificationQueueFiltersis{ status?: 'pending' | 'in_review' }(services/verification/types.ts:177-179) — no name/phone search, no counts, no age signal;AdminDataTable.tsxhas 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:56usesd.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-69renderspending_payment/in_progressliterally 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'); misleadingholidays/page.tsx:106(TODAY_ISO = ''claims to be seeded — the field starts blank); static chevron, noaria-expandedonAuditLogRow.tsx; pager indicator has no total (callers pass only{ page }, e.g.partner/bookings/page.tsx:101, though every caller computespageCount).
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 threepartner/*pages;services/search/filterParams.ts. - Service seams you'll touch:
services/{tickets,verification,payouts,admin,partnerCenter}— note which are mock-primary (USE_*_MOCKin each domain'sconstants.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 search —
AdminVerificationQueueFiltershas onlystatus; 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-warningpast 48h,--bal-errorpast 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). AdduseCloseTicket/useReopenTicket/useAssignTickethooks with fullmockApiimplementations andclientApimapped to the proposed routes; file the REQ (§4). On the real path, gate the controls behind aTICKET_LIFECYCLE_ENABLEDconstant inservices/tickets/constants.ts— never show a button that can only 404. Close isConfirmDialog-guarded. - Queue columns: unread + last-activity. REQ-028 delivered
unreadCount/lastMessageAtfor the user list, but the admin queue is explicitly servedunreadCount = 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 tocreatedAt. - Scroll-to-latest on open — anchor the
:150message Box to the newest message on thread load. - Internal-note mode made unmistakable: when
internalis active, the composer Paper gets an amber surface (--bal-warningsoft, 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'stoISOString().slice(0,10)with local-date formatting (and adopt phase 1'sJalaliDatePickerfor 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 throughuseRetryPayout; localize skipped-reason strings if served as codes (raw free-text reasons staydir="ltr"as today).
3.6 Primitives v2 (extend, never fork)
AdminDataTable: optional per-column server-param sort (asortcallback + 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-columnminWidth; a footer line «نمایش ۱–۲۰ از ۱۲۴» — callers already holdtotal. Keepalign: 'inherit'(RTL-safe) and the horizontal-scroll container.AdminPager: taketotal, 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 thepartnersicon) onto phase 1's sharedPageHeader(title, back affordance, chips slot). IfPageHeaderlacks 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 nativetype="date"remains under/admin. AuditLogRow: rotate the chevron on expand, addaria-expanded+ button semantics; resolve actor IDs to names via 3.2's batch id→label lookup (same REQ), falling back to#iduntil 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 toStatusChipkinds + 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/partnerCenterhas 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
toCsvutil 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 viauseMyPartnerCenter). If this toucheslayout/, 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 misleadingTODAY_ISOconstant/comment atholidays/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
— 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.
useAdminCapabilitiesflags 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). ConfirmDialogon 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_internalnever 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 checkgreen;npm run test:cigreen including updated tests forAdminDataTable,AdminPager,UserPicker,useAdminListState, and the CSV util;en.json/fa.jsonin 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
StatusChipstatuses 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)
/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./fa/admin/config: page past page 1 (mock or seeded rows) → edit a row from page 2 → saves; the history drawer pages too./fa/admin/roles→ «اعطای نقش»: type a partial name → options show name + masked phone + id → pick one → the ConfirmDialog names the person, not a number./fa/admin/alertsbefore/mehydrates (throttle the network): "assign to me" is disabled./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.- 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.
/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./fa/admin/audit: pick from/to with the Jalali picker (no Gregorian native input); expand a row → chevron rotates,aria-expandedtoggles, actor shows a name (or#idfallback)./fa/partner/bookings: statuses render as PersianStatusChips in table and filter;/fa/partner/settlement→ «خروجی CSV» opens in Excel with correct Persian.- 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, anyPageHeaderextension, 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.