Ignore unresolved supply data in planning
Some checks failed
Build and Deploy / build-and-deploy (push) Failing after 49s

This commit is contained in:
Cauê Faleiros
2026-08-04 13:51:25 -03:00
parent 30cdcd9b0e
commit c780b3134b
3 changed files with 36 additions and 49 deletions

View File

@@ -17,6 +17,11 @@ const normalizeNonNegativeNumber = (value) => {
return Number.isFinite(number) && number >= 0 ? number : null;
};
// Tiny may temporarily report a negative balance while its own adjustments are
// being reconciled. Negative inventory must not inflate the material demand:
// for planning, it represents no available stock, never an extra shortage.
const getAvailablePlanningStock = (value) => Math.max(0, Number(value) || 0);
const normalizeKey = (value) => normalizeText(value)
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
@@ -176,15 +181,16 @@ const buildPurchaseNeeds = (plans, lots, receipts) => {
return Array.from(needsByMaterial.values())
.map(need => {
const purchaseKg = Math.max(need.plannedKg - need.stockKg - need.pendingKg, 0);
const stockKg = getAvailablePlanningStock(need.stockKg);
const purchaseKg = Math.max(need.plannedKg - stockKg - need.pendingKg, 0);
let status = 'ok';
if (purchaseKg > 0 && (need.priority === 'Crítico' || need.stockKg === 0)) status = 'critical';
if (purchaseKg > 0 && (need.priority === 'Crítico' || stockKg === 0)) status = 'critical';
else if (purchaseKg > 0) status = 'attention';
return {
material: need.material,
plannedKg: need.plannedKg,
stockKg: need.stockKg,
stockKg,
pendingKg: need.pendingKg,
purchaseKg,
priority: need.priority,
@@ -543,15 +549,16 @@ const buildProjectPurchaseNeeds = async (lots, receipts) => {
});
return Array.from(needsByMaterial.values()).map(need => {
const purchaseKg = Math.max(need.plannedKg - need.stockKg - need.pendingKg, 0);
const stockKg = getAvailablePlanningStock(need.stockKg);
const purchaseKg = Math.max(need.plannedKg - stockKg - need.pendingKg, 0);
let status = 'ok';
if (purchaseKg > 0 && (need.priority === 'Crítico' || need.stockKg === 0)) status = 'critical';
if (purchaseKg > 0 && (need.priority === 'Crítico' || stockKg === 0)) status = 'critical';
else if (purchaseKg > 0) status = 'attention';
return {
material: need.material,
plannedKg: need.plannedKg,
stockKg: need.stockKg,
stockKg,
pendingKg: need.pendingKg,
purchaseKg,
priority: need.priority,
@@ -607,12 +614,14 @@ const mergePurchaseNeeds = (manualNeeds, projectNeeds) => {
return Array.from(mergedByKey.values())
.map(need => {
const purchaseKg = Math.max(need.plannedKg - need.stockKg - need.pendingKg, 0);
const stockKg = getAvailablePlanningStock(need.stockKg);
const purchaseKg = Math.max(need.plannedKg - stockKg - need.pendingKg, 0);
return {
...need,
stockKg,
purchaseKg,
status: purchaseKg > 0
? (need.priority === 'Crítico' || need.stockKg === 0 ? 'critical' : 'attention')
? (need.priority === 'Crítico' || stockKg === 0 ? 'critical' : 'attention')
: 'ok',
suppliers: Array.from(need.suppliers),
colors: Array.from(need.colors)

View File

@@ -247,7 +247,10 @@ const Cutting = () => {
[...cutPlan.rows].sort((a, b) => b.suggestedCutQuantity - a.suggestedCutQuantity || a.name.localeCompare(b.name, 'pt-BR')).forEach(row => {
const composition = compositionsById.get(normalizeMaterialKey(row.id));
if (!composition?.components.length) {
readinessByProductId.set(row.id, { status: 'missing', blockers: [] });
// Products without an imported composition remain eligible for cutting.
// They are excluded from material-demand calculations instead of
// blocking the rest of the production queue.
readinessByProductId.set(row.id, { status: 'ready', blockers: [] });
return;
}
const componentPlans = composition.components
@@ -260,7 +263,7 @@ const Cutting = () => {
return { component, stockIndex, available, required };
});
if (!componentPlans.length) {
readinessByProductId.set(row.id, { status: 'missing', blockers: ['Sem material produtivo na composição'] });
readinessByProductId.set(row.id, { status: 'ready', blockers: [] });
return;
}
const blockers = componentPlans.flatMap(({ component, available, required }) => {
@@ -276,7 +279,7 @@ const Cutting = () => {
componentPlans.forEach(({ stockIndex, required }) => {
if (stockIndex !== undefined) remainingStock[stockIndex] -= required;
});
readinessByProductId.set(row.id, { status: row.issues.length === 0 ? 'ready' : 'material_ready', blockers: [] });
readinessByProductId.set(row.id, { status: 'ready', blockers: [] });
});
return readinessByProductId;
}, [compositions, cutPlan.rows, materialStock]);

View File

@@ -294,35 +294,10 @@ const SuppliesHub = () => {
}, []);
const materialPurchaseNeeds = summary.purchaseNeeds.filter(need => !need.missingReference && !isServicePurchaseNeed(need) && need.purchaseKg > 0);
const negativeMaterialNeeds = materialPurchaseNeeds.filter(need => need.stockKg < 0);
const noStockMaterialNeeds = materialPurchaseNeeds.filter(need => need.stockKg === 0);
const mappingSkuCount = new Set(summary.purchaseNeeds
.filter(need => need.missingReference)
.flatMap(need => (need.products || []).map(product => product.productId))
.filter(Boolean)).size;
const activeProductionCount = productionCounts.open + productionCounts.inProgress;
const nextAction = negativeMaterialNeeds.length > 0
const nextAction = materialPurchaseNeeds.length > 0
? {
label: 'Prioridade imediata',
title: `Conferir ${formatNumber(negativeMaterialNeeds.length, 0)} saldos negativos`,
description: 'Estoque negativo distorce a compra sugerida e pode liberar corte sem material físico.',
meta: 'Abrir compra de materiais',
icon: AlertTriangle,
to: '/supplies/purchase-needs?tab=materials&filter=negative_balance',
tone: 'critical' as const,
}
: mappingSkuCount > 0
? {
label: 'Próxima ação',
title: `Mapear o consumo de ${formatNumber(mappingSkuCount, 0)} SKUs`,
description: 'Sem composição confiável, a necessidade de compra e o corte não representam a demanda real.',
meta: 'Abrir pendências de composição',
icon: LinkIcon,
to: '/supplies/purchase-needs?tab=mapping',
tone: 'info' as const,
}
: materialPurchaseNeeds.length > 0
? {
label: 'Próxima ação',
title: `Comprar ${formatNumber(materialPurchaseNeeds.length, 0)} materiais`,
description: 'A necessidade já desconta saldo e recebimentos para apoiar a decisão de compra.',
@@ -330,9 +305,9 @@ const SuppliesHub = () => {
icon: BarChart3,
to: '/supplies/purchase-needs?tab=materials&filter=to_buy',
tone: 'attention' as const,
}
: activeProductionCount > 0
? {
}
: activeProductionCount > 0
? {
label: 'Próxima ação',
title: `Acompanhar ${formatNumber(activeProductionCount, 0)} ordens em execução`,
description: 'Há produção aberta ou em andamento para confirmar antes de planejar uma nova rodada.',
@@ -340,8 +315,8 @@ const SuppliesHub = () => {
icon: ClipboardList,
to: '/production-orders',
tone: 'ready' as const,
}
: {
}
: {
label: 'Próxima ação',
title: 'Definir o próximo corte',
description: 'Use a demanda e a cobertura para escolher os SKUs que devem entrar em produção.',
@@ -349,7 +324,7 @@ const SuppliesHub = () => {
icon: Scissors,
to: '/cutting',
tone: 'default' as const,
};
};
const NextActionIcon = nextAction.icon;
const handleExportDiagnostic = async () => {
@@ -404,13 +379,13 @@ const SuppliesHub = () => {
</RouterLink>
<div className={`${panelClassName} p-5`}>
<div>
<h2 className="text-base font-bold text-dark-text">Pontos que bloqueiam o fluxo</h2>
<p className="mt-1 text-sm font-semibold text-dark-muted">Resolva estes pontos antes de confiar no plano.</p>
<h2 className="text-base font-bold text-dark-text">Situação operacional</h2>
<p className="mt-1 text-sm font-semibold text-dark-muted">Itens que entram na decisão atual de compra e produção.</p>
</div>
<div className="mt-4 space-y-2">
<FlowBlocker title="Saldos a conferir" description="Materiais com estoque negativo" count={negativeMaterialNeeds.length} to="/supplies/purchase-needs?tab=materials&filter=negative_balance" tone="critical" />
<FlowBlocker title="Dados para mapear" description="SKUs sem composição confiável" count={mappingSkuCount} to="/supplies/purchase-needs?tab=mapping" tone="info" />
<FlowBlocker title="Materiais para comprar" description="Faltas reais após saldo e recebimentos" count={materialPurchaseNeeds.length} to="/supplies/purchase-needs?tab=materials&filter=to_buy" tone="attention" />
<FlowBlocker title="Materiais sem saldo" description="Itens necessários sem disponibilidade" count={noStockMaterialNeeds.length} to="/supplies/purchase-needs?tab=materials&filter=no_stock" tone="attention" />
<FlowBlocker title="Produção em execução" description="OPs abertas ou em andamento" count={activeProductionCount} to="/production-orders" tone="info" />
</div>
</div>
</section>
@@ -420,8 +395,8 @@ const SuppliesHub = () => {
<p className="mt-1 text-sm font-semibold text-dark-muted">Da necessidade de material até a produção acompanhada.</p>
</div>
<div className="grid grid-cols-1 gap-4 xl:grid-cols-3">
<FlowStepCard title="Garantir materiais" description="Confira saldos, pendências de composição e a compra sugerida." icon={BarChart3} to={negativeMaterialNeeds.length > 0 ? '/supplies/purchase-needs?tab=materials&filter=negative_balance' : materialPurchaseNeeds.length > 0 ? '/supplies/purchase-needs?tab=materials&filter=to_buy' : mappingSkuCount > 0 ? '/supplies/purchase-needs?tab=mapping' : '/supplies/purchase-needs?tab=materials&filter=all'} tone={negativeMaterialNeeds.length > 0 ? 'critical' : materialPurchaseNeeds.length > 0 ? 'attention' : mappingSkuCount > 0 ? 'attention' : 'ready'} meta={negativeMaterialNeeds.length > 0 ? `${formatNumber(negativeMaterialNeeds.length, 0)} saldos negativos` : materialPurchaseNeeds.length > 0 ? `${formatNumber(materialPurchaseNeeds.length, 0)} materiais para comprar` : mappingSkuCount > 0 ? 'Sem falta calculada · dados pendentes' : 'Materiais cobertos'} />
<FlowStepCard title="Definir corte" description="Priorize SKUs, cores e tamanhos com necessidade e material disponível." icon={Scissors} to={mappingSkuCount > 0 ? '/supplies/purchase-needs?tab=mapping' : '/cutting'} tone={mappingSkuCount > 0 ? 'attention' : 'default'} meta={mappingSkuCount > 0 ? `${formatNumber(mappingSkuCount, 0)} SKUs precisam de mapeamento` : 'Abrir plano de corte'} />
<FlowStepCard title="Garantir materiais" description="Confira saldo disponível, recebimentos e a compra sugerida." icon={BarChart3} to={materialPurchaseNeeds.length > 0 ? '/supplies/purchase-needs?tab=materials&filter=to_buy' : '/supplies/purchase-needs?tab=materials&filter=all'} tone={materialPurchaseNeeds.length > 0 ? 'attention' : 'ready'} meta={materialPurchaseNeeds.length > 0 ? `${formatNumber(materialPurchaseNeeds.length, 0)} materiais para comprar` : 'Materiais cobertos'} />
<FlowStepCard title="Definir corte" description="Priorize SKUs, cores e tamanhos com necessidade e material disponível." icon={Scissors} to="/cutting" tone="default" meta="Abrir plano de corte" />
<FlowStepCard title="Acompanhar produção" description="Gerencie as OPs criadas a partir do corte até sua finalização." icon={ClipboardList} to="/production-orders" tone={activeProductionCount > 0 ? 'ready' : 'default'} meta={activeProductionCount > 0 ? `${formatNumber(productionCounts.open, 0)} abertas · ${formatNumber(productionCounts.inProgress, 0)} em andamento` : 'Nenhuma OP em execução'} />
</div>
</section>