# Shop · Specialist time-off — Parity matrix

Demo (canonical) = `/private/tmp/Navagoo_MI_dev/navagoo-app/src`. Ours = Yii2 frontend portal.

| Demo behavior | Demo ref | Our ref | Status | Note |
|---|---|---|---|---|
| TimeOff entity (scope, specialist, start/end, reason, note, allDay) | `types.ts:367-379` | `common/models/AgentTimeOff.php:30-39`; migration `m260622_130000_create_agent_time_off.php` | done | Demo stores ISO start/end; ours stores off_date + from/to + all_day. Equivalent. |
| "Block time" modal — open trigger | `DayCalendar.tsx:407-412` | `frontend/views/booking/calendar.php:85-87` | done | data-modal-open="timeoff" |
| Applies-to: specialist / whole shop segmented | `NewTimeOffModal.tsx:99-107` | `frontend/views/booking/_timeoff_modal.php:43-50` | done | hidden scope input + JS toggle |
| Specialist select (active, shop) shown only for specialist scope | `NewTimeOffModal.tsx:109-119` | `_timeoff_modal.php:53-60,141-142` | done | |
| Date / Reason / Note / All-day fields | `NewTimeOffModal.tsx:121-154` | `_timeoff_modal.php:62-103` | done | toggle is a checkbox vs styled Toggle (cosmetic) |
| Modal header shows shop name subtitle | `NewTimeOffModal.tsx:84-85` | `_timeoff_modal.php:33` | missing | no subtitle rendered |
| Submit disabled until valid (date + specialist/shop + times differ) | `NewTimeOffModal.tsx:48,91` | `_timeoff_modal.php:107-113` | missing | button always enabled, no live validity |
| Non-all-day requires start !== end | `NewTimeOffModal.tsx:48` | `AgentTimeOff.php:64-68` (only presence) | missing | zero-length block can be saved |
| All-day → spans shop open→close | `NewTimeOffModal.tsx:54-57` | `BookingController.php:652-653` + `BookingScheduleService.php:168-170` | done | |
| Overnight block end<=start → +1440 | `NewTimeOffModal.tsx:61` | `BookingScheduleService.php:177-178` | done | computed at read vs write — equivalent |
| "Ends next day (+1d)" overnight hint | `NewTimeOffModal.tsx:143` | — | missing | no hint shown |
| Create persists block | `store.ts:1695` | `BookingController::actionTimeOff` `:640-669` | done | |
| Specialist must belong to shop | `NewTimeOffModal.tsx:37` (filter) | `BookingController.php:657-663` | done | ours server-validates |
| Shop scoping on write (shop_id from session) | `shopId` prop | `BookingController.php:644-647` | done | |
| Success feedback | toast, in-place insert (`NewTimeOffModal.tsx:75`) | `location.reload()` (`_timeoff_modal.php:202`) | partial | hard reload vs optimistic |
| Server error surfaced | n/a (never fails) | alert(message) `_timeoff_modal.php:201-206` | done | ours stronger |
| Time-off filter for business day (scope-aware) | `schedule.ts:195-209` | `BookingScheduleService::timeOffBlocks` `:157-191` | done | |
| businessDayOf reassigns post-midnight block to prior session | `schedule.ts:100-106,205` | — | partial | ours keys on stored off_date only |
| availability = working − time-off (interval subtraction) | `schedule.ts:212-223,133-151` | `BookingScheduleService.php:217-227,194-214` | done | line-for-line port |
| Out-of-shift shading = complement of free | `SpecialistColumn` free-block shading | `BookingScheduleService::shadedGaps` `:230-245` | done | |
| Booking placement rejects over time-off (precedence) | `schedule.ts:312-348` | `BookingScheduleService.php:273+` (`:301`) | done | |
| Placement reason text in sync | `schedule.ts:352-365` | `BookingScheduleService.php:327` | done | |
| Calendar block: hatch, label, time, min-height | `TimeOffBlock.tsx:25-62` | `_calendar_day.php:133-161` | done | |
| Block label = note || titleCase(reason) | `TimeOffBlock.tsx:43` | `_calendar_day.php:149` (reason only; note in tooltip) | partial | note not used as primary label |
| Hover remove X on block | `TimeOffBlock.tsx:49-59` | `_calendar_day.php:154-158` | done | |
| Remove with confirm dialog | `DayCalendar.tsx:334-339` | `calendar.php:208-213` (`confirm`) + `actionRemoveTimeOff` `:672-684` | done | |
| Remove shop-scoped | n/a (no tenancy) | `BookingController.php:678` | done | ours adds guard |
| No edit flow (create + delete only) | `store.ts` (no edit) | controller (no edit action) | done | |
| Reason enum fixed (6 values) | `types.ts:368` | `AgentTimeOff.php:32-39,62` | done | |
| No money/quota/VAT on time-off | `schedule.ts:1-17` | model has no money fields | done | N/A by design |
| Bilingual i18n strings | English literals | `common/messages/{en,ar}/frontend.php` (all keys present) | done | verified ar+en |

