# Admin Section / Category Organisation + Sub-Category Removal

- **Date:** 2026-08-03
- **Status:** In Progress — shell shipped and tested; Phase 2 and Phase 0b outstanding
- **Status Date:** 2026-08-04
- **Phases:** 5
- **Phases Complete:** 2 of 5 — *Phase 1 (absorbed into 3) and Phase 3 ✅ tested by Dave
  2026-08-03/04 and committed (`43ed6fe` Admin Layout Update). Phase 0 is half done: 0a
  decided, 0b not started. Phase 2 not started. Phase 4 stays conditional on 0b.*
- **Notes:** Promotes the backlog item *Admin fields page: rethink section/category
  organisation UI*, and absorbs the *"Main Details" is a hardcoded tab* item found while
  testing the icon picker on 2026-08-03. Sibling plan:
  `docs/2026-08-03-admin-status-management.md`.
  **Phase 0 is a design gate.** The organisation UI and the underlying data-model question
  are genuinely open, so competing options get built in `www/test.php` with a switcher and
  chosen from, rather than settled in prose here. Phases 3–4 stay deliberately thin until
  that gate produces an answer.
  **Scope change 2026-08-03 (Dave):** sub-categories are **hidden, not removed** — see
  Phase 1. **Phase 0 split:** the gate now runs in two parts. Part 1 (the admin *shell* —
  where module configuration lives) is built; part 2 (the section/category *inner* UI and
  its data-model fork) has not started, and is deliberately separate so the two decisions
  aren't taken from one picture.

## Plan Phases

0. Prototype the admin organisation in `www/test.php` (design gate) — **0a ✅ decided:
   left rail, build order; 0b section/category inner UI + data-model fork outstanding**
1. ✅ Hide sub-categories (admin UI only — backend stays) — *delivered inside Phase 3*
2. "Main Details" — stop hardcoding it, render it from the meta like every other section
   — **not started; the riskiest thing left in this plan**
3. ✅ Build the chosen shell (left rail, build order), with dashboard config moved in
4. Data-model change, only if Phase 0b selects reusable categories

## Problem

### 1. The admin view of sections and categories is confusing

Each module's admin has four tabs: Fields | Sections | Categories | Sub Categories.
Categories are stored **per section**, so reusable names repeat — wayleave has a
`Main Details` plus three separate `General` rows under Commercial, Legal and Delivery.
On the actual editor page this reads fine; in a flat admin table it is baffling, because
nothing shows which section each `General` belongs to.

### 2. Sub-categories are dead weight — but not *quite* unreferenced

The backlog said they are referenced only by `admin_fields.js`. Verified 2026-08-03, and
that is **not quite right**:

- No editor renders them — confirmed across `project_edit_v2.js`, `account_edit_v2.js`,
  `stocklist_edit.js`, `wayleave_edit_v2.js`.
- `get_update_form.php` never serves them.
- `global_functions.php` mentions them only in commented-out lines (`:645`, `:671`, `:696`).
- **But `wayleave_load.php:66-72` joins `agreement_field_sub_category`, returns an unused
  `sub_category_name`, and — the bit that matters — *orders fields by it*:**
  ```sql
  ORDER BY s.field_section_display_order, c.field_category_display_order,
           sc.field_sub_category_id, f.field_display_order
  ```
  Dropping the table without fixing that query changes wayleave's field order.

So removal is safe, but it is not a pure delete.

### 3. "Main Details" is hardcoded, so admin cannot reach it

Every other section tab is rendered from `*_field_sections` by the editors' loop. Main
Details is static markup in all four editor templates — `html_body_projectedit.php:84`,
`html_body_account_edit_v2.php:84`, `html_body_stocklistedit_v2.php:75`,
`html_body_wayleaveedit.php:34`. Its icon, label and position are unreachable from admin;
changing its icon there appears to do nothing, which is how this was found.

