refactor(ui): migrate remaining inline pages to the concept design language (#1470)

* refactor(ui): migrate remaining inline pages to the concept design language

Catch-up pass for surfaces the 2026-07 UI migration missed:

- Bankavstämning: de-boxed toolbar, dry-table sections (preview, omatchade
  verifikationer, ignorerade, matchade), instructional copy moved behind the
  page "?" (HelpPopover via FocusedReport, sv+en), AttnLine for the dirty-
  dates hint, EmptyState for the blank page, space-y-8 rhythm.
- Report detail views (trial balance, income statement, balance sheet,
  resultat-/balansrapport, reskontror, huvudbok, grundbok, dimension-P&L):
  shared Skeleton/Error/EmptyState shells, border-2 totals bands flattened
  to hairline cards with font-display tabular-nums headline numbers,
  ReportSectionTable rebuilt on the group-band idiom, font-mono money ->
  tabular-nums, house tablist for Förenklad/Detaljerad, GL filter de-boxed
  onto Input primitives, verdicts follow chips-mark-exceptions.
- Extensions browse: PageHeader, locked section headers, rounded-lg
  secondary icon tiles, flat hover shift on cards, p-6 content.
- Återkommande fakturor: page-level list moved off ui/table onto dry-table
  with hover-revealed quiet row actions; Skeleton loading.
- Help: EmptyState for no search hits, flat hover shift on resource links.
- Chart of accounts: spinner loading blocks -> Skeleton rows.
- Kunskap graph + salary calendar popovers: rounded-lg, Input/Textarea
  primitives instead of hand-rolled shadow-sm controls.

No logic, endpoint, or data changes. Verified via sandbox screenshots;
lint 0 errors, 13214 tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ui): review triage: skip empty industry sectors, keyboard path to schedule edit

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-08-08 16:40:19 +02:00
committed by GitHub
co-authored by Claude Fable 5 Jakob Wennberg
parent 645ed0a53e
commit 02a5d10538
17 changed files with 781 additions and 883 deletions
+259 -357
View File
@@ -12,7 +12,7 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Badge } from '@/components/ui/badge'
import { AlertCircle, Check, ChevronDown, ChevronRight, ExternalLink, FileCode, FileDown, Percent } from 'lucide-react'
import { AlertCircle, Check, ChevronDown, ChevronRight, ExternalLink, FileCode, FileDown, FileText, Percent } from 'lucide-react'
import { Skeleton } from '@/components/ui/skeleton'
import { EmptyState } from '@/components/ui/empty-state'
import { FyPicker } from '@/components/common/FyPicker'
@@ -83,6 +83,36 @@ function formatAmount(amount: number): string {
return amount.toLocaleString('sv-SE', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
// Shared shells for the report bodies, so all views read as the same
// instrument: Skeleton while loading, EmptyState when the period has no data,
// a quiet destructive card on fetch errors (design.md primitives).
function ReportLoadingCard() {
return (
<Card>
<CardContent className="p-6 space-y-2">
{[1, 2, 3, 4, 5, 6].map((i) => (
<Skeleton key={i} className="h-4 w-full" />
))}
</CardContent>
</Card>
)
}
function ReportErrorCard({ message }: { message: string }) {
return (
<Card>
<CardContent className="p-8 text-center text-destructive">
<AlertCircle className="h-6 w-6 mx-auto mb-2" />
{message}
</CardContent>
</Card>
)
}
function ReportEmptyState({ title, description }: { title: string; description: string }) {
return <EmptyState icon={FileText} title={title} description={description} />
}
function reportQuery(
periodId: string,
range?: DateRangeValue,
@@ -132,35 +162,19 @@ export function TrialBalanceView({ periodId, onNavigateToAccount }: { periodId:
}, [periodId])
if (loading) {
return (
<Card>
<CardContent className="p-6 space-y-2">
{[1, 2, 3, 4, 5, 6].map((i) => (
<Skeleton key={i} className="h-4 w-full" />
))}
</CardContent>
</Card>
)
return <ReportLoadingCard />
}
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>
)
return <ReportErrorCard message={error} />
}
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>
<ReportEmptyState
title="Inga verifikationer i perioden"
description="Det finns inga bokförda verifikationer i den valda perioden."
/>
)
}
@@ -195,30 +209,38 @@ export function TrialBalanceView({ periodId, onNavigateToAccount }: { periodId:
<div className="flex items-center justify-between">
<CardTitle>Saldobalans</CardTitle>
<div className="flex items-center gap-3">
<div className="flex items-center gap-0.5 rounded-md border border-border bg-card p-0.5">
<div className="inline-flex shrink-0 gap-0.5 rounded-lg bg-muted/70 p-[3px]" role="tablist">
<button
type="button"
role="tab"
aria-selected={viewMode === 'simplified'}
onClick={() => setViewMode('simplified')}
className={`px-3 py-1 text-xs rounded-sm transition-colors ${
className={cn(
'rounded-md px-3.5 py-[5px] text-[12.5px] transition-colors duration-150',
viewMode === 'simplified'
? 'bg-secondary text-foreground'
: 'text-muted-foreground hover:text-foreground'
}`}
? 'border border-border bg-card font-medium text-foreground'
: 'text-muted-foreground hover:text-foreground',
)}
>
Förenklad
</button>
<button
type="button"
role="tab"
aria-selected={viewMode === 'detailed'}
onClick={() => setViewMode('detailed')}
className={`px-3 py-1 text-xs rounded-sm transition-colors ${
className={cn(
'rounded-md px-3.5 py-[5px] text-[12.5px] transition-colors duration-150',
viewMode === 'detailed'
? 'bg-secondary text-foreground'
: 'text-muted-foreground hover:text-foreground'
}`}
? 'border border-border bg-card font-medium text-foreground'
: 'text-muted-foreground hover:text-foreground',
)}
>
Detaljerad
</button>
</div>
{data.isBalanced ? (
<Badge variant="success">Balanserad</Badge>
<span className="text-sm text-muted-foreground">Balanserad</span>
) : (
<Badge variant="destructive">Ej balanserad</Badge>
)}
@@ -471,35 +493,19 @@ export function IncomeStatementView({ periodId, dateRange, dimensionFilter = nul
}, [periodId, reportQs])
if (loading) {
return (
<Card>
<CardContent className="p-6 space-y-2">
{[1, 2, 3, 4, 5, 6].map((i) => (
<Skeleton key={i} className="h-4 w-full" />
))}
</CardContent>
</Card>
)
return <ReportLoadingCard />
}
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>
)
return <ReportErrorCard message={error} />
}
if (!data) {
return (
<Card>
<CardContent className="p-8 text-center text-muted-foreground">
Ingen data för denna period.
</CardContent>
</Card>
<ReportEmptyState
title="Ingen data för perioden"
description="Det finns inget bokfört underlag för den valda perioden."
/>
)
}
@@ -521,12 +527,12 @@ export function IncomeStatementView({ periodId, dateRange, dimensionFilter = nul
<CardHeader>
<CardTitle className="text-base">Rörelseintäkter</CardTitle>
</CardHeader>
<CardContent>
<ReportSectionTable sections={data.revenue_sections} onNavigateToAccount={onNavigateToAccount} />
<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 className="p-0">
<ReportSectionTable
sections={data.revenue_sections}
onNavigateToAccount={onNavigateToAccount}
footer={{ label: 'Summa rörelseintäkter', amount: data.total_revenue }}
/>
</CardContent>
</Card>
@@ -535,24 +541,23 @@ export function IncomeStatementView({ periodId, dateRange, dimensionFilter = nul
<CardHeader>
<CardTitle className="text-base">Rörelsekostnader</CardTitle>
</CardHeader>
<CardContent>
<ReportSectionTable sections={data.expense_sections} negate onNavigateToAccount={onNavigateToAccount} />
<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 className="p-0">
<ReportSectionTable
sections={data.expense_sections}
negate
onNavigateToAccount={onNavigateToAccount}
footer={{ label: 'Summa rörelsekostnader', amount: data.total_expenses, negate: true }}
/>
</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 className="flex items-baseline justify-between p-6">
<span className="text-sm font-medium">Rörelseresultat</span>
<span className={`font-display text-xl tabular-nums ${data.total_revenue - data.total_expenses >= 0 ? 'text-success' : 'text-destructive'}`}>
{formatAmount(data.total_revenue - data.total_expenses)} kr
</span>
</CardContent>
</Card>
@@ -562,25 +567,23 @@ export function IncomeStatementView({ periodId, dateRange, dimensionFilter = nul
<CardHeader>
<CardTitle className="text-base">Finansiella poster</CardTitle>
</CardHeader>
<CardContent>
<ReportSectionTable sections={data.financial_sections} onNavigateToAccount={onNavigateToAccount} />
<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 className="p-0">
<ReportSectionTable
sections={data.financial_sections}
onNavigateToAccount={onNavigateToAccount}
footer={{ label: 'Summa finansiella poster', amount: data.total_financial }}
/>
</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>
<Card>
<CardContent className="flex items-baseline justify-between p-6">
<span className="text-sm font-medium">Årets resultat</span>
<span className={`font-display text-xl tabular-nums ${data.net_result >= 0 ? 'text-success' : 'text-destructive'}`}>
{formatAmount(data.net_result)} kr
</span>
</CardContent>
</Card>
</div>
@@ -616,33 +619,19 @@ export function BalanceSheetView({ periodId, dateRange, onNavigateToAccount }: {
}, [periodId, reportQs])
if (loading) {
return (
<Card>
<CardContent className="p-8 text-center text-muted-foreground">
Laddar balansräkning...
</CardContent>
</Card>
)
return <ReportLoadingCard />
}
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>
)
return <ReportErrorCard message={error} />
}
if (!data) {
return (
<Card>
<CardContent className="p-8 text-center text-muted-foreground">
Ingen data för denna period.
</CardContent>
</Card>
<ReportEmptyState
title="Ingen data för perioden"
description="Det finns inget bokfört underlag för den valda perioden."
/>
)
}
@@ -662,12 +651,12 @@ export function BalanceSheetView({ periodId, dateRange, onNavigateToAccount }: {
<CardHeader>
<CardTitle className="text-base">Tillgångar</CardTitle>
</CardHeader>
<CardContent>
<ReportSectionTable sections={data.asset_sections} onNavigateToAccount={onNavigateToAccount} />
<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 className="p-0">
<ReportSectionTable
sections={data.asset_sections}
onNavigateToAccount={onNavigateToAccount}
footer={{ label: 'Summa tillgångar', amount: data.total_assets }}
/>
</CardContent>
</Card>
@@ -676,30 +665,26 @@ export function BalanceSheetView({ periodId, dateRange, onNavigateToAccount }: {
<CardHeader>
<CardTitle className="text-base">Eget kapital och skulder</CardTitle>
</CardHeader>
<CardContent>
<ReportSectionTable sections={data.equity_liability_sections} onNavigateToAccount={onNavigateToAccount} />
<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 className="p-0">
<ReportSectionTable
sections={data.equity_liability_sections}
onNavigateToAccount={onNavigateToAccount}
footer={{ label: 'Summa eget kapital och skulder', amount: data.total_equity_liabilities }}
/>
</CardContent>
</Card>
{/* Balance check */}
<Card className="border-2">
<CardContent className="py-4">
<Card>
<CardContent className="p-6">
<div className="flex justify-between items-center">
<span className="font-bold text-lg">Balanscheck</span>
<span className="text-sm font-medium">Balanscheck</span>
{isBalanced ? (
<Badge variant="success" className="text-base px-3 py-1">
Balanserar
</Badge>
<span className="text-sm text-muted-foreground">Balanserar</span>
) : (
<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">
<Badge variant="destructive">Balanserar ej</Badge>
<p className="text-sm text-destructive mt-1 tabular-nums">
Differens: {formatAmount(Math.abs(data.total_assets - data.total_equity_liabilities))} kr
</p>
</div>
@@ -747,35 +732,19 @@ export function ResultatrapportView({ periodId, dateRange, dimensionFilter = nul
}, [periodId, reportQs])
if (loading) {
return (
<Card>
<CardContent className="p-6 space-y-2">
{[1, 2, 3, 4, 5, 6].map((i) => (
<Skeleton key={i} className="h-4 w-full" />
))}
</CardContent>
</Card>
)
return <ReportLoadingCard />
}
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>
)
return <ReportErrorCard message={error} />
}
if (!data || data.groups.length === 0) {
return (
<Card>
<CardContent className="p-8 text-center text-muted-foreground">
Inga bokförda intäkter eller kostnader i denna period.
</CardContent>
</Card>
<ReportEmptyState
title="Inga intäkter eller kostnader"
description="Det finns inga bokförda intäkter eller kostnader i den valda perioden."
/>
)
}
@@ -859,14 +828,14 @@ export function ResultatrapportView({ periodId, dateRange, dimensionFilter = nul
</CardContent>
</Card>
<Card className="border-2">
<CardContent className="py-4">
<Card>
<CardContent className="p-6">
<div className="grid gap-x-6 items-baseline grid-cols-[1fr_auto_auto]">
<span className="font-bold text-lg">Beräknat resultat</span>
<span className={`tabular-nums font-bold text-lg w-32 text-right ${data.net_result_current >= 0 ? 'text-success' : 'text-destructive'}`}>
<span className="text-sm font-medium">Beräknat resultat</span>
<span className={`font-display text-xl tabular-nums w-32 text-right ${data.net_result_current >= 0 ? 'text-success' : 'text-destructive'}`}>
{formatAmount(data.net_result_current)} kr
</span>
<span className="tabular-nums text-base text-muted-foreground w-32 text-right">
<span className="tabular-nums text-sm text-muted-foreground w-32 text-right">
{hasPrior ? `${formatAmount(data.net_result_prior)} kr` : '-'}
</span>
</div>
@@ -907,33 +876,19 @@ export function BalansrapportView({ periodId, dateRange, onNavigateToAccount }:
}, [periodId, reportQs])
if (loading) {
return (
<Card>
<CardContent className="p-8 text-center text-muted-foreground">
Laddar balansrapport...
</CardContent>
</Card>
)
return <ReportLoadingCard />
}
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>
)
return <ReportErrorCard message={error} />
}
if (!data || data.groups.length === 0) {
return (
<Card>
<CardContent className="p-8 text-center text-muted-foreground">
Inga balansposter i denna period.
</CardContent>
</Card>
<ReportEmptyState
title="Inga balansposter"
description="Det finns inga bokförda balansposter i den valda perioden."
/>
)
}
@@ -1005,8 +960,8 @@ export function BalansrapportView({ periodId, dateRange, onNavigateToAccount }:
</CardContent>
</Card>
<Card className="border-2">
<CardContent className="py-4 space-y-2">
<Card>
<CardContent className="p-6 space-y-2">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Summa tillgångar</span>
<span className="tabular-nums">{formatAmount(data.total_assets_ub)} kr</span>
@@ -1020,18 +975,14 @@ export function BalansrapportView({ periodId, dateRange, onNavigateToAccount }:
<span className="tabular-nums">{formatAmount(data.beraknat_resultat)} kr</span>
</div>
<div className="flex justify-between items-center pt-2 border-t">
<span className="font-bold text-lg">Balanscheck</span>
<span className="text-sm font-medium">Balanscheck</span>
{data.is_balanced ? (
<Badge variant="success" className="text-base px-3 py-1">
Balanserar
</Badge>
<span className="text-sm text-muted-foreground">Balanserar</span>
) : (
<div className="text-right">
<Badge variant="destructive" className="text-base px-3 py-1">
Balanserar ej
</Badge>
<Badge variant="destructive">Balanserar ej</Badge>
{data.imbalance_diagnosis && (
<p className="text-sm text-destructive mt-1">
<p className="text-sm text-destructive mt-1 tabular-nums">
Differens: {formatAmount(Math.abs(data.imbalance_diagnosis.differens))} kr
</p>
)}
@@ -1053,45 +1004,64 @@ function ReportSectionTable({
sections,
negate,
onNavigateToAccount,
footer,
}: {
sections: { title: string; rows: { account_number: string; account_name: string; amount: number }[]; subtotal: number }[]
negate?: boolean
onNavigateToAccount?: (account: string) => void
/** Report total, rendered as the table's tfoot (e.g. "Summa tillgångar"). */
footer?: { label: string; amount: number; negate?: boolean }
}) {
const fmt = (amount: number, neg?: boolean) =>
neg ? `-${formatAmount(amount)}` : formatAmount(amount)
if (sections.length === 0) {
return <p className="text-sm text-muted-foreground">Inga poster.</p>
return <p className="p-6 pt-0 text-sm text-muted-foreground">Inga poster.</p>
}
// One table with group-band rows (design.md tabular rules), matching the
// Resultatrapport/Balansrapport idiom, instead of one boxed sub-table per
// section with a repeated title line.
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>
<div className="overflow-x-auto -mx-2 px-2"><table className="w-full text-sm min-w-[400px]">
<tbody>
<div className="overflow-x-auto">
<table className="w-full text-sm min-w-[400px]">
<tbody>
{sections.map((section) => (
<React.Fragment key={section.title}>
<tr className="bg-muted/30">
<td colSpan={3} className="px-4 py-2 text-[12px] font-semibold text-muted-foreground">
{section.title}
</td>
</tr>
{section.rows.map((row) => (
<tr
key={row.account_number}
className={`border-b last:border-0 ${onNavigateToAccount ? 'cursor-pointer hover:bg-muted/50 transition-colors' : ''}`}
onClick={onNavigateToAccount ? () => onNavigateToAccount(row.account_number) : undefined}
>
<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 className="px-4 py-1.5 w-20"><AccountNumber number={row.account_number} name={row.account_name} /></td>
<td className="px-4 py-1.5">{row.account_name}</td>
<td className="px-4 py-1.5 text-right tabular-nums w-32 whitespace-nowrap">
{fmt(row.amount, negate)} kr
</td>
</tr>
))}
</tbody>
</table></div>
<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>
))}
<tr className="border-b font-medium">
<td colSpan={2} className="px-4 py-1.5 text-right text-muted-foreground">Summa</td>
<td className="px-4 py-1.5 text-right tabular-nums whitespace-nowrap">{fmt(section.subtotal, negate)} kr</td>
</tr>
</React.Fragment>
))}
</tbody>
{footer && (
<tfoot>
<tr className="font-medium">
<td colSpan={2} className="px-4 py-2">{footer.label}</td>
<td className="px-4 py-2 text-right tabular-nums whitespace-nowrap">{fmt(footer.amount, footer.negate)} kr</td>
</tr>
</tfoot>
)}
</table>
</div>
)
}
@@ -2524,33 +2494,19 @@ export function SupplierLedgerView({ periodId }: { periodId: string }) {
}, [periodId, asOfDate])
if (loading) {
return (
<Card>
<CardContent className="p-8 text-center text-muted-foreground">
Laddar leverantörsreskontra...
</CardContent>
</Card>
)
return <ReportLoadingCard />
}
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>
)
return <ReportErrorCard message={error} />
}
if (!data || !data.ledger) {
return (
<Card>
<CardContent className="p-8 text-center text-muted-foreground">
Ingen data tillgänglig.
</CardContent>
</Card>
<ReportEmptyState
title="Ingen leverantörsreskontra"
description="Det finns inga leverantörsfakturor att visa för den valda perioden."
/>
)
}
@@ -2642,7 +2598,7 @@ export function SupplierLedgerView({ periodId }: { periodId: string }) {
{/* Reconciliation */}
{reconciliation && (
<Card className="border-2">
<Card>
<CardHeader>
<CardTitle>Avstämning mot <AccountNumber number="2440" /></CardTitle>
</CardHeader>
@@ -2650,21 +2606,21 @@ export function SupplierLedgerView({ periodId }: { periodId: string }) {
<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>
<span className="tabular-nums">{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>
<span className="tabular-nums">{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'}>
<span className={`tabular-nums ${reconciliation.is_reconciled ? 'text-success' : 'text-destructive'}`}>
{formatAmount(reconciliation.difference)} kr
</span>
</div>
<div className="pt-2 space-y-2">
{reconciliation.is_reconciled ? (
<Badge variant="success">Avstämd</Badge>
<span className="text-sm text-muted-foreground">Avstämd</span>
) : (
<Badge variant="destructive">Ej avstämd - kontrollera bokföring</Badge>
)}
@@ -2810,73 +2766,57 @@ export function GeneralLedgerView({ periodId, initialAccountFilter, dimensionFil
}, [periodId, initialAccountFilter, dimensionFilter, dateRange])
if (loading) {
return (
<Card>
<CardContent className="p-8 text-center text-muted-foreground">
Laddar huvudbok...
</CardContent>
</Card>
)
return <ReportLoadingCard />
}
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>
)
return <ReportErrorCard message={error} />
}
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>
<ReportEmptyState
title="Inga verifikationer i perioden"
description="Det finns inga bokförda verifikationer i den valda perioden."
/>
)
}
return (
<div className="space-y-4">
<ReportExportMenu items={[{ format: 'xlsx', href: `/api/reports/general-ledger/xlsx?${reportQuery(periodId, dateRange, dimensionFilter)}` }]} />
{/* 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>
{/* Account range filter: flat toolbar on the panel, no box */}
<div className="flex flex-wrap items-end gap-4">
<div>
<Label htmlFor="gl-account-from">Konto från</Label>
<Input
id="gl-account-from"
type="text"
value={accountFrom}
onChange={(e) => setAccountFrom(e.target.value)}
placeholder="t.ex. 1510"
className="mt-1 w-32"
/>
</div>
<div>
<Label htmlFor="gl-account-to">Konto till</Label>
<Input
id="gl-account-to"
type="text"
value={accountTo}
onChange={(e) => setAccountTo(e.target.value)}
placeholder="t.ex. 1519"
className="mt-1 w-32"
/>
</div>
<Button onClick={() => fetchData()} variant="outline">
Filtrera
</Button>
</div>
{data.period.start && (
<p className="text-sm text-muted-foreground">
Period: {data.period.start}: {data.period.end} | {data.accounts.length} konton
<p className="text-sm text-muted-foreground tabular-nums">
Period {data.period.start} till {data.period.end} · {data.accounts.length} konton
</p>
)}
@@ -2933,16 +2873,16 @@ export function GeneralLedgerView({ periodId, initialAccountFilter, dimensionFil
<td className="py-1.5 text-right tabular-nums">
{line.credit > 0 ? formatAmount(line.credit) : ''}
</td>
<td className="py-1.5 text-right font-mono tabular-nums">{formatAmount(line.balance)}</td>
<td className="py-1.5 text-right tabular-nums">{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>
<td className="py-2 text-right tabular-nums">{formatAmount(account.total_debit)}</td>
<td className="py-2 text-right tabular-nums">{formatAmount(account.total_credit)}</td>
<td className="py-2 text-right tabular-nums">{formatAmount(account.closing_balance)}</td>
</tr>
</tfoot>
</table></div>
@@ -3022,33 +2962,19 @@ export function JournalRegisterView({ periodId }: { periodId: string }) {
}
if (loading) {
return (
<Card>
<CardContent className="p-8 text-center text-muted-foreground">
Laddar grundbok...
</CardContent>
</Card>
)
return <ReportLoadingCard />
}
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>
)
return <ReportErrorCard message={error} />
}
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>
<ReportEmptyState
title="Inga verifikationer i perioden"
description="Det finns inga bokförda verifikationer i den valda perioden."
/>
)
}
@@ -3056,8 +2982,8 @@ export function JournalRegisterView({ periodId }: { periodId: string }) {
<div className="space-y-4">
<ReportExportMenu items={[{ format: 'xlsx', href: `/api/reports/journal-register/xlsx?period_id=${periodId}` }]} />
{data.period.start && (
<p className="text-sm text-muted-foreground">
Period: {data.period.start}: {data.period.end} | {data.total_entries} verifikationer
<p className="text-sm text-muted-foreground tabular-nums">
Period {data.period.start} till {data.period.end} · {data.total_entries} verifikationer
</p>
)}
@@ -3324,33 +3250,19 @@ export function ARLedgerView({ periodId }: { periodId: string }) {
}
if (loading) {
return (
<Card>
<CardContent className="p-8 text-center text-muted-foreground">
Laddar kundreskontra...
</CardContent>
</Card>
)
return <ReportLoadingCard />
}
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>
)
return <ReportErrorCard message={error} />
}
if (!data || !data.ledger) {
return (
<Card>
<CardContent className="p-8 text-center text-muted-foreground">
Ingen data tillgänglig.
</CardContent>
</Card>
<ReportEmptyState
title="Ingen kundreskontra"
description="Det finns inga kundfakturor att visa för den valda perioden."
/>
)
}
@@ -3471,7 +3383,7 @@ export function ARLedgerView({ periodId }: { periodId: string }) {
{/* Reconciliation */}
{reconciliation && (
<Card className="border-2">
<Card>
<CardHeader>
<CardTitle>Avstämning mot <AccountNumber number="1510" /></CardTitle>
</CardHeader>
@@ -3479,21 +3391,21 @@ export function ARLedgerView({ periodId }: { periodId: string }) {
<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>
<span className="tabular-nums">{formatAmount(reconciliation.ar_ledger_total)} kr</span>
</div>
<div className="flex justify-between">
<span>Kundfordringar (<AccountNumber number="1510" /> + <AccountNumber number="1513" />) saldo</span>
<span className="font-mono">{formatAmount(reconciliation.account_1510_balance)} kr</span>
<span className="tabular-nums">{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'}>
<span className={`tabular-nums ${reconciliation.is_reconciled ? 'text-success' : 'text-destructive'}`}>
{formatAmount(reconciliation.difference)} kr
</span>
</div>
<div className="pt-2 space-y-2">
{reconciliation.is_reconciled ? (
<Badge variant="success">Avstämd</Badge>
<span className="text-sm text-muted-foreground">Avstämd</span>
) : (
<Badge variant="destructive">Ej avstämd - kontrollera bokföring</Badge>
)}
@@ -3593,11 +3505,7 @@ export function DimensionPnlView({ periodId, dateRange }: { periodId: string; da
return (
<div className="space-y-4">
{pivotPicker}
<Card>
<CardContent className="p-8 text-center text-muted-foreground">
Laddar rapport...
</CardContent>
</Card>
<ReportLoadingCard />
</div>
)
}
@@ -3606,12 +3514,7 @@ export function DimensionPnlView({ periodId, dateRange }: { periodId: string; da
return (
<div className="space-y-4">
{pivotPicker}
<Card>
<CardContent className="p-8 text-center text-destructive">
<AlertCircle className="h-6 w-6 mx-auto mb-2" />
{error}
</CardContent>
</Card>
<ReportErrorCard message={error} />
</div>
)
}
@@ -3620,11 +3523,10 @@ export function DimensionPnlView({ periodId, dateRange }: { periodId: string; da
return (
<div className="space-y-4">
{pivotPicker}
<Card>
<CardContent className="p-8 text-center text-muted-foreground">
Inga taggade intäkter eller kostnader i denna period.
</CardContent>
</Card>
<ReportEmptyState
title="Inga taggade poster"
description="Inga intäkter eller kostnader är taggade med en dimension i den valda perioden."
/>
</div>
)
}
@@ -3696,20 +3598,20 @@ export function DimensionPnlView({ periodId, dateRange }: { periodId: string; da
</CardContent>
</Card>
<Card className="border-2">
<CardContent className="py-4">
<Card>
<CardContent className="p-6">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<tbody>
<tr>
<td className="px-4 font-bold text-lg">Beräknat resultat</td>
<td className="px-4 text-sm font-medium">Beräknat resultat</td>
<td className="px-4" />
{data.net_per_column.map((v, i) => (
<td key={i} className={`px-4 text-right tabular-nums font-semibold w-32 ${v >= 0 ? 'text-success' : 'text-destructive'}`}>
<td key={i} className={`px-4 text-right tabular-nums font-medium w-32 ${v >= 0 ? 'text-success' : 'text-destructive'}`}>
{formatAmount(v)}
</td>
))}
<td className={`px-4 text-right tabular-nums font-bold text-lg w-32 ${data.net_total >= 0 ? 'text-success' : 'text-destructive'}`}>
<td className={`px-4 text-right font-display text-xl tabular-nums w-32 ${data.net_total >= 0 ? 'text-success' : 'text-destructive'}`}>
{formatAmount(data.net_total)} kr
</td>
</tr>