# MetaReport — Report Variant Picker: Purpose, Flow, and Guide

**Status as of 2026-08-12:** persisted and authorable. What started as a single hand-authored
static JSON file (Trial Balance only) is now backed by real gb5 tables with full CRUD, plus an
authoring screen with a live preview. One real example (Trial Balance) exists end-to-end; a
second, illustrative example is included in §5 purely to show the pattern generalizes — it is
**not** built, and is clearly marked as such.

**Audience:** this doc has sections for developers (how to add one), functional/implementation
consultants (how to plan one for a client), and anyone trying to understand what "MetaReport"
means when they see it in code or comments. If you read the previous version of this doc: the
"no backend, no admin UI" limitation it led with is gone — see §7 for what's built and §10 for
what's still genuinely open.

---

## 1. What MetaReport is

MetaReport is a **report variant picker**: a small, guided form that lets a user narrow down to
one specific report out of a family of near-identical variants, without cluttering the menu tree
with one entry per variant.

### The core idea, in one sentence

> A single conceptual report (e.g. "Trial Balance") that would otherwise need **8 separate menu
> items** in `MMENU` (one per Level × Type combination) is presented to the end user as **one
> screen** with a compact aspect-picker, instead of 8 items cluttering the navigation tree.

MetaReport never talks to the backend for report *data* itself — it only resolves which `MenuId`
the user means, then hands that `MenuId` to the exact same `gb-reportviewer` component and
`defaultreportsettings()` → `Reportdetailservice(MenuId)` → `ReportConfigservice`/`Rowservice`/
`CriteriaConfigservice` pipeline every other menu-driven report in the system already uses.
Everything downstream of that handoff — `MREPORTVIEW`/`MREPORTVIEWFIELDS` field configuration,
grouping/subtotal/grandtotal, criteria/filter handling, Excel/PDF/CSV export via
`BaseReportEndpoint<TRequest, TData>` — is the **standard reportviewer pipeline**, untouched by
MetaReport. **MetaReport's entire job is picking the right `MenuId`** — nothing else.

### Two halves: the picker, and (now) its definition's home

- **The picker (frontend, gb4.7mfe)** — `features/gbmetareport/` (`MetaReportComponent`,
  selector `gbmeta-reportviewer`) shows the aspect form and resolves a variant.
  `features/gbmetacontainer/` (`GBMetaContainerComponent`, selector `gb-metacontainer`) is the
  host screen: picker on the right, the real `gb-reportviewer` on the left once a report is
  resolved. Neither of these changed in shape this round — see §6 for the one addition (tab
  grouping).
- **The definition (now backend, gb5)** — previously a single static JSON file; now a full
  persisted aggregate (`MMETAREPORT` + variants + aspects + options + applicability), with CRUD
  endpoints and an authoring screen. See §3.

The name is genuinely unrelated to `gbmetaform` (the declarative CRUD-form renderer covered
elsewhere in this repo) — they happen to share the "meta" prefix and a similar config-loading
pattern, but one renders data-entry forms and the other picks among report variants. Don't
confuse them.

---

## 2. End-to-end flow

```
User opens /metacontainer/:reportConfigId (or bare /metacontainer → defaults to "trial-balance")
        │
        ▼
GBMetaContainerComponent.ngOnInit()
        │  reads :reportConfigId route param
        ▼
MetaReportLoaderService.load(reportConfigId)
        │  reportConfigId is a positive integer (a real MMETAREPORT.MetaReportId)
        │    → GET Framework.MetaReport.Get?MetaReportId=<id>  (live, gb5 backend)
        │  reportConfigId is a slug string (e.g. "trial-balance")
        │    → dev:  require('projects/gbhost/public/metareport/<id>.meta.json')
        │    → prod: HTTP GET <gbhost baseURI>/metareport/<id>.meta.json
        │  (either path: 30-minute in-memory cache)
        ▼
<gbmeta-reportviewer [metaReport]="metaReport">   (MetaReportComponent)
        │
        │  User picks EITHER:
        │    (a) a named variant from the dropdown → aspect fields auto-fill with each
        │        aspect's first applicable option → "Generate Report" button, or
        │    (b) individual aspect values (Type / Level / PeriodType / ...), grouped into tabs
        │        once the config splits across more than one tabGroup → the component
        │        intersects each selected option's `applicableVariants` lists to find the
        │        one matching variant → "Find & Generate Report" button
        │
        ▼  (reportGenerated) event: {variantId, variantName, menuid, reportid, selectedOptions}
GBMetaContainerComponent.onMetaReportOutput()
        │  wraps the payload into the same {id, MenuId, MenuDetails} "Tab" shape used
        │  everywhere else in the app for menu-driven reports, with MenuDetails.Criteria
        │  forced to 'NONE' so the standard bootstrap path runs
        ▼
<gb-reportviewer [MenuDetails]="selectedTab">      (the REAL, pre-existing report viewer)
        │
        ▼
defaultreportsettings() → Reportdetailservice(MenuId) → ReportConfigservice / Rowservice /
CriteriaConfigservice → the real gb5 backend (MREPORTVIEW, MREPORTVIEWFIELDS,
BaseReportEndpoint<TRequest,TData>-derived endpoint) → grid render, grouping/subtotal/
grandtotal, export
```

