diff --git a/DECISIONS.md b/DECISIONS.md index 79b5d3a0..3b829c5d 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1198,3 +1198,4 @@ One line per decision: `[YYYY-MM-DD] : `. Appended by agents and [2026-08-24] Reconciliation underlag (Reko bilagor, PR 2) is its own table (account_reconciliation_attachments) scoped by (company, account_key, through_date), not extra columns on document_attachments: that table's link is a verifikat and its WORM version chain is about digitized receipts, while a bilaga belongs to a balansdag and may be attached before the sign-off exists. Files stay in the `documents` bucket under `documents//reconciliation/...` so the bucket's company-scoped RLS applies unchanged; removal is a stamp (never a delete, BFL 7 kap.) enforced by trigger; the full archive copies the files into `bilagor/` with a hash manifest. No v1 API endpoints in this PR on purpose: the concurrent reconciliation-residual work edits the v1 route loader, spec snapshot and scopes, and files cannot be uploaded by an agent anyway. [2026-08-24] Bokslut checklist (Reko bilagor, PR 3) keeps the item catalogue in code and only the per-period state in bokslut_checklist_items: steps the system can judge (drafts, voucher gaps, trial balance, sign-offs through balansdagen, reskontra tie-outs) are computed live every time and a stored row only overrides them, so the checklist never claims a state the ledger contradicts; manual steps (inventering, osäkra fordringar, dispositioner) are what the konsult ticks. Mutable on purpose (a late verifikat reopens a step), no DELETE policy. The missing-fiscal-year check is a pure helper reused by the readiness warnings and the SIE import result; the non-adjacent previous_period_id fix is #1849 and is not duplicated here. [2026-08-24] Bokslutsbilagor pärm (Reko bilagor, PR 4) is generated from the sign-off rows, the trial balance through balansdagen and the attachment rows, never by recomputing each account's live status: the bilaga documents what was attested (numbers as they stood at sign-off, who, when, note) plus the files with their SHA-256, which is what a kvalitetskontroll reads. Whole period only (a bilaga is per balansdag), PDF-only export, written into every period folder of the full archive as JSON + PDF; an archive run has no acting user, so the checklist's readiness-derived items are left as stored there. +[2026-08-25] A period klarmarkerad as closed in a previous system (closed_externally) no longer trips the trial balance's "closed without closing_entry_id" guard for statutory pre-closing balances: its closing verifikat never existed in these books, so the booked balances are the pre-closing balances and there is nothing to strip. The guard stays for periods our own engine closed, where a missing link is a real inconsistency. Found by Väla Redovisning: Klarmarkera + Årsredovisning = 500. diff --git a/components/dashboard/DashboardNav.tsx b/components/dashboard/DashboardNav.tsx index b83b70f4..b8bdcce7 100644 --- a/components/dashboard/DashboardNav.tsx +++ b/components/dashboard/DashboardNav.tsx @@ -45,6 +45,7 @@ import { PanelLeftClose, Library, BookCheck, + FolderArchive, ShoppingCart, Car, } from 'lucide-react' @@ -134,6 +135,7 @@ type NavLabelKey = | 'deadlines' | 'periodiseringar' | 'year_end' + | 'bokslutsbilagor' | 'annual_report' | 'income_declaration' | 'help' @@ -247,6 +249,7 @@ const navItems: NavItem[] = [ { href: '/deadlines', labelKey: 'deadlines', icon: CalendarClock, group: 'skatt' }, { href: '/bookkeeping/periodiseringar', labelKey: 'periodiseringar', icon: CalendarRange, group: 'skatt', fold: 'bokslut' }, { href: '/bookkeeping/year-end', labelKey: 'year_end', icon: FileCheck, group: 'skatt', fold: 'bokslut' }, + { href: '/reports/bokslutsbilagor', labelKey: 'bokslutsbilagor', icon: FolderArchive, group: 'skatt', fold: 'bokslut' }, { href: '/bookkeeping/year-end/arsredovisning', labelKey: 'annual_report', icon: ScrollText, group: 'skatt', fold: 'bokslut', entityOnly: 'aktiebolag' }, { href: '/reports/ink2-declaration', labelKey: 'income_declaration', icon: FileSpreadsheet, group: 'skatt', fold: 'bokslut', entityOnly: 'aktiebolag' }, { href: '/reports/ne-declaration', labelKey: 'income_declaration', icon: FileSpreadsheet, group: 'skatt', fold: 'bokslut', entityOnly: 'enskild_firma' }, diff --git a/components/reconciliation/ReconciliationRail.tsx b/components/reconciliation/ReconciliationRail.tsx index 980d3c16..5b566870 100644 --- a/components/reconciliation/ReconciliationRail.tsx +++ b/components/reconciliation/ReconciliationRail.tsx @@ -1,8 +1,9 @@ 'use client' -import { Fragment } from 'react' +import { Fragment, useState } from 'react' import Image from 'next/image' import { useTranslations } from 'next-intl' +import { ChevronDown, ChevronRight } from 'lucide-react' import { cn, formatDate } from '@/lib/utils' import type { ReconciliationAccount } from '@/lib/reconciliation/schemas' @@ -63,10 +64,38 @@ interface ReconciliationRailProps { onSelect: (accountKey: string) => void } +const MANUAL_OPEN_STORAGE_KEY = 'Accounted:recon-rail-manual-open' + export function ReconciliationRail({ accounts, selectedKey, onSelect }: ReconciliationRailProps) { const t = useTranslations('reconciliation') const fed = accounts.filter((a) => a.kind !== 'manual') const manual = accounts.filter((a) => a.kind === 'manual') + const selectedIsManual = selectedKey?.startsWith('manual:') ?? false + + // The manual group folds: a migrated company has twenty-odd balance + // accounts, and the bank rows must stay in view. It opens when a manual + // account is selected and otherwise remembers the last choice per browser. + const [manualOpen, setManualOpen] = useState(() => { + if (selectedIsManual) return true + try { + return window.localStorage.getItem(MANUAL_OPEN_STORAGE_KEY) === '1' + } catch { + return false + } + }) + // Derived, not synced: the selected row must stay visible. + const showManual = manualOpen || selectedIsManual + const toggleManual = () => { + setManualOpen((open) => { + try { + window.localStorage.setItem(MANUAL_OPEN_STORAGE_KEY, open ? '0' : '1') + } catch { + // Per-browser convenience only. + } + return !open + }) + } + const manualUnsigned = manual.filter((a) => a.status?.state !== 'reconciled').length const stateLabel = (account: ReconciliationAccount): string => { const state = account.status?.state ?? 'unknown' @@ -150,15 +179,31 @@ export function ReconciliationRail({ accounts, selectedKey, onSelect }: Reconcil {fed.map(renderRow)} {manual.length > 0 && ( - {fed.length > 0 && ( -
  • 0 && 'mt-3')}> +
  • + {showManual ? ( +