Files
baya-monorepo/dev/phases/frontend/frontend-phase-15-b15.md
T
2026-06-28 21:59:59 +03:30

56 KiB
Raw Blame History

Frontend Phase 15 — Admin backoffice & partner-center consoles

Mission: ship the operational cockpit that runs Balinyaar — the internal, role-gated admin backoffice in the desktop sidebar shell from f0, plus the partner-center portal (a separate authz scope for the licensed sponsoring centers). The backoffice consolidates the worklists ops needs: the verification review queue (pass/reject nurse steps with a signed-URL document viewer + structured credential entry), refund admin (ticket-linked, fee/payout-decomposed, channel-aware, BNPL ETA), the payout dashboard (batch preview → processing → completed/partially-failed, retry), review moderation (publish/hide/reject), the config editor (typed inputs by data_type, audited save + change-history), the holiday calendar manager, the audit-log viewer (filtered, paginated), and the support-alert worklist (assign/resolve). The partner portal shows a center its onboarding/ verification state, its sponsored nurses, its sponsored bookings, and — when it is merchant-of-record — its settlement/invoice view. There is no wireframe for any of these screens — you design them with the frontend-designer skill. Internal-only data (support_alerts, internal ticket notes) must never leak to a non-admin. When this lands, MVP is complete.

Track: frontend · Depends on: frontend-phase-14-b15 (the services/tickets + services/notifications domains this phase layers the admin lens over) + the b15 contract (messaging-notifications-admin) and the admin endpoints across b1 (config/holidays/audit/support-alerts), b6 (verification queue), b11 (refunds/invoices), b13 (payout batches), b14 (review moderation) · Unlocks: MVP complete — this is the final frontend phase. Before you start, read ../_shared/agent-operating-rules.md. It is not optional.


1. Context — where this sits

This is the last frontend phase and the only one whose primary audience is internal staff (and the licensed partner centers), not families or nurses. Every customer- and nurse-facing surface already exists; what is missing is the back office that makes the marketplace run: an admin must be able to verify a nurse, process a refund, preview and run a payout batch, moderate a review, edit a config value, and resolve a support alert — and a partner-center admin must be able to see the nurses and bookings their license covers. All of this rides the desktop sidebar admin shell established in frontend-phase-0 (the third actor shell) and the existing services/{domain} + TanStack Query patterns. No new app-shell architecture; this phase fills the admin shell with real worklists and adds a separately-scoped partner portal.

The backoffice is a read-and-act surface over data other domains own. It does not re-implement verification logic, refund math, payout scheduling, moderation recompute, or config typing — those are server authority (b1/b6/b11/b13/b14). The client renders the contract's values and issues the sanctioned admin commands; it never computes eligibility, money decomposition, holiday shifts, or the is_verified flip on the client (§5).

What already exists (do not rebuild) — link the prior phases:

  • f0 foundations (frontend-phase-0): the three actor app shells + route groups, the admin/backoffice desktop sidebar shell (the shell this phase lives in), role-aware nav from AuthContext, the services/{domain} + TanStack Query caching pattern (keys.ts factory, apis/clientApi.ts, one-hook-per-file, hooks-only index.ts), the contracts→types.ts step, the money/format util in src/utils/ (formatIrrToToman, integer-safe IRR-string parse, Shamsi date display), the shared composites (status chip, stepper/progress header, price-breakdown, cards), and the i18n namespace conventions (the admin namespace was reserved in f0 — fill it). Reuse the money util, the status chip, and the price-breakdown — do not re-implement them.
  • f1-b2 auth (frontend-phase-1-b2): phone-OTP login, the role router, roles in AuthContext. Admins arrive authenticated with an admin role (super_admin / admin / support / finance / moderator); partner-center admins arrive with the partner-center scope. This phase role-gates every admin route and command off these roles — there is no separate admin login to build.
  • f5-b6 nurse verification flow (frontend-phase-5-b6): the nurse-facing verification checklist, the per-step status enums, the services/verification types, the document uploader, and the trust badge. The admin verification queue is the staff lens over the same services/verification domain — extend it with the admin-review endpoints; reuse the step/status enums and the badge, do not fork them.
  • f10-b11 refund & cancellation (frontend-phase-10-b11): the customer-side cancellation/refund-status UI, the policy-fee disclosure, the BNPL ETA display, and the refund money-display conventions. The admin refund tool is the staff lens — reuse the same fee/payout decomposition rendering and the money util; the admin initiates/approves refunds the customer can only watch.
  • f12-b13 nurse earnings & payouts (frontend-phase-12-b13): the read-only nurse earnings/payout-history view, the four earnings states, the payout-status enum (pending/processing/paid/failed), the batch-detail shape with booking links. The admin payout dashboard is the action surface explicitly deferred from f12 — build the batch preview → run → retry flow here; reuse the payout shapes and money rendering.
  • f13-b14 reviews & patient records (frontend-phase-13-b14): the services/reviews domain, the review shapes, the star/tag rendering. The admin moderation queue is the staff lens — add the publish/hide/reject moderation actions over the same domain.
  • f14-b15 messaging & notifications (frontend-phase-14-b15): the services/tickets and services/notifications domains, the ticket thread, the reference_code rendering. This phase layers the admin ticket lens (the global queue, the internal-note composer, the refund-from-ticket entry) on top of services/tickets, and reuses the notification bell in the admin shell. The user-app types deliberately omit isInternal; the admin types include it (admins see internal notes) — keep the user/admin type surfaces distinct (§5).
  • f9-b10 / f11-b12 money UI conventions (checkout, BNPL) — the Toman-display + IRR-string handling the admin money screens must match. Reuse the single money util; do not fork a formatter.
  • clientFetch/serverFetch + ApiError, the toast bridge (already toasts 401/403/5xx — do not re-toast those in hooks), the cookie manager, APP_THEME_LTR/RTL, tokens.css.

