'use client' import { useState, useMemo } from 'react' import type { WorkspaceComponentProps } from '@/lib/extensions/workspace-registry' import { useExtensionData } from '@/lib/extensions/use-extension-data' import KPICard from '@/components/extensions/shared/KPICard' import CsvImportWizard from '@/components/extensions/shared/CsvImportWizard' import MonthlyTrendTable from '@/components/extensions/shared/MonthlyTrendTable' import ExtensionLoadingSkeleton from '@/components/extensions/shared/ExtensionLoadingSkeleton' import ConfirmDeleteDialog from '@/components/extensions/shared/ConfirmDeleteDialog' import EditEntryDialog from '@/components/extensions/shared/EditEntryDialog' import DataEntryForm from '@/components/extensions/shared/DataEntryForm' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' 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 { Badge } from '@/components/ui/badge' import { Pencil, Trash2, ChevronLeft, ChevronRight } from 'lucide-react' interface ShopifyOrder { id: string name: string createdAt: string total: number subtotal: number shipping: number taxes: number paymentMethod: string fulfillmentStatus: string } interface ImportRecord { id: string date: string rowCount: number } const TARGET_FIELDS = [ { key: 'name', label: 'Order', required: true }, { key: 'createdAt', label: 'Datum', required: true }, { key: 'total', label: 'Total', required: true }, { key: 'subtotal', label: 'Subtotal' }, { key: 'shipping', label: 'Frakt' }, { key: 'taxes', label: 'Moms' }, { key: 'paymentMethod', label: 'Betalmetod' }, { key: 'fulfillmentStatus', label: 'Leveransstatus' }, ] const DEFAULT_MAPPINGS: Record = { name: 'Name', createdAt: 'Created at', total: 'Total', subtotal: 'Subtotal', shipping: 'Shipping', taxes: 'Taxes', paymentMethod: 'Payment Method', fulfillmentStatus: 'Fulfillment Status', } const PAGES_SIZE = 20 export default function ShopifyImportWorkspace({}: WorkspaceComponentProps) { const { data, save, remove, refresh, isLoading } = useExtensionData('ecommerce', 'shopify-import') const orders = useMemo(() => data.filter(d => d.key.startsWith('order:')) .map(d => ({ id: d.key.replace('order:', ''), ...(d.value as Omit) })) .sort((a, b) => b.createdAt.localeCompare(a.createdAt)) , [data]) const imports = useMemo(() => data.filter(d => d.key.startsWith('import:')) .map(d => ({ id: d.key, ...(d.value as Omit) })) .sort((a, b) => b.date.localeCompare(a.date)) , [data]) // --------------------------------------------------------------------------- // Filter state // --------------------------------------------------------------------------- const [searchQuery, setSearchQuery] = useState('') const [dateFrom, setDateFrom] = useState('') const [dateTo, setDateTo] = useState('') const [paymentFilter, setPaymentFilter] = useState('__all__') const [fulfillmentFilter, setFulfillmentFilter] = useState('__all__') // Pagination state const [currentPage, setCurrentPage] = useState(1) // Edit order dialog state const [editOrder, setEditOrder] = useState(null) const [editName, setEditName] = useState('') const [editDate, setEditDate] = useState('') const [editTotal, setEditTotal] = useState('') const [editSubtotal, setEditSubtotal] = useState('') const [editShipping, setEditShipping] = useState('') const [editTaxes, setEditTaxes] = useState('') const [editPaymentMethod, setEditPaymentMethod] = useState('') const [editFulfillmentStatus, setEditFulfillmentStatus] = useState('') const [isSavingEdit, setIsSavingEdit] = useState(false) // Delete order dialog state const [deleteOrderId, setDeleteOrderId] = useState(null) const [isDeleting, setIsDeleting] = useState(false) // Manual entry form state const [manualName, setManualName] = useState('') const [manualDate, setManualDate] = useState(new Date().toISOString().slice(0, 10)) const [manualTotal, setManualTotal] = useState('') const [manualSubtotal, setManualSubtotal] = useState('') const [manualShipping, setManualShipping] = useState('') const [manualTaxes, setManualTaxes] = useState('') const [manualPaymentMethod, setManualPaymentMethod] = useState('') const [manualFulfillmentStatus, setManualFulfillmentStatus] = useState('') const [isSubmittingManual, setIsSubmittingManual] = useState(false) // --------------------------------------------------------------------------- // Distinct values for filter dropdowns // --------------------------------------------------------------------------- const paymentMethods = useMemo(() => { const set = new Set() for (const o of orders) { if (o.paymentMethod) set.add(o.paymentMethod) } return Array.from(set).sort() }, [orders]) const fulfillmentStatuses = useMemo(() => { const set = new Set() for (const o of orders) { if (o.fulfillmentStatus) set.add(o.fulfillmentStatus) } return Array.from(set).sort() }, [orders]) // --------------------------------------------------------------------------- // Active filter count // --------------------------------------------------------------------------- const activeFilterCount = useMemo(() => { let count = 0 if (searchQuery.trim()) count++ if (dateFrom) count++ if (dateTo) count++ if (paymentFilter !== '__all__') count++ if (fulfillmentFilter !== '__all__') count++ return count }, [searchQuery, dateFrom, dateTo, paymentFilter, fulfillmentFilter]) // --------------------------------------------------------------------------- // CSV import handler // --------------------------------------------------------------------------- const handleImport = async (rows: Record[]) => { const importId = crypto.randomUUID() let count = 0 for (const row of rows) { const parseNum = (v?: string) => { if (!v) return 0 return Math.round(parseFloat(v.replace(/\s/g, '').replace(',', '.')) * 100) / 100 || 0 } const orderId = crypto.randomUUID() await save(`order:${orderId}`, { name: row.name ?? '', createdAt: row.createdAt ?? new Date().toISOString().slice(0, 10), total: parseNum(row.total), subtotal: parseNum(row.subtotal), shipping: parseNum(row.shipping), taxes: parseNum(row.taxes), paymentMethod: row.paymentMethod ?? '', fulfillmentStatus: row.fulfillmentStatus ?? '', }) count++ } await save(`import:${importId}`, { date: new Date().toISOString().slice(0, 10), rowCount: count, }) await refresh() } // --------------------------------------------------------------------------- // Manual order entry handler // --------------------------------------------------------------------------- const handleManualSubmit = async (e: React.FormEvent) => { e.preventDefault() if (!manualName.trim()) return const parseNum = (v: string) => Math.round(parseFloat(v.replace(/\s/g, '').replace(',', '.')) * 100) / 100 || 0 setIsSubmittingManual(true) const orderId = crypto.randomUUID() await save(`order:${orderId}`, { name: manualName.trim(), createdAt: manualDate || new Date().toISOString().slice(0, 10), total: parseNum(manualTotal), subtotal: parseNum(manualSubtotal), shipping: parseNum(manualShipping), taxes: parseNum(manualTaxes), paymentMethod: manualPaymentMethod, fulfillmentStatus: manualFulfillmentStatus, }) setManualName('') setManualDate(new Date().toISOString().slice(0, 10)) setManualTotal('') setManualSubtotal('') setManualShipping('') setManualTaxes('') setManualPaymentMethod('') setManualFulfillmentStatus('') await refresh() setIsSubmittingManual(false) } // --------------------------------------------------------------------------- // Edit order handlers // --------------------------------------------------------------------------- const openEditOrder = (order: ShopifyOrder) => { setEditOrder(order) setEditName(order.name) setEditDate(order.createdAt) setEditTotal(String(order.total)) setEditSubtotal(String(order.subtotal)) setEditShipping(String(order.shipping)) setEditTaxes(String(order.taxes)) setEditPaymentMethod(order.paymentMethod) setEditFulfillmentStatus(order.fulfillmentStatus) } const handleSaveEdit = async () => { if (!editOrder) return const parseNum = (v: string) => Math.round(parseFloat(v.replace(/\s/g, '').replace(',', '.')) * 100) / 100 || 0 setIsSavingEdit(true) await save(`order:${editOrder.id}`, { name: editName, createdAt: editDate || new Date().toISOString().slice(0, 10), total: parseNum(editTotal), subtotal: parseNum(editSubtotal), shipping: parseNum(editShipping), taxes: parseNum(editTaxes), paymentMethod: editPaymentMethod, fulfillmentStatus: editFulfillmentStatus, }) await refresh() setIsSavingEdit(false) } // --------------------------------------------------------------------------- // Delete order handler // --------------------------------------------------------------------------- const handleConfirmDelete = async () => { if (!deleteOrderId) return setIsDeleting(true) await remove(`order:${deleteOrderId}`) await refresh() setIsDeleting(false) } // --------------------------------------------------------------------------- // Stats // --------------------------------------------------------------------------- const totalRevenue = orders.reduce((s, o) => s + o.total, 0) const aov = orders.length > 0 ? Math.round(totalRevenue / orders.length) : 0 const totalTaxes = orders.reduce((s, o) => s + o.taxes, 0) const totalSubtotal = orders.reduce((s, o) => s + o.subtotal, 0) const avgVatRate = totalSubtotal > 0 ? Math.round((totalTaxes / totalSubtotal) * 10000) / 100 : 0 // Monthly trend const monthlyTrend = useMemo(() => { const map = new Map() for (const o of orders) { const month = o.createdAt.slice(0, 7) const existing = map.get(month) ?? { revenue: 0, count: 0 } existing.revenue += o.total existing.count++ map.set(month, existing) } return Array.from(map.entries()) .sort(([a], [b]) => a.localeCompare(b)) .map(([month, data]) => ({ month, value: data.revenue })) }, [orders]) // Monthly VAT breakdown const monthlyVat = useMemo(() => { const map = new Map() for (const o of orders) { const month = o.createdAt.slice(0, 7) const existing = map.get(month) ?? { taxes: 0, subtotal: 0 } existing.taxes += o.taxes existing.subtotal += o.subtotal map.set(month, existing) } return Array.from(map.entries()) .sort(([a], [b]) => a.localeCompare(b)) .map(([month, d]) => ({ month, taxes: Math.round(d.taxes * 100) / 100, subtotal: Math.round(d.subtotal * 100) / 100, rate: d.subtotal > 0 ? Math.round((d.taxes / d.subtotal) * 10000) / 100 : 0, })) }, [orders]) // Payment method breakdown const paymentBreakdown = useMemo(() => { const map = new Map() for (const o of orders) { const method = o.paymentMethod || 'Okant' const existing = map.get(method) ?? { count: 0, total: 0 } existing.count++ existing.total += o.total map.set(method, existing) } return Array.from(map.entries()) .map(([method, data]) => ({ method, ...data })) .sort((a, b) => b.total - a.total) }, [orders]) // Fulfillment breakdown const fulfillmentBreakdown = useMemo(() => { const map = new Map() for (const o of orders) { const status = o.fulfillmentStatus || 'Okant' map.set(status, (map.get(status) ?? 0) + 1) } return Array.from(map.entries()) .map(([status, count]) => ({ status, count })) .sort((a, b) => b.count - a.count) }, [orders]) // --------------------------------------------------------------------------- // Filtered & paginated orders // --------------------------------------------------------------------------- const filteredOrders = useMemo(() => { let result = orders if (searchQuery.trim()) { const q = searchQuery.toLowerCase() result = result.filter(o => o.name.toLowerCase().includes(q) || o.paymentMethod.toLowerCase().includes(q) || o.fulfillmentStatus.toLowerCase().includes(q) ) } if (dateFrom) { result = result.filter(o => o.createdAt >= dateFrom) } if (dateTo) { result = result.filter(o => o.createdAt <= dateTo) } if (paymentFilter !== '__all__') { result = result.filter(o => o.paymentMethod === paymentFilter) } if (fulfillmentFilter !== '__all__') { result = result.filter(o => o.fulfillmentStatus === fulfillmentFilter) } return result }, [orders, searchQuery, dateFrom, dateTo, paymentFilter, fulfillmentFilter]) const totalPages = Math.max(1, Math.ceil(filteredOrders.length / PAGES_SIZE)) const safePage = Math.min(currentPage, totalPages) const paginatedOrders = filteredOrders.slice( (safePage - 1) * PAGES_SIZE, safePage * PAGES_SIZE ) // Reset page when filters change const resetPage = () => setCurrentPage(1) // --------------------------------------------------------------------------- // Clear filters // --------------------------------------------------------------------------- const clearFilters = () => { setSearchQuery('') setDateFrom('') setDateTo('') setPaymentFilter('__all__') setFulfillmentFilter('__all__') setCurrentPage(1) } // --------------------------------------------------------------------------- // Render // --------------------------------------------------------------------------- if (isLoading) return return (
{/* Edit order dialog */} { if (!open) setEditOrder(null) }} title="Redigera order" description="Andra uppgifterna for denna order." onSave={handleSaveEdit} isSaving={isSavingEdit} >
setEditName(e.target.value)} />
setEditDate(e.target.value)} />
setEditTotal(e.target.value)} />
setEditSubtotal(e.target.value)} />
setEditShipping(e.target.value)} />
setEditTaxes(e.target.value)} />
setEditPaymentMethod(e.target.value)} />
setEditFulfillmentStatus(e.target.value)} />
{/* Delete order dialog */} { if (!open) setDeleteOrderId(null) }} title="Ta bort order" description="Ar du saker pa att du vill ta bort denna order? Atgarden kan inte angras." onConfirm={handleConfirmDelete} isDeleting={isDeleting} /> Import Ordrar Statistik {/* ------------------------------------------------------------------ */} {/* Import tab */} {/* ------------------------------------------------------------------ */} {/* Manual order entry */}
setManualName(e.target.value)} placeholder="t.ex. #1001" required />
setManualDate(e.target.value)} required />
setManualTotal(e.target.value)} />
setManualSubtotal(e.target.value)} />
setManualShipping(e.target.value)} />
setManualTaxes(e.target.value)} />
setManualPaymentMethod(e.target.value)} placeholder="t.ex. Stripe" />
setManualFulfillmentStatus(e.target.value)} placeholder="t.ex. fulfilled" />
{imports.length > 0 && (

Importhistorik

Datum Ordrar {imports.map(imp => ( {imp.date} {imp.rowCount} ))}
)}
{/* ------------------------------------------------------------------ */} {/* Orders tab */} {/* ------------------------------------------------------------------ */} {/* Filters */}
{ setSearchQuery(e.target.value); resetPage() }} className="w-48" />
{ setDateFrom(e.target.value); resetPage() }} className="w-40" />
{ setDateTo(e.target.value); resetPage() }} className="w-40" />
{activeFilterCount > 0 && (
{activeFilterCount} aktiva filter
)}
{filteredOrders.length === 0 ? (

Inga ordrar hittades.

) : ( <>
Order Datum Total Frakt Moms Betalning Status {paginatedOrders.map(o => ( {o.name} {o.createdAt} {o.total.toLocaleString('sv-SE')} kr {o.shipping.toLocaleString('sv-SE')} kr {o.taxes.toLocaleString('sv-SE')} kr {o.paymentMethod} {o.fulfillmentStatus || 'Okant'}
))}
{/* Pagination */}

{filteredOrders.length} ordrar totalt

Sida {safePage} av {totalPages}
)}
{/* ------------------------------------------------------------------ */} {/* Stats tab */} {/* ------------------------------------------------------------------ */}
{/* VAT analytics */}

Momsanalys

{monthlyVat.length > 0 && (
Manad Subtotal Moms Momssats {monthlyVat.map(m => ( {m.month} {m.subtotal.toLocaleString('sv-SE')} kr {m.taxes.toLocaleString('sv-SE')} kr {m.rate}% ))}
)}
{monthlyTrend.length > 0 && (

Intakt per manad

)} {paymentBreakdown.length > 0 && (

Per betalmetod

Betalmetod Ordrar Total {paymentBreakdown.map(p => ( {p.method} {p.count} {p.total.toLocaleString('sv-SE')} kr ))}
)} {fulfillmentBreakdown.length > 0 && (

Per leveransstatus

Status Ordrar {fulfillmentBreakdown.map(f => ( {f.status} {f.count} ))}
)}
) }