**Authoring flow (new, separate from the above):**

```
Author opens /dev/metareport-admin (gb4.7mfe admin project, dev-only route today)
        │
        ▼
MetaReportAdminComponent  ── loads list ──▶  Framework.MetaReport.GetList
        │  pick existing, or "+ New"
        ▼  select(id) ──▶ Framework.MetaReport.Get?MetaReportId=<id>
Edit name/description, variants (via a real MMENU picklist — see §6), aspects, options,
and the option×variant applicability matrix — all in-memory, nothing saved yet
        │
        ▼  "Show preview" — renders the REAL <gbmeta-reportviewer> fed straight from the
        │  in-progress draft object, so tab grouping / variant resolution can be checked
        │  before committing anything
        ▼
Save ──▶ Framework.MetaReport.Save (SaveMetaReportDefinition)
        │  backend assigns AutoNumber ids to every new Variant/Aspect/Option, then
        │  diff-reconciles all four levels (Variants, Aspects, Options, applicability
        │  links) in one transaction
        ▼
List reloads; the MetaReport is now loadable by real MetaReportId from any /metacontainer/<id>
```

**Key point for anyone auditing this feature:** the picker side (top diagram, from
`GBMetaContainerComponent` down to `gb-reportviewer`) is still the *entire* interface between
MetaReport and the report-rendering pipeline. Persistence only changed *where the definition
comes from* — it did not add a MetaReport-specific backend call anywhere in the actual
report-loading chain.

---

## 3. Data model

### 3.1 Backend — gb5 tables (new this round)

