e11f70b347
* 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
1073 lines
45 KiB
TypeScript
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 på <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 då årsstämman fastställde årsredovisningen: fyller i datumraden på
|
|
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 "Inga." 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 då.
|
|
</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 på 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 på 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>
|
|
)
|
|
}
|