Compare commits

...

2 Commits

Author SHA1 Message Date
Cauê Faleiros
76de11f4de Use stock status labels for products
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 45s
2026-07-06 14:01:05 -03:00
Cauê Faleiros
578835d9da Close product filters with escape 2026-07-06 13:50:40 -03:00

View File

@@ -12,7 +12,7 @@ type StockStatusFilter = 'all' | StockRisk;
type StockQuantityFilter = 'all' | 'zero' | 'positive' | 'low' | 'high';
type SalesFilter = 'all' | 'sold' | 'not_sold';
type CoverageFilter = 'all' | 'up_to_7' | 'up_to_14' | 'up_to_30' | 'over_30' | 'none';
type ProductSortOption = 'sold_desc' | 'sold_asc' | 'revenue_desc' | 'revenue_asc' | 'stock_asc' | 'stock_desc' | 'coverage_asc' | 'coverage_desc' | 'name_asc';
type ProductSortOption = 'sold_desc' | 'sold_asc' | 'stock_priority' | 'revenue_desc' | 'revenue_asc' | 'stock_asc' | 'stock_desc' | 'coverage_asc' | 'coverage_desc' | 'name_asc';
type ProductRow = ProductAnalyticsItem & {
dailySales: number;
@@ -28,22 +28,22 @@ const riskStyles: Record<StockRisk, { label: string; className: string; dotClass
dotClass: 'bg-zinc-400'
},
critical: {
label: '0-7 dias',
label: 'Repor agora',
className: 'border-red-400/35 bg-red-400/10 text-red-300',
dotClass: 'bg-red-400'
},
attention: {
label: '8-14 dias',
label: 'Atenção',
className: 'border-amber-400/35 bg-amber-400/10 text-amber-300',
dotClass: 'bg-amber-400'
},
monitor: {
label: '15-30 dias',
label: 'Monitorar',
className: 'border-sky-400/35 bg-sky-400/10 text-sky-300',
dotClass: 'bg-sky-400'
},
healthy: {
label: '>30 dias',
label: 'Saudável',
className: 'border-emerald-400/35 bg-emerald-400/10 text-emerald-300',
dotClass: 'bg-emerald-400'
},
@@ -57,13 +57,22 @@ const riskStyles: Record<StockRisk, { label: string; className: string; dotClass
const stockStatusOptions: Array<{ value: StockStatusFilter; label: string }> = [
{ value: 'all', label: 'Todos' },
{ value: 'rupture', label: 'Sem estoque' },
{ value: 'critical', label: '0-7 dias' },
{ value: 'attention', label: '8-14 dias' },
{ value: 'monitor', label: '15-30 dias' },
{ value: 'healthy', label: '>30 dias' },
{ value: 'critical', label: 'Repor agora' },
{ value: 'attention', label: 'Atenção' },
{ value: 'monitor', label: 'Monitorar' },
{ value: 'healthy', label: 'Saudável' },
{ value: 'no_sales', label: 'Sem venda' }
];
const stockPriority: Record<StockRisk, number> = {
rupture: 0,
critical: 1,
attention: 2,
monitor: 3,
healthy: 4,
no_sales: 5
};
const dateFilterPresets = [
{ value: 'today', label: 'Hoje', getRange: () => rangeForDay(new Date()) },
{ value: 'yesterday', label: 'Ontem', getRange: () => rangeForPreviousDay() },
@@ -198,14 +207,24 @@ const Products = () => {
useEffect(() => {
if (!isFilterMenuOpen) return;
const handleClickOutside = (event: MouseEvent) => {
const handlePointerDown = (event: PointerEvent) => {
if (!filterMenuRef.current?.contains(event.target as Node)) {
setIsFilterMenuOpen(false);
}
};
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setIsFilterMenuOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
document.addEventListener('pointerdown', handlePointerDown);
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('pointerdown', handlePointerDown);
document.removeEventListener('keydown', handleKeyDown);
};
}, [isFilterMenuOpen]);
const productsData = useMemo<ProductRow[]>(() => {
@@ -248,8 +267,8 @@ const Products = () => {
coverageFilter === 'all' ||
(coverageFilter === 'none' && product.daysOfCover === null) ||
(coverageFilter === 'up_to_7' && product.daysOfCover !== null && product.daysOfCover <= 7) ||
(coverageFilter === 'up_to_14' && product.daysOfCover !== null && product.daysOfCover <= 14) ||
(coverageFilter === 'up_to_30' && product.daysOfCover !== null && product.daysOfCover <= 30) ||
(coverageFilter === 'up_to_14' && product.daysOfCover !== null && product.daysOfCover > 7 && product.daysOfCover <= 14) ||
(coverageFilter === 'up_to_30' && product.daysOfCover !== null && product.daysOfCover > 14 && product.daysOfCover <= 30) ||
(coverageFilter === 'over_30' && product.daysOfCover !== null && product.daysOfCover > 30);
return matchesStatus && matchesStockQuantity && matchesSales && matchesCoverage;
@@ -258,6 +277,11 @@ const Products = () => {
return detailedFilteredProducts.sort((a, b) => {
switch (sortBy) {
case 'sold_asc': return a.quantitySold - b.quantitySold;
case 'stock_priority': {
const priorityDiff = stockPriority[a.risk] - stockPriority[b.risk];
if (priorityDiff !== 0) return priorityDiff;
return b.quantitySold - a.quantitySold;
}
case 'revenue_desc': return b.revenue - a.revenue;
case 'revenue_asc': return a.revenue - b.revenue;
case 'stock_asc': return a.stock - b.stock;
@@ -423,6 +447,7 @@ const Products = () => {
>
<option value="sold_desc">Mais vendidos</option>
<option value="sold_asc">Menos vendidos</option>
<option value="stock_priority">Prioridade de estoque</option>
<option value="revenue_desc">Maior receita</option>
<option value="revenue_asc">Menor receita</option>
<option value="stock_asc">Menor estoque</option>
@@ -454,7 +479,7 @@ const Products = () => {
</label>
<label className="block text-xs font-bold uppercase tracking-wider text-dark-muted">
Faixa de cobertura
Status
<select
value={stockStatusFilter}
onChange={(event) => {
@@ -549,7 +574,7 @@ const Products = () => {
const exportData = productsData.map(product => ({
'ID Produto': product.id,
'Descrição': product.name,
'Faixa de Cobertura': product.riskLabel,
'Status': product.riskLabel,
'Preço Atual (R$)': product.lastPrice.toFixed(2).replace('.', ','),
'Total Vendido (un.)': product.quantitySold,
'Estoque': product.stock,
@@ -591,7 +616,7 @@ const Products = () => {
<tr>
<th className="px-6 py-4 font-bold uppercase tracking-wider text-[10px]">ID Produto</th>
<th className="px-6 py-4 font-bold uppercase tracking-wider text-[10px]">Descrição</th>
<th className="px-6 py-4 font-bold uppercase tracking-wider text-[10px]">Faixa de cobertura</th>
<th className="px-6 py-4 font-bold uppercase tracking-wider text-[10px]">Status</th>
<th className="px-6 py-4 font-bold uppercase tracking-wider text-[10px]">Total Vendido</th>
<th className="px-6 py-4 font-bold uppercase tracking-wider text-[10px]">Estoque</th>
<th className="px-6 py-4 font-bold uppercase tracking-wider text-[10px]">Média Diária</th>