mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 12:35:24 +02:00
199 lines
12 KiB
TypeScript
199 lines
12 KiB
TypeScript
|
|||
|
|
import React, { useState, useMemo } from 'react';
|
||
|
|
import { ItemGrowthMetric } from '../types';
|
||
|
|
|
||
|
|
interface ItemGrowthTableProps {
|
||
|
|
title: string;
|
||
|
|
data: ItemGrowthMetric[];
|
||
|
|
type: 'growth' | 'decline';
|
||
|
|
periods: { current: string; previous: string };
|
||
|
|
}
|
||
|
|
|
||
|
|
type SortConfig = { key: keyof ItemGrowthMetric | null; direction: 'asc' | 'desc' };
|
||
|
|
|
||
|
|
const ItemGrowthTable: React.FC<ItemGrowthTableProps> = ({ title, data, type, periods }) => {
|
||
|
|
const [sortConfig, setSortConfig] = useState<SortConfig>({ key: null, direction: 'desc' });
|
||
|
|
|
||
|
|
const sortedData = useMemo(() => {
|
||
|
|
if (!sortConfig.key) {
|
||
|
|
// Default sort by units growth value for initial view
|
||
|
|
return [...data].sort((a, b) =>
|
||
|
|
type === 'growth' ? b.unitsGrowthValue - a.unitsGrowthValue : a.unitsGrowthValue - b.unitsGrowthValue
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return [...data].sort((a, b) => {
|
||
|
|
const aVal = a[sortConfig.key!] as number | string;
|
||
|
|
const bVal = b[sortConfig.key!] as number | string;
|
||
|
|
|
||
|
|
if (typeof aVal === 'string' && typeof bVal === 'string') {
|
||
|
|
return sortConfig.direction === 'asc'
|
||
|
|
? (aVal as string).localeCompare(bVal as string)
|
||
|
|
: (bVal as string).localeCompare(aVal as string);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (aVal < bVal) return sortConfig.direction === 'asc' ? -1 : 1;
|
||
|
|
if (aVal > bVal) return sortConfig.direction === 'asc' ? 1 : -1;
|
||
|
|
return 0;
|
||
|
|
});
|
||
|
|
}, [data, sortConfig, type]);
|
||
|
|
|
||
|
|
const requestSort = (key: keyof ItemGrowthMetric) => {
|
||
|
|
let direction: 'asc' | 'desc' = 'desc';
|
||
|
|
// If already sorting by this key, toggle direction
|
||
|
|
if (sortConfig.key === key && sortConfig.direction === 'desc') {
|
||
|
|
direction = 'asc';
|
||
|
|
}
|
||
|
|
setSortConfig({ key, direction });
|
||
|
|
};
|
||
|
|
|
||
|
|
const getSortIndicator = (key: keyof ItemGrowthMetric) => {
|
||
|
|
if (sortConfig.key !== key) {
|
||
|
|
return (
|
||
|
|
<svg className="w-2.5 h-2.5 ml-1 text-slate-600 opacity-0 group-hover:opacity-50" fill="currentColor" viewBox="0 0 20 20">
|
||
|
|
<path fillRule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clipRule="evenodd" />
|
||
|
|
</svg>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
return (
|
||
|
|
<svg className="w-2.5 h-2.5 ml-1 text-primary" fill="currentColor" viewBox="0 0 20 20">
|
||
|
|
{sortConfig.direction === 'asc'
|
||
|
|
? <path fillRule="evenodd" d="M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293 3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 010 1.414z" clipRule="evenodd" />
|
||
|
|
: <path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
|
||
|
|
}
|
||
|
|
</svg>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="overflow-auto h-full relative">
|
||
|
|
<table className="w-full text-left text-sm h-full border-separate border-spacing-0">
|
||
|
|
<thead className="bg-slate-950 text-xs uppercase font-semibold text-slate-500 sticky top-0 z-10 shadow-sm">
|
||
|
|
<tr>
|
||
|
|
<th
|
||
|
|
className="px-4 py-3 bg-slate-950 min-w-[100px] cursor-pointer hover:text-slate-300 group select-none transition-colors"
|
||
|
|
onClick={() => requestSort('sku')}
|
||
|
|
>
|
||
|
|
<div className="flex items-center">SKU {getSortIndicator('sku')}</div>
|
||
|
|
</th>
|
||
|
|
<th
|
||
|
|
className="px-4 py-3 bg-slate-950 min-w-[100px] cursor-pointer hover:text-slate-300 group select-none transition-colors"
|
||
|
|
onClick={() => requestSort('asin')}
|
||
|
|
>
|
||
|
|
<div className="flex items-center">ASIN {getSortIndicator('asin')}</div>
|
||
|
|
</th>
|
||
|
|
<th
|
||
|
|
className="px-4 py-3 bg-slate-950 min-w-[200px] cursor-pointer hover:text-slate-300 group select-none transition-colors"
|
||
|
|
onClick={() => requestSort('title')}
|
||
|
|
>
|
||
|
|
<div className="flex items-center">Product Title {getSortIndicator('title')}</div>
|
||
|
|
</th>
|
||
|
|
<th
|
||
|
|
className="px-4 py-3 bg-slate-950 min-w-[120px] cursor-pointer hover:text-slate-300 group select-none transition-colors"
|
||
|
|
onClick={() => requestSort('line')}
|
||
|
|
>
|
||
|
|
<div className="flex items-center">Product Line {getSortIndicator('line')}</div>
|
||
|
|
</th>
|
||
|
|
|
||
|
|
{/* Sell Out Columns */}
|
||
|
|
<th
|
||
|
|
className="px-4 py-3 text-right bg-slate-950 text-slate-400 cursor-pointer hover:text-slate-300 group select-none transition-colors"
|
||
|
|
onClick={() => requestSort('previousYearSellOut')}
|
||
|
|
>
|
||
|
|
<div className="flex items-center justify-end">Sell Out {periods.previous} {getSortIndicator('previousYearSellOut')}</div>
|
||
|
|
</th>
|
||
|
|
<th
|
||
|
|
className="px-4 py-3 text-right bg-slate-950 text-slate-200 cursor-pointer hover:text-white group select-none transition-colors"
|
||
|
|
onClick={() => requestSort('currentYearSellOut')}
|
||
|
|
>
|
||
|
|
<div className="flex items-center justify-end">Sell Out {periods.current} {getSortIndicator('currentYearSellOut')}</div>
|
||
|
|
</th>
|
||
|
|
<th
|
||
|
|
className="px-4 py-3 text-right bg-slate-950 cursor-pointer hover:text-slate-300 group select-none transition-colors"
|
||
|
|
onClick={() => requestSort('sellOutGrowthValue')}
|
||
|
|
>
|
||
|
|
<div className="flex items-center justify-end">SO Diff {getSortIndicator('sellOutGrowthValue')}</div>
|
||
|
|
</th>
|
||
|
|
<th
|
||
|
|
className="px-4 py-3 text-right bg-slate-950 cursor-pointer hover:text-slate-300 group select-none transition-colors"
|
||
|
|
onClick={() => requestSort('sellOutGrowthPercentage')}
|
||
|
|
>
|
||
|
|
<div className="flex items-center justify-end">SO Growth % {getSortIndicator('sellOutGrowthPercentage')}</div>
|
||
|
|
</th>
|
||
|
|
|
||
|
|
{/* Units Columns */}
|
||
|
|
<th
|
||
|
|
className="px-4 py-3 text-right bg-slate-950 text-slate-400 cursor-pointer hover:text-slate-300 group select-none transition-colors"
|
||
|
|
onClick={() => requestSort('previousYearUnits')}
|
||
|
|
>
|
||
|
|
<div className="flex items-center justify-end">Units {periods.previous} {getSortIndicator('previousYearUnits')}</div>
|
||
|
|
</th>
|
||
|
|
<th
|
||
|
|
className="px-4 py-3 text-right bg-slate-950 text-slate-200 cursor-pointer hover:text-white group select-none transition-colors"
|
||
|
|
onClick={() => requestSort('currentYearUnits')}
|
||
|
|
>
|
||
|
|
<div className="flex items-center justify-end">Units {periods.current} {getSortIndicator('currentYearUnits')}</div>
|
||
|
|
</th>
|
||
|
|
<th
|
||
|
|
className="px-4 py-3 text-right bg-slate-950 cursor-pointer hover:text-slate-300 group select-none transition-colors"
|
||
|
|
onClick={() => requestSort('unitsGrowthValue')}
|
||
|
|
>
|
||
|
|
<div className="flex items-center justify-end">Units Diff {getSortIndicator('unitsGrowthValue')}</div>
|
||
|
|
</th>
|
||
|
|
<th
|
||
|
|
className="px-4 py-3 text-right bg-slate-950 cursor-pointer hover:text-slate-300 group select-none transition-colors"
|
||
|
|
onClick={() => requestSort('unitsGrowthPercentage')}
|
||
|
|
>
|
||
|
|
<div className="flex items-center justify-end">Units Growth % {getSortIndicator('unitsGrowthPercentage')}</div>
|
||
|
|
</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody className="divide-y divide-border text-slate-300">
|
||
|
|
{sortedData.length > 0 ? (
|
||
|
|
sortedData.map((item, idx) => (
|
||
|
|
<tr key={idx} className="hover:bg-slate-800/50">
|
||
|
|
<td className="px-4 py-2 font-medium">{item.sku || '-'}</td>
|
||
|
|
<td className="px-4 py-2">{item.asin || '-'}</td>
|
||
|
|
<td className="px-4 py-2">{item.title || '-'}</td>
|
||
|
|
<td className="px-4 py-2">{item.line || '-'}</td>
|
||
|
|
|
||
|
|
{/* Sell Out Columns */}
|
||
|
|
<td className="px-4 py-2 text-right text-slate-400">€{item.previousYearSellOut.toLocaleString(undefined, {maximumFractionDigits: 0})}</td>
|
||
|
|
<td className="px-4 py-2 text-right font-medium text-slate-200">€{item.currentYearSellOut.toLocaleString(undefined, {maximumFractionDigits: 0})}</td>
|
||
|
|
<td className={`px-4 py-2 text-right font-medium ${item.sellOutGrowthValue >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
|
||
|
|
{item.sellOutGrowthValue > 0 ? '+' : ''}€{item.sellOutGrowthValue.toLocaleString(undefined, {maximumFractionDigits: 0})}
|
||
|
|
</td>
|
||
|
|
<td className="px-4 py-2 text-right">
|
||
|
|
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${item.sellOutGrowthPercentage >= 0 ? 'bg-emerald-500/10 text-emerald-400' : 'bg-red-500/10 text-red-400'}`}>
|
||
|
|
{item.sellOutGrowthPercentage.toFixed(1)}%
|
||
|
|
</span>
|
||
|
|
</td>
|
||
|
|
|
||
|
|
{/* Units Columns */}
|
||
|
|
<td className="px-4 py-2 text-right text-slate-400">{item.previousYearUnits.toLocaleString()}</td>
|
||
|
|
<td className="px-4 py-2 text-right font-medium text-slate-200">{item.currentYearUnits.toLocaleString()}</td>
|
||
|
|
<td className={`px-4 py-2 text-right font-medium ${item.unitsGrowthValue >= 0 ? 'text-violet-400' : 'text-orange-400'}`}>
|
||
|
|
{item.unitsGrowthValue > 0 ? '+' : ''}{item.unitsGrowthValue.toLocaleString()}
|
||
|
|
</td>
|
||
|
|
<td className="px-4 py-2 text-right">
|
||
|
|
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${item.unitsGrowthPercentage >= 0 ? 'bg-violet-500/10 text-violet-400' : 'bg-orange-500/10 text-orange-400'}`}>
|
||
|
|
{item.unitsGrowthPercentage.toFixed(1)}%
|
||
|
|
</span>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
))
|
||
|
|
) : (
|
||
|
|
<tr>
|
||
|
|
<td colSpan={13} className="px-4 py-6 text-center text-slate-500 italic">
|
||
|
|
Insufficient data to calculate {type}. (Select a customer and at least 2 distinct years/periods)
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
)}
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export default ItemGrowthTable;
|