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