Five tables, all under `GB5Framework` (Menu subsystem's neighborhood), migrations at
`DB/Migrations/20260812_Framework_MetaReport_Schema_{SqlServer,Postgres}.sql`:

| Table | Holds |
|---|---|
| `MMETAREPORT` | One row per report family — name, description, status |
| `MMETAREPORTVARIANT` | One row per named variant — `VariantName`, the real `MenuId`/`ReportId` it resolves to |
| `MMETAREPORTASPECT` | One row per picker dimension — name, `AspectType` (radio/select/button/checkbox), mandatory flag, display order, `TabGroup` |
| `MMETAREPORTASPECTOPTION` | One row per selectable option within an aspect — label, value (+ a `ValueType` discriminator: string/number/boolean) |
| `MMETAREPORTOPTIONVARIANT` | Many-to-many: which options apply to which variants (empty = "always applicable") |

CRUD lives in `GB5Framework/Framework{DAL,BLL,SL}/MetaReport/` — same SL→BLL→DAL shape as every
other module in this codebase. `SaveMetaReportDefinition` is one transaction: it assigns
AutoNumber ids to every new Variant/Aspect/Option, then diff-reconciles all four child levels in a
carefully FK-safe statement order (see `MetaReportDAL.SaveMetaReportDefinition`'s own remarks for
exactly why the order matters). Endpoints:

```
GET    /MetaReport/GetMetaReport?MetaReportId=      → full aggregate
GET    /MetaReport/GetMetaReportList                → lightweight index
POST   /MetaReport/SaveMetaReportDefinition         → whole-aggregate upsert
DELETE /MetaReport/DeleteMetaReport?MetaReportId=
```

**Not yet done:** these migrations have not been applied to any live database (including
GB5DEMO) — they exist as reviewed, build-verified code only. Applying them is a deployment step,
not a documentation one; see §10.

### 3.2 Frontend — the picker's own shape (unchanged)

The picker component still consumes the same `IMetaReport` TypeScript shape it always has
(`features/gbmetareport/metareport.interfaces.ts`) — camelCase, plus the historical lowercase
`menuid`/`reportid` on `IMetaReportVariant`, kept as-is since nothing downstream needed them
renamed:

```typescript
interface IMetaReportOption {
  optionId: number;
  label: string;
  value: string | number | boolean;
  applicableVariants?: number[];   // omitted/empty = applies to every variant
}

interface IMetaReportAspect {
  aspectId: number;
  aspectName: string;
  aspectType: 'radio' | 'select' | 'button' | 'checkbox';
  isMandatory: boolean;
  displayOrder: number;
  options: IMetaReportOption[];
  tooltip?: string;
  tabGroup?: string;   // NEW this round — see §6
}

interface IMetaReportVariant {
  variantId: number;
  variantName: string;
  menuid: number;     // the real MMENU.MenuId this variant resolves to
  reportid: number;   // rides along for the summary panel — see §10 caveat
}

interface IMetaReport {
  metaReportId: number;
  metaReportName: string;
  description?: string;
  variants: IMetaReportVariant[];
  aspects: IMetaReportAspect[];
}
```

### 3.3 The bridge between them — `metareport-mapper.ts` (new this round)

The backend returns PascalCase (`MetaReportId`, `VariantName`, `ApplicableVariantIds`, ...) —
matching every other real gb5 response this app consumes. Rather than change the picker
component's field names (and risk the existing, working Trial Balance render path), a single
mapper file translates both directions:

- `mapBackendToMetaReport()` — used by `MetaReportLoaderService` (reading) and the authoring
  screen (reading, and for the live preview — the preview renders the in-memory draft directly,
  no round-trip through this mapper needed there).
- `mapMetaReportToBackend()` — used by the authoring screen's Save action. Also converts each
  option's typed `value` back into `{Value: string, ValueType: 0|1|2}` for storage.

One mapping definition, both directions, one file — so the two never drift out of sync with each
other.

---

## 4. Worked example — Trial Balance (real, live-verifiable data)

`trial-balance.meta.json` (and, once migrated, the equivalent `MMETAREPORT` row) models Trial
Balance's 8 real menu items as one screen:

| variantId | variantName | menuid | reportid |
|---|---|---|---|
| 1 | TB AsOn Schedule | -1399999886 | -1399967378 |
| 2 | TB AsOn Group | -1399999885 | -1399967378 |
| 3 | TB AsOn Account | -1399999884 | -1399967378 |
| 4 | TB AsOn SubAccount | -1399999883 | -1399967378 |
| 5 | TB Detail Schedule | -1399999880 | -1399967378 |
| 6 | TB Detail Group | -1399999879 | -1399967378 |
| 7 | TB Detail Account | -1399999878 | -1399967378 |
| 8 | TB Detail SubAccount | -1399999877 | -1399967378 |

Four aspects narrow those 8 down:

- **Type** (radio, mandatory): `AsOn` → variants 1-4, `Detail` → variants 5-8, `Periodic` →
  no variants yet (empty `applicableVariants`, so it never disqualifies anything — effectively
  a placeholder for a future variant set).
- **Level** (button toggle, mandatory): `Schedule`/`Group`/`Account`/`SubAccount`, each mapped to
  the matching pair of AsOn/Detail variants.
- **PeriodType** (select, optional): `Year`/`Quarter`/`Month`/`Week`/`Day`, only applicable to
  variants 3-4 (TB AsOn Account/SubAccount) — rides along in `selectedOptions` but see §10 for
  what's *not* verified about it downstream.
- **Include Dimension** (checkbox, optional): applies to variants 3, 4, 7, 8.

Picking Type=AsOn + Level=Account resolves — via `findMatchingVariant()`'s intersection of
`applicableVariants` — to variant 3 ("TB AsOn Account", `MenuId -1399999884`).

All four aspects currently share the single default tab (no `tabGroup` set on any of them) — so
Trial Balance still renders exactly as a flat Single Pane, per §6.

---

## 5. Worked example — a second, illustrative "set" (hypothetical — not built)

Stakeholders new to this concept often ask "does this only work for Trial-Balance-shaped
reports?" It doesn't — the pattern also collapses cleanly to a **single aspect**, which is a
useful contrast to Trial Balance's two-aspect grid. The example below is invented for teaching
purposes only: it does **not** exist as real `MMENU`/`MREPORT` rows, and the ids used are
deliberately small round numbers (5001, 5002, ...) so nobody mistakes them for real AutoNumber
values. Treat it as "here's what you'd fill in," not "here's what's live."

**Report family: Stock Valuation.** Imagine three existing, independently-working menu items —
"Stock Valuation (FIFO)", "Stock Valuation (LIFO)", "Stock Valuation (Weighted Average)" — each
its own report today, each reachable only by hunting through the Inventory menu. One aspect
("Costing Method") replaces all three menu items with one guided pick:

| variantId | variantName | menuid (illustrative) | reportid (illustrative) |
|---|---|---|---|
| 101 | Stock Valuation (FIFO) | 5001 | 5010 |
| 102 | Stock Valuation (LIFO) | 5002 | 5010 |
| 103 | Stock Valuation (Weighted Average) | 5003 | 5010 |

One aspect, one option per variant, 1:1 — no intersection logic needed since each option maps to
exactly one variant on its own:

```jsonc
{
  "metaReportId": 2,
  "metaReportName": "Stock Valuation",
  "description": "Pick a costing method to see current stock value",
  "variants": [
    { "variantId": 101, "variantName": "Stock Valuation (FIFO)", "menuid": 5001, "reportid": 5010 },
    { "variantId": 102, "variantName": "Stock Valuation (LIFO)", "menuid": 5002, "reportid": 5010 },
    { "variantId": 103, "variantName": "Stock Valuation (Weighted Average)", "menuid": 5003, "reportid": 5010 }
  ],
  "aspects": [
    {
      "aspectId": 201,
      "aspectName": "Costing Method",
      "aspectType": "radio",
      "isMandatory": true,
      "displayOrder": 1,
      "options": [
        { "optionId": 301, "label": "FIFO", "value": "FIFO", "applicableVariants": [101] },
        { "optionId": 302, "label": "LIFO", "value": "LIFO", "applicableVariants": [102] },
        { "optionId": 303, "label": "Weighted Average", "value": "WAVG", "applicableVariants": [103] }
      ]
    }
  ]
}
```

What this second example is meant to show a stakeholder:

- **The pattern scales down, not just up.** Trial Balance needed two aspects and a real
  intersection; Stock Valuation needs one aspect with a direct 1:1 option-to-variant map. Both are
  the same mechanism (`applicableVariants`), just with different cardinality.
- **The "always applicable" case doesn't come up here** — every option is scoped to exactly one
  variant, unlike Trial Balance's `PeriodType`/`Include Dimension`, which apply broadly.
- **It's still just packaging.** If someone actually wanted to build this, the real work is making
  sure all three Stock Valuation menu items already render correctly on their own — the MetaReport
  layer contributes nothing to *that*, only to how a user finds the right one of the three.

If this example gets built for real, replace every id above with actual `MMENU.MenuId`/
`MREPORT.ReportId` values (verified the same way Trial Balance's were — direct SQL against the
live schema, not assumed from a naming convention) before treating it as documentation of a real
feature.

---

## 6. UI walkthrough

The container (`gb-metacontainer`) is a two-pane layout:

- **Left pane** — empty with a "select something to generate" hint until a report is resolved;
  once resolved, it's the full `gb-reportviewer` grid (same widget every other report uses —
  toolbar, grouping, export buttons, drilldown, everything).
