feat(supplier-invoices): dokument-forst editor rebuild (prototype shell + 4 flow optimizations) (#1653)
* refactor(supplier-invoices): extract payload builder and form hooks, pin wire contract with parity tests Zero visual/behavioral change. Pulls the pure payload builder (buildSupplierInvoicePayload + inferVatTreatment + vatRateFromAi) out of NewSupplierInvoiceForm into lib/supplier-invoices/form-payload.ts and pins it with a mode/feature-matrix parity test suite (document_id vs inbox, privately paid due-date default, reverse charge rate forcing, accrual attach/drop, dimensions bags, apply_slp validity, FX parsing, empty-string stripping, ore_rounding passthrough). Also extracts, verbatim: the VatRateCell/RcRateSelect cells, the reference data loading hook (suppliers/accounts/settings/periods), the inbox AI prefill hook (exposing applyInboxItem for reuse), and the submit orchestration hook (endpoint chooser, three submit paths, duplicate-number conflict recovery, inbox field sync-back). Deliberately NOT moved: the effect-ordering couplings (pendingAccountFillRef/accountFillTick supplier-defaults dance, the icke-momsregistrerad gross-up re-run keyed on hasPrefilled, the RC accrual-clearing effect, per-currency FX touched flags) stay in the component untouched; their ordering semantics are load-bearing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(supplier-invoices): dokument-forst editor rebuild with prototype shell and four flow optimizations Rebuilds NewSupplierInvoiceForm to the approved Leverantorsflodet prototype: single 640px column, flat sections (Underlag first, then Leverantor, Fakturauppgifter, Kontering, Forval, Summering), honest state marks (RequiredMark, sage checks for binary facts, muted row counts), a single ochre next-step line (aria-live polite) whose link focuses the missing field, and a sticky bottom action bar with the live total that binds to the dialog scroll container in bare mode and the page panel scroll standalone. Dokument-forst (1): the standalone upload now tries the invoice-inbox pipeline over HTTP first (POST upload, poll items/:id past 'processing'), then runs the same applyInboxItem prefill path as an inbox arrival (settle tint on filled fields, reset(getValues()) dirty baseline, submit through the convert endpoint so the document links and the item is stamped). Extension off or extraction failed degrades to the plain /api/documents attachment; manual entry is never blocked. Total cross-check (2): optional "Totalt enligt fakturan" field in Summering, client-only compare against the displayed payable (sage match line, terracotta diff line), prefilled from extraction totals. Duplicate advisory (3): new index-only GET /api/supplier-invoices/exists (withRouteContext + validateQuery, mirrors the partial unique index's credited/reversed exclusion, full route tests), debounce-called on fakturanummer change; terracotta field-adjacent line with a link to the existing invoice. The structured 409 conflict dialog stays the backstop. Terms-based due date (4): muted caption "Fran leverantorens villkor (N dagar)" when auto-set, re-derives on invoice-date and supplier change, stops the moment the user or the AI supplies a date; terms 0 leaves the field empty with "Star pa fakturan". OCR hint (5): "Anvands i betalningsfilen." under the payment reference when the chosen supplier has bankgiro or plusgiro. Table model: rows start empty; the ghost tfoot entry row (never part of form state) commits an account via the existing AccountCombobox (opens on focus, Enter commits) and moves focus to the new row's amount cell; the supplier default/history fill plants the first row when the table is empty. Row controls are hover-revealed via HOVER_REVEAL_CLASS at a 24px hit area with per-row aria-labels carrying the description. The primary button is never disabled pre-click for writable users (in-flight only); every submit-time hard block stays in onSubmit; viewers keep the lock treatment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(supplier-invoices): re-run gross-up per apply, guard deferred prefill, honest un-plant - Gross-up/zero-rate pass for icke momsregistrerade re-runs per applied extraction (applyCount bumps in applyInboxItem) instead of keying on the one-shot hasPrefilled flag: a remove + re-upload could previously push AI 25 % rates to the convert endpoint with the moms columns hidden. - Deferred extraction on the standalone upload path no longer overwrites what the user typed mid-poll: the result auto-applies only while the form is pristine (live isDirty ref), otherwise it is buffered behind a quiet "Tolkning klar" click-to-apply line. Inbox arrivals are unchanged. - Supplier-switch un-plant keeps rows the user edited in ANY field, not just amount (plant-time snapshot compare in lib/supplier-invoices/planted-rows.ts, since dirtyFields is unreliable for appended array rows), clearing only the stale account; untouched plant-created rows are still removed and rows that existed before the fill are never removed. - default_expense_account plants now register in plantedRef too, so a supplier switch un-plants them under the same rules as history plants. - applyInboxItem reads suppliers through a ref: the 90 s poll no longer resolves matched suppliers against a stale empty list. - The duplicate advisory bumps its seq in the clear branch, so an in-flight exists response cannot resurrect a warning under a cleared field. - Drop 7 orphaned supplier_invoice_editor keys from both message files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(supplier-invoices): retry the entry-row focus hand-off on the next frame A single requestAnimationFrame after appending the row can fire before the new amount input's ref is mounted, silently dropping the focus hand-off (observed in headless verification). One retry frame makes the signature interaction reliable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(supplier-invoices): deterministic entry-row focus hand-off via effect The rAF retry still lost to the dialog focus scope re-parking focus when the entry input remounts mid-commit. An effect keyed on the pending row index runs after the new row's input has mounted and wins deterministically. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(supplier-invoices): comma-tolerant amount cell and surviving focus routing The focus trace exposed two real issues behind a probe mystery: the amount cell was type=number (ArrowDown decrements money by 0.01, Enter fires the form's implicit submit mid-edit, and Swedish comma decimals are rejected outright), and the supplier menu's close-autofocus yanked focus back to the trigger, undoing the routed hand-off to the invoice-number field. AmountCell mirrors VatRateCell's draft pattern: text input with decimal inputMode, digits-and-one-separator whitelist, Enter commits via blur. The supplier DropdownMenuContent prevents default close autofocus. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(supplier-invoices): show comma decimals in the amount cell display Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(supplier-invoices): stop dialog grid item overflowing small viewports min-w-0 on the form root (DialogContent is display:grid, so the kontering table's min-w otherwise forces the column past narrow screens) and wrap the sticky-bar action cluster. 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>
This commit is contained in:
co-authored by
Claude Fable 5
Jakob Wennberg
parent
dc311726de
commit
93e99012d7
+55
-7
@@ -4146,6 +4146,61 @@
|
||||
"link_failed_title": "Could not link the journal entry"
|
||||
},
|
||||
"supplier_invoice_editor": {
|
||||
"section_underlag": "Source document",
|
||||
"section_supplier": "Supplier",
|
||||
"section_details": "Invoice details",
|
||||
"section_forval": "Defaults",
|
||||
"section_summary": "Summary",
|
||||
"underlag_recommended": "recommended",
|
||||
"underlag_caption_default": "The reader picks up supplier, dates and amounts and suggests the booking. You review before anything is posted.",
|
||||
"underlag_caption_done": "Read: check against the invoice before you register.",
|
||||
"underlag_caption_attached": "Attached as the voucher's source document.",
|
||||
"underlag_caption_inbox": "The inbox document travels with the invoice and is archived on the voucher.",
|
||||
"underlag_drop_prompt": "Drop the invoice here and we fill it in for you",
|
||||
"underlag_uploading": "Uploading…",
|
||||
"underlag_processing": "Reading…",
|
||||
"underlag_remove": "Remove",
|
||||
"underlag_remove_aria": "Remove the document",
|
||||
"underlag_unsupported_type": "Unsupported file type. Allowed formats: PDF, JPEG, PNG and WebP.",
|
||||
"underlag_too_large": "The file is too large. The maximum size is 10 MB.",
|
||||
"underlag_upload_failed": "The document could not be uploaded. Remove the file and try again.",
|
||||
"extraction_ready_line": "Reading done.",
|
||||
"extraction_ready_apply": "Fill in from the document",
|
||||
"mark_selected": "selected",
|
||||
"mark_filled": "filled in",
|
||||
"mark_attached": "attached",
|
||||
"rows_count": "{count, plural, =1 {1 row} other {# rows}}",
|
||||
"org_number_prefix": "Org no {number}",
|
||||
"forval_books_at_registration": "Posted at registration",
|
||||
"forval_books_at_payment": "Posted at payment",
|
||||
"forval_toggle_open": "Change defaults",
|
||||
"forval_toggle_close": "Close",
|
||||
"chip_paid_privately": "Paid privately",
|
||||
"chip_ore_rounding_off": "No öre rounding",
|
||||
"chip_delivery_date": "Delivery date {date}",
|
||||
"chip_notes": "Notes",
|
||||
"chip_currency_rate": "{currency} (rate {rate})",
|
||||
"next_step_prefix": "Next step: ",
|
||||
"next_step_supplier": "choose a supplier",
|
||||
"next_step_invoice_number": "enter the supplier's invoice number",
|
||||
"next_step_add_row": "add a booking row",
|
||||
"next_step_row_account": "choose an account for row {row}",
|
||||
"ready_line_review": "Ready to review: posted as a voucher when you approve.",
|
||||
"ready_line_register": "Ready to register: posted as a voucher right away.",
|
||||
"ready_line_register_cash": "Ready to register: posted at payment.",
|
||||
"crosscheck_label": "Total according to the invoice",
|
||||
"crosscheck_optional": "optional",
|
||||
"crosscheck_match": "Matches the rows.",
|
||||
"crosscheck_diff": "Differs by {diff} from the rows. Check amounts and VAT.",
|
||||
"duplicate_warning_line": "{number} is already registered for this supplier.",
|
||||
"duplicate_warning_link": "View the invoice",
|
||||
"due_from_terms_caption": "From the supplier's terms ({days} days)",
|
||||
"due_on_invoice_caption": "Stated on the invoice",
|
||||
"ocr_payment_file_caption": "Used in the payment file.",
|
||||
"remove_row_named_aria": "Remove row {index}: {description}",
|
||||
"row_no_description": "no description",
|
||||
"ghost_description": "description",
|
||||
"total_payable_label": "Total to pay",
|
||||
"ore_rounding_label": "Öre rounding",
|
||||
"ore_rounding_help": "Round the invoice total to whole kronor",
|
||||
"page_title": "Register supplier invoice",
|
||||
@@ -4162,14 +4217,11 @@
|
||||
"ai_no_org_number": "No organisation number found",
|
||||
"ai_supplier_not_in_system": ": supplier is not yet registered.",
|
||||
"create_and_select": "Create & select",
|
||||
"section_invoice": "Invoice",
|
||||
"section_accounting": "Accounting",
|
||||
"account_from_history": "Account {account} suggested from previous bookings of {counterparty}",
|
||||
"section_other": "Other",
|
||||
"paid_privately_label": "I paid this privately",
|
||||
"paid_privately_help_ef": "Booked as a liability from the company to you (2018 Egen insättning). Refund manually later from the company account.",
|
||||
"paid_privately_help_ab": "Booked as a liability from the company to you (2893 Skuld till ägare). Refund manually later from the company account.",
|
||||
"supplier_label": "Supplier",
|
||||
"supplier_placeholder": "Select supplier",
|
||||
"add_new_supplier": "+ Add new supplier...",
|
||||
"supplier_invoice_number_label": "Supplier's invoice number",
|
||||
@@ -4178,14 +4230,12 @@
|
||||
"due_date_label": "Due date",
|
||||
"payment_reference_label": "OCR / Payment reference",
|
||||
"payment_reference_placeholder": "OCR-nummer",
|
||||
"document_label": "Invoice document",
|
||||
"document_upload_in_progress_title": "The document is still uploading",
|
||||
"document_upload_in_progress_description": "Wait for the upload to finish before registering the invoice.",
|
||||
"document_upload_failed_title": "The document could not be uploaded",
|
||||
"document_upload_failed_description": "Remove the file and try uploading it again.",
|
||||
"document_link_warning_title": "The invoice was registered, but the document is missing from the verifikation",
|
||||
"document_link_warning_description": "Open the verifikation and attach the document there.",
|
||||
"add_row": "Add row",
|
||||
"currency_label": "Currency",
|
||||
"exchange_rate_label": "Exchange rate",
|
||||
"exchange_rate_to_sek": "(to SEK)",
|
||||
@@ -4204,8 +4254,6 @@
|
||||
"col_credit": "Credit",
|
||||
"vat_rate_presets_aria": "Pick VAT rate from list",
|
||||
"description_placeholder": "Description",
|
||||
"row_label": "Row {index}",
|
||||
"remove_row_aria": "Remove row {index}",
|
||||
"row_dimensions_aria": "Cost centre/project for row {index}",
|
||||
"row_dimensions_title": "Cost centre/project",
|
||||
"row_dimensions_inherit_hint": "Empty fields inherit the invoice default ({dims}).",
|
||||
|
||||
+55
-7
@@ -4146,6 +4146,61 @@
|
||||
"link_failed_title": "Kunde inte länka verifikationen"
|
||||
},
|
||||
"supplier_invoice_editor": {
|
||||
"section_underlag": "Underlag",
|
||||
"section_supplier": "Leverantör",
|
||||
"section_details": "Fakturauppgifter",
|
||||
"section_forval": "Förval",
|
||||
"section_summary": "Summering",
|
||||
"underlag_recommended": "rekommenderas",
|
||||
"underlag_caption_default": "Tolkningen läser leverantör, datum och belopp och föreslår kontering. Du kontrollerar innan något bokförs.",
|
||||
"underlag_caption_done": "Tolkat: kontrollera mot fakturan innan du registrerar.",
|
||||
"underlag_caption_attached": "Bifogas som underlag till verifikationen.",
|
||||
"underlag_caption_inbox": "Underlaget från inkorgen följer med och arkiveras på verifikationen.",
|
||||
"underlag_drop_prompt": "Släpp fakturan här så fyller vi i den åt dig",
|
||||
"underlag_uploading": "Laddar upp…",
|
||||
"underlag_processing": "Tolkar…",
|
||||
"underlag_remove": "Ta bort",
|
||||
"underlag_remove_aria": "Ta bort underlaget",
|
||||
"underlag_unsupported_type": "Filtypen stöds inte. Tillåtna format: PDF, JPEG, PNG och WebP.",
|
||||
"underlag_too_large": "Filen är för stor. Maxstorlek är 10 MB.",
|
||||
"underlag_upload_failed": "Underlaget kunde inte laddas upp. Ta bort filen och försök igen.",
|
||||
"extraction_ready_line": "Tolkning klar.",
|
||||
"extraction_ready_apply": "Fyll i från underlaget",
|
||||
"mark_selected": "vald",
|
||||
"mark_filled": "ifyllt",
|
||||
"mark_attached": "bifogat",
|
||||
"rows_count": "{count, plural, =1 {1 rad} other {# rader}}",
|
||||
"org_number_prefix": "Org.nr {number}",
|
||||
"forval_books_at_registration": "Bokförs vid registrering",
|
||||
"forval_books_at_payment": "Bokförs vid betalning",
|
||||
"forval_toggle_open": "Ändra förval",
|
||||
"forval_toggle_close": "Stäng",
|
||||
"chip_paid_privately": "Betald privat",
|
||||
"chip_ore_rounding_off": "Utan öresavrundning",
|
||||
"chip_delivery_date": "Leveransdatum {date}",
|
||||
"chip_notes": "Anteckningar",
|
||||
"chip_currency_rate": "{currency} (kurs {rate})",
|
||||
"next_step_prefix": "Nästa steg: ",
|
||||
"next_step_supplier": "välj leverantör",
|
||||
"next_step_invoice_number": "ange leverantörens fakturanummer",
|
||||
"next_step_add_row": "lägg till en konteringsrad",
|
||||
"next_step_row_account": "välj konto för rad {row}",
|
||||
"ready_line_review": "Klar att granska: bokförs som verifikat när du godkänner.",
|
||||
"ready_line_register": "Klar att registrera: bokförs som verifikat direkt.",
|
||||
"ready_line_register_cash": "Klar att registrera: bokförs vid betalning.",
|
||||
"crosscheck_label": "Totalt enligt fakturan",
|
||||
"crosscheck_optional": "valfritt",
|
||||
"crosscheck_match": "Stämmer mot raderna.",
|
||||
"crosscheck_diff": "Diff {diff} mot raderna. Kontrollera belopp och moms.",
|
||||
"duplicate_warning_line": "{number} är redan registrerad för den här leverantören.",
|
||||
"duplicate_warning_link": "Visa fakturan",
|
||||
"due_from_terms_caption": "Från leverantörens villkor ({days} dagar)",
|
||||
"due_on_invoice_caption": "Står på fakturan",
|
||||
"ocr_payment_file_caption": "Används i betalningsfilen.",
|
||||
"remove_row_named_aria": "Ta bort rad {index}: {description}",
|
||||
"row_no_description": "utan beskrivning",
|
||||
"ghost_description": "beskrivning",
|
||||
"total_payable_label": "Totalt att betala",
|
||||
"ore_rounding_label": "Öresavrundning",
|
||||
"ore_rounding_help": "Avrunda fakturatotal till hel krona",
|
||||
"page_title": "Registrera leverantörsfaktura",
|
||||
@@ -4162,14 +4217,11 @@
|
||||
"ai_no_org_number": "Ingen organisationsnummer hittades",
|
||||
"ai_supplier_not_in_system": ": leverantören finns inte upplagd ännu.",
|
||||
"create_and_select": "Skapa & välj",
|
||||
"section_invoice": "Faktura",
|
||||
"section_accounting": "Kontering",
|
||||
"account_from_history": "Konto {account} föreslaget från tidigare bokföringar av {counterparty}",
|
||||
"section_other": "Övrigt",
|
||||
"paid_privately_label": "Jag har betalat detta privat",
|
||||
"paid_privately_help_ef": "Bokförs som skuld från bolaget till dig (2018 Egen insättning). Återbetalas senare manuellt från företagskontot.",
|
||||
"paid_privately_help_ab": "Bokförs som skuld från bolaget till dig (2893 Skuld till ägare). Återbetalas senare manuellt från företagskontot.",
|
||||
"supplier_label": "Leverantör",
|
||||
"supplier_placeholder": "Välj leverantör",
|
||||
"add_new_supplier": "+ Lägg till ny leverantör...",
|
||||
"supplier_invoice_number_label": "Leverantörens fakturanummer",
|
||||
@@ -4178,14 +4230,12 @@
|
||||
"due_date_label": "Förfallodatum",
|
||||
"payment_reference_label": "OCR / Betalningsreferens",
|
||||
"payment_reference_placeholder": "OCR-nummer",
|
||||
"document_label": "Fakturaunderlag",
|
||||
"document_upload_in_progress_title": "Underlaget laddas fortfarande upp",
|
||||
"document_upload_in_progress_description": "Vänta tills uppladdningen är klar innan du registrerar fakturan.",
|
||||
"document_upload_failed_title": "Underlaget kunde inte laddas upp",
|
||||
"document_upload_failed_description": "Ta bort filen och försök ladda upp den igen.",
|
||||
"document_link_warning_title": "Fakturan registrerades, men underlaget saknas på verifikationen",
|
||||
"document_link_warning_description": "Öppna verifikationen och bifoga underlaget där.",
|
||||
"add_row": "Lägg till rad",
|
||||
"currency_label": "Valuta",
|
||||
"exchange_rate_label": "Växelkurs",
|
||||
"exchange_rate_to_sek": "(till SEK)",
|
||||
@@ -4204,8 +4254,6 @@
|
||||
"col_credit": "Kredit",
|
||||
"vat_rate_presets_aria": "Välj momssats från lista",
|
||||
"description_placeholder": "Beskrivning",
|
||||
"row_label": "Rad {index}",
|
||||
"remove_row_aria": "Ta bort rad {index}",
|
||||
"row_dimensions_aria": "Kostnadsställe/Projekt för rad {index}",
|
||||
"row_dimensions_title": "Kostnadsställe/Projekt",
|
||||
"row_dimensions_inherit_hint": "Tomma fält ärver fakturans standard ({dims}).",
|
||||
|
||||
Reference in New Issue
Block a user