* fix(skatteverket): request the ska scope for skattekonto v2 The skattekonto v2 API rejects skahmst-only tokens with 403 "The required scopes are not authorized" (observed in prod 2026-07-20; no company has synced since 2026-05-10). The requested `skattekonto` scope is silently dropped from every grant, while `ska` appears in one real May grant, so request it too: SKV grants the intersection, so this is harmless if wrong. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(skatteverket): correct the skattekonto scope model around ska Root cause of the May 10 skattekonto outage, confirmed via git history and prod token data: the `ska` scope (the interactive skattekonto API's actual scope, requested since the extension's first commit in March) was removed by the "remove unused scopes" cleanup in the #431 series. Every token issued after that hour lacks it and the API answers 403 "The required scopes are not authorized"; no company has synced since. The May 15 repair re-added skahmst, which per its tjanstebeskrivning is a different bulk E-transport service and does not substitute; `skattekonto` is not a real SKV scope name and is silently dropped from grants. Follow-up to the ska re-request (cd8f7a30): - document the confirmed scope model in oauth.ts so ska is never "cleaned up" again - panel missing-scope warning and reconnect-button now gate on ska, not skahmst/skattekonto - scope badge labels: ska takes the saldo & transaktioner label, skahmst relabeled as the E-transport file service - consent-page note covers both terse scope names and says ska is required Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(year-end): warn on untaxed profit at verkstall, Swedish readiness messages, always-visible period selector An aktiebolag could execute year-end with a profit and zero bolagsskatt booked without any warning (support case: closing moved 592k to 2099 untaxed). The preview now computes bolagsskattMissing (AB + profit + no 89xx account among closed accounts, 8999 excluded) and both the preview and execute steps render an advisory, bypassable warning. validateYearEndReadiness messages are now Swedish (the bokslut wizard is a stays-Swedish surface); the MCP year_end_readiness classifier matches both the new Swedish strings and the legacy English ones. The wizard period selector now always renders, keeps a selected-but- ineligible period selectable, and resets a stale ?period= id from another company instead of leaving the user stuck on the wrong year. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(year-end): administrative undo of an executed year-end closing Storno-only reset used when a bokslut was executed prematurely (e.g. without bolagsskatt) and no arsredovisning exists yet: reverses the next period's result_appropriation and opening_balance entries, reopens the period, reverses the closing entry, and detaches closing_entry_id. Resumable if interrupted midway; attribution per BFL 5 kap 6. Migration 20260720140000 adds the trigger escape hatch: closing_entry_id may only change once set when the old closing entry is reversed with a posted storno chain (status flag alone is forgeable via PostgREST), and a non-NULL replacement must be a posted year_end entry in the same period. Covered by a pg-real test. planResultAppropriation idempotency is now posted-only: a reversed omforing no longer blocks the re-run from posting a fresh 2099 -> 2098 reclassification (it previously returned null silently, leaving the new year's equity polluted). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(review): address CodeRabbit, PR-Agent and compliance findings - undo script: company_id filters on verify queries, period-scope the arsredovisning precondition checks, validate service-key format, escalate audit_log insert failure to a hard error (BFNAR 2013:2) - detach migration: company-scope the storno chain EXISTS, replace the em dash in the new error message Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(review): address round-2 compliance swarm and Swedish review findings - undo script: require --confirm-url with --commit so an env swap fails loud; retry the audit_log insert 3x and direct the operator to insert the behandlingshistorik row manually on final failure (BFNAR 2013:2) - year-end preview: document why resultAccountSummary is a complete 89xx scan; warning text now also names periodiseringsfond and overavskrivningar as legitimate zero-tax reasons Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
178 lines
6.3 KiB
TypeScript
178 lines
6.3 KiB
TypeScript
'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,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '@/components/ui/table'
|
|
import { formatCurrency } from '@/lib/utils'
|
|
import type { YearEndPreview } from '@/types'
|
|
|
|
interface PreviewStepProps {
|
|
preview: YearEndPreview | null
|
|
isLoading: boolean
|
|
error: string | null
|
|
onBack: () => void
|
|
onContinue: () => void
|
|
}
|
|
|
|
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>
|
|
)
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<p className="text-destructive">{error}</p>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
if (!preview) return null
|
|
|
|
const isProfit = preview.netResult > 0
|
|
const isLoss = preview.netResult < 0
|
|
|
|
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>
|
|
|
|
{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>
|
|
)}
|
|
|
|
{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>
|
|
<div className="grid grid-cols-3 gap-4 text-sm">
|
|
<div>
|
|
<p className="text-muted-foreground text-xs uppercase tracking-wider">Kursvinst</p>
|
|
<p className="font-display text-xl tabular-nums">
|
|
{formatCurrency(preview.currencyRevaluation.totalGain)}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-muted-foreground text-xs uppercase tracking-wider">Kursförlust</p>
|
|
<p className="font-display text-xl tabular-nums">
|
|
{formatCurrency(preview.currencyRevaluation.totalLoss)}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-muted-foreground text-xs uppercase tracking-wider">Nettoeffekt</p>
|
|
<p className="font-display text-xl tabular-nums">
|
|
{formatCurrency(preview.currencyRevaluation.netEffect)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
<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">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Konto</TableHead>
|
|
<TableHead>Beskrivning</TableHead>
|
|
<TableHead className="text-right">Debet</TableHead>
|
|
<TableHead className="text-right">Kredit</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{preview.closingLines.map((line, i) => (
|
|
<TableRow key={i}>
|
|
<TableCell className="tabular-nums">{line.account_number}</TableCell>
|
|
<TableCell className="text-sm">{line.line_description}</TableCell>
|
|
<TableCell className="text-right tabular-nums">
|
|
{line.debit_amount > 0 ? formatCurrency(line.debit_amount) : '-'}
|
|
</TableCell>
|
|
<TableCell className="text-right tabular-nums">
|
|
{line.credit_amount > 0 ? formatCurrency(line.credit_amount) : '-'}
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<div className="flex justify-between">
|
|
<Button variant="outline" onClick={onBack}>
|
|
Tillbaka
|
|
</Button>
|
|
<Button onClick={onContinue}>
|
|
Verkställ bokslut <ArrowRight className="ml-1 h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|