feat: add Week filter and Grid Growth % columns

This commit is contained in:
Christian Vidal Wolf
2026-01-16 11:47:59 +01:00
parent 15b44d4d91
commit 0075129d76
5 changed files with 162 additions and 87 deletions
+2
View File
@@ -108,6 +108,7 @@ const App: React.FC = () => {
asin: [], asin: [],
sku: [], sku: [],
title: [], title: [],
week: [],
}); });
}; };
@@ -249,6 +250,7 @@ const App: React.FC = () => {
asin: getUniqueValues(rawData, 'asin'), asin: getUniqueValues(rawData, 'asin'),
sku: getUniqueValues(rawData, 'sku'), sku: getUniqueValues(rawData, 'sku'),
title: getUniqueValues(rawData, 'title'), title: getUniqueValues(rawData, 'title'),
week: Array.from(new Set(rawData.map(r => r.week).filter(w => w !== undefined))).sort((a, b) => (a as number) - (b as number)).map(w => `W${w}`),
}; };
}, [rawData]); }, [rawData]);
+81 -22
View File
@@ -651,27 +651,47 @@ const DataGrid: React.FC<DataGridProps> = ({ data }) => {
); );
})} })}
{/* Total Columns per Year */} {/* Data Columns: Year Groups */}
{years.map(year => ( {years.map((year, index) => {
<React.Fragment key={year}> const prevYear = years[index + 1]; // Since years are sorted desc: 2025, 2024... next index is prev year
<th return (
className="px-4 py-3 border-b border-border text-right cursor-pointer hover:text-white bg-slate-950 min-w-[100px]" <React.Fragment key={year}>
onClick={() => requestSort(`total_sellOut_${year}`)} {/* Sell Out & Units */}
> <th
<div className="flex items-center justify-end"> className="px-4 py-3 border-b border-border text-right cursor-pointer hover:text-white bg-slate-950 min-w-[100px]"
Sell Out {year} {getSortIcon(`total_sellOut_${year}`)} onClick={() => requestSort(`total_sellOut_${year}`)}
</div> >
</th> <div className="flex items-center justify-end">
<th Sell Out {year} {getSortIcon(`total_sellOut_${year}`)}
className="px-4 py-3 border-b border-border text-right cursor-pointer hover:text-white bg-slate-950 min-w-[100px]" </div>
onClick={() => requestSort(`total_units_${year}`)} </th>
> <th
<div className="flex items-center justify-end"> className="px-4 py-3 border-b border-border text-right cursor-pointer hover:text-white bg-slate-950 min-w-[80px]"
Units {year} {getSortIcon(`total_units_${year}`)} onClick={() => requestSort(`total_units_${year}`)}
</div> >
</th> <div className="flex items-center justify-end">
</React.Fragment> Units {year} {getSortIcon(`total_units_${year}`)}
))} </div>
</th>
{/* Growth Columns (if prev year exists) */}
{prevYear && (
<>
<th className="px-2 py-3 border-b border-border text-center bg-slate-950/50 min-w-[80px]">
<div className="flex items-center justify-center text-[10px] text-slate-500 uppercase">
S.O. Δ%
</div>
</th>
<th className="px-2 py-3 border-b border-border text-center bg-slate-950/50 min-w-[80px]">
<div className="flex items-center justify-center text-[10px] text-slate-500 uppercase">
Units Δ%
</div>
</th>
</>
)}
</React.Fragment>
);
})}
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-border text-slate-300"> <tbody className="divide-y divide-border text-slate-300">
@@ -688,8 +708,27 @@ const DataGrid: React.FC<DataGridProps> = ({ data }) => {
))} ))}
{/* Metric Values */} {/* Metric Values */}
{years.map(year => { {years.map((year, index) => {
const data = row.totalsByYear[year]; const data = row.totalsByYear[year];
const prevYear = years[index + 1];
const prevData = prevYear ? row.totalsByYear[prevYear] : null;
// Calculate Growth
let sellOutGrowth = null;
let unitsGrowth = null;
if (prevYear) {
const currSO = data?.sellOut || 0;
const prevSO = prevData?.sellOut || 0;
if (prevSO !== 0) sellOutGrowth = ((currSO - prevSO) / prevSO) * 100;
else if (currSO > 0) sellOutGrowth = 100; // New entry
const currUnits = data?.units || 0;
const prevUnits = prevData?.units || 0;
if (prevUnits !== 0) unitsGrowth = ((currUnits - prevUnits) / prevUnits) * 100;
else if (currUnits > 0) unitsGrowth = 100;
}
return ( return (
<React.Fragment key={year}> <React.Fragment key={year}>
<td className="px-4 py-3 text-right font-medium text-white group-hover:text-emerald-400 transition-colors"> <td className="px-4 py-3 text-right font-medium text-white group-hover:text-emerald-400 transition-colors">
@@ -698,6 +737,26 @@ const DataGrid: React.FC<DataGridProps> = ({ data }) => {
<td className="px-4 py-3 text-right text-slate-400"> <td className="px-4 py-3 text-right text-slate-400">
{data ? data.units.toLocaleString() : '-'} {data ? data.units.toLocaleString() : '-'}
</td> </td>
{/* Growth Cells */}
{prevYear && (
<>
<td className={`px-2 py-3 text-center font-bold text-xs ${sellOutGrowth !== null ? (sellOutGrowth >= 0 ? 'text-emerald-500' : 'text-red-500') : 'text-slate-600'}`}>
{sellOutGrowth !== null ? (
<span className="flex items-center justify-center gap-1">
{sellOutGrowth >= 0 ? '↑' : '↓'} {Math.abs(sellOutGrowth).toFixed(0)}%
</span>
) : '-'}
</td>
<td className={`px-2 py-3 text-center font-bold text-xs ${unitsGrowth !== null ? (unitsGrowth >= 0 ? 'text-emerald-500' : 'text-red-500') : 'text-slate-600'}`}>
{unitsGrowth !== null ? (
<span className="flex items-center justify-center gap-1">
{unitsGrowth >= 0 ? '↑' : '↓'} {Math.abs(unitsGrowth).toFixed(0)}%
</span>
) : '-'}
</td>
</>
)}
</React.Fragment> </React.Fragment>
); );
})} })}
+50 -42
View File
@@ -14,6 +14,7 @@ interface FilterBarProps {
asin: string[]; asin: string[];
sku: string[]; sku: string[];
title: string[]; title: string[];
week: string[];
}; };
} }
@@ -21,54 +22,61 @@ const FilterBar: React.FC<FilterBarProps> = ({ filters, onFilterChange, options
return ( return (
<div className="bg-slate-950 border-b border-border sticky top-0 z-[60] p-4 shadow-xl"> <div className="bg-slate-950 border-b border-border sticky top-0 z-[60] p-4 shadow-xl">
<div className="max-w-7xl mx-auto flex flex-wrap gap-4 items-end"> <div className="max-w-7xl mx-auto flex flex-wrap gap-4 items-end">
<MultiSelectDropdown <MultiSelectDropdown
label="Customer" label="Customer"
selected={filters.customer} selected={filters.customer}
options={options.customer} options={options.customer}
onChange={(v) => onFilterChange('customer', v)} onChange={(v) => onFilterChange('customer', v)}
className="flex-1" className="flex-1"
/> />
<MultiSelectDropdown <MultiSelectDropdown
label="Year" label="Year"
selected={filters.year} selected={filters.year}
options={options.year} options={options.year}
onChange={(v) => onFilterChange('year', v)} onChange={(v) => onFilterChange('year', v)}
className="flex-1" className="flex-1"
/> />
<MultiSelectDropdown <MultiSelectDropdown
label="Month" label="Month"
selected={filters.month} selected={filters.month}
options={options.month} options={options.month}
onChange={(v) => onFilterChange('month', v)} onChange={(v) => onFilterChange('month', v)}
className="flex-1" className="flex-1"
/> />
<MultiSelectDropdown <MultiSelectDropdown
label="Product Line" label="Week"
selected={filters.line} selected={filters.week}
options={options.line} options={options.week}
onChange={(v) => onFilterChange('line', v)} onChange={(v) => onFilterChange('week', v)}
className="flex-1" className="flex-1"
/> />
<MultiSelectDropdown <MultiSelectDropdown
label="SKU" label="Product Line"
selected={filters.sku} selected={filters.line}
options={options.sku} options={options.line}
onChange={(v) => onFilterChange('sku', v)} onChange={(v) => onFilterChange('line', v)}
className="flex-1" className="flex-1"
/> />
<MultiSelectDropdown <MultiSelectDropdown
label="Title" label="SKU"
selected={filters.title} selected={filters.sku}
options={options.title} options={options.sku}
onChange={(v) => onFilterChange('title', v)} onChange={(v) => onFilterChange('sku', v)}
className="flex-1" className="flex-1"
/> />
<MultiSelectDropdown <MultiSelectDropdown
label="ASIN" label="Title"
selected={filters.asin} selected={filters.title}
options={options.asin} options={options.title}
onChange={(v) => onFilterChange('asin', v)} onChange={(v) => onFilterChange('title', v)}
className="flex-1" className="flex-1"
/>
<MultiSelectDropdown
label="ASIN"
selected={filters.asin}
options={options.asin}
onChange={(v) => onFilterChange('asin', v)}
className="flex-1"
/> />
</div> </div>
</div> </div>
+6 -1
View File
@@ -531,7 +531,12 @@ export const filterData = (data: SalesRecord[], filters: FilterState): SalesReco
const skuMatch = filters.sku.length === 0 || filters.sku.includes(item.sku); const skuMatch = filters.sku.length === 0 || filters.sku.includes(item.sku);
const titleMatch = filters.title.length === 0 || filters.title.includes(item.title); const titleMatch = filters.title.length === 0 || filters.title.includes(item.title);
return customerMatch && yearMatch && monthMatch && lineMatch && asinMatch && skuMatch && titleMatch; // Week Logic: Match "W1", "W2" etc.
// item.week is a number (e.g. 1), filter uses strings "W1"
const weekStr = item.week ? `W${item.week}` : '';
const weekMatch = filters.week.length === 0 || (weekStr !== '' && filters.week.includes(weekStr));
return customerMatch && yearMatch && monthMatch && lineMatch && asinMatch && skuMatch && titleMatch && weekMatch;
}); });
}; };
+23 -22
View File
@@ -22,6 +22,7 @@ export interface FilterState {
asin: string[]; asin: string[];
sku: string[]; sku: string[];
title: string[]; // Added Title filter title: string[]; // Added Title filter
week: string[]; // Added Week filter
} }
export interface GrowthMetric { export interface GrowthMetric {
@@ -30,7 +31,7 @@ export interface GrowthMetric {
previousYearSellOut: number; previousYearSellOut: number;
sellOutGrowthValue: number; sellOutGrowthValue: number;
sellOutGrowthPercentage: number; sellOutGrowthPercentage: number;
currentYearUnits: number; currentYearUnits: number;
previousYearUnits: number; previousYearUnits: number;
unitsGrowthValue: number; unitsGrowthValue: number;
@@ -62,7 +63,7 @@ export interface SeasonalityPoint {
export interface YearlySplitData { export interface YearlySplitData {
name: string; name: string;
// Dynamic keys like "2023", "2024" or "2023_value", "2023_units" // Dynamic keys like "2023", "2024" or "2023_value", "2023_units"
[key: string]: number | string; [key: string]: number | string;
} }
export interface AggregatedData { export interface AggregatedData {
@@ -90,27 +91,27 @@ export interface ChatMessage {
// New Interfaces for Dynamic Pivot Grid // New Interfaces for Dynamic Pivot Grid
export interface YearlyData { export interface YearlyData {
sellOut: number; sellOut: number;
units: number; units: number;
} }
export interface MonthlyPivot { export interface MonthlyPivot {
monthIndex: number; monthIndex: number;
byYear: Record<string, YearlyData>; byYear: Record<string, YearlyData>;
} }
export interface PivotRow { export interface PivotRow {
id: string; id: string;
customer: string; customer: string;
line: string; line: string;
title: string; // Added Title title: string; // Added Title
articleName: string; articleName: string;
sku: string; sku: string;
asin: string; asin: string;
// Dynamic buckets // Dynamic buckets
totalsByYear: Record<string, YearlyData>; totalsByYear: Record<string, YearlyData>;
months: MonthlyPivot[]; // Always 12 elements months: MonthlyPivot[]; // Always 12 elements
} }
export interface TimeSeriesData { export interface TimeSeriesData {
@@ -145,22 +146,22 @@ export interface CombinedKPIs {
title: string; title: string;
line: string; line: string;
sku: string; sku: string;
salesTotal: number; salesTotal: number;
unitsTotal: number; unitsTotal: number;
salesAds: number; salesAds: number;
unitsAds: number; unitsAds: number;
cost: number; cost: number;
clicks: number; clicks: number;
impressions: number; impressions: number;
salesOrganic: number; salesOrganic: number;
unitsOrganic: number; unitsOrganic: number;
paidSalesShare: number; paidSalesShare: number;
organicSalesShare: number; organicSalesShare: number;
acos: number; acos: number;
tacos: number; tacos: number;
roas: number; roas: number;