diff --git a/src/pages/ProductionOrders.tsx b/src/pages/ProductionOrders.tsx index 604c00e..254137d 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, useOutletContext } from 'react-router-dom'; -import { ArrowLeft, CalendarDays, CheckCircle2, ClipboardList, Clock3, Download, PackageCheck, Search } from 'lucide-react'; +import { ArrowLeft, CheckCircle2, ClipboardList, Clock3, Download, Eye, PackageCheck, PlayCircle, Search, X, XCircle } from 'lucide-react'; import DateRangePicker from '../components/DateRangePicker'; import PaginationControls from '../components/PaginationControls'; import RefreshStatus from '../components/RefreshStatus'; @@ -22,9 +22,12 @@ const statusTabs: Array<{ key: ProductionOrderStatusTab; label: string; dotClass { key: 'canceled', label: 'Cancelada', dotClass: 'bg-zinc-500' } ]; -const editableStatusOptions: Array<{ value: ProductionOrderStatusTab; label: string }> = statusTabs - .filter(tab => tab.key !== 'all') - .map(tab => ({ value: tab.key, label: tab.label })); +const editableStatusOptions: Array<{ value: Exclude; label: string; description: string }> = [ + { value: 'open', label: 'Em aberto', description: 'Aguardando o início da produção.' }, + { value: 'in_progress', label: 'Em andamento', description: 'A produção já foi iniciada.' }, + { value: 'finished', label: 'Finalizada', description: 'A produção desta OP foi concluída.' }, + { value: 'canceled', label: 'Cancelada', description: 'Esta OP não seguirá para produção.' } +]; const statusStyles: Record = { open: { @@ -63,16 +66,6 @@ const formatQuantity = (value: number) => ( }).format(value) ); -const formatOptionalQuantity = (value: number | null) => ( - value === null ? '-' : formatQuantity(value) -); - -const hasProductionOrderDetails = (order: ProductionOrderItem) => ( - order.components.length > 0 || - order.steps.length > 0 || - Boolean(order.notes || order.supplier || order.lotCode || order.rollQuantity || order.fabricKg || order.ribKg || order.yieldPiecesPerKg) -); - const getStatusStyle = (status: ProductionOrderStatus, fallbackLabel: string) => ( statusStyles[String(status)] || { label: fallbackLabel || 'Em aberto', @@ -137,10 +130,11 @@ const ProductionOrders = () => { const [supplyMessage, setSupplyMessage] = useState(''); const [busyStatusOrderId, setBusyStatusOrderId] = useState(null); const [statusMessage, setStatusMessage] = useState(''); + const [selectedOrderId, setSelectedOrderId] = useState(null); + const [statusEditorOrderId, setStatusEditorOrderId] = useState(null); const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(20); const [exitForm, setExitForm] = useState({ - orderId: '', lotId: '', quantity: '', reason: '', @@ -201,7 +195,8 @@ const ProductionOrders = () => { const progressCount = summary.counts.in_progress || 0; const finishedCount = summary.counts.finished || 0; const totalQuantity = summary.orders.reduce((total, order) => total + order.quantity, 0); - const activeOrders = summary.orders.filter(order => order.status !== 'finished' && order.status !== 'canceled'); + const selectedOrder = summary.orders.find(order => order.id === selectedOrderId) || null; + const statusEditorOrder = summary.orders.find(order => order.id === statusEditorOrderId) || null; const handleManualRefresh = () => { void loadProductionOrders({ force: true }); @@ -221,6 +216,7 @@ const ProductionOrders = () => { setStatusMessage(error instanceof Error ? error.message : 'Não foi possível atualizar o status da OP.'); } finally { setBusyStatusOrderId(null); + setStatusEditorOrderId(null); } }; @@ -229,7 +225,6 @@ const ProductionOrders = () => { setSupplyLots(supplySummary.lots); }; - const selectedOrder = summary.orders.find(order => `${order.id}` === exitForm.orderId); const selectedLot = supplyLots.find(lot => `${lot.id}` === exitForm.lotId); const handleProductionExit = async (event: FormEvent) => { @@ -247,7 +242,7 @@ const ProductionOrders = () => { reason: exitForm.reason.trim() || selectedOrder.productDescription, }); await refreshSupplyLots(); - setExitForm({ orderId: '', lotId: '', quantity: '', reason: '' }); + setExitForm({ lotId: '', quantity: '', reason: '' }); setSupplyMessage('Saída de material registrada no estoque.'); } catch (error) { setSupplyMessage(error instanceof Error ? error.message : 'Não foi possível baixar o material.'); @@ -384,86 +379,6 @@ const ProductionOrders = () => { ) : ( <> - {activeOrders.length > 0 && ( -
-
-
-

Baixa de material da OP

-

Consome um lote do estoque e registra a saída nas movimentações.

-
- {selectedLot && ( - - Saldo lote #{selectedLot.id}: {formatQuantity(selectedLot.quantity)} {selectedLot.unit} - - )} -
-
- - - - - -
- {supplyMessage && ( -
- {supplyMessage} -
- )} -
- )} -
{statusMessage && ( @@ -527,153 +442,61 @@ const ProductionOrders = () => {
- +
- - - - + - - + {paginatedOrders.map((order: ProductionOrderItem) => { const statusStyle = getStatusStyle(order.status, order.statusLabel); - const showDetails = hasProductionOrderDetails(order); return ( - - + - - - - - + - {showDetails && ( - - - - )} - ); })} @@ -713,6 +536,80 @@ const ProductionOrders = () => { )} )} + + {selectedOrder && ( +
+ +
+ +
+
+

Quantidade

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

+

Status

+

Emissão

{formatDate(selectedOrder.issueDate)}

+

Previsão

{formatDate(selectedOrder.expectedDate)}

+
+ +
+

Produto e marcadores

+
+

{selectedOrder.productSku || 'Sem SKU'}

+ {selectedOrder.markers.length > 0 &&
{selectedOrder.markers.map(marker => {marker.label})}
} +
Pedido: {selectedOrder.orderReference || '-'}Integração: {selectedOrder.integrationStatus || 'Local'}
+
+
+ +
+

Composição

+ {selectedOrder.components.length ? ( +
+
NúmeroPedidosDataData PrevistaSKU / ProdutoProduto QuantidadeMarcadoresIntegrações StatusAções
{order.number || '-'}{order.orderReference || '-'} - - - {formatDate(order.issueDate)} - - {formatDate(order.expectedDate)}
{order.productDescription}
-
{order.productSku || 'Sem SKU'}
+
+ {order.productSku || 'Sem SKU'} + {formatDate(order.issueDate)} + {order.orderReference && {order.orderReference}} +
{formatQuantity(order.quantity)} {order.unit} - {order.markers.length ? ( -
- {order.markers.map(marker => ( - - - {marker.label} - - ))} -
- ) : ( - - - )} -
- - - {order.integrationStatus || 'Tiny'} - - - - + + {statusStyle.label} + + +
-
-
-

Composição

- {order.components.length ? ( -
- - - - - - - - - - - {order.components.map(component => ( - - - - - - - ))} - -
ProdutoSKUQtd.Total
{component.componentName}{component.componentSku || '-'}{formatQuantity(component.quantityPerUnit)} {component.unit}{formatQuantity(component.totalQuantity)} {component.unit}
-
- ) : ( -

Sem composição sincronizada.

- )} -
- -
-

Etapas

- {order.steps.length ? ( -
- {order.steps.map(step => ( -
- {step.stepNumber ?? '-'} -
-

{step.name}

-

{formatDate(step.startDate)} - {formatDate(step.endDate)}

-
- -
- ))} -
- ) : ( -

Sem etapas sincronizadas.

- )} -
- -
-

Observações

-
-
Fornecedor
{order.supplier || '-'}
-
Lote
{order.lotCode || '-'}
-
Rolos
{formatOptionalQuantity(order.rollQuantity)}
-
Malha kg
{formatOptionalQuantity(order.fabricKg)}
-
Ribana kg
{formatOptionalQuantity(order.ribKg)}
-
Rendimento
{formatOptionalQuantity(order.yieldPiecesPerKg)}
-
- {order.notes &&

{order.notes}

} -
-
-
{selectedOrder.components.map(component => )}
MaterialPor unidadeTotal

{component.componentName}

{component.componentSku || 'Sem SKU'}

{formatQuantity(component.quantityPerUnit)} {component.unit}{formatQuantity(component.totalQuantity)} {component.unit}
+
+ ) :

Sem composição sincronizada.

} + + +
+

Baixa de material

+ {selectedOrder.status === 'in_progress' ? ( +
+

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 andamento antes de registrar a baixa de materiais.
} +
+ + {(selectedOrder.steps.length > 0 || selectedOrder.notes || selectedOrder.supplier || selectedOrder.lotCode || selectedOrder.rollQuantity || selectedOrder.fabricKg || selectedOrder.ribKg || selectedOrder.yieldPiecesPerKg) &&

Informações adicionais

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

Etapas

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

{selectedOrder.notes}

}
} +
+ + + )} + + {statusEditorOrder && ( +
+
+
{editableStatusOptions.map(option => { const style = getStatusStyle(option.value, option.label); const isCurrent = statusEditorOrder.status === option.value; const Icon = option.value === 'finished' ? CheckCircle2 : option.value === 'canceled' ? XCircle : option.value === 'in_progress' ? PlayCircle : Clock3; return ; })}
+ + + )} ); };