feat(reports): bokslutsbilagor, the pärm per räkenskapsår (Reko bilagor, PR 4) (#1874)

* feat(reports): bokslutsbilagor, the pärm per räkenskapsår (Reko bilagor, PR 4)

One bilaga per balance account as of the balansdag: IB, movement and UB
from the trial balance, what it was reconciled against, the difference,
the sign-off with who, when and note, and every attached file with its
SHA-256; the closing checklist as the first page. JSON and PDF through
/api/reports/bokslutsbilagor, in the reports library and on the
Avstämning page, and written into every period folder of the full
archive. Built from the attested rows, never by recomputing live status.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvFveUpbdPBXdm7f5FEYoz

* fix(reports): load the pärm renderer on demand in the full archive so PDF stubs elsewhere keep working

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvFveUpbdPBXdm7f5FEYoz

* fix(reconciliation): neutral rail dot for a manual account that is merely not attested yet

An unsigned manual account without a system specification has nothing to
compare against, so an amber dot read as a problem on every balance account
of a freshly migrated company. Neutral until it is signed or a
specification differs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvFveUpbdPBXdm7f5FEYoz

---------

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-25 09:35:02 +02:00
committed by GitHub
co-authored by Claude Fable 5 Jakob Wennberg
parent c62321988b
commit 9ce1ebc65f
17 changed files with 1257 additions and 1 deletions
@@ -81,9 +81,17 @@ export function ReconciliationRail({ accounts, selectedKey, onSelect }: Reconcil
return synced ? t('rail_synced', { date: formatDate(synced) }) : t('rail_never_synced')
}
// A manual account with nothing to compare against is not a problem, just
// not attested yet: neutral until it is signed or a specification differs.
const dotState = (account: ReconciliationAccount): keyof typeof DOT_CLASS => {
const state = account.status?.state ?? 'unknown'
if (account.kind === 'manual' && state === 'open' && account.status?.unexplained_difference == null) return 'unknown'
return state
}
const renderRow = (account: ReconciliationAccount) => {
const selected = account.account_key === selectedKey
const state = account.status?.state ?? 'unknown'
const state = dotState(account)
const open = account.status
? account.status.open_counts.proposed +
account.status.open_counts.unmatched_external +
@@ -1,9 +1,12 @@
'use client'
import { useCallback, useEffect, useMemo, useState } from 'react'
import Link from 'next/link'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import { useTranslations } from 'next-intl'
import { Scale } from 'lucide-react'
import { QUIET_LINK_CLASS } from '@/components/ui/dry-table'
import { cn } from '@/lib/utils'
import { PageHeader } from '@/components/ui/page-header'
import { HelpPopover } from '@/components/ui/help-popover'
import { EmptyState } from '@/components/ui/empty-state'
@@ -41,6 +44,7 @@ interface ReconciliationWorkspaceProps {
export function ReconciliationWorkspace({ initialPeriods, initialCompanyId }: ReconciliationWorkspaceProps) {
const t = useTranslations('reconciliation')
const tParm = useTranslations('bokslutsbilagor')
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
@@ -120,6 +124,9 @@ export function ReconciliationWorkspace({ initialPeriods, initialCompanyId }: Re
}
action={
<div className="flex flex-wrap items-center justify-end gap-2">
<Link href="/reports/bokslutsbilagor" className={cn(QUIET_LINK_CLASS, 'mr-2')}>
{tParm('open_parm')}
</Link>
<SegmentedControl
value={mode}
onChange={setMode}