- **Right pane** — the picker:
  1. Header: report family name + description, both run through Transloco so they're
     translatable.
  2. **Variant selector** dropdown — "None, select by aspects" is always the first option.
  3. **Aspects**, in `displayOrder`:
     - If every aspect shares one `tabGroup` (or none set it at all — Trial Balance's current
       state) — flat, exactly as before this feature existed.
     - If a config actually splits aspects across more than one `tabGroup` — rendered as Material
       tabs, one per group, each showing that group's aspects. Both paths render through the same
       `#aspectRow` template, so there's exactly one definition of what an aspect control looks
       like regardless of layout.
     - Each aspect renders per its `aspectType`: `radio` → Material radio group, `select` →
       Material dropdown, `button` → Material button-toggle group, `checkbox` → single Material
       checkbox (labeled from `options[0].label`).
  4. When a variant is selected, a **summary panel** shows the variant name + its `reportid`/
     `menuid` — useful for support/debugging, since those are the exact values handed to the
     real reportviewer.
  5. **Action buttons**: Cancel, Reset, and exactly one of **Generate Report** (variant mode) or
     **Find & Generate Report** (aspect mode) — the latter shows a "no matching report" snackbar
     if the current aspect selection doesn't intersect to exactly one variant.

Disabled options (grayed out, not hidden) are how the picker signals "this option doesn't apply
to your current variant/aspect selection" — `isOptionEnabled()` checks the option's
`applicableVariants` against whatever's currently selected.

### The authoring screen (`MetaReportAdminComponent`, new this round)

Dev route: `/dev/metareport-admin` (admin project). Left pane: a list of existing MetaReports plus
"+ New". Right pane, top to bottom:

- Name / description.
- **Variants table** — variant name, a real **MMENU picklist** (`gb-newpicklist`, wired to
  `Framework.Menu.Picklist` → `MenuBLL.GetSelectListMenu`), and a read-only `ReportId` column.
  Picking a menu auto-resolves `ReportId` from that same `MMENU` row (`Framework.Menu.Get`) —
  there's deliberately no independent `ReportId` picklist, since gb5 has no raw-`MREPORT`
  select-list endpoint today, and a manually-typed `ReportId` risks silently drifting from the
  menu actually picked.
- **Aspects** — expandable cards: name, type, mandatory, display order, tab group, tooltip, and
  each aspect's own **Options table with an inline applicability matrix** — one checkbox column
  per current variant, checked cells feeding `ApplicableVariantIds`.
- **Show preview** — renders the real `<gbmeta-reportviewer>` fed from the in-progress draft, so
  tab grouping and variant resolution can be checked before saving anything.
- Save / Delete.

Known editing gap: when re-opening an *existing* MetaReport, the menu picklist shows the raw
`MenuId` as its initial text rather than the menu's real code/name — the display label only
appears once the author re-searches or re-picks. The underlying id is correct either way; only the
initial label is a placeholder. See §10.

