# Admin · Bookings — Business Rules

Rules the demo admin bookings screen encodes (and a few from the shared data model that bound this
screen). Numbered + implementable. Refs: demo `portals/admin/Bookings.tsx`, `types.ts`,
`Badge.tsx`; ours `BookingController.php`, `common/models/search/BookingSearch.php`,
`common/models/base/Booking.php`, `backend/views/booking/index.php`.

R-1. **Platform-wide visibility.** The admin sees bookings across ALL shops; there is no shop
scoping for the admin. Demo: filters over the full `state.bookings` with shop filter defaulting to
`all` (`Bookings.tsx:16,22`). Ours: `Booking::find()` with no owner scoping
(`BookingSearch.php:102`). MATCH.

R-2. **Read-only at admin level.** The admin screen exposes no create/edit/cancel/delete — only
view + filter. Demo has zero row actions. Ours adds a non-destructive "view" link only on the index;
create/update/delete actions exist on the controller (`BookingController.php:83-158`) but are not
linked from the admin list. Cancel (`:224-248`) is a shop-portal flow. PARTIAL (controller exposes
mutating actions that the demo admin role does not).

R-3. **Status vocabulary.** Canonical statuses: `scheduled · in_progress · completed · no_show ·
cancelled` (`types.ts:298`). Ours has 9 DB statuses (NEW, SELECTED_NOT_PAID, SCHEDULED, COMPLETED,
INPROGRESS, CANCELED, ACCEPTED, CANCELED_BY_SHOP, NO_SHOW — `Booking.php:73-81`) collapsed to the
same 5 visual chips (`index.php:36-53`). Superset; mapping is correct.

R-4. **Status filter set.** Demo filter chips expose only All + scheduled/completed/no_show/
cancelled (NOT in_progress) (`Bookings.tsx:88-94`). Ours `statusesFilter()` exposes scheduled,
completed, in_progress, canceled, no_show, canceled_by_shop (`Booking.php:353-362`). Ours is a
superset and DOES expose in_progress. PARTIAL/superset.

R-5. **Default list ordering.** Demo: `appointmentDate DESC` (`Bookings.tsx:24`). Ours:
`id DESC` (`BookingController.php:46`). MISMATCH — implementable fix: change defaultOrder to
`booking_date DESC` (or expose appt-date sort) to match the demo's "next/most-recent appointment
first" intent.

R-6. **Base-status floor.** Ours hides bookings below SCHEDULED (`status >= 2`,
`BookingSearch.php:102`), i.e. NEW/SELECTED_NOT_PAID drafts never appear in the admin list. The
demo has no such draft states, so this is a reasonable Navagoo-specific rule, not a demo conflict.

R-7. **Displayed monetary value.** Demo shows `bookingValue = subtotal − discount`
(`types.ts:317`). Ours shows `total_amount` formatted `SAR x.xx` (`index.php:284-286`). These are
not guaranteed to be the same field (total_amount may include/exclude VAT or discount differently).
NOTE — confirm `total_amount` semantics equal `subtotal − discount`; if not, displayed value
diverges from the canonical "booking value".

R-8. **Customer / shop resolution.** Demo: `shopById` / `customerById`, falling back to `'—'` when
the customer is missing (`Bookings.tsx:55`). Ours resolves via relations with `-`/`—` fallbacks
throughout (`index.php:258-266`). MATCH.

R-9. **Empty state.** When no rows match, show an empty-state message rather than a blank table.
Demo: DataTable empty render. Ours: "No results found." row (`index.php:246-247`). MATCH.

R-10. **Currency.** Demo formats via `lib/format.money` (locale symbol). Ours hardcodes `SAR`
(`index.php:285`). Acceptable for a single-currency (SAR) platform; note if multi-currency arrives.

R-11. **Bilingual.** All admin strings go through `Yii::t('backend', …)` (per CLAUDE.md). The index
view complies (`index.php` uses `Yii::t` throughout). MATCH (process rule, not a demo rule).

## Out-of-scope-for-this-screen data-model rules (shared model, not surfaced in demo admin)
- Group bookings: `groupBookingId` / `guestLabel` define a "party" of N independent child bookings
  (`types.ts:351-365`). Not shown on the demo admin screen and not on ours.
- Earnings/refund/in-store/tips fields on `Booking` (`types.ts:318-360`) drive finance views, not
  this list.
