Files
accounted/components/settings/CompanyProfileSection.tsx
T
Jakob Wennberg ec27228a8e style: remove em/en dashes repo-wide, add CLAUDE.md rule against them (#890)
Em dashes (—) and en dashes (–) had spread across comments, docs, tests,
and a few UI strings, reading as AI-generated boilerplate rather than
house style. Replaced each with punctuation matching its context: colon
for explanatory clauses, comma for asides, plain hyphen for numeric/legal
ranges (e.g. "21-23§"), "to"/"till" for date ranges, parentheses for
paired-dash asides. messages/en.json and messages/sv.json were fixed by
hand together to keep sv/en in sync.

Left untouched where the dash is the functional subject rather than
decorative punctuation: date-range-parser.ts's separator regex,
charset-repair.ts's CP1252 byte-mapping table (and its test), the SIE
encoding mojibake docs, generic-csv.ts's minus-sign normalizer, the
agent system-prompt files that already instruct against em dashes, and
a golden iXBRL test fixture compared byte-for-byte.

Also fixes two bugs surfaced along the way: an off-by-one in
ApiKeysPanel's scope-label split (a leftover from an earlier partial
pass), and a charset-repair test that had lost the literal en-dash it
exists to verify.

Regenerated the agent atom seed migration (skills:generate) since 27
SKILL.md files changed. Added a CLAUDE.md rule against em/en dashes,
with an explicit carve-out for the functional-dash cases above.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 15:58:06 +02:00

125 lines
4.6 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 { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Skeleton } from '@/components/ui/skeleton'
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 "Hämta" form 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 (
<div className="space-y-4">
<CompanyProfileView snapshot={snapshot} fetchedAt={fetchedAt} />
<Card>
<CardHeader>
<CardTitle className="text-base">
{snapshot ? 'Uppdatera bolagsuppgifter' : 'Hämta bolagsuppgifter'}
</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleFetch} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="tic_org_number">Organisationsnummer eller personnummer</Label>
<div className="flex gap-2">
<Input
id="tic_org_number"
value={orgInput}
onChange={(e) => setOrgInput(e.target.value)}
placeholder="XXXXXX-XXXX"
inputMode="numeric"
autoComplete="off"
className="max-w-xs tabular-nums"
/>
<Button type="submit" disabled={submitting || !orgInput.trim()}>
{submitting ? (
<>
<Loader2 className="animate-spin" />
Hämtar
</>
) : (
'Hämta'
)}
</Button>
</div>
<p className="text-xs text-muted-foreground">
Uppgifterna hämtas från Bolagsverket. För enskild firma anges
personnumret.
</p>
{error && <p className="text-sm text-destructive">{error}</p>}
</div>
</form>
</CardContent>
</Card>
</div>
)
}