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:
co-authored by
Claude Fable 5
parent
49e86e2e67
commit
98e1a48c2b
@@ -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} />
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -5,9 +5,15 @@ import { useTranslations } from 'next-intl'
|
||||
import { AlertCircle, CheckCircle2, FileClock, Loader2, LockKeyhole, Save } from 'lucide-react'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import { getErrorMessage as getUserErrorMessage } from '@/lib/errors/get-error-message'
|
||||
import type {
|
||||
@@ -60,18 +66,18 @@ function BooleanQuestion({
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={id}>{label}</Label>
|
||||
<select
|
||||
id={id}
|
||||
className="min-h-11 w-full rounded-md border border-border bg-background px-3 text-sm"
|
||||
<Select
|
||||
value={value === null ? '' : value ? 'yes' : 'no'}
|
||||
onChange={(event) =>
|
||||
onChange(event.target.value === '' ? null : event.target.value === 'yes')
|
||||
}
|
||||
onValueChange={(next) => onChange(next === 'yes')}
|
||||
>
|
||||
<option value="">{t('choose')}</option>
|
||||
<option value="no">{t('no')}</option>
|
||||
<option value="yes">{t('yes')}</option>
|
||||
</select>
|
||||
<SelectTrigger id={id}>
|
||||
<SelectValue placeholder={t('choose')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="no">{t('no')}</SelectItem>
|
||||
<SelectItem value="yes">{t('yes')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -289,60 +295,57 @@ export function AnnualReportStudio({
|
||||
|
||||
if (loading || !profile || !compliance) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex min-h-24 items-center justify-center p-6 text-sm text-muted-foreground">
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> {t('loading')}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="flex min-h-24 items-center justify-center px-1 py-6 text-sm text-muted-foreground">
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> {t('loading')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader className="space-y-3">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<CardTitle className="text-lg">{t('title')}</CardTitle>
|
||||
<p className="mt-1 text-sm text-muted-foreground">{t('description')}</p>
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-3 px-1">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="font-sans text-sm font-medium">{t('title')}</h3>
|
||||
<p className="mt-1 text-sm text-muted-foreground">{t('description')}</p>
|
||||
</div>
|
||||
{blockingIssues.length === 0 ? (
|
||||
<span className="text-xs text-muted-foreground">{t('no_blockers')}</span>
|
||||
) : (
|
||||
<Badge variant="warning">{t('blocker_count', { count: blockingIssues.length })}</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid gap-2 sm:grid-cols-4">
|
||||
{[
|
||||
[
|
||||
t('step_scope'),
|
||||
compliance.eligibility.issues.every((issue) => issue.severity !== 'error'),
|
||||
],
|
||||
[t('step_content'), Boolean(profile.narrative_confirmed_at)],
|
||||
[t('step_signatures'), versions.some((version) => version.status === 'signed')],
|
||||
[t('step_filing'), versions.some((version) => ['filed', 'registered'].includes(version.status))],
|
||||
].map(([label, complete], index) => (
|
||||
<div key={String(label)} className="flex items-center gap-2 border-b border-border/60 px-1 py-2 text-sm">
|
||||
{complete ? (
|
||||
<CheckCircle2 className="h-4 w-4 shrink-0 text-success" />
|
||||
) : (
|
||||
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full border border-border text-xs">
|
||||
{index + 1}
|
||||
</span>
|
||||
)}
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
<Badge variant={blockingIssues.length === 0 ? 'success' : 'warning'}>
|
||||
{blockingIssues.length === 0
|
||||
? t('no_blockers')
|
||||
: t('blocker_count', { count: blockingIssues.length })}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="grid gap-2 sm:grid-cols-4">
|
||||
{[
|
||||
[
|
||||
t('step_scope'),
|
||||
compliance.eligibility.issues.every((issue) => issue.severity !== 'error'),
|
||||
],
|
||||
[t('step_content'), Boolean(profile.narrative_confirmed_at)],
|
||||
[t('step_signatures'), versions.some((version) => version.status === 'signed')],
|
||||
[t('step_filing'), versions.some((version) => ['filed', 'registered'].includes(version.status))],
|
||||
].map(([label, complete], index) => (
|
||||
<div key={String(label)} className="flex min-h-11 items-center gap-2 border-b border-border px-1 py-2 text-sm">
|
||||
{complete ? (
|
||||
<CheckCircle2 className="h-4 w-4 shrink-0 text-success" />
|
||||
) : (
|
||||
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full border border-border text-xs">
|
||||
{index + 1}
|
||||
</span>
|
||||
)}
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">{t('scope_title')}</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">{t('scope_description')}</p>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<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">{t('scope_title')}</h3>
|
||||
<div className="h-px flex-1 bg-border/60" />
|
||||
</div>
|
||||
<p className="px-1 text-sm text-muted-foreground">{t('scope_description')}</p>
|
||||
<div className="space-y-6 px-1 pt-4">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<BooleanQuestion
|
||||
id="ar-public"
|
||||
@@ -376,20 +379,23 @@ export function AnnualReportStudio({
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="ar-reporting-currency">{t('reporting_currency')}</Label>
|
||||
<select
|
||||
id="ar-reporting-currency"
|
||||
className="min-h-11 w-full rounded-md border border-border bg-background px-3 text-sm"
|
||||
<Select
|
||||
value={profile.reporting_currency}
|
||||
onChange={(event) =>
|
||||
onValueChange={(next) =>
|
||||
updateProfile(
|
||||
'reporting_currency',
|
||||
event.target.value as AnnualReportProfile['reporting_currency'],
|
||||
next as AnnualReportProfile['reporting_currency'],
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="SEK">SEK</option>
|
||||
<option value="EUR">EUR</option>
|
||||
</select>
|
||||
<SelectTrigger id="ar-reporting-currency">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="SEK">SEK</SelectItem>
|
||||
<SelectItem value="EUR">EUR</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{profile.auditor_report_required && (
|
||||
<BooleanQuestion
|
||||
@@ -405,21 +411,23 @@ export function AnnualReportStudio({
|
||||
<div className="grid gap-4 border-t border-border pt-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="ar-group-size">{t('group_size')}</Label>
|
||||
<select
|
||||
id="ar-group-size"
|
||||
className="min-h-11 w-full rounded-md border border-border bg-background px-3 text-sm"
|
||||
<Select
|
||||
value={profile.parent_group_size ?? ''}
|
||||
onChange={(event) =>
|
||||
onValueChange={(next) =>
|
||||
updateProfile(
|
||||
'parent_group_size',
|
||||
(event.target.value || null) as AnnualReportProfile['parent_group_size'],
|
||||
next as AnnualReportProfile['parent_group_size'],
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="">{t('choose')}</option>
|
||||
<option value="small">{t('group_small')}</option>
|
||||
<option value="large">{t('group_large')}</option>
|
||||
</select>
|
||||
<SelectTrigger id="ar-group-size">
|
||||
<SelectValue placeholder={t('choose')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="small">{t('group_small')}</SelectItem>
|
||||
<SelectItem value="large">{t('group_large')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<BooleanQuestion
|
||||
id="ar-consolidated"
|
||||
@@ -446,7 +454,7 @@ export function AnnualReportStudio({
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
className="min-h-11"
|
||||
|
||||
value={profile.building_revenue_share_pct ?? ''}
|
||||
onChange={(event) =>
|
||||
updateProfile(
|
||||
@@ -478,19 +486,20 @@ export function AnnualReportStudio({
|
||||
)}
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button className="min-h-11" onClick={() => void saveProfile()} disabled={saving}>
|
||||
<Button onClick={() => void saveProfile()} disabled={saving}>
|
||||
{saving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Save className="mr-2 h-4 w-4" />}
|
||||
{t('save_scope')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">{t('checks_title')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<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">{t('checks_title')}</h3>
|
||||
<div className="h-px flex-1 bg-border/60" />
|
||||
</div>
|
||||
<div className="space-y-4 px-1 pt-2">
|
||||
{blockingIssues.length === 0 ? (
|
||||
<div className="flex items-center gap-2 text-sm text-success">
|
||||
<CheckCircle2 className="h-4 w-4" /> {t('no_blockers')}
|
||||
@@ -509,8 +518,8 @@ export function AnnualReportStudio({
|
||||
</ul>
|
||||
)}
|
||||
{digitalOnlyIssues.length > 0 && (
|
||||
<div className="space-y-2 rounded-md border border-warning/40 bg-warning/5 p-3">
|
||||
<p className="text-sm font-medium">{t('digital_checks_title')}</p>
|
||||
<div className="space-y-2">
|
||||
<p className="text-[12.5px] font-medium leading-5 text-attn">{t('digital_checks_title')}</p>
|
||||
<ul className="space-y-2">
|
||||
{digitalOnlyIssues.map((issue) => (
|
||||
<li key={issue.code} className="text-sm text-muted-foreground">
|
||||
@@ -520,10 +529,10 @@ export function AnnualReportStudio({
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-wrap justify-end gap-3 border-t border-border pt-4">
|
||||
<div className="flex flex-wrap justify-end gap-3 border-t border-border/60 pt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="min-h-11"
|
||||
|
||||
onClick={() => void confirmSignerRoster()}
|
||||
disabled={saving || Boolean(profile.signer_roster_confirmed_at)}
|
||||
>
|
||||
@@ -534,30 +543,31 @@ export function AnnualReportStudio({
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="min-h-11"
|
||||
|
||||
onClick={() => void confirmNarrative()}
|
||||
disabled={saving || hasUnsavedNarrative || Boolean(profile.narrative_confirmed_at)}
|
||||
>
|
||||
<CheckCircle2 className="mr-2 h-4 w-4" />
|
||||
{profile.narrative_confirmed_at ? t('content_confirmed') : t('confirm_content')}
|
||||
</Button>
|
||||
<Button variant="outline" className="min-h-11" onClick={() => void createVersion('snapshot')} disabled={creatingVersion !== null}>
|
||||
<Button variant="outline" onClick={() => void createVersion('snapshot')} disabled={creatingVersion !== null}>
|
||||
{creatingVersion === 'snapshot' ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <FileClock className="mr-2 h-4 w-4" />}
|
||||
{t('create_snapshot')}
|
||||
</Button>
|
||||
<Button className="min-h-11" onClick={() => void createVersion('finalize')} disabled={creatingVersion !== null || blockingIssues.length > 0 || hasUnsavedNarrative}>
|
||||
<Button onClick={() => void createVersion('finalize')} disabled={creatingVersion !== null || blockingIssues.length > 0 || hasUnsavedNarrative}>
|
||||
{creatingVersion === 'finalize' ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <LockKeyhole className="mr-2 h-4 w-4" />}
|
||||
{t('lock_version')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">{t('versions_title')}</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">{t('versions_title')}</h3>
|
||||
<div className="h-px flex-1 bg-border/60" />
|
||||
</div>
|
||||
<div className="px-1 pt-2">
|
||||
{versions.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">{t('versions_empty')}</p>
|
||||
) : (
|
||||
@@ -572,7 +582,7 @@ export function AnnualReportStudio({
|
||||
<Badge variant={version.status === 'registered' ? 'success' : version.status === 'draft' ? 'outline' : 'secondary'}>
|
||||
{t(`status_${version.status}`)}
|
||||
</Badge>
|
||||
<Button className="min-h-11" variant="outline" size="sm" asChild>
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<a href={`/api/bookkeeping/fiscal-periods/${periodId}/arsredovisning/pdf?version=${version.id}`} target="_blank" rel="noopener noreferrer">
|
||||
{t('open_pdf')}
|
||||
</a>
|
||||
@@ -582,8 +592,8 @@ export function AnnualReportStudio({
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,11 +18,17 @@
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
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 { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import { formatDate } from '@/lib/utils'
|
||||
import {
|
||||
@@ -421,25 +427,26 @@ export function DigitalInlamning({ periodId }: { periodId: string }) {
|
||||
}
|
||||
>
|
||||
{/* Steg: Granska & validera */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Digital inlämning: granska & validera (iXBRL)</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Bolagsverket tar emot årsredovisningen som iXBRL (XHTML). Dokumentet nedan är
|
||||
exakt det som lämnas in: granska det som den slutliga presentationen.
|
||||
</p>
|
||||
</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">Digital inlämning: granska & validera (iXBRL)</h3>
|
||||
<div className="h-px flex-1 bg-border/60" />
|
||||
</div>
|
||||
<p className="px-1 text-sm text-muted-foreground">
|
||||
Bolagsverket tar emot årsredovisningen som iXBRL (XHTML). Dokumentet nedan är
|
||||
exakt det som lämnas in: granska det som den slutliga presentationen.
|
||||
</p>
|
||||
<div className="space-y-4 px-1 pt-4 text-sm">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button className="min-h-11" variant="outline" onClick={() => setShowPreview((value) => !value)}>
|
||||
<Button variant="outline" onClick={() => setShowPreview((value) => !value)}>
|
||||
{showPreview ? 'Dölj förhandsgranskning' : 'Förhandsgranska iXBRL'}
|
||||
</Button>
|
||||
<Button className="min-h-11" variant="outline" asChild>
|
||||
<Button variant="outline" asChild>
|
||||
<a href={downloadUrl}>
|
||||
<FileDown className="mr-2 h-4 w-4" /> Ladda ner tekniskt iXBRL-underlag
|
||||
</a>
|
||||
</Button>
|
||||
<Button className="min-h-11" onClick={() => void handleValidate()} disabled={validating}>
|
||||
<Button onClick={() => void handleValidate()} disabled={validating}>
|
||||
{validating ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
@@ -486,20 +493,21 @@ export function DigitalInlamning({ periodId }: { periodId: string }) {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Steg: Skicka in */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Skicka in till Bolagsverket</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Årsredovisningen laddas upp till företagets eget utrymme hos Bolagsverket.
|
||||
Undertecknaren får ett e-postmeddelande och signerar fastställelseintyget med
|
||||
e-legitimation hos Bolagsverket: först då är årsredovisningen inlämnad.
|
||||
</p>
|
||||
</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">Skicka in till Bolagsverket</h3>
|
||||
<div className="h-px flex-1 bg-border/60" />
|
||||
</div>
|
||||
<p className="px-1 text-sm text-muted-foreground">
|
||||
Årsredovisningen laddas upp till företagets eget utrymme hos Bolagsverket.
|
||||
Undertecknaren får ett e-postmeddelande och signerar fastställelseintyget med
|
||||
e-legitimation hos Bolagsverket: först då är årsredovisningen inlämnad.
|
||||
</p>
|
||||
<div className="space-y-4 px-1 pt-4 text-sm">
|
||||
{extensionActive === null && (
|
||||
<p className="text-muted-foreground">
|
||||
<Loader2 className="inline h-4 w-4 animate-spin mr-2" />
|
||||
@@ -515,7 +523,7 @@ export function DigitalInlamning({ periodId }: { periodId: string }) {
|
||||
pappersinlämning per post, eller aktivera integrationen efter avtal,
|
||||
organisationscertifikat och godkänt acceptanstest.
|
||||
</p>
|
||||
<Button className="min-h-11" variant="outline" asChild>
|
||||
<Button variant="outline" asChild>
|
||||
<a
|
||||
href="https://bolagsverket.se/foretag/aktiebolag/arsredovisningforaktiebolag.759.html"
|
||||
target="_blank"
|
||||
@@ -544,12 +552,9 @@ export function DigitalInlamning({ periodId }: { periodId: string }) {
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="di-version">Undertecknad årsredovisningsversion</Label>
|
||||
<select
|
||||
id="di-version"
|
||||
className="min-h-11 w-full rounded-md border border-border bg-background px-3 text-sm"
|
||||
<Select
|
||||
value={selectedVersionId}
|
||||
onChange={(event) => {
|
||||
const versionId = event.target.value
|
||||
onValueChange={(versionId) => {
|
||||
setSelectedVersionId(versionId)
|
||||
const signer = versions.find((version) => version.id === versionId)
|
||||
?.certificate_signer
|
||||
@@ -560,18 +565,22 @@ export function DigitalInlamning({ periodId }: { periodId: string }) {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="">Välj undertecknad version</option>
|
||||
{versions
|
||||
.filter(
|
||||
(version) =>
|
||||
version.status === 'signed' && version.digital_filing_eligible,
|
||||
)
|
||||
.map((version) => (
|
||||
<option key={version.id} value={version.id}>
|
||||
Version {version.version_number}: skapad {formatDate(version.created_at)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<SelectTrigger id="di-version">
|
||||
<SelectValue placeholder="Välj undertecknad version" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{versions
|
||||
.filter(
|
||||
(version) =>
|
||||
version.status === 'signed' && version.digital_filing_eligible,
|
||||
)
|
||||
.map((version) => (
|
||||
<SelectItem key={version.id} value={version.id}>
|
||||
Version {version.version_number}: skapad {formatDate(version.created_at)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{versions.every(
|
||||
(version) =>
|
||||
version.status !== 'signed' || !version.digital_filing_eligible,
|
||||
@@ -593,7 +602,7 @@ export function DigitalInlamning({ periodId }: { periodId: string }) {
|
||||
<Label htmlFor="di-avsandare-pnr">Ditt personnummer (avsändare)</Label>
|
||||
<Input
|
||||
id="di-avsandare-pnr"
|
||||
className="min-h-11"
|
||||
|
||||
inputMode="numeric"
|
||||
placeholder="ÅÅÅÅMMDDNNNN eller ÅÅMMDD-NNNN"
|
||||
value={avsandarePnr}
|
||||
@@ -604,7 +613,7 @@ export function DigitalInlamning({ periodId }: { periodId: string }) {
|
||||
<Label htmlFor="di-pnr">Undertecknarens personnummer</Label>
|
||||
<Input
|
||||
id="di-pnr"
|
||||
className="min-h-11"
|
||||
|
||||
inputMode="numeric"
|
||||
placeholder="ÅÅÅÅMMDDNNNN eller ÅÅMMDD-NNNN"
|
||||
value={pnr}
|
||||
@@ -615,7 +624,7 @@ export function DigitalInlamning({ periodId }: { periodId: string }) {
|
||||
<Label htmlFor="di-fornamn">Undertecknarens förnamn</Label>
|
||||
<Input
|
||||
id="di-fornamn"
|
||||
className="min-h-11"
|
||||
|
||||
value={fornamn}
|
||||
onChange={(event) => setFornamn(event.target.value)}
|
||||
readOnly={Boolean(selectedVersionId)}
|
||||
@@ -625,7 +634,7 @@ export function DigitalInlamning({ periodId }: { periodId: string }) {
|
||||
<Label htmlFor="di-efternamn">Undertecknarens efternamn</Label>
|
||||
<Input
|
||||
id="di-efternamn"
|
||||
className="min-h-11"
|
||||
|
||||
value={efternamn}
|
||||
onChange={(event) => setEfternamn(event.target.value)}
|
||||
readOnly={Boolean(selectedVersionId)}
|
||||
@@ -633,23 +642,26 @@ export function DigitalInlamning({ periodId }: { periodId: string }) {
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="di-roll">Roll</Label>
|
||||
<select
|
||||
id="di-roll"
|
||||
className="min-h-11 w-full rounded-md border border-border bg-background px-3 text-sm"
|
||||
<Select
|
||||
value={roll}
|
||||
onChange={(event) => setRoll(event.target.value)}
|
||||
onValueChange={setRoll}
|
||||
disabled={Boolean(selectedVersionId)}
|
||||
>
|
||||
<option>Styrelseledamot</option>
|
||||
<option>Styrelseordförande</option>
|
||||
<option>Verkställande direktör</option>
|
||||
</select>
|
||||
<SelectTrigger id="di-roll">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Styrelseledamot">Styrelseledamot</SelectItem>
|
||||
<SelectItem value="Styrelseordförande">Styrelseordförande</SelectItem>
|
||||
<SelectItem value="Verkställande direktör">Verkställande direktör</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="di-epost">Undertecknarens e-post</Label>
|
||||
<Input
|
||||
id="di-epost"
|
||||
className="min-h-11"
|
||||
|
||||
type="email"
|
||||
placeholder="namn@foretag.se"
|
||||
value={epost}
|
||||
@@ -663,7 +675,7 @@ export function DigitalInlamning({ periodId }: { periodId: string }) {
|
||||
</p>
|
||||
|
||||
{registryInformation && (
|
||||
<div className="rounded-md border border-border bg-muted/20 p-4 text-xs">
|
||||
<div className="border-t border-border/60 pt-3 text-xs">
|
||||
<p className="font-medium">Grunduppgifter från Bolagsverket</p>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
{registryInformation.namn}
|
||||
@@ -689,12 +701,12 @@ export function DigitalInlamning({ periodId }: { periodId: string }) {
|
||||
)}
|
||||
|
||||
{avtal && (
|
||||
<div className="rounded-lg border border-border p-4 space-y-3">
|
||||
<div className="space-y-3 border-y border-border/60 py-4">
|
||||
<p className="font-medium">Villkor för eget utrymme hos Bolagsverket</p>
|
||||
<p className="text-muted-foreground whitespace-pre-wrap text-xs max-h-48 overflow-y-auto">
|
||||
{avtal.text}
|
||||
</p>
|
||||
<label className="flex min-h-11 cursor-pointer items-start gap-3 text-sm">
|
||||
<label className="flex cursor-pointer items-start gap-3 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mt-0.5"
|
||||
@@ -737,7 +749,7 @@ export function DigitalInlamning({ periodId }: { periodId: string }) {
|
||||
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button
|
||||
className="min-h-11"
|
||||
|
||||
onClick={() => void handleSubmit()}
|
||||
disabled={
|
||||
submitting ||
|
||||
@@ -755,7 +767,7 @@ export function DigitalInlamning({ periodId }: { periodId: string }) {
|
||||
</Button>
|
||||
{utfall && utfall.length > 0 && !utfallHasErrors && (
|
||||
<Button
|
||||
className="min-h-11"
|
||||
|
||||
variant="outline"
|
||||
onClick={() => void handleSubmit({ ignoreWarnings: true })}
|
||||
disabled={submitting}
|
||||
@@ -766,16 +778,16 @@ export function DigitalInlamning({ periodId }: { periodId: string }) {
|
||||
</div>
|
||||
|
||||
{kvittens && (
|
||||
<div className="rounded-lg border border-border p-4 space-y-2">
|
||||
<div className="space-y-2 border-t border-border/60 pt-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<ShieldCheck className="h-4 w-4" />
|
||||
<ShieldCheck className="h-4 w-4 text-success" />
|
||||
<p className="font-medium">Uppladdad till eget utrymme</p>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Undertecknaren har fått e-post från Bolagsverket och signerar
|
||||
fastställelseintyget där. Ärendet startar först efter signering.
|
||||
</p>
|
||||
<Button className="min-h-11" variant="outline" size="sm" asChild>
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<a href={kvittens.url} target="_blank" rel="noopener noreferrer">
|
||||
<ExternalLink className="mr-2 h-4 w-4" /> Signera hos Bolagsverket
|
||||
</a>
|
||||
@@ -784,22 +796,22 @@ export function DigitalInlamning({ periodId }: { periodId: string }) {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Steg: Status */}
|
||||
{extensionActive === true && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Inlämningsstatus</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Status uppdateras automatiskt via händelseaviseringar från Bolagsverket.
|
||||
</p>
|
||||
</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">Inlämningsstatus</h3>
|
||||
<div className="h-px flex-1 bg-border/60" />
|
||||
</div>
|
||||
<p className="px-1 text-sm text-muted-foreground">
|
||||
Status uppdateras automatiskt via händelseaviseringar från Bolagsverket.
|
||||
</p>
|
||||
<div className="space-y-4 px-1 pt-4 text-sm">
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
className="min-h-11"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={pollingEvents}
|
||||
@@ -859,7 +871,7 @@ export function DigitalInlamning({ periodId }: { periodId: string }) {
|
||||
)}
|
||||
</div>
|
||||
{submission.bolagsverket_url && submission.status === 'uploaded' && (
|
||||
<Button className="min-h-11" variant="outline" size="sm" asChild>
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<a href={submission.bolagsverket_url} target="_blank" rel="noopener noreferrer">
|
||||
<ExternalLink className="mr-1 h-3.5 w-3.5" /> Signera
|
||||
</a>
|
||||
@@ -868,8 +880,8 @@ export function DigitalInlamning({ periodId }: { periodId: string }) {
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
{INLAMNING_COMING_SOON && (
|
||||
|
||||
@@ -273,8 +273,8 @@ export function AccrualsStep({ periodId, onBack, onContinue }: AccrualsStepProps
|
||||
)}
|
||||
|
||||
<div className="flex justify-between">
|
||||
<Button variant="outline" onClick={onBack} disabled={posting}>
|
||||
Tillbaka
|
||||
<Button variant="outline" size="sm" onClick={onBack} disabled={posting}>
|
||||
← Tillbaka
|
||||
</Button>
|
||||
<Button onClick={handleCommit} disabled={posting}>
|
||||
{posting ? (
|
||||
|
||||
@@ -216,9 +216,11 @@ export function DispositionsStep({ periodId, onBack, onContinue }: DispositionsS
|
||||
fiscalYear={fiscalYear}
|
||||
/>
|
||||
<div className="flex justify-between">
|
||||
<Button variant="outline" onClick={onBack}>Tillbaka</Button>
|
||||
<Button onClick={onContinue}>
|
||||
Fortsätt <ArrowRight className="ml-1 h-4 w-4" />
|
||||
<Button variant="outline" size="sm" onClick={onBack}>
|
||||
← Tillbaka
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={onContinue}>
|
||||
Nästa: Förhandsgranska →
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -322,8 +324,8 @@ export function DispositionsStep({ periodId, onBack, onContinue }: DispositionsS
|
||||
)}
|
||||
|
||||
<div className="flex justify-between">
|
||||
<Button variant="outline" onClick={onBack} disabled={posting}>
|
||||
Tillbaka
|
||||
<Button variant="outline" size="sm" onClick={onBack} disabled={posting}>
|
||||
← Tillbaka
|
||||
</Button>
|
||||
<Button onClick={handleCommit} disabled={posting || hasCorrectionRequired}>
|
||||
{posting ? (
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { DestructiveConfirmDialog } from '@/components/ui/destructive-confirm-dialog'
|
||||
import { Lock, Loader2, AlertTriangle } from 'lucide-react'
|
||||
import { Lock, Loader2 } from 'lucide-react'
|
||||
|
||||
interface ExecuteStepProps {
|
||||
periodName: string
|
||||
@@ -27,14 +26,15 @@ export function ExecuteStep({ periodName, isRunning, error, bolagsskattMissing,
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Lock className="h-4 w-4" />
|
||||
<section>
|
||||
<div className="mb-3 flex items-center gap-2 px-1">
|
||||
<Lock className="h-3.5 w-3.5 text-muted-foreground" aria-hidden="true" />
|
||||
<h3 className="font-sans text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
||||
Klar att verkställa
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
</h3>
|
||||
<div className="h-px flex-1 bg-border/60" />
|
||||
</div>
|
||||
<div className="space-y-4 px-1">
|
||||
<p className="text-sm">
|
||||
När du verkställer bokslutet för <strong>{periodName}</strong> kommer följande att hända
|
||||
i en transaktion:
|
||||
@@ -51,27 +51,20 @@ export function ExecuteStep({ periodName, isRunning, error, bolagsskattMissing,
|
||||
stornering eller bokar i den nya perioden.
|
||||
</p>
|
||||
{bolagsskattMissing && (
|
||||
<div className="rounded-md border border-border p-3 text-sm flex items-start gap-2">
|
||||
<AlertTriangle className="h-4 w-4 text-warning-foreground mt-0.5 shrink-0" />
|
||||
<p>
|
||||
Ingen bolagsskatt är bokförd trots att året visar vinst. Om det inte är avsiktligt
|
||||
(t.ex. underskottsavdrag, periodiseringsfond eller överavskrivningar som nollar det
|
||||
skattemässiga resultatet), gå tillbaka och boka skatten i dispositionssteget innan
|
||||
du verkställer.
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-[12.5px] leading-5 text-attn">
|
||||
Ingen bolagsskatt är bokförd trots att året visar vinst. Om det inte är avsiktligt
|
||||
(t.ex. underskottsavdrag, periodiseringsfond eller överavskrivningar som nollar det
|
||||
skattemässiga resultatet), gå tillbaka och boka skatten i dispositionssteget innan
|
||||
du verkställer.
|
||||
</p>
|
||||
)}
|
||||
{error && (
|
||||
<div className="rounded-md border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<Button variant="outline" onClick={onBack} disabled={isRunning}>
|
||||
Tillbaka
|
||||
<Button variant="outline" size="sm" onClick={onBack} disabled={isRunning}>
|
||||
← Tillbaka
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
'use client'
|
||||
|
||||
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'
|
||||
import { CheckCircle2, AlertTriangle, XCircle, Info } from 'lucide-react'
|
||||
import { QUIET_LINK_CLASS } from '@/components/ui/dry-table'
|
||||
import { AlertTriangle, Info, XCircle } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import type { BokslutReadinessReport } from '@/lib/bokslut/readiness-aggregator'
|
||||
|
||||
@@ -15,28 +15,33 @@ interface PreflightStepProps {
|
||||
onContinue: () => void
|
||||
}
|
||||
|
||||
/** Sans eyebrow section head with a trailing hairline (house idiom). */
|
||||
function SectionHead({ icon, children }: { icon?: React.ReactNode; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="mb-1 flex items-center gap-2 px-1">
|
||||
{icon}
|
||||
<h3 className="font-sans text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
||||
{children}
|
||||
</h3>
|
||||
<div className="h-px flex-1 bg-border/60" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function PreflightStep({ report, isLoading, error, onContinue }: PreflightStepProps) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-6 space-y-3">
|
||||
<Skeleton className="h-6 w-1/3" />
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
<Skeleton className="h-4 w-2/3" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="space-y-3">
|
||||
<Skeleton className="h-6 w-1/3" />
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
<Skeleton className="h-4 w-2/3" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<p className="text-destructive">{error}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
return <p className="py-4 text-sm text-destructive">{error}</p>
|
||||
}
|
||||
|
||||
if (!report) {
|
||||
@@ -44,95 +49,71 @@ export function PreflightStep({ report, isLoading, error, onContinue }: Prefligh
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<CardTitle className="text-base">{report.period.name}</CardTitle>
|
||||
<p className="text-sm text-muted-foreground tabular-nums">
|
||||
{report.period.period_start}: {report.period.period_end}
|
||||
</p>
|
||||
</div>
|
||||
{report.ready ? (
|
||||
<Badge variant="success" className="gap-1">
|
||||
<CheckCircle2 className="h-3.5 w-3.5" /> Redo för bokslut
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="destructive" className="gap-1">
|
||||
<XCircle className="h-3.5 w-3.5" /> Inte redo
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<div className="space-y-8">
|
||||
{/* Period line: muted text for the normal state, chip only when the
|
||||
period deviates (design.md: chips mark exceptions). */}
|
||||
<div className="flex flex-wrap items-baseline justify-between gap-3 px-1">
|
||||
<div className="flex items-baseline gap-3">
|
||||
<h2 className="font-sans text-sm font-medium">{report.period.name}</h2>
|
||||
<span className="text-xs tabular-nums text-muted-foreground">
|
||||
{report.period.period_start} till {report.period.period_end}
|
||||
</span>
|
||||
</div>
|
||||
{report.ready ? (
|
||||
<span className="text-xs text-muted-foreground">Redo för bokslut</span>
|
||||
) : (
|
||||
<Badge variant="destructive" className="font-normal">Inte redo</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{report.blockers.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<XCircle className="h-4 w-4 text-destructive" />
|
||||
Måste åtgärdas innan bokslut
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
{report.blockers.map((blocker, i) => (
|
||||
<BlockerRow key={i} blocker={blocker} report={report} />
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<section>
|
||||
<SectionHead icon={<XCircle className="h-3.5 w-3.5 text-destructive" aria-hidden="true" />}>
|
||||
Måste åtgärdas innan bokslut
|
||||
</SectionHead>
|
||||
{report.blockers.map((blocker, i) => (
|
||||
<BlockerRow key={i} blocker={blocker} report={report} />
|
||||
))}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{report.warnings.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<AlertTriangle className="h-4 w-4 text-warning-foreground" />
|
||||
Varningar
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
{report.warnings.map((warning, i) => (
|
||||
<p key={i} className="text-sm">
|
||||
{warning}
|
||||
</p>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<section>
|
||||
<SectionHead icon={<AlertTriangle className="h-3.5 w-3.5 text-attn" aria-hidden="true" />}>
|
||||
Varningar
|
||||
</SectionHead>
|
||||
{report.warnings.map((warning, i) => (
|
||||
<p key={i} className="border-b border-border/60 px-1 py-3 text-[13px] leading-5 last:border-b-0">
|
||||
{warning}
|
||||
</p>
|
||||
))}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{report.reminders.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Info className="h-4 w-4 text-muted-foreground" />
|
||||
Påminnelser
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{report.reminders.map((reminder) => (
|
||||
<div key={reminder.code} className="flex items-start gap-3 text-sm">
|
||||
<span className="mt-1 h-1.5 w-1.5 rounded-full bg-muted-foreground shrink-0" />
|
||||
<div className="flex-1">
|
||||
<p>{reminder.message}</p>
|
||||
{reminder.href && (
|
||||
<Link
|
||||
href={reminder.href}
|
||||
className="text-xs text-primary hover:underline"
|
||||
>
|
||||
Öppna
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<section>
|
||||
<SectionHead icon={<Info className="h-3.5 w-3.5 text-muted-foreground" aria-hidden="true" />}>
|
||||
Påminnelser
|
||||
</SectionHead>
|
||||
{report.reminders.map((reminder) => (
|
||||
<div
|
||||
key={reminder.code}
|
||||
className="flex items-baseline justify-between gap-3 border-b border-border/60 px-1 py-3 text-[13px] leading-5 text-muted-foreground last:border-b-0"
|
||||
>
|
||||
<p className="flex-1">{reminder.message}</p>
|
||||
{reminder.href && (
|
||||
<Link href={reminder.href} className={QUIET_LINK_CLASS}>
|
||||
Öppna
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={onContinue} disabled={!report.ready}>
|
||||
Fortsätt
|
||||
<Button variant="outline" size="sm" onClick={onContinue} disabled={!report.ready}>
|
||||
Nästa: Periodiseringar →
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -162,10 +143,10 @@ function BlockerRow({ blocker, report }: { blocker: string; report: BokslutReadi
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-start justify-between gap-3 text-sm">
|
||||
<div className="flex items-baseline justify-between gap-3 border-b border-border/60 px-1 py-3 text-[13px] leading-5 last:border-b-0">
|
||||
<p className="flex-1">{blocker}</p>
|
||||
{href && actionLabel && (
|
||||
<Link href={href} className="text-xs text-primary hover:underline shrink-0 mt-0.5">
|
||||
<Link href={href} className={QUIET_LINK_CLASS}>
|
||||
{actionLabel}
|
||||
</Link>
|
||||
)}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
'use client'
|
||||
|
||||
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'
|
||||
import { ArrowRight, AlertTriangle } from 'lucide-react'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -27,23 +25,15 @@ interface PreviewStepProps {
|
||||
export function PreviewStep({ preview, isLoading, error, onBack, onContinue }: PreviewStepProps) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<p className="text-destructive">{error}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
return <p className="py-4 text-sm text-destructive">{error}</p>
|
||||
}
|
||||
|
||||
if (!preview) return null
|
||||
@@ -53,58 +43,49 @@ export function PreviewStep({ preview, isLoading, error, onBack, onContinue }: P
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Årets resultat</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-baseline justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Nettoresultat överförs till {preview.closingAccount} {preview.closingAccountName}
|
||||
</p>
|
||||
{isProfit && <Badge variant="success" className="mt-2">Vinst</Badge>}
|
||||
{isLoss && <Badge variant="destructive" className="mt-2">Förlust</Badge>}
|
||||
</div>
|
||||
<p className="font-display text-3xl tabular-nums">
|
||||
{formatCurrency(preview.netResult)}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="px-1">
|
||||
<p className="font-sans text-[11px] font-medium uppercase tracking-wider text-muted-foreground">
|
||||
Årets resultat
|
||||
</p>
|
||||
<div className="mt-1 flex items-baseline justify-between gap-4">
|
||||
<p
|
||||
className={`font-display text-2xl tabular-nums tracking-tight ${isLoss ? 'text-destructive' : ''}`}
|
||||
>
|
||||
{formatCurrency(preview.netResult)}
|
||||
</p>
|
||||
{isProfit && <span className="text-xs text-muted-foreground">Vinst</span>}
|
||||
{isLoss && <Badge variant="destructive" className="font-normal">Förlust</Badge>}
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Nettoresultat överförs till {preview.closingAccount} {preview.closingAccountName}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{preview.bolagsskattMissing && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<AlertTriangle className="h-4 w-4 text-warning-foreground" />
|
||||
Ingen bolagsskatt är bokförd
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<p className="text-sm">
|
||||
Året visar vinst men ingen skatt på årets resultat (konto 8910) finns bland de konton
|
||||
som stängs. Gå tillbaka till dispositionssteget och boka bolagsskatten innan du
|
||||
verkställer, om inte skattemässigt resultat är noll (t.ex. genom underskottsavdrag,
|
||||
avsättning till periodiseringsfond eller överavskrivningar).
|
||||
</p>
|
||||
<Button variant="outline" size="sm" onClick={onBack}>
|
||||
Till dispositionssteget
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<p className="px-1 text-[12.5px] leading-5 text-attn">
|
||||
Året visar vinst men ingen skatt på årets resultat (konto 8910) finns bland de konton
|
||||
som stängs. Gå tillbaka till dispositionssteget och boka bolagsskatten innan du
|
||||
verkställer, om inte skattemässigt resultat är noll (t.ex. genom underskottsavdrag,
|
||||
avsättning till periodiseringsfond eller överavskrivningar).{' '}
|
||||
<button type="button" onClick={onBack} className="underline underline-offset-2 hover:opacity-80">
|
||||
Till dispositionssteget
|
||||
</button>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{preview.currencyRevaluation && preview.currencyRevaluation.items.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Kursrevaluering (ÅRL 4:13)</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Öppna fordringar/skulder i utländsk valuta värderas om till balansdagens kurs innan bokslut.
|
||||
Detta sker automatiskt som en del av verkställandet.
|
||||
</p>
|
||||
</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">
|
||||
Kursrevaluering (ÅRL 4:13)
|
||||
</h3>
|
||||
<div className="h-px flex-1 bg-border/60" />
|
||||
</div>
|
||||
<p className="px-1 text-xs leading-5 text-muted-foreground">
|
||||
Öppna fordringar/skulder i utländsk valuta värderas om till balansdagens kurs innan
|
||||
bokslut. Detta sker automatiskt som en del av verkställandet.
|
||||
</p>
|
||||
<div className="px-1 pt-4">
|
||||
<div className="grid grid-cols-3 gap-4 text-sm">
|
||||
<div>
|
||||
<p className="text-muted-foreground text-xs uppercase tracking-wider">Kursvinst</p>
|
||||
@@ -125,18 +106,20 @@ export function PreviewStep({ preview, isLoading, error, onBack, onContinue }: P
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Bokslutsverifikation: förhandsgranskning</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{preview.closingLines.length} kontorader. Nollställer klass 3-8 mot {preview.closingAccount}.
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<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">
|
||||
Bokslutsverifikation: förhandsgranskning
|
||||
</h3>
|
||||
<div className="h-px flex-1 bg-border/60" />
|
||||
</div>
|
||||
<p className="px-1 pb-2 text-xs leading-5 text-muted-foreground">
|
||||
{preview.closingLines.length} kontorader. Nollställer klass 3-8 mot {preview.closingAccount}.
|
||||
</p>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -161,15 +144,14 @@ export function PreviewStep({ preview, isLoading, error, onBack, onContinue }: P
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<Button variant="outline" onClick={onBack}>
|
||||
Tillbaka
|
||||
<Button variant="outline" size="sm" onClick={onBack}>
|
||||
← Tillbaka
|
||||
</Button>
|
||||
<Button onClick={onContinue}>
|
||||
Verkställ bokslut <ArrowRight className="ml-1 h-4 w-4" />
|
||||
<Button variant="outline" size="sm" onClick={onContinue}>
|
||||
Nästa: Verkställ →
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'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'
|
||||
@@ -36,8 +35,7 @@ export function ResultStep({ result }: ResultStepProps) {
|
||||
// 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="space-y-4 py-6 text-center">
|
||||
<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>
|
||||
@@ -45,14 +43,14 @@ export function ResultStep({ result }: ResultStepProps) {
|
||||
<p className="text-muted-foreground">
|
||||
Perioden är stängd och en ny räkenskapsperiod har skapats.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Resultat</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 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">Resultat</h3>
|
||||
<div className="h-px flex-1 bg-border/60" />
|
||||
</div>
|
||||
<div className="space-y-3 px-1 pt-2 text-sm">
|
||||
<ResultRow
|
||||
label="Bokslutsverifikation"
|
||||
value={formatVoucher(result.closingEntry)}
|
||||
@@ -78,12 +76,11 @@ export function ResultStep({ result }: ResultStepProps) {
|
||||
/>
|
||||
)}
|
||||
<ResultRow label="Ny räkenskapsperiod" value={result.nextPeriod.name} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{result.resultAppropriationFailed && (
|
||||
<Card className="border-destructive/30 bg-destructive/5">
|
||||
<CardContent className="p-4 flex items-start gap-3">
|
||||
<div className="flex items-start gap-3 px-1 py-2 text-destructive">
|
||||
<AlertTriangle className="h-4 w-4 mt-0.5 text-destructive shrink-0" />
|
||||
<p className="text-sm">
|
||||
<span className="font-medium">
|
||||
@@ -94,8 +91,7 @@ export function ResultStep({ result }: ResultStepProps) {
|
||||
Det måste flyttas till 2098 innan balansräkningen stämmer. Kör om bokslutet
|
||||
eller kontakta support: felet är loggat.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{continuity && (
|
||||
@@ -105,8 +101,7 @@ export function ResultStep({ result }: ResultStepProps) {
|
||||
/>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6 space-y-4">
|
||||
<section className="space-y-4 px-1">
|
||||
<label className="flex items-start gap-3 cursor-pointer">
|
||||
<Checkbox
|
||||
checked={acknowledged}
|
||||
@@ -153,8 +148,7 @@ export function ResultStep({ result }: ResultStepProps) {
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -197,9 +191,9 @@ function ContinuityPanel({ discrepancies, checkedAccounts }: ContinuityPanelProp
|
||||
)
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
||||
<CardTitle className="text-base">IB/UB-avstämning</CardTitle>
|
||||
<section>
|
||||
<div className="mb-3 flex items-center justify-between gap-2 px-1">
|
||||
<h3 className="font-sans text-xs font-medium uppercase tracking-wider text-muted-foreground">IB/UB-avstämning</h3>
|
||||
{hasIssues ? (
|
||||
<Badge variant="destructive" className="gap-1">
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
@@ -211,8 +205,8 @@ function ContinuityPanel({ discrepancies, checkedAccounts }: ContinuityPanelProp
|
||||
Stämmer
|
||||
</Badge>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
</div>
|
||||
<div className="space-y-4 px-1">
|
||||
<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.
|
||||
@@ -230,7 +224,7 @@ function ContinuityPanel({ discrepancies, checkedAccounts }: ContinuityPanelProp
|
||||
if (rows.length === 0) return null
|
||||
return (
|
||||
<div key={klass}>
|
||||
<h3 className="text-sm font-medium uppercase tracking-wider text-muted-foreground mb-2">
|
||||
<h3 className="font-sans text-xs font-medium uppercase tracking-wider text-muted-foreground mb-2">
|
||||
Klass {klass}: {klass === 1 ? 'Tillgångar' : 'Skulder & eget kapital'}
|
||||
</h3>
|
||||
<Table>
|
||||
@@ -280,7 +274,7 @@ function ContinuityPanel({ discrepancies, checkedAccounts }: ContinuityPanelProp
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { EmptyState } from '@/components/ui/empty-state'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { useCompany } from '@/contexts/CompanyContext'
|
||||
import { FiscalYearSelector } from '@/components/common/FiscalYearSelector'
|
||||
import { FyPicker } from '@/components/common/FyPicker'
|
||||
import { ReportDateRange, type DateRangeValue } from '@/components/common/ReportDateRange'
|
||||
import { DimensionFilter, type DimensionFilterValue } from '@/components/reports/DimensionFilter'
|
||||
import { DATE_RANGE_SLUGS, DIMENSION_FILTER_SLUGS, getReport } from '@/lib/reports/catalog'
|
||||
@@ -117,7 +117,7 @@ function FocusedReportInner({
|
||||
title={reportName}
|
||||
action={
|
||||
isStandalone ? undefined : (
|
||||
<FiscalYearSelector
|
||||
<FyPicker
|
||||
value={selectedPeriod || null}
|
||||
onChange={(id, period) => {
|
||||
setSelectedPeriod(id || '')
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Table, TableBody, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
@@ -97,26 +96,22 @@ export function INK2DeclarationView({ periodId }: { periodId: string }) {
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-6 flex flex-wrap items-center gap-3 text-destructive">
|
||||
<AlertCircle className="h-4 w-4 shrink-0" />
|
||||
<span className="text-sm">{error}</span>
|
||||
<Button variant="outline" onClick={() => setRetryKey((k) => k + 1)}>
|
||||
Försök igen
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="flex flex-wrap items-center gap-3 py-4 text-destructive">
|
||||
<AlertCircle className="h-4 w-4 shrink-0" />
|
||||
<span className="text-sm">{error}</span>
|
||||
<Button variant="outline" onClick={() => setRetryKey((k) => k + 1)}>
|
||||
Försök igen
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (loading && !data) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-6 space-y-4">
|
||||
<Skeleton className="h-5 w-48" />
|
||||
<Skeleton className="h-64" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-5 w-48" />
|
||||
<Skeleton className="h-64" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -127,11 +122,10 @@ export function INK2DeclarationView({ periodId }: { periodId: string }) {
|
||||
className={`space-y-8 transition-opacity duration-150 ${loading ? 'opacity-60' : ''}`}
|
||||
>
|
||||
{/* Header: company, year, filing artifact, and how to file it */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="px-1">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<CardTitle className="text-base">INK2 (Aktiebolag)</CardTitle>
|
||||
<h3 className="font-sans text-sm font-medium">INK2 (Aktiebolag)</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{data.companyInfo.companyName} · {data.fiscalYear.name}
|
||||
{data.companyInfo.orgNumber && ` · Org.nr: ${data.companyInfo.orgNumber}`}
|
||||
@@ -148,11 +142,9 @@ export function INK2DeclarationView({ periodId }: { periodId: string }) {
|
||||
{downloadError}
|
||||
</div>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-start gap-2 rounded-lg border border-border bg-muted/30 p-3">
|
||||
<Info className="h-4 w-4 mt-1 shrink-0 text-muted-foreground" />
|
||||
<div className="text-sm text-muted-foreground space-y-2">
|
||||
<div className="mt-4 flex items-start gap-2">
|
||||
<Info className="mt-1 h-4 w-4 shrink-0 text-muted-foreground" aria-hidden="true" />
|
||||
<div className="text-[13px] leading-6 text-muted-foreground space-y-2">
|
||||
<p>
|
||||
INK2 visar det bokföringsmässiga resultatet baserat på din bokföring.
|
||||
</p>
|
||||
@@ -172,35 +164,31 @@ export function INK2DeclarationView({ periodId }: { periodId: string }) {
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Warnings */}
|
||||
{data.warnings.length > 0 && (
|
||||
<Card>
|
||||
<CardContent className="py-4">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertTriangle className="h-4 w-4 mt-1 shrink-0 text-warning" />
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium">
|
||||
{data.warnings.length}{' '}
|
||||
{data.warnings.length === 1 ? 'varning' : 'varningar'}
|
||||
</p>
|
||||
{data.warnings.map((warning, i) => (
|
||||
<p key={i} className="text-sm text-muted-foreground">{warning}</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="flex items-start gap-2 px-1">
|
||||
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-attn" aria-hidden="true" />
|
||||
<div className="space-y-1">
|
||||
<p className="text-[12.5px] font-medium leading-5 text-attn">
|
||||
{data.warnings.length}{' '}
|
||||
{data.warnings.length === 1 ? 'varning' : 'varningar'}
|
||||
</p>
|
||||
{data.warnings.map((warning, i) => (
|
||||
<p key={i} className="text-[12.5px] leading-5 text-muted-foreground">{warning}</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Assets section */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Tillgångar</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">Tillgångar</h3>
|
||||
<div className="h-px flex-1 bg-border/60" />
|
||||
</div>
|
||||
<div className="px-1 pt-2">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -228,15 +216,16 @@ export function INK2DeclarationView({ periodId }: { periodId: string }) {
|
||||
</tr>
|
||||
</tfoot>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Equity & Liabilities section */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Eget kapital och skulder</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">Eget kapital och skulder</h3>
|
||||
<div className="h-px flex-1 bg-border/60" />
|
||||
</div>
|
||||
<div className="px-1 pt-2">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -264,15 +253,16 @@ export function INK2DeclarationView({ periodId }: { periodId: string }) {
|
||||
</tr>
|
||||
</tfoot>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Income Statement section */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Resultaträkning</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">Resultaträkning</h3>
|
||||
<div className="h-px flex-1 bg-border/60" />
|
||||
</div>
|
||||
<div className="px-1 pt-2">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -314,15 +304,16 @@ export function INK2DeclarationView({ periodId }: { periodId: string }) {
|
||||
</tr>
|
||||
</tfoot>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* INK2S summary */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">INK2S: Skattemässiga justeringar</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">INK2S: Skattemässiga justeringar</h3>
|
||||
<div className="h-px flex-1 bg-border/60" />
|
||||
</div>
|
||||
<div className="px-1 pt-2">
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
Grundläggande justeringar och de återläggningar som sparats i bokslutet
|
||||
beräknas automatiskt. Kontrollera övriga deklarationsjusteringar med din
|
||||
@@ -384,8 +375,8 @@ export function INK2DeclarationView({ periodId }: { periodId: string }) {
|
||||
</tr>
|
||||
</tfoot>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Table, TableBody, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
@@ -108,26 +107,22 @@ export function NEDeclarationView({ periodId }: { periodId: string }) {
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-6 flex flex-wrap items-center gap-3 text-destructive">
|
||||
<AlertCircle className="h-4 w-4 shrink-0" />
|
||||
<span className="text-sm">{error}</span>
|
||||
<Button variant="outline" onClick={() => setRetryKey((k) => k + 1)}>
|
||||
Försök igen
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="flex flex-wrap items-center gap-3 py-4 text-destructive">
|
||||
<AlertCircle className="h-4 w-4 shrink-0" />
|
||||
<span className="text-sm">{error}</span>
|
||||
<Button variant="outline" onClick={() => setRetryKey((k) => k + 1)}>
|
||||
Försök igen
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (loading && !data) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-6 space-y-4">
|
||||
<Skeleton className="h-5 w-48" />
|
||||
<Skeleton className="h-64" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-5 w-48" />
|
||||
<Skeleton className="h-64" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -138,11 +133,10 @@ export function NEDeclarationView({ periodId }: { periodId: string }) {
|
||||
className={`space-y-8 transition-opacity duration-150 ${loading ? 'opacity-60' : ''}`}
|
||||
>
|
||||
{/* Header: company, year, and the filing artifact */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="px-1">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<CardTitle className="text-base">NE-bilaga (Enskild firma)</CardTitle>
|
||||
<h3 className="font-sans text-sm font-medium">NE-bilaga (Enskild firma)</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{data.companyInfo.companyName} · {data.fiscalYear.name}
|
||||
{data.companyInfo.orgNumber && ` · Org.nr: ${data.companyInfo.orgNumber}`}
|
||||
@@ -159,35 +153,31 @@ export function NEDeclarationView({ periodId }: { periodId: string }) {
|
||||
{downloadError}
|
||||
</div>
|
||||
)}
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Warnings */}
|
||||
{data.warnings.length > 0 && (
|
||||
<Card>
|
||||
<CardContent className="py-4">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertTriangle className="h-4 w-4 mt-1 shrink-0 text-warning" />
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium">
|
||||
{data.warnings.length}{' '}
|
||||
{data.warnings.length === 1 ? 'varning' : 'varningar'}
|
||||
</p>
|
||||
{data.warnings.map((warning, i) => (
|
||||
<p key={i} className="text-sm text-muted-foreground">{warning}</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="flex items-start gap-2 px-1">
|
||||
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-attn" aria-hidden="true" />
|
||||
<div className="space-y-1">
|
||||
<p className="text-[12.5px] font-medium leading-5 text-attn">
|
||||
{data.warnings.length}{' '}
|
||||
{data.warnings.length === 1 ? 'varning' : 'varningar'}
|
||||
</p>
|
||||
{data.warnings.map((warning, i) => (
|
||||
<p key={i} className="text-[12.5px] leading-5 text-muted-foreground">{warning}</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Revenue section */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Intäkter</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">Intäkter</h3>
|
||||
<div className="h-px flex-1 bg-border/60" />
|
||||
</div>
|
||||
<div className="px-1 pt-2">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -217,15 +207,16 @@ export function NEDeclarationView({ periodId }: { periodId: string }) {
|
||||
</tr>
|
||||
</tfoot>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Expenses section */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Kostnader</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">Kostnader</h3>
|
||||
<div className="h-px flex-1 bg-border/60" />
|
||||
</div>
|
||||
<div className="px-1 pt-2">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -261,27 +252,21 @@ export function NEDeclarationView({ periodId }: { periodId: string }) {
|
||||
</tr>
|
||||
</tfoot>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Result */}
|
||||
<Card>
|
||||
<CardContent className="py-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<span className="font-mono text-xs bg-muted px-1 rounded mr-2">R11</span>
|
||||
<span className="font-display text-xl">Årets resultat</span>
|
||||
</div>
|
||||
<span
|
||||
className={`font-display text-2xl tabular-nums ${
|
||||
data.rutor.R11 >= 0 ? 'text-success' : 'text-destructive'
|
||||
}`}
|
||||
>
|
||||
{formatWholeKronor(data.rutor.R11)}
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/* Result: the emphasized document foot (house skv-foot idiom). */}
|
||||
<div className="flex items-baseline gap-3 border-t-2 border-foreground/80 px-1 pt-3">
|
||||
<span className="font-mono text-xs text-muted-foreground">R11</span>
|
||||
<span className="flex-1 text-sm font-medium">Årets resultat</span>
|
||||
<span
|
||||
className={`text-sm font-semibold tabular-nums ${
|
||||
data.rutor.R11 >= 0 ? '' : 'text-destructive'
|
||||
}`}
|
||||
>
|
||||
{formatWholeKronor(data.rutor.R11)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -946,7 +946,7 @@ function SkatteverketPanelInner({
|
||||
{/* Validation results from Skatteverket */}
|
||||
{kontroller.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<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">
|
||||
Skatteverkets valideringsresultat
|
||||
</h3>
|
||||
{kontroller.map((k, i) => (
|
||||
|
||||
@@ -316,10 +316,7 @@ export function VatChecksCard({
|
||||
) : (
|
||||
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-attn" aria-hidden="true" />
|
||||
)}
|
||||
<div>
|
||||
<span className="mr-2 font-mono text-xs text-muted-foreground">{c.code}</span>
|
||||
{c.message}
|
||||
</div>
|
||||
<div>{c.message}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -327,12 +324,15 @@ export function VatChecksCard({
|
||||
|
||||
{showGapWorklist && (
|
||||
<div className="space-y-3">
|
||||
<h3
|
||||
className="text-sm font-medium uppercase tracking-wider text-muted-foreground"
|
||||
aria-live="polite"
|
||||
>
|
||||
Verifikationer som saknar basbelopp ({gaps.length})
|
||||
</h3>
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
<h3
|
||||
className="font-sans text-xs font-medium uppercase tracking-wider text-muted-foreground"
|
||||
aria-live="polite"
|
||||
>
|
||||
Verifikationer som saknar basbelopp ({gaps.length})
|
||||
</h3>
|
||||
<div className="h-px flex-1 bg-border/60" />
|
||||
</div>
|
||||
|
||||
{gapsLoading ? (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
|
||||
@@ -21,7 +21,8 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { FiscalYearSelector } from '@/components/common/FiscalYearSelector'
|
||||
import { FyPicker } from '@/components/common/FyPicker'
|
||||
import { ContextPicker } from '@/components/common/ContextPicker'
|
||||
import { cn, formatDate } from '@/lib/utils'
|
||||
import { roundOre } from '@/lib/money'
|
||||
import { formatVoucher } from '@/lib/bookkeeping/voucher-series-resolver'
|
||||
@@ -1671,35 +1672,29 @@ export function VatDeclarationView() {
|
||||
return (
|
||||
<VatDrillContext.Provider value={{ fiscalPeriodId: isYearly ? fiscalPeriodId : undefined }}>
|
||||
<div className="space-y-8">
|
||||
{/* XML and PDF live in "Lämna in" below: they are filing artifacts, not
|
||||
report exports, and one home avoids two competing download surfaces. */}
|
||||
<ReportExportMenu
|
||||
variant="default"
|
||||
items={[
|
||||
{ format: 'xlsx', href: `/api/reports/vat-declaration/xlsx?${vatQueryString()}` },
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Period selection — the declaration below follows it automatically.
|
||||
Flat toolbar row (concept language), far right like a context picker. */}
|
||||
{/* One flat toolbar row (concept language): period pickers + Exportera
|
||||
far right. XML and PDF live in "Lämna in" below: they are filing
|
||||
artifacts, not report exports, and one home avoids two competing
|
||||
download surfaces. */}
|
||||
<div className="flex flex-wrap items-center justify-end gap-2">
|
||||
<Select
|
||||
{/* Cadence lives behind a settings-style "Period" chip: the
|
||||
concrete period chip next to it already shows the cadence
|
||||
("Kvartal 2" implies quarterly, "Räkenskapsår ..." yearly). */}
|
||||
<ContextPicker
|
||||
items={[
|
||||
{ id: 'monthly', label: 'Månadsvis' },
|
||||
{ id: 'quarterly', label: 'Kvartalsvis' },
|
||||
{ id: 'yearly', label: 'Årsvis' },
|
||||
]}
|
||||
value={periodType}
|
||||
onValueChange={(value) => handlePeriodTypeChange(value as VatPeriodType)}
|
||||
>
|
||||
<SelectTrigger aria-label="Periodicitet" className="h-9 w-36">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="monthly">Månadsvis</SelectItem>
|
||||
<SelectItem value="quarterly">Kvartalsvis</SelectItem>
|
||||
<SelectItem value="yearly">Årsvis</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
onChange={(value) => handlePeriodTypeChange(value as VatPeriodType)}
|
||||
triggerLabel="Period"
|
||||
ariaLabel="Periodicitet"
|
||||
/>
|
||||
{isYearly ? (
|
||||
// Annual VAT covers a räkenskapsår: picked here, not a
|
||||
// calendar year.
|
||||
<FiscalYearSelector
|
||||
<FyPicker
|
||||
value={fiscalPeriodId || null}
|
||||
onChange={(id, fp) => {
|
||||
setFiscalPeriodId(id || '')
|
||||
@@ -1710,35 +1705,35 @@ export function VatDeclarationView() {
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Select value={String(year)} onValueChange={(value) => setYear(parseInt(value))}>
|
||||
<SelectTrigger aria-label="År" className="h-9 w-24 tabular-nums">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{yearOptions.map((y) => (
|
||||
<SelectItem key={y} value={String(y)} className="tabular-nums">
|
||||
{y}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
<ContextPicker
|
||||
items={yearOptions.map((y) => ({ id: String(y), label: String(y) }))}
|
||||
value={String(year)}
|
||||
onChange={(value) => setYear(parseInt(value))}
|
||||
triggerLabel={String(year)}
|
||||
ariaLabel="År"
|
||||
className="tabular-nums"
|
||||
/>
|
||||
<ContextPicker
|
||||
items={getPeriodOptions().map((opt) => ({
|
||||
id: String(opt.value),
|
||||
label: opt.label,
|
||||
}))}
|
||||
value={String(period)}
|
||||
onValueChange={(value) => setPeriod(parseInt(value))}
|
||||
>
|
||||
<SelectTrigger aria-label="Period" className="h-9 w-44">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{getPeriodOptions().map((opt) => (
|
||||
<SelectItem key={opt.value} value={String(opt.value)}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
onChange={(value) => setPeriod(parseInt(value))}
|
||||
triggerLabel={
|
||||
getPeriodOptions().find((opt) => opt.value === period)?.label ??
|
||||
String(period)
|
||||
}
|
||||
ariaLabel="Redovisningsperiod"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<ReportExportMenu
|
||||
variant="default"
|
||||
items={[
|
||||
{ format: 'xlsx', href: `/api/reports/vat-declaration/xlsx?${vatQueryString()}` },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
|
||||
Reference in New Issue
Block a user