# Shop · Team / specialists + shifts — Logic & Flows

Demo (canonical): `navagoo-app/src/portals/shop/Team.tsx`, `navagoo-app/src/lib/specialists.ts`,
store actions in `navagoo-app/src/store/store.ts`, selectors in `navagoo-app/src/store/selectors.ts`,
types in `navagoo-app/src/types.ts`.

Ours: `frontend/controllers/AgentsController.php`, `frontend/views/agents/{index,_form,view,_detail}.php`,
`common/models/UserShift.php`, `backend/models/UserForm.php`, `frontend/controllers/AgentsWalletController.php`.

---

## 1. Page shape / tabs

- **Demo** — `ShopTeam` is one page with a `Segmented` control toggling two views:
  `specialists` and `payroll` (`Team.tsx:44`, `Team.tsx:71-84`). Add-specialist button only
  shows on the `specialists` tab (`Team.tsx:64-69`). All data is scoped to the active shop:
  `state.specialists.filter(s => s.shopId === shop.id)` (`Team.tsx:47`, `Team.tsx:101`).
- **Ours** — `AgentsController::actionIndex` (`AgentsController.php:69-88`) renders a single
  card grid of agents (User models, `user_type = AGENT`) for the current shop via `AgentSearch`.
  **No tabs. No payroll view exists at all.** Shop-scoping happens inside `AgentSearch`
  (session `UserRole=user` + owner shop). Add/edit are full pages (`create`/`update`), not a
  modal.

## 2. Specialist list

- **Demo** — `DataTable` with columns: avatar+name, title, wage-type badge, fixed salary,
  commission %, status badge, row actions edit / activate-toggle / delete (`Team.tsx:128-215`).
- **Ours** — card grid (`index.php:56-196`): avatar/initials, name, role, availability toggle,
  MOL/Mostaqqel/tips chips, email/mobile, a single working-hours line, and edit / show-hide /
  view actions. **No wage-type, fixed-salary, or commission columns** (those fields don't exist
  in our data model). Adds show/hide (`hide` flag) which the demo has no concept of.

## 3. Add / Edit specialist

- **Demo** — `SpecialistModal` (`Team.tsx:458-899`). One modal, four sections: Profile,
  Compensation, Services, Working Hours. `buildPayload` (`Team.tsx:425-456`) trims fields,
  defaults title to `"Specialist"`, zeroes the hidden salary/commission depending on wageType,
  and packs everything into `Omit<Specialist,'id'>`. `submit` (`Team.tsx:547-559`) calls
  `addSpecialist`/`updateSpecialist` then `setSpecialistServices` and toasts.
- **Ours** — `actionCreate`/`actionUpdate` (`AgentsController.php:113-274`) drive a full-page
  `_form.php`. Profile (Personal info card) + Bio + Opening Hours + Services. **No Compensation
  section.** Saving splits `full_name` into first/middle/last on `UserProfile`
  (`AgentsController.php:137-140,215-218`), persists working-days CSV, services rows in
  `UserShopService`, and shifts via `_persistUserShifts`.

## 4. Service linking (specialist ↔ service)

- **Demo** — link lives on the **service** (`Service.specialistIds[]`); Team edits it via
  `setSpecialistServices(id, serviceIds)` which reconciles each shop service's `specialistIds`
  set (`store.ts:1863-1879`). Idempotent.
- **Ours** — link lives in a **join table** `UserShopService(user_id, service_id)`. On update we
  `deleteAll` then re-insert (`AgentsController.php:245-253`). Equivalent end result; different
  storage. **Parity: done.**

## 5. Working hours / shifts data model — DIVERGENT

- **Demo** — `Specialist.workingHours: WorkingDay[]` (`types.ts:129-133`): per weekday
  `{ day, enabled, shifts: Shift[] }`, `Shift = { start:'HH:mm', end:'HH:mm' }`. Sun-first index
  alignment via `WEEKDAY_TOKENS` (`specialists.ts:8`). `defaultWorkingHours()` opens Sat–Thu
  10:00–22:00, Friday off (`specialists.ts:35-41`). Multiple shifts/day model breaks.
  `withAddedShift` (`Team.tsx:910-925`): splits a ≥2h single block around a ~1h midday break,
  else appends a 1h block. Overnight shift detected client-side when `end <= start` → shows a
  `+1d` badge (`Team.tsx:848-855`).
