From 5e742feb607ba6528af1ff8b18e5ba84f6175667 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Faleiros?= Date: Thu, 27 Aug 2026 20:30:15 -0300 Subject: [PATCH] Streamline production order workflow --- src/pages/ProductionOrders.tsx | 100 ++++++++++++++++++--------------- 1 file changed, 56 insertions(+), 44 deletions(-) diff --git a/src/pages/ProductionOrders.tsx b/src/pages/ProductionOrders.tsx index e27a31d..6a11154 100644 --- a/src/pages/ProductionOrders.tsx +++ b/src/pages/ProductionOrders.tsx @@ -1,6 +1,6 @@ -import { Fragment, type FormEvent, useCallback, useEffect, useMemo, useState } from 'react'; +import { type FormEvent, useCallback, useEffect, useMemo, useState } from 'react'; import { Link, useNavigate, useOutletContext } from 'react-router-dom'; -import { AlertTriangle, ArrowLeft, CheckCircle2, ChevronDown, ChevronUp, ClipboardList, Clock3, Download, PackageCheck, PlayCircle, Printer, Scissors, Search, Undo2, XCircle } from 'lucide-react'; +import { AlertTriangle, ArrowLeft, CheckCircle2, ChevronDown, ChevronUp, ClipboardList, Clock3, Download, PackageCheck, PlayCircle, Printer, Scissors, Search, Undo2, X, XCircle } from 'lucide-react'; import DateRangePicker from '../components/DateRangePicker'; import PaginationControls from '../components/PaginationControls'; import RefreshStatus from '../components/RefreshStatus'; @@ -88,6 +88,16 @@ const getStatusStyle = (status: ProductionOrderStatus, fallbackLabel: string) => } ); +const getNextStage = (status: ProductionOrderStatus): { status: Exclude; label: string; description: string } | null => { + switch (status) { + case 'open': return { status: 'cutting', label: 'Iniciar corte', description: 'Libera o apontamento de corte e a baixa física de materiais.' }; + case 'cutting': return { status: 'sewing', label: 'Avançar para costura', description: 'Use quando o corte desta OP estiver concluído.' }; + case 'sewing': return { status: 'review', label: 'Enviar para revisão', description: 'Use quando as peças estiverem prontas para a conferência final.' }; + case 'review': return { status: 'finished', label: 'Finalizar OP', description: 'Confirme a quantidade aprovada antes de encerrar a ordem.' }; + default: return null; + } +}; + const ProductionOrdersSkeleton = () => (
@@ -216,8 +226,10 @@ const ProductionOrders = () => { const openCount = summary.counts.open || 0; const progressCount = (summary.counts.cutting || 0) + (summary.counts.sewing || 0) + (summary.counts.review || 0); const finishedCount = summary.counts.finished || 0; - const totalQuantity = summary.orders.reduce((total, order) => total + order.quantity, 0); + const finishedQuantity = summary.orders.filter(order => order.status === 'finished').reduce((total, order) => total + (order.finishedQuantity ?? order.quantity), 0); const selectedOrder = summary.orders.find(order => order.id === selectedOrderId) || null; + const selectedOrderNextStage = selectedOrder ? getNextStage(selectedOrder.status) : null; + const selectedOrderCanReturnToCutPlan = Boolean(selectedOrder && selectedOrder.status === 'open' && (selectedOrder.integrationStatus || '').toLowerCase() === 'local'); const handleManualRefresh = () => { void loadProductionOrders({ force: true }); @@ -379,18 +391,10 @@ const ProductionOrders = () => { }; const renderOrderDetails = (order: ProductionOrderItem) => { - const canReturnToCutPlan = order.status === 'open' && (order.integrationStatus || '').toLowerCase() === 'local'; + const canRecordOperationalData = ['cutting', 'sewing', 'review'].includes(order.status); return ( -
-
-
-

Detalhes da OP

-

{order.number || `OP #${order.id}`} · {order.productDescription}

-
- {canReturnToCutPlan && } -
- +

Quantidade

{formatQuantity(order.quantity)} {order.unit}

Status

{statusEditorOrderId === order.id &&

Alterar status

{editableStatusOptions.map(option => { const style = getStatusStyle(option.value, option.label); const isCurrent = order.status === option.value; const Icon = option.value === 'finished' ? CheckCircle2 : option.value === 'canceled' ? XCircle : option.value === 'cutting' ? Scissors : option.value === 'sewing' ? PlayCircle : option.value === 'review' ? ClipboardList : Clock3; return ; })}
}
@@ -398,6 +402,8 @@ const ProductionOrders = () => {

Previsão

{formatDate(order.expectedDate)}

+ {order.status === 'finished' &&

Resultado registrado

Cortado

{formatQuantity(order.actualCutQuantity ?? 0)} {order.unit}

Perdas

{formatQuantity(order.rejectedQuantity || 0)} {order.unit}

Aprovado

{formatQuantity(order.finishedQuantity ?? 0)} {order.unit}

{(order.markerReference || order.qualityNotes) &&
{order.markerReference &&

Referência do risco: {order.markerReference}

}{order.qualityNotes &&

{order.qualityNotes}

}
}
} +

Produto e marcadores

@@ -407,14 +413,8 @@ const ProductionOrders = () => {
-
-
-
-

Execução e qualidade

-

Registre o corte real, perdas e a quantidade aprovada antes de finalizar.

-
- -
+ {canRecordOperationalData &&
+

Execução e qualidade

Registre o corte real, perdas e a quantidade aprovada antes de avançar a OP.

void handleExecutionSave(event, order)} className="mt-3 rounded-xl border border-dark-border bg-dark-card p-4">
@@ -426,14 +426,14 @@ const ProductionOrders = () => {

Planejado: {formatQuantity(order.quantity)} {order.unit}

{executionMessage &&

{executionMessage}

} -
+
} -
+ {(canRecordOperationalData || order.defects.length > 0) &&

Defeitos e origem

Registre cada ocorrência para acompanhar perdas por posto, lote e tipo de defeito.

-
void handleDefectSave(event, order)} className="mt-3 rounded-xl border border-dark-border bg-dark-card p-4"> + {canRecordOperationalData && void handleDefectSave(event, order)} className="mt-3 rounded-xl border border-dark-border bg-dark-card p-4">
@@ -444,10 +444,10 @@ const ProductionOrders = () => {

{workstations.length ? `${workstations.length} postos ativos disponíveis.` : 'Nenhum posto de trabalho ativo cadastrado.'}

{defectMessage &&

{defectMessage}

} -
+ } {order.defects.length > 0 &&
OcorrênciaQuantidade
{order.defects.map(defect =>

{defect.defectType}

{[defect.workstationName || 'Sem posto', defect.origin, defect.lotCode ? `Lote ${defect.lotCode}` : ''].filter(Boolean).join(' · ')}

{defect.notes &&

{defect.notes}

}

{formatQuantity(defect.quantity)} {order.unit}

)}
} - {!order.defects.length &&

Nenhum defeito registrado nesta OP.

} -
+ {!order.defects.length && canRecordOperationalData &&

Nenhum defeito registrado nesta OP.

} +
}

Composição

@@ -459,10 +459,10 @@ const ProductionOrders = () => {
{order.lotReservations.map(reservation => )}
LoteMaterialReservadoConsumido

#{reservation.lotId}

{reservation.lotSupplier || 'Sem fornecedor'}

{reservation.lotProduct}

{reservation.materialKey}

{formatQuantity(reservation.quantity)} {reservation.lotUnit}{formatQuantity(reservation.consumedQuantity)} {reservation.lotUnit}
} -
+ {order.status === 'cutting' &&

Baixa de material

{order.status === 'cutting' ?

Registre uma saída somente quando o material for realmente consumido pela OP.

{selectedLot &&

Saldo do lote #{selectedLot.id}: {formatQuantity(selectedLot.quantity)} {selectedLot.unit}

}{supplyMessage &&

{supplyMessage}

}
:
Altere a OP para Em corte antes de registrar a baixa de materiais.
} -
+
} {(order.steps.length > 0 || order.notes || order.supplier || order.lotCode || order.rollQuantity || order.fabricKg || order.ribKg || order.yieldPiecesPerKg) &&

Informações adicionais

{(order.supplier || order.lotCode || order.rollQuantity || order.fabricKg || order.ribKg || order.yieldPiecesPerKg) &&
{order.supplier &&
Fornecedor
{order.supplier}
}{order.lotCode &&
Lote
{order.lotCode}
}{order.rollQuantity !== null &&
Rolos
{formatQuantity(order.rollQuantity)}
}{order.fabricKg !== null &&
Malha
{formatQuantity(order.fabricKg)} kg
}{order.ribKg !== null &&
Ribana
{formatQuantity(order.ribKg)} kg
}{order.yieldPiecesPerKg !== null &&
Rendimento
{formatQuantity(order.yieldPiecesPerKg)} un./kg
}
}{order.steps.length > 0 &&

Etapas

{order.steps.map(step =>
{step.name}{formatDate(step.startDate)} - {formatDate(step.endDate)}
)}
}{order.notes &&

{order.notes}

}
}
@@ -478,7 +478,7 @@ const ProductionOrders = () => { Suprimentos

Ordens de Produção

-

Acompanhe as ordens locais geradas pelo plano de corte.

+

Acompanhe a fila de produção e o histórico das ordens.

Ver planos de produçãoVer qualidade
{

Finalizadas

{finishedCount}

-

{formatQuantity(totalQuantity)} un. no período

+

{formatQuantity(finishedQuantity)} un. aprovadas

@@ -658,8 +658,7 @@ const ProductionOrders = () => { {paginatedOrders.map((order: ProductionOrderItem) => { const statusStyle = getStatusStyle(order.status, order.statusLabel); return ( - - + {order.number || '-'}
{order.productDescription}
@@ -683,22 +682,20 @@ const ProductionOrders = () => { - {selectedOrderId === order.id && {renderOrderDetails(order)}} -
); })} @@ -717,7 +714,7 @@ const ProductionOrders = () => {
)} - 1 && { setItemsPerPage(pageSize); setCurrentPage(1); }} - /> + />}
)}
)} + {selectedOrder &&
busyStatusOrderId !== selectedOrder.id && setSelectedOrderId(null)}> +
event.stopPropagation()}> +
+
{selectedOrder.number || `OP #${selectedOrder.id}`}{getStatusStyle(selectedOrder.status, selectedOrder.statusLabel).label}

{selectedOrder.productDescription}

{selectedOrder.productSku || 'Sem SKU'}{selectedOrder.productionPlanNumber && {selectedOrder.productionPlanNumber}}Previsão: {formatDate(selectedOrder.expectedDate)}
+
+
+
+ {statusMessage &&
{statusMessage}
} + {selectedOrderNextStage ?

Próxima ação

{selectedOrderNextStage.label}

{selectedOrderNextStage.description}

:
{selectedOrder.status === 'finished' ? 'Esta OP foi finalizada. Os registros abaixo permanecem disponíveis para consulta.' : 'Esta OP foi cancelada e não possui próxima etapa operacional.'}
} +
{renderOrderDetails(selectedOrder)}
+
+ +
+
} + ); };