import { useState, useEffect } from 'react' import { useQuery } from '@tanstack/react-query' import { useAuth } from '../App' interface SupplierBreakdown { supplier_id: number | null supplier_name: string net_purchases: number percentage: number } interface GLAccountBreakdown { gl_account_id: number gl_account_name: string net_revenue: number percentage: number } interface DateRangeGPResponse { from_date: string to_date: string period_label: string net_food_sales: number net_food_purchases: number gross_profit: number gross_profit_percent: number supplier_breakdown: SupplierBreakdown[] gl_account_breakdown: GLAccountBreakdown[] // Cost distribution breakdown cd_deductions_total: number | null cd_reallocations_total: number | null // Allowances breakdown by type wastage_total: number | null transfer_total: number | null staff_food_total: number | null manual_adjustment_total: number | null disputes_total: number | null } interface DailyDataPoint { date: string net_sales: number net_purchases: number occupancy: number | null lunch_covers: number | null dinner_covers: number | null total_covers: number | null } interface DailyChartData { from_date: string to_date: string data: DailyDataPoint[] } interface TopSellerItem { item_name: string qty: number revenue: number } interface PackageFavoriteItem { item_name: string qty: number } interface CategoryTopSellers { category: string top_by_qty: TopSellerItem[] top_by_revenue: TopSellerItem[] } interface TopSellersResponse { from_date: string to_date: string source: 'sambapos' | 'newbook' // SambaPOS category-based format categories: CategoryTopSellers[] // Legacy Newbook format top_by_qty: TopSellerItem[] top_by_revenue: TopSellerItem[] package_favorites: PackageFavoriteItem[] total_charges_processed: number total_items_aggregated: number } // Helper to format date as YYYY-MM-DD for input fields and API const formatDate = (d: Date): string => { return d.toISOString().split('T')[0] } // Get the start and end of a given month const getMonthBounds = (year: number, month: number): { start: string; end: string } => { const start = new Date(year, month, 1) const end = new Date(year, month + 1, 0) // Last day of month return { start: formatDate(start), end: formatDate(end) } } // Generate list of months for the picker (last 12 months + next 2 months) const getMonthOptions = (): { label: string; year: number; month: number }[] => { const options: { label: string; year: number; month: number }[] = [] const today = new Date() // Start from 12 months ago for (let i = -12; i <= 2; i++) { const d = new Date(today.getFullYear(), today.getMonth() + i, 1) const label = d.toLocaleDateString('en-GB', { month: 'long', year: 'numeric' }) options.push({ label, year: d.getFullYear(), month: d.getMonth() }) } return options.reverse() // Most recent first } // Session storage keys for persisting dates while tab is open const STORAGE_KEY_FROM = 'gp-report-from-date' const STORAGE_KEY_TO = 'gp-report-to-date' // Get initial dates - from sessionStorage if available, otherwise default to last 30 days const getInitialDates = () => { const storedFrom = sessionStorage.getItem(STORAGE_KEY_FROM) const storedTo = sessionStorage.getItem(STORAGE_KEY_TO) if (storedFrom && storedTo) { return { from: storedFrom, to: storedTo } } // Default: rolling past 30 days from yesterday const yesterday = new Date() yesterday.setDate(yesterday.getDate() - 1) const thirtyDaysAgo = new Date(yesterday) thirtyDaysAgo.setDate(yesterday.getDate() - 30) return { from: formatDate(thirtyDaysAgo), to: formatDate(yesterday) } } export default function GPReport() { const { token } = useAuth() // Get initial dates (from session or defaults) const initialDates = getInitialDates() // Input state (for typing without triggering queries) const [fromDate, setFromDate] = useState(initialDates.from) const [toDate, setToDate] = useState(initialDates.to) const [selectedMonth, setSelectedMonth] = useState('') // Empty means custom range const [selectionMode, setSelectionMode] = useState<'last30' | 'week' | 'month' | 'custom'>('custom') // Submitted state (actually used for queries - only changes on Generate click) const [submittedFromDate, setSubmittedFromDate] = useState(initialDates.from) const [submittedToDate, setSubmittedToDate] = useState(initialDates.to) // Persist submitted dates to sessionStorage useEffect(() => { sessionStorage.setItem(STORAGE_KEY_FROM, submittedFromDate) sessionStorage.setItem(STORAGE_KEY_TO, submittedToDate) }, [submittedFromDate, submittedToDate]) const monthOptions = getMonthOptions() // Allowances checkbox state - default: all checked EXCEPT wastage const _defaultAllowances = { wastage: false, transfer: true, staffFood: true, manualAdjustment: true, disputes: true, cdDeductions: true, cdReallocations: true, } const [allowancesSelection, setAllowancesSelection] = useState(() => { try { const stored = localStorage.getItem('gpreport_allowances_v1') return stored ? { ..._defaultAllowances, ...JSON.parse(stored) } : _defaultAllowances } catch { return _defaultAllowances } }) // Track if dates have changed since last generation const hasUnsavedChanges = fromDate !== submittedFromDate || toDate !== submittedToDate // Generate report with current date selection const handleGenerate = () => { setSubmittedFromDate(fromDate) setSubmittedToDate(toDate) } // When month selection changes, update the date fields and submit useEffect(() => { if (selectedMonth) { const [year, month] = selectedMonth.split('-').map(Number) const bounds = getMonthBounds(year, month) setFromDate(bounds.start) setToDate(bounds.end) setSubmittedFromDate(bounds.start) setSubmittedToDate(bounds.end) } }, [selectedMonth]) // When date fields are manually changed, clear the month selection // Auto-adjust the other date if the range becomes invalid const handleFromDateChange = (value: string) => { setFromDate(value) // If from date is after to date, set to date to match from date if (value > toDate) { setToDate(value) } setSelectedMonth('') setSelectionMode('custom') } const handleToDateChange = (value: string) => { setToDate(value) // If to date is before from date, set from date to match to date if (value < fromDate) { setFromDate(value) } setSelectedMonth('') setSelectionMode('custom') } const handleMonthChange = (value: string) => { setSelectedMonth(value) if (value) { setSelectionMode('month') } else { setSelectionMode('custom') } } // Quick preset buttons - these also immediately submit the dates const setLast30Days = () => { const end = new Date() end.setDate(end.getDate() - 1) // Yesterday (today won't have confirmed sales) const start = new Date(end) start.setDate(end.getDate() - 30) const startStr = formatDate(start) const endStr = formatDate(end) setFromDate(startStr) setToDate(endStr) setSubmittedFromDate(startStr) setSubmittedToDate(endStr) setSelectedMonth('') setSelectionMode('last30') } const setThisMonth = () => { const now = new Date() const monthKey = `${now.getFullYear()}-${now.getMonth()}` const bounds = getMonthBounds(now.getFullYear(), now.getMonth()) setFromDate(bounds.start) setToDate(bounds.end) setSubmittedFromDate(bounds.start) setSubmittedToDate(bounds.end) setSelectedMonth(monthKey) setSelectionMode('month') } const setLastMonth = () => { const now = new Date() const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1) const monthKey = `${lastMonth.getFullYear()}-${lastMonth.getMonth()}` const bounds = getMonthBounds(lastMonth.getFullYear(), lastMonth.getMonth()) setFromDate(bounds.start) setToDate(bounds.end) setSubmittedFromDate(bounds.start) setSubmittedToDate(bounds.end) setSelectedMonth(monthKey) setSelectionMode('month') } const setThisWeek = () => { const today = new Date() const dayOfWeek = today.getDay() // Calculate Monday of current week (0 = Sunday, 1 = Monday, etc.) const monday = new Date(today) monday.setDate(today.getDate() - (dayOfWeek === 0 ? 6 : dayOfWeek - 1)) // End date is yesterday (today won't have confirmed sales) const end = new Date() end.setDate(end.getDate() - 1) const startStr = formatDate(monday) const endStr = formatDate(end) setFromDate(startStr) setToDate(endStr) setSubmittedFromDate(startStr) setSubmittedToDate(endStr) setSelectedMonth('') setSelectionMode('week') } const setLastWeek = () => { const today = new Date() const dayOfWeek = today.getDay() // Calculate Monday of last week const lastMonday = new Date(today) lastMonday.setDate(today.getDate() - (dayOfWeek === 0 ? 6 : dayOfWeek - 1) - 7) // Sunday of last week const lastSunday = new Date(lastMonday) lastSunday.setDate(lastMonday.getDate() + 6) const startStr = formatDate(lastMonday) const endStr = formatDate(lastSunday) setFromDate(startStr) setToDate(endStr) setSubmittedFromDate(startStr) setSubmittedToDate(endStr) setSelectedMonth('') setSelectionMode('week') } // Get the period prefix based on selection mode const getPeriodPrefix = (): string => { if (selectionMode === 'last30') { return 'Last 30 Days: ' } else if (selectionMode === 'week') { return 'Week: ' } else if (selectionMode === 'month' && selectedMonth) { const [year, month] = selectedMonth.split('-').map(Number) const monthName = new Date(year, month, 1).toLocaleDateString('en-GB', { month: 'long', year: 'numeric' }) return `Month of ${monthName}: ` } else { return 'Custom Dates: ' } } const { data, isLoading, error } = useQuery({ queryKey: ['gp-range', submittedFromDate, submittedToDate], queryFn: async () => { const res = await fetch(`/kitchen/api/reports/gp/range?from_date=${submittedFromDate}&to_date=${submittedToDate}`, { credentials: 'include', }) if (!res.ok) throw new Error('Failed to fetch GP data') return res.json() }, staleTime: 5 * 60 * 1000, // Keep data fresh for 5 minutes gcTime: 30 * 60 * 1000, // Keep in cache for 30 minutes }) // Fetch daily chart data const { data: chartData } = useQuery({ queryKey: ['gp-daily', submittedFromDate, submittedToDate], queryFn: async () => { const res = await fetch(`/kitchen/api/reports/gp/daily?from_date=${submittedFromDate}&to_date=${submittedToDate}`, { credentials: 'include', }) if (!res.ok) throw new Error('Failed to fetch chart data') return res.json() }, staleTime: 5 * 60 * 1000, gcTime: 30 * 60 * 1000, }) // Fetch top sellers data const { data: topSellers, isLoading: topSellersLoading } = useQuery({ queryKey: ['gp-top-sellers', submittedFromDate, submittedToDate], queryFn: async () => { const res = await fetch(`/kitchen/api/reports/gp/top-sellers?from_date=${submittedFromDate}&to_date=${submittedToDate}`, { credentials: 'include', }) if (!res.ok) { // Don't throw for top sellers - just return empty data return { from_date: submittedFromDate, to_date: submittedToDate, source: 'newbook' as const, categories: [], top_by_qty: [], top_by_revenue: [], package_favorites: [], total_charges_processed: 0, total_items_aggregated: 0 } } return res.json() }, staleTime: 5 * 60 * 1000, gcTime: 30 * 60 * 1000, }) const formatCurrency = (value: number, showCR = false) => { const num = Number(value) if (num < 0 && showCR) { return `-£${Math.abs(num).toFixed(2)} CR` } return `£${num.toFixed(2)}` } // Simple SVG line chart component const renderChart = () => { if (!chartData?.data?.length) return null const width = 500 const height = 200 const padding = { top: 20, right: 20, bottom: 30, left: 50 } const chartWidth = width - padding.left - padding.right const chartHeight = height - padding.top - padding.bottom const dataPoints = chartData.data const maxValue = Math.max( ...dataPoints.map(d => Math.max(d.net_sales, d.net_purchases)), 1 // Avoid division by zero ) // Scale functions const xScale = (index: number) => padding.left + (index / (dataPoints.length - 1 || 1)) * chartWidth const yScale = (value: number) => padding.top + chartHeight - (value / maxValue) * chartHeight // Create path for sales line const salesPath = dataPoints.map((d, i) => `${i === 0 ? 'M' : 'L'} ${xScale(i)} ${yScale(d.net_sales)}` ).join(' ') // Create path for purchases line const purchasesPath = dataPoints.map((d, i) => `${i === 0 ? 'M' : 'L'} ${xScale(i)} ${yScale(d.net_purchases)}` ).join(' ') // Y-axis labels const yLabels = [0, maxValue / 2, maxValue].map(v => ({ value: v, y: yScale(v), label: `£${(v / 1000).toFixed(v >= 1000 ? 0 : 1)}k` })) // X-axis labels (show first, middle, last dates) const xLabels = [ { index: 0, label: new Date(dataPoints[0].date).toLocaleDateString('en-GB', { day: 'numeric', month: 'short' }) }, { index: Math.floor(dataPoints.length / 2), label: new Date(dataPoints[Math.floor(dataPoints.length / 2)]?.date).toLocaleDateString('en-GB', { day: 'numeric', month: 'short' }) }, { index: dataPoints.length - 1, label: new Date(dataPoints[dataPoints.length - 1].date).toLocaleDateString('en-GB', { day: 'numeric', month: 'short' }) }, ] // Find week boundaries (Mondays) for vertical lines const weekLines: number[] = [] dataPoints.forEach((d, i) => { const date = new Date(d.date) if (date.getDay() === 1 && i > 0) { // Monday and not first point weekLines.push(i) } }) return (

Daily Sales & Purchases

{/* Horizontal grid lines */} {yLabels.map((l, i) => ( {l.label} ))} {/* Vertical week boundary lines */} {weekLines.map((index, i) => ( ))} {/* X-axis labels */} {xLabels.map((l, i) => ( {l.label} ))} {/* Sales line (green) */} {/* Purchases line (red) */}
Net Sales Net Purchases
) } if (isLoading) { return
Loading GP data...
} if (error) { return
Error loading GP data: {(error as Error).message}
} const { period_label = '', net_food_sales = 0, net_food_purchases = 0, gross_profit = 0, gross_profit_percent = 0, wastage_total = null, transfer_total = null, staff_food_total = null, manual_adjustment_total = null, disputes_total = null, cd_deductions_total = null, cd_reallocations_total = null, } = data || {} const isNegativeGP = gross_profit < 0 // Check which allowance types have data const hasWastage = wastage_total !== null && wastage_total > 0 const hasTransfer = transfer_total !== null && transfer_total > 0 const hasStaffFood = staff_food_total !== null && staff_food_total > 0 const hasManualAdjustment = manual_adjustment_total !== null && manual_adjustment_total > 0 const hasDisputes = disputes_total !== null && disputes_total > 0 const hasCdDeductions = cd_deductions_total !== null && cd_deductions_total !== 0 const hasCdReallocations = cd_reallocations_total !== null && cd_reallocations_total !== 0 // Check if any allowances/adjustments data exists const hasAnyAllowancesData = hasWastage || hasTransfer || hasStaffFood || hasManualAdjustment || hasDisputes || hasCdDeductions || hasCdReallocations // Calculate selected allowances total based on checkbox selection // Note: API returns decimals as strings, so we need Number() conversion const salesNum = Number(net_food_sales) || 0 const purchasesNum = Number(net_food_purchases) || 0 const cdDeductions = Number(cd_deductions_total) || 0 const cdReallocations = Number(cd_reallocations_total) || 0 const calculateSelectedAdjustments = () => { let total = 0 // CD adjustments reduce/increase purchases (deductions are negative = reduce cost) if (allowancesSelection.cdDeductions && hasCdDeductions) total += cdDeductions // negative value if (allowancesSelection.cdReallocations && hasCdReallocations) total += cdReallocations // positive value return total } const calculateSelectedAllowances = () => { let total = 0 if (allowancesSelection.wastage && hasWastage) total += Number(wastage_total) || 0 if (allowancesSelection.transfer && hasTransfer) total += Number(transfer_total) || 0 if (allowancesSelection.staffFood && hasStaffFood) total += Number(staff_food_total) || 0 if (allowancesSelection.manualAdjustment && hasManualAdjustment) total += Number(manual_adjustment_total) || 0 if (allowancesSelection.disputes && hasDisputes) total += Number(disputes_total) || 0 return total } const selectedCdTotal = calculateSelectedAdjustments() const selectedAllowancesTotal = calculateSelectedAllowances() const adjustedPurchases = purchasesNum + selectedCdTotal const hasSelectedAllowances = selectedAllowancesTotal > 0 || selectedCdTotal !== 0 // Calculate GP with selected allowances + CD adjustments const gpWithSelectedAllowances = salesNum > 0 ? Math.min((salesNum - adjustedPurchases + selectedAllowancesTotal) / salesNum * 100, 100) : 0 const downloadCSV = () => { if (!data) return const rows: string[][] = [ ['Kitchen Flash GP Report'], ['Period', period_label], [], ['Metric', 'Value'], ['Net Food Sales', net_food_sales.toString()], ['Net Food Purchases', net_food_purchases.toString()], ['Gross Profit', gross_profit.toString()], ['Gross Profit %', Number(gross_profit_percent).toFixed(2)], ] if (hasAnyAllowancesData) { rows.push([]) rows.push(['Adjustments', '']) if (hasWastage) rows.push(['Wastage', (wastage_total ?? 0).toString()]) if (hasTransfer) rows.push(['Transfers', (transfer_total ?? 0).toString()]) if (hasStaffFood) rows.push(['Staff Food', (staff_food_total ?? 0).toString()]) if (hasManualAdjustment) rows.push(['Manual Adjustments', (manual_adjustment_total ?? 0).toString()]) if (hasDisputes) rows.push(['Disputes', (disputes_total ?? 0).toString()]) rows.push(['GP with Adjustments %', gpWithSelectedAllowances.toFixed(2)]) } if (data.supplier_breakdown?.length) { rows.push([]) rows.push(['Supplier', 'Net Purchases', '% of Total']) data.supplier_breakdown.forEach(s => { rows.push([s.supplier_name, s.net_purchases.toString(), s.percentage.toFixed(1)]) }) } if (chartData?.data?.length) { rows.push([]) rows.push(['Date', 'Net Sales', 'Net Purchases', 'Covers']) chartData.data.forEach(d => { rows.push([d.date, d.net_sales.toString(), d.net_purchases.toString(), (d.total_covers ?? '').toString()]) }) } const csv = rows.map(r => r.map(c => `"${String(c).replace(/"/g, '""')}"`).join(',')).join('\n') const blob = new Blob([csv], { type: 'text/csv' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = `kitchen-flash-${submittedFromDate}-to-${submittedToDate}.csv` a.click() URL.revokeObjectURL(url) } // Toggle checkbox handler const toggleAllowance = (key: keyof typeof allowancesSelection) => { setAllowancesSelection(prev => { const next = { ...prev, [key]: !prev[key] } try { localStorage.setItem('gpreport_allowances_v1', JSON.stringify(next)) } catch {} return next }) } return (
{/* Header */}

