// Registros da acao no app do promotor — orcamento 310, item 3. // // EnvioFotoModal -> foto com categoria, legenda e localizacao // ResultadosModal -> indicadores configurados pela producao no evento // // Por que a categoria e obrigatoria e a legenda nao: no fim da acao a producao // precisa montar o relatorio do cliente separando montagem, abordagem e // resultado. Sem categoria, sobram 200 fotos soltas e alguem tem que // classificar na mao. Ja a legenda so serve quando tem algo a dizer — exigir // texto a cada foto derruba o envio. const FOTO_CATEGORIAS = [ 'Montagem', 'Equipe', 'Início da ação', 'Abordagem', 'Público', 'Ativação', 'Brindes', 'Estrutura', 'Resultado', 'Encerramento', 'Ocorrência', 'Outros', ]; function EnvioFotoModal({ open, escala, freelancer, onClose, onEnviada, showToast }) { const [categoria, setCategoria] = React.useState('Abordagem'); const [legenda, setLegenda] = React.useState(''); const [arquivo, setArquivo] = React.useState(null); const [preview, setPreview] = React.useState(''); const [enviando, setEnviando] = React.useState(false); React.useEffect(() => () => { if (preview) URL.revokeObjectURL(preview); }, [preview]); React.useEffect(() => { if (!open) return; setCategoria('Abordagem'); setLegenda(''); setArquivo(null); setPreview(''); }, [open]); const escolher = (file) => { if (!file) return; if (preview) URL.revokeObjectURL(preview); setArquivo(file); setPreview(URL.createObjectURL(file)); }; const enviar = async () => { if (!arquivo) return; setEnviando(true); const ext = (arquivo.name.split('.').pop() || 'jpg').toLowerCase(); const path = 'escala_' + escala.id + '/' + crypto.randomUUID() + '.' + ext; try { const { error: upErr } = await sb.storage .from('event-photos').upload(path, arquivo, { contentType: arquivo.type }); if (upErr) throw upErr; const { data: pub } = sb.storage.from('event-photos').getPublicUrl(path); // Localizacao da foto e best-effort: a promotora ja provou presenca no // check-in. Travar o envio por GPS fraco so faria ela parar de mandar foto. let pos = null; try { pos = await obterPosicao(); } catch (err) { console.warn('[EnvioFotoModal] sem GPS:', err.message); } const endereco = pos ? await enderecoAproximado(pos.lat, pos.lng) : null; const { data, error } = await sb.from('escala_fotos').insert({ escala_id: escala.id, event_id: escala.event_id, freelancer_id: freelancer.id, url: pub.publicUrl, storage_path: path, categoria, legenda: legenda.trim() || null, lat: pos ? pos.lat : null, lng: pos ? pos.lng : null, endereco, }).select().single(); if (error) { await sb.storage.from('event-photos').remove([path]); // rollback throw error; } onEnviada(data); showToast('Foto enviada!'); onClose(); } catch (err) { console.error('[EnvioFotoModal.enviar] error:', err); showToast(err.message || 'Erro ao enviar a foto.', 'danger'); } setEnviando(false); }; return (
O que a foto mostra?
{FOTO_CATEGORIAS.map(c => ( ))}
Cancelar {enviando ? 'Enviando…' : 'Enviar foto'}
); } // ─────────────────────────────────────────────────────────── // Resultados — os indicadores que a producao configurou nesta acao // ─────────────────────────────────────────────────────────── function ResultadosModal({ open, escala, onClose, onSalvo, showToast }) { const campos = ((escala && escala.events && escala.events.resultado_campos) || []); const [valores, setValores] = React.useState({}); const [salvando, setSalvando] = React.useState(false); React.useEffect(() => { if (!open) return; // Reabrir precisa mostrar o que ja foi registrado, nao um formulario vazio. const atuais = {}; campos.forEach(c => { const v = (escala.resultados || {})[c.chave]; atuais[c.chave] = (v === null || v === undefined) ? '' : String(v); }); setValores(atuais); }, [open, escala && escala.id]); const salvar = async () => { const payload = {}; for (const c of campos) { const bruto = String(valores[c.chave] || '').trim(); if (!bruto) continue; const n = Number(bruto.replace(',', '.')); if (!Number.isFinite(n) || n < 0) { showToast('Confira o campo "' + c.label + '": use apenas números.', 'danger'); return; } payload[c.chave] = n; } setSalvando(true); const { data, error } = await sb.rpc('promotor_salvar_resultados', { p_escala_id: escala.id, p_resultados: payload, }); setSalvando(false); if (error) { console.error('[ResultadosModal.salvar] Supabase error:', error); showToast(error.message || 'Não foi possível salvar.', 'danger'); return; } onSalvo(Array.isArray(data) ? data[0] : data); showToast('Resultados salvos!'); onClose(); }; return ( {campos.length === 0 ? (
A produção ainda não definiu indicadores para esta ação.
) : (
{campos.map(c => (
setValores(prev => ({ ...prev, [c.chave]: v }))} placeholder="0" /> {c.meta ? (
Meta da ação: {Number(c.meta).toLocaleString('pt-BR')}
) : null}
))}
Cancelar {salvando ? 'Salvando…' : 'Salvar resultados'}
)}
); } Object.assign(window, { EnvioFotoModal, ResultadosModal, FOTO_CATEGORIAS }); // ─────────────────────────────────────────────────────────── // Ocorrencia — secao 14. Orcamento 310, Fase 2. // // Botao destacado e caminho curto de proposito: quem esta registrando ocorrencia // esta no meio de um problema. Categoria + descricao bastam; a foto ajuda mas // nao trava o envio. // ─────────────────────────────────────────────────────────── const OCORRENCIA_CATEGORIAS = [ 'Falta de material', 'Atraso', 'Ausência de profissional', 'Problema com estrutura', 'Problema com cliente/local', 'Problema com uniforme', 'Problema técnico', 'Acidente', 'Outro', ]; function OcorrenciaModal({ open, escala, onClose, onRegistrada, showToast }) { const [categoria, setCategoria] = React.useState('Falta de material'); const [descricao, setDescricao] = React.useState(''); const [arquivo, setArquivo] = React.useState(null); const [preview, setPreview] = React.useState(''); const [enviando, setEnviando] = React.useState(false); React.useEffect(() => () => { if (preview) URL.revokeObjectURL(preview); }, [preview]); React.useEffect(() => { if (!open) return; setCategoria('Falta de material'); setDescricao(''); setArquivo(null); setPreview(''); }, [open]); const escolher = (file) => { if (!file) return; if (preview) URL.revokeObjectURL(preview); setArquivo(file); setPreview(URL.createObjectURL(file)); }; const enviar = async () => { if (descricao.trim().length < 3) { showToast('Descreva rapidamente o que aconteceu.', 'danger'); return; } setEnviando(true); let fotoUrl = null, fotoPath = null; try { if (arquivo) { const ext = (arquivo.name.split('.').pop() || 'jpg').toLowerCase(); const caminho = 'ocorrencia_' + escala.id + '/' + crypto.randomUUID() + '.' + ext; const { error: upErr } = await sb.storage .from('event-photos').upload(caminho, arquivo, { contentType: arquivo.type }); if (upErr) throw upErr; const { data: pub } = sb.storage.from('event-photos').getPublicUrl(caminho); fotoUrl = pub.publicUrl; fotoPath = caminho; } let pos = null; try { pos = await obterPosicao(); } catch (err) { console.warn('[OcorrenciaModal] sem GPS:', err.message); } const endereco = pos ? await enderecoAproximado(pos.lat, pos.lng) : null; const { data, error } = await sb.rpc('promotor_registrar_ocorrencia', { p_escala_id: escala.id, p_categoria: categoria, p_descricao: descricao.trim(), p_foto_url: fotoUrl, p_foto_path: fotoPath, p_lat: pos ? pos.lat : null, p_lng: pos ? pos.lng : null, p_endereco: endereco, }); if (error) { if (fotoPath) await sb.storage.from('event-photos').remove([fotoPath]); throw error; } onRegistrada(Array.isArray(data) ? data[0] : data); showToast('Ocorrência enviada. A produção foi avisada.'); onClose(); } catch (err) { console.error('[OcorrenciaModal.enviar] error:', err); showToast(err.message || 'Não foi possível enviar a ocorrência.', 'danger'); } setEnviando(false); }; return (
O que aconteceu?
{OCORRENCIA_CATEGORIAS.map(c => ( ))}
Cancelar {enviando ? 'Enviando…' : 'Enviar ocorrência'}
); } Object.assign(window, { OcorrenciaModal, OCORRENCIA_CATEGORIAS });