# Frontend Phase 15 (b15) — Admin backoffice & partner-center consoles — report **Date:** 2026-07-10 · **Track:** frontend · **Status:** ✅ complete — **this is the final frontend phase; MVP is complete.** **Gate:** `npm run check` green (0 errors / 0 warnings) · `npm run test:ci` green (**325 tests, +36**) · `npm run build` green (all 18 new routes compile) · `admin`/`partner` i18n in sync · i18n key audit clean. --- ## 1. What was built The internal **operational cockpit** that runs the marketplace: a role-gated **admin backoffice** in the f0 desktop sidebar shell, plus a separately-scoped **partner-center portal**. All of it is a *read-and-act* surface over data other domains own — the client renders the contract's values and issues the sanctioned commands; it **never** computes eligibility, money decomposition, holiday shifts, the `is_verified` flip, or the review aggregate (§5). ### Two new domain services (the `auth`-service shape) - **`services/admin`** — config, holidays, audit, support-alerts, RBAC. 12 hooks; filters + page in every key so each worklist filter/page caches independently. Mock-primary. - **`services/partnerCenter`** — admin-side center management + the center-scoped portal reads. 12 hooks. Mock-primary. ### Admin-endpoint additions to 5 existing domains (the staff lens — *not* new domains) - **verification** (b6): `useVerificationQueue` / `useVerificationCase` / `useVerificationDocumentUrl` (on-demand signed URL) / `useDecideStep` / `useApproveVerification` / `useRejectVerification`. - **refunds** (b11): `useRefundPreview` / `useInitiateRefund` / `useApproveRefund` / `useRejectRefund` (ticket-linked). - **payouts** (b13): `usePayoutBatches` / `usePayoutBatchDetail` / `usePreviewPayoutBatch` / `useRunPayoutBatch` (idempotency-keyed) / `useRetryPayout` / `useRecordTransferReference`. - **reviews** (b14): `useModerationQueue` / `useModerateReview`. - **tickets** (b15): `useAdminTickets` / `useAdminTicket` / `useAdminTicketThread` / `usePostAdminMessage`. The **admin** ticket types carry `isInternal`; the **user-app** types deliberately do not — the two surfaces stay distinct so an internal note can never bleed into a user view (§5). ### Shared composites — `@/components/admin` (13, each with a co-located `*.test.tsx`; 36 tests) `AdminPageHeader`, `AdminEmptyState`, `AdminErrorState`, `AdminPager`, `ConfirmDialog` (optional required-reason), `AdminDataTable` (generic dense worklist table, horizontal-scroll-contained), `ConfigRow`, `AuditLogRow` (expandable diff), `SupportAlertCard`, `PartnerSettlementRow` (reconciling commission/VAT breakdown via the f0 `PriceBreakdown`), `DocumentViewer` (signed-URL on-demand + expired→re-request), `RefundPanel` (server-computed preview → initiate → provider-failure retry / reject), `AdminMessageBubble` (internal-note styled distinctly). ### Screens (18 new routes) - **Admin** (`/admin/*`, role-gated): overview landing · verification (queue + `[nurseId]` case) · tickets (queue + `[id]` thread w/ internal-note composer + RefundPanel) · payouts (dashboard + `[batchId]` detail) · reviews (moderation) · config · holidays · alerts · audit · partners (list + `[id]` detail) · roles (**DEFERRED-IF-MISSING**). - **Partner portal** (`/partner/*`, separate scope): home · nurses · bookings · settlement (MoR-gated). ### Foundation - **`useAdminCapabilities()`** (`@/hooks`) — a memoized selector over the session's fine-grained `roleCodes` (added to `SessionUser`, hydrated from `/me` by `useSessionRoleSync`). Drives per-console `can*` booleans that hide/disable controls a role can't use (server still authorizes every command). - **`PartnerLayout`** + the `(private-routes)/partner/` route group (a separate authz scope). - The `admin` + `partner` i18n namespaces in both locales (incl. پروانه تأسیس / مسئول فنی / نماد اعتماد الکترونیکی / سامانه مودیان and the enum-label prefixes keyed off stable codes). - New `AppIcon` registry entries; new `ROUTES.ADMIN_*` / `ROUTES.PARTNER_*` + path helpers. --- ## 2. What is now testable, and exactly how Run `npm run dev`, sign in as an **admin** (mock-primary, so no live backend needed). The §7 acceptance steps: 1. **Verify a nurse.** `/admin/verification` → pick a pending nurse → the case loads each document via a **signed URL** (`DocumentViewer`; sentinel `documentId 9999` exercises the error→re-request path). Pass the steps (reject one with a required reason), enter a **structured credential** on a credential-bearing step; **Approve** is disabled until every step is `passed` → confirm → the nurse leaves the queue. The client never wrote `is_verified`. 2. **Process a refund.** `/admin/tickets` → open the **refund** ticket (bookingId + refundId seeded) → **RefundPanel** shows the **server-computed** tiered % + fee/payout decomposition (reconciles to the rial) + channel + BNPL ETA; the post-payout booking shows the read-only **clawback notice**; the decline **sentinel** shows **retry**. 3. **Preview + run a payout batch.** `/admin/payouts` → **preview next batch** → eligibility breakdown (eligible + no-IBAN-skipped + clawback line + **holiday-shifted** processing date) → **run batch** (confirm requires an **idempotency key** — a double-click can't double-pay) → `processing`. `[batchId]` detail → a **failed** payout → **retry**; reconcile a `transfer_reference`. 4. **Moderate a review.** `/admin/reviews` → a `pending_moderation` review (low-rating flag) → **publish/hide/reject** (reject/hide require a reason) → it leaves the queue; the client never computed the aggregate. 5. **Edit a config value.** `/admin/config` → edit `vat_rate` → the input validates **0–1** (`1.5` blocked) → save → the dialog states it is **audited, immediate, non-retroactive** → the **history drawer** shows old→new + actor + Shamsi time. 6. **Resolve a support alert.** `/admin/alerts` → filter to **open** → **assign to me** → **resolve** with a note. It appears in **no** customer/nurse/partner view. 7. **Holiday manager.** `/admin/holidays` → add a holiday with **`is_bank_closed` on** → listed (the client never computes the shift; the payout preview in step 3 shows the server-shifted date). 8. **Audit viewer.** `/admin/audit` → filter by entity/actor/date → paginated; expand a row for the `changedFields` diff. **No** edit/delete control exists. 9. **Partner portal.** Sign in mapping to a center admin → `/partner` shows onboarding state + license + MoR; `/partner/nurses` + `/partner/bookings` show only that center's data; `/partner/settlement` renders the commission/VAT invoices + signed-URL PDF + **masked IBAN** **only when merchant-of-record** (a non-MoR center shows the "via Balinyaar" state). Back as an admin → `/admin/partners` → create/verify/activate + assign a nurse; IBAN is write-then-masked (submit full, only last-4 ever shows). 10. **RBAC + i18n + RTL + caching.** A `support` role sees no payout/refund controls; a `moderation` role sees only moderation (drive via `useAdminCapabilities`). `fa`↔`en` translates every label (incl. the Persian legal terms) and the sidebar mirrors. Switching worklist filters/pages caches separately (filters+page in the key) — no refetch of loaded data. --- ## 3. What is mocked, and how to make it real All f15 data is **mock-primary** (the real `clientApi` in each domain maps the live routes and targets proposed slugs for the gaps; the swap is a one-line flag flip per domain — no hook/component change). Full detail in `reports/mocks-registry.md` (three new/updated rows). Seams + flags: - `services/admin` — `USE_ADMIN_MOCK` (config/holidays/audit/support-alerts live at b1; RBAC + config audit columns + rich audit filters are gaps → REQ-029/030/031). - `services/partnerCenter` — `USE_PARTNER_MOCK` (admin CRUD/verify/sponsor live at b15; portal split reads + activate/suspend + invoice total are gaps → REQ-032/033). - verification/refunds/payouts/reviews/tickets admin methods — the owning domain's flag (REQ-034/035/036/037). **No backend seams were introduced** — `IObjectStorage`/`IBankTransferProvider`/`IBnplProvider`/`IMoadianClient`/ `ILicenseVerificationService`/the audit interceptor/the notification dispatcher are all server-side (b1/b6/b11/b13/b14/b15); the frontend never touches them. --- ## 4. Contracts consumed `messaging-notifications-admin.md` (b15, primary) + `verification.md` (b6) + `refunds-invoices.md` (b11) + `payouts.md` (b13) + `reviews-records.md` (b14) + `config-reference.md` (b1). All types derived from these — none guessed. ## 5. Requests filed (`frontend/requests/for-backend.md`) **REQ-029** config `updatedAt`/`updatedBy` · **REQ-030** audit actor/action/date filters · **REQ-031** RBAC role endpoints · **REQ-032** partner-portal split reads + activate/suspend + IBAN write-then-masked · **REQ-033** center-scoped invoice list + invoice `totalIrr` · **REQ-034** verification nurse-level queue + on-demand doc URL + whole-verification approve/reject · **REQ-035** refund preview + explicit approve/reject · **REQ-036** payout single-preview + `holidayShifted` + record-transfer-reference · **REQ-037** moderation `tagCodes`. --- ## 6. MVP-complete closeout This is the **final frontend phase** — every customer, nurse, admin, and partner surface in the build plan now exists. - **Deferred-if-missing:** the **RBAC roles console** (`/admin/roles`) is built against the mock and tagged DEFERRED-IF-MISSING; it activates when the b15 role endpoints land (REQ-031). It is not on the testable acceptance path. - **Modeled-but-inactive, no UI (as specified):** `organizations` / `organization_nurses` (employer model), `fraud_flags` (ML console), `recurring_booking_schedules` (recurrence) — data-model/13. Also **no UI** for: full سامانه مودیان e-invoice automation / digital-signature (the portal only *views* the issued invoice ref + PDF), push/SMS channels, an analytics-warehouse dashboard, on-demand/instant payout, per-nurse payout-frequency settings. - **Minor i18n substitutions the screen agents flagged** (all use existing keys; cosmetic, non-blocking): the payout retry-success toast reuses `admin.saved`; the partner nurse verified/unverified column borrows `center_state_*` labels; partner booking-status renders the raw stable status code. Add dedicated keys later if desired.