Ignore unresolved supply data in planning
Some checks failed
Build and Deploy / build-and-deploy (push) Failing after 49s
Some checks failed
Build and Deploy / build-and-deploy (push) Failing after 49s
This commit is contained in:
@@ -17,6 +17,11 @@ const normalizeNonNegativeNumber = (value) => {
|
|||||||
return Number.isFinite(number) && number >= 0 ? number : null;
|
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)
|
const normalizeKey = (value) => normalizeText(value)
|
||||||
.normalize('NFD')
|
.normalize('NFD')
|
||||||
.replace(/[\u0300-\u036f]/g, '')
|
.replace(/[\u0300-\u036f]/g, '')
|
||||||
@@ -176,15 +181,16 @@ const buildPurchaseNeeds = (plans, lots, receipts) => {
|
|||||||
|
|
||||||
return Array.from(needsByMaterial.values())
|
return Array.from(needsByMaterial.values())
|
||||||
.map(need => {
|
.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';
|
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';
|
else if (purchaseKg > 0) status = 'attention';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
material: need.material,
|
material: need.material,
|
||||||
plannedKg: need.plannedKg,
|
plannedKg: need.plannedKg,
|
||||||
stockKg: need.stockKg,
|
stockKg,
|
||||||
pendingKg: need.pendingKg,
|
pendingKg: need.pendingKg,
|
||||||
purchaseKg,
|
purchaseKg,
|
||||||
priority: need.priority,
|
priority: need.priority,
|
||||||
@@ -543,15 +549,16 @@ const buildProjectPurchaseNeeds = async (lots, receipts) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return Array.from(needsByMaterial.values()).map(need => {
|
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';
|
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';
|
else if (purchaseKg > 0) status = 'attention';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
material: need.material,
|
material: need.material,
|
||||||
plannedKg: need.plannedKg,
|
plannedKg: need.plannedKg,
|
||||||
stockKg: need.stockKg,
|
stockKg,
|
||||||
pendingKg: need.pendingKg,
|
pendingKg: need.pendingKg,
|
||||||
purchaseKg,
|
purchaseKg,
|
||||||
priority: need.priority,
|
priority: need.priority,
|
||||||
@@ -607,12 +614,14 @@ const mergePurchaseNeeds = (manualNeeds, projectNeeds) => {
|
|||||||
|
|
||||||
return Array.from(mergedByKey.values())
|
return Array.from(mergedByKey.values())
|
||||||
.map(need => {
|
.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 {
|
return {
|
||||||
...need,
|
...need,
|
||||||
|
stockKg,
|
||||||
purchaseKg,
|
purchaseKg,
|
||||||
status: purchaseKg > 0
|
status: purchaseKg > 0
|
||||||
? (need.priority === 'Crítico' || need.stockKg === 0 ? 'critical' : 'attention')
|
? (need.priority === 'Crítico' || stockKg === 0 ? 'critical' : 'attention')
|
||||||
: 'ok',
|
: 'ok',
|
||||||
suppliers: Array.from(need.suppliers),
|
suppliers: Array.from(need.suppliers),
|
||||||
colors: Array.from(need.colors)
|
colors: Array.from(need.colors)
|
||||||
|
|||||||
@@ -247,7 +247,10 @@ const Cutting = () => {
|
|||||||
[...cutPlan.rows].sort((a, b) => b.suggestedCutQuantity - a.suggestedCutQuantity || a.name.localeCompare(b.name, 'pt-BR')).forEach(row => {
|
[...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));
|
const composition = compositionsById.get(normalizeMaterialKey(row.id));
|
||||||
if (!composition?.components.length) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
const componentPlans = composition.components
|
const componentPlans = composition.components
|
||||||
@@ -260,7 +263,7 @@ const Cutting = () => {
|
|||||||
return { component, stockIndex, available, required };
|
return { component, stockIndex, available, required };
|
||||||
});
|
});
|
||||||
if (!componentPlans.length) {
|
if (!componentPlans.length) {
|
||||||
readinessByProductId.set(row.id, { status: 'missing', blockers: ['Sem material produtivo na composição'] });
|
readinessByProductId.set(row.id, { status: 'ready', blockers: [] });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const blockers = componentPlans.flatMap(({ component, available, required }) => {
|
const blockers = componentPlans.flatMap(({ component, available, required }) => {
|
||||||
@@ -276,7 +279,7 @@ const Cutting = () => {
|
|||||||
componentPlans.forEach(({ stockIndex, required }) => {
|
componentPlans.forEach(({ stockIndex, required }) => {
|
||||||
if (stockIndex !== undefined) remainingStock[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;
|
return readinessByProductId;
|
||||||
}, [compositions, cutPlan.rows, materialStock]);
|
}, [compositions, cutPlan.rows, materialStock]);
|
||||||
|
|||||||
@@ -294,35 +294,10 @@ const SuppliesHub = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const materialPurchaseNeeds = summary.purchaseNeeds.filter(need => !need.missingReference && !isServicePurchaseNeed(need) && need.purchaseKg > 0);
|
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 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 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',
|
label: 'Próxima ação',
|
||||||
title: `Comprar ${formatNumber(materialPurchaseNeeds.length, 0)} materiais`,
|
title: `Comprar ${formatNumber(materialPurchaseNeeds.length, 0)} materiais`,
|
||||||
description: 'A necessidade já desconta saldo e recebimentos para apoiar a decisão de compra.',
|
description: 'A necessidade já desconta saldo e recebimentos para apoiar a decisão de compra.',
|
||||||
@@ -330,9 +305,9 @@ const SuppliesHub = () => {
|
|||||||
icon: BarChart3,
|
icon: BarChart3,
|
||||||
to: '/supplies/purchase-needs?tab=materials&filter=to_buy',
|
to: '/supplies/purchase-needs?tab=materials&filter=to_buy',
|
||||||
tone: 'attention' as const,
|
tone: 'attention' as const,
|
||||||
}
|
}
|
||||||
: activeProductionCount > 0
|
: activeProductionCount > 0
|
||||||
? {
|
? {
|
||||||
label: 'Próxima ação',
|
label: 'Próxima ação',
|
||||||
title: `Acompanhar ${formatNumber(activeProductionCount, 0)} ordens em execuçã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.',
|
description: 'Há produção aberta ou em andamento para confirmar antes de planejar uma nova rodada.',
|
||||||
@@ -340,8 +315,8 @@ const SuppliesHub = () => {
|
|||||||
icon: ClipboardList,
|
icon: ClipboardList,
|
||||||
to: '/production-orders',
|
to: '/production-orders',
|
||||||
tone: 'ready' as const,
|
tone: 'ready' as const,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
label: 'Próxima ação',
|
label: 'Próxima ação',
|
||||||
title: 'Definir o próximo corte',
|
title: 'Definir o próximo corte',
|
||||||
description: 'Use a demanda e a cobertura para escolher os SKUs que devem entrar em produção.',
|
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,
|
icon: Scissors,
|
||||||
to: '/cutting',
|
to: '/cutting',
|
||||||
tone: 'default' as const,
|
tone: 'default' as const,
|
||||||
};
|
};
|
||||||
const NextActionIcon = nextAction.icon;
|
const NextActionIcon = nextAction.icon;
|
||||||
|
|
||||||
const handleExportDiagnostic = async () => {
|
const handleExportDiagnostic = async () => {
|
||||||
@@ -404,13 +379,13 @@ const SuppliesHub = () => {
|
|||||||
</RouterLink>
|
</RouterLink>
|
||||||
<div className={`${panelClassName} p-5`}>
|
<div className={`${panelClassName} p-5`}>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-base font-bold text-dark-text">Pontos que bloqueiam o fluxo</h2>
|
<h2 className="text-base font-bold text-dark-text">Situação operacional</h2>
|
||||||
<p className="mt-1 text-sm font-semibold text-dark-muted">Resolva estes pontos antes de confiar no plano.</p>
|
<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>
|
||||||
<div className="mt-4 space-y-2">
|
<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="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="Dados para mapear" description="SKUs sem composição confiável" count={mappingSkuCount} to="/supplies/purchase-needs?tab=mapping" tone="info" />
|
|
||||||
<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="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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
<p className="mt-1 text-sm font-semibold text-dark-muted">Da necessidade de material até a produção acompanhada.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 gap-4 xl:grid-cols-3">
|
<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="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={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="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'} />
|
<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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user