diff --git a/components/WeeklyGrid.tsx b/components/WeeklyGrid.tsx index 5563730..6c004da 100644 --- a/components/WeeklyGrid.tsx +++ b/components/WeeklyGrid.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState } from 'react'; +import React, { useMemo, useState, useEffect } from 'react'; import { CombinedKPIs } from '../types'; import { pivotWeeklySalesData, WeeklyPivotRow } from '../services/dataProcessor'; @@ -11,9 +11,14 @@ type SortConfig = { direction: 'asc' | 'desc'; } | 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); + // Default sort: most recent week, descending const [sortConfig, setSortConfig] = useState(() => { if (weeks.length > 0) { @@ -22,6 +27,11 @@ const WeeklyGrid: React.FC = ({ data }) => { return null; }); + // Reset pagination when search changes + useEffect(() => { + setCurrentPage(1); + }, [searchTerm]); + const handleSort = (weekKey: string) => { setSortConfig(prev => { if (prev?.key === weekKey) { @@ -31,9 +41,20 @@ const WeeklyGrid: React.FC = ({ data }) => { }); }; - // Sort rows globally (Ungrouped) + // 1. Filter by search term + const filteredRows = useMemo(() => { + if (!searchTerm) return rows; + const lowSearch = searchTerm.toLowerCase(); + return rows.filter(r => + r.sku.toLowerCase().includes(lowSearch) || + r.title.toLowerCase().includes(lowSearch) || + r.line.toLowerCase().includes(lowSearch) + ); + }, [rows, searchTerm]); + + // 2. Sort results const sortedRows = useMemo(() => { - const result = [...rows]; + const result = [...filteredRows]; if (sortConfig) { result.sort((a, b) => { const valA = a.unitsByWeek[sortConfig.key] || 0; @@ -45,20 +66,28 @@ const WeeklyGrid: React.FC = ({ data }) => { }); } return result; - }, [rows, sortConfig]); + }, [filteredRows, sortConfig]); - // Calculate totals per week + // 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 (always based on full filtered dataset, not just page) const weekTotals = useMemo(() => { const totals: { [weekKey: string]: { units: number, spend: number } } = {}; weeks.forEach(week => { - totals[week] = rows.reduce((acc, row) => { + 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; - }, [rows, weeks]); + }, [filteredRows, weeks]); const renderGrowth = (current: number, previous: number) => { if (!previous || previous === 0) return null; @@ -73,81 +102,132 @@ const WeeklyGrid: React.FC = ({ data }) => { }; return ( -
-
- - - - - {weeks.map(week => ( - - ))} - - - - {weeks.map((week, idx) => ( - + ); + })} + + )) + ) : ( + + + + )} + +
Product Details handleSort(week)} - > -
- {week.split('-')[1]}/{week.split('-')[0].slice(-2)} - {sortConfig?.key === week && ( - {sortConfig.direction === 'asc' ? '↑' : '↓'} - )} -
Units / Spend
-
-
TOTALS -
-
- {weekTotals[week].units.toLocaleString('de-DE')} - {renderGrowth(weekTotals[week].units, weekTotals[weeks[idx + 1]]?.units)} +
+ {/* Toolbar: Search & Pagination */} +
+
+ 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" + /> + + + +
+ +
+ + Showing {Math.min(sortedRows.length, (currentPage - 1) * ROWS_PER_PAGE + 1)}-{Math.min(sortedRows.length, currentPage * ROWS_PER_PAGE)} of {sortedRows.length} + +
+ + + {currentPage} / {Math.max(1, totalPages)} + + +
+
+
+ +
+
+ + + + + {weeks.map(week => ( + + ))} + + + + {weeks.map((week, idx) => ( + - ))} - - - - {sortedRows.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 ( - + + + {paginatedRows.length > 0 ? ( + paginatedRows.map((row) => ( + + - ); - })} - - ))} - -
Product Details handleSort(week)} + > +
+ {week.split('-')[1]}/{week.split('-')[0].slice(-2)} + {sortConfig?.key === week && ( + {sortConfig.direction === 'asc' ? '↑' : '↓'} + )} +
Units / Spend
-
- €{weekTotals[week].spend.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} +
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.title} - {row.line} -
-
-
-
- 0 ? (sortConfig?.key === week ? '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 })} - - )} + + ))} +
+
+ {row.sku} + {row.title} + {row.line}
+ {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 ( +
+
+
+ 0 ? (sortConfig?.key === week ? '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}" +
+
); diff --git a/services/dataProcessor.ts b/services/dataProcessor.ts index fb64633..39b0363 100644 --- a/services/dataProcessor.ts +++ b/services/dataProcessor.ts @@ -152,7 +152,8 @@ const getColumnValue = (row: any, aliases: string[]): string => { return ''; }; // Allowed Customers Whitelist -const ALLOWED_CUSTOMERS = ['Amazon DE', 'Amazon FR', 'Amazon ES', 'Amazon IT', 'Amazon UK', 'Amazon SC']; +export const PAN_EU_COUNTRIES = ['Amazon DE', 'Amazon IT', 'Amazon FR', 'Amazon ES']; +const ALLOWED_CUSTOMERS = [...PAN_EU_COUNTRIES, 'Amazon UK', 'Amazon SC']; const isAllowedCustomer = (customer: string): boolean => { if (!customer) return false; @@ -550,8 +551,9 @@ export const mergeSalesAndAdsData = (salesData: SalesRecord[], adsData: AdsRecor export const filterAdsData = (adsData: AdsRecord[], filters: FilterState): AdsRecord[] => { return adsData.filter(ad => { // Country/Customer match (ads use 'country', sales use 'customer') - const countryMatch = filters.customer.length === 0 || - filters.customer.some(c => c.toUpperCase() === ad.country.toUpperCase()); + const countryMatch = filters.customer.length === 0 + ? PAN_EU_COUNTRIES.some(c => c.toUpperCase() === ad.country.toUpperCase()) + : filters.customer.some(c => c.toUpperCase() === ad.country.toUpperCase()); // Year match const yearMatch = filters.year.length === 0 || @@ -576,7 +578,9 @@ export const filterData = (data: SalesRecord[], filters: FilterState): SalesReco const pureMonth = recordMonth.split('-')[0]; // "Apr" // 2. Filter Checks - const customerMatch = filters.customer.length === 0 || filters.customer.includes(item.customer); + const customerMatch = filters.customer.length === 0 + ? PAN_EU_COUNTRIES.includes(item.customer) + : filters.customer.includes(item.customer); const yearMatch = filters.year.length === 0 || filters.year.includes(item.year.toString()); // Check match against pure month ("Apr") OR full month ("Apr-23") just in case filters evolve @@ -998,9 +1002,6 @@ export const applyPanEUGrouping = ( return data; } - // Define Pan-EU countries - const PAN_EU_COUNTRIES = ['Amazon DE', 'Amazon IT', 'Amazon FR', 'Amazon ES']; - // Replace Pan-EU country names with "Pan-EU" for grouping return data.map(record => { if (PAN_EU_COUNTRIES.includes(record.customer)) {