mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 16:25:23 +02:00
feat: add Week filter and Grid Growth % columns
This commit is contained in:
@@ -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
@@ -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>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
+43
-35
@@ -14,6 +14,7 @@ interface FilterBarProps {
|
|||||||
asin: string[];
|
asin: string[];
|
||||||
sku: string[];
|
sku: string[];
|
||||||
title: string[];
|
title: string[];
|
||||||
|
week: string[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,53 +23,60 @@ const FilterBar: React.FC<FilterBarProps> = ({ filters, onFilterChange, options
|
|||||||
<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>
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user