Skip to content

feat(chemicals-web): PR E3 — inventory, history, exports (W1, W2, W3, W6) - #1437

Merged
renemadsen merged 63 commits into
stablefrom
feat/chemicals-web-inventory
Oct 18, 2026
Merged

renemadsen merged 63 commits into
stablefrom
feat/chemicals-web-inventory

Conversation

@renemadsen

@renemadsen renemadsen commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

Summary

Plan E, PR E3 of the Kemi (chemicals) web admin: the inventory, location, history and export screens on top of E1 (W4 Rettigheder, #1430) and E2 (W5 Indstillinger, #1434). Tasks 9–16 of the plan (flutter-chemistry spec §5.1, §6, §11).

Screens (Angular, modules/chemicals/)

  • W1 Beholdning (/chemicals/inventory): the property's open placements in a grid with hazard pictograms, deadlines and (stock on) the balance. "Kræver handling" chip, banner and red/amber rows follow the same rules as the server's alert job. "Registrér placering" searches the BMD register. The row menu opens details, move, note, stock, remove and the safety data sheet. The page has explicit loading and load-failed states, so it never looks empty while loading.
  • W2 side panel and dialogs: product details, placements and stock history. Move (full or partial), note, stock movement (received, consumed, adjusted) and remove (used or disposed, with a hazardous-waste warning). Every dialog stays open on a refusal. Every W2 dialog asks before discarding typed input (the W1 register dialog has no discard confirm yet; follow-up).
  • W3 Lokationer (/chemicals/locations): create, edit with photo, reorder by drag or up/down buttons (the order is saved and the app uses it), archive after a confirm (refused while the location has open placements), "show archived".
  • W6 Historik (/chemicals/history): the immutable log derived from the version tables, the stock ledger and ChemicalAlertLog. It is paged on the server and filtered by period (Europe/Copenhagen days), user, action and location, with an Excel export of the whole filtered period.
  • Exports on W1: authority list PDF (QuestPDF), Excel of the current filter, Excel of removed placements. Downloads run on a raw client (no host interceptors), so each failure is toasted on the page: the server's reason after the localized "Export failed", "not allowed" for a 403, or the generic text.
  • Digest deeplink (Plan D C4): /chemicals?propertyId=…&filter=requires-action redirects to W1 with the property and the chip set.

Server (BackendConfiguration.Pn)

  • ChemicalReportsController adds five routes under api/backend-configuration-pn/chemicals/properties/{propertyId}:

    • POST history
    • POST history/export
    • GET exports/authority-list
    • POST exports/inventory
    • GET exports/removed-placements

    Each route requires the plugin policy and [FirstUserOnly] (TODO chemistry-GA), and checks IChemicalPermissionService on the property.

    • JSON failures answer 200 + Success=false with ": ".
    • File failures keep their status. Unexpected errors go to Sentry and are answered without the raw message.
  • ChemicalHistoryService and ChemicalExportService: every query is keyed on the property. One ledger fold (BalanceAfterByEntry, Id order) is shared with the inventory, and the removal write-off is folded into the Removed event.

  • Excel cells are always inline strings (never formulas), and control characters are stripped.

  • ChemicalBase:BaseUrl builds the SDS links; the file name is URL-escaped.

  • QuestPDF licence (E-08): QuestPDF.Settings.License = LicenseType.Community is set once in AddChemicalReporting and pinned by a test. Microting is within the Community limits (owner, 2026-10-14), so no commercial licence is needed.

Menu sync: all five Kemi children

PluginMenuSyncService (from SeedDatabase, keyed on the menu template E2EId) adds the missing children on an existing tenant's next startup: Beholdning 0, Lokationer 1, Rettigheder 2, Indstillinger 3, Historik 4. It makes room when an admin has renumbered the group, never re-adds what an admin deleted in the menu editor, and never rewrites a kept row. No plugin disable/enable is needed.

Date fix (T16)

The host's DateInterceptor turns every ISO date-time string in a JSON answer into a Date, and W1 crashed on a deadline (value.substring is not a function). restoreIsoDates at the Kemi service boundary turns those back into ISO UTC text (…000Z) on every JSON method. Offset-less server times are read as UTC, so the day is kept. This is pinned by a round trip through the real DateInterceptor.

Why W6 Details is formatted twice (E-13)

W6 Details is formatted twice: in ChemicalHistoryDetailsFormatter (C#, for the Excel the server builds) and in formatHistoryDetails (TS, for the page). The page copy lets W6 re-translate live without a round trip, and the paged API stays structured data. Parity is pinned by the same input→output vectors, copied into each suite (C# and TS), and by ChemicalHistoryWordsParityTests (the same words in all 26 locales).

Tests

  • C# unit and integration tests: history derivation, ledger order, Copenhagen days, period validation, property scoping of W6 and its Excel against a second property, xlsx sanitising, the PDF builder, the controller gate on every route, and the menu sync for E1/E2/E3 tenants, renumbered groups, deletions and races.
  • Jest: every service route, restoreIsoDates, page load and failure states, dialogs, download toasts.
  • Playwright (in-CI stack only, no retries, no sleeps):
    • shard h: W1/W2 register, overdue, deeplink, remove, move and the three exports (each download asserted to have completed);
    • shard k: W3 create, drag and up/down persisted across a reload, archive;
    • shard o: W6 log, server-side action filter, Excel.

Owner items (outside this PR)

  1. Host HttpErrorInterceptor re-sends non-idempotent requests. Its default branch re-runs any request up to 5 times, 15 s apart, including POST and PUT, and then ends with no answer.
    • A gateway 502/504 that arrives after the server has committed could double a Kemi stock entry, a registration or a move.
    • Kemi narrows this: unexpected server errors answer 200 + Success=false, so only infrastructure 5xx trigger it.
    • Possible host fix: retry only GET/HEAD, or only on status 0. Longer term, give the commands an idempotency key.
  2. Global .flex-column { flex: 1 0 0 } (host styles.scss). It collapses any scroll container with that class inside a flex column. Kemi's register dialog is fixed locally; other plugin screens may be exposed in the same way.
  3. Host DateInterceptor.
    • It has no opt-out, and its regex is unanchored, so free text that contains an ISO date-time also becomes a Date.
    • Plugins work around it separately (Kemi restoreIsoDates, compliance report, adhoc history).
    • Possible host fix: an HttpContextToken opt-out and an anchored regex.
  4. Flaky tests on stable that can block this merge gate:
    • ArchiveConcurrencyTests ("not blocked on the folder lock within 30 s"), also red on stable's own push run 38906317104;
    • UpdatePlanning_AreaRuleMonths24_ReturnsSuccess (MariaDB 1205 lock wait).
  5. Release steps:
    • Run the Step 9 checklist (no plugin disable/enable needed).
    • The "SDS links resolve without login" check depends on chemicalbase being reachable.
    • A tenant that ran a pre-release E3 build may keep /chemicals as Beholdning's link. Check tenant 855 or any test tenant, and fix that row by hand.

🤖 Generated with Claude Code

https://claude.ai/code/session_0122jmSDSB5HSHQK36Bnt5Wy

renemadsen and others added 30 commits October 16, 2026 06:21
…tries and alert log; xlsx writer

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122jmSDSB5HSHQK36Bnt5Wy
…ys, xml-invalid chars, removed placements, remaining actions

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122jmSDSB5HSHQK36Bnt5Wy
…days, drops XML-invalid chars, skips removed placements

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122jmSDSB5HSHQK36Bnt5Wy
…emporarily restore the old implementation to prove red

The two tests failed on arrange (removal before registration, stock not enabled), not on behaviour. The next commit restores the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122jmSDSB5HSHQK36Bnt5Wy
…ays, XML-safe xlsx text, removed placements)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122jmSDSB5HSHQK36Bnt5Wy
…ent of its arrange

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122jmSDSB5HSHQK36Bnt5Wy
…st PDF, Excel, formatter, licence)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122jmSDSB5HSHQK36Bnt5Wy
…y/removed/history Excel exports

