# Admin Demo→Portal Parity Audit — Bookings · People · Users & Roles · Events

Demo truth: `Navagoo_MI/navagoo-app` v0.28 — `src/portals/admin/{Bookings,People,UsersRoles,Events}.tsx` + `src/i18n/en.ts`.
Portal: Yii2 `backend/` tier. Audit date 2026-07-27. **No files modified.**

Legend: **GAP** = demo has it, portal doesn't / differs. **SUPERSET** = portal-only addition (not a demo-fidelity failure, listed separately).

---

## 1. BOOKINGS

Demo: `src/portals/admin/Bookings.tsx` (117 lines).
Portal: `backend/views/booking/index.php` + `backend/controllers/BookingController.php` (`booking/index`).

### Gaps

| # | Type | Demo | Portal | Detail |
|---|------|------|--------|--------|
| B1 | Filter control | **Status Segmented pills** `All \| Scheduled \| Completed \| No-show \| Cancelled` (Bookings.tsx:89-99, keys `admin.bookings.filterAll` + `shop.bookingStatus.*`) | Status is a plain `<select>` (`BookingSearch[status]`, index.php:197-205) buried at the end of a 10-input filter form | The signature pill row is missing entirely; interaction model differs (1-click pill vs select+Submit) |
| B2 | Layout | 7-column table: BOOKING · SHOP · CUSTOMER · SERVICE · WHEN · VALUE · STATUS | 31-column table (index.php:224-254) | The demo's compact reading layout is lost; portal is a data dump with horizontal scroll. (The extra columns themselves are a SUPERSET, see below, but the *default view shape* is a fidelity gap) |
| B3 | Column render | SHOP column shows **ShopTile avatar chip** (hue-tinted initials, Bookings.tsx:41-49) | `Shop Name` is plain text (index.php:335); avatar helper exists but is only applied to Customer + Specialist | Missing shop avatar chip |
| B4 | Column render | WHEN = one cell: `date + small time` → "22 May 2026 13:00" (Bookings.tsx:62-70, `date()`+`timeShort()`) | Split across 4 columns: Appointment Date (full datetime), Appointment Start, Appointment Finish, Duration | Combined "When" cell format not reproduced |
| B5 | Copy | Title `'Booking details'` (en.ts `admin.bookings.title`) | `Yii::t('backend', 'Booking Details')` (index.php:31) | Case diff: "Details" vs "details" |
| B6 | Copy | Shop select first option `'All shops'` (`admin.bookings.allShops`) | `'Choose Shop'` (index.php:150) | Copy diff; demo filters by shop id, portal by shop **title** string (`BookingSearch[shop_name]`) |
| B7 | Footer | `'{{shown}} of {{total}} bookings'` (`admin.bookings.footerCount`), e.g. "38 of 120 bookings", plain text under the card | Yii `getDataProviderSummary()` ("Showing 1-20 of N items") inside the card footer (index.php:404) | Different copy + placement |
| B8 | Column header copy | `Booking / Shop / Customer / Service / When / Value / Status` | `Booking ID / Shop Name / Customer Name / Services / (4 date cols) / Total Amount / Booking Status` | Every header label differs from the demo terms |
| B9 | Alignment | VALUE right-aligned (`align: 'right'`), STATUS centered | Total Amount **center**-aligned (`$thCenter`, index.php:248) | Minor |
| B10 | Interaction | Client-side instant filtering (pills+select re-filter immediately); no submit button | GET form requires "Search" click; "Reset" appears only when filters active | Interaction parity gap inherent to server render, but no JS auto-submit was added |
| B11 | Sort | Demo sorts by `appointmentDate` DESC | Portal default sort comes from BookingSearch (created_at) | Verify default order matches "newest appointment first" |

