Separate replenishment coverage target
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m9s

This commit is contained in:
Cauê Faleiros
2026-07-07 14:29:21 -03:00
parent 18d2b6e407
commit c881dd7dde

View File

@@ -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[]>();
@@ -287,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>
@@ -310,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('.', ','),
@@ -352,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" />
@@ -387,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' },
@@ -425,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) => {