Files
accounted/app/(dashboard)/reports/page.tsx
T
Jakob WennbergandClaude Opus 4.6 091d043c85 feat: UI polish, lint fixes, onboarding redesign, help page expansion, and test improvements
Broad update across dashboard pages, components, extensions, and lib code. Includes ESLint config additions, onboarding flow redesign, settings page refactor, help page content expansion, dead code removal, and test mock fixes. Adds dev docs and public assets.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 23:05:49 +01:00

1841 lines
64 KiB
TypeScript

'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 { INK2DeclarationView } from '@/components/reports/INK2DeclarationView'
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<FiscalPeriod[]>([])
const [selectedPeriod, setSelectedPeriod] = useState('')
const [activeTab, setActiveTab] = useState('trial-balance')
const [entityType, setEntityType] = useState<string | null>(null)
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
}
}
useEffect(() => {
fetchPeriods()
fetchEntityType()
}, [])
const isEnskildFirma = entityType === 'enskild_firma'
const isAktiebolag = entityType === 'aktiebolag'
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">Rapporter</h1>
<p className="text-muted-foreground">
Huvudbok, grundbok, kundreskontra, saldobalans, resultaträkning, balansräkning, momsdeklaration och mer
</p>
</div>
</div>
<div className="flex items-end gap-4">
<div>
<Label>Räkenskapsår</Label>
<select
value={selectedPeriod}
onChange={(e) => setSelectedPeriod(e.target.value)}
className="w-full mt-1 rounded-md border border-input bg-background px-3 py-2 text-sm"
>
{periods.map((p) => (
<option key={p.id} value={p.id}>
{p.name} ({p.period_start} — {p.period_end})
</option>
))}
</select>
</div>
{selectedPeriod && (
<Button
variant="outline"
onClick={() => {
window.open(`/api/reports/sie-export?period_id=${selectedPeriod}`, '_blank')
}}
>
<Download className="h-4 w-4 mr-2" />
Ladda ner SIE-fil
</Button>
)}
</div>
{selectedPeriod ? (
<Tabs value={activeTab} onValueChange={setActiveTab}>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-2">
{/* Bokslut (Financial Statements) */}
<div className="space-y-1">
<p className="text-[10px] font-semibold text-muted-foreground/70 uppercase tracking-[0.08em] px-1">
Bokslut
</p>
<TabsList className="flex flex-col h-auto w-full gap-0.5 p-1">
<TabsTrigger value="trial-balance" className="w-full justify-start">
Saldobalans
</TabsTrigger>
<TabsTrigger value="income-statement" className="w-full justify-start">
Resultaträkning
</TabsTrigger>
<TabsTrigger value="balance-sheet" className="w-full justify-start">
Balansräkning
</TabsTrigger>
</TabsList>
</div>
{/* Skatt & moms (Tax & VAT) */}
<div className="space-y-1">
<p className="text-[10px] font-semibold text-muted-foreground/70 uppercase tracking-[0.08em] px-1">
Skatt & moms
</p>
<TabsList className="flex flex-col h-auto w-full gap-0.5 p-1">
<TabsTrigger value="vat-declaration" className="w-full justify-start">
Momsdeklaration
</TabsTrigger>
{isEnskildFirma && (
<TabsTrigger value="ne-declaration" className="w-full justify-start">
NE-bilaga
</TabsTrigger>
)}
{isAktiebolag && (
<TabsTrigger value="ink2-declaration" className="w-full justify-start">
INK2
</TabsTrigger>
)}
</TabsList>
</div>
{/* Huvudböcker (Ledgers) */}
<div className="space-y-1">
<p className="text-[10px] font-semibold text-muted-foreground/70 uppercase tracking-[0.08em] px-1">
Huvudböcker
</p>
<TabsList className="flex flex-col h-auto w-full gap-0.5 p-1">
<TabsTrigger value="huvudbok" className="w-full justify-start">
Huvudbok
</TabsTrigger>
<TabsTrigger value="grundbok" className="w-full justify-start">
Grundbok
</TabsTrigger>
<TabsTrigger value="kundreskontra" className="w-full justify-start">
Kundreskontra
</TabsTrigger>
<TabsTrigger value="supplier-ledger" className="w-full justify-start">
Leverantörsreskontra
</TabsTrigger>
</TabsList>
</div>
{/* Avstämning (Reconciliation) */}
<div className="space-y-1">
<p className="text-[10px] font-semibold text-muted-foreground/70 uppercase tracking-[0.08em] px-1">
Avstämning
</p>
<TabsList className="flex flex-col h-auto w-full gap-0.5 p-1">
<TabsTrigger value="bank-reconciliation" className="w-full justify-start">
Bankavstämning
</TabsTrigger>
</TabsList>
</div>
</div>
<TabsContent value="trial-balance">
<TrialBalanceView periodId={selectedPeriod} />
</TabsContent>
<TabsContent value="income-statement">
<IncomeStatementView periodId={selectedPeriod} />
</TabsContent>
<TabsContent value="balance-sheet">
<BalanceSheetView periodId={selectedPeriod} />
</TabsContent>
<TabsContent value="vat-declaration">
<VatDeclarationView />
</TabsContent>
{isEnskildFirma && (
<TabsContent value="ne-declaration">
<NEDeclarationView periodId={selectedPeriod} />
</TabsContent>
)}
{isAktiebolag && (
<TabsContent value="ink2-declaration">
<INK2DeclarationView periodId={selectedPeriod} />
</TabsContent>
)}
<TabsContent value="huvudbok">
<GeneralLedgerView periodId={selectedPeriod} />
</TabsContent>
<TabsContent value="grundbok">
<JournalRegisterView periodId={selectedPeriod} />
</TabsContent>
<TabsContent value="kundreskontra">
<ARLedgerView periodId={selectedPeriod} />
</TabsContent>
<TabsContent value="supplier-ledger">
<SupplierLedgerView periodId={selectedPeriod} />
</TabsContent>
<TabsContent value="bank-reconciliation">
<BankReconciliationView />
</TabsContent>
</Tabs>
) : (
<Card>
<CardContent className="p-8 text-center text-muted-foreground">
Inget räkenskapsår valt. Skapa ett räkenskapsår under Inställningar.
</CardContent>
</Card>
)}
</div>
)
}
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<string | null>(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 (
<Card>
<CardContent className="p-8 text-center text-muted-foreground">
Laddar saldobalans...
</CardContent>
</Card>
)
}
if (error) {
return (
<Card>
<CardContent className="p-8 text-center text-destructive">
<AlertCircle className="h-6 w-6 mx-auto mb-2" />
{error}
</CardContent>
</Card>
)
}
if (!data || data.rows.length === 0) {
return (
<Card>
<CardContent className="p-8 text-center text-muted-foreground">
Inga bokförda verifikationer i denna period.
</CardContent>
</Card>
)
}
return (
<div className="space-y-4">
<TrialBalanceChart rows={data.rows} />
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>Saldobalans</CardTitle>
{data.isBalanced ? (
<Badge className="bg-success/10 text-success">Balanserad</Badge>
) : (
<Badge variant="destructive">Ej balanserad</Badge>
)}
</div>
</CardHeader>
<CardContent>
<table className="w-full text-sm">
<thead>
<tr className="border-b text-left text-muted-foreground">
<th className="py-2 w-20">Konto</th>
<th className="py-2">Namn</th>
<th className="py-2 w-28 text-right">Period debet</th>
<th className="py-2 w-28 text-right">Period kredit</th>
<th className="py-2 w-28 text-right">Saldo debet</th>
<th className="py-2 w-28 text-right">Saldo kredit</th>
</tr>
</thead>
<tbody>
{data.rows.map((row) => (
<tr key={row.account_number} className="border-b last:border-0">
<td className="py-2"><AccountNumber number={row.account_number} name={row.account_name} /></td>
<td className="py-2">{row.account_name}</td>
<td className="py-2 text-right">
{row.period_debit > 0 ? formatAmount(row.period_debit) : ''}
</td>
<td className="py-2 text-right">
{row.period_credit > 0 ? formatAmount(row.period_credit) : ''}
</td>
<td className="py-2 text-right">
{row.closing_debit > 0 ? formatAmount(row.closing_debit) : ''}
</td>
<td className="py-2 text-right">
{row.closing_credit > 0 ? formatAmount(row.closing_credit) : ''}
</td>
</tr>
))}
</tbody>
<tfoot>
<tr className="font-semibold border-t-2">
<td colSpan={2} className="py-2">Summa</td>
<td className="py-2 text-right">
{formatAmount(data.rows.reduce((s, r) => s + r.period_debit, 0))}
</td>
<td className="py-2 text-right">
{formatAmount(data.rows.reduce((s, r) => s + r.period_credit, 0))}
</td>
<td className={`py-2 text-right ${data.isBalanced ? 'text-success' : 'text-destructive'}`}>
{formatAmount(data.totalDebit)}
</td>
<td className={`py-2 text-right ${data.isBalanced ? 'text-success' : 'text-destructive'}`}>
{formatAmount(data.totalCredit)}
</td>
</tr>
</tfoot>
</table>
</CardContent>
</Card>
</div>
)
}
function IncomeStatementView({ periodId }: { periodId: string }) {
const [data, setData] = useState<IncomeStatementReport | null>(null)
const [monthlyData, setMonthlyData] = useState<MonthlyDataPoint[]>([])
const [monthlyLoading, setMonthlyLoading] = useState(false)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(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 (
<Card>
<CardContent className="p-8 text-center text-muted-foreground">
Laddar resultaträkning...
</CardContent>
</Card>
)
}
if (error) {
return (
<Card>
<CardContent className="p-8 text-center text-destructive">
<AlertCircle className="h-6 w-6 mx-auto mb-2" />
{error}
</CardContent>
</Card>
)
}
if (!data) {
return (
<Card>
<CardContent className="p-8 text-center text-muted-foreground">
Ingen data för denna period.
</CardContent>
</Card>
)
}
return (
<div className="space-y-4">
{!monthlyLoading && monthlyData.length > 0 && (
<IncomeExpenseChart months={monthlyData} />
)}
{/* Revenue */}
<Card>
<CardHeader>
<CardTitle className="text-lg">Rörelseintäkter</CardTitle>
</CardHeader>
<CardContent>
<ReportSectionTable sections={data.revenue_sections} />
<div className="flex justify-between font-semibold pt-2 border-t mt-2">
<span>Summa rörelseintäkter</span>
<span>{formatAmount(data.total_revenue)} kr</span>
</div>
</CardContent>
</Card>
{/* Expenses */}
<Card>
<CardHeader>
<CardTitle className="text-lg">Rörelsekostnader</CardTitle>
</CardHeader>
<CardContent>
<ReportSectionTable sections={data.expense_sections} negate />
<div className="flex justify-between font-semibold pt-2 border-t mt-2">
<span>Summa rörelsekostnader</span>
<span>-{formatAmount(data.total_expenses)} kr</span>
</div>
</CardContent>
</Card>
{/* Operating result */}
<Card>
<CardContent className="py-4">
<div className="flex justify-between font-bold text-lg">
<span>Rörelseresultat</span>
<span className={data.total_revenue - data.total_expenses >= 0 ? 'text-success' : 'text-destructive'}>
{formatAmount(data.total_revenue - data.total_expenses)} kr
</span>
</div>
</CardContent>
</Card>
{/* Financial items */}
{data.financial_sections.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="text-lg">Finansiella poster</CardTitle>
</CardHeader>
<CardContent>
<ReportSectionTable sections={data.financial_sections} />
<div className="flex justify-between font-semibold pt-2 border-t mt-2">
<span>Summa finansiella poster</span>
<span>{formatAmount(data.total_financial)} kr</span>
</div>
</CardContent>
</Card>
)}
{/* Net result */}
<Card className="border-2">
<CardContent className="py-4">
<div className="flex justify-between font-bold text-xl">
<span>Årets resultat</span>
<span className={data.net_result >= 0 ? 'text-success' : 'text-destructive'}>
{formatAmount(data.net_result)} kr
</span>
</div>
</CardContent>
</Card>
</div>
)
}
function BalanceSheetView({ periodId }: { periodId: string }) {
const [data, setData] = useState<BalanceSheetReport | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(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 (
<Card>
<CardContent className="p-8 text-center text-muted-foreground">
Laddar balansräkning...
</CardContent>
</Card>
)
}
if (error) {
return (
<Card>
<CardContent className="p-8 text-center text-destructive">
<AlertCircle className="h-6 w-6 mx-auto mb-2" />
{error}
</CardContent>
</Card>
)
}
if (!data) {
return (
<Card>
<CardContent className="p-8 text-center text-muted-foreground">
Ingen data för denna period.
</CardContent>
</Card>
)
}
const isBalanced = Math.abs(data.total_assets - data.total_equity_liabilities) < 0.01
return (
<div className="space-y-4">
{/* Assets */}
<Card>
<CardHeader>
<CardTitle className="text-lg">Tillgångar</CardTitle>
</CardHeader>
<CardContent>
<ReportSectionTable sections={data.asset_sections} />
<div className="flex justify-between font-semibold pt-2 border-t mt-2">
<span>Summa tillgångar</span>
<span>{formatAmount(data.total_assets)} kr</span>
</div>
</CardContent>
</Card>
{/* Equity and liabilities */}
<Card>
<CardHeader>
<CardTitle className="text-lg">Eget kapital och skulder</CardTitle>
</CardHeader>
<CardContent>
<ReportSectionTable sections={data.equity_liability_sections} />
<div className="flex justify-between font-semibold pt-2 border-t mt-2">
<span>Summa eget kapital och skulder</span>
<span>{formatAmount(data.total_equity_liabilities)} kr</span>
</div>
</CardContent>
</Card>
{/* Balance check */}
<Card className="border-2">
<CardContent className="py-4">
<div className="flex justify-between items-center">
<span className="font-bold text-lg">Balanscheck</span>
{isBalanced ? (
<Badge className="bg-success/10 text-success text-base px-3 py-1">
Balanserar
</Badge>
) : (
<div className="text-right">
<Badge variant="destructive" className="text-base px-3 py-1">
Balanserar ej
</Badge>
<p className="text-sm text-destructive mt-1">
Differens: {formatAmount(Math.abs(data.total_assets - data.total_equity_liabilities))} kr
</p>
</div>
)}
</div>
</CardContent>
</Card>
</div>
)
}
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 <p className="text-sm text-muted-foreground">Inga poster.</p>
}
return (
<div className="space-y-3">
{sections.map((section) => (
<div key={section.title}>
<h4 className="text-sm font-semibold text-muted-foreground mb-1">{section.title}</h4>
<table className="w-full text-sm">
<tbody>
{section.rows.map((row) => (
<tr key={row.account_number} className="border-b last:border-0">
<td className="py-1 w-16"><AccountNumber number={row.account_number} name={row.account_name} /></td>
<td className="py-1">{row.account_name}</td>
<td className="py-1 text-right w-28">
{negate ? `-${formatAmount(row.amount)}` : formatAmount(row.amount)} kr
</td>
</tr>
))}
</tbody>
</table>
<div className="flex justify-between text-sm font-medium border-t pt-1 mt-1">
<span>{section.title}</span>
<span>
{negate ? `-${formatAmount(section.subtotal)}` : formatAmount(section.subtotal)} kr
</span>
</div>
</div>
))}
</div>
)
}
function VatDeclarationView() {
const currentYear = new Date().getFullYear()
const currentMonth = new Date().getMonth() + 1
const currentQuarter = Math.ceil(currentMonth / 3)
const [periodType, setPeriodType] = useState<VatPeriodType>('quarterly')
const [year, setYear] = useState(currentYear)
const [period, setPeriod] = useState(currentQuarter)
const [data, setData] = useState<VatDeclaration | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(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 (
<div className="space-y-4">
{/* Period selection */}
<Card>
<CardHeader>
<CardTitle className="text-lg">Välj period</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap items-end gap-4">
<div>
<Label>Periodicitet</Label>
<select
value={periodType}
onChange={(e) => setPeriodType(e.target.value as VatPeriodType)}
className="w-full mt-1 rounded-md border border-input bg-background px-3 py-2 text-sm"
>
<option value="monthly">Månadsvis</option>
<option value="quarterly">Kvartalsvis</option>
<option value="yearly">Årsvis</option>
</select>
</div>
<div>
<Label>År</Label>
<select
value={year}
onChange={(e) => setYear(parseInt(e.target.value))}
className="w-full mt-1 rounded-md border border-input bg-background px-3 py-2 text-sm"
>
{yearOptions.map((y) => (
<option key={y} value={y}>
{y}
</option>
))}
</select>
</div>
<div>
<Label>Period</Label>
<select
value={period}
onChange={(e) => setPeriod(parseInt(e.target.value))}
className="w-full mt-1 rounded-md border border-input bg-background px-3 py-2 text-sm"
>
{getPeriodOptions().map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
<Button onClick={fetchDeclaration} disabled={loading}>
{loading ? 'Laddar...' : 'Hämta'}
</Button>
</div>
</CardContent>
</Card>
{error && (
<Card>
<CardContent className="p-8 text-center text-destructive">
<AlertCircle className="h-6 w-6 mx-auto mb-2" />
{error}
</CardContent>
</Card>
)}
{data && (
<>
<VatCompositionChart rutor={data.rutor} />
{/* Summary */}
<Card className="border-2">
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>Momsdeklaration - {data.period.start} till {data.period.end}</CardTitle>
<Badge
className={
data.rutor.ruta49 > 0
? 'bg-orange-100 text-orange-800'
: data.rutor.ruta49 < 0
? 'bg-success/10 text-success'
: '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'}
</Badge>
</div>
</CardHeader>
<CardContent>
<div className="text-sm text-muted-foreground mb-4">
Baserat på {data.invoiceCount} fakturor och {data.transactionCount} transaktioner
</div>
<div className="grid md:grid-cols-2 gap-6">
{/* Utgående moms */}
<div>
<h4 className="font-semibold mb-3">Utgående moms (försäljning)</h4>
<table className="w-full text-sm">
<tbody>
{data.rutor.ruta05 > 0 && (
<tr className="border-b">
<td className="py-2">
<span className="font-mono text-xs bg-muted px-1 rounded mr-2">05</span>
Momspliktig försäljning
</td>
<td className="py-2 text-right">{formatAmount(data.rutor.ruta05)} kr</td>
</tr>
)}
<VatRutaRow
ruta="10"
label="Utgående moms 25%"
amount={data.rutor.ruta10}
baseAmount={data.breakdown.invoices.base25}
/>
<VatRutaRow
ruta="11"
label="Utgående moms 12%"
amount={data.rutor.ruta11}
baseAmount={data.breakdown.invoices.base12}
/>
<VatRutaRow
ruta="12"
label="Utgående moms 6%"
amount={data.rutor.ruta12}
baseAmount={data.breakdown.invoices.base6}
/>
<VatRutaRow
ruta="39"
label="Tjänster EU (omvänd skattskyldighet)"
amount={0}
baseAmount={data.rutor.ruta39}
noVat
/>
<VatRutaRow
ruta="40"
label="Export utanför EU"
amount={0}
baseAmount={data.rutor.ruta40}
noVat
/>
</tbody>
<tfoot>
<tr className="border-t-2 font-semibold">
<td className="py-2">Summa utgående</td>
<td className="py-2 text-right">
{formatAmount(data.rutor.ruta10 + data.rutor.ruta11 + data.rutor.ruta12)} kr
</td>
</tr>
</tfoot>
</table>
</div>
{/* Ingående moms */}
<div>
<h4 className="font-semibold mb-3">Ingående moms (avdragsgill)</h4>
<table className="w-full text-sm">
<tbody>
<tr className="border-b">
<td className="py-2">
<span className="font-mono text-xs bg-muted px-1 rounded mr-2">48</span>
Ingående moms att dra av
</td>
<td className="py-2 text-right">{formatAmount(data.rutor.ruta48)} kr</td>
</tr>
{data.breakdown.transactions.ruta48 > 0 && (
<tr className="text-muted-foreground">
<td className="py-1 pl-6 text-xs">- från transaktioner</td>
<td className="py-1 text-right text-xs">
{formatAmount(data.breakdown.transactions.ruta48)} kr
</td>
</tr>
)}
{data.breakdown.receipts.ruta48 > 0 && (
<tr className="text-muted-foreground">
<td className="py-1 pl-6 text-xs">- från kvitton</td>
<td className="py-1 text-right text-xs">
{formatAmount(data.breakdown.receipts.ruta48)} kr
</td>
</tr>
)}
</tbody>
<tfoot>
<tr className="border-t-2 font-semibold">
<td className="py-2">Summa ingående</td>
<td className="py-2 text-right">{formatAmount(data.rutor.ruta48)} kr</td>
</tr>
</tfoot>
</table>
</div>
</div>
{/* Net result */}
<div className="mt-6 pt-4 border-t-2">
<div className="flex justify-between items-center">
<div>
<span className="font-mono text-xs bg-muted px-1 rounded mr-2">49</span>
<span className="font-bold text-lg">
{data.rutor.ruta49 >= 0 ? 'Moms att betala' : 'Moms att återfå'}
</span>
</div>
<span
className={`text-xl font-bold ${
data.rutor.ruta49 > 0
? 'text-orange-600'
: data.rutor.ruta49 < 0
? 'text-success'
: ''
}`}
>
{formatAmount(Math.abs(data.rutor.ruta49))} kr
</span>
</div>
</div>
</CardContent>
</Card>
</>
)}
{!data && !loading && !error && (
<Card>
<CardContent className="p-8 text-center text-muted-foreground">
Välj period och klicka &quot;Hämta&quot; för att se momsdeklaration.
</CardContent>
</Card>
)}
</div>
)
}
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 (
<>
<tr className="border-b">
<td className="py-2">
<span className="font-mono text-xs bg-muted px-1 rounded mr-2">{ruta}</span>
{label}
</td>
<td className="py-2 text-right">{noVat ? '-' : `${formatAmount(amount)} kr`}</td>
</tr>
<tr className="text-muted-foreground">
<td className="py-1 pl-6 text-xs">Underlag</td>
<td className="py-1 text-right text-xs">{formatAmount(baseAmount)} kr</td>
</tr>
</>
)
}
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<SupplierLedgerData | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(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 (
<Card>
<CardContent className="p-8 text-center text-muted-foreground">
Laddar leverantörsreskontra...
</CardContent>
</Card>
)
}
if (error) {
return (
<Card>
<CardContent className="p-8 text-center text-destructive">
<AlertCircle className="h-6 w-6 mx-auto mb-2" />
{error}
</CardContent>
</Card>
)
}
if (!data || !data.ledger) {
return (
<Card>
<CardContent className="p-8 text-center text-muted-foreground">
Ingen data tillgänglig.
</CardContent>
</Card>
)
}
const { ledger, reconciliation } = data
return (
<div className="space-y-4">
{/* Summary cards */}
<div className="grid md:grid-cols-3 gap-4">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm text-muted-foreground">Totalt utestående</CardTitle>
</CardHeader>
<CardContent>
<p className="text-2xl font-bold">{formatAmount(ledger.total_outstanding)} kr</p>
<p className="text-xs text-muted-foreground">{ledger.unpaid_count} fakturor</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm text-muted-foreground">Ej förfallet</CardTitle>
</CardHeader>
<CardContent>
<p className="text-2xl font-bold text-success">{formatAmount(ledger.total_current)} kr</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm text-muted-foreground">Förfallet</CardTitle>
</CardHeader>
<CardContent>
<p className="text-2xl font-bold text-destructive">{formatAmount(ledger.total_overdue)} kr</p>
</CardContent>
</Card>
</div>
{/* Aging table */}
{ledger.entries.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Ålderfördelning per leverantör</CardTitle>
</CardHeader>
<CardContent>
<table className="w-full text-sm">
<thead>
<tr className="border-b text-left text-muted-foreground">
<th className="py-2">Leverantör</th>
<th className="py-2 text-right">Ej förfallet</th>
<th className="py-2 text-right">1-30 dagar</th>
<th className="py-2 text-right">31-60 dagar</th>
<th className="py-2 text-right">61-90 dagar</th>
<th className="py-2 text-right">90+ dagar</th>
<th className="py-2 text-right font-semibold">Totalt</th>
</tr>
</thead>
<tbody>
{ledger.entries.map((entry) => (
<tr key={entry.supplier_id} className="border-b last:border-0">
<td className="py-2">{entry.supplier_name}</td>
<td className="py-2 text-right">{entry.current > 0 ? formatAmount(entry.current) : ''}</td>
<td className="py-2 text-right">{entry.days_1_30 > 0 ? formatAmount(entry.days_1_30) : ''}</td>
<td className="py-2 text-right">{entry.days_31_60 > 0 ? formatAmount(entry.days_31_60) : ''}</td>
<td className="py-2 text-right">{entry.days_61_90 > 0 ? formatAmount(entry.days_61_90) : ''}</td>
<td className="py-2 text-right text-destructive">{entry.days_90_plus > 0 ? formatAmount(entry.days_90_plus) : ''}</td>
<td className="py-2 text-right font-semibold">{formatAmount(entry.total_outstanding)}</td>
</tr>
))}
</tbody>
<tfoot>
<tr className="font-semibold border-t-2">
<td className="py-2">Summa</td>
<td className="py-2 text-right">{formatAmount(ledger.entries.reduce((s, e) => s + e.current, 0))}</td>
<td className="py-2 text-right">{formatAmount(ledger.entries.reduce((s, e) => s + e.days_1_30, 0))}</td>
<td className="py-2 text-right">{formatAmount(ledger.entries.reduce((s, e) => s + e.days_31_60, 0))}</td>
<td className="py-2 text-right">{formatAmount(ledger.entries.reduce((s, e) => s + e.days_61_90, 0))}</td>
<td className="py-2 text-right text-destructive">{formatAmount(ledger.entries.reduce((s, e) => s + e.days_90_plus, 0))}</td>
<td className="py-2 text-right">{formatAmount(ledger.total_outstanding)}</td>
</tr>
</tfoot>
</table>
</CardContent>
</Card>
)}
{/* Reconciliation */}
{reconciliation && (
<Card className="border-2">
<CardHeader>
<CardTitle>Avstämning mot <AccountNumber number="2440" /></CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span>Leverantörsreskontra (summa utestående)</span>
<span className="font-mono">{formatAmount(reconciliation.supplier_ledger_total)} kr</span>
</div>
<div className="flex justify-between">
<span><AccountNumber number="2440" /> saldo (huvudbok)</span>
<span className="font-mono">{formatAmount(reconciliation.account_2440_balance)} kr</span>
</div>
<div className="flex justify-between pt-2 border-t font-semibold">
<span>Differens</span>
<span className={reconciliation.is_reconciled ? 'text-success' : 'text-destructive'}>
{formatAmount(reconciliation.difference)} kr
</span>
</div>
<div className="pt-2">
{reconciliation.is_reconciled ? (
<Badge className="bg-success/10 text-success">Avstämd</Badge>
) : (
<Badge variant="destructive">Ej avstämd - kontrollera bokföring</Badge>
)}
</div>
</div>
</CardContent>
</Card>
)}
</div>
)
}
// --- 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<GeneralLedgerData | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(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 (
<Card>
<CardContent className="p-8 text-center text-muted-foreground">
Laddar huvudbok...
</CardContent>
</Card>
)
}
if (error) {
return (
<Card>
<CardContent className="p-8 text-center text-destructive">
<AlertCircle className="h-6 w-6 mx-auto mb-2" />
{error}
</CardContent>
</Card>
)
}
if (!data || data.accounts.length === 0) {
return (
<Card>
<CardContent className="p-8 text-center text-muted-foreground">
Inga bokförda verifikationer i denna period.
</CardContent>
</Card>
)
}
return (
<div className="space-y-4">
{/* Account range filter */}
<Card>
<CardContent className="pt-6">
<div className="flex flex-wrap items-end gap-4">
<div>
<Label>Konto från</Label>
<input
type="text"
value={accountFrom}
onChange={(e) => 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"
/>
</div>
<div>
<Label>Konto till</Label>
<input
type="text"
value={accountTo}
onChange={(e) => 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"
/>
</div>
<Button onClick={fetchData} variant="outline">
Filtrera
</Button>
</div>
</CardContent>
</Card>
{data.period.start && (
<p className="text-sm text-muted-foreground">
Period: {data.period.start} — {data.period.end} | {data.accounts.length} konton
</p>
)}
{data.accounts.map((account) => (
<Card key={account.account_number}>
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-base">
<AccountNumber number={account.account_number} name={account.account_name} showName />
</CardTitle>
<span className="text-sm text-muted-foreground">
IB: {formatAmount(account.opening_balance)} kr
</span>
</div>
</CardHeader>
<CardContent>
<table className="w-full text-sm">
<thead>
<tr className="border-b text-left text-muted-foreground">
<th className="py-2 w-16">Ver.nr</th>
<th className="py-2 w-24">Datum</th>
<th className="py-2">Beskrivning</th>
<th className="py-2 w-24 text-right">Debet</th>
<th className="py-2 w-24 text-right">Kredit</th>
<th className="py-2 w-28 text-right">Saldo</th>
</tr>
</thead>
<tbody>
{account.lines.map((line, i) => (
<tr key={i} className="border-b last:border-0">
<td className="py-1.5 font-mono text-xs">
{line.voucher_series}{line.voucher_number}
</td>
<td className="py-1.5">{line.date}</td>
<td className="py-1.5 truncate max-w-[200px]">{line.description}</td>
<td className="py-1.5 text-right">
{line.debit > 0 ? formatAmount(line.debit) : ''}
</td>
<td className="py-1.5 text-right">
{line.credit > 0 ? formatAmount(line.credit) : ''}
</td>
<td className="py-1.5 text-right font-mono">{formatAmount(line.balance)}</td>
</tr>
))}
</tbody>
<tfoot>
<tr className="font-semibold border-t-2">
<td colSpan={3} className="py-2">Summa / Utgående balans</td>
<td className="py-2 text-right">{formatAmount(account.total_debit)}</td>
<td className="py-2 text-right">{formatAmount(account.total_credit)}</td>
<td className="py-2 text-right font-mono">{formatAmount(account.closing_balance)}</td>
</tr>
</tfoot>
</table>
</CardContent>
</Card>
))}
</div>
)
}
// --- 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<JournalRegisterData | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [expandedEntries, setExpandedEntries] = useState<Set<number>>(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 (
<Card>
<CardContent className="p-8 text-center text-muted-foreground">
Laddar grundbok...
</CardContent>
</Card>
)
}
if (error) {
return (
<Card>
<CardContent className="p-8 text-center text-destructive">
<AlertCircle className="h-6 w-6 mx-auto mb-2" />
{error}
</CardContent>
</Card>
)
}
if (!data || data.entries.length === 0) {
return (
<Card>
<CardContent className="p-8 text-center text-muted-foreground">
Inga bokförda verifikationer i denna period.
</CardContent>
</Card>
)
}
return (
<div className="space-y-4">
{data.period.start && (
<p className="text-sm text-muted-foreground">
Period: {data.period.start} — {data.period.end} | {data.total_entries} verifikationer
</p>
)}
<Card>
<CardHeader>
<CardTitle>Grundbok (registreringsordning)</CardTitle>
</CardHeader>
<CardContent>
<table className="w-full text-sm">
<thead>
<tr className="border-b text-left text-muted-foreground">
<th className="py-2 w-8"></th>
<th className="py-2 w-16">Ver.nr</th>
<th className="py-2 w-24">Datum</th>
<th className="py-2">Beskrivning</th>
<th className="py-2 w-24">Typ</th>
<th className="py-2 w-24 text-right">Debet</th>
<th className="py-2 w-24 text-right">Kredit</th>
</tr>
</thead>
<tbody>
{data.entries.map((entry, index) => {
const isExpanded = expandedEntries.has(index)
const isReversed = entry.status === 'reversed'
return (
<React.Fragment key={index}>
<tr
className={`border-b cursor-pointer hover:bg-muted/50 ${isReversed ? 'line-through opacity-60' : ''}`}
onClick={() => toggleEntry(index)}
>
<td className="py-2">
{isExpanded ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
</td>
<td className="py-2 font-mono text-xs">
{entry.voucher_series}{entry.voucher_number}
</td>
<td className="py-2">{entry.date}</td>
<td className="py-2">
{entry.description}
{isReversed && (
<Badge variant="outline" className="ml-2 text-xs">Makulerad</Badge>
)}
</td>
<td className="py-2 text-xs text-muted-foreground">{entry.source_type}</td>
<td className="py-2 text-right">{formatAmount(entry.total_debit)}</td>
<td className="py-2 text-right">{formatAmount(entry.total_credit)}</td>
</tr>
{isExpanded && entry.lines.map((line, lineIndex) => (
<tr key={`${index}-${lineIndex}`} className="bg-muted/30 border-b last:border-0">
<td></td>
<td></td>
<td className="py-1"><AccountNumber number={line.account_number} name={line.account_name} size="sm" /></td>
<td className="py-1 text-muted-foreground">{line.account_name}</td>
<td></td>
<td className="py-1 text-right">
{line.debit > 0 ? formatAmount(line.debit) : ''}
</td>
<td className="py-1 text-right">
{line.credit > 0 ? formatAmount(line.credit) : ''}
</td>
</tr>
))}
</React.Fragment>
)
})}
</tbody>
<tfoot>
<tr className="font-semibold border-t-2">
<td colSpan={5} className="py-2">Summa</td>
<td className="py-2 text-right">{formatAmount(data.total_debit)}</td>
<td className="py-2 text-right">{formatAmount(data.total_credit)}</td>
</tr>
</tfoot>
</table>
</CardContent>
</Card>
</div>
)
}
// --- 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<ARLedgerData | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [expandedCustomers, setExpandedCustomers] = useState<Set<string>>(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 (
<Card>
<CardContent className="p-8 text-center text-muted-foreground">
Laddar kundreskontra...
</CardContent>
</Card>
)
}
if (error) {
return (
<Card>
<CardContent className="p-8 text-center text-destructive">
<AlertCircle className="h-6 w-6 mx-auto mb-2" />
{error}
</CardContent>
</Card>
)
}
if (!data || !data.ledger) {
return (
<Card>
<CardContent className="p-8 text-center text-muted-foreground">
Ingen data tillgänglig.
</CardContent>
</Card>
)
}
const { ledger, reconciliation } = data
return (
<div className="space-y-4">
{/* Summary cards */}
<div className="grid md:grid-cols-3 gap-4">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm text-muted-foreground">Totalt utestående</CardTitle>
</CardHeader>
<CardContent>
<p className="text-2xl font-bold">{formatAmount(ledger.total_outstanding)} kr</p>
<p className="text-xs text-muted-foreground">{ledger.unpaid_count} fakturor</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm text-muted-foreground">Ej förfallet</CardTitle>
</CardHeader>
<CardContent>
<p className="text-2xl font-bold text-success">{formatAmount(ledger.total_current)} kr</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm text-muted-foreground">Förfallet</CardTitle>
</CardHeader>
<CardContent>
<p className="text-2xl font-bold text-destructive">{formatAmount(ledger.total_overdue)} kr</p>
</CardContent>
</Card>
</div>
{/* Aging table with expandable invoice details */}
{ledger.entries.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Ålderfördelning per kund</CardTitle>
</CardHeader>
<CardContent>
<table className="w-full text-sm">
<thead>
<tr className="border-b text-left text-muted-foreground">
<th className="py-2 w-8"></th>
<th className="py-2">Kund</th>
<th className="py-2 text-right">Ej förfallet</th>
<th className="py-2 text-right">1-30 dagar</th>
<th className="py-2 text-right">31-60 dagar</th>
<th className="py-2 text-right">61-90 dagar</th>
<th className="py-2 text-right">90+ dagar</th>
<th className="py-2 text-right font-semibold">Totalt</th>
</tr>
</thead>
<tbody>
{ledger.entries.map((entry) => {
const isExpanded = expandedCustomers.has(entry.customer_id)
return (
<React.Fragment key={entry.customer_id}>
<tr
className="border-b cursor-pointer hover:bg-muted/50"
onClick={() => toggleCustomer(entry.customer_id)}
>
<td className="py-2">
{isExpanded ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
</td>
<td className="py-2">{entry.customer_name}</td>
<td className="py-2 text-right">{entry.current > 0 ? formatAmount(entry.current) : ''}</td>
<td className="py-2 text-right">{entry.days_1_30 > 0 ? formatAmount(entry.days_1_30) : ''}</td>
<td className="py-2 text-right">{entry.days_31_60 > 0 ? formatAmount(entry.days_31_60) : ''}</td>
<td className="py-2 text-right">{entry.days_61_90 > 0 ? formatAmount(entry.days_61_90) : ''}</td>
<td className="py-2 text-right text-destructive">{entry.days_90_plus > 0 ? formatAmount(entry.days_90_plus) : ''}</td>
<td className="py-2 text-right font-semibold">{formatAmount(entry.total_outstanding)}</td>
</tr>
{isExpanded && entry.invoices.map((inv) => (
<tr key={inv.invoice_id} className="bg-muted/30 border-b last:border-0">
<td></td>
<td className="py-1 text-xs" colSpan={2}>
<span className="font-mono">{inv.invoice_number}</span>
<span className="text-muted-foreground ml-2">{inv.invoice_date}</span>
<span className="text-muted-foreground ml-2">förfaller {inv.due_date}</span>
</td>
<td className="py-1 text-right text-xs text-muted-foreground" colSpan={2}>
{inv.days_overdue > 0 ? `${inv.days_overdue} dagar förfallen` : 'Ej förfallen'}
</td>
<td className="py-1 text-right text-xs text-muted-foreground">
{inv.paid_amount > 0 ? `Betalt: ${formatAmount(inv.paid_amount)}` : ''}
</td>
<td></td>
<td className="py-1 text-right text-xs font-medium">
{formatAmount(inv.outstanding)} {inv.currency}
</td>
</tr>
))}
</React.Fragment>
)
})}
</tbody>
<tfoot>
<tr className="font-semibold border-t-2">
<td className="py-2"></td>
<td className="py-2">Summa</td>
<td className="py-2 text-right">{formatAmount(ledger.entries.reduce((s, e) => s + e.current, 0))}</td>
<td className="py-2 text-right">{formatAmount(ledger.entries.reduce((s, e) => s + e.days_1_30, 0))}</td>
<td className="py-2 text-right">{formatAmount(ledger.entries.reduce((s, e) => s + e.days_31_60, 0))}</td>
<td className="py-2 text-right">{formatAmount(ledger.entries.reduce((s, e) => s + e.days_61_90, 0))}</td>
<td className="py-2 text-right text-destructive">{formatAmount(ledger.entries.reduce((s, e) => s + e.days_90_plus, 0))}</td>
<td className="py-2 text-right">{formatAmount(ledger.total_outstanding)}</td>
</tr>
</tfoot>
</table>
</CardContent>
</Card>
)}
{/* Reconciliation */}
{reconciliation && (
<Card className="border-2">
<CardHeader>
<CardTitle>Avstämning mot <AccountNumber number="1510" /></CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span>Kundreskontra (summa utestående)</span>
<span className="font-mono">{formatAmount(reconciliation.ar_ledger_total)} kr</span>
</div>
<div className="flex justify-between">
<span><AccountNumber number="1510" /> saldo (huvudbok)</span>
<span className="font-mono">{formatAmount(reconciliation.account_1510_balance)} kr</span>
</div>
<div className="flex justify-between pt-2 border-t font-semibold">
<span>Differens</span>
<span className={reconciliation.is_reconciled ? 'text-success' : 'text-destructive'}>
{formatAmount(reconciliation.difference)} kr
</span>
</div>
<div className="pt-2">
{reconciliation.is_reconciled ? (
<Badge className="bg-success/10 text-success">Avstämd</Badge>
) : (
<Badge variant="destructive">Ej avstämd - kontrollera bokföring</Badge>
)}
</div>
</div>
</CardContent>
</Card>
)}
</div>
)
}