# SCREENS INVENTORY — Platform (DALSEEN Super Admin)

> **Verified accurate:** 2026-05-02 — file count exact (10/10), module overview, fixtures-as-spec stance, two-faces-one-fixture pattern, and OpenAPI gap call-out (~30 missing platform endpoints) all hold. No drift detected.
> **Status:** active source-of-truth doc; the canonical Platform Super Admin catalog.

> **Module:** `front/platform/`
> **Files:** 10 (~7,800 LOC compiled)
> **Routes (handled by app shell):** `platform`, `platform.tenants`, `platform.billing`, `platform.health`, `platform.compliance`, `platform.incidents`, `platform.releases`, `platform.audit`, `platform.onboarding`, `platform.signups`, `platform.workbench`, `platform.plansEditor`, `platform.support`
> **Globals:** `window.DALSEEN_PLATFORM` (fixtures, in `front/common/roles.js`), `window.DALSEEN_BILLING` (commercial model, in `dalseen-billing-data.js`), `window.DALSEEN_ONBOARDING_STEPS`
> **Top-level components on `window.*`:** `PlatformDashboard`, `RoleSwitcher`, `PlatformTenants`, `PlatformBilling`, `PlatformHealth` (overridden to `PlatformHealthFull`), `PlatformCompliance`, `PlatformIncidents`, `PlatformReleases`, `PlatformAudit`, `PlatformOnboarding`, `PlatformSignupQueue`, `PlatformOnboardingWorkbench`, `PlatformPlansEditor`, `PlatformSupportDesk`, `OwnerPlanShop`, `OwnerInvoices`, `OwnerSupportInbox`, `NewTenantWizard`, `TenantActionModals`, `BillingActions`, `InvoiceDrawer`, `MrrTrend`, `HealthFactorsPanel`, `ImpersonationBanner`
> **Storage namespace:** none (Super Admin is server-of-record). Mock writes mutate `window.DALSEEN_PLATFORM.*` arrays in-place.
> **Live wrappers:** `platform-live.jsx` rebinds `PlatformDashboard`, `PlatformOnboarding`, `HealthFactorsPanel`, and `TenantActionModals` to `useApi(API.platform.*)` — non-destructive (originals preserved on `Origs.*`, fixtures still seed the inner components).
> **Status:** Fixtures are the richest in the codebase but **OpenAPI coverage is the thinnest** — only the 4 wrapped surfaces above hit `/api/v1/platform/*`. Day-1 cut-over requires defining ~30 new platform endpoints (tenants CRUD + actions, billing/plans/invoices, health metrics, compliance, incidents, releases, audit, signup queue, onboarding workbench, support tickets).

---

## §0 — How to read this document

Same conventions as the other inventories: every screen lists **what renders**, **what it reads**, **what it writes**, **API mapping**, **permission gates**, and **production additions**. The Platform module differs from the tenant-side modules in three ways:

1. **It crosses tenant boundaries.** Every screen reads or writes data that belongs to multiple tenants, so every endpoint must be guarded by a `platform.staff` middleware (not the per-tenant `X-Tenant-Id` scope used elsewhere).
2. **The fixtures are the spec.** `roles.js` (847 tenants, 8 plans, 12 invoices, 12 health services, 6 compliance items, 14 incidents, 4 releases, 16 audit events) and `dalseen-billing-data.js` (3 plan tiers, 10 services, 3 support tiers, 6 account managers, 8 signup-queue items, 8 onboarding steps, 12 support tickets, 4 invoices, 4 payment methods) define the exact shape every endpoint must return.
3. **Two faces, one fixture.** `dalseen-commercial-screens.compiled.js` mounts both Super Admin screens (`PlatformSignupQueue`, `PlatformOnboardingWorkbench`, `PlatformPlansEditor`, `PlatformSupportDesk`) **and** the tenant-side mirror (`OwnerPlanShop`, `OwnerInvoices`, `OwnerSupportInbox`) against the same `window.DALSEEN_BILLING` object. Backend must serve both views — staff sees the queue, tenant sees only its own row.

---

## §1 — Module overview

### 1.1 File census

| File | LOC | Role |
|---|---|---|
| `platform.compiled.js` | 729 | `PlatformDashboard` (super-admin home: hero, KPIs, system cards, tenant table, MRR trend, incidents feed); `RoleSwitcher` |
| `platform-screens.compiled.js` | 3,315 | The 7 **first-gen** super-admin screens: `PlatformTenants`, `PlatformBilling`, `PlatformHealth` (basic, later overridden), `PlatformCompliance`, `PlatformIncidents`, `PlatformReleases`, `PlatformAudit`. Includes `PlansGrid`, `Sparkline`, `pSectionHeader`, `pillBtn`, `chip`, `FilterGroup` helpers |
| `platform-admin.compiled.js` | 1,137 | `PlatformOnboarding` (per-tenant 8-step funnel viewer); `OnboardingDrawer` (step inspector); `ImpersonationBanner` (global staff-impersonating-tenant strip); `HealthFactorsPanel` |
| `platform-health.compiled.js` | 2,190 | `PlatformHealthFull` (deep version that **overrides** `window.PlatformHealth`); `LatencyChart`, `NodeGrid`, `ServiceDrawer`, `HealthActionModals` (Restart, Failover, Drain, Acknowledge), `HealthSparkline`, `HFilter` |
| `dalseen-commercial-screens.compiled.js` | ~1,950 | The 4 **second-gen** commercial screens (Signup Queue, Onboarding Workbench, Plans Editor, Support Desk) **plus** their tenant-side mirrors (`OwnerPlanShop`, `OwnerInvoices`, `OwnerSupportInbox`) |
| `billing-actions.compiled.js` | 1,840 | `BillingActions` switcher → `PlanEditorModal`, `PlanStatusModal`, `RemindModal`, `MarkPaidModal`, `RunBillingModal`; `InvoiceDrawer`; `MrrTrend`; `exportInvoicesCSV`; `BILLING_META` (period/kind option metadata) |
| `tenant-actions.compiled.js` | ~520 | `TenantActionModals` switcher → Impersonate / Change plan / Message / Suspend / Reactivate / Export CSV. Each mutates `DALSEEN_PLATFORM.tenants` and calls `onUpdate()` |
| `new-tenant-wizard.compiled.js` | 1,657 | `NewTenantWizard` (5-step: Business → Systems → Plan → Admin user → Review/Provision) + `Step1`–`Step5`, `PlanCard`, `ReviewRow`, `ProvisioningView` |
| `dalseen-billing-data.js` | 495 | The commercial fixture — see §1.3 |
| `platform-live.jsx` | ~285 | Live API wrappers around `PlatformDashboard`, `PlatformOnboarding`, `HealthFactorsPanel`, `TenantActionModals` (the only 4 surfaces with OpenAPI coverage today) |

