'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 ExtensionLoadingSkeleton from '@/components/extensions/shared/ExtensionLoadingSkeleton' 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, } 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', } // ── Component ───────────────────────────────────────────────── export default function IntrastatWorkspace({ userId }: WorkspaceComponentProps) { void userId 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 () => { 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]) useEffect(() => { fetchReport() }, [fetchReport]) // 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) && !report) { return } return (
{/* ── Period Selector ─────────────────────────────────── */}
{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
)} {/* ── 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.

) }