feat(compliance): standalone Compliance page shell, filter bar and view-mode toggle (#1163) - #1173
Conversation
There was a problem hiding this comment.
🟡 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-reportroute (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
loginAsswallows both the token response wait and the post-login URL wait (via.catch()), and then useswaitForTimeout(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); |
| <mat-date-range-input [rangePicker]="rangePicker"> | ||
| <input matStartDate id="complianceCustomFrom" [(ngModel)]="customFrom" /> | ||
| <input matEndDate id="complianceCustomTo" [(ngModel)]="customTo" /> | ||
| </mat-date-range-input> |
) 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
0d712e9 to
a7c8327
Compare
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
AuthGuardonly. No permission guard, norequiredPermission,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 whilePOST calendar/compliance-reportwas 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.
setFilter, which resets the page, clearstotal, setsreportVisible = falseand does not fetch. Only Opdater tabel emits onfetchRequested$.setFilterSilently, which has exactly two callers, both inside the service — unreachable from a template.open/all.addClampedMonthshandles 31 May − 3 mdr. → 28/29 Feb.Vis allesendspageSize: 0— feat(compliance): server-side paging, sorting and multi-select filters for the compliance report #1161's unpaged path, capped server-side at 5000.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.scssand_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-cardsurface, 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.tsand propagated. Every one of the 25 other locale files gained exactly 14 lines;enUS+22 (14 keys + comments);translates.tsunchanged; 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/ofare 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 aLinkmatching the Angular route exactly.It will not appear on an existing install. The plugin's menu seeding only inserts
MenuItemrows 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:
mtx-selectshipped with no accessible name.MtxSelecthas its own host binding forattr.aria-labelbound to an@Inputaliasedaria-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.boardIds. Now driven offfilters$.fetchRequested$was a plainSubjectandngSwitchdestroys the child, so a child created by a mode switch would never receive the emission and would render empty. Now aReplaySubject(1)gated onreportVisible. This also exposed that the container's*ngIfincluded!loading, which would unmount/remount the child on a loading toggle and, with a replayable trigger, loop.overflow-x: hiddenmade the page a clipping container, trapping the inline ng-select dropdown panel. Removed;flex-wrapalready handled the wrapping.requestModelfabricated a one-day window for an incomplete custom range instead of omitting the period.Deliberately not done
setTotalCount), so "Opdater tabel populates" is today only assertable as "placeholder disappears, pagination chrome appears".dateFrom/dateToentirely. The C# model's fields are non-nullableDateTime, so an absent key deserialises todefaultand the window collapses to an empty result — visibly nothing rather than a plausible-looking wrong day. An explicitnullwould 400. Giving the server real "no period" semantics would need aDateTime?and is out of scope.Verification
ng build --configuration developmentsucceeds and emits thecompliance-reportlazy chunk. Playwright type-check is clean underplaywright/. Tests were not run locally; CI runs them.🤖 Generated with Claude Code
https://claude.ai/code/session_01LcMeVFxqWqQzvqHjAa3Xkc