Fixed extensions bugs

This commit is contained in:
Emil
2026-02-24 16:06:36 +01:00
parent 0764bc11a6
commit e0b66fe397
26 changed files with 3394 additions and 167 deletions
@@ -2,7 +2,11 @@
import { useState, useEffect, 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 { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import {
@@ -12,7 +16,7 @@ import {
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
} from '@/components/ui/table'
import {
TrendingUp, TrendingDown, RefreshCw, Info, ArrowUpDown,
TrendingUp, TrendingDown, RefreshCw, Info, ArrowUpDown, FlaskConical,
} from 'lucide-react'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
@@ -124,11 +128,126 @@ function currentYear(): number { return new Date().getFullYear() }
type SortField = 'unrealizedGainLoss' | 'foreignAmount' | 'daysOutstanding' | 'customerName'
type SortDir = 'asc' | 'desc'
// ── Mock Data Config ──────────────────────────────────────────
const MOCK_CSV_FIELDS: CsvFieldDef[] = [
{ key: 'invoiceNumber', label: 'Fakturanummer', required: true },
{ key: 'customerName', label: 'Kund', required: true },
{ key: 'currency', label: 'Valuta', required: true },
{ key: 'foreignAmount', label: 'Belopp (utl. valuta)', required: true },
{ key: 'bookedSekAmount', label: 'Bokfört (SEK)' },
{ key: 'bookedRate', label: 'Bokförd kurs' },
{ key: 'currentSekAmount', label: 'Aktuellt (SEK)' },
{ key: 'currentRate', label: 'Aktuell kurs' },
{ key: 'invoiceDate', label: 'Fakturadatum' },
{ key: 'dueDate', label: 'Förfallodatum' },
]
const MOCK_CSV_TEMPLATE = `invoiceNumber;customerName;currency;foreignAmount;bookedSekAmount;bookedRate;currentSekAmount;currentRate;invoiceDate;dueDate
1001;Beispiel GmbH;EUR;10000;112500;11.25;114200;11.42;2025-01-15;2025-02-15
1002;Example Corp;USD;25000;262500;10.50;260000;10.40;2025-01-20;2025-02-20
1003;London Ltd;GBP;8000;106400;13.30;108000;13.50;2025-02-01;2025-03-01`
function parseMockCsvRows(rows: Record<string, string>[]): ReportData {
const today = new Date().toISOString().slice(0, 10)
const receivables: ForeignReceivable[] = rows.map(r => {
const foreignAmount = parseFloat(r.foreignAmount || '0') || 0
const bookedRate = parseFloat(r.bookedRate || '0') || 0
const currentRate = parseFloat(r.currentRate || '0') || bookedRate
const bookedSek = parseFloat(r.bookedSekAmount || '0') || Math.round(foreignAmount * bookedRate * 100) / 100
const currentSek = parseFloat(r.currentSekAmount || '0') || Math.round(foreignAmount * currentRate * 100) / 100
const invoiceDate = r.invoiceDate || today
const dueDate = r.dueDate || today
const daysOutstanding = Math.max(0, Math.floor((Date.now() - new Date(invoiceDate).getTime()) / 86400000))
return {
invoiceId: r.invoiceNumber || '',
invoiceNumber: r.invoiceNumber || '',
customerName: r.customerName || '',
customerCountry: '',
currency: r.currency || 'EUR',
foreignAmount,
bookedSekAmount: bookedSek,
bookedRate,
currentSekAmount: currentSek,
currentRate,
unrealizedGainLoss: Math.round((currentSek - bookedSek) * 100) / 100,
invoiceDate,
dueDate,
daysOutstanding,
}
})
// Group by currency for exposure
const currencyMap = new Map<string, CurrencyExposure>()
for (const r of receivables) {
const existing = currencyMap.get(r.currency)
if (existing) {
existing.totalForeignAmount += r.foreignAmount
existing.bookedSekValue += r.bookedSekAmount
existing.currentSekValue += r.currentSekAmount
existing.unrealizedGainLoss += r.unrealizedGainLoss
existing.invoiceCount++
} else {
currencyMap.set(r.currency, {
currency: r.currency,
totalForeignAmount: r.foreignAmount,
bookedSekValue: r.bookedSekAmount,
currentSekValue: r.currentSekAmount,
unrealizedGainLoss: r.unrealizedGainLoss,
invoiceCount: 1,
averageBookedRate: r.bookedRate,
currentRate: r.currentRate,
})
}
}
const exposureByCurrency = Array.from(currencyMap.values())
const totalBookedSek = receivables.reduce((s, r) => s + r.bookedSekAmount, 0)
const totalCurrentSek = receivables.reduce((s, r) => s + r.currentSekAmount, 0)
const totalUnrealized = Math.round((totalCurrentSek - totalBookedSek) * 100) / 100
return {
referenceDate: today,
exchangeRates: exposureByCurrency.map(e => ({ currency: e.currency, rate: e.currentRate, date: today })),
exposureByCurrency,
receivables,
realizedGainLoss: { year: new Date().getFullYear(), gains: 0, losses: 0, net: 0 },
monthlyTrend: [],
revalPreview: {
totalUnrealizedGainLoss: totalUnrealized,
gains: Math.max(0, totalUnrealized),
losses: Math.abs(Math.min(0, totalUnrealized)),
},
totals: {
bookedSekValue: totalBookedSek,
currentSekValue: totalCurrentSek,
totalUnrealizedGainLoss: totalUnrealized,
receivableCount: receivables.length,
currencyCount: exposureByCurrency.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<string, unknown>
if (!Array.isArray(obj.receivables) && !Array.isArray(obj.exposureByCurrency)) {
return { valid: false, error: 'Fältet "receivables" eller "exposureByCurrency" saknas' }
}
if (!obj.totals || typeof obj.totals !== 'object') return { valid: false, error: 'Fältet "totals" saknas' }
return { valid: true }
}
// ── Component ─────────────────────────────────────────────────
export default function CurrencyReceivablesWorkspace({ userId }: WorkspaceComponentProps) {
void userId
// Mock data
const { mockReport, isMockActive, isLoading: mockLoading, importedAt, saveMockData, clearMockData } = useMockData<ReportData>('export', 'currency-receivables')
const [importDialogOpen, setImportDialogOpen] = useState(false)
const [year, setYear] = useState(currentYear())
const [report, setReport] = useState<ReportData | null>(null)
const [isLoading, setIsLoading] = useState(true)
@@ -141,6 +260,13 @@ export default function CurrencyReceivablesWorkspace({ userId }: WorkspaceCompon
const years = [currentYear(), currentYear() - 1, currentYear() - 2]
const fetchReport = useCallback(async () => {
if (isMockActive && mockReport) {
setReport(mockReport)
setIsLoading(false)
setRefreshing(false)
return
}
setIsLoading(true)
setError(null)
try {
@@ -161,12 +287,32 @@ export default function CurrencyReceivablesWorkspace({ userId }: WorkspaceCompon
setIsLoading(false)
setRefreshing(false)
}
}, [year])
}, [year, 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) })
const res = await fetch(`/api/extensions/export/currency-receivables/report?${params}`)
if (res.ok) {
const json = await res.json()
setReport(json.data)
}
} catch { /* ignore */ }
setIsLoading(false)
}, [clearMockData, year])
const handleRefresh = () => {
setRefreshing(true)
fetchReport()
@@ -200,7 +346,7 @@ export default function CurrencyReceivablesWorkspace({ userId }: WorkspaceCompon
return m <= new Date().getMonth() + 1
}) || []
if (isLoading && !report) {
if ((isLoading || mockLoading) && !report) {
return <ExtensionLoadingSkeleton />
}
@@ -217,12 +363,27 @@ export default function CurrencyReceivablesWorkspace({ userId }: WorkspaceCompon
</SelectContent>
</Select>
</div>
<Button variant="outline" size="sm" onClick={handleRefresh} disabled={refreshing} className="ml-auto">
<RefreshCw className={cn('h-4 w-4 mr-1.5', refreshing && 'animate-spin')} />
{refreshing ? 'Uppdaterar...' : 'Uppdatera kurser'}
</Button>
<div className="flex gap-2 ml-auto">
<Button variant="outline" size="sm" onClick={() => setImportDialogOpen(true)}>
<FlaskConical className="h-4 w-4 mr-1.5" />
Importera testdata
</Button>
<Button variant="outline" size="sm" onClick={handleRefresh} disabled={refreshing}>
<RefreshCw className={cn('h-4 w-4 mr-1.5', refreshing && 'animate-spin')} />
{refreshing ? 'Uppdaterar...' : 'Uppdatera kurser'}
</Button>
</div>
</div>
{/* ── Mock Data Banner ──────────────────────────────── */}
{isMockActive && (
<MockDataBanner
importedAt={importedAt}
onClear={handleMockClear}
onReplace={() => setImportDialogOpen(true)}
/>
)}
{error && (
<Card className="border-destructive/50 bg-destructive/5">
<CardContent className="pt-6">
@@ -454,6 +615,18 @@ export default function CurrencyReceivablesWorkspace({ userId }: WorkspaceCompon
)}
</>
)}
{/* ── Mock Data Import Dialog ───────────────────────── */}
<MockDataImportDialog<ReportData>
open={importDialogOpen}
onOpenChange={setImportDialogOpen}
csvFields={MOCK_CSV_FIELDS}
parseCsvRows={parseMockCsvRows}
validateReport={validateMockReport}
templateCsvContent={MOCK_CSV_TEMPLATE}
templateFileName="currency-receivables-template.csv"
onImport={handleMockImport}
/>
</div>
)
}
@@ -2,7 +2,11 @@
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'
@@ -16,6 +20,7 @@ import {
import {
AlertTriangle, CheckCircle2, FileSpreadsheet, FileCode,
Clock, ChevronDown, ChevronUp, Users, Package, Briefcase,
FlaskConical,
} from 'lucide-react'
import { cn } from '@/lib/utils'
@@ -99,11 +104,73 @@ 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<string, string>[]): 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<string, unknown>
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<ReportData>('export', 'eu-sales-list')
const [importDialogOpen, setImportDialogOpen] = useState(false)
// Period selection state
const [year, setYear] = useState(currentYear())
const [periodType, setPeriodType] = useState<'monthly' | 'quarterly'>('quarterly')
@@ -133,6 +200,12 @@ export default function EuSalesListWorkspace({ userId }: WorkspaceComponentProps
// Fetch report
const fetchReport = useCallback(async () => {
if (isMockActive && mockReport) {
setReport(mockReport)
setIsLoading(false)
return
}
setIsLoading(true)
setError(null)
@@ -159,12 +232,39 @@ export default function EuSalesListWorkspace({ userId }: WorkspaceComponentProps
} finally {
setIsLoading(false)
}
}, [year, month, quarter, periodType])
}, [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 []
@@ -245,7 +345,7 @@ export default function EuSalesListWorkspace({ userId }: WorkspaceComponentProps
const errorCount = report?.warnings.filter(w => w.severity === 'error').length ?? 0
const warningCount = report?.warnings.filter(w => w.severity === 'warning').length ?? 0
if (isLoading && !report) {
if ((isLoading || mockLoading) && !report) {
return <ExtensionLoadingSkeleton />
}
@@ -309,8 +409,16 @@ export default function EuSalesListWorkspace({ userId }: WorkspaceComponentProps
)}
</div>
{/* Download buttons */}
{/* Download + Import buttons */}
<div className="flex gap-2 ml-auto">
<Button
variant="outline"
size="sm"
onClick={() => setImportDialogOpen(true)}
>
<FlaskConical className="h-4 w-4 mr-1.5" />
Importera testdata
</Button>
<Button
variant="outline"
size="sm"
@@ -332,6 +440,15 @@ export default function EuSalesListWorkspace({ userId }: WorkspaceComponentProps
</div>
</div>
{/* ── Mock Data Banner ──────────────────────────────── */}
{isMockActive && (
<MockDataBanner
importedAt={importedAt}
onClear={handleMockClear}
onReplace={() => setImportDialogOpen(true)}
/>
)}
{/* ── Error state ────────────────────────────────────── */}
{error && (
<Card className="border-destructive/50 bg-destructive/5">
@@ -578,6 +695,18 @@ export default function EuSalesListWorkspace({ userId }: WorkspaceComponentProps
</div>
</>
)}
{/* ── Mock Data Import Dialog ───────────────────────── */}
<MockDataImportDialog<ReportData>
open={importDialogOpen}
onOpenChange={setImportDialogOpen}
csvFields={MOCK_CSV_FIELDS}
parseCsvRows={parseMockCsvRows}
validateReport={validateMockReport}
templateCsvContent={MOCK_CSV_TEMPLATE}
templateFileName="eu-sales-list-template.csv"
onImport={handleMockImport}
/>
</div>
)
}
@@ -3,7 +3,11 @@
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'
@@ -21,7 +25,7 @@ import {
} from '@/components/ui/dialog'
import {
AlertTriangle, Plus, Pencil, Trash2, FileSpreadsheet, Clock,
ChevronDown, ChevronUp, Package,
ChevronDown, ChevronUp, Package, FlaskConical,
} from 'lucide-react'
import { cn } from '@/lib/utils'
@@ -102,11 +106,75 @@ 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<string, string>[]): 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<string, unknown>
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<ReportData>('export', 'intrastat')
const [importDialogOpen, setImportDialogOpen] = useState(false)
const [year, setYear] = useState(currentYear())
const [month, setMonth] = useState(currentMonth())
@@ -146,6 +214,12 @@ export default function IntrastatWorkspace({ userId }: WorkspaceComponentProps)
// Fetch report
const fetchReport = useCallback(async () => {
if (isMockActive && mockReport) {
setReport(mockReport)
setIsLoading(false)
return
}
setIsLoading(true)
setError(null)
@@ -166,12 +240,32 @@ export default function IntrastatWorkspace({ userId }: WorkspaceComponentProps)
} finally {
setIsLoading(false)
}
}, [year, month])
}, [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)
@@ -248,7 +342,7 @@ export default function IntrastatWorkspace({ userId }: WorkspaceComponentProps)
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) {
if ((isLoading || productsLoading || mockLoading) && !report) {
return <ExtensionLoadingSkeleton />
}
@@ -275,6 +369,13 @@ export default function IntrastatWorkspace({ userId }: WorkspaceComponentProps)
</Select>
</div>
<div className="flex gap-2 ml-auto">
<Button
variant="outline" size="sm"
onClick={() => setImportDialogOpen(true)}
>
<FlaskConical className="h-4 w-4 mr-1.5" />
Importera testdata
</Button>
<Button
variant="outline" size="sm"
onClick={handleDownload}
@@ -286,6 +387,15 @@ export default function IntrastatWorkspace({ userId }: WorkspaceComponentProps)
</div>
</div>
{/* ── Mock Data Banner ──────────────────────────────── */}
{isMockActive && (
<MockDataBanner
importedAt={importedAt}
onClear={handleMockClear}
onReplace={() => setImportDialogOpen(true)}
/>
)}
{error && (
<Card className="border-destructive/50 bg-destructive/5">
<CardContent className="pt-6">
@@ -529,6 +639,18 @@ export default function IntrastatWorkspace({ userId }: WorkspaceComponentProps)
</>
)}
{/* ── Mock Data Import Dialog ───────────────────────── */}
<MockDataImportDialog<ReportData>
open={importDialogOpen}
onOpenChange={setImportDialogOpen}
csvFields={MOCK_CSV_FIELDS}
parseCsvRows={parseMockCsvRows}
validateReport={validateMockReport}
templateCsvContent={MOCK_CSV_TEMPLATE}
templateFileName="intrastat-template.csv"
onImport={handleMockImport}
/>
{/* ── Product Dialog ────────────────────────────────────── */}
<Dialog open={productDialogOpen} onOpenChange={setProductDialogOpen}>
<DialogContent>
@@ -2,7 +2,11 @@
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'
@@ -15,7 +19,7 @@ import {
} from '@/components/ui/table'
import {
AlertTriangle, CheckCircle2, ChevronDown, ChevronUp,
ArrowUp, ArrowDown, Minus, BarChart3,
ArrowUp, ArrowDown, Minus, BarChart3, FlaskConical,
} from 'lucide-react'
import { cn } from '@/lib/utils'
@@ -120,11 +124,102 @@ const REVENUE_CARDS: { key: keyof Omit<RevenueBreakdown, 'totalRevenue'>; label:
// 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<string, string>[]): 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<string, unknown>
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<ReportData>('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())
@@ -143,6 +238,12 @@ export default function VatMonitorWorkspace({ userId }: WorkspaceComponentProps)
}, [])
const fetchReport = useCallback(async () => {
if (isMockActive && mockReport) {
setReport(mockReport)
setIsLoading(false)
return
}
setIsLoading(true)
setError(null)
@@ -172,12 +273,41 @@ export default function VatMonitorWorkspace({ userId }: WorkspaceComponentProps)
} finally {
setIsLoading(false)
}
}, [year, month, quarter, periodType, compareEnabled])
}, [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
@@ -190,7 +320,7 @@ export default function VatMonitorWorkspace({ userId }: WorkspaceComponentProps)
.filter((b): b is VatBoxData => b !== undefined)
}, [report])
if (isLoading && !report) {
if ((isLoading || mockLoading) && !report) {
return <ExtensionLoadingSkeleton />
}
@@ -254,7 +384,15 @@ export default function VatMonitorWorkspace({ userId }: WorkspaceComponentProps)
)}
</div>
<div className="ml-auto">
<div className="flex gap-2 ml-auto">
<Button
variant="outline"
size="sm"
onClick={() => setImportDialogOpen(true)}
>
<FlaskConical className="h-4 w-4 mr-1.5" />
Importera testdata
</Button>
<Button
variant={compareEnabled ? 'default' : 'outline'}
size="sm"
@@ -266,6 +404,15 @@ export default function VatMonitorWorkspace({ userId }: WorkspaceComponentProps)
</div>
</div>
{/* ── Mock Data Banner ──────────────────────────────── */}
{isMockActive && (
<MockDataBanner
importedAt={importedAt}
onClear={handleMockClear}
onReplace={() => setImportDialogOpen(true)}
/>
)}
{/* ── Error state ────────────────────────────────────── */}
{error && (
<Card className="border-destructive/50 bg-destructive/5">
@@ -462,6 +609,18 @@ export default function VatMonitorWorkspace({ userId }: WorkspaceComponentProps)
</div>
</>
)}
{/* ── Mock Data Import Dialog ───────────────────────── */}
<MockDataImportDialog<ReportData>
open={importDialogOpen}
onOpenChange={setImportDialogOpen}
csvFields={MOCK_CSV_FIELDS}
parseCsvRows={parseMockCsvRows}
validateReport={validateMockReport}
templateCsvContent={MOCK_CSV_TEMPLATE}
templateFileName="vat-monitor-template.csv"
onImport={handleMockImport}
/>
</div>
)
}
@@ -0,0 +1,49 @@
'use client'
import { Card, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { FlaskConical, X, Replace } from 'lucide-react'
interface MockDataBannerProps {
importedAt: string | null
onClear: () => void
onReplace: () => void
}
export default function MockDataBanner({ importedAt, onClear, onReplace }: MockDataBannerProps) {
const formatted = importedAt
? new Date(importedAt).toLocaleString('sv-SE', {
year: 'numeric', month: 'short', day: 'numeric',
hour: '2-digit', minute: '2-digit',
})
: null
return (
<Card className="border-l-4 border-l-amber-500 bg-amber-50/50 dark:bg-amber-950/20 dark:border-l-amber-400">
<CardContent className="pt-4 pb-4">
<div className="flex items-center gap-3">
<FlaskConical className="h-5 w-5 text-amber-600 dark:text-amber-400 shrink-0" />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-amber-900 dark:text-amber-200">
Testdata aktivt
</p>
<p className="text-xs text-amber-700 dark:text-amber-400 mt-0.5">
Rapporten visar importerad testdata istället för bokföringsdata.
{formatted && <> Importerat {formatted}.</>}
</p>
</div>
<div className="flex gap-1.5 shrink-0">
<Button variant="outline" size="sm" onClick={onReplace} className="h-7 text-xs">
<Replace className="h-3.5 w-3.5 mr-1" />
Ersätt
</Button>
<Button variant="outline" size="sm" onClick={onClear} className="h-7 text-xs">
<X className="h-3.5 w-3.5 mr-1" />
Rensa
</Button>
</div>
</div>
</CardContent>
</Card>
)
}
@@ -0,0 +1,404 @@
'use client'
import { useState, useCallback, useRef } from 'react'
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Label } from '@/components/ui/label'
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from '@/components/ui/select'
import {
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
} from '@/components/ui/table'
import {
Upload, FileJson, FileSpreadsheet, Download, AlertCircle, Check,
} from 'lucide-react'
import { cn } from '@/lib/utils'
// ── Types ─────────────────────────────────────────────────────
export interface CsvFieldDef {
key: string
label: string
required?: boolean
}
interface MockDataImportDialogProps<T> {
open: boolean
onOpenChange: (open: boolean) => void
csvFields: CsvFieldDef[]
defaultMappings?: Record<string, string>
parseCsvRows: (rows: Record<string, string>[]) => T
validateReport: (data: unknown) => { valid: boolean; error?: string }
templateCsvContent: string
templateFileName: string
onImport: (report: T, meta: { source: 'csv' | 'json'; fileName: string; rowCount: number }) => Promise<void>
}
function parseCsv(text: string): { headers: string[]; rows: string[][] } {
const lines = text.split(/\r?\n/).filter(line => line.trim())
if (lines.length === 0) return { headers: [], rows: [] }
const separator = lines[0].includes(';') ? ';' : ','
const headers = lines[0].split(separator).map(h => h.trim().replace(/^"(.*)"$/, '$1'))
const rows = lines.slice(1).map(line =>
line.split(separator).map(cell => cell.trim().replace(/^"(.*)"$/, '$1'))
)
return { headers, rows }
}
// ── Component ─────────────────────────────────────────────────
type Step = 'upload' | 'map-csv' | 'preview-json' | 'importing'
export default function MockDataImportDialog<T>({
open,
onOpenChange,
csvFields,
defaultMappings,
parseCsvRows,
validateReport,
templateCsvContent,
templateFileName,
onImport,
}: MockDataImportDialogProps<T>) {
const [step, setStep] = useState<Step>('upload')
const [isDragging, setIsDragging] = useState(false)
const [error, setError] = useState<string | null>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
// CSV state
const [csvHeaders, setCsvHeaders] = useState<string[]>([])
const [csvRows, setCsvRows] = useState<string[][]>([])
const [mappings, setMappings] = useState<Record<string, string>>({})
const [fileName, setFileName] = useState('')
// JSON state
const [jsonReport, setJsonReport] = useState<T | null>(null)
const [jsonSummary, setJsonSummary] = useState('')
const reset = useCallback(() => {
setStep('upload')
setError(null)
setCsvHeaders([])
setCsvRows([])
setMappings({})
setFileName('')
setJsonReport(null)
setJsonSummary('')
setIsDragging(false)
}, [])
const handleOpenChange = useCallback((open: boolean) => {
if (!open) reset()
onOpenChange(open)
}, [onOpenChange, reset])
const processFile = useCallback((file: File) => {
setError(null)
setFileName(file.name)
const reader = new FileReader()
reader.onload = (ev) => {
const text = ev.target?.result as string
if (file.name.endsWith('.json')) {
// JSON path
try {
const parsed = JSON.parse(text)
const validation = validateReport(parsed)
if (!validation.valid) {
setError(validation.error || 'Ogiltig JSON-struktur')
return
}
setJsonReport(parsed as T)
// Build summary
const keys = Object.keys(parsed)
const lines = Array.isArray(parsed.lines) ? parsed.lines.length
: Array.isArray(parsed.receivables) ? parsed.receivables.length
: Array.isArray(parsed.boxes) ? parsed.boxes.length
: null
setJsonSummary(
`${keys.length} fält` + (lines !== null ? `, ${lines} rader` : '')
)
setStep('preview-json')
} catch {
setError('Kunde inte tolka JSON-filen. Kontrollera formatet.')
}
} else {
// CSV path
const parsed = parseCsv(text)
if (parsed.headers.length === 0 || parsed.rows.length === 0) {
setError('Ingen data hittades i CSV-filen.')
return
}
setCsvHeaders(parsed.headers)
setCsvRows(parsed.rows)
// Auto-map columns
const autoMappings: Record<string, string> = {}
for (const field of csvFields) {
const defaultCol = defaultMappings?.[field.key]
if (defaultCol && parsed.headers.includes(defaultCol)) {
autoMappings[field.key] = defaultCol
} else {
const match = parsed.headers.find(
h => h.toLowerCase() === field.key.toLowerCase() ||
h.toLowerCase() === field.label.toLowerCase()
)
if (match) autoMappings[field.key] = match
}
}
setMappings(autoMappings)
setStep('map-csv')
}
}
reader.readAsText(file)
}, [csvFields, defaultMappings, validateReport])
const handleDrop = useCallback((e: React.DragEvent) => {
e.preventDefault()
setIsDragging(false)
const file = e.dataTransfer.files[0]
if (file) processFile(file)
}, [processFile])
const handleFileInput = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (file) processFile(file)
}, [processFile])
const handleCsvImport = useCallback(async () => {
setStep('importing')
setError(null)
try {
const mappedRows = csvRows.map(row => {
const obj: Record<string, string> = {}
for (const [fieldKey, csvCol] of Object.entries(mappings)) {
const colIdx = csvHeaders.indexOf(csvCol)
if (colIdx >= 0 && row[colIdx]) {
obj[fieldKey] = row[colIdx]
}
}
return obj
}).filter(row => Object.keys(row).length > 0)
const report = parseCsvRows(mappedRows)
await onImport(report, { source: 'csv', fileName, rowCount: mappedRows.length })
handleOpenChange(false)
} catch (e) {
setError(e instanceof Error ? e.message : 'Import misslyckades')
setStep('map-csv')
}
}, [csvRows, csvHeaders, mappings, parseCsvRows, onImport, fileName, handleOpenChange])
const handleJsonImport = useCallback(async () => {
if (!jsonReport) return
setStep('importing')
setError(null)
try {
await onImport(jsonReport, { source: 'json', fileName, rowCount: 0 })
handleOpenChange(false)
} catch (e) {
setError(e instanceof Error ? e.message : 'Import misslyckades')
setStep('preview-json')
}
}, [jsonReport, onImport, fileName, handleOpenChange])
const downloadTemplate = useCallback(() => {
const blob = new Blob([templateCsvContent], { type: 'text/csv;charset=utf-8;' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = templateFileName
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
}, [templateCsvContent, templateFileName])
const requiredFieldsMapped = csvFields
.filter(f => f.required)
.every(f => mappings[f.key])
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="max-w-2xl max-h-[85vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>
{step === 'upload' && 'Importera testdata'}
{step === 'map-csv' && 'Kolumnmappning'}
{step === 'preview-json' && 'Förhandsgranska JSON'}
{step === 'importing' && 'Importerar...'}
</DialogTitle>
</DialogHeader>
{/* ── Error ─────────────────────────────────────── */}
{error && (
<div className="flex items-start gap-2 p-3 rounded-md bg-destructive/5 text-destructive text-sm">
<AlertCircle className="h-4 w-4 mt-0.5 shrink-0" />
<span>{error}</span>
</div>
)}
{/* ── Step: Upload ──────────────────────────────── */}
{step === 'upload' && (
<div className="space-y-4">
<div
className={cn(
'border-2 border-dashed rounded-lg p-10 text-center transition-colors',
isDragging
? 'border-primary bg-primary/5'
: 'border-muted-foreground/25 hover:border-muted-foreground/50'
)}
onDragOver={(e) => { e.preventDefault(); setIsDragging(true) }}
onDragLeave={() => setIsDragging(false)}
onDrop={handleDrop}
>
<Upload className="h-8 w-8 mx-auto mb-3 text-muted-foreground" />
<p className="text-sm font-medium mb-1">
Dra och släpp en fil här
</p>
<p className="text-xs text-muted-foreground mb-4">
CSV (.csv) eller JSON (.json)
</p>
<Button
variant="outline"
size="sm"
onClick={() => fileInputRef.current?.click()}
>
Välj fil
</Button>
<input
ref={fileInputRef}
type="file"
accept=".csv,.json"
onChange={handleFileInput}
className="hidden"
/>
</div>
<div className="flex items-center justify-center">
<Button
variant="ghost"
size="sm"
onClick={downloadTemplate}
className="text-xs text-muted-foreground"
>
<Download className="h-3.5 w-3.5 mr-1.5" />
Ladda ner CSV-mall
</Button>
</div>
</div>
)}
{/* ── Step: Map CSV ─────────────────────────────── */}
{step === 'map-csv' && (
<div className="space-y-4">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<FileSpreadsheet className="h-4 w-4" />
<span>{fileName} — {csvRows.length} rader</span>
</div>
<div className="space-y-3">
{csvFields.map(field => (
<div key={field.key} className="flex items-center gap-3">
<Label className="w-40 text-sm shrink-0">
{field.label}{field.required && ' *'}
</Label>
<Select
value={mappings[field.key] ?? '___none___'}
onValueChange={(val) => setMappings(prev => ({
...prev,
[field.key]: val === '___none___' ? '' : val,
}))}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Välj kolumn..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="___none___">— Välj kolumn —</SelectItem>
{csvHeaders.map(h => (
<SelectItem key={h} value={h}>{h}</SelectItem>
))}
</SelectContent>
</Select>
</div>
))}
</div>
{/* Preview first 5 rows */}
{csvRows.length > 0 && (
<div className="rounded-lg border overflow-auto max-h-48">
<Table>
<TableHeader>
<TableRow>
{csvHeaders.map(h => (
<TableHead key={h} className="text-xs whitespace-nowrap">{h}</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{csvRows.slice(0, 5).map((row, i) => (
<TableRow key={i}>
{row.map((cell, j) => (
<TableCell key={j} className="text-xs whitespace-nowrap">{cell}</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={reset}>Tillbaka</Button>
<Button
onClick={handleCsvImport}
disabled={!requiredFieldsMapped}
>
Importera {csvRows.length} rader
</Button>
</DialogFooter>
</div>
)}
{/* ── Step: Preview JSON ────────────────────────── */}
{step === 'preview-json' && (
<div className="space-y-4">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<FileJson className="h-4 w-4" />
<span>{fileName}</span>
</div>
<div className="flex items-center gap-2 p-3 rounded-md bg-green-50 dark:bg-green-950/30 text-green-700 dark:text-green-400 text-sm">
<Check className="h-4 w-4 shrink-0" />
<span>Giltig JSON — {jsonSummary}</span>
</div>
<DialogFooter>
<Button variant="outline" onClick={reset}>Tillbaka</Button>
<Button onClick={handleJsonImport}>
Importera testdata
</Button>
</DialogFooter>
</div>
)}
{/* ── Step: Importing ───────────────────────────── */}
{step === 'importing' && (
<div className="py-8 text-center">
<div className="animate-spin h-8 w-8 border-2 border-primary border-t-transparent rounded-full mx-auto mb-3" />
<p className="text-sm text-muted-foreground">Importerar testdata...</p>
</div>
)}
</DialogContent>
</Dialog>
)
}