**Total: ~14,000 LOC across 10 files.** `platform-screens` + `platform-health` + `new-tenant-wizard` alone are >7,100 LOC.

### 1.2 Routing

The app shell (in `front/common/roles.js` or the root `index.html` chooser) reads a single string route stored in `localStorage['dalseen.route']` (default `'platform'`) and switches on it. Cross-screen navigation uses an `onNav(route)` prop threaded down from the shell — there is no central router or deep-link map.

| Route id | Component | Permission gate |
|---|---|---|
| `platform` | `PlatformDashboard` | `platform.tenants.view` (or `platform`) |
| `platform.tenants` | `PlatformTenants` | `platform.tenants.view` |
| `platform.billing` | `PlatformBilling` | `platform.billing.view` |
| `platform.health` | `PlatformHealth` (= `PlatformHealthFull`) | `platform.health.view` |
| `platform.compliance` | `PlatformCompliance` | `platform.compliance.view` |
| `platform.incidents` | `PlatformIncidents` | `platform.incidents.view` |
| `platform.releases` | `PlatformReleases` | `platform.releases.view` |
| `platform.audit` | `PlatformAudit` | `platform.audit.view` |
| `platform.onboarding` | `PlatformOnboarding` | `platform.signups.view` |
| `platform.signups` | `PlatformSignupQueue` | `platform.signups.view` |
| `platform.workbench` | `PlatformOnboardingWorkbench` | `platform.signups.edit` |
| `platform.plansEditor` | `PlatformPlansEditor` | `platform.plans.edit` |
| `platform.support` | `PlatformSupportDesk` | `platform.support.view` |

> **Production refactor:** Same gap as Accounting/Owner — replace the prop-drilled string route with the platform-wide router, and gate every route through `platform.staff` middleware on the server.

The dashboard's "New tenant" button calls `window.__openNewTenant && window.__openNewTenant()` — a global hook the shell installs. The wizard host listens; pattern is the same as `__openComposer` in Accounting.

### 1.3 The commercial model (`window.DALSEEN_BILLING`)

This is the source of truth for plans/services/onboarding/support. Every commercial screen reads from here.

**3 plan tiers** (`PLANS`):

| id | Monthly (SAR) | Annual (SAR, 15% off) | Included users | Included branches | Included services | Support |
|---|---:|---:|---:|---:|---|---|
| `starter` | 199 | 2,030 | 3 | 1 | `[retail]` | `standard` |
| `growth` (featured) | 499 | 5,090 | 3 | 1 | `[retail, pay, dine]` | `priority` |
| `enterprise` | 1,499 | 15,290 | 3 | 1 | all 10 services | `platinum` |

**Unit add-ons** (`UNIT_ADDONS`): `extraUser` 10 SAR/user/month, `extraBranch` 999 SAR/branch/year.

**10 service add-ons** (`SERVICES`): `retail` 149/1520, `pay` 99/1010 (+ 0.9% mada txn fee), `dine` 199/2030, `flow` 249/2540, `accounting` 129/1320, `hr` 179/1830, `ai` 79/810, `zatca` 49/500, `wa` 69/700, `marketing` 89/910 (monthly/annual SAR).

**3 support tiers** (`SUPPORT_TIERS`): `standard` (≤4h Sun–Thu, 0 phone min) → `priority` (≤1h 7 days, 180 min) → `platinum` (≤15min 24/7 named CSM, unlimited).

**6 account managers** (`ACCOUNT_MANAGERS`): each has region, plan-affinity, workload/capacity. `suggestAM(signup)` picks by region match (3pts) + free capacity + senior-on-enterprise bonus (2pts).

**8 onboarding steps** (`ONBOARDING_STEPS`): kyc → contract → billing → branches → users → zatca → training → golive (each with owner=am|tenant + estimated minutes).

**Persisted helpers exposed on `DALSEEN_BILLING`:**
- `findPlan(id)`, `findService(id)`, `findAM(id)`
- `suggestAM(signup)` — returns the best-fit AM
- `quote({ planId, cadence, extraUsers, extraBranches, services })` — returns `{ lines, cadence, subtotal, vat, grand }` line-by-line breakdown. **This is the single source of pricing math.** Used by `OwnerPlanShop` for live re-quote and by `NewTenantWizard` Step 3.

> **Backend equivalence:** The `quote()` function must be reimplemented server-side as `POST /api/v1/platform/subscriptions/preview` (or `/billing/quote`). It is the chokepoint for every plan-change action and must produce byte-identical line items so the UI doesn't drift.

### 1.4 Fixtures (`window.DALSEEN_PLATFORM` in `roles.js`)

Top-level keys, in the order the dashboard reads them:

- `kpis = { tenants:{value,delta}, mrr, gmv, uptime }` — hero numbers
- `systems = [{ id, usage, … }]` — Retail / Pay / Dine adoption cards
- `tenants = [12 items]` — `{ id, name:{en,ar}, plan, systems[], branches, users, mrr, status, since, region, contact, gmv, lastActive, health }`
- `plans = [8 items]` — full plan catalogue (subset of `DALSEEN_BILLING.PLANS` with `tenants` count + `status` field for archive/active)
- `invoices = [12 items]` — `{ id, tenant, date, due, amount, status: paid|pending|overdue }`
- `healthMetrics = [12 services]` — `{ id, en, ar, region, latency, sla, uptime, req24h, deps[], status }`
- `compliance = [6 items]` — `{ id, name, scope, auditor, nextAudit, score, status: ok|warn|err }`
- `incidents = [14 items]` — `{ date, t, sev: info|warn|ok|err, area, en, ar, impact, duration }`
- `releases = [4 items]` — `{ v, date, ring: prod|staging, changes:[{ t: feat|fix|perf|sec, en, ar }] }`
- `audit = [16 events]` — `{ date, t, act, actor, target, meta, ip }`