---

## 7. What changed this round — a straight status comparison

| Area | Before | Now |
|---|---|---|
| Definition storage | Static JSON file, hand-edited, redeploy to change | `MMETAREPORT` + 4 child tables, full CRUD |
| Reuse across environments | Copy the JSON file | Load by `MetaReportId` from any environment pointed at the same DB |
| Authoring | Hand-edit JSON, no validation | Dedicated screen — variants/aspects/options/applicability, all in one place |
| Preview before saving | Not possible | Live preview using the real picker component fed the draft |
| Aspect grouping | Always one flat list | Flat by default; tabs when a config actually needs them (`tabGroup`) |
| MenuId entry | N/A (JSON hand-typed) | Real MMENU picklist; ReportId auto-resolved, not hand-typed |
| Second example | None | One illustrative (not built) example in §5, to generalize the concept for stakeholders |

---

## 8. Developer guide — adding a new MetaReport

MetaReport only makes sense when a single reporting *concept* already exists as **multiple real
menu items** pointing at variations of the same underlying report. If your report is just one
menu item, you don't need MetaReport — wire it into navigation normally.

Steps, assuming the underlying menu items already exist and already work individually:

1. **Inventory the real variants.** For each menu item in the family, record its `MMENU.MenuId`
   (the authoring screen resolves `ReportId` for you once you pick the menu — see §6).
2. **Design the aspects.** Identify the dimensions that distinguish the variants (Trial Balance
   used Type × Level; Stock Valuation in §5 needs only Costing Method). Decide `aspectType` per
   dimension based on cardinality/UX fit: 2-4 short options → `radio` or `button`; longer lists →
   `select`; a single on/off toggle → `checkbox`. If you have more than ~4-5 aspects, or a clear
   split between "always relevant" and "optional/refining" aspects, give them different
   `tabGroup` values so the picker splits them into tabs (§6) instead of one long flat list.
3. **Map applicability.** For every option, decide which variants it's compatible with — leave it
   empty only when the option genuinely applies to every variant (see Trial Balance's `Periodic`
   Type option, which has none yet).
4. **Verify aspect-intersection coverage.** Walk every combination of mandatory-aspect option
   values a user could pick and confirm exactly one variant's applicability sets intersect to a
   single id — `findMatchingVariant()` returns the *first* element of the intersection if there's
   more than one, which will silently pick the wrong variant if your mapping is ambiguous. There's
   no build-time or runtime validation of this — check it by hand, or in the authoring screen's
   preview by trying every combination yourself.
5. **Build it in the authoring screen** (`/dev/metareport-admin`) — variants, aspects, options,
   applicability matrix, then Save. (The legacy path — hand-writing a `.meta.json` file under
   `projects/gbhost/public/metareport/` — still works for slug-style ids and is what Trial Balance
   still uses; new work should go through the authoring screen and a real `MetaReportId`.)
6. **Load it** at `/metacontainer/<your-real-MetaReportId>`.
7. **No backend report-logic work is needed** — that's the entire point of this pattern. If the
   individual menu items don't already render correctly through `gb-reportviewer` on their own,
   fix that first; MetaReport can't fix a broken underlying report.
8. **No production menu wiring exists yet**, for either the picker or the authoring screen — both
   are dev-only routes today (`gbhost`'s own `app.routes.ts` for the picker, `admin`'s for the
   authoring screen). Wiring a real `MMENU` entry (with `componentPaths` registration, matching
   every other GB5Solution screen) is a follow-up before end users can reach either without a
   bookmark.

---

## 9. Functional / implementation guide

For a functional consultant or implementation lead scoping whether a client's report family is a
good MetaReport candidate:

**Good fit:**
- A report already exists in 2+ near-identical variants differentiated by a small number of clean
  dimensions — anywhere from Stock Valuation's one-aspect/1:1 shape (§5) up to Trial Balance's
  two-aspect grid (§4) and beyond.
- The variants are all real, working, independently-reachable menu items already — MetaReport is
  packaging, not new reporting logic.
- The client's users find the current menu clutter confusing ("why are there 8 Trial Balance menu
  items?") and would benefit from one guided picker instead.

**Poor fit / don't use MetaReport for:**
- A single report with runtime parameters that are naturally handled by the report's own
  criteria/filter panel (`MENTITYCRITERIA`) — that's what the criteria system is for; don't
  duplicate it as MetaReport aspects.
- A report family where the "aspects" don't cleanly map to a fixed, enumerable option list (e.g.
  a free-text date range) — MetaReport's aspect model is closed-option only (radio/select/
  button/checkbox), no date pickers or numeric inputs.
- Anything requiring new backend logic — MetaReport adds zero backend capability; if the report
  doesn't already work as separate menu items, building those correctly comes first.

