'use client' import { useEffect, useState } from 'react' import { Button } from '@/components/ui/button' import { Skeleton } from '@/components/ui/skeleton' import { Table, TableBody, TableHead, TableHeader, TableRow } from '@/components/ui/table' import { Download, AlertCircle, AlertTriangle } from 'lucide-react' import { DeclarationRutaRow, formatWholeKronor, } from '@/components/reports/DeclarationRutaRow' import type { NEDeclaration } from '@/lib/reports/ne-bilaga/types' import { parseApiError } from './api-error' export function NEDeclarationView({ periodId }: { periodId: string }) { // Fetch outcome tagged with the key it was requested under: switching // fiscal year discards stale responses instead of leaving last year's // declaration on screen (same pattern as the momsdeklaration view). const [result, setResult] = useState<{ key: string data?: NEDeclaration error?: string } | null>(null) const [retryKey, setRetryKey] = useState(0) const [downloading, setDownloading] = useState(false) const [downloadError, setDownloadError] = useState(null) const fetchKey = periodId ? `${periodId}:${retryKey}` : null useEffect(() => { if (!fetchKey || !periodId) return let cancelled = false fetch(`/api/reports/ne-bilaga?period_id=${periodId}`) .then(async (res) => { const json = await res.json().catch(() => null) if (cancelled) return if (!res.ok || json?.error) { setResult({ key: fetchKey, error: parseApiError(json?.error, 'Kunde inte hämta NE-bilaga'), }) } else { setResult({ key: fetchKey, data: json.data }) } }) .catch(() => { if (!cancelled) setResult({ key: fetchKey, error: 'Kunde inte hämta NE-bilaga' }) }) return () => { cancelled = true } }, [fetchKey, periodId]) const upToDate = result !== null && result.key === fetchKey const data = result?.data ?? null const error = upToDate ? (result.error ?? null) : null const loading = fetchKey !== null && !upToDate const downloadSRU = async () => { setDownloading(true) setDownloadError(null) try { const res = await fetch(`/api/reports/ne-bilaga?period_id=${periodId}&format=sru`) if (!res.ok) throw new Error('Download failed') const blob = await res.blob() const filename = res.headers.get('Content-Disposition')?.match(/filename="(.+)"/)?.[1] || 'NE_SRU.zip' const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = filename a.click() URL.revokeObjectURL(url) } catch { setDownloadError('Kunde inte ladda ner SRU-filer') } finally { setDownloading(false) } } // NE ruta labels const rutaLabels: Record = { R1: 'Försäljning med moms (25%)', R2: 'Momsfria intäkter', R3: 'Bil/bostadsförmån', R4: 'Ränteintäkter', R5: 'Varuinköp', R6: 'Övriga kostnader', R7: 'Lönekostnader', R8: 'Räntekostnader', R9: 'Avskrivningar fastighet', R10: 'Avskrivningar övriga tillgångar', R11: 'Årets resultat', } // Categorize rutor const revenueRutor = ['R1', 'R2', 'R3', 'R4'] as const const expenseRutor = ['R5', 'R6', 'R7', 'R8', 'R9', 'R10'] as const if (error) { return (
{error}
) } if (loading && !data) { return (
) } if (!data) return null return (
{/* Header: company, year, and the filing artifact */}

NE-bilaga (Enskild firma)

{data.companyInfo.companyName} · {data.fiscalYear.name} {data.companyInfo.orgNumber && ` · Org.nr: ${data.companyInfo.orgNumber}`}

{downloadError && (
{downloadError}
)}
{/* Warnings */} {data.warnings.length > 0 && (
)} {/* Revenue section */}

Intäkter

Post Belopp {revenueRutor.map((ruta) => ( ))}
Summa intäkter {formatWholeKronor( data.rutor.R1 + data.rutor.R2 + data.rutor.R3 + data.rutor.R4 )}
{/* Expenses section */}

Kostnader

Post Belopp {expenseRutor.map((ruta) => ( // Costs display negated: the signed value keeps its true sign // when an expense ruta carries a credit balance. ({ ...acc, amount: -acc.amount, }))} /> ))}
Summa kostnader {formatWholeKronor( -(data.rutor.R5 + data.rutor.R6 + data.rutor.R7 + data.rutor.R8 + data.rutor.R9 + data.rutor.R10) )}
{/* Result: the emphasized document foot (house skv-foot idiom). */}
R11 Årets resultat = 0 ? '' : 'text-destructive' }`} > {formatWholeKronor(data.rutor.R11)}
) }