feat(parties): Förslag från bokföringen, confirmed straight into Leverantörer and Kunder (#2206)
* feat(parties): Kontakter register, suggestion queue, dossier and merge Phase 1's two surfaces on top of the parties substrate: - /parties page: one list with the five-way switch (Alla, Kunder, Leverantörer, Förslag, Bara i bokföringen), search, a 12-month/all period picker, and at most one attention line. Confirmed rows show roles as muted text, rhythm, underlag, dominant account and money. Observed rows are computed and never stored; a generic band keeps unattributed spend visible. - Suggestion queue: a reason per row, hard-key rows pre-ticked, bulk confirm behind one dialog, dismiss on hover, undo on the toast. - Dossier slide-over: Pengar, Bokföring, Vad Accounted vet (facts and identities with source and count), Underlag och verifikat, Historik. - Merge dialog with a visible, swappable survivor and undo. - API: GET /api/parties, GET /api/parties/[id], POST suggest, decide, decide/undo, merge, merge/undo (withRouteContext, Zod, 15 tests). - Migration 20260903090000: decide_parties snapshots the reason it clears; undo_party_decisions reverses confirm/dismiss within 30 days; decision kind 'undo'. - The pipeline runs after SIE import and provider migration (non-blocking) so a migrant's register is full on arrival. - Nav entry under Register; sv/en strings. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(parties): pass explicit interpolation values to next-intl next build's type check rejects a typed interface where the translator wants an index-signature record. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(parties): retry label on the load-failed state Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(parties): hard keys for companies without org number, readable names, look-alikes at read time - get_ledger_key_evidence dropped every document for a company whose own org number is NULL (the self check compared against NULL). Replaced in 20260903100000 with a coalesced comparison; pg test covers it. - Display names come from the printed name on documents, otherwise from the voucher text with the AP/AR prefix and supplier number removed. - Look-alike parties (same core, or one core extending the other by whole words: Fortnox / Fortnox Finans) are detected when the register is read, never stored, and feed the Dubblett? chip and the merge dialog. - Queue shows Intäkt beside Kostnad; dossier hides zero money rows and formats bankgiro/plusgiro; merge dialog cancels with Avbryt; no synchronous setState inside effects. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(parties): link every new supplier and customer to a party on write The backfill covered the rows that existed on 2026-09-02; 108 rows created since had no party and never reached the register. A BEFORE INSERT/UPDATE trigger on customers and suppliers now calls ensure_party on every write path at once: find-or-create by org number inside the company, never by name; a private customer gets a kind=person party without any number; a nameless row stays unlinked; a foreign party id is refused with the same error as the composite foreign key; a link to a merged party follows the chain to the survivor; the clear that ON DELETE SET NULL performs is kept. ensure_party lets the trigger act for the row's owner (pg_trigger_depth() > 0); the RPC path is unchanged. The migration also links the rows created since the backfill. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(parties): dossier hides dismissed parties and follows merges to the survivor The register hid archived parties while the dossier still served them by id, and a merged party's dossier pointed at a dead row. Superagent P2 on #2206; three unit tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * chore(parties): move the role-link migration past main's 20260903110000 Two files with one version would collide in schema_migrations. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(parties): confirm suggestions into Leverantörer and Kunder, no third noun Founder decision after the walkthrough: users know two words. The page becomes the queue 'Förslag från bokföringen' with 'Bara i bokföringen' beside it; the Kontakter nav entry and the Alla/Kunder/Leverantörer views go. Each suggestion shows what it becomes (Blir), read from the ledger side and changeable per row; confirming calls promote_parties, which creates the supplier and/or customer row from the party's facts, never a duplicate, and is undoable for 30 days through undo_party_promotions (the created rows are archived, the party returns to the queue). Leverantörer and Kunder carry the one attention line that leads here. The dossier offers Lägg upp som leverantör / som kund. Migration 20260903130000, 5 pg tests, route and unit tests updated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(parties): write bankgiro and plusgiro the way the supplier form does Identities are stored as digits; suppliers carry 5317-0900. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * chore(parties): move the four queue migrations past main's 20260903170000 Main merged 20260903120000_skattekonto_transactions_realtime_publication with the same version as the role-link trigger; the preview database refused the duplicate key. All four now sit after main's newest so the set applies in one ordered run on prod. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
Jakob Wennberg
parent
e2d38b0ab3
commit
b996da60ee
@@ -8223,5 +8223,130 @@
|
||||
"signed": "{who}, {when}",
|
||||
"signed_other_date": "through {date} ({who}), not the balance sheet date",
|
||||
"unsigned": "Not signed off"
|
||||
},
|
||||
"parties": {
|
||||
"title": "Suggestions from the books",
|
||||
"help": "Counterparts your vouchers and documents point to that are not among your suppliers and customers yet. Confirm and they are added, with the details from the documents. Nothing is posted, and it can be undone for 30 days.",
|
||||
"summary": "{suggested} suggestions · {observed} only in the books",
|
||||
"refresh": "Fetch suggestions",
|
||||
"refreshing": "Fetching…",
|
||||
"refreshed_title": "Suggestions updated",
|
||||
"refreshed_description": "{created} new suggestions, {attached} attached to existing ones.",
|
||||
"attn_suppliers": "{count} supplier suggestions are waiting from the books.",
|
||||
"attn_customers": "{count} customer suggestions are waiting from the books.",
|
||||
"attn_review": "Review",
|
||||
"attn_fetch": "Show",
|
||||
"attn_observed": "The books know {count} counterparts that are not in the register.",
|
||||
"view_suggested": "Suggestions",
|
||||
"view_observed": "Only in the books",
|
||||
"search_placeholder": "Search name or org number",
|
||||
"period_label": "Period",
|
||||
"period_12m": "Last 12 months",
|
||||
"period_all": "All history",
|
||||
"th_name": "Name",
|
||||
"th_why": "Why",
|
||||
"th_becomes": "Becomes",
|
||||
"th_rhythm": "Rhythm",
|
||||
"th_account": "Account",
|
||||
"th_revenue": "Revenue",
|
||||
"th_expense": "Cost",
|
||||
"th_vouchers": "Vouchers",
|
||||
"becomes_aria": "What {name} becomes",
|
||||
"role_customer": "Customer",
|
||||
"role_supplier": "Supplier",
|
||||
"role_none": "In the register",
|
||||
"rhythm_weekly": "weekly",
|
||||
"rhythm_monthly": "monthly",
|
||||
"rhythm_quarterly": "quarterly",
|
||||
"rhythm_yearly": "yearly",
|
||||
"rhythm_irregular": "irregular",
|
||||
"vouchers_count": "{count} vouchers",
|
||||
"invoices_count": "{count} invoices",
|
||||
"chip_duplicate": "Duplicate?",
|
||||
"uncertain_name": "Uncertain name",
|
||||
"reason_org": "Org number {org} in {docs} documents",
|
||||
"reason_docs": "Name in {docs} documents",
|
||||
"reason_ledger_only": "no org number or bankgiro in documents",
|
||||
"reason_ambiguous": "several org numbers in the documents",
|
||||
"reason_similar": "looks like {name}",
|
||||
"reason_vouchers": "{count} vouchers",
|
||||
"dismiss": "Hide",
|
||||
"deselect": "Deselect",
|
||||
"select_all": "Select all",
|
||||
"selected_n": "{count} selected",
|
||||
"selected_hint": "Added with the details from the documents. Undoable for 30 days; nothing is posted.",
|
||||
"promote_n": "Add {count}",
|
||||
"promote_dialog_title": "Add {count} to the register?",
|
||||
"promote_dialog_body": "{detail}. Name, org number and bankgiro come from your documents and can be edited afterwards. Undoable for 30 days; no vouchers change and nothing is posted.",
|
||||
"summary_suppliers": "{count} become suppliers",
|
||||
"summary_customers": "{count} become customers",
|
||||
"promoted_title": "{count} added · {detail}",
|
||||
"promote_supplier": "Add as supplier",
|
||||
"promote_customer": "Add as customer",
|
||||
"dismissed_title": "{count} suggestions hidden",
|
||||
"undo": "Undo",
|
||||
"undone_title": "Undone",
|
||||
"merge": "Merge",
|
||||
"merge_dialog_title": "Merge",
|
||||
"merge_body": "Vouchers, documents and invoices stay where they are. Undoable for 30 days.",
|
||||
"merge_keep": "Keep",
|
||||
"merge_kept": "kept",
|
||||
"merge_confirm": "Merge {count}",
|
||||
"merged_title": "Merged",
|
||||
"merge_undone_title": "Merge undone",
|
||||
"merge_none": "No similar ones found.",
|
||||
"merge_search_placeholder": "Search a supplier or customer to merge with",
|
||||
"dossier_kicker_suggested": "Suggestion",
|
||||
"dossier_seen": "Seen {count} times",
|
||||
"dossier_last": "last {date}",
|
||||
"dossier_variants": "{count} name variants",
|
||||
"section_money": "Money",
|
||||
"section_bookkeeping": "Bookkeeping",
|
||||
"section_facts": "What Accounted knows",
|
||||
"section_vouchers": "Documents and vouchers",
|
||||
"section_history": "History",
|
||||
"money_expense": "Cost",
|
||||
"money_revenue": "Revenue",
|
||||
"money_period_12m": "last 12 months",
|
||||
"money_period_all": "all history",
|
||||
"money_first": "First seen",
|
||||
"money_last": "Last seen",
|
||||
"bk_account": "Account",
|
||||
"bk_account_share": "Posted here in {count} of {total} cases",
|
||||
"bk_when": "When posting",
|
||||
"bk_when_value": "Suggested, never posted without you",
|
||||
"fact_name": "Name",
|
||||
"fact_legal_name": "Legal name",
|
||||
"fact_org": "Org number",
|
||||
"fact_vat": "VAT number",
|
||||
"fact_bankgiro": "Bankgiro",
|
||||
"fact_plusgiro": "Plusgiro",
|
||||
"fact_missing": "Missing",
|
||||
"fact_from_documents": "{count} documents",
|
||||
"fact_from_ledger": "the books",
|
||||
"fact_from_user": "you",
|
||||
"identity_known": "known",
|
||||
"identity_unverified": "unverified",
|
||||
"vouchers_none": "No vouchers found.",
|
||||
"decision_confirm": "Confirmed",
|
||||
"decision_role": "Added",
|
||||
"decision_dismiss": "Hidden",
|
||||
"decision_merge": "Merged",
|
||||
"decision_split": "Merge undone",
|
||||
"decision_undo": "Undone",
|
||||
"empty_suggested_title": "No suggestions right now",
|
||||
"empty_suggested_description": "Fetch suggestions once you have imported vouchers or uploaded documents.",
|
||||
"empty_observed_title": "No more counterparts in the books",
|
||||
"empty_observed_description": "Everything in the vouchers is already among your suppliers and customers.",
|
||||
"empty_search_title": "No matches",
|
||||
"empty_search_description": "Try another name or org number.",
|
||||
"go_suppliers": "To suppliers",
|
||||
"generic_band": "Without a name in the text ({count})",
|
||||
"generic_band_description": "{examples}. Your old system only wrote the category.",
|
||||
"load_failed": "Could not load the suggestions.",
|
||||
"action_failed": "That did not work. Try again.",
|
||||
"viewer_disabled_tooltip": "You have read access in this company.",
|
||||
"count_summary": "{count} suggestions",
|
||||
"open_dossier": "Open {name}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8223,5 +8223,130 @@
|
||||
"signed": "{who}, {when}",
|
||||
"signed_other_date": "t.o.m. {date} ({who}), inte balansdagen",
|
||||
"unsigned": "Ej signerad"
|
||||
},
|
||||
"parties": {
|
||||
"title": "Förslag från bokföringen",
|
||||
"help": "Motparter som dina verifikat och underlag pekar ut men som inte finns bland dina leverantörer och kunder. Bekräfta så läggs de upp, med uppgifterna från underlagen. Ingenting bokförs, och det går att ångra i 30 dagar.",
|
||||
"summary": "{suggested} förslag · {observed} bara i bokföringen",
|
||||
"refresh": "Hämta förslag",
|
||||
"refreshing": "Hämtar…",
|
||||
"refreshed_title": "Förslag uppdaterade",
|
||||
"refreshed_description": "{created} nya förslag, {attached} kopplade till befintliga.",
|
||||
"attn_suppliers": "{count} förslag på leverantörer väntar från bokföringen.",
|
||||
"attn_customers": "{count} förslag på kunder väntar från bokföringen.",
|
||||
"attn_review": "Granska",
|
||||
"attn_fetch": "Visa",
|
||||
"attn_observed": "Bokföringen känner {count} motparter som inte finns i registret.",
|
||||
"view_suggested": "Förslag",
|
||||
"view_observed": "Bara i bokföringen",
|
||||
"search_placeholder": "Sök namn eller org.nr",
|
||||
"period_label": "Period",
|
||||
"period_12m": "Senaste 12 mån",
|
||||
"period_all": "Hela historiken",
|
||||
"th_name": "Namn",
|
||||
"th_why": "Varför",
|
||||
"th_becomes": "Blir",
|
||||
"th_rhythm": "Rytm",
|
||||
"th_account": "Konto",
|
||||
"th_revenue": "Intäkt",
|
||||
"th_expense": "Kostnad",
|
||||
"th_vouchers": "Verifikat",
|
||||
"becomes_aria": "Vad {name} blir",
|
||||
"role_customer": "Kund",
|
||||
"role_supplier": "Leverantör",
|
||||
"role_none": "I registret",
|
||||
"rhythm_weekly": "varje vecka",
|
||||
"rhythm_monthly": "varje månad",
|
||||
"rhythm_quarterly": "varje kvartal",
|
||||
"rhythm_yearly": "varje år",
|
||||
"rhythm_irregular": "oregelbundet",
|
||||
"vouchers_count": "{count} verifikat",
|
||||
"invoices_count": "{count} fakturor",
|
||||
"chip_duplicate": "Dubblett?",
|
||||
"uncertain_name": "Osäkert namn",
|
||||
"reason_org": "Org.nr {org} i {docs} underlag",
|
||||
"reason_docs": "Namn i {docs} underlag",
|
||||
"reason_ledger_only": "inget org.nr eller bankgiro i underlag",
|
||||
"reason_ambiguous": "flera org.nr i underlagen",
|
||||
"reason_similar": "liknar {name}",
|
||||
"reason_vouchers": "{count} verifikat",
|
||||
"dismiss": "Dölj",
|
||||
"deselect": "Avmarkera",
|
||||
"select_all": "Markera alla",
|
||||
"selected_n": "{count} valda",
|
||||
"selected_hint": "Läggs upp med uppgifterna från underlagen. Går att ångra i 30 dagar; ingenting bokförs.",
|
||||
"promote_n": "Lägg upp {count}",
|
||||
"promote_dialog_title": "Lägg upp {count} i registret?",
|
||||
"promote_dialog_body": "{detail}. Namn, org.nr och bankgiro kommer från dina underlag och går att ändra efteråt. Går att ångra i 30 dagar; inga verifikat ändras och ingenting bokförs.",
|
||||
"summary_suppliers": "{count} blir leverantörer",
|
||||
"summary_customers": "{count} blir kunder",
|
||||
"promoted_title": "{count} upplagda · {detail}",
|
||||
"promote_supplier": "Lägg upp som leverantör",
|
||||
"promote_customer": "Lägg upp som kund",
|
||||
"dismissed_title": "{count} förslag dolda",
|
||||
"undo": "Ångra",
|
||||
"undone_title": "Ångrat",
|
||||
"merge": "Slå ihop",
|
||||
"merge_dialog_title": "Slå ihop",
|
||||
"merge_body": "Verifikat, underlag och fakturor ligger kvar där de är. Går att ångra i 30 dagar.",
|
||||
"merge_keep": "Behåll",
|
||||
"merge_kept": "behålls",
|
||||
"merge_confirm": "Slå ihop {count}",
|
||||
"merged_title": "Ihopslagna",
|
||||
"merge_undone_title": "Ihopslagning ångrad",
|
||||
"merge_none": "Inga liknande hittade.",
|
||||
"merge_search_placeholder": "Sök leverantör eller kund att slå ihop med",
|
||||
"dossier_kicker_suggested": "Förslag",
|
||||
"dossier_seen": "Sedd {count} gånger",
|
||||
"dossier_last": "senast {date}",
|
||||
"dossier_variants": "{count} namnvarianter",
|
||||
"section_money": "Pengar",
|
||||
"section_bookkeeping": "Bokföring",
|
||||
"section_facts": "Vad Accounted vet",
|
||||
"section_vouchers": "Underlag och verifikat",
|
||||
"section_history": "Historik",
|
||||
"money_expense": "Kostnad",
|
||||
"money_revenue": "Intäkt",
|
||||
"money_period_12m": "senaste 12 mån",
|
||||
"money_period_all": "hela historiken",
|
||||
"money_first": "Första gången",
|
||||
"money_last": "Senast",
|
||||
"bk_account": "Konto",
|
||||
"bk_account_share": "Bokförd hit i {count} av {total} fall",
|
||||
"bk_when": "Vid bokföring",
|
||||
"bk_when_value": "Föreslås, bokförs aldrig utan dig",
|
||||
"fact_name": "Namn",
|
||||
"fact_legal_name": "Juridiskt namn",
|
||||
"fact_org": "Org.nr",
|
||||
"fact_vat": "Momsnr",
|
||||
"fact_bankgiro": "Bankgiro",
|
||||
"fact_plusgiro": "Plusgiro",
|
||||
"fact_missing": "Saknas",
|
||||
"fact_from_documents": "{count} underlag",
|
||||
"fact_from_ledger": "bokföringen",
|
||||
"fact_from_user": "du",
|
||||
"identity_known": "känd",
|
||||
"identity_unverified": "ej verifierad",
|
||||
"vouchers_none": "Inga verifikat hittade.",
|
||||
"decision_confirm": "Bekräftad",
|
||||
"decision_role": "Upplagd",
|
||||
"decision_dismiss": "Dold",
|
||||
"decision_merge": "Ihopslagen",
|
||||
"decision_split": "Ihopslagning ångrad",
|
||||
"decision_undo": "Ångrad",
|
||||
"empty_suggested_title": "Inga förslag just nu",
|
||||
"empty_suggested_description": "Hämta förslag när du har importerat verifikat eller laddat upp underlag.",
|
||||
"empty_observed_title": "Inga fler motparter i bokföringen",
|
||||
"empty_observed_description": "Allt som syns i verifikaten finns redan bland dina leverantörer och kunder.",
|
||||
"empty_search_title": "Inga träffar",
|
||||
"empty_search_description": "Prova ett annat namn eller org.nr.",
|
||||
"go_suppliers": "Till leverantörer",
|
||||
"generic_band": "Utan namn i texten ({count})",
|
||||
"generic_band_description": "{examples}. Ditt gamla system skrev bara kategori.",
|
||||
"load_failed": "Kunde inte hämta förslagen.",
|
||||
"action_failed": "Det gick inte. Försök igen.",
|
||||
"viewer_disabled_tooltip": "Du har läsbehörighet i det här bolaget.",
|
||||
"count_summary": "{count} förslag",
|
||||
"open_dossier": "Öppna {name}"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user