// 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 (
);
}
// ---------------------------------------------------------------------
// 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
{itens.map((it, i) => (
{editavel ? (
setItem(i, 'material_id', v)}
options={[{ value: '', label: '— Selecionar —' }, ...(materiais || []).map(m => ({ value: String(m.id), label: m.nome }))]} />
) : (
{nomeDoItem(it) || '—'}
)}
{editavel ? (
setItem(i, 'qtd_enviada', v)} />
) : (
{it.qtd_enviada}
)}
{editavel ? (
setItem(i, 'estado_saida', v)}
options={ESTADOS_CONSERVACAO.map(e => ({ value: e, label: e }))} />
) : (
{it.estado_saida}
)}
{editavel
? removeItem(i)} disabled={itens.length === 1} />
:
}
))}
{editavel && (
Adicionar item
)}
{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) => (
);
const assinatura = (papel, nome) => (
);
return (
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)}
Material
Qtd
Conservação
{(doc.itens || []).map((it, i) => (
{it.nome || '—'}
{it.qtd}
{it.estado}
))}
{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,
});