'use client' import { useState, useEffect, useMemo, useCallback } from 'react' import type { WorkspaceComponentProps } from '@/lib/extensions/workspace-registry' import ExtensionLoadingSkeleton from '@/components/extensions/shared/ExtensionLoadingSkeleton' import KPICard from '@/components/extensions/shared/KPICard' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { AlertTriangle, CheckCircle2, ChevronDown, ChevronUp, ArrowUp, ArrowDown, Minus, BarChart3, } from 'lucide-react' import { cn } from '@/lib/utils' // ── Types ───────────────────────────────────────────────────── interface VatBoxData { boxNumber: string label: string amount: number accounts: string[] } interface RevenueBreakdown { domestic: { amount: number; percentage: number } euGoods: { amount: number; percentage: number } euServices: { amount: number; percentage: number } exportGoods: { amount: number; percentage: number } exportServices: { amount: number; percentage: number } triangular: { amount: number; percentage: number } totalRevenue: number } interface PeriodDelta { current: number previous: number change: number changePercent: number | null } interface PeriodComparison { domestic: PeriodDelta euGoods: PeriodDelta euServices: PeriodDelta exportGoods: PeriodDelta exportServices: PeriodDelta triangular: PeriodDelta totalRevenue: PeriodDelta netVat: PeriodDelta } interface VatMonitorWarning { type: string severity: 'error' | 'warning' invoiceId?: string invoiceNumber?: string customerName?: string message: string } interface ReportData { period: { year: number; month?: number; quarter?: number } boxes: VatBoxData[] revenueBreakdown: RevenueBreakdown vatSummary: { outputVat25: number outputVat12: number outputVat6: number totalOutputVat: number inputVat: number netVat: number isRefund: boolean } warnings: VatMonitorWarning[] comparison: PeriodComparison | null } // ── Helpers ─────────────────────────────────────────────────── function formatSEK(amount: number): string { return Math.round(amount).toLocaleString('sv-SE') } const MONTHS = [ 'Januari', 'Februari', 'Mars', 'April', 'Maj', 'Juni', 'Juli', 'Augusti', 'September', 'Oktober', 'November', 'December', ] const QUARTERS = ['Q1 (jan–mar)', 'Q2 (apr–jun)', 'Q3 (jul–sep)', 'Q4 (okt–dec)'] function currentYear(): number { return new Date().getFullYear() } function currentMonth(): number { return new Date().getMonth() + 1 } function currentQuarter(): number { return Math.ceil(currentMonth() / 3) } // Revenue breakdown cards config const REVENUE_CARDS: { key: keyof Omit; label: string; compKey: keyof PeriodComparison }[] = [ { key: 'domestic', label: 'Inrikes', compKey: 'domestic' }, { key: 'euGoods', label: 'EU varor', compKey: 'euGoods' }, { key: 'euServices', label: 'EU tjänster', compKey: 'euServices' }, { key: 'exportGoods', label: 'Export varor', compKey: 'exportGoods' }, { key: 'exportServices', label: 'Export tjänster', compKey: 'exportServices' }, { key: 'triangular', label: 'Trepartshandel', compKey: 'triangular' }, ] // Box display order (only show relevant ones) const DISPLAY_BOX_ORDER = ['05', '10', '11', '12', '35', '36', '38', '39', '40', '48', '49'] // ── Component ───────────────────────────────────────────────── export default function VatMonitorWorkspace({ userId }: WorkspaceComponentProps) { void userId const [year, setYear] = useState(currentYear()) const [periodType, setPeriodType] = useState<'monthly' | 'quarterly'>('monthly') const [month, setMonth] = useState(currentMonth()) const [quarter, setQuarter] = useState(currentQuarter()) const [compareEnabled, setCompareEnabled] = useState(true) const [report, setReport] = useState(null) const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) const [warningsExpanded, setWarningsExpanded] = useState(false) const years = useMemo(() => { const cy = currentYear() return [cy, cy - 1, cy - 2] }, []) const fetchReport = useCallback(async () => { setIsLoading(true) setError(null) const params = new URLSearchParams({ year: String(year) }) if (periodType === 'monthly') { params.set('month', String(month)) } else { params.set('quarter', String(quarter)) } if (compareEnabled) { params.set('compare', 'previous') } try { const res = await fetch(`/api/extensions/export/vat-monitor/report?${params}`) if (!res.ok) { const json = await res.json() setError(json.error || 'Kunde inte generera rapporten') setReport(null) return } const json = await res.json() setReport(json.data) } catch { setError('Nätverksfel — kunde inte hämta rapporten') setReport(null) } finally { setIsLoading(false) } }, [year, month, quarter, periodType, compareEnabled]) useEffect(() => { fetchReport() }, [fetchReport]) const errorCount = report?.warnings.filter(w => w.severity === 'error').length ?? 0 const warningCount = report?.warnings.filter(w => w.severity === 'warning').length ?? 0 // Filter boxes to only show ones in display order that have data or are always shown const displayBoxes = useMemo(() => { if (!report) return [] const boxMap = new Map(report.boxes.map(b => [b.boxNumber, b])) return DISPLAY_BOX_ORDER .map(num => boxMap.get(num)) .filter((b): b is VatBoxData => b !== undefined) }, [report]) if (isLoading && !report) { return } return (
{/* ── Period Selector ─────────────────────────────────── */}
{periodType === 'monthly' ? ( ) : ( )}
{/* ── Error state ────────────────────────────────────── */} {error && (

{error}

)} {report && ( <> {/* ── Revenue Breakdown Cards ──────────────────────── */}

Intäktsfördelning

{REVENUE_CARDS.map(({ key, label, compKey }) => { const data = report.revenueBreakdown[key] const delta = report.comparison?.[compKey] return (

{label}

{formatSEK(data.amount)}

{data.percentage}% {delta && }
) })}
{/* ── VAT Summary + Moms Box Table ─────────────────── */}
{/* VAT Summary cards */}

Moms

{report.vatSummary.isRefund ? 'Moms att få tillbaka' : 'Moms att betala'}

{formatSEK(Math.abs(report.vatSummary.netVat))} SEK
{report.comparison && (
)}
{/* Momsdeklaration preview table */}

Momsdeklaration (förhandsvisning)

Ruta Beskrivning Belopp (SEK) {displayBoxes.map(box => { const isNetVat = box.boxNumber === '49' const isInputVat = box.boxNumber === '48' return ( {box.boxNumber} {box.label} {formatSEK(box.amount)} ) })} {displayBoxes.length === 0 && ( Ingen bokföringsdata för perioden. )}
{/* ── Warnings ─────────────────────────────────────── */} {report.warnings.length > 0 && ( 0 ? 'border-l-destructive' : 'border-l-warning' )}> {warningsExpanded && (
{report.warnings.map((w, i) => (
{w.message}
))}
)}
)} {/* ── Total Revenue Footer ─────────────────────────── */}
Total omsättning: {formatSEK(report.revenueBreakdown.totalRevenue)} SEK {report.period.year} {report.period.month !== undefined && `, ${MONTHS[report.period.month - 1]}`} {report.period.quarter !== undefined && `, ${QUARTERS[report.period.quarter - 1]}`}
)}
) } // ── Sub-components ──────────────────────────────────────────── function DeltaIndicator({ delta, invert = false }: { delta: PeriodDelta; invert?: boolean }) { if (delta.changePercent === null || delta.change === 0) { return ( ) } // For most metrics, positive = green (revenue growing) // For netVat (invert=true), positive = red (paying more VAT) const isPositive = delta.change > 0 const isGood = invert ? !isPositive : isPositive return ( {isPositive ? : } {delta.changePercent > 0 ? '+' : ''}{delta.changePercent}% ) }