* fix(ux): smoothness follow-ups - detail pages, batches, toasts, and the last edges
Follow-up batch to #1629: the six documented deferred items from
dev_docs/loading_states_analysis.md, in the same vocabulary (first-load-only
takeovers, background reconcile behind mounted content, row/button-level
pending, sequence guards).
- Invoice detail pages: kundfaktura and leverantorsfaktura detail no longer
blank the whole page for one-field changes. fetchInvoice shows the blocking
spinner/skeleton only before the first paint (or when the pager steps to a
different invoice); Bokfor / status / finalize / payment / send / Attestera /
Markera betald / kreditera refetch behind the mounted page, the acting
button shows a spinner-in-button, and the handlers await the refetch so
pending covers until the content reflects the new state. The supplier
detail's single isProcessing boolean became processingAction so the spinner
lands on the clicked button only. (The leverantorsfakturor LIST
try/catch/res.ok item was already fixed by #1629.)
- useDestructiveConfirm: confirm(opts, action?) can now carry the destructive
operation, so the dialog's existing isLoading spinner actually shows while
it runs, dismissal is blocked meanwhile, and confirm resolves false if the
action throws. Adopted at the /transactions row delete and the supplier-
invoice detail delete (which previously permitted duplicate DELETEs with
zero feedback).
- Batch parallelization: new lib/concurrency.ts mapWithConcurrency (bounded
worker pool, order-preserving, tested). /transactions batch categorize /
ignore / delete run per-row requests 5 at a time instead of strictly
sequentially; the bulkbar counter ticks per completed row.
- Toast-spam reduction: batch categorize rows run silent (exit animation,
count decrement and state patch stay; no per-row Bokford or generic failure
toast) and ONE aggregate toast reports "N bokforda[, M misslyckades]" with
a single Angra alla action that pools the same /uncategorize endpoint over
every booked row (per-row undo is feasible today, so the aggregate is too).
Interactive escalations (SI/CI match suggestions, duplicate warning,
activate-account) deliberately keep their dialogs.
- Underlag row-click flash: InvoiceInboxWorkspace handleSelect seeds the
detail pane synchronously from the clicked list row and starts the document
load in parallel with the detail GET (which hydrates on arrival), so a row
click never flashes the onboarding/empty state, and a stale-response guard
keeps a slow fetch from overwriting a newer selection.
- #1629 round-2 edges: /pending holds the loading state when a fetch for a
not-yet-loaded tab FAILS (never renders the previous tab's rows under the
new tab's header, and never fakes an empty state); /transactions clears
transactions/skvRows (+ count/paging) and bumps both fetch sequences on
company switch, and loadSkvRows got the same sequence-guard pattern as
fetchTransactions.
Gates: full vitest suite green (14772 passed), tsc byte-identical to the
origin/main baseline (stash-diffed), eslint 0 errors on touched files
(warnings identical to baseline), check:guards green, package-lock untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): harden action feedback against stale responses and failures
Address the seven CodeRabbit findings on #1633:
- invoices/[id] + supplier-invoices/[id]: latest-request guard in
fetchInvoice (sequence token) so a mutation refresh overlapping pager
navigation can never commit invoice A's state under invoice B's URL;
the deferred related-document writes are guarded too
- supplier-invoices/[id]: try/catch/finally in approve/book/mark-paid/
credit/uncredit so a rejected fetch()/json() clears processingAction
instead of leaving every invoice action disabled until reload
- transactions: extend the skattekonto sequence guard to the
connection-status write so a status response started under the
previous company cannot flip the reconnect banner for the new one
- transactions: runCategorize resolves { ok, journalEntryId } so the
batch aggregate counts a 200-with-null-journal-entry booking (flag
flip) as success instead of narrating it as misslyckades; Angra alla
only targets rows with an actual verifikat, since the storno endpoint
rejects rows without one
- transactions: shared undoneIdsRef lets "Angra alla" cancel a pending
finishBooking state patch; a fresh booking clears its row's entry so
re-booked rows still get their delayed patch
- InvoiceInboxWorkspace: monotonic request tokens for the detail and
document reads so a same-item reload cannot resolve out of order and
paint a stale snapshot or document URL
- messages: ICU plural for the success part of both partial batch
descriptions in sv and en (1 bokford, not 1 bokforda)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>