'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 { VatPeriodType } from '@/types' import { formatRedovisare, formatRedovisningsperiod } from '@/lib/skatteverket/format' 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 } 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 } function SkatteverketPanelInner({ periodType, year, period, hasData }: SkatteverketPanelProps) { const [status, setStatus] = useState(null) const [loading, setLoading] = useState(true) const [actionLoading, setActionLoading] = useState(null) const [error, setError] = useState(null) const [success, setSuccess] = useState(null) const [kontroller, setKontroller] = useState([]) const [signeringslank, setSigneringslank] = useState(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: KontrollResult[] = result.data?.kontrollResultat?.resultat || [] setKontroller(controls) if (controls.length === 0) { setSuccess('Valideringen godkänd — inga fel eller varningar') } else { const errors = controls.filter(k => k.status === '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: 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 => { 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 ( Kontrollerar Skatteverket-anslutning... ) } // Not connected if (!status?.connected) { return ( Skicka till Skatteverket {error && (
{error}
)}

Anslut till Skatteverket med BankID för att skicka momsdeklarationen direkt.

) } // Connected — show actions const hasErrors = kontroller.some(k => k.status === 'ERROR') const hasWarnings = kontroller.some(k => k.status === 'WARNING') return (
Skicka till Skatteverket
Ansluten {status.expired && ( Session utgången )}
{/* Messages */} {error && (
{error}
)} {success && !error && (
{success}
)} {/* Validation results */} {kontroller.length > 0 && (

Valideringsresultat

{kontroller.map((k, i) => (
{k.status === 'ERROR' ? ( ) : ( )}
{k.kod} {k.beskrivning}
))}
)} {/* Submitted confirmation */} {submitted && (

Inlämnad

{submitted.kvittensnummer && (

Kvittensnummer: {submitted.kvittensnummer}

)} {submitted.tidpunkt && (

Tidpunkt: {new Date(submitted.tidpunkt).toLocaleString('sv-SE')}

)}
)} {/* Signing link */} {signeringslank && (

Utkastet är låst och redo att signeras

Öppna länken nedan och signera med BankID på Skatteverkets sida.

)} {/* Forward-lifecycle buttons */}
{/* Read-only fetches from Skatteverket. */}
{/* 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. */}
{/* Disconnect */}
) }