# Admin · Bookings — Logic & Flows

Canonical target: `portals/admin/Bookings.tsx` (React demo). Our side: `BookingController`
+ `BookingSearch` + `backend/views/booking/index.php`. (`ExtendedBooking*` is a legacy/parallel
CRUD surface, NOT the admin bookings screen — see note at bottom.)

## Demo logic (AdminBookings)

The admin bookings screen in the demo is **read-only and platform-wide** — no create, no edit,
no cancel, no row actions.

1. **Data source** — `state.bookings` (the full Zustand booking list, all shops).
   `Bookings.tsx:16`.
2. **Two client-side filters** held in local state: `status` (`BookingStatus | 'all'`) and
   `shopId` (`string | 'all'`). `Bookings.tsx:17-18`.
3. **Row pipeline** (`useMemo`, `Bookings.tsx:20-28`):
   - filter by status (skip when `'all'`),
   - filter by shop (skip when `'all'`),
   - sort by `appointmentDate` **descending** (newest appointment first).
4. **Columns** (`Bookings.tsx:30-79`) — 7 only: Booking id (`b.id`, mono), Shop
   (ShopTile + commercialName, resolved via `shopById`), Customer (`customerById(...).name ?? '—'`),
   Service (`b.lines.map(l => l.name).join(', ')`), When (`date(appointmentDate)` + `timeShort(...)`),
   Value (`Money value={b.bookingValue}` right-aligned), Status (`<BookingBadge>`).
5. **Footer count** — `{rows.length} of {state.bookings.length} bookings`. `Bookings.tsx:113-115`.
6. **Empty state** — handled inside the shared `DataTable` (no rows → table-level empty render).
7. **Status set** (`types.ts:298`, `Badge.tsx:80-86`): `scheduled · in_progress · completed ·
   no_show · cancelled`. NOTE: the Segmented filter only exposes 4 + All
   (`scheduled/completed/no_show/cancelled`) — `in_progress` is a valid status but is **not**
   a filter chip. `Bookings.tsx:88-95`.

`bookingValue` is precomputed on the model (`subtotal − discount`, `types.ts:317`); the demo does
no pricing/VAT math in this view — it just renders the stored value through `Money`/`lib/format`.

## Our logic

1. **Data source** — `BookingSearch::search()` over `Booking::find()`, **all shops** (admin has no
   shop scoping here). `BookingController.php:43-44`. Base query restricts to
   `status >= STATUS_SCHEDULED (2)` so NEW(0)/SELECTED_NOT_PAID(1) are normally excluded
   (`BookingSearch.php:102`).
2. **Sort** — `defaultOrder id DESC` (`BookingController.php:45-47`), i.e. newest *created* first,
   **not** by appointment date as the demo does. GridView column sort is otherwise available.
3. **Filters** — server-side GET (`BookingSearch[...]`): Booking ID, Shop Name, City, District,
   Customer Name, Gender, Customer Mobile, Booking (created) Date range, Appointment Date range,
   Status. `index.php:130-207`. Far richer than the demo's 2 filters.
4. **Columns** — 28 columns incl. Shop ID, Country, City, District, Customer ID, Gender, Mobile,
   Specialist id/name, Booking Method, Services, Service Categories, created date, appt date/start/
   finish/duration, actual start/finish/time, Total Amount, Status, Specialist Rating, Shop Rating,
   Actions. `index.php:213-344`. A strict superset of the demo's 7.
5. **Value** — `total_amount` formatted as `SAR <decimal,2>` (`index.php:284-286`), not the demo's
   `bookingValue` (subtotal−discount). Different semantic field; see business.md R-7.
6. **Footer** — `getDataProviderSummary()` + real `LinkPager` server pagination
   (`index.php:351-362`); demo has no pagination (renders all rows) and a plain `N of M` count.
7. **Status chip mapping** — `index.php:36-61` maps our 9 DB statuses onto 5 chip kinds
   (scheduled/inprogress/completed/cancelled/noshow), mirroring the demo's 5 badge colors.
8. **Row action** — view (eye) link to `booking/view`. The demo has no row action at all.
9. **Cancel flow** — `BookingController::actionCancel()` (`:224-248`) sets
   `STATUS_CANCELED_BY_SHOP`, fires `NotificationHelper::shopCancelBooking`, stores a reason.
   This is a SHOP-portal flow, not used by the admin index view. No analog in the demo admin screen.

## Gaps / deltas
- **Sort key differs**: demo sorts by `appointmentDate DESC`; ours by `id DESC`. (partial)
- **Value field differs**: demo `bookingValue` vs our `total_amount`. (note)
- Everything in the demo admin screen is present and exceeded on our side. The remaining demo
  concepts (`in_progress` as data, group bookings `groupBookingId`) are not *surfaced* in the
  demo admin screen either, so they are out of scope for THIS screen (tracked in business.md /
  newInDemo).

## ExtendedBooking note
`ExtendedBookingController` + `backend/views/extended-booking/*` is a generic Gii CRUD surface for
an `ExtendedBooking` model. It has **no counterpart** in the demo and is unrelated to the admin
bookings list. Flagged here so it is not mistaken for parity scope.