### Supersets (portal-only)
- 24 extra columns: Group (GRP badge + guest label), Shop ID, Country, City, District, Customer ID, Gender, Mobile, Specialist ID/Name, Booking Method, Service Categories, Booking Date (created), Actual Start/Finish/Service Time, **Charges**, **Outstanding** (finance-ledger), Specialist/Shop Rating stars, Actions (view).
- 8 extra filters: Booking ID search, City, District, Customer name, Gender, Customer mobile, created-at range, appointment-date range.
- Row-level `{view}` action → `booking/view`.
- Real pagination via LinkPager (demo has none on Bookings — renders all rows).

---

## 2. PEOPLE

Demo: `src/portals/admin/People.tsx` (273 lines) — tabs **Customers | Classifications | Specialists** (3 tabs).
Portal: `backend/views/user/people.php` + `UserController::actionPeople` (`user/people?tab=`).

### Gaps

| # | Type | Demo | Portal | Detail |
|---|------|------|--------|--------|
| P1 | Tabs | 3 tabs: Customers, Classifications, Specialists (`admin.people.tab*`) | **4 tabs** — adds a `Users` tab (people.php:91-96) | Extra tab is stale: v0.28 moved users to Users & Roles. Duplicates that screen with a *different* (weaker) rendering — role badge instead of role select, "Created At" instead of "Last active" |
| P2 | Copy | Subtitle `'Customers, classifications & specialists'` (`admin.people.subtitle`) | `'Customers, classifications, specialists and platform users'` (people.php:108) | Follows the extra tab; diverges from demo copy |
| P3 | Copy (Customers col) | `colCustomer: 'Customer'` | `'Full Name'` (people.php:142) | Header copy diff |
| P4 | Copy/i18n bug (Customers col) | `colGender: 'Gender'` | `Yii::t('backend', 'Gender – الجنس')` (people.php:144) | **Hardcoded Arabic inside the English source string** — violates the bilingual rule (Arabic must come from `common/messages/ar/`) |
| P5 | Alignment | Bookings & Shops counts right-aligned (`align: 'right'`) | Center-aligned (`$thC`) | Minor |
| P6 | Classifications columns | `Customer · Shop · Classification · Reason · Set on · [Override]` — Reason = **classification-origin badge** (`classificationReason.*`: 'Freeze List' / 'App First Booking' / 'Deep Link' / 'Shop Admin Walkin'); `Set on` date | `Customer · Shop · Classification · Source · Override Reason · Locked At · Actions` (people.php:196-204). Source badge derived by `ucwords(str_replace('_',' ', source))` (people.php:221) — produces "Freeze List"-ish text but **not** via the demo's translated label set; extra "Override Reason" column; `'Locked At'` vs demo `'Set on'` | Column-set + copy diffs |
| P7 | Classifications shop cell | ShopTile avatar chip + name (People.tsx:104-107) | Plain text shop name (people.php:217) | Missing shop chip |
| P8 | Classifications subtitle | `'Immutable once set — admin override is logged with a mandatory reason'` (`admin.people.classificationsSubtitle`) | Same + appended `'…and re-derives charges.'` (people.php:187) | Copy drift |
| P9 | Override modal — subtitle | `'Changes the marketing-fee behaviour on this customer's bookings at this shop'` (`overrideModalSubtitle`) | Shows `customer · shop` names instead (people.php:330-334); the marketing-fee explanation is absent | Missing explanatory copy |
| P10 | Override modal — Current chip | `Current:` + ClassificationBadge of existing value (People.tsx:202-205, `currentLabel`) | Not rendered; select is just pre-set to current value | Missing element |
| P11 | Override modal — field labels/options | Label `'New classification'`; options `'Shop customer (no marketing fee)'` / `'Navagoo customer (marketing fee applies)'` (`classificationOption.*`); reason label `'Reason (required, logged)'`, placeholder `'e.g. verified pre-existing customer'` | Label `'Classification'`; options `'Shop-owned'` / `'Navagoo-sourced'` (people.php:342-343) — fee parentheticals dropped; reason label `'Override Reason'`, placeholder `'Why is this override being made?'` (people.php:348-351) | Multiple copy diffs |
| P12 | Override feedback | Success **toast**: `'Classification overridden'` + `'{{mobile}} → {{class}} · charges re-derived'` (People.tsx:176-179) | Full-page POST + session flash banner (people.php:112-119) | No ngToast; flash-on-reload instead |
| P13 | Specialists col header | `colSpecialist: 'Specialist'` | `'Full Name'` (people.php:254) | Copy diff |
| P14 | Specialists wage label | `wageType.both: 'Both'` | `'Fixed + Commission'` (people.php:81) | Copy diff |
| P15 | Specialists status | Badge `'Active'`/`'Inactive'` (`specialistStatus.*`) | `User::getStatuses()` labels (may be "Active/Not Active/…") (people.php:267) | Verify label text matches Active/Inactive exactly |
| P16 | Tab control | Demo Segmented — plain labels, no counts, no icons | Portal pills carry lucide icons + count chips (people.php:122-133) | Visual additions not in demo (harmless but not faithful) |