Backend readiness note. The primary contract you consume, messaging-notifications-admin.md, is produced by backend-phase-b15 (admin ticket queue, support-alert worklist, partner centers, RBAC role grants) and is the consolidation point for the admin endpoints. The other admin endpoints live in their own domain contracts — verification (b6, verification.md), refunds/invoices (b11, refunds.md), payouts (b13, payouts.md), reviews (b14, reviews.md), config/holidays/audit (b1, config-reference.md). If a shape you need is absent or wrong when you start, do not guess and do not block — append a REQ-NNN request to ../../shared-working-context/frontend/requests/for-backend.md and mock behind the services/admin / services/partnerCenter seam meanwhile (operating-rules §6). Record every mock in your report so it swaps cleanly once the endpoint lands.

2. Required reading (do this first)

Operating rules & checklists

Product / business truth (read before designing any screen)

  • ../../../product/business/14-notifications-and-admin.mdthe admin operational spine: the five worklists (verification / refund / payout / support-alert / RBAC), the append-only audit trail, config-change auditing, in-app notifications, and that back-office must reason over the Shamsi calendar + iranian_holidays. This is why each console exists.
  • ../../../product/business/13-tax-invoicing-and-legal.md — the partner-center / merchant-of-record model: the licensed center (پروانه تأسیس + مسئول فنی + نماد اعتماد الکترونیکی) sponsors nurses and may be the merchant-of-record / invoice issuer; the commission invoice (gross / platform commission / BNPL commission / VAT on commission) and the config-driven VAT rate. This drives the partner portal's settlement/invoice view and what it may show.
  • ../../../product/data-model/12-audit-config-and-reference.mdaudit_logs (immutable, append-only, changed_fields_json), platform_configs (typed by data_type; the seeded keys — platform_fee_rate, vat_rate, dispute_window_hours, nurse_payout_interval_days, evv_location_tolerance_meters, min_rating_for_support_alert, cancellation-tier defaults, BNPL keys), and iranian_holidays (holiday_date, name_fa, type, is_bank_closed). These shape the config editor, the holiday manager, and the audit viewer.
  • ../../../product/data-model/13-partner-centers-and-future.mdpartner_centers fields (name, legal_entity_type, moh_establishment_permit_no, technical_director_nurse_user_id + technical_director_license_no, enamad_code, settlement_iban (enc), is_merchant_of_record, commission_rate, admin_user_id, is_active, verified_at); the 1:N relations to nurse_profiles / bookings / invoices; and the deferred tables (organizations, organization_nurses, fraud_flags, recurring_booking_schedules) that have no UI this phase.

No wireframe exists for the admin or partner screens (the wireframe is the mobile customer/nurse flow A1E3 — see product/wireframes/index.html for the brand/RTL baseline only). The GTM notes explicitly flag the backoffice/ticket/partner surfaces as a design gap. You design these screens from scratch with the frontend-designer skill against the brand system — desktop, sidebar-driven, dense worklist layout, RTL-first.

Contracts & types (the source of truth for shapes — do not guess)

  • ../../contracts/domains/messaging-notifications-admin.mdthe primary contract (b15): admin ticket queue + internal-note composer, the support-alert worklist (list/filter, assign, resolve), partner-center CRUD/verify/sponsor + roster, and RBAC role grant/revoke. The user-vs-admin is_internal filtering note is here.
  • The per-domain admin contracts: verification.md (b6 — queue, record-step, upsert-credential, approve/reject, signed-URL document fetch), refunds.md (b11 — initiate/approve/reject, decomposition, channel, BNPL ETA, invoices), payouts.md (b13 — batch preview/list/detail, initiate, retry, transfer-reference reconcile), reviews.md (b14 — moderation queue, set-status), config-reference.md (b1 — list/update config, config change history, holidays CRUD, audit-log list, support-alert raise/list).
  • ../../contracts/conventions/api-conventions.md and money-and-types.md — the envelope (OperationResult → already unwrapped by clientFetch), snake_case routes/properties, pagination (page/page_size, default/max), enums as stable string codes (mirror as string-literal unions; labels are i18n keys), IRR as integer string on the wire (parse integer-safe, Toman display-only), UTC timestamps → Shamsi on the client.

