# Shop · Bookings calendar — UI screens, components & states

## Screens / components map

| Demo component | Our view |
|---|---|
| `portals/shop/Bookings.tsx` (page shell, segmented List/Day/Month) | `frontend/views/booking/calendar.php` (shell + day toolbar) |
| `calendar/DayCalendar.tsx` (toolbar, grid, now-line, zoom, fullscreen) | `calendar.php` (toolbar) + `_calendar_day.php` (grid) |
| `calendar/SpecialistColumn.tsx` (column header, shading, hover, blocks) | `_calendar_day.php` per-column loop (:91-207) |
| `calendar/AppointmentBlock.tsx` (positioned, status-tinted, draggable) | `_calendar_day.php` block loop (:164-197) |
| `calendar/TimeOffBlock.tsx` (hatched unavailable block + remove X) | `_calendar_day.php` time-off loop (:134-161) |
| `calendar/NewTimeOffModal.tsx` | `_timeoff_modal.php` |
| `calendar/TimeGutter.tsx` | inline gutter in `_calendar_day.php:80-88` |
| `components/ui/Calendar.tsx` MonthCalendar | `_calendar_month.php` + `BookingCalendarPresenter::monthGrid` |
| `portals/bookings/SlotPicker`/`RescheduleModal` | `_reschedule_modal.php` |
| `portals/bookings/BookingDetailModal` | `_detail_modal.php` (loaded AJAX) |

## Day grid layout — match

Both: sticky left time gutter + horizontally-scrollable specialist columns; per-column
sticky header (avatar/initials + name, tinted by the specialist's round-robin colour);
out-of-shift areas hatched/shaded; hour gridlines; status-coloured appointment blocks
positioned with `top = topPad + (min - winStart)*pxPerMin`, `height = max(dur*pxPerMin, 22)`,
lane width `100/lanes`. The px formulas are identical (geometry.ts ≈ presenter constants:
`PX_PER_MIN 0.8`, `TOP_PAD 10`, min-block 22, min-col 184).

Block visual: soft `color-mix 16%` fill + 4px status-coloured inline-start border + inset
hairline ring — matched (`AppointmentBlock.tsx:70-72` ≈ `_calendar_day.php:180-182`).
Compact (<38px) hides the service + time lines on both sides (`AppointmentBlock.tsx:77` ≈
`_calendar_day.php:174`).

## Concrete UI gaps

1. **Per-status stat cards / count legend (Day toolbar)** — Demo shows clickable count
   cards per status for the viewed day, doubling as the colour legend
   (`DayCalendar.tsx:246-271`, via `statusCounts`). Ours shows a **static colour legend
   only** (`calendar.php:75-81`) — no per-day counts. **MISSING (counts).**

2. **Live hover slot highlight** — Demo follows the cursor with a purple snapped slot band
   showing the target time (`SpecialistColumn.tsx:180-192`). Ours has no hover preview; the
   cursor is just `cursor-pointer` on the body. **MISSING.**

3. **Live drag preview snap** — Demo snaps the dragged block vertically during drag
   (`snapVertical` modifier, DayCalendar:131-134). Ours uses the native browser drag ghost,
   snapping only on drop. **PARTIAL.**

4. **Muted column "Inactive" vs "Off"** — Demo distinguishes inactive specialists from
   off-day ones in the muted badge (`SpecialistColumn.tsx:118-121`). Ours always shows a
   single **"Off"** badge for any muted column (`_calendar_day.php:104`). **PARTIAL.**

5. **Add-booking affordance** — Demo opens a pre-filled modal on empty-slot click. Ours has
   that (page nav) **plus** an explicit `+` button in each non-muted column header
   (`_calendar_day.php:106-110`) the demo lacks. **OURS RICHER.**

6. **"No one is working this day" empty state** — Demo: `EmptyState` with icon + helpful
   copy "No specialist is scheduled for <date>… you can still block time or pick another
   day" (`DayCalendar.tsx:293-298`). Ours: a thinner empty card "No specialists are set up
   for this shop yet." (`_calendar_day.php:63-72`) — and it triggers on
   `!hasGrid` (no columns at all), conflating "nobody works today" with "no specialists
   exist". **PARTIAL — weaker copy, can't block-time from the empty state.**

7. **Zoom control** — Demo: smooth vertical slider + in/out buttons (`DayCalendar.tsx:358-383`).
   Ours: two `+/-` link buttons that reload the page (`calendar.php:91-102`). **PARTIAL.**

8. **Reschedule confirm dialog** — Demo: rich confirm with names/times + "customer will be
   notified" note (DayCalendar:171-197). Ours: reassign uses a bare `window.confirm`;
   reschedule has **no confirm**. **PARTIAL/MISSING.**

9. **In-group badge on blocks** — Demo draws a small Users icon on blocks belonging to a
   group booking (`AppointmentBlock.tsx:93-101`). Ours has no group concept on the calendar
   block. **MISSING (group feature not in scope here, but noted).**

## Month view — match

Both render a 6-row grid, per-day status count pills (max 3 + overflow chip), today
highlighted, click-through to the day. Differences:
- **Week start**: demo MonthCalendar is locale-driven; ours is **hard Mon-first**
  (`_calendar_month.php` weekdays + leading-blank math in presenter :293). Minor.
- Ours adds a bottom **legend** row the demo month grid does not (`_calendar_month.php:161-169`).

## RTL / i18n

- Ours uses logical properties throughout (`start-/end-`, `ms-/me-`, `border-inline-start`,
  `insetInlineStart`) so the grid mirrors correctly in Arabic — matches the demo's logical
  CSS. Good.
- All ours strings go through `Yii::t('frontend'|'backend', ...)`; **bilingual is mandatory**
  per CLAUDE.md — verify every string used here (e.g. 'Block time', 'Reschedule booking',
  'No open times this day.', 'Reassign this booking…', month '{n} bookings') exists in both
  `common/messages/ar/*` and `common/messages/en/*`.
- **Mixed domains**: the day legend + month legend pull status labels from
  `Yii::t('backend', ...)` while surrounding chrome uses `Yii::t('frontend', ...)`
  (calendar.php:38-44, _calendar_month.php:81-87). Works, but split-domain — confirm the
  backend keys are translated.