**What to hand the developer/author:** the list of real menu items in the family (they'll pick
each one via the authoring screen's picklist — no need to hand over raw ids anymore), the
dimension names and option labels, and the applicability mapping (which options apply to which
variants) — §8 steps 1-4. Building it in the authoring screen (step 5) is then mechanical.

**What to tell the client/end user:** this screen doesn't add new report capability — it's a
friendlier front door to reports that already existed. Grouping, subtotals, export, and drilldown
inside the resulting report grid work exactly as they did before, because it's the same
`gb-reportviewer` component and the same backend report endpoint underneath.

---

## 10. Known limitations and gaps (verified, not guessed)

- **Migrations not applied anywhere live.** The `MMETAREPORT`/variant/aspect/option/optionvariant
  schema (§3.1) is code-reviewed and build-verified, but has not been run against any real
  database, including GB5DEMO. This is a deployment step someone still needs to do.
- **Trial Balance's static JSON hasn't been migrated into the new tables.** It still loads via the
  legacy slug path (`trial-balance` → the static file); the persistence layer is proven by tests
  and by the authoring screen's own round-trip, not yet by that one real example living in the DB.
- **No production menu wiring** for either the picker (`/metacontainer`) or the authoring screen
  (`/dev/metareport-admin`) — both are dev-only routes; see §8 step 8.
- **No authorization on the write endpoints.** `SaveMetaReportDefinition`/`DeleteMetaReport` are
  `AllowAnonymous()`, matching this codebase's current Framework/Menu-family convention — same
  deferral already tracked for MenuGroup/MenuSet.
- **Menu picklist shows a raw id, not a label, when editing an existing MetaReport.** See §6's
  "Known editing gap." The stored `MenuId` is correct; only the initial display text in the
  picklist is a placeholder until re-searched.
- **`reportid` may be vestigial.** `gb-reportviewer`'s actual bootstrap (`defaultreportsettings()`)
  resolves everything from `MenuDetails.MenuId` alone (verified at
  `features/gbreportviewer/reportviewer/reportviewer.component.ts:157-160`). Whether `reportid` is
  independently consumed anywhere downstream of that wasn't traced further for this doc — treat it
  as "rides along for display purposes, confirmed unused for the actual data-load call." If you're
  relying on it for something, verify that assumption yourself first.
- **`findMatchingVariant()` has no ambiguity guard.** If two variants' applicability sets both
  match the user's current aspect selection, the component silently returns the first intersecting
  id (`gbmetareport.component.ts`) rather than flagging the mapping as ambiguous. Design your
  applicability mapping so this can't happen (§8 step 4) — the authoring screen's preview is the
  practical way to check this by hand today.
- **`PeriodType`/`Include Dimension` aspects' downstream effect wasn't traced** — they ride along
  in `selectedOptions`, but whether the backend report call actually consumes them (versus the
  report's own criteria panel taking over once `gb-reportviewer` loads) was not verified.
- **No live end-to-end test.** Everything backend-side is verified by unit tests (mocked DAL/DB)
  and a clean build; nothing has been exercised against a running gb5 service + real browser for
  this feature yet.
- **No unit/integration test coverage on the frontend** beyond the presence of empty
  `gbmetareport.component.spec.ts`/`gbmetacontainer.component.spec.ts`/
  `metareportadmin.component.spec.ts` files.

**Fixed since first written:** `MetaReportAdminComponent`'s constructor originally called
`this.loadList()` eagerly and unconditionally — unlike every sibling `/dev/*` admin screen, which
waits for an explicit user action. This app has SSR enabled and prerenders routes by default;
an eager HTTP call at construction time, with no backend reachable during a build-time prerender,
risked hanging or failing that step. Fixed by guarding it behind `isPlatformBrowser()` (gb4.7mfe
commit `6673accd5`). **If you add a new dev-only admin screen, don't call a load method directly
in the constructor** — either wait for `ngOnInit` behind the same guard, or wait for an explicit
button click like the other screens do.

---

## 11. Decision records

Two real decisions were made along the way, kept here so neither gets re-discovered from scratch.

### 11.1 Picker layout (Single Pane + tabs)

Three candidate layouts for the *picker* (not the authoring screen) were mocked up using the real
Trial Balance data and reviewed with stakeholders before any persistence work began:

- **A — Guided steps**: a wizard, one aspect per step, each step only shows options still
  reachable given prior answers.
- **B — Single pane**: every aspect visible together, non-applicable options greyed out as
  choices narrow, one Generate/Find-and-Generate button.
- **C — Direct grid**: the two core aspects (Type × Level) as a literal table; clicking a cell
  opens the report immediately, no separate Generate step.

**Decision: B (Single pane), with tab grouping added for aspect-heavy MetaReports**, rather than
escalating to A or C. A trades speed for hand-holding (better for occasional/new users, worse for
repeat use); C is extremely fast but only works for exactly two grid-axis dimensions and doesn't
generalize past that without bolting on a separate filter strip anyway (Trial Balance already
needs two more aspects beyond its Type×Level grid). Single Pane was already closest to shipped
code, generalizes better past two aspects, and tabs solve the "too much on screen at once"
problem A and C were actually reacting to — without a new layout family. Tab grouping (§6) is the
direct result of this decision, now implemented.

The three mock-ups this decision was based on are preserved outside the repo (an internal Artifact
link, not part of version control) — ask whoever ran the review session for the link if you need
to see the actual side-by-side comparison rather than this summary.

### 11.2 MenuId as a real picklist; no independent ReportId field

The authoring screen originally had plain number inputs for both `MenuId` and `ReportId` per
variant. Two problems with that: typed-in ids are error-prone and unverifiable at entry time, and
a real `MREPORT` select-list endpoint doesn't exist in gb5 today (confirmed by search — only a
`ReportView`-scoped select-list exists, not a raw-`MREPORT` one). Since every real `MMENU` row
already carries exactly one `ReportId`, the fix was to make `MenuId` a real picklist
(`gb-newpicklist` → `Framework.Menu.Picklist`) and auto-resolve `ReportId` from that same menu row
via `Framework.Menu.Get`, rather than trust a second, independently-typed number that could drift
from the menu actually selected. This is why §6 describes `ReportId` as read-only in the authoring
screen — that's deliberate, not a missing feature.

---

## 12. Combining MenuGroup + MetaReport — a consolidation workflow

A team asked to "clean up" a client's sprawling, flat menu tree usually finds two different
problems tangled together, and it's worth naming them separately before starting:

1. **Too many near-duplicate report menu items** (Trial Balance's 8, or any report family with a
   Level/Type/Method-style split) — this is what **MetaReport** collapses.
2. **A flat, unorganized menu list** with no folder structure at all (forms and reports alike,
   whether or not they're report families) — this is what **MenuGroup** organizes.

They solve different problems and compose cleanly: MetaReport reduces *how many entries* a report
family needs (N variants → 1 entry point); MenuGroup organizes *where* whatever entries remain —
including that one MetaReport entry point — sit in the navigation tree. Neither one requires the
other, but doing a real consolidation pass almost always wants both, in this order.

### Step 1 — Inventory before touching anything

Pull a flat export of the client's `MMENU` rows for the module(s) in scope (Code, Name, Section,
ParentId/Levels, ReportId where applicable). Sort by name and look for two patterns:

- **Report families**: 3+ menu items whose names differ only by a suffix pattern (`AsOn Schedule`
  / `AsOn Group` / `AsOn Account` / ..., or `(FIFO)` / `(LIFO)` / `(Weighted Average)`) and that
  all point at a real, working report already. These are MetaReport candidates — list them
  grouped by family, not as individual rows, so the team scopes work by *family*, not by menu item.
- **Everything else that's just poorly organized** — forms, single-variant reports, anything with
  no natural sibling variants. These are MenuGroup-only candidates: they don't need MetaReport,
  they just need a sensible folder.

Keep a simple tracking sheet (family/item → decision → status) for the duration of the
consolidation — there's no bulk-import tool for either MenuGroup or MetaReport today (see Step 5),
so this is genuinely a one-family-at-a-time, one-group-at-a-time manual pass, and losing track of
what's already been migrated is the main way this kind of work goes sideways.

### Step 2 — Build MetaReports first, MenuGroups second

Order matters. If you build the MenuGroup tree first and place all 8 Trial Balance variants
individually into folders, then build the MetaReport afterward, you've done placement work you
immediately have to undo (the 8 individual entries disappear from the tree once they're collapsed
behind one MetaReport entry point). Collapse variants into MetaReports *first* — for each report
family from Step 1, follow §8's developer steps (inventory the real menu items, design aspects,
map applicability, build and preview in `/dev/metareport-admin`, verify against the real backend)
— so that by the time you get to the menu tree, each report family is already down to the one
entry point it actually needs.