`window.DALSEEN_ONBOARDING_STEPS` mirrors `DALSEEN_BILLING.ONBOARDING_STEPS` and is read by `PlatformOnboarding`.

---

## §2 — Screen-by-screen

### 2.1 `PlatformDashboard` — `platform.compiled.js`

**Route:** `platform`

**Renders:**
- **Hero strip** — gradient `T.ink → T.accent2`, "DALSEEN Super Admin", H1 "Platform", subtitle, then 4 KPI tiles: Tenants / MRR / GMV·30d / Uptime (each with `delta` line)
- **Systems row** — 3 cards (Retail / Pay / Dine) showing tenant count, % adoption bar, "Operational" badge
- **Tenants table** (left, 1.8fr) — header row "Business · Plan · Systems · Br · MRR · Since · Status", scrollable list of `P.tenants` (max 420px). Each row → click opens drawer (handled by `TenantActionModals`?). "+ New tenant" button → `window.__openNewTenant()`
- **Right column** (1fr) — Recent incidents feed (severity dot + msg) + MRR sparkline (uses `window.MrrTrend`)

**Reads:** `DALSEEN_PLATFORM.kpis`, `.systems`, `.tenants`, `.incidents`

**Writes:** none directly. "New tenant" hands off to `NewTenantWizard`; row clicks open `TenantActionModals`.

**API mapping (live wrapper `PlatformDashboardLive`):**
- `GET /api/v1/platform/dashboard` → returns `{ kpis, systems, recentIncidents, mrrTrend }` (the wrapper currently passes data through but the inner component still reads from `window.DALSEEN_PLATFORM` — at cut-over this read must move into props).

**Permission gate:** `platform.tenants.view` || `platform`

**Production additions:**
- `mrrTrend` should come from a real time-series query, not a deterministic seed
- Tenant row badges (`active/trial/overdue`) must derive from billing state, not a frozen field
- "Recent incidents" feed should respect `read_at` per-staff-user

---

### 2.2 `PlatformTenants` — `platform-screens.compiled.js` (lines ~1–620)

**Route:** `platform.tenants`

**Renders:**
- Section header "Tenants" with subtitle "Manage every business · their plans · users · health"
- Filter bar: search (id or name), plan filter pills (All / Starter / Growth / Enterprise), status filter pills (All / Active / Trial / Overdue), system filter (All / Retail / Pay / Dine), result count
- Tenants table with columns: Business / Plan badge / Systems icons / Branches / Users / MRR / Last active / Health dot / GMV·30d / Action menu
- Row click → **Tenant detail drawer** (right-side, 540px wide): tabs "Overview / Health / Billing / Audit"; impersonate/suspend/change-plan buttons fire `TenantActionModals`

**Reads:** `DALSEEN_PLATFORM.tenants`

**Writes (via `TenantActionModals`):**
- Impersonate → sets `window.__impersonating = { tenantId, until }`, mounts `ImpersonationBanner` globally, redirects to `owner` route
- Change plan → mutates `tenant.plan` and `tenant.mrr` via `quote()` re-preview
- Suspend → mutates `tenant.status = 'suspended'`
- Reactivate → mutates `tenant.status = 'active'`
- Message → no-op stub (TODO: ticket creation)
- Export CSV → `exportInvoicesCSV()` from `billing-actions`

**API mapping:**
- `GET /api/v1/platform/tenants?per_page=200` → list (already wrapped by `PlatformOnboardingLive`, reused here)
- `GET /api/v1/platform/tenants/{id}` → drawer detail (NEW — needed)
- `POST /api/v1/platform/tenants/{id}/impersonate` → returns short-lived staff-as-tenant token (NEW)
- `POST /api/v1/platform/tenants/{id}/suspend` (NEW)
- `POST /api/v1/platform/tenants/{id}/reactivate` (NEW)
- `POST /api/v1/platform/tenants/{id}/change-plan` body `{ planId, cadence, services[], extraUsers, extraBranches }` (NEW — must call `subscriptions/preview` first for confirmation)
- `POST /api/v1/platform/tenants/{id}/messages` body `{ subject, body, channel }` (NEW)
- `GET /api/v1/platform/tenants/{id}/export.csv` (NEW)

**Permission gate:** `platform.tenants.view` (read), `platform.tenants.edit` (mutations), `platform.tenants.impersonate` (impersonation specifically — must be a separate permission, audit-logged on every use)

**Production additions:**
- Audit log entry for every action (already mocked — see `audit` fixture for shape: `act: 'tenant.suspend'`, `actor`, `target`, `meta`, `ip`)
- Impersonation must time out (drawer shows `until`); banner must stay visible for the entire impersonation window
- Suspend must cascade: terminate tenant sessions, freeze billing, lock POS

---

### 2.3 `PlatformBilling` — `platform-screens.compiled.js` (lines ~620–1100)

**Route:** `platform.billing`

**Renders:**
- Section header "Billing & Plans" with "+ New plan" button (opens `PlanEditorModal`) and "Run billing" button (opens `RunBillingModal`)
- 4 KPI tiles: MRR / Net new MRR / Overdue / Pending
- `MrrTrend` chart (12-month sparkline + axis)
- `PlansGrid` — 3-up cards per plan (price, tenants count, status, edit/archive buttons); featured plan gets gold ring
- **Overdue strip** (if any) — danger card: "{n} overdue invoices · {sum SAR} · Send bulk reminders" → `RemindModal` for all overdue
- **Invoices table** — search by id/tenant, filter by status (All/Paid/Pending/Overdue), columns Invoice / Tenant / Issued / Amount / Due / Status / actions (mark-paid + remind icons). Click row → `InvoiceDrawer`

**Reads:** `DALSEEN_PLATFORM.plans`, `.invoices`, helpers from `DALSEEN_BILLING`

