feat: Add 4 major features to BOC Dashboard

1. Balance Sheet Chart — grafisk visning med progress bars
   - Assets (grön), Liabilities (röd), Equity (blå)
   - Balance check längst ner

2. Period-väljare — dropdown för månadsval
   - 2026-02, 2026-03, 2026-04

3. Drill-down — klicka på konto för transaktionshistorik
   - Modal med Date, Description, Debit, Credit, Balance
   - Mock data (ersätt med API-anrop)

4. Export-knappar — SIE4, PDF, CSV
   - Download-funktionalitet
   - Loading-tillstånd

Alla komponenter bygger och integreras i DashboardPage.
This commit is contained in:
Bernt
2026-07-29 20:17:34 +00:00
parent f793fc46a4
commit dd145bc3d9
9 changed files with 791 additions and 467 deletions
@@ -0,0 +1,105 @@
import { useState, useEffect } from 'react'
import { Card, CardHeader } from '@/components/ui/Card'
import {
Table,
TableHead,
TableBody,
TableRow,
TableHeader,
TableCell,
} from '@/components/ui/Table'
interface Transaction {
id: string
date: string
description: string
debit: number
credit: number
balance: number
}
interface AccountDetailModalProps {
accountCode: string
accountName: string
onClose: () => void
}
export function AccountDetailModal({ accountCode, accountName, onClose }: AccountDetailModalProps) {
const [transactions, setTransactions] = useState<Transaction[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
// Mock data - replace with actual API call
const mockTransactions: Transaction[] = [
{ id: '1', date: '2026-02-01', description: 'Opening balance', debit: 0, credit: 0, balance: 44313 },
{ id: '2', date: '2026-02-15', description: 'Purchase - Dell laptops', debit: 25000, credit: 0, balance: 69313 },
{ id: '3', date: '2026-02-20', description: 'Depreciation', debit: 0, credit: 25000, balance: 44313 },
]
setTransactions(mockTransactions)
setLoading(false)
}, [accountCode])
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-surface rounded-lg w-full max-w-4xl max-h-[80vh] overflow-auto">
<Card>
<CardHeader
title={`${accountCode} - ${accountName}`}
subtitle="Transaction History"
action={
<button
onClick={onClose}
className="text-sm text-primary hover:underline"
>
Close
</button>
}
/>
<div className="p-6">
{loading ? (
<div className="text-center py-8">Loading...</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Date</TableHead>
<TableHead>Description</TableHead>
<TableHead className="text-right">Debit</TableHead>
<TableHead className="text-right">Credit</TableHead>
<TableHead className="text-right">Balance</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{transactions.map((tx) => (
<TableRow key={tx.id}>
<TableCell>{tx.date}</TableCell>
<TableCell>{tx.description}</TableCell>
<TableCell className="text-right text-success">
{tx.debit > 0 ? formatCurrency(tx.debit) : '-'}
</TableCell>
<TableCell className="text-right text-danger">
{tx.credit > 0 ? formatCurrency(tx.credit) : '-'}
</TableCell>
<TableCell className="text-right font-medium">
{formatCurrency(tx.balance)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</div>
</Card>
</div>
</div>
)
}
function formatCurrency(value: number): string {
return new Intl.NumberFormat('sv-SE', {
style: 'currency',
currency: 'SEK',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(value)
}
+116
View File
@@ -0,0 +1,116 @@
import { useMemo } from 'react'
import { Card, CardHeader } from '@/components/ui/Card'
interface BalanceSheetData {
assets: { code: string; name: string; balance: number }[]
liabilities: { code: string; name: string; balance: number }[]
equity: { code: string; name: string; balance: number }[]
total_assets: number
total_liabilities: number
total_equity: number
}
export function BalanceSheetChart({ data }: { data: BalanceSheetData }) {
const maxValue = useMemo(() => {
return Math.max(
Math.abs(data.total_assets),
Math.abs(data.total_liabilities),
Math.abs(data.total_equity),
1
)
}, [data])
const barWidth = (value: number) => {
return `${(Math.abs(value) / maxValue) * 100}%`
}
return (
<Card>
<CardHeader title="Balance Sheet (Balansräkning)" subtitle="Assets vs Liabilities & Equity" />
<div className="p-6 space-y-6">
{/* Assets */}
<div>
<div className="flex justify-between mb-2">
<span className="font-semibold text-success">Assets (Tillgångar)</span>
<span className="font-bold">{formatCurrency(data.total_assets)}</span>
</div>
<div className="w-full bg-surface-2 rounded-full h-4">
<div
className="bg-success h-4 rounded-full transition-all"
style={{ width: barWidth(data.total_assets) }}
/>
</div>
<div className="mt-2 space-y-1">
{data.assets.slice(0, 5).map((a) => (
<div key={a.code} className="flex justify-between text-sm">
<span>{a.name}</span>
<span>{formatCurrency(a.balance)}</span>
</div>
))}
</div>
</div>
{/* Liabilities */}
<div>
<div className="flex justify-between mb-2">
<span className="font-semibold text-danger">Liabilities (Skulder)</span>
<span className="font-bold">{formatCurrency(data.total_liabilities)}</span>
</div>
<div className="w-full bg-surface-2 rounded-full h-4">
<div
className="bg-danger h-4 rounded-full transition-all"
style={{ width: barWidth(data.total_liabilities) }}
/>
</div>
<div className="mt-2 space-y-1">
{data.liabilities.slice(0, 5).map((l) => (
<div key={l.code} className="flex justify-between text-sm">
<span>{l.name}</span>
<span>{formatCurrency(l.balance)}</span>
</div>
))}
</div>
</div>
{/* Equity */}
{data.equity && data.equity.length > 0 && (
<div>
<div className="flex justify-between mb-2">
<span className="font-semibold text-primary">Equity (Eget kapital)</span>
<span className="font-bold">{formatCurrency(data.total_equity)}</span>
</div>
<div className="w-full bg-surface-2 rounded-full h-4">
<div
className="bg-primary h-4 rounded-full transition-all"
style={{ width: barWidth(data.total_equity) }}
/>
</div>
</div>
)}
{/* Balance check */}
<div className="border-t pt-4">
<div className="flex justify-between items-center">
<span className="text-sm text-muted">Balance check:</span>
<span className={`font-mono ${
Math.abs(data.total_assets + data.total_liabilities + data.total_equity) < 0.01
? 'text-success'
: 'text-danger'
}`}>
{formatCurrency(data.total_assets + data.total_liabilities + data.total_equity)}
</span>
</div>
</div>
</div>
</Card>
)
}
function formatCurrency(value: number): string {
return new Intl.NumberFormat('sv-SE', {
style: 'currency',
currency: 'SEK',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(value)
}
+62
View File
@@ -0,0 +1,62 @@
import { useState } from 'react'
import { Card, CardHeader } from '@/components/ui/Card'
interface ExportButtonsProps {
period: string
}
export function ExportButtons({ period }: ExportButtonsProps) {
const [exporting, setExporting] = useState<string | null>(null)
const handleExport = async (format: string) => {
setExporting(format)
try {
const response = await fetch(`/api/v1/finance/export?period=${period}&format=${format}`)
if (!response.ok) throw new Error('Export failed')
const blob = await response.blob()
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `ledger-${period}.${format.toLowerCase()}`
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(url)
document.body.removeChild(a)
} catch (err) {
console.error('Export error:', err)
alert(`Failed to export ${format}`)
} finally {
setExporting(null)
}
}
return (
<Card>
<CardHeader title="Export Reports" subtitle="Download ledger data in various formats" />
<div className="p-6 flex gap-4">
<button
onClick={() => handleExport('SIE4')}
disabled={exporting === 'SIE4'}
className="px-4 py-2 bg-primary text-white rounded hover:bg-primary-dark disabled:opacity-50"
>
{exporting === 'SIE4' ? 'Exporting...' : 'Export SIE4'}
</button>
<button
onClick={() => handleExport('PDF')}
disabled={exporting === 'PDF'}
className="px-4 py-2 bg-danger text-white rounded hover:bg-danger-dark disabled:opacity-50"
>
{exporting === 'PDF' ? 'Exporting...' : 'Export PDF'}
</button>
<button
onClick={() => handleExport('CSV')}
disabled={exporting === 'CSV'}
className="px-4 py-2 bg-success text-white rounded hover:bg-success-dark disabled:opacity-50"
>
{exporting === 'CSV' ? 'Exporting...' : 'Export CSV'}
</button>
</div>
</Card>
)
}