'use client' import { useState, useEffect, useMemo, useCallback } from 'react' import type { WorkspaceComponentProps } from '@/lib/extensions/workspace-registry' import { useMockData } from '@/lib/extensions/use-mock-data' import ExtensionLoadingSkeleton from '@/components/extensions/shared/ExtensionLoadingSkeleton' import MockDataBanner from '@/components/extensions/shared/MockDataBanner' import MockDataImportDialog from '@/components/extensions/shared/MockDataImportDialog' import type { CsvFieldDef } from '@/components/extensions/shared/MockDataImportDialog' 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, FlaskConical, } 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'] // ── Mock Data Config ────────────────────────────────────────── const MOCK_CSV_FIELDS: CsvFieldDef[] = [ { key: 'boxNumber', label: 'Ruta', required: true }, { key: 'label', label: 'Beskrivning', required: true }, { key: 'amount', label: 'Belopp (SEK)', required: true }, { key: 'accounts', label: 'Konton (kommaseparerade)' }, ] const MOCK_CSV_TEMPLATE = `boxNumber;label;amount;accounts 05;Momspliktiga intäkter;500000;3001,3002,3003 10;Utgående moms 25%;100000;2611 11;Utgående moms 12%;6000;2621 12;Utgående moms 6%;3000;2631 35;Varuförsäljning EU;75000;3305 36;Tjänsteförsäljning EU;45000;3308 38;Exportförsäljning;30000;3305 39;Omvänd skattskyldighet;20000; 40;Inköp varor EU;60000; 48;Ingående moms;65000;2641 49;Moms att betala;44000;` function parseMockCsvRows(rows: Record[]): ReportData { const boxes: VatBoxData[] = rows.map(r => ({ boxNumber: r.boxNumber || '', label: r.label || '', amount: parseFloat(r.amount || '0') || 0, accounts: r.accounts ? r.accounts.split(',').map(a => a.trim()) : [], })) // Derive revenue breakdown from box values const getBox = (num: string) => boxes.find(b => b.boxNumber === num)?.amount || 0 const domestic = getBox('05') const euGoods = getBox('35') const euServices = getBox('36') const exportGoods = getBox('38') const exportServices = 0 const triangular = getBox('39') const totalRevenue = domestic + euGoods + euServices + exportGoods + exportServices + triangular const revenueBreakdown: RevenueBreakdown = { domestic: { amount: domestic, percentage: totalRevenue > 0 ? Math.round(domestic / totalRevenue * 100) : 0 }, euGoods: { amount: euGoods, percentage: totalRevenue > 0 ? Math.round(euGoods / totalRevenue * 100) : 0 }, euServices: { amount: euServices, percentage: totalRevenue > 0 ? Math.round(euServices / totalRevenue * 100) : 0 }, exportGoods: { amount: exportGoods, percentage: totalRevenue > 0 ? Math.round(exportGoods / totalRevenue * 100) : 0 }, exportServices: { amount: exportServices, percentage: 0 }, triangular: { amount: triangular, percentage: totalRevenue > 0 ? Math.round(triangular / totalRevenue * 100) : 0 }, totalRevenue, } const outputVat25 = getBox('10') const outputVat12 = getBox('11') const outputVat6 = getBox('12') const inputVat = getBox('48') const netVat = getBox('49') return { period: { year: new Date().getFullYear(), month: new Date().getMonth() + 1 }, boxes, revenueBreakdown, vatSummary: { outputVat25, outputVat12, outputVat6, totalOutputVat: outputVat25 + outputVat12 + outputVat6, inputVat, netVat, isRefund: netVat < 0, }, warnings: [], comparison: null, } } function validateMockReport(data: unknown): { valid: boolean; error?: string } { if (!data || typeof data !== 'object') return { valid: false, error: 'Data måste vara ett objekt' } const obj = data as Record if (!Array.isArray(obj.boxes)) return { valid: false, error: 'Fältet "boxes" saknas eller är inte en array' } if (!obj.revenueBreakdown || typeof obj.revenueBreakdown !== 'object') { return { valid: false, error: 'Fältet "revenueBreakdown" saknas' } } if (!obj.vatSummary || typeof obj.vatSummary !== 'object') { return { valid: false, error: 'Fältet "vatSummary" saknas' } } return { valid: true } } // ── Component ───────────────────────────────────────────────── export default function VatMonitorWorkspace({ userId }: WorkspaceComponentProps) { void userId // Mock data const { mockReport, isMockActive, isLoading: mockLoading, importedAt, saveMockData, clearMockData } = useMockData('export', 'vat-monitor') const [importDialogOpen, setImportDialogOpen] = useState(false) 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 () => { if (isMockActive && mockReport) { setReport(mockReport) setIsLoading(false) return } 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, isMockActive, mockReport]) useEffect(() => { fetchReport() }, [fetchReport]) const handleMockImport = useCallback(async (data: ReportData, meta: { source: 'csv' | 'json'; fileName: string; rowCount: number }) => { await saveMockData(data, meta) setReport(data) }, [saveMockData]) const handleMockClear = useCallback(async () => { await clearMockData() setReport(null) 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() setReport(json.data) } } catch { /* ignore */ } setIsLoading(false) }, [clearMockData, year, month, quarter, periodType, compareEnabled]) 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 || mockLoading) && !report) { return } return (
{/* ── Period Selector ─────────────────────────────────── */}
{periodType === 'monthly' ? ( ) : ( )}
{/* ── Mock Data Banner ──────────────────────────────── */} {isMockActive && ( setImportDialogOpen(true)} /> )} {/* ── 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]}`}
)} {/* ── Mock Data Import Dialog ───────────────────────── */} open={importDialogOpen} onOpenChange={setImportDialogOpen} csvFields={MOCK_CSV_FIELDS} parseCsvRows={parseMockCsvRows} validateReport={validateMockReport} templateCsvContent={MOCK_CSV_TEMPLATE} templateFileName="vat-monitor-template.csv" onImport={handleMockImport} />
) } // ── 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}% ) }