# Reschedule / Reassign + Slot Picker — Parity Table

Demo = `/private/tmp/Navagoo_MI_dev/navagoo-app/src`. Ours = Yii2 `frontend/`.

| Demo behavior | Demo ref | Our ref (file:line) | Status | Note |
|---|---|---|---|---|
| Free-slot engine: shifts − time-off − bookings − past | `SlotPicker.tsx:34-58`, `schedule.ts specialistAvailability` | `BookingScheduleService.php:217-227,348-376` | done | Faithful PHP port |
| `checkPlacement` rejection order overlap→time-off→outside→cannot-perform | `schedule.ts checkPlacement` | `BookingScheduleService.php:273-325` | done | Identical order + reasons |
| Slot step from `slotStepMin` (default 15) | `SlotPicker.tsx:46` | `BookingScheduleService.php:351,358` | done | |
| Today: exclude past slots | `SlotPicker.tsx:38` | `BookingScheduleService.php:360` | done | |
| 15-min duration floor | `bookingMinutes` | `BookingScheduleService.php:33,352` | done | |
| End time derived, finance untouched | `store.ts:184-185` | `BookingController.php:554-557` | done | |
| Shop-scoped slot search/placement | `sp.shopId` | `BookingController.php:540,619`; `checkOwnership` `:538` | done | Ours adds explicit ownership check |
| Reschedule keeps specialist; reassign changes it | `store.ts:1374,1414` | `BookingController.php:541` | done | |
| Reassign verifies can-perform (serviceIds) | `store.ts:1437` | `BookingController.php:561-569`; `BookingScheduleService.php:250-259` | done | |
| **Status gate: only `rescheduleStatuses` (default scheduled) movable** | `lib/status.ts:85-91`; `store.ts:1381,1419` | — | **missing** | `moveBooking` never checks status server-side; only the drag handle is gated client-side via `isDraggable()` (scheduled+accepted) |
| Admin-configurable `rescheduleStatuses` | `store.ts:1903-1904`, `seed.ts:71` | — | missing | No config concept; ours hardcodes scheduled+accepted as draggable |
| Live conflict preview + disabled confirm | `modals.tsx:282-326` | — | missing | Ours surfaces conflicts only via POST `alert` |
| Reschedule modal w/ SlotPicker, ignoreBookingId, defaultDayKey | `modals.tsx:330-337` | `_reschedule_modal.php:54,182-197` | partial | Day pre-seed + ignore present; no selected-state/confirm |
| **Book now** (next boundary, glowing pill) | `SlotPicker.tsx:120-187` | — | missing | |
| **Earliest** badge on first available slot | `SlotPicker.tsx:148-160,289-318` | — | missing | |
| **Next available · {date}** jump from empty day | `SlotPicker.tsx:170-280` | — | missing | Ours shows only "No open times this day." |
| Off-day vs "no openings for {duration}" distinction | `SlotPicker.tsx:230-237` | `_reschedule_modal.php:136-138` | missing | One generic empty message |
| Tappable date pill → native calendar (`showPicker`) | `SlotPicker.tsx:191-225` | `_reschedule_modal.php:44-47` | missing | Pill is display-only |
| `relativeDays` sublabel (Today / in N days) | `SlotPicker.tsx:236-238` | — | missing | |
| Prev arrow disabled at today; clamp to today | `SlotPicker.tsx:208-219` | `_reschedule_modal.php:118,193` | done | |
| Reassign via drag across columns | `DayCalendar.tsx:204`, `SpecialistColumn.tsx:235` | `BookingScheduleService.php:632 reassignUrl`; `_calendar_day.php`/`_script.php` | done | Endpoint + drag wiring present |
| Server-side placement re-check on write (safety net) | `store.ts:1388,1431` | `BookingController.php:569` | done | |
| Activity-log entry on move | `store.ts:1399-1408,1444-1452` | — | missing | Ours increments `reschedule_count` instead (no audit row) |
| Slot-mirror (`agent_slots`) rebuild in txn | — (in-memory) | `BookingController.php:577-598` | done | Ours-only correctness detail |
| Overnight / business-day slot values (roll past midnight) | `schedule.ts businessDayOf/fromBusinessMinutes` | `BookingScheduleService.php:368` (single-date) | partial | Blocks are +1440-aware; slot value/overlap query are single-calendar-date |
| Group-booking reschedule (atomic, all guests) | `store.ts:1614-1655` | — | missing | No group-booking concept in our codebase |

## Area score: 60%

Core engine and the reassign/reschedule write paths are a faithful, well-scoped
port (placement, can-perform, shop-scoping, derived end-times, txn slot mirror —
all solid, and arguably stronger than the demo on ownership + transactional
integrity). The gap is concentrated in two places:

- **Correctness/permissions (high priority):** the reschedule/reassign endpoints
  have **no status gate** — the demo's single most important business rule
  (`canRescheduleStatus`) is not enforced server-side, and there is no
  `rescheduleStatuses` config. A non-scheduled booking can be moved by a crafted
  POST.
