# Admin · Dashboard — Business rules

Demo: `portals/admin/Dashboard.tsx`, `store/selectors.ts`
Ours: `backend/controllers/SiteController.php`

Numbered, implementable rules the demo encodes. "Ours" = our equivalent or MISSING.

## Permissions / scoping
1. **Admin dashboard is platform-wide.** Demo aggregates across ALL shops (no shop filter on KPIs).
   Ours: admin/manager see all shops; can drill to one via `?shop_id=`. `SiteController.php:89`.
2. **Access gate.** Ours restricts to roles `manager | administrator | shopOwner`; others are logged
   out and redirected to login. `SiteController.php:69`, `:129`. (Demo has no auth layer — store only.)
3. **Shop-owner scoping.** A shop owner sees ONLY their own shop's data; queries add
   `shop_id = {shop->id}`. `SiteController.php:79`, `:446`, `:567`. (Demo's admin dashboard has no
   owner mode — that's a separate portal in the demo.)

## KPI computation rules
4. **Platform GMV** = Σ `bookingValue` over bookings with `status = completed`. `selectors.ts:432`. — MISSING (we have no GMV/booking-value KPI).
5. **Navagoo Earnings** = Σ `chargeAmount` over charges where `status ≠ reversed` AND `pending = false`.
   `selectors.ts:429`. — MISSING (no Charge model).
6. **Pending transfer total** = Σ `netPayout` over transfers with `status = requested`. `selectors.ts:435`. — MISSING (no TransferRequest model).
7. **Open refund cases** = count bookings where `status = cancelled` AND `refundValue > 0`.
   `selectors.ts:449`. — MISSING.
8. **Today's bookings** = bookings whose `appointmentDate` is the same calendar day as `simNow`.
   `selectors.ts:443`. — PARTIAL: ours counts by `booking_date` within a chosen range, default "today".
9. **Active shops** = shops `status = active`; **cities** = distinct `shop.city`. `selectors.ts:441`. —
   PARTIAL: ours has active-shop count (`getShopStatistics`, `:859`) but no distinct-city metric.

## Our extra rules (no demo equivalent)
10. **Date range** presets today/7d/30d/custom; custom clamped to **max 30 days**; invalid → today.
    `SiteController.php:379`, `:403`.
11. **Trend %** = `(current − previous) / previous × 100`, rounded 1dp; `null` if both 0; `100` if
    prev 0. Previous window = same length immediately before. `:530`, `:543`.
12. **Cancelled** KPI/summary unions `STATUS_CANCELED` + `STATUS_CANCELED_BY_SHOP`. `:453`, `:698`.
13. **Upcoming bookings** = SCHEDULED/INPROGRESS with `booking_date >= today`, ordered asc, limit 5;
    NOT filtered by the selected range. `:560`.
14. **Specialist occupancy** capacity = `days × 8h × 60min`; booked% capped at 100. `:671`, `:678`.
15. **New vs returning customer**: a customer is "new" on a day if they have NO booking with an
    earlier `booking_date` (lifetime); counted once per range. `:794`, `:805`.
16. **Ratings** are lifetime (NOT range-filtered): avg+count over all `Rate` rows (shop-scoped for
    owners), latest 3. `:714`.

## Subscription rules (demo only — MISSING on our side)
17. Subscription watch shows **every** shop. Label: no sub → "No subscription"; `free_period` →
    "Free period · {relativeDays to freePeriodEndsAt}"; else "Active · renews {nextBillingDate}".
    `Dashboard.tsx:218`.
18. **Warn flag** when: no subscription OR `status = free_period` OR `status = flagged`.
    `Dashboard.tsx:223`. — MISSING (no subscription model; note: the active Social-Media feature has
    its own `ShopSocialSettings`/quota tables, unrelated to platform subscriptions).

## Status / settlement
19. Demo settlement state per booking resolved by `settlementStateFor` (in_tr/settled/pending) for the
    finance area; the dashboard only counts `requested` transfers + oldest. `selectors.ts:81`. — MISSING.