### Supersets (portal-only)
- `Add to freeze list` button + modal (NVG-BEA-005 W4 post-window freeze, people.php:190-193, 394-475) — deliberate BRD feature, keep.
- Customers tab: Actions column (view link) + row link to `user/view`.
- Classifications: separate Override Reason column with tooltip; `user-cog` overridden marker.
- Users 4th tab (flagged as gap P1 because it contradicts demo IA).

---

## 3. USERS & ROLES  *(largest gap of the four)*

Demo: `src/portals/admin/UsersRoles.tsx` (561 lines) — fully interactive user + role management.
Portal: `backend/views/users-roles/index.php` + `UsersRolesController.php` — **read-only catalogue by design** (deferral documented in the controller docblock, lines 20-24).

### Gaps

| # | Type | Demo | Portal | Detail |
|---|------|------|--------|--------|
| U1 | Interaction — role select | ROLE column is a per-row `<Select>` with optgroups **Platform** (Super Admin, Admin, Finance, Support) / **Shop** (Owner, Manager, Front Desk) → `assignRole` immediately (UsersRoles.tsx:111-135) | Static role badge; editing deferred to `/managers` editor ("Manage" link) | Core interaction missing |
| U2 | Role model | 7 built-in roles + custom roles, scoped platform/shop | 4 RBAC roles: administrator, manager, shopOwner, user (index.php:25-30) | No Finance / Support / Front Desk equivalents; no custom roles; no scope concept |
| U3 | Interaction — Deactivate/Reactivate | Row action `Deactivate` (red, confirm dialog `'Deactivate {{name}}?' / 'They can't log in until reactivated.'`) and `Reactivate` for inactive users; deactivated rows render name with line-through + `Deactivated` red badge | No row action; status shown as Active/Inactive dot; no strike-through | Missing action + missing `Deactivated` state styling |
| U4 | Interaction — Add user modal | Card-header `Add user` button → modal (Name / Email / Role select of platform roles) + hint `'The user is created without a password; they set one on first login…'`; success toast `'{{name}} added'` (UsersRoles.tsx:203-256) | Page-header `Add user` **link → /managers/create** (index.php:42-46) | No modal; different placement (page header vs Users card header) |
| U5 | Roles & Access tab | Master-detail **matrix editor**: left role list grouped Platform/Shop with per-role user counts + lock icon on built-ins; right = grouped permission checkbox matrix (PERMISSION_CATALOGUE), Super-admin locked keys, `Built-in`/`Custom` badge, subtitle `'{{scope}} role · {{count}} permissions · {{users}} user(s)'`, Rename / Delete actions (UsersRoles.tsx:262-453) | Static 2-col grid of role cards: badge + `{n} user(s)` + description + permission-name chips (index.php:117-147) | **Entire configurable access matrix missing** — the screen's headline feature (`subtitle: 'configurable access matrix'` is claimed but not delivered) |
| U6 | Create role modal | `New role` → modal with **BilingualField** (EN+AR role name, both mandatory: `missingEnName`/`missingArName`/`requiredHintPrefix`), scope select `'Platform (admin portal)'`/`'Shop (shop portal)'`, hint `'The role starts with no permissions…'` (UsersRoles.tsx:455-513) | Absent | Missing modal + bilingual name flow |
| U7 | Rename / Delete role | Rename modal (bilingual), Delete with in-use guard toast `'Role in use' / '{{name}} is assigned to users — reassign them first.'` + confirm | Absent | Missing |
| U8 | Copy — subtitle | `'Platform & shop logins · configurable access matrix'` (`admin.usersRoles.subtitle`) | `'Platform & staff logins · configurable access matrix'` (index.php:40) | "shop" → "staff" copy diff |
| U9 | Copy — status badge | `Deactivated` (red) / `Active` (teal) (`deactivatedBadge`/`activeBadge`) | `Active` / `Inactive` (index.php:94-96) | Copy diff |
| U10 | Data scope | Users list = ALL logins incl. shop-scope users (owner/manager/front-desk with SHOP column bound) | Only RBAC administrators + managers (controller:61-63); shop owners not listed | Rows the demo shows are absent |
| U11 | Tab control | Segmented pill control (rounded, bg-slate-100) | Underline `border-b` tabs (index.php:50-57) | Different tab visual language vs every other aurora admin page that uses pills |
| U12 | User cell | Avatar (initials) + name (UsersRoles.tsx:99-104) | Plain text name (index.php:81) | Missing avatar |
| U13 | Roles tab hint | — (demo needs no disclaimer) | Info banner "Roles below are platform-wide…" (index.php:118-121) | Portal-only copy admitting the deferral |

