# Admin · Bookings — Parity

Canonical: `portals/admin/Bookings.tsx`. Ours: `backend/controllers/BookingController.php`,
`backend/views/booking/index.php`, `common/models/search/BookingSearch.php`,
`common/models/base/Booking.php`.

| Demo behavior | Demo ref | Our ref | Status | Note |
|---|---|---|---|---|
| Platform-wide booking list (all shops) | Bookings.tsx:16,22 | BookingController.php:39-53; BookingSearch.php:102 | done | No admin shop scoping on either side |
| Filter by status | Bookings.tsx:88-95 | index.php:188-196; BookingSearch.php:183 | done | Ours is a `<select>`; demo is a Segmented toggle |
| Filter by shop | Bookings.tsx:96-104 | index.php:138-144 (Shop Name) | done | Demo filters by shop id; ours by shop name string |
| Sort by appointmentDate DESC | Bookings.tsx:24 | BookingController.php:46 (`id DESC`) | partial | Ours sorts by id, not appointment date |
| Col: Booking id (mono) | Bookings.tsx:33-36 | index.php:291-293 | done | |
| Col: Shop (tile + name) | Bookings.tsx:38-50 | index.php:294-295 | partial | Ours: plain shop name, no shop tile/avatar |
| Col: Customer name (— fallback) | Bookings.tsx:51-55 | index.php:300-305 | done | Ours adds an initials avatar |
| Col: Service (line names joined) | Bookings.tsx:56-60 | index.php:316 (Services) | done | |
| Col: When (date + time) | Bookings.tsx:61-69 | index.php:319-325 | done | Ours splits into appt date/start/finish cols |
| Col: Value (bookingValue, right) | Bookings.tsx:70-74 | index.php:284-286,330 | partial | Ours renders `total_amount`, not `subtotal−discount` |
| Col: Status badge (5 hues) | Bookings.tsx:75-78; Badge.tsx:80-86 | index.php:36-61,331 | done | 9 DB statuses mapped to 5 chips |
| Footer: N of M bookings | Bookings.tsx:111-113 | index.php:352 (`getDataProviderSummary`) | done | Ours also adds real pagination |
| Empty state | DataTable (shared) | index.php:246-247 | done | "No results found." |
| Read-only (no row mutate actions) | Bookings.tsx (none) | index.php:334-342 (view only) | done | View-only on index; mutating controller actions not linked |
| RTL / bilingual | demo shell | index.php (logical props, Yii::t) | done | |
| Pagination | — (renders all) | index.php:353-360 | done | Extra on our side |
| Extra filters (city/district/gender/mobile/dates/id) | — | index.php:145-187 | done | Superset, no demo analog |
| Per-row view detail | — | index.php:334-342 → booking/view | done | Superset |

## Notable deltas
- **Sort key** is the one real behavioral mismatch (id DESC vs appointmentDate DESC).
- **Value field** semantics (`total_amount` vs `bookingValue`) should be confirmed equal.
- Cosmetic: shop-tile avatar, page subtitle, segmented-vs-select.
- **New in demo data model (not surfaced in this admin screen on EITHER side):** `in_progress`
  status as first-class, group bookings (`groupBookingId`/`guestLabel`), earning/refund/in-store/
  tip fields. None block parity for this screen.
- **Ours has no demo counterpart:** `ExtendedBookingController` + `views/extended-booking/*`
  (generic CRUD, unrelated).

Area score: 88%

## Verified verdict (adversarial)

Each "done"/high claim was re-checked against both the demo and our *actually-wired*
code. Note: the controller imports `common\models\search\BookingSearch` (the 14 KB file
whose `search()` is at line 100-265) — NOT `backend\models\search\BookingSearch.php`,
which is a dead duplicate that *does* shop-scope (line 45) but is unused. Verdict below
uses the live `common\...` search model.

