Skip to content

feat(compliance): standalone Compliance page shell, filter bar and view-mode toggle (#1163) - #1173

Merged
renemadsen merged 2 commits into
stablefrom
feat/1163-compliance-page-shell
Sep 4, 2026
Merged

feat(compliance): standalone Compliance page shell, filter bar and view-mode toggle (#1163)#1173
renemadsen merged 2 commits into
stablefrom
feat/1163-compliance-page-shell

Conversation

@renemadsen

Copy link
Copy Markdown
Member

Closes #1163. Part of #1160. Depends on #1161 (merged).

Builds the shell of the standalone Compliance page: route, lazy module, left-menu entry, the shared ten-control filter bar, the view-mode toggle, and the shared result container with pagination. The three view modes themselves are #1164 / #1165 / #1167.

No access gating — a deliberate behaviour change

AuthGuard only. No permission guard, no requiredPermission, Permissions = [] on the menu entry, and no *ngIf="isAdmin" anywhere. Per #1160 decision 6 this is intentional: the calendar hid the Compliance option behind four client-side admin guards while POST calendar/compliance-report was itself bare [Authorize]. The consequence, accepted: non-admin users will now see compliance data across properties. The server-side hole is pre-existing and is neither widened nor closed here.

A Playwright test asserts a genuine non-admin (plugin-access claim only) can load the page — so re-adding a guard fails CI rather than silently reversing the decision.

The filter machine

This is the substance of the issue, and where the review found most of its defects.

  • Every filter change blanks the results. All seven controls route through setFilter, which resets the page, clears total, sets reportVisible = false and does not fetch. Only Opdater tabel emits on fetchRequested$.
  • Mode switches and the Oversigt drill-down bypass that via setFilterSilently, which has exactly two callers, both inside the service — unreachable from a template.
  • Fixed periods and YTD are bounded above by today, so future-dated tasks are excluded unless a custom range reaches forward. This deliberately differs from the calendar view, which extends into the future for open/all. addClampedMonths handles 31 May − 3 mdr. → 28/29 Feb.
  • Vis alle sends pageSize: 0feat(compliance): server-side paging, sorting and multi-select filters for the compliance report #1161's unpaged path, capped server-side at 5000.
  • Status is disabled in Oversigt with its explanatory tooltip on a wrapper that actually receives pointer events.

Design and theming

Built from Angular Material and existing eForm SCSS — the mockup's hand-rolled Google-Calendar CSS is not imported. The current calendar compliance view is not theme-aware (hardcoded #fff/#e0e0e0); this replacement is: the new stylesheets contain zero colour literals, and every custom property they use is defined in both _eform.scss and _workspace.scss, in the light and dark maps.

Caveat, stated plainly: both browser automation servers were unavailable this session, so the theme claim rests on a token audit, not on rendered screenshots. A token audit cannot cover what the page inherits — the mat-card surface, the mode-toggle pills, the form-field pill height. The "screenshotted under both themes" criterion is not met; the no-hardcoded-colour half is.

i18n

14 keys added to enUS.ts and propagated. Every one of the 25 other locale files gained exactly 14 lines; enUS +22 (14 keys + comments); translates.ts unchanged; zero removed lines anywhere — so neither of the propagation script's known failure modes (mass-backfill, truncation on escaped quotes) fired. Danish hand-corrected. Previous/Next/of are left to the core frontend rather than shadowed.

Menu entry — read this before testing

The entry is added to GetNavigationMenu (Position 13, Permissions = []), modelled on the Adhoc overview entry, with a Link matching the Angular route exactly.

It will not appear on an existing install. The plugin's menu seeding only inserts MenuItem rows on first install — a pre-existing core bug, not fixed here. To see it: Admin → Plugins → disable the plugin, re-enable it, restart the backend. The page is deep-linkable meanwhile, which is why the e2e navigates by URL.

Reviewed, and what the review caught

Gate 1 found nine issues; seven were real defects, all fixed:

  1. Every mtx-select shipped with no accessible name. MtxSelect has its own host binding for attr.aria-label bound to an @Input aliased aria-label; an attribute binding never reaches that input, so Angular removed the attribute on the first change-detection pass. Seven unlabelled comboboxes. Now bound as an input.
  2. The filter bar never reacted to state it did not write, so the board list went stale on re-entry (module-scoped state survives navigation) and would have broken feat(compliance): Oversigt view with per-property compliance rates and drill-down #1164's drill-down — Kalender rendering enabled-and-empty while still serialising a stale boardIds. Now driven off filters$.
  3. The mode switch had no query triggerfetchRequested$ was a plain Subject and ngSwitch destroys the child, so a child created by a mode switch would never receive the emission and would render empty. Now a ReplaySubject(1) gated on reportVisible. This also exposed that the container's *ngIf included !loading, which would unmount/remount the child on a loading toggle and, with a replayable trigger, loop.
  4. overflow-x: hidden made the page a clipping container, trapping the inline ng-select dropdown panel. Removed; flex-wrap already handled the wrapping.
  5. The "issues no network request" e2e assertion was vacuous.
  6. Two routing-spec tests could not fail for the reason they named.
  7. requestModel fabricated a one-day window for an incomplete custom range instead of omitting the period.

Deliberately not done

Verification

ng build --configuration development succeeds and emits the compliance-report lazy chunk. Playwright type-check is clean under playwright/. Tests were not run locally; CI runs them.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LcMeVFxqWqQzvqHjAa3Xkc

Copilot AI lite review requested due to automatic review settings September 4, 2026 05:27

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The Playwright suite currently uses avoidable sleeps and non-fail-fast login waits, and the custom date-range inputs need accessible names to meet basic a11y expectations.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Implements the foundation for the new standalone Compliance page in the Backend Configuration plugin: a lazy-loaded route with a state-driven filter bar, a view-mode toggle, and a shared results container + pagination chrome that upcoming PRs (#1164/#1165/#1167) will populate.

Changes:

  • Adds the /plugins/backend-configuration-pn/compliance-report route (AuthGuard-only) and a corresponding backend navigation menu entry.
  • Introduces a module-scoped ComplianceReportStateService (with Jest unit tests) that enforces the “blank-on-change, fetch-on-button” state machine and paging/sorting inputs.
  • Adds the page shell + 10-control filter bar UI, i18n keys across locales, and a Playwright suite covering the shell and non-admin access.
File summaries
File Description
eFormAPI/Plugins/BackendConfiguration.Pn/BackendConfiguration.Pn/EformBackendConfigurationPlugin.cs Adds plugin menu entry for the standalone Compliance page.
eform-client/src/app/plugins/modules/backend-configuration-pn/services/index.ts Exports the new compliance-report API service.
eform-client/src/app/plugins/modules/backend-configuration-pn/services/backend-configuration-pn-compliance-report.service.ts Adds API client for api/backend-configuration-pn/compliance-report/index.
eform-client/src/app/plugins/modules/backend-configuration-pn/modules/compliance-report/store/index.ts Store barrel export for the compliance-report module.
eform-client/src/app/plugins/modules/backend-configuration-pn/modules/compliance-report/store/compliance-report-state.service.ts Implements module-scoped state machine for filters/mode/paging/fetch triggers.
eform-client/src/app/plugins/modules/backend-configuration-pn/modules/compliance-report/store/compliance-report-state.service.spec.ts Unit tests pinning the state-machine contract (blank-on-change, replayed fetch trigger, drill-down unwind).
eform-client/src/app/plugins/modules/backend-configuration-pn/modules/compliance-report/components/index.ts Components barrel export for the compliance-report module.
eform-client/src/app/plugins/modules/backend-configuration-pn/modules/compliance-report/components/compliance-report-page/compliance-report-page.component.ts Page shell: mode toggle, shared container, pagination handlers, and initial Oversigt auto-fetch.
eform-client/src/app/plugins/modules/backend-configuration-pn/modules/compliance-report/components/compliance-report-page/compliance-report-page.component.scss Theme-token-only styling for the shell layout and pagination chrome.
eform-client/src/app/plugins/modules/backend-configuration-pn/modules/compliance-report/components/compliance-report-page/compliance-report-page.component.html Shell template: filter bar, mode toggle, result container + placeholder/spinner, pagination chrome.
eform-client/src/app/plugins/modules/backend-configuration-pn/modules/compliance-report/components/compliance-report-filters/compliance-report-filters.component.ts Implements the 10-control filter bar and reference-data loaders wired to state.
eform-client/src/app/plugins/modules/backend-configuration-pn/modules/compliance-report/components/compliance-report-filters/compliance-report-filters.component.scss Theme-token-only filter bar styling + width overrides for mtx-select fields.
eform-client/src/app/plugins/modules/backend-configuration-pn/modules/compliance-report/components/compliance-report-filters/compliance-report-filters.component.html Filter bar template (mtx-select accessibility binding, custom range UI, export controls).
eform-client/src/app/plugins/modules/backend-configuration-pn/modules/compliance-report/compliance-report.routing.ts Declares the compliance-report module route.
eform-client/src/app/plugins/modules/backend-configuration-pn/modules/compliance-report/compliance-report.module.ts Declares lazy module + Material imports + module-scoped state provider.
eform-client/src/app/plugins/modules/backend-configuration-pn/models/index.ts Exports compliance-report models.
eform-client/src/app/plugins/modules/backend-configuration-pn/models/compliance-report/index.ts Barrel export for compliance-report models.
eform-client/src/app/plugins/modules/backend-configuration-pn/models/compliance-report/compliance-report.model.ts Defines request/response DTOs for the standalone compliance report endpoint.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/bgBG.ts Adds Compliance page shell/filter/pagination i18n keys.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/csCZ.ts Adds Compliance page shell/filter/pagination i18n keys.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/da.ts Adds Compliance page shell/filter/pagination i18n keys.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/deDE.ts Adds Compliance page shell/filter/pagination i18n keys.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/elGR.ts Adds Compliance page shell/filter/pagination i18n keys.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/enUS.ts Adds Compliance page shell/filter/pagination i18n keys (with comments).
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/esES.ts Adds Compliance page shell/filter/pagination i18n keys.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/etET.ts Adds Compliance page shell/filter/pagination i18n keys.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/fiFI.ts Adds Compliance page shell/filter/pagination i18n keys.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/frFR.ts Adds Compliance page shell/filter/pagination i18n keys.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/hrHR.ts Adds Compliance page shell/filter/pagination i18n keys.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/huHU.ts Adds Compliance page shell/filter/pagination i18n keys.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/isIS.ts Adds Compliance page shell/filter/pagination i18n keys.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/itIT.ts Adds Compliance page shell/filter/pagination i18n keys.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/ltLT.ts Adds Compliance page shell/filter/pagination i18n keys.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/lvLV.ts Adds Compliance page shell/filter/pagination i18n keys.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/nlNL.ts Adds Compliance page shell/filter/pagination i18n keys.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/noNO.ts Adds Compliance page shell/filter/pagination i18n keys.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/plPL.ts Adds Compliance page shell/filter/pagination i18n keys.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/ptBR.ts Adds Compliance page shell/filter/pagination i18n keys.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/ptPT.ts Adds Compliance page shell/filter/pagination i18n keys.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/roRO.ts Adds Compliance page shell/filter/pagination i18n keys.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/skSK.ts Adds Compliance page shell/filter/pagination i18n keys.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/slSL.ts Adds Compliance page shell/filter/pagination i18n keys.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/svSE.ts Adds Compliance page shell/filter/pagination i18n keys.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/ukUA.ts Adds Compliance page shell/filter/pagination i18n keys.
eform-client/src/app/plugins/modules/backend-configuration-pn/backend-configuration-pn.routing.ts Adds lazy compliance-report sibling route with AuthGuard-only.
eform-client/src/app/plugins/modules/backend-configuration-pn/backend-configuration-pn.routing.spec.ts Adds route-placement + guard assertions for compliance-report.
eform-client/playwright/e2e/plugins/backend-configuration-pn/s/compliance-page-shell.spec.ts Adds Playwright suite for the Compliance page shell + non-admin access.
Review details

Suppressed comments (1)

eform-client/playwright/e2e/plugins/backend-configuration-pn/s/compliance-page-shell.spec.ts:72

  • loginAs swallows both the token response wait and the post-login URL wait (via .catch()), and then uses waitForTimeout(1000). This can let the helper continue even when login fails, producing confusing downstream failures. Make the waits fail-fast and replace the sleep with a deterministic UI wait.
  const loginResponsePromise = page.waitForResponse(
    (r) => r.url().includes('/api/auth/token'),
    { timeout: 30000 },
  ).catch(() => null);
  await loginBtn.click();
  • Files reviewed: 47/47 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

async function goToCompliancePage(page: Page): Promise<void> {
await page.goto(BASE_URL);
await new LoginPage(page).login();
await page.waitForTimeout(2000);
Comment on lines +163 to +166
<mat-date-range-input [rangePicker]="rangePicker">
<input matStartDate id="complianceCustomFrom" [(ngModel)]="customFrom" />
<input matEndDate id="complianceCustomTo" [(ngModel)]="customTo" />
</mat-date-range-input>
renemadsen and others added 2 commits September 4, 2026 07:49
)

Add the standalone Compliance page at
/plugins/backend-configuration-pn/compliance-report: its route, lazy module,
left-menu entry, the shared ten-control filter bar, the three-way view-mode
toggle and the shared result container plus pagination.

The page is its own route rather than a calendar view mode, and carries no
access gating -- AuthGuard only, no permission guard, no requiredPermission,
and Permissions is empty on the menu entry. That is a deliberate change: the
calendar hid the Compliance option behind four client-side admin guards while
the endpoint itself was bare [Authorize], so non-admins will now see compliance
data across properties.

The filter machine is the substance. Every one of the seven filter controls
routes through setFilter, which blanks the results, clears pagination and does
NOT fetch; only Opdater tabel emits on fetchRequested$. Mode switches and the
Oversigt drill-down bypass that invalidation through setFilterSilently, which
is reachable only from inside the service. Fixed periods and YTD are bounded
above by today, so future-dated tasks are excluded unless a custom range
reaches forward -- a deliberate divergence from the calendar view, which
extends into the future for open/all. Vis alle sends pageSize 0, the unpaged
path #1161 caps at 5000 rows.

fetchRequested$ is a ReplaySubject(1) gated on reportVisible: the view modes
are an ngSwitch, so a mode switch destroys and recreates the child, and a child
created after the Opdater tabel emission would otherwise never receive it and
would render empty. The result container's *ngIf deliberately does not include
!loading, because unmounting the child on a loading toggle would replay the
trigger and loop.

Built from Angular Material and existing eForm SCSS rather than the mockup's
hand-rolled CSS. The new stylesheets contain no colour literals; every custom
property used is defined in both theme-eform and theme-workspace, in their
light and dark maps.

14 i18n keys added to enUS.ts and propagated; every one of the 25 other locale
files gained exactly 14 lines, with no truncation and no mass-backfill. Danish
hand-corrected. Previous/Next/of are left to the core frontend.

The result container stays empty until #1164/#1165/#1167 land -- those issues
own the queries and call setTotalCount.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcMeVFxqWqQzvqHjAa3Xkc
…abel (#1163)

Second-gate review findings.

Entering the page in Detaljer or Rapport re-fetched with no user gesture. The
state service is module-scoped and Angular caches a lazy NgModuleRef for the
app lifetime, so reportVisible, mode, page, total and the ReplaySubject buffer
all survived navigating away and back: the container rendered, the child
mounted, and the buffered trigger replayed into an unbounded row query. Section
6 forbids exactly that. A new enterPage() drops reportVisible for those two
modes, which closes the fetchRequested$ gate so the buffered trigger cannot
reach the new child, and clears the previous visit's total, page and loading so
the pagination chrome does not draw stale counts. Oversigt still auto-fetches
once on entry, and the within-visit mode-switch replay is untouched -- both are
now pinned by tests.

setFilter and setMode both unmount the active child but neither reset loading,
so a child torn down mid-flight could leave loading true forever. canFetch is
isPeriodValid && !loading, so Opdater tabel died until a page reload. Both now
reset it. setMode also failed to reset total, so a mode switch drew the previous
mode's count -- one row per property versus one per task, wrong by an order of
magnitude -- until the new child reported.

The e2e request counter was armed before the mode switch that is only its
setup. Once #1165 wires the Detaljer query that switch legitimately issues one
request, so the assertion would have failed on correct behaviour. The counter
now covers the filter change alone, and the URL substring lost its trailing
slash so a post to the controller root also counts.

drillIntoProperty captured preDrillStatus unconditionally, so a second drill
overwrote it with the "all" the first one forced and the unwind restored the
wrong status. Not reachable through today's UI, but #1164 builds on it next.

The first describe block is no longer serial; one failure was skipping the
other seven and losing signal. The non-admin block stays serial -- it shares
an account across its tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcMeVFxqWqQzvqHjAa3Xkc
@renemadsen
renemadsen force-pushed the feat/1163-compliance-page-shell branch from 0d712e9 to a7c8327 Compare September 4, 2026 05:49
@renemadsen
renemadsen merged commit 1943f3e into stable Sep 4, 2026
60 of 61 checks passed
@renemadsen
renemadsen deleted the feat/1163-compliance-page-shell branch September 4, 2026 06:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants