'use client' import { useCallback, useEffect, useMemo, useState } from 'react' import Link from 'next/link' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Skeleton } from '@/components/ui/skeleton' import { AlertTriangle, FileDown, Info } from 'lucide-react' import { formatCurrency } from '@/lib/utils' import type { EgenavgiftCategory } from '@/lib/bokslut/enskild-firma/egenavgifter-calculator' import type { EfDeclarationItem } from '@/lib/bokslut/enskild-firma/types' import { getErrorMessage as getUserErrorMessage } from '@/lib/errors/get-error-message' interface EfDeclarationSectionProps { fiscalPeriodId: string /** Bokfört resultat (income statement net_result): shown as the surplus * base in the wizard header. Server recomputes from the trial balance. */ bookedSurplus: number /** Closing year of the fiscal period (for periodiseringsfond cohort). */ fiscalYear: number } interface EfOverrideInputs { category: EgenavgiftCategory kapitalunderlag: string priorSchablon: string priorActual: string pfondDesired: string expansionsfondBalance: string expansionsfondChange: string } interface EfPreviewResponse { fiscalPeriod: { id: string name: string period_start: string period_end: string } bookedSurplus: number items: EfDeclarationItem[] postedEntryCount: number inputWarnings: string[] } const DEFAULT_OVERRIDES: EfOverrideInputs = { category: 'full', kapitalunderlag: '', priorSchablon: '', priorActual: '', pfondDesired: '', expansionsfondBalance: '', expansionsfondChange: '', } /** * EF declaration step: fetches the four calculator outputs (egenavgifter, * räntefördelning, periodiseringsfond-EF, expansionsfond) from the server * so the same source-of-truth (computeEfDeclarationPreview) is used by * wizard, MCP tool and NE-bilaga. * * EF tax mechanisms are declaration-only: they NEVER produce journal * entries. The banner makes that BFL distinction visible. * * Override inputs persist to localStorage scoped by fiscal period id, so * re-entering the wizard recalls them without round-tripping a write. */ export function EfDeclarationSection({ fiscalPeriodId, bookedSurplus, fiscalYear, }: EfDeclarationSectionProps) { const storageKey = `ef-declaration-overrides:${fiscalPeriodId}` const [overrides, setOverrides] = useState(DEFAULT_OVERRIDES) const [preview, setPreview] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) // Restore overrides from localStorage on mount. useEffect(() => { if (typeof window === 'undefined') return try { const raw = window.localStorage.getItem(storageKey) if (raw) { const parsed = JSON.parse(raw) as Partial setOverrides({ ...DEFAULT_OVERRIDES, ...parsed }) } } catch { // Ignore: start with defaults. } }, [storageKey]) // Persist overrides on change. useEffect(() => { if (typeof window === 'undefined') return try { window.localStorage.setItem(storageKey, JSON.stringify(overrides)) } catch { // Quota exceeded or disabled, non-fatal. } }, [storageKey, overrides]) const queryString = useMemo(() => { const params = new URLSearchParams() params.set('category', overrides.category) const kap = parseFloat(overrides.kapitalunderlag) if (Number.isFinite(kap)) params.set('kapitalunderlag', String(kap)) const ps = parseFloat(overrides.priorSchablon) if (Number.isFinite(ps)) params.set('priorYearSchablonavdrag', String(ps)) const pa = parseFloat(overrides.priorActual) if (Number.isFinite(pa)) params.set('priorYearActualCharged', String(pa)) const pf = parseFloat(overrides.pfondDesired) if (Number.isFinite(pf)) params.set('pfondDesiredAmount', String(pf)) const eb = parseFloat(overrides.expansionsfondBalance) if (Number.isFinite(eb)) params.set('expansionsfondExistingBalance', String(eb)) const ec = parseFloat(overrides.expansionsfondChange) if (Number.isFinite(ec) && ec !== 0) params.set('expansionsfondDesiredChange', String(ec)) return params.toString() }, [overrides]) const loadPreview = useCallback(async () => { setLoading(true) setError(null) try { const res = await fetch( `/api/bookkeeping/fiscal-periods/${fiscalPeriodId}/ef-declaration?${queryString}`, ) const body = await res.json() if (!res.ok) { setError(getUserErrorMessage(body?.error) ?? 'Kunde inte ladda EF-deklaration') setPreview(null) return } setPreview(body.data as EfPreviewResponse) } catch { setError('Kunde inte ladda EF-deklaration') setPreview(null) } finally { setLoading(false) } }, [fiscalPeriodId, queryString]) // Debounce refetch so each keystroke doesn't slam the API. 350 ms feels // responsive without being noisy. useEffect(() => { const handle = setTimeout(() => { void loadPreview() }, 350) return () => clearTimeout(handle) }, [loadPreview]) const update = useCallback( (field: K, value: EfOverrideInputs[K]) => { setOverrides((prev) => ({ ...prev, [field]: value })) }, [], ) const items = preview?.items ?? [] const inputWarnings = preview?.inputWarnings ?? [] const noPostedEntries = preview ? preview.postedEntryCount === 0 : false return (
{/* BFL distinction banner: EF values are declaration-only, never booked. */}

Skattemässiga justeringar: NE-bilaga
För enskild firma bokförs varken skatt, egenavgifter, fonder eller räntefördelning. Värdena nedan visar vad du fyller i på NE-bilagan när du deklarerar. Inga verifikat skapas.

{/* "No journal entries posted" banner: surfaced when the period has zero posted vouchers, so the user knows the surplus is 0 because nothing's booked yet, not because the calculators failed. */} {noPostedEntries && (

Inga verifikat bokförda i perioden.{' '} Värdena nedan baseras enbart på NE-bilagans räkenskapsschema (intäkter och kostnader hittills). Bokför löpande verifikat först för att få ett realistiskt överskott.

)} {/* Input form */} Indata

Bokfört överskott:{' '} {formatCurrency(preview?.bookedSurplus ?? bookedSurplus)}

update('kapitalunderlag', e.target.value)} placeholder="0" className="tabular-nums h-9" />
update('priorSchablon', e.target.value)} placeholder="0" className="tabular-nums h-9" />
update('priorActual', e.target.value)} placeholder="0" className="tabular-nums h-9" />
update('pfondDesired', e.target.value)} placeholder="0" className="tabular-nums h-9" />
update('expansionsfondBalance', e.target.value)} placeholder="0" className="tabular-nums h-9" />
update('expansionsfondChange', e.target.value)} placeholder="0" className="tabular-nums h-9" />
{/* Top-level input warnings (e.g. missing kapitalunderlag) */} {inputWarnings.map((w, i) => (

{w}

))} {/* Loading / error / items */} {error && ( {error} )} {loading && !preview && ( )} {items.map((item) => (
{item.label}

{item.description}

NE-ruta {item.ne_ruta}

{formatCurrency(item.amount)}

{item.warnings.length > 0 && ( {item.warnings.map((w, i) => (

{w}

))}
)}
))} {/* NE-bilaga download */} NE-bilaga räkenskapsschema (R1-R11)

Räkenskapsschema-delen genereras automatiskt från bokföringen för räkenskapsåret {fiscalYear}. Ladda ner SRU-filen och ladda upp den i Skatteverkets e-tjänst för Inkomstdeklaration 1.

) }