56 KiB
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(theservices/tickets+services/notificationsdomains 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 fromAuthContext, theservices/{domain}+ TanStack Query caching pattern (keys.tsfactory,apis/clientApi.ts, one-hook-per-file, hooks-onlyindex.ts), the contracts→types.tsstep, the money/format util insrc/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 (theadminnamespace 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 inAuthContext. 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, theservices/verificationtypes, the document uploader, and the trust badge. The admin verification queue is the staff lens over the sameservices/verificationdomain — 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): theservices/reviewsdomain, the review shapes, the star/tag rendering. The admin moderation queue is the staff lens — add thepublish/hide/rejectmoderation actions over the same domain. - f14-b15 messaging & notifications (
frontend-phase-14-b15): theservices/ticketsandservices/notificationsdomains, the ticket thread, thereference_coderendering. This phase layers the admin ticket lens (the global queue, the internal-note composer, the refund-from-ticket entry) on top ofservices/tickets, and reuses the notification bell in the admin shell. The user-app types deliberately omitisInternal; 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 aREQ-NNNrequest to../../shared-working-context/frontend/requests/for-backend.mdand mock behind theservices/admin/services/partnerCenterseam 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
../_shared/agent-operating-rules.mdand../_shared/frontend-conventions-checklist.md— how you work, the gate, the contract/handoff lanes, the mock-then-swap rule (§6).../_shared/definition-of-done.md— the bar this phase adds to (§6).
Product / business truth (read before designing any screen)
../../../product/business/14-notifications-and-admin.md— the 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.md—audit_logs(immutable, append-only,changed_fields_json),platform_configs(typed bydata_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), andiranian_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.md—partner_centersfields (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 tonurse_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 A1–E3 — see
product/wireframes/index.htmlfor 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 thefrontend-designerskill 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.md— the 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-adminis_internalfiltering 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.mdandmoney-and-types.md— the envelope (OperationResult→ already unwrapped byclientFetch),snake_caseroutes/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-designerskill 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, theApp*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 fromAuthContext; the partner portal mounts under a distinct partner-scope segment (e.g.(private-routes)/partner/…) gated by the partner-center scope. Asupportadmin must not see the payout-run control, amoderatormust 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 smalluseAdminCapabilities()selector offAuthContextroles. Update the Project Structure tree inclient/CLAUDE.mdfor 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_typeenumstring|int|decimal|bool|json,description,updated_at,updated_by), and aConfigChange(audit) row (changed_at,actor,old_value,new_value). - Holidays:
Holiday(id,holiday_date,name_fa,typeenumofficial|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,typeenum e.g.low_rating|no_show|evv_mismatch|verification_expiry|fraud_signal,statusenumopen|assigned|resolved,entity_type,entity_id, nullablebooking_id/review_id,assigned_to,resolved_at,resolution_note,created_at). - RBAC:
AdminRoleenum (super_admin|admin|support|finance|moderator),RoleGrant(user_id,role,granted_by,granted_at,revoked_at).
- Config:
keys.ts—adminKeys.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.tswrappingclientFetch(exactsnake_caseroutes from the contracts): configlist_platform_configs/update_platform_config/get_config_change_history; holidayslist_holidays/create_holiday/update_holiday; auditlist_audit_logs; support-alertslist_support_alerts/assign_support_alert/resolve_support_alert; RBAClist_roles/grant_role/revoke_role.hooks/(one per file):usePlatformConfigs,useUpdatePlatformConfig,useConfigChangeHistory,useHolidays,useUpsertHoliday,useAuditLogs,useSupportAlerts,useAssignSupportAlert,useResolveSupportAlert,useAdminRoles,useGrantRole,useRevokeRole. Mutations invalidate the relevantadminKeys(and the config-history key after a config save) on settle so cached data isn't refetched needlessly.index.tsbarrel (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.ts—PartnerCenter(id,name,legal_entity_type,moh_establishment_permit_no(پروانه تأسیس),technical_director_nurse_user_id,technical_director_license_no,enamad_code,settlement_ibanmasked 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 whenis_merchant_of_record— gross / platform commission / BNPL commission / VAT / total,moadian_reference_number,pdfsigned-URL link). A center verification/onboarding state enum (draft|pending_verification|verified|suspended).keys.ts—centerKeys.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 invalidatecenterKeys.index.tsbarrel (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 statuspending|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 eachverification_documentsitem 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 arepassed(the server flipsis_verifiedtransactionally — the client only enables the button and shows a confirmation; it never writesis_verifieditself, §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_appliedfrom 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 theticket_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 ofnurse_payout_batcheswith status (pending|processing|paid|partially_failed|failed), period (holiday-shiftedperiod_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 anddispute_window_ends_atpassed), 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-nursenurse_payoutsrows with status, the net decomposition (gross_earnings_irr − clawback_applied_irr = net_amount_irr), the masked IBAN (last-4), and thetransfer_reference. A failed payout shows itsfailure_reasonand a retry action (retry_payout) — also idempotency-keyed. TheRecordPayoutTransferReferencereconciliation 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 inpending_moderation(filterable by status), each showing the rating, body, tags, the booking/nurse context, and a low-rating flag (whenrating < min_rating_for_support_alert). Actions per review: publish / hide / reject (moderate_reviewwith 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 renderpending_moderationcontent 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) — allplatform_configsrows grouped sensibly (fees/VAT, deadlines, EVV, BNPL, cancellation tiers), each rendered with a typed input bydata_type:bool→ switch,int/decimal→ numeric field with range validation (e.g. a rate field validates 0–1),json→ a validated JSON editor,string→ text. Show thedescriptionandupdated_at/updated_by. - Audited save —
update_platform_configbehind 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 ofiranian_holidays(by year/range), each rowholiday_date(Shamsi),name_fa,typechip, and anis_bank_closedtoggle (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 overservices/adminsupport-alerts: filter bytype/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_alertwith a resolution note). States: empty ("هیچ هشدار بازی نیست / No open alerts"), loading, error.support_alertscontent NEVER appears in any non-admin surface (§5). - Audit-log viewer (
/admin/audit) — a read-only table overlist_audit_logswith 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 expandablechanged_fields_jsondiff. 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 overservices/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 withis_internal=true). The refund panel (§3.4) opens from here. The admin types includeisInternal; 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 overservices/adminRBAC with grant/revoke (confirmation + recordsgranted_by/granted_at). If the b15 contract doesn't expose the role endpoints when you run, defer this screen (file aREQand 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 (پروانه تأسیس / مسئول فنی / نماد اعتماد الکترونیکی), andis_merchant_of_recordclearly 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 whenis_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), themoadian_reference_numberwhen 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— thesettlement_ibanfield 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
adminnamespace (reserved in f0) and add apartnernamespace to bothmessages/en.jsonandmessages/fa.json, in sync, RTL-first (fadefault). 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-NNNtofor-backend.mdand mock behind theservices/admin/services/partnerCenterseam (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/adminseam — if b1/b15 admin endpoints aren't merged, ship a mockclientApi(same method signatures) returning realistic typed configs (one perdata_typeso the typed inputs + the 0–1 range validation are exercisable), a change-history trail, holidays (some bank-closed), a paged audit log withchanged_fields_jsondiffs, and a support-alert list spanning all statuses/types — including at least one of each alert type so the worklist filters are testable.services/partnerCenterseam — 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-digitmoadian_reference_numberand a stub PDF URL. Thesettlement_ibanmock 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 domainclientApi(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_alertsand 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 includeisInternal(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 viafor-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
supportadmin can't run a payout, amoderatorcan'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_verifiedflip (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 bydata_type. The client renders contract values and issues commands. - Money correctness (verbatim — the sacred invariants across b9–b13): money is IRR
BIGINT, no floats — parse the wire integer string with the f0 integer-safe util, neverNumber()/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 ANDdispute_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_idis 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 whenis_merchant_of_record === true; the issuer/settlement target followspartner_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_typeand validate at the boundary (a rate field is 0–1, 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 payoutiban_snapshotare encrypted/masked — show last-4 only, never a full IBAN;transfer_reference/moadian_reference_numberare 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
clientFetchinservices/{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 (selectto 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 atsrc/components/…with co-located tests; colours fromtokens.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/adminandservices/partnerCenterexist in theauth-service shape (types from the b1/b15 contracts,keys.tswith filters/page in the key,apis/clientApi.ts, one hook per file, hooks-onlyindex.ts), and the admin endpoints are added to the existingverification/refunds/payouts/reviews/ticketsdomains (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_moderationcontent is never shown as public. - Config editor renders each value by
data_type, validates (rate 0–1), 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_closedtoggle (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_jsondiff. - Support-alert worklist filters by type/status/owner and supports assign/resolve with a note;
no
support_alertscontent appears in any non-admin surface. - Global ticket queue + internal-note composer exist (admin sees
is_internalnotes, 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+partneri18n namespaces inen.jsonandfa.jsonin 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 checkgreen;npm run test:cigreen. - Any contract gap is a
REQ-NNNinfor-backend.mdand the corresponding client-side mock is behind the seam and recorded in the report + mock registry. client/CLAUDE.mdProject Structure updated forservices/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):
- 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 wroteis_verifieditself. - 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_refundedreconciles). If the nurse was already paid, a clawback notice shows (read-only). Expected: the refund carries theticket_id; a forced provider-revert failure shows retry. - Preview + run a payout batch. Open /admin/payouts → preview 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 processing → completed (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. - Moderate a review. Open /admin/reviews → a
pending_moderationreview (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. - Edit a config value (audited + history). Open /admin/config → edit
vat_rate→ the input validates 0–1 (try1.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. - Resolve a support alert. Open /admin/alerts → filter to open → assign 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.
- Holiday manager. Open /admin/holidays → add a holiday with
is_bank_closedon → 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. - Audit viewer. Open /admin/audit → filter by entity/actor/date → results paginate/virtualize;
expand a row to see the
changed_fields_jsondiff. Expected: no edit/delete control exists. - 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.
- RBAC + i18n + RTL + caching. A
support-role admin cannot see the payout-run control; amoderatorcannot see the refund tool. Switchfa↔en→ 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. - Gate:
npm run checkandnpm run test:cipass.
8. Hand off & document (close the phase)
- Docs to update:
client/CLAUDE.mdProject Structure — addservices/admin,services/partnerCenter, the admin endpoint additions to the existingverification/refunds/payouts/reviews/ticketsdomains, the/admin/*route segments + the/partner/*scope, the new shared admin/partner components, and a one-line note on theuseAdminCapabilities()role-gating selector and the signed-URL on-demand-fetch pattern so they're reused. If you discover/decide a business rule theproduct/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 relevantproduct/**.md— don'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 inverification.md(b6),refunds.md(b11),payouts.md(b13),reviews.md(b14), andconfig-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 configdata_type, the partner settlement rows, the adminis_internalmessage flag, the RBAC role endpoints) → append aREQ-NNNto../../shared-working-context/frontend/requests/for-backend.mdand 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 theservices/admin/services/partnerCenter/ extended-domain seams and how each swaps to the real endpoint, the contracts consumed, theREQgaps 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.mdfor every client-side mock. - Memory: save a
projectmemory note (with aMEMORY.mdpointer) 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 (isInternaladmin-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-sideis_verifiedflip / eligibility / decomposition / aggregate / holiday-shift). Don't record what the code/docs already make obvious.