// Romaneio de Saida de Materiais — orcamento 291. // // Substitui a ficha de papel: um documento por acao, com os itens que estao // saindo, os tres responsaveis e assinatura. A reserva (material_reservas) // continua sendo o planejamento; a SAIDA acontece so aqui. // // Confirmar fecha o documento: a partir dai o romaneio nao e mais editavel, so // conferido ou cancelado. E isso que faz o comprovante valer alguma coisa. const ESTADOS_CONSERVACAO = ['Novo', 'Bom', 'Regular', 'Ruim']; const SAIDA_STATUS_VARIANT = { 'Rascunho': 'default', 'Confirmada': 'primary', 'Conferida': 'success', 'Cancelada': 'danger', }; const romaneioNumero = (saida) => `ROM-${String(saida?.id ?? 0).padStart(4, '0')}`; const romDataHora = (iso) => iso ? new Date(iso).toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }) : '—'; // Quantidade ainda em campo de um item: o que saiu menos o que ja voltou. const emCampoDoItem = (item) => Math.max(0, (item.qtd_enviada || 0) - (item.qtd_recebida || 0)); // Input/Select do shared.jsx nao tem prop disabled — documento ja confirmado // vira texto, em vez de campo editavel que finge estar travado. function CampoLeitura({ label, value }) { return (
{label}
{value || '—'}
); } // --------------------------------------------------------------------- // Painel dentro do modal de Materiais do evento // --------------------------------------------------------------------- function RomaneiosEventoPanel({ event, materiais, reservas, saidas, saidaItens, currentUser, onChange, showToast }) { const [romaneioModal, setRomaneioModal] = React.useState(null); // { saida } | { saida: null } const [conferencia, setConferencia] = React.useState(null); const doEvento = React.useMemo( () => (saidas || []).filter(s => s.event_id === event?.id), [saidas, event?.id] ); const itensDe = React.useCallback( (saidaId) => (saidaItens || []).filter(i => i.saida_id === saidaId), [saidaItens] ); const cancelar = async (saida) => { if (!window.confirm(`Cancelar o ${romaneioNumero(saida)}? O saldo volta para o estoque.`)) return; const { error } = await sb.from('material_saidas').update({ status: 'Cancelada' }).eq('id', saida.id); if (error) { console.error('[Romaneio.cancelar] Supabase error:', error); showToast('Erro ao cancelar o romaneio.', 'danger'); return; } // Solta as reservas que este romaneio tinha consumido, para voltarem a // aparecer no planejamento em vez de sumirem. await sb.from('material_reservas').update({ saida_id: null }).eq('saida_id', saida.id); showToast('Romaneio cancelado.'); onChange(); }; return (
Romaneios de saída ({doEvento.length})
setRomaneioModal({ saida: null })}>Gerar Romaneio de Saída
{doEvento.length === 0 ? ( ) : (
{doEvento.map(s => { const itens = itensDe(s.id); const totalPecas = itens.reduce((acc, i) => acc + (i.qtd_enviada || 0), 0); return (
{romaneioNumero(s)} {' '}· {itens.length} {itens.length === 1 ? 'item' : 'itens'} · {totalPecas} {totalPecas === 1 ? 'peça' : 'peças'}
{s.saida_em ? <>Saída em {romDataHora(s.saida_em)} : 'Ainda não confirmado'} {s.responsavel_retirada && <> · Retirou: {s.responsavel_retirada}} {s.conferido_em && <> · Conferido por {s.conferido_por || '—'} em {romDataHora(s.conferido_em)}}
{s.status} setRomaneioModal({ saida: s })}> {s.status === 'Rascunho' ? 'Editar' : 'Abrir'} {s.status === 'Confirmada' && ( setConferencia(s)}>Conferir retorno )} {s.status !== 'Cancelada' && s.status !== 'Conferida' && ( cancelar(s)}>Cancelar )}
); })}
)} {romaneioModal && ( setRomaneioModal(null)} event={event} materiais={materiais} reservas={reservas} currentUser={currentUser} saida={romaneioModal.saida} itensIniciais={romaneioModal.saida ? itensDe(romaneioModal.saida.id) : null} onSaved={() => { setRomaneioModal(null); onChange(); }} /> )} {conferencia && ( setConferencia(null)} saida={conferencia} itens={itensDe(conferencia.id)} currentUser={currentUser} onSaved={() => { setConferencia(null); onChange(); }} /> )}
); } // --------------------------------------------------------------------- // Montagem / visualizacao do romaneio // --------------------------------------------------------------------- function RomaneioModal({ open, onClose, event, materiais, reservas, currentUser, saida, itensIniciais, onSaved }) { const editavel = !saida || saida.status === 'Rascunho'; const [cab, setCab] = React.useState(() => ({ responsavel_retirada: saida?.responsavel_retirada ?? (currentUser?.name || ''), supervisor_acao: saida?.supervisor_acao ?? (event?.coordenador_contato || ''), responsavel_entrega: saida?.responsavel_entrega ?? (currentUser?.name || ''), obs: saida?.obs ?? '', })); // Rascunho novo ja nasce com as reservas do evento — "sem redigitacao". const [itens, setItens] = React.useState(() => { if (itensIniciais && itensIniciais.length) { return itensIniciais.map(i => ({ material_id: String(i.material_id || ''), qtd_enviada: String(i.qtd_enviada ?? 1), estado_saida: i.estado_saida || 'Bom', reserva_id: i.reserva_id || null, material_nome: i.material_nome, })); } const daReserva = (reservas || []) .filter(r => r.event_id === event?.id && r.situacao === 'Reservado' && !r.saida_id) .map(r => ({ material_id: String(r.material_id), qtd_enviada: String(r.quantidade), estado_saida: 'Bom', reserva_id: r.id, material_nome: null, })); return daReserva.length ? daReserva : [{ material_id: '', qtd_enviada: '1', estado_saida: 'Bom', reserva_id: null, material_nome: null }]; }); const [saving, setSaving] = React.useState(false); const [erro, setErro] = React.useState(null); const [exporting, setExporting] = React.useState(false); const pdfRef = React.useRef(null); const materialPorId = React.useMemo(() => { const map = {}; (materiais || []).forEach(m => { map[String(m.id)] = m; }); return map; }, [materiais]); const nomeDoItem = (it) => it.material_nome || materialPorId[it.material_id]?.nome || ''; const setItem = (idx, campo, valor) => setItens(prev => prev.map((it, i) => i === idx ? { ...it, [campo]: valor } : it)); const addItem = () => setItens(prev => [...prev, { material_id: '', qtd_enviada: '1', estado_saida: 'Bom', reserva_id: null, material_nome: null }]); const removeItem = (idx) => setItens(prev => prev.filter((_, i) => i !== idx)); const itensValidos = () => itens .filter(it => it.material_id && (parseInt(it.qtd_enviada, 10) || 0) > 0) .map(it => ({ material_id: parseInt(it.material_id, 10), material_nome: nomeDoItem(it), qtd_enviada: parseInt(it.qtd_enviada, 10), estado_saida: it.estado_saida || 'Bom', reserva_id: it.reserva_id || null, })); // Grava cabecalho + itens. Itens seguem o padrao de budget_items: apaga tudo // e reinsere, que e mais simples do que reconciliar linha a linha. const persistir = async (patchCabecalho) => { const validos = itensValidos(); if (!validos.length) { setErro('Adicione pelo menos um material.'); return null; } const payload = { event_id: event.id, client_id: event.client_id || null, client_nome: event.client_name || event.client || null, responsavel_retirada: cab.responsavel_retirada?.trim() || null, supervisor_acao: cab.supervisor_acao?.trim() || null, responsavel_entrega: cab.responsavel_entrega?.trim() || null, obs: cab.obs?.trim() || null, ...(patchCabecalho || {}), }; let row = saida; if (saida) { const { data, error } = await sb.from('material_saidas').update(payload).eq('id', saida.id).select().single(); if (error) { console.error('[Romaneio.persistir/update] Supabase error:', error); setErro('Erro ao salvar: ' + (error.message || 'desconhecido')); return null; } row = data; await sb.from('material_saida_itens').delete().eq('saida_id', saida.id); } else { const { data, error } = await sb.from('material_saidas') .insert({ ...payload, created_by: currentUser?.id || null }) .select().single(); if (error) { console.error('[Romaneio.persistir/insert] Supabase error:', error); setErro('Erro ao criar o romaneio: ' + (error.message || 'desconhecido')); return null; } row = data; } const { error: itErr } = await sb.from('material_saida_itens') .insert(validos.map(v => ({ ...v, saida_id: row.id }))); if (itErr) { console.error('[Romaneio.persistir/itens] Supabase error:', itErr); setErro('Erro ao salvar os itens: ' + (itErr.message || 'desconhecido')); return null; } return row; }; const salvarRascunho = async () => { setErro(null); setSaving(true); const row = await persistir(); setSaving(false); if (row) onSaved(); }; const confirmarSaida = async () => { setErro(null); const validos = itensValidos(); if (!validos.length) { setErro('Adicione pelo menos um material.'); return; } if (!cab.responsavel_retirada?.trim()) { setErro('Informe quem está retirando o material.'); return; } setSaving(true); // Revalida o saldo contra o banco na hora de confirmar: o rascunho pode ter // ficado aberto enquanto outra pessoa mexeu no estoque. const [{ data: mats }, { data: res }, { data: its }] = await Promise.all([ sb.from('materiais').select('*'), sb.from('material_reservas').select('*'), sb.from('material_saida_itens').select('*, material_saidas(status)'), ]); const itensAtivos = (its || []) .filter(i => i.material_saidas?.status === 'Confirmada') .filter(i => !saida || i.saida_id !== saida.id); const reservasSemEste = (res || []).filter(r => !saida || r.saida_id !== saida.id); const idsReservaDoDoc = new Set(validos.map(v => v.reserva_id).filter(Boolean)); for (const v of validos) { const mat = (mats || []).find(m => m.id === v.material_id); if (!mat) continue; const saldo = calcSaldo(mat, reservasSemEste, itensAtivos); // A reserva de origem deste romaneio ainda esta contando como reservada, // entao ela soma de volta ao disponivel. const jaReservadoAqui = reservasSemEste .filter(r => r.material_id === v.material_id && idsReservaDoDoc.has(r.id) && r.situacao === 'Reservado') .reduce((s, r) => s + (r.quantidade || 0), 0); const disponivel = saldo.livre + jaReservadoAqui; if (v.qtd_enviada > disponivel) { setSaving(false); setErro(`${mat.nome}: só há ${disponivel} disponível(is) e o romaneio pede ${v.qtd_enviada}.`); return; } } const row = await persistir({ status: 'Confirmada', saida_em: new Date().toISOString() }); if (!row) { setSaving(false); return; } // Marca as reservas consumidas para nao contarem saldo duas vezes. const reservaIds = [...idsReservaDoDoc]; if (reservaIds.length) { await sb.from('material_reservas').update({ saida_id: row.id }).in('id', reservaIds); } setSaving(false); onSaved(); }; const exportarPDF = async () => { if (!window.renderPDF || !pdfRef.current) return; setExporting(true); await window.renderPDF(pdfRef.current, `${romaneioNumero(saida || { id: 0 })}.pdf`); setExporting(false); }; const docParaPDF = { ...(saida || {}), numero: romaneioNumero(saida || { id: 0 }), evento_nome: event?.name, cliente_nome: event?.client_name || event?.client || (saida?.client_nome ?? ''), responsavel_retirada: cab.responsavel_retirada, supervisor_acao: cab.supervisor_acao, responsavel_entrega: cab.responsavel_entrega, obs: cab.obs, itens: itens .filter(it => it.material_id) .map(it => ({ nome: nomeDoItem(it), qtd: parseInt(it.qtd_enviada, 10) || 0, estado: it.estado_saida })), }; const acoes = saida && saida.status !== 'Rascunho' ? ( {exporting ? 'Gerando…' : 'Comprovante'} ) : null; const colunas = 'minmax(180px,1fr) 90px 130px 32px'; return (
{saida && saida.status !== 'Rascunho' && (
Saída confirmada em {romDataHora(saida.saida_em)}. Documento emitido não é mais editável.
)} {/* Responsaveis */}
{editavel ? ( <> setCab(c => ({ ...c, responsavel_retirada: v }))} placeholder="Quem está levando" /> setCab(c => ({ ...c, supervisor_acao: v }))} /> setCab(c => ({ ...c, responsavel_entrega: v }))} /> ) : ( <> )}
{/* Itens */}
Materiais que estão saindo
Material
Qtd
Conservação
{itens.map((it, i) => (
{editavel ? ( setItem(i, 'qtd_enviada', v)} /> ) : (
{it.qtd_enviada}
)} {editavel ? ( setCab(c => ({ ...c, obs: v }))} /> : (cab.obs ? : null)} {erro && (
{erro}
)}
Fechar {editavel && ( <> Salvar rascunho {saving ? 'Confirmando…' : 'Confirmar saída'} )}
); } // --------------------------------------------------------------------- // Comprovante — substitui a ficha de papel, entao tem linha de assinatura // --------------------------------------------------------------------- function RomaneioPDFTemplate({ doc, innerRef }) { if (!doc) return null; const td = { padding: '7px 9px', borderBottom: '1px solid #E2E8F0', fontSize: 11, color: '#334155' }; const th = { padding: '7px 9px', borderBottom: '2px solid #CBD5E1', fontSize: 9, fontWeight: 700, color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.05em', textAlign: 'left' }; const campo = (k, v) => (
{k}
{v || '—'}
); const assinatura = (papel, nome) => (
{nome || ' '}
{papel}
); return (
Promoup
Promoup Marketing & Eventos
Romaneio de Saída
{doc.numero}
Saída em {romDataHora(doc.saida_em)}
{campo('Ação / Evento', doc.evento_nome)} {campo('Cliente', doc.cliente_nome)}
{(doc.itens || []).map((it, i) => ( ))}
Material Qtd Conservação
{it.nome || '—'} {it.qtd} {it.estado}
{doc.obs && (
Observações
{doc.obs}
)}
{assinatura('Responsável pela retirada', doc.responsavel_retirada)} {assinatura('Supervisor da ação', doc.supervisor_acao)} {assinatura('Entregue no estoque por', doc.responsavel_entrega)}
A assinatura deste romaneio confirma o recebimento dos materiais nas quantidades e no estado de conservação descritos acima. A devolução é conferida item a item no retorno da ação.
); } Object.assign(window, { RomaneiosEventoPanel, RomaneioModal, RomaneioPDFTemplate, CampoLeitura, romaneioNumero, romDataHora, emCampoDoItem, ESTADOS_CONSERVACAO, SAIDA_STATUS_VARIANT, });