From 22dd4ca376cfa3e0857292ccbe98100d532d5e62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Faleiros?= Date: Tue, 4 Aug 2026 11:38:22 -0300 Subject: [PATCH] Make supplies hub action-driven --- src/pages/Supplies.tsx | 169 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 162 insertions(+), 7 deletions(-) diff --git a/src/pages/Supplies.tsx b/src/pages/Supplies.tsx index 4fff5d7..91cab03 100644 --- a/src/pages/Supplies.tsx +++ b/src/pages/Supplies.tsx @@ -201,6 +201,81 @@ const ModuleCard = ({ ); }; +const FlowStepCard = ({ + number, + title, + description, + meta, + icon: Icon, + to, + tone = 'default', +}: { + number: string; + title: string; + description: string; + meta: string; + icon: typeof Package; + to: string; + tone?: 'default' | 'critical' | 'attention' | 'ready'; +}) => { + const tones = { + default: 'border-dark-border bg-dark-card text-brand-primary', + critical: 'border-red-400/30 bg-red-500/[0.055] text-red-300', + attention: 'border-amber-400/30 bg-amber-500/[0.055] text-amber-300', + ready: 'border-emerald-400/30 bg-emerald-500/[0.055] text-emerald-300', + }; + + return ( + +
+ {number} +
+ +
+
+
+

{title}

+

{description}

+
+
+ {meta} + +
+
+ ); +}; + +const FlowBlocker = ({ + title, + description, + count, + to, + tone, +}: { + title: string; + description: string; + count: number; + to: string; + tone: 'critical' | 'attention' | 'info'; +}) => { + const tones = { + critical: 'border-red-400/25 bg-red-500/[0.045] text-red-300', + attention: 'border-amber-400/25 bg-amber-500/[0.045] text-amber-300', + info: 'border-sky-400/25 bg-sky-500/[0.045] text-sky-300', + }; + + return ( + + {formatNumber(count, 0)} + + {title} + {description} + + + + ); +}; + const SuppliesHub = () => { const [isExportingDiagnostic, setIsExportingDiagnostic] = useState(false); const [diagnosticError, setDiagnosticError] = useState(''); @@ -224,10 +299,63 @@ 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 + ? { + 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', + 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', + 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.', + meta: 'Ver lista de compra', + icon: BarChart3, + to: '/supplies/purchase-needs', + tone: 'attention' as const, + } + : 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.', + meta: 'Abrir ordens de produção', + 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.', + meta: 'Abrir plano de corte', + icon: Scissors, + to: '/cutting', + tone: 'default' as const, + }; + const NextActionIcon = nextAction.icon; const handleExportDiagnostic = async () => { setIsExportingDiagnostic(true); @@ -262,17 +390,44 @@ const SuppliesHub = () => { {diagnosticError} )} -
-
+
+ +
+
+

{nextAction.label}

+

{nextAction.title}

+

{nextAction.description}

+
+
+ +
+
+
+ {nextAction.meta} + +
+
+
-

Ações de hoje

-

Comprar materiais, definir o corte e acompanhar a produção.

+

Pontos que bloqueiam o fluxo

+

Resolva estes pontos antes de confiar no plano.

+
+
+ + +
+
+
+
+

Fluxo operacional

+

Da necessidade de material até a produção acompanhada.

+
- - - + 0 ? 'critical' : materialPurchaseNeeds.length > 0 ? 'attention' : 'ready'} meta={negativeMaterialNeeds.length > 0 ? `${formatNumber(negativeMaterialNeeds.length, 0)} saldos negativos` : materialPurchaseNeeds.length > 0 ? `${formatNumber(materialPurchaseNeeds.length, 0)} materiais para comprar` : 'Materiais cobertos'} /> + 0 ? 'attention' : 'default'} meta={mappingSkuCount > 0 ? `${formatNumber(mappingSkuCount, 0)} SKUs precisam de mapeamento` : 'Abrir plano de corte'} /> + 0 ? 'ready' : 'default'} meta={activeProductionCount > 0 ? `${formatNumber(productionCounts.open, 0)} abertas · ${formatNumber(productionCounts.inProgress, 0)} em andamento` : 'Nenhuma OP em execução'} />