feat: counterparty templates, Skatteverket extension, complete VAT form (#117)
* feat: separate AR/AP/accounting into distinct nav groups (#92) Split the flat "Finans" sidebar group into three visually distinct sections — Försäljning (AR), Inköp (AP), and Redovisning — so users coming from Fortnox immediately find customer invoicing and supplier invoices as top-level concepts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: journal entry detail view, correction chain, and account name display - Add journal entry detail page at /bookkeeping/[id] with full entry view - Add correction chain API and component showing storno relationships - Add JournalEntryStatusBadge component for entry status display - Show debit/credit account names in template picker and review dialogs - Expand client-side BAS account name mapping with additional accounts - Show account codes on transaction inbox suggestion buttons Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address review feedback — N+1 query, duplicate name, nav dedup - Batch reverse-lookup into single query per BFS iteration (was N+1) - Differentiate account 2393 from 2893 in display names - Extract shared loop for desktop/mobile nav group rendering Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: counterparty templates, Skatteverket extension, VAT form completeness, and UI cleanup - Add counterparty-based categorization templates (learned from user approvals and auto-ingestion) with fuzzy matching in the mapping engine - Add Skatteverket extension for direct VAT declaration submission via API - Complete VAT declaration form with all 30 SKV 4700 boxes (ruta 08, 35-42, 50, 60-62) - Fix ruta 49 formula to include import VAT (ruta 60+61+62) - Simplify dashboard UI: remove redundant icons from stat cards, customer cards, invoice list, supplier invoices; use Badge variants consistently - Add SkatteverketPanel component to reports page - Add categorization_templates and skatteverket_tokens migrations - Update tests and helpers for new types Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address PR review feedback — VAT detection, migration timestamps, dedup - Fix detectVatTreatment to derive actual rate (12%/6%) from VAT line description instead of hardcoding standard_25 - Rename skatteverket_tokens migration to 20260324120001 to avoid duplicate timestamp with categorization_templates (fixes Supabase deployment failure) - Make refreshAccessToken accept previousRefreshCount param to enforce refresh limit contract at the type level - Fix rate limiter TOCTOU by claiming slot before await - Extract formatRedovisare/formatRedovisningsperiod to shared lib/skatteverket/format.ts — eliminates duplication between mappers.ts and SkatteverketPanel.tsx Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
3050743013
commit
d8e0a22495
@@ -18,7 +18,7 @@ import AccountCombobox from '@/components/bookkeeping/AccountCombobox'
|
||||
import { proposePaymentLines } from '@/lib/bookkeeping/propose-payment-lines'
|
||||
import { formatCurrency } from '@/lib/utils'
|
||||
import { createClient } from '@/lib/supabase/client'
|
||||
import { Plus, Trash2, Loader2, CheckCircle2, AlertTriangle } from 'lucide-react'
|
||||
import { Plus, Trash2, Loader2 } from 'lucide-react'
|
||||
import type { FormLine } from '@/components/bookkeeping/JournalEntryForm'
|
||||
import type { Invoice, InvoiceItem, Customer, BASAccount, EntityType } from '@/types'
|
||||
|
||||
@@ -356,13 +356,11 @@ export default function PaymentBookingDialog({
|
||||
<div className="flex items-center justify-between border-t pt-3">
|
||||
<div className="flex items-center gap-2">
|
||||
{isBalanced ? (
|
||||
<Badge variant="secondary" className="bg-success/10 text-success gap-1">
|
||||
<CheckCircle2 className="h-3 w-3" />
|
||||
<Badge variant="secondary" className="bg-success/10 text-success">
|
||||
Debet = Kredit
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="destructive" className="gap-1">
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
<Badge variant="destructive">
|
||||
Obalanserad ({formatCurrency(Math.abs(totalDebit - totalCredit))})
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,527 @@
|
||||
'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,
|
||||
ExternalLink,
|
||||
Link2,
|
||||
Link2Off,
|
||||
Loader2,
|
||||
FileCheck,
|
||||
Lock,
|
||||
Unlock,
|
||||
Send,
|
||||
ShieldAlert,
|
||||
} from 'lucide-react'
|
||||
import type { VatPeriodType } from '@/types'
|
||||
import { formatRedovisare, formatRedovisningsperiod } from '@/lib/skatteverket/format'
|
||||
|
||||
interface SkatteverketStatus {
|
||||
connected: boolean
|
||||
expired?: boolean
|
||||
canRefresh?: boolean
|
||||
scope?: string
|
||||
expiresAt?: string
|
||||
}
|
||||
|
||||
interface KontrollResult {
|
||||
id: string
|
||||
typ: 'ERROR' | 'WARNING'
|
||||
text: string
|
||||
}
|
||||
|
||||
interface SkatteverketPanelProps {
|
||||
periodType: VatPeriodType
|
||||
year: number
|
||||
period: number
|
||||
hasData: boolean
|
||||
}
|
||||
|
||||
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 }: 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)
|
||||
|
||||
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 () => {
|
||||
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 = result.data?.kontrollresultat?.kontroller || []
|
||||
setKontroller(controls)
|
||||
if (controls.length === 0) {
|
||||
setSuccess('Valideringen godkänd — inga fel eller varningar')
|
||||
} else {
|
||||
const errors = controls.filter((k: KontrollResult) => k.typ === 'ERROR')
|
||||
if (errors.length > 0) {
|
||||
setError(`${errors.length} valideringsfel hittades`)
|
||||
} else {
|
||||
setSuccess('Valideringen godkänd med varningar')
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
setError('Kunde inte validera deklarationen')
|
||||
} finally {
|
||||
setActionLoading(null)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSaveDraft = async () => {
|
||||
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 = result.data?.kontrollresultat?.kontroller || []
|
||||
setKontroller(controls)
|
||||
const errors = controls.filter((k: KontrollResult) => k.typ === '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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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.typ === 'ERROR')
|
||||
const hasWarnings = kontroller.some(k => k.typ === '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>
|
||||
)}
|
||||
|
||||
{/* Validation results */}
|
||||
{kontroller.length > 0 && (
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
||||
Valideringsresultat
|
||||
</p>
|
||||
{kontroller.map((k, i) => (
|
||||
<div
|
||||
key={`${k.id}-${i}`}
|
||||
className={`flex items-start gap-2 text-sm rounded-lg p-2.5 ${
|
||||
k.typ === 'ERROR'
|
||||
? 'bg-destructive/5 text-destructive'
|
||||
: 'bg-amber-50 text-amber-800 dark:bg-amber-950/30 dark:text-amber-200'
|
||||
}`}
|
||||
>
|
||||
{k.typ === '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.id}</span>
|
||||
{k.text}
|
||||
</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>
|
||||
)}
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex flex-wrap gap-2 pt-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleValidate}
|
||||
disabled={!hasData || actionLoading !== null}
|
||||
className="gap-1.5"
|
||||
>
|
||||
{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 || actionLoading !== null}
|
||||
className="gap-1.5"
|
||||
>
|
||||
{actionLoading === 'draft' ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<Send className="h-3.5 w-3.5" />
|
||||
)}
|
||||
Spara utkast
|
||||
</Button>
|
||||
|
||||
{!signeringslank ? (
|
||||
<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>
|
||||
) : (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleUnlock}
|
||||
disabled={actionLoading !== null}
|
||||
className="gap-1.5"
|
||||
>
|
||||
{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="ghost"
|
||||
size="sm"
|
||||
onClick={handleCheckSubmitted}
|
||||
disabled={actionLoading !== null}
|
||||
className="gap-1.5"
|
||||
>
|
||||
{actionLoading === 'check' ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<CheckCircle2 className="h-3.5 w-3.5" />
|
||||
)}
|
||||
Kontrollera inlämning
|
||||
</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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user