# Admin · Settings — Business Rules (canonical = demo)

Rules the demo Settings surface encodes. Numbered + implementable. Refs to demo on the left,
our status noted inline. Most are **missing** on our side because we have no workflow/theme editor.

## Booking-workflow rules

1. **Status transitions are an admin-editable allow-list.** `bookingTransitions[from]` lists the
   statuses a booking may move TO. Any move not listed is rejected by the store guard
   (`store/store.ts:518`, `lib/status.ts:60-65`). Default seed (`store/seed.ts:62-68`):
   - `scheduled → [in_progress, no_show, cancelled]`
   - `in_progress → [completed]`
   - `completed → []`, `no_show → []`, `cancelled → []` (terminal).
   Our side: **missing** — booking status changes are not gated by an editable transition table.

2. **A FROM status cannot transition to itself** — the editor never renders a self-chip
   (Settings.tsx:115 filters `t !== from`).

3. **Terminal statuses** (empty allowed set) render "Final — no onward moves" and offer no chips
   (Settings.tsx:140-142). `completed/no_show/cancelled` are terminal by default.

4. **Reschedule is a separate capability from status moves.** `rescheduleStatuses` lists which
   statuses allow time-move/reassign (incl. day-calendar drag). Default `['scheduled']`
   (`store/seed.ts:71`, `lib/status.ts:96-99`). Enforced at `store.ts:1381,1419,1620,1641`.
   Our side: **missing** — no reschedule allow-list.

5. **No-show requires a grace delay.** A booking may be marked `no_show` only once
   `now >= appointmentStart + noShowGraceMin minutes` (`canMarkNoShow`, `lib/status.ts:106-108`;
   enforced `store.ts:523`). Default 15 min (`seed.ts:72`). Grace value clamped `>= 0` in the editor
   (Settings.tsx:165). Our side: **missing**.

6. **Reset-to-defaults** restores transitions, reschedule statuses, and 15-min grace in one action
   (Settings.tsx:73-80). Our side: **missing**.

7. **Cancel still runs the refund-zone flow.** `cancelled` is part of the transition map so it gates
   the Cancel action, which independently triggers refund handling (Settings.tsx:170-176). Our side: N/A (no editor).

8. **Config change reconciles billing.** Every `updateConfig` calls `reconcileBilling()`
   (`store.ts:459`), so workflow edits can ripple into financial reconciliation. Our side: N/A.

## Theme / color rules

9. **One base color per token; tints derived at render.** ~26 tokens (`lib/theme.ts:50-90`) across 8
   groups. Soft badge tints are computed via `color-mix()` from the single base; solid elements use the
   base directly (`badgeTint`, `theme.ts`). Our side: **missing**.

10. **Override vs saved-default vs built-in default** is a 3-layer model:
    `effective = override ?? savedDefault ?? DEFAULT_THEME_COLORS` (Settings.tsx:282-283). "Save default"
    promotes the current value to baseline and clears the override (`store.ts:466-470`). Our side: **missing**.

11. **Save-default only enabled when the value differs from the effective default**
    (`canSaveDefault = normalizeHex(cur) !== normalizeHex(effectiveDefault)`, Settings.tsx:323). **missing**.

12. **Reset-all disabled when nothing is dirty** (`dirtyCount === 0`, Settings.tsx:286). **missing**.

13. **Cancelled status color also themes danger buttons** (Delete/Cancel) — a single token drives
    multiple surfaces (`theme.ts` `status.cancelled` note). **missing**.

## Policies/documents rules

14. **Documents carry version + updated-date + published/draft status** (demo `DOCS`, Settings.tsx:30-37).
    Published=blue badge, draft=slate. Our side: **partial** — we store/upload the file and a `*_path`,
    but **no version, no updated date, no published/draft status** per document.

15. **Demo policies tab is read-only / view-only** (View = simulated download, Settings.tsx:213-220).
    Our side does the inverse: it is an **upload** surface (`_form.php:96-110`,
    `SettingsController.php:104-146`) and additionally **notifies customers/agents/shops** when a policy
    file changes (`SettingsController.php:151-159`) — behavior the demo does not encode.

## Permissions / scoping

16. **Admin-only.** The demo Settings lives in `portals/admin`. Our `SettingsController::beforeAction`
    requires login and, for `manager` role, checks `checkPermissions('settings_'.$action)` else 403
    (`SettingsController.php:26-41`). Non-manager roles pass through. Settings is a **single global row**
    (id=1, `findModel(1)` at `SettingsController.php:90`) — global, not shop-scoped, matching the demo's
    `GlobalConfig` being platform-wide.

## Our extra (non-demo) Settings rules — financial/system, here for completeness

17. `max_deposit_percent` is an integer 1–100 (`backend/models/Settings.php:105`).
18. `taxes_type` must be in range [1,2] (`Settings.php:83`); options from `Settings::serviceFees()`.
19. `distance_range`, `platform_commission`, `not_paid_bookings_period` are sanitized to numeric-or-null
    (`Settings.php:154-177`). `show_agents` is 0/1 (`Settings.php:113`).
    (These map to other parity areas, not the demo Settings screen.)
