// app.jsx — UI do CheckIngresso · Parceiros em React (sem build: React UMD // + Babel standalone). Painéis por papel: admin (produtores) · produtor // (eventos + empresas) · empresa (participantes). Visual laranja. const { useState, useEffect, useCallback } = React; // ---------- helpers ---------- function toast(msg, err) { let t = document.getElementById('toast'); if (!t) { t = document.createElement('div'); t.id = 'toast'; document.body.appendChild(t); } t.textContent = msg; t.className = 'toast' + (err ? ' err' : ''); t.hidden = false; clearTimeout(t._t); t._t = setTimeout(() => { t.hidden = true; }, 3200); } async function tokenIA(rotulo) { try { const r = await API.criarToken(rotulo); window.prompt('Token de API (copie agora — não aparece de novo):', r.token); } catch (e) { toast(e.message, true); } } function CotaPill({ e }) { if (e.cota <= 0) return {e.usados} · ∞; const cls = e.usados >= e.cota ? 'full' : (e.usados / e.cota > 0.8 ? 'warn' : 'ok'); return {e.usados}/{e.cota}; } function Topbar({ eu, left, onSair }) { const [brand, setBrand] = useState(null); useEffect(() => { API.branding().then(setBrand).catch(() => {}); }, []); return (
CheckIngressoParceiros
{brand && brand.produtor_logo ? ( <> {brand.produtor_nome} ) : null} {left} {(eu.nome || eu.login)} · {eu.papel}
); } // ---------- App ---------- function App() { const cadastroPid = new URLSearchParams(window.location.search).get('cadastro'); const [eu, setEu] = useState(undefined); // undefined=loading · null=deslogado useEffect(() => { if (!cadastroPid) API.me().then(setEu).catch(() => setEu(null)); }, [cadastroPid]); const onSair = async () => { try { await API.logout(); } catch (_) {} setEu(null); }; if (cadastroPid) return ; if (eu === undefined) return
carregando…
; if (!eu) return ; if (eu.papel === 'admin') return ; if (eu.papel === 'produtor') return ; return ; } // ---------- Cadastro público de empresa (via link do produtor) ---------- function CadastroEmpresa({ produtorId }) { const [info, setInfo] = useState(undefined); // undefined=loading · null=link inválido const [enviado, setEnviado] = useState(false); const [busy, setBusy] = useState(false); useEffect(() => { API.cadastroInfo(produtorId).then(setInfo).catch(() => setInfo(null)); }, [produtorId]); const submit = async (e) => { e.preventDefault(); const f = e.target; setBusy(true); try { await API.solicitarCadastro({ produtor_id: Number(produtorId), nome: f.nome.value.trim(), contato: f.contato.value.trim(), login: f.login.value.trim(), senha: f.senha.value }); setEnviado(true); } catch (err) { toast(err.message, true); setBusy(false); } }; const Wrap = ({ children }) =>
{children}
; if (info === undefined) return
carregando…
; if (info === null) return
CheckIngresso
Link de cadastro inválido ou expirado.
; if (enviado) return (
CheckIngresso

Solicitação enviada ✓

Recebemos seu cadastro para {info.produtor}. Assim que for aprovado e sua cota definida, você poderá entrar com o usuário e senha que escolheu.

Ir para o login
); return (
CheckIngressoParceiros
Cadastro de parceiro · {info.produtor}

O produtor aprova o cadastro e define sua cota de participantes.

); } function Login({ onLogin }) { const [busy, setBusy] = useState(false); const submit = async (e) => { e.preventDefault(); setBusy(true); const f = e.target; try { onLogin(await API.login(f.login.value.trim(), f.senha.value)); } catch (err) { toast(err.message, true); setBusy(false); } }; return (
CheckIngresso
Portal de parceiros
); } // ---------- Relatório de participantes (admin/produtor) ---------- function BarChart({ data, color }) { if (!data.length) return
sem dados
; const max = Math.max(1, ...data.map(d => d.value)); return (
{data.map(d => (
{d.label}
{d.value}
))}
); } function agrupar(arr, key) { const m = {}; arr.forEach(d => { const k = d[key] || '—'; m[k] = (m[k] || 0) + 1; }); return Object.keys(m).map(label => ({ label, value: m[label] })).sort((a, b) => b.value - a.value); } function Relatorio() { const [dados, setDados] = useState(null); const [fEvento, setFEvento] = useState(''); const [fEmpresa, setFEmpresa] = useState(''); const [busca, setBusca] = useState(''); // Dados REAIS do sistema de eventos (eventos passados), não mock. useEffect(() => { API.relatorioAttendees().then(setDados).catch(e => toast(e.message, true)); }, []); if (dados == null) return
carregando dados do sistema de eventos…
; const eventos = [...new Set(dados.map(d => d.evento_nome))].sort(); const empresas = [...new Set(dados.map(d => d.empresa).filter(Boolean))].sort((a, b) => a.localeCompare(b)); const q = busca.trim().toLowerCase(); const filtrados = dados.filter(d => (!fEvento || d.evento_nome === fEvento) && (!fEmpresa || d.empresa === fEmpresa) && (!q || (d.nome + ' ' + d.sobrenome + ' ' + (d.email || '')).toLowerCase().includes(q)) ); const nEmpresas = new Set(filtrados.map(d => d.empresa).filter(Boolean)).size; const nEventos = new Set(filtrados.map(d => d.evento_nome)).size; const presentes = filtrados.filter(d => d.presente).length; const porEmpresa = agrupar(filtrados, 'empresa').slice(0, 12); const porEvento = agrupar(filtrados, 'evento_nome'); const exportarCSV = () => { const head = ['Nome', 'Sobrenome', 'Empresa', 'Evento', 'E-mail', 'Telefone', 'Pedido']; const linhas = filtrados.map(d => [d.nome, d.sobrenome, d.empresa, d.evento_nome, d.email, d.telefone, d.pedido]); const csv = [head, ...linhas].map(r => r.map(c => '"' + String(c == null ? '' : c).replace(/"/g, '""') + '"').join(';')).join('\n'); const blob = new Blob(['' + csv], { type: 'text/csv;charset=utf-8' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'participantes.csv'; a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 1000); }; return (

Relatório de participantes

Dados reais do sistema de eventos · {filtrados.length} de {dados.length} participantes

setBusca(e.target.value)} placeholder="nome ou e-mail" />
{filtrados.length}
Participantes
{nEmpresas}
Empresas
{nEventos}
Eventos
{presentes}
Check-ins

Participantes por empresa{porEmpresa.length >= 12 ? ' (top 12)' : ''}

Participantes por evento

Presença

{filtrados.length === 0 ?
Nenhum participante com esses filtros.
:
{filtrados.map((d, i) => ( ))}
NomeEmpresaEventoE-mailTelefone
{d.nome} {d.sobrenome}{d.empresa || '—'} {d.evento_nome}{d.email}{d.telefone}
}
); } // ---------- Check-in (admin/produtor) ---------- function Checkin() { const [dados, setDados] = useState(null); const [evento, setEvento] = useState(''); const [fEmpresa, setFEmpresa] = useState(''); const [fStatus, setFStatus] = useState(''); // '' | presentes | ausentes const [busca, setBusca] = useState(''); const [saving, setSaving] = useState({}); useEffect(() => { API.relatorioAttendees().then(setDados).catch(e => toast(e.message, true)); }, []); if (dados == null) return
carregando…
; const eventos = [...new Set(dados.map(d => d.evento_nome))].sort(); const ev = evento || (eventos.length === 1 ? eventos[0] : ''); const doEvento = dados.filter(d => d.evento_nome === ev); const empresas = [...new Set(doEvento.map(d => d.empresa).filter(Boolean))].sort((a, b) => a.localeCompare(b)); const q = busca.trim().toLowerCase(); const lista = doEvento.filter(d => (!fEmpresa || d.empresa === fEmpresa) && (fStatus === '' || (fStatus === 'presentes' ? d.presente : !d.presente)) && (!q || (d.nome + ' ' + d.sobrenome + ' ' + (d.email || '') + ' ' + (d.empresa || '')).toLowerCase().includes(q)) ); const presentes = doEvento.filter(d => d.presente).length; const toggle = async (d) => { const novo = !d.presente; setSaving(s => ({ ...s, [d.chave]: true })); try { await API.checkin(d.chave, novo); setDados(ds => ds.map(x => x.chave === d.chave ? { ...x, presente: novo } : x)); } catch (e) { toast(e.message, true); } finally { setSaving(s => ({ ...s, [d.chave]: false })); } }; return (

Check-in

Credenciamento no dia — marque a presença dos participantes reais do evento.

setBusca(e.target.value)} placeholder="nome, e-mail ou empresa" autoFocus />
{!ev ?
Escolha um evento pra iniciar o check-in.
: ( <>
{presentes}
Presentes
{doEvento.length - presentes}
Ausentes
{doEvento.length}
Total
{lista.length === 0 ?
Nenhum participante encontrado.
:
{lista.map(d => ( ))}
NomeEmpresa
{d.nome} {d.sobrenome}{d.presente ? ✓ presente : null} {d.empresa || '—'}
}
)}
); } // ---------- Histórico de alterações (admin/produtor) ---------- function Historico() { const [logs, setLogs] = useState(null); const [busca, setBusca] = useState(''); useEffect(() => { API.listarLogs().then(setLogs).catch(e => toast(e.message, true)); }, []); if (logs == null) return
carregando…
; const q = busca.trim().toLowerCase(); const filtrados = logs.filter(l => !q || (l.acao + ' ' + l.detalhe + ' ' + l.ator).toLowerCase().includes(q)); const fmt = (s) => { try { return new Date(s).toLocaleString('pt-BR'); } catch (_) { return s; } }; const destrutiva = (a) => /EXCL|REMOV|TRAVAD|rejeit|desativ/i.test(a); return (

Histórico de alterações

Quem fez o quê e quando. Exclusões ficam registradas aqui mesmo depois do registro sumir.

setBusca(e.target.value)} placeholder="ação, detalhe ou usuário" />
{filtrados.length === 0 ?
Nada no histórico ainda.
:
{filtrados.map(l => ( ))}
QuandoQuemAçãoDetalhe
{fmt(l.criado_em)} {l.ator}{l.papel ? {l.papel} : null} {l.acao} {l.detalhe}
}
); } // ---------- Admin: configuração de e-mail (SMTP) ---------- function ConfigSMTP() { const [cfg, setCfg] = useState(null); const [senha, setSenha] = useState(''); const [testEmail, setTestEmail] = useState(''); const [busy, setBusy] = useState(false); const load = () => API.getSMTP().then(c => { setCfg(c); setSenha(''); }).catch(e => toast(e.message, true)); useEffect(() => { load(); }, []); if (cfg == null) return
carregando…
; const set = (k, v) => setCfg(c => ({ ...c, [k]: v })); const payload = () => ({ host: cfg.host.trim(), porta: parseInt(cfg.porta || '587', 10), secure: !!cfg.secure, usuario: cfg.usuario.trim(), senha: senha, from_nome: cfg.from_nome.trim(), from_email: cfg.from_email.trim(), portal_url: cfg.portal_url.trim(), ativo: !!cfg.ativo }); const salvar = async (e) => { e.preventDefault(); setBusy(true); try { const c = await API.saveSMTP(payload()); setCfg(c); setSenha(''); toast('Configuração salva'); } catch (err) { toast(err.message, true); } finally { setBusy(false); } }; const testar = async () => { if (!testEmail.trim()) { toast('Informe um e-mail pra receber o teste', true); return; } setBusy(true); try { await API.testSMTP({ ...payload(), para: testEmail.trim() }); toast('E-mail de teste enviado para ' + testEmail.trim()); } catch (err) { toast(err.message, true); } finally { setBusy(false); } }; return (

E-mail (SMTP)

Servidor de envio do portal — usado no e-mail de boas-vindas quando uma empresa é aprovada.

Servidor

set('host', e.target.value)} placeholder="smtp.gmail.com" />
set('porta', e.target.value)} />
set('usuario', e.target.value)} placeholder="seu-email@gmail.com" autoComplete="off" />
setSenha(e.target.value)} placeholder={cfg.tem_senha ? '••••••••' : 'app password'} autoComplete="new-password" />

Remetente

set('from_nome', e.target.value)} placeholder="CheckIngresso Parceiros" />
set('from_email', e.target.value)} placeholder="contato@checkingresso.com.br" />
set('portal_url', e.target.value)} placeholder="https://parceiros.coanconsultoria.com.br/portal" />

Testar

setTestEmail(e.target.value)} placeholder="voce@gmail.com" />
O teste usa os dados acima (não precisa salvar antes).
); } // ---------- Admin: produtores ---------- function AdminPanel({ eu, onSair }) { const [produtores, setProdutores] = useState(null); const [drill, setDrill] = useState(null); // {id, nome} de um produtor const [aba, setAba] = useState('produtores'); const load = useCallback(() => { API.listarProdutores().then(setProdutores).catch(e => toast(e.message, true)); }, []); useEffect(() => { load(); }, [load]); if (drill) return { setDrill(null); load(); }} />; const criar = async (e) => { e.preventDefault(); const f = e.target; try { await API.criarProdutor({ nome: f.nome.value.trim(), login: f.login.value.trim(), senha: f.senha.value }); f.reset(); toast('Produtor cadastrado'); load(); } catch (err) { toast(err.message, true); } }; const abas = ( <> ); if (aba === 'relatorio') return
; if (aba === 'checkin') return
; if (aba === 'historico') return
; if (aba === 'smtp') return
; return (

Produtores

Organizadores de evento. Cada produtor cadastra os próprios eventos e empresas.

Novo produtor

{produtores == null ?
carregando…
: produtores.length === 0 ?
Nenhum produtor ainda.
:
{produtores.map(p => ( ))}
ProdutorEmpresas
{p.nome}{p.empresas}
}
); } function ProdutorHome({ eu, onSair }) { return ; } // ---------- Produtor: eventos + empresas ---------- function GestaoProdutor({ produtorId, titulo, ehAdmin, eu, onSair, onVoltar, logoInicial }) { const [empresaSel, setEmpresaSel] = useState(null); const [aba, setAba] = useState('gestao'); if (empresaSel) return setEmpresaSel(null)} />; const left = ( <> {onVoltar && } {!ehAdmin && <> } ); return (
{(!ehAdmin && aba === 'relatorio') ? : (!ehAdmin && aba === 'checkin') ? : (!ehAdmin && aba === 'historico') ? : ( <>

{titulo}

)}
); } function LogoCard({ produtorId, logoInicial }) { const [logo, setLogo] = useState(logoInicial); const [busy, setBusy] = useState(false); useEffect(() => { if (logoInicial === undefined) API.branding().then(b => setLogo(b.produtor_logo)).catch(() => {}); }, []); const enviar = async (e) => { const file = e.target.files[0]; if (!file) return; setBusy(true); try { const r = await API.uploadLogo(produtorId, file); setLogo(r.logo_url + '?t=' + Date.now()); toast('Logo atualizada'); } catch (err) { toast(err.message, true); } finally { setBusy(false); } }; return (

Identidade visual

A logo aparece no topo, ao lado da do CheckIngresso, pros seus parceiros.

{logo ? logo : sem logo ainda}
); } function Eventos({ produtorId, ehAdmin }) { const [eventos, setEventos] = useState(null); const [wpEventos, setWpEventos] = useState(null); // eventos do sistema de eventos (dropdown) const load = useCallback(() => { API.listarEventos(ehAdmin ? produtorId : null).then(setEventos).catch(e => toast(e.message, true)); }, [produtorId, ehAdmin]); useEffect(() => { load(); }, [load]); useEffect(() => { API.eventosWP().then(setWpEventos).catch(() => setWpEventos([])); }, []); const onPickWp = (e) => { const opt = e.target.selectedOptions[0]; const nome = opt ? opt.getAttribute('data-nome') : ''; const f = e.target.form; if (f) f.nome.value = nome || ''; // o nome acompanha o evento escolhido (editável) }; const criar = async (e) => { e.preventDefault(); const f = e.target; const wp = (f.wp && f.wp.value) || ''; if (wpEventos && wpEventos.length > 0 && !wp) { toast('Escolha o evento do sistema de eventos.', true); return; } const payload = { nome: f.nome.value.trim(), wp_event_id: wp }; if (ehAdmin) payload.produtor_id = produtorId; try { await API.criarEvento(payload); f.reset(); toast('Evento criado'); load(); } catch (err) { toast(err.message, true); } }; const ativar = async (id) => { try { await API.ativarEvento(id); toast('Inscrições abertas'); load(); } catch (e) { toast(e.message, true); } }; const fechar = async (id) => { if (!confirm('Fechar as inscrições? Não aceita novos cadastros, mas as empresas seguem editando/removendo quem já cadastraram.')) return; try { await API.fecharEvento(id); toast('Inscrições fechadas (edição segue liberada)'); load(); } catch (e) { toast(e.message, true); } }; const travar = async (id) => { if (!confirm('Travar a edição? Congela editar e remover participantes neste evento.')) return; try { await API.travarEdicao(id); toast('Edição travada'); load(); } catch (e) { toast(e.message, true); } }; const liberar = async (id) => { try { await API.liberarEdicao(id); toast('Edição liberada'); load(); } catch (e) { toast(e.message, true); } }; return (

Eventos

Você pode manter vários eventos abertos ao mesmo tempo. Dois controles independentes por evento: inscrições (aceitar novos cadastros) e edição (mexer em quem já cadastrou). Fechar inscrições não trava a edição — pra congelar tudo, use Travar edição.

{wpEventos && wpEventos.length > 0 ? (
Todo evento é vinculado ao sistema de eventos — é o que gera os ingressos. Escolher já puxa nome, criativo, data e local.
) : null}
{eventos == null ?
carregando…
: eventos.length === 0 ?
Nenhum evento. Crie um e ative pra abrir inscrições.
:
{eventos.map(ev => ( ))}
EventoInscriçõesEdição
{ev.nome}{ev.wp_event_id ? 🔗 vinculado : avulso} {ev.ativo ? abertas : fechadas} {ev.edicao_travada ? travada : liberada}
{ev.ativo ? : } {ev.edicao_travada ? : }
}
); } function Empresas({ produtorId, ehAdmin, onAbrir }) { const [empresas, setEmpresas] = useState(null); const [solics, setSolics] = useState([]); const load = useCallback(() => { API.listarEmpresas().then(es => setEmpresas(ehAdmin ? es.filter(e => e.produtor_id === produtorId) : es)).catch(e => toast(e.message, true)); API.listarSolicitacoes().then(ss => setSolics(ehAdmin ? ss.filter(e => e.produtor_id === produtorId) : ss)).catch(() => {}); }, [produtorId, ehAdmin]); useEffect(() => { load(); }, [load]); const criar = async (e) => { e.preventDefault(); const f = e.target; const payload = { nome: f.nome.value.trim(), cota: parseInt(f.cota.value || '0', 10), login: f.login.value.trim(), senha: f.senha.value }; if (ehAdmin) payload.produtor_id = produtorId; try { await API.criarEmpresa(payload); f.reset(); f.cota.value = 0; toast('Empresa cadastrada'); load(); } catch (err) { toast(err.message, true); } }; const novaCota = async (e) => { const v = window.prompt('Nova cota (0 = ilimitada):', e.cota); if (v === null) return; try { await API.atualizarEmpresa(e.id, { cota: parseInt(v, 10) }); toast('Cota atualizada'); load(); } catch (err) { toast(err.message, true); } }; const aprovar = async (e) => { const v = window.prompt('Aprovar "' + e.nome + '" com qual cota? (0 = ilimitada)', '0'); if (v === null) return; try { await API.aprovarEmpresa(e.id, parseInt(v || '0', 10)); toast('Empresa aprovada'); load(); } catch (err) { toast(err.message, true); } }; const rejeitar = async (e) => { if (!confirm('Rejeitar e apagar a solicitação de "' + e.nome + '"?')) return; try { await API.rejeitarEmpresa(e.id); toast('Solicitação rejeitada'); load(); } catch (err) { toast(err.message, true); } }; const desativar = async (e) => { if (!confirm('Desativar "' + e.nome + '"? Ela não vai mais conseguir entrar nem cadastrar. Dá pra reativar depois.')) return; try { await API.desativarEmpresa(e.id); toast('Empresa desativada'); load(); } catch (err) { toast(err.message, true); } }; const reativar = async (e) => { try { await API.ativarEmpresa(e.id); toast('Empresa reativada'); load(); } catch (err) { toast(err.message, true); } }; const excluir = async (e) => { if (!confirm('EXCLUIR "' + e.nome + '" e TODOS os participantes dela? Os ingressos no sistema de eventos serão cancelados. Esta ação não dá pra desfazer.\n\n(Pra só impedir o acesso sem apagar, use Desativar.)')) return; if (!confirm('Tem certeza? Digite OK no próximo aviso pra confirmar a exclusão de "' + e.nome + '".')) return; try { await API.excluirEmpresa(e.id); toast('Empresa excluída'); load(); } catch (err) { toast(err.message, true); } }; const linkCadastro = window.location.origin + window.location.pathname + '?cadastro=' + produtorId; const copiarLink = () => { navigator.clipboard.writeText(linkCadastro).then(() => toast('Link copiado')).catch(() => window.prompt('Copie o link:', linkCadastro)); }; const aprovadas = empresas ? empresas.filter(e => e.status !== 'pendente') : null; return (

Empresas parceiras

🔗 Link de cadastro: envie pros parceiros se cadastrarem sozinhos (você aprova e define a cota).
{solics.length > 0 ? (

Solicitações pendentes ({solics.length})

{solics.map(e => ( ))}
EmpresaContato
{e.nome}{e.contato || '—'}
) : null}
Cota 0 = ilimitada. A cota vale por evento — a empresa pode usar a cota cheia em cada evento aberto.
{aprovadas == null ?
carregando…
: aprovadas.length === 0 ?
Nenhuma empresa ainda.
:
{aprovadas.map(e => ( ))}
EmpresaCota · uso total
{e.nome} {e.ativa === false ? inativa : null} {e.wp_customer_id ? WP : null}
{e.ativa === false ? : }
}
); } // ---------- Modal + formulário de participante ---------- function Modal({ titulo, onClose, children }) { useEffect(() => { const h = (e) => { if (e.key === 'Escape') onClose(); }; document.addEventListener('keydown', h); return () => document.removeEventListener('keydown', h); }, [onClose]); return (
{ if (e.target === e.currentTarget) onClose(); }}>

{titulo}

{children}
); } function ParticipanteForm({ inicial, onSalvar, onCancelar, salvando }) { const submit = (e) => { e.preventDefault(); const f = e.target; onSalvar({ nome: f.nome.value.trim(), sobrenome: f.sobrenome.value.trim(), email: f.email.value.trim(), telefone: f.telefone.value.trim(), }); }; return (
); } // ---------- Empresa: participantes ---------- function EmpresaHome({ eu, onSair }) { const [empresa, setEmpresa] = useState(null); useEffect(() => { API.obterEmpresa(eu.empresa_id).then(setEmpresa).catch(e => toast(e.message, true)); }, [eu.empresa_id]); if (!empresa) return
carregando…
; return ; } function Participantes({ empresa, eu, onSair, onVoltar }) { const [eventos, setEventos] = useState(null); // null=loading · []=nenhum · [{evento,cota,usados,restante,pode_cadastrar,pode_editar}] const [selId, setSelId] = useState(null); // id do evento escolhido const [lista, setLista] = useState(null); const [modal, setModal] = useState(null); // null | { participante:

|null } const [salvando, setSalvando] = useState(false); const [info, setInfo] = useState(null); // detalhes do evento (criativo/data/local) const carregarEventos = useCallback(async () => { try { const evs = await API.eventosDaEmpresa(empresa.id); setEventos(evs); setSelId(prev => { if (prev && evs.some(x => x.evento.id === prev)) return prev; // mantém escolha return evs.length === 1 ? evs[0].evento.id : null; // 1 evento: auto-seleciona }); } catch (e) { toast(e.message, true); setEventos([]); } }, [empresa.id]); useEffect(() => { carregarEventos(); }, [carregarEventos]); const carregarLista = useCallback(async () => { if (!selId) { setLista(null); return; } try { setLista(await API.listarParticipantes(empresa.id, selId)); } catch (e) { toast(e.message, true); } }, [empresa.id, selId]); useEffect(() => { carregarLista(); }, [carregarLista]); // detalhes de vitrine do evento (criativo, data, local) do sistema de eventos useEffect(() => { setInfo(null); if (!selId) return; API.eventoInfo(selId).then(r => setInfo(r && r.info ? r.info : null)).catch(() => {}); }, [selId]); const item = eventos && selId ? eventos.find(x => x.evento.id === selId) : null; const evento = item ? item.evento : null; const cota = item && item.cota != null ? item.cota : empresa.cota; const usados = lista ? lista.length : (item && item.usados != null ? item.usados : 0); const cheia = cota > 0 && usados >= cota; const fechado = eventos != null && eventos.length === 0; const podeCadastrar = item ? item.pode_cadastrar : false; const podeEditar = item ? item.pode_editar : false; const salvar = async (d) => { setSalvando(true); try { if (modal && modal.participante) { await API.editarParticipante(modal.participante.id, d); toast('Participante atualizado'); } else { await API.cadastrarParticipante(empresa.id, { ...d, evento_id: selId }); toast('Participante cadastrado'); } setModal(null); await Promise.all([carregarLista(), carregarEventos()]); } catch (err) { toast(err.message, true); } finally { setSalvando(false); } }; const excluir = async (p) => { if (!confirm('Excluir este participante? O ingresso dele também será cancelado no sistema de eventos.')) return; try { await API.removerParticipante(p.id); toast('Excluído'); await Promise.all([carregarLista(), carregarEventos()]); } catch (e) { toast(e.message, true); } }; const reemitir = async (p) => { try { const r = await API.reemitirParticipante(p.id); toast('Ingresso reenviado' + (r && r.email ? ' para ' + r.email : '')); } catch (e) { toast(e.message, true); } }; const cotaDe = (it) => ({ cota: it.cota != null ? it.cota : empresa.cota, usados: it.usados != null ? it.usados : 0 }); const statusEv = (ev) => ev.edicao_travada ? { cls: 'full', txt: 'travado' } : ev.ativo ? { cls: 'ok', txt: 'aberto' } : { cls: 'warn', txt: 'inscrições fechadas' }; return (

← Voltar} />

{empresa.nome}

{eventos == null ?
carregando…
: fechado ? (
⚠️ Nenhum evento disponível — não há evento aberto, nem evento fechado com participantes seus. Quando o produtor abrir um evento, o cadastro libera por aqui.
) : !selId ? (

Escolha o evento

Você tem mais de um evento. Selecione em qual quer trabalhar (os abertos aceitam novos cadastros; os fechados seguem editáveis até o produtor travar).

{eventos.map(it => { const st = statusEv(it.evento); return ( ); })}
) : ( <> {(() => { const st = statusEv(evento); const aviso = (evento && evento.edicao_travada) ? { txt: '🔒 Edição travada — esta lista está só para leitura.', cls: 'fechado' } : podeCadastrar ? { txt: cheia ? '🎟️ Cota cheia — não há vagas restantes neste evento.' : '🎟️ Inscrições abertas — cadastre os participantes da sua empresa.', cls: '' } : { txt: '✏️ Inscrições fechadas — você ainda pode editar e remover quem já cadastrou.', cls: '' }; return (
{info && info.imagem ?
: null}

{(evento && evento.nome) || (info && info.nome) || '…'}

{st.txt}
{eventos.length > 1 && }
{info && (info.data || info.local) ? (
{info.data ? 📅 {info.data} : null} {info.local ? 📍 {info.local} : null}
) : null}
{aviso.txt}
); })()}

Participantes

{podeCadastrar && } Exportar CSV
{lista == null ?
carregando…
: lista.length === 0 ?
{podeCadastrar ? <>Nenhum participante neste evento ainda. Clique em + Inserir participante. : 'Nenhum participante neste evento.'}
:
{lista.map(p => ( ))}
NomeE-mailTelefone
{p.nome} {p.sobrenome}{p.ticket_ref ? 🎟️ emitido : null} {p.email}{p.telefone}
{p.ticket_ref ? : null}
}
)}
{modal && ( setModal(null)}> {!modal.participante && cheia ?
Cota cheia neste evento — não há vagas restantes.
: setModal(null)} />}
)}
); } ReactDOM.createRoot(document.getElementById('root')).render();