- **Slot-picker UX:** Book now, Earliest, Next-available jump, off-day messaging,
  tappable calendar, live conflict banner, and selected-state confirm are all
  absent.

Also fully missing: activity-log entries on move, and group-booking reschedule
(no group concept exists on our side). Overnight slot generation is partial.

## Verified verdict (adversarial)

Independent re-check of every analyst "done" against demo + our source. Most
"done" rows hold up; one **new** overclaim surfaced that the analyst missed.

| Claim | Analyst | Verified | Evidence |
|---|---|---|---|
| Free-slot engine (shifts − time-off − bookings − past) | done | **done** | `BookingScheduleService.php:217-227,348-376` is a faithful port of `schedule.ts:212-223` + `SlotPicker.tsx:34-62`. Same subtract algebra, ceil-to-step, today/past skip. |
| checkPlacement rejection order | done | **done** | `BookingScheduleService.php:273-325` ↔ `schedule.ts:312-348`. Identical order overlap→time-off→outside→cannot-perform and reasons. |
| Slot step / today-exclusion / 15-min floor | done | **done** | step `:351`, past-skip `:360`, floor `:352` (`max($durationMin, 15)`) ↔ `SlotPicker.tsx:45-47`, `schedule.ts:166`. |
| End time derived, finance untouched on move | done | **done (narrow)** | `BookingController.php:554-557` derives end from `getScheduledDuration`; no charge re-derivation. BUT see write-path gap below — "mirrors authoritative API write path" (comment `:521`) is **false**. |
| Reassign verifies can-perform (serviceIds) | done | **done** | `BookingController.php:561-569` collects serviceIds and passes to `checkPlacement`; `BookingScheduleService.php:250-259` ↔ `schedule.ts:289-295`. |
| Shop-scoping of slots/placement | done | **done (stronger)** | `checkOwnership` (`FrontEndController.php:120`) throws 404 on shop mismatch; slots/move read `identity->shop_id` (`BookingController.php:540,619`). Stronger than demo's `sp.shopId`. |
| Reassign via drag across columns | done | **done** | `_calendar_day.php:263-334`: dragstart gated on `[draggable="true"]`, cross-column drop → `reassignUrl` (with confirm), same-column → `rescheduleUrl`. Endpoints `BookingScheduleService.php:631-632`. |
| Server-side placement re-check on write | done | **done** | `BookingController.php:569` calls `checkPlacement` before the txn. |
| Slot-mirror rebuild in txn | done | **done** | `BookingController.php:577-598`. Ours-only correctness detail; accepted. |
| **Reschedule-LIMIT gate on shop move** | *(not listed)* | **MISSING (new overclaim)** | The customer API path enforces `reschedule_count >= shop.reschedule_limit` → 400 (`api/controllers/BookingController.php:434-437`) AND `status != STATUS_SCHEDULED` → 400 (`:430`). The shop `moveBooking` enforces **neither**: it blindly does `reschedule_count + 1` (`BookingController.php:585`) with no cap and no status check. The `:521` comment claiming it "mirrors the authoritative api write path" is inaccurate. Compounds the status-gate gap below. |

Re-confirmed as the analyst had them (no change): status gate **missing**
(`moveBooking` has no `STATUS_*` check; only the client drag handle is gated via
`isDraggable()` scheduled+accepted at `BookingScheduleService.php:42-45,552`);
admin-configurable `rescheduleStatuses` **missing**; live conflict preview /
disabled confirm **missing** (modal POSTs on slot click, no selected-state, errors
via `alert` at `_reschedule_modal.php:217-247`); Book now / Earliest / Next-available
**missing**; off-day-vs-no-openings, tappable date pill (`showPicker`), relativeDays
sublabel **missing** (one generic `No open times this day.` `_reschedule_modal.php:77,137`;
pill display-only `:44-47`); activity-log on move **missing** (ours increments
`reschedule_count` instead, no audit row); group-booking reschedule **missing**;
overnight slot values **partial** (`minToClock` wraps mod-1440 `:85` but slot `value`
keeps the single calendar `$date` `:368` and overlap query is `LIKE date%` `:280` —
not business-day-aware like `schedule.ts businessDayOf`).

i18n spot-check: all sampled new `Yii::t('frontend', …)` strings present in BOTH
`common/messages/{ar,en}/frontend.php`. No RTL/i18n regression found.

### Adjusted area score: **55%**

Down 5 from the analyst's 60. Rationale: the analyst's engine/write-path "done"
rows genuinely hold (verified line-by-line, and shop-scoping/txn-mirror are
stronger than the demo). But a **second** server-side safeguard — the
reschedule-count limit — is missing on the shop move path while present on the
customer API path in the same repo, widening the correctness/permissions gap
beyond the single status-gate item the analyst counted. Combined with the
unenforced status gate, the shop reschedule/reassign endpoints accept a crafted
POST that the rest of the codebase would reject, so the "faithful write path"
framing is weaker than scored.
