import React, { useMemo, useState, useEffect } from 'react'; import * as XLSX from 'xlsx'; import { CombinedKPIs } from '../types'; import { pivotWeeklySalesData, WeeklyPivotRow } from '../services/dataProcessor'; interface WeeklyGridProps { data: CombinedKPIs[]; } type SortConfig = { key: string; // weekKey direction: 'asc' | 'desc'; metric: 'units' | 'spend'; } | null; const ROWS_PER_PAGE = 50; const WeeklyGrid: React.FC = ({ data }) => { const { rows, weeks } = useMemo(() => pivotWeeklySalesData(data), [data]); const [searchTerm, setSearchTerm] = useState(''); const [currentPage, setCurrentPage] = useState(1); const [growthFilterMode, setGrowthFilterMode] = useState<'all' | 'up' | 'down' | 'stable'>('all'); const [growthThreshold, setGrowthThreshold] = useState(10); // Default sort: most recent week, descending, units const [sortConfig, setSortConfig] = useState(() => { if (weeks.length > 0) { return { key: weeks[0], direction: 'desc', metric: 'units' }; } return null; }); // Reset pagination when search changes useEffect(() => { setCurrentPage(1); }, [searchTerm]); const handleSort = (weekKey: string, metric: 'units' | 'spend') => { setSortConfig(prev => { if (prev?.key === weekKey && prev.metric === metric) { // Toggle direction if same week & same metric return { key: weekKey, direction: prev.direction === 'asc' ? 'desc' : 'asc', metric }; } // Switch to new week/metric with default DESC return { key: weekKey, direction: 'desc', metric }; }); }; // 1. Filter by search term const filteredRows = useMemo(() => { let result = rows; // 1. Search Filter if (searchTerm) { const lowSearch = searchTerm.toLowerCase(); result = result.filter(r => r.sku.toLowerCase().includes(lowSearch) || r.asin.toLowerCase().includes(lowSearch) || r.title.toLowerCase().includes(lowSearch) || r.line.toLowerCase().includes(lowSearch) ); } // 2. Growth Filter (if active) if (growthFilterMode !== 'all' && sortConfig) { const currentWeek = sortConfig.key; const currentWeekIdx = weeks.indexOf(currentWeek); const prevWeek = weeks[currentWeekIdx + 1]; if (prevWeek) { result = result.filter(r => { const currentUnits = r.unitsByWeek[currentWeek] || 0; const prevUnits = r.unitsByWeek[prevWeek] || 0; // Handle edge case: units were 0 in previous week if (prevUnits === 0) { if (currentUnits === 0) return growthFilterMode === 'stable'; return growthFilterMode === 'up'; // Gained from 0 } // Calculate percentage growth const growth = ((currentUnits - prevUnits) / prevUnits) * 100; // For 'down', we use the absolute threshold to check if it dropped BY at least that amount if (growthFilterMode === 'up') return growth >= growthThreshold; if (growthFilterMode === 'down') return growth <= -Math.abs(growthThreshold); if (growthFilterMode === 'stable') return Math.abs(growth) < growthThreshold; return true; }); } } return result; }, [rows, searchTerm, growthFilterMode, growthThreshold, sortConfig, weeks]); // 2. Sort results const sortedRows = useMemo(() => { const result = [...filteredRows]; if (sortConfig) { result.sort((a, b) => { const metricKey = sortConfig.metric === 'units' ? 'unitsByWeek' : 'spendByWeek'; const valA = a[metricKey][sortConfig.key] || 0; const valB = b[metricKey][sortConfig.key] || 0; if (sortConfig.direction === 'asc') { return valA - valB; } return valB - valA; }); } return result; }, [filteredRows, sortConfig]); // 3. Paginate const paginatedRows = useMemo(() => { const start = (currentPage - 1) * ROWS_PER_PAGE; return sortedRows.slice(start, start + ROWS_PER_PAGE); }, [sortedRows, currentPage]); const totalPages = Math.ceil(sortedRows.length / ROWS_PER_PAGE); // Calculate totals per week const weekTotals = useMemo(() => { const totals: { [weekKey: string]: { units: number, spend: number } } = {}; weeks.forEach(week => { totals[week] = filteredRows.reduce((acc, row) => { acc.units += (row.unitsByWeek[week] || 0); acc.spend += (row.spendByWeek[week] || 0); return acc; }, { units: 0, spend: 0 }); }); return totals; }, [filteredRows, weeks]); const renderGrowth = (current: number, previous: number) => { if (!previous || previous === 0) return null; const pct = ((current - previous) / previous) * 100; const isPositive = pct >= 0; return ( {isPositive ? '▲' : '▼'}{Math.abs(pct).toFixed(0)}% ); }; // Export to Excel const handleExportExcel = () => { // Prepare data for export - use sortedRows to match on-screen order const exportData = sortedRows.map(row => { const rowData: { [key: string]: string | number } = { SKU: row.sku, ASIN: row.asin, Title: row.title, Line: row.line, }; weeks.forEach(week => { rowData[`${week} Units`] = row.unitsByWeek[week] || 0; rowData[`${week} Spend`] = row.spendByWeek[week] || 0; }); return rowData; }); const ws = XLSX.utils.json_to_sheet(exportData); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, 'Weekly Sales'); XLSX.writeFile(wb, `Weekly_Sales_Export_${new Date().toISOString().slice(0, 10)}.xlsx`); }; return (
{/* Toolbar: Search & Pagination */}
{/* Search Input */}
setSearchTerm(e.target.value)} className="w-full bg-slate-950 border border-white/10 rounded-lg px-4 py-2 text-sm text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-all pl-10" />
{/* Growth Filter */}
Growth Filter {growthFilterMode !== 'all' && (
Thresh: setGrowthThreshold(Number(e.target.value))} className="w-12 bg-slate-900 border border-white/10 rounded px-1.5 py-1 text-xs text-white text-center focus:outline-none focus:ring-1 focus:ring-indigo-500" /> %
)}
{/* Export Button */}
Showing {Math.min(filteredRows.length, (currentPage - 1) * ROWS_PER_PAGE + 1)}-{Math.min(filteredRows.length, currentPage * ROWS_PER_PAGE)} of {filteredRows.length}
{currentPage} / {Math.max(1, totalPages)}
{weeks.map(week => ( ))} {weeks.map((week, idx) => ( ))} {paginatedRows.length > 0 ? ( paginatedRows.map((row) => ( {weeks.map((week, idx) => { const val = row.unitsByWeek[week] || 0; const prevVal = row.unitsByWeek[weeks[idx + 1]] || 0; const spend = row.spendByWeek[week] || 0; return ( ); })} )) ) : ( )}
Product Details
{/* Week Label */}
{week.split('-')[1]}/{week.split('-')[0].slice(-2)}
{/* Units Sort Trigger */}
handleSort(week, 'units')} className={`flex-1 p-1.5 cursor-pointer hover:bg-indigo-500/10 transition-colors flex items-center justify-center gap-1 border-b border-white/5 ${sortConfig?.key === week && sortConfig.metric === 'units' ? 'bg-indigo-500/5 text-indigo-400' : 'text-slate-500 hover:text-slate-300'}`} > Units {sortConfig?.key === week && sortConfig.metric === 'units' && ( {sortConfig.direction === 'asc' ? '↑' : '↓'} )}
{/* Spend Sort Trigger */}
handleSort(week, 'spend')} className={`flex-1 p-1.5 cursor-pointer hover:bg-amber-500/10 transition-colors flex items-center justify-center gap-1 ${sortConfig?.key === week && sortConfig.metric === 'spend' ? 'bg-amber-500/5 text-amber-400' : 'text-slate-500 hover:text-slate-300'}`} > Spend {sortConfig?.key === week && sortConfig.metric === 'spend' && ( {sortConfig.direction === 'asc' ? '↑' : '↓'} )}
TOTALS
{weekTotals[week].units.toLocaleString('de-DE')} {renderGrowth(weekTotals[week].units, weekTotals[weeks[idx + 1]]?.units)}
€{weekTotals[week].spend.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}
{row.sku} {row.asin}
{row.title} {row.line}
0 ? (sortConfig?.key === week && sortConfig.metric === 'units' ? 'text-indigo-400' : 'text-white') : 'text-slate-700'}`}> {val > 0 ? val.toLocaleString('de-DE') : '-'} {val > 0 && renderGrowth(val, prevVal)}
{spend > 0 && ( €{spend.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} )}
No SKUs found matching "{searchTerm}"
); }; export default WeeklyGrid;