feat(invoices): rebuild the invoice editor as the snabbflöde single column (#1654)

* refactor(invoices): extract editor payload builders with parity tests

Extract the three near-identical inline payload builders in InvoiceEditor.tsx
(handleConfirm, saveDraftData, saveEdit) and the self-billed body mapper into
pure functions in lib/invoices/editor-payload.ts. Zero behavioral change: the
new lib module carries a 300-case parity suite asserting JSON byte equality
against verbatim copies of the legacy inline recipes across the full
mode x deduction x dimensions x ore-rounding matrix. This is the
byte-compatibility ratchet under the upcoming editor re-layout: the repo
renders no components in tests, so the wire bodies are what CI can pin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(invoices): rebuild the invoice editor as the snabbflöde single column

Reshape InvoiceEditor to the approved prototype: one 640px column with
uppercase section labels and honest state marks (RequiredMark asterisks,
sage check on a picked customer, muted row counts), a dense in-table rows
surface with a unified last-row entry (autocomplete over the artikelregister,
italic ghost cells, Enter commits free text and lands in the price cell,
ArrowDown+Enter commits an article through the same applyArticle side
effects), hover-revealed 24px row controls with 40px coarse-pointer targets
and per-row aria-labels, a Förval chip line whose collapsed settings
re-surface as chips whenever a value deviates from its default (critical in
edit/copy so PATCH never round-trips invisible values), a single ochre
next-step line (aria-live polite) that doubles as the invalid-submit focus
router, and a sticky bottom action bar with the live total: position sticky
in both hosts, never fixed, since DialogContent's transform re-anchors fixed
children in bare mode.

Behavioral deltas, all pre-decided: the primary action is never disabled
pre-click for writable users (viewers keep the lock+tooltip treatment);
client-side validation failures route focus instead of toasting; genuine
field errors stay terracotta and field-adjacent while the two ochre
disclosures (taxed-where-performed, labor-only) demote to muted text;
committed free-text rows expose a quiet Spara-som-artikel link; the review
dialog lists the applied förval (currency, öre rounding, payment-link
state); a freshly committed row gets a brief background settle that
collapses under prefers-reduced-motion. ArticleCombobox gains the missing
combobox ARIA (listbox/option roles, aria-controls, aria-activedescendant
only after explicit arrowing). New pure module invoice-editor-flow.ts pins
the next-step priority order, the Förval chip derivation and the suggestion
filter with unit tests. All payload builders, submit targets and the VAT
baseline refs are untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(invoices): editor review nits: orphaned keys, housing gate, listbox ARIA

Three review findings on the snabbflode editor:

- Delete 13 orphaned invoice_editor keys from both message files
  (subtitle_*, add_row, remove_row, remove_row_aria, details_card_title,
  save_as_draft_short, validation_toast_*, delivery_date_placeholder);
  each verified unused on the branch, sv/en parity kept.
- Gate the housing next-step on a claimed deduction amount so it matches
  the ROT/RUT claim card's mount condition: a ROT-flagged line with a
  zero amount mounts no card, and the ochre link would try to focus an
  unmounted field. Extracted as deriveRequiresHousing in the flow module
  with a test proven to fail on the old gate.
- Move the entry-row popover hint out of the role=listbox element
  (listbox children must be options) into a sibling inside the absolute
  wrapper, referenced via aria-describedby on the combobox input.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(invoices): drop the in-editor faktura/sjalvfaktura tabs

The Ny faktura split button already chooses the mode (?self=1); a second
switcher inside the editor was double steering. The mode is now fixed for
the editor's lifetime and the heading (Registrera sjalvfaktura) carries
the distinction. Orphaned tab keys removed from both message files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(invoices): wrap sticky-bar actions so they fit small viewports

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(invoices): stop dialog grid item overflowing small viewports

min-w-0 on the editor root: DialogContent is display:grid, so the row
grid's min-w otherwise forces the column past narrow screens.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(invoices): lift assistant FAB above the standalone editor's action bar

The rebuilt editor introduces the first page-level sticky bottom bar; the
assistant FAB (fixed, z-30) covered its Spara/Granska buttons on the
/invoices/[id]/edit page. The editor now sets body[data-page-bottom-bar]
in non-bare mode and AgentTrigger lifts to bottom-20 when it is present.

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:
Jakob Wennberg
2026-08-18 09:56:35 +02:00
committed by GitHub
co-authored by Claude Fable 5 Jakob Wennberg
parent 93e99012d7
commit 2b5b813b7a
12 changed files with 2879 additions and 1274 deletions
+42 -15
View File
@@ -3420,6 +3420,43 @@
"cancel": "Cancel"
},
"invoice_editor": {
"amount_label": "Amount",
"rows_count": "{count, plural, =1 {1 line} other {# lines}}",
"customer_done": "selected",
"optional_label": "(optional)",
"section_forval": "Defaults",
"forval_edit": "Change defaults",
"entry_placeholder": "Type freely or search articles ...",
"entry_aria": "New invoice line",
"entry_hint_matches": "Arrow down picks an article, Enter adds it as free text",
"entry_hint_free": "Enter adds the line as free text",
"row_menu_pick_article": "Choose article",
"remove_row_aria_named": "Remove line: {description}",
"validation_price_invalid": "Enter a unit price",
"chip_currency": "Currency {currency}",
"chip_due_days": "Due in {days} days ({date})",
"chip_due_date": "Due {date}",
"chip_received": "Received {date}",
"chip_delivery": "Delivery date {date}",
"chip_your_reference": "Your reference: {reference}",
"chip_stripe_auto": "Stripe link created on send",
"chip_payment_link": "Payment link added",
"chip_ore_off": "No öre rounding",
"chip_dims": "Dimensions: {dims}",
"next_step_prefix": "Next step:",
"next_step_customer": "choose a customer",
"next_step_invoice_date": "set the invoice date",
"next_step_due_date": "set the due date",
"next_step_add_row": "add an invoice line",
"next_step_row_incomplete": "complete line {index}",
"next_step_payment_link": "check the payment link",
"next_step_personnummer": "enter the personal number for the deduction",
"next_step_housing": "enter the property designation",
"next_step_external_number": "enter the customer's invoice number",
"next_step_received_date": "set the received date",
"ready_create": "Ready to review: everything else has sensible defaults.",
"ready_edit": "Ready to save.",
"ready_self_billed": "Ready to register.",
"row_menu_set_account": "Set posting account",
"row_menu_remove_account": "Remove posting account",
"row_menu_set_dimensions": "Set cost centre/project",
@@ -3442,10 +3479,6 @@
"title_proforma": "New proforma invoice",
"title_delivery_note": "New delivery note",
"title_copy": "Copy invoice",
"subtitle_invoice": "Create a new invoice",
"subtitle_proforma": "Create a proforma invoice (no bookkeeping)",
"subtitle_delivery_note": "Create a delivery note (without prices)",
"subtitle_copy": "Create a new invoice from reused content",
"copy_notice": "The content was copied from {number}. A new draft is created with a new date and invoice number. Check the customer, references, and ROT/RUT details before continuing.",
"copy_load_failed_title": "Could not copy the invoice",
"copy_load_failed_description": "The invoice does not exist or cannot be used as a template.",
@@ -3471,17 +3504,13 @@
"vat_label": "VAT",
"vat_taxed_where_performed_hint": "Swedish VAT on an invoice to a foreign business applies only to services taxed where they are performed, for example hotel, restaurant, passenger transport, property services or admission to cultural and sports events (ML 6 kap.).",
"row_label": "Row {index}",
"add_row": "Add row",
"add_text_row": "Add text row",
"text_row_label": "Free text",
"text_row_placeholder": "Explanatory text: leave empty for a blank row",
"remove_row_aria": "Remove row",
"remove_row": "Remove row",
"row_actions_aria": "Row actions",
"drag_handle_aria": "Drag to move the row",
"notes_card_title": "Notes",
"notes_placeholder": "E.g. payment terms or thanks for the collaboration...",
"details_card_title": "Invoice details",
"document_type_label": "Document type",
"doctype_invoice": "Invoice",
"doctype_proforma": "Proforma invoice",
@@ -3490,7 +3519,6 @@
"invoice_date_label": "Invoice date",
"due_date_label": "Due date",
"delivery_date_label": "Delivery date",
"delivery_date_placeholder": "If different from invoice date",
"your_reference_label": "Your reference",
"your_reference_placeholder": "Customer contact person",
"our_reference_label": "Our reference",
@@ -3509,19 +3537,15 @@
"total_label": "Total",
"review_and_create": "Review & create",
"save_as_draft": "Save as draft",
"save_as_draft_short": "Draft",
"save_as_draft_tooltip": "Saves without an invoice number: can be deleted later",
"toast_draft_saved_title": "Draft saved",
"toast_draft_saved_description": "The draft is in your invoice list. Open it to review and create the invoice.",
"save_draft_failed_title": "Could not save draft",
"title_edit": "Edit draft",
"subtitle_edit": "Change the draft and save. The invoice number is unchanged.",
"save_changes": "Save changes",
"toast_draft_updated_title": "Draft updated",
"toast_draft_updated_description": "Your changes have been saved.",
"update_failed_title": "Could not save your changes",
"mode_invoice": "Invoice",
"mode_self_billed": "Self-billing",
"viewer_disabled_tooltip": "You only have read-only access to this company",
"review_dialog_title_invoice": "Review invoice",
"review_dialog_title_proforma": "Review proforma invoice",
@@ -3557,8 +3581,6 @@
"validation_invoice_date_required": "Invoice date required",
"validation_due_date_required": "Due date required",
"validation_min_one_row": "At least one row required",
"validation_toast_title": "Check the details",
"validation_toast_description": "A field is missing or invalid. Fix the highlighted fields and try again.",
"article_search_empty": "No article matches your search",
"deduction_menu_label": "Tax reduction",
"deduction_none": "None",
@@ -3589,6 +3611,11 @@
"review_customer_missing_description": "Reload the page and try again. Contact support if the problem persists."
},
"invoice_review": {
"forval_currency": "Currency {currency}",
"forval_ore_on": "Rounded to whole kronor",
"forval_ore_off": "No öre rounding",
"forval_link_auto": "Stripe payment link created on send",
"forval_link_manual": "Payment link attached",
"assigned_number_prefix": "Will be assigned invoice number",
"accrual_line_info": "Accrued {from} to {to}",
"customer_type_individual": "Private individual",
+42 -15
View File
@@ -3420,6 +3420,43 @@
"cancel": "Avbryt"
},
"invoice_editor": {
"amount_label": "Belopp",
"rows_count": "{count, plural, =1 {1 rad} other {# rader}}",
"customer_done": "vald",
"optional_label": "(valfritt)",
"section_forval": "Förval",
"forval_edit": "Ändra förval",
"entry_placeholder": "Skriv fritt eller sök artikel ...",
"entry_aria": "Ny fakturarad",
"entry_hint_matches": "Pil ned väljer artikel, Enter lägger till som fritextrad",
"entry_hint_free": "Enter lägger till som fritextrad",
"row_menu_pick_article": "Välj artikel",
"remove_row_aria_named": "Ta bort rad: {description}",
"validation_price_invalid": "Ange ett à-pris",
"chip_currency": "Valuta {currency}",
"chip_due_days": "Förfaller {days} dagar ({date})",
"chip_due_date": "Förfaller {date}",
"chip_received": "Mottagen {date}",
"chip_delivery": "Leveransdatum {date}",
"chip_your_reference": "Er referens: {reference}",
"chip_stripe_auto": "Stripe-länk skapas vid utskick",
"chip_payment_link": "Betalningslänk inlagd",
"chip_ore_off": "Ingen öresavrundning",
"chip_dims": "Dimensioner: {dims}",
"next_step_prefix": "Nästa steg:",
"next_step_customer": "välj kund",
"next_step_invoice_date": "ange fakturadatum",
"next_step_due_date": "ange förfallodatum",
"next_step_add_row": "lägg till en fakturarad",
"next_step_row_incomplete": "komplettera rad {index}",
"next_step_payment_link": "kontrollera betalningslänken",
"next_step_personnummer": "ange personnummer för skattereduktionen",
"next_step_housing": "ange fastighetsbeteckning",
"next_step_external_number": "ange kundens fakturanummer",
"next_step_received_date": "ange mottagningsdatum",
"ready_create": "Klar att granska: allt annat har smarta förval.",
"ready_edit": "Klart att spara.",
"ready_self_billed": "Klar att registrera.",
"row_menu_set_account": "Ange bokföringskonto",
"row_menu_remove_account": "Ta bort bokföringskonto",
"row_menu_set_dimensions": "Ange kostnadsställe/projekt",
@@ -3442,10 +3479,6 @@
"title_proforma": "Ny proformafaktura",
"title_delivery_note": "Ny följesedel",
"title_copy": "Kopiera faktura",
"subtitle_invoice": "Skapa en ny faktura",
"subtitle_proforma": "Skapa en proformafaktura (ingen bokföring)",
"subtitle_delivery_note": "Skapa en följesedel (utan priser)",
"subtitle_copy": "Skapa en ny faktura med återanvänt innehåll",
"copy_notice": "Innehållet har kopierats från {number}. Ett nytt utkast skapas med nytt datum och nytt fakturanummer. Kontrollera kund, referenser och ROT/RUT-uppgifter innan du fortsätter.",
"copy_load_failed_title": "Kunde inte kopiera fakturan",
"copy_load_failed_description": "Fakturan finns inte eller kan inte användas som mall.",
@@ -3471,17 +3504,13 @@
"vat_label": "Moms",
"vat_taxed_where_performed_hint": "Svensk moms på en faktura till ett utländskt företag gäller bara tjänster som beskattas där de utförs, till exempel hotell, restaurang, persontransport, fastighetstjänst eller entré till kultur- och sportevenemang (ML 6 kap.).",
"row_label": "Rad {index}",
"add_row": "Lägg till rad",
"add_text_row": "Lägg till textrad",
"text_row_label": "Fritext",
"text_row_placeholder": "Förklarande text: lämna tom för en tomrad",
"remove_row_aria": "Ta bort rad",
"remove_row": "Ta bort rad",
"row_actions_aria": "Radåtgärder",
"drag_handle_aria": "Dra för att flytta raden",
"notes_card_title": "Anteckningar",
"notes_placeholder": "T.ex. betalningsvillkor eller tack för samarbetet...",
"details_card_title": "Fakturadetaljer",
"document_type_label": "Dokumenttyp",
"doctype_invoice": "Faktura",
"doctype_proforma": "Proformafaktura",
@@ -3490,7 +3519,6 @@
"invoice_date_label": "Fakturadatum",
"due_date_label": "Förfallodatum",
"delivery_date_label": "Leveransdatum",
"delivery_date_placeholder": "Om det skiljer sig från fakturadatum",
"your_reference_label": "Er referens",
"your_reference_placeholder": "Kontaktperson hos kund",
"our_reference_label": "Vår referens",
@@ -3509,19 +3537,15 @@
"total_label": "Totalt",
"review_and_create": "Granska & skapa",
"save_as_draft": "Spara som utkast",
"save_as_draft_short": "Utkast",
"save_as_draft_tooltip": "Sparar utan fakturanummer: kan tas bort senare",
"toast_draft_saved_title": "Utkast sparat",
"toast_draft_saved_description": "Utkastet finns i fakturalistan. Öppna det för att granska och skapa fakturan.",
"save_draft_failed_title": "Kunde inte spara utkast",
"title_edit": "Redigera utkast",
"subtitle_edit": "Ändra utkastet och spara. Inget fakturanummer ändras.",
"save_changes": "Spara ändringar",
"toast_draft_updated_title": "Utkast uppdaterat",
"toast_draft_updated_description": "Ändringarna har sparats.",
"update_failed_title": "Kunde inte spara ändringarna",
"mode_invoice": "Faktura",
"mode_self_billed": "Självfaktura",
"viewer_disabled_tooltip": "Du har endast läsbehörighet i detta företag",
"review_dialog_title_invoice": "Granska faktura",
"review_dialog_title_proforma": "Granska proformafaktura",
@@ -3557,8 +3581,6 @@
"validation_invoice_date_required": "Fakturadatum krävs",
"validation_due_date_required": "Förfallodatum krävs",
"validation_min_one_row": "Minst en rad krävs",
"validation_toast_title": "Kontrollera uppgifterna",
"validation_toast_description": "Något fält saknas eller är felaktigt. Rätta de markerade fälten och försök igen.",
"article_search_empty": "Ingen artikel matchar sökningen",
"deduction_menu_label": "Skattereduktion",
"deduction_none": "Ingen",
@@ -3589,6 +3611,11 @@
"review_customer_missing_description": "Ladda om sidan och försök igen. Kontakta support om det inte hjälper."
},
"invoice_review": {
"forval_currency": "Valuta {currency}",
"forval_ore_on": "Öresavrundning till hel krona",
"forval_ore_off": "Ingen öresavrundning",
"forval_link_auto": "Stripe-betalningslänk skapas vid utskick",
"forval_link_manual": "Betalningslänk bifogas",
"assigned_number_prefix": "Tilldelas fakturanummer",
"accrual_line_info": "Periodiseras {from} till {to}",
"customer_type_individual": "Privatperson",