**Area score: 86%**

## Verified verdict (adversarial)

Both demo and our refs were re-opened and cross-checked. Most analyst rows hold,
including the analyst's own `missing`/`partial` downgrades. One `done` row is
downgraded, and one already-`missing` row has its severity corrected.

| Demo behavior | Our ref | Analyst | Verified | Evidence / reason |
|---|---|---|---|---|
| Overnight block end<=start → +1440 (read-vs-write "equivalent") | `BookingScheduleService.php:177-178` | done | **partial** | Equivalent only for genuine overnight (`end<start`). For `end==start` (zero-length — demo forbids via disabled submit, ours allows) our read does `end += 1440` → a spurious **24-hour** block. Not equivalent on that input. |
| Non-all-day requires start !== end | `AgentTimeOff.php:64-68` | missing | **missing (severity raised)** | Confirmed no `start!=end` rule. Worse than "zero-length no-op": `from==to` saves, then `timeOffBlocks` (`:177`) turns it into a full-day 24h block, silently closing the column. Demo blocks submit (`NewTimeOffModal.tsx:48,91`). |
| Modal header shop-name subtitle | `_timeoff_modal.php:31-34` | missing | **missing (confirmed)** | Demo `subtitle={shop.commercialName}` (`:85`); our header renders title only. |
| Submit disabled until valid | `_timeoff_modal.php:109-113` | missing | **missing (confirmed)** | Demo `disabled={!valid}` (`:91`); our submit button is unconditionally enabled, no live validity. |
| "Ends next day (+1d)" overnight hint | — | missing | **missing (confirmed)** | Demo `Field hint` at `:143`; absent in ours. |
| Success feedback (toast vs reload) | `_timeoff_modal.php:202` | partial | **partial (confirmed)** | Demo `toast.success` + optimistic insert (`:75`); ours `location.reload()`. |
| businessDayOf reassigns post-midnight block | — | partial | **partial (confirmed)** | Demo `businessDayOf` (`schedule.ts:100-106,205`) keys overnight pre-close hours to prior session; ours keys on stored `off_date` only. Low impact for non-overnight shops. |
| Block label = note || titleCase(reason) | `_calendar_day.php:149` | partial | **partial (confirmed)** | Demo `off.note || titleCase(off.reason)` (`TimeOffBlock.tsx:43`); ours shows reason label only, note in tooltip. |
| All-day spans shop open→close | `BookingScheduleService.php:141-152,168-170` | done | **done (confirmed)** | `shopWindow` overnight handling (`end+=1440`) matches demo `shopWindow`. Computed at read vs write — equivalent for stable hours. |
| Placement rejects over time-off (precedence) | `BookingScheduleService.php:295-322` | done | **done (confirmed)** | Rejection order overlap→time-off→outside-availability→cannot-perform matches `schedule.ts:312-348` line-for-line. |
| Specialist must belong to shop (validated) | `BookingController.php:657-663` | done | **done (confirmed)** | Server validates agent ∈ shop+USER_TYPE_AGENT; stronger than demo's client filter. Caveat: controller derives shop-wide purely from blank `agent_id` and ignores the `scope` field — scope=specialist with blank agent silently becomes a shop block (no demo divergence in normal flow). |
| Remove with confirm + shop-scoped delete | `calendar.php:208-213`; `BookingController.php:672-684` | done | **done (confirmed)** | `confirm()` + `findOne(['id','shop_id'])` tenancy guard. |
| Bilingual i18n (ar + en) | `messages/{en,ar}/{frontend,backend}.php` | done | **done (confirmed)** | All frontend keys present in both. Reason options + `getReasonLabel()` use `Yii::t('backend',…)`; Break/Vacation/Sick leave/Holiday/Closed/Other all present in ar+en backend. |

All other analyst rows re-verified and stand as claimed.

**Adjusted area score: 82%** (analyst 86 − overnight `done`→`partial` downgrade and
the under-stated zero-length→24h corruption severity).