**Writes (via `BillingActions` switcher):**
- `PlanEditorModal` (create/edit) — pushes/edits `DALSEEN_PLATFORM.plans`
- `PlanStatusModal` (suspend/archive/activate) — sets `plan.status`
- `RemindModal` (single or bulk) — no-op mutation (logs the send), should eventually return `{ remindersSent, channel, queuedAt }`
- `MarkPaidModal` — sets `invoice.status = 'paid'`
- `RunBillingModal` — generates invoices for current period (the bulk version of "issue invoice")
- `exportInvoicesCSV(invoices, lang)` — builds CSV blob, triggers download

**API mapping:**
- `GET /api/v1/platform/plans` (NEW)
- `POST /api/v1/platform/plans` (NEW)
- `PATCH /api/v1/platform/plans/{id}` (NEW)
- `POST /api/v1/platform/plans/{id}/status` body `{ to: active|suspended|archived }` (NEW)
- `GET /api/v1/platform/invoices?status=&q=&page=` (NEW)
- `POST /api/v1/platform/invoices/{id}/mark-paid` (NEW — idempotency-key required, must record audit)
- `POST /api/v1/platform/invoices/{id}/remind` body `{ channel: email|whatsapp|sms }` (NEW)
- `POST /api/v1/platform/invoices/remind-bulk` body `{ filter: { status: 'overdue' } }` (NEW)
- `POST /api/v1/platform/billing/run` body `{ period }` (NEW — kicks the period invoice generation job)
- `GET /api/v1/platform/billing/mrr-trend?months=12` (NEW)

**Permission gate:** `platform.billing.view` / `platform.billing.edit` / `platform.plans.edit`

**Production additions:**
- `RunBillingModal` is a **long-running job** — needs job-queue + status polling (`GET /platform/jobs/{id}`); UI must handle "in progress" / "completed" states
- Plan changes must replay against active tenants (resolve which tenants are on the plan; trigger plan-migration flow per tenant)
- Mark-paid must accept reference (`{ method, reference, amount, occurred_at }`); current modal already collects these

---

### 2.4 `PlatformHealth` (= `PlatformHealthFull`) — `platform-health.compiled.js`

**Route:** `platform.health`

**Renders:**
- Section header "System Health" with "All systems operational" pill, refresh hint "Live · refreshes every 30s"
- 4 KPI tiles: Avg uptime / Avg latency / Req·24h / Error rate
- Filter bar: status (All / Healthy / Degraded / Down), region (All / Riyadh / Jeddah / Dammam), text search
- **Services table** — columns Service / Latency / SLA / Uptime / Req 24h / Latency trend sparkline. Click → `ServiceDrawer`
- **Region cards** — 3-up: Riyadh AZ-1 / Jeddah AZ-2 / Dammam AZ-3 with uptime %

**`ServiceDrawer`** (mounted on click):
- Header: service name + region + status pill
- `LatencyChart` — 60-point time-series with P50/P95/P99 lines + SLA threshold marker
- `NodeGrid` — per-instance status (3×4 grid of green/yellow/red cells)
- Dependents list (computed from `s.deps`)
- Recent events list (event type · timestamp)
- Action buttons → `HealthActionModals`: Restart / Failover / Drain / Acknowledge

**Reads:** `DALSEEN_PLATFORM.healthMetrics`

**Writes (via `HealthActionModals`):**
- Restart service — no-op fixture mutation
- Failover to standby — sets `service.region` swap
- Drain (graceful traffic stop) — sets `service.status = 'draining'`
- Acknowledge incident — appends to `service.recentEvents`

**API mapping:**
- `GET /api/v1/platform/health` (already wrapped by `HealthFactorsPanelLive`)
- `GET /api/v1/platform/health/{serviceId}` (NEW — drawer detail with time-series)
- `GET /api/v1/platform/health/{serviceId}/latency?range=1h|24h|7d` (NEW)
- `POST /api/v1/platform/health/{serviceId}/restart` (NEW — must use idempotency keys; node-by-node)
- `POST /api/v1/platform/health/{serviceId}/failover` body `{ to: 'AZ-2' }` (NEW)
- `POST /api/v1/platform/health/{serviceId}/drain` (NEW)
- `POST /api/v1/platform/health/{serviceId}/acknowledge` body `{ note }` (NEW)

**Permission gate:** `platform.health.view` / `platform.health.act`

**Production additions:**
- Replace deterministic `hSpark()` sparkline data with real Prometheus/CloudWatch query
- The 30-second refresh hint must actually fire — wire to `useApi` interval (currently a static label)
- Failover/drain are destructive; need confirm-with-typed-name modal pattern

---

### 2.5 `PlatformCompliance` — `platform-screens.compiled.js`

**Route:** `platform.compliance`

**Renders:**
- Section header
- 6 compliance cards in 3-up grid: top status stripe + shield icon + ID (e.g. `ISO-27001`) + scope + auditor + next audit + score donut + status pill
- **Data Residency card** (full width, AI-bg gradient, gold accent) — "All tenant data is stored and processed inside KSA in compliance with PDPL", 4 quick facts (Primary region: Riyadh / DR region: Jeddah / Retention: 7 years / Encryption: AES-256·KMS)

**Reads:** `DALSEEN_PLATFORM.compliance`

**Writes:** none (read-only screen for now)

**API mapping:**
- `GET /api/v1/platform/compliance` (NEW)
- `GET /api/v1/platform/compliance/{id}` (NEW — drawer with control-by-control breakdown, missing controls, evidence uploads)
- `POST /api/v1/platform/compliance/{id}/evidence` (NEW — upload audit evidence)

**Permission gate:** `platform.compliance.view`

**Production additions:**
- The "score" must be computed from a control checklist (not a stored number); evidence-management UI is missing today
- Data residency block hard-codes facts — should come from `GET /platform/data-residency`

---

### 2.6 `PlatformIncidents` — `platform-screens.compiled.js`

**Route:** `platform.incidents`

**Renders:**
- Section header with "Export" + "Log incident" buttons
- 4 KPI tiles: Today / Warnings / Errors / Avg MTTR
- **Timeline grouped by date** — for each date: header label + list of `{ t, sev, area, en/ar message, impact, duration, severity pill }`. Severity dot + colored background tint per row.