### Supersets (portal-only)
- `Permissions` count column on Users (menu-permission CSV count / "All" for admins, index.php:73,91).
- `Manage` per-row link into the existing Managers editor.
- Role card descriptions from RBAC `description`.

**Note:** the controller explicitly documents this as a deliberate deferral (read-only surface; live authorization graph never mutated from the new UI). The audit still records it as the parity gap it is — the demo subtitle promise ("configurable access matrix") is shown but not functional.

---

## 4. EVENTS

Demo: `src/portals/admin/Events.tsx` (283 lines) + `src/lib/eventLog.ts`.
Portal: `backend/views/timeline-event/audit.php` + `TimelineEventController::actionAudit/actionAuditCsv`.

### Gaps

| # | Type | Demo | Portal | Detail |
|---|------|------|--------|--------|
| E1 | Surface filter | **Segmented pills** `All \| admin \| shop \| customer \| specialist \| system` (Events.tsx:178-190) | `<select>` over distinct `timeline_event.application` values → **backend / frontend / console / …** (audit.php:57-63, controller `distinct('application')`) | Both the control (pills→select) and the **taxonomy** differ: demo surfaces are personas, portal surfaces are app tiers. Needs a mapping (backend→admin, frontend→shop, console→system, api→customer/specialist) or the pills read wrong |
| E2 | Shop filter | Shop combobox (`eventsLog.filters.shop`, options = shops seen in the log, `filters.allShops: 'All shops'`) (Events.tsx:120-126,194) | **Missing entirely** — no shop filter in `auditFilters()` | Missing filter |
| E3 | Actor filter | **Combobox of actor names** (resolved user names, `anyActor: 'Any actor'`) (Events.tsx:113-119) | Free-text numeric input labeled `'Actor (user id)'` (audit.php:74-76) | Admin must know raw user IDs; demo is a searchable name picker |
| E4 | Action filter | Combobox of distinct **full action ids** (`booking.created`, `auth.logged_in`, …) with placeholder `'Action'` | Select labeled `'Category'` over `category` column only (audit.php:65-71) | Coarser granularity (category vs category.event) + label copy diff (`Action` vs `Category`) |
| E5 | Target filter | Combobox of distinct target **types** found in the log (`anyTarget: 'Any target'`) | Hardcoded 2 options: Shop / User, matched via `LIKE '"shop_id"'` on JSON (audit.php:79-84, controller:133-137) | Demo targets include booking, invoice, subscription, etc. |
| E6 | Actor display | `'Muhannad Islambouli (Super Admin)'` — name **+ role in parentheses** (eventLog actorLabel) | `publicIdentity ?? username ?? email` — no role suffix (controller:184-190) | Missing role in actor label |
| E7 | Target display | `targetLabel` + small grey `targetType` (e.g. "Riyadh Charm *shop*") (Events.tsx:137-146) | Single string `public_identity` or `shop#123`/`user#45` (controller:192-194) | No two-part label/type rendering; raw `#id` fallbacks |
| E8 | Pagination summary | `'Showing {{from}}–{{to}} of {{total}}'` → "Showing 1–25 of 1084" (`eventsLog.showing`) | `'{n} events'` — total only, no from–to range (audit.php:171) | Copy + information gap |
| E9 | Pager control | `Prev` `1 / 44` `Next` buttons (Events.tsx:239-243, `common.prev`/`common.next`) | Yii `LinkPager` numbered page links (audit.php:189-195) | Different pager model |
| E10 | Default page size | **25** (useState(25); options 25/50/100) | **50** (`per_page` default 50, controller:54) — options match 25/50/100 | Default mismatch |
| E11 | Clear filters | Ghost button `'Clear filters' (n)` with active-filter count, shown only when filters active (Events.tsx:201-205) | Always-visible `X` icon reset link, no count (audit.php:97-99) | Interaction/copy diff |
| E12 | Row expand | Click anywhere on the **row** toggles the detail panel (`onRowClick`) | Dedicated chevron button in a leading column (audit.php:132-137) | Interaction diff (portal also spends an extra column) |
| E13 | Filter submit | Instant client-side filtering on every change | GET form + `Apply` button | Server-render constraint; no auto-submit JS added |
| E14 | Action badge tones | Action chip tinted by domain (booking=blue, payment=cyan, invoice=orange… Events.tsx:36-56); surface tones navy/blue/indigo/sky/slate | Action rendered as plain text (audit.php:141); surface tone switch keyed on backend/frontend/console | Missing action badge + tone system |
| E15 | CSV filename/source | `navagoo-events-<simdate>.csv` from **filtered rows** incl. all pages | ✅ same name pattern, filtered, 10k cap — parity OK; CSV lacks Payload column like demo (`eventsToCsv` check) | Verify demo CSV column list; portal exports 6 cols (no payload) |

