'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, FileSpreadsheet, FileCode, Clock, ChevronDown, ChevronUp, Users, Package, Briefcase, FlaskConical, } from 'lucide-react' import { cn } from '@/lib/utils' // ── Types ───────────────────────────────────────────────────── interface ECSalesListLine { customerVatNumber: string customerName: string customerCountry: string customerId: string goodsAmount: number servicesAmount: number triangulationAmount: number invoiceCount: number } interface ECSalesListWarning { type: string severity: 'error' | 'warning' invoiceId?: string invoiceNumber?: string customerId?: string customerName?: string message: string } interface CrossCheckResult { box35Match: boolean box35ReportTotal: number box35GLTotal: number box39Match: boolean box39ReportTotal: number box39GLTotal: number } interface ReportData { period: { year: number; month?: number; quarter?: number } filingType: 'monthly' | 'quarterly' reporterVatNumber: string reporterName: string lines: ECSalesListLine[] totals: { goods: number; services: number; triangulation: number; total: number } warnings: ECSalesListWarning[] crossCheck: CrossCheckResult | null invoiceCount: number customerCount: number deadline: string daysUntilDeadline: number } type SortField = 'country' | 'vatNumber' | 'goods' | 'services' | 'invoices' type SortDir = 'asc' | 'desc' // ── Helpers ─────────────────────────────────────────────────── function formatSEK(amount: number): string { return Math.round(amount).toLocaleString('sv-SE') } function formatDeadlineDate(dateStr: string): string { const d = new Date(dateStr + 'T00:00:00') return d.toLocaleDateString('sv-SE', { year: 'numeric', month: 'long', day: 'numeric' }) } 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) } // ── Mock Data Config ────────────────────────────────────────── const MOCK_CSV_FIELDS: CsvFieldDef[] = [ { key: 'customerVatNumber', label: 'VAT-nummer', required: true }, { key: 'customerName', label: 'Kundnamn', required: true }, { key: 'customerCountry', label: 'Land', required: true }, { key: 'goodsAmount', label: 'Varor (SEK)' }, { key: 'servicesAmount', label: 'Tjänster (SEK)' }, { key: 'triangulationAmount', label: 'Trepartshandel (SEK)' }, { key: 'invoiceCount', label: 'Antal fakturor' }, ] const MOCK_CSV_TEMPLATE = `customerVatNumber;customerName;customerCountry;goodsAmount;servicesAmount;triangulationAmount;invoiceCount DE123456789;Beispiel GmbH;DE;150000;25000;0;3 FR987654321;Exemple SARL;FR;0;80000;0;2 NL456789012;Voorbeeld BV;NL;45000;0;12000;1` function parseMockCsvRows(rows: Record[]): ReportData { const lines: ECSalesListLine[] = rows.map(r => ({ customerVatNumber: r.customerVatNumber || '', customerName: r.customerName || '', customerCountry: r.customerCountry || '', customerId: r.customerVatNumber || '', goodsAmount: parseFloat(r.goodsAmount || '0') || 0, servicesAmount: parseFloat(r.servicesAmount || '0') || 0, triangulationAmount: parseFloat(r.triangulationAmount || '0') || 0, invoiceCount: parseInt(r.invoiceCount || '1', 10) || 1, })) const goods = lines.reduce((s, l) => s + l.goodsAmount, 0) const services = lines.reduce((s, l) => s + l.servicesAmount, 0) const triangulation = lines.reduce((s, l) => s + l.triangulationAmount, 0) const invoiceCount = lines.reduce((s, l) => s + l.invoiceCount, 0) return { period: { year: new Date().getFullYear(), quarter: Math.ceil((new Date().getMonth() + 1) / 3) }, filingType: 'quarterly', reporterVatNumber: 'SE000000000001', reporterName: 'Testdata', lines, totals: { goods, services, triangulation, total: goods + services + triangulation }, warnings: [], crossCheck: null, invoiceCount, customerCount: lines.length, deadline: new Date(Date.now() + 30 * 86400000).toISOString().slice(0, 10), daysUntilDeadline: 30, } } 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.lines)) return { valid: false, error: 'Fältet "lines" saknas eller är inte en array' } if (!obj.totals || typeof obj.totals !== 'object') return { valid: false, error: 'Fältet "totals" saknas' } return { valid: true } } // ── Component ───────────────────────────────────────────────── export default function EuSalesListWorkspace({ userId }: WorkspaceComponentProps) { void userId // Mock data const { mockReport, isMockActive, isLoading: mockLoading, importedAt, saveMockData, clearMockData } = useMockData('export', 'eu-sales-list') const [importDialogOpen, setImportDialogOpen] = useState(false) // Period selection state const [year, setYear] = useState(currentYear()) const [periodType, setPeriodType] = useState<'monthly' | 'quarterly'>('quarterly') const [month, setMonth] = useState(currentMonth()) const [quarter, setQuarter] = useState(currentQuarter()) // Report state const [report, setReport] = useState(null) const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) // Table sort const [sortField, setSortField] = useState('country') const [sortDir, setSortDir] = useState('asc') // Warning expansion const [warningsExpanded, setWarningsExpanded] = useState(false) // Download state const [downloading, setDownloading] = useState<'csv' | 'xml' | null>(null) // Available years (current year and 2 previous) const years = useMemo(() => { const cy = currentYear() return [cy, cy - 1, cy - 2] }, []) // Fetch report 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)) } try { const res = await fetch(`/api/extensions/export/eu-sales-list/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, 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) // Re-fetch from API 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)) } try { const res = await fetch(`/api/extensions/export/eu-sales-list/report?${params}`) if (res.ok) { const json = await res.json() setReport(json.data) } } catch { /* ignore */ } setIsLoading(false) }, [clearMockData, year, month, quarter, periodType]) // Sort lines const sortedLines = useMemo(() => { if (!report) return [] const lines = [...report.lines] lines.sort((a, b) => { let cmp = 0 switch (sortField) { case 'country': cmp = a.customerCountry.localeCompare(b.customerCountry) break case 'vatNumber': cmp = a.customerVatNumber.localeCompare(b.customerVatNumber) break case 'goods': cmp = a.goodsAmount - b.goodsAmount break case 'services': cmp = a.servicesAmount - b.servicesAmount break case 'invoices': cmp = a.invoiceCount - b.invoiceCount break } return sortDir === 'asc' ? cmp : -cmp }) return lines }, [report, sortField, sortDir]) // Toggle sort const toggleSort = (field: SortField) => { if (sortField === field) { setSortDir(d => d === 'asc' ? 'desc' : 'asc') } else { setSortField(field) setSortDir('asc') } } // Download handler const handleDownload = async (format: 'csv' | 'xml') => { setDownloading(format) const params = new URLSearchParams({ year: String(year), format }) if (periodType === 'monthly') { params.set('month', String(month)) } else { params.set('quarter', String(quarter)) } try { const res = await fetch(`/api/extensions/export/eu-sales-list/download?${params}`) if (!res.ok) { const json = await res.json() setError(json.error || 'Kunde inte ladda ner filen') return } const blob = await res.blob() const disposition = res.headers.get('Content-Disposition') || '' const filenameMatch = disposition.match(/filename="(.+)"/) const filename = filenameMatch ? filenameMatch[1] : `PS_${year}.${format}` const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = filename document.body.appendChild(a) a.click() document.body.removeChild(a) URL.revokeObjectURL(url) } catch { setError('Kunde inte ladda ner filen') } finally { setDownloading(null) } } // Derived counts const errorCount = report?.warnings.filter(w => w.severity === 'error').length ?? 0 const warningCount = report?.warnings.filter(w => w.severity === 'warning').length ?? 0 if ((isLoading || mockLoading) && !report) { return } return (
{/* ── Period Selector ─────────────────────────────────── */}
{periodType === 'monthly' ? ( ) : ( )}
{/* Download + Import buttons */}
{/* ── Mock Data Banner ──────────────────────────────── */} {isMockActive && ( setImportDialogOpen(true)} /> )} {/* ── Error state ────────────────────────────────────── */} {error && (

{error}

)} {report && ( <> {/* ── KPI Cards ────────────────────────────────────── */}
{/* ── Deadline + Cross-Check Row ────────────────────── */}
{/* Deadline */}

Inlämningsdeadline

{formatDeadlineDate(report.deadline)}

{report.daysUntilDeadline > 0 ? `${report.daysUntilDeadline} dagar kvar` : report.daysUntilDeadline === 0 ? 'Deadline idag!' : `${Math.abs(report.daysUntilDeadline)} dagar försenad` }

{/* Cross-check */}

Avstämning mot huvudbok

{report.crossCheck ? (
) : (

Ingen bokföringsdata tillgänglig för perioden.

)}
{/* ── Warnings ─────────────────────────────────────── */} {report.warnings.length > 0 && ( 0 ? 'border-l-destructive' : 'border-l-warning' )}> {warningsExpanded && (
{report.warnings.map((w, i) => (
{w.message}
))}
)}
)} {/* ── Customer Table ────────────────────────────────── */} Kunder per land {sortedLines.length === 0 ? (

Inga EU-försäljningar hittades för vald period.

) : (
Land VAT-nummer Kund Varor (ruta 35) Tjänster (ruta 39) Fakturor {sortedLines.map(line => ( {line.customerCountry} {line.customerVatNumber} {line.customerName} {line.goodsAmount !== 0 ? formatSEK(line.goodsAmount) : '—'} {line.servicesAmount !== 0 ? formatSEK(line.servicesAmount) : '—'} {line.invoiceCount} ))} {/* Totals row */} Summa ({sortedLines.length} kunder) {formatSEK(report.totals.goods)} {formatSEK(report.totals.services)} {report.invoiceCount}
)}
{/* ── Filing Info Footer ────────────────────────────── */}
Uppgiftslämnare: {report.reporterName} ({report.reporterVatNumber}) Redovisningsperiod: {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="eu-sales-list-template.csv" onImport={handleMockImport} />
) } // ── Sub-components ──────────────────────────────────────────── function CrossCheckRow({ label, reportTotal, glTotal, match, }: { label: string reportTotal: number glTotal: number match: boolean }) { const diff = Math.round(reportTotal * 100) / 100 - Math.round(glTotal * 100) / 100 return (
{match ? ( ) : ( )} {label} {formatSEK(reportTotal)} SEK {!match && ( (diff: {diff > 0 ? '+' : ''}{formatSEK(diff)}) )}
) } function SortableHead({ field, current, dir, onSort, className, children, }: { field: SortField current: SortField dir: SortDir onSort: (field: SortField) => void className?: string children: React.ReactNode }) { const isActive = current === field return ( onSort(field)}> {children} {isActive && ( dir === 'asc' ? : )} ) }