| Demo behavior | Demo ref | Our ref | Analyst | Verified | Evidence |
|---|---|---|---|---|---|
| Platform-wide list (no shop scoping) | Bookings.tsx:16,22 | common/.../BookingSearch.php:100-102 | done | done | Live search model does NOT scope by shop; only `status >= SCHEDULED`. Confirmed. |
| Filter by status | Bookings.tsx:88-95 | index.php:188-196 | done | done | `BookingSearch[status]` exact filter; `statusesFilter()` lists 6 statuses incl. in_progress (superset of demo's 5). |
| Filter by shop | Bookings.tsx:96-104 | index.php:138-144; BookingSearch.php:261 | done | **partial** | Demo: exact `b.shopId === shopId` (Bookings.tsx:23). Ours: `['like','shop.title',...]` — substring match on shop *name*, collides on duplicate/similar names. Different matching semantics. |
| Default sort appointmentDate DESC | Bookings.tsx:24 | BookingController.php:46; BookingSearch.php:109 | partial | partial | Confirmed `id DESC` both at controller and search model. Real mismatch. |
| Col: Booking id (mono) | Bookings.tsx:33-36 | index.php:291-293 | done | done | `id-mono`, links to booking/view. |
| Col: Shop (tile + name) | Bookings.tsx:38-50 | index.php:294-295 | partial | partial | Confirmed: plain title text, no `ShopTile`/logo avatar. |
| Col: Customer name (— fallback) | Bookings.tsx:51-55 | index.php:264,300-305 | done | **partial** | Demo fallback is em-dash `'—'` (Bookings.tsx:50). Ours name fallback is hyphen `'-'` (index.php:264); only the avatar glyph uses `'—'`. Cosmetic but not exact. |
| Col: Service (joined names) | Bookings.tsx:56-60 | index.php:316 | done | done | `getServicesList()`. |
| Col: When (date+time) | Bookings.tsx:61-69 | index.php:319-325 | done | done | Demo: one cell. Ours: split appt date/start/finish cols — superset, content equivalent. |
| Col: Value | Bookings.tsx:70-74 | index.php:284-286,330 | partial | partial | Demo `bookingValue = subtotal − discount`, VAT-inclusive (types.ts:317, seed.ts:1209). Ours renders raw `total_amount`; our model carries separate `sub_amount`/`vat`/`discount_value` columns (base/Booking.php:35-39), so `total_amount` composition (VAT additive?) cannot be confirmed equal to demo's after-discount VAT-inclusive value. Unconfirmable → stays partial. |
| Col: Status badge (5 hues) | Bookings.tsx:75-78; Badge.tsx:80-86 | index.php:36-61,331 | done | done | 9 DB statuses → 5 chip keys; clean map onto demo's 5 hues. |
| Footer: N of M count | Bookings.tsx:108-110 | index.php:352; helpers.php:83 | done | **partial** | Demo: `{filtered} of {total} bookings` (plain, unpaginated). Ours: `getDataProviginSummary` → Yii "Showing **1-20** of **M** items." — a per-page *range*, word "items", different text & semantics. Not an exact match. |
| Empty state | DataTable (shared) | index.php:246-247 | done | done | colspan row "No results found." |
| Read-only admin (no row mutate) | Bookings.tsx | index.php:334-342 | done | done | Index links only `{view}`. create/update/delete/cancel actions exist on controller but are not linked from the list. |
| RTL / bilingual | demo shell | index.php (logical props, Yii::t) | done | done | `text-start`, `ps-9`, all strings via `Yii::t('backend', …)`. |
| Page subtitle | Bookings.tsx:83 | index.php:120-127 | partial | partial | Confirmed: ours has title only, no "Platform-wide bookings across all shops" subtitle. |
| Extra filters (superset) | — | index.php:145-187 | done | done | No demo analog. |
| Per-row view detail (superset) | — | index.php:334-342 | done | done | No demo analog. |

### Downgrades from analyst
- **Filter by shop**: done → partial (exact-id vs LIKE-on-name substring; BookingSearch.php:261).
- **Footer N of M**: done → partial (Yii paginated "Showing X-Y of M items" ≠ demo's "N of M bookings"; helpers.php:83).
- **Customer — fallback**: done → partial (hyphen vs em-dash; index.php:264 vs Bookings.tsx:50).

### Confirmed (no change)
Platform-wide list, status filter, booking-id col, service col, when col, status-badge col,
empty state, read-only index, RTL/bilingual, extra filters, per-row view — all hold.
Analyst's existing partials (default sort, shop tile, value semantics, subtitle) all confirmed.

### Adjusted area score
Three additional partial downgrades (shop filter, footer, customer fallback) on top of the
analyst's four partials. With seven partials across 18 rows and the value-semantics risk
unresolved, the adversarial score is **81%** (down from the claimed 88%).