- **Ours** — two stores:
  1. `UserProfile.working_days` = CSV of day IDs (which days are enabled).
  2. `UserShift(user_id, from_time, to_time)` rows (`common/models/base/UserShift.php`), where
     **`from_time` is overloaded as `"dayId:HH:mm"`** and `to_time` is plain `"HH:mm"`
     (`AgentsController.php:385-386`, parsed back in `_form.php:878-889` with regex
     `^([1-7]):(.+)$`). There is **no `day` column and no `enabled` column** on `user_shift`.
  Multiple shifts/day are supported (`Shifts[dayId][idx][from_time|to_time]`, `_form.php:939-949`).
  Add-shift JS chains the new slot from the previous shift's end up to shop close
  (`_form.php:1107-1166`) — **different default logic than the demo's midday-split.**
  Default seed first shift = shop open→close, not 10:00–22:00 (`_form.php:94-99`).
  No per-specialist calendar `color`; no `+1d` overnight badge on the row (overnight is a
  shop-level concept handled in bounds math).

## 6. Shop working-hours bounds — NEW ON OUR SIDE (not in demo)

- **Ours** — both client (`_form.php:219-301`) and server (`AgentsController.php:346-406`)
  validate each shift stays within the shop's `open_at`/`close_at`, with overnight handling.
  Out-of-bounds shifts are **dropped on save** and a warning flash is shown
  (`AgentsController.php:152-158,236-242`). Also: no overlap between shifts on a day; from<to
  ordering. **The demo has none of this** — it stores whatever times you enter.

## 7. Status / availability

- **Demo** — `status: 'active'|'inactive'` (`types.ts:140`); `toggleStatus` confirms before
  deactivating and toasts (`Team.tsx:116-126`).
- **Ours** — `User.status` ACTIVE / NOT_ACTIVE via `actionToggleStatus`
  (`AgentsController.php:408-439`): POST + confirm, writes `UserStatusLogs`, sends a suspension
  notification. Plus a separate `hide` show/hide toggle (`actionToggleShowHide`,
  `AgentsController.php:442-466`) with no demo equivalent. **Status parity: done** (richer on
  our side); demo's "hidden from new bookings on deactivate" intent maps to our status.

## 8. Delete

- **Demo** — `requestDelete` (`Team.tsx:106-114`): if the specialist has bookings, opens a
  "deactivate instead / delete anyway" modal (`Team.tsx:220-257`); otherwise a plain confirm.
- **Ours** — `actionDelete` (`AgentsController.php:317-324`) hard-deletes after a generic
  `data-confirm`. **No booking-count guard, no "deactivate instead" branch.** Note: index UI
  currently exposes show/hide + status, not a delete button — delete route exists but is largely
  unsurfaced. **Parity: partial/missing on the guarded-delete UX.**

## 9. Payroll computation — MISSING

- **Demo** — `Payroll` (`Team.tsx:262-371`): for the current sim-month, per specialist sums
  completed bookings' `bookingValue` → serviced; commission base is serviced or
  `serviced - bookingFeesIncurred` when `commissionBasis==='net_of_fees'`
  (`Team.tsx:275-279`, `lib/finance.ts:424`); `commission = base * commissionPct/100`;
  `total = fixedSalary + commission`; plus `tipsOwedToSpecialist` (unsettled tips,
  `selectors.ts:382-393`). Grand totals for pay and tips owed.
- **Ours** — **no payroll page, no commission math, no wage fields.** The only adjacent thing is
  `AgentsWalletController::actionSpecialistTips` (`AgentsWalletController.php:103-165`) which
  summarises tips per specialist — covers the *tips-owed* slice only, on a different screen, and
  does **not** compute salary/commission/total pay. **Parity: missing.**

## 10. Photo upload

- **Demo** — in-modal file→square data URL (`Team.tsx:488-501`, `lib/image.ts`).
- **Ours** — filekit Upload widget posting to `avatar-upload`, server-side resize to 215×215 via
  Intervention (`AgentsController.php:48-62`). **Parity: done** (different mechanism).
</content>
</invoke>
