# Admin · People — Logic & Flows

Canonical target: React demo `src/portals/admin/People.tsx` (read-only tables + one
classification-override modal). Our side spreads the same concerns across several Yii2
backend controllers/models.

## Demo overview

`AdminPeople` (`People.tsx:17`) is a single page with a 4-way `Segmented` switch
(`People.tsx:28`): **Customers · Classifications · Specialists · Users**. Each tab is a
read-only `DataTable` reading directly from the Zustand store. Only the Classifications tab
has a mutation (admin override).

### 1. Customers tab — `CustomersView` (`People.tsx:48`)
- Source: `state.customers` (`store.ts` `customers: Customer[]`; shape `types.ts:268`).
- Columns: avatar+name, mobile (`id-mono`), gender (`titleCase`), **Bookings count** =
  `state.bookings.filter(b => b.customerId === c.id).length` (`People.tsx:69`), **Shops count**
  = distinct `shopId` over that customer's bookings (`People.tsx:76`). Both are derived live
  from the bookings array — no stored counters.

### 2. Classifications tab — `ClassificationsView` (`People.tsx:88`)
- Source: `state.classifications` (`CustomerClassification[]`, shape `types.ts:282`).
- One row per **(customerMobile, shopId)**: customer name (resolved by mobile lookup,
  `People.tsx:90`), shop tile (`shopById`), `ClassificationBadge`, reason badge, set-on date,
  and an **Override** button opening `OverrideModal`.
- Header copy states the rule: *"Immutable once set — admin override is logged with a
  mandatory reason"* (`People.tsx:140`).
- **Override flow** (`OverrideModal`, `People.tsx:152` + `store.ts:1717 overrideClassification`):
  1. Admin picks a new classification (`shop_owned` | `navagoo_sourced`) and types a
     **mandatory reason** (Apply button disabled until reason non-empty, `People.tsx:207`).
  2. Store upserts the classification row, stamps `overriddenBy:'Navagoo Admin'` +
     `overrideReason` (`store.ts:1722`).
  3. **Charges are re-derived**: every booking for that customer at that shop is run through
     `rederiveCharges` (`store.ts:1750`) so marketing-fee charges appear/disappear to match
     the new classification.
  4. An activity-log entry is appended (`store.ts`, message `Classification override · …`).
  5. Toast: "Classification overridden … charges re-derived" (`People.tsx:172`).
- Marketing-fee derivation it feeds: `marketingFeeDraft` only emits a fee when
  `classification === 'navagoo_sourced'` (`lib/finance.ts:132`); `classificationOf` defaults
  to `shop_owned` when no row exists (`store.ts:351`).

### 3. Specialists tab — `SpecialistsView` (`People.tsx:218`)
- Source: `state.specialists` (`Specialist[]`, shape `types.ts:135`).
- Columns: avatar+name, shop (`shopById().commercialName`), title, **wage type** badge
  (`compensation.wageType` ∈ fixed|commission|both, `types.ts:107`), status badge
  (active|inactive). Read-only here (specialist CRUD lives elsewhere in the demo Team page).

### 4. Users tab — `UsersView` (`People.tsx:289`)
- Source: `state.users` (`User[]`, shape `types.ts:568`).
- Columns: avatar+name, email, **role** badge with role→tone map
  (admin=purple, owner=teal, manager=cyan, support=slate; `People.tsx:307`), shop
  (`commercialName` or literal **"Platform"** when `shopId` is undefined, `People.tsx:330`),
  last-active date. Role enum: `owner|manager|admin|support` (`types.ts:567`). Read-only.

## Our implementation — how the same data is computed

There is **no single "People" page**. Concerns are split across three controllers, each its
own admin grid, all reskinned to the Tailwind "aurora" layout (`$this->layout='tailwind'`):

| Demo tab | Our controller / view | Notes |
|---|---|---|
| Customers | `backend/controllers/UserController.php:65` `actionIndex` → `backend/views/user/index.php` | Filters `user_type=USER_TYPE_CUSTOMER` (`User.php:75`). |
| Classifications | **none (admin)** | No admin override surface. Classification ≈ shop-side `CustomerFreeze` exemption, managed in **frontend** `frontend/controllers/CustomersController.php:50`. |
| Specialists | `backend/controllers/AgentController.php:58` `actionIndex` → `backend/views/agent/index.php` | Filters `user_type=USER_TYPE_AGENT` (`User.php:76`). |
| Users (platform/managers) | `backend/controllers/ManagersController.php:45` `actionIndex` → `backend/views/managers/index.php` | RBAC roles via `Yii::$app->authManager`. |

### Customers (our `UserController` + `UserSearch`)
- `UserSearch` (`backend/models/search/UserSearch.php:49`) queries
  `user_type IN (CUSTOMER, MANAGER)` joined to `rbac_auth_assignment`, filtered by
  `user_role` session value ("user"). Grid columns (`user/index.php:120`): formatted_id,
  full_name, email, mobile, gender (from `userProfile`), Mode (demo/live), status, timestamps.
- **Bookings / Shops counts are NOT shown** — the demo's two derived count columns have no
  analog. Our grid instead adds Mode and Status columns the demo lacks.

### Specialists (our `AgentController`)
- `agent/index.php` columns: formatted_id, full_name, gender, mobile, email, shop name,
  Mode, status, timestamps. **No wage-type column** (compensation lives in agent
  profile/services, not surfaced here). Has status + demo toggles the demo lacks.

### Users / managers (our `ManagersController`)
- Full CRUD: `actionCreate` (`:68`), `actionUpdate` (`:111`), `actionView` (`:151`),
  `actionDelete` (`:205`). Role list from `authManager->getRoles()` (`:106`).
- Role assignment via `ManagerForm::save()` — `authManager->revokeAll()` then stores roles as
  a CSV string on `user.roles` (`backend/models/ManagerForm.php:152-164`); the live
  `$auth->assign(...)` calls are **commented out** (`ManagerForm.php:149,169`), so RBAC
  auth_assignment rows may not actually be written. Roles enum (`User.php:84`):
  `user|manager|administrator|shopOwner` — **different vocabulary** from the demo
  (`owner|manager|admin|support`).

### Classification override — MISSING on our admin
- No `overrideClassification` analog anywhere in `backend/`. Grep for
  `navagoo_sourced|shop_owned|overrideClassification` across backend = **0 hits**.
- The closest concept: shop-managed **freeze list** (`common/models/CustomerFreeze.php`)
  exempting a mobile from the marketing fee, surfaced in the **shop portal**
  (`frontend/controllers/CustomersController.php` actions `freeze`/`unfreeze`, `:109/:135`),
  NOT in admin. There is no admin-logged, reason-mandatory, charge-re-deriving override.
- Marketing-fee re-derivation on classification change (demo `rederiveCharges`) has **no
  admin trigger** on our side.

## Status snapshot
- Customers list: partial (no bookings/shops count columns; extra Mode/Status).
- Classifications tab + override: **missing** on admin.
- Specialists list: partial (no wage-type/compensation column).
- Users list: partial (role vocab differs; RBAC assign appears disabled/commented).