**Reads:** `DALSEEN_PLATFORM.incidents`

**Writes:** none in current FE (Log incident button is a stub).

**API mapping:**
- `GET /api/v1/platform/incidents?from=&to=&sev=&area=` (NEW)
- `POST /api/v1/platform/incidents` body `{ sev, area, message, impact, scope:[tenantIds] }` (NEW — must broadcast to affected tenants)
- `PATCH /api/v1/platform/incidents/{id}` (NEW — update during incident)
- `POST /api/v1/platform/incidents/{id}/resolve` (NEW)
- `GET /api/v1/platform/incidents.csv` (NEW — Export button)

**Permission gate:** `platform.incidents.view` / `platform.incidents.edit`

**Production additions:**
- Tenant-scoped cascade — if `area === 'mada'`, affected tenants need a status banner; current FE has no such broadcast
- `duration` must be auto-computed from `started_at`/`resolved_at`, not stored

---

### 2.7 `PlatformReleases` — `platform-screens.compiled.js`

**Route:** `platform.releases`

**Renders:**
- Section header with "Publish" button (opens release wizard, currently stub)
- Release cards (one per `release` row): version `v{ver}` with date + ring badge (prod = accent green, staging = warn orange), N changes count, "Notes →" button
- Each card body: change list grouped by type (Feature / Fix / Perf / Security) — color-coded type pill + change description (en/ar)

**Reads:** `DALSEEN_PLATFORM.releases`

**Writes:** none in current FE.

**API mapping:**
- `GET /api/v1/platform/releases` (NEW)
- `POST /api/v1/platform/releases` body `{ v, ring, changes:[{t,en,ar}] }` (NEW)
- `POST /api/v1/platform/releases/{v}/promote` body `{ from: 'staging', to: 'prod' }` (NEW)
- `GET /api/v1/platform/releases/{v}/notes` (NEW — full release-notes markdown)

**Permission gate:** `platform.releases.view` / `platform.releases.edit`

**Production additions:**
- Tenant-side release-notes feed (every tenant sees a "What's new" badge when a prod release is promoted) — needs `read_at` per user
- Staging → prod promotion must check incident state, run health checks, get sign-off

---

### 2.8 `PlatformAudit` — `platform-screens.compiled.js`

**Route:** `platform.audit`

**Renders:**
- Section header with Search + Export buttons
- Timeline grouped by date — each row: timestamp · color dot · `act` (mono, color-coded by action type) · actor · target · meta · IP

**Action color map:**
- `tenant.suspend` → danger
- `tenant.login` (impersonation) → warn
- `tenant.create` → ok
- `plan.change` → accent
- `release.publish` → gold
- `feature.enable` → accent
- `invoice.send` → pay
- `policy.update` / `policy.create` → ink

**Reads:** `DALSEEN_PLATFORM.audit`

**Writes:** none — this screen is **append-only consumer** of the audit ledger. **Every other platform action writes to it.**

**API mapping:**
- `GET /api/v1/platform/audit?actor=&target=&act=&from=&to=&page=` (NEW)
- `GET /api/v1/platform/audit.csv` (NEW)

**Permission gate:** `platform.audit.view`

**Production additions:**
- The audit ledger is the **one append-only table** in the system — never edit, never delete. Backend should write here from every other platform endpoint as a side-effect (transactional outbox pattern recommended).
- Index on `(actor, target, act, ts)` is mandatory; this view will outgrow the table scan path quickly.
- IP must always be set; failed auth attempts should still hit this log.

---

### 2.9 `PlatformOnboarding` — `platform-admin.compiled.js`

**Route:** `platform.onboarding`

**Renders:**
- Section header "Onboarding funnel"
- Stage funnel chart — 5 stages (Lead → Trial → Activated → Live → Churned), with counts and conversion %
- Per-tenant onboarding table — columns Business / Stage / Days in stage / Health / AM / Progress %
- Row click → `OnboardingDrawer` (right-side):
  - Tenant header
  - 8-step checklist (`DALSEEN_ONBOARDING_STEPS`): each step shows owner (am/tenant), estimated time, completed-at if done, action button if pending
  - Notes thread (free-text comments by AMs)
  - "Mark as live" button when all 8 done

**Reads:** `DALSEEN_PLATFORM.tenants` (filtered by `status === 'trial'` or progress < 100), `DALSEEN_ONBOARDING_STEPS`

**Writes:**
- Mark step done → mutates `tenant.onboarding[stepId] = { done:true, doneAt }` (currently no fixture has this nested shape — see Production additions)
- Mark as live → `tenant.status = 'active'` and emits a `tenant.activated` audit event

**API mapping:**
- `GET /api/v1/platform/onboarding` — funnel + per-tenant list (the wrapper currently calls `tenants.list({per_page:200})` and synthesizes the funnel client-side; backend should provide aggregate)
- `PATCH /api/v1/platform/tenants/{id}/onboarding/{stepId}` body `{ done, note }` (NEW)
- `POST /api/v1/platform/tenants/{id}/activate` (NEW — alias for the all-steps-done finalize)

**Permission gate:** `platform.signups.view` (read), `platform.signups.edit` (mutate)

**Production additions:**
- Add a nested `tenant.onboarding` shape to the fixture: `{ kyc:{done,doneAt}, contract:{done}, …, golive:{done} }` so the drawer doesn't have to fake it
- AM notes thread needs its own table (`onboarding_notes` per tenant) — currently inline in `tenant.onboarding`
- The funnel chart must paginate at >1000 active onboardings — server-side aggregate is non-negotiable

---

### 2.10 `PlatformSignupQueue` — `dalseen-commercial-screens.compiled.js`

**Route:** `platform.signups`

**Renders:**
- Section header "Signup queue"
- Status filter pills: Submitted / Assigned / Onboarding / Live
- Two-column layout:
  - **Left:** scrollable list of `DALSEEN_BILLING.SIGNUP_QUEUE` rows showing business name, plan badge, services count, submitted-at, status pill, AM avatar (or "unassigned" pill)
  - **Right:** `SignupDetail` panel — KYC info (CRN / VAT / NID), owner contact, region/city, business type, requested plan + services with `quote()` live preview, AM suggestion (`suggestAM(signup)`) with override picker, "Assign to {AM}" / "Start onboarding" buttons
