Files
CrazeAnalytix/components/ItemGrowthTable.tsx
T
Christian 9ba63ab8f8 feat: Initialize Craze Analytix project structure
Sets up the project with Vite, React, Tailwind CSS, Gemini AI integration, and necessary dependencies for data analysis. Includes initial configuration for TypeScript, Tailwind, and project metadata.
2025-12-11 11:25:26 +01:00

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;