# Admin · Geography — Parity Matrix

Demo (canonical): `portals/admin/Geography.tsx` (+ `types.ts:559`, `store/seed.ts:418`)
Ours: `backend/controllers/{City,District,Government}Controller.php`, `backend/views/{city,district,government}/*`, `common/models/base/{City,District}.php`

| Demo behavior | Demo ref | Our ref | Status | Note |
|---|---|---|---|---|
| Single consolidated "Geography" page (cities + districts together) | Geography.tsx:20-21 | — | missing | Ours is 3 separate CRUD screens (city/district/government) |
| Responsive card grid (1/2/3 cols) of cities | Geography.tsx:22 | backend/views/city/index.php:67-110 | missing | Ours is a flat table, not cards |
| City name + MapPin icon tile | Geography.tsx:30-35 | city/index.php:84 | partial | Name shown as link; no icon/card styling |
| Per-city shop count ("{n} shops") | Geography.tsx:26,36 | — (FK exists Shop.php:468) | missing | Derivable via shop.city FK; not surfaced anywhere |
| Singular/plural "shop"/"shops" | Geography.tsx:36 | — | missing | N/A until count surfaced |
| Per-city district count ("{m} districts") | Geography.tsx:36 | — (District.city_id, base/District.php:21) | missing | Derivable; not surfaced on city UI |
| District chips/badges under each city | Geography.tsx:48-54 | — | missing | Districts only on their own list page |
| City `active` boolean field | types.ts:563; seed.ts:434 | — | missing | No active/status column on `city` table (base/City.php:43-50) |
| Toggle city active/inactive | Geography.tsx:39-46 | — | missing | No toggle, no endpoint, no field |
| Toast on toggle ("<city> enabled/disabled") | Geography.tsx:42-45 | — | missing | CRUD flash exists but no toggle |
| Admin-only scoping | portals/admin/ | GovernmentController.php:21-22 | partial | Government guards + permission; City/District rely on BackendController default — confirm |
| Create / edit / delete city | — (none in demo) | CityController.php:64-141 | done | Ours exceeds demo |
| Create / edit / delete district | — (none in demo) | DistrictController.php:64-141 | done | Ours exceeds demo |
| Name search filter (city) | — | city/index.php:46-64 | done | Ours exceeds demo |
| Name + city filter (district) | — | district/index.php:53-72 | done | Ours exceeds demo |
| Pagination + summary | — | city/index.php:113-124 | done | Ours exceeds demo |
| Government / region tier | — (absent in demo) | GovernmentController.php; views/government/* | done | NEW on our side; demo has no region tier |
| District SEO fields (slug/meta/direction/region) | — | base/District.php:48-51 | done | NEW on our side |

## Summary
The demo's Geography is a lightweight **read + active-toggle card dashboard with derived shop/district counts**. Our implementation is heavier CRUD (city/district/government) but is **missing the demo's entire interaction model**: the consolidated card grid, the `active` flag (no DB column), the toggle + toast, and the live shop/district counts. CRUD, search, pagination, and the government tier are all things we have and the demo does not.

Core demo behaviors (9 rows): 0 done, 1 partial (admin scoping), 8 missing.

**Area score: 25%**

## Verified verdict (adversarial)

Re-audited every "done" and "partial" claim by reading both the demo and our actual code. Key evidence:

- `backend/controllers/BackendController.php` has **no `beforeAction`** — only `init()` (language) and `actions()` (error). So City/District controllers carry **no controller-level guard** of their own (unlike `GovernmentController.php:18-23`, which adds a guest guard + `checkPermmissions('government')`).
- Guests are nonetheless blocked at the **app level** by `backend/config/web.php:76` `'as globalAccess'` (`common\behaviors\GlobalAccessBehavior.php`), whose catch-all rule (`web.php:111-114`) is `['allow'=>true,'roles'=>['manager','administrator','shopOwner']]`.
- That catch-all grants **shopOwner** (a salon owner, not an admin) access to the admin City/District CRUD. The demo's Geography lives in the admin portal only. City/District have **no granular permission gate** (Government does). → over-grant, not admin-only.

| Demo behavior | Analyst status | Verified status | Evidence |
|---|---|---|---|
| Admin-only permission scoping | partial (note: "confirm BackendController default enforces it") | **partial — note corrected** | `BackendController.php` has no beforeAction; guard is app-level `globalAccess` (web.php:76). Guests blocked, but catch-all (web.php:111-114) also allows `shopOwner`; City/District have no granular `checkPermmissions` gate like Government (GovernmentController.php:21). Over-grant, not admin-only. |
| Create / edit / delete city | done ("exceeds demo") | **done — but not parity** | CityController.php:64-141 real CRUD. Demo has no city CRUD; this is extra surface, contributes nothing to demo parity. |
| Create / edit / delete district | done ("exceeds demo") | **done — but not parity** | DistrictController.php:64-141. Same: extra, not a demo behavior. |
| Name search filter (city) | done | **done — but not parity** | city/index.php:46-64 + CitySearch (note: lives at `backend/models/CitySearch.php`, namespace `app\models`, not `app/models/`). Demo has none. |
| Name + city filter (district) | done | **done — but not parity** | district/index.php:53-72 + DistrictSearch. Demo has none. |
| Pagination + summary | done | **done — but not parity** | city/index.php:113-124 LinkPager + getDataProviderSummary. Demo has none. |

### Overclaim downgrades / corrections
- **Admin-only scoping**: status stays *partial*, but the analyst's note ("confirm BackendController default enforces it") is **wrong** — BackendController enforces nothing; the real gate is app-level globalAccess and it **over-grants to shopOwner**. Functionally weaker than the demo's admin-only portal.
- **The five "done" rows are all "exceeds demo" extras**, not demo-parity matches. They were (correctly) excluded from the 9-row demo-behavior score, but the analyst's headline 25% is not derivable from the 9 demo rows.

### Score recomputation
Demo core behaviors = 9 rows. Verified: **0 done, 1 partial (~0.5), 8 missing** → 0.5 / 9 ≈ **5.6%**. The partial is itself a weakening over-grant. The analyst's **25% is inflated** (counted no demo behavior as met). The "done" extras do not raise parity *to the demo* because the demo lacks those features entirely.

**Adjusted area score: 6%** (was 25%).
