'use client' import { useTranslations } from 'next-intl' import { useEffect, useState } from 'react' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { useToast } from '@/components/ui/use-toast' import { useCapability } from '@/contexts/CompanyContext' import { CAPABILITY } from '@/lib/entitlements/keys' import { CheckCircle2, ExternalLink, ShieldOff, FlaskConical, ShieldAlert } from 'lucide-react' type Environment = 'test' | 'prod' type Status = | { connected: false; environment?: Environment; disabled?: boolean } | { connected: true expired: boolean canRefresh: boolean scope: string expiresAt: string environment?: Environment disabled?: boolean } export function SkatteverketConnectPanel() { const t = useTranslations('settings_skatteverket_connect') const { toast } = useToast() const hasSkatteverket = useCapability(CAPABILITY.skatteverket) const [status, setStatus] = useState(null) const [loading, setLoading] = useState(true) const [disconnecting, setDisconnecting] = useState(false) // docs: https://www7.skatteverket.se/portal-wapi/open/apier-och-oppna-data/utvecklarportalen/v1/getFile/tjanstebeskrivning-skattekonto-hamta-huvudmans-saldo-och-transaktioner-v101 const SCOPE_LABELS: Record = { momsdeklaration: t('scope_momsdeklaration'), inkforetag: t('scope_inkforetag'), skahmst: t('scope_skahmst'), skattekonto: t('scope_skattekonto'), agd: t('scope_agd'), } async function loadStatus() { setLoading(true) try { const res = await fetch('/api/extensions/ext/skatteverket/status') if (res.status === 503) { setStatus({ connected: false }) return } const data = (await res.json()) as Status setStatus(data) } catch { setStatus({ connected: false }) } finally { setLoading(false) } } useEffect(() => { loadStatus() }, []) function startConnect() { const returnTo = encodeURIComponent('/settings/tax') window.location.href = `/api/extensions/ext/skatteverket/authorize?return_to=${returnTo}` } async function disconnect() { setDisconnecting(true) try { const res = await fetch('/api/extensions/ext/skatteverket/disconnect', { method: 'POST', }) if (!res.ok) throw new Error(t('disconnect_failed')) toast({ title: t('toast_disconnected') }) await loadStatus() } catch (err) { toast({ title: t('toast_disconnect_failed'), description: err instanceof Error ? err.message : undefined, variant: 'destructive', }) } finally { setDisconnecting(false) } } if (loading) { return ( {t('loading_status')} ) } if (!status?.connected) { return (
{t('title')}
{status?.disabled && (

{t('disabled_message')}

)}

{t('connect_intro')}

{t.rich('skahmst_note', { code: (chunks) => {chunks}, })}
{!hasSkatteverket && (
Anslutning till Skatteverket kräver ett abonnemang.{' '} Uppgradera
)}
) } const scopes = (status.scope || '').split(/\s+/).filter(Boolean) const expiresAtDate = new Date(status.expiresAt) const expiresInMinutes = Math.round( (expiresAtDate.getTime() - Date.now()) / 60_000, ) return (
{t('title')} {status.expired ? ( {t('expired')} ) : ( {t('connected')} )}
{t('token_expires_label')}
{expiresAtDate.toLocaleString('sv-SE')} {!status.expired && expiresInMinutes > 0 && ( {t('expires_in_minutes', { minutes: expiresInMinutes })} )}
{t('refresh_label')}
{status.canRefresh ? t('refresh_auto') : t('refresh_exhausted')}

{t('permissions_label')}

{scopes.map(s => ( {SCOPE_LABELS[s] ?? s} ))}
{!scopes.includes('skahmst') && !scopes.includes('skattekonto') && (

{t('missing_skattekonto')}

)} {!scopes.includes('agd') && (

{t('missing_agd')}

)}
{status.disabled && (

{t('disabled_filings_message')}

)}
{(status.expired || !status.canRefresh || !scopes.includes('skattekonto') || !scopes.includes('agd')) && ( )}
) } function EnvironmentBadge({ environment, disabled }: { environment?: Environment; disabled?: boolean }) { const t = useTranslations('settings_skatteverket_connect') if (disabled) { return ( {t('env_disabled')} ) } if (environment === 'test') { return ( {t('env_test')} ) } if (environment === 'prod') { return ( {t('env_prod')} ) } return null }