Files
accounted/components/bookkeeping/year-end/ResultStep.tsx
T
Jakob WennbergandClaude Sonnet 5 ec27228a8e style: remove em/en dashes repo-wide, add CLAUDE.md rule against them (#890)
Em dashes (—) and en dashes (–) had spread across comments, docs, tests,
and a few UI strings, reading as AI-generated boilerplate rather than
house style. Replaced each with punctuation matching its context: colon
for explanatory clauses, comma for asides, plain hyphen for numeric/legal
ranges (e.g. "21-23§"), "to"/"till" for date ranges, parentheses for
paired-dash asides. messages/en.json and messages/sv.json were fixed by
hand together to keep sv/en in sync.

Left untouched where the dash is the functional subject rather than
decorative punctuation: date-range-parser.ts's separator regex,
charset-repair.ts's CP1252 byte-mapping table (and its test), the SIE
encoding mojibake docs, generic-csv.ts's minus-sign normalizer, the
agent system-prompt files that already instruct against em dashes, and
a golden iXBRL test fixture compared byte-for-byte.

Also fixes two bugs surfaced along the way: an off-by-one in
ApiKeysPanel's scope-label split (a leftover from an earlier partial
pass), and a charset-repair test that had lost the literal en-dash it
exists to verify.

Regenerated the agent atom seed migration (skills:generate) since 27
SKILL.md files changed. Added a CLAUDE.md rule against em/en dashes,
with an explicit carve-out for the functional-dash cases above.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 15:58:06 +02:00

287 lines
11 KiB
TypeScript

