// ========== 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}
`;
} 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 = '
'; 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 = `
Nenhum batch criado ainda.
`;
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();