'use client' import React, { useState, useEffect } from 'react' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Label } from '@/components/ui/label' import { Badge } from '@/components/ui/badge' import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs' import { Download, AlertCircle, ChevronDown, ChevronRight } from 'lucide-react' import { AccountNumber } from '@/components/ui/account-number' import { NEDeclarationView } from '@/components/reports/NEDeclarationView' import { BankReconciliationView } from '@/components/reports/BankReconciliationView' import { TrialBalanceChart } from '@/components/reports/TrialBalanceChart' import { VatCompositionChart } from '@/components/reports/VatCompositionChart' import { IncomeExpenseChart } from '@/components/reports/IncomeExpenseChart' import type { MonthlyDataPoint } from '@/components/reports/IncomeExpenseChart' import type { FiscalPeriod, TrialBalanceRow, IncomeStatementReport, BalanceSheetReport, VatDeclaration, VatPeriodType, } from '@/types' function formatAmount(amount: number): string { return amount.toLocaleString('sv-SE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) } export default function ReportsPage() { const [periods, setPeriods] = useState([]) const [selectedPeriod, setSelectedPeriod] = useState('') const [activeTab, setActiveTab] = useState('trial-balance') const [entityType, setEntityType] = useState(null) useEffect(() => { fetchPeriods() fetchEntityType() }, []) async function fetchPeriods() { const res = await fetch('/api/bookkeeping/fiscal-periods') const { data } = await res.json() setPeriods(data || []) if (data && data.length > 0) { setSelectedPeriod(data[0].id) } } async function fetchEntityType() { try { const res = await fetch('/api/settings') const { data } = await res.json() if (data?.entity_type) { setEntityType(data.entity_type) } } catch { // Ignore - entity type is optional for tab visibility } } const isEnskildFirma = entityType === 'enskild_firma' return (

Rapporter

Huvudbok, grundbok, kundreskontra, saldobalans, resultaträkning, balansräkning, momsdeklaration och mer

{selectedPeriod && ( )}
{selectedPeriod ? (
{/* Bokslut (Financial Statements) */}

Bokslut

Saldobalans Resultaträkning Balansräkning
{/* Skatt & moms (Tax & VAT) */}

Skatt & moms

Momsdeklaration {isEnskildFirma && ( NE-bilaga )}
{/* Huvudböcker (Ledgers) */}

Huvudböcker

Huvudbok Grundbok Kundreskontra Leverantörsreskontra
{/* Avstämning (Reconciliation) */}

Avstämning

Bankavstämning
{isEnskildFirma && ( )}
) : ( Inget räkenskapsår valt. Skapa ett räkenskapsår under Inställningar. )}
) } function TrialBalanceView({ periodId }: { periodId: string }) { const [data, setData] = useState<{ rows: TrialBalanceRow[] totalDebit: number totalCredit: number isBalanced: boolean } | null>(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) useEffect(() => { setLoading(true) setError(null) fetch(`/api/reports/trial-balance?period_id=${periodId}`) .then((res) => res.json()) .then((result) => { if (result.error) { setError(result.error) } else { setData(result.data) } setLoading(false) }) .catch(() => { setError('Kunde inte hämta saldobalans') setLoading(false) }) }, [periodId]) if (loading) { return ( Laddar saldobalans... ) } if (error) { return ( {error} ) } if (!data || data.rows.length === 0) { return ( Inga bokförda verifikationer i denna period. ) } return (
Saldobalans {data.isBalanced ? ( Balanserad ) : ( Ej balanserad )}
{data.rows.map((row) => ( ))}
Konto Namn Period debet Period kredit Saldo debet Saldo kredit
{row.account_name} {row.period_debit > 0 ? formatAmount(row.period_debit) : ''} {row.period_credit > 0 ? formatAmount(row.period_credit) : ''} {row.closing_debit > 0 ? formatAmount(row.closing_debit) : ''} {row.closing_credit > 0 ? formatAmount(row.closing_credit) : ''}
Summa {formatAmount(data.rows.reduce((s, r) => s + r.period_debit, 0))} {formatAmount(data.rows.reduce((s, r) => s + r.period_credit, 0))} {formatAmount(data.totalDebit)} {formatAmount(data.totalCredit)}
) } function IncomeStatementView({ periodId }: { periodId: string }) { const [data, setData] = useState(null) const [monthlyData, setMonthlyData] = useState([]) const [monthlyLoading, setMonthlyLoading] = useState(false) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) useEffect(() => { setLoading(true) setError(null) setMonthlyLoading(true) fetch(`/api/reports/income-statement?period_id=${periodId}`) .then((res) => res.json()) .then((result) => { if (result.error) { setError(result.error) } else { setData(result.data) } setLoading(false) }) .catch(() => { setError('Kunde inte hämta resultaträkning') setLoading(false) }) fetch(`/api/reports/monthly-breakdown?period_id=${periodId}`) .then((res) => res.json()) .then((result) => { if (result.data?.months) { setMonthlyData(result.data.months) } setMonthlyLoading(false) }) .catch(() => { setMonthlyLoading(false) }) }, [periodId]) if (loading) { return ( Laddar resultaträkning... ) } if (error) { return ( {error} ) } if (!data) { return ( Ingen data för denna period. ) } return (
{!monthlyLoading && monthlyData.length > 0 && ( )} {/* Revenue */} Rörelseintäkter
Summa rörelseintäkter {formatAmount(data.total_revenue)} kr
{/* Expenses */} Rörelsekostnader
Summa rörelsekostnader -{formatAmount(data.total_expenses)} kr
{/* Operating result */}
Rörelseresultat = 0 ? 'text-green-600' : 'text-red-600'}> {formatAmount(data.total_revenue - data.total_expenses)} kr
{/* Financial items */} {data.financial_sections.length > 0 && ( Finansiella poster
Summa finansiella poster {formatAmount(data.total_financial)} kr
)} {/* Net result */}
Årets resultat = 0 ? 'text-green-600' : 'text-red-600'}> {formatAmount(data.net_result)} kr
) } function BalanceSheetView({ periodId }: { periodId: string }) { const [data, setData] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) useEffect(() => { setLoading(true) setError(null) fetch(`/api/reports/balance-sheet?period_id=${periodId}`) .then((res) => res.json()) .then((result) => { if (result.error) { setError(result.error) } else { setData(result.data) } setLoading(false) }) .catch(() => { setError('Kunde inte hämta balansräkning') setLoading(false) }) }, [periodId]) if (loading) { return ( Laddar balansräkning... ) } if (error) { return ( {error} ) } if (!data) { return ( Ingen data för denna period. ) } const isBalanced = Math.abs(data.total_assets - data.total_equity_liabilities) < 0.01 return (
{/* Assets */} Tillgångar
Summa tillgångar {formatAmount(data.total_assets)} kr
{/* Equity and liabilities */} Eget kapital och skulder
Summa eget kapital och skulder {formatAmount(data.total_equity_liabilities)} kr
{/* Balance check */}
Balanscheck {isBalanced ? ( Balanserar ) : (
Balanserar ej

Differens: {formatAmount(Math.abs(data.total_assets - data.total_equity_liabilities))} kr

)}
) } function ReportSectionTable({ sections, negate, }: { sections: { title: string; rows: { account_number: string; account_name: string; amount: number }[]; subtotal: number }[] negate?: boolean }) { if (sections.length === 0) { return

Inga poster.

} return (
{sections.map((section) => (

{section.title}

{section.rows.map((row) => ( ))}
{row.account_name} {negate ? `-${formatAmount(row.amount)}` : formatAmount(row.amount)} kr
{section.title} {negate ? `-${formatAmount(section.subtotal)}` : formatAmount(section.subtotal)} kr
))}
) } function VatDeclarationView() { const currentYear = new Date().getFullYear() const currentMonth = new Date().getMonth() + 1 const currentQuarter = Math.ceil(currentMonth / 3) const [periodType, setPeriodType] = useState('quarterly') const [year, setYear] = useState(currentYear) const [period, setPeriod] = useState(currentQuarter) const [data, setData] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) // Generate year options (last 5 years) const yearOptions = Array.from({ length: 5 }, (_, i) => currentYear - i) // Generate period options based on type const getPeriodOptions = () => { switch (periodType) { case 'monthly': return [ { value: 1, label: 'Januari' }, { value: 2, label: 'Februari' }, { value: 3, label: 'Mars' }, { value: 4, label: 'April' }, { value: 5, label: 'Maj' }, { value: 6, label: 'Juni' }, { value: 7, label: 'Juli' }, { value: 8, label: 'Augusti' }, { value: 9, label: 'September' }, { value: 10, label: 'Oktober' }, { value: 11, label: 'November' }, { value: 12, label: 'December' }, ] case 'quarterly': return [ { value: 1, label: 'Kvartal 1 (jan-mar)' }, { value: 2, label: 'Kvartal 2 (apr-jun)' }, { value: 3, label: 'Kvartal 3 (jul-sep)' }, { value: 4, label: 'Kvartal 4 (okt-dec)' }, ] case 'yearly': return [{ value: 1, label: 'Helår' }] default: return [] } } // Reset period when type changes useEffect(() => { if (periodType === 'monthly') { setPeriod(currentMonth) } else if (periodType === 'quarterly') { setPeriod(currentQuarter) } else { setPeriod(1) } }, [periodType, currentMonth, currentQuarter]) const fetchDeclaration = async () => { setLoading(true) setError(null) try { const res = await fetch( `/api/reports/vat-declaration?periodType=${periodType}&year=${year}&period=${period}` ) const result = await res.json() if (result.error) { setError(result.error) } else { setData(result.data) } } catch { setError('Kunde inte hämta momsdeklaration') } finally { setLoading(false) } } return (
{/* Period selection */} Välj period
{error && ( {error} )} {data && ( <> {/* Summary */}
Momsdeklaration - {data.period.start} till {data.period.end} 0 ? 'bg-orange-100 text-orange-800' : data.rutor.ruta49 < 0 ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800' } > {data.rutor.ruta49 > 0 ? `Att betala: ${formatAmount(data.rutor.ruta49)} kr` : data.rutor.ruta49 < 0 ? `Att återfå: ${formatAmount(Math.abs(data.rutor.ruta49))} kr` : 'Ingen moms'}
Baserat på {data.invoiceCount} fakturor och {data.transactionCount} transaktioner
{/* Utgående moms */}

Utgående moms (försäljning)

Summa utgående {formatAmount(data.rutor.ruta05 + data.rutor.ruta06 + data.rutor.ruta07)} kr
{/* Ingående moms */}

Ingående moms (avdragsgill)

{data.breakdown.transactions.ruta48 > 0 && ( )} {data.breakdown.receipts.ruta48 > 0 && ( )}
48 Ingående moms att dra av {formatAmount(data.rutor.ruta48)} kr
- från transaktioner {formatAmount(data.breakdown.transactions.ruta48)} kr
- från kvitton {formatAmount(data.breakdown.receipts.ruta48)} kr
Summa ingående {formatAmount(data.rutor.ruta48)} kr
{/* Net result */}
49 {data.rutor.ruta49 >= 0 ? 'Moms att betala' : 'Moms att återfå'}
0 ? 'text-orange-600' : data.rutor.ruta49 < 0 ? 'text-green-600' : '' }`} > {formatAmount(Math.abs(data.rutor.ruta49))} kr
)} {!data && !loading && !error && ( Välj period och klicka "Hämta" för att se momsdeklaration. )}
) } function VatRutaRow({ ruta, label, amount, baseAmount, noVat, }: { ruta: string label: string amount: number baseAmount: number noVat?: boolean }) { // Don't show rows with zero values if (baseAmount === 0 && amount === 0) return null return ( <> {ruta} {label} {noVat ? '-' : `${formatAmount(amount)} kr`} Underlag (ruta {parseInt(ruta) + 5}) {formatAmount(baseAmount)} kr ) } interface SupplierLedgerData { ledger: { entries: { supplier_id: string supplier_name: string current: number days_1_30: number days_31_60: number days_61_90: number days_90_plus: number total_outstanding: number }[] total_outstanding: number total_current: number total_overdue: number unpaid_count: number } reconciliation: { supplier_ledger_total: number account_2440_balance: number difference: number is_reconciled: boolean } | null } function SupplierLedgerView({ periodId }: { periodId: string }) { const [data, setData] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const fetchData = async () => { setLoading(true) setError(null) try { const res = await fetch(`/api/reports/supplier-ledger?period_id=${periodId}`) const result = await res.json() if (result.error) { setError(result.error) } else { setData(result.data) } } catch { setError('Kunde inte hämta leverantörsreskontra') } finally { setLoading(false) } } useEffect(() => { if (periodId) fetchData() }, [periodId]) if (loading) { return ( Laddar leverantörsreskontra... ) } if (error) { return ( {error} ) } if (!data || !data.ledger) { return ( Ingen data tillgänglig. ) } const { ledger, reconciliation } = data return (
{/* Summary cards */}
Totalt utestående

{formatAmount(ledger.total_outstanding)} kr

{ledger.unpaid_count} fakturor

Ej förfallet

{formatAmount(ledger.total_current)} kr

Förfallet

{formatAmount(ledger.total_overdue)} kr

{/* Aging table */} {ledger.entries.length > 0 && ( Ålderfördelning per leverantör {ledger.entries.map((entry) => ( ))}
Leverantör Ej förfallet 1-30 dagar 31-60 dagar 61-90 dagar 90+ dagar Totalt
{entry.supplier_name} {entry.current > 0 ? formatAmount(entry.current) : ''} {entry.days_1_30 > 0 ? formatAmount(entry.days_1_30) : ''} {entry.days_31_60 > 0 ? formatAmount(entry.days_31_60) : ''} {entry.days_61_90 > 0 ? formatAmount(entry.days_61_90) : ''} {entry.days_90_plus > 0 ? formatAmount(entry.days_90_plus) : ''} {formatAmount(entry.total_outstanding)}
Summa {formatAmount(ledger.entries.reduce((s, e) => s + e.current, 0))} {formatAmount(ledger.entries.reduce((s, e) => s + e.days_1_30, 0))} {formatAmount(ledger.entries.reduce((s, e) => s + e.days_31_60, 0))} {formatAmount(ledger.entries.reduce((s, e) => s + e.days_61_90, 0))} {formatAmount(ledger.entries.reduce((s, e) => s + e.days_90_plus, 0))} {formatAmount(ledger.total_outstanding)}
)} {/* Reconciliation */} {reconciliation && ( Avstämning mot
Leverantörsreskontra (summa utestående) {formatAmount(reconciliation.supplier_ledger_total)} kr
saldo (huvudbok) {formatAmount(reconciliation.account_2440_balance)} kr
Differens {formatAmount(reconciliation.difference)} kr
{reconciliation.is_reconciled ? ( Avstämd ) : ( Ej avstämd - kontrollera bokföring )}
)}
) } // --- General Ledger (Huvudbok) --- interface GeneralLedgerData { accounts: { account_number: string account_name: string opening_balance: number lines: { date: string voucher_series: string voucher_number: number description: string source_type: string debit: number credit: number balance: number }[] closing_balance: number total_debit: number total_credit: number }[] period: { start: string; end: string } } function GeneralLedgerView({ periodId }: { periodId: string }) { const [data, setData] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [accountFrom, setAccountFrom] = useState('') const [accountTo, setAccountTo] = useState('') const fetchData = async () => { setLoading(true) setError(null) try { const params = new URLSearchParams({ period_id: periodId }) if (accountFrom) params.set('account_from', accountFrom) if (accountTo) params.set('account_to', accountTo) const res = await fetch(`/api/reports/general-ledger?${params}`) const result = await res.json() if (result.error) { setError(result.error) } else { setData(result.data) } } catch { setError('Kunde inte hämta huvudbok') } finally { setLoading(false) } } useEffect(() => { if (periodId) fetchData() }, [periodId]) if (loading) { return ( Laddar huvudbok... ) } if (error) { return ( {error} ) } if (!data || data.accounts.length === 0) { return ( Inga bokförda verifikationer i denna period. ) } return (
{/* Account range filter */}
setAccountFrom(e.target.value)} placeholder="t.ex. 1510" className="w-full mt-1 rounded-md border border-input bg-background px-3 py-2 text-sm" />
setAccountTo(e.target.value)} placeholder="t.ex. 1519" className="w-full mt-1 rounded-md border border-input bg-background px-3 py-2 text-sm" />
{data.period.start && (

Period: {data.period.start} — {data.period.end} | {data.accounts.length} konton

)} {data.accounts.map((account) => (
IB: {formatAmount(account.opening_balance)} kr
{account.lines.map((line, i) => ( ))}
Ver.nr Datum Beskrivning Debet Kredit Saldo
{line.voucher_series}{line.voucher_number} {line.date} {line.description} {line.debit > 0 ? formatAmount(line.debit) : ''} {line.credit > 0 ? formatAmount(line.credit) : ''} {formatAmount(line.balance)}
Summa / Utgående balans {formatAmount(account.total_debit)} {formatAmount(account.total_credit)} {formatAmount(account.closing_balance)}
))}
) } // --- Journal Register (Grundbok) --- interface JournalRegisterData { entries: { voucher_series: string voucher_number: number date: string description: string source_type: string status: string lines: { account_number: string account_name: string debit: number credit: number }[] total_debit: number total_credit: number }[] total_entries: number total_debit: number total_credit: number period: { start: string; end: string } } function JournalRegisterView({ periodId }: { periodId: string }) { const [data, setData] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [expandedEntries, setExpandedEntries] = useState>(new Set()) const fetchData = async () => { setLoading(true) setError(null) setExpandedEntries(new Set()) try { const res = await fetch(`/api/reports/journal-register?period_id=${periodId}`) const result = await res.json() if (result.error) { setError(result.error) } else { setData(result.data) } } catch { setError('Kunde inte hämta grundbok') } finally { setLoading(false) } } useEffect(() => { if (periodId) fetchData() }, [periodId]) const toggleEntry = (index: number) => { setExpandedEntries((prev) => { const next = new Set(prev) if (next.has(index)) { next.delete(index) } else { next.add(index) } return next }) } if (loading) { return ( Laddar grundbok... ) } if (error) { return ( {error} ) } if (!data || data.entries.length === 0) { return ( Inga bokförda verifikationer i denna period. ) } return (
{data.period.start && (

Period: {data.period.start} — {data.period.end} | {data.total_entries} verifikationer

)} Grundbok (registreringsordning) {data.entries.map((entry, index) => { const isExpanded = expandedEntries.has(index) const isReversed = entry.status === 'reversed' return ( toggleEntry(index)} > {isExpanded && entry.lines.map((line, lineIndex) => ( ))} ) })}
Ver.nr Datum Beskrivning Typ Debet Kredit
{isExpanded ? ( ) : ( )} {entry.voucher_series}{entry.voucher_number} {entry.date} {entry.description} {isReversed && ( Makulerad )} {entry.source_type} {formatAmount(entry.total_debit)} {formatAmount(entry.total_credit)}
{line.account_name} {line.debit > 0 ? formatAmount(line.debit) : ''} {line.credit > 0 ? formatAmount(line.credit) : ''}
Summa {formatAmount(data.total_debit)} {formatAmount(data.total_credit)}
) } // --- AR Ledger (Kundreskontra) --- interface ARLedgerData { ledger: { entries: { customer_id: string customer_name: string invoices: { invoice_id: string invoice_number: string invoice_date: string due_date: string total: number paid_amount: number outstanding: number days_overdue: number currency: string }[] current: number days_1_30: number days_31_60: number days_61_90: number days_90_plus: number total_outstanding: number }[] total_outstanding: number total_current: number total_overdue: number unpaid_count: number } reconciliation: { ar_ledger_total: number account_1510_balance: number difference: number is_reconciled: boolean } | null } function ARLedgerView({ periodId }: { periodId: string }) { const [data, setData] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [expandedCustomers, setExpandedCustomers] = useState>(new Set()) const fetchData = async () => { setLoading(true) setError(null) try { const res = await fetch(`/api/reports/ar-ledger?period_id=${periodId}`) const result = await res.json() if (result.error) { setError(result.error) } else { setData(result.data) } } catch { setError('Kunde inte hämta kundreskontra') } finally { setLoading(false) } } useEffect(() => { if (periodId) fetchData() }, [periodId]) const toggleCustomer = (customerId: string) => { setExpandedCustomers((prev) => { const next = new Set(prev) if (next.has(customerId)) { next.delete(customerId) } else { next.add(customerId) } return next }) } if (loading) { return ( Laddar kundreskontra... ) } if (error) { return ( {error} ) } if (!data || !data.ledger) { return ( Ingen data tillgänglig. ) } const { ledger, reconciliation } = data return (
{/* Summary cards */}
Totalt utestående

{formatAmount(ledger.total_outstanding)} kr

{ledger.unpaid_count} fakturor

Ej förfallet

{formatAmount(ledger.total_current)} kr

Förfallet

{formatAmount(ledger.total_overdue)} kr

{/* Aging table with expandable invoice details */} {ledger.entries.length > 0 && ( Ålderfördelning per kund {ledger.entries.map((entry) => { const isExpanded = expandedCustomers.has(entry.customer_id) return ( toggleCustomer(entry.customer_id)} > {isExpanded && entry.invoices.map((inv) => ( ))} ) })}
Kund Ej förfallet 1-30 dagar 31-60 dagar 61-90 dagar 90+ dagar Totalt
{isExpanded ? ( ) : ( )} {entry.customer_name} {entry.current > 0 ? formatAmount(entry.current) : ''} {entry.days_1_30 > 0 ? formatAmount(entry.days_1_30) : ''} {entry.days_31_60 > 0 ? formatAmount(entry.days_31_60) : ''} {entry.days_61_90 > 0 ? formatAmount(entry.days_61_90) : ''} {entry.days_90_plus > 0 ? formatAmount(entry.days_90_plus) : ''} {formatAmount(entry.total_outstanding)}
{inv.invoice_number} {inv.invoice_date} förfaller {inv.due_date} {inv.days_overdue > 0 ? `${inv.days_overdue} dagar förfallen` : 'Ej förfallen'} {inv.paid_amount > 0 ? `Betalt: ${formatAmount(inv.paid_amount)}` : ''} {formatAmount(inv.outstanding)} {inv.currency}
Summa {formatAmount(ledger.entries.reduce((s, e) => s + e.current, 0))} {formatAmount(ledger.entries.reduce((s, e) => s + e.days_1_30, 0))} {formatAmount(ledger.entries.reduce((s, e) => s + e.days_31_60, 0))} {formatAmount(ledger.entries.reduce((s, e) => s + e.days_61_90, 0))} {formatAmount(ledger.entries.reduce((s, e) => s + e.days_90_plus, 0))} {formatAmount(ledger.total_outstanding)}
)} {/* Reconciliation */} {reconciliation && ( Avstämning mot
Kundreskontra (summa utestående) {formatAmount(reconciliation.ar_ledger_total)} kr
saldo (huvudbok) {formatAmount(reconciliation.account_1510_balance)} kr
Differens {formatAmount(reconciliation.difference)} kr
{reconciliation.is_reconciled ? ( Avstämd ) : ( Ej avstämd - kontrollera bokföring )}
)}
) }