### Parity confirmed (no gap)
- Title `Events` / subtitle `'Platform audit log — read-only.'` — exact match.
- `Export CSV` button copy + placement.
- Time format `d M, H:i:s` → "19 May, 09:39:13" matches demo `dateTimeSec`.
- Columns TIME · SURFACE · ACTION · ACTOR · TARGET · MESSAGE match.
- Empty state `'No events match these filters.'` exact match.
- `'Rows per page'` copy + 25/50/100 options.
- Expanded detail layout (Actor/Target/Timestamp + Payload `<pre>`, gradient bg) is a faithful port.
- Free-text search placeholder `'Search id, name, or message…'` exact match.

### Supersets (portal-only)
- 10,000-row cap on CSV export (sane guard).
- `per_page` preserved as hidden input across filter submits.

---

## Cross-cutting notes

1. **`Gender – الجنس` hardcoded bilingual string** (people.php:144) violates the project's mandatory bilingual rule — Arabic belongs in `common/messages/ar/backend.php`, not inline in the source string.
2. Demo screens do all filtering client-side/instant; all four portal screens are GET-form + submit. Where fidelity matters (Bookings status pills, Events surface pills), pills can be links that self-submit — no SPA needed.
3. Portal supersets (finance columns on Bookings, freeze-list modal on People, permission counts on Users) are deliberate BRD features — do not remove when closing gaps.
4. Users & Roles interactivity is a **documented deferral** (`UsersRolesController` docblock) tied to not mutating the live RBAC graph from a new UI — closing U1/U3–U7 is a product decision, not just a UI task.
