import { Fragment, 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 DateRangePicker from '../components/DateRangePicker'; import PaginationControls from '../components/PaginationControls'; import RefreshStatus from '../components/RefreshStatus'; import { consumeSupplyLotForProduction, exportToCSV, fetchProductionOrders, fetchSupplySummary, updateProductionOrderStatus } from '../dataService'; import type { DateRange, ProductionOrderItem, ProductionOrderStatus, ProductionOrderSummary, SupplyLot } from '../types'; type ProductionOrderStatusTab = 'all' | 'open' | 'in_progress' | 'finished' | 'canceled'; const emptySummary: ProductionOrderSummary = { orders: [], counts: { all: 0, open: 0, in_progress: 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: 'in_progress', label: 'Em andamento', dotClass: 'bg-sky-400' }, { key: 'finished', label: 'Finalizada', dotClass: 'bg-emerald-400' }, { 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 statusStyles: Record = { open: { label: 'Em aberto', className: 'border-amber-400/35 bg-amber-400/10 text-amber-300', dotClass: 'bg-amber-400' }, in_progress: { label: 'Em andamento', className: 'border-sky-400/35 bg-sky-400/10 text-sky-300', dotClass: 'bg-sky-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 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', className: 'border-dark-border bg-dark-input text-dark-muted', dotClass: 'bg-dark-muted' } ); 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 [searchTerm, setSearchTerm] = useState(''); const [statusFilter, setStatusFilter] = useState('all'); const [summary, setSummary] = useState(emptySummary); const [supplyLots, setSupplyLots] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isSupplyBusy, setIsSupplyBusy] = useState(false); const [supplyMessage, setSupplyMessage] = useState(''); const [busyStatusOrderId, setBusyStatusOrderId] = useState(null); const [statusMessage, setStatusMessage] = useState(''); const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(20); const [exitForm, setExitForm] = useState({ orderId: '', 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] = await Promise.all([ fetchProductionOrders(dateRange, { search: searchTerm }), fetchSupplySummary() ]); if (isMounted) { setSummary(nextSummary); setSupplyLots(nextSupplySummary.lots); 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.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 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); } }; const refreshSupplyLots = async () => { const supplySummary = await fetchSupplySummary(); 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) => { 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({ orderId: '', 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 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`); }; return (
Suprimentos

Ordens de Produção

Acompanhe as ordens locais geradas pelo plano de corte.

{ setDateRange(range); setCurrentPage(1); }} refreshInterval={refreshInterval} setRefreshInterval={setRefreshInterval} onManualRefresh={handleManualRefresh} />
{isLoading && !summary.orders.length ? ( ) : (

Total no período

{summary.counts.all}

Ordens cadastradas

Em aberto

{openCount}

Aguardando produção

Em andamento

{progressCount}

Em processo

Finalizadas

{finishedCount}

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

{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 && (
{statusMessage}
)}
{ setSearchTerm(event.target.value); setCurrentPage(1); }} className="w-full rounded-xl border border-dark-border bg-dark-input py-2.5 pl-10 pr-4 text-sm font-semibold text-dark-text shadow-sm transition-colors placeholder:text-dark-muted focus:border-brand-primary focus:outline-none" />
{statusTabs.map(tab => { const isActive = statusFilter === tab.key; const count = summary.counts[tab.key] || 0; return ( ); })}
{paginatedOrders.map((order: ProductionOrderItem) => { const statusStyle = getStatusStyle(order.status, order.statusLabel); const showDetails = hasProductionOrderDetails(order); return ( {showDetails && ( )} ); })}
Número Pedidos Data Data Prevista SKU / Produto Quantidade Marcadores Integrações Status
{order.number || '-'} {order.orderReference || '-'} {formatDate(order.issueDate)} {formatDate(order.expectedDate)}
{order.productDescription}
{order.productSku || 'Sem SKU'}
{formatQuantity(order.quantity)} {order.unit} {order.markers.length ? (
{order.markers.map(marker => ( {marker.label} ))}
) : ( - )}
{order.integrationStatus || 'Tiny'}

Composição

{order.components.length ? (
{order.components.map(component => ( ))}
Produto SKU Qtd. 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}

}
{!filteredOrders.length && (

Nenhuma ordem de produção encontrada.

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

Ir para Plano de Corte
)} { setItemsPerPage(pageSize); setCurrentPage(1); }} />
)}
); }; export default ProductionOrders;