db8983ba9e
* feat(arcim-migration): Briox provider with SIE-over-API import - Briox auth via account ID + application token (no app-level credentials); both tokens rotate on refresh and are persisted - New sie-fetcher pulls the general ledger as SIE through the provider API for Fortnox, Briox and Bjorn Lunden - Wizard stops on a failed SIE import and surfaces the real errors instead of proceeding to the misleading migrate-guard message - PROVIDER_SIE_ONLY_FORTNOX renamed to PROVIDER_SIE_NOT_SUPPORTED; new PROVIDER_TOKEN_INVALID for rejected provider credentials Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(bookkeeping): per-line accruals (periodisering) on invoices and supplier invoices Defer revenue/costs per invoice line to 29xx/17xx interim accounts with automatic monthly dissolution (nightly cron + catch-up at registration), schedule cancellation on credit, year-end auto-detect exclusion for already-scheduled invoices, invoice-inbox service-period extraction for prefill, and an MCP tool to list schedules. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(bokslut): iXBRL arsredovisning generation and Bolagsverket digital filing Generate the annual report as iXBRL from a generated taxonomy registry (K2 element lists, taxonomy:generate/check scripts + CI guard), expose it via the fiscal-period API, and add the bolagsverket extension for digital submission to eget utrymme with webhook-driven status tracking (submissions table + pg tests, lifecycle events, year-end wizard UI). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(mcp): raise origin-guard test timeout to 20s The dynamic import pulls in the full server module; the parse alone flirts with the 5s default under full-suite parallel load. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Add new scripts and documentation for K2 AB taxonomy generation and validation - Introduced `generate-taxonomy-registry.ts` to automate the generation of the iXBRL taxonomy concept registry from official element lists and tuple models. - Added `validate-ixbrl.mjs` for validating generated iXBRL reports against the official taxonomy package using Arelle. - Included new documentation files: - `k2-ab-arsredovisning-elementlista-2024-09-12_rev20250312_sv.xlsx` - `tuple-innehallsmodell-arsredovisning-k2-2024-09-12.xlsx` - `taxonomi-paket-2024-09-12_rev20250312.zip` * Add tests for bookkeeping accruals dissolution and supplier invoices - Implement tests for the POST /api/bookkeeping/accruals/[id]/dissolve route, covering success and error scenarios. - Add tests for the DELETE /api/supplier-invoices/[id] route, including authentication checks and validation of invoice deletion conditions. - Introduce tests for the Arcim migration provider client, ensuring token handling and error classification. - Create tests for the Bolagsverket extension, validating submission role enforcement and environment settings. - Add Zod schemas for Bolagsverket response payloads to ensure proper validation. - Implement tests for MCP server's list accrual schedules, confirming registration and scope mapping. - Add consistency tests for IXBRL document generation, ensuring duplicate facts and XML escaping are handled correctly. - Introduce typed domain errors for accrual schedules to improve error handling in the service. - Add tests for resolving consent with Briox token refresh concurrency, ensuring proper token management and error handling. * fix(tests): update payload size guard comments to reflect recent changes in tool descriptions and ceiling adjustments * fix(gitattributes): mark generated JSON files in bokslut taxonomy as linguist-generated * feat(migrations): add backfill for invoices.journal_entry_id and fallback for next_voucher_number user_id * feat(bokslut): enhance compliance and financial processing features with new submission details and security measures --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
738 lines
29 KiB
TypeScript
738 lines
29 KiB
TypeScript
'use client'
|
|
|
|
import { useCallback, useEffect, useState } from 'react'
|
|
import Link from 'next/link'
|
|
import { useRouter, useSearchParams } from 'next/navigation'
|
|
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 { PageHeader } from '@/components/ui/page-header'
|
|
import { ArrowLeft, FileDown, Plus, ExternalLink, Loader2, Save, CheckCircle2 } from 'lucide-react'
|
|
import { useToast } from '@/components/ui/use-toast'
|
|
import { FiscalYearSelector } from '@/components/common/FiscalYearSelector'
|
|
import { DigitalInlamning, INLAMNING_COMING_SOON } from '@/components/bokslut/DigitalInlamning'
|
|
import type { ArsredovisningData } from '@/lib/bokslut/arsredovisning/types'
|
|
import type { SignatureRequest } from '@/lib/bokslut/arsredovisning/signature-service'
|
|
|
|
export default function ArsredovisningPage() {
|
|
const router = useRouter()
|
|
const searchParams = useSearchParams()
|
|
const periodId = searchParams.get('period')
|
|
const { toast } = useToast()
|
|
|
|
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 [savedDescription, setSavedDescription] = useState('')
|
|
const [savedImportantEvents, setSavedImportantEvents] = useState('')
|
|
const [savedResultatdisposition, setSavedResultatdisposition] = useState('')
|
|
const [agmDate, setAgmDate] = useState('')
|
|
const [savedAgmDate, setSavedAgmDate] = 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 [savingNarrative, setSavingNarrative] = useState(false)
|
|
const [savedAt, setSavedAt] = useState<number | null>(null)
|
|
|
|
// Add-signer form
|
|
const [signerName, setSignerName] = useState('')
|
|
const [signerRole, setSignerRole] = useState('Styrelseledamot')
|
|
|
|
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(arBody.error.message ?? '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)
|
|
setAgmDate(d.forvaltningsberattelse.agm_date ?? '')
|
|
setSavedDescription(d.forvaltningsberattelse.description)
|
|
setSavedImportantEvents(d.forvaltningsberattelse.important_events)
|
|
setSavedResultatdisposition(d.forvaltningsberattelse.resultatdisposition)
|
|
setSavedAgmDate(d.forvaltningsberattelse.agm_date ?? '')
|
|
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 ?? '')
|
|
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 ||
|
|
agmDate !== savedAgmDate ||
|
|
longTermDebt !== savedLongTermDebt ||
|
|
securitiesPledged !== savedSecuritiesPledged ||
|
|
contingentLiabilities !== savedContingentLiabilities ||
|
|
parentName !== savedParentName ||
|
|
parentOrgNr !== savedParentOrgNr ||
|
|
parentCity !== savedParentCity
|
|
|
|
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
|
|
}
|
|
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,
|
|
agm_date: agmDate || 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,
|
|
}),
|
|
},
|
|
)
|
|
const body = await res.json()
|
|
if (!res.ok) {
|
|
toast({
|
|
title: 'Kunde inte spara texten',
|
|
description: body?.error?.message ?? '',
|
|
variant: 'destructive',
|
|
})
|
|
return
|
|
}
|
|
setSavedDescription(description)
|
|
setSavedImportantEvents(importantEvents)
|
|
setSavedResultatdisposition(resultatdisposition)
|
|
setSavedAgmDate(agmDate)
|
|
setSavedLongTermDebt(longTermDebt)
|
|
setSavedSecuritiesPledged(securitiesPledged)
|
|
setSavedContingentLiabilities(contingentLiabilities)
|
|
setSavedParentName(parentName)
|
|
setSavedParentOrgNr(parentOrgNr)
|
|
setSavedParentCity(parentCity)
|
|
setSavedAt(Date.now())
|
|
} catch (err) {
|
|
toast({
|
|
title: 'Kunde inte spara texten',
|
|
description: err instanceof Error ? err.message : 'Okänt fel',
|
|
variant: 'destructive',
|
|
})
|
|
} finally {
|
|
setSavingNarrative(false)
|
|
}
|
|
}, [
|
|
periodId,
|
|
description,
|
|
importantEvents,
|
|
resultatdisposition,
|
|
agmDate,
|
|
longTermDebt,
|
|
securitiesPledged,
|
|
contingentLiabilities,
|
|
parentName,
|
|
parentOrgNr,
|
|
parentCity,
|
|
toast,
|
|
])
|
|
|
|
const handleMarkSigned = useCallback(
|
|
async (signatureId: string) => {
|
|
if (!periodId) 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' }),
|
|
},
|
|
)
|
|
const body = await res.json()
|
|
if (!res.ok) {
|
|
toast({
|
|
title: 'Kunde inte markera som signerad',
|
|
description: body?.error?.message ?? '',
|
|
variant: 'destructive',
|
|
})
|
|
return
|
|
}
|
|
setSignatures((prev) =>
|
|
prev.map((s) => (s.id === signatureId ? (body.data as SignatureRequest) : s)),
|
|
)
|
|
toast({ title: 'Underskrift registrerad' })
|
|
} catch (err) {
|
|
toast({
|
|
title: 'Kunde inte markera som signerad',
|
|
description: err instanceof Error ? err.message : 'Okänt fel',
|
|
variant: 'destructive',
|
|
})
|
|
}
|
|
},
|
|
[periodId, 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: body?.error?.message ?? '',
|
|
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 ? err.message : 'Okänt fel',
|
|
variant: 'destructive',
|
|
})
|
|
}
|
|
}, [periodId, signerName, signerRole, 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>
|
|
)}
|
|
|
|
<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-1.5">
|
|
<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-1.5">
|
|
<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-1.5">
|
|
<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-1.5">
|
|
<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="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-1.5">
|
|
<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>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<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>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<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>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<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-1.5">
|
|
<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-1.5">
|
|
<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>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between pt-2">
|
|
<div className="text-xs text-muted-foreground">
|
|
{hasUnsavedNarrative ? (
|
|
<span>Ändringar sparas inte automatiskt.</span>
|
|
) : savedAt ? (
|
|
<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 className="w-full text-sm">
|
|
<thead>
|
|
<tr className="text-left text-xs uppercase tracking-wider text-muted-foreground border-b border-border">
|
|
<th className="py-2">År</th>
|
|
<th className="py-2 text-right">Nettoomsättning</th>
|
|
<th className="py-2 text-right">Resultat efter fin.</th>
|
|
<th className="py-2 text-right">Soliditet</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{data.forvaltningsberattelse.flerarsoversikt.map((row) => (
|
|
<tr key={row.year} className="border-b border-border last:border-b-0">
|
|
<td className="py-2">{row.year}</td>
|
|
<td className="py-2 text-right tabular-nums">
|
|
{row.net_revenue.toLocaleString('sv-SE')}
|
|
</td>
|
|
<td className="py-2 text-right tabular-nums">
|
|
{row.result_after_financial.toLocaleString('sv-SE')}
|
|
</td>
|
|
<td className="py-2 text-right tabular-nums">
|
|
{row.soliditet_pct === null
|
|
? '—'
|
|
: `${row.soliditet_pct.toFixed(1)} %`}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">Underskrifter</CardTitle>
|
|
<p className="text-sm text-muted-foreground">
|
|
Lägg till varje styrelseledamot + VD som ska skriva under. BankID-signering
|
|
kommer i en kommande version — för nu visas slottar och status här, och
|
|
själva underskriften görs på pappret.
|
|
</p>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{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 items-center justify-between border-b border-border last:border-b-0 pb-3 last:pb-0"
|
|
>
|
|
<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 items-center gap-2">
|
|
{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>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => void handleMarkSigned(sig.id)}
|
|
>
|
|
Markera som signerad
|
|
</Button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
<div className="flex flex-wrap gap-2 items-end pt-2">
|
|
<div className="space-y-1">
|
|
<Label htmlFor="signer-role" className="text-xs">
|
|
Roll
|
|
</Label>
|
|
<select
|
|
id="signer-role"
|
|
className="border border-border rounded-md h-9 text-sm px-2 bg-background"
|
|
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="h-9"
|
|
/>
|
|
</div>
|
|
<Button 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">Ladda ner & lämna in</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4 text-sm">
|
|
<p className="text-muted-foreground">
|
|
Ladda ner PDF-utkastet, granska, skriv ut och låt undertecknarna signera
|
|
fastställelseintyget. Ladda sedan upp PDF:en till Bolagsverkets e-tjänst.
|
|
</p>
|
|
<div className="flex flex-wrap gap-3">
|
|
<Button 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 variant="outline" asChild>
|
|
<Link
|
|
href="https://www.bolagsverket.se/foretag/aktiebolag/arsredovisning/lamna-in-arsredovisning"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
<ExternalLink className="mr-2 h-4 w-4" /> Bolagsverket Mina Sidor
|
|
</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-warning/40 bg-warning/5 p-3 text-xs text-warning-foreground 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-warning/40 bg-warning/5 p-3 text-xs text-warning-foreground">
|
|
<strong>Notis om digital inlämning:</strong> Digital inlämning (iXBRL) av
|
|
årsredovisning föreslås bli obligatorisk för K2/K3-aktiebolag för
|
|
räkenskapsår som inleds efter 2025-12-31. Använd avsnittet{' '}
|
|
<strong>Digital inlämning</strong> nedan för att granska, validera och lämna
|
|
in årsredovisningen som iXBRL — PDF:en ovan är ett läsexemplar.
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{data.accounting_framework === 'k2' && periodId && (
|
|
<DigitalInlamning periodId={periodId} />
|
|
)}
|
|
</div>
|
|
)
|
|
}
|