// Conferencia de Retorno e Status Automatico dos Equipamentos — orcamento 291. // // A tela abre com TUDO que saiu no romaneio, ja preenchido. Quem confere so // diz o que aconteceu com cada item. Sem redigitacao, que era o ponto de a // ficha de papel virar sistema. // // Item com avaria exige FOTO. Sem foto nao salva: "voltou quebrado" sem prova // vira discussao com o cliente tres semanas depois. // // O status fisico do equipamento sai daqui sozinho. "Em acao" NAO e gravado — // e derivado de romaneio confirmado (ver calcSaldo em Estoque.jsx). const RETORNO_OPCOES = ['Retornou', 'Retornou com avaria', 'Não retornou']; const OCORRENCIAS = ['Danificado', 'Sujo', 'Faltando', 'Necessita manutenção', 'Reposição']; const SITUACAO_FISICA_VARIANT = { 'Disponível': 'success', 'Em ação': 'primary', 'Em manutenção': 'warning', 'Danificado': 'danger', 'Extraviado': 'danger', }; // Quanto mais alto, mais grave: quando o mesmo material aparece em varias // linhas, o pior resultado e o que fica valendo. const GRAVIDADE = { 'Disponível': 0, 'Em manutenção': 1, 'Danificado': 2, 'Extraviado': 3 }; // Traducao do que foi conferido para a situacao do equipamento. // "Sujo" e "Faltando" ficam so registrados na ocorrencia: sujeira nao tira o // material de circulacao, e faltar peca ja aparece na diferenca enviado x recebido. function situacaoDoRetorno(linha) { if (linha.retorno === 'Não retornou') return 'Extraviado'; if (linha.retorno === 'Retornou com avaria') { if (linha.ocorrencia === 'Danificado') return 'Danificado'; if (linha.ocorrencia === 'Necessita manutenção' || linha.ocorrencia === 'Reposição') return 'Em manutenção'; } return 'Disponível'; } // Mesmo bucket e mesmo padrao do upload de foto do material (Estoque.jsx). async function uploadFotoAvaria(file) { const ext = (file.name.split('.').pop() || 'jpg').toLowerCase(); const path = `avarias/${crypto.randomUUID()}.${ext}`; const { error } = await sb.storage.from('event-photos').upload(path, file, { contentType: file.type }); if (error) throw error; const { data: { publicUrl } } = sb.storage.from('event-photos').getPublicUrl(path); return publicUrl; } function ConferenciaModal({ open, onClose, saida, itens, currentUser, onSaved }) { const jaConferido = saida?.status === 'Conferida'; const [linhas, setLinhas] = React.useState(() => (itens || []).map(i => ({ id: i.id, material_id: i.material_id, material_nome: i.material_nome, qtd_enviada: i.qtd_enviada || 0, estado_saida: i.estado_saida, retorno: i.retorno || 'Retornou', qtd_recebida: String(i.retorno ? (i.qtd_recebida ?? 0) : (i.qtd_enviada || 0)), ocorrencia: i.ocorrencia || '', foto_url: i.foto_url || '', obs_retorno: i.obs_retorno || '', }))); const [conferente, setConferente] = React.useState(saida?.conferido_por || currentUser?.name || ''); const [saving, setSaving] = React.useState(false); const [uploading, setUploading] = React.useState(null); // indice em upload const [erro, setErro] = React.useState(null); const setLinha = (idx, campo, valor) => setLinhas(prev => prev.map((l, i) => { if (i !== idx) return l; const nova = { ...l, [campo]: valor }; // Trocar o resultado reajusta a quantidade para o caso obvio, mas quem // confere ainda pode corrigir na mao. if (campo === 'retorno') { if (valor === 'Não retornou') nova.qtd_recebida = '0'; else if (valor === 'Retornou') { nova.qtd_recebida = String(l.qtd_enviada); nova.ocorrencia = ''; } } return nova; })); const handleFoto = async (idx, file) => { if (!file) return; setUploading(idx); try { const url = await uploadFotoAvaria(file); setLinha(idx, 'foto_url', url); } catch (err) { console.error('[Conferencia.handleFoto] upload falhou:', err); setErro('Não foi possível enviar a foto. Tente de novo.'); } setUploading(null); }; const validar = () => { for (const l of linhas) { const recebida = parseInt(l.qtd_recebida, 10); if (Number.isNaN(recebida) || recebida < 0) return `${l.material_nome}: informe a quantidade recebida.`; if (recebida > l.qtd_enviada) return `${l.material_nome}: recebido (${recebida}) maior que o enviado (${l.qtd_enviada}).`; if (l.retorno === 'Retornou' && recebida !== l.qtd_enviada) { return `${l.material_nome}: voltou menos do que saiu. Marque "Retornou com avaria" ou "Não retornou".`; } if (l.retorno === 'Retornou com avaria') { if (!l.ocorrencia) return `${l.material_nome}: escolha a ocorrência da avaria.`; if (!l.foto_url) return `${l.material_nome}: a foto da avaria é obrigatória.`; } } if (!conferente.trim()) return 'Informe quem está conferindo.'; return null; }; const concluir = async () => { const problema = validar(); if (problema) { setErro(problema); return; } setErro(null); setSaving(true); // 1) grava a conferencia item a item for (const l of linhas) { const { error } = await sb.from('material_saida_itens').update({ retorno: l.retorno, qtd_recebida: parseInt(l.qtd_recebida, 10) || 0, ocorrencia: l.retorno === 'Retornou com avaria' ? (l.ocorrencia || null) : null, foto_url: l.foto_url || null, obs_retorno: l.obs_retorno?.trim() || null, }).eq('id', l.id); if (error) { console.error('[Conferencia.concluir/item] Supabase error:', error); setSaving(false); setErro('Erro ao salvar a conferência: ' + (error.message || 'desconhecido')); return; } } // 2) status automatico do equipamento — pior resultado por material const pior = {}; linhas.forEach(l => { if (!l.material_id) return; const s = situacaoDoRetorno(l); const atual = pior[l.material_id]; if (!atual || GRAVIDADE[s] > GRAVIDADE[atual.situacao]) { pior[l.material_id] = { situacao: s, linha: l }; } }); const agora = new Date().toISOString(); for (const [materialId, info] of Object.entries(pior)) { // Retorno normal nao "conserta" material que ja estava com problema por // outro motivo: so a liberacao manual na ficha do material faz isso. if (info.situacao === 'Disponível') continue; const { error } = await sb.from('materiais').update({ situacao_fisica: info.situacao, situacao_obs: [info.linha.ocorrencia, info.linha.obs_retorno].filter(Boolean).join(' — ') || null, situacao_alterada_em: agora, situacao_alterada_por: conferente.trim(), }).eq('id', Number(materialId)); if (error) console.error('[Conferencia.concluir/material] Supabase error:', error); } // 3) fecha o romaneio const { error: sErr } = await sb.from('material_saidas').update({ status: 'Conferida', conferido_em: agora, conferido_por: conferente.trim(), }).eq('id', saida.id); setSaving(false); if (sErr) { console.error('[Conferencia.concluir/saida] Supabase error:', sErr); setErro('A conferência foi salva, mas o romaneio não fechou: ' + (sErr.message || 'desconhecido')); return; } onSaved(); }; const colunas = 'minmax(150px,1fr) 110px 180px 190px'; return (
Saiu em {romDataHora(saida?.saida_em)} com {saida?.responsavel_retirada || '—'}. {jaConferido && <> Já conferido por {saida.conferido_por} em {romDataHora(saida.conferido_em)}.}
Material
Enviado × recebido
Resultado
Ocorrência / foto
{linhas.map((l, i) => { const comAvaria = l.retorno === 'Retornou com avaria'; return (
{l.material_nome}
Saiu como {l.estado_saida}
{l.qtd_enviada} × setLinha(i, 'qtd_recebida', v)} style={{ width: 56 }} />
setLinha(i, 'ocorrencia', v)} options={[{ value: '', label: '— Ocorrência —' }, ...OCORRENCIAS.map(o => ({ value: o, label: o }))]} /> {l.foto_url ? (
Avaria setLinha(i, 'foto_url', '')}>Trocar
) : ( )} ) : ( )} setLinha(i, 'obs_retorno', v)} placeholder="Observações" />
); })}
{erro && (
{erro}
)}
Fechar {saving ? 'Salvando…' : (jaConferido ? 'Salvar correção' : 'Concluir conferência')}
); } Object.assign(window, { ConferenciaModal, uploadFotoAvaria, situacaoDoRetorno, RETORNO_OPCOES, OCORRENCIAS, SITUACAO_FISICA_VARIANT, });