'use client' import { useState, useMemo, useCallback } from 'react' import type { WorkspaceComponentProps } from '@/lib/extensions/workspace-registry' import { useAccountTotals } from '@/lib/extensions/use-account-totals' import { useExtensionData } from '@/lib/extensions/use-extension-data' import KPICard from '@/components/extensions/shared/KPICard' import DateRangeFilter from '@/components/extensions/shared/DateRangeFilter' 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 { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Button } from '@/components/ui/button' import { Textarea } from '@/components/ui/textarea' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { ArrowUp, ArrowDown, Minus } from 'lucide-react' import { cn } from '@/lib/utils' const FOOD_CATEGORIES = ['Kott', 'Fisk', 'Gronsaker', 'Mejeri', 'Drycker', 'Ovrigt'] as const type FoodCategory = typeof FOOD_CATEGORIES[number] function computePreviousPeriod(start: string, end: string): { start: string; end: string } { const startDate = new Date(start + 'T00:00:00') const endDate = new Date(end + 'T00:00:00') const durationMs = endDate.getTime() - startDate.getTime() const prevEnd = new Date(startDate.getTime() - 1) const prevStart = new Date(prevEnd.getTime() - durationMs) return { start: prevStart.toISOString().slice(0, 10), end: prevEnd.toISOString().slice(0, 10), } } function DeltaArrow({ current, previous }: { current: number; previous: number }) { const delta = Math.round((current - previous) * 100) / 100 if (delta === 0 || (previous === 0 && current === 0)) { return ( 0 pp ) } // For food cost: lower is better, so negative delta = green (improving) const improving = delta < 0 return ( {delta < 0 ? : } {delta > 0 ? '+' : ''}{delta} pp ) } export default function FoodCostWorkspace({}: WorkspaceComponentProps) { const now = new Date() const [dateRange, setDateRange] = useState({ start: new Date(now.getFullYear(), now.getMonth(), 1).toISOString().slice(0, 10), end: new Date(now.getFullYear(), now.getMonth() + 1, 0).toISOString().slice(0, 10), }) const prevPeriod = useMemo( () => computePreviousPeriod(dateRange.start, dateRange.end), [dateRange.start, dateRange.end] ) // Yearly range for monthly trend const yearStart = `${now.getFullYear()}-01-01` const yearEnd = `${now.getFullYear()}-12-31` const { data: extData, save, remove, isLoading: settingsLoading } = useExtensionData('restaurant', 'food-cost') const settings = extData.find(d => d.key === 'settings')?.value as { targetPct?: number } | undefined const [targetPctInput, setTargetPct] = useState(null) const [editingTarget, setEditingTarget] = useState(false) const targetPct = targetPctInput ?? (settings?.targetPct != null ? String(settings.targetPct) : '') // Category assignments from extension_data (key = "category:{accountNumber}") const categoryMap = useMemo(() => { const map: Record = {} for (const d of extData) { if (d.key.startsWith('category:')) { const account = d.key.replace('category:', '') map[account] = (d.value as { category: FoodCategory }).category } } return map }, [extData]) // Notes from extension_data (key = "note:YYYY-MM") const currentMonth = dateRange.start.slice(0, 7) const currentNote = extData.find(d => d.key === `note:${currentMonth}`)?.value as { text: string } | undefined const [noteText, setNoteText] = useState('') const [editingNote, setEditingNote] = useState(false) const [savingNote, setSavingNote] = useState(false) const [deleteNoteOpen, setDeleteNoteOpen] = useState(false) const [deletingNote, setDeletingNote] = useState(false) // Target edit dialog state const [editTargetDialogOpen, setEditTargetDialogOpen] = useState(false) const [newTargetInput, setNewTargetInput] = useState('') const [savingTarget, setSavingTarget] = useState(false) // --- Current period account totals --- const { totals: purchaseTotals, isLoading: purchasesLoading } = useAccountTotals({ from: '4000', to: '4999', dateFrom: dateRange.start, dateTo: dateRange.end, }) const { totals: revenueTotals, isLoading: revenueLoading } = useAccountTotals({ from: '3000', to: '3999', dateFrom: dateRange.start, dateTo: dateRange.end, }) // --- Previous period account totals --- const { totals: prevPurchaseTotals, isLoading: prevPurchasesLoading } = useAccountTotals({ from: '4000', to: '4999', dateFrom: prevPeriod.start, dateTo: prevPeriod.end, }) const { totals: prevRevenueTotals, isLoading: prevRevenueLoading } = useAccountTotals({ from: '3000', to: '3999', dateFrom: prevPeriod.start, dateTo: prevPeriod.end, }) // Monthly trend data const { monthly: purchaseMonthly } = useAccountTotals({ from: '4000', to: '4999', dateFrom: yearStart, dateTo: yearEnd, groupBy: 'month', }) const { monthly: revenueMonthly } = useAccountTotals({ from: '3000', to: '3999', dateFrom: yearStart, dateTo: yearEnd, groupBy: 'month', }) // Current period calculations const totalPurchases = purchaseTotals.reduce((sum, t) => sum + t.debit, 0) const totalRevenue = revenueTotals.reduce((sum, t) => sum + t.credit, 0) const foodCostPct = totalRevenue > 0 ? Math.round((totalPurchases / totalRevenue) * 10000) / 100 : 0 // Previous period calculations const prevTotalPurchases = prevPurchaseTotals.reduce((sum, t) => sum + t.debit, 0) const prevTotalRevenue = prevRevenueTotals.reduce((sum, t) => sum + t.credit, 0) const prevFoodCostPct = prevTotalRevenue > 0 ? Math.round((prevTotalPurchases / prevTotalRevenue) * 10000) / 100 : 0 const target = settings?.targetPct ?? 30 // Monthly trend rows const monthlyTrend = useMemo(() => { const months = new Set([ ...purchaseMonthly.map(m => m.month), ...revenueMonthly.map(m => m.month), ]) return Array.from(months).sort().map(month => { const purch = purchaseMonthly.filter(m => m.month === month).reduce((s, m) => s + m.debit, 0) const rev = revenueMonthly.filter(m => m.month === month).reduce((s, m) => s + m.credit, 0) const pct = rev > 0 ? Math.round((purch / rev) * 10000) / 100 : 0 return { month, value: pct } }) }, [purchaseMonthly, revenueMonthly]) // Category breakdown const categoryBreakdown = useMemo(() => { const groups: Record = {} for (const cat of FOOD_CATEGORIES) { groups[cat] = { category: cat, total: 0, accounts: [] } } let uncategorizedTotal = 0 const uncategorizedAccounts: string[] = [] for (const t of purchaseTotals) { const cat = categoryMap[t.account_number] if (cat && groups[cat]) { groups[cat].total += t.debit groups[cat].accounts.push(t.account_number) } else { uncategorizedTotal += t.debit uncategorizedAccounts.push(t.account_number) } } const result = FOOD_CATEGORIES .map(cat => groups[cat]) .filter(g => g.total > 0 || g.accounts.length > 0) if (uncategorizedTotal > 0) { result.push({ category: 'Ovrigt' as FoodCategory, total: uncategorizedTotal, accounts: uncategorizedAccounts }) } return result }, [purchaseTotals, categoryMap]) // --- Handlers --- const saveTarget = async () => { const val = parseFloat(targetPct) if (!isNaN(val)) { // Save target history before changing const oldTarget = settings?.targetPct if (oldTarget != null && oldTarget !== val) { await save(`target-history:${Date.now()}`, { previousTarget: oldTarget, newTarget: val, changedAt: new Date().toISOString(), }) } await save('settings', { targetPct: val }) setEditingTarget(false) } } const handleSaveTargetDialog = async () => { setSavingTarget(true) const val = parseFloat(newTargetInput) if (!isNaN(val)) { const oldTarget = settings?.targetPct if (oldTarget != null && oldTarget !== val) { await save(`target-history:${Date.now()}`, { previousTarget: oldTarget, newTarget: val, changedAt: new Date().toISOString(), }) } await save('settings', { targetPct: val }) } setSavingTarget(false) } const handleCategoryChange = useCallback(async (accountNumber: string, category: string) => { if (category === '__none__') { await remove(`category:${accountNumber}`) } else { await save(`category:${accountNumber}`, { category }) } }, [save, remove]) const handleSaveNote = async () => { setSavingNote(true) await save(`note:${currentMonth}`, { text: noteText }) setEditingNote(false) setSavingNote(false) } const handleDeleteNote = async () => { setDeletingNote(true) await remove(`note:${currentMonth}`) setNoteText('') setEditingNote(false) setDeletingNote(false) } const startEditNote = () => { setNoteText(currentNote?.text ?? '') setEditingNote(true) } const isLoading = purchasesLoading || revenueLoading || prevPurchasesLoading || prevRevenueLoading || settingsLoading if (isLoading) return return (
setDateRange({ start, end })} /> {/* KPI Cards with period comparison */}
{/* Period comparison */}

Periodjamforelse

Food Cost % (nuvarande)

{foodCostPct}%

Food Cost % (foregaende)

{prevFoodCostPct}%

Foregaende period

{prevPeriod.start} — {prevPeriod.end}

Varuinkop (foregaende)

{prevTotalPurchases.toLocaleString('sv-SE')} kr

Intakter (foregaende)

{prevTotalRevenue.toLocaleString('sv-SE')} kr
{/* Target setting */}

Malvarde

Riktvarde for food cost (vanligtvis 25-35%)

{editingTarget ? (
setTargetPct(e.target.value)} className="w-20 h-8 text-sm" />
) : ( )}
{/* Edit target dialog (saves history) */}
setNewTargetInput(e.target.value)} className="w-32" />
{/* Notes per period */}

Anteckningar for {currentMonth}

Notera avvikelser och forklaringar for perioden

{!editingNote && ( )}
{editingNote ? (