It also **uses a different icon library** — `<i class="bi bi-card-list">`, Bootstrap Icons,
while every meta-driven tab now carries FontAwesome. Both libraries load (`index.php:202`
and `:206`), so the editor page currently mixes two icon sets in one tab strip.

The same templates still carry the original section vocabulary as commented-out tabs
(`html_body_projectedit.php:72-99`): Map, Main Details, Planning, Wayleave, Build,
Governance, Other. Useful as intent, and the source of the icon groupings chosen in the
previous plan.

## Build

### Phase 0 — Prototype the organisation UI (design gate, no production code)

Split into two gates 2026-08-03, because they are two decisions and one picture would force
them to be taken together.

#### Phase 0a — the shell: where module configuration lives ✅ **decided 2026-08-03**

> **DECISION — option B (left rail), build order.** Chosen by Dave 2026-08-03 from the
> `www/test.php` prototype, whose defaults now open on it.
>
> - **Left rail**, not tabs. The deciding factor is that a missing area is a *main* case,
>   not an edge one: wayleave has no Statuses and no Dashboard, which is one module in four,
>   and the rail has room to say *why* where a greyed tab does not. It also keeps both axes
>   visible and absorbs a sixth area later for the cost of one row.
> - **Build order** — Sections, Categories, Fields, Statuses, Dashboard. Containers before
>   contents, then the lists that fields point at.
> - **Module Management**, replacing Field Management, with dashboard config moving in from
>   its own top-level page.
> - **Admin home becomes a module × area matrix**, which is what pays back the cross-module
>   glance that today's `admin_dashboard` gives for free.

The options as prototyped, kept for the record:

In `www/test.php`, switchers for design / page / scenario / area order.

**The problem it frames.** Admin has two axes — config *area* and *module* — and is
inconsistent about which is primary. Field Management is module-first then area (five tabs);
`admin_dashboard` is area-first with every module stacked on one page; Users, Map Layers and
Distance Analysis have no module axis at all. So "configure the projects module" is answered
in two different shapes depending on which part of it you want.

All three options agree on Dave's two structural calls — module comes first, and dashboard
config moves in beside the rest — and differ only in the container:

- **A — tabs.** The proposal as put: `Field Management` → `Module Management`, module in the
  nav dropdown, five tabs across.
- **B — left rail.** Same content, areas down a rail with counts, state and a line of
  explanation each; module as a segmented control above.
- **C — one page.** No container: every area stacked on one scrolling page behind a sticky
  anchor rail.

**Scenarios**, which is where they separate — all three look identical on a healthy Projects
module: *Wayleave* (no Statuses, no Dashboard config — two of five areas genuinely absent),
*Heavy* (120 fields, 40 categories, long names), *Fresh* (nothing configured yet).

**The `Page` switcher carries the one real cost of the move.** Today's `admin_dashboard`
lists every module together, so "which modules are unconfigured?" is one glance; split
per-module, that glance is gone. Each option's Admin home answers it with a module × area
matrix, which gives the same view back for *every* area rather than just the dashboard one.

**The `Order` switcher** offers Dave's order, build order and frequency order. It matters
least in A and most in C, where it stops being a tab sequence and becomes the reading order
of the page.

**Availability is a main case, not an edge case.** Wayleave lacks Statuses and Dashboard
because both derive from `dashboardModuleMap()`, which has no wayleave entry — that is one
module in four. Whichever option wins, *the reason has to be on screen*: an unexplained
disabled tab reads as a fault and will be reported as one.

#### Phase 0b — the section/category inner UI + data-model fork *(not started)*

Deliberately held back so the shell can be chosen on its own. All three shell options show
today's section and category tables unchanged.

**Option A — side-by-side managers + hierarchy tree.** Compact section and category
managers, with a drag-and-drop tree beneath for arranging `Section › Category`, and
possibly fields as a third level.

**Option B — section-centric accordion.** One expandable block per section, its categories
listed and reorderable inside it, fields optionally nested. Removes the "which section does
this `General` belong to" problem by construction, since categories are never shown
detached from their parent.

