import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { formatDate, formatDateLong } from '@/lib/utils' // Read-only "Bolagsuppgifter" view of the cached TIC company profile // (companies.tic_snapshot). Lives in core — reads the snapshot as plain // JSON rather than importing the TIC extension's types, so the // core-build CI boundary (no core → @/extensions/) stays intact. // // The snapshot is written by the TIC /profile endpoint; shape mirrors // TICCompanyProfile. We type only the fields we render and treat // everything as optional/defensive since older snapshots predate some // sections. interface SnapshotShape { companyName?: string | null orgNumber?: string | null legalEntityType?: string | null address?: { street?: string | null; postalCode?: string | null; city?: string | null } | null registration?: { fTax?: boolean; vat?: boolean; payroll?: boolean } | null sniCodes?: { code: string; name: string }[] | null bankAccounts?: { type: string; accountNumber: string; bic?: string | null }[] | null purpose?: string | null employeeRange?: string | null financials?: { periodStart?: number periodEnd?: number netSalesK?: number | null operatingProfitK?: number | null } | null statuses?: { code?: string | null description?: string | null color?: 'red' | 'yellow' | 'green' | 'neutral' | string | null statusDate?: string | null isCeased?: boolean | null }[] | null fiscalYear?: { startMonthDay?: string | null; endMonthDay?: string | null } | null signatory?: { description: string }[] | null board?: { numberOfBoardMembers?: number | null numberOfDeputyBoardMembers?: number | null } | null representatives?: { name?: string | null positionType?: string | null positionStart?: string | null }[] | null } // Clean Bolagsverket signatory text: the source carries ">" list markers // and collapses several rules onto one line. Strip the markers, normalise // whitespace, and split run-on "Firman tecknas …" clauses onto their own // lines so each rule reads as a sentence. function cleanSignatory(raw: string): string[] { const normalised = raw .replace(/>/g, ' ') .replace(/\s+/g, ' ') .trim() // Each firmateckningsregel starts with "Firman tecknas". Split on the // boundary before subsequent occurrences so they stack vertically. return normalised .split(/(?=Firman tecknas)/g) .map((s) => s.trim()) .filter((s) => s.length > 0) } function Section({ title, children }: { title: string; children: React.ReactNode }) { return (

{title}

{children}
) } export function CompanyProfileView({ snapshot, fetchedAt, }: { snapshot: SnapshotShape | null fetchedAt: string | null }) { if (!snapshot) { return ( Bolagsuppgifter

Inga företagsuppgifter hämtade ännu. Uppgifterna hämtas automatiskt från Bolagsverket via organisationsnumret.

) } const entityLabel = snapshot.legalEntityType === 'AB' ? 'Aktiebolag' : snapshot.legalEntityType === 'EF' ? 'Enskild firma' : snapshot.legalEntityType ?? null const reg = snapshot.registration const regBadges = [ reg?.fTax ? 'F-skatt' : null, reg?.vat ? 'Moms' : null, reg?.payroll ? 'Arbetsgivare' : null, ].filter(Boolean) as string[] const fyLabel = snapshot.fiscalYear?.startMonthDay && snapshot.fiscalYear?.endMonthDay ? `${snapshot.fiscalYear.startMonthDay} – ${snapshot.fiscalYear.endMonthDay}` : null return ( Bolagsuppgifter {fetchedAt && (

Uppdaterad {formatDateLong(fetchedAt)}

)}
{/* Identity */}

{snapshot.companyName ?? 'Okänt företag'}

{[snapshot.orgNumber, entityLabel].filter(Boolean).join(' · ')}

{snapshot.address && (

{[ snapshot.address.street, [snapshot.address.postalCode, snapshot.address.city].filter(Boolean).join(' '), ] .filter(Boolean) .join(', ')}

)}
{regBadges.length > 0 && (
{regBadges.map((b) => ( {b} ))}
)} {Array.isArray(snapshot.sniCodes) && snapshot.sniCodes.length > 0 && (
    {snapshot.sniCodes.map((s) => (
  • {s.code}{' '} {s.name}
  • ))}
)} {Array.isArray(snapshot.bankAccounts) && snapshot.bankAccounts.length > 0 && (
    {snapshot.bankAccounts.map((b, i) => (
  • {b.type}:{' '} {b.accountNumber}
  • ))}
)} {snapshot.purpose && (

{snapshot.purpose}

)}

{snapshot.employeeRange ?? 'Inga anställda'}

{snapshot.financials ? (
Nettoomsättning
{snapshot.financials.netSalesK != null ? `${snapshot.financials.netSalesK.toLocaleString('sv-SE')} tkr` : '—'}
Rörelseresultat
{snapshot.financials.operatingProfitK != null ? `${snapshot.financials.operatingProfitK.toLocaleString('sv-SE')} tkr` : '—'}
) : (

Inga finansiella uppgifter tillgängliga.

)}
{(() => { // Only show dated status entries — Bolagsverket emits informational // flags like "Har aldrig varit verksam" with no date that read as // noise next to the real ones. Plain text, no colour: per the // design system, semantic colour is data-only and never chrome. const datedStatuses = (snapshot.statuses ?? []).filter((s) => s.statusDate) if (datedStatuses.length === 0) return null return (
{datedStatuses.map((s, i) => (
{s.description ?? s.code ?? '—'}
{formatDate(s.statusDate!)}
))}
) })()} {fyLabel && (

Nuvarande: {fyLabel}

)} {(() => { // Flatten every signatory row, clean ">" markers, split run-on // clauses, and dedupe — the source repeats "Firman tecknas av // styrelsen" across rows. const rules = Array.from( new Set( (snapshot.signatory ?? []).flatMap((s) => cleanSignatory(s.description)), ), ) if (rules.length === 0) return null return (
    {rules.map((rule, i) => (
  • {rule}
  • ))}
) })()} {Array.isArray(snapshot.representatives) && snapshot.representatives.length > 0 && (
{snapshot.board && (

{[ snapshot.board.numberOfBoardMembers != null ? `${snapshot.board.numberOfBoardMembers} styrelseledamot/-ledamöter` : null, snapshot.board.numberOfDeputyBoardMembers != null ? `${snapshot.board.numberOfDeputyBoardMembers} suppleant(er)` : null, ] .filter(Boolean) .join(' · ')}

)}
    {snapshot.representatives.map((r, i) => (
  • {r.name ?? '—'} {[r.positionType, r.positionStart ? formatDate(r.positionStart) : null] .filter(Boolean) .join(' · ')}
  • ))}
)}
) }