### Step 3 — Organize the (now smaller) menu list with MenuGroup

With report families already collapsed, build the MenuGroup folder hierarchy
(`/dev/menugroup`) over what's left: each MetaReport's single entry point, plus every
MenuGroup-only item from Step 1. This is the same MenuGroup admin workflow documented in the
Menu Presentation Layer plan — group/subgroup structure, then `MenuGroupDetail` to place leaves.

### Step 4 — Decision checklist (use per family/item, not once for the whole project)

| Question | If yes | If no |
|---|---|---|
| Does this "report" already exist as 2+ real, independently-working menu items differentiated by a small number of clean dimensions? | MetaReport candidate — see §9's Good/Poor fit list for the finer-grained call | Skip MetaReport; MenuGroup-only |
| Does the client's users find this specific corner of the menu confusing/cluttered (not just "the tree is long")? | Worth prioritizing this family/branch first | Lower priority — clean it up, but it's not urgent |
| Is this a single report with real runtime parameters (a date range, a free-text filter)? | Leave it to the report's own criteria panel — don't force it into MetaReport aspects (§9) | — |

### Step 5 — Known constraints on doing this today

- **Both admin screens are dev-only routes** (`/dev/menugroup`, `/dev/metareport-admin`) with no
  production `MMENU` wiring and no authorization (`AllowAnonymous()`) — see §10 and the Menu
  Presentation Layer plan's own deferred-authorization note. In practice this means today's
  consolidation work happens through a developer/admin session hitting these dev routes directly,
  not through a client-facing admin flow — plan the rollout accordingly (someone with backend
  access runs the actual authoring session, or at minimum accompanies whoever does).