**The data-model fork, which this gate must answer:** does a category get recreated per
section (today's model), or become a reusable entity linked to sections? This changes the
`*_field_category` shape and the admin UX, and the editor rendering contract
(`get_update_form.php` → section/category loops) has to keep working either way. Option B
suits the current model; Option A leans toward reusable categories.

The repeated-`General` problem is visible in the 0a prototype under the Wayleave scenario,
flagged but not solved.

Record both decisions back into this doc before Phase 3 starts.

### Phase 1 — Hide sub-categories

**Changed 2026-08-03 (Dave): hide, don't remove.** They come out of the admin UI; the
column, the four tables and the save-endpoint modes stay exactly where they are, in case
sub-categories turn out to be wanted later. Nothing is dropped, so nothing is irreversible.

- **Admin UI:** hide the Sub Categories tab and its pane, and the Sub Category select in the
  field modal. Leave `sub_cats_list` / `sub_cat_add` / `sub_cat_update` / `sub_cat_delete`
  in `admin_save.php` and `admin_load.php` untouched — unreachable, not deleted.
- **`field_sub_category` keeps being written** as whatever it already holds. Removing it
  from `collectFieldParams` / `bindFieldParams` would null the column on the next save of
  every field, which is a data change, and the whole point of hiding rather than removing is
  to avoid one. Leave the plumbing; just don't render the control.
- **No migration.** `db/023_drop_field_sub_categories.sql` is *not* written. The next
  migration number stays free for whatever needs it.
- **`wayleave_load.php:66-72` stays as it is.** It joins `agreement_field_sub_category` and
  orders fields by it. That was urgent only because the table was going to disappear
  underneath it; since nothing is being dropped, the query keeps working and the wayleave
  field order is unchanged. It becomes a live issue again if the tables are ever actually
  deleted — recorded here so that link isn't lost.
- **The pre-flight query is deferred with the deletion**, not run now. Kept below for
  whenever that day comes:
  ```sql
  SELECT 'projects' AS m, count(*) FILTER (WHERE field_sub_category IS NOT NULL) AS fields_using,
         (SELECT count(*) FROM projects.project_field_sub_category) AS rows_defined
  FROM projects.project_fields
  UNION ALL SELECT 'accounts', count(*) FILTER (WHERE field_sub_category IS NOT NULL),
         (SELECT count(*) FROM accounts.account_field_sub_category)
  FROM accounts.account_fields
  UNION ALL SELECT 'stocklists', count(*) FILTER (WHERE field_sub_category IS NOT NULL),
         (SELECT count(*) FROM stocklists.stocklist_field_sub_category)
  FROM stocklists.stocklist_fields
  UNION ALL SELECT 'wayleave', count(*) FILTER (WHERE field_sub_category IS NOT NULL),
         (SELECT count(*) FROM wayleave.agreement_field_sub_category)
  FROM wayleave.agreement_fields;
  ```

### Phase 2 — "Main Details" rendered from the meta

- Seed a `Main Details` row into each of the four `*_field_sections` tables (migration),
  with `field_section_ref = 'main_details'` to match the existing hardcoded ids, a
  FontAwesome icon from the picker's set, and display order 1.
- Point the existing static fields at it — most already sit in a Main Details section from
  the `db/012`–`015` seeds, so this is mostly confirming rather than repointing.
- **Delete the hardcoded tab *and* its pane together** in all four templates. The editors
  skip a section whose `#nav-{ref}` already exists in the DOM, so removing only the tab
  would leave the pane orphaned, and removing neither renders it twice.
- Resolves the Bootstrap-Icons-vs-FontAwesome mix in the tab strip as a side effect.

### Phase 3 — Build the chosen shell (option B, build order)

**Phase 1 folds into this.** Rebuilding the shell means the Sub Categories tab is simply not
carried across — hiding it separately first would be editing markup this phase replaces.
The field-modal select still needs its own hide, and the `field_sub_category` plumbing still
stays (see Phase 1).

Constant regardless: the editor rendering contract must not change, `get_update_form.php`
keeps serving sections and categories in display order, and drag-reorder keeps writing
through the existing `sections_reorder` / `categories_reorder` modes.

- **`nav_admin.php`** — `Field Management` → `Module Management`; drop the top-level
  `Dashboard` entry; the module dropdown loses the per-item "Fields" suffix.
- **`html_body_admin_fields.php`** — tab strip becomes the rail + pane split, areas in build
  order, module segmented control above. Each rail row carries its count; Dashboard carries
  *set / not set* rather than a count.
- **Unavailable areas state their reason in the pane** — not a bare disabled row. Wayleave's
  Statuses and Dashboard both trace to having no `dashboardModuleMap()` entry, and that
  sentence goes on screen.
- **Dashboard config moves in.** Rather than merging `admin_dashboard.js` into
  `admin_fields.js`, load it on this route too and have it render **only the current
  module's** card into the pane — it already builds per-module cards from the
  `dashboard_config` mode, so this is a filter plus a mount point, not a rewrite. Keep the
  `admin_dashboard` route for a release so existing links resolve.
- **`admin.css`** — rail styles. The prototype's `.ax-rail` / `.ax-split` are the reference.
- **Admin home** (`html_body_admin_home.php`) — link cards become the module × area matrix,
  with the non-module areas (Users, Map Layers, Distance Analysis) below it.

#### Build notes — Phase 3 ✅ *(tested by Dave 2026-08-03/04, committed `43ed6fe`)*

**The rail is Bootstrap pills, not hand-rolled buttons.** `shown.bs.tab` fires for pills as
well as tabs, so the lazy-load and add-button wiring in `admin_fields.js` needed no changes
at all — the strip became a rail and the event contract stayed put. Bootstrap 5.2.3 resolves
a tab's container with `closest('.list-group, .nav, [role="tablist"]')`, so the rail carries
`role="tablist"` and needs no `.nav` class (which would have brought `display:flex` and laid
it out horizontally).

**Fields is listed third but selected first.** Build order is how a module is *built*; the
default area is what people actually come for. Those are different questions and answering
them the same way would have made the common case cost a click.

**The rail's counts are preloaded** from the new `module_overview` mode. Each area still sets
its own count when it loads, but areas load lazily — without the preload the rail advertises
four numbers and shows one until you have clicked through everything. That was tolerable on a
tab strip where counts were incidental; on a rail they are half the reason it is a rail.
`module_overview` deliberately counts **all** fields rather than only active ones, matching
`fields_list`, so the preloaded number is replaced by an identical one rather than flickering.

**`admin_dashboard.js` serves two hosts and tells them apart by `data-module` on `#main`**,
which only the module page sets. Embedded it filters to that module and drops the card
chrome (the header would repeat the page title, and the border would draw a box inside the
pane's own box); standalone it renders every module exactly as before. This was preferred to
merging it into `admin_fields.js`: the difference between the two hosts really is a filter
and a mount point, and a merge would have made one screen out of two that are still both
wanted.

**The Save button is owned outright by `admin_dashboard.js`** rather than added to
`admin_fields.js`'s `tabAddButtons` map. Two files toggling one element is how it ends up
visible on the wrong screen — so the map has no dashboard entry, and that file shows and
hides the button from its own `shown.bs.tab` handler.

**Unavailable areas explain themselves in the pane.** Both Statuses and Dashboard trace to
the same missing `dashboardModuleMap()` entry for wayleave, and both now say so. The
statuses pane also hides its "Drag rows to reorder" hint when unavailable, which otherwise
sat above "not available for this module".

**Switching module keeps your area** *(added 2026-08-04 after testing)*. The first cut always
landed on Fields, because the module control navigates rather than swapping in place and the
area was not on the URL. Defaulting to Fields *on arrival* is deliberate; losing your place
when you switch module was not. The module links now carry `&area=` and are kept in step as
you move around the rail, reusing the deep-link support the matrix already needed.

**Sub Categories, per the hide-don't-remove decision:** the rail has no entry, the field
modal's select is `d-none` but still present, and the button and modal stay in the DOM
unreachable — `admin_fields.js` binds listeners to both at init and would throw on a missing
element, taking the whole page down with it.

### Phase 4 — Data-model change (conditional)

Only if Phase 0 selects reusable categories: a join table between categories and sections,
a migration to de-duplicate the repeated names (wayleave's three `General` rows collapse to
one), and `get_update_form.php` updated to resolve categories per section. Skipped entirely
if the prototype keeps the current model.

## Testing checklist

Phase 3 ✅ *(tested by Dave 2026-08-03/04, committed `43ed6fe`)*

- [X] Rail renders in build order; Fields is the area selected on load *(Dave 2026-08-03)*
- [X] Every area still loads, adds, edits, reorders and deletes as it did *(Dave 2026-08-03)*
- [X] Switching module keeps the current area rather than dropping back to Fields
      *(gap found by Dave 2026-08-03, fixed and confirmed 2026-08-04)*
- [X] Rail counts are populated on arrival, before any area has been opened *(Dave 2026-08-03)*
- [X] Counts do not change when an area is then opened *(Dave 2026-08-03)*
- [X] Sub Categories area and field-modal select gone; adding and editing fields still works
      in all four modules *(Dave 2026-08-03)*
- [X] Editing a field twice does not null its `field_sub_category` (the plumbing stays)
      *(Dave 2026-08-03)*
- [X] Wayleave field order unchanged — *nothing to verify by construction:
      `wayleave_load.php` was never edited, and the sub-category tables it joins and orders
      by are still there because the decision was to hide rather than remove. This item
      existed for the original "drop the tables" plan and stops applying with it.*
- [X] Wayleave: Statuses and Dashboard both explain why they are unavailable, and neither
      offers an Add or Save button *(Dave 2026-08-03)*
- [X] Dashboard area saves, and the value is the one the dashboard then uses
      *(Dave 2026-08-03 — surfaced a separate dashboard counting issue, logged in
      `improvement-opportunities.md`, not caused by this change)*
- [X] `?do=admin_dashboard` still loads, shows its moved notice, and still saves
      *(Dave 2026-08-03)*
- [X] Admin home matrix matches the per-module rail counts *(Dave 2026-08-03)*
- [X] Matrix "Configure →" lands on the right module *(Dave 2026-08-03)*
- [X] Nav no longer offers a top-level Dashboard; `?do=admin_dashboard` highlights
      Module Management *(Dave 2026-08-03)*
- [ ] Main Details renders from the meta on all four editors, with a FontAwesome icon
- [ ] Main Details icon, label and position are now changeable from admin
- [ ] Main Details renders exactly once — no duplicate tab or orphaned pane
- [ ] Static fields still appear in Main Details and still save
- [ ] Chosen organisation UI: a category can be moved between sections and the editor follows
- [ ] Wayleave's three `General` categories are comprehensible in the new UI

## Risks

- **Phase 2 is the risky one.** Deleting hardcoded markup from four editor templates while
  the loop takes over is a swap that fails visibly (duplicate or missing tab) rather than
  quietly — but it touches the main editing surface of every module, so it wants testing
  per module, not per pattern.
- ~~**Sub-category removal is irreversible** once the tables are dropped.~~ **Retired
  2026-08-03** by the decision to hide rather than remove. Nothing is dropped, so there is
  nothing to be irreversible about — which is most of why the hide is the better call for
  now. The risk returns in full if deletion is ever revisited.
- **Phase 4 rewrites live organisation data** by de-duplicating categories. Only reachable
  if the prototype chooses it, and it deserves its own backup/verification step.
- **No local runtime** — verified by reading; migrations applied by Dave on dev.
