Skip to content

feat(compliance): Oversigt view with per-property compliance rates and drill-down (#1164) - #1179

Merged
renemadsen merged 2 commits into
stablefrom
feat/1164-compliance-oversigt-view
Sep 4, 2026
Merged

feat(compliance): Oversigt view with per-property compliance rates and drill-down (#1164)#1179
renemadsen merged 2 commits into
stablefrom
feat/1164-compliance-oversigt-view

Conversation

@renemadsen

Copy link
Copy Markdown
Member

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. null percentage renders the en dash , never 0.

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 on mode === '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". Only renderCasesPage fills 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 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 <tfoot> totals row is deliberately not drillable — no handler, no tabindex, no role.

Contrast took three passes and is worth recording, because the first two were wrong:

  1. The implementation measured against #F7F9FA / #2D2F31.
  2. Review found the mat-card is painted by Material's 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).
  3. Re-checking found _theme-mixin.scss:266 redeclares --mat-card-elevated-container-color as md-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-none pill 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 s would 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. The ngOnDestroy rationale contradicted the file it cited. A test did not test its own title, and the load-bearing buildRequest guarantee 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" setTotalCount comment was false — the filter bar's Download button is gated on state.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 shard s, 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-StartDate guard is live); a numeric re-order needs differing values, and both seeded rows legitimately carry overdue = 0 / pct = null; pagination-absent belongs to the shell suite that owns that template.

Known and deliberate

  • Only enUS and da carry 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. Note Overdue — one of the three column headers — was already enUS/da-only before this change.
  • Sort state resets on a mode round-trip. The prototype keeps it in a module-level variable; here it is component state and the ngSwitch destroys the component. Fixing it means moving sort onto the shared state service.
  • No browser or screenshot verification. Every contrast figure is computed from token values, not measured on screen.
  • --text-secondary is 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

…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
Copilot AI lite review requested due to automatic review settings September 4, 2026 09:06

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 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, and openCreateModalAtSlot() has its own explicit waits. Keeping this waitForTimeout(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.

Comment on lines +156 to +163
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
@renemadsen
renemadsen merged commit 875038d into stable Sep 4, 2026
31 checks passed
@renemadsen
renemadsen deleted the feat/1164-compliance-oversigt-view branch September 4, 2026 10:01
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