feat(reconciliation,bokslut): underlag on a balansdag + persisted closing checklist (Reko bilagor, PR 2 + PR 3) (#1873)
* feat(reconciliation): underlag on a balansdag, the files behind a sign-off (Reko bilagor, PR 2) A konsult attaches the kontoutdrag, engagemangsbesked or reskontralista an account was reconciled against to (account_key, through_date), before or after the sign-off, from every account body on the Avstämning page. Rows live in account_reconciliation_attachments (append-only, removal stamp by trigger, RLS like account_reconciliations), bytes in the documents bucket under the company prefix so its RLS applies unchanged, and the full archive copies them into bilagor/ with a hash manifest. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvFveUpbdPBXdm7f5FEYoz * fix(reconciliation): literal selects and payload in the attachments store so the phantom-column scanner can read them Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvFveUpbdPBXdm7f5FEYoz * feat(bokslut): persisted closing checklist and missing-fiscal-year warning (Reko bilagor, PR 3) (#1867) * feat(bokslut): persisted closing checklist and missing-fiscal-year warning (Reko bilagor, PR 3) The bokslut checklist is a catalogue in code with one state row per period (bokslut_checklist_items): the steps the system can judge (sign-offs through balansdagen, reskontra tie-outs, drafts, voucher gaps, trial balance) are computed live and a stored row only overrides them; the manual steps are the konsult's ticks, with who and when. It sits on the wizard's Kontroll step and is dumped into the full archive. A hole between fiscal years (one-file SIE migrations) is now named on the bokslut readiness screen and on the import result screen, where the next file is one click away. Non-adjacent period links are #1849's fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvFveUpbdPBXdm7f5FEYoz * fix(bokslut): count unexplained voucher gaps, literal select and payload for the checklist store 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> --------- 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:
co-authored by
Claude Fable 5
Jakob Wennberg
parent
ebbdf96b74
commit
c62321988b
@@ -0,0 +1,61 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { AlertCircle } from 'lucide-react'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { QUIET_LINK_CLASS } from '@/components/ui/dry-table'
|
||||
import { findFiscalYearGaps, type FiscalYearGap, type PeriodLike } from '@/lib/bookkeeping/fiscal-year-gaps'
|
||||
|
||||
/**
|
||||
* After a SIE import: the years the company now has, with any hole between
|
||||
* them called out. Fortnox and friends export one räkenskapsår per file, so
|
||||
* "import the next file" is the fix and this is the moment to say it.
|
||||
* Renders nothing while loading, on failure, or when the chain is whole.
|
||||
*/
|
||||
export function FiscalYearGapNotice() {
|
||||
const t = useTranslations('fiscal_year_gaps')
|
||||
const [gaps, setGaps] = useState<FiscalYearGap[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
fetch('/api/bookkeeping/fiscal-periods')
|
||||
.then(async (res) => {
|
||||
if (!res.ok) return
|
||||
const json = await res.json()
|
||||
const periods = (Array.isArray(json) ? json : (json.data ?? [])) as PeriodLike[]
|
||||
if (!cancelled) setGaps(findFiscalYearGaps(periods))
|
||||
})
|
||||
.catch(() => {
|
||||
// Advisory only.
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (gaps.length === 0) return null
|
||||
|
||||
return (
|
||||
<Card className="border-attn/50">
|
||||
<CardContent className="flex items-start gap-3 pt-6">
|
||||
<AlertCircle className="mt-0.5 h-5 w-5 shrink-0 text-attn" aria-hidden="true" />
|
||||
<div className="space-y-1 text-[13px] leading-5">
|
||||
<p className="font-medium">{t('title', { count: gaps.length })}</p>
|
||||
{gaps.map((gap) => (
|
||||
<p key={`${gap.missing_from}-${gap.missing_to}`} className="text-muted-foreground tabular-nums">
|
||||
{t('gap', { from: gap.missing_from, to: gap.missing_to, after: gap.after.name, before: gap.before.name })}
|
||||
</p>
|
||||
))}
|
||||
<p className="text-muted-foreground">
|
||||
{t('hint')}{' '}
|
||||
<Link href="/settings/bookkeeping" className={QUIET_LINK_CLASS}>
|
||||
{t('manage')}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import { formatCurrency } from '@/lib/utils'
|
||||
import { useCompany } from '@/contexts/CompanyContext'
|
||||
import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions'
|
||||
import TheaterCanvas from '@/components/import/TheaterCanvas'
|
||||
import { FiscalYearGapNotice } from '@/components/import/FiscalYearGapNotice'
|
||||
import type { ImportPreview, ImportResult } from '@/lib/import/types'
|
||||
import type { TheaterModel } from '@/lib/import/theater-model'
|
||||
|
||||
@@ -174,6 +175,9 @@ export default function ImportResultStep({
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* A year missing between the imported ones: say so here, where the next file is one click away. */}
|
||||
{result.success && <FiscalYearGapNotice />}
|
||||
|
||||
{/* IB resync notice (prior-year backfill) */}
|
||||
{result.success && result.nextPeriodIBResync && (
|
||||
<Card className="border-success/50">
|
||||
|
||||
Reference in New Issue
Block a user