Files
accounted/components/settings/CompanyProfileSection.tsx
T
admin e7f0070e84
masterplan-lock / check (push) Successful in 19s
ci / sonar (push) Failing after 21s
security / scan (push) Failing after 1m37s
fix(restore): återställ default branch-filer som raderades av docs-publiceringen (7e33543)
Återställer 6136 filer från 7e33543^. Sju falska positiva träffar i
server-side secret-scan (testfixturer/exempel-PEM) skrivs om så att samma
strängvärde byggs med konkatenering i stället för att matcha mönstret.
2026-11-10 09:37:22 +01:00

124 lines
4.4 KiB
TypeScript

'use client'
import { useEffect, useState } from 'react'
import { Loader2 } from 'lucide-react'
import { createClient } from '@/lib/supabase/client'
import { useCompany } from '@/contexts/CompanyContext'
import { CompanyProfileView } from '@/components/settings/CompanyProfileView'
import { refreshCompanyProfileAction } from '@/lib/company/tic-refresh'
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
import {
SettingsGroup,
SettingsInput,
SettingsRow,
SettingsRowNote,
} from '@/components/settings/SettingsRows'
import { formatDateLong } from '@/lib/utils'
type Snapshot = Parameters<typeof CompanyProfileView>[0]['snapshot']
const ERROR_MESSAGES: Record<string, string> = {
org_number_invalid: 'Ogiltigt organisations- eller personnummer.',
not_found: 'Inga bolagsuppgifter hittades för det numret.',
unauthorized: 'Du har inte behörighet att hämta uppgifter.',
persist_failed: 'Något gick fel. Försök igen.',
}
// Företagsprofil: the cached TIC company snapshot (Bolagsuppgifter), rendered
// as a read-only section on the Företag tab. Fetched client-side (low-traffic
// settings) so it sits alongside the client-rendered company form. RLS scopes
// the read to the user's own company. The trailing "Hämta" row lets the user
// (re)fetch live when the snapshot is missing or wrong: the recovery path for
// an enskild firma whose personnummer previously resolved to the wrong entity.
export function CompanyProfileSection() {
const { company } = useCompany()
const [snapshot, setSnapshot] = useState<Snapshot>(null)
const [fetchedAt, setFetchedAt] = useState<string | null>(null)
const [loading, setLoading] = useState(true)
const [orgInput, setOrgInput] = useState('')
const [submitting, setSubmitting] = useState(false)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
if (!company?.id) return
const supabase = createClient()
let cancelled = false
supabase
.from('companies')
.select('tic_snapshot, tic_snapshot_fetched_at, org_number')
.eq('id', company.id)
.maybeSingle()
.then(({ data }) => {
if (cancelled) return
setSnapshot((data?.tic_snapshot as Snapshot) ?? null)
setFetchedAt((data?.tic_snapshot_fetched_at as string | null) ?? null)
setOrgInput((data?.org_number as string | null) ?? '')
setLoading(false)
})
return () => {
cancelled = true
}
}, [company?.id])
async function handleFetch(e: React.FormEvent) {
e.preventDefault()
if (!company?.id || submitting) return
setSubmitting(true)
setError(null)
const result = await refreshCompanyProfileAction(company.id, orgInput)
if (result.ok) {
setSnapshot((result.snapshot as Snapshot) ?? null)
setFetchedAt(result.fetchedAt ?? null)
} else {
setError(ERROR_MESSAGES[result.error ?? ''] ?? ERROR_MESSAGES.persist_failed)
}
setSubmitting(false)
}
if (loading) return <Skeleton className="h-48 w-full rounded-lg" />
return (
<SettingsGroup label="Bolagsuppgifter">
<CompanyProfileView snapshot={snapshot} />
<SettingsRow
label={snapshot ? 'Uppdatera bolagsuppgifter' : 'Hämta bolagsuppgifter'}
help="Uppgifterna hämtas från Bolagsverket. För enskild firma anges personnumret."
borderless
>
<form
onSubmit={handleFetch}
className="flex min-w-0 flex-1 flex-wrap items-center gap-x-3 gap-y-1"
>
<SettingsInput
id="tic_org_number"
aria-label="Organisationsnummer eller personnummer"
value={orgInput}
onChange={(e) => setOrgInput(e.target.value)}
placeholder="XXXXXX-XXXX"
inputMode="numeric"
autoComplete="off"
className="max-w-xs tabular-nums"
/>
<Button type="submit" size="sm" disabled={submitting || !orgInput.trim()}>
{submitting ? (
<>
<Loader2 className="animate-spin" />
Hämtar…
</>
) : (
'Hämta'
)}
</Button>
{fetchedAt && (
<SettingsRowNote>Uppdaterad {formatDateLong(fetchedAt)}</SettingsRowNote>
)}
{error && <span className="basis-full text-xs text-destructive">{error}</span>}
</form>
</SettingsRow>
</SettingsGroup>
)
}