// Indicadores da acao — lado da producao. Orcamento 310, item 3. // // Cada acao mede uma coisa: degustacao conta amostras, panfletagem conta // panfletos, ativacao conta leads. Por isso os campos do formulario de // resultados do app sao definidos AQUI, por evento, e nao fixos no codigo. // // Gravam em events.resultado_campos (jsonb): // [{"chave":"abordagens","label":"Pessoas abordadas","meta":500}] // A 'chave' e derivada do label e e o que fica salvo em event_escalas.resultados. // Ela nunca muda depois de criada: renomear o label nao perde o que ja foi // registrado em campo. const INDICADOR_LIMITE = 8; // 'Pessoas abordadas' -> 'pessoas_abordadas' function chaveDoLabel(texto) { const semAcento = String(texto || '') .normalize('NFD') .split('') .filter(c => { const n = c.charCodeAt(0); return n < 768 || n > 879; }) .join('') .toLowerCase(); let saida = semAcento.split('').map(c => { const ok = (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'); return ok ? c : '_'; }).join('').replace(/_+/g, '_'); while (saida.startsWith('_')) saida = saida.slice(1); while (saida.endsWith('_')) saida = saida.slice(0, -1); return saida.slice(0, 40); } function IndicadoresEventoCard({ event, showToast }) { const [campos, setCampos] = React.useState(() => (event && event.resultado_campos) || []); const [novo, setNovo] = React.useState({ label: '', meta: '' }); const [salvando, setSalvando] = React.useState(false); const adicionar = () => { const label = novo.label.trim(); if (!label) { showToast('Dê um nome ao indicador.', 'danger'); return; } if (campos.length >= INDICADOR_LIMITE) { showToast('Máximo de ' + INDICADOR_LIMITE + ' indicadores por ação.', 'danger'); return; } const chave = chaveDoLabel(label); if (!chave) { showToast('Use letras ou números no nome do indicador.', 'danger'); return; } if (campos.some(c => c.chave === chave)) { showToast('Já existe um indicador com esse nome.', 'danger'); return; } const meta = novo.meta.trim() ? Number(novo.meta.replace(',', '.')) : null; if (novo.meta.trim() && (!Number.isFinite(meta) || meta <= 0)) { showToast('A meta precisa ser um número maior que zero.', 'danger'); return; } const item = { chave, label }; if (meta) item.meta = meta; setCampos(prev => [...prev, item]); setNovo({ label: '', meta: '' }); }; const remover = (chave) => setCampos(prev => prev.filter(c => c.chave !== chave)); const salvar = async () => { setSalvando(true); const { error } = await sb.from('events') .update({ resultado_campos: campos }) .eq('id', event.id); setSalvando(false); if (error) { console.error('[IndicadoresEventoCard.salvar] Supabase error:', error); showToast('Erro ao salvar os indicadores.', 'danger'); return; } event.resultado_campos = campos; showToast('Indicadores salvos!'); }; return (
Indicadores desta ação
É o que o app pede no botão “Resultados”. Deixe vazio se esta ação não mede nada. A meta é opcional e aparece no acompanhamento ao vivo.
{campos.length > 0 && (
{campos.map(c => (
{c.label}
{c.chave}{c.meta ? ' · meta ' + Number(c.meta).toLocaleString('pt-BR') : ''}
remover(c.chave)}>Remover
))}
)}
setNovo(n => ({ ...n, label: v }))} placeholder="Ex: Pessoas abordadas" /> setNovo(n => ({ ...n, meta: v }))} placeholder="Ex: 500" /> Adicionar
{salvando ? 'Salvando…' : 'Salvar indicadores'}
); } Object.assign(window, { IndicadoresEventoCard, chaveDoLabel });