# Admin · Catalogue — Logic & Flows

Scope note: the **admin** Catalogue is narrowly a *global service-category taxonomy
manager*. The demo screen (`portals/admin/Catalogue.tsx`) does **not** create or edit
individual services, bundles or packages — those are authored per-shop in the shop
portal. The admin owns only the shared category tree. This doc compares that screen.

## Demo (canonical)

Source: `/private/tmp/Navagoo_MI_dev/navagoo-app/src/portals/admin/Catalogue.tsx`
Store: `src/store/store.ts:236-238` (action types), `:1789-1808` (implementations)
Type: `src/types.ts:161-165` (`ServiceCategory { id, name, parentId? }`)
Tests: `src/store/catalogue.test.ts`

### Data shape
- `ServiceCategory = { id, name, parentId? }` — **one level of nesting only**
  (`types.ts:161`). A category with no `parentId` is a *group*; a category whose
  `parentId` points at a group is a *child*. The store comment is explicit:
  "Global category taxonomy (one level of nesting)" (`store.ts:1788`).
- A `Service` references a category by `categoryId` (source of truth) with a legacy
  `category` string fallback for un-migrated rows (`types.ts:224-225`).

### Row composition / ordering (`Catalogue.tsx:34-61`)
1. `counts` (`:34-46`): per-category-id map of `{ services, shops:Set }`. For each
   service, resolve its category id as `sv.categoryId ?? byName.get(sv.category)`
   (legacy-name fallback), increment service count, add `sv.shopId` to the shop set.
2. `rows` (`:49-61`): list groups (no `parentId`) in array order; after each group,
   emit its children (`parentId === group.id`). Orphans (child whose parent is gone)
   are effectively not shown because iteration is group-driven. Each row carries
   `services` count and `shops` (distinct shop count).

### Add / edit flow (`:65-86`, modal `:139-171`)
- `openAdd`: clear `name`/`parentId`, open modal in "add" mode.
- `openEdit(cat)`: prefill `name` and `parentId`.
- Parent `<Select>` lists only groups, **excluding the category being edited**
  (`:162` `g.id !== editing?.id`) so a group can't nest under itself.
- Save: `addCategory(name.trim(), parentId || undefined)` or
  `updateCategory(id,{name:name.trim(), parentId: parentId||undefined})`, then a toast.
- Save button disabled while `!name.trim()` (`:148`).

### Delete flow (`:87-91`, confirm modal `:174-198`)
- `deleteCategory(id)` then an info toast.
- Confirm copy warns, **only when deleting a group that has children**, that nested
  categories will also be removed (`:192-194`), and always that "Services in it become
  uncategorised."

### Store semantics (`store.ts:1789-1808`)
- `addCategory`: appends `{ id: uid('CAT'), name, parentId }`.
- `updateCategory`: shallow patch by id.
- `deleteCategory`: **cascade** — removes the id + all its children; for every service
  whose `categoryId` is in the removed set, clears `categoryId` (the legacy `category`
  string stays so the service shows an "uncategorised/legacy" label). Verified by
  `catalogue.test.ts:56-72`.

## Ours (Yii2 backend)

Controller: `backend/controllers/ShopCategoryController.php`
Model: `common/models/ShopCategory.php` + `common/models/base/ShopCategory.php`
Views: `backend/views/shop-category/{index,_form,view,create,update}.php`
Table: `shop_category` (`common/migrations/db/m231012_180650_add_category_service_table.php:22-32`)

### Data shape
- `shop_category` columns: `id, name, created_at, updated_at, created_by, updated_by,
  image_path, image_base_url` + `sort_order` (added later). **No `parent_id`.** The
  table is strictly **flat** — there is no group/child concept at all.
- Bilingual name via `MultiLanguageBehavior` (`base/ShopCategory.php` uses
  `MultiLanguageTrait`); the English value comes from `translationsWithText`
  (`index.php:165`).
- Services↔category and shops↔category are many-to-many relations
  (`relationNames()` → `services`, `shops`; `shop_category_assignment`,
  `service_category_assignment` join tables).

### CRUD flow
- `actionIndex` (`ShopCategoryController.php:76-87`): `ShopCategorySearch` + paged
  `ActiveDataProvider`, name filter.
- `actionCreate`/`actionUpdate` (`:117-183`): `loadAll`/`saveAll` (mootensai relation
  trait) — supports tabular sub-forms for related services & shops
  (`_formService`, `_formShop`, `actionAddService`/`actionAddShop` `:353-383`).
- `actionDelete` (`:191-196`): `deleteWithRelated()`.
- Ordering: `actionMoveUp`/`actionMoveDown`/`actionSaveOrder` (`:206-266`) +
  `swapWithNeighbour` (`:275-327`) — drag-and-drop + arrow reorder persisting
  `sort_order`. **This whole reorder system has no analogue in the demo.**

### Counts
- Service & shop relations exist and are shown on the **detail** page
  (`view.php:88`, `:122` render related services/shops), but there is **no inline
  per-row `N svc / N shops` count** in the list like the demo.

## Key logic gaps (ours vs demo)
1. **No nesting.** Demo's core model is groups + one level of children; ours is flat.
   Missing: `parent_id` column, parent-group select, grouped row rendering,
   self-nest exclusion.
2. **No cascade-on-delete of children** (no children to cascade) and **no
   uncategorise-services side effect** documented/implemented on delete — ours uses
   `deleteWithRelated()` whose behaviour on the assignment tables is not the demo's
   "clear categoryId, keep legacy string" semantic.
3. **No inline svc/shop counts** in the list.
4. Ours **adds** features absent in the demo: image upload, sort_order + drag/arrow
   reordering, bilingual name, tabular service/shop assignment editor, pagination,
   PDF/detail view.
