// App do Promotor — PWA mobile, dentro do proprio PROMO UP 360. Orcamento 290. // // O promotor ve SOMENTE as acoes em que esta escalado (RLS da migration 027). // Confirmar presenca / check-in / check-out passam pelas RPCs da migration 026, // nunca por UPDATE direto — senao ele conseguiria mudar o proprio cache. // // Boa parte do detalhe da acao (traje, briefing, cronograma, endereco // detalhado) vem do payload do Pedido de Producao (event_reports type='PP'), // que ja existe no sistema. Por isso nao criamos colunas novas em events. const fmtPromoMoney = v => 'R$ ' + (parseFloat(v) || 0).toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); const fmtPromoData = (iso) => { if (!iso) return '—'; const [y, m, d] = String(iso).split('-'); return (y && m && d) ? `${d}/${m}/${y}` : iso; }; const fmtPromoDataLonga = (iso) => { if (!iso) return '—'; // 'T00:00:00' evita o deslocamento de um dia por UTC (padrao do projeto). return new Date(iso + 'T00:00:00').toLocaleDateString('pt-BR', { weekday: 'long', day: '2-digit', month: 'long' }); }; const fmtPromoHora = iso => iso ? new Date(iso).toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }) : null; function PromotorApp({ session, freelancer }) { const [escalas, setEscalas] = React.useState([]); const [loading, setLoading] = React.useState(true); const [abertaId, setAbertaId] = React.useState(null); const [toast, setToast] = React.useState(null); const showToast = (msg, type = 'success') => { setToast({ msg, type }); setTimeout(() => setToast(null), 3000); }; const carregar = React.useCallback(async () => { const { data, error } = await sb .from('event_escalas') .select('*, events(id, name, client_name, event_date, event_end_date, event_time, location, coordenador_contato)') .order('id', { ascending: false }); if (error) console.error('[PromotorApp.carregar] Supabase error:', error); setEscalas(data || []); setLoading(false); }, []); React.useEffect(() => { carregar(); sb.rpc('promotor_registrar_login').then(() => {}); }, [carregar]); // Proximas primeiro; as sem data vao para o fim. const ordenadas = React.useMemo(() => { const copia = [...escalas]; copia.sort((a, b) => { const da = a.events?.event_date || '9999-12-31'; const db = b.events?.event_date || '9999-12-31'; return da.localeCompare(db); }); return copia; }, [escalas]); const hojeISO = new Date().toISOString().slice(0, 10); const proximas = ordenadas.filter(e => (e.events?.event_date || '9999-12-31') >= hojeISO); const passadas = ordenadas.filter(e => (e.events?.event_date || '9999-12-31') < hojeISO).reverse(); const aberta = escalas.find(e => e.id === abertaId) || null; const atualizarEscala = (nova) => setEscalas(prev => prev.map(e => e.id === nova.id ? { ...e, ...nova } : e)); const handleLogout = () => sb.auth.signOut(); return (
{/* Topo */}
{aberta ? ( ) : ( )}
{aberta ? 'Detalhe da ação' : `Olá, ${(freelancer?.name || '').split(' ')[0] || 'promotor'}!`}
{aberta ? (aberta.events?.name || '') : 'Suas ações no PROMO UP 360'}
{loading ? : aberta ? : ( <> {escalas.length === 0 && ( )} {proximas.length > 0 && ( <> Próximas ações {proximas.map(e => setAbertaId(e.id)} />)} )} {passadas.length > 0 && ( <> Ações anteriores {passadas.map(e => setAbertaId(e.id)} />)} )} )}
{toast && setToast(null)} />}
); } function SecaoTitulo({ children }) { return (
{children}
); } function CardAcao({ escala, onOpen }) { const ev = escala.events || {}; return (
{ev.name || 'Ação'}
{ev.client_name || '—'}
{escala.status}
{fmtPromoData(ev.event_date)} {ev.event_time && ( {ev.event_time} )} {fmtPromoMoney(escala.cache)}
); } // ─────────────────────────────────────────────────────────── // Detalhe da acao + confirmar / check-in / fotos / check-out // ─────────────────────────────────────────────────────────── function DetalheAcao({ escala, freelancer, onChange, showToast }) { const ev = escala.events || {}; const [pp, setPp] = React.useState(null); const [fotos, setFotos] = React.useState([]); const [busy, setBusy] = React.useState(false); const [enviandoFoto, setEnviandoFoto] = React.useState(false); const [checkoutOpen, setCheckoutOpen] = React.useState(false); const [co, setCo] = React.useState({ realizada: true, houve_problema: false, problema_desc: '', observacao: '' }); React.useEffect(() => { if (!ev.id) return; sb.from('event_reports').select('payload').eq('event_id', ev.id).eq('type', 'PP').maybeSingle() .then(({ data }) => setPp(data?.payload || null)); sb.from('escala_fotos').select('*').eq('escala_id', escala.id).order('enviada_em', { ascending: false }) .then(({ data }) => setFotos(data || [])); }, [ev.id, escala.id]); const chamarRpc = async (fn, args, msgOk) => { setBusy(true); const { data, error } = await sb.rpc(fn, args); setBusy(false); if (error) { console.error(`[DetalheAcao.${fn}] Supabase error:`, error); showToast(error.message || 'Não foi possível concluir.', 'danger'); return null; } const linha = Array.isArray(data) ? data[0] : data; if (linha) onChange(linha); showToast(msgOk); return linha; }; const handleFoto = async (file) => { if (!file) return; setEnviandoFoto(true); try { const ext = (file.name.split('.').pop() || 'jpg').toLowerCase(); const path = `escala_${escala.id}/${crypto.randomUUID()}.${ext}`; const { error: upErr } = await sb.storage.from('event-photos').upload(path, file, { contentType: file.type }); if (upErr) throw upErr; const { data: { publicUrl } } = sb.storage.from('event-photos').getPublicUrl(path); const { data, error } = await sb.from('escala_fotos').insert({ escala_id: escala.id, event_id: ev.id, freelancer_id: freelancer.id, url: publicUrl, storage_path: path, }).select().single(); if (error) { await sb.storage.from('event-photos').remove([path]); // rollback throw error; } setFotos(prev => [data, ...prev]); showToast('Foto enviada!'); } catch (err) { console.error('[DetalheAcao.handleFoto] error:', err); showToast('Erro ao enviar a foto.', 'danger'); } setEnviandoFoto(false); }; const confirmarCheckout = async () => { const ok = await chamarRpc('promotor_check_out', { p_escala_id: escala.id, p_realizada: co.realizada, p_houve_problema: co.houve_problema, p_problema_desc: co.houve_problema ? (co.problema_desc || null) : null, p_observacao: co.observacao || null, }, 'Check-out registrado. Obrigado!'); if (ok) setCheckoutOpen(false); }; const endereco = [ev.location, pp?.ponto_referencia, pp?.cidade_estado].filter(Boolean).join(' · '); const briefing = [pp?.objetivo, pp?.script, pp?.regras].filter(Boolean).join('\n\n'); const horario = pp?.horario_inicio ? `${pp.horario_inicio}${pp.horario_fim ? ' às ' + pp.horario_fim : ''}` : (ev.event_time || '—'); const linhas = [ { icon: 'crm', label: 'Cliente', valor: ev.client_name }, { icon: 'calendar', label: 'Data', valor: fmtPromoDataLonga(ev.event_date) }, { icon: 'clock', label: 'Horário', valor: horario }, { icon: 'map', label: 'Endereço', valor: endereco }, { icon: 'users', label: 'Traje', valor: pp?.dress_code }, { icon: 'phone', label: 'Coordenador', valor: ev.coordenador_contato || pp?.equipe?.coordenador?.nome }, { icon: 'wallet', label: 'Cachê', valor: fmtPromoMoney(escala.cache) + (escala.funcao ? ` · ${escala.funcao}` : '') }, { icon: 'budget', label: 'Prazo de pagamento', valor: escala.prazo_pagamento }, ].filter(l => l.valor); const jaConfirmou = !!escala.confirmado_em; const jaCheckIn = !!escala.check_in_em; const jaCheckOut = !!escala.check_out_em; return (
{ev.name || 'Ação'}
{escala.status}
{linhas.map(l => (
{l.label}
{l.valor}
))}
{briefing && (
Briefing
{briefing}
)} {/* Acoes */} {!jaConfirmou && !jaCheckOut && ( chamarRpc('promotor_confirmar_presenca', { p_escala_id: escala.id }, 'Presença confirmada!')}> Confirmar presença )} {jaConfirmou && !jaCheckIn && ( chamarRpc('promotor_check_in', { p_escala_id: escala.id }, 'Check-in registrado!')}> Fazer check-in )} {jaCheckIn && !jaCheckOut && ( setCheckoutOpen(true)}> Fazer check-out )} {jaCheckOut && (
Ação encerrada em {fmtPromoHora(escala.check_out_em)}
)} {(escala.confirmado_em || escala.check_in_em) && (
{escala.confirmado_em && <>Confirmou {fmtPromoHora(escala.confirmado_em)}} {escala.check_in_em && <> · Check-in {fmtPromoHora(escala.check_in_em)}}
)}
{fotos.length > 0 && (
Fotos enviadas ({fotos.length})
{fotos.map(f => ( ))}
)} {/* Check-out — formulario curto */} setCheckoutOpen(false)} title="Check-out" width={420}>
A ação foi realizada?
setCo(c => ({ ...c, realizada: true }))}>Sim setCo(c => ({ ...c, realizada: false }))}>Não
Houve algum problema?
setCo(c => ({ ...c, houve_problema: false }))}>Não setCo(c => ({ ...c, houve_problema: true }))}>Sim
{co.houve_problema && ( setCo(c => ({ ...c, problema_desc: v }))} placeholder="Descreva rapidamente" /> )} setCo(c => ({ ...c, observacao: v }))} placeholder="Algo que a produção precisa saber" />
setCheckoutOpen(false)}>Cancelar {busy ? 'Enviando…' : 'Encerrar ação'}
); } Object.assign(window, { PromotorApp });