'use client'
import { useMemo, useState } from 'react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Checkbox } from '@/components/ui/checkbox'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import { CheckCircle2, AlertTriangle } from 'lucide-react'
import Link from 'next/link'
import type { YearEndResult, ContinuityDiscrepancy } from '@/types'
import { formatCurrency } from '@/lib/utils'
import { formatVoucher } from '@/lib/bookkeeping/voucher-series-resolver'
interface ResultStepProps {
result: YearEndResult
}
const ORE_TOLERANCE = 0.005
export function ResultStep({ result }: ResultStepProps) {
const [acknowledged, setAcknowledged] = useState(false)
const continuity = result.continuity
const discrepancies = continuity?.discrepancies ?? []
// If the wizard reached ResultStep, executeYearEndClosing already enforced
// that no per-account diff exceeded ORE_TOLERANCE: but surface a panel
// grouped by BAS class so the user can confirm visually before leaving.
return (
<div className="space-y-6">
<Card>
<CardContent className="p-6 text-center space-y-4">
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-success/10">
<CheckCircle2 className="h-7 w-7 text-success" />
</div>
<h2 className="font-display text-2xl">Bokslutet är klart</h2>
<p className="text-muted-foreground">
Perioden är stängd och en ny räkenskapsperiod har skapats.
</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">Resultat</CardTitle>
</CardHeader>
<CardContent className="space-y-3 text-sm">
<ResultRow
label="Bokslutsverifikation"
value={formatVoucher(result.closingEntry)}
href={`/bookkeeping/${result.closingEntry.id}`}
/>
{result.revaluationEntry && (
<ResultRow
label="Kursrevaluering"
value={formatVoucher(result.revaluationEntry)}
href={`/bookkeeping/${result.revaluationEntry.id}`}
/>
)}
<ResultRow
label="Ingående balanser i ny period"
value={formatVoucher(result.openingBalanceEntry)}
href={`/bookkeeping/${result.openingBalanceEntry.id}`}
/>
{result.resultAppropriationEntry && (
<ResultRow
label="Omföring av föregående års resultat (2099 → 2098)"
value={formatVoucher(result.resultAppropriationEntry)}
href={`/bookkeeping/${result.resultAppropriationEntry.id}`}
/>
)}
<ResultRow label="Ny räkenskapsperiod" value={result.nextPeriod.name} />
</CardContent>
</Card>
{result.resultAppropriationFailed && (
<Card className="border-destructive/30 bg-destructive/5">
<CardContent className="p-4 flex items-start gap-3">
<AlertTriangle className="h-4 w-4 mt-0.5 text-destructive shrink-0" />
<p className="text-sm">
<span className="font-medium">
Omföringen av föregående års resultat (2099 → 2098) kunde inte bokföras.
</span>{' '}
Bokslutet och de ingående balanserna är klara, men konto 2099 “Årets
resultat” bär fortfarande föregående års resultat in i den nya perioden.
Det måste flyttas till 2098 innan balansräkningen stämmer. Kör om bokslutet
eller kontakta support: felet är loggat.
</p>
</CardContent>
</Card>
)}
{continuity && (
<ContinuityPanel
discrepancies={discrepancies}
checkedAccounts={continuity.checked_accounts}
/>
)}
<Card>
<CardContent className="p-6 space-y-4">
<label className="flex items-start gap-3 cursor-pointer">
<Checkbox
checked={acknowledged}
onCheckedChange={(v) => setAcknowledged(v === true)}
className="mt-0.5"
aria-label="Bekräfta bokslut"
/>
<span className="text-sm leading-relaxed">
Jag har granskat bokslutet och IB/UB-kontinuiteten ovan, och
bekräftar att alla balanskonton stämmer mot föregående periods
utgående balans.
</span>
</label>
<div className="flex flex-col sm:flex-row gap-3 sm:justify-end">
<Button variant="outline" asChild disabled={!acknowledged}>
<Link
href="/bookkeeping"
aria-disabled={!acknowledged}
tabIndex={acknowledged ? undefined : -1}
className={!acknowledged ? 'pointer-events-none opacity-50' : ''}
>
Till bokföringen
</Link>
</Button>
<Button variant="outline" asChild disabled={!acknowledged}>
<Link
href="/reports"
aria-disabled={!acknowledged}
tabIndex={acknowledged ? undefined : -1}
className={!acknowledged ? 'pointer-events-none opacity-50' : ''}
>
Generera rapporter
</Link>
</Button>
<Button asChild disabled={!acknowledged}>
<Link
href={`/bookkeeping/year-end/arsredovisning?period=${result.closingEntry.fiscal_period_id}`}
aria-disabled={!acknowledged}
tabIndex={acknowledged ? undefined : -1}
className={!acknowledged ? 'pointer-events-none opacity-50' : ''}
>
Skapa årsredovisning
</Link>
</Button>
</div>
</CardContent>
</Card>
</div>
)
}
function ResultRow({ label, value, href }: { label: string; value: string; href?: string }) {
return (
<div className="flex items-center justify-between border-b border-border last:border-b-0 pb-3 last:pb-0">
<span className="text-muted-foreground">{label}</span>
{href ? (
<Link href={href} className="font-medium tabular-nums text-primary hover:underline">
{value}
</Link>
) : (
<span className="font-medium tabular-nums">{value}</span>
)}
</div>
)
}
interface ContinuityPanelProps {
discrepancies: ContinuityDiscrepancy[]
checkedAccounts: number
}
function ContinuityPanel({ discrepancies, checkedAccounts }: ContinuityPanelProps) {
const grouped = useMemo(() => {
const byClass = new Map<number, ContinuityDiscrepancy[]>()
for (const d of discrepancies) {
const klass = parseInt(d.account_number[0]) || 0
if (klass !== 1 && klass !== 2) continue
const list = byClass.get(klass) ?? []
list.push(d)
byClass.set(klass, list)
}
return byClass
}, [discrepancies])
const hasIssues = discrepancies.some(
(d) => Math.abs(d.difference) > ORE_TOLERANCE
)
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0">
<CardTitle className="text-base">IB/UB-avstämning</CardTitle>
{hasIssues ? (
<Badge variant="destructive" className="gap-1">
<AlertTriangle className="h-3 w-3" />
Avvikelser
</Badge>
) : (
<Badge variant="success" className="gap-1">
<CheckCircle2 className="h-3 w-3" />
Stämmer
</Badge>
)}
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-muted-foreground">
{checkedAccounts} balanskonto(n) jämförda mellan utgående balans i
stängd period och ingående balans i ny period.
</p>
{discrepancies.length === 0 ? (
<p className="text-sm">
Inga avvikelser. Alla balanskonton i klass 1 och 2 matchar inom
tolerans (±0,005 SEK).
</p>
) : (
<div className="space-y-6">
{[1, 2].map((klass) => {
const rows = grouped.get(klass) ?? []
if (rows.length === 0) return null
return (
<div key={klass}>
<h3 className="text-sm font-medium uppercase tracking-wider text-muted-foreground mb-2">
Klass {klass}: {klass === 1 ? 'Tillgångar' : 'Skulder & eget kapital'}
</h3>
<Table>
<TableHeader>
<TableRow>
<TableHead>Konto</TableHead>
<TableHead className="text-right">UB (föregående)</TableHead>
<TableHead className="text-right">IB (ny period)</TableHead>
<TableHead className="text-right">Diff</TableHead>
<TableHead className="text-right">Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{rows.map((d) => {
const overTol = Math.abs(d.difference) > ORE_TOLERANCE
return (
<TableRow key={d.account_number}>
<TableCell className="font-medium tabular-nums">
{d.account_number}
<span className="ml-2 font-normal text-muted-foreground">
{d.account_name}
</span>
</TableCell>
<TableCell className="text-right tabular-nums">
{formatCurrency(d.previous_ub_net)}
</TableCell>
<TableCell className="text-right tabular-nums">
{formatCurrency(d.current_ib_net)}
</TableCell>
<TableCell className="text-right tabular-nums">
{formatCurrency(d.difference)}
</TableCell>
<TableCell className="text-right">
{overTol ? (
<Badge variant="destructive">Avviker</Badge>
) : (
<Badge variant="success">OK</Badge>
)}
</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
</div>
)
})}
</div>
)}
</CardContent>
</Card>
)
}