feat(bokslut): the bokslut trio in the flat concept language (scenes 34-36) (#1161)

* feat(bokslut): Arsbokslut hub as Stegen with de-boxed linear steps (trio 1/3)

Scene 34: the six-step wizard gets the house horizontal stepper (done
checks behind the current step, click-back navigation, forward stays
with each step's continue action), the period select moves into the
header as the context control, and the progress-bar and picker cards
die. Preflight, Preview, Execute and Result are de-boxed: sans eyebrow
sections with hairlines, quiet action links, attn lines instead of
boxed alerts, muted text for normal states. Step content is centered
at reading width. Accruals/Dispositions keep their internals for now
(interactive panels; follow-up pass). All wizard logic untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(bokslut): INK2 and NE-bilaga views in the flat house language (trio 2/3)

Scene 36 direction: the declaration views lose every card. Statutory
sections (Tillgangar, Eget kapital och skulder, Resultatrakning, INK2S,
Intakter, Kostnader) become sans eyebrow sections with hairlines, the
header card becomes a flat block with the SRU download beside it, the
filing instructions lose their info box, warnings render as attn lines,
and the NE R11 result becomes the emphasized document-foot row. All
ruta tables, SRU downloads and warnings logic untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(bokslut): Arsredovisning page, studio and digital filing in the flat house language (trio 3/3)

De-boxes the whole scene-35 surface: the page shell (period picker,
K3 note, narrativ, flerarsoversikt, underskrifter, PDF section), the
AnnualReportStudio (workflow strip, scope form, completeness checks,
versions) and DigitalInlamning (iXBRL review, submission form,
status history) all move from Card shells to sans-eyebrow sections
with hairlines. Inner info boxes flatten to bordered text blocks;
the digital-checks warning becomes an attn line. The iframe border
and the Kommer snart overlay sign stay: one frames an external
document, the other floats above blurred content.

Statutory Swedish-only surface: no new i18n keys.

* polish(bokslut): align trio controls with the house language

Founder feedback: buttons and selects still read old-style next to the
bookkeeping page. Sweeps all trio surfaces:

- Drop every min-h-11 override on Buttons and Inputs (44px chunky
  controls) so the compact house pill and h-10 input apply.
- Replace all native <select> elements (BooleanQuestion, currency,
  group size, signing method, signer role, versions, AGM outcome,
  roll) with the shadcn Select primitive used system-wide.
- Step navigation matches the merged Moms Stegen: white outline
  size-sm pills labelled 'Nästa: <steg> →' and '← Tillbaka'. Booking
  commits (Verkställ, Bokför valda dispositioner) stay primary.
- Year-end period picker becomes the ContextPicker chip (the house
  context-picker idiom), and the stepper row widens to max-w-4xl so
  step 6 'Klart' no longer clips.
- font-sans on two uppercase h3 eyebrows that rendered serif via the
  global h1-h3 display rule.

* fix(vat): one toolbar row and the FyPicker chip on every fiscal-year surface

The prod momsdeklaration (helårsmoms) broke into two sparse rows:
Exportera alone, then a labelled 280px FiscalYearSelector below it,
plus a serif uppercase worklist heading and internal check codes in
the UI.

- The VAT toolbar is one flat row: periodicity picker, fiscal-year
  chip, black Exportera, all h-9 aligned.
- FyPicker (the rounded ContextPicker chip from UI-migration PR 3)
  replaces FiscalYearSelector on every page-level surface it had
  left: the VAT toolbar, FocusedReport's header (all report detail
  pages) and Kassaflödesanalys. Dialog/settings forms keep the
  labelled select, which is a form field, not a context picker.
- 'Verifikationer som saknar basbelopp' becomes a sans eyebrow with
  hairline; internal codes (RC_BASIS_MISSING et al) no longer render
  in check rows, the Swedish message already cites the SKV felkod.
- font-sans on the SkatteverketPanel validation eyebrow.

* polish(vat): periodicity, year and quarter/month pickers as ContextPicker chips

Founder feedback: the Arsvis dropdown should also be the rounded
style. The cadence choice now lives behind a settings-style 'Period'
chip (Manadsvis/Kvartalsvis/Arsvis with a check on the active one),
and the year and quarter/month selects become chips too, so the
whole momsdeklaration toolbar is chip-shaped: Period, 2026,
Kvartal 3 (jul-sep) or Rakenskapsar 2026, then the black Exportera
pill. The concrete period chip already implies the cadence, so the
Period chip can stay label-only.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-07-24 20:20:00 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent 49e86e2e67
commit 98e1a48c2b
17 changed files with 778 additions and 824 deletions
@@ -4,7 +4,6 @@ import { useCallback, useEffect, useState } from 'react'
import Link from 'next/link'
import { useRouter, useSearchParams } from 'next/navigation'
import { useTranslations } from 'next-intl'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Skeleton } from '@/components/ui/skeleton'
@@ -12,12 +11,19 @@ import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { Checkbox } from '@/components/ui/checkbox'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { PageHeader } from '@/components/ui/page-header'
import { ArrowLeft, FileDown, Plus, ExternalLink, Loader2, Save, CheckCircle2, Trash2 } from 'lucide-react'
import { useToast } from '@/components/ui/use-toast'
import { formatCurrency } from '@/lib/utils'
import { FiscalYearSelector } from '@/components/common/FiscalYearSelector'
import { FyPicker } from '@/components/common/FyPicker'
import { DigitalInlamning, INLAMNING_COMING_SOON } from '@/components/bokslut/DigitalInlamning'
import { AnnualReportStudio } from '@/components/bokslut/AnnualReportStudio'
import type { ArsredovisningData } from '@/lib/bokslut/arsredovisning/types'
@@ -452,10 +458,9 @@ export default function ArsredovisningPage() {
title="Årsredovisning"
description="Förhandsgranska och ladda ner årsredovisningen för valt räkenskapsår."
/>
<Card>
<CardHeader>
<CardTitle className="text-base">Välj räkenskapsår</CardTitle>
<p className="text-sm text-muted-foreground">
<div className="max-w-xl px-1">
<h3 className="font-sans text-sm font-medium">Välj räkenskapsår</h3>
<p className="mt-1 text-sm text-muted-foreground">
Välj det räkenskapsår du vill se årsredovisningen för. Du kan
förhandsgranska och ladda ner PDF-utkastet utan att stänga året: det
fullständiga bokslutet görs sedan via{' '}
@@ -464,19 +469,17 @@ export default function ArsredovisningPage() {
</Link>
.
</p>
</CardHeader>
<CardContent>
<FiscalYearSelector
<div className="mt-4">
<FyPicker
value={null}
onChange={(id) => {
if (id) router.replace(`/bookkeeping/year-end/arsredovisning?period=${id}`)
}}
includeAllOption={false}
hideFuturePeriods
label={null}
/>
</CardContent>
</Card>
</div>
</div>
</div>
)
}
@@ -485,12 +488,10 @@ export default function ArsredovisningPage() {
return (
<div className="space-y-8">
<PageHeader title="Årsredovisning" />
<Card>
<CardContent className="p-6 space-y-3">
<Skeleton className="h-6 w-1/3" />
<Skeleton className="h-32 w-full" />
</CardContent>
</Card>
<div className="space-y-3">
<Skeleton className="h-6 w-1/3" />
<Skeleton className="h-32 w-full" />
</div>
</div>
)
}
@@ -499,11 +500,7 @@ export default function ArsredovisningPage() {
return (
<div className="space-y-8">
<PageHeader title="Årsredovisning" />
<Card>
<CardContent className="p-6 text-destructive">
{error ?? 'Kunde inte hämta data'}
</CardContent>
</Card>
<p className="py-4 text-sm text-destructive">{error ?? 'Kunde inte hämta data'}</p>
</div>
)
}
@@ -531,16 +528,14 @@ export default function ArsredovisningPage() {
/>
{data.accounting_framework === 'k3' && (
<Card>
<CardContent className="p-4 text-sm">
<p className="font-medium">Årsredovisning enligt K3 (BFNAR 2012:1)</p>
<p className="text-muted-foreground mt-1">
Dokumentet innehåller kassaflödesanalys, förändring av eget kapital och
utökade noter (uppskjuten skatt, redovisningsprinciper, materiella
anläggningstillgångar): krav som följer K3 men inte K2.
</p>
</CardContent>
</Card>
<div className="px-1 text-sm">
<p className="font-medium">Årsredovisning enligt K3 (BFNAR 2012:1)</p>
<p className="mt-1 text-xs leading-5 text-muted-foreground">
Dokumentet innehåller kassaflödesanalys, förändring av eget kapital och
utökade noter (uppskjuten skatt, redovisningsprinciper, materiella
anläggningstillgångar): krav som följer K3 men inte K2.
</p>
</div>
)}
<AnnualReportStudio
@@ -553,15 +548,16 @@ export default function ArsredovisningPage() {
onVersionsChanged={handleVersionsChanged}
/>
<Card>
<CardHeader>
<CardTitle className="text-base">Förvaltningsberättelse: narrativ</CardTitle>
<p className="text-sm text-muted-foreground">
<section>
<div className="mb-1 flex items-center gap-2 px-1">
<h3 className="font-sans text-xs font-medium uppercase tracking-wider text-muted-foreground">Förvaltningsberättelse: narrativ</h3>
<div className="h-px flex-1 bg-border/60" />
</div>
<p className="px-1 text-xs leading-5 text-muted-foreground">
Texten nedan visas i PDF:en. Klicka på <strong>Spara texten</strong> nedan
för att behålla ändringarna mellan sessioner.
</p>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-4 px-1 pt-4">
<div className="space-y-2">
<Label htmlFor="ar-description">Verksamhetsbeskrivning</Label>
<Textarea
@@ -597,7 +593,7 @@ export default function ArsredovisningPage() {
value={proposedDividend}
onChange={(event) => setProposedDividend(event.target.value)}
placeholder="0"
className="min-h-11 max-w-[220px] tabular-nums"
className="max-w-[220px] tabular-nums"
/>
<p className="text-xs text-muted-foreground">
Beloppet används i resultatdispositionen i samma version av PDF och iXBRL.
@@ -619,23 +615,22 @@ export default function ArsredovisningPage() {
</div>
<div className="space-y-2">
<Label htmlFor="ar-agm-outcome">Årsstämmans beslut om resultatdisposition</Label>
<select
id="ar-agm-outcome"
className="min-h-11 w-full max-w-xl rounded-md border border-border bg-background px-3 text-sm"
<Select
value={agmDispositionOutcome}
onChange={(event) =>
onValueChange={(next) =>
setAgmDispositionOutcome(
event.target.value as
| ''
| 'proposal_approved'
| 'alternative_decision',
next as 'proposal_approved' | 'alternative_decision',
)
}
>
<option value="">Välj efter genomförd årsstämma</option>
<option value="proposal_approved">Styrelsens förslag godkändes</option>
<option value="alternative_decision">Årsstämman fattade ett annat beslut</option>
</select>
<SelectTrigger id="ar-agm-outcome" className="max-w-xl">
<SelectValue placeholder="Välj efter genomförd årsstämma" />
</SelectTrigger>
<SelectContent>
<SelectItem value="proposal_approved">Styrelsens förslag godkändes</SelectItem>
<SelectItem value="alternative_decision">Årsstämman fattade ett annat beslut</SelectItem>
</SelectContent>
</Select>
</div>
{agmDispositionOutcome === 'alternative_decision' && (
<div className="space-y-2">
@@ -651,7 +646,7 @@ export default function ArsredovisningPage() {
<div className="pt-4 border-t border-border space-y-4">
<div>
<h3 className="text-sm font-medium uppercase tracking-wider text-muted-foreground">
<h3 className="font-sans text-xs font-medium uppercase tracking-wider text-muted-foreground">
Lagstadgade upplysningar
</h3>
<p className="text-xs text-muted-foreground mt-1">
@@ -675,7 +670,7 @@ export default function ArsredovisningPage() {
<p className="text-xs text-muted-foreground">
ÅRL 5:13 §. Lämna tomt om inga skulder förfaller senare än fem år.
</p>
<label className="flex min-h-11 cursor-pointer items-center gap-3 text-sm">
<label className="flex cursor-pointer items-center gap-3 text-sm">
<Checkbox
id="ar-ltd-confirmed"
checked={longTermDebtConfirmed}
@@ -694,7 +689,7 @@ export default function ArsredovisningPage() {
placeholder="t.ex. Företagsinteckning 500 000 kr som säkerhet för bankkredit."
/>
<p className="text-xs text-muted-foreground">ÅRL 5:14 §.</p>
<label className="flex min-h-11 cursor-pointer items-center gap-3 text-sm">
<label className="flex cursor-pointer items-center gap-3 text-sm">
<Checkbox
id="ar-securities-confirmed"
checked={securitiesPledgedConfirmed}
@@ -713,7 +708,7 @@ export default function ArsredovisningPage() {
placeholder="t.ex. Borgensåtagande för dotterbolags krediter 200 000 kr."
/>
<p className="text-xs text-muted-foreground">ÅRL 5:15 §.</p>
<label className="flex min-h-11 cursor-pointer items-center gap-3 text-sm">
<label className="flex cursor-pointer items-center gap-3 text-sm">
<Checkbox
id="ar-contingent-confirmed"
checked={contingentLiabilitiesConfirmed}
@@ -757,7 +752,7 @@ export default function ArsredovisningPage() {
/>
</div>
</div>
<label className="flex min-h-11 cursor-pointer items-center gap-3 text-sm">
<label className="flex cursor-pointer items-center gap-3 text-sm">
<Checkbox
id="ar-parent-confirmed"
checked={parentCompanyConfirmed}
@@ -794,14 +789,15 @@ export default function ArsredovisningPage() {
)}
</Button>
</div>
</CardContent>
</Card>
</div>
</section>
<Card>
<CardHeader>
<CardTitle className="text-base">Flerårsöversikt</CardTitle>
</CardHeader>
<CardContent>
<section>
<div className="mb-1 flex items-center gap-2 px-1">
<h3 className="font-sans text-xs font-medium uppercase tracking-wider text-muted-foreground">Flerårsöversikt</h3>
<div className="h-px flex-1 bg-border/60" />
</div>
<div className="px-1 pt-2">
<Table>
<TableHeader>
<TableRow>
@@ -830,64 +826,67 @@ export default function ArsredovisningPage() {
))}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
</section>
<Card>
<CardHeader>
<CardTitle className="text-base">Underskrifter</CardTitle>
<p className="text-sm text-muted-foreground">
<section>
<div className="mb-1 flex items-center gap-2 px-1">
<h3 className="font-sans text-xs font-medium uppercase tracking-wider text-muted-foreground">Underskrifter</h3>
<div className="h-px flex-1 bg-border/60" />
</div>
<p className="px-1 text-xs leading-5 text-muted-foreground">
Lägg till varje styrelseledamot och eventuell VD. Lås först en version i
arbetsflödet ovan. När originalet eller en extern e-signatur är klar registrerar
du datum och bevisreferens mot exakt den version som skrevs under.
</p>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4 rounded-md border border-border bg-muted/20 p-4 md:grid-cols-2">
<div className="space-y-4 px-1 pt-4">
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="signature-version">Låst version</Label>
<select
id="signature-version"
className="min-h-11 w-full rounded-md border border-border bg-background px-3 text-sm"
<Select
value={selectedSignatureVersionId}
onChange={(event) => setSelectedSignatureVersionId(event.target.value)}
onValueChange={setSelectedSignatureVersionId}
>
<option value="">Välj version</option>
{versions
.filter((version) => version.status === 'ready_for_signature')
.map((version) => (
<option key={version.id} value={version.id}>
Version {version.version_number}: {version.content_hash.slice(0, 12)}
</option>
))}
</select>
<SelectTrigger id="signature-version">
<SelectValue placeholder="Välj version" />
</SelectTrigger>
<SelectContent>
{versions
.filter((version) => version.status === 'ready_for_signature')
.map((version) => (
<SelectItem key={version.id} value={version.id}>
Version {version.version_number}: {version.content_hash.slice(0, 12)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="signature-method">Underskriftsmetod</Label>
<select
id="signature-method"
className="min-h-11 w-full rounded-md border border-border bg-background px-3 text-sm"
<Select
value={signingMethod}
onChange={(event) =>
onValueChange={(next) =>
setSigningMethod(
event.target.value as
| 'paper_original'
| 'advanced_e_signature'
| 'bankid',
next as 'paper_original' | 'advanced_e_signature' | 'bankid',
)
}
>
<option value="paper_original">Undertecknat original på papper</option>
<option value="advanced_e_signature">Avancerad e-signatur</option>
<option value="bankid">BankID via extern signeringstjänst</option>
</select>
<SelectTrigger id="signature-method">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="paper_original">Undertecknat original på papper</SelectItem>
<SelectItem value="advanced_e_signature">Avancerad e-signatur</SelectItem>
<SelectItem value="bankid">BankID via extern signeringstjänst</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="signature-date">Underskriftsdatum</Label>
<Input
id="signature-date"
type="date"
className="min-h-11"
value={signatureDate}
onChange={(event) => setSignatureDate(event.target.value)}
/>
@@ -896,7 +895,7 @@ export default function ArsredovisningPage() {
<Label htmlFor="signature-evidence">Bevisreferens</Label>
<Input
id="signature-evidence"
className="min-h-11"
value={signatureEvidence}
onChange={(event) => setSignatureEvidence(event.target.value)}
placeholder="archive:AR-2026-001"
@@ -935,7 +934,7 @@ export default function ArsredovisningPage() {
<Button
size="icon"
variant="ghost"
className="min-h-11 min-w-11"
className="min-w-11"
aria-label={tStudio('remove_signer')}
onClick={() => void handleRemoveSigner(sig.id)}
>
@@ -945,7 +944,7 @@ export default function ArsredovisningPage() {
<Button
size="sm"
variant="outline"
className="min-h-11"
onClick={() => void handleMarkSigned(sig.id)}
disabled={
!selectedSignatureVersionId ||
@@ -965,17 +964,17 @@ export default function ArsredovisningPage() {
<Label htmlFor="signer-role" className="text-xs">
Roll
</Label>
<select
id="signer-role"
className="min-h-11 rounded-md border border-border bg-background px-3 text-sm"
value={signerRole}
onChange={(e) => setSignerRole(e.target.value)}
>
<option>Styrelseledamot</option>
<option>Styrelseordförande</option>
<option>VD</option>
<option>Verkställande direktör</option>
</select>
<Select value={signerRole} onValueChange={setSignerRole}>
<SelectTrigger id="signer-role" className="sm:w-56">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="Styrelseledamot">Styrelseledamot</SelectItem>
<SelectItem value="Styrelseordförande">Styrelseordförande</SelectItem>
<SelectItem value="VD">VD</SelectItem>
<SelectItem value="Verkställande direktör">Verkställande direktör</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1 flex-1 min-w-[200px]">
<Label htmlFor="signer-name" className="text-xs">
@@ -986,21 +985,22 @@ export default function ArsredovisningPage() {
value={signerName}
onChange={(e) => setSignerName(e.target.value)}
placeholder="t.ex. Anna Andersson"
className="min-h-11"
/>
</div>
<Button className="min-h-11 w-full sm:w-auto" onClick={handleAddSigner} disabled={!signerName.trim()}>
<Button className="w-full sm:w-auto" onClick={handleAddSigner} disabled={!signerName.trim()}>
<Plus className="mr-1 h-4 w-4" /> Lägg till
</Button>
</div>
</CardContent>
</Card>
</div>
</section>
<Card>
<CardHeader>
<CardTitle className="text-base">PDF för pappersinlämning</CardTitle>
</CardHeader>
<CardContent className="space-y-4 text-sm">
<section>
<div className="mb-1 flex items-center gap-2 px-1">
<h3 className="font-sans text-xs font-medium uppercase tracking-wider text-muted-foreground">PDF för pappersinlämning</h3>
<div className="h-px flex-1 bg-border/60" />
</div>
<div className="space-y-4 px-1 pt-2 text-sm">
<p className="text-muted-foreground">
Ladda ner PDF-utkastet och granska det. För pappersinlämning ska
årsredovisningens original skrivas under av samtliga styrelseledamöter och
@@ -1008,7 +1008,7 @@ export default function ArsredovisningPage() {
till Bolagsverket. PDF-filen kan inte laddas upp som digital årsredovisning.
</p>
<div className="flex flex-wrap gap-3">
<Button className="min-h-11" asChild>
<Button asChild>
<Link href={pdfUrl} target="_blank" rel="noopener noreferrer">
<FileDown className="mr-2 h-4 w-4" /> Ladda ner PDF (utkast)
</Link>
@@ -1024,7 +1024,7 @@ export default function ArsredovisningPage() {
: undefined
}
>
<Button className="min-h-11" variant="outline" asChild>
<Button variant="outline" asChild>
<Link
href="https://bolagsverket.se/foretag/aktiebolag/arsredovisningforaktiebolag.759.html"
target="_blank"
@@ -1045,24 +1045,24 @@ export default function ArsredovisningPage() {
}
>
{data.warnings.length > 0 && (
<div className="rounded-md border border-border bg-muted/30 p-3 text-xs space-y-1">
<div className="space-y-1 border-t border-border/60 pt-3 text-xs">
<p className="font-medium">Innan inlämning till Bolagsverket:</p>
<ul className="list-disc pl-5 space-y-1">
<ul className="list-disc space-y-1 pl-5 text-muted-foreground">
{data.warnings.map((w, i) => (
<li key={i}>{w}</li>
))}
</ul>
</div>
)}
<div className="rounded-md border border-border bg-muted/30 p-3 text-xs">
<strong>Digital inlämning är frivillig.</strong> Den görs som iXBRL genom en
<p className="text-xs text-muted-foreground">
<strong className="text-foreground">Digital inlämning är frivillig.</strong> Den görs som iXBRL genom en
ansluten programvara. Accounteds direktinlämning förblir stängd tills avtal,
certifikat och Bolagsverkets acceptanstest är klara. PDF-flödet ovan är den
separata vägen för pappersinlämning.
</p>
</div>
</div>
</CardContent>
</Card>
</div>
</section>
{data.accounting_framework === 'k2' && periodId && (
<DigitalInlamning periodId={periodId} />
+80 -71
View File
@@ -1,21 +1,12 @@
'use client'
import { useCallback, useEffect, useMemo, useState } from 'react'
import Link from 'next/link'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import { Card, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Progress } from '@/components/ui/progress'
import { EmptyState } from '@/components/ui/empty-state'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { ContextPicker } from '@/components/common/ContextPicker'
import { Skeleton } from '@/components/ui/skeleton'
import { ArrowLeft, CalendarPlus, Lock } from 'lucide-react'
import { CalendarPlus, Check, Lock } from 'lucide-react'
import AgentSparkleButton from '@/components/agent/AgentSparkleButton'
import { cn } from '@/lib/utils'
import { useToast } from '@/components/ui/use-toast'
@@ -215,7 +206,6 @@ export default function YearEndPage() {
}, [selectedPeriodId, report?.period.name, toast])
const currentStepIndex = STEP_ORDER.indexOf(step)
const progressValue = ((currentStepIndex + 1) / STEP_ORDER.length) * 100
const showWizard = useMemo(
() => selectedPeriodId !== null && (periods?.length ?? 0) > 0,
@@ -226,19 +216,34 @@ export default function YearEndPage() {
<div className="space-y-8">
<div className="flex items-center justify-between gap-3 flex-wrap">
<h1 className="font-display text-2xl leading-8 tracking-tight">Årsbokslut</h1>
<div className="flex gap-2">
<div className="flex items-center gap-2">
<AgentSparkleButton
intentId="bokslut.step"
intentArgs={{ step_id: null }}
contextRef="bokslut:overview"
size="default"
/>
<Button variant="outline" asChild>
<Link href="/bookkeeping">
<ArrowLeft className="mr-2 h-4 w-4" />
Bokföring
</Link>
</Button>
{showWizard && periods && periods.length > 0 && step !== 'result' && (
<ContextPicker
items={periods.map((p) => ({
id: p.id,
label: p.name,
annotation: `${p.period_start} till ${p.period_end}`,
}))}
value={selectedPeriodId}
onChange={(value) => {
setSelectedPeriodId(value)
setStep('preflight')
setPreview(null)
setResult(null)
setExecuteError(null)
}}
triggerLabel={
periods.find((p) => p.id === selectedPeriodId)?.name ?? 'Välj period'
}
ariaLabel="Period"
/>
)}
</div>
</div>
@@ -275,66 +280,68 @@ export default function YearEndPage() {
)
)}
{showWizard && periods && periods.length > 0 && step !== 'result' && (
<Card>
<CardContent className="p-4 flex items-center gap-4">
<label className="text-sm font-medium shrink-0">Period</label>
<Select
value={selectedPeriodId ?? undefined}
onValueChange={(value) => {
setSelectedPeriodId(value)
setStep('preflight')
setPreview(null)
setResult(null)
setExecuteError(null)
}}
>
<SelectTrigger className="w-full max-w-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
{periods.map((p) => (
<SelectItem key={p.id} value={p.id}>
{p.name} ({p.period_start} till {p.period_end})
</SelectItem>
))}
</SelectContent>
</Select>
</CardContent>
</Card>
)}
{/* Stegen (concept scene 34): dots walk check -> filled -> number.
Steps behind the current one are clickable for going back; forward
navigation stays with each step's own continue action. */}
{showWizard && step !== 'result' && (
<Card>
<CardContent className="p-4 space-y-2">
<div className="flex justify-between text-sm">
<span className="sm:hidden text-primary font-medium">
Steg {currentStepIndex + 1}/{STEP_ORDER.length}: {STEP_LABELS[step]}
</span>
{STEP_ORDER.map((s, i) => (
<div
className="mx-auto flex w-full max-w-4xl items-center gap-3 overflow-x-auto px-1"
role="tablist"
aria-label="Bokslutets steg"
>
{STEP_ORDER.map((s, i) => (
<React.Fragment key={s}>
{i > 0 && <span className="h-px min-w-4 flex-1 bg-border" aria-hidden="true" />}
<button
type="button"
role="tab"
aria-selected={s === step}
disabled={i >= currentStepIndex}
onClick={() => {
if (i < currentStepIndex) setStep(s)
}}
className={cn(
'group flex shrink-0 items-center gap-2 text-left',
i >= currentStepIndex && 'cursor-default',
)}
>
<span
key={s}
className={cn(
'hidden sm:inline',
i <= currentStepIndex ? 'text-primary font-medium' : 'text-muted-foreground',
'flex h-7 w-7 shrink-0 items-center justify-center rounded-full border text-xs tabular-nums transition-colors duration-150',
i < currentStepIndex
? 'border-success/40 text-success'
: s === step
? 'border-foreground bg-foreground text-background'
: 'border-border text-muted-foreground',
)}
>
{i < currentStepIndex ? <Check className="h-3.5 w-3.5" aria-hidden="true" /> : i + 1}
</span>
<span
className={cn(
'text-[12.5px] transition-colors duration-150',
s === step
? 'font-medium'
: i < currentStepIndex
? 'text-muted-foreground group-hover:text-foreground'
: 'text-muted-foreground',
)}
>
{STEP_LABELS[s]}
</span>
))}
</div>
<Progress value={progressValue} className="h-2" />
</CardContent>
</Card>
</button>
</React.Fragment>
))}
</div>
)}
{showWizard && step === 'preflight' && (
<PreflightStep
<div className="mx-auto w-full max-w-3xl"><PreflightStep
report={report}
isLoading={reportLoading}
error={reportError}
onContinue={() => setStep('accruals')}
/>
/></div>
)}
{showWizard && step === 'accruals' && selectedPeriodId && (
@@ -354,27 +361,29 @@ export default function YearEndPage() {
)}
{showWizard && step === 'preview' && (
<PreviewStep
<div className="mx-auto w-full max-w-3xl"><PreviewStep
preview={preview}
isLoading={previewLoading}
error={previewError}
onBack={() => setStep('dispositions')}
onContinue={() => setStep('execute')}
/>
/></div>
)}
{showWizard && step === 'execute' && report && (
<ExecuteStep
<div className="mx-auto w-full max-w-3xl"><ExecuteStep
periodName={report.period.name}
isRunning={executing}
error={executeError}
bolagsskattMissing={preview?.bolagsskattMissing ?? false}
onBack={() => setStep('preview')}
onExecute={executeYearEnd}
/>
/></div>
)}
{step === 'result' && result && <ResultStep result={result} />}
{step === 'result' && result && (
<div className="mx-auto w-full max-w-3xl"><ResultStep result={result} /></div>
)}
</div>
)
}
@@ -3,7 +3,7 @@
import Link from 'next/link'
import { useEffect, useState, useCallback } from 'react'
import { PageHeader } from '@/components/ui/page-header'
import { FiscalYearSelector } from '@/components/common/FiscalYearSelector'
import { FyPicker } from '@/components/common/FyPicker'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
@@ -105,7 +105,7 @@ export function KassaflodesanalysClient() {
<PageHeader title="Kassaflödesanalys" />
<div className="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
<FiscalYearSelector
<FyPicker
value={selectedPeriod}
onChange={(id) => setSelectedPeriod(id)}
includeAllOption={false}