feat: enhance Weekly Sales UI with larger fonts and advanced metric filtering

- Increased font sizes for better readability across the grid.
- Implemented multi-metric sorting (Units DESC/ASC, Spend DESC/ASC) for week columns.
- Added visual indicators for active sort metric and direction.
This commit is contained in:
Christian Vidal Wolf
2026-01-21 16:37:49 +01:00
parent e2e5314b55
commit daad27c26d
+46 -34
View File
@@ -9,6 +9,7 @@ interface WeeklyGridProps {
type SortConfig = {
key: string; // weekKey
direction: 'asc' | 'desc';
metric: 'units' | 'spend';
} | null;
const ROWS_PER_PAGE = 50;
@@ -19,10 +20,10 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data }) => {
const [searchTerm, setSearchTerm] = useState('');
const [currentPage, setCurrentPage] = useState(1);
// Default sort: most recent week, descending
// Default sort: most recent week, descending, units
const [sortConfig, setSortConfig] = useState<SortConfig>(() => {
if (weeks.length > 0) {
return { key: weeks[0], direction: 'desc' };
return { key: weeks[0], direction: 'desc', metric: 'units' };
}
return null;
});
@@ -35,9 +36,16 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data }) => {
const handleSort = (weekKey: string) => {
setSortConfig(prev => {
if (prev?.key === weekKey) {
return { key: weekKey, direction: prev.direction === 'asc' ? 'desc' : 'asc' };
// Cycle: (Units, desc) -> (Units, asc) -> (Spend, desc) -> (Spend, asc)
if (prev.metric === 'units') {
if (prev.direction === 'desc') return { key: weekKey, direction: 'asc', metric: 'units' };
return { key: weekKey, direction: 'desc', metric: 'spend' };
} else {
if (prev.direction === 'desc') return { key: weekKey, direction: 'asc', metric: 'spend' };
return { key: weekKey, direction: 'desc', metric: 'units' };
}
}
return { key: weekKey, direction: 'desc' };
return { key: weekKey, direction: 'desc', metric: 'units' };
});
};
@@ -57,8 +65,9 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data }) => {
const result = [...filteredRows];
if (sortConfig) {
result.sort((a, b) => {
const valA = a.unitsByWeek[sortConfig.key] || 0;
const valB = b.unitsByWeek[sortConfig.key] || 0;
const metricKey = sortConfig.metric === 'units' ? 'unitsByWeek' : 'spendByWeek';
const valA = a[metricKey][sortConfig.key] || 0;
const valB = b[metricKey][sortConfig.key] || 0;
if (sortConfig.direction === 'asc') {
return valA - valB;
}
@@ -95,7 +104,7 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data }) => {
const isPositive = pct >= 0;
return (
<span className={`text-[7px] font-bold ${isPositive ? 'text-emerald-400' : 'text-red-400'}`}>
<span className={`text-[10px] font-bold ${isPositive ? 'text-emerald-400' : 'text-red-400'}`}>
{isPositive ? '▲' : '▼'}{Math.abs(pct).toFixed(0)}%
</span>
);
@@ -111,34 +120,34 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data }) => {
placeholder="Search SKU, Title, or Line..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full bg-slate-950 border border-white/10 rounded-lg px-4 py-2 text-sm text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-all pl-10"
className="w-full bg-slate-950 border border-white/10 rounded-lg px-4 py-2 text-base text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-all pl-10"
/>
<svg className="absolute left-3 top-2.5 w-4 h-4 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg className="absolute left-3 top-3 w-5 h-5 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<div className="flex items-center gap-3">
<span className="text-xs text-slate-500">
<span className="text-sm text-slate-500">
Showing {Math.min(sortedRows.length, (currentPage - 1) * ROWS_PER_PAGE + 1)}-{Math.min(sortedRows.length, currentPage * ROWS_PER_PAGE)} of {sortedRows.length}
</span>
<div className="flex items-center gap-1">
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage === 1}
className="p-2 bg-slate-800 hover:bg-slate-700 disabled:opacity-30 disabled:hover:bg-slate-800 rounded-lg transition-colors border border-white/5"
className="p-2.5 bg-slate-800 hover:bg-slate-700 disabled:opacity-30 disabled:hover:bg-slate-800 rounded-lg transition-colors border border-white/5"
>
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" /></svg>
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" /></svg>
</button>
<span className="bg-slate-950 border border-white/10 px-3 py-1.5 rounded-lg text-xs font-bold text-white min-w-[60px] text-center">
<span className="bg-slate-950 border border-white/10 px-4 py-2 rounded-lg text-sm font-bold text-white min-w-[70px] text-center">
{currentPage} / {Math.max(1, totalPages)}
</span>
<button
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
disabled={currentPage >= totalPages}
className="p-2 bg-slate-800 hover:bg-slate-700 disabled:opacity-30 disabled:hover:bg-slate-800 rounded-lg transition-colors border border-white/5"
className="p-2.5 bg-slate-800 hover:bg-slate-700 disabled:opacity-30 disabled:hover:bg-slate-800 rounded-lg transition-colors border border-white/5"
>
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /></svg>
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /></svg>
</button>
</div>
</div>
@@ -149,33 +158,36 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data }) => {
<table className="w-full text-left border-collapse min-w-[max-content]">
<thead className="sticky top-0 z-20">
<tr className="bg-slate-900 border-b border-white/10">
<th className="p-2 text-[9px] font-black text-slate-400 uppercase tracking-widest sticky left-0 z-30 bg-slate-900 border-r border-white/10 min-w-[200px]">Product Details</th>
<th className="p-3 text-[11px] font-black text-slate-400 uppercase tracking-widest sticky left-0 z-30 bg-slate-900 border-r border-white/10 min-w-[240px]">Product Details</th>
{weeks.map(week => (
<th
key={week}
className={`p-2 text-[9px] font-black uppercase tracking-widest text-center border-r border-white/10 min-w-[100px] cursor-pointer hover:bg-white/5 transition-colors select-none ${sortConfig?.key === week ? 'text-indigo-400 bg-white/[0.02]' : 'text-slate-400'}`}
className={`p-3 text-[10px] font-black uppercase tracking-widest text-center border-r border-white/10 min-w-[120px] cursor-pointer hover:bg-white/5 transition-colors select-none ${sortConfig?.key === week ? 'text-indigo-400 bg-white/[0.02]' : 'text-slate-400'}`}
onClick={() => handleSort(week)}
>
<div className="flex flex-col items-center gap-0.5">
<span>{week.split('-')[1]}/{week.split('-')[0].slice(-2)}</span>
<div className="flex flex-col items-center gap-1">
<span className="text-xs">{week.split('-')[1]}/{week.split('-')[0].slice(-2)}</span>
{sortConfig?.key === week && (
<span className="text-[10px]">{sortConfig.direction === 'asc' ? '↑' : '↓'}</span>
<div className="flex flex-col items-center -mt-0.5">
<span className="text-[12px] leading-none">{sortConfig.direction === 'asc' ? '↑' : '↓'}</span>
<span className="text-[7px] font-bold text-indigo-500 uppercase">{sortConfig.metric}</span>
</div>
)}
<div className="text-[7px] font-normal text-slate-500 mt-0.5">Units / Spend</div>
<div className="text-[8px] font-normal text-slate-500">Units / Spend</div>
</div>
</th>
))}
</tr>
<tr className="bg-indigo-950/40 backdrop-blur-md border-b border-white/10">
<th className="p-2 text-xs font-black text-white sticky left-0 z-30 bg-indigo-900/60 border-r border-white/10">TOTALS</th>
<th className="p-3 text-sm font-black text-white sticky left-0 z-30 bg-indigo-900/60 border-r border-white/10">TOTALS</th>
{weeks.map((week, idx) => (
<th key={week} className="p-2 text-xs font-black text-white text-center border-r border-white/10">
<th key={week} className="p-3 text-sm font-black text-white text-center border-r border-white/10">
<div className="flex flex-col items-center">
<div className="flex items-center gap-1">
<span>{weekTotals[week].units.toLocaleString('de-DE')}</span>
{renderGrowth(weekTotals[week].units, weekTotals[weeks[idx + 1]]?.units)}
</div>
<div className="text-[9px] text-indigo-400">
<div className="text-[10px] text-indigo-400">
{weekTotals[week].spend.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}
</div>
</div>
@@ -187,11 +199,11 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data }) => {
{paginatedRows.length > 0 ? (
paginatedRows.map((row) => (
<tr key={row.id} className="hover:bg-white/[0.02] transition-colors group">
<td className="p-2 py-1.5 sticky left-0 z-10 bg-slate-900 md:bg-slate-900/95 backdrop-blur-sm group-hover:bg-slate-800 border-r border-white/10">
<td className="p-3 py-2 sticky left-0 z-10 bg-slate-900 md:bg-slate-900/95 backdrop-blur-sm group-hover:bg-slate-800 border-r border-white/10">
<div className="flex flex-col">
<span className="text-[10px] font-black text-indigo-400 uppercase tracking-tighter truncate w-[180px]">{row.sku}</span>
<span className="text-[9px] text-white/60 truncate w-[180px] leading-tight mb-0.5" title={row.title}>{row.title}</span>
<span className="text-[7px] text-fuchsia-400/70 font-bold uppercase tracking-widest">{row.line}</span>
<span className="text-xs font-black text-indigo-400 uppercase tracking-tighter truncate w-[210px]">{row.sku}</span>
<span className="text-[11px] text-white/70 truncate w-[210px] leading-tight mb-1" title={row.title}>{row.title}</span>
<span className="text-[9px] text-fuchsia-400/80 font-bold uppercase tracking-widest">{row.line}</span>
</div>
</td>
{weeks.map((week, idx) => {
@@ -199,16 +211,16 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data }) => {
const prevVal = row.unitsByWeek[weeks[idx + 1]] || 0;
const spend = row.spendByWeek[week] || 0;
return (
<td key={week} className={`p-2 py-1 text-center border-r border-white/5 align-middle ${sortConfig?.key === week ? 'bg-white/[0.01]' : ''}`}>
<div className="flex flex-col items-center justify-center gap-0">
<div className="flex items-center gap-0.5">
<span className={`text-xs font-bold ${val > 0 ? (sortConfig?.key === week ? 'text-indigo-400' : 'text-white') : 'text-slate-700'}`}>
<td key={week} className={`p-3 py-2 text-center border-r border-white/5 align-middle ${sortConfig?.key === week ? 'bg-white/[0.01]' : ''}`}>
<div className="flex flex-col items-center justify-center gap-0.5">
<div className="flex items-center gap-1">
<span className={`text-sm font-bold ${val > 0 ? (sortConfig?.key === week && sortConfig.metric === 'units' ? 'text-indigo-400' : 'text-white') : 'text-slate-700'}`}>
{val > 0 ? val.toLocaleString('de-DE') : '-'}
</span>
{val > 0 && renderGrowth(val, prevVal)}
</div>
{spend > 0 && (
<span className="text-[9px] text-indigo-400/80 font-medium">
<span className={`text-[11px] font-medium ${sortConfig?.key === week && sortConfig.metric === 'spend' ? 'text-indigo-300' : 'text-indigo-400/80'}`}>
{spend.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}
</span>
)}
@@ -220,7 +232,7 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data }) => {
))
) : (
<tr>
<td colSpan={weeks.length + 1} className="p-10 text-center text-slate-500 italic text-sm">
<td colSpan={weeks.length + 1} className="p-10 text-center text-slate-500 italic text-base">
No SKUs found matching "{searchTerm}"
</td>
</tr>