'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 } 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 = 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 => { 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.typ === 'ERROR') const hasWarnings = kontroller.some(k => k.typ === '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.typ === 'ERROR' ? ( ) : ( )}
{k.id} {k.text}
))}
)} {/* 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.

)} {/* Action buttons */}
{!signeringslank ? ( ) : ( )}
{/* Disconnect */}
) }