fix(parties): one "från SCB" note per contact section (#2316)

* fix(parties): one "från SCB" note per contact section, not one under every field

The founder read four tags in a row. Kontaktuppgifter now ends with one
sentence naming the fields the register gave: "E-post, Telefon, adress
och Momsnr från SCB." Nothing else changes; the fill rule and the
equality test behind it are the same.

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

* fix(parties): the customer page's registry note tolerates the loading state

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

* fix(parties): the supplier page's registry note tolerates the loading state

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:
Jakob Wennberg
2026-09-05 14:53:13 +02:00
committed by GitHub
co-authored by Claude Fable 5.1 Jakob Wennberg
parent 971952fe19
commit bff44e5757
5 changed files with 38 additions and 19 deletions
+13 -9
View File
@@ -28,7 +28,7 @@ import type { Customer, CustomerType, CreateCustomerInput } from '@/types'
import { DetailPageSkeleton } from '@/components/common/DetailPageSkeleton'
import { PartyFactsSection } from '@/components/parties/PartyFactsSection'
import { usePartyDossier } from '@/components/parties/use-party-dossier'
import { fromRegistry, addressRowsFromRegistry } from '@/lib/parties/registry-summary'
import { fromRegistry, addressRowsFromRegistry, listSv } from '@/lib/parties/registry-summary'
const CUSTOMER_TYPE_KEY: Record<CustomerType, string> = {
individual: 'type_individual',
@@ -68,7 +68,17 @@ export default function CustomerDetailPage({
const partyId = customer && customer.customer_type !== 'individual' ? ((customer as { party_id?: string | null }).party_id ?? null) : null
const party = usePartyDossier(partyId)
const registryAddress = party.registry?.contact.address ? addressRowsFromRegistry(party.registry.contact.address) : null
const scbNote = (isFromRegistry: boolean) => (isFromRegistry ? <span className="block text-xs text-muted-foreground">{tParties('facts_from_registry')}</span> : null)
// Which contact fields carry what the register said: one note for the
// section, not a tag under every row.
const registryFields = [
fromRegistry(customer?.email, party.registry?.contact.email) ? tParties('fact_email') : null,
fromRegistry(customer?.phone, party.registry?.contact.phone) ? tParties('fact_phone') : null,
!!registryAddress && fromRegistry(customer?.address_line1, registryAddress.address_line1) && fromRegistry(customer?.city, registryAddress.city) ? tParties('facts_address_short') : null,
fromRegistry(customer?.vat_number, party.registry?.vat_number) ? tParties('fact_vat') : null,
].filter((x): x is string => !!x)
const registryNote = registryFields.length ? (
<p className="pt-2 text-xs text-muted-foreground">{tParties('facts_contact_from_registry', { fields: listSv(registryFields, tParties('facts_list_and')) })}</p>
) : null
const [isLoading, setIsLoading] = useState(true)
const [isEditOpen, setIsEditOpen] = useState(false)
const [isUpdating, setIsUpdating] = useState(false)
@@ -271,11 +281,9 @@ export default function CustomerDetailPage({
) : (
<DefEmpty />
)}
{scbNote(fromRegistry(customer.email, party.registry?.contact.email))}
</DefRow>
<DefRow label={t('def_phone')}>
{customer.phone || <DefEmpty />}
{scbNote(fromRegistry(customer.phone, party.registry?.contact.phone))}
</DefRow>
<DefRow label={t('def_address')}>
{customer.address_line1 || customer.city ? (
@@ -285,17 +293,13 @@ export default function CustomerDetailPage({
{(customer.postal_code || customer.city) && (
<p>{[customer.postal_code, customer.city].filter(Boolean).join(' ')}</p>
)}
{scbNote(
!!registryAddress &&
fromRegistry(customer.address_line1, registryAddress.address_line1) &&
fromRegistry(customer.city, registryAddress.city),
)}
{customer.country && <p>{getCountryName(customer.country, errorLocale)}</p>}
</div>
) : (
<DefEmpty />
)}
</DefRow>
{registryNote}
</DetailSection>
{partyId && party.dossier ? (
+13 -10
View File
@@ -21,7 +21,7 @@ import type { Supplier, SupplierType, CreateSupplierInput, SupplierInvoice } fro
import { DetailPageSkeleton } from '@/components/common/DetailPageSkeleton'
import { PartyFactsSection } from '@/components/parties/PartyFactsSection'
import { usePartyDossier } from '@/components/parties/use-party-dossier'
import { fromRegistry, addressRowsFromRegistry } from '@/lib/parties/registry-summary'
import { fromRegistry, addressRowsFromRegistry, listSv } from '@/lib/parties/registry-summary'
import { formatOrgNumber } from '@/lib/utils'
// Supplier invoices carry their own currency; "kr" is only correct for SEK.
@@ -51,7 +51,17 @@ export default function SupplierDetailPage() {
const partyId = (supplier as { party_id?: string | null } | null)?.party_id ?? null
const party = usePartyDossier(partyId)
const registryAddress = party.registry?.contact.address ? addressRowsFromRegistry(party.registry.contact.address) : null
const scbNote = (isFromRegistry: boolean) => (isFromRegistry ? <span className="block text-xs text-muted-foreground">{tParties('facts_from_registry')}</span> : null)
// Which contact fields carry what the register said: one note for the
// section, not a tag under every row.
const registryFields = [
fromRegistry(supplier?.email, party.registry?.contact.email) ? tParties('fact_email') : null,
fromRegistry(supplier?.phone, party.registry?.contact.phone) ? tParties('fact_phone') : null,
!!registryAddress && fromRegistry(supplier?.address_line1, registryAddress.address_line1) && fromRegistry(supplier?.city, registryAddress.city) ? tParties('facts_address_short') : null,
fromRegistry(supplier?.vat_number, party.registry?.vat_number) ? tParties('fact_vat') : null,
].filter((x): x is string => !!x)
const registryNote = registryFields.length ? (
<p className="pt-2 text-xs text-muted-foreground">{tParties('facts_contact_from_registry', { fields: listSv(registryFields, tParties('facts_list_and')) })}</p>
) : null
const [invoices, setInvoices] = useState<SupplierInvoice[]>([])
const [isLoading, setIsLoading] = useState(true)
const [isEditOpen, setIsEditOpen] = useState(false)
@@ -290,11 +300,9 @@ export default function SupplierDetailPage() {
<DetailSection kicker={t('contact_section_title')}>
<DefRow label={t('def_email')}>
{supplier.email || <DefEmpty />}
{scbNote(fromRegistry(supplier.email, party.registry?.contact.email))}
</DefRow>
<DefRow label={t('def_phone')}>
{supplier.phone || <DefEmpty />}
{scbNote(fromRegistry(supplier.phone, party.registry?.contact.phone))}
</DefRow>
<DefRow label={t('def_address')}>
{supplier.address_line1 || supplier.city ? (
@@ -304,11 +312,6 @@ export default function SupplierDetailPage() {
{(supplier.postal_code || supplier.city) && (
<p>{[supplier.postal_code, supplier.city].filter(Boolean).join(' ')}</p>
)}
{scbNote(
!!registryAddress &&
fromRegistry(supplier.address_line1, registryAddress.address_line1) &&
fromRegistry(supplier.city, registryAddress.city),
)}
</div>
) : (
<DefEmpty />
@@ -317,9 +320,9 @@ export default function SupplierDetailPage() {
{supplier.vat_number && (
<DefRow label={t('def_vat')}>
{supplier.vat_number}
{scbNote(fromRegistry(supplier.vat_number, party.registry?.vat_number))}
</DefRow>
)}
{registryNote}
</DetailSection>
<DetailSection kicker={t('payment_section_title')}>
+6
View File
@@ -149,3 +149,9 @@ export function contactFill(row: ContactRow, now: RegistrySummary['contact'], be
export function fromRegistry(rowValue: string | null | undefined, registryValue: string | null | undefined): boolean {
return !!norm(rowValue) && norm(rowValue) === norm(registryValue)
}
/** "E-post, telefon och adress": a list the way Swedish (or English) joins it. */
export function listSv(items: string[], and: string): string {
if (items.length <= 1) return items.join('')
return `${items.slice(0, -1).join(', ')} ${and} ${items[items.length - 1]}`
}
+3
View File
@@ -8722,6 +8722,9 @@
"facts_from_registry": "from SCB",
"facts_filled_title": "Contact details filled in from SCB",
"facts_filled_description": "{fields}. Edit freely; what you type is not replaced on the next fetch.",
"facts_contact_from_registry": "{fields} from SCB.",
"facts_address_short": "address",
"facts_list_and": "and",
"fact_trade_name": "Trade name",
"open_dossier": "Open {name}",
"attn_create": "Create suggestions",
+3
View File
@@ -8722,6 +8722,9 @@
"facts_from_registry": "från SCB",
"facts_filled_title": "Kontaktuppgifter ifyllda från SCB",
"facts_filled_description": "{fields}. Ändra fritt; det du skriver ersätts inte vid nästa hämtning.",
"facts_contact_from_registry": "{fields} från SCB.",
"facts_address_short": "adress",
"facts_list_and": "och",
"fact_trade_name": "Firma",
"open_dossier": "Öppna {name}",
"attn_create": "Skapa förslag",