'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 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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { Pencil, Trash2, AlertTriangle, ChevronLeft, ChevronRight } from 'lucide-react' interface DailySale { date: string total: number cash: number card: number swish: number vat: number } interface ImportRecord { id: string date: string fileName: string rowCount: number } const PAGE_SIZE = 20 const TARGET_FIELDS = [ { key: 'date', label: 'Datum', required: true }, { key: 'total', label: 'Totalt', required: true }, { key: 'cash', label: 'Kontant' }, { key: 'card', label: 'Kort' }, { key: 'swish', label: 'Swish' }, { key: 'vat', label: 'Moms' }, ] const DEFAULT_MAPPINGS: Record = { date: 'Datum', total: 'Total', cash: 'Kontant', card: 'Kort', swish: 'Swish', vat: 'Moms', } const parseNum = (v?: string) => { if (!v) return 0 return Math.round(parseFloat(v.replace(/\s/g, '').replace(',', '.')) * 100) / 100 || 0 } export default function PosImportWorkspace({}: WorkspaceComponentProps) { const { data, save, remove, refresh, isLoading } = useExtensionData('restaurant', 'pos-import') // Pagination const [page, setPage] = useState(0) // Manual entry form state const now = new Date() const [entryDate, setEntryDate] = useState(now.toISOString().slice(0, 10)) const [entryTotal, setEntryTotal] = useState('') const [entryCash, setEntryCash] = useState('') const [entryCard, setEntryCard] = useState('') const [entrySwish, setEntrySwish] = useState('') const [entryVat, setEntryVat] = useState('') const [isSubmitting, setIsSubmitting] = useState(false) // Edit dialog state const [editEntry, setEditEntry] = useState(null) const [editTotal, setEditTotal] = useState('') const [editCash, setEditCash] = useState('') const [editCard, setEditCard] = useState('') const [editSwish, setEditSwish] = useState('') const [editVat, setEditVat] = useState('') const [isSaving, setIsSaving] = useState(false) // Delete dialog state const [deleteEntry, setDeleteEntry] = useState(null) const [isDeleting, setIsDeleting] = useState(false) const dailySales = useMemo(() => data.filter(d => d.key.startsWith('daily:')) .map(d => ({ date: d.key.replace('daily:', ''), ...(d.value as Omit) })) .sort((a, b) => b.date.localeCompare(a.date)) , [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]) // Pagination calculations const totalPages = Math.max(1, Math.ceil(dailySales.length / PAGE_SIZE)) const paginatedSales = dailySales.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE) const handleImport = async (rows: Record[]) => { const importId = crypto.randomUUID() let count = 0 for (const row of rows) { const date = row.date if (!date) continue await save(`daily:${date}`, { total: parseNum(row.total), cash: parseNum(row.cash), card: parseNum(row.card), swish: parseNum(row.swish), vat: parseNum(row.vat), }) count++ } await save(`import:${importId}`, { date: new Date().toISOString().slice(0, 10), fileName: `CSV-import`, rowCount: count, }) await refresh() setPage(0) } // Manual entry validation const manualPaymentSum = Math.round( ((parseFloat(entryCash) || 0) + (parseFloat(entryCard) || 0) + (parseFloat(entrySwish) || 0)) * 100 ) / 100 const manualTotal = Math.round((parseFloat(entryTotal) || 0) * 100) / 100 const manualDiffPct = manualTotal > 0 ? Math.round(Math.abs(manualPaymentSum - manualTotal) / manualTotal * 10000) / 100 : 0 const showManualWarning = manualTotal > 0 && manualPaymentSum > 0 && manualDiffPct > 5 const handleManualSubmit = async (e: React.FormEvent) => { e.preventDefault() if (!entryDate || !entryTotal) return setIsSubmitting(true) const total = Math.round((parseFloat(entryTotal) || 0) * 100) / 100 const cash = Math.round((parseFloat(entryCash) || 0) * 100) / 100 const card = Math.round((parseFloat(entryCard) || 0) * 100) / 100 const swish = Math.round((parseFloat(entrySwish) || 0) * 100) / 100 const vat = Math.round((parseFloat(entryVat) || 0) * 100) / 100 await save(`daily:${entryDate}`, { total, cash, card, swish, vat }) await refresh() setEntryTotal('') setEntryCash('') setEntryCard('') setEntrySwish('') setEntryVat('') setIsSubmitting(false) setPage(0) } // Edit handlers const openEdit = (entry: DailySale) => { setEditEntry(entry) setEditTotal(String(entry.total)) setEditCash(String(entry.cash)) setEditCard(String(entry.card)) setEditSwish(String(entry.swish)) setEditVat(String(entry.vat)) } const handleEditSave = async () => { if (!editEntry) return setIsSaving(true) const total = Math.round((parseFloat(editTotal) || 0) * 100) / 100 const cash = Math.round((parseFloat(editCash) || 0) * 100) / 100 const card = Math.round((parseFloat(editCard) || 0) * 100) / 100 const swish = Math.round((parseFloat(editSwish) || 0) * 100) / 100 const vat = Math.round((parseFloat(editVat) || 0) * 100) / 100 await save(`daily:${editEntry.date}`, { total, cash, card, swish, vat }) await refresh() setIsSaving(false) } // Edit dialog validation const editPaymentSum = Math.round( ((parseFloat(editCash) || 0) + (parseFloat(editCard) || 0) + (parseFloat(editSwish) || 0)) * 100 ) / 100 const editTotalVal = Math.round((parseFloat(editTotal) || 0) * 100) / 100 const editDiffPct = editTotalVal > 0 ? Math.round(Math.abs(editPaymentSum - editTotalVal) / editTotalVal * 10000) / 100 : 0 const showEditWarning = editTotalVal > 0 && editPaymentSum > 0 && editDiffPct > 5 // Delete handler const handleDelete = async () => { if (!deleteEntry) return setIsDeleting(true) await remove(`daily:${deleteEntry.date}`) await refresh() setIsDeleting(false) } // KPI calculations const totals = useMemo(() => { const total = dailySales.reduce((s, d) => s + d.total, 0) const cash = dailySales.reduce((s, d) => s + d.cash, 0) const card = dailySales.reduce((s, d) => s + d.card, 0) const swish = dailySales.reduce((s, d) => s + d.swish, 0) const vat = dailySales.reduce((s, d) => s + d.vat, 0) const avg = dailySales.length > 0 ? Math.round(total / dailySales.length) : 0 return { total, cash, card, swish, vat, avg } }, [dailySales]) // VAT analytics const vatPct = totals.total > 0 ? Math.round(totals.vat / totals.total * 10000) / 100 : 0 const vatOutOfRange = vatPct > 0 && (vatPct < 20 || vatPct > 30) // Payment method monthly breakdown const paymentMonthly = useMemo(() => { const map = new Map() for (const d of dailySales) { const month = d.date.slice(0, 7) const existing = map.get(month) ?? { cash: 0, card: 0, swish: 0, total: 0 } existing.cash += d.cash existing.card += d.card existing.swish += d.swish existing.total += d.total map.set(month, existing) } return Array.from(map.entries()) .sort(([a], [b]) => b.localeCompare(a)) .map(([month, vals]) => ({ month, cashPct: vals.total > 0 ? Math.round(vals.cash / vals.total * 10000) / 100 : 0, cardPct: vals.total > 0 ? Math.round(vals.card / vals.total * 10000) / 100 : 0, swishPct: vals.total > 0 ? Math.round(vals.swish / vals.total * 10000) / 100 : 0, cash: vals.cash, card: vals.card, swish: vals.swish, total: vals.total, })) }, [dailySales]) // VAT per month const vatMonthly = useMemo(() => { const map = new Map() for (const d of dailySales) { const month = d.date.slice(0, 7) const existing = map.get(month) ?? { vat: 0, total: 0 } existing.vat += d.vat existing.total += d.total map.set(month, existing) } return Array.from(map.entries()) .sort(([a], [b]) => b.localeCompare(a)) .map(([month, vals]) => ({ month, vat: Math.round(vals.vat * 100) / 100, total: Math.round(vals.total * 100) / 100, vatPct: vals.total > 0 ? Math.round(vals.vat / vals.total * 10000) / 100 : 0, })) }, [dailySales]) if (isLoading) return return (
Import Registrera Historik {/* CSV Import tab */} {/* Manual entry tab */}
setEntryDate(e.target.value)} required />
setEntryTotal(e.target.value)} required />
setEntryCash(e.target.value)} />
setEntryCard(e.target.value)} />
setEntrySwish(e.target.value)} />
setEntryVat(e.target.value)} />
{showManualWarning && (
Kontant + Kort + Swish ({manualPaymentSum.toLocaleString('sv-SE')} kr) avviker {manualDiffPct}% fran Totalt ({manualTotal.toLocaleString('sv-SE')} kr).
)}
{/* History tab */} {/* KPI cards */}
{/* VAT alert */} {vatOutOfRange && dailySales.length > 0 && (
Momsandelen ({vatPct}%) ligger utanfor forvantat intervall (20-30%) for svenska restauranger. Kontrollera att moms registreras korrekt.
)} {/* Import history */} {imports.length > 0 && (

Importer

Datum Fil Rader {imports.map(imp => ( {imp.date} {imp.fileName} {imp.rowCount} ))}
)} {/* VAT per month */} {vatMonthly.length > 0 && (

Moms per manad

Period Forsaljning Moms Momsandel {vatMonthly.map(row => ( {row.month} {row.total.toLocaleString('sv-SE')} kr {row.vat.toLocaleString('sv-SE')} kr 0 && (row.vatPct < 20 || row.vatPct > 30) ? 'text-yellow-600 dark:text-yellow-400' : ''}> {row.vatPct}% ))}
)} {/* Payment method trend */} {paymentMonthly.length > 0 && (

Betalmetoder per manad

Period Totalt Kontant % Kontant Kort % Kort Swish % Swish {paymentMonthly.map(row => ( {row.month} {row.total.toLocaleString('sv-SE')} kr {row.cash.toLocaleString('sv-SE')} kr {row.cashPct}% {row.card.toLocaleString('sv-SE')} kr {row.cardPct}% {row.swish.toLocaleString('sv-SE')} kr {row.swishPct}% ))}
)} {/* Daily sales table with pagination */}

