'use client' import { useState, useEffect, useMemo, useCallback } from 'react' import type { WorkspaceComponentProps } from '@/lib/extensions/workspace-registry' import { useExtensionData } from '@/lib/extensions/use-extension-data' 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 { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Progress } from '@/components/ui/progress' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from '@/components/ui/dialog' import { AlertTriangle, Plus, Pencil, Trash2, FileSpreadsheet, Clock, ChevronDown, ChevronUp, Package, FlaskConical, } from 'lucide-react' import { cn } from '@/lib/utils' // ── Types ───────────────────────────────────────────────────── interface IntrastatLine { cnCode: string partnerCountry: string countryOfOrigin: string transactionNature: string deliveryTerms: string invoicedValue: number netMass: number supplementaryUnit: number | null supplementaryUnitType: string | null partnerVatId: string } interface ThresholdStatus { cumulativeValue: number threshold: number isObligated: boolean percentageUsed: number } interface IntrastatWarning { type: string severity: 'error' | 'warning' invoiceId?: string invoiceNumber?: string productId?: string message: string } interface ReportData { period: { year: number; month: number } reporterVatNumber: string reporterName: string lines: IntrastatLine[] totals: { invoicedValue: number; netMass: number; lineCount: number } thresholdStatus: ThresholdStatus warnings: IntrastatWarning[] invoiceCount: number } interface ProductRecord { key: string productId: string description: string cn_code: string | null net_weight_kg: number | null country_of_origin: string } interface ProductForm { productId: string description: string cnCode: string netWeightKg: string countryOfOrigin: string } // ── 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', ] function currentYear(): number { return new Date().getFullYear() } function currentMonth(): number { return new Date().getMonth() + 1 } const EMPTY_PRODUCT: ProductForm = { productId: '', description: '', cnCode: '', netWeightKg: '', countryOfOrigin: 'SE', } // ── Mock Data Config ────────────────────────────────────────── const MOCK_CSV_FIELDS: CsvFieldDef[] = [ { key: 'cnCode', label: 'CN-kod', required: true }, { key: 'partnerCountry', label: 'Partnerland', required: true }, { key: 'countryOfOrigin', label: 'Ursprungsland' }, { key: 'transactionNature', label: 'Transaktionstyp' }, { key: 'deliveryTerms', label: 'Leveransvillkor' }, { key: 'invoicedValue', label: 'Fakturerat värde (SEK)', required: true }, { key: 'netMass', label: 'Nettovikt (kg)' }, { key: 'partnerVatId', label: 'Partner VAT-ID' }, ] const MOCK_CSV_TEMPLATE = `cnCode;partnerCountry;countryOfOrigin;transactionNature;deliveryTerms;invoicedValue;netMass;partnerVatId 72163100;DE;SE;11;DAP;245000;4500;DE123456789 84713000;FR;CN;11;EXW;128000;85;FR987654321 39269090;NL;SE;11;FCA;67000;320;NL456789012` function parseMockCsvRows(rows: Record[]): ReportData { const lines: IntrastatLine[] = rows.map(r => ({ cnCode: r.cnCode || '00000000', partnerCountry: r.partnerCountry || '', countryOfOrigin: r.countryOfOrigin || 'SE', transactionNature: r.transactionNature || '11', deliveryTerms: r.deliveryTerms || 'DAP', invoicedValue: parseFloat(r.invoicedValue || '0') || 0, netMass: parseFloat(r.netMass || '0') || 0, supplementaryUnit: null, supplementaryUnitType: null, partnerVatId: r.partnerVatId || '', })) const invoicedValue = lines.reduce((s, l) => s + l.invoicedValue, 0) const netMass = lines.reduce((s, l) => s + l.netMass, 0) return { period: { year: new Date().getFullYear(), month: new Date().getMonth() + 1 }, reporterVatNumber: 'SE000000000001', reporterName: 'Testdata', lines, totals: { invoicedValue, netMass, lineCount: lines.length }, thresholdStatus: { cumulativeValue: invoicedValue, threshold: 9000000, isObligated: invoicedValue >= 9000000, percentageUsed: Math.round(invoicedValue / 9000000 * 100), }, warnings: [], invoiceCount: lines.length, } } 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 IntrastatWorkspace({ userId }: WorkspaceComponentProps) { void userId // Mock data const { mockReport, isMockActive, isLoading: mockLoading, importedAt, saveMockData, clearMockData } = useMockData('export', 'intrastat') const [importDialogOpen, setImportDialogOpen] = useState(false) const [year, setYear] = useState(currentYear()) const [month, setMonth] = useState(currentMonth()) const [report, setReport] = useState(null) const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) const [downloading, setDownloading] = useState(false) const [warningsExpanded, setWarningsExpanded] = useState(false) // Product CRUD const { data: extData, save, remove, isLoading: productsLoading } = useExtensionData('export', 'intrastat') const [productDialogOpen, setProductDialogOpen] = useState(false) const [editingProduct, setEditingProduct] = useState(null) const [productForm, setProductForm] = useState(EMPTY_PRODUCT) const [deleteConfirm, setDeleteConfirm] = useState(null) const years = useMemo(() => { const cy = currentYear() return [cy, cy - 1, cy - 2] }, []) // Parse products from extension data const products: ProductRecord[] = useMemo(() => { return extData .filter(d => d.key.startsWith('product:')) .map(d => ({ key: d.key, productId: d.key.replace('product:', ''), description: String(d.value.description || ''), cn_code: d.value.cn_code ? String(d.value.cn_code) : null, net_weight_kg: d.value.net_weight_kg !== undefined ? Number(d.value.net_weight_kg) : null, country_of_origin: String(d.value.country_of_origin || 'SE'), })) .sort((a, b) => a.description.localeCompare(b.description)) }, [extData]) // Fetch report const fetchReport = useCallback(async () => { if (isMockActive && mockReport) { setReport(mockReport) setIsLoading(false) return } setIsLoading(true) setError(null) try { const params = new URLSearchParams({ year: String(year), month: String(month) }) const res = await fetch(`/api/extensions/export/intrastat/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') setReport(null) } finally { setIsLoading(false) } }, [year, month, 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) try { const params = new URLSearchParams({ year: String(year), month: String(month) }) const res = await fetch(`/api/extensions/export/intrastat/report?${params}`) if (res.ok) { const json = await res.json() setReport(json.data) } } catch { /* ignore */ } setIsLoading(false) }, [clearMockData, year, month]) // Product CRUD handlers const openNewProduct = () => { setEditingProduct(null) setProductForm(EMPTY_PRODUCT) setProductDialogOpen(true) } const openEditProduct = (productId: string) => { const product = products.find(p => p.productId === productId) if (!product) return setEditingProduct(productId) setProductForm({ productId, description: product.description, cnCode: product.cn_code || '', netWeightKg: product.net_weight_kg !== null ? String(product.net_weight_kg) : '', countryOfOrigin: product.country_of_origin, }) setProductDialogOpen(true) } const saveProduct = async () => { const id = editingProduct || productForm.productId.trim() if (!id) return await save(`product:${id}`, { description: productForm.description.trim(), cn_code: productForm.cnCode.trim() || null, net_weight_kg: productForm.netWeightKg ? parseFloat(productForm.netWeightKg) : null, country_of_origin: productForm.countryOfOrigin || 'SE', }) setProductDialogOpen(false) // Refresh report to pick up new product metadata fetchReport() } const deleteProduct = async (productId: string) => { await remove(`product:${productId}`) setDeleteConfirm(null) fetchReport() } // Download handler const handleDownload = async () => { setDownloading(true) try { const params = new URLSearchParams({ year: String(year), month: String(month) }) const res = await fetch(`/api/extensions/export/intrastat/download?${params}`) if (!res.ok) { setError('Kunde inte ladda ner filen') return } const blob = await res.blob() const disposition = res.headers.get('Content-Disposition') || '' const match = disposition.match(/filename="(.+)"/) const filename = match ? match[1] : `INTRASTAT_${year}-${String(month).padStart(2, '0')}.csv` 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(false) } } const errorCount = report?.warnings.filter(w => w.severity === 'error').length ?? 0 const warningCount = report?.warnings.filter(w => w.severity === 'warning').length ?? 0 if ((isLoading || productsLoading || mockLoading) && !report) { return } return (
{/* ── Period Selector ─────────────────────────────────── */}
{/* ── Mock Data Banner ──────────────────────────────── */} {isMockActive && ( setImportDialogOpen(true)} /> )} {error && (

{error}

)} {report && ( <> {/* ── Threshold Progress ───────────────────────────── */}

Tröskelvärde Intrastat (utförsel)

{report.thresholdStatus.isObligated ? 'Obligatorisk rapportering' : 'Frivillig rapportering'}
Ackumulerat (12 mån): {formatSEK(report.thresholdStatus.cumulativeValue)} SEK {report.thresholdStatus.percentageUsed}% av {formatSEK(report.thresholdStatus.threshold)} SEK
{/* ── KPI Row ──────────────────────────────────────── */}

Fakturerat värde

{formatSEK(report.totals.invoicedValue)}

SEK

Nettovikt

{report.totals.netMass.toLocaleString('sv-SE')}

kg

Deklarationsrader

{report.totals.lineCount}

{report.invoiceCount} fakturor

{/* ── Product Registry ─────────────────────────────── */}
Produktregister
{products.length === 0 ? (

Inga produkter registrerade. Lägg till produkter med CN-kod och vikt för att generera Intrastat-deklarationer.

) : (
Produkt CN-kod Vikt (kg) Ursprung {products.map(p => (
{p.description || p.productId} {p.productId !== p.description && ( ({p.productId}) )}
{p.cn_code ? ( {p.cn_code} ) : ( Saknas )} {p.net_weight_kg !== null ? String(p.net_weight_kg) : — } {p.country_of_origin}
))}
)}
{/* ── Declaration Table ─────────────────────────────── */} Deklaration {MONTHS[month - 1]} {year} {report.lines.length === 0 ? (

Inga EU-varuförsäljningar hittades för perioden.

) : (
CN-kod Land Urspr. Värde (SEK) Vikt (kg) Partner-VAT {report.lines.map((line, i) => ( {line.cnCode} {line.partnerCountry} {line.countryOfOrigin} {formatSEK(line.invoicedValue)} {line.netMass > 0 ? line.netMass.toLocaleString('sv-SE') : '—'} {line.partnerVatId || '—'} ))} Summa {formatSEK(report.totals.invoicedValue)} {report.totals.netMass.toLocaleString('sv-SE')}
)}
{/* ── Warnings ─────────────────────────────────────── */} {report.warnings.length > 0 && ( 0 ? 'border-l-destructive' : 'border-l-warning')}> {warningsExpanded && (
{report.warnings.map((w, i) => (
{w.message}
))}
)}
)} {/* ── Deadline Footer ───────────────────────────────── */}
Deadline: 10:e arbetsdagen efter redovisningsperiodens slut
)} {/* ── Mock Data Import Dialog ───────────────────────── */} open={importDialogOpen} onOpenChange={setImportDialogOpen} csvFields={MOCK_CSV_FIELDS} parseCsvRows={parseMockCsvRows} validateReport={validateMockReport} templateCsvContent={MOCK_CSV_TEMPLATE} templateFileName="intrastat-template.csv" onImport={handleMockImport} /> {/* ── Product Dialog ────────────────────────────────────── */} {editingProduct ? 'Redigera produkt' : 'Lägg till produkt'}
{!editingProduct && (
setProductForm(f => ({ ...f, productId: e.target.value }))} placeholder="T.ex. STALBALK-M8" />
)}
setProductForm(f => ({ ...f, description: e.target.value }))} placeholder="T.ex. Stålbalk M8 200mm" />
setProductForm(f => ({ ...f, cnCode: e.target.value.replace(/\D/g, '').slice(0, 8) }))} placeholder="T.ex. 72163100" maxLength={8} className="font-mono" />
setProductForm(f => ({ ...f, netWeightKg: e.target.value }))} placeholder="45.5" />
setProductForm(f => ({ ...f, countryOfOrigin: e.target.value.toUpperCase().slice(0, 2) }))} placeholder="SE" maxLength={2} />
{/* ── Delete Confirmation ───────────────────────────────── */} setDeleteConfirm(null)}> Ta bort produkt?

Är du säker på att du vill ta bort produkten “{deleteConfirm}”? Denna åtgärd kan inte ångras.

) }