Compare commits
2 Commits
5a5b9a035f
...
c881dd7dde
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c881dd7dde | ||
|
|
18d2b6e407 |
@@ -59,6 +59,8 @@ const filterOptions: Array<{ value: ReplenishmentFilter; label: string }> = [
|
|||||||
{ value: 'no_sales', label: 'Sem venda' }
|
{ value: 'no_sales', label: 'Sem venda' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const coverageTargetOptions = [7, 15, 30, 60];
|
||||||
|
|
||||||
const formatNumber = (value: number, maximumFractionDigits = 0) => (
|
const formatNumber = (value: number, maximumFractionDigits = 0) => (
|
||||||
new Intl.NumberFormat('pt-BR', { maximumFractionDigits }).format(value)
|
new Intl.NumberFormat('pt-BR', { maximumFractionDigits }).format(value)
|
||||||
);
|
);
|
||||||
@@ -121,9 +123,9 @@ const Replenishment = () => {
|
|||||||
});
|
});
|
||||||
const [sortBy, setSortBy] = useState<ReplenishmentSort>('need_desc');
|
const [sortBy, setSortBy] = useState<ReplenishmentSort>('need_desc');
|
||||||
const [viewMode, setViewMode] = useState<ReplenishmentView>(() => searchParams.get('view') === 'group' ? 'group' : 'sku');
|
const [viewMode, setViewMode] = useState<ReplenishmentView>(() => searchParams.get('view') === 'group' ? 'group' : 'sku');
|
||||||
|
const [targetCoverageDays, setTargetCoverageDays] = useState(30);
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [itemsPerPage, setItemsPerPage] = useState(10);
|
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||||
const horizonDays = useMemo(() => getRangeDays(dateRange), [dateRange]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isMounted = true;
|
let isMounted = true;
|
||||||
@@ -150,7 +152,7 @@ const Replenishment = () => {
|
|||||||
|
|
||||||
return products.map(product => {
|
return products.map(product => {
|
||||||
const dailySales = product.quantitySold / rangeDays;
|
const dailySales = product.quantitySold / rangeDays;
|
||||||
const projectedDemand = dailySales * horizonDays;
|
const projectedDemand = dailySales * targetCoverageDays;
|
||||||
const rawNeed = projectedDemand - product.stock;
|
const rawNeed = projectedDemand - product.stock;
|
||||||
const suggestedQuantity = Math.max(0, Math.ceil(rawNeed));
|
const suggestedQuantity = Math.max(0, Math.ceil(rawNeed));
|
||||||
const daysOfCover = dailySales > 0 ? product.stock / dailySales : null;
|
const daysOfCover = dailySales > 0 ? product.stock / dailySales : null;
|
||||||
@@ -180,7 +182,7 @@ const Replenishment = () => {
|
|||||||
sizes: metadata.size ? [metadata.size] : []
|
sizes: metadata.size ? [metadata.size] : []
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, [dateRange, horizonDays, products]);
|
}, [dateRange, products, targetCoverageDays]);
|
||||||
|
|
||||||
const groupedRows = useMemo<ReplenishmentRow[]>(() => {
|
const groupedRows = useMemo<ReplenishmentRow[]>(() => {
|
||||||
const groups = new Map<string, ReplenishmentRow[]>();
|
const groups = new Map<string, ReplenishmentRow[]>();
|
||||||
@@ -249,7 +251,11 @@ const Replenishment = () => {
|
|||||||
|
|
||||||
const statusRows = statusFilter === 'all'
|
const statusRows = statusFilter === 'all'
|
||||||
? searchedRows
|
? searchedRows
|
||||||
: searchedRows.filter(row => row.status === statusFilter);
|
: searchedRows.filter(row => (
|
||||||
|
statusFilter === 'need'
|
||||||
|
? row.suggestedQuantity > 0
|
||||||
|
: row.status === statusFilter
|
||||||
|
));
|
||||||
|
|
||||||
return [...statusRows].sort((a, b) => {
|
return [...statusRows].sort((a, b) => {
|
||||||
switch (sortBy) {
|
switch (sortBy) {
|
||||||
@@ -283,7 +289,7 @@ const Replenishment = () => {
|
|||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold mb-2 text-zinc-900 dark:text-dark-text">Necessidade de Reposição</h1>
|
<h1 className="text-2xl font-bold mb-2 text-zinc-900 dark:text-dark-text">Necessidade de Reposição</h1>
|
||||||
<p className="text-zinc-500 dark:text-dark-muted font-medium">
|
<p className="text-zinc-500 dark:text-dark-muted font-medium">
|
||||||
O período selecionado define a base de vendas e a duração da projeção de reposição.
|
O período selecionado define a base de vendas; a cobertura define quantos dias de estoque sugerir.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -306,7 +312,7 @@ const Replenishment = () => {
|
|||||||
'Tamanhos': row.sizes.join(' | '),
|
'Tamanhos': row.sizes.join(' | '),
|
||||||
'SKUs': row.productCount,
|
'SKUs': row.productCount,
|
||||||
'Status': row.statusLabel,
|
'Status': row.statusLabel,
|
||||||
'Horizonte (dias)': horizonDays,
|
'Cobertura alvo (dias)': targetCoverageDays,
|
||||||
'Vendido no Periodo': row.quantitySold,
|
'Vendido no Periodo': row.quantitySold,
|
||||||
'Media Diaria': row.dailySales.toFixed(2).replace('.', ','),
|
'Media Diaria': row.dailySales.toFixed(2).replace('.', ','),
|
||||||
'Demanda Projetada': row.projectedDemand.toFixed(2).replace('.', ','),
|
'Demanda Projetada': row.projectedDemand.toFixed(2).replace('.', ','),
|
||||||
@@ -348,7 +354,7 @@ const Replenishment = () => {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Sugestão total</p>
|
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Sugestão total</p>
|
||||||
<p className="mt-2 text-3xl font-bold text-dark-text">{formatNumber(totalSuggestedQuantity)}</p>
|
<p className="mt-2 text-3xl font-bold text-dark-text">{formatNumber(totalSuggestedQuantity)}</p>
|
||||||
<p className="mt-1 text-xs font-semibold text-dark-muted">Unidades para cobrir {horizonDays} dias</p>
|
<p className="mt-1 text-xs font-semibold text-dark-muted">Unidades para cobrir {targetCoverageDays} dias</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-xl border border-brand-primary/25 bg-brand-primary/10 p-3 text-brand-primary">
|
<div className="rounded-xl border border-brand-primary/25 bg-brand-primary/10 p-3 text-brand-primary">
|
||||||
<Package className="h-5 w-5" />
|
<Package className="h-5 w-5" />
|
||||||
@@ -383,7 +389,7 @@ const Replenishment = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-3 rounded-2xl border border-dark-border bg-dark-card p-4 shadow-sm xl:grid-cols-[auto_1fr_180px_190px]">
|
<div className="grid grid-cols-1 gap-3 rounded-2xl border border-dark-border bg-dark-card p-4 shadow-sm xl:grid-cols-[auto_1fr_150px_180px_190px]">
|
||||||
<div className="inline-flex rounded-xl border border-dark-border bg-dark-input p-1">
|
<div className="inline-flex rounded-xl border border-dark-border bg-dark-input p-1">
|
||||||
{[
|
{[
|
||||||
{ key: 'sku' as const, label: 'SKU' },
|
{ key: 'sku' as const, label: 'SKU' },
|
||||||
@@ -421,6 +427,20 @@ const Replenishment = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<select
|
||||||
|
value={targetCoverageDays}
|
||||||
|
onChange={(event) => {
|
||||||
|
setTargetCoverageDays(Number(event.target.value));
|
||||||
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
|
className="h-11 rounded-xl border border-dark-border bg-dark-input px-3 text-sm font-semibold text-dark-text focus:outline-none focus:border-brand-primary cursor-pointer"
|
||||||
|
aria-label="Dias de cobertura alvo"
|
||||||
|
>
|
||||||
|
{coverageTargetOptions.map(days => (
|
||||||
|
<option key={days} value={days}>Cobrir {days} dias</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
|
||||||
<select
|
<select
|
||||||
value={statusFilter}
|
value={statusFilter}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user