# Admin parity audit — Home · Dashboard · Analytics · Costs · Settings · Live chat

Demo truth: `Navagoo_MI/navagoo-app` v0.28 (`src/portals/admin/*`, i18n `src/i18n/en.ts`).
Portal: `Navagoo/backend` (views listed per section). Audit is source-level, prop-by-prop.
Legend: **GAP** = demo has it, portal doesn't / differs. **SUPERSET** = portal-only addition.

---

## 1. HOME — `backend/views/site/_admin_home.php` (rendered from `site/index.php:199` when `!$isShopOwner`)
Demo: `src/portals/admin/Home.tsx` + `home/SidePanels.tsx` + `home/BottomBand.tsx`

### Data-fidelity gaps (highest severity on this screen)
- **KPI tile 4 "Settlement Liability" wired to the wrong metric**: portal uses `$moneyInMotion['transferQueue']` (`_admin_home.php:107`); demo tile uses `settlementLiability(state)` — a distinct figure (total positive running balances, not just the queued transfers). Demo's *Money in motion → In transfer queue* row is the `transferQueue` figure.
- **KPI tile 5 "Outstanding Receivables" wired to the wrong metric**: portal uses `$moneyInMotion['pendingVerification']` (`:115`); demo uses `outstandingReceivables(state)` (unpaid charges). Pending-verification is the *Money in motion* row 2 value, not the receivables KPI.
- **Attention jump-chip count**: demo counts attention *items* (`items.length` → "N items need attention ↓"); portal sums per-item `count` columns (`array_sum(array_column($attention,'count'))`, `:62`) → inflated number.
- **No permission-gated ops variant**: demo swaps tiles 3-6 to Total bookings / Active specialists / Cities / Pending activations and MoneyInMotion→ActivationQueue when the admin lacks `admin.finance`. Portal always renders the money variant.

### Missing elements
- **KPI delta chip on Earnings MTD only** — present ✓, but demo also animates all 6 KPIs with count-up on mount (cosmetic, skip or note).
- **Recent activity "View events log" header action link** (demo `BottomBand.tsx:96-101` → `/admin/events`): portal card has no action link (`:331-349`).
- **Card subtitles missing**: Today vs Yesterday sub `live counters · delta vs the same time window` (portal has none, `:201`).
- **Platform week x-axis**: demo `WeekBars` shows per-day bars w/ day labels; portal shows only first/last date under the bars — acceptable, but demo sub is `bookings per day · last 7 days` vs portal `Last 7 days bookings`.
- **Top Shops non-money variant** (`{{n}} bookings` per row when !canMoney) absent.
- **Attention feed row rendering**: portal shows count-chip + title + optional money — demo AttentionFeed additionally has per-item severity icon set and CTA labels; verify against `src/components/attention/AttentionFeed.tsx` if pixel-parity needed.

### Copy diffs (demo en.ts → portal Yii::t)
| Demo (en.ts) | Portal (`backend` category) |
|---|---|
| digest: `{{bookings}} bookings so far today · {{newShops}} new shops · {{newCustomers}} new customers · {{cancellations}} cancellations` | `{bookings} bookings, {newShops} new shops, {newCustomers} new customers, {cancellations} cancellations` — commas instead of `·`, **"so far today" dropped** |
| kpiActiveShops caption: `+{{n}} this month · {{dormant}} dormant` | `Total active shops on platform` |
| kpiTodayBookings: `Today's bookings`, caption `{{amount}} booked value` | `Today Bookings`, caption `GMV: {amount}` |
| kpiMrrNoRisk: `no MRR at risk` | `Healthy` |
| kpiEarningsMtd caption `vs last month` | ✓ same |
| attentionTitle: `Needs attention` | `Needs Attention` (case) |
| allClear: `All clear ✓` | `All clear. No active flags.` |
| moneyInMotionSub: `⃁ currently in flight` | `currently in flight` (riyal glyph dropped) |
| topShops: `Top shops this week` | `Top Shops` (loses "this week") |
| recentActivitySub: `latest admin-surface events` | `Platform live feed` |
| platformWeek: `Platform week` | `Platform Week` |
| subsPulse: `Subscriptions pulse` ✓ / cells ✓ | ✓ match |
| watchlist: `{{count}} negative-contribution — view P&L` | ✓ match |

### SUPERSET
- Header CTA button **"Process settlements"** → `/admin-finance/shop-balances` (not in demo).
- Attention rows deep-link to fixing screens with `money` column (demo has URLs too — parity ✓, money column is parity ✓).

---

## 2. DASHBOARD — `backend/views/dashboard/index.php` (+ `DashboardController.php`)
Demo: `Dashboard.tsx` + `dashboard/{Overview,Volumes,Performance,Financials,Flags}.tsx`

### Structural gaps
- **Refresh affordance**: demo shows `Updated {{time}} · auto every 5 min` text + Refresh button (RefreshControls), with genuine 5-min auto snapshot refresh. Portal: only a Refresh **link** whose last-updated lives in a hover `title` tooltip (`:154-159`); **no visible "Updated … · auto every 5 min" text, no auto-refresh timer**.
- **Financials tab is missing the hero chart**: demo Financials renders full-width "Revenue, COGS & gross margin" ComposedChart *first* (`Financials.tsx:174-214`); portal Financials section starts at "Revenue by line" (`:288`) — the composed chart exists only on Overview.
- **Performance → Activation funnel not split**: demo splits into acquisition chain (Requested→Activated→Active, FunnelChartCard) + a separate "Parallel outcomes" bar list (First booking, Subscribed) with subtitle `Acquisition chain → parallel outcomes`. Portal renders one flat 5-bar list, no subtitle, no chain/leaf distinction (`:256-271`).
- **Performance → Cancellations & marketing mix**: demo is 3 AttrBar rows each with absolute **count** + fill bar + % riding the fill + sub-caption (`of all bookings in period` / `of bookings sourced by Navagoo`). Portal is a single horizontal Chart.js bar of 3 percentages — counts and sub-captions lost (`:274-276`, JS `:543-549`).
- **KPI RAG statuses / target hints**: demo KpiCards carry `status={ragOf(...)}` + `targetHint` (No-show, Completion, On-time start, Take rate) → colored status treatment. Portal shows plain captions (literal `target ≤ 5%` on two tiles only); Completion caption is `{n} bookings` (not in demo), On-time start caption `Derived from booking outcomes` (not in demo).
- **Volumes → donut center label** `{{n}} total` missing (portal doughnut has legend only, JS `:518-522`).
- **Volumes → GMV chart mark**: demo GMV is an **Area** + bookings Line; portal GMV is a **Bar** + line (`:513-516`). Demo treemap for GMV-by-shop → portal horizontal bar (documented fallback; adds explanatory footnote line not in demo).
- **Money format**: demo KPI money is compact (`Money compact`, e.g. `SAR 12.5K`); portal always `SAR n,nnn.nn` full 2dp → tile overflow risk on 6-up row.
- **Axis labels**: demo maps `monthIso` → localized `Jul` / `5 Jul` via `trendLabel()`; portal charts use raw `b.key` from the controller — **verify keys are pre-formatted, else axes show `2026-07`**.
- No count-up animation on KPI values (cosmetic).

### Copy diffs
| Demo | Portal |
|---|---|
| kpi.activeShopsCaption `across {{n}} cities` | `in {n} cities` |
| overview.subtitlePlatformFinancials `Platform financials for the selected period` | `Platform financials over the period` |
| volumes.subtitleBookingsGmv `Completed booking value + volume` | `Volume and value over the period` |
| volumes.titleNewVsReturning `New vs returning customers` | `New vs returning` |
| volumes.subtitleCustomersActive `Customers active in period` | `Customers active in the period` |
| volumes.titleBookingsByCity sub `All bookings in the period by shop location` | `By city` |
| volumes.titleGmvByShop `GMV by shop (top 8)` / sub `Completed booking value, current period` | `GMV by shop` / `Top shops by gross transacted value` |
| performance.gaugesTitle `Utilization gauges` / sub `Platform-wide operational capacity` | `Utilization & throughput` / `Derived from booking outcomes` |
| performance.gaugeSpecialistUtil `Avg specialist util.` | `Specialist utilization` |
| performance.qualitySubtitle `5-axis platform quality radar` | `Operational quality axes (0–100)` |
| performance.cancelMktSubtitle `Cancellation attribution and Navagoo-sourced bookings` | `Attribution across all bookings` |
| financials.titleRevenueByLineStacked `Revenue by line (stacked)` / sub `All revenue lines over the period` | `Revenue by line` / `All revenue lines, stacked` |
| financials.subtitleRevenueMix `Period revenue mix by line` | `Revenue mix this period` |
| financials.titleGrossMarginGauge `Gross margin gauge` / sub `Period gross margin as % of revenue` | `Gross margin %` / `Gross margin as a share of revenue` |
| financials.titleNetContribution `Net contribution by shop (top 8)` / sub `Fees earned minus shop-attributable COGS` | `Net contribution by shop` / `Fees minus allocated cost (cost split by booking share)` |
| financials.noContributionData `No contribution data for the selected period` | `No contribution data` |
| financials.statArrCaption `Annual recurring revenue` | `Annualized recurring revenue` |
| financials.statMrrAtRiskCaption `Past-due / expired subs` | `Past-due & flagged MRR` |
| flags.opsFlagsSubtitle `Platform-level items requiring attention` | `Items needing attention right now` |
| flags.settlementAgingSubtitle `Pending transfer requests by age (days since request)` | `Pending payout requests by age (days)` |
| flags.atRiskShopsTitle `At-risk (dormant) shops` / sub `Active shops with no non-cancelled bookings in the last 30 days` | `At-risk shops` / `Active shops with no bookings in 30 days` |
| flags.none `none` (last-booking cell) | `—` |
| flags.settlementLiabilityCaption `Total Navagoo owes to shops (positive running balances)` | `Owed to shops (pending settlements)` |
| flags.outstandingReceivablesCaption `Unpaid charges owed to Navagoo (status=unpaid, not pending)` | `Unpaid charges owed to Navagoo` |

### SUPERSET
- Flags tab shows a **red count badge** on the tab pill; flag tiles + overview flag pills are **clickable links** to fixing screens (demo tiles are static). Keep.
- Period is URL state (`?period=`) — better than demo's local state; matches web patterns.

---

## 3. ANALYTICS (Platform P&L) — `backend/views/analytics/index.php` (+ `AnalyticsController.php`)
Demo: `Analytics.tsx` + `analytics/Trends.tsx` + `analytics/pnlView.ts`

### Structural gaps
- **2 of 3 trend charts missing**: demo `<Trends>` renders (a) Revenue/COGS/Gross margin full-width ✓ ported, (b) **"GMV & bookings"** dual-axis bar+line (`sub: Completed booking value + volume`), (c) **"Revenue by line"** stacked area (`sub: Stacked, by revenue line`). Portal has only (a) (`:177-185`). (b) and (c) absent entirely.
- **No RefreshControls**: demo header has `Updated {{time}} · auto every 5 min` + Refresh next to the period pills; portal header has period pills only (`:91-111`).
- **Default period**: demo `useState<PeriodKey>('last_6m')`; portal defaults to `'this_month'` (`AnalyticsController.php:43`).
- **Statement rows**: portal hard-codes revenue-line order marketing→processing→notifications→subscription and renders zero-value lines; demo iterates `pnl.byLine` (data-driven set/order). Also chart axis uses raw `b.key` (see Dashboard axis note).
- No count-up on the 3 money KPIs (cosmetic).

### Copy — near-exact parity ✓
Titles, subtitles, KPI captions (`fees earned`, `provider + infra cost`, `revenue − COGS`, `gross margin ÷ revenue`), statement title/sub, `assumed` badge, total rows, margin `—` fallback: **all match**. Only nit: trend-card subtitle demo `For the selected period` vs portal `Platform financials for the selected period`.

---

## 4. COSTS — `backend/views/cost/index.php` (+ `CostController`)
Demo: `Costs.tsx`

### Gaps
- **Save button never disabled**: demo `disabled={!dirty}` + `DisabledAction perm="admin.finance.editConfig"` wrapper. Portal `markDirty()` only strips a `data-clean` attr that nothing sets; Save is always active and there's no permission-gated disabled state (`:111-115`, JS `:186`).
- **Delete-confirm body**: demo `Remove "{{label}}" from the register?` (interpolates the line's label); portal `Remove this line from the register? Save to apply.` (no label).
- **New-row label prefill**: demo new lines get label `New cost`; portal template row has empty label (`:160`).
- **Saved feedback copy**: demo toast `Costs saved` / `Feeds the P&L only — never affects charges billed to shops`; portal shows a server flash — verify flash text in `CostController::saveRegister` matches; the "never affects charges billed to shops" reassurance line should survive.
- Empty-state placeholder removal bug-ette: `rows.querySelector('p')` removes the *first* `<p>` in the register on add — currently only the placeholder is a direct `<p>`, but fragile (labels are `<label>`, fine today). Note only.

### Parity ✓
Page title/sub, Add cost / Save costs buttons, amber assumed-values notice (prefix/**assumed**/suffix structure preserved), register title/sub, 8 basis options, %/+Fixed swap for per-online-txn, On/Off toggle, assumed badge, trash icon + ngConfirm. All copy matches en.ts.

### SUPERSET
- Success/error flash banners; explicit empty-state line `No cost lines yet…`; DB persistence with upsert/delete semantics. Keep.

---

## 5. SETTINGS — `backend/views/settings/{_tabs,workflow,policies,colors}.php`
Demo: `Settings.tsx` (tabs workflow | docs | colors)

### Shell
- Portal `_tabs.php` adds a 4th **"General"** pill (legacy Website Settings) — **SUPERSET**, keep.
- Subtitle: demo `Booking workflow, policies, documents & colours` — portal adds trailing period. Tabs are separate server pages (deep-linkable) vs demo client tabs — acceptable, arguably better.

### Workflow tab
- **Card subtitle missing**: demo `Which status moves are allowed (from → to). The store and every screen enforce this.` Portal header has icon+title only (`workflow.php:49-53`).
- **Footnote rewritten**: demo: `"Cancelled" gates the **Cancel** action (which still runs the refund-zone flow). The **Reschedule** pill controls whether a booking in that status can be time-moved or reassigned — including by dragging its card on the day calendar — without changing the status.` Portal: one condensed sentence, drops the refund-zone and drag-on-calendar explanations (`:170-172`).
- **Interaction model**: demo toggles apply **live** (store updates on click; reset shows toast `Workflow reset` / `Restored the default booking status flow`). Portal requires an explicit **Save** post, and Reset posts with a native-ish `data-confirm` dialog. Server-side reality — flag as accepted deviation, but the reset confirm should use `ngConfirm` copy parity and a success toast after.
- **Hint copy drift**: no-show hint demo `Minutes after the appointment start before a booking can be marked no-show.` vs portal `A booking may be marked No-show only after this many minutes past the appointment start.` (on-time start/finish hints match ✓; customer-reschedule limit hint matches ✓; both toggles' title/desc match ✓ apart from straight-vs-curly apostrophe).
- **Reschedule pill tooltips**: demo has distinct on/off tooltips (`Reschedulable — …` / `Locked — …`); portal single static title `Allow time-move / reassign for bookings in this status`.
- **Toggle style**: demo Full-refund/package-cancel rows use a Toggle switch; portal uses plain checkboxes (aurora has toggle knobs elsewhere — style gap).
- Status set: portal includes `accepted(6)` mapped to scheduled colour — DB reality, superset.

### Policies & documents tab
- **Card subtitle missing**: demo `Legal and operational documents shown across the platform`.
- **Status badge case**: demo lowercase `published` / `draft` (blue/slate Badge); portal `Published` / `Draft`.
- **Doc-name icon chrome**: demo wraps FileText icon in brand-tinted 8×8 rounded square; portal bare 4×4 muted icon.
- **View affordance**: demo `Download` icon + text `View` as one link (toast-simulated); portal icon-only download link, no "View" label.
- **SUPERSET**: Audience column, upload-new-version control, publish/unpublish action, real `policy_document` persistence. Keep (documented as intended extension in the view docblock).

### Color system tab
- **Per-token controls missing**: demo ColorPicker per token exposes **Save default**, **Reset (per token)**, and `isDefault` detection (`canSaveDefault` only when changed). Portal has one global Save, one global **Save default** (applies to *all* pickers), one global Reset-all — no per-token reset or per-token save-default (`colors.php:93-102`).
- **Token preview**: demo renders the actual **Badge/status chip** styled by the token (live preview of the real component); portal shows a plain colour swatch square + hex text.
- **Live cross-portal application**: demo applies instantly to CSS vars; portal preview only updates the swatch until Save (accepted server model, note).
- **Intro copy**: demo `Pick a base colour for any badge, status or core brand colour — applied **live across all portals** and saved. "Save default" makes the current colour the baseline that Reset returns to.` Portal: `Override brand, text and status colours. Changes are saved as overrides; promote one to the baseline with Save default.`
- **Reset-all label**: demo swaps `Reset all` ↔ `Reset all ({{n}})` only when dirty; portal always shows `Reset all (n)` incl. `(0)` disabled — minor.
- Grid: demo 4-col `TOKEN_GROUPS`; portal 3-col with 5 named groups (brand/accent/ink/surface/status) — verify group set matches demo `lib/theme.ts` TOKEN_GROUPS.
- **SUPERSET**: `overridden` badge per token — nice, keep.

---

## 6. LIVE CHAT — `backend/views/live-chat/index.php`
Demo: `LiveChat.tsx` (stub screen)

- **Deliberate deviation (documented in view docblock): no seeded fake threads.** Demo shows 3 seeded threads (Beauty Center / Boy Squad Barbers / Glow Lounge) with unread dot; portal renders an honest empty state `No conversations yet`. This is a policy call (no static/mock data rule) — record as accepted, not a bug.
- **Preview badge copy split**: demo one badge `Preview — real chat lands with the backend` in the header. Portal header badge says just `Preview` and the remainder `Real chat lands with the backend` moved into a second chip inside the coming-soon card.
- **comingSoonDesc drift**: demo `This is a preview of the support inbox. Real-time messaging with shops (and customers) will be wired up when the backend messaging rail is in place.` Portal: `Real-time messaging with shops arrives with the backend. This is a preview of the interface.` (drops "support inbox" and "(and customers)").
- Title/subtitle (`Live chat` / `Support conversations with shops`) and coming-soon title match ✓. Layout grid `320px_1fr` matches ✓.
- Note: portal also has a full `technical-support/` + `conversation/` module — if that is the real support surface, consider cross-linking it from this stub (portal-side idea, not demo parity).

---

## Cross-cutting
- **i18n**: every drifted `Yii::t('backend', …)` string above needs its `ar` twin updated in the same pass (`common/messages/{ar,en}/backend.php` + `backend/messages` if applicable).
- **Chart axis labels**: both dashboard and analytics pass raw `key` to Chart.js labels; demo localizes to `Jul`/`5 Jul` (`pnlView.trendLabel`). Verify controller output; if ISO, add a month-abbrev map (both locales).
- **Count-up + RAG KPI treatments** are the two systemic KpiCard features the PHP `$kpi()` helper lacks (delta chip is ported; status ring/target hint and count-up are not).
