# Group Bookings — UI & States (parity)

Area: **Shop · Group bookings** (NEW in dev). Demo at `/private/tmp/Navagoo_MI_dev/navagoo-app/src`.

> No matching UI exists in our `frontend/` shop portal. Our bookings views
> (`frontend/views/booking/*`, `frontend/views/booking-service/*`,
> `frontend/components/BookingScheduleService.php`) handle **solo** bookings only. Every
> screen below is **MISSING** on our side.

## 1. Entry point — "New group" button + unified list

Demo ref: `src/portals/shop/Bookings.tsx`.
- A **"New group"** button sits next to "New booking" in the Bookings toolbar
  (`Bookings.tsx:337-339`), opening `NewGroupBookingModal` (`:448`).
- Solo bookings **and** parties render in **ONE table**, newest slot first
  (`:103-135`). A `ListRow` is `{kind:'solo'} | {kind:'group'}` (`:40`).
- A party is a **single row**: stacked/overlapping avatars (`AvatarCluster`, `:42`,
  `:182-205` "Party of N"), the `GRP-…` id under a **"Group"** tag with a `Users` icon
  (`:153-161`), aggregate status badge, party value + outstanding.
- Clicking the row expands to `<GroupExpanded>` (`:411-415`); a solo expands to the usual
  booking drawer.

Our ref: `frontend/views/booking/index.php` / `_calendar_day.php` list solo bookings
only; no group row, no "New group" button, no avatar cluster. **MISSING.**

## 2. Create modal — `NewGroupBookingModal`

Demo ref: `src/portals/bookings/GroupBookingModal.tsx` (full file).
Sections, top → bottom:
1. **Organiser** — Existing/New segmented toggle (`:313-321`). Existing → customer
   `<Select>` scoped to this shop's customers (`:323-331`). New → name + mobile inputs (`:333-336`).
2. **Guests (N)** — starts with **2 empty guest cards** (`:81`). Per card: order badge,
   label input (or auto "Organiser" chip on row 1 when *organiser is also a guest*),
   "Choose services" picker button, specialist `<Select>` filtered to those who can
   perform the chosen services **and not already taken by another guest** (`:356-358`).
   Per-card inline **conflict banner** (overlap/time-off/hours/can't-perform) (`:433-438`).
   - Checkbox **"Organiser is also getting a service"** (default on) — row 1 becomes the
     organiser's own appointment, auto-labelled `"{name} (organiser)"` (`:346-354`,`:182-188`).
   - **Add guest** ghost button under the last card (`:443-453`).
   - **Remove guest** only when >2 guests and not the organiser row (`:386-390`).
   - **Duplicate-specialist** banner if two guests share one (`:454-459`).
3. **Payment method (whole party)** `<Select>` — online / deposit% / on-visit, gated by
   shop accept flags (walk-in vs app variants) (`:55-68`,`:463-472`). **Party total** chip
   (`:473-478`).
4. **When (shared slot)** — `SlotPicker` anchored to the first ready guest; hint shown
   until a guest is set up (`:481-499`).

Footer actions (`:251-284`): **Cancel**, **Pay after service** (creates, collect on
visit), **Pay now** (→ if balance due, switches to the **pay-now sub-screen** with
`CollectPaymentBody` method+tip; `:286-306`,`:234-241`). Success toasts summarise
"N guests · value · timing" (`:218`,`:226`,`:239`).

Validation gating the submit (`:143-151`): ≥2 guests, every guest has specialist +
≥1 service, no duplicate specialist, a slot chosen, no per-guest conflicts, organiser set.

Our ref: `frontend/views/booking/_form.php` is a single-customer single-specialist form.
**MISSING** — no multi-guest repeater, no per-guest conflict check, no party-total, no
"organiser is a guest" concept.

## 3. Expanded party drawer — `GroupExpanded`

Demo ref: `src/portals/shop/GroupBookingsView.tsx:29-208`.
- **Guest list**: each row = order badge, guest label, services, specialist avatar+name,
  status badge, value + "due" (`:90-131`). Each guest expands to the standard
  `BookingDetailBody` (reschedule/cancel/collect a single guest) (`:132-141`).
- **Whole-party action bar** (`:147-192`), shown when applicable:
  - **Start all** — moves every `scheduled` child → `in_progress` (`:60-63`,`:151-155`).
  - **Collect all · {outstanding}** — opens `GroupCollectModal` (`:156-165`,`:274-325`).
  - **Complete all** — blocked while outstanding>0; toasts "collect first" and opens
    collect (`:64-72`,`:166-175`).
  - **Reschedule all** — opens `GroupRescheduleModal` (`:176-185`,`:210-272`).
  - **Cancel all** — `confirm()` dialog "Cancel the whole party? cancels all N…"
    then `cancelGroupBooking` (`:73-83`,`:186-190`).

Our ref: `frontend/views/booking/view.php` (solo detail). **MISSING.**

## 4. Sub-modals

- **`GroupRescheduleModal`** (`GroupBookingsView.tsx:210-272`): single shared-slot
  `SlotPicker` anchored to organiser; note "Every guest keeps their specialist and
  service; only the start time changes." **MISSING.**
- **`GroupCollectModal`** (`:274-325`): "Collect from the organiser · the whole party in
  one payment", `CollectPaymentBody` (method + card tip). **MISSING.**
- **`ServicePickerModal`** reused per guest (`GroupBookingModal.tsx:504-512`). We have a
  service picker for solo only.

## 5. Empty / error / RTL states

- **Empty slot**: "Add a guest's specialist & services first to see open times."
  (`GroupBookingModal.tsx:483,495-498`).
- **Per-guest conflict** + **duplicate-specialist** banners (red, AlertTriangle).
- **Create rejected**: `toast.warning('Could not create group', res.error)` (`:209`).
- **Reschedule rejected**: `toast.warning('Could not reschedule party', …)` (`:231`).
- **RTL**: demo uses logical props (`ms-2`, `me-1`) so the layout mirrors in Arabic
  (`GroupBookingModal.tsx:476`, `GroupBookingsView.tsx:150`). Our app is bilingual
  (ar/en) — any port must add the strings to `common/messages/{ar,en}/frontend.php` and
  use logical CSS. No group strings exist today. **MISSING.**

## UI gap summary
0% of the group UI exists on our side: no entry button, no unified list row, no create
modal, no expanded drawer, no reschedule/collect sub-modals, no i18n.
