feat(compliance): Oversigt view with per-property compliance rates and drill-down (#1164) - #1179
Conversation
…1164) Add the default view mode of the standalone Compliance page: one row per property with an overdue count and a compliance percentage, a weighted totals row, client-side sorting, and a row click that drills into Detaljer filtered to that property. Nothing is recomputed. The percentages, the overdue counts and the weighted totals all come verbatim from the aggregation endpoint -- which is exactly why #1162 put that maths server-side, so the export renders the same numbers as the screen. The request is built field by field, so the parameters the aggregation endpoint does not have (status, paging, sort) provably never reach the wire. Sorting is client-side over a handful of rows, as the issue requires. What belongs here is presentation: the empty-cell en dash for a property where nothing has fallen due, the three compliance bands and the overdue urgency band, and the thresholds behind them. The drill-down calls the shell's existing drillIntoProperty and nothing else -- no filter writes of its own -- so the silent set, the forced status and the unwind on return stay in one place. The totals row is deliberately not drillable: no handler, no tabindex, no role. Rows carry tabindex and Enter/Space but NOT role="button". Re-roling a <tr> orphans its cells from the table structure, so a screen-reader user would trade the two numbers the row exists to report for the word "button". The prototype makes the same call. The shell's pagination is now shown only in Detaljer. Oversigt is one row per property and its endpoint has no paging parameters; Rapport does not paginate either, since each sub-report is shown whole. That is the prototype's own behaviour in both cases, and doing it now keeps #1167 off this shared line. Colours are measured, not assumed. The card is painted by a theme override of Material's elevated-card token rather than by the wrapper token, so the text here uses a muted token that clears 4.5:1 against the card under both readings of which token wins, in all four theme and mode combinations. The neutral pill gains a border: its fill is barely over 1:1 against the card in light mode and had no visible shape at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LcMeVFxqWqQzvqHjAa3Xkc
There was a problem hiding this comment.
🟡 Changes recommended
The new Playwright suite introduces fixed waitForTimeout() sleeps that should be replaced with condition-based waits to keep CI shards fast and non-flaky.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Implements the Compliance “Oversigt” (overview) mode for the standalone compliance report page, wiring it into the existing page shell and backend aggregation contract so the UI displays per-property compliance/overdue with a totals row and drill-down into Detaljer.
Changes:
- Added an Oversigt aggregation client API + DTOs and a pure helper for Oversigt sorting/formatting/banding.
- Introduced
ComplianceOverviewViewComponent(table, accessibility behaviors, drill-down) and integrated it into the compliance-report page shell, including gating pagination to Detaljer-only. - Added Jest unit tests for the helper/request-reduction guarantees and Playwright coverage for the Oversigt view + updated shell e2e expectations.
File summaries
| File | Description |
|---|---|
| eform-client/src/app/plugins/modules/backend-configuration-pn/services/backend-configuration-pn-compliance-report.service.ts | Adds overview() API call + endpoint constant for Oversigt aggregation. |
| eform-client/src/app/plugins/modules/backend-configuration-pn/modules/compliance-report/helpers/index.ts | Re-exports the new Oversigt helper. |
| eform-client/src/app/plugins/modules/backend-configuration-pn/modules/compliance-report/helpers/compliance-overview.helper.ts | Pure client-side sorting/formatting/banding logic for Oversigt. |
| eform-client/src/app/plugins/modules/backend-configuration-pn/modules/compliance-report/helpers/compliance-overview.helper.spec.ts | Unit tests pinning helper behavior (sorting, thresholds, en-dash rendering). |
| eform-client/src/app/plugins/modules/backend-configuration-pn/modules/compliance-report/components/index.ts | Exports the new Oversigt component. |
| eform-client/src/app/plugins/modules/backend-configuration-pn/modules/compliance-report/components/compliance-report-page/compliance-report-page.component.html | Mounts Oversigt view and gates pagination <nav> to mode === 'details'. |
| eform-client/src/app/plugins/modules/backend-configuration-pn/modules/compliance-report/components/compliance-overview-view/compliance-overview-view.component.ts | Oversigt component: fetches aggregation, reduces request fields, client-side sorting, drill-down. |
| eform-client/src/app/plugins/modules/backend-configuration-pn/modules/compliance-report/components/compliance-overview-view/compliance-overview-view.component.html | Oversigt table template with sortable headers, keyboard row activation, totals row. |
| eform-client/src/app/plugins/modules/backend-configuration-pn/modules/compliance-report/components/compliance-overview-view/compliance-overview-view.component.scss | Theme-token-only styling, contrast-aware muted text, pill/overdue banding styles. |
| eform-client/src/app/plugins/modules/backend-configuration-pn/modules/compliance-report/components/compliance-overview-view/compliance-overview-view.component.spec.ts | Unit tests pinning request reduction + total count semantics. |
| eform-client/src/app/plugins/modules/backend-configuration-pn/modules/compliance-report/compliance-report.module.ts | Declares the new Oversigt component in the module. |
| eform-client/src/app/plugins/modules/backend-configuration-pn/models/compliance-report/compliance-report.model.ts | Adds Oversigt request/response interfaces (rows + weighted totals). |
| eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/enUS.ts | Adds Oversigt-related translation keys. |
| eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/da.ts | Adds Danish translations for Oversigt keys. |
| eform-client/playwright/e2e/plugins/backend-configuration-pn/s/compliance-page-shell.spec.ts | Updates shell assertions to match pagination being Detaljer-only. |
| eform-client/playwright/e2e/plugins/backend-configuration-pn/s/compliance-overview.spec.ts | New Oversigt Playwright suite: columns/order, en-dash, sorting aria-sort, drill-down, totals row, empty state. |
Review details
Suppressed comments (2)
eform-client/playwright/e2e/plugins/backend-configuration-pn/s/compliance-overview.spec.ts:175
- Avoid
page.waitForTimeout(500)after the response; wait for the UI to settle (e.g. the spinner container to hide) so failures are faster and the suite doesn't burn time across shards.
async function showReport(page: Page): Promise<void> {
const response = page.waitForResponse(
r => r.url().includes('/api/backend-configuration-pn/compliance-report/overview')
&& r.request().method() === 'POST',
{ timeout: 60000 },
);
await page.locator('#complianceShowReportBtn').click();
await response;
await page.waitForTimeout(500);
}
eform-client/playwright/e2e/plugins/backend-configuration-pn/s/compliance-overview.spec.ts:277
- This extra fixed sleep is likely redundant:
CalendarUiEnhancementsPage.selectProperty()already waits after the click, andopenCreateModalAtSlot()has its own explicit waits. Keeping thiswaitForTimeout(1000)slows the shard without increasing determinism.
await calendarPage.selectProperty(propertyName);
await folderResponse.catch(() => undefined);
await page.waitForTimeout(1000);
- Files reviewed: 16/16 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| await page.locator('.compliance-filters__custom-range mat-datepicker-toggle button').click(); | ||
| await selectDateRangeOnNewDatePicker( | ||
| page, | ||
| from.getFullYear(), from.getMonth() + 1, from.getDate(), | ||
| to.getFullYear(), to.getMonth() + 1, to.getDate(), | ||
| ); | ||
| await page.waitForTimeout(300); | ||
| } |
…1164) The Oversigt sort e2e failed in CI asserting that a property named AAA- comes before one named ZZZ- after a single click on the name header. It does not. The view collates with localeCompare(..., 'da'), and Danish treats the digraph aa as a variant of aa-with-ring, the LAST letter of the alphabet. So 'A' sorts before 'Z' but 'AA' sorts after it, and AAA-ovw- collates after ZZZ-ovw-. The fixture applied an ASCII assumption to a Danish collator; ascending really did put ZZZ first, and the second click would have failed for the same reason. The product is correct -- nextOverviewSort and sortSummaries both match the prototype and are unchanged. The seeded names are now BBB- and YYY-: no digraph, no ae/oe/aa equivalence and no v-w merge. The test also asserts its own premise with the same collator it is measuring, so a future rename fails by name rather than looking like a sort regression. While here, closed a second hole in the same test. Both seeded rows tie on every numeric column and sortSummaries breaks ties on the name ascending, so the landing order already matched what the first click was supposed to produce -- the row assertion would have passed even if the click never registered. The test now also asserts aria-sort after each click and walks the two-state cycle back to ascending. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LcMeVFxqWqQzvqHjAa3Xkc
Closes #1164. Part of #1160. Depends on #1162 and #1163 (both merged).
The page's default view: one row per property with an overdue count and a compliance percentage, a weighted totals row, client-side sorting, and a row click that drills into Detaljer filtered to that property.
Nothing is recomputed
Percentages, overdue counts and the weighted totals all come verbatim from
Overview(). That is why #1162 put the maths server-side — so the export renders the same numbers as the screen.nullpercentage renders the en dash–, never0.The request is built field by field, so
status/pageIndex/pageSize/sort/isSortDsc— none of which #1162's endpoint has — provably never reach the wire. A new component spec pins that, asserting first that the shared paged model does carry all five, then that the built request carries none, so the test cannot become trivially true.Sorting is client-side over a handful of rows, as the issue requires. Formatting, banding and the three thresholds live here, not in the backend.
Pagination is now Detaljer-only — a shared-file change
The shell's pagination
<nav>is now gated onmode === 'details'. Oversigt is one row per property and its endpoint has no paging parameters; Rapport does not paginate either — the prototype empties the same container there, with the comment "Rapport paginerer ikke - hver delrapport vises hel". OnlyrenderCasesPagefills it.Doing this now keeps #1167 off the same shared line. The shell e2e is updated accordingly, including two assertions that were previously absent for the Rapport and Oversigt steps, so the behaviour is locked rather than merely uncontradicted.
Accessibility, measured rather than assumed
Rows are
tabindex="0"with click and Enter/Space but notrole="button": re-roling a<tr>orphans its cells from the table structure, so a screen-reader user would trade the two numbers the row exists to report for the word "button". The prototype makes the same call. The<tfoot>totals row is deliberately not drillable — no handler, notabindex, norole.Contrast took three passes and is worth recording, because the first two were wrong:
#F7F9FA/#2D2F31.surface-container-low(#f1f4f3/#191c1c), not the wrapper token--content-card-bg— which has exactly one consumer in the whole client — and that the chosen token was therefore still failing AA in eform light (4.37)._theme-mixin.scss:266redeclares--mat-card-elevated-container-colorasmd-surface-container!important, at higher specificity than Material's own value — so the original surface was right after all.Rather than pick a side, the text now uses
--text-child-muted, which clears 4.5:1 under both readings, in all four theme × mode combinations: 5.44 / 5.29 / 6.05 / 9.69 against the real card (5.19 / 6.76 / 5.47 / 10.07 against Material's default). The column headers were separately still using--text-secondary— 3.49:1 in eform light, the very token the fix had removed — and now match.The neutral
is-nonepill gained a border: its fill is 1.09:1 against the card in eform light, i.e. no visible shape at all. The border clears the 3:1 non-text floor everywhere.Reviewed twice — 13 defects found
Gate 1 (8): the pagination guard broke a merged e2e assertion in the shell spec that expected pagination visible right after load — the page lands in Oversigt, so BC shard
swould have gone red in a test belonging to #1163/#1165. No e2e existed for the new view at all. Three of twelve documented contrast ratios were wrong and two cited the wrong surface. The empty state and calm counter failed AA in eform light. A comment described behaviour the same change removed. ThengOnDestroyrationale contradicted the file it cited. A test did not test its own title, and the load-bearingbuildRequestguarantee had no client-side test. The Jest case count was overstated as 24 when it is 22.Gate 2 (5): the contrast fix above was measured against the wrong surface and still failed; the column headers still used the removed token; the "corrected"
setTotalCountcomment was false — the filter bar's Download button is gated onstate.total, so dropping the call would leave Download permanently disabled after an Oversigt fetch; the pagination guard diverged from the prototype for Rapport; and two stale comments sat inside the rewritten shell test.Tests
22 Jest cases for the sorting/formatting/banding helper (the count is stated honestly — the prototype has 24, of which 10 are the aggregation maths that now lives server-side in
ComplianceReportOverviewTests, and the render cases are covered against the real DOM by the new e2e instead). 7 component-spec cases for the request reduction. 11 Playwright cases in shards, self-seeding through the admin API.Three e2e behaviours are skipped with reasons, not asserted vacuously: band pills need a past-due occurrence and no API backdates one (the create path's past-
StartDateguard is live); a numeric re-order needs differing values, and both seeded rows legitimately carryoverdue = 0/pct = null; pagination-absent belongs to the shell suite that owns that template.Known and deliberate
enUSanddacarry the four new keys. The translation endpoint was unavailable; English fallbacks were deliberately not written into the other 24 locale files, since that breaks the auto-translation tooling. Those keys are English sentences, so a French user sees readable English rather than a missing-string marker, and a later script run picks them up. NoteOverdue— one of the three column headers — was alreadyenUS/da-only before this change.ngSwitchdestroys the component. Fixing it means moving sort onto the shared state service.--text-secondaryis used the same failing way in merged files (compliance-details-view.component.scss,compliance-report-page.component.scss). Deliberately not touched here — it wants its own issue.🤖 Generated with Claude Code
https://claude.ai/code/session_01LcMeVFxqWqQzvqHjAa3Xkc