Files
accounted/components/reports/SkatteverketPanel.tsx
T
MattssonandClaude Opus 4.7 980f29dae8 Bug/momsdeklaration skv (#449)
* fix(salary): show birthdate in masked personnummer, hide the 4-digit suffix

Flip the personnummer display format from XXXXXXXX-NNNN to YYYYMMDD-XXXX so
the sensitive 4-digit suffix is hidden while the (public) birthdate stays
visible. Affects the employees list/detail, salary run, payslip PDF, payslip
email, and the MCP server tools (list_employees, get_salary_run). Each call
site now decrypts the stored personnummer before masking.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(transactions): allow deleting unbooked transactions from "Alla transaktioner"

The history list only let users delete via the inbox card; once a category or
mall was picked but the verifikation hadn't been created, the row showed
"Ej bokförd" with no way to remove it. The API already permits delete while
journal_entry_id is null, so the gap was purely a missing UI affordance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(vat): populate ruta 20-24 for reverse charge + dishonest "Validera OK"

Three connected issues caused Skatteverket to reject momsdeklarationer
with FK004 even after our local "Validera"-knapp returned OK.

1. supplier-invoice-entries booked fiktiv moms (2614/2624/2634 + 2645/2647)
   on reverse-charge invoices but never the underlying basbelopp on 44xx/45xx.
   Ruta 30-32 filled up at SKV while ruta 20-24 stayed at 0 — SKV's FK004
   ("silent netting prohibited", ML 13 kap kräver båda sidor).

   Fix: generateReverseChargeBasisLines in vat-entries.ts emits parallel
   45xx/44xx debit + 4598 motkonto credit per rate group. Engine calls it
   from registration, cash, and credit-note paths. Skipped when the user
   booked the expense directly on a basis account to avoid double-counting.
   4598 added to BAS reference (no migration needed; account_number is
   plain text on journal_entry_lines).

2. rutorToMomsuppgift rounded each ruta independently but computed
   summaMoms from the unrounded ruta49. SKV recomputes the sum from
   integer rutor on their side, so fractional öres caused ±1 SEK drift
   and SKV rejected with FK009.

   Fix: derive summaMoms from the already-rounded VAT-amount rutor.

3. "Validera"-knappen only confirmed SKV's internal arithmetic — a
   declaration with ruta 30-32 populated and ruta 20-24 empty validated
   fine until /utkast hit FK004. Users got a false green light.

   Fix: vat-declaration-checks.ts runs locally before the SKV call,
   blocks Validera/Spara when ERROR-level findings exist, and surfaces
   them in a separate "Lokala kontroller"-section. Success message
   reworded so SKV's OK is no longer presented as filing-ready.

Tests: 4535/4536/4531/4425 lines + 4598 motkonto on EU/non-EU/byggtjänster
RC, credit-note reversal, fractional-öres summaMoms, all four pre-flight
codes (RC_BASIS_MISSING, RC_OUTPUT_MISSING, RC_INPUT_VAT_MISMATCH,
SUMMA_MOMS_DRIFT).

Backfill for already-posted entries follows in the next commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: add skattekonto matching functionality

- Enhance TransactionInboxCard to display a warning for potential 1930↔1630 transfers.
- Implement match suggestions for skattekonto transactions in the backend.
- Create SkattekontoMatchDialog component for linking skattekonto rows to existing journal entries.
- Develop SkattekontoInboxCard component to handle skattekonto transactions in the inbox.
- Introduce skattekonto-match utility functions for candidate matching and linking.
- Update types to include match suggestions and enriched transaction responses.

* refactor: reorganize skattekonto types and implement bank counterpart matching logic

* docs: update CLAUDE.md to streamline integrations and clarify architecture details

* refactor: enhance reverse charge logic to handle non-basis accounts and prevent double-counting

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:04:48 +02:00

741 lines
24 KiB
TypeScript

'use client'
import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions'
import React, { useState, useEffect, useCallback } from 'react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import {
AlertCircle,
CheckCircle2,
Download,
ExternalLink,
Gavel,
Link2,
Link2Off,
Loader2,
FileCheck,
Lock,
Unlock,
Send,
ShieldAlert,
Trash2,
} from 'lucide-react'
import type { VatDeclarationRutor, VatPeriodType } from '@/types'
import { formatRedovisare, formatRedovisningsperiod } from '@/lib/skatteverket/format'
import {
runVatDeclarationChecks,
type VatDeclarationCheck,
} from '@/lib/reports/vat-declaration-checks'
interface SkatteverketStatus {
connected: boolean
expired?: boolean
canRefresh?: boolean
scope?: string
expiresAt?: string
}
// Shape per Skatteverket Momsdeklaration v1.0.24 RAML
// (kontrollResultat.resultat[].{kod, status, beskrivning})
interface KontrollResult {
kod: string
status: 'ERROR' | 'WARNING'
beskrivning: string
}
interface SkatteverketPanelProps {
periodType: VatPeriodType
year: number
period: number
hasData: boolean
/**
* Calculated rutor for the current period. Used to run local pre-flight
* checks before Skatteverket sees the payload — SKV only validates internal
* arithmetic consistency, so we have to catch "ruta 30-32 present but
* 20-24 empty" locally before letting the user submit.
*/
rutor?: VatDeclarationRutor | null
}
function formatAmount(amount: number): string {
return amount.toLocaleString('sv-SE', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
const SKV_ENABLED = ENABLED_EXTENSION_IDS.has('skatteverket')
export function SkatteverketPanel(props: SkatteverketPanelProps) {
if (!SKV_ENABLED) return null
return <SkatteverketPanelInner {...props} />
}
function SkatteverketPanelInner({ periodType, year, period, hasData, rutor }: SkatteverketPanelProps) {
const [status, setStatus] = useState<SkatteverketStatus | null>(null)
const [loading, setLoading] = useState(true)
const [actionLoading, setActionLoading] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState<string | null>(null)
const [kontroller, setKontroller] = useState<KontrollResult[]>([])
const [signeringslank, setSigneringslank] = useState<string | null>(null)
const [submitted, setSubmitted] = useState<{
kvittensnummer?: string
tidpunkt?: string
} | null>(null)
// Local sanity checks against the calculated declaration, run before any
// SKV call. SKV's "OK" only confirms arithmetic — these checks confirm
// the declaration looks plausible (no orphaned RC output, no missing
// basis, no summaMoms drift).
const localChecks: VatDeclarationCheck[] = rutor ? runVatDeclarationChecks(rutor) : []
const localErrors = localChecks.filter((c) => c.status === 'ERROR')
const localBlocked = localErrors.length > 0
const fetchStatus = useCallback(async () => {
try {
const res = await fetch('/api/extensions/ext/skatteverket/status')
if (res.ok) {
const data = await res.json()
setStatus(data)
}
} catch {
// Extension might not be enabled
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
fetchStatus()
// Check URL params for OAuth callback results
const params = new URLSearchParams(window.location.search)
if (params.get('skv_connected') === 'true') {
setSuccess('Ansluten till Skatteverket')
fetchStatus()
// Clean URL
const url = new URL(window.location.href)
url.searchParams.delete('skv_connected')
window.history.replaceState({}, '', url.toString())
}
const skvError = params.get('skv_error')
if (skvError) {
setError(decodeURIComponent(skvError))
const url = new URL(window.location.href)
url.searchParams.delete('skv_error')
window.history.replaceState({}, '', url.toString())
}
}, [fetchStatus])
const handleConnect = () => {
window.location.href = '/api/extensions/ext/skatteverket/authorize'
}
const handleDisconnect = async () => {
setActionLoading('disconnect')
setError(null)
try {
const res = await fetch('/api/extensions/ext/skatteverket/disconnect', {
method: 'POST',
})
if (res.ok) {
setStatus({ connected: false })
setSuccess(null)
setKontroller([])
setSigneringslank(null)
setSubmitted(null)
}
} catch {
setError('Kunde inte koppla bort')
} finally {
setActionLoading(null)
}
}
const handleValidate = async () => {
if (localBlocked) {
setError(
'Lokala kontroller hittade fel i bokföringen. Åtgärda dessa innan ' +
'du skickar till Skatteverket.',
)
return
}
setActionLoading('validate')
setError(null)
setKontroller([])
try {
const res = await fetch('/api/extensions/ext/skatteverket/declaration/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ periodType, year, period }),
})
const result = await res.json()
if (result.error) {
setError(result.error)
} else {
const controls: KontrollResult[] = result.data?.kontrollResultat?.resultat || []
setKontroller(controls)
if (controls.length === 0) {
// SKV's OK only confirms arithmetic — it does NOT confirm that the
// declaration is materially correct. We say so explicitly so the
// user doesn't read this as a green light for actual filing.
setSuccess(
'Skatteverket har inga tekniska invändningar mot deklarationen. ' +
'Kontrollera siffrorna i förhandsgranskningen innan du skickar in.',
)
} else {
const errors = controls.filter(k => k.status === 'ERROR')
if (errors.length > 0) {
setError(`${errors.length} valideringsfel hittades`)
} else {
setSuccess('Skatteverket har inga tekniska invändningar (med varningar)')
}
}
}
} catch {
setError('Kunde inte validera deklarationen')
} finally {
setActionLoading(null)
}
}
const handleSaveDraft = async () => {
if (localBlocked) {
setError(
'Lokala kontroller hittade fel i bokföringen. Åtgärda dessa innan ' +
'du sparar utkastet hos Skatteverket.',
)
return
}
setActionLoading('draft')
setError(null)
try {
const res = await fetch('/api/extensions/ext/skatteverket/declaration/draft', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ periodType, year, period }),
})
const result = await res.json()
if (result.error) {
setError(result.error)
} else {
const controls: KontrollResult[] = result.data?.kontrollResultat?.resultat || []
setKontroller(controls)
const errors = controls.filter(k => k.status === 'ERROR')
if (errors.length === 0) {
setSuccess('Utkast sparat i Eget utrymme hos Skatteverket')
} else {
setError(`Utkastet sparades men har ${errors.length} valideringsfel`)
}
}
} catch {
setError('Kunde inte spara utkast')
} finally {
setActionLoading(null)
}
}
const handleLock = async () => {
setActionLoading('lock')
setError(null)
try {
const res = await fetch(
`/api/extensions/ext/skatteverket/declaration/lock?redovisare=${encodeURIComponent(
await getRedovisare()
)}&redovisningsperiod=${getRedovisningsperiod()}`,
{ method: 'PUT' }
)
const result = await res.json()
if (result.error) {
setError(result.error)
} else if (result.data?.signeringsLank) {
setSigneringslank(result.data.signeringsLank)
setSuccess('Utkastet är låst. Öppna signeringslänken för att signera med BankID.')
}
} catch {
setError('Kunde inte låsa utkastet')
} finally {
setActionLoading(null)
}
}
const handleUnlock = async () => {
setActionLoading('unlock')
setError(null)
try {
const res = await fetch(
`/api/extensions/ext/skatteverket/declaration/lock?redovisare=${encodeURIComponent(
await getRedovisare()
)}&redovisningsperiod=${getRedovisningsperiod()}`,
{ method: 'DELETE' }
)
const result = await res.json()
if (result.error) {
setError(result.error)
} else {
setSigneringslank(null)
setSuccess('Utkastet har låsts upp')
}
} catch {
setError('Kunde inte låsa upp utkastet')
} finally {
setActionLoading(null)
}
}
const handleCheckSubmitted = async () => {
setActionLoading('check')
setError(null)
try {
const res = await fetch(
`/api/extensions/ext/skatteverket/declaration/submitted?redovisare=${encodeURIComponent(
await getRedovisare()
)}&redovisningsperiod=${getRedovisningsperiod()}`
)
const result = await res.json()
if (result.error) {
setError(result.error)
} else if (result.data) {
setSubmitted(result.data)
setSuccess('Deklarationen har lämnats in')
} else {
setSuccess('Ingen inlämnad deklaration hittades för denna period')
}
} catch {
setError('Kunde inte kontrollera inlämningsstatus')
} finally {
setActionLoading(null)
}
}
const handleDeleteDraft = async () => {
setActionLoading('delete')
setError(null)
try {
const res = await fetch(
`/api/extensions/ext/skatteverket/declaration/draft?redovisare=${encodeURIComponent(
await getRedovisare()
)}&redovisningsperiod=${getRedovisningsperiod()}`,
{ method: 'DELETE' }
)
if (res.status === 204 || res.ok) {
setKontroller([])
setSigneringslank(null)
setSuccess('Utkastet har raderats från Eget utrymme')
} else {
const result = await res.json().catch(() => ({}))
setError(result.error || `Kunde inte radera utkast (${res.status})`)
}
} catch {
setError('Kunde inte radera utkast')
} finally {
setActionLoading(null)
}
}
const handleFetchDraft = async () => {
setActionLoading('fetchDraft')
setError(null)
try {
const res = await fetch(
`/api/extensions/ext/skatteverket/declaration/draft?redovisare=${encodeURIComponent(
await getRedovisare()
)}&redovisningsperiod=${getRedovisningsperiod()}`
)
const result = await res.json()
if (result.error) {
setError(result.error)
} else if (!result.data) {
setSuccess('Inget sparat utkast hittades för perioden')
} else {
const locked = result.data?.locked ? ' (låst)' : ''
const summa = result.data?.momsuppgift?.summaMoms
const summaLabel = summa !== undefined ? `, summaMoms = ${formatAmount(summa)}` : ''
setSuccess(`Sparat utkast hittades${locked}${summaLabel}`)
}
} catch {
setError('Kunde inte hämta utkast')
} finally {
setActionLoading(null)
}
}
const handleFetchDecided = async () => {
setActionLoading('fetchDecided')
setError(null)
try {
const res = await fetch(
`/api/extensions/ext/skatteverket/declaration/decided?redovisare=${encodeURIComponent(
await getRedovisare()
)}&redovisningsperiod=${getRedovisningsperiod()}`
)
const result = await res.json()
if (result.error) {
setError(result.error)
} else if (!result.data) {
setSuccess('Inget beslut hittades för perioden')
} else {
const tid = result.data?.beslutadTidpunkt
const tidLabel = tid ? ` (beslutad ${new Date(tid).toLocaleDateString('sv-SE')})` : ''
setSuccess(`Beslut hittades${tidLabel}`)
}
} catch {
setError('Kunde inte hämta beslutade uppgifter')
} finally {
setActionLoading(null)
}
}
// Helper to get redovisare from settings
const getRedovisare = async (): Promise<string> => {
const res = await fetch('/api/settings')
const { data } = await res.json()
if (!data?.org_number) throw new Error('Organisationsnummer saknas')
return formatRedovisare(data.org_number, data.entity_type)
}
const getRedovisningsperiod = (): string => {
return formatRedovisningsperiod(periodType, year, period)
}
if (loading) {
return (
<Card>
<CardContent className="p-6 flex items-center gap-2 text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" />
Kontrollerar Skatteverket-anslutning...
</CardContent>
</Card>
)
}
// Not connected
if (!status?.connected) {
return (
<Card>
<CardHeader>
<CardTitle className="text-lg flex items-center gap-2">
<FileCheck className="h-5 w-5" />
Skicka till Skatteverket
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{error && (
<div className="flex items-start gap-2 text-sm text-destructive bg-destructive/5 rounded-lg p-3">
<AlertCircle className="h-4 w-4 mt-0.5 shrink-0" />
<span>{error}</span>
</div>
)}
<p className="text-sm text-muted-foreground">
Anslut till Skatteverket med BankID för att skicka momsdeklarationen direkt.
</p>
<Button onClick={handleConnect} className="gap-2">
<Link2 className="h-4 w-4" />
Anslut med BankID
</Button>
</CardContent>
</Card>
)
}
// Connected — show actions
const hasErrors = kontroller.some(k => k.status === 'ERROR')
const hasWarnings = kontroller.some(k => k.status === 'WARNING')
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="text-lg flex items-center gap-2">
<FileCheck className="h-5 w-5" />
Skicka till Skatteverket
</CardTitle>
<div className="flex items-center gap-2">
<Badge variant="outline" className="text-success border-success/30 gap-1">
<CheckCircle2 className="h-3 w-3" />
Ansluten
</Badge>
{status.expired && (
<Badge variant="destructive" className="gap-1">
<AlertCircle className="h-3 w-3" />
Session utgången
</Badge>
)}
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
{/* Messages */}
{error && (
<div className="flex items-start gap-2 text-sm text-destructive bg-destructive/5 rounded-lg p-3">
<AlertCircle className="h-4 w-4 mt-0.5 shrink-0" />
<span>{error}</span>
</div>
)}
{success && !error && (
<div className="flex items-start gap-2 text-sm text-success bg-success/5 rounded-lg p-3">
<CheckCircle2 className="h-4 w-4 mt-0.5 shrink-0" />
<span>{success}</span>
</div>
)}
{/* Local pre-flight check results — surfaced separately from SKV's
kontroller so the user knows these are gnubok's own sanity checks,
not Skatteverket's. ERRORs block the submit/validate buttons. */}
{localChecks.length > 0 && (
<div className="space-y-1.5">
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
Lokala kontroller
</p>
{localChecks.map((c, i) => (
<div
key={`${c.code}-${i}`}
className={`flex items-start gap-2 text-sm rounded-lg p-2.5 ${
c.status === 'ERROR'
? 'bg-destructive/5 text-destructive'
: 'bg-amber-50 text-amber-800 dark:bg-amber-950/30 dark:text-amber-200'
}`}
>
{c.status === 'ERROR' ? (
<ShieldAlert className="h-4 w-4 mt-0.5 shrink-0" />
) : (
<AlertCircle className="h-4 w-4 mt-0.5 shrink-0" />
)}
<div>
<span className="font-mono text-xs mr-1.5">{c.code}</span>
{c.message}
</div>
</div>
))}
</div>
)}
{/* Validation results from Skatteverket */}
{kontroller.length > 0 && (
<div className="space-y-1.5">
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
Skatteverkets valideringsresultat
</p>
{kontroller.map((k, i) => (
<div
key={`${k.kod}-${i}`}
className={`flex items-start gap-2 text-sm rounded-lg p-2.5 ${
k.status === 'ERROR'
? 'bg-destructive/5 text-destructive'
: 'bg-amber-50 text-amber-800 dark:bg-amber-950/30 dark:text-amber-200'
}`}
>
{k.status === 'ERROR' ? (
<ShieldAlert className="h-4 w-4 mt-0.5 shrink-0" />
) : (
<AlertCircle className="h-4 w-4 mt-0.5 shrink-0" />
)}
<div>
<span className="font-mono text-xs mr-1.5">{k.kod}</span>
{k.beskrivning}
</div>
</div>
))}
</div>
)}
{/* Submitted confirmation */}
{submitted && (
<div className="rounded-lg border p-3 space-y-1">
<p className="text-sm font-medium flex items-center gap-1.5">
<CheckCircle2 className="h-4 w-4 text-success" />
Inlämnad
</p>
{submitted.kvittensnummer && (
<p className="text-xs text-muted-foreground">
Kvittensnummer: <span className="font-mono">{submitted.kvittensnummer}</span>
</p>
)}
{submitted.tidpunkt && (
<p className="text-xs text-muted-foreground">
Tidpunkt: {new Date(submitted.tidpunkt).toLocaleString('sv-SE')}
</p>
)}
</div>
)}
{/* Signing link */}
{signeringslank && (
<div className="rounded-lg border border-success/30 bg-success/5 p-3 space-y-2">
<p className="text-sm font-medium">Utkastet är låst och redo att signeras</p>
<p className="text-xs text-muted-foreground">
Öppna länken nedan och signera med BankID på Skatteverkets sida.
</p>
<Button
variant="outline"
size="sm"
className="gap-1.5"
onClick={() => window.open(signeringslank, '_blank')}
>
<ExternalLink className="h-3.5 w-3.5" />
Öppna signeringssidan
</Button>
</div>
)}
{/* Forward-lifecycle buttons */}
<div className="flex flex-wrap gap-2 pt-1">
<Button
variant="outline"
size="sm"
onClick={handleValidate}
disabled={!hasData || localBlocked || actionLoading !== null}
className="gap-1.5"
title={localBlocked ? 'Åtgärda lokala kontrollfel innan validering' : undefined}
>
{actionLoading === 'validate' ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<FileCheck className="h-3.5 w-3.5" />
)}
Validera
</Button>
<Button
variant="outline"
size="sm"
onClick={handleSaveDraft}
disabled={!hasData || localBlocked || actionLoading !== null}
className="gap-1.5"
title={localBlocked ? 'Åtgärda lokala kontrollfel innan inlämning' : undefined}
>
{actionLoading === 'draft' ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<Send className="h-3.5 w-3.5" />
)}
Spara utkast
</Button>
<Button
size="sm"
onClick={handleLock}
disabled={!hasData || hasErrors || actionLoading !== null}
className="gap-1.5"
title={hasErrors ? 'Valideringsfel måste åtgärdas först' : ''}
>
{actionLoading === 'lock' ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<Lock className="h-3.5 w-3.5" />
)}
Lås och signera
</Button>
</div>
{/* Read-only fetches from Skatteverket. */}
<div className="flex flex-wrap gap-2">
<Button
variant="ghost"
size="sm"
onClick={handleFetchDraft}
disabled={actionLoading !== null}
className="gap-1.5 text-muted-foreground"
title="Hämta sparat utkast från Eget utrymme"
>
{actionLoading === 'fetchDraft' ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<Download className="h-3.5 w-3.5" />
)}
Hämta utkast
</Button>
<Button
variant="ghost"
size="sm"
onClick={handleCheckSubmitted}
disabled={actionLoading !== null}
className="gap-1.5 text-muted-foreground"
title="Kontrollera om en signerad deklaration har lämnats in"
>
{actionLoading === 'check' ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<CheckCircle2 className="h-3.5 w-3.5" />
)}
Kontrollera inlämning
</Button>
<Button
variant="ghost"
size="sm"
onClick={handleFetchDecided}
disabled={actionLoading !== null}
className="gap-1.5 text-muted-foreground"
title="Hämta Skatteverkets beslut för perioden"
>
{actionLoading === 'fetchDecided' ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<Gavel className="h-3.5 w-3.5" />
)}
Hämta beslut
</Button>
</div>
{/* Recovery / cleanup buttons. Always visible when connected so
the user can back out of a locked or stale draft state without
depending on local UI state surviving a reload. SKV returns
404/409 if the action isn't applicable; we surface that as an
error message rather than hiding the button. */}
<div className="flex flex-wrap gap-2">
<Button
variant="outline"
size="sm"
onClick={handleUnlock}
disabled={actionLoading !== null}
className="gap-1.5 text-muted-foreground"
title="Lås upp en låst period så att utkastet kan ändras eller raderas"
>
{actionLoading === 'unlock' ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<Unlock className="h-3.5 w-3.5" />
)}
Lås upp
</Button>
<Button
variant="outline"
size="sm"
onClick={handleDeleteDraft}
disabled={actionLoading !== null}
className="gap-1.5 text-muted-foreground"
title="Radera sparat utkast från Skatteverkets Eget utrymme"
>
{actionLoading === 'delete' ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<Trash2 className="h-3.5 w-3.5" />
)}
Radera utkast
</Button>
</div>
{/* Disconnect */}
<div className="pt-2 border-t">
<Button
variant="ghost"
size="sm"
onClick={handleDisconnect}
disabled={actionLoading !== null}
className="gap-1.5 text-muted-foreground hover:text-destructive"
>
{actionLoading === 'disconnect' ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<Link2Off className="h-3.5 w-3.5" />
)}
Koppla bort Skatteverket
</Button>
</div>
</CardContent>
</Card>
)
}