diff --git a/DECISIONS.md b/DECISIONS.md index 5c1efe93..77a9b51b 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1604,4 +1604,5 @@ One line per decision: `[YYYY-MM-DD] : `. Appended by agents and [2026-09-05] Supplier and customer pages: the register fills the row's own contact fields (e-mail, phone, postal address, VAT number) when they are empty or still carry what the register said last time, marked "från SCB" by equality with the registry fact, never a value a person typed. Chosen over a read-only fallback because the row is what payment files and documents use; provenance by equality instead of a source column because it needs no schema and a person's edit ends it by itself. Företagsuppgifter keeps only what the register alone knows (status line, industry, seat, size). Agents get the party read-only first: ?expand=party on v1 supplier/customer detail, party_id on list rows, and gnubok_get_party in MCP; the parties resource (suggest, promote, enrich) comes as its own v1 surface next. [2026-09-05] Invoice PDF statutory notices follow the document language: the export notice (stored in Swedish on invoices.reverse_charge_text at create time) is matched against the shared EXPORT_NOTICE_SV constant and rendered from LABELS, so existing invoices are fixed too, and the English footer reads "Approved for F-tax (Godkänd för F-skatt)". Rejected translating at create time (would leave every existing export invoice Swedish) and deriving purely from vat_treatment (would override custom or v1-supplied text). The old Swedish-only F-skatt footer cited Peppol SE-R-005, which governs the UBL file, not the PDF; the swedish-invoice-compliance skill (invoice-rules.md §4) states ML has no language requirement for invoice text, and the literal Swedish phrase stays on the English PDF in parentheses. All notice boxes (proforma, quote, VAT notice, notes) now share one neutral style; custom invoice layouts are a separate feature request. [2026-09-05] Utlägg becomes an answer, not a page: the Underlag pane asks "Vem betalade?" (Företaget / Jag, privat / En anställd / Ingen ännu) and books a privately paid receipt in place through POST /api/expense-claims; the person owed surfaces as a Betala row in Att göra (lib/worklist expense_payout, one item per person) and the Utlägg nav row is gated on existing claims like Körjournal. Chosen over a fourth item in the Bokföring split button (that menu is three ways to type one verifikat, not a list of document kinds) and over keeping the two-step wizard as the entry point: a kvitto paid with a private card differs from any other purchase only in the credit account, and 93 percent of companies on prod are owner-only, for whom a module for that one bit is the wrong shape. Phase 2 (bank-driven repayment, open items shared with leverantörsfakturor, via lön) and phase 3 (retire the wizard, per-person list under Löner) are filed as follow-ups. +[2026-09-05] Cross-tab company guard (WL-09) stays a blocking two-exit dialog, founder re-confirmed today after a forensic pass on a real firing (a switch made elsewhere under the same login, no server-side or agent path involved): auto-follow, per-tab company scoping and a reads-continue banner were offered and declined. Only change: the dialog now names the company the other tab switched to (resolved from the memberships the shell already ships to the client, no request), so the two exits read as a choice between two named companies instead of a named one and "the new one". [2026-09-05] Björn Lundén connect: a 403 whose body says "out of allowed scope for service provider" is mapped to its own BL_INTEGRATION_NOT_ACTIVATED verdict (the key is right, the company never activated the integration) instead of the generic "leverantören avvisade autentiseringen"; live-verified against a real customer key, where every read endpoint answered exactly that while a made-up key answered 500. Root cause of every failed BL connect in prod (10 consents, only BL's own sandbox company ever got tokens): the integration is still a sandbox listing at BL, so no real company can activate it. Chose a message that names the fix (activate in Lundify, else SIE) over hiding the provider state; the Lundify activation-redirect flow and document/line-level fetching are filed as follow-ups rather than built blind before BL releases the integration. diff --git a/components/dashboard/CompanyTabSync.tsx b/components/dashboard/CompanyTabSync.tsx index f3abdce1..70ba98a5 100644 --- a/components/dashboard/CompanyTabSync.tsx +++ b/components/dashboard/CompanyTabSync.tsx @@ -12,6 +12,7 @@ import { guardStore, isTabMismatch, requestHasNextActionHeader, + resolveObservedCompanyName, shouldBlockMutation, } from '@/lib/company/tab-guard' @@ -119,10 +120,16 @@ function uninstallFetchGuard(): void { } export default function CompanyTabSync() { - const { company } = useCompany() + const { company, companies, foreignCompanies } = useCompany() const t = useTranslations('company_tab_guard') const currentCompanyId = company?.id ?? null const [mismatch, setMismatch] = useState(false) + // The company the other tab switched to, so the dialog can name it: the + // choice between "switch back" and "reload as the new one" is only obvious + // when both sides are named. Read from the guard store at the moment the + // dialog is raised (both the observe path and the blocked-write path set + // observedCompanyId first). + const [observedCompanyId, setObservedCompanyId] = useState(null) const [resolving, setResolving] = useState(false) useEffect(() => { @@ -134,7 +141,10 @@ export default function CompanyTabSync() { // A self-initiated switch is hard-navigating this tab away: blocked // stray writes still get their 409, but the "switched in another tab" // dialog would just flash over the tab's own page load. - if (!guardStore.selfSwitchTargetId) setMismatch(true) + if (!guardStore.selfSwitchTargetId) { + setObservedCompanyId(guardStore.observedCompanyId) + setMismatch(true) + } } installFetchGuard() @@ -143,6 +153,7 @@ export default function CompanyTabSync() { guardStore.observedCompanyId = observedId if (observedId === guardStore.selfSwitchTargetId) return if (isTabMismatch(currentCompanyId, observedId)) { + setObservedCompanyId(observedId) setMismatch(true) } } @@ -212,6 +223,12 @@ export default function CompanyTabSync() { if (!mismatch || !currentCompanyId) return null + const newCompanyName = resolveObservedCompanyName( + observedCompanyId, + companies, + foreignCompanies ?? [], + ) + const handleSwitchBack = async () => { setResolving(true) // Re-activate THIS tab's company and reload the page we are on (same @@ -249,7 +266,9 @@ export default function CompanyTabSync() { {t('title')}

- {t('body', { company: company?.name ?? '' })} + {newCompanyName + ? t('body_named', { company: company?.name ?? '', newCompany: newCompanyName }) + : t('body', { company: company?.name ?? '' })}