Compare commits
2 Commits
dae0b227c1
...
76de11f4de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76de11f4de | ||
|
|
578835d9da |
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user