diff --git a/src/pages/Cutting.tsx b/src/pages/Cutting.tsx index dea5353..6080651 100644 --- a/src/pages/Cutting.tsx +++ b/src/pages/Cutting.tsx @@ -1,6 +1,6 @@ import { useEffect, useMemo, useState } from 'react'; import { Link, useOutletContext, useSearchParams } from 'react-router-dom'; -import { AlertTriangle, ArrowLeft, ClipboardList, Download, Eye, Layers3, Palette, Pencil, RotateCcw, Ruler, Save as SaveIcon, Scissors, Search, Settings2, X } from 'lucide-react'; +import { AlertTriangle, ArrowLeft, ClipboardList, Download, Eye, Layers3, Pencil, RotateCcw, Ruler, Save as SaveIcon, Scissors, Search, Settings2, X } from 'lucide-react'; import DateRangePicker from '../components/DateRangePicker'; import PaginationControls from '../components/PaginationControls'; import ProductColorBadge from '../components/ProductColorBadge'; @@ -327,6 +327,24 @@ const Cutting = () => { filteredRows.filter(row => row.suggestedCutQuantity > 0 && materialReadinessByProductId.get(row.id)?.status === 'ready') ), [filteredRows, materialReadinessByProductId]); + const cutExecutionSummary = useMemo(() => { + const rowsWithNeed = cutPlan.needRows.filter(row => row.suggestedCutQuantity > 0); + const readyRows = rowsWithNeed.filter(row => materialReadinessByProductId.get(row.id)?.status === 'ready'); + const blockedByMaterial = rowsWithNeed.filter(row => { + const status = materialReadinessByProductId.get(row.id)?.status; + return status === 'blocked' || status === 'missing'; + }); + const blockedByData = rowsWithNeed.filter(row => materialReadinessByProductId.get(row.id)?.status === 'material_ready'); + return { + rawUnits: rowsWithNeed.reduce((total, row) => total + row.suggestedCutQuantity, 0), + readyUnits: readyRows.reduce((total, row) => total + row.suggestedCutQuantity, 0), + readyRows: readyRows.length, + blockedRows: blockedByMaterial.length + blockedByData.length, + blockedByMaterial: blockedByMaterial.length, + blockedByData: blockedByData.length, + }; + }, [cutPlan.needRows, materialReadinessByProductId]); + const orderPreviewTotalUnits = useMemo(() => ( rowsAvailableForOrder.reduce((total, row) => total + row.suggestedCutQuantity, 0) ), [rowsAvailableForOrder]); @@ -1007,8 +1025,8 @@ const Cutting = () => {
-

Unidades a cortar

-

{formatNumber(cutPlan.summary.suggestedCutQuantity)}

+

Necessidade bruta

+

{formatNumber(cutExecutionSummary.rawUnits)}

{formatNumber(cutPlan.summary.skuCount)} SKUs com necessidade

@@ -1020,11 +1038,9 @@ const Cutting = () => {
-

Famílias

-

{formatNumber(cutPlan.summary.familiesWithNeed)}

-

- {cutPlan.summary.estimatedRolls === null ? 'Rolos pendentes de rendimento' : `${formatNumber(cutPlan.summary.estimatedRolls)} rolos estimados`} -

+

Pronto para corte

+

{formatNumber(cutExecutionSummary.readyUnits)}

+

{formatNumber(cutExecutionSummary.readyRows)} SKUs liberados para gerar OP

@@ -1035,14 +1051,12 @@ const Cutting = () => {
-

Cores / tamanhos

-

- {formatNumber(cutPlan.summary.colorsWithNeed)} / {formatNumber(cutPlan.summary.sizesWithNeed)} -

-

Com corte sugerido

+

Bloqueios

+

{formatNumber(cutExecutionSummary.blockedRows)}

+

{formatNumber(cutExecutionSummary.blockedByMaterial)} material · {formatNumber(cutExecutionSummary.blockedByData)} dados

-
- +
+
@@ -1050,17 +1064,27 @@ const Cutting = () => {
-

OP aberta

-

{formatNumber(cutPlan.summary.openProductionQuantity)}

-

Unidades abatidas quando há match

+

Famílias

+

{formatNumber(cutPlan.summary.familiesWithNeed)}

+

{cutPlan.summary.estimatedRolls === null ? 'Rendimento ainda não configurado' : `${formatNumber(cutPlan.summary.estimatedRolls)} rolos estimados`}

-
- +
+
+ {cutExecutionSummary.rawUnits > 0 && cutExecutionSummary.readyUnits === 0 && ( +
+
+

Há demanda, mas nenhum SKU está liberado para corte.

+

Corrija materiais bloqueados e rendimentos antes de gerar ordens de produção.

+
+ +
+ )} +
+ {activeOrders.length > 0 && (
@@ -373,7 +375,7 @@ const ProductionOrders = () => { className="mt-1 h-10 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-semibold text-dark-text outline-none focus:border-brand-primary" > - {summary.orders.filter(order => order.status !== 'finished' && order.status !== 'canceled').map(order => ( + {activeOrders.map(order => ( @@ -429,6 +431,7 @@ const ProductionOrders = () => {
)} + )}
@@ -655,6 +658,13 @@ const ProductionOrders = () => {

Gere ordens pelo Plano de Corte para acompanhar status, produto, quantidade e baixa de material dentro do Graphs.

+ + + Ir para Plano de Corte +
)} diff --git a/src/pages/Supplies.tsx b/src/pages/Supplies.tsx index f85a024..a375060 100644 --- a/src/pages/Supplies.tsx +++ b/src/pages/Supplies.tsx @@ -1838,6 +1838,14 @@ const getNeedPendingLabel = (need: SupplyPurchaseNeed) => { return /^Cadastrar rendimento:/i.test(need.material) ? 'Rendimento' : 'Referência'; }; +const hasNegativeMaterialBalance = (need: SupplyPurchaseNeed) => need.stockKg < 0; + +const getNeedStatusLabel = (need: SupplyPurchaseNeed) => { + if (hasNegativeMaterialBalance(need)) return 'Saldo negativo'; + if (need.missingReference) return 'Sem referência'; + return purchaseStatusLabels[need.status]; +}; + type PurchaseNeedTab = 'materials' | 'services' | 'mapping'; const sumNeedProducts = ( @@ -1887,7 +1895,13 @@ const PurchaseNeedsScreen = () => { const materialNeeds = summary.purchaseNeeds.filter(need => !need.missingReference && !isServicePurchaseNeed(need)); const serviceNeeds = summary.purchaseNeeds.filter(need => !need.missingReference && isServicePurchaseNeed(need)); const mappingNeeds = summary.purchaseNeeds.filter(need => need.missingReference); - const activeNeeds = activeTab === 'materials' ? materialNeeds : activeTab === 'services' ? serviceNeeds : mappingNeeds; + const activeNeeds = activeTab === 'materials' + ? [...materialNeeds].sort((a, b) => { + if (hasNegativeMaterialBalance(a) !== hasNegativeMaterialBalance(b)) return hasNegativeMaterialBalance(a) ? -1 : 1; + if (a.status !== b.status) return a.status === 'critical' ? -1 : b.status === 'critical' ? 1 : 0; + return b.purchaseKg - a.purchaseKg; + }) + : activeTab === 'services' ? serviceNeeds : mappingNeeds; const normalizedSearch = normalizeSearch(search); const visibleNeeds = activeNeeds.filter(need => ( !normalizedSearch || normalizeSearch(`${need.material} ${getNeedDisplayName(need)} ${need.suppliers.join(' ')} ${need.colors.join(' ')} ${(need.products || []).map(product => `${product.productId} ${product.name}`).join(' ')}`).includes(normalizedSearch) @@ -1908,7 +1922,7 @@ const PurchaseNeedsScreen = () => { ? 'Necessidades de beneficiamento e serviços. Não entram no total de compra de materiais.' : 'SKUs sem consumo confiável. Corrija o cadastro antes de usar estes valores para comprar.'; const stats = [ - { label: 'Materiais a comprar', value: `${purchaseItemCount}` }, + { label: 'Itens para comprar', value: `${purchaseItemCount} materiais` }, { label: 'Compra sugerida', value: suggestedPurchaseSummary }, { label: 'Serviços a revisar', value: `${serviceNeeds.filter(need => need.purchaseKg > 0).length}` }, { label: 'SKUs sem mapeamento', value: `${impactedSkuCount || mappingNeeds.length}` }, @@ -1969,9 +1983,9 @@ const PurchaseNeedsScreen = () => {
{[ - { id: 'materials' as const, label: `Materiais (${materialNeeds.length})` }, + { id: 'materials' as const, label: `Materiais (${materialNeeds.length} linhas)` }, { id: 'services' as const, label: `Serviços (${serviceNeeds.length})` }, - { id: 'mapping' as const, label: `Mapear dados (${mappingNeeds.length})` }, + { id: 'mapping' as const, label: `Mapear dados (${impactedSkuCount || mappingNeeds.length} SKUs)` }, ].map(tab => (
{formatNeedQuantity(need, need.plannedKg)} - {activeTab === 'services' ? '—' : formatNeedQuantity(need, need.stockKg)} + {activeTab === 'services' ? '—' : formatNeedQuantity(need, need.stockKg)} {activeTab === 'services' ? '—' : formatNeedQuantity(need, need.pendingKg)} 0 ? 'text-amber-300' : 'text-emerald-300'}`}>{formatNeedQuantity(need, need.purchaseKg)} { ? 'border-amber-400/30 bg-amber-400/10 text-amber-300' : 'border-emerald-400/30 bg-emerald-400/10 text-emerald-300' }`}> - {activeTab === 'services' ? 'Programar serviço' : need.missingReference ? 'Sem referência' : purchaseStatusLabels[need.status]} + {activeTab === 'services' ? 'Programar serviço' : getNeedStatusLabel(need)}