From 6a368ffd1fd7cd974188d45cbb6e859a434cefb3 Mon Sep 17 00:00:00 2001 From: Christian Vidal Wolf Date: Fri, 16 Jan 2026 11:07:35 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20exclude=202023=20data=20&=20improve=20w?= =?UTF-8?q?eekly=20sales=20tooltip=20(add=20Sell=20Out=20=E2=82=AC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/DataGrid.tsx | 1070 ++++++++++++++++++------------------- services/dataProcessor.ts | 4 +- 2 files changed, 537 insertions(+), 537 deletions(-) diff --git a/components/DataGrid.tsx b/components/DataGrid.tsx index 67b6364..803c15c 100644 --- a/components/DataGrid.tsx +++ b/components/DataGrid.tsx @@ -1,18 +1,18 @@ import React, { useState, useMemo, useEffect } from 'react'; import { - LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer + LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; import { SalesRecord, PivotRow } from '../types'; import { pivotSalesData, generateCSV, aggregateForTimeSeries, aggregateForComparisonTimeSeries } from '../services/dataProcessor'; import { DownloadIcon, FunnelIcon, CloseIcon, ChartIcon } from './Icons'; interface DataGridProps { - data: SalesRecord[]; + data: SalesRecord[]; } type SortConfig = { - key: string | null; - direction: 'asc' | 'desc'; + key: string | null; + direction: 'asc' | 'desc'; }; type ConditionalFilter = { @@ -27,11 +27,11 @@ const CHART_COLORS = ['#6366f1', '#ec4899', '#10b981', '#f59e0b', '#8b5cf6', '#0 // Available grouping dimensions const DIMENSION_OPTIONS = [ - { label: 'Product Line', value: 'line' }, - { label: 'Customer', value: 'customer' }, - { label: 'SKU', value: 'sku' }, - { label: 'Title', value: 'title' }, - { label: 'ASIN', value: 'asin' }, + { label: 'Product Line', value: 'line' }, + { label: 'Customer', value: 'customer' }, + { label: 'SKU', value: 'sku' }, + { label: 'Title', value: 'title' }, + { label: 'ASIN', value: 'asin' }, ]; // Tooltip for single-period view with Week-over-Week comparison @@ -41,7 +41,7 @@ const WoWTooltip = ({ active, payload, label, data }: any) => { const prevData = currentIndex > 0 ? data[currentIndex - 1] : null; return ( -
+

{label}

{payload.map((p: any) => { let wowEl = null; @@ -60,15 +60,15 @@ const WoWTooltip = ({ active, payload, label, data }: any) => { return (
- {p.name}: -
+ {p.name}: +
{p.dataKey === 'sellOut' - ? `€${Number(p.value).toLocaleString(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 0})}` - : `${Number(p.value).toLocaleString()} u`} + ? `€${Number(p.value).toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 0 })}` + : `${Number(p.value).toLocaleString()} u`} {wowEl} -
+
); })} @@ -81,7 +81,7 @@ const WoWTooltip = ({ active, payload, label, data }: any) => { // Tooltip for multi-year comparison view const ComparisonTooltip = ({ active, payload, label }: any) => { if (active && payload && payload.length) { - + interface YearData { sellOut?: number; units?: number; @@ -93,7 +93,7 @@ const ComparisonTooltip = ({ active, payload, label }: any) => { payload.forEach((p: any) => { const nameParts = p.name.split(' '); if (nameParts.length < 2) return; - + const year = nameParts[nameParts.length - 1]; const metric = nameParts.slice(0, nameParts.length - 1).join(' '); @@ -101,14 +101,14 @@ const ComparisonTooltip = ({ active, payload, label }: any) => { dataByYear[year] = {}; } // Use the color from the Sell Out line for consistency for that year block - if (metric.toLowerCase().includes('so')) { + if (metric.toLowerCase().includes('sell out')) { dataByYear[year].sellOut = p.value; dataByYear[year].color = p.stroke || p.color; } else if (metric.toLowerCase().includes('units')) { dataByYear[year].units = p.value; - if(!dataByYear[year].color) { // fallback color from units line + if (!dataByYear[year].color) { // fallback color from units line dataByYear[year].color = p.stroke || p.color; - } + } } }); @@ -141,17 +141,17 @@ const ComparisonTooltip = ({ active, payload, label }: any) => { ); } - + return (

{year}

- + {yearData.sellOut != null && (
Sell Out:
- €{Number(yearData.sellOut).toLocaleString(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 0})} + €{Number(yearData.sellOut).toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 0 })} {sellOutGrowthEl}
@@ -180,566 +180,566 @@ const ComparisonTooltip = ({ active, payload, label }: any) => { // Reusable Expandable Chart Card const ExpandableChartCard: React.FC<{ title: string; children: React.ReactNode; className?: string }> = ({ title, children, className }) => { - const [isExpanded, setIsExpanded] = useState(false); + const [isExpanded, setIsExpanded] = useState(false); + + const toggleExpand = () => setIsExpanded(!isExpanded); + + if (isExpanded) { + return ( +
+
+

{title}

+ +
+
+ {children} +
+
+ ); + } - const toggleExpand = () => setIsExpanded(!isExpanded); - - if (isExpanded) { return ( -
-
-

{title}

- +
+
+

{title}

+ +
+
+ {children} +
-
- {children} -
-
); - } - - return ( -
-
-

{title}

- -
-
- {children} -
-
- ); }; const DataGrid: React.FC = ({ data }) => { - const [currentPage, setCurrentPage] = useState(1); - const [sortConfig, setSortConfig] = useState({ key: null, direction: 'desc' }); - const [showChart, setShowChart] = useState(true); - const [visibleMetrics, setVisibleMetrics] = useState<('sellOut' | 'units')[]>(['sellOut', 'units']); - - // State for dynamic grouping - const [selectedDimensions, setSelectedDimensions] = useState(['line', 'customer', 'sku', 'title']); + const [currentPage, setCurrentPage] = useState(1); + const [sortConfig, setSortConfig] = useState({ key: null, direction: 'desc' }); + const [showChart, setShowChart] = useState(true); + const [visibleMetrics, setVisibleMetrics] = useState<('sellOut' | 'units')[]>(['sellOut', 'units']); - // State for Advanced Filtering - const [showFilterBuilder, setShowFilterBuilder] = useState(false); - const [rowFilters, setRowFilters] = useState([]); - // Temp state for new filter inputs - const [newFilterMetric, setNewFilterMetric] = useState(''); - const [newFilterOperator, setNewFilterOperator] = useState<'gt' | 'lt'>('gt'); - const [newFilterValue, setNewFilterValue] = useState(''); + // State for dynamic grouping + const [selectedDimensions, setSelectedDimensions] = useState(['line', 'customer', 'sku', 'title']); - // Effective dimensions for rendering - const effectiveDimensions = useMemo(() => - selectedDimensions.length > 0 ? selectedDimensions : ['customer'], - [selectedDimensions]); + // State for Advanced Filtering + const [showFilterBuilder, setShowFilterBuilder] = useState(false); + const [rowFilters, setRowFilters] = useState([]); + // Temp state for new filter inputs + const [newFilterMetric, setNewFilterMetric] = useState(''); + const [newFilterOperator, setNewFilterOperator] = useState<'gt' | 'lt'>('gt'); + const [newFilterValue, setNewFilterValue] = useState(''); - // Transform flat data into Pivot structure - const { rows: pivotRows, years } = useMemo(() => { - return pivotSalesData(data, effectiveDimensions); - }, [data, effectiveDimensions]); - - // Data for the time series chart, supporting single and multi-year comparison - const { chartData, uniqueYears, isComparisonView, chartTitle } = useMemo(() => { - const yearsInView = Array.from(new Set(data.map(d => d.year.toString()))).sort((a: string, b: string) => parseInt(b) - parseInt(a)); - const isMultiYear = yearsInView.length > 1; + // Effective dimensions for rendering + const effectiveDimensions = useMemo(() => + selectedDimensions.length > 0 ? selectedDimensions : ['customer'], + [selectedDimensions]); - if (isMultiYear) { - return { - chartData: aggregateForComparisonTimeSeries(data), - uniqueYears: yearsInView, - isComparisonView: true, - chartTitle: `Weekly Sales Comparison: ${yearsInView.join(' vs ')}` - }; - } else { - return { - chartData: aggregateForTimeSeries(data), - uniqueYears: yearsInView, - isComparisonView: false, - chartTitle: `Weekly Sales Evolution ${yearsInView[0] || ''}` - }; - } - }, [data]); + // Transform flat data into Pivot structure + const { rows: pivotRows, years } = useMemo(() => { + return pivotSalesData(data, effectiveDimensions); + }, [data, effectiveDimensions]); - // Filter Options based on available data - const metricOptions = useMemo(() => { - const options = []; - // Totals - years.forEach(y => { - options.push({ label: `Total Sell Out ${y} (€)`, value: `total_sellOut_${y}` }); - options.push({ label: `Total Units ${y}`, value: `total_units_${y}` }); - }); - // Growth (Latest vs Previous) - if (years.length >= 2) { - options.push({ label: `Growth % Sell Out (${years[0]} vs ${years[1]})`, value: 'growth_sellOut' }); - options.push({ label: `Growth % Units (${years[0]} vs ${years[1]})`, value: 'growth_units' }); - } - return options; - }, [years]); + // Data for the time series chart, supporting single and multi-year comparison + const { chartData, uniqueYears, isComparisonView, chartTitle } = useMemo(() => { + const yearsInView = Array.from(new Set(data.map(d => d.year.toString()))).sort((a: string, b: string) => parseInt(b) - parseInt(a)); + const isMultiYear = yearsInView.length > 1; - // Apply Advanced Row Filters THEN Sort - const processedRows = useMemo(() => { - let result = pivotRows; + if (isMultiYear) { + return { + chartData: aggregateForComparisonTimeSeries(data), + uniqueYears: yearsInView, + isComparisonView: true, + chartTitle: `Weekly Sales Comparison: ${yearsInView.join(' vs ')}` + }; + } else { + return { + chartData: aggregateForTimeSeries(data), + uniqueYears: yearsInView, + isComparisonView: false, + chartTitle: `Weekly Sales Evolution ${yearsInView[0] || ''}` + }; + } + }, [data]); - // 1. Filter - if (rowFilters.length > 0) { - const latestYear = years[0]; - const prevYear = years[1]; + // Filter Options based on available data + const metricOptions = useMemo(() => { + const options = []; + // Totals + years.forEach(y => { + options.push({ label: `Total Sell Out ${y} (€)`, value: `total_sellOut_${y}` }); + options.push({ label: `Total Units ${y}`, value: `total_units_${y}` }); + }); + // Growth (Latest vs Previous) + if (years.length >= 2) { + options.push({ label: `Growth % Sell Out (${years[0]} vs ${years[1]})`, value: 'growth_sellOut' }); + options.push({ label: `Growth % Units (${years[0]} vs ${years[1]})`, value: 'growth_units' }); + } + return options; + }, [years]); - result = result.filter(row => { - return rowFilters.every(filter => { - let rowValue = 0; + // Apply Advanced Row Filters THEN Sort + const processedRows = useMemo(() => { + let result = pivotRows; - if (filter.metric.startsWith('total_sellOut_')) { - const y = filter.metric.split('_')[2]; - rowValue = row.totalsByYear[y]?.sellOut || 0; - } - else if (filter.metric.startsWith('total_units_')) { - const y = filter.metric.split('_')[2]; - rowValue = row.totalsByYear[y]?.units || 0; - } - else if (filter.metric === 'growth_sellOut') { - if (!prevYear) return true; - const curr = row.totalsByYear[latestYear]?.sellOut || 0; - const prev = row.totalsByYear[prevYear]?.sellOut || 0; - if (prev === 0) return curr > 0; - rowValue = ((curr - prev) / prev) * 100; - } - else if (filter.metric === 'growth_units') { - if (!prevYear) return true; - const curr = row.totalsByYear[latestYear]?.units || 0; - const prev = row.totalsByYear[prevYear]?.units || 0; - if (prev === 0) return curr > 0; - rowValue = ((curr - prev) / prev) * 100; - } + // 1. Filter + if (rowFilters.length > 0) { + const latestYear = years[0]; + const prevYear = years[1]; - if (filter.operator === 'gt') return rowValue > filter.value; - if (filter.operator === 'lt') return rowValue < filter.value; - return true; + result = result.filter(row => { + return rowFilters.every(filter => { + let rowValue = 0; + + if (filter.metric.startsWith('total_sellOut_')) { + const y = filter.metric.split('_')[2]; + rowValue = row.totalsByYear[y]?.sellOut || 0; + } + else if (filter.metric.startsWith('total_units_')) { + const y = filter.metric.split('_')[2]; + rowValue = row.totalsByYear[y]?.units || 0; + } + else if (filter.metric === 'growth_sellOut') { + if (!prevYear) return true; + const curr = row.totalsByYear[latestYear]?.sellOut || 0; + const prev = row.totalsByYear[prevYear]?.sellOut || 0; + if (prev === 0) return curr > 0; + rowValue = ((curr - prev) / prev) * 100; + } + else if (filter.metric === 'growth_units') { + if (!prevYear) return true; + const curr = row.totalsByYear[latestYear]?.units || 0; + const prev = row.totalsByYear[prevYear]?.units || 0; + if (prev === 0) return curr > 0; + rowValue = ((curr - prev) / prev) * 100; + } + + if (filter.operator === 'gt') return rowValue > filter.value; + if (filter.operator === 'lt') return rowValue < filter.value; + return true; + }); }); - }); - } + } - // 2. Sort - if (sortConfig.key) { - result.sort((a, b) => { - let valA: number | string = ''; - let valB: number | string = ''; + // 2. Sort + if (sortConfig.key) { + result.sort((a, b) => { + let valA: number | string = ''; + let valB: number | string = ''; - // Handle sorting by dimensions - if (['customer', 'line', 'sku', 'title', 'articleName', 'asin'].includes(sortConfig.key as string)) { - valA = a[sortConfig.key as keyof PivotRow] as string || ''; - valB = b[sortConfig.key as keyof PivotRow] as string || ''; - } - // Handle sorting by Total Metrics (total_sellOut_2023) - else if ((sortConfig.key as string).startsWith('total_')) { - const parts = (sortConfig.key as string).split('_'); - // parts[1] = metric (sellOut/units), parts[2] = year - if (parts.length === 3) { - const y = parts[2]; - const m = parts[1] as 'sellOut' | 'units'; - valA = a.totalsByYear[y]?.[m] || 0; - valB = b.totalsByYear[y]?.[m] || 0; - } - } + // Handle sorting by dimensions + if (['customer', 'line', 'sku', 'title', 'articleName', 'asin'].includes(sortConfig.key as string)) { + valA = a[sortConfig.key as keyof PivotRow] as string || ''; + valB = b[sortConfig.key as keyof PivotRow] as string || ''; + } + // Handle sorting by Total Metrics (total_sellOut_2023) + else if ((sortConfig.key as string).startsWith('total_')) { + const parts = (sortConfig.key as string).split('_'); + // parts[1] = metric (sellOut/units), parts[2] = year + if (parts.length === 3) { + const y = parts[2]; + const m = parts[1] as 'sellOut' | 'units'; + valA = a.totalsByYear[y]?.[m] || 0; + valB = b.totalsByYear[y]?.[m] || 0; + } + } - if (valA < valB) return sortConfig.direction === 'asc' ? -1 : 1; - if (valA > valB) return sortConfig.direction === 'asc' ? 1 : -1; - return 0; - }); - } + if (valA < valB) return sortConfig.direction === 'asc' ? -1 : 1; + if (valA > valB) return sortConfig.direction === 'asc' ? 1 : -1; + return 0; + }); + } - return result; - }, [pivotRows, rowFilters, sortConfig, years]); + return result; + }, [pivotRows, rowFilters, sortConfig, years]); - const paginatedRows = useMemo(() => { - const start = (currentPage - 1) * ROWS_PER_PAGE; - return processedRows.slice(start, start + ROWS_PER_PAGE); - }, [processedRows, currentPage]); + const paginatedRows = useMemo(() => { + const start = (currentPage - 1) * ROWS_PER_PAGE; + return processedRows.slice(start, start + ROWS_PER_PAGE); + }, [processedRows, currentPage]); - const totalPages = Math.ceil(processedRows.length / ROWS_PER_PAGE); + const totalPages = Math.ceil(processedRows.length / ROWS_PER_PAGE); - const requestSort = (key: string) => { - let direction: 'asc' | 'desc' = 'desc'; - if (sortConfig.key === key && sortConfig.direction === 'desc') { - direction = 'asc'; - } - setSortConfig({ key, direction }); - }; + const requestSort = (key: string) => { + let direction: 'asc' | 'desc' = 'desc'; + if (sortConfig.key === key && sortConfig.direction === 'desc') { + direction = 'asc'; + } + setSortConfig({ key, direction }); + }; - const getSortIcon = (key: string) => { - if (sortConfig.key !== key) return ; - return {sortConfig.direction === 'asc' ? '↑' : '↓'}; - }; + const getSortIcon = (key: string) => { + if (sortConfig.key !== key) return ; + return {sortConfig.direction === 'asc' ? '↑' : '↓'}; + }; - const handleExport = () => { - generateCSV(processedRows, effectiveDimensions, years); - }; + const handleExport = () => { + generateCSV(processedRows, effectiveDimensions, years); + }; - const addFilter = () => { - if (newFilterMetric && newFilterValue) { - setRowFilters([ - ...rowFilters, - { - id: Date.now().toString(), - metric: newFilterMetric, - operator: newFilterOperator, - value: parseFloat(newFilterValue) - } - ]); - setNewFilterMetric(''); - setNewFilterValue(''); - setShowFilterBuilder(false); - } - }; + const addFilter = () => { + if (newFilterMetric && newFilterValue) { + setRowFilters([ + ...rowFilters, + { + id: Date.now().toString(), + metric: newFilterMetric, + operator: newFilterOperator, + value: parseFloat(newFilterValue) + } + ]); + setNewFilterMetric(''); + setNewFilterValue(''); + setShowFilterBuilder(false); + } + }; - const removeFilter = (id: string) => { - setRowFilters(rowFilters.filter(f => f.id !== id)); - }; + const removeFilter = (id: string) => { + setRowFilters(rowFilters.filter(f => f.id !== id)); + }; - // Reset pagination when filters change - useEffect(() => { - setCurrentPage(1); - }, [rowFilters, data, effectiveDimensions]); + // Reset pagination when filters change + useEffect(() => { + setCurrentPage(1); + }, [rowFilters, data, effectiveDimensions]); - return ( -
- - {/* 1. Time Series Chart Section */} - {showChart && chartData.length > 0 && ( - -
- - - - - `€${(val/1000).toFixed(0)}k`} - /> - isComparisonView ? : } /> - - - {isComparisonView ? ( - uniqueYears.flatMap((year, idx) => [ - visibleMetrics.includes('sellOut') && ( - - ), - visibleMetrics.includes('units') && ( - - ) - ]) - ) : ( - [ - visibleMetrics.includes('sellOut') && ( - - ), - visibleMetrics.includes('units') && ( - - ) - ] - )} - - -
-
- )} + return ( +
- {/* 2. Controls & Grid */} -
- - {/* Toolbar */} -
- -
- {/* Dimensions Selector */} -
- -
- {DIMENSION_OPTIONS.map(dim => ( -
+ `€${(val / 1000).toFixed(0)}k`} + /> + isComparisonView ? : } /> + - {/* Filter Builder Trigger */} - - - {/* Chart Toggle */} - -
+ {isComparisonView ? ( + uniqueYears.flatMap((year, idx) => [ + visibleMetrics.includes('sellOut') && ( + + ), + visibleMetrics.includes('units') && ( + + ) + ]) + ) : ( + [ + visibleMetrics.includes('sellOut') && ( + + ), + visibleMetrics.includes('units') && ( + + ) + ] + )} + + +
+ + )} -
-
- Showing {processedRows.length} rows -
- -
-
- - {/* Filter Builder Panel */} - {showFilterBuilder && ( -
-
-
- - { + if (selectedDimensions.includes(dim.value)) { + setSelectedDimensions(selectedDimensions.filter(d => d !== dim.value)); + } else { + setSelectedDimensions([...selectedDimensions, dim.value]); + } + }} + className="rounded border-slate-600 bg-slate-800 text-primary focus:ring-primary" + /> + {dim.label} + + ))} +
+
+ + {/* Filter Builder Trigger */} +
-
- - + + {showChart ? 'Hide Chart' : 'Show Chart'} +
-
- - setNewFilterValue(e.target.value)} - placeholder="0" - className="w-full bg-slate-950 border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-primary" - /> + +
+
+ Showing {processedRows.length} rows +
+
-
- {/* Active Filters Chips */} - {rowFilters.length > 0 && ( -
- {rowFilters.map(filter => { - const metricLabel = metricOptions.find(m => m.value === filter.metric)?.label || filter.metric; - return ( -
- {metricLabel} {filter.operator === 'gt' ? '>' : '<'} {filter.value} - -
- ); - })} + {/* Filter Builder Panel */} + {showFilterBuilder && ( +
+
+
+ + +
+
+ + +
+
+ + setNewFilterValue(e.target.value)} + placeholder="0" + className="w-full bg-slate-950 border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-primary" + /> +
+ +
+ + {/* Active Filters Chips */} + {rowFilters.length > 0 && ( +
+ {rowFilters.map(filter => { + const metricLabel = metricOptions.find(m => m.value === filter.metric)?.label || filter.metric; + return ( +
+ {metricLabel} {filter.operator === 'gt' ? '>' : '<'} {filter.value} + +
+ ); + })} +
+ )}
)} -
- )} - {/* Data Table */} -
- - - - {/* Dynamic Dimension Headers */} - {effectiveDimensions.map(dim => { - const label = DIMENSION_OPTIONS.find(d => d.value === dim)?.label || dim; - return ( - - ); - })} - - {/* Total Columns per Year */} - {years.map(year => ( - - - - - ))} - - - - {paginatedRows.map((row) => ( - - {/* Dimension Values */} - {effectiveDimensions.map(dim => ( - - ))} - - {/* Metric Values */} - {years.map(year => { - const data = row.totalsByYear[year]; - return ( - - - - - ); - })} - - ))} - - {paginatedRows.length === 0 && ( - - - - )} - -
requestSort(dim)} + {/* Data Table */} +
+ + + + {/* Dynamic Dimension Headers */} + {effectiveDimensions.map(dim => { + const label = DIMENSION_OPTIONS.find(d => d.value === dim)?.label || dim; + return ( + + ); + })} + + {/* Total Columns per Year */} + {years.map(year => ( + + + + + ))} + + + + {paginatedRows.map((row) => ( + + {/* Dimension Values */} + {effectiveDimensions.map(dim => ( + + ))} + + {/* Metric Values */} + {years.map(year => { + const data = row.totalsByYear[year]; + return ( + + + + + ); + })} + + ))} + + {paginatedRows.length === 0 && ( + + + + )} + +
requestSort(dim)} + > +
+ {label} {getSortIcon(dim)} +
+
requestSort(`total_sellOut_${year}`)} + > +
+ Sell Out {year} {getSortIcon(`total_sellOut_${year}`)} +
+
requestSort(`total_units_${year}`)} + > +
+ Units {year} {getSortIcon(`total_units_${year}`)} +
+
+ {dim === 'title' + ?
{row.title || '-'}
+ : (row[dim as keyof PivotRow] as string) || '-' + } +
+ {data ? `€${data.sellOut.toLocaleString(undefined, { maximumFractionDigits: 0 })}` : '-'} + + {data ? data.units.toLocaleString() : '-'} +
+ No data matches your filters. +
+
+ + {/* Pagination */} +
+
+ Page {currentPage} of {totalPages || 1} +
+
+
requestSort(`total_sellOut_${year}`)} - > -
- Sell Out {year} {getSortIcon(`total_sellOut_${year}`)} -
-
requestSort(`total_units_${year}`)} - > -
- Units {year} {getSortIcon(`total_units_${year}`)} -
-
- {dim === 'title' - ?
{row.title || '-'}
- : (row[dim as keyof PivotRow] as string) || '-' - } -
- {data ? `€${data.sellOut.toLocaleString(undefined, { maximumFractionDigits: 0 })}` : '-'} - - {data ? data.units.toLocaleString() : '-'} -
- No data matches your filters. -
-
- - {/* Pagination */} -
-
- Page {currentPage} of {totalPages || 1} -
-
- - + Previous + + +
+
-
-
- ); + ); }; export default DataGrid; \ No newline at end of file diff --git a/services/dataProcessor.ts b/services/dataProcessor.ts index 530f1f8..c6120ea 100644 --- a/services/dataProcessor.ts +++ b/services/dataProcessor.ts @@ -224,8 +224,8 @@ export const processCSV = (fileOrContent: File | string): Promise const data: SalesRecord[] = results.data.map((row: any, index: number) => { return mapRowToRecord(row, index); }) - // Filter: Valid Year AND Allowed Customer - .filter((r: SalesRecord) => r.year > 0 && isAllowedCustomer(r.customer)); + // Filter: Valid Year > 2023 (exclude incomplete 2023 data) AND Allowed Customer + .filter((r: SalesRecord) => r.year > 2023 && isAllowedCustomer(r.customer)); resolve(data); } catch (err) {