212 lines
7.9 KiB
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>
|