// ========== CONFIG ========== const SUPABASE_URL = 'https://ftujctzhsitfdlctbohr.supabase.co'; const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImZ0dWpjdHpoc2l0ZmRsY3Rib2hyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Njg0MDQzNzcsImV4cCI6MjA4Mzk4MDM3N30.NivluvmqGq7Jc-zpZHsCdjUxC4GzWiAWoR4Ee0N78GI'; const supabaseClient = window.supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY); const MATRIZ_SIZE = 490; const MATRIZ_PADDING = 5; const DRAW_SIZE = MATRIZ_SIZE - (MATRIZ_PADDING * 2); const RAIO_PONTO = 20; const RAIO_PG = 24; const RAIO_T = 14; const MIN_COORDS_FOR_CLUSTER = 15; let currentCompany = null; let currentUser = null; let matrizData = []; // Individuais let activeLabels = {}; let uniqueLabelsMap = {}; const predefinedColors = ['#FF5733', '#33FF57', '#5733FF', '#F6D833', '#33F6D8', '#D833F6', '#FF335E', '#5E33FF', '#3EFF33', '#33FFAE']; let labelColors = {}; let colorIndex = 0; let allIndividuais = []; let pendingLinks = []; let selectedBatchFilter = 'all'; // Partnership badges const PARTNERSHIP_BADGES = { alpha: 'https://cdn.prod.website-files.com/62ff7cf45599abc365c6f4e1/684c70512a71d13fe58fe194_alpha.avif', beta: 'https://cdn.prod.website-files.com/62ff7cf45599abc365c6f4e1/684c7051f1fc0627c9f73e7f_beta.avif', gamma: 'https://cdn.prod.website-files.com/62ff7cf45599abc365c6f4e1/684c70510819bb69688219ba_gamma.avif' }; // Batches let batches = []; let batchData = {}; let activeBatches = {}; let batchNameMap = {}; // Modal let amostraAtiva = true; let tipoSelecionado = null; // ========== HELPERS ========== function getRandomColor() { const l = '0123456789ABCDEF'; let c = '#'; for (let i = 0; i < 6; i++) c += l[Math.floor(Math.random() * 16)]; return c; } function getColorForLabel(id) { if (!labelColors[id]) labelColors[id] = colorIndex < predefinedColors.length ? predefinedColors[colorIndex++] : getRandomColor(); return labelColors[id]; } function calcularPosicaoSVG(f, e, c, a) { f = parseFloat(f) || 0; e = parseFloat(e) || 0; c = parseFloat(c) || 0; a = parseFloat(a) || 0; const normalX = (c - a) / 100; const normalY = (f - e) / 100; return { svgX: MATRIZ_PADDING + (DRAW_SIZE / 2) + (normalX * DRAW_SIZE / 2), svgY: MATRIZ_PADDING + (DRAW_SIZE / 2) - (normalY * DRAW_SIZE / 2) }; } // ========== LABEL PARSER ========== // Interpreta o campo label do banco para propriedades de renderização function parseLabel(label) { if (!label) return { type: 'pura' }; let str = label.trim(); // Aspiracional if (str === 'as' || str.startsWith('as-')) return { type: 'as' }; // pG manual if (/^pG/i.test(str)) return { type: 'pG', variant: str }; // Tendência funcional: T-FECA ou T if (/^T$/i.test(str) || /^T-/i.test(str)) return { type: 'T', variant: str }; // mD (média) if (/^mD/i.test(str)) return { type: 'mD' }; // Extrair grupo [...] let group = null; const gm = str.match(/^\[([^\]]+)\]/); if (gm) { group = gm[1]; str = str.substring(gm[0].length); } // Extrair indicador {X} let indicator = null; const im = str.match(/^\{([ROYGB])\}/i); if (im) { indicator = im[1].toUpperCase(); str = str.substring(im[0].length); } // Extrair raio (r=X) let radius = 1; const rm = str.match(/\(r=(\d+)\)/); if (rm) { radius = parseInt(rm[1]); str = str.replace(rm[0], ''); } // O que sobra é o nome do label const name = str.replace(/-$/, '').trim() || 'label'; const toggleKey = group || name; const displayName = group || name; return { type: 'labeled', name, group, indicator, radius, toggleKey, displayName }; } const INDICATOR_COLORS = { R: '#ef4444', O: '#f97316', Y: '#eab308', G: '#22c55e', B: '#3b82f6' }; // ========== INIT ========== async function initDash() { try { const { data: { session }, error: se } = await supabaseClient.auth.getSession(); if (se || !session) { window.location.href = '/dash/login'; return; } currentUser = session.user; const { data: cd, error: ce } = await supabaseClient.rpc('get_current_company'); if (ce || !cd.success) { await supabaseClient.auth.signOut(); window.location.href = '/dash/login'; return; } currentCompany = cd.company; // Buscar campos de parceria diretamente (RPC pode não retornar colunas novas) try { const { data: extra } = await supabaseClient .from('companies') .select('partnership_level, discount_percentage') .eq('id', currentCompany.id) .single(); if (extra) { currentCompany.partnership_level = extra.partnership_level || null; currentCompany.discount_percentage = extra.discount_percentage || 0; } } catch(e) { /* colunas podem não existir ainda */ } updateHeader(); loadNotifications(); await loadBatchesAndRespondents(); initModal(); document.getElementById('loading-overlay').classList.add('hidden'); } catch (e) { console.error('Init error:', e); window.location.href = '/dash/login'; } } function updateHeader() { const d = (currentCompany.monthly_credits - currentCompany.monthly_credits_used) + currentCompany.extra_credits; document.getElementById('credits-total').textContent = d; // Tornar credits-display clicável como link para a loja const creditsEl = document.querySelector('.credits-display'); if (creditsEl) { creditsEl.style.cursor = 'pointer'; creditsEl.style.transition = 'opacity 0.2s'; creditsEl.title = 'Comprar créditos'; creditsEl.onmouseenter = () => creditsEl.style.opacity = '0.7'; creditsEl.onmouseleave = () => creditsEl.style.opacity = '1'; creditsEl.onclick = () => window.location.href = '/dash/loja'; } // Nome da empresa + badge de parceria (ao lado) const nameEl = document.getElementById('user-name'); const level = currentCompany.partnership_level; if (level && PARTNERSHIP_BADGES[level]) { nameEl.style.display = 'inline-flex'; nameEl.style.alignItems = 'center'; nameEl.style.gap = '8px'; nameEl.innerHTML = `${currentCompany.name}${level}`; } else { nameEl.textContent = currentCompany.name; } } // ========== NOTIFICATIONS ========== async function loadNotifications() { const { data: n, error } = await supabaseClient .from('notifications').select('*') .eq('company_id', currentCompany.id) .order('created_at', { ascending: false }).limit(20); if (error) return; const uc = n.filter(x => !x.read).length; const b = document.getElementById('notification-badge'); b.textContent = uc; b.classList.toggle('show', uc > 0); const l = document.getElementById('notifications-list'); if (n.length === 0) { l.innerHTML = '

Nenhuma notificação

'; return; } l.innerHTML = n.map(x => `

${x.title}

${x.message || ''}

`).join(''); } // ========== LOAD DATA ========== async function loadBatchesAndRespondents() { // Carregar batches (inclui name e diagnostico_url) const { data: batchesData, error: batchError } = await supabaseClient .from('diagnosis_batches').select('*') .eq('company_id', currentCompany.id) .order('diagnosis_date', { ascending: false }); batches = (!batchError && batchesData) ? batchesData : []; // Mapa de nomes batchNameMap = {}; batches.forEach(b => { batchNameMap[b.id] = b.name || b.label; }); // Carregar respondentes const { data: resp, error: respError } = await supabaseClient .from('respondents').select('*') .eq('company_id', currentCompany.id) .order('completed_at', { ascending: false }); if (respError) { console.error('Erro respondentes:', respError); return; } // Carregar links pendentes (ativos, não respondidos) const { data: linksData } = await supabaseClient .from('assessment_links') .select('id, token, respondent_name, batch_id, status, created_at') .eq('company_id', currentCompany.id) .eq('status', 'active') .order('created_at', { ascending: false }); pendingLinks = linksData || []; // Separar: individuais (tem main_style) vs coordenadas (não tem) const individuais = resp.filter(r => r.main_style); const coords = resp.filter(r => !r.main_style); allIndividuais = individuais; // Inicializar labels individuais: TUDO OFF individuais.forEach(r => { if (!activeLabels.hasOwnProperty(r.id)) activeLabels[r.id] = false; }); // Inicializar dados por batch batchData = {}; batches.forEach(batch => { batchData[batch.id] = { id: batch.id, name: batch.name || null, label: batch.label, color: batch.color || '#3B82F6', type: batch.type, diagnostico_url: batch.diagnostico_url || null, puras: [], aspiracionais: [], labeled: [], pGs: [], tendencias: [] }; // Inicializar estado: BATCH OFF, sub-toggles ON (quando ligar o batch, tudo aparece) if (!activeBatches[batch.id]) { activeBatches[batch.id] = { visible: false, showAs: true, showCluster: true, labels: {} }; } }); // Distribuir coordenadas nos batches coords.forEach(r => { if (r.coord_flexible == null) return; const batchId = r.batch_id; if (!batchId || !batchData[batchId]) return; const coord = { f: r.coord_flexible, e: r.coord_stable, c: r.coord_independent, a: r.coord_interdependent }; const parsed = parseLabel(r.label); if (parsed.type === 'pura') { batchData[batchId].puras.push(coord); } else if (parsed.type === 'as') { batchData[batchId].aspiracionais.push(coord); } else if (parsed.type === 'pG') { batchData[batchId].pGs.push({ ...coord, label: r.label, parsed }); } else if (parsed.type === 'T') { batchData[batchId].tendencias.push({ ...coord, label: r.label, parsed }); } else { batchData[batchId].labeled.push({ ...coord, label: r.label, parsed }); } }); // Renderizar lista renderRespondentsList(individuais); // Preparar pontos individuais para matriz const pontos = []; uniqueLabelsMap = generateUniqueLabels(individuais); individuais.forEach(r => { if (r.coord_flexible != null) { const lb = uniqueLabelsMap[r.id] || { shortLabel: r.name.split(' ')[0] }; pontos.push({ f: r.coord_flexible, e: r.coord_stable, c: r.coord_independent, a: r.coord_interdependent, id: r.id, shortLabel: lb.shortLabel, tipo: 'individual' }); } }); drawMatriz(pontos); } // ========== RESPONDENTES LIST ========== function renderRespondentsList(individuais) { const list = document.getElementById('respondentes-list'); // Merge: respondentes concluídos + links pendentes const allItems = []; // Concluídos individuais.forEach(r => { allItems.push({ type: 'completed', data: r, batch_id: r.batch_id, name: r.name, sortDate: r.completed_at }); }); // Pendentes pendingLinks.forEach(l => { allItems.push({ type: 'pending', data: l, batch_id: l.batch_id, name: l.respondent_name || 'Sem nome', sortDate: l.created_at }); }); // Filtrar por batch selecionado let filtered; if (selectedBatchFilter === 'all') { filtered = allItems; } else if (selectedBatchFilter === 'none') { filtered = allItems.filter(i => !i.batch_id); } else { filtered = allItems.filter(i => i.batch_id === selectedBatchFilter); } // Contagem const totalCount = filtered.length; const pendingCount = filtered.filter(i => i.type === 'pending').length; const countLabel = pendingCount > 0 ? `${totalCount} individual${totalCount !== 1 ? 'is' : ''} · ${pendingCount} pendente${pendingCount !== 1 ? 's' : ''}` : `${totalCount} individual${totalCount !== 1 ? 'is' : ''}`; document.getElementById('respondentes-count').textContent = countLabel; // Montar dropdown de filtro const batchesComItens = {}; allItems.forEach(i => { if (i.batch_id && batchNameMap[i.batch_id]) { if (!batchesComItens[i.batch_id]) batchesComItens[i.batch_id] = 0; batchesComItens[i.batch_id]++; } }); const temSemBatch = allItems.some(i => !i.batch_id); const temFiltro = Object.keys(batchesComItens).length > 0 || temSemBatch; let filterHtml = ''; if (temFiltro && allItems.length > 1) { filterHtml = `
`; } if (filtered.length === 0) { list.innerHTML = filterHtml + `

${selectedBatchFilter !== 'all' ? 'Nenhum individual neste grupo.' : 'Nenhum individual ainda.
Gere um link para começar.'}

`; return; } // Ordenar: pendentes primeiro, depois por data desc filtered.sort((a, b) => { if (a.type === 'pending' && b.type !== 'pending') return -1; if (a.type !== 'pending' && b.type === 'pending') return 1; return new Date(b.sortDate) - new Date(a.sortDate); }); list.innerHTML = filterHtml + filtered.map(item => { if (item.type === 'pending') { const l = item.data; const batchDisplay = l.batch_id && batchNameMap[l.batch_id] ? batchNameMap[l.batch_id] : 'Sem batch'; const linkUrl = `https://www.qore.me/ind?t=${l.token}`; return `
${l.respondent_name || 'Sem nome'}
⏳ ${batchDisplay} · não respondido
`; } else { const r = item.data; const lb = uniqueLabelsMap[r.id] || { shortLabel: r.name.split(' ')[0] }; const ia = activeLabels[r.id] === true; const co = getColorForLabel(r.id); const rl = r.report_url || `/dash/relatorio/${r.id}`; const ie = r.report_url && r.report_url.startsWith('http'); const batchDisplay = r.batch_id && batchNameMap[r.batch_id] ? batchNameMap[r.batch_id] : (r.assessment_type === 'qore_scan' ? 'Qore Scan' : r.assessment_type === 'manual' ? 'Manual' : r.assessment_type || 'Individual'); return `
${r.name}
${batchDisplay} • ${formatDate(r.completed_at)}
${r.main_style}
`; } }).join(''); } function filterByBatch(value) { selectedBatchFilter = value; renderRespondentsList(allIndividuais); } function copyPendingLink(url, btn) { navigator.clipboard.writeText(url).then(() => { btn.textContent = 'copiado!'; btn.style.background = 'rgba(16,185,129,0.3)'; setTimeout(() => { btn.textContent = 'copiar link'; btn.style.background = 'rgba(16,185,129,0.15)'; }, 2000); }).catch(() => { // Fallback const ta = document.createElement('textarea'); ta.value = url; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); btn.textContent = 'copiado!'; setTimeout(() => { btn.textContent = 'copiar link'; }, 2000); }); } async function deleteIndividualLink(linkId, name) { if (!confirm(`Excluir link de "${name}"?\n\nO link será invalidado e os 10 créditos serão devolvidos.`)) return; // Atualizar UI imediatamente (optimistic) pendingLinks = pendingLinks.filter(l => l.id !== linkId); renderRespondentsList(allIndividuais); // DB em background try { const INDIVIDUAL_COST = 10; const deletePromise = supabaseClient .from('assessment_links') .delete() .eq('id', linkId); const monthlyUsed = currentCompany.monthly_credits_used; const canReturnMonthly = Math.min(INDIVIDUAL_COST, monthlyUsed); const returnExtra = INDIVIDUAL_COST - canReturnMonthly; const promises = [deletePromise]; if (canReturnMonthly > 0) { promises.push(supabaseClient.from('companies') .update({ monthly_credits_used: monthlyUsed - canReturnMonthly }) .eq('id', currentCompany.id)); currentCompany.monthly_credits_used -= canReturnMonthly; } if (returnExtra > 0) { promises.push(supabaseClient.from('companies') .update({ extra_credits: currentCompany.extra_credits + returnExtra }) .eq('id', currentCompany.id)); currentCompany.extra_credits += returnExtra; } promises.push(supabaseClient.from('credit_transactions').insert({ company_id: currentCompany.id, type: 'refund', amount: INDIVIDUAL_COST, description: `Link cancelado: ${name}` })); await Promise.all(promises); updateHeader(); } catch (err) { console.error('Erro ao excluir:', err); // Recarregar pra garantir consistência loadBatchesAndRespondents(); } } async function deleteIndividual(respondentId, name) { if (!confirm(`Excluir "${name}"?\n\nOs dados do assessment serão removidos permanentemente.`)) return; try { const { error } = await supabaseClient .from('respondents') .delete() .eq('id', respondentId); if (error) throw error; // Atualizar local allIndividuais = allIndividuais.filter(r => r.id !== respondentId); delete activeLabels[respondentId]; delete uniqueLabelsMap[respondentId]; renderRespondentsList(allIndividuais); redrawMatriz(); } catch (err) { alert('Erro ao excluir: ' + err.message); } } // ========== MATRIZ ========== function drawMatriz(pontos) { const w = MATRIZ_SIZE, h = MATRIZ_SIZE; d3.select('#matrix').html(''); const svg = d3.select('#matrix').append('svg') .attr('width', '100%').attr('height', '100%') .attr('viewBox', `0 0 ${w} ${h}`); matrizData = pontos.map(p => { const pos = calcularPosicaoSVG(p.f, p.e, p.c, p.a); return { ...p, svgX: pos.svgX, svgY: pos.svgY }; }); // 1. Coordenadas por batch Object.keys(batchData).forEach(batchId => { const batch = batchData[batchId]; const state = activeBatches[batchId]; if (!state || !state.visible) return; // Puras (cor do batch) const opPuras = batch.puras.length > 100 ? 0.15 : batch.puras.length > 50 ? 0.25 : 0.4; batch.puras.forEach(coord => { const pos = calcularPosicaoSVG(coord.f, coord.e, coord.c, coord.a); svg.append('circle') .attr('cx', pos.svgX).attr('cy', pos.svgY) .attr('r', RAIO_PONTO).attr('fill', batch.color).attr('opacity', opPuras); }); // Aspiracionais (vermelho) if (state.showAs && batch.aspiracionais.length > 0) { batch.aspiracionais.forEach(coord => { const pos = calcularPosicaoSVG(coord.f, coord.e, coord.c, coord.a); svg.append('circle') .attr('cx', pos.svgX).attr('cy', pos.svgY) .attr('r', RAIO_PONTO).attr('fill', '#ea3659').attr('opacity', 0.4); }); } // Labeled (colab, indicadores, etc.) if (batch.labeled.length > 0) { batch.labeled.forEach(coord => { const p = coord.parsed; // Checar toggle: se undefined (não setado), mostra. Se false, esconde. if (p.toggleKey && state.labels[p.toggleKey] === false) return; let dotColor = batch.color; if (p.indicator && INDICATOR_COLORS[p.indicator]) dotColor = INDICATOR_COLORS[p.indicator]; const dotRadius = RAIO_PONTO * (p.radius || 1); const dotOpacity = p.indicator ? 0.6 : 0.5; const pos = calcularPosicaoSVG(coord.f, coord.e, coord.c, coord.a); svg.append('circle') .attr('cx', pos.svgX).attr('cy', pos.svgY) .attr('r', dotRadius).attr('fill', dotColor).attr('opacity', dotOpacity); // Texto do label dentro do ponto if (p.name && p.name !== 'label' && dotRadius >= RAIO_PONTO) { const shortName = p.name.length > 4 ? p.name.substring(0, 4) : p.name; svg.append('text') .attr('x', pos.svgX).attr('y', pos.svgY + 4) .attr('fill', 'white').attr('font-size', '8px') .attr('text-anchor', 'middle').attr('opacity', 0.8) .text(shortName); } }); } // pG importados (com radar animado) if (state.showCluster && batch.pGs.length > 0) { batch.pGs.forEach(pg => { const variant = pg.parsed.variant || 'pG'; const isAs = /as/i.test(variant); if (isAs && !state.showAs) return; const pgColor = isAs ? '#ea3659' : batch.color; const pgDisplayName = batch.name || batch.label; const pgLabel = isAs ? (pgDisplayName + 'ₐ') : pgDisplayName; const pos = calcularPosicaoSVG(pg.f, pg.e, pg.c, pg.a); animRadar(svg, pos.svgX, pos.svgY, RAIO_PG, pgColor); svg.append('circle').attr('cx', pos.svgX).attr('cy', pos.svgY).attr('r', RAIO_PG).attr('fill', pgColor).attr('opacity', 0.3); const shortLabel = pgLabel.length > 10 ? pgLabel.substring(0, 10) : pgLabel; svg.append('text').attr('x', pos.svgX).attr('y', pos.svgY + 4).attr('fill', 'white').attr('font-size', '9px').attr('font-weight', 'bold').attr('text-anchor', 'middle').text(shortLabel); // Tendência: linha pontilhada do pG até T if (batch.tendencias.length > 0) { batch.tendencias.forEach(t => { const tVariant = t.parsed.variant || 'T'; const tIsAs = /as/i.test(tVariant); // Associar: T normal com pG normal, Tas com pGa if (tIsAs !== isAs) return; const tPos = calcularPosicaoSVG(t.f, t.e, t.c, t.a); // Calcular ângulo para a seta const dx = tPos.svgX - pos.svgX; const dy = tPos.svgY - pos.svgY; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < 1) return; const ux = dx / dist; const uy = dy / dist; // Pontos de início (borda do pG) e fim (borda do T) const startX = pos.svgX + ux * RAIO_PG; const startY = pos.svgY + uy * RAIO_PG; const endX = tPos.svgX - ux * (RAIO_T + 4); const endY = tPos.svgY - uy * (RAIO_T + 4); // Linha pontilhada svg.append('line') .attr('x1', startX).attr('y1', startY) .attr('x2', endX).attr('y2', endY) .attr('stroke', pgColor).attr('stroke-width', 2) .attr('stroke-dasharray', '6,4').attr('opacity', 0.7); // Seta (triângulo na ponta) const arrowSize = 7; const ax1 = endX - arrowSize * (ux - uy * 0.5); const ay1 = endY - arrowSize * (uy + ux * 0.5); const ax2 = endX - arrowSize * (ux + uy * 0.5); const ay2 = endY - arrowSize * (uy - ux * 0.5); svg.append('polygon') .attr('points', `${endX},${endY} ${ax1},${ay1} ${ax2},${ay2}`) .attr('fill', pgColor).attr('opacity', 0.8); // Círculo T svg.append('circle') .attr('cx', tPos.svgX).attr('cy', tPos.svgY) .attr('r', RAIO_T).attr('fill', pgColor).attr('opacity', 0.25) .attr('stroke', pgColor).attr('stroke-width', 1.5).attr('stroke-dasharray', '3,2'); svg.append('text') .attr('x', tPos.svgX).attr('y', tPos.svgY + 4) .attr('fill', 'white').attr('font-size', '10px').attr('font-weight', 'bold') .attr('text-anchor', 'middle').attr('opacity', 0.9) .text('T'); }); } }); } }); // 2. Individuais (independente de batch — controlado por activeLabels) const individuais = matrizData.filter(d => d.tipo === 'individual'); individuais.forEach(d => { if (!activeLabels.hasOwnProperty(d.id)) activeLabels[d.id] = false; if (activeLabels[d.id] === false) return; const co = getColorForLabel(d.id); svg.append('circle') .attr('cx', d.svgX).attr('cy', d.svgY) .attr('r', RAIO_PONTO).attr('fill', co).attr('opacity', 0.7); svg.append('text') .attr('x', d.svgX).attr('y', d.svgY + 4) .attr('fill', 'white').attr('font-size', '10px') .attr('text-anchor', 'middle') .text(d.shortLabel); }); // 3. Estrutura visual drawStruct(svg, w, h); // 4. Controles updateBatchControls(); } function drawPGForBatch(svg, pontos, label, color) { if (pontos.length < 3) return; const cl = kMeansClusterizacao(pontos, NUM_CLUSTERS_PG); const cu = unificarClustersProximos(cl); let cv = cu.filter(c => c.densidade / pontos.length >= MIN_PERCENTUAL_CLUSTER).sort((a, b) => b.densidade - a.densidade); if (cv.length === 0) return; const c = cv[0]; const pos = calcularPosicaoSVG(100 - c.centro.y, c.centro.y, 100 - c.centro.x, c.centro.x); animRadar(svg, pos.svgX, pos.svgY, RAIO_PG, color); svg.append('circle').attr('cx', pos.svgX).attr('cy', pos.svgY).attr('r', RAIO_PG).attr('fill', color).attr('opacity', 0.3); const shortLabel = label.length > 8 ? label.substring(0, 8) : label; svg.append('text').attr('x', pos.svgX).attr('y', pos.svgY + 5).attr('fill', 'white').attr('font-size', '10px').attr('font-weight', 'bold').attr('text-anchor', 'middle').text(shortLabel); } function drawStruct(svg, w, h) { const dg = 'white', lg = '#A781F3', cx = w / 2, cy = h / 2; svg.append('line').attr('x1', cx).attr('y1', 0).attr('x2', cx).attr('y2', h).attr('stroke', dg).attr('stroke-width', 1); svg.append('line').attr('x1', 0).attr('y1', cy).attr('x2', w).attr('y2', cy).attr('stroke', dg).attr('stroke-width', 1); svg.append('line').attr('x1', 0).attr('y1', 0).attr('x2', w).attr('y2', h).attr('stroke', lg).attr('stroke-width', 1).attr('stroke-dasharray', '5,5'); svg.append('line').attr('x1', w).attr('y1', 0).attr('x2', 0).attr('y2', h).attr('stroke', lg).attr('stroke-width', 1).attr('stroke-dasharray', '5,5'); [1, 2, 3].forEach(i => { const o = w / 6 * i; svg.append('rect').attr('x', o).attr('y', o).attr('width', w - 2 * o).attr('height', h - 2 * o).attr('stroke', lg).attr('stroke-width', 1).attr('stroke-dasharray', '5,5').attr('fill', 'none'); }); const ql = [ { x: w * 0.10, y: h * 0.28, t: 'APRENDIZADO' }, { x: w * 0.32, y: h * 0.06, t: 'PRAZER' }, { x: w * 0.68, y: h * 0.06, t: 'PROPÓSITO' }, { x: w * 0.90, y: h * 0.28, t: 'ACOLHIMENTO' }, { x: w * 0.90, y: h * 0.74, t: 'ORDEM' }, { x: w * 0.68, y: h * 0.94, t: 'SEGURANÇA' }, { x: w * 0.32, y: h * 0.94, t: 'AUTORIDADE' }, { x: w * 0.10, y: h * 0.74, t: 'RESULTADO' } ]; ql.forEach(l => svg.append('text').attr('x', l.x).attr('y', l.y).attr('fill', 'white').attr('font-size', '9px').attr('text-anchor', 'middle').attr('opacity', 0.6).text(l.t)); } function animRadar(svg, x, y, r, co) { for (let i = 0; i < 3; i++) { const c = svg.append('circle').attr('cx', x).attr('cy', y).attr('r', r).attr('fill', co).attr('opacity', 0); (function rp() { c.attr('r', r).attr('opacity', 0).transition().duration(1000).attr('r', r * 3).attr('opacity', 0.1).transition().duration(1000).attr('r', r * 6).attr('opacity', 0).on('end', rp); })(); } for (let i = 0; i < 3; i++) { const c = svg.append('circle').attr('cx', x).attr('cy', y).attr('r', r).attr('fill', 'none').attr('stroke', co).attr('stroke-width', 2).attr('opacity', 0.05); (function rp() { c.transition().duration(2000).attr('r', r * 4).attr('opacity', 0).transition().duration(0).attr('r', r).attr('opacity', 0.15).on('end', rp); })(); } } // ========== BATCH CONTROLS ========== function updateBatchControls() { const cc = document.getElementById('matriz-controls'); cc.innerHTML = ''; if (batches.length === 0) return; const mainDiv = document.createElement('div'); mainDiv.style.cssText = 'display: flex; flex-direction: column; gap: 12px; width: 100%;'; const activeDiv = document.createElement('div'); activeDiv.style.cssText = 'display: flex; flex-direction: column; gap: 8px;'; const availDiv = document.createElement('div'); availDiv.style.cssText = 'display: flex; flex-wrap: wrap; gap: 8px; justify-content: center;'; batches.forEach(batch => { const data = batchData[batch.id]; const state = activeBatches[batch.id] || { visible: false, showAs: true, showCluster: true, labels: {} }; if (!data) return; // Só mostrar batch se tiver coordenadas não-individuais const totalCoords = data.puras.length + data.aspiracionais.length + data.labeled.length + data.pGs.length + data.tendencias.length; if (totalCoords === 0) return; const hasAs = data.aspiracionais.length > 0; const hasCluster = data.pGs.length > 0; const displayName = data.name || data.label; const fullName = data.name ? `${data.name} · ${data.label}` : data.label; // Descobrir toggles de labels const labelToggles = {}; data.labeled.forEach(c => { if (c.parsed.toggleKey) { if (!labelToggles[c.parsed.toggleKey]) { labelToggles[c.parsed.toggleKey] = { count: 0, displayName: c.parsed.displayName, indicator: c.parsed.indicator }; } labelToggles[c.parsed.toggleKey].count++; } }); const hasLabels = Object.keys(labelToggles).length > 0; // === Botão do batch (linha de baixo) === const btn = document.createElement('button'); btn.style.cssText = ` display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; border-radius: 20px; background: ${state.visible ? 'rgba(255,255,255,0.15)' : 'rgba(255,255,255,0.05)'}; border: 2px solid ${state.visible ? batch.color : 'rgba(255,255,255,0.2)'}; color: ${state.visible ? '#fff' : 'rgba(255,255,255,0.5)'}; font-size: 12px; font-weight: 500; cursor: pointer; transition: all 0.2s; `; btn.innerHTML = ` ${displayName} `; btn.onclick = () => toggleBatchVisibility(batch.id); availDiv.appendChild(btn); // === Controles do batch ativo (linha de cima) === if (state.visible && (hasAs || hasCluster || hasLabels || data.diagnostico_url)) { const row = document.createElement('div'); row.style.cssText = ` display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding: 6px 12px; border-radius: 8px; background: rgba(255,255,255,0.05); border-left: 3px solid ${batch.color}; `; // Label do batch const lbl = document.createElement('span'); lbl.style.cssText = `font-size: 12px; color: ${batch.color}; font-weight: 500; min-width: 80px;`; lbl.textContent = fullName; row.appendChild(lbl); // Toggle AS if (hasAs) { row.appendChild(createToggleBtn( `as (${data.aspiracionais.length})`, state.showAs, '#ea3659', (e) => { e.stopPropagation(); toggleBatchAs(batch.id); } )); } // Toggle pG if (hasCluster) { row.appendChild(createToggleBtn( 'pG', state.showCluster, batch.color, (e) => { e.stopPropagation(); toggleBatchCluster(batch.id); } )); } // Toggles de labels Object.keys(labelToggles).forEach(key => { const toggle = labelToggles[key]; const isOn = state.labels[key] !== false; const color = toggle.indicator ? INDICATOR_COLORS[toggle.indicator] : batch.color; row.appendChild(createToggleBtn( `${toggle.displayName} (${toggle.count})`, isOn, color || batch.color, (e) => { e.stopPropagation(); toggleBatchLabel(batch.id, key); } )); }); // Link diagnóstico (só para batches DC com URL) if (data.diagnostico_url) { const diagLink = document.createElement('a'); diagLink.href = data.diagnostico_url; diagLink.target = '_blank'; diagLink.style.cssText = ` padding: 4px 8px; border-radius: 4px; font-size: 11px; background: #A781F3; border: 1px solid #A781F3; color: rgba(0, 0, 0, 0.85); cursor: pointer; text-decoration: none; white-space: nowrap; font-weight: 500; `; diagLink.textContent = 'Diagnóstico'; diagLink.title = 'Ver Diagnóstico Completo'; row.appendChild(diagLink); } activeDiv.appendChild(row); } }); if (activeDiv.children.length > 0) mainDiv.appendChild(activeDiv); mainDiv.appendChild(availDiv); cc.appendChild(mainDiv); } function createToggleBtn(text, isActive, color, onClick) { const btn = document.createElement('button'); btn.style.cssText = ` padding: 4px 8px; border-radius: 4px; font-size: 11px; background: ${isActive ? color + '33' : 'rgba(255,255,255,0.1)'}; border: 1px solid ${isActive ? color : 'rgba(255,255,255,0.2)'}; color: ${isActive ? color : 'rgba(255,255,255,0.5)'}; cursor: pointer; `; btn.textContent = text; btn.onclick = onClick; return btn; } // ========== TOGGLE FUNCTIONS ========== function toggleBatchVisibility(batchId) { if (!activeBatches[batchId]) { activeBatches[batchId] = { visible: true, showAs: true, showCluster: true, labels: {} }; } else { activeBatches[batchId].visible = !activeBatches[batchId].visible; } redrawMatriz(); } function toggleBatchAs(batchId) { if (activeBatches[batchId]) activeBatches[batchId].showAs = !activeBatches[batchId].showAs; redrawMatriz(); } function toggleBatchCluster(batchId) { if (activeBatches[batchId]) activeBatches[batchId].showCluster = !activeBatches[batchId].showCluster; redrawMatriz(); } function toggleBatchLabel(batchId, toggleKey) { if (activeBatches[batchId]) { activeBatches[batchId].labels[toggleKey] = activeBatches[batchId].labels[toggleKey] === false ? true : false; } redrawMatriz(); } function toggleRespondentLabel(id) { activeLabels[id] = activeLabels[id] === true ? false : true; const ia = activeLabels[id] === true; const co = getColorForLabel(id); document.querySelectorAll(`.respondente-style[data-id="${id}"]`).forEach(b => { b.classList.toggle('inactive', !ia); b.style.background = ia ? co : 'rgba(255,255,255,0.1)'; b.style.color = ia ? '#fff' : 'rgba(255,255,255,0.4)'; }); redrawMatriz(); } function redrawMatriz() { drawMatriz(matrizData); } // ========== MODAL LOGIC ========== function initModal() { const ta = document.getElementById('toggle-amostra'); const ac = document.getElementById('amostra-config'); const cc = document.getElementById('config-completo'); const s2 = document.getElementById('diag-step-2'); ta.addEventListener('click', () => { amostraAtiva = !amostraAtiva; ta.classList.toggle('active', amostraAtiva); ac.style.display = amostraAtiva ? 'block' : 'none'; recalc(); }); document.querySelectorAll('input[name="diag-type"]').forEach(r => r.addEventListener('change', e => { tipoSelecionado = e.target.value; document.querySelectorAll('.radio-option[data-type]').forEach(o => o.classList.remove('selected')); e.target.closest('.radio-option').classList.add('selected'); cc.style.display = tipoSelecionado === 'completo' ? 'block' : 'none'; s2.classList.remove('hidden'); recalc(); })); ['diag-populacao', 'diag-confianca', 'diag-erro', 'diag-recortes', 'diag-perguntas'].forEach(id => { const el = document.getElementById(id); if (el) { el.addEventListener('input', recalc); el.addEventListener('change', recalc); } }); } function recalc() { const pop = parseInt(document.getElementById('diag-populacao').value) || 0; const conf = parseInt(document.getElementById('diag-confianca').value) || 95; const err = parseInt(document.getElementById('diag-erro').value) || 2; const rec = parseInt(document.getElementById('diag-recortes').value) || 2; const per = parseInt(document.getElementById('diag-perguntas').value) || 0; let resp = pop; if (amostraAtiva && pop > 0) { resp = calcularAmostra(pop, conf, err); document.getElementById('amostra-resultado').textContent = resp; document.getElementById('amostra-detail').textContent = `${conf}% de confiança, ${err}% de margem de erro`; } else if (!amostraAtiva && pop > 0) { document.getElementById('amostra-resultado').textContent = pop; document.getElementById('amostra-detail').textContent = 'Censo completo (100% dos colaboradores)'; } else { document.getElementById('amostra-resultado').textContent = '--'; document.getElementById('amostra-detail').textContent = 'Preencha a quantidade de colaboradores'; } let preco = 0; if (resp > 0 && tipoSelecionado) { preco = tipoSelecionado === 'completo' ? calcularPrecoCompleto(resp, rec, per) : calcularPrecoPulso(resp); } // Desconto da empresa const desconto = currentCompany?.discount_percentage || 0; const precoFinal = desconto > 0 ? Math.round(preco * (1 - desconto / 100)) : preco; const precoEl = document.getElementById('preco-resultado'); const precoParent = precoEl.parentElement; // .value div que contém "R$ " if (desconto > 0 && preco > 0) { // Substituir o conteúdo inteiro do parent para controlar o R$ precoParent.innerHTML = `R$ ${preco.toLocaleString('pt-BR')}
R$ ${precoFinal.toLocaleString('pt-BR')} -${desconto}%`; } else { // Restaurar formato original: R$ valor precoParent.innerHTML = `R$ ${preco.toLocaleString('pt-BR')}`; } document.getElementById('btn-solicitar-orcamento').disabled = preco === 0 || !tipoSelecionado; } function resetModal() { document.getElementById('diag-populacao').value = ''; document.getElementById('diag-step-2').classList.add('hidden'); document.querySelectorAll('input[name="diag-type"]').forEach(r => r.checked = false); document.querySelectorAll('.radio-option[data-type]').forEach(o => o.classList.remove('selected')); tipoSelecionado = null; amostraAtiva = true; document.getElementById('toggle-amostra').classList.add('active'); document.getElementById('amostra-config').style.display = 'block'; document.getElementById('config-completo').style.display = 'block'; document.getElementById('diag-form-content').classList.remove('hidden'); document.getElementById('diag-success').classList.add('hidden'); document.getElementById('diag-footer').style.display = 'flex'; recalc(); } // ========== EVENT LISTENERS ========== document.getElementById('btn-novo-diagnostico').addEventListener('click', () => { resetModal(); openModal('modal-novo-diagnostico'); }); document.getElementById('btn-solicitar-orcamento').addEventListener('click', async () => { const btn = document.getElementById('btn-solicitar-orcamento'); btn.disabled = true; btn.textContent = 'Enviando...'; const pop = parseInt(document.getElementById('diag-populacao').value) || 0; const conf = parseInt(document.getElementById('diag-confianca').value) || 95; const err = parseInt(document.getElementById('diag-erro').value) || 2; const rec = parseInt(document.getElementById('diag-recortes').value) || 2; const per = parseInt(document.getElementById('diag-perguntas').value) || 0; const resp = amostraAtiva ? calcularAmostra(pop, conf, err) : pop; const precoBase = tipoSelecionado === 'completo' ? calcularPrecoCompleto(resp, rec, per) : calcularPrecoPulso(resp); const desconto = currentCompany?.discount_percentage || 0; const preco = desconto > 0 ? Math.round(precoBase * (1 - desconto / 100)) : precoBase; const tn = tipoSelecionado === 'completo' ? 'Diagnóstico Completo' : 'Pulso Rápido'; try { const { data: qd, error: qe } = await supabaseClient.from('quote_requests').insert({ company_id: currentCompany.id, tipo: tipoSelecionado, populacao: pop, respondentes: resp, amostra: amostraAtiva, confianca: amostraAtiva ? conf : null, erro: amostraAtiva ? err : null, recortes: tipoSelecionado === 'completo' ? rec : null, perguntas: tipoSelecionado === 'completo' ? per : null, preco_estimado: preco }).select().single(); if (qe) throw qe; await supabaseClient.from('notifications').insert({ company_id: currentCompany.id, title: '📋 Orçamento solicitado', message: `Seu pedido de ${tn} (R$ ${preco.toLocaleString('pt-BR')}) foi recebido. Em breve entraremos em contato.`, type: 'quote', reference_id: qd.id, read: false }); await supabaseClient.from('admin_alerts').insert({ type: 'quote_request', title: `Novo orçamento: ${currentCompany.name}`, message: `${tn} • ${resp} respondentes • R$ ${preco.toLocaleString('pt-BR')}`, reference_id: qd.id, company_id: currentCompany.id }); document.getElementById('diag-form-content').classList.add('hidden'); document.getElementById('diag-success').classList.remove('hidden'); document.getElementById('diag-footer').style.display = 'none'; loadNotifications(); } catch (e) { console.error(e); alert('Erro ao enviar orçamento.'); btn.disabled = false; btn.textContent = 'Solicitar Orçamento'; } }); // ========== MODAL GERAR INDIVIDUAL (redesenhado) ========== let gerarLinkInitialized = false; function initGerarLinkModal() { if (gerarLinkInitialized) { populateIndividualBatchSelect(); return; } const modal = document.getElementById('modal-gerar-link'); if (!modal) { console.error('modal-gerar-link não encontrado'); return; } const modalBody = modal.querySelector('.modal-body'); if (!modalBody) { console.error('modal-body não encontrado'); return; } try { // 1. Esconder o

"Selecione o tipo de assessment:" const pText = modalBody.querySelector('p'); if (pText) pText.style.display = 'none'; // 2. Esconder o container dos radio buttons modalBody.querySelectorAll('.radio-option').forEach(el => el.style.display = 'none'); const radioContainer = modalBody.querySelector('div[style*="flex-direction"]'); if (radioContainer && !radioContainer.querySelector('#link-result')) { radioContainer.style.display = 'none'; } // 3. Criar formulário const form = document.createElement('div'); form.id = 'gerar-link-form'; form.style.cssText = 'display: flex; flex-direction: column; gap: 16px;'; form.innerHTML = `

`; // 4. Inserir no .modal-body ANTES do #link-result const linkResult = document.getElementById('link-result'); if (linkResult && linkResult.parentElement === modalBody) { modalBody.insertBefore(form, linkResult); } else { modalBody.appendChild(form); } // 5. NÃO tocar no .modal-footer — Cancelar e Gerar Link já estão corretos lá // 6. Popular select de batches populateIndividualBatchSelect(); // 7. Listeners do mini-form de batch document.getElementById('btn-novo-batch-inline').onclick = () => { document.getElementById('novo-batch-inline').style.display = 'block'; document.getElementById('inline-batch-date').value = new Date().toISOString().split('T')[0]; document.getElementById('inline-batch-name').focus(); }; document.getElementById('btn-cancelar-batch-inline').onclick = () => { document.getElementById('novo-batch-inline').style.display = 'none'; }; document.getElementById('btn-criar-batch-inline').onclick = async () => { const batchName = document.getElementById('inline-batch-name').value.trim(); const batchDate = document.getElementById('inline-batch-date').value; if (!batchName) { alert('Digite o nome do batch'); return; } if (!batchDate) { alert('Selecione uma data'); return; } const BATCH_COLORS_INLINE = ['#3B82F6', '#8B5CF6', '#EC4899', '#F59E0B', '#10B981', '#EF4444', '#06B6D4', '#84CC16']; const code = 'DC'; const formattedDate = new Date(batchDate + 'T12:00:00').toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit', year: 'numeric' }); const label = `${code}-${formattedDate}`; const color = BATCH_COLORS_INLINE[batches.length % BATCH_COLORS_INLINE.length]; try { const { data, error } = await supabaseClient .from('diagnosis_batches') .insert({ company_id: currentCompany.id, type: 'completo', code, label, diagnosis_date: batchDate, description: null, has_aspirational: false, color, name: batchName }) .select().single(); if (error) throw error; batches.unshift(data); batchNameMap[data.id] = data.name || data.label; populateIndividualBatchSelect(); document.getElementById('individual-batch').value = data.id; document.getElementById('novo-batch-inline').style.display = 'none'; document.getElementById('inline-batch-name').value = ''; } catch (err) { alert('Erro ao criar batch: ' + err.message); } }; gerarLinkInitialized = true; } catch (err) { console.error('Erro ao inicializar modal gerar link:', err); } } function populateIndividualBatchSelect() { const sel = document.getElementById('individual-batch'); if (!sel) return; sel.innerHTML = ''; batches.forEach(b => { const opt = document.createElement('option'); opt.value = b.id; opt.textContent = b.name || b.label; sel.appendChild(opt); }); } document.getElementById('btn-gerar-link').addEventListener('click', () => { const lr = document.getElementById('link-result'); if (lr) lr.style.display = 'none'; const cb = document.getElementById('btn-confirmar-link'); if (cb) { cb.style.display = ''; cb.disabled = false; cb.textContent = 'Gerar Link'; } const nomeInput = document.getElementById('individual-nome'); if (nomeInput) nomeInput.value = ''; const batchSel = document.getElementById('individual-batch'); if (batchSel) batchSel.value = ''; const novoBatch = document.getElementById('novo-batch-inline'); if (novoBatch) novoBatch.style.display = 'none'; initGerarLinkModal(); openModal('modal-gerar-link'); }); document.getElementById('btn-confirmar-link').addEventListener('click', async () => { const nomeInput = document.getElementById('individual-nome'); const nome = nomeInput ? nomeInput.value.trim() : ''; if (!nome) { alert('Digite o nome da pessoa'); nomeInput?.focus(); return; } const batchSel = document.getElementById('individual-batch'); const batchId = batchSel ? batchSel.value || null : null; const btn = document.getElementById('btn-confirmar-link'); btn.disabled = true; btn.textContent = 'Gerando...'; try { // Checar créditos const INDIVIDUAL_COST = 10; const monthlyAvail = currentCompany.monthly_credits - currentCompany.monthly_credits_used; const extraAvail = currentCompany.extra_credits; if (monthlyAvail + extraAvail < INDIVIDUAL_COST) throw new Error(`Créditos insuficientes. São necessários ${INDIVIDUAL_COST} créditos para gerar um link individual.`); // Gerar token único const token = crypto.randomUUID(); // Inserir assessment link const { data: newLink, error: linkError } = await supabaseClient .from('assessment_links') .insert({ company_id: currentCompany.id, token: token, assessment_type: 'individual', status: 'active', respondent_name: nome, batch_id: batchId, created_by_email: currentUser.email, max_responses: 1, credits_cost: INDIVIDUAL_COST }) .select('id, token, respondent_name, batch_id, status, created_at') .single(); if (linkError) throw linkError; // Deduzir créditos let remaining = INDIVIDUAL_COST; if (monthlyAvail > 0) { const fromMonthly = Math.min(remaining, monthlyAvail); await supabaseClient.from('companies') .update({ monthly_credits_used: currentCompany.monthly_credits_used + fromMonthly }) .eq('id', currentCompany.id); currentCompany.monthly_credits_used += fromMonthly; remaining -= fromMonthly; } if (remaining > 0) { await supabaseClient.from('companies') .update({ extra_credits: currentCompany.extra_credits - remaining }) .eq('id', currentCompany.id); currentCompany.extra_credits -= remaining; } // Registrar transação await supabaseClient.from('credit_transactions').insert({ company_id: currentCompany.id, type: 'consumption', amount: -INDIVIDUAL_COST, description: `Link individual: ${nome}` }); // Adicionar ao array local e re-renderizar pendingLinks.unshift(newLink); renderRespondentsList(allIndividuais); // Mostrar URL const url = `https://www.qore.me/ind?t=${token}`; document.getElementById('generated-link').value = url; document.getElementById('link-result').style.display = 'block'; btn.style.display = 'none'; updateHeader(); loadNotifications(); } catch (e) { alert(e.message || 'Erro ao gerar link'); btn.disabled = false; btn.textContent = 'Gerar Link'; } }); function copyLink() { const i = document.getElementById('generated-link'); i.select(); document.execCommand('copy'); event.target.textContent = 'Copiado!'; setTimeout(() => event.target.textContent = 'Copiar', 2000); } document.getElementById('btn-autodiagnostico').addEventListener('click', () => window.location.href = '/dash/autodiagnostico'); document.getElementById('notification-btn').addEventListener('click', () => openModal('modal-notificacoes')); document.getElementById('mark-all-read').addEventListener('click', async () => { await supabaseClient.from('notifications').update({ read: true }).eq('company_id', currentCompany.id); loadNotifications(); }); document.getElementById('clear-read').addEventListener('click', async () => { await supabaseClient.from('notifications').delete().eq('company_id', currentCompany.id).eq('read', true); loadNotifications(); }); function openModal(id) { document.getElementById(id).classList.add('show'); } function closeModal(id) { document.getElementById(id).classList.remove('show'); } function formatDate(ds) { return new Date(ds).toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit', year: 'numeric' }); } // ========== BOTÕES EXTRAS (injetados via JS) ========== (function injectExtraButtons() { const actionBar = document.querySelector('.action-buttons'); if (!actionBar) return; // 1. Gerenciar Batches const btnBatches = document.createElement('button'); btnBatches.className = 'btn btn-secondary'; btnBatches.id = 'btn-gerenciar-batches'; btnBatches.innerHTML = ` Batches`; actionBar.appendChild(btnBatches); // 2. Calculadora de ROI const btnROI = document.createElement('a'); btnROI.className = 'btn btn-secondary'; btnROI.href = '/dash/calculadora'; btnROI.target = '_blank'; btnROI.innerHTML = ` Calculadora ROI`; actionBar.appendChild(btnROI); // 3. Chalkboard const btnChalk = document.createElement('a'); btnChalk.className = 'btn btn-secondary'; btnChalk.href = '/dash/chalkboard'; btnChalk.target = '_blank'; btnChalk.innerHTML = ` Chalkboard`; actionBar.appendChild(btnChalk); })(); // ========== MODAL GERENCIAR BATCHES ========== (function createBatchModal() { const overlay = document.createElement('div'); overlay.className = 'modal-overlay'; overlay.id = 'modal-gerenciar-batches'; overlay.innerHTML = ` `; document.body.appendChild(overlay); })(); function renderBatchList() { const list = document.getElementById('mgr-batch-list'); const empty = document.getElementById('mgr-batch-empty'); if (!list) return; list.innerHTML = ''; if (batches.length === 0) { list.style.display = 'none'; if (empty) empty.style.display = 'block'; return; } list.style.display = 'block'; if (empty) empty.style.display = 'none'; batches.forEach(b => { const row = document.createElement('div'); row.style.cssText = 'display: flex; align-items: center; gap: 12px; padding: 10px 24px; transition: background 0.15s;'; row.onmouseover = () => row.style.background = 'rgba(255,255,255,0.04)'; row.onmouseout = () => row.style.background = 'transparent'; // Color dot const dot = document.createElement('div'); dot.style.cssText = `width: 10px; height: 10px; border-radius: 50%; background: ${b.color || '#8B5CF6'}; flex-shrink: 0;`; row.appendChild(dot); // Info const info = document.createElement('div'); info.style.cssText = 'flex: 1; min-width: 0;'; const name = b.name || b.label; const date = b.diagnosis_date ? new Date(b.diagnosis_date + 'T12:00:00').toLocaleDateString('pt-BR') : '—'; info.innerHTML = `
${name}
${b.label} · ${date}
`; row.appendChild(info); // Diagnostico URL indicator if (b.diagnostico_url) { const urlBadge = document.createElement('span'); urlBadge.style.cssText = 'font-size: 10px; color: #6ee7b7; background: rgba(110,231,183,0.12); padding: 2px 6px; border-radius: 4px; white-space: nowrap;'; urlBadge.textContent = '🔗 URL'; row.appendChild(urlBadge); } // Delete button const del = document.createElement('button'); del.style.cssText = 'padding: 4px 8px; background: rgba(239,68,68,0.12); border: 1px solid rgba(239,68,68,0.25); border-radius: 6px; color: #ef4444; font-size: 12px; cursor: pointer; flex-shrink: 0; transition: background 0.15s;'; del.textContent = 'Excluir'; del.onmouseover = () => del.style.background = 'rgba(239,68,68,0.25)'; del.onmouseout = () => del.style.background = 'rgba(239,68,68,0.12)'; del.onclick = () => deleteBatch(b.id, name); row.appendChild(del); list.appendChild(row); }); } async function deleteBatch(batchId, batchName) { if (!confirm(`Excluir batch "${batchName}"?\n\n⚠️ Todos os respondentes e dados vinculados a este batch serão excluídos permanentemente.`)) return; try { // 1. Apagar respondentes do batch const { error: respErr } = await supabaseClient .from('respondents') .delete() .eq('batch_id', batchId); if (respErr) console.warn('Erro ao excluir respondentes:', respErr.message); // 2. Apagar assessment_links do batch (se a coluna existir) try { await supabaseClient .from('assessment_links') .delete() .eq('batch_id', batchId); } catch(e) { /* coluna pode não existir */ } // 3. Apagar o batch const { error } = await supabaseClient .from('diagnosis_batches') .delete() .eq('id', batchId); if (error) throw error; // Atualizar arrays locais const idx = batches.findIndex(b => b.id === batchId); if (idx > -1) batches.splice(idx, 1); delete batchNameMap[batchId]; // Remover respondentes locais desse batch if (typeof respondents !== 'undefined') { for (let i = respondents.length - 1; i >= 0; i--) { if (respondents[i].batch_id === batchId) respondents.splice(i, 1); } } renderBatchList(); redrawMatriz(); updateBatchControls(); if (typeof allIndividuais !== 'undefined') { // Remover do array local também allIndividuais = allIndividuais.filter(r => r.batch_id !== batchId); renderRespondentsList(allIndividuais); } } catch (err) { alert('Erro ao excluir: ' + err.message); } } // Listener: abrir modal document.getElementById('btn-gerenciar-batches').addEventListener('click', () => { document.getElementById('mgr-batch-date').value = new Date().toISOString().split('T')[0]; document.getElementById('mgr-batch-name').value = ''; renderBatchList(); openModal('modal-gerenciar-batches'); }); // Listener: criar batch no modal document.getElementById('mgr-batch-create').addEventListener('click', async () => { const nameInput = document.getElementById('mgr-batch-name'); const dateInput = document.getElementById('mgr-batch-date'); const batchName = nameInput.value.trim(); const batchDate = dateInput.value; if (!batchName) { alert('Digite o nome do batch'); nameInput.focus(); return; } if (!batchDate) { alert('Selecione uma data'); dateInput.focus(); return; } const BATCH_COLORS_MGR = ['#3B82F6', '#8B5CF6', '#EC4899', '#F59E0B', '#10B981', '#EF4444', '#06B6D4', '#84CC16']; const code = 'DC'; const formattedDate = new Date(batchDate + 'T12:00:00').toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit', year: 'numeric' }); const label = `${code}-${formattedDate}`; const color = BATCH_COLORS_MGR[batches.length % BATCH_COLORS_MGR.length]; const btn = document.getElementById('mgr-batch-create'); btn.disabled = true; btn.textContent = 'Criando...'; try { const { data, error } = await supabaseClient .from('diagnosis_batches') .insert({ company_id: currentCompany.id, type: 'completo', code, label, diagnosis_date: batchDate, description: null, has_aspirational: false, color, name: batchName }) .select().single(); if (error) throw error; batches.unshift(data); batchNameMap[data.id] = data.name || data.label; nameInput.value = ''; renderBatchList(); updateBatchControls(); } catch (err) { alert('Erro ao criar batch: ' + err.message); } finally { btn.disabled = false; btn.textContent = '+ Criar'; } }); // ========== INIT ========== initDash();