Kitchen Flash Report

{/* Date Selection Controls */}
handleFromDateChange(e.target.value)} style={styles.dateInput} />
handleToDateChange(e.target.value)} style={styles.dateInput} />
{/* Period Label */}
{getPeriodPrefix()}{period_label}
{data && ( )}
{/* Main Content - GP Estimate and Chart side by side */}
{/* Gross Profit Estimate Section */}

Gross Profit Estimate

Net Food Sales {formatCurrency(net_food_sales)}
Net Food Purchases {formatCurrency(net_food_purchases)}
Gross Profit {formatCurrency(gross_profit)}
Gross Profit % {Number(gross_profit_percent).toFixed(1)}%
{hasAnyAllowancesData && ( <>
Adjustments
{hasCdDeductions && (
{formatCurrency(cd_deductions_total || 0)}
)} {hasCdReallocations && (
+{formatCurrency(cd_reallocations_total || 0)}
)} {hasWastage && (
{formatCurrency(wastage_total || 0)}
)} {hasTransfer && (
{formatCurrency(transfer_total || 0)}
)} {hasStaffFood && (
{formatCurrency(staff_food_total || 0)}
)} {hasManualAdjustment && (
{formatCurrency(manual_adjustment_total || 0)}
)} {hasDisputes && (
{formatCurrency(disputes_total || 0)}
)}
{selectedCdTotal !== 0 && (
Adjusted Purchases {formatCurrency(adjustedPurchases)}
)} {selectedAllowancesTotal > 0 && (
Selected Allowances {formatCurrency(selectedAllowancesTotal)}
)}
Adjusted GP % {gpWithSelectedAllowances.toFixed(1)}%
)}
{/* Chart Section */}
{renderChart()}
{/* Breakdown Tables Row */}
{/* Supplier Breakdown Table */}