- Click "Start onboarding" → sets `window._dalseenActiveSignup = signup.id` and navigates to `platform.workbench`

**Reads:** `DALSEEN_BILLING.SIGNUP_QUEUE`, `.ACCOUNT_MANAGERS`; calls `quote()` and `suggestAM()`

**Writes:**
- Assign AM → `signup.amId = amId`, `signup.status = 'assigned'`
- Reject signup → `signup.status = 'rejected'` (not currently in fixture status enum — gap)

**API mapping:**
- `GET /api/v1/platform/signups?status=&page=` (NEW)
- `POST /api/v1/platform/signups/{id}/assign` body `{ amId }` (NEW)
- `POST /api/v1/platform/signups/{id}/reject` body `{ reason }` (NEW)
- `POST /api/v1/platform/signups/{id}/start-onboarding` (NEW — transitions status to `onboarding` and creates the per-tenant onboarding record)
- `POST /api/v1/platform/account-managers/suggest` body `{ planId, region }` → `{ amId, fitScore }` (NEW — backend implementation of `suggestAM`)
- `POST /api/v1/platform/subscriptions/preview` body `{ planId, cadence, services, extraUsers, extraBranches }` → quote (NEW — backend implementation of `quote`)

**Permission gate:** `platform.signups.view` / `platform.signups.edit`

**Production additions:**
- KYC fields (CRN, VAT, NID) need real verification — integrate with Wathq / Saudi Business Center; today they're free-text in the fixture
- Auto-assign should happen at submission time on the backend (not deferred to UI), with manual override as the exception path
- Trial 14-day timer must enforce — auto-suspend if not converted

---

### 2.11 `PlatformOnboardingWorkbench` — `dalseen-commercial-screens.compiled.js`

**Route:** `platform.workbench` (deep-linked from Signup Queue via `window._dalseenActiveSignup`)

**Renders:**
- Header: tenant name + plan + AM + days-in-trial counter
- Vertical step list (`ONBOARDING_STEPS`) — each step shows owner badge (AM/tenant), estimated time, status (done/in-progress/pending), and a `workbenchStepBody(T, lang, step, signup)` body that's bespoke per step:
  - **kyc** → CRN/VAT/NID lookup + "Verify with Wathq" button
  - **contract** → MSA upload + "Send for e-sig" button (DocuSign-style flow)
  - **billing** → payment method capture (mada/visa/bank transfer)
  - **branches** → branch CRUD form (name, address, city)
  - **users** → invite list (email, role)
  - **zatca** → device serials, certificate request
  - **training** → calendar slot picker (1-hour live training)
  - **golive** → final checklist + "Mark as live" button
- Right rail: notes thread between AM and tenant; `quote()` live re-preview

**Reads:** `DALSEEN_BILLING.SIGNUP_QUEUE` filtered by active id, `.ONBOARDING_STEPS`, `.ACCOUNT_MANAGERS`

**Writes:**
- Per-step completion → `signup.progress` increments, step body mutates fixture
- Notes appended to `signup.notes[]` (not in fixture — gap)

**API mapping:** same as `PlatformOnboarding` (§2.9) plus:
- `POST /api/v1/platform/signups/{id}/notes` body `{ message, visibility: am-only|tenant-visible }` (NEW)
- `POST /api/v1/platform/signups/{id}/kyc/verify` body `{ crn, vat, nid }` → Wathq proxy (NEW)
- `POST /api/v1/platform/signups/{id}/contract/send` (NEW — e-sig integration)

**Permission gate:** `platform.signups.edit`

**Production additions:**
- Per-step body components must be split out into their own files at production scale; the inline switch in `workbenchStepBody()` will not scale beyond 8 steps
- Each step needs SLA tracking (started_at / done_at) for AM performance reports
- Tenant-side mirror: tenant should see the same checklist with their own action buttons (this is half-built today via `OwnerOnboarding`, but not wired)

---

### 2.12 `PlatformPlansEditor` — `dalseen-commercial-screens.compiled.js`

**Route:** `platform.plansEditor`

**Renders:**
- Section header "Plans & Pricing"
- 3 plan cards (Starter / Growth / Enterprise) with editable fields:
  - Monthly price (SAR)
  - Annual price (SAR — auto-calc 15% off, override-able)
  - Included users / branches (qty editors)
  - Included services (toggle chips, 10 services)
  - Tagline (en/ar)
  - Featured toggle
  - Color picker
- "Save changes" button — diffs against `DALSEEN_BILLING.PLANS` and writes back

**Reads:** `DALSEEN_BILLING.PLANS`, `.SERVICES`, `.UNIT_ADDONS`

**Writes:**
- Saves edits to `DALSEEN_BILLING.PLANS` in-place — every screen using `findPlan()` updates immediately

**API mapping:**
- `GET /api/v1/platform/plans` (overlap with §2.3)
- `PATCH /api/v1/platform/plans/{id}` (overlap with §2.3)
- `GET /api/v1/platform/services` (NEW — service catalogue)
- `PATCH /api/v1/platform/services/{id}` (NEW — service price/cadence edit)
- `GET /api/v1/platform/unit-addons` (NEW)
- `PATCH /api/v1/platform/unit-addons/{id}` (NEW)

**Permission gate:** `platform.plans.edit`

**Production additions:**
- Plan price changes must trigger a notice cycle to active tenants (30-day notice mandatory in KSA for SaaS pricing changes); current FE has no such workflow
- Color picker should restrict to design-system tokens, not free hex
- Diff preview ("12 active tenants will see a +49 SAR/month increase") is missing

---

### 2.13 `PlatformSupportDesk` — `dalseen-commercial-screens.compiled.js`

**Route:** `platform.support`

