// Escalas — lado do admin. Orcamento 290. // // EscalaEventoModal -> escalar a equipe dentro do evento (funcao, cache, // prazo de pagamento) + acompanhar presenca/check-in. // PainelPromotores -> acompanhamento em tempo real de confirmacoes, // check-ins, fotos e check-outs. // // O promotor NAO altera estas linhas direto: ele usa as RPCs da migration 026. const ESCALA_FUNCOES = [ { value: 'Promotor', label: 'Promotor' }, { value: 'Supervisor', label: 'Supervisor' }, { value: 'Coordenador', label: 'Coordenador' }, { value: 'Motorista', label: 'Motorista' }, ]; const ESCALA_STATUS_VARIANT = { 'Convidado': 'default', 'Confirmado': 'primary', 'Recusado': 'danger', 'Check-in': 'warning', 'Concluído': 'success', 'Falta': 'danger', }; const fmtEscalaMoney = v => 'R$ ' + (parseFloat(v) || 0).toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); const fmtEscalaHora = iso => iso ? new Date(iso).toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }) : null; function EscalaEventoModal({ open, onClose, event }) { const [freelancers, setFreelancers] = React.useState([]); const [escalas, setEscalas] = React.useState([]); const [loading, setLoading] = React.useState(true); const [saving, setSaving] = React.useState(false); const [coordenador, setCoordenador] = React.useState(''); const [form, setForm] = React.useState({ freelancer_id: '', funcao: 'Promotor', cache: '', prazo_pagamento: '' }); const [toast, setToast] = React.useState(null); const showToast = (msg, type = 'success') => { setToast({ msg, type }); setTimeout(() => setToast(null), 3000); }; React.useEffect(() => { if (!open || !event?.id) return; setLoading(true); setCoordenador(event.coordenador_contato || ''); Promise.all([ sb.from('freelancers').select('id, name, role, rate, phone, app_ativo, auth_user_id').order('name'), sb.from('event_escalas').select('*').eq('event_id', event.id).order('created_at'), ]).then(([{ data: fl }, { data: es }]) => { setFreelancers(fl || []); setEscalas(es || []); setLoading(false); }); }, [open, event?.id]); const freelancerPorId = React.useMemo(() => { const map = {}; freelancers.forEach(f => { map[f.id] = f; }); return map; }, [freelancers]); // Ao escolher o freelancer, o cache ja vem preenchido com a diaria dele. const escolherFreelancer = (id) => { const f = freelancerPorId[id]; setForm(prev => ({ ...prev, freelancer_id: id, cache: f ? String(f.rate ?? '') : prev.cache })); }; const jaEscalados = React.useMemo(() => new Set(escalas.map(e => e.freelancer_id)), [escalas]); const handleEscalar = async () => { const f = freelancerPorId[form.freelancer_id]; if (!f) { showToast('Escolha um freelancer.', 'danger'); return; } setSaving(true); const { data, error } = await sb.from('event_escalas').insert({ event_id: event.id, freelancer_id: f.id, funcao: form.funcao, cache: parseFloat(String(form.cache).replace(',', '.')) || 0, prazo_pagamento: form.prazo_pagamento || null, }).select().single(); setSaving(false); if (error) { console.error('[EscalaEventoModal.handleEscalar] Supabase error:', error); showToast(error.code === '23505' ? 'Esse freelancer já está escalado neste evento.' : 'Erro ao escalar: ' + (error.message || 'desconhecido'), 'danger'); return; } setEscalas(prev => [...prev, data]); setForm({ freelancer_id: '', funcao: 'Promotor', cache: '', prazo_pagamento: form.prazo_pagamento }); showToast(`${f.name} escalado!`); }; const handleRemover = async (escala) => { const { error } = await sb.from('event_escalas').delete().eq('id', escala.id); if (error) { console.error('[EscalaEventoModal.handleRemover] Supabase error:', error); showToast('Erro ao remover.', 'danger'); return; } setEscalas(prev => prev.filter(e => e.id !== escala.id)); showToast('Removido da escala.'); }; const handleSalvarCoordenador = async () => { setSaving(true); const { error } = await sb.from('events').update({ coordenador_contato: coordenador || null }).eq('id', event.id); setSaving(false); if (error) { console.error('[EscalaEventoModal.handleSalvarCoordenador] Supabase error:', error); showToast('Erro ao salvar o contato.', 'danger'); return; } if (event) event.coordenador_contato = coordenador; showToast('Contato do coordenador salvo!'); }; const totalCache = escalas.reduce((s, e) => s + (parseFloat(e.cache) || 0), 0); return ( {loading ? : (
{/* Contato do coordenador — aparece no app do promotor */}
Salvar
{/* Escalar */}
Escalar freelancer
setForm(f => ({ ...f, funcao: v }))} options={ESCALA_FUNCOES} /> setForm(f => ({ ...f, cache: v }))} placeholder="Ex: 180" />
setForm(f => ({ ...f, prazo_pagamento: v }))} placeholder="Ex: até 5 dias úteis após a ação" /> {saving ? 'Salvando…' : 'Escalar'}
{/* Equipe escalada */}
Equipe escalada ({escalas.length})
{escalas.length > 0 && (
Total de cachês: {fmtEscalaMoney(totalCache)}
)}
{escalas.length === 0 ? : (
{escalas.map(e => { const f = freelancerPorId[e.freelancer_id]; return (
{f?.name || `Freelancer #${e.freelancer_id}`}
{e.funcao} · {fmtEscalaMoney(e.cache)} {e.prazo_pagamento ? ` · ${e.prazo_pagamento}` : ''} {f && !f.auth_user_id ? ' · sem acesso ao app' : ''}
{e.status} handleRemover(e)}>Remover
{(e.confirmado_em || e.check_in_em || e.check_out_em) && (
{e.confirmado_em && Confirmou {fmtEscalaHora(e.confirmado_em)}} {e.check_in_em && Check-in {fmtEscalaHora(e.check_in_em)}} {e.check_out_em && Check-out {fmtEscalaHora(e.check_out_em)}} {e.houve_problema && Problema: {e.problema_desc || 'relatado'}}
)}
); })}
)}
Fechar
)} {toast && setToast(null)} />}
); } // ─────────────────────────────────────────────────────────── // Acompanhamento em tempo real — confirmacoes, check-ins, fotos, check-outs. // ─────────────────────────────────────────────────────────── function PainelPromotores() { const [linhas, setLinhas] = React.useState([]); const [fotos, setFotos] = React.useState([]); const [loading, setLoading] = React.useState(true); const carregar = React.useCallback(async () => { const [{ data: es }, { data: fo }] = await Promise.all([ sb.from('event_escalas') .select('*, events(name, event_date), freelancers(name)') .order('created_at', { ascending: false }) .limit(100), sb.from('escala_fotos').select('*').order('enviada_em', { ascending: false }).limit(60), ]); setLinhas(es || []); setFotos(fo || []); setLoading(false); }, []); React.useEffect(() => { carregar(); const channel = sb.channel('escalas-painel') .on('postgres_changes', { event: '*', schema: 'public', table: 'event_escalas' }, carregar) .on('postgres_changes', { event: '*', schema: 'public', table: 'escala_fotos' }, carregar) .subscribe(); return () => { sb.removeChannel(channel); }; }, [carregar]); const fotosPorEscala = React.useMemo(() => { const map = {}; fotos.forEach(f => { (map[f.escala_id] = map[f.escala_id] || []).push(f); }); return map; }, [fotos]); const stats = React.useMemo(() => ({ confirmados: linhas.filter(l => l.confirmado_em).length, checkins: linhas.filter(l => l.check_in_em).length, checkouts: linhas.filter(l => l.check_out_em).length, problemas: linhas.filter(l => l.houve_problema).length, }), [linhas]); if (loading) return ; return (
{[ { label: 'Presenças confirmadas', value: stats.confirmados, color: DS.colors.primary }, { label: 'Check-ins', value: stats.checkins, color: DS.colors.warning }, { label: 'Check-outs', value: stats.checkouts, color: DS.colors.success }, { label: 'Com problema', value: stats.problemas, color: DS.colors.danger }, ].map(s => (
{s.label}
{s.value}
))}
{linhas.length === 0 ? : (
{linhas.map((l, i) => { const fs = fotosPorEscala[l.id] || []; return (
{l.freelancers?.name || '—'}
{l.events?.name || '—'} · {l.funcao} · {fmtEscalaMoney(l.cache)}
{fs.length > 0 && ( {fs.length} )} {l.status}
{(l.confirmado_em || l.check_in_em || l.check_out_em || l.houve_problema) && (
{l.confirmado_em && Confirmou {fmtEscalaHora(l.confirmado_em)}} {l.check_in_em && Check-in {fmtEscalaHora(l.check_in_em)}} {l.check_out_em && Check-out {fmtEscalaHora(l.check_out_em)}} {l.houve_problema && Problema: {l.problema_desc || 'relatado'}} {l.observacao && Obs: {l.observacao}}
)} {fs.length > 0 && (
{fs.slice(0, 8).map(f => ( ))}
)}
); })}
)}
); } Object.assign(window, { EscalaEventoModal, PainelPromotores, ESCALA_FUNCOES, ESCALA_STATUS_VARIANT });