Daglig forsaljning

{dailySales.length === 0 ? (

Ingen data importerad annu.

) : ( <>
Datum Totalt Kontant Kort Swish Moms Moms % {paginatedSales.map(d => { const rowVatPct = d.total > 0 ? Math.round(d.vat / d.total * 10000) / 100 : 0 return ( {d.date} {d.total.toLocaleString('sv-SE')} {d.cash.toLocaleString('sv-SE')} {d.card.toLocaleString('sv-SE')} {d.swish.toLocaleString('sv-SE')} {d.vat.toLocaleString('sv-SE')} 0 && (rowVatPct < 20 || rowVatPct > 30) ? 'text-yellow-600 dark:text-yellow-400' : ''}> {rowVatPct}%
) })}
{/* Pagination */} {totalPages > 1 && (

Visar {page * PAGE_SIZE + 1}-{Math.min((page + 1) * PAGE_SIZE, dailySales.length)} av {dailySales.length} rader

Sida {page + 1} av {totalPages}
)} )}
{/* Edit dialog */} { if (!open) setEditEntry(null) }} title="Redigera dagskassa" description={editEntry ? `Redigera data for ${editEntry.date}` : ''} onSave={handleEditSave} isSaving={isSaving} >
setEditTotal(e.target.value)} />
setEditCash(e.target.value)} />
setEditCard(e.target.value)} />
setEditSwish(e.target.value)} />
setEditVat(e.target.value)} />
{showEditWarning && (
Kontant + Kort + Swish ({editPaymentSum.toLocaleString('sv-SE')} kr) avviker {editDiffPct}% fran Totalt ({editTotalVal.toLocaleString('sv-SE')} kr).
)}
{/* Delete confirmation dialog */} { if (!open) setDeleteEntry(null) }} title="Ta bort dagskassa" description={deleteEntry ? `Vill du ta bort data for ${deleteEntry.date}? Atgarden kan inte angras.` : ''} onConfirm={handleDelete} isDeleting={isDeleting} />
) }