**Renders:**
- Section header "Support desk"
- 4 KPI tiles: Open / SLA breached / Waiting customer / Resolved today
- Ticket queue with filters (priority / channel / tier / assignee). Columns Subject / Tenant / Channel / Tier / Wait time / Status / Assignee. SLA-breached rows get a red left stripe.
- Click ticket → drawer: subject, full thread, reply box, internal notes (AM-only), close button. Tier badge + included channels list.
- "Assign to me" button (AM auto-assigns)

**Reads:** `DALSEEN_BILLING.SUPPORT_TICKETS`, `.SUPPORT_TIERS`, `.SUPPORT_CHANNELS`, `.ACCOUNT_MANAGERS`

**Writes:**
- Reply → appends to `ticket.thread[]` (not in fixture — gap; thread is computed)
- Assign → `ticket.assignee = amId`
- Resolve → `ticket.status = 'resolved'`, `ticket.resolved = now`
- Re-open → `ticket.status = 'open'`

**API mapping:**
- `GET /api/v1/platform/support/tickets?status=&priority=&assignee=&page=` (NEW)
- `GET /api/v1/platform/support/tickets/{id}` (NEW)
- `POST /api/v1/platform/support/tickets/{id}/reply` body `{ message, attachments[] }` (NEW)
- `POST /api/v1/platform/support/tickets/{id}/assign` body `{ amId }` (NEW)
- `POST /api/v1/platform/support/tickets/{id}/resolve` body `{ resolution }` (NEW)
- `POST /api/v1/platform/support/tickets/{id}/reopen` (NEW)

**Permission gate:** `platform.support.view` / `platform.support.act`

**Production additions:**
- Tickets need attachment upload (signed S3 URL) — fixture has none
- Channel routing: WhatsApp/email/phone require provider integrations (Twilio, etc.) — UI assumes the channel works
- SLA timer must compute breach state server-side; UI is just rendering the count

---

### 2.14 Tenant-side mirrors — `dalseen-commercial-screens.compiled.js`

These three screens belong to the **tenant**, not Super Admin, but live in the same file because they share the `DALSEEN_BILLING` fixture.

#### 2.14.1 `OwnerPlanShop`
**Route (tenant-side):** `owner.plans` (or wherever the tenant shell mounts it)

- Knobs: plan tier (Starter / Growth / Enterprise), cadence (monthly / annual), users (qty), branches (qty), services (toggle chips)
- Live quote breakdown via `quote()` — line-by-line with subtotal / VAT / grand total
- "Apply changes" button → updates the active subscription
- The body component is `OwnerPlanShopBody` with helper `qtyControl` for the +/− steppers

**API mapping:**
- `GET /api/v1/me/subscription` (NEW)
- `POST /api/v1/me/subscription/preview` body `{ knobs }` → quote (already covered as `subscriptions/preview`)
- `POST /api/v1/me/subscription/change` body `{ knobs }` (NEW)

#### 2.14.2 `OwnerInvoices`
**Route:** `owner.invoices`

- `PaymentMethodsStrip` — chips for each payment method (mada/visa/Apple Pay/bank), default star, "Add method" button
- Invoice list (left, 360px) + `InvoiceDetail` (right) with line items, "Pay now" / "Download PDF" buttons
- Pays via the default payment method; 422 errors branch on `error.code` per `docs/02-CONVENTIONS.md`

**API mapping:**
- `GET /api/v1/me/invoices?status=&page=` (NEW)
- `GET /api/v1/me/invoices/{id}` (NEW)
- `GET /api/v1/me/invoices/{id}/download` (NEW — PDF blob)
- `POST /api/v1/me/invoices/{id}/pay` body `{ paymentMethodId }` (NEW — idempotency-key required)
- `GET /api/v1/me/payment-methods` (NEW)
- `POST /api/v1/me/payment-methods` (NEW — tokenized via Pay gateway)
- `POST /api/v1/me/payment-methods/{id}/default` (NEW)

#### 2.14.3 `OwnerSupportInbox`
**Route:** `owner.support`

- SLA card (per tenant's tier — Standard / Priority / Platinum)
- Ticket list + `TicketDetail` with reply box; channel selector limited to tenant's tier
- "New ticket" → `NewTicketDialog` with 422 (missing subject) and 403 (`tier_locked` — channel not allowed for tier) error branches

**API mapping:**
- `GET /api/v1/me/support/tickets` (NEW)
- `POST /api/v1/me/support/tickets` body `{ subject, channel, priority, message }` (NEW — 422 on missing subject, 403 on tier-locked channel)
- `POST /api/v1/me/support/tickets/{id}/reply` (NEW)
- `POST /api/v1/me/support/tickets/{id}/close` (NEW)

---

### 2.15 `NewTenantWizard` — `new-tenant-wizard.compiled.js`

**Mounting:** Modal triggered by `window.__openNewTenant && window.__openNewTenant()` from `PlatformDashboard` and `PlatformTenants`. Hosted by the shell.

**5 steps:**
1. **Step1 – Business** — name (en/ar), CRN, VAT, owner name + NID, mobile, email, region, city, business type
2. **Step2 – Systems** — pick from 10 services as toggle chips
3. **Step3 – Plan** — `PlanCard` per tier (Starter/Growth/Enterprise), cadence toggle, extras (users/branches), live quote via `quote()`
4. **Step4 – Admin user** — first user invite (email, name, role default = owner)
5. **Step5 – Review** — `ReviewRow` per section, with edit-this-step links; "Provision tenant" button → `ProvisioningView` animation

**`ProvisioningView`:** 6-second animated sequence with step reveals — Creating workspace → Seeding COA → Provisioning ZATCA cert → Inviting users → Booking training → Done. On done, prepends new tenant to `DALSEEN_PLATFORM.tenants` and emits `onCreate(newTenant)`.

**API mapping:**
- `POST /api/v1/platform/tenants` body `{ business, systems, plan, cadence, extras, adminUser }` (NEW — long-running, returns `{ id, jobId }`)
- `GET /api/v1/platform/tenants/{id}/provisioning-status` (NEW — poll while wizard's progress bar advances)

**Permission gate:** `platform.tenants.create`

**Production additions:**
- Provisioning is a multi-step orchestration (Saga pattern recommended): if ZATCA cert request fails, the workspace must be rolled back
- Idempotency key on `POST /platform/tenants` — wizard must keep the same key across step5 retries
- Email/SMS to admin user with first-login link must fire on success (not part of FE today)

---

### 2.16 `ImpersonationBanner` — `platform-admin.compiled.js`

**Mounting:** Always rendered at app shell top when `window.__impersonating` is set.

- Renders as a sticky orange strip across the top of every screen (above topbar)
- Shows: "Acting as **{tenantName}** · {minutes} left · [End impersonation]"
- "End impersonation" button → calls `window.__endImpersonation()` (shell hook), restores staff session, clears banner

**API mapping:** part of impersonation flow (§2.2)

**Production additions:**
- The impersonation token must be a **separately-scoped** JWT (cannot use it to act AS the tenant for irreversible actions like e-invoice clearance — read-only or "as-staff-impersonating-X" audit-tagged writes only)
- Banner must persist across page refreshes (token is the source of truth, not the global)

---

## §3 — Cross-cutting concerns

### 3.1 Audit log is universal

Every Platform action — every plan change, suspension, impersonation, invoice mark-paid, signup assignment, ticket reply, release publish — must write to the `audit` ledger. The `act` field uses `domain.action` form (`tenant.suspend`, `plan.change`, `invoice.send`, `release.publish`, `policy.update`, `feature.enable`, `tenant.login` for impersonation).

The fixture has 9 distinct `act` types but the production list will be ~30. Recommended canonical set:

| Domain | Actions |
|---|---|
| `tenant` | `create`, `update`, `suspend`, `reactivate`, `delete`, `login` (impersonate), `change-plan`, `activate` |
| `plan` | `create`, `update`, `suspend`, `archive`, `activate` |
| `invoice` | `send`, `mark-paid`, `remind`, `void`, `download` |
| `signup` | `submit`, `assign`, `reject`, `start-onboarding`, `complete-step` |
| `health` | `restart`, `failover`, `drain`, `acknowledge` |
| `incident` | `create`, `update`, `resolve` |
| `release` | `publish`, `promote`, `rollback` |
| `support` | `create`, `reply`, `assign`, `resolve`, `reopen` |
| `policy` | `create`, `update` |
| `feature` | `enable`, `disable` |

### 3.2 Permission model

The live wrapper hints at the staff permission shape:
- `platform` (super-admin wildcard)
- `platform.tenants.{view, edit, impersonate, create}`
- `platform.billing.{view, edit}`
- `platform.plans.{view, edit}`
- `platform.health.{view, act}`
- `platform.compliance.view`
- `platform.incidents.{view, edit}`
- `platform.releases.{view, edit}`
- `platform.audit.view`
- `platform.signups.{view, edit}`
- `platform.support.{view, act}`

Tenant-side mirror permissions live under `me.*` and `owner.*` namespaces (`me.subscription.edit`, `me.invoices.pay`, `me.support.create`, etc.).

### 3.3 Backend coverage gap

Only 4 surfaces are wired through `useApi` today: `PlatformDashboard`, `PlatformOnboarding`, `HealthFactorsPanel`, `TenantActionModals`. Approximate **NEW endpoint count for Day-1 cut-over**: ~50 routes spanning tenants, plans, invoices, billing, health, compliance, incidents, releases, audit, signups, onboarding, support, account managers, services, unit-addons, payment methods, subscriptions/preview, jobs.

Suggested phasing (in priority order):

1. **Tenants CRUD + actions** (suspend, impersonate, change-plan) — unblocks Super Admin
2. **Subscriptions/preview + plans** — unblocks Plan Shop and New Tenant Wizard
3. **Invoices + payment methods** — unblocks tenant-side billing
4. **Audit log writes** (transactional outbox from every other endpoint)
5. **Signup queue + onboarding workbench** — replaces AM Excel sheet
6. **Support tickets** — replaces external helpdesk tool
7. **Health/incidents** — replaces ops dashboard
8. **Compliance/releases** — lowest priority, currently manual

### 3.4 Cross-tenant data must enforce `platform.staff` middleware

Every endpoint under `/api/v1/platform/*` must:
- Reject any request without a staff token
- Ignore `X-Tenant-Id` header (or require it explicitly per-endpoint, e.g. `GET /platform/tenants/{id}/onboarding` takes `id` in the path, not the header)
- Write the actor (staff user id) and target (tenant id when applicable) to the audit log atomically with the business write
- Use idempotency keys for every mutation (already standard elsewhere in the codebase)

---

## §4 — Pointer index

For "where do I find …" questions during the cut-over:

| Question | File · function |
|---|---|
| The pricing math (line items, VAT, annual discount) | `dalseen-billing-data.js` · `quote()` |
| The AM auto-assign logic | `dalseen-billing-data.js` · `suggestAM()` |
| The 8-step onboarding checklist | `dalseen-billing-data.js` · `ONBOARDING_STEPS` |
| The list of every staff permission used | `platform-live.jsx` · `canSee` checks (search `session.can(`) |
| How tenant impersonation actually mounts | `platform-admin.compiled.js` · `ImpersonationBanner`; shell hooks `__impersonating` / `__endImpersonation` |
| The audit-action color map | `platform-screens.compiled.js` · `PlatformAudit.actColor` |
| The release type-color map | `platform-screens.compiled.js` · `PlatformReleases.typeCol` |
| The incident severity-color map | `platform-screens.compiled.js` · `PlatformIncidents.sevCol` |
| The CSV exporter | `billing-actions.compiled.js` · `window.exportInvoicesCSV` |
| The MRR sparkline | `billing-actions.compiled.js` · `MrrTrend` |
| The full health drawer (latency chart + node grid) | `platform-health.compiled.js` · `ServiceDrawer` |
| Health action modals (Restart/Failover/Drain/Acknowledge) | `platform-health.compiled.js` · `HealthActionModals` |
| Where `__openNewTenant` is wired | `platform.compiled.js` (caller) + shell host (callee) |
| How the tenant-side "Plan Shop" reuses Super Admin data | `dalseen-commercial-screens.compiled.js` · `OwnerPlanShop` reads `DALSEEN_BILLING` exactly like the staff screens |

---

*End of Platform inventory.*
