import { 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 { exportToCSV, fetchProductionOrders } from '../dataService'; import type { DateRange, ProductionOrderItem, ProductionOrderStatus, ProductionOrderSummary } 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 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 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 [isLoading, setIsLoading] = useState(true); const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(20); 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 = await fetchProductionOrders(dateRange, { search: searchTerm }); if (isMounted) { setSummary(nextSummary); 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 handleManualRefresh = () => { void loadProductionOrders({ force: true }); }; 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 de produção sincronizadas do Tiny.

{ 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

{ 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); return ( ); })}
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'} {statusStyle.label}
{!filteredOrders.length && (

Nenhuma ordem de produção encontrada.

Quando a sincronização com o Tiny estiver ativa, as ordens aparecerão aqui com status, produto, quantidade e marcadores.

)} { setItemsPerPage(pageSize); setCurrentPage(1); }} />
)}
); }; export default ProductionOrders;