Removed history details now show the folded write-off amount; QuestPDF Community licence set once at start-up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122jmSDSB5HSHQK36Bnt5Wy
…ation tests run the licence start-up

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122jmSDSB5HSHQK36Bnt5Wy
…er, page count, real history assertions

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122jmSDSB5HSHQK36Bnt5Wy
… filter in the PDF, SDS address on paper

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122jmSDSB5HSHQK36Bnt5Wy
…lookup, one xlsx tail, one PDF column set

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122jmSDSB5HSHQK36Bnt5Wy
…e-only period and first-user gate

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122jmSDSB5HSHQK36Bnt5Wy
…hape, checks View, hides unexpected messages

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122jmSDSB5HSHQK36Bnt5Wy
…View, hide unexpected messages

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122jmSDSB5HSHQK36Bnt5Wy
…ng menu child, no-redirect root and deferred URL write (red)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122jmSDSB5HSHQK36Bnt5Wy
…rts and register dialog

Beholdning is the Kemi root (no redirect, M-3) with its own menu child; the
container writes ?propertyId= only once no navigation is in flight (R-2);
every export button toasts its own failure and shows a busy state (m-2).
The container specs read the Router.currentNavigation signal instead of the
deprecated getCurrentNavigation() (mock member rename only).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122jmSDSB5HSHQK36Bnt5Wy
…Model needs its value accessor)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122jmSDSB5HSHQK36Bnt5Wy
…ing, /chemicals/inventory with root redirect, menu renumbering (red)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122jmSDSB5HSHQK36Bnt5Wy
… placement wording, /chemicals/inventory