Code to mirror (existing patterns — copy, don't invent)

  • client/src/services/auth/* (types.ts / keys.ts / apis/clientApi.ts / hooks/use*.ts / index.ts) — the exact shape every new domain service copies.
  • The prior domains this phase extends: services/verification (f5), services/refunds (f10), services/payouts (f12), services/reviews (f13), services/tickets + services/notifications (f14). Extend these with the admin endpoints; do not re-create them. Only the genuinely new admin-owned data (config, holidays, audit, support-alerts, RBAC) and partner centers get new domains (services/admin, services/partnerCenter — §3).
  • The f5 document uploader / signed-URL handling (the verification document viewer reuses the signed-URL pattern), the f0 money util + status chip + price-breakdown, the f10 refund decomposition rendering, the f12 payout shapes, the f14 ticket thread.
  • client/CLAUDE.md — RSC/client boundary, the admin route group, layouts, i18n, theme, fetch services, anti-patterns.

Design

  • Invoke the frontend-designer skill before building any screen. Every admin worklist, the document viewer, the refund/payout action panels, the config editor, the holiday manager, the audit table, the support-alert board, and the entire partner portal go through it — brand palette, tokens, typography, the App* library, the desktop sidebar density, table/virtualization treatment, empty/loading/error states, RTL mirroring. Do not hand-roll colours, spacing, or table styling. Because there is no wireframe, the designer skill owns the visual language for the whole back office.

3. Scope — build this

Two new domain services (services/admin, services/partnerCenter) plus admin endpoint additions to the existing verification / refunds / payouts / reviews / tickets domains, and the admin-shell screens + the partner-center portal on top of them. Everything is internal-only, behind role-gated routes in the f0 admin shell (the partner portal is its own scope). Build desktop- first, RTL, both locales, query-cached, virtualized lists, minimal re-renders.

Routing & RBAC. All admin screens mount under the f0 admin route group (e.g. (private-routes)/admin/…) gated by an admin role from AuthContext; the partner portal mounts under a distinct partner-scope segment (e.g. (private-routes)/partner/…) gated by the partner-center scope. A support admin must not see the payout-run control, a moderator must not see the refund tool, etc. — the server enforces role scopes on every command (don't rely on UI hiding for security), but the UI also hides/disables actions the current role can't perform so a user never sees a control that will 403. Drive this from a small useAdminCapabilities() selector off AuthContext roles. Update the Project Structure tree in client/CLAUDE.md for the new route segments + services.

3.1 services/admin (new domain — config, holidays, audit, support-alerts, RBAC)

Copy the auth service shape into client/src/services/admin/. Types come from the b1 + b15 contracts — do not invent. (Sub-namespace by area if it keeps files small: config, holidays, audit, supportAlerts, rbac.)

  • types.ts — string-literal unions + DTOs from the contracts:
    • Config: PlatformConfig (key, value (string on the wire), data_type enum string|int|decimal|bool|json, description, updated_at, updated_by), and a ConfigChange (audit) row (changed_at, actor, old_value, new_value).
    • Holidays: Holiday (id, holiday_date, name_fa, type enum official|religious|national, is_bank_closed).
    • Audit: AuditLogEntry (id, entity_type, entity_id, action, changed_fields_json (parsed to a typed record), actor, created_at) + paged list envelope; filter params (entity_type, entity_id, actor_id, from/to, action).
    • Support alerts: SupportAlert (id, type enum e.g. low_rating|no_show|evv_mismatch|verification_expiry|fraud_signal, status enum open|assigned|resolved, entity_type, entity_id, nullable booking_id/review_id, assigned_to, resolved_at, resolution_note, created_at).
    • RBAC: AdminRole enum (super_admin|admin|support|finance|moderator), RoleGrant (user_id, role, granted_by, granted_at, revoked_at).
  • keys.tsadminKeys.config(), adminKeys.configHistory(key), adminKeys.holidays(yearOrRange), adminKeys.audit(filters, page), adminKeys.supportAlerts(filters, page), adminKeys.roles(userId?). Filters + page are part of the key so each filter caches separately.
  • apis/clientApi.ts wrapping clientFetch (exact snake_case routes from the contracts): config list_platform_configs / update_platform_config / get_config_change_history; holidays list_holidays / create_holiday / update_holiday; audit list_audit_logs; support-alerts list_support_alerts / assign_support_alert / resolve_support_alert; RBAC list_roles / grant_role / revoke_role.
  • hooks/ (one per file): usePlatformConfigs, useUpdatePlatformConfig, useConfigChangeHistory, useHolidays, useUpsertHoliday, useAuditLogs, useSupportAlerts, useAssignSupportAlert, useResolveSupportAlert, useAdminRoles, useGrantRole, useRevokeRole. Mutations invalidate the relevant adminKeys (and the config-history key after a config save) on settle so cached data isn't refetched needlessly.
  • index.ts barrel (hooks only).

3.2 services/partnerCenter (new domain — partner portal + admin-side center management)

Copy the same skeleton into client/src/services/partnerCenter/. Types from the b15 contract.

  • types.tsPartnerCenter (id, name, legal_entity_type, moh_establishment_permit_no (پروانه تأسیس), technical_director_nurse_user_id, technical_director_license_no, enamad_code, settlement_iban masked last-4 only (never the full IBAN), is_merchant_of_record, commission_rate, admin_user_id, is_active, verified_at); SponsoredNurse (nurse summary + verification badge); SponsoredBooking (booking summary the center covers); CenterSettlementRow / CenterInvoice (only meaningful when is_merchant_of_record — gross / platform commission / BNPL commission / VAT / total, moadian_reference_number, pdf signed-URL link). A center verification/onboarding state enum (draft|pending_verification|verified|suspended).
  • keys.tscenterKeys.list(filters), centerKeys.detail(id), centerKeys.sponsoredNurses(id), centerKeys.sponsoredBookings(id, filters), centerKeys.settlement(id, filters), centerKeys.myCenter() (the partner-scope "my center" view).
  • apis/clientApi.ts — admin-side: list_partner_centers, get_partner_center, create_partner_center, update_partner_center, verify_partner_center, set_partner_center_active, assign_nurse_to_partner_center; partner-scope: get_my_partner_center, list_my_sponsored_nurses, list_my_sponsored_bookings, list_my_settlement.
  • hooks/: admin — usePartnerCenters, usePartnerCenter, useCreatePartnerCenter, useUpdatePartnerCenter, useVerifyPartnerCenter, useSetPartnerCenterActive, useAssignNurseToPartnerCenter; partner-scope — useMyPartnerCenter, useMySponsoredNurses, useMySponsoredBookings, useMySettlement. Mutations invalidate centerKeys.
  • index.ts barrel (hooks only).

3.3 Admin: verification review queue

The staff lens over services/verification (f5) — add the admin endpoints to that domain (queue list, record-step, upsert-credential, approve/reject, signed-URL document fetch); reuse the f5 step/status enums and the trust badge.

  • Queue list (/admin/verification) — a paginated, status-filtered worklist of nurses with pending verification (filter by aggregate status pending|in_review; sort by oldest-first). Each row: nurse name/photo, the step progress (e.g. "۳ از ۵"), the next pending step, submitted-at (Shamsi), and any expiring credential warning. Virtualize/paginate; empty state "صف خالی است / Queue clear".
  • Per-nurse review screen (/admin/verification/[nurseId]) — the ordered steps with status chips, a document viewer that fetches each verification_documents item via a signed URL (never a public URL — handle loading / expired-link → re-request / load-error states; the URL is short-lived, so fetch on demand, don't cache the URL in a long-lived query). For each step an admin can pass or reject (with a required reason)record_step; for credential steps a structured credential entry form (credential_number, issuing authority, issue/expiry dates) → upsert_credential. The screen's Approve / Reject action (approve_verification / reject_verification) is enabled only when all required steps are passed (the server flips is_verified transactionally — the client only enables the button and shows a confirmation; it never writes is_verified itself, §5). Approve/reject require a confirmation dialog and, on reject, a reason. On success, invalidate the queue + the nurse detail so the row leaves the queue.

3.4 Admin: refund tooling (inside the ticket lens)

The staff lens over services/refunds (f10) + services/tickets (f14). Refunds are admin-only and ticket-linked — the entry point is from a ticket, not a standalone form.

  • Refund panel in the admin ticket view — opened from a ticket (the admin global ticket queue, §3.9): shows the linked booking, computes a preview of the tiered refund_percentage_applied from the cancellation policy and the fee/payout decomposition (platform_fee_refunded_irr + nurse_payout_refunded_irr) via the f0 price-breakdown primitive (the server computes these — the client renders the preview the contract returns, never recomputes the percentage), a channel selector (psp_card / bnpl_revert / manual_bank), and — for BNPL — an ETA banner (expected_customer_refund_eta). Actions: Initiate refund (initiate_refund, carries the ticket_id), Approve (approve_refund), Reject (reject_refund). If the nurse was already paid, surface that a clawback will be created (read-only notice — the server creates it). States: preview / confirm / provider-revert-failure → retry (BNPL/PSP), success. Invalidate the refund + ticket + (if shown) the customer refund-status query on settle.

3.5 Admin: payout dashboard

The action surface over services/payouts (b13), explicitly deferred from f12. Reuse the f12 payout shapes, the payout-status enum, and the money util.

  • Batch dashboard (/admin/payouts) — a list of nurse_payout_batches with status (pending|processing|paid|partially_failed|failed), period (holiday-shifted period_start/ period_end, Shamsi), payout_count, total_amount, and a holiday-shift indicator when the processing date moved off a bank-closed day. Empty/loading/error states.
  • Batch preview → run — a "preview next batch" action (preview_payout_batch) that shows the eligibility breakdown: which completed/unpaid bookings qualify (EVV confirmed and dispute_window_ends_at passed), the per-nurse roll-up, the clawback-netting line (clawback_applied_irr), and the holiday-shifted processing date — all server-computed; the client only renders the preview (never computes eligibility or the holiday shift, §5). A "run batch" action (initiate_payout_batch) behind a confirmation dialog that requires an idempotency key (the contract's mechanism) so a double-click or retry cannot pay a booking twice (one-payout-per-booking, §5). After running, the batch moves to processing → poll/refetch to completed or partially-failed.
  • Batch detail + per-nurse drill-down (/admin/payouts/[batchId]) — the per-nurse nurse_payouts rows with status, the net decomposition (gross_earnings_irr clawback_applied_irr = net_amount_irr), the masked IBAN (last-4), and the transfer_reference. A failed payout shows its failure_reason and a retry action (retry_payout) — also idempotency-keyed. The RecordPayoutTransferReference reconciliation action (record_transfer_reference) lets finance attach the real bank transfer reference to a payout. Invalidate the batch/detail on each action.

3.6 Admin: review moderation queue

The staff lens over services/reviews (f13).

  • Moderation queue (/admin/reviews) — a paginated list of reviews in pending_moderation (filterable by status), each showing the rating, body, tags, the booking/nurse context, and a low-rating flag (when rating < min_rating_for_support_alert). Actions per review: publish / hide / reject (moderate_review with the target status; reject carries a reason). Each transition triggers a server-side aggregate recompute of the nurse's rating — the client just invalidates the review + the nurse's reviews query; it never computes the aggregate (§5). States: empty ("صف بررسی خالی است / Nothing to moderate"), loading, error, optimistic-vs-confirmed on the action. Never render pending_moderation content as if public.

3.7 Admin: config editor + change history

The config surface over services/admin (b1). Config edits are money-correctness sensitive and audited (§5).

  • Config list (/admin/config) — all platform_configs rows grouped sensibly (fees/VAT, deadlines, EVV, BNPL, cancellation tiers), each rendered with a typed input by data_type: bool → switch, int/decimal → numeric field with range validation (e.g. a rate field validates 01), json → a validated JSON editor, string → text. Show the description and updated_at/updated_by.
  • Audited saveupdate_platform_config behind a confirmation dialog that states "this change is audited and takes effect immediately; it does not retroactively change already-computed bookings/ledger" (copy, both locales). On success show the optimistic-vs-confirmed save state and invalidate the config + the change-history key.
  • Change-history drawer — per config key, a drawer (get_config_change_history) listing each change (old → new value, actor, Shamsi timestamp) so finance can prove the rate in effect at any past moment.

3.8 Admin: holiday calendar manager

Over services/admin holidays (b1).

  • Holiday manager (/admin/holidays) — a calendar/list of iranian_holidays (by year/range), each row holiday_date (Shamsi), name_fa, type chip, and an is_bank_closed toggle (this is what shifts payout scheduling — surface that consequence in the UI copy). Add/edit a holiday (create_holiday/update_holiday). The client does not compute next-business-day shifts — it only maintains the calendar the server uses for scheduling (§5). States: empty, loading, error, save-confirmation.

3.9 Admin: support-alert worklist + audit viewer + global ticket queue

  • Support-alert worklist (/admin/alerts) — the internal-only triage board over services/admin support-alerts: filter by type / status (open|assigned|resolved) / assigned_to; each card shows the alert type (low-rating / no-show / EVV-mismatch / verification-expiry / fraud-signal), the linked entity (deep-link to the booking/review/nurse), and severity styling (admin-only). Actions: assign (assign_support_alert — to self or another admin) and resolve (resolve_support_alert with a resolution note). States: empty ("هیچ هشدار بازی نیست / No open alerts"), loading, error. support_alerts content NEVER appears in any non-admin surface (§5).
  • Audit-log viewer (/admin/audit) — a read-only table over list_audit_logs with filters (entity type/id, actor, action, date range) and pagination/virtualization for large result sets; each row shows the entity, action, actor, Shamsi timestamp, and an expandable changed_fields_json diff. Append-only — there are no edit/delete affordances (§5). Empty/loading/error states.
  • Global ticket queue + internal-note composer (/admin/tickets, /admin/tickets/[id]) — the admin lens over services/tickets (f14): a queue across all tickets (filter by status / linked booking / reference_code), and the admin thread view that — unlike the user thread — renders internal (is_internal) messages distinctly and provides an internal-note composer (post a message with is_internal=true). The refund panel (§3.4) opens from here. The admin types include isInternal; the user-app types from f14 do not — keep them separate so an internal note can never bleed into the user view (§5).
  • (Optional) RBAC admin (/admin/roles) — a user↔role grid over services/admin RBAC with grant/revoke (confirmation + records granted_by/granted_at). If the b15 contract doesn't expose the role endpoints when you run, defer this screen (file a REQ and build it when the endpoints land); it is not part of the testable acceptance path. Tag it (DEFERRED-IF-MISSING) in your report.

3.10 Partner-center portal (separate authz scope)

The partner portal mounts under the distinct partner segment gated by the partner-center scope (a center admin is not a Balinyaar admin and must see only their own center's data — server-enforced tenancy; never fetch a center by raw id the user doesn't own, §5).

  • Center home / onboarding state (/partner) — the center's onboarding/verification state (draft|pending_verification|verified|suspended) with an unverified banner when not yet active, its license fields read-mostly (پروانه تأسیس / مسئول فنی / نماد اعتماد الکترونیکی), and is_merchant_of_record clearly indicated.
  • Sponsored-nurse list (/partner/nurses) — the nurses this center sponsors (list_my_sponsored_nurses), each with their verification badge; empty ("هنوز پرستاری اسپانسر نشده / No nurses sponsored yet").
  • Sponsored-bookings list (/partner/bookings) — the bookings the center legally covers (list_my_sponsored_bookings), filterable by status/date; read-only summaries (no PII beyond what the contract exposes to a center).
  • Settlement / invoice view (/partner/settlement) — rendered only when is_merchant_of_record === true (otherwise show a "not merchant-of-record / settlement runs through Balinyaar" state, no settlement table): the per-booking commission invoices (gross / platform commission / BNPL commission / VAT on the commission line / total, via the price-breakdown), the moadian_reference_number when issued, and the invoice PDF via a signed-URL download. Money via the f0 util (Toman display, IRR-string integer-safe). States: empty, loading, error on PDF fetch → retry.
  • Admin-side partner management (/admin/partners, /admin/partners/[id]) — the Balinyaar-admin surface for centers: list/create/edit a center (create_partner_center/update_partner_center — the settlement_iban field is write-then-masked: submit a full IBAN, but the list/detail only ever shows last-4), verify (verify_partner_center) and activate/suspend (set_partner_center_active) toggles, and the sponsored-nurse roster with assign-nurse (assign_nurse_to_partner_center).

3.11 i18n + types housekeeping

  • Fill the admin namespace (reserved in f0) and add a partner namespace to both messages/en.json and messages/fa.json, in sync, RTL-first (fa default). Every user-visible string is a key, including the Persian legal terms (پروانه تأسیس, مسئول فنی, نماد اعتماد الکترونیکی, سامانه مودیان) and the admin worklist labels.
  • Types come from the published contracts; any gap → append a REQ-NNN to for-backend.md and mock behind the services/admin / services/partnerCenter seam (or the extended-domain seam) meanwhile.

(DEFERRED)organizations / organization_nurses employer model, fraud_flags ML console, recurring_booking_schedules recurrence UI (data-model/13 — modeled-but-inactive, no UI); full سامانه مودیان e-invoice automation / digital-signature pipeline (the portal only views the issued invoice/ref + PDF — it does not submit); push/SMS notification channels; an analytics-warehouse dashboard over system_events; on-demand/instant payout; per-nurse payout-frequency settings. Build none of these — flag them in the report if a contract field hints at them.

4. Mocks & seams in this phase

This is a frontend phase — its only "seams" are the domain services behind which a mock clientApi lives until each backend endpoint is merged (operating-rules §6, frontend-checklist last bullet). Do not introduce backend seams — IObjectStorage (signed URLs), IBankTransferProvider (payouts), IBnplProvider/IPaymentGateway (refund reverts), IMoadianClient (invoices), ILicenseVerificationService (partner-center verify), the audit interceptor, the notification dispatcher — those are server-side (b1/b6/b11/b13/b14/b15) and the frontend never touches them. Reuse the f5 signed-URL document pattern, the f10 refund decomposition, the f12 payout shapes, and the f14 ticket thread — do not re-implement them.

  • services/admin seam — if b1/b15 admin endpoints aren't merged, ship a mock clientApi (same method signatures) returning realistic typed configs (one per data_type so the typed inputs + the 01 range validation are exercisable), a change-history trail, holidays (some bank-closed), a paged audit log with changed_fields_json diffs, and a support-alert list spanning all statuses/types — including at least one of each alert type so the worklist filters are testable.
  • services/partnerCenter seam — a mock returning a merchant-of-record center (so the settlement view renders) and a non-MoR center (so the "settlement runs through Balinyaar" state renders), sponsored nurses (verified + unverified), sponsored bookings, and a couple of commission invoices with a fake 22-digit moadian_reference_number and a stub PDF URL. The settlement_iban mock returns last-4 only.
  • Extended-domain seams (verification/refunds/payouts/reviews/tickets) — for the admin endpoints added to existing domains, mock the new admin methods behind the same domain clientApi (e.g. a verification queue with documents needing a signed URL, a refund preview with a fee/payout split and a BNPL ETA, a batch preview with an eligibility breakdown + a partially-failed batch + a failed payout to retry, a moderation queue with a low-rating review, an admin ticket thread with an internal note).

Record every mock in your frontend report and the mocks-registry.md so the real-endpoint swap is a one-file change per domain (the hooks/screens stay unchanged — only apis/clientApi.ts flips). Append the corresponding shape requests to for-backend.md.

5. Critical rules you must not get wrong

  • Internal-only data never reaches a non-admin. support_alerts and internal ticket notes (is_internal=true) are staff-only — they appear only in admin routes, are fetched only by admin-scoped queries, and must never be joined into or rendered in any customer/nurse/partner surface. The admin ticket types include isInternal (admins see internal notes, styled distinctly); the user-app types from f14 deliberately omit it — keep the two type surfaces separate so an internal note cannot bleed into a user view. Treat any internal content in a non-admin payload as a backend defect — file it via for-backend.md, don't render it.
  • Internal-only & role-gated routes. Every admin screen is behind the role-gated admin shell; the partner portal is a separate authz scope. The server enforces role scopes on every command (a support admin can't run a payout, a moderator can't refund, a center admin sees only their own center) — never rely on UI hiding for security — but the UI must also hide/disable controls the current role can't use so a user never sees a button that 403s.
  • The server is the only authority; the client never computes the load-bearing values. Never compute on the client: the is_verified flip (the server flips it transactionally when all required steps pass — the UI only enables Approve and confirms), the refund percentage / fee-vs-payout decomposition (render the server's preview), payout eligibility or the holiday-shifted processing date (render the server's breakdown), the review aggregate recompute (just invalidate), and config parsing beyond rendering by data_type. The client renders contract values and issues commands.
  • Money correctness (verbatim — the sacred invariants across b9b13): money is IRR BIGINT, no floats — parse the wire integer string with the f0 integer-safe util, never Number()/float math; Toman is display-only. The three booking amounts always satisfy gross = commission + payout (gross_price_irr = balinyaar_commission_irr + nurse_payout_amount); render every breakdown so it sums. Escrow is an append-only, balanced double-entry ledger — refund/payout/clawback figures are ledger-derived; a clawback nets, it does not auto-reverse, and a nurse's payable balance may go negative (don't clamp). Payout gating is dispute-window gating: an amount is eligible only after EVV completion AND dispute_window_ends_at < now() — never show "eligible"/run a payout for an amount still in its dispute window, and never compute eligibility on the client. One payout per booking (nurse_payout_booking_links.booking_id is UNIQUE) — the "run batch"/"retry payout" commands are idempotency-keyed so a double-click or retry can never pay a booking twice; render the status the contract returns (pending/processing/paid/failed/partially_failed), never an optimistic "done". Webhook idempotency is a server concern, but its client consequence is real: settlement/transfer is never instant — poll/refetch the status, don't assume completion.
  • Commission invoice / VAT (partner portal): the platform issues only its commission invoice (never the nurse's service invoice); VAT applies to the commission line, not the gross service fee; the VAT rate is config-driven (read from platform_configs.vat_rate, snapshotted on the invoice) — never hardcode 10%. Merchant-of-record drives the settlement/invoice view: render the settlement table only when is_merchant_of_record === true; the issuer/settlement target follows partner_centers, not a hardcoded platform.
  • Refunds are admin-only and ticket-linked. No customer self-service initiation — the refund panel opens from a ticket and every initiate carries the ticket_id. The decomposition (fee leg vs payout leg) and channel (psp_card/bnpl_revert/manual_bank) come from the server; a post-payout refund creates a clawback (server-created — show it read-only).
  • Append-only audit is read-only. The audit viewer has no edit/delete affordance; config edits are audited and a config change does not retroactively alter already-computed bookings/ledger — say so in the save confirmation. Finance must be able to prove the commission/VAT rate at any past moment via the change-history drawer.
  • Config typing + validation. Render each config by its data_type and validate at the boundary (a rate field is 01, an int is integer, a bool is a switch, json must parse) before allowing the audited save.
  • Signed URLs are short-lived. Verification documents and invoice PDFs load via signed URLs (never public) — fetch the URL on demand, don't cache it in a long-lived query; handle loading / expired → re-request / load-error → retry.
  • PII / masking. settlement_iban (center) and the payout iban_snapshot are encrypted/masked — show last-4 only, never a full IBAN; transfer_reference/moadian_reference_number are opaque strings shown for reconciliation. Don't log full sensitive values. Verification documents are PII — signed-URL only, never embedded as a public asset.
  • Tenancy. A partner-center admin sees only their own center; an admin sees only what their role scopes. Never fetch by a raw id the current principal doesn't own (server-enforced — don't bypass it).
  • Frontend conventions (non-negotiable): fetch only through clientFetch in services/{domain}; TanStack Query caching with deliberate keys (filters + page in the key) + invalidation/setQueryData (no needless refetch — switching a worklist filter or paging must not refetch loaded data); large worklists are paginated/virtualized with empty/loading/error states each; minimise re-renders (select to subscribe to slices, stable refs, colocate filter state low — a fast-changing config-input value must not re-render the whole config table); MUI primitives stay MUI, shared composites (the worklist row/table, the document viewer, the refund panel, the config row, the audit row, the support-alert card, the partner settlement row) live at src/components/… with co-located tests; colours from tokens.css (status chips off the --bal-{success,warning,info,error} semantic tokens); both locales in sync, RTL-correct (the desktop sidebar mirrors; Persian legal terms render correctly); no layout above [locale]; respect the RSC/client boundary.

6. Definition of Done

The shared definition-of-done.md, plus this phase's specifics:

  • services/admin and services/partnerCenter exist in the auth-service shape (types from the b1/b15 contracts, keys.ts with filters/page in the key, apis/clientApi.ts, one hook per file, hooks-only index.ts), and the admin endpoints are added to the existing verification/refunds/payouts/reviews/tickets domains (not re-created); mutations invalidate the right keys.
  • Verification queue lists pending nurses, the per-nurse review loads documents via signed URLs (loading/expired/error handled), supports pass/reject + reason per step and structured credential entry, and Approve is enabled only when all required steps pass — the client never writes is_verified.
  • Refund tooling opens from a ticket, renders the server-computed fee/payout decomposition + channel + BNPL ETA, supports initiate/approve/reject with provider-revert-failure → retry, and shows the read-only clawback notice when applicable.
  • Payout dashboard shows batches with status + holiday-shift indicator, a batch preview with the server-computed eligibility breakdown + clawback netting, a run-batch action that is idempotency-keyed (no double-pay), and a batch detail with per-nurse rows, masked IBAN, transfer_reference, a failed-payout retry, and transfer-reference reconciliation.
  • Review moderation queue supports publish/hide/reject (reject reason); the client invalidates and never computes the aggregate; pending_moderation content is never shown as public.
  • Config editor renders each value by data_type, validates (rate 01), saves with an audited-save confirmation ("audited; effective immediately; not retroactive"), and exposes a change-history drawer.
  • Holiday manager lists/adds/edits holidays with the is_bank_closed toggle (consequence surfaced); the client never computes the next-business-day shift.
  • Audit-log viewer is read-only (no edit/delete), filtered (entity/actor/action/date) and paginated/virtualized with a changed_fields_json diff.
  • Support-alert worklist filters by type/status/owner and supports assign/resolve with a note; no support_alerts content appears in any non-admin surface.
  • Global ticket queue + internal-note composer exist (admin sees is_internal notes, styled distinctly); the refund panel opens from here.
  • Partner-center portal (separate scope) shows the center's onboarding/verification state, sponsored nurses, sponsored bookings, and — only when is_merchant_of_record — the settlement/invoice view (commission/VAT decomposition, signed-URL PDF, masked IBAN); the admin-side partner management supports create/edit/verify/activate + assign-nurse, IBAN write-then-masked.
  • Every admin route is role-gated (controls hidden/disabled per the current role) and the partner portal is a separate scope; UI hiding never substitutes for the server's enforcement.
  • admin + partner i18n namespaces in en.json and fa.json in sync (incl. پروانه تأسیس / مسئول فنی / نماد اعتماد الکترونیکی / سامانه مودیان); RTL verified on the desktop shell.
  • Shared composites (worklist row/table, document viewer, refund panel, config row, audit row, support-alert card, partner settlement row) live at the shared level each with a co-located *.test.tsx; all money via the f0 util (no float math); npm run check green; npm run test:ci green.
  • Any contract gap is a REQ-NNN in for-backend.md and the corresponding client-side mock is behind the seam and recorded in the report + mock registry.
  • client/CLAUDE.md Project Structure updated for services/admin, services/partnerCenter, the admin endpoint additions to existing domains, the /admin/* route segments, the /partner/* scope, and the new shared admin components; the frontend-designer skill was invoked for the visual work.

7. How to test (what a human can verify after this phase)

Run npm run dev signed in as an admin (with the b1/b6/b11/b13/b14/b15 admin endpoints live, or the seam mocks if not yet merged):

  1. Verify a nurse. Open /admin/verification → pick a pending nurse → the per-nurse screen loads each document via a signed URL (let one expire → it offers re-request). Pass the remaining steps (reject one with a reason to see the reason captured), enter a structured credential, then Approve → confirm dialog → the nurse leaves the queue and their trust badge flips. Expected: Approve is disabled until all required steps are passed; the client never wrote is_verified itself.
  2. Process a refund. From the /admin/tickets queue, open a ticket linked to a booking → open the refund panel → see the server-computed tiered percentage + fee/payout decomposition + the channel selector; for a BNPL booking see the ETA banner. Initiate → Approve → success; the decomposition sums (fee_refunded + payout_refunded reconciles). If the nurse was already paid, a clawback notice shows (read-only). Expected: the refund carries the ticket_id; a forced provider-revert failure shows retry.
  3. Preview + run a payout batch. Open /admin/payoutspreview next batch → the eligibility breakdown lists only EVV-confirmed, dispute-window-closed bookings, the clawback-netting line, and the holiday-shifted processing date. Run batch (confirmation requires an idempotency key) → it goes processingcompleted (or partially-failed). Open the batch detail → a failed payout offers retry; reconcile a transfer_reference. Expected: clicking "run" twice does not pay any booking twice; an amount still in its dispute window never appears as eligible.
  4. Moderate a review. Open /admin/reviews → a pending_moderation review (with a low-rating flag) → publish it → it leaves the queue and the nurse's public reviews update (server-recomputed aggregate); hide/reject behave likewise (reject captures a reason). Expected: pending content is never shown publicly; the client didn't compute the aggregate.
  5. Edit a config value (audited + history). Open /admin/config → edit vat_rate → the input validates 01 (try 1.5 → blocked) → save → the confirmation states it's audited, immediate, non-retroactive → open the change-history drawer → the old→new change, actor, and Shamsi timestamp appear. Expected: the change is recorded; already-computed bookings are unaffected.
  6. Resolve a support alert. Open /admin/alerts → filter to openassign an alert to yourself (status → assigned) → resolve it with a note (status → resolved). Expected: the alert is internal-only — it appears in no customer/nurse/partner view anywhere.
  7. Holiday manager. Open /admin/holidays → add a holiday with is_bank_closed on → it's listed; (cross-check in §3 step 3 that the next payout preview's processing date shifts off it, server-side). Expected: the client only maintains the calendar; it doesn't compute the shift.
  8. Audit viewer. Open /admin/audit → filter by entity/actor/date → results paginate/virtualize; expand a row to see the changed_fields_json diff. Expected: no edit/delete control exists.
  9. Partner portal (separate scope). Sign in as a partner-center admin/partner shows the center's onboarding/verification state; /partner/nurses and /partner/bookings show only that center's sponsored nurses/bookings; /partner/settlement renders the commission/VAT invoice view only when the center is merchant-of-record (a non-MoR center shows the "settlement via Balinyaar" state), with a signed-URL PDF and a masked IBAN (last-4). Sign back in as a Balinyaar admin → /admin/partners → create/verify/activate a center and assign a nurse. Expected: a center admin cannot see another center's data; the IBAN is never shown in full.
  10. RBAC + i18n + RTL + caching. A support-role admin cannot see the payout-run control; a moderator cannot see the refund tool. Switch faen → every label (incl. the Persian legal terms) translates and the desktop sidebar mirrors. Switch worklist filters / page the lists → React Query Devtools shows separate cache entries per filter/page and no refetch of loaded data.
  11. Gate: npm run check and npm run test:ci pass.

8. Hand off & document (close the phase)

  • Docs to update: client/CLAUDE.md Project Structure — add services/admin, services/partnerCenter, the admin endpoint additions to the existing verification/refunds/payouts/reviews/tickets domains, the /admin/* route segments + the /partner/* scope, the new shared admin/partner components, and a one-line note on the useAdminCapabilities() role-gating selector and the signed-URL on-demand-fetch pattern so they're reused. If you discover/decide a business rule the product/ docs don't capture (e.g. an admin-only payout-preview field, a partner-center onboarding sub-state, a config validation bound), record it in the relevant product/**.mddon't invent rules; record decisions and flag uncertain ones in your report.
  • Contracts to consume: the primary ../../contracts/domains/messaging-notifications-admin.md (b15 — admin tickets, support-alert worklist, partner centers, RBAC) plus the per-domain admin endpoints in verification.md (b6), refunds.md (b11), payouts.md (b13), reviews.md (b14), and config-reference.md (b1). Derive all types from these — never guess shapes. Any missing field/filter/endpoint (e.g. the payout preview eligibility breakdown, the refund decomposition preview, the config data_type, the partner settlement rows, the admin is_internal message flag, the RBAC role endpoints) → append a REQ-NNN to ../../shared-working-context/frontend/requests/for-backend.md and mock behind the seam meanwhile.
  • Handoff & report: append your phase summary to ../../shared-working-context/frontend/STATUS.md; write ../../shared-working-context/reports/frontend-phase-15-report.md (operating-rules §7) — what was built, what is now testable and exactly how (the §7 steps), which client-side mocks sit behind the services/admin / services/partnerCenter / extended-domain seams and how each swaps to the real endpoint, the contracts consumed, the REQ gaps filed, and — since this is the final frontend phase — a short "MVP complete" closeout noting any deferred-if-missing screen (e.g. RBAC admin) and the modeled-but-inactive tables with no UI. Update the mock registry ../../shared-working-context/reports/mocks-registry.md for every client-side mock.
  • Memory: save a project memory note (with a MEMORY.md pointer) for the non-obvious decisions this phase locks in — the admin-shell route-gating + useAdminCapabilities() role selector, the separate partner-center authz scope and its tenancy, the admin-vs-user ticket type split (isInternal admin-only), the merchant-of-record-gated settlement view + commission-only/VAT-on-commission invoice rule, the signed-URL on-demand-fetch pattern for documents/PDFs, the idempotency-keyed payout-run/retry, and the "server is the only authority" boundary (no client-side is_verified flip / eligibility / decomposition / aggregate / holiday-shift). Don't record what the code/docs already make obvious.