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, X, XCircle } from 'lucide-react'; import DateRangePicker from '../components/DateRangePicker'; import PaginationControls from '../components/PaginationControls'; import RefreshStatus from '../components/RefreshStatus'; import { addProductionOrderDefect, consumeSupplyLotForProduction, exportToCSV, fetchProductionOrders, fetchProductionWorkstations, fetchSupplySummary, updateProductionOrderExecution, updateProductionOrderStatus } from '../dataService'; import type { DateRange, ProductionOrderItem, ProductionOrderStatus, ProductionOrderSummary, ProductionWorkstation, SupplyLot } from '../types'; type ProductionOrderStatusTab = 'all' | 'open' | 'cutting' | 'sewing' | 'review' | 'finished' | 'canceled'; const emptySummary: ProductionOrderSummary = { orders: [], counts: { all: 0, open: 0, cutting: 0, sewing: 0, review: 0, finished: 0, canceled: 0 } }; const statusTabs: Array<{ key: ProductionOrderStatusTab; label: string; dotClass: string }> = [ { key: 'all', label: 'Todas', dotClass: 'bg-dark-muted' }, { key: 'open', label: 'Em aberto', dotClass: 'bg-amber-400' }, { key: 'cutting', label: 'Em corte', dotClass: 'bg-amber-300' }, { key: 'sewing', label: 'Em costura', dotClass: 'bg-sky-400' }, { key: 'review', label: 'Em revisão', dotClass: 'bg-violet-400' }, { key: 'finished', label: 'Finalizada', dotClass: 'bg-emerald-400' }, { key: 'canceled', label: 'Cancelada', dotClass: 'bg-zinc-500' } ]; const editableStatusOptions: Array<{ value: Exclude; label: string; description: string }> = [ { value: 'open', label: 'Em aberto', description: 'Aguardando o início da produção.' }, { value: 'cutting', label: 'Em corte', description: 'Os materiais estão sendo transformados em peças cortadas.' }, { value: 'sewing', label: 'Em costura', description: 'As peças cortadas estão em montagem.' }, { value: 'review', label: 'Em revisão', description: 'A qualidade e as perdas estão sendo conferidas.' }, { 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: { label: 'Em aberto', className: 'border-amber-400/35 bg-amber-400/10 text-amber-300', dotClass: 'bg-amber-400' }, cutting: { label: 'Em corte', className: 'border-amber-300/35 bg-amber-300/10 text-amber-200', dotClass: 'bg-amber-300' }, sewing: { label: 'Em costura', className: 'border-sky-400/35 bg-sky-400/10 text-sky-300', dotClass: 'bg-sky-400' }, review: { label: 'Em revisão', className: 'border-violet-400/35 bg-violet-400/10 text-violet-300', dotClass: 'bg-violet-400' }, finished: { label: 'Finalizada', className: 'border-emerald-400/35 bg-emerald-400/10 text-emerald-300', dotClass: 'bg-emerald-400' }, canceled: { label: 'Cancelada', className: 'border-zinc-500/35 bg-zinc-500/10 text-zinc-400', dotClass: 'bg-zinc-500' } }; const formatDate = (date: string | null) => { if (!date) return '-'; const parsedDate = new Date(`${date}T00:00:00`); if (Number.isNaN(parsedDate.getTime())) return date; return new Intl.DateTimeFormat('pt-BR').format(parsedDate); }; const formatQuantity = (value: number) => ( new Intl.NumberFormat('pt-BR', { minimumFractionDigits: Number.isInteger(value) ? 0 : 2, maximumFractionDigits: 4 }).format(value) ); const getStatusStyle = (status: ProductionOrderStatus, fallbackLabel: string) => ( statusStyles[String(status)] || { label: fallbackLabel || 'Em aberto', className: 'border-dark-border bg-dark-input text-dark-muted', dotClass: 'bg-dark-muted' } ); 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 = () => (
{[0, 1, 2, 3].map(item => (
))}
{[0, 1, 2, 3, 4].map(item => (
))}
{[0, 1, 2, 3, 4, 5, 6, 7].map(row => (
{[0, 1, 2, 3, 4, 5, 6].map(column => (
))}
))}
); const ProductionOrders = () => { const { dateRange, setDateRange, refreshInterval, setRefreshInterval } = useOutletContext<{ dateRange: DateRange; setDateRange: (range: DateRange) => void; refreshInterval: number; setRefreshInterval: (interval: number) => void; }>(); const navigate = useNavigate(); const [searchTerm, setSearchTerm] = useState(''); const [statusFilter, setStatusFilter] = useState('all'); const [summary, setSummary] = useState(emptySummary); const [supplyLots, setSupplyLots] = useState([]); const [workstations, setWorkstations] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isSupplyBusy, setIsSupplyBusy] = useState(false); const [busyExecutionOrderId, setBusyExecutionOrderId] = useState(null); const [supplyMessage, setSupplyMessage] = useState(''); const [executionMessage, setExecutionMessage] = useState(''); const [busyDefectOrderId, setBusyDefectOrderId] = useState(null); const [defectMessage, setDefectMessage] = 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({ lotId: '', quantity: '', reason: '', }); const loadProductionOrders = useCallback(async (options?: { force?: boolean }) => { setIsLoading(true); const nextSummary = await fetchProductionOrders(dateRange, { search: searchTerm }, options); setSummary(nextSummary); setIsLoading(false); }, [dateRange, searchTerm]); useEffect(() => { let isMounted = true; const load = async () => { setIsLoading(true); const [nextSummary, nextSupplySummary, nextWorkstations] = await Promise.all([ fetchProductionOrders(dateRange, { search: searchTerm }), fetchSupplySummary(), fetchProductionWorkstations() ]); if (isMounted) { setSummary(nextSummary); setSupplyLots(nextSupplySummary.lots); setWorkstations(nextWorkstations); setIsLoading(false); } }; void load(); return () => { isMounted = false; }; }, [dateRange, searchTerm]); useEffect(() => { if (refreshInterval === 0) return undefined; const intervalId = setInterval(() => { void loadProductionOrders({ force: true }); }, refreshInterval); return () => clearInterval(intervalId); }, [loadProductionOrders, refreshInterval]); const filteredOrders = useMemo(() => { if (statusFilter === 'all') return summary.orders; return summary.orders.filter(order => order.status === statusFilter); }, [statusFilter, summary.orders]); const totalPages = Math.ceil(filteredOrders.length / itemsPerPage); const safeCurrentPage = Math.min(currentPage, totalPages || 1); const startIndex = (safeCurrentPage - 1) * itemsPerPage; const paginatedOrders = filteredOrders.slice(startIndex, startIndex + itemsPerPage); const isRefreshing = isLoading && summary.orders.length > 0; 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 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 }); }; const handleStatusChange = async (order: ProductionOrderItem, status: ProductionOrderStatusTab) => { if (status === 'all' || status === order.status) return; setBusyStatusOrderId(order.id); setStatusMessage(''); try { await updateProductionOrderStatus(order.id, status); const nextSummary = await fetchProductionOrders(dateRange, { search: searchTerm }, { force: true }); setSummary(nextSummary); setStatusMessage(`OP ${order.number || `#${order.id}`} atualizada para ${statusStyles[status]?.label || status}.`); } catch (error) { setStatusMessage(error instanceof Error ? error.message : 'Não foi possível atualizar o status da OP.'); } finally { setBusyStatusOrderId(null); setStatusEditorOrderId(null); } }; const returnOrderToCutPlan = async (order: ProductionOrderItem) => { if (order.status !== 'open' || (order.integrationStatus || '').toLowerCase() !== 'local') return; const orderNumber = order.number || `#${order.id}`; if (!window.confirm(`Devolver a OP ${orderNumber} ao Plano de Corte?\n\nA OP será cancelada, preservando o histórico. A necessidade voltará para o Plano de Corte e poderá ser gerada novamente quando você decidir.`)) return; setBusyStatusOrderId(order.id); setStatusMessage(''); try { await updateProductionOrderStatus(order.id, 'canceled'); setSelectedOrderId(null); setStatusMessage(`OP ${orderNumber} devolvida ao Plano de Corte.`); navigate('/supplies/cutting'); } catch (error) { setStatusMessage(error instanceof Error ? error.message : 'Não foi possível devolver a OP ao Plano de Corte.'); } finally { setBusyStatusOrderId(null); } }; const refreshSupplyLots = async () => { const supplySummary = await fetchSupplySummary(); setSupplyLots(supplySummary.lots); }; const selectedLot = supplyLots.find(lot => `${lot.id}` === exitForm.lotId); const handleProductionExit = async (event: FormEvent) => { event.preventDefault(); const lotId = Number(exitForm.lotId); const quantity = Number(exitForm.quantity.replace(',', '.')); if (!lotId || !selectedOrder || !Number.isFinite(quantity) || quantity <= 0) return; setIsSupplyBusy(true); setSupplyMessage(''); try { await consumeSupplyLotForProduction(lotId, { quantity, productionOrderNumber: selectedOrder.number || `${selectedOrder.id}`, reason: exitForm.reason.trim() || selectedOrder.productDescription, }); await refreshSupplyLots(); 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.'); } finally { setIsSupplyBusy(false); } }; const handleExecutionSave = async (event: FormEvent, order: ProductionOrderItem) => { event.preventDefault(); const form = new FormData(event.currentTarget); const parseOptionalQuantity = (name: string) => { const raw = String(form.get(name) || '').trim(); return raw ? Number(raw.replace(',', '.')) : null; }; setBusyExecutionOrderId(order.id); setExecutionMessage(''); try { await updateProductionOrderExecution(order.id, { actualCutQuantity: parseOptionalQuantity('actualCutQuantity'), rejectedQuantity: parseOptionalQuantity('rejectedQuantity'), finishedQuantity: parseOptionalQuantity('finishedQuantity'), qualityNotes: String(form.get('qualityNotes') || '').trim(), markerReference: String(form.get('markerReference') || '').trim(), }); const nextSummary = await fetchProductionOrders(dateRange, { search: searchTerm }, { force: true }); setSummary(nextSummary); setExecutionMessage(`Execução da OP ${order.number || `#${order.id}`} registrada.`); } catch (error) { setExecutionMessage(error instanceof Error ? error.message : 'Não foi possível registrar a execução.'); } finally { setBusyExecutionOrderId(null); } }; const handleDefectSave = async (event: FormEvent, order: ProductionOrderItem) => { event.preventDefault(); const form = new FormData(event.currentTarget); const quantity = Number(String(form.get('quantity') || '').replace(',', '.')); const workstationId = Number(form.get('workstationId')); if (!workstationId || !Number.isFinite(quantity) || quantity <= 0) return; setBusyDefectOrderId(order.id); setDefectMessage(''); try { await addProductionOrderDefect(order.id, { workstationId, defectType: String(form.get('defectType') || '').trim(), origin: String(form.get('origin') || '').trim(), quantity, lotCode: String(form.get('lotCode') || '').trim(), notes: String(form.get('notes') || '').trim(), }); event.currentTarget.reset(); const nextSummary = await fetchProductionOrders(dateRange, { search: searchTerm }, { force: true }); setSummary(nextSummary); setDefectMessage(`Defeito registrado na OP ${order.number || `#${order.id}`}.`); } catch (error) { setDefectMessage(error instanceof Error ? error.message : 'Não foi possível registrar o defeito.'); } finally { setBusyDefectOrderId(null); } }; const printProductionSheet = (order: ProductionOrderItem) => { const printWindow = window.open('', '_blank', 'noopener,noreferrer'); if (!printWindow) { setStatusMessage('Permita pop-ups para imprimir a ficha de produção.'); return; } const componentRows = order.components.map(component => `${component.componentName}${formatQuantity(component.totalQuantity)} ${component.unit}`).join('') || 'Composição não sincronizada.'; printWindow.document.write(`Ficha ${order.number || order.id}

Ficha de Produção · ${order.number || `OP #${order.id}`}

${order.productDescription}

SKU${order.productSku || '-'}
Quantidade planejada${formatQuantity(order.quantity)} ${order.unit}
Status${order.statusLabel}
Referência${order.orderReference || '-'}

Composição prevista

${componentRows}
MaterialQuantidade

Apontamento de execução

Quantidade cortada
Perdas / defeitos
Quantidade aprovada
Responsável

Observações

`); printWindow.document.close(); printWindow.focus(); printWindow.print(); }; const handleExport = () => { const exportData = filteredOrders.map(order => ({ 'Numero': order.number, 'Pedidos': order.orderReference, 'Status': order.statusLabel, 'Data': formatDate(order.issueDate), 'Data Prevista': formatDate(order.expectedDate), 'SKU': order.productSku, 'Descricao': order.productDescription, 'Quantidade': formatQuantity(order.quantity), 'Unidade': order.unit, 'Marcadores': order.markers.map(marker => marker.label).join('; '), 'Integracao': order.integrationStatus })); exportToCSV(exportData, `ordens_producao_${new Date().toISOString().split('T')[0]}.csv`); }; const renderOrderDetails = (order: ProductionOrderItem) => { const canRecordOperationalData = ['cutting', 'sewing', 'review'].includes(order.status); return (

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 ; })}
}

Emissão

{formatDate(order.issueDate)}

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

{order.productSku || 'Sem SKU'}

{order.markers.length > 0 &&
{order.markers.map(marker => {marker.label})}
}
Pedido: {order.orderReference || '-'}Integração: {order.integrationStatus || 'Local'}
{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">