- **No bulk import/export exists for either screen.** If a family has a genuinely large number of
  variants, or there are many families to process, this is real per-family manual authoring time,
  not a script you can point at a spreadsheet. If the team ends up doing this for many more than a
  handful of families, building a one-time CSV-import helper (map rows to
  `SaveMetaReportDefinition`'s payload shape, §3.1) is a reasonable follow-up ask — it doesn't
  exist today, don't assume it does.
- **Verify before you migrate, not after.** For every report family, confirm the real
  `MenuId`s/behavior by checking the live menu items directly (the same way Trial Balance's ids
  were verified in this doc — direct SQL/API, never assumed from a naming convention). A wrong
  `applicableVariants` mapping fails silently at pick-time (§10's ambiguity-guard gap) — the
  authoring screen's preview is the practical way to catch that before it reaches a user.

---

## 13. Quick reference — file map

**Backend (gb5):**

| File | Role |
|---|---|
| `DB/Migrations/20260812_Framework_MetaReport_Schema_{SqlServer,Postgres}.sql` | Schema for the 5 new tables + AutoNumber seeds |
| `GB5Framework/FrameworkDAL/DTO/MetaReport/*.cs` | `MetaReportDTO` and its child DTOs (Variant/Aspect/AspectOption/OptionVariantLink/ListItem) |
| `GB5Framework/FrameworkDAL/Query/MetaReport/MetaReportQB.cs` | SQL — reads, upserts, diff-save statements |
| `GB5Framework/FrameworkDAL/CustomCode/MetaReport/{I,}MetaReportDAL.cs` | DAL — assembles the aggregate, runs the transactional diff-save |
| `GB5Framework/FrameworkBLL/MetaReport/{I,}MetaReportBLL.cs` | BLL — AutoNumber assignment, cache invalidation, validation |
| `GB5Framework/FrameworkSL/Endpoints/MetaReport/*.cs` | The 4 endpoints (§3.1) |
| `GB5Framework/FrameworkTests/MetaReportBLLTests.cs` | Aggregate-assembly + AutoNumber-assignment unit tests |

**Frontend (gb4.7mfe):**

| File | Role |
|---|---|
| `features/gbmetareport/metareport.interfaces.ts` | TypeScript shapes for the config and its output event (now incl. `tabGroup`) |
| `features/gbmetareport/metareport-mapper.ts` | PascalCase-backend ↔ camelCase-FE mapping (§3.3) — shared by the loader and the authoring screen |
| `features/gbmetareport/gbmetareport.component.ts` / `.html` / `.scss` | The picker UI (`gbmeta-reportviewer`) — now with tab-grouping support |
| `features/gbmetareport/service/metareport-loader.service.ts` | Loads/caches a config by id — numeric id → backend, slug → static file |
| `features/gbmetacontainer/gbmetacontainer.component.ts` / `.html` | Host screen (`gb-metacontainer`); wires picker output into `gb-reportviewer` |
| `projects/admin/master/dbservice/metareport.db.service.ts` | HTTP wrapper for the 4 backend endpoints |
| `projects/admin/master/rolesandrights/metareportadmin/*` | The authoring screen (§6) |
| `projects/gbhost/public/metareport/trial-balance.meta.json` | The one existing static-file config (legacy path) |
| `projects/gbhost/public/api/gb5api/framework.ts` | `Framework.MetaReport.{Get,GetList,Save,Delete}` mappings |
| `projects/gbhost/public/formactionurls/idfield.ts` | `metareportadmin = "MetaReportId"` |
| `projects/gbhost/src/app/app.routes.ts` | Route registration for `/metacontainer` and `/metacontainer/:reportConfigId` |
| `projects/admin/src/app/app.routes.ts` | Route registration for `/dev/metareport-admin` |
| `features/gbreportviewer/reportviewer/reportviewer.component.ts` | The real report renderer MetaReport hands off to — unmodified by MetaReport |