- I-1 loading and failed states; nothing exportable before the inventory loads
- I-2 a new search drops the pick; stale search answers are ignored
- I-3 placement texts re-translated as registered chemicals in 24 locales
- I-4 W1 at /chemicals/inventory; the Kemi root redirects there with its query
- m-1 property left to the container; m-2 chip off drops ?filter (R-2 safe)
- m-3 newest inventory answer wins; m-4..m-9 validation, barcode package,
  a11y, ids, row labels, export reasons; m-13 menu sync makes room on ties

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122jmSDSB5HSHQK36Bnt5Wy
… a technical error message

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122jmSDSB5HSHQK36Bnt5Wy
finalize frees the export button; summary, active locations and columns are
set with the inventory instead of per change detection; shared spec fixtures;
TODO(Task 13) on openDetails; dead CSS classes dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122jmSDSB5HSHQK36Bnt5Wy
Union-resolve localization.json (chemicals + archive folder keys).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122jmSDSB5HSHQK36Bnt5Wy
…-06/E-20, N-m1, N-m4 (red)

Specs only: helper placementStockHistory (server balanceAfter, origin labels)
and placementsOfChemical; the five new components; page chaining and row
actions; the drawer and row-menu templates; i18n key list and meaning guards.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122jmSDSB5HSHQK36Bnt5Wy
… remove dialogs

- Side panel (overview, placements, stock history with the server balance
  and move / removal write-off labels, E-06) chains move/stock/remove into
  their dialogs; W1 row menu gains move, note, stock (stock on) and remove.
- Stock dialog refuses an adjustment to the current balance (E-20).
- Register dialog drops a stale pick when a search answers (N-m1).
- Export toast never shows a raw HttpErrorResponse/Error message (N-m4).
- 45 new keys in 26 locales, MT from disambiguated sources, hand-fixed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122jmSDSB5HSHQK36Bnt5Wy
I-1 move meaning guard and M11 translation guards; M2 busy cancel, M3 amount
limits, M4 move empty state, M5 discard confirm, M6/M7 drawer close and name,
M8 tab pattern, M9 radio names; M10 boundaries, drawer tabs, N-m4 Error
branch; R1/R2 shared SDS and save helpers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122jmSDSB5HSHQK36Bnt5Wy

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.

🔵 Needs a closer look

Cross-layer reporting and tenant migration need integrated human validation, and the registration search race remains unresolved.

2 open issues

Review effort: Balanced

Every reload bumps a generation; an order save that answers after it no
longer takes over its older location snapshot, so an archive (or an edit)
done meanwhile stays visible (Copilot 4306102463).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122jmSDSB5HSHQK36Bnt5Wy
Copilot AI balanced review requested due to automatic review settings October 18, 2026 07:14

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.

🔵 Needs a closer look

One or more issues must be addressed before approval.

2 open issues

Review effort: Balanced

…once

Red test for Copilot review 4306130349: while a new search is pending the
old results must not be pickable (Save could register one). Also fixes
the W3 stale-order test, which miscounted the archived locations (there
are two); its red assertion on the visible rows is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122jmSDSB5HSHQK36Bnt5Wy
Copilot AI balanced review requested due to automatic review settings October 18, 2026 07:24

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.

🔵 Needs a closer look

Reporting, frontend workflows, and tenant-menu migration require final human validation against full-stack CI results.

2 open issues

Review effort: Balanced

While a new search was pending, an old result could still be picked and
saved. The results, total, "no match" state and searched barcode now go
at once; the answer fills them again (Copilot 4306130349).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122jmSDSB5HSHQK36Bnt5Wy
Copilot AI balanced review requested due to automatic review settings October 18, 2026 07:33

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.

🔵 Needs a closer look

The changes require final human review because they are too complex or risky for automated approval.

1 open issue
1 resolved since last review

Review effort: Balanced

@renemadsen

Copy link
Copy Markdown
Member Author

Copilot round 2, findings that appear only in the review bodies:

  • Review 5561808509 (head d6680b4), "previously missed issue": chemical-dialogs.helper.spec.ts:70, the new EMPTY test fails because openSafetyDataSheet has no completion handling. That was the intentional red test-only commit. The fix 67a8b2a adds the completion toast, and the test passes from there on.
  • Reviews 5561893338, 5561919047, 5561945325 and 5561970576 list only the inline threads (4306102463 fixed in 71ade2e, 4306130349 fixed in 1f99568) plus general "needs human validation" notes. Those notes have no code finding to act on.
    No i18n, low-confidence or suppressed items appeared in any body this round.

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