Supplier Breakdown

{data?.supplier_breakdown?.length ? ( data.supplier_breakdown.map((supplier, index) => { const isCredit = supplier.net_purchases < 0 return ( ) }) ) : ( )}
Supplier Net Purchases %
{supplier.supplier_name} {isCredit && (CR)} {formatCurrency(supplier.net_purchases, true)} {Number(supplier.percentage).toFixed(1)}%
No supplier data
{/* GL Account Revenue Breakdown Table */}

Revenue Breakdown

{data?.gl_account_breakdown?.length ? ( data.gl_account_breakdown.map((account) => ( )) ) : ( )}
GL Account Net Revenue %
{account.gl_account_name} {formatCurrency(account.net_revenue)} {Number(account.percentage).toFixed(1)}%
No revenue data
{/* Top Sellers Section */} {topSellersLoading ? (
Loading top sellers...
) : topSellers?.source === 'sambapos' && topSellers?.categories?.length > 0 ? ( /* SambaPOS Category-based Top Sellers */ <> {/* Top Sellers by Quantity - One column per category */}

Top Sellers by Quantity

{topSellers.categories.map((cat) => (

{cat.category}

{cat.top_by_qty.length > 0 ? ( cat.top_by_qty.map((item) => ( )) ) : ( )}
Item Qty
{item.item_name} {item.qty}
No data
))}
{/* Top Sellers by Revenue - One column per category */}

Top Sellers by Gross Revenue

{topSellers.categories.map((cat) => (

{cat.category}

{cat.top_by_revenue.length > 0 ? ( cat.top_by_revenue.map((item) => ( )) ) : ( )}
Item Gross
{item.item_name} {formatCurrency(item.revenue)}
No data
))}
) : topSellers?.source === 'sambapos' && (!topSellers?.categories || topSellers.categories.length === 0) ? ( /* SambaPOS with no categories configured */

Top Sellers

No categories configured. Configure SambaPOS categories to see top sellers by category.

) : ( /* Legacy Newbook format - flat lists */
{/* Top Sellers by Quantity */}

Top Sellers by Quantity

{topSellers?.top_by_qty?.length ? ( topSellers.top_by_qty.map((item, index) => ( )) ) : ( )}
# Item Qty Revenue
{index + 1} {item.item_name} {item.qty} {formatCurrency(item.revenue)}
No top sellers data
{/* Top Sellers by Revenue */}

Top Sellers by Gross Revenue

{topSellers?.top_by_revenue?.length ? ( topSellers.top_by_revenue.map((item, index) => ( )) ) : ( )}
# Item Gross Qty
{index + 1} {item.item_name} {formatCurrency(item.revenue)} {item.qty}
No top sellers data
{/* Package Guest Favorites */}

Package Guest Favorites

{topSellers?.package_favorites?.length ? ( topSellers.package_favorites.map((item, index) => ( )) ) : ( )}
# Item Qty
{index + 1} {item.item_name} {item.qty}
No package data
)} {/* Daily Data Table */}

Daily Breakdown

{chartData?.data?.length ? ( [...chartData.data].reverse().map((day) => ( )) ) : ( )}
Date Net Sales Net Purchases Occupancy Lunch Covers Dinner Covers Total Covers
{new Date(day.date).toLocaleDateString('en-GB', { weekday: 'short', day: 'numeric', month: 'short' })} {formatCurrency(day.net_sales)} {formatCurrency(day.net_purchases)} {day.occupancy ?? '—'} {day.lunch_covers ?? '—'} {day.dinner_covers ?? '—'} {day.total_covers ?? '—'}
No daily data
) } const styles: Record = { loading: { padding: '2rem', textAlign: 'center', color: '#666', }, error: { padding: '2rem', textAlign: 'center', color: '#c00', background: '#fee', borderRadius: '8px', }, header: { marginBottom: '1.5rem', }, title: { color: '#1a1a2e', margin: 0, }, dateControls: { background: 'white', borderRadius: '12px', padding: '1.5rem', boxShadow: '0 2px 8px rgba(0,0,0,0.06)', marginBottom: '1rem', }, dateRow: { display: 'flex', gap: '1.5rem', flexWrap: 'wrap', marginBottom: '1rem', }, dateField: { display: 'flex', flexDirection: 'column', gap: '0.25rem', }, dateLabel: { fontSize: '0.85rem', color: '#666', fontWeight: 500, }, dateInput: { padding: '0.5rem 0.75rem', border: '1px solid #ddd', borderRadius: '6px', fontSize: '1rem', minWidth: '150px', }, monthSelect: { padding: '0.5rem 0.75rem', border: '1px solid #ddd', borderRadius: '6px', fontSize: '1rem', minWidth: '180px', background: 'white', }, presetRow: { display: 'flex', gap: '0.5rem', flexWrap: 'wrap', }, presetBtn: { padding: '0.4rem 0.75rem', background: '#f0f0f0', border: '1px solid #ddd', borderRadius: '6px', cursor: 'pointer', fontSize: '0.85rem', }, generateBtn: { padding: '0.4rem 1rem', background: '#e94560', color: 'white', border: 'none', borderRadius: '6px', cursor: 'pointer', fontSize: '0.85rem', fontWeight: 'bold', marginLeft: 'auto', }, generateBtnDisabled: { background: '#ccc', cursor: 'default', }, periodLabel: { fontSize: '1.1rem', fontWeight: 'bold', color: '#1a1a2e', textAlign: 'center', }, csvBtn: { padding: '0.4rem 0.9rem', fontSize: '0.8rem', background: 'transparent', border: '1px solid #1a1a2e', color: '#1a1a2e', borderRadius: '6px', cursor: 'pointer', whiteSpace: 'nowrap' as const, }, mainContent: { display: 'flex', gap: '1.5rem', flexWrap: 'wrap', alignItems: 'stretch', }, sectionContainer: { background: 'white', borderRadius: '12px', padding: '1.5rem', boxShadow: '0 2px 8px rgba(0,0,0,0.06)', marginBottom: '2rem', flex: '1 1 auto', minWidth: '300px', }, sectionTitle: { margin: '0 0 1.5rem 0', color: '#1a1a2e', }, calculationContainer: { maxWidth: '400px', }, calcRow: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '0.75rem 0', }, calcLabel: { color: '#555', fontSize: '1rem', }, calcValue: { fontSize: '1rem', fontFamily: 'monospace', }, calcLabelBold: { color: '#1a1a2e', fontSize: '1.1rem', fontWeight: 'bold', }, calcValueBold: { fontSize: '1.2rem', fontWeight: 'bold', fontFamily: 'monospace', }, divider: { borderTop: '2px solid #dee2e6', margin: '0.5rem 0', }, wastageSection: { borderTop: '1px dashed #e67e22', margin: '0.75rem 0 0.5rem 0', }, allowancesHeader: { fontSize: '0.9rem', fontWeight: 600, color: '#666', marginBottom: '0.5rem', textTransform: 'uppercase' as const, letterSpacing: '0.5px', }, allowanceRow: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '0.4rem 0', }, allowanceCheckbox: { display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer', fontSize: '0.95rem', color: '#444', }, positiveValue: { color: '#155724', }, negativeValue: { color: '#dc3545', }, chartContainer: { width: '100%', }, chartTitle: { margin: '0 0 1rem 0', color: '#1a1a2e', fontSize: '1rem', fontWeight: 600, }, chartSvg: { display: 'block', maxWidth: '100%', }, chartLegend: { display: 'flex', gap: '1.5rem', marginTop: '0.75rem', justifyContent: 'center', }, legendItem: { display: 'flex', alignItems: 'center', gap: '0.4rem', fontSize: '0.85rem', color: '#555', }, legendColor: { width: '12px', height: '12px', borderRadius: '2px', display: 'inline-block', }, breakdownRow: { display: 'flex', gap: '1.5rem', flexWrap: 'wrap', marginBottom: '1.5rem', }, breakdownContainer: { background: 'white', borderRadius: '12px', padding: '1.5rem', boxShadow: '0 2px 8px rgba(0,0,0,0.06)', flex: '1 1 300px', minWidth: '280px', }, breakdownTable: { width: '100%', borderCollapse: 'collapse', }, tableHeader: { textAlign: 'left', padding: '0.75rem 0.5rem', borderBottom: '2px solid #dee2e6', color: '#1a1a2e', fontWeight: 600, fontSize: '0.85rem', }, tableCell: { padding: '0.5rem', borderBottom: '1px solid #f0f0f0', fontSize: '0.9rem', }, dailyTableContainer: { background: 'white', borderRadius: '12px', padding: '1.5rem', boxShadow: '0 2px 8px rgba(0,0,0,0.06)', marginBottom: '2rem', }, tableWrapper: { overflowX: 'auto', }, dailyTable: { width: '100%', borderCollapse: 'collapse', minWidth: '700px', }, // SambaPOS category-based top sellers styles topSellersSection: { background: 'white', borderRadius: '12px', padding: '1.5rem', boxShadow: '0 2px 8px rgba(0,0,0,0.06)', marginBottom: '1.5rem', }, categoryGrid: { display: 'flex', gap: '1rem', flexWrap: 'wrap', }, categoryColumn: { flex: '1 1 180px', minWidth: '160px', maxWidth: '250px', }, categoryHeader: { margin: '0 0 0.75rem 0', padding: '0.5rem 0.75rem', background: '#f8f9fa', borderRadius: '6px', fontSize: '0.95rem', fontWeight: 600, color: '#1a1a2e', borderLeft: '3px solid #e94560', }, }