Files
portal-epromos/admin/dashboard_landing_widgets_adv.php

212 lines
7.9 KiB
PHP

<?php
// ruleta/admin/dashboard_landing_widgets_adv.php
if (!isset($conn)) { require_once __DIR__ . '/db.php'; }
if (session_status() === PHP_SESSION_NONE) { session_start(); }
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
?>
<div class="container-fluid px-0" id="landing-widget">
<div class="d-flex flex-wrap align-items-end gap-2 mb-2">
<h4 class="mb-0">Landings</h4>
<div class="ms-auto d-flex flex-wrap gap-2">
<div>
<label class="form-label mb-0 small">Landing</label>
<select id="lw-landing" class="form-select form-select-sm" style="min-width:220px">
<option value="0">— Todas las landings —</option>
</select>
</div>
<div>
<label class="form-label mb-0 small">Desde</label>
<input type="date" id="lw-from" class="form-control form-control-sm" />
</div>
<div>
<label class="form-label mb-0 small">Hasta</label>
<input type="date" id="lw-to" class="form-control form-control-sm" />
</div>
<button id="lw-refresh" class="btn btn-sm btn-primary">Actualizar</button>
</div>
</div>
<!-- KPIs -->
<div class="row g-3" id="lw-kpis">
<!-- Se llena por JS -->
</div>
<!-- Top por CR -->
<div class="card shadow-sm mt-3">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-2">
<h5 class="mb-0">Top CR por Landing</h5>
<a href="landing.php" class="btn btn-sm btn-outline-primary">Ver todas</a>
</div>
<div class="table-responsive">
<table class="table table-sm align-middle" id="lw-top-table">
<thead>
<tr>
<th>Landing</th>
<th class="text-end">Impresiones</th>
<th class="text-end">Conversiones</th>
<th class="text-end">CR %</th>
<th style="width:180px">Tendencia</th>
<th class="text-nowrap">Acciones</th>
</tr>
</thead>
<tbody>
<!-- Se llena por JS -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script>
(function() {
const $ = (s) => document.querySelector(s);
const API = 'dashboard_landing_widget_data.php';
function fmt(n){ return new Intl.NumberFormat().format(n||0); }
function esc(s){ const d=document.createElement('div'); d.textContent=(s??''); return d.innerHTML; }
function defaultDates(){
const to = new Date();
const from = new Date(); from.setDate(to.getDate()-30);
const f = (d)=>d.toISOString().slice(0,10);
$('#lw-from').value = f(from);
$('#lw-to').value = f(to);
}
async function fetchData(includeList=false){
const params = new URLSearchParams({
from: $('#lw-from').value || '',
to: $('#lw-to').value || '',
landing_id: $('#lw-landing').value || '0',
meta: includeList ? '1' : '0'
});
const res = await fetch(API + '?' + params.toString(), {credentials:'same-origin'});
const txt = await res.text();
try { return JSON.parse(txt); }
catch(e){ console.error('Respuesta no JSON:', txt); throw e; }
}
function renderLandingOptions(list){
const sel = $('#lw-landing');
const cur = sel.value || '0';
sel.innerHTML = '<option value="0">— Todas las landings —</option>';
(list||[]).forEach(r=>{
const opt = document.createElement('option');
opt.value = r.id;
opt.textContent = r.titulo + ' (#'+ r.id +')';
sel.appendChild(opt);
});
// mantener selección si existía
if ([...sel.options].some(o=>o.value===cur)) sel.value = cur;
}
function renderKPIs(k){
$('#lw-kpis').innerHTML = `
<div class="col-md-3"><div class="card shadow-sm border-0"><div class="card-body d-flex justify-content-between">
<div><h6 class="text-muted mb-1">Landings</h6><h3 class="mb-0">${fmt(k.landings)}</h3></div><div class="fs-3">🧭</div></div></div></div>
<div class="col-md-3"><div class="card shadow-sm border-0"><div class="card-body d-flex justify-content-between">
<div><h6 class="text-muted mb-1">Variantes</h6><h3 class="mb-0">${fmt(k.variants)}</h3></div><div class="fs-3">🧪</div></div></div></div>
<div class="col-md-3"><div class="card shadow-sm border-0"><div class="card-body d-flex justify-content-between">
<div><h6 class="text-muted mb-1">Bloques</h6><h3 class="mb-0">${fmt(k.blocks)}</h3></div><div class="fs-3">🧱</div></div></div></div>
<div class="col-md-3"><div class="card shadow-sm border-0"><div class="card-body d-flex justify-content-between">
<div><h6 class="text-muted mb-1">Leads ${k.filtered ? '(landing seleccionada)' : '(rango)'}<br><small class="text-muted">${k.range_label}</small></h6>
<h3 class="mb-0">${fmt(k.leads_window)}</h3></div><div class="fs-3">📈</div></div></div></div>
`;
}
function makeSparkline(canvas, series){
const labels = series.map(p=>p.d.slice(5)); // MM-DD
const imps = series.map(p=>p.imp);
const conv = series.map(p=>p.conv);
new Chart(canvas, {
type: 'line',
data: {
labels,
datasets: [
{ label:'Imps', data: imps, tension: .3, pointRadius: 0 },
{ label:'Conv', data: conv, tension: .3, pointRadius: 0, yAxisID:'y1' }
]
},
options:{
responsive:true, maintainAspectRatio:false,
scales: {
x: { display:false },
y: { display:false, beginAtZero:true },
y1:{ display:false, beginAtZero:true, position:'right' }
},
plugins: { legend:{display:false}, tooltip:{enabled:true} }
}
});
}
function renderTop(rows){
const tbody = $('#lw-top-table tbody');
tbody.innerHTML = '';
rows.forEach(r=>{
const tr = document.createElement('tr');
tr.innerHTML = `
<td class="fw-semibold">${esc(r.titulo)}</td>
<td class="text-end">${fmt(r.imps)}</td>
<td class="text-end">${fmt(r.conv)}</td>
<td class="text-end">${r.cr!==null ? (r.cr.toFixed(2)) : '—'}</td>
<td><div style="height:38px"><canvas id="spk-${r.landing_id}"></canvas></div></td>
<td class="text-nowrap">
<a class="btn btn-sm btn-outline-secondary" href="landing_stats.php?landing_id=${r.landing_id}">Stats</a>
<a class="btn btn-sm btn-outline-secondary" href="landing_variants.php?landing_id=${r.landing_id}">Variantes</a>
<a class="btn btn-sm btn-outline-secondary" href="landing_builder.php?landing_id=${r.landing_id}">Builder</a>
<a class="btn btn-sm btn-outline-secondary" href="landing_crm.php?landing_id=${r.landing_id}">CRM</a>
<a class="btn btn-sm btn-outline-secondary" href="landing_forms.php?landing_id=${r.landing_id}">Leads</a>
</td>
`;
tbody.appendChild(tr);
// sparkline
const cv = document.getElementById(`spk-${r.landing_id}`);
if (cv && r.series && r.series.length) makeSparkline(cv, r.series);
});
if (!rows.length){
tbody.innerHTML = `<tr><td colspan="6" class="text-center text-muted">Sin datos</td></tr>`;
}
}
async function boot(){
defaultDates();
// Primer fetch: pedir datos + lista de landings
try {
const data = await fetchData(true);
if (data.landings_list) renderLandingOptions(data.landings_list);
renderKPIs(data.kpis);
renderTop(data.top);
} catch(e) {
console.error(e); alert('No se pudo cargar el widget de landings.');
}
}
async function refresh(){
$('#lw-refresh').disabled = true;
try{
const data = await fetchData(false);
renderKPIs(data.kpis);
renderTop(data.top);
}catch(e){
console.error(e);
alert('No se pudo cargar el widget de landings.');
}finally{
$('#lw-refresh').disabled = false;
}
}
// eventos
$('#lw-refresh').addEventListener('click', refresh);
$('#lw-landing').addEventListener('change', refresh);
// iniciar
boot();
})();
</script>