diff --git a/DECISIONS.md b/DECISIONS.md index c36306e3..2d5e287d 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -838,6 +838,7 @@ One line per decision: `[YYYY-MM-DD] : `. Appended by agents and [2026-08-08] Per-voucher RC basis gaps (findRcBasisGaps) downgrade from filing-blocking ERROR to WARNING only under per-rate evidence: the 44xx/45xx basis accounts grouped by momssats must match ruta30/0.25, ruta31/0.12, ruta32/0.06 two-sided within 0.5 kr, all rate boxes non-negative, and no RC_OUTPUT_MISSING present. A first cross-rate-sum predicate was refuted by /skeptic (wrong-rate fiktiv moms reached parity and unblocked a 7 800 kr under-declaration; a negative rate box made the predicate vacuous), so the certificate is per-rate, which rutor alone cannot express (rutor 20-24 are partitioned by purchase type, not rate); evidence therefore flows from the account totals. Why downgrade at all: a moms-only rattelseverifikat carries fiktiv moms whose basbelopp lives in another (often reversed) verifikat, and no voucher arrangement satisfies both the per-voucher scan and the aggregate identity in that state, so the ERROR was an unfixable dead end (Orto Engineering 3DJake case 2026-08; support vouchers A169/A175/A177 joined the blocklist they were meant to clear). Data side repaired separately with voucher A177 restoring bank parity and the basis/moms identity exactly. [2026-08-08] Fenced-JSON fix uses brace-slice, not fence-regex: also rescues preamble/postamble prose around the object, and degrades to the existing empty-result path when no braces exist. [2026-08-08] extractJsonObject upgraded from brace-slice to depth-aware balanced scan after PR 1460 review: prose containing braces around the JSON no longer poisons the slice; first parseable candidate wins. +[2026-08-08] Inline-page UI catch-up applies chips-mark-exceptions to report verdicts uniformly: positive verdicts (Balanserad, Balanserar, Avstämd) render as muted text and only deviations keep a destructive Badge; page-flow instructional copy on Bankavstämning moved behind the FocusedReport "?" HelpPopover (convention 7) instead of being deleted, since the IB/preview semantics are load-bearing for correct reconciliation. [2026-08-08] Journey branch question (PR 2 of the activation concept) renders only in mode='first' and persists initial_setup_path as a fire-and-forget PATCH: in mode='add' a silently-failed setActiveCompany (deliberately non-fatal in createCompanyFromOnboarding) would make the PATCH land on the PREVIOUS company's settings, and experienced multi-company users get the Hem checklist anyway; navigation never blocks on the PATCH because the checklist path is a nicety, not a prerequisite. Provider preselect at /import?mode=migration&provider=X auto-advances only for sieViaApi providers (fortnox/bjornlunden/briox): visma/bokio must land on the provider list where the "SIE krävs först" gate renders with its async connection status. [2026-08-08] SIE export always emits #FORMAT PC8 even when bytes are UTF-8: the record is compulsory in the spec and strict importers (Visma Spiris) reject files without it, while real encoding is detected from bytes (Fortnox ships the same shape). Default bytes stay UTF-8; encoding=cp437 remains opt-in. [2026-08-08] Login panel is method-stated (BankID hero default, remembered via accounted-login-method cookie) instead of a stacked method list: matches the Swedish bank/Fortnox convention, gives exactly one primary action per view; errors moved from boxed banner to a field-adjacent single line (NN/g 3/4/10), reset link surfaces from the second failed attempt. diff --git a/app/(dashboard)/extensions/[sector]/[extension]/page.tsx b/app/(dashboard)/extensions/[sector]/[extension]/page.tsx index e256fe59..aea82337 100644 --- a/app/(dashboard)/extensions/[sector]/[extension]/page.tsx +++ b/app/(dashboard)/extensions/[sector]/[extension]/page.tsx @@ -72,8 +72,8 @@ export default async function ExtensionDetailPage({ {/* Header */}
-
- +
+

{extensionName}

@@ -95,14 +95,14 @@ export default async function ExtensionDetailPage({ {/* Details */}
-

{t('description_heading')}

+

{t('description_heading')}

{extensionLongDescription}

-

{t('data_source_heading')}

+

{t('data_source_heading')}

{dataPatternLabels[definition.dataPattern]}

diff --git a/app/(dashboard)/extensions/[sector]/page.tsx b/app/(dashboard)/extensions/[sector]/page.tsx index ef339fab..5603e36f 100644 --- a/app/(dashboard)/extensions/[sector]/page.tsx +++ b/app/(dashboard)/extensions/[sector]/page.tsx @@ -40,8 +40,8 @@ export default async function SectorExtensionsPage({ {/* Header */}
-
- +
+

{sectorName}

@@ -50,7 +50,7 @@ export default async function SectorExtensionsPage({
{/* Extensions grid */} -
+
{sector.extensions.map(ext => ( ))} diff --git a/app/(dashboard)/extensions/page.tsx b/app/(dashboard)/extensions/page.tsx index 93357d68..2e04c81d 100644 --- a/app/(dashboard)/extensions/page.tsx +++ b/app/(dashboard)/extensions/page.tsx @@ -1,13 +1,16 @@ import { getTranslations } from 'next-intl/server' import { SECTORS } from '@/lib/extensions/sectors' import { sectorNameKey } from '@/lib/extensions/i18n' +import { PageHeader } from '@/components/ui/page-header' import ExtensionCard from '@/components/extensions/ExtensionCard' import SectorCard from '@/components/extensions/SectorCard' export default async function ExtensionsPage() { const t = await getTranslations('extensions') const generalSector = SECTORS.find(s => s.slug === 'general') - const industrySectors = SECTORS.filter(s => s.slug !== 'general') + // Only sectors that actually ship extensions: a shell with zero extensions + // would render a dead card and an empty grid. + const industrySectors = SECTORS.filter(s => s.slug !== 'general' && s.extensions.length > 0) const generalSectorName = (() => { if (!generalSector) return '' @@ -16,21 +19,16 @@ export default async function ExtensionsPage() { })() return ( -
-
-

{t('page_title')}

-

- {t('page_description')} -

-
+
+ {/* General extensions */} {generalSector && ( -
-

+
+

{generalSectorName}

-
+
{generalSector.extensions.map(ext => ( ))} @@ -38,17 +36,19 @@ export default async function ExtensionsPage() {
)} - {/* Industry sectors */} -
-

- {t('industry_tools')} -

-
- {industrySectors.map(sector => ( - - ))} -
-
+ {/* Industry sectors (hidden while no industry sector ships extensions) */} + {industrySectors.length > 0 && ( +
+

+ {t('industry_tools')} +

+
+ {industrySectors.map(sector => ( + + ))} +
+
+ )}

) } diff --git a/app/(dashboard)/help/page.tsx b/app/(dashboard)/help/page.tsx index 4a1b9410..607eef69 100644 --- a/app/(dashboard)/help/page.tsx +++ b/app/(dashboard)/help/page.tsx @@ -6,6 +6,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com import { Input } from '@/components/ui/input' import { HelpLink } from '@/components/ui/info-tooltip' import { PageHeader } from '@/components/ui/page-header' +import { EmptyState } from '@/components/ui/empty-state' import { Search, BookOpen, @@ -377,14 +378,11 @@ export default function HelpPage() { {/* Terms list */}
{filteredTerms.length === 0 ? ( - - - -

- {t('no_results', { query: searchQuery })} -

-
-
+ ) : ( filteredTerms.map((term) => (
@@ -421,7 +419,7 @@ export default function HelpPage() {
@@ -444,7 +442,7 @@ export default function HelpPage() {
@@ -456,7 +454,7 @@ export default function HelpPage() {
diff --git a/app/(dashboard)/invoices/recurring/page.tsx b/app/(dashboard)/invoices/recurring/page.tsx index d3b7bb39..ab59a310 100644 --- a/app/(dashboard)/invoices/recurring/page.tsx +++ b/app/(dashboard)/invoices/recurring/page.tsx @@ -4,18 +4,12 @@ import { useState, useEffect } from 'react' import { useRouter, useSearchParams } from 'next/navigation' import { useTranslations } from 'next-intl' import { Button } from '@/components/ui/button' -import { Card, CardContent } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { PageHeader } from '@/components/ui/page-header' -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table' +import { Skeleton } from '@/components/ui/skeleton' +import { TH_CLASS, TD_CLASS, QUIET_LINK_CLASS, HOVER_REVEAL_CLASS } from '@/components/ui/dry-table' import { EmptyState } from '@/components/ui/empty-state' +import { cn } from '@/lib/utils' import { DestructiveConfirmDialog, useDestructiveConfirm, @@ -196,134 +190,144 @@ export default function RecurringInvoicesPage() { /> {isLoading ? ( - - - {t('loading')} - - +
+ {[1, 2, 3, 4].map((i) => ( + + ))} +
) : schedules.length === 0 ? ( - - - - - + ) : ( - - - - - - {t('th_name')} - {t('th_customer')} - {t('th_day')} - {t('th_next_run')} - {t('th_status')} - {t('th_generated')} - {t('th_actions')} - - - - {schedules.map((s) => ( - openEdit(s.id) : undefined} - > - -
- {s.name} - {s.last_run_warning && ( - - )} -
-
- - {s.customer?.name ?? '-'} - - - {s.day_of_month} - - {' · '} - {t('send_time', { - time: `${String(s.send_hour ?? 8).padStart(2, '0')}:00`, - })} - {/* Monthly is the norm; only a deviating cadence is - worth a label (chips-mark-exceptions convention). */} - {(s.interval_months ?? 1) > 1 && ( - <> - {' · '} - {s.interval_months === 3 - ? t('interval_quarterly') - : s.interval_months === 6 - ? t('interval_semiannual') - : s.interval_months === 12 - ? t('interval_yearly') - : t('interval_every_n', { n: s.interval_months })} - - )} - - - {formatDate(s.next_run_date)} - - {s.status === 'active' ? ( - {t('status_active')} +
+
+ + + + + + + + + + + + + {schedules.map((s) => ( + openEdit(s.id) : undefined} + > + + + + + + +
{t('th_name')}{t('th_customer')}{t('th_day')}{t('th_next_run')}{t('th_status')}{t('th_generated')}{t('th_actions')}
+
+ {/* Focusable edit affordance: the row onClick is mouse-only, + so keyboard users open the editor through the name. */} + {canWrite ? ( + ) : ( - {t('status_paused')} + s.name )} - - - {s.generated_count} - - + {s.last_run_warning && ( + + )} +
+
+ {s.customer?.name ?? '-'} + + {s.day_of_month} + + {' · '} + {t('send_time', { + time: `${String(s.send_hour ?? 8).padStart(2, '0')}:00`, + })} + {/* Monthly is the norm; only a deviating cadence is + worth a label (chips-mark-exceptions convention). */} + {(s.interval_months ?? 1) > 1 && ( + <> + {' · '} + {s.interval_months === 3 + ? t('interval_quarterly') + : s.interval_months === 6 + ? t('interval_semiannual') + : s.interval_months === 12 + ? t('interval_yearly') + : t('interval_every_n', { n: s.interval_months })} + + )} + + {formatDate(s.next_run_date)} + {s.status === 'active' ? ( + {t('status_active')} + ) : ( + {t('status_paused')} + )} + + {s.generated_count} + + {canWrite && (
e.stopPropagation()} > - {canWrite && ( - <> - - - - - )} + + +
- - - ))} - -
-
-
+ )} + + + ))} + + +
)} - +
diff --git a/components/agent-knowledge/LedgerGraph.tsx b/components/agent-knowledge/LedgerGraph.tsx index 8e95b993..4f76c138 100644 --- a/components/agent-knowledge/LedgerGraph.tsx +++ b/components/agent-knowledge/LedgerGraph.tsx @@ -277,7 +277,7 @@ export function LedgerGraph({ deep, companyName }: { deep: DeepLedgerContext; co if (model.payees.length === 0) { return (
{t('none_cp')} @@ -304,7 +304,7 @@ export function LedgerGraph({ deep, companyName }: { deep: DeepLedgerContext; co return (
{loading ? ( -
- - {t('loading')} +
+ {[1, 2, 3, 4, 5, 6].map((i) => ( + + ))}
) : view === 'my-accounts' ? ( filteredAccounts.length === 0 ? ( @@ -669,9 +671,10 @@ export default function ChartOfAccountsManager() { ) : (
{referenceLoading && referenceAccounts.length === 0 ? ( -
- - {t('loading')} +
+ {[1, 2, 3, 4, 5, 6].map((i) => ( + + ))}
) : filteredReference.length === 0 ? (

{t('no_matches')}

diff --git a/components/extensions/ExtensionCard.tsx b/components/extensions/ExtensionCard.tsx index 659ddf88..9638593b 100644 --- a/components/extensions/ExtensionCard.tsx +++ b/components/extensions/ExtensionCard.tsx @@ -18,10 +18,10 @@ export default async function ExtensionCard({ extension }: { extension: Extensio return ( - +
-
- +
+
- - + +
-
- +
+
-

+

{name}

{description}

diff --git a/components/reports/BankReconciliationView.tsx b/components/reports/BankReconciliationView.tsx index 28b0ae14..1ed54524 100644 --- a/components/reports/BankReconciliationView.tsx +++ b/components/reports/BankReconciliationView.tsx @@ -10,8 +10,11 @@ import { Label } from '@/components/ui/label' import { Badge } from '@/components/ui/badge' import { Switch } from '@/components/ui/switch' import { Skeleton } from '@/components/ui/skeleton' +import { EmptyState } from '@/components/ui/empty-state' +import { AttnLine } from '@/components/ui/attn-line' +import { TH_CLASS, TD_CLASS } from '@/components/ui/dry-table' import { AccountNumber } from '@/components/ui/account-number' -import { AlertCircle, ChevronDown, ChevronRight, Link2, Unlink, Play, Eye, EyeOff, PiggyBank, MoreHorizontal } from 'lucide-react' +import { AlertCircle, ChevronDown, ChevronRight, Landmark, Link2, Unlink, Play, Eye, EyeOff, PiggyBank, MoreHorizontal } from 'lucide-react' import { formatCurrency, formatDate } from '@/lib/utils' import { formatVoucher } from '@/lib/bookkeeping/voucher-series-resolver' import { CashAccountSelector } from '@/components/common/CashAccountSelector' @@ -878,7 +881,7 @@ export function BankReconciliationView({ periodId, periodBounds }: BankReconcili } return ( -
+
{error && ( @@ -898,14 +901,11 @@ export function BankReconciliationView({ periodId, periodBounds }: BankReconcili
Avstämning mot {status.is_reconciled ? ( - Avstämd + Avstämd ) : ( Ej avstämd )}
-

- Avstämningen körs mot ({accountCurrency}). Övriga bankkonton (t.ex. Plusgiro , kreditkort eller valutakonton) stäms av separat. Välj kontot i listan nedan. -

@@ -963,139 +963,124 @@ export function BankReconciliationView({ periodId, periodBounds }: BankReconcili )} - {/* Action Bar */} - - -
- +
+ +
+ + setDateFrom(e.target.value)} + className="mt-1" /> -
- - setDateFrom(e.target.value)} - className="mt-1" - /> -
-
- - setDateTo(e.target.value)} - className="mt-1" - /> -
- -
- - {dryRunResults && dryRunResults.length > 0 && ( - - )}
- {datesDirty && ( -

- Datumfiltret är ändrat men inte tillämpat: klicka Filtrera för att uppdatera - listorna innan du förhandsgranskar. -

+
+ + setDateTo(e.target.value)} + className="mt-1" + /> +
+ +
+ + {dryRunResults && dryRunResults.length > 0 && ( + )} - - +
+ {datesDirty && ( + + Datumfiltret är ändrat men inte tillämpat: klicka Filtrera för att uppdatera + listorna innan du förhandsgranskar. + + )} +
{/* Dry Run Preview */} {dryRunResults && dryRunResults.length > 0 && ( - - - - Förhandsgranskning: {dryRunResults.length}{' '} - {dryRunResults.length === 1 ? 'matchning hittad' : 'matchningar hittade'} - -

- Starka träffar är förvalda. Ungefärliga träffar kräver att du bockar i dem själv: - granska verifikationen först. -

-
- -
- - - - - - - - - - - - - - - - {dryRunResults.map((m) => { - const key = matchKey(m.transaction_id, m.journal_entry_id) - const badge = confidenceLabel(m.confidence) - return ( - - - - - - - - - - - - ) - })} - -
TransaktionDatumBeloppVerifikationDatumMetodTräff
- toggleMatchSelection(key)} - aria-label={`Tillämpa matchning för ${m.transaction_description}`} - /> - {m.transaction_description}{formatDate(m.transaction_date)}{formatAmount(m.transaction_amount)} - - {formatVoucher(m)} - - {m.entry_description} - {formatDate(m.entry_date)} - - {METHOD_LABELS[m.method] || m.method} - - - {badge.label} -
-
-
-
+
+

+ Förhandsgranskning ({dryRunResults.length}) +

+
+ + + + + + + + + + + + + + + + {dryRunResults.map((m) => { + const key = matchKey(m.transaction_id, m.journal_entry_id) + const badge = confidenceLabel(m.confidence) + return ( + + + + + + + + + + + + ) + })} + +
TransaktionDatumBeloppVerifikationDatumMetodTräff
+ toggleMatchSelection(key)} + aria-label={`Tillämpa matchning för ${m.transaction_description}`} + /> + {m.transaction_description}{formatDate(m.transaction_date)}{formatAmount(m.transaction_amount)} + + {formatVoucher(m)} + + {m.entry_description} + {formatDate(m.entry_date)} + {METHOD_LABELS[m.method] || m.method} + + {badge.label} +
+
+
)} {dryRunResults && dryRunResults.length === 0 && ( - - - Inga automatiska matchningar hittades. - - +

+ Inga automatiska matchningar hittades. +

)} {/* Unmatched Transactions */} @@ -1278,230 +1263,208 @@ export function BankReconciliationView({ periodId, periodBounds }: BankReconcili {/* Unmatched GL Lines */} {unmatchedGlLines.length > 0 && ( - - - - Omatchade verifikationer på ({unmatchedGlLines.length}) - - - -

- Är en manuellt eller importerat bokförd verifikation egentligen en ingående balans? Markera den som IB: då räknas den inte med i avstämningen utan visas separat som ingående balans. -

-
- - - - - - - - - - - - - {unmatchedGlLines.map((line) => { - const amount = line.debit_amount > 0 ? line.debit_amount : -line.credit_amount - const isRetaggable = line.source_type === 'manual' || line.source_type === 'import' - return ( - - + + ) + })} + +
Ver.nrDatumBeskrivningBeloppTyp
- +

+ Omatchade verifikationer på ({unmatchedGlLines.length}) +

+
+ + + + + + + + + + + + + {unmatchedGlLines.map((line) => { + const amount = line.debit_amount > 0 ? line.debit_amount : -line.credit_amount + const isRetaggable = line.source_type === 'manual' || line.source_type === 'import' + return ( + + + + + + + - - - - - - - ) - })} - -
Ver.nrDatumBeskrivningBeloppTyp
+ + {formatVoucher(line)} + + {formatDate(line.entry_date)} + {line.line_description || line.entry_description} + + {formatCurrency(amount)} + + {SOURCE_TYPE_LABELS[line.source_type] ?? line.source_type} + + {isRetaggable && ( + {formatDate(line.entry_date)} - {line.line_description || line.entry_description} - - {formatCurrency(amount)} - - {SOURCE_TYPE_LABELS[line.source_type] ?? line.source_type} - - {isRetaggable && ( - - )} -
-
- - + {markLoading === line.journal_entry_id ? 'Markerar…' : 'Märk som IB'} + + )} +
+
+ )} {/* Ignored transactions (undo) */} {ignoredTx.length > 0 && ( - - + {showIgnored && ( - -

- Rader du valt att dölja från avstämningen. De påverkar inte saldot på : de är bara gömda från listan. -

-
- - - - - - - - +
+
DatumBeskrivningValutaBelopp
+ + + + + + + + + + + {ignoredTx.map((tx) => ( + + + + + + - - - {ignoredTx.map((tx) => ( - - - - - - - - ))} - -
DatumBeskrivningValutaBelopp
{formatDate(tx.date)}{tx.description}{tx.currency} + {formatCurrency(tx.amount, tx.currency)} + + +
{formatDate(tx.date)}{tx.description} - {tx.currency} - - {formatCurrency(tx.amount, tx.currency)} - - -
-
-
+ ))} + + +
)} - + )} {/* Recently Matched */} {matchedTx.length > 0 && ( - - + {showMatched && ( - -
- - - - - - - - - +
+
DatumBeskrivningBeloppMetodVerifikation
+ + + + + + + + + + + + {matchedTx.map((tx) => ( + + + + + + + - - - {matchedTx.map((tx) => ( - - - - - - - - - ))} - -
DatumBeskrivningBeloppMetodVerifikation
{formatDate(tx.date)}{tx.description} + {formatCurrency(tx.amount, accountCurrency)} + + {tx.reconciliation_method + ? METHOD_LABELS[tx.reconciliation_method] || tx.reconciliation_method + : null} + + {tx.journal_entry_id && ( + + Öppna verifikat + + )} + + {tx.reconciliation_method && ( + + )} +
{formatDate(tx.date)}{tx.description} - {formatCurrency(tx.amount, accountCurrency)} - - {tx.reconciliation_method && ( - - {METHOD_LABELS[tx.reconciliation_method] || tx.reconciliation_method} - - )} - - {tx.journal_entry_id && ( - - Öppna verifikat - - )} - - {tx.reconciliation_method && ( - - )} -
-
-
+ ))} + + +
)} -
+ )} {/* Empty state */} {unmatchedTx.length === 0 && glLines.length === 0 && matchedTx.length === 0 && ignoredTx.length === 0 && !loading && ( - - - Inga transaktioner eller verifikationer att stämma av. - - + )} diff --git a/components/reports/FocusedReport.tsx b/components/reports/FocusedReport.tsx index 7c7b56ca..898edcc6 100644 --- a/components/reports/FocusedReport.tsx +++ b/components/reports/FocusedReport.tsx @@ -7,6 +7,7 @@ import { useRouter, useSearchParams } from 'next/navigation' import { useTranslations } from 'next-intl' import { ChevronLeft } from 'lucide-react' import { PageHeader } from '@/components/ui/page-header' +import { HelpPopover } from '@/components/ui/help-popover' import { EmptyState } from '@/components/ui/empty-state' import { Card, CardContent } from '@/components/ui/card' import { Skeleton } from '@/components/ui/skeleton' @@ -119,6 +120,20 @@ function FocusedReportInner({ {!isStandalone && ( +
+

{t('help_bank_reconciliation_scope')}

+

{t('help_bank_reconciliation_preview')}

+

{t('help_bank_reconciliation_ib')}

+

{t('help_bank_reconciliation_ignored')}

+
+ + ) : undefined + } action={ + + {[1, 2, 3, 4, 5, 6].map((i) => ( + + ))} + + + ) +} + +function ReportErrorCard({ message }: { message: string }) { + return ( + + + + {message} + + + ) +} + +function ReportEmptyState({ title, description }: { title: string; description: string }) { + return +} + function reportQuery( periodId: string, range?: DateRangeValue, @@ -132,35 +162,19 @@ export function TrialBalanceView({ periodId, onNavigateToAccount }: { periodId: }, [periodId]) if (loading) { - return ( - - - {[1, 2, 3, 4, 5, 6].map((i) => ( - - ))} - - - ) + return } if (error) { - return ( - - - - {error} - - - ) + return } if (!data || data.rows.length === 0) { return ( - - - Inga bokförda verifikationer i denna period. - - + ) } @@ -195,30 +209,38 @@ export function TrialBalanceView({ periodId, onNavigateToAccount }: { periodId:
Saldobalans
-
+
{data.isBalanced ? ( - Balanserad + Balanserad ) : ( Ej balanserad )} @@ -471,35 +493,19 @@ export function IncomeStatementView({ periodId, dateRange, dimensionFilter = nul }, [periodId, reportQs]) if (loading) { - return ( - - - {[1, 2, 3, 4, 5, 6].map((i) => ( - - ))} - - - ) + return } if (error) { - return ( - - - - {error} - - - ) + return } if (!data) { return ( - - - Ingen data för denna period. - - + ) } @@ -521,12 +527,12 @@ export function IncomeStatementView({ periodId, dateRange, dimensionFilter = nul Rörelseintäkter - - -
- Summa rörelseintäkter - {formatAmount(data.total_revenue)} kr -
+ + @@ -535,24 +541,23 @@ export function IncomeStatementView({ periodId, dateRange, dimensionFilter = nul Rörelsekostnader - - -
- Summa rörelsekostnader - -{formatAmount(data.total_expenses)} kr -
+ + {/* Operating result */} - -
- Rörelseresultat - = 0 ? 'text-success' : 'text-destructive'}> - {formatAmount(data.total_revenue - data.total_expenses)} kr - -
+ + Rörelseresultat + = 0 ? 'text-success' : 'text-destructive'}`}> + {formatAmount(data.total_revenue - data.total_expenses)} kr +
@@ -562,25 +567,23 @@ export function IncomeStatementView({ periodId, dateRange, dimensionFilter = nul Finansiella poster - - -
- Summa finansiella poster - {formatAmount(data.total_financial)} kr -
+ + )} {/* Net result */} - - -
- Årets resultat - = 0 ? 'text-success' : 'text-destructive'}> - {formatAmount(data.net_result)} kr - -
+ + + Årets resultat + = 0 ? 'text-success' : 'text-destructive'}`}> + {formatAmount(data.net_result)} kr +
@@ -616,33 +619,19 @@ export function BalanceSheetView({ periodId, dateRange, onNavigateToAccount }: { }, [periodId, reportQs]) if (loading) { - return ( - - - Laddar balansräkning... - - - ) + return } if (error) { - return ( - - - - {error} - - - ) + return } if (!data) { return ( - - - Ingen data för denna period. - - + ) } @@ -662,12 +651,12 @@ export function BalanceSheetView({ periodId, dateRange, onNavigateToAccount }: { Tillgångar - - -
- Summa tillgångar - {formatAmount(data.total_assets)} kr -
+ + @@ -676,30 +665,26 @@ export function BalanceSheetView({ periodId, dateRange, onNavigateToAccount }: { Eget kapital och skulder - - -
- Summa eget kapital och skulder - {formatAmount(data.total_equity_liabilities)} kr -
+ + {/* Balance check */} - - + +
- Balanscheck + Balanscheck {isBalanced ? ( - - Balanserar - + Balanserar ) : (
- - Balanserar ej - -

+ Balanserar ej +

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

@@ -747,35 +732,19 @@ export function ResultatrapportView({ periodId, dateRange, dimensionFilter = nul }, [periodId, reportQs]) if (loading) { - return ( - - - {[1, 2, 3, 4, 5, 6].map((i) => ( - - ))} - - - ) + return } if (error) { - return ( - - - - {error} - - - ) + return } if (!data || data.groups.length === 0) { return ( - - - Inga bokförda intäkter eller kostnader i denna period. - - + ) } @@ -859,14 +828,14 @@ export function ResultatrapportView({ periodId, dateRange, dimensionFilter = nul - - + +
- Beräknat resultat - = 0 ? 'text-success' : 'text-destructive'}`}> + Beräknat resultat + = 0 ? 'text-success' : 'text-destructive'}`}> {formatAmount(data.net_result_current)} kr - + {hasPrior ? `${formatAmount(data.net_result_prior)} kr` : '-'}
@@ -907,33 +876,19 @@ export function BalansrapportView({ periodId, dateRange, onNavigateToAccount }: }, [periodId, reportQs]) if (loading) { - return ( - - - Laddar balansrapport... - - - ) + return } if (error) { - return ( - - - - {error} - - - ) + return } if (!data || data.groups.length === 0) { return ( - - - Inga balansposter i denna period. - - + ) } @@ -1005,8 +960,8 @@ export function BalansrapportView({ periodId, dateRange, onNavigateToAccount }:
- - + +
Summa tillgångar {formatAmount(data.total_assets_ub)} kr @@ -1020,18 +975,14 @@ export function BalansrapportView({ periodId, dateRange, onNavigateToAccount }: {formatAmount(data.beraknat_resultat)} kr
- Balanscheck + Balanscheck {data.is_balanced ? ( - - Balanserar - + Balanserar ) : (
- - Balanserar ej - + Balanserar ej {data.imbalance_diagnosis && ( -

+

Differens: {formatAmount(Math.abs(data.imbalance_diagnosis.differens))} kr

)} @@ -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

Inga poster.

+ return

Inga poster.

} + // 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 ( -
- {sections.map((section) => ( -
-

{section.title}

-
- +
+
+ + {sections.map((section) => ( + + + + {section.rows.map((row) => ( onNavigateToAccount(row.account_number) : undefined} > - - - + + ))} - -
+ {section.title} +
{row.account_name} - {negate ? `-${formatAmount(row.amount)}` : formatAmount(row.amount)} kr + {row.account_name} + {fmt(row.amount, negate)} kr
-
- {section.title} - - {negate ? `-${formatAmount(section.subtotal)}` : formatAmount(section.subtotal)} kr - -
-
- ))} + + Summa + {fmt(section.subtotal, negate)} kr + + + ))} + + {footer && ( + + + {footer.label} + {fmt(footer.amount, footer.negate)} kr + + + )} +
) } @@ -2524,33 +2494,19 @@ export function SupplierLedgerView({ periodId }: { periodId: string }) { }, [periodId, asOfDate]) if (loading) { - return ( - - - Laddar leverantörsreskontra... - - - ) + return } if (error) { - return ( - - - - {error} - - - ) + return } if (!data || !data.ledger) { return ( - - - Ingen data tillgänglig. - - + ) } @@ -2642,7 +2598,7 @@ export function SupplierLedgerView({ periodId }: { periodId: string }) { {/* Reconciliation */} {reconciliation && ( - + Avstämning mot @@ -2650,21 +2606,21 @@ export function SupplierLedgerView({ periodId }: { periodId: string }) {
Leverantörsreskontra (summa utestående) - {formatAmount(reconciliation.supplier_ledger_total)} kr + {formatAmount(reconciliation.supplier_ledger_total)} kr
saldo (huvudbok) - {formatAmount(reconciliation.account_2440_balance)} kr + {formatAmount(reconciliation.account_2440_balance)} kr
Differens - + {formatAmount(reconciliation.difference)} kr
{reconciliation.is_reconciled ? ( - Avstämd + Avstämd ) : ( Ej avstämd - kontrollera bokföring )} @@ -2810,73 +2766,57 @@ export function GeneralLedgerView({ periodId, initialAccountFilter, dimensionFil }, [periodId, initialAccountFilter, dimensionFilter, dateRange]) if (loading) { - return ( - - - Laddar huvudbok... - - - ) + return } if (error) { - return ( - - - - {error} - - - ) + return } 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" - /> -
- -
-
-
+ {/* Account range filter: flat toolbar on the panel, no box */} +
+
+ + setAccountFrom(e.target.value)} + placeholder="t.ex. 1510" + className="mt-1 w-32" + /> +
+
+ + setAccountTo(e.target.value)} + placeholder="t.ex. 1519" + className="mt-1 w-32" + /> +
+ +
{data.period.start && ( -

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

+ Period {data.period.start} till {data.period.end} · {data.accounts.length} konton

)} @@ -2933,16 +2873,16 @@ export function GeneralLedgerView({ periodId, initialAccountFilter, dimensionFil {line.credit > 0 ? formatAmount(line.credit) : ''} - {formatAmount(line.balance)} + {formatAmount(line.balance)} ))} Summa / Utgående balans - {formatAmount(account.total_debit)} - {formatAmount(account.total_credit)} - {formatAmount(account.closing_balance)} + {formatAmount(account.total_debit)} + {formatAmount(account.total_credit)} + {formatAmount(account.closing_balance)}
@@ -3022,33 +2962,19 @@ export function JournalRegisterView({ periodId }: { periodId: string }) { } if (loading) { - return ( - - - Laddar grundbok... - - - ) + return } if (error) { - return ( - - - - {error} - - - ) + return } if (!data || data.entries.length === 0) { return ( - - - Inga bokförda verifikationer i denna period. - - + ) } @@ -3056,8 +2982,8 @@ export function JournalRegisterView({ periodId }: { periodId: string }) {
{data.period.start && ( -

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

+ Period {data.period.start} till {data.period.end} · {data.total_entries} verifikationer

)} @@ -3324,33 +3250,19 @@ export function ARLedgerView({ periodId }: { periodId: string }) { } if (loading) { - return ( - - - Laddar kundreskontra... - - - ) + return } if (error) { - return ( - - - - {error} - - - ) + return } if (!data || !data.ledger) { return ( - - - Ingen data tillgänglig. - - + ) } @@ -3471,7 +3383,7 @@ export function ARLedgerView({ periodId }: { periodId: string }) { {/* Reconciliation */} {reconciliation && ( - + Avstämning mot @@ -3479,21 +3391,21 @@ export function ARLedgerView({ periodId }: { periodId: string }) {
Kundreskontra (summa utestående) - {formatAmount(reconciliation.ar_ledger_total)} kr + {formatAmount(reconciliation.ar_ledger_total)} kr
Kundfordringar ( + ) saldo - {formatAmount(reconciliation.account_1510_balance)} kr + {formatAmount(reconciliation.account_1510_balance)} kr
Differens - + {formatAmount(reconciliation.difference)} kr
{reconciliation.is_reconciled ? ( - Avstämd + Avstämd ) : ( Ej avstämd - kontrollera bokföring )} @@ -3593,11 +3505,7 @@ export function DimensionPnlView({ periodId, dateRange }: { periodId: string; da return (
{pivotPicker} - - - Laddar rapport... - - +
) } @@ -3606,12 +3514,7 @@ export function DimensionPnlView({ periodId, dateRange }: { periodId: string; da return (
{pivotPicker} - - - - {error} - - +
) } @@ -3620,11 +3523,10 @@ export function DimensionPnlView({ periodId, dateRange }: { periodId: string; da return (
{pivotPicker} - - - Inga taggade intäkter eller kostnader i denna period. - - +
) } @@ -3696,20 +3598,20 @@ export function DimensionPnlView({ periodId, dateRange }: { periodId: string; da - - + +
- + ))} - diff --git a/components/salary/SalaryCalendar.tsx b/components/salary/SalaryCalendar.tsx index 6dd53bf8..24625864 100644 --- a/components/salary/SalaryCalendar.tsx +++ b/components/salary/SalaryCalendar.tsx @@ -29,6 +29,8 @@ import { } from 'lucide-react' import { useLocale, useTranslations } from 'next-intl' import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Textarea } from '@/components/ui/textarea' import { DestructiveConfirmDialog, useDestructiveConfirm, @@ -696,7 +698,7 @@ function BulkWorkedDialog({
- setHours(e.target.value)} autoFocus - className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm tabular-nums shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" + className="tabular-nums" />

{t('hours_zero_hint')} @@ -714,13 +716,13 @@ function BulkWorkedDialog({

-
Beräknat resultatBeräknat resultat {data.net_per_column.map((v, i) => ( - = 0 ? 'text-success' : 'text-destructive'}`}> + = 0 ? 'text-success' : 'text-destructive'}`}> {formatAmount(v)} = 0 ? 'text-success' : 'text-destructive'}`}> + = 0 ? 'text-success' : 'text-destructive'}`}> {formatAmount(data.net_total)} kr