Files
accounted/app/(dashboard)/bookkeeping/year-end/arsredovisning/page.tsx
T
Mattsson e11f70b347 Bug/gh issues fiz (#1103)
* refactor: optimize page loading and data fetching

* fix: resolve recurring production runtime errors

* feat: add MCP company and customer updates

* fix: handle year-end tax adjustments

* feat: harden annual report compliance

* fix: expand invoice logo and font support

* fix: sanitize API route error responses

* fix: sanitize user-facing error messages

* feat: persist onboarding and tax assessment notices

* fix: reduce cloud backup audit churn

* feat: refine invoice editor layout

* fix: show saved tax adjustments in INK2

* fix: complete annual report API mappings

* docs: record operational safeguards and decisions

* fix: harden annual report review findings

* fix: adjust column span for description based on VAT registration

* New css class name
2026-07-21 23:00:15 +02:00

1073 lines
45 KiB
TypeScript

'use client'
import { useCallback, useEffect, useState } from 'react'
import Link from 'next/link'
import { useRouter, useSearchParams } from 'next/navigation'
import { useTranslations } from 'next-intl'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Skeleton } from '@/components/ui/skeleton'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { Checkbox } from '@/components/ui/checkbox'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { PageHeader } from '@/components/ui/page-header'
import { ArrowLeft, FileDown, Plus, ExternalLink, Loader2, Save, CheckCircle2, Trash2 } from 'lucide-react'
import { useToast } from '@/components/ui/use-toast'
import { formatCurrency } from '@/lib/utils'
import { FiscalYearSelector } from '@/components/common/FiscalYearSelector'
import { DigitalInlamning, INLAMNING_COMING_SOON } from '@/components/bokslut/DigitalInlamning'
import { AnnualReportStudio } from '@/components/bokslut/AnnualReportStudio'
import type { ArsredovisningData } from '@/lib/bokslut/arsredovisning/types'
import type { SignatureRequest } from '@/lib/bokslut/arsredovisning/signature-service'
import type { AnnualReportVersionSummary } from '@/lib/bokslut/arsredovisning/compliance-types'
import { getErrorMessage as getUserErrorMessage } from '@/lib/errors/get-error-message'
const SIGNATURE_EVIDENCE_REFERENCE_PATTERN =
/^(archive|document|receipt):[A-Za-z0-9][A-Za-z0-9._/-]{0,119}$/
export default function ArsredovisningPage() {
const router = useRouter()
const searchParams = useSearchParams()
const periodId = searchParams.get('period')
const { toast } = useToast()
const tStudio = useTranslations('annualReportStudio')
const [data, setData] = useState<ArsredovisningData | null>(null)
const [signatures, setSignatures] = useState<SignatureRequest[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
// Editable narrative fields: persisted to arsredovisning_narratives so
// the PDF always reflects the latest saved version and a refresh / new
// user picks up the same content.
const [description, setDescription] = useState('')
const [importantEvents, setImportantEvents] = useState('')
const [resultatdisposition, setResultatdisposition] = useState('')
const [proposedDividend, setProposedDividend] = useState('')
const [savedDescription, setSavedDescription] = useState('')
const [savedImportantEvents, setSavedImportantEvents] = useState('')
const [savedResultatdisposition, setSavedResultatdisposition] = useState('')
const [savedProposedDividend, setSavedProposedDividend] = useState('')
const [agmDate, setAgmDate] = useState('')
const [savedAgmDate, setSavedAgmDate] = useState('')
const [agmDispositionOutcome, setAgmDispositionOutcome] = useState<
'' | 'proposal_approved' | 'alternative_decision'
>('')
const [savedAgmDispositionOutcome, setSavedAgmDispositionOutcome] = useState('')
const [agmDispositionDecision, setAgmDispositionDecision] = useState('')
const [savedAgmDispositionDecision, setSavedAgmDispositionDecision] = useState('')
// Disclosure fields per ÅRL 5:13-15 § + BFNAR koncernförhållanden.
// Persisted via the same POST endpoint as the förvaltningsberättelse text.
const [longTermDebt, setLongTermDebt] = useState('')
const [savedLongTermDebt, setSavedLongTermDebt] = useState('')
const [securitiesPledged, setSecuritiesPledged] = useState('')
const [savedSecuritiesPledged, setSavedSecuritiesPledged] = useState('')
const [contingentLiabilities, setContingentLiabilities] = useState('')
const [savedContingentLiabilities, setSavedContingentLiabilities] = useState('')
const [parentName, setParentName] = useState('')
const [savedParentName, setSavedParentName] = useState('')
const [parentOrgNr, setParentOrgNr] = useState('')
const [savedParentOrgNr, setSavedParentOrgNr] = useState('')
const [parentCity, setParentCity] = useState('')
const [savedParentCity, setSavedParentCity] = useState('')
const [longTermDebtConfirmed, setLongTermDebtConfirmed] = useState(false)
const [savedLongTermDebtConfirmed, setSavedLongTermDebtConfirmed] = useState(false)
const [securitiesPledgedConfirmed, setSecuritiesPledgedConfirmed] = useState(false)
const [savedSecuritiesPledgedConfirmed, setSavedSecuritiesPledgedConfirmed] = useState(false)
const [contingentLiabilitiesConfirmed, setContingentLiabilitiesConfirmed] = useState(false)
const [savedContingentLiabilitiesConfirmed, setSavedContingentLiabilitiesConfirmed] = useState(false)
const [parentCompanyConfirmed, setParentCompanyConfirmed] = useState(false)
const [savedParentCompanyConfirmed, setSavedParentCompanyConfirmed] = useState(false)
const [savingNarrative, setSavingNarrative] = useState(false)
const [narrativeRevision, setNarrativeRevision] = useState<string | null>(null)
// Add-signer form
const [signerName, setSignerName] = useState('')
const [signerRole, setSignerRole] = useState('Styrelseledamot')
const [versions, setVersions] = useState<AnnualReportVersionSummary[]>([])
const [selectedSignatureVersionId, setSelectedSignatureVersionId] = useState('')
const [signingMethod, setSigningMethod] = useState<
'paper_original' | 'advanced_e_signature' | 'bankid'
>('paper_original')
const [signatureEvidence, setSignatureEvidence] = useState('')
const [signatureDate, setSignatureDate] = useState(() => new Date().toISOString().slice(0, 10))
const handleVersionsChanged = useCallback((nextVersions: AnnualReportVersionSummary[]) => {
setVersions(nextVersions)
setSelectedSignatureVersionId((current) => {
if (nextVersions.some((version) => version.id === current && version.status === 'ready_for_signature')) {
return current
}
return nextVersions.find((version) => version.status === 'ready_for_signature')?.id ?? ''
})
}, [])
useEffect(() => {
if (!periodId) return
let cancelled = false
Promise.all([
fetch(`/api/bookkeeping/fiscal-periods/${periodId}/arsredovisning`).then((r) => r.json()),
fetch(`/api/bookkeeping/fiscal-periods/${periodId}/arsredovisning/signatures`).then((r) =>
r.json(),
),
])
.then(([arBody, sigBody]) => {
if (cancelled) return
if (arBody?.error) {
setError(getUserErrorMessage(arBody.error) ?? 'Kunde inte hämta årsredovisning')
return
}
const d = arBody.data as ArsredovisningData
setData(d)
// buildArsredovisningData merges persisted narrative + boilerplate,
// so the values here are whatever the user will see in the PDF
// unless they edit. Track both "current draft" and "last saved" so
// we can disable Spara when there's nothing pending.
setDescription(d.forvaltningsberattelse.description)
setImportantEvents(d.forvaltningsberattelse.important_events)
setResultatdisposition(d.forvaltningsberattelse.resultatdisposition)
const dividend = String(d.forvaltningsberattelse.proposed_dividend || '')
setProposedDividend(dividend)
setAgmDate(d.forvaltningsberattelse.agm_date ?? '')
setSavedDescription(d.forvaltningsberattelse.description)
setSavedImportantEvents(d.forvaltningsberattelse.important_events)
setSavedResultatdisposition(d.forvaltningsberattelse.resultatdisposition)
setSavedProposedDividend(dividend)
setSavedAgmDate(d.forvaltningsberattelse.agm_date ?? '')
setAgmDispositionOutcome(d.forvaltningsberattelse.agm_disposition_outcome ?? '')
setSavedAgmDispositionOutcome(d.forvaltningsberattelse.agm_disposition_outcome ?? '')
setAgmDispositionDecision(d.forvaltningsberattelse.agm_disposition_decision ?? '')
setSavedAgmDispositionDecision(d.forvaltningsberattelse.agm_disposition_decision ?? '')
const ltd = d.disclosures.long_term_debt_over_five_years
const ltdStr = ltd != null ? String(ltd) : ''
setLongTermDebt(ltdStr)
setSavedLongTermDebt(ltdStr)
setSecuritiesPledged(d.disclosures.securities_pledged ?? '')
setSavedSecuritiesPledged(d.disclosures.securities_pledged ?? '')
setContingentLiabilities(d.disclosures.contingent_liabilities ?? '')
setSavedContingentLiabilities(d.disclosures.contingent_liabilities ?? '')
setParentName(d.disclosures.parent_company_name ?? '')
setSavedParentName(d.disclosures.parent_company_name ?? '')
setParentOrgNr(d.disclosures.parent_company_org_number ?? '')
setSavedParentOrgNr(d.disclosures.parent_company_org_number ?? '')
setParentCity(d.disclosures.parent_company_city ?? '')
setSavedParentCity(d.disclosures.parent_company_city ?? '')
setLongTermDebtConfirmed(d.disclosures.confirmations.long_term_debt_over_five_years)
setSavedLongTermDebtConfirmed(d.disclosures.confirmations.long_term_debt_over_five_years)
setSecuritiesPledgedConfirmed(d.disclosures.confirmations.securities_pledged)
setSavedSecuritiesPledgedConfirmed(d.disclosures.confirmations.securities_pledged)
setContingentLiabilitiesConfirmed(d.disclosures.confirmations.contingent_liabilities)
setSavedContingentLiabilitiesConfirmed(d.disclosures.confirmations.contingent_liabilities)
setParentCompanyConfirmed(d.disclosures.confirmations.parent_company)
setSavedParentCompanyConfirmed(d.disclosures.confirmations.parent_company)
setSignatures((sigBody.data ?? []) as SignatureRequest[])
})
.catch(() => {
if (!cancelled) setError('Kunde inte hämta årsredovisning')
})
.finally(() => {
if (!cancelled) setLoading(false)
})
return () => {
cancelled = true
}
}, [periodId])
const hasUnsavedNarrative =
description !== savedDescription ||
importantEvents !== savedImportantEvents ||
resultatdisposition !== savedResultatdisposition ||
proposedDividend !== savedProposedDividend ||
agmDate !== savedAgmDate ||
agmDispositionOutcome !== savedAgmDispositionOutcome ||
agmDispositionDecision !== savedAgmDispositionDecision ||
longTermDebt !== savedLongTermDebt ||
securitiesPledged !== savedSecuritiesPledged ||
contingentLiabilities !== savedContingentLiabilities ||
parentName !== savedParentName ||
parentOrgNr !== savedParentOrgNr ||
parentCity !== savedParentCity ||
longTermDebtConfirmed !== savedLongTermDebtConfirmed ||
securitiesPledgedConfirmed !== savedSecuritiesPledgedConfirmed ||
contingentLiabilitiesConfirmed !== savedContingentLiabilitiesConfirmed ||
parentCompanyConfirmed !== savedParentCompanyConfirmed
const handleSaveNarrative = useCallback(async () => {
if (!periodId) return
// Parse the long-term debt input; empty string and zero both clear the
// override (the note then falls back to the "Inga." default). Reject
// non-numeric input with a toast so the API doesn't return a 400.
let longTermDebtParsed: number | null = null
if (longTermDebt.trim()) {
const parsed = Number(longTermDebt.replace(',', '.'))
if (!Number.isFinite(parsed) || parsed < 0) {
toast({
title: 'Ogiltigt belopp',
description:
'Långfristiga skulder förfallande efter mer än fem år måste vara ett positivt tal (eller lämnas tomt).',
variant: 'destructive',
})
return
}
longTermDebtParsed = parsed
}
let proposedDividendParsed = 0
if (proposedDividend.trim()) {
const parsed = Number(proposedDividend.replace(/\s/g, '').replace(',', '.'))
if (!Number.isFinite(parsed) || parsed < 0) {
toast({
title: 'Ogiltigt belopp',
description: 'Föreslagen utdelning måste vara noll eller ett positivt belopp.',
variant: 'destructive',
})
return
}
proposedDividendParsed = Math.round(parsed * 100) / 100
}
setSavingNarrative(true)
try {
const res = await fetch(
`/api/bookkeeping/fiscal-periods/${periodId}/arsredovisning/narrative`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
description,
important_events: importantEvents,
resultatdisposition,
proposed_dividend: proposedDividendParsed,
agm_date: agmDate || null,
agm_disposition_outcome: agmDispositionOutcome || null,
agm_disposition_decision:
agmDispositionOutcome === 'alternative_decision'
? agmDispositionDecision.trim() || null
: null,
long_term_debt_over_five_years: longTermDebtParsed,
securities_pledged: securitiesPledged.trim() || null,
contingent_liabilities: contingentLiabilities.trim() || null,
parent_company_name: parentName.trim() || null,
parent_company_org_number: parentOrgNr.trim() || null,
parent_company_city: parentCity.trim() || null,
long_term_debt_over_five_years_confirmed: longTermDebtConfirmed,
securities_pledged_confirmed: securitiesPledgedConfirmed,
contingent_liabilities_confirmed: contingentLiabilitiesConfirmed,
parent_company_confirmed: parentCompanyConfirmed,
}),
},
)
const body = await res.json()
if (!res.ok) {
toast({
title: 'Kunde inte spara texten',
description: getUserErrorMessage(body?.error) ?? '',
variant: 'destructive',
})
return
}
setSavedDescription(description)
setSavedImportantEvents(importantEvents)
setSavedResultatdisposition(resultatdisposition)
setSavedProposedDividend(proposedDividend)
setSavedAgmDate(agmDate)
setSavedAgmDispositionOutcome(agmDispositionOutcome)
setSavedAgmDispositionDecision(agmDispositionDecision)
setSavedLongTermDebt(longTermDebt)
setSavedSecuritiesPledged(securitiesPledged)
setSavedContingentLiabilities(contingentLiabilities)
setSavedParentName(parentName)
setSavedParentOrgNr(parentOrgNr)
setSavedParentCity(parentCity)
setSavedLongTermDebtConfirmed(longTermDebtConfirmed)
setSavedSecuritiesPledgedConfirmed(securitiesPledgedConfirmed)
setSavedContingentLiabilitiesConfirmed(contingentLiabilitiesConfirmed)
setSavedParentCompanyConfirmed(parentCompanyConfirmed)
setNarrativeRevision(
typeof body.data?.updated_at === 'string' ? body.data.updated_at : null,
)
} catch (err) {
toast({
title: 'Kunde inte spara texten',
description: err instanceof Error ? getUserErrorMessage(err) : 'Okänt fel',
variant: 'destructive',
})
} finally {
setSavingNarrative(false)
}
}, [
periodId,
description,
importantEvents,
resultatdisposition,
proposedDividend,
agmDate,
agmDispositionOutcome,
agmDispositionDecision,
longTermDebt,
securitiesPledged,
contingentLiabilities,
parentName,
parentOrgNr,
parentCity,
longTermDebtConfirmed,
securitiesPledgedConfirmed,
contingentLiabilitiesConfirmed,
parentCompanyConfirmed,
toast,
])
const handleMarkSigned = useCallback(
async (signatureId: string) => {
if (!periodId) return
if (
!selectedSignatureVersionId ||
!SIGNATURE_EVIDENCE_REFERENCE_PATTERN.test(signatureEvidence.trim()) ||
!signatureDate
) {
toast({
title: 'Underskriftsbevis saknas',
description:
'Välj en låst version och ange datum samt en strukturerad referens, till exempel archive:AR-2026-001.',
variant: 'destructive',
})
return
}
try {
const res = await fetch(
`/api/bookkeeping/fiscal-periods/${periodId}/arsredovisning/signatures/${signatureId}`,
{
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
status: 'signed',
annual_report_version_id: selectedSignatureVersionId,
signing_method: signingMethod,
evidence_reference: signatureEvidence.trim(),
signed_at: new Date(`${signatureDate}T12:00:00`).toISOString(),
}),
},
)
const body = await res.json()
if (!res.ok) {
toast({
title: 'Kunde inte markera som signerad',
description: getUserErrorMessage(body?.error) ?? '',
variant: 'destructive',
})
return
}
setSignatures((prev) =>
prev.map((s) => (s.id === signatureId ? (body.data as SignatureRequest) : s)),
)
const versionsResponse = await fetch(
`/api/bookkeeping/fiscal-periods/${periodId}/arsredovisning/versions`,
)
if (versionsResponse.ok) {
const versionsBody = await versionsResponse.json()
handleVersionsChanged((versionsBody.data ?? []) as AnnualReportVersionSummary[])
}
toast({ title: 'Underskrift registrerad' })
} catch (err) {
toast({
title: 'Kunde inte markera som signerad',
description: err instanceof Error ? getUserErrorMessage(err) : 'Okänt fel',
variant: 'destructive',
})
}
},
[
handleVersionsChanged,
periodId,
selectedSignatureVersionId,
signatureDate,
signatureEvidence,
signingMethod,
toast,
],
)
const handleAddSigner = useCallback(async () => {
if (!periodId || !signerName.trim()) return
try {
const res = await fetch(
`/api/bookkeeping/fiscal-periods/${periodId}/arsredovisning/signatures`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ role: signerRole, signer_name: signerName.trim() }),
},
)
const body = await res.json()
if (!res.ok) {
toast({
title: 'Kunde inte lägga till undertecknare',
description: getUserErrorMessage(body?.error) ?? '',
variant: 'destructive',
})
return
}
setSignatures((prev) => [...prev, body.data as SignatureRequest])
setSignerName('')
toast({ title: 'Undertecknare tillagd', description: `${signerRole}: ${signerName}` })
} catch (err) {
toast({
title: 'Kunde inte lägga till undertecknare',
description: err instanceof Error ? getUserErrorMessage(err) : 'Okänt fel',
variant: 'destructive',
})
}
}, [periodId, signerName, signerRole, toast])
const handleRemoveSigner = useCallback(
async (signatureId: string) => {
if (!periodId) return
try {
const response = await fetch(
`/api/bookkeeping/fiscal-periods/${periodId}/arsredovisning/signatures/${signatureId}`,
{ method: 'DELETE' },
)
if (!response.ok) {
const body = await response.json()
throw new Error(getUserErrorMessage(body?.error))
}
setSignatures((current) => current.filter((signature) => signature.id !== signatureId))
toast({ title: tStudio('signer_removed') })
} catch (err) {
toast({
title: tStudio('signer_remove_error'),
description: err instanceof Error ? getUserErrorMessage(err) : undefined,
variant: 'destructive',
})
}
},
[periodId, tStudio, toast],
)
if (!periodId) {
return (
<div className="space-y-8">
<PageHeader
title="Årsredovisning"
description="Förhandsgranska och ladda ner årsredovisningen för valt räkenskapsår."
/>
<Card>
<CardHeader>
<CardTitle className="text-base">Välj räkenskapsår</CardTitle>
<p className="text-sm text-muted-foreground">
Välj det räkenskapsår du vill se årsredovisningen för. Du kan
förhandsgranska och ladda ner PDF-utkastet utan att stänga året: det
fullständiga bokslutet görs sedan via{' '}
<Link href="/bookkeeping/year-end" className="text-foreground underline underline-offset-4 decoration-muted-foreground/40 hover:decoration-foreground">
Bokslut
</Link>
.
</p>
</CardHeader>
<CardContent>
<FiscalYearSelector
value={null}
onChange={(id) => {
if (id) router.replace(`/bookkeeping/year-end/arsredovisning?period=${id}`)
}}
includeAllOption={false}
hideFuturePeriods
label={null}
/>
</CardContent>
</Card>
</div>
)
}
if (loading) {
return (
<div className="space-y-8">
<PageHeader title="Årsredovisning" />
<Card>
<CardContent className="p-6 space-y-3">
<Skeleton className="h-6 w-1/3" />
<Skeleton className="h-32 w-full" />
</CardContent>
</Card>
</div>
)
}
if (error || !data) {
return (
<div className="space-y-8">
<PageHeader title="Årsredovisning" />
<Card>
<CardContent className="p-6 text-destructive">
{error ?? 'Kunde inte hämta data'}
</CardContent>
</Card>
</div>
)
}
// PDF route reads persisted narrative from the new arsredovisning_narratives
// table. The save button below writes overrides; the URL stays clean.
const pdfUrl = `/api/bookkeeping/fiscal-periods/${periodId}/arsredovisning/pdf`
return (
<div className="space-y-8">
<PageHeader
title={`Årsredovisning ${data.fiscal_period.name}`}
description={
data.company.org_number
? `${data.company.name} · ${data.company.org_number}`
: data.company.name
}
action={
<Button variant="outline" asChild>
<Link href={`/bookkeeping/year-end?period=${periodId}`}>
<ArrowLeft className="mr-2 h-4 w-4" /> Tillbaka till bokslut
</Link>
</Button>
}
/>
{data.accounting_framework === 'k3' && (
<Card>
<CardContent className="p-4 text-sm">
<p className="font-medium">Årsredovisning enligt K3 (BFNAR 2012:1)</p>
<p className="text-muted-foreground mt-1">
Dokumentet innehåller kassaflödesanalys, förändring av eget kapital och
utökade noter (uppskjuten skatt, redovisningsprinciper, materiella
anläggningstillgångar): krav som följer K3 men inte K2.
</p>
</CardContent>
</Card>
)}
<AnnualReportStudio
periodId={periodId}
periodStart={data.fiscal_period.period_start}
periodEnd={data.fiscal_period.period_end}
framework={data.accounting_framework}
hasUnsavedNarrative={hasUnsavedNarrative}
narrativeRevision={narrativeRevision}
onVersionsChanged={handleVersionsChanged}
/>
<Card>
<CardHeader>
<CardTitle className="text-base">Förvaltningsberättelse: narrativ</CardTitle>
<p className="text-sm text-muted-foreground">
Texten nedan visas i PDF:en. Klicka <strong>Spara texten</strong> nedan
för att behålla ändringarna mellan sessioner.
</p>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="ar-description">Verksamhetsbeskrivning</Label>
<Textarea
id="ar-description"
value={description}
onChange={(e) => setDescription(e.target.value)}
rows={3}
/>
</div>
<div className="space-y-2">
<Label htmlFor="ar-events">Väsentliga händelser</Label>
<Textarea
id="ar-events"
value={importantEvents}
onChange={(e) => setImportantEvents(e.target.value)}
rows={4}
/>
</div>
<div className="space-y-2">
<Label htmlFor="ar-rd">Resultatdisposition</Label>
<Textarea
id="ar-rd"
value={resultatdisposition}
onChange={(e) => setResultatdisposition(e.target.value)}
rows={3}
/>
</div>
<div className="space-y-2">
<Label htmlFor="ar-dividend">Föreslagen utdelning (kr)</Label>
<Input
id="ar-dividend"
inputMode="decimal"
value={proposedDividend}
onChange={(event) => setProposedDividend(event.target.value)}
placeholder="0"
className="min-h-11 max-w-[220px] tabular-nums"
/>
<p className="text-xs text-muted-foreground">
Beloppet används i resultatdispositionen i samma version av PDF och iXBRL.
</p>
</div>
<div className="space-y-2">
<Label htmlFor="ar-agm-date">Datum för årsstämma</Label>
<Input
id="ar-agm-date"
type="date"
value={agmDate}
onChange={(e) => setAgmDate(e.target.value)}
className="max-w-[220px]"
/>
<p className="text-xs text-muted-foreground">
Datum årsstämman fastställde årsredovisningen: fyller i datumraden
fastställelseintyget i PDF:en (krävs för inlämning till Bolagsverket).
</p>
</div>
<div className="space-y-2">
<Label htmlFor="ar-agm-outcome">Årsstämmans beslut om resultatdisposition</Label>
<select
id="ar-agm-outcome"
className="min-h-11 w-full max-w-xl rounded-md border border-border bg-background px-3 text-sm"
value={agmDispositionOutcome}
onChange={(event) =>
setAgmDispositionOutcome(
event.target.value as
| ''
| 'proposal_approved'
| 'alternative_decision',
)
}
>
<option value="">Välj efter genomförd årsstämma</option>
<option value="proposal_approved">Styrelsens förslag godkändes</option>
<option value="alternative_decision">Årsstämman fattade ett annat beslut</option>
</select>
</div>
{agmDispositionOutcome === 'alternative_decision' && (
<div className="space-y-2">
<Label htmlFor="ar-agm-decision">Årsstämmans beslut</Label>
<Textarea
id="ar-agm-decision"
value={agmDispositionDecision}
onChange={(event) => setAgmDispositionDecision(event.target.value)}
rows={3}
/>
</div>
)}
<div className="pt-4 border-t border-border space-y-4">
<div>
<h3 className="text-sm font-medium uppercase tracking-wider text-muted-foreground">
Lagstadgade upplysningar
</h3>
<p className="text-xs text-muted-foreground mt-1">
Noter som krävs enligt ÅRL men inte kan härledas automatiskt. Tomma
fält visas som &quot;Inga.&quot; i PDF:en.
</p>
</div>
<div className="space-y-2">
<Label htmlFor="ar-ltd">
Långfristiga skulder förfallande efter mer än fem år (kr)
</Label>
<Input
id="ar-ltd"
type="text"
inputMode="decimal"
value={longTermDebt}
onChange={(e) => setLongTermDebt(e.target.value)}
placeholder="0"
className="max-w-[220px] tabular-nums"
/>
<p className="text-xs text-muted-foreground">
ÅRL 5:13 §. Lämna tomt om inga skulder förfaller senare än fem år.
</p>
<label className="flex min-h-11 cursor-pointer items-center gap-3 text-sm">
<Checkbox
id="ar-ltd-confirmed"
checked={longTermDebtConfirmed}
onCheckedChange={(checked) => setLongTermDebtConfirmed(Boolean(checked))}
/>
Jag har kontrollerat uppgiften, även om beloppet är noll.
</label>
</div>
<div className="space-y-2">
<Label htmlFor="ar-securities">Ställda säkerheter</Label>
<Textarea
id="ar-securities"
value={securitiesPledged}
onChange={(e) => setSecuritiesPledged(e.target.value)}
rows={2}
placeholder="t.ex. Företagsinteckning 500 000 kr som säkerhet för bankkredit."
/>
<p className="text-xs text-muted-foreground">ÅRL 5:14 §.</p>
<label className="flex min-h-11 cursor-pointer items-center gap-3 text-sm">
<Checkbox
id="ar-securities-confirmed"
checked={securitiesPledgedConfirmed}
onCheckedChange={(checked) => setSecuritiesPledgedConfirmed(Boolean(checked))}
/>
Jag har kontrollerat ställda säkerheter, även om svaret är inga.
</label>
</div>
<div className="space-y-2">
<Label htmlFor="ar-contingent">Eventualförpliktelser</Label>
<Textarea
id="ar-contingent"
value={contingentLiabilities}
onChange={(e) => setContingentLiabilities(e.target.value)}
rows={2}
placeholder="t.ex. Borgensåtagande för dotterbolags krediter 200 000 kr."
/>
<p className="text-xs text-muted-foreground">ÅRL 5:15 §.</p>
<label className="flex min-h-11 cursor-pointer items-center gap-3 text-sm">
<Checkbox
id="ar-contingent-confirmed"
checked={contingentLiabilitiesConfirmed}
onCheckedChange={(checked) => setContingentLiabilitiesConfirmed(Boolean(checked))}
/>
Jag har kontrollerat eventualförpliktelser, även om svaret är inga.
</label>
</div>
<div className="space-y-2">
<Label htmlFor="ar-parent-name">
Moderföretag: namn (om koncerntillhörighet)
</Label>
<Input
id="ar-parent-name"
value={parentName}
onChange={(e) => setParentName(e.target.value)}
placeholder="t.ex. AB Koncernholding"
/>
<p className="text-xs text-muted-foreground">
BFNAR 2016:10 kap. 19 / BFNAR 2012:1 kap. 8. Lämna tomt om bolaget
inte ingår i en koncern: noten utelämnas .
</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div className="space-y-2">
<Label htmlFor="ar-parent-orgnr">Moderföretagets org.nr</Label>
<Input
id="ar-parent-orgnr"
value={parentOrgNr}
onChange={(e) => setParentOrgNr(e.target.value)}
placeholder="556677-8899"
/>
</div>
<div className="space-y-2">
<Label htmlFor="ar-parent-city">Moderföretagets säte</Label>
<Input
id="ar-parent-city"
value={parentCity}
onChange={(e) => setParentCity(e.target.value)}
placeholder="Stockholm"
/>
</div>
</div>
<label className="flex min-h-11 cursor-pointer items-center gap-3 text-sm">
<Checkbox
id="ar-parent-confirmed"
checked={parentCompanyConfirmed}
onCheckedChange={(checked) => setParentCompanyConfirmed(Boolean(checked))}
/>
Jag har kontrollerat koncernförhållandet, även om bolaget saknar moderföretag.
</label>
</div>
<div className="flex items-center justify-between pt-2">
<div className="text-xs text-muted-foreground">
{hasUnsavedNarrative ? (
<span>Ändringar sparas inte automatiskt.</span>
) : narrativeRevision ? (
<span className="inline-flex items-center gap-1 text-success">
<CheckCircle2 className="h-3.5 w-3.5" /> Sparat
</span>
) : (
<span>Alla ändringar är sparade.</span>
)}
</div>
<Button
onClick={handleSaveNarrative}
disabled={savingNarrative || !hasUnsavedNarrative}
>
{savingNarrative ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> Sparar
</>
) : (
<>
<Save className="mr-2 h-4 w-4" /> Spara texten
</>
)}
</Button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">Flerårsöversikt</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>År</TableHead>
<TableHead className="text-right">Nettoomsättning</TableHead>
<TableHead className="text-right">Resultat efter fin.</TableHead>
<TableHead className="text-right">Soliditet</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.forvaltningsberattelse.flerarsoversikt.map((row) => (
<TableRow key={row.year}>
<TableCell>{row.year}</TableCell>
<TableCell className="text-right tabular-nums">
{formatCurrency(row.net_revenue)}
</TableCell>
<TableCell className="text-right tabular-nums">
{formatCurrency(row.result_after_financial)}
</TableCell>
<TableCell className="text-right tabular-nums">
{row.soliditet_pct === null
? '-'
: `${row.soliditet_pct.toFixed(1)} %`}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">Underskrifter</CardTitle>
<p className="text-sm text-muted-foreground">
Lägg till varje styrelseledamot och eventuell VD. Lås först en version i
arbetsflödet ovan. När originalet eller en extern e-signatur är klar registrerar
du datum och bevisreferens mot exakt den version som skrevs under.
</p>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4 rounded-md border border-border bg-muted/20 p-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="signature-version">Låst version</Label>
<select
id="signature-version"
className="min-h-11 w-full rounded-md border border-border bg-background px-3 text-sm"
value={selectedSignatureVersionId}
onChange={(event) => setSelectedSignatureVersionId(event.target.value)}
>
<option value="">Välj version</option>
{versions
.filter((version) => version.status === 'ready_for_signature')
.map((version) => (
<option key={version.id} value={version.id}>
Version {version.version_number}: {version.content_hash.slice(0, 12)}
</option>
))}
</select>
</div>
<div className="space-y-2">
<Label htmlFor="signature-method">Underskriftsmetod</Label>
<select
id="signature-method"
className="min-h-11 w-full rounded-md border border-border bg-background px-3 text-sm"
value={signingMethod}
onChange={(event) =>
setSigningMethod(
event.target.value as
| 'paper_original'
| 'advanced_e_signature'
| 'bankid',
)
}
>
<option value="paper_original">Undertecknat original papper</option>
<option value="advanced_e_signature">Avancerad e-signatur</option>
<option value="bankid">BankID via extern signeringstjänst</option>
</select>
</div>
<div className="space-y-2">
<Label htmlFor="signature-date">Underskriftsdatum</Label>
<Input
id="signature-date"
type="date"
className="min-h-11"
value={signatureDate}
onChange={(event) => setSignatureDate(event.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="signature-evidence">Bevisreferens</Label>
<Input
id="signature-evidence"
className="min-h-11"
value={signatureEvidence}
onChange={(event) => setSignatureEvidence(event.target.value)}
placeholder="archive:AR-2026-001"
maxLength={128}
autoComplete="off"
/>
</div>
<p className="text-xs text-muted-foreground md:col-span-2">
Accounted registrerar beviset men skapar inte själva underskriften. Spara
originalet eller signeringskvittot enligt bolagets dokumenthantering.
</p>
</div>
{signatures.length === 0 && (
<p className="text-sm text-muted-foreground italic">
Inga undertecknare tillagda än.
</p>
)}
{signatures.map((sig) => (
<div
key={sig.id}
className="flex flex-col gap-3 border-b border-border pb-3 last:border-b-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between"
>
<div>
<p className="text-sm font-medium">{sig.signer_name}</p>
<p className="text-xs text-muted-foreground">{sig.role}</p>
</div>
<div className="flex w-full flex-wrap items-center justify-end gap-2 sm:w-auto">
{sig.status === 'signed' ? (
<Badge variant="success">Signerad</Badge>
) : sig.status === 'declined' ? (
<Badge variant="destructive">Avböjd</Badge>
) : (
<>
<Badge variant="outline">Väntar underskrift</Badge>
{sig.annual_report_version_id === null && (
<Button
size="icon"
variant="ghost"
className="min-h-11 min-w-11"
aria-label={tStudio('remove_signer')}
onClick={() => void handleRemoveSigner(sig.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
)}
<Button
size="sm"
variant="outline"
className="min-h-11"
onClick={() => void handleMarkSigned(sig.id)}
disabled={
!selectedSignatureVersionId ||
!SIGNATURE_EVIDENCE_REFERENCE_PATTERN.test(signatureEvidence.trim()) ||
!signatureDate
}
>
Markera som signerad
</Button>
</>
)}
</div>
</div>
))}
<div className="flex flex-col gap-3 pt-2 sm:flex-row sm:flex-wrap sm:items-end">
<div className="space-y-1">
<Label htmlFor="signer-role" className="text-xs">
Roll
</Label>
<select
id="signer-role"
className="min-h-11 rounded-md border border-border bg-background px-3 text-sm"
value={signerRole}
onChange={(e) => setSignerRole(e.target.value)}
>
<option>Styrelseledamot</option>
<option>Styrelseordförande</option>
<option>VD</option>
<option>Verkställande direktör</option>
</select>
</div>
<div className="space-y-1 flex-1 min-w-[200px]">
<Label htmlFor="signer-name" className="text-xs">
Namn
</Label>
<Input
id="signer-name"
value={signerName}
onChange={(e) => setSignerName(e.target.value)}
placeholder="t.ex. Anna Andersson"
className="min-h-11"
/>
</div>
<Button className="min-h-11 w-full sm:w-auto" onClick={handleAddSigner} disabled={!signerName.trim()}>
<Plus className="mr-1 h-4 w-4" /> Lägg till
</Button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">PDF för pappersinlämning</CardTitle>
</CardHeader>
<CardContent className="space-y-4 text-sm">
<p className="text-muted-foreground">
Ladda ner PDF-utkastet och granska det. För pappersinlämning ska
årsredovisningens original skrivas under av samtliga styrelseledamöter och
eventuell VD. En bestyrkt kopia med fastställelseintyg skickas sedan per post
till Bolagsverket. PDF-filen kan inte laddas upp som digital årsredovisning.
</p>
<div className="flex flex-wrap gap-3">
<Button className="min-h-11" asChild>
<Link href={pdfUrl} target="_blank" rel="noopener noreferrer">
<FileDown className="mr-2 h-4 w-4" /> Ladda ner PDF (utkast)
</Link>
</Button>
{/* Bolagsverket-delarna blurras tills integrationen är godkänd:
rubriken, instruktionstexten och PDF-knappen förblir skarpa. */}
<span
inert={INLAMNING_COMING_SOON}
aria-hidden={INLAMNING_COMING_SOON}
className={
INLAMNING_COMING_SOON
? 'pointer-events-none select-none blur-[3px] opacity-60'
: undefined
}
>
<Button className="min-h-11" variant="outline" asChild>
<Link
href="https://bolagsverket.se/foretag/aktiebolag/arsredovisningforaktiebolag.759.html"
target="_blank"
rel="noopener noreferrer"
>
<ExternalLink className="mr-2 h-4 w-4" /> Bolagsverket om årsredovisning
</Link>
</Button>
</span>
</div>
<div
inert={INLAMNING_COMING_SOON}
aria-hidden={INLAMNING_COMING_SOON}
className={
INLAMNING_COMING_SOON
? 'pointer-events-none select-none blur-[3px] opacity-60 space-y-4'
: 'space-y-4'
}
>
{data.warnings.length > 0 && (
<div className="rounded-md border border-border bg-muted/30 p-3 text-xs space-y-1">
<p className="font-medium">Innan inlämning till Bolagsverket:</p>
<ul className="list-disc pl-5 space-y-1">
{data.warnings.map((w, i) => (
<li key={i}>{w}</li>
))}
</ul>
</div>
)}
<div className="rounded-md border border-border bg-muted/30 p-3 text-xs">
<strong>Digital inlämning är frivillig.</strong> Den görs som iXBRL genom en
ansluten programvara. Accounteds direktinlämning förblir stängd tills avtal,
certifikat och Bolagsverkets acceptanstest är klara. PDF-flödet ovan är den
separata vägen för pappersinlämning.
</div>
</div>
</CardContent>
</Card>
{data.accounting_framework === 'k2' && periodId && (
<DigitalInlamning periodId={periodId} />
)}
</div>
)
}