# Admin · Dashboard — Logic parity

Demo (canonical): `/private/tmp/Navagoo_MI_dev/navagoo-app/src/portals/admin/Dashboard.tsx`
Ours: `backend/controllers/SiteController.php::actionIndex` + `backend/views/site/index.php`

## TL;DR — different dashboards

The demo admin dashboard is a **platform-operator finance/ops console**: it surfaces
network-wide money (GMV, Navagoo fee earnings), the **settlement payout queue**, a
**subscription watch** rail, and a cross-shop **recent bookings** feed.

Ours is an **operational booking-analytics dashboard**: booking-status counts with
period-over-period trend, specialist hours/occupancy, store ratings, and new/returning
customer cohorts. It also doubles as the **shop-owner** dashboard (same action, role-scoped).

Overlap between the two is small — only shop-scoping and a bookings table. Most demo
KPIs have **no backing model** on our side (no charges, transfers, or subscriptions tables).

## Demo logic (selectors.ts)

`adminKpis(state)` — `src/store/selectors.ts:428`:
- `navagooEarnings` = sum of `charges[].chargeAmount` where `status !== 'reversed' && !pending` (`:429`).
- `platformGmv` = sum of `bookings[].bookingValue` where `status === 'completed'` (`:432`).
- `activeShops` = count shops with `status === 'active'`; `cities` = distinct `shop.city` (`:441`).
- `todaysBookings` = bookings where `sameDay(appointmentDate, simNow)`; `totalBookings` = all (`:443`).
- `pendingTransfers` = transfers with `status === 'requested'`; `pendingTransfersTotal` = sum of their `netPayout` (`:435`, `:446`).
- `openRefundCases` = bookings `status === 'cancelled' && refundValue > 0` (`:449`).
- (also computes `newShopsThisMonth`, `pendingActivation`, `flaggedShops` — not all rendered).

`Dashboard.tsx` body:
- `recent` = bookings sorted by `bookingDate` desc, sliced to 6 (`Dashboard.tsx:29`).
- `requested` = transfers `status === 'requested'`; oldest shown in settlement card (`:33`, `:200`).
- Subscription watch maps **every** shop → `subscriptionForShop` (`selectors.ts:41`); label depends on
  `status` (`free_period` → "Free period · {relativeDays}", else "Active · renews {date}"); warns when
  no sub / `free_period` / `flagged` (`Dashboard.tsx:223`).
- All numbers go through `lib/format` helpers (`money`, `moneyCompact`, `num`, `dateLong`).
- "Process settlements" CTA → `/admin/finance`; table row click → `/admin/bookings` (`:88`, `:163`).

## Our logic (SiteController.php)

- `actionIndex` — `SiteController.php:60`. Role gate: `manager|administrator|shopOwner`; everyone else is logged out (`:69`,`:129`). Shop-owner is scoped to their own shop; admin/manager can pick any shop via `?shop_id=` (`:79`–`:99`). Forces `layout = 'tailwind'` (`:66`).
- `resolveDashboardRange` — `:371`. Presets `today | 7d | 30d | custom`; custom is clamped to 30 days; bad input falls back to today.
- `buildDashboardStats` — `:439`. Four booking-status KPIs (scheduled/completed/in_progress/cancelled) **with period-over-period trend** via `buildPreviousRange` (`:530`) + `calculateTrend` (`:543`).
- `getUpcomingBookings` — `:555`: next 5 SCHEDULED/INPROGRESS from today forward (NOT date-range filtered), role-scoped.
- `calculateSpecialistStats` — `:628`: per-agent minutes → hours + occupancy% (capacity = days×8h).
- `buildStatusSummary` — `:692`: doughnut data (5 statuses incl. no-show).
- `buildRatingSummary` — `:714`: avg + count + latest 3 reviews from `Rate` (lifetime, not range-filtered).
- `buildCustomerSeries` — `:759`: per-day new vs returning cohort (new = no prior booking ever).
- `getShopStatistics` — `:841`: admin-only shop totals (total/new/active/inactive) + last-access time.

## Gaps in logic

1. **No finance layer.** `platformGmv`, `navagooEarnings`, `pendingTransfers(+total)`, `openRefundCases`
   have no equivalent. There are **no `Charge`, `TransferRequest`, or `Subscription` models** in
   `common/models/` (verified: glob for transfer/charge/subscription/settlement returns nothing).
2. **No settlement queue** and no `/admin/finance` destination.
3. **No subscription-watch** rail — no subscription model/table at all.
4. **`activeShops` / `cities`** demo KPIs are absent as headline KPIs; we have a shop-stats card
   (total/new/active/inactive) but no city count and it is buried below the booking KPIs.
5. Demo "recent bookings" = last 6 by `bookingDate` desc (any status). Ours = next 5
   **upcoming** SCHEDULED/INPROGRESS. Different intent (history vs lookahead).
6. **NEW on our side (no demo equiv):** trend %, specialist occupancy/hours charts, rating widget,
   new/returning customer cohort charts, date-range filter, and the dual shop-owner role.
