// ---- Adapters: DB <-> UI ---- const ORDER_STATUS_DB_TO_UI = { emitido: 'Emitido', confirmado: 'Confirmado', em_transito: 'Em Trânsito', entregue: 'Entregue', cancelado: 'Cancelado', }; const ORDER_STATUS_UI_TO_DB = { 'Emitido': 'emitido', 'Confirmado': 'confirmado', 'Em Trânsito': 'em_transito', 'Entregue': 'entregue', 'Cancelado': 'cancelado', }; const toUiOrder = (row) => ({ id: row.id, code: row.code, total: Number(row.total) || 0, status: ORDER_STATUS_DB_TO_UI[row.status] || 'Emitido', status_db: row.status, emitted_at: row.emitted_at, eta_date: row.eta, emitido: row.emitted_at ? new Date(row.emitted_at).toLocaleDateString('pt-BR') : '—', eta: row.eta ? new Date(row.eta).toLocaleDateString('pt-BR') : '—', cotacao: row.quotation?.code || '—', quotation_id: row.quotation?.id || row.quotation_id, supplier: row.supplier?.name || '—', supplier_id: row.supplier?.id || row.supplier_id, supplier_obj: row.supplier, items: row.itemsCount, }); function ScreenOrders({ setRoute, openQuotation }) { const { BRL } = window.SCP; const [filter, setFilter] = useState('todos'); const [openOrder, setOpenOrder] = useState(null); const [rows, setRows] = useState([]); const [loading, setLoading] = useState(true); const [loadErr, setLoadErr] = useState(null); useEffect(() => { let mounted = true; (async () => { try { const data = await window.scpDb.orders.list(); if (mounted) setRows(data.map(toUiOrder)); } catch (e) { if (mounted) setLoadErr(e.message || 'Falha ao carregar pedidos'); } finally { if (mounted) setLoading(false); } })(); return () => { mounted = false; }; }, []); const counts = useMemo(() => { const c = { todos: rows.length, 'Emitido': 0, 'Confirmado': 0, 'Em Trânsito': 0, 'Entregue': 0, 'Cancelado': 0 }; rows.forEach(o => { if (c[o.status] !== undefined) c[o.status]++; }); return c; }, [rows]); const filtered = rows.filter(o => filter === 'todos' || o.status === filter); const kpis = useMemo(() => { const monthNow = new Date().getMonth(); const yearNow = new Date().getFullYear(); const thisMonth = rows.filter(r => { if (!r.emitted_at) return false; const d = new Date(r.emitted_at); return d.getMonth() === monthNow && d.getFullYear() === yearNow; }); const totalMes = thisMonth.reduce((s, r) => s + r.total, 0); const emTransito = counts['Em Trânsito'] + counts['Confirmado']; const todayStr = new Date().toISOString().slice(0, 10); const entregaHoje = rows.filter(r => r.eta_date === todayStr && (r.status === 'Em Trânsito' || r.status === 'Confirmado')).length; const atrasados = rows.filter(r => r.eta_date && r.eta_date < todayStr && r.status !== 'Entregue' && r.status !== 'Cancelado').length; const validOrders = rows.filter(r => r.status !== 'Cancelado'); const ticketMedio = validOrders.length ? validOrders.reduce((s, r) => s + r.total, 0) / validOrders.length : 0; const itensMedio = validOrders.length ? validOrders.reduce((s, r) => s + (r.items || 0), 0) / validOrders.length : 0; return { totalMes, qtdMes: thisMonth.length, emTransito, entregaHoje, atrasados, ticketMedio, itensMedio }; }, [rows, counts]); const updateStatus = async (order, newStatusUi) => { try { const newStatusDb = ORDER_STATUS_UI_TO_DB[newStatusUi]; const updated = await window.scpDb.orders.updateStatus(order.id, newStatusDb); setRows(prev => prev.map(r => r.id === order.id ? toUiOrder({ ...updated, supplier: order.supplier_obj, quotation: { code: order.cotacao, id: order.quotation_id }, itemsCount: order.items }) : r)); setOpenOrder(null); window.scpToast(`Pedido ${newStatusUi}`, { kind: 'emerald', sub: order.code }); } catch (e) { window.scpToast('Erro ao atualizar', { kind: 'coral', sub: e.message }); } }; return (
}/> } delta={kpis.emTransito > 0 ? 'acompanhar' : 'ok'} deltaTone={kpis.emTransito > 0 ? 'amber' : 'emerald'}/> }/> }/>
{loadErr && ( Falha ao carregar pedidos:{' '} {loadErr} )}
{[['todos','Todos'],['Emitido','Emitido'],['Confirmado','Confirmado'],['Em Trânsito','Em Trânsito'],['Entregue','Entregue'],['Cancelado','Cancelado']].map(([k, l]) => ( ))}
}>Exportar
{loading && ( )} {!loading && filtered.length === 0 && ( )} {!loading && filtered.map(o => { const tone = OSTATUS[o.status]?.tone || 'muted'; return ( setOpenOrder(o)}> ); })}
Pedido Fornecedor Itens Valor Status Emitido Previsão Cotação
Carregando pedidos…
{rows.length === 0 ? 'Nenhum pedido emitido ainda. Pedidos são criados automaticamente ao aprovar uma cotação.' : 'Nenhum pedido com este filtro.'}
{o.code}
{o.supplier}
{o.items} {BRL(o.total)} {o.status} {o.emitido} {o.eta} {o.cotacao} e.stopPropagation()}>
} size="sm" kind="ghost" title="Visualizar" onClick={() => setOpenOrder(o)}/> } size="sm" kind="ghost" title="PDF"/> } size="sm" kind="ghost" title="WhatsApp"/>
{/* Live tracking timeline */} {kpis.emTransito} em trânsito}/>
{rows.filter(o => o.status === 'Em Trânsito' || o.status === 'Confirmado').length === 0 && (
Nenhum pedido em trânsito no momento.
)} {rows.filter(o => o.status === 'Em Trânsito' || o.status === 'Confirmado').map(o => (
setOpenOrder(o)} role="button" tabIndex={0}>
{o.supplier} · {o.code} {o.status} ETA {o.eta}
{['Emitido', 'Confirmado', 'Em Trânsito', 'Entregue'].map((stage, i) => { const idx = ['Emitido', 'Confirmado', 'Em Trânsito', 'Entregue'].indexOf(o.status); return (
{stage}
); })}
))}
setOpenOrder(null)} eyebrow={openOrder?.code} title={openOrder ? `Pedido para ${openOrder.supplier}` : ''} headerRight={openOrder && {openOrder.status}} footer={openOrder && ( <> } onClick={() => { const num = (openOrder.supplier_obj?.whatsapp || '').replace(/\D/g, ''); if (num) window.open(`https://wa.me/55${num}?text=${encodeURIComponent('Olá! Sobre o pedido ' + openOrder.code)}`, '_blank', 'noopener'); else window.scpToast('Fornecedor sem WhatsApp cadastrado', { kind: 'amber' }); }}>WhatsApp } onClick={() => window.scpToast('Gerando PDF do pedido', { kind: 'sky', sub: openOrder.code + ' · download em ~3s' })}>Baixar PDF {openOrder.status === 'Emitido' && ( } onClick={() => updateStatus(openOrder, 'Confirmado')}>Marcar Confirmado )} {openOrder.status === 'Confirmado' && ( } onClick={() => updateStatus(openOrder, 'Em Trânsito')}>Marcar em Trânsito )} {openOrder.status === 'Em Trânsito' && ( } onClick={() => updateStatus(openOrder, 'Entregue')}>Marcar Entregue )} } onClick={() => { if (openOrder.quotation_id && openQuotation) { openQuotation(openOrder.quotation_id); setOpenOrder(null); } else if (setRoute) { setRoute('quotations'); setOpenOrder(null); } }}>Ver cotação )} > {openOrder && }
); } function OrderDetail({ order: o }) { const { BRL } = window.SCP; const supplier = o.supplier_obj; const [orderItems, setOrderItems] = useState(null); useEffect(() => { let mounted = true; (async () => { try { const full = await window.scpDb.orders.getById(o.id); if (mounted) setOrderItems(full.items || []); } catch (e) { if (mounted) setOrderItems([]); } })(); return () => { mounted = false; }; }, [o.id]); const stages = ['Emitido', 'Confirmado', 'Em Trânsito', 'Entregue']; const idx = o.status === 'Cancelado' ? -1 : stages.indexOf(o.status); const cancelled = o.status === 'Cancelado'; return ( <>
{o.supplier}
{o.code} · emitido em {o.emitido}

Resumo

Valor total {BRL(o.total)} {o.items} itens no pedido
Previsão {o.eta} Entrega ao restaurante

Rastreio

{stages.map((stage, i) => (
{stage}
))}
{cancelled && (
Pedido cancelado
)}

Itens do pedido

{orderItems === null ? (
Carregando itens…
) : orderItems.length ? (
{orderItems.map(it => (
{it.name_snapshot}
{Number(it.qty)} {it.unit_snapshot || ''} · unit. {BRL(it.unit_price)}
{BRL(Number(it.qty) * Number(it.unit_price))}
))}
) : (
Sem itens registrados.
)}

Dados do pedido

Código
{o.code}
Fornecedor
{o.supplier}
Itens
{o.items}
Valor
{BRL(o.total)}
Emitido em
{o.emitido}
Previsão
{o.eta}
Cotação origem
{o.cotacao}
{supplier && (

Sobre o fornecedor

Score {supplier.score}
Cidade {supplier.city || '—'}
)} ); } (function() { if (document.getElementById('scp-orders-css')) return; const s = document.createElement('style'); s.id = 'scp-orders-css'; s.textContent = ` .scp-track-list { display: flex; flex-direction: column; gap: 14px; margin-top: 6px; } .scp-track { padding: 14px 16px; border-radius: 12px; background: var(--surface-2); border: 1px solid var(--border-soft); transition: background .12s, border-color .12s; } .scp-track.clickable { cursor: pointer; } .scp-track.clickable:hover { background: var(--surface-3); border-color: var(--border); } .scp-track .head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 14px; } .scp-track .head .t { font-size: 13px; color: var(--fg-2); flex: 1; min-width: 0; } .scp-track .head .t strong { color: var(--fg); font-weight: 600; } .scp-track .head .eta { font-size: 12px; color: var(--fg-3); font-family: 'JetBrains Mono', monospace; } .scp-track .bar { display: grid; grid-template-columns: repeat(4, 1fr); position: relative; } .scp-track .bar::before { content: ""; position: absolute; left: 8px; right: 8px; top: 7px; height: 2px; background: var(--border); } .scp-track .stage { display: flex; flex-direction: column; align-items: center; gap: 6px; position: relative; z-index: 1; } .scp-track .stage .dot { width: 16px; height: 16px; border-radius: 999px; background: var(--surface-3); border: 2px solid var(--border); } .scp-track .stage.done .dot { background: oklch(0.74 0.16 160); border-color: oklch(0.74 0.16 160); } .scp-track .stage.current .dot { box-shadow: 0 0 0 4px oklch(0.74 0.16 160 / .25); animation: pulseDot 1.4s ease-in-out infinite; } @keyframes pulseDot { 50% { box-shadow: 0 0 0 8px oklch(0.74 0.16 160 / .12); } } .scp-track .stage .l { font-size: 10.5px; color: var(--fg-4); text-transform: uppercase; letter-spacing: .08em; font-weight: 600; } .scp-track .stage.done .l { color: var(--fg-2); } .scp-track .stage.current .l { color: oklch(0.92 0.14 160); } `; document.head.appendChild(s); })(); window.ScreenOrders = ScreenOrders;