File: /home/storage/5/78/dd/wicomm2/public_html/clientes/lookai/index.php
<?php $cfg = require __DIR__ . '/config.php';
date_default_timezone_set($cfg['app']['timezone']); ?>
<!doctype html>
<html lang="pt-br">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title><?= htmlspecialchars($cfg['app']['name']) ?></title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<style>
body {
background: #0f1116;
color: #f1f1f1;
font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, Arial;
margin: 0
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 28px
}
header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 18px
}
h1 {
font-size: 22px;
margin: 0
}
select,
button {
background: #1a1d24;
color: #f1f1f1;
border: 1px solid #333;
padding: 8px 12px;
border-radius: 8px
}
button.primary {
background: #5c6df4;
border: none;
cursor: pointer
}
.tabs {
display: flex;
gap: 10px;
margin: 12px 0 20px
}
.tab {
padding: 8px 16px;
border-radius: 10px;
background: #1a1d24;
cursor: pointer
}
.tab.active {
background: #5c6df4
}
.grid {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 16px
}
.card {
background: #171a21;
border: 1px solid #262a35;
border-radius: 14px;
padding: 16px;
box-shadow: 0 0 12px #0003
}
.kpis {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 12px
}
.kpi {
background: #20232a;
border: 1px solid #2a2f3b;
border-radius: 10px;
padding: 14px;
text-align: center
}
.kpi h3 {
margin: 0 0 8px;
font-weight: 600;
color: #bfc3cc;
font-size: 13px
}
.kpi .v {
font-weight: 800;
font-size: 20px
}
.table {
width: 100%;
border-collapse: collapse
}
.table th,
.table td {
padding: 10px;
border-bottom: 1px solid #2a2f3b;
text-align: left;
font-size: 14px
}
.muted {
color: #9aa0aa;
font-size: 12px
}
.chart-wrap {
height: 280px
}
#ai-output {
white-space: pre-wrap;
background: #20232a;
border: 1px solid #2a2f3b;
border-radius: 10px;
padding: 14px
}
#ai-output p,
#ai-output ul {
font-size: 14px;
line-height: 1.4;
}
</style>
</head>
<body>
<div id="authModal" style="position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,1);display:flex;align-items:center;justify-content:center;z-index:9999;">
<div style="background:#1a1d24;padding:30px;border-radius:10px;text-align:center;width:320px;">
<h3 style="margin-bottom:15px;color:#f1f1f1;">Autenticação</h3>
<p style="color:#aaa;font-size:13px;margin-bottom:15px;">Insira o token de acesso para continuar</p>
<input type="password" id="tokenInput" placeholder="Token" style="width:100%;padding:8px;border-radius:6px;border:1px solid #333;background:#0f1116;color:#fff;">
<button id="checkToken" style="margin-top:15px;width:100%;background:#5c6df4;color:#fff;padding:8px;border:none;border-radius:6px;cursor:pointer;">Acessar</button>
<p id="errorMsg" style="color:#ff6666;font-size:12px;margin-top:10px;display:none;">Token inválido.</p>
</div>
</div>
<script>
const CORRECT_TOKEN = 'L1omruSe+gP.|,BGFSY,!,.a2yEZ6z+|;MrS]dmOt1o|+ZEJo.g!YgOV';
document.getElementById('checkToken').addEventListener('click', ()=>{
const val = document.getElementById('tokenInput').value.trim();
if(val === CORRECT_TOKEN){
localStorage.setItem('auth_token', val);
document.getElementById('authModal').style.display = 'none';
} else {
document.getElementById('errorMsg').style.display = 'block';
}
});
// Verifica se o token já está salvo
window.addEventListener('DOMContentLoaded', ()=>{
const saved = localStorage.getItem('auth_token');
if(saved === CORRECT_TOKEN){
document.getElementById('authModal').style.display = 'none';
}
});
</script>
<div class="container">
<header>
<h1><?= htmlspecialchars($cfg['app']['name']) ?></h1>
<div>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
<label>Período:
<input type="text" id="date-range" placeholder="Selecione o intervalo" style="background:#1a1d24;color:#f1f1f1;border:1px solid #333;padding:8px 12px;border-radius:8px;width:220px;">
</label>
<button id="refresh" class="primary">Atualizar</button>
</div>
</header>
<div class="tabs">
<div class="tab active" data-area="CRO">CRO</div>
<div class="tab" data-area="BI">BI</div>
<div class="tab" data-area="SEO">SEO</div>
</div>
<div class="grid">
<div class="card" style="grid-column:1/13">
<h3>KPIs principais</h3>
<div class="kpis">
<div class="kpi">
<h3>Sessões</h3>
<div class="v" id="kpi-sessions">--</div>
</div>
<div class="kpi">
<h3>Receita</h3>
<div class="v" id="kpi-revenue">--</div>
</div>
<div class="kpi">
<h3>Meta mensal</h3>
<div class="v" id="kpi-goal">--</div>
<div id="goal-bar" style="height:8px;background:#2a2f3b;border-radius:6px;overflow:hidden;margin-top:6px;">
<div id="goal-progress" style="height:8px;width:0;background:#26d7a0;transition:width .4s"></div>
</div>
</div>
<div class="kpi">
<h3>Novos usuários</h3>
<div class="v" id="kpi-users">--</div>
</div>
<div class="kpi">
<h3>Taxa de conversão</h3>
<div class="v" id="kpi-conv-rate">--</div>
</div>
<div class="kpi">
<h3>AOV</h3>
<div class="v" id="kpi-aov">--</div>
</div>
</div>
</div>
<div class="card" style="grid-column:1/13">
<h3>Sessões diárias</h3>
<div class="chart-wrap">
<canvas id="chart-sessions"></canvas>
</div>
</div>
<div class="card" style="grid-column:1/13">
<h3>Receita diária</h3>
<div class="chart-wrap">
<canvas id="chart-revenue"></canvas>
</div>
</div>
<div class="card" style="grid-column:1/7">
<h3>Top 5 produtos (por receita)</h3>
<table class="table" id="tbl-top"></table>
</div>
<div class="card" style="grid-column:7/13">
<h3>Receita mensal (MoM)</h3>
<div class="chart-wrap">
<canvas id="chart-monthly"></canvas>
</div>
<div class="muted" id="mom-note"></div>
</div>
<div class="card" style="grid-column:1/13">
<h3>Funil de Conversão</h3>
<div id="funnel-visual" style="display:flex;justify-content:flex-start;align-items:center;gap:20px;padding:10px 0"></div>
</div>
<div class="card" style="grid-column:1/13; display:none;">
<h3>Resumo de Produtos</h3>
<div id="product-summary" style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
<div>
<h4>Top 3 por conversão</h4>
<ul id="top-products"></ul>
</div>
<div>
<h4>Baixa adição ao carrinho</h4>
<ul id="low-cart-products"></ul>
</div>
</div>
</div>
<div class="card" style="grid-column:1/13">
<h3>Desempenho por Canal</h3>
<div class="chart-wrap">
<canvas id="chart-channels"></canvas>
</div>
</div>
<div class="card" style="grid-column:1/13">
<h3>Insights por IA (<span id="area-tag">CRO</span>)</h3>
<div id="ai-output">Carregando…</div>
</div>
</div>
</div>
<script>
let area = 'CRO', days = 60;
let cs, cr, cm; // charts
const el = id => document.getElementById(id);
async function fetchAnalyze() {
const qs = new URLSearchParams({ area, days: String(days) });
if (window.customDateRange && window.customDateRange.start && window.customDateRange.end) {
qs.set('start', window.customDateRange.start);
qs.set('end', window.customDateRange.end);
}
const r = await fetch(`api/analyze.php?${qs.toString()}`);
return r.json();
}
async function fetchAI(context) {
const r = await fetch('api/ai.php', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ area, context })
});
return r.json();
}
function n(v,d=0){ return Number(v||0).toLocaleString('pt-BR',{minimumFractionDigits:d,maximumFractionDigits:d}); }
function renderKPIs(k){
el('kpi-sessions').textContent = n(k.sessions);
el('kpi-revenue').textContent = 'R$ ' + n(k.revenue, 2);
el('kpi-users').textContent = n(k.new_users);
el('kpi-conv-rate').textContent = n(k.conv_rate,1) + '%';
el('kpi-aov').textContent = 'R$ ' + n(k.aov,2);
}
function renderProductSummary(items){
if(!items || items.length === 0) return;
const sortedByCart = [...items].sort((a,b)=>(b.add_to_cart/b.view_item)-(a.add_to_cart/a.view_item));
const top3 = sortedByCart.slice(0,3);
const low3 = sortedByCart.slice(-3);
el('top-products').innerHTML = top3.map(i=>
`<li>${i.item_name} — ${(i.add_to_cart/i.view_item*100).toFixed(1)}%</li>`
).join('');
el('low-cart-products').innerHTML = low3.map(i=>
`<li>${i.item_name} — ${(i.add_to_cart/i.view_item*100).toFixed(1)}%</li>`
).join('');
}
function buildLine(canvasId, label, labels, data, color){
const ctx = el(canvasId).getContext('2d');
const inst = new Chart(ctx, {
type:'line',
data:{ labels, datasets:[{ label, data, borderColor:color, tension:.25, pointRadius:0, fill:false }] },
options:{ responsive:true, maintainAspectRatio:false, plugins:{legend:{display:false}}, scales:{ x:{ grid:{color:'#222'}}, y:{ grid:{color:'#222'} } } }
});
return inst;
}
function renderSeries(series){
if (cs) cs.destroy(); if (cr) cr.destroy();
cs = buildLine('chart-sessions', 'Sessões', series.labels, series.sessions, '#8390ff');
cr = buildLine('chart-revenue', 'Receita', series.labels, series.revenue, '#26d7a0');
}
function renderTop(items){
const tbl = el('tbl-top');
tbl.innerHTML = '<tr><th>Produto</th><th>Receita</th><th>Qtd</th></tr>' +
(items||[]).map(i=>`<tr><td>${i.item_name}</td><td>R$ ${n(i.item_revenue,2)}</td><td>${n(i.items_purchased)}</td></tr>`).join('');
}
function renderMonthly(monthly, mom){
if (cm) cm.destroy();
const ctx = el('chart-monthly').getContext('2d');
cm = new Chart(ctx, {
type:'bar',
data:{ labels: monthly.labels||[], datasets:[{ label:'Receita', data: monthly.revenue||[] }]},
options:{ responsive:true, maintainAspectRatio:false, plugins:{legend:{display:false}}, scales:{ x:{ grid:{color:'#222'}}, y:{ grid:{color:'#222'} } } }
});
el('mom-note').textContent = (mom===null||mom===undefined) ? '' : `Variação mês/mês: ${n(mom,1)}%`;
}
// Função para converter markdown básico para HTML
function markdownToHTML(md){
if (!md) return '';
// helpers
const mdInline = (s) =>
s
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>') // **bold**
.replace(/_(.+?)_/g, '<em>$1</em>'); // _italic_
const lines = md.replace(/\r\n/g, '\n').split('\n');
let html = '';
let inUL = false, inOL = false;
let para = '';
const closePara = () => {
if (para.trim()) { html += `<p>${para.trim()}</p>`; para = ''; }
};
const closeLists = () => {
if (inUL) { html += '</ul>'; inUL = false; }
if (inOL) { html += '</ol>'; inOL = false; }
};
for (let raw of lines) {
const line = raw.trimRight();
// linha em branco => fecha parágrafo e listas
if (/^\s*$/.test(line)) {
closePara();
closeLists();
continue;
}
// headings
const h = line.match(/^(#{1,6})\s+(.*)$/);
if (h) {
closePara(); closeLists();
const level = h[1].length;
html += `<h${level}>${mdInline(h[2].trim())}</h${level}>`;
continue;
}
// lista ordenada: "1. item"
const ol = line.match(/^\d+\.\s+(.*)$/);
if (ol) {
closePara();
if (!inOL) { closeLists(); html += '<ol>'; inOL = true; }
html += `<li>${mdInline(ol[1].trim())}</li>`;
continue;
}
// lista com hífen: "- item"
const ul = line.match(/^-+\s+(.*)$/) || line.match(/^-\s+(.*)$/);
if (ul) {
closePara();
if (!inUL) { closeLists(); html += '<ul>'; inUL = true; }
html += `<li>${mdInline(ul[1].trim())}</li>`;
continue;
}
// linha normal => acumula no parágrafo atual
para += (para ? ' ' : '') + mdInline(line);
}
// fecha estruturas abertas
closePara();
closeLists();
return html;
}
function renderFunnel(funnel){
const container = el('funnel-visual');
if(!funnel || funnel.length === 0){
container.innerHTML = '<p class="muted">Sem dados de funil disponíveis</p>';
return;
}
const max = Math.max(...funnel.map(f=>f.count));
container.style.flexDirection = 'column';
container.style.alignItems = 'center';
container.innerHTML = funnel.map((f, i) => {
const width = 90 - i * 10; // cada etapa mais estreita
const opacity = 0.4 + i * 0.1; // leve diferença visual
return `
<div style="
width:${width}%;
background:#5c6df4;
height:40px;
border-radius:8px;
margin:4px 0;
display:flex;
align-items:center;
justify-content:center;
color:#fff;
font-size:13px;
font-weight:600;
opacity:${opacity};
transition:width 0.3s;
">
${f.event_name}: ${f.count.toLocaleString('pt-BR')}
</div>
`;
}).join('');
}
async function loadAll(){
const data = await fetchAnalyze();
if (!data.ok) { el('ai-output').textContent = data.error || 'Falha'; return; }
console.log(data);
renderKPIs(data.kpis);
renderSeries(data.series);
renderTop(data.top_items);
renderMonthly(data.monthly, data.monthly_mom);
// Adiciona renderização da meta mensal
renderGoal(data, <?= json_encode(preg_replace('/[^\d,]/', '', $cfg['ecommerce']['goal'])) ?>);
renderFunnel(data.funnel_events);
renderProductSummary(data.top_items);
renderChannels(data.channels);
el('area-tag').textContent = area;
el('ai-output').textContent = 'Gerando insights…';
const cacheKey = `ai_cache_${area}`;
const cache = JSON.parse(localStorage.getItem(cacheKey) || '{}');
const today = new Date().toISOString().slice(0,10);
if(cache.date === today && cache.text){
el('ai-output').innerHTML = markdownToHTML(cache.text) + '<br><br><small class="muted">🔁 Insight carregado do cache (últimas 24h)</small>';
return;
}
const ai = await fetchAI({
kpis:data.kpis, trend:data.trend, series:data.series,
top_items:data.top_items, monthly:data.monthly, channels:data.channels
});
if(ai.text){
localStorage.setItem(cacheKey, JSON.stringify({ date: today, text: ai.text }));
}
el('ai-output').innerHTML = markdownToHTML(ai.text || 'Sem resposta da IA.');
}
// Função para renderizar progresso da meta mensal (ajustada para filtro de datas)
function renderGoal(data, goal){
if(!data || !data.series || !data.series.labels) return;
// Converte meta de string "R$ 15.672.897" para número
const numericGoal = parseFloat(String(goal).replace(/[^\d,.-]/g, '').replace(/\./g, '').replace(',', '.')) || 0;
// Determina intervalo selecionado (customDateRange ou padrão)
let startDate, endDate;
if (window.customDateRange) {
startDate = new Date(window.customDateRange.start);
endDate = new Date(window.customDateRange.end);
} else {
endDate = new Date();
startDate = new Date();
startDate.setDate(endDate.getDate() - days);
}
// Usa o mesmo valor do KPI de receita (já filtrado no backend)
let monthlyRevenue = parseFloat(data.kpis.revenue || 0);
// Normaliza escala caso a receita esteja 100x maior que a meta (ex.: centavos x reais)
if (numericGoal > 0 && monthlyRevenue > numericGoal * 20) {
monthlyRevenue = monthlyRevenue / 100;
}
// Calcula progresso em relação à meta (%) corretamente
const pct = numericGoal > 0 ? (monthlyRevenue / numericGoal) * 100 : 0;
// Atualiza elementos no front-end
el('kpi-goal').innerHTML = `
${pct.toFixed(1)}%<br>
<small style="color:#9aa0aa;font-size:12px;">
R$ ${n(monthlyRevenue,2)} / R$ ${n(numericGoal,2)}
</small>
`;
el('goal-progress').style.width = Math.min(100, pct) + '%';
}
// UI
document.querySelectorAll('.tab').forEach(t=>{
t.addEventListener('click', ()=>{
document.querySelectorAll('.tab').forEach(x=>x.classList.remove('active'));
t.classList.add('active'); area = t.dataset.area; loadAll();
});
});
flatpickr("#date-range", {
mode: "range",
dateFormat: "Y-m-d",
locale: "pt",
onClose: async function(selectedDates) {
if (selectedDates.length === 2) {
const [start, end] = selectedDates;
const startStr = start.toISOString().slice(0,10);
const endStr = end.toISOString().slice(0,10);
// Atualiza GA4 com base nas novas datas
await fetch(`api/refresh_ga4.php?start=${startStr}&end=${endStr}`);
window.customDateRange = { start: startStr, end: endStr };
loadAll();
}
}
});
document.getElementById('refresh').addEventListener('click', ()=>{
const fp = document.querySelector('#date-range')._flatpickr;
if (fp.selectedDates.length === 2) {
const [start, end] = fp.selectedDates;
window.customDateRange = {
start: start.toISOString().slice(0,10),
end: end.toISOString().slice(0,10)
};
} else {
window.customDateRange = null;
}
loadAll();
});
loadAll();
function renderChannels(channels){
const ctx = document.getElementById('chart-channels').getContext('2d');
if(window.chartChannels) window.chartChannels.destroy();
const labels = channels.map(c=>c.channel);
const revenue = channels.map(c=>c.revenue);
const transactions = channels.map(c=>c.transactions);
window.chartChannels = new Chart(ctx, {
type:'bar',
data:{ labels, datasets:[{ label:'Receita (R$)', data:revenue, backgroundColor:'#26d7a0' }] },
options:{ responsive:true, plugins:{ tooltip:{ callbacks:{ afterBody:(ctx)=>`Transações: ${transactions[ctx[0].dataIndex]}` } } } }
});
}
</script>
</body>
</html>