cambios importates

This commit is contained in:
Lizandro Guarnizo
2026-05-11 12:21:56 -05:00
parent 39de37e0a7
commit f197118acb
7 changed files with 1364 additions and 13 deletions
+502 -1
View File
@@ -202,6 +202,194 @@ try {
</div>
</div>
<!-- ══ KPI: Actividad de Pacientes ══════════════════════════════ -->
<div class="row mt-4" id="kpi-actividad-section">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white d-flex flex-wrap align-items-center gap-2 justify-content-between">
<h5 class="mb-0"><i class="fas fa-chart-bar text-primary me-2"></i>Actividad de Pacientes — KPI</h5>
<div class="d-flex flex-wrap gap-2 align-items-center">
<!-- Rango rápido -->
<div class="btn-group btn-group-sm" role="group">
<button class="btn btn-outline-secondary kpi-quick" data-days="7">7 días</button>
<button class="btn btn-secondary kpi-quick active" data-days="30">30 días</button>
<button class="btn btn-outline-secondary kpi-quick" data-days="90">90 días</button>
</div>
<!-- Rango personalizado -->
<input type="date" id="kpi-desde" class="form-control form-control-sm" style="width:140px">
<span class="text-muted small"></span>
<input type="date" id="kpi-hasta" class="form-control form-control-sm" style="width:140px">
<button class="btn btn-primary btn-sm" onclick="cargarKPI()">
<i class="fas fa-filter me-1"></i>Filtrar
</button>
</div>
</div>
<div class="card-body">
<!-- Tarjetas de totales -->
<div class="row g-3 mb-4" id="kpi-cards">
<div class="col-6 col-md-3">
<div class="card border-0 bg-primary bg-opacity-10 text-center py-3">
<div class="fs-2 fw-bold text-primary" id="kpi-total-usuarios">—</div>
<div class="small text-muted mt-1"><i class="fas fa-users me-1"></i>Usuarios únicos</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card border-0 bg-success bg-opacity-10 text-center py-3">
<div class="fs-2 fw-bold text-success" id="kpi-total-mensajes">—</div>
<div class="small text-muted mt-1"><i class="fab fa-whatsapp me-1"></i>Mensajes recibidos</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card border-0 bg-warning bg-opacity-10 text-center py-3">
<div class="fs-2 fw-bold text-warning" id="kpi-total-agend">—</div>
<div class="small text-muted mt-1"><i class="fas fa-calendar-check me-1"></i>Agendamientos</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card border-0 bg-info bg-opacity-10 text-center py-3">
<div class="fs-4 fw-bold text-info" id="kpi-pico-dia">—</div>
<div class="small text-muted mt-1"><i class="fas fa-trophy me-1"></i>Día más activo</div>
</div>
</div>
</div>
<!-- Gráficas -->
<div class="row g-3">
<div class="col-lg-8">
<div class="card border-0 bg-light">
<div class="card-body">
<h6 class="text-muted mb-3"><i class="fas fa-calendar-alt me-1"></i>Por día</h6>
<div style="position:relative; height:220px">
<canvas id="kpiChartDia"></canvas>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card border-0 bg-light">
<div class="card-body">
<h6 class="text-muted mb-3"><i class="fas fa-calendar-week me-1"></i>Por día de la semana</h6>
<div style="position:relative; height:220px">
<canvas id="kpiChartSemana"></canvas>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ════════════════════════════════════════════════════════════ -->
<!-- ══ KPI: Tiempo de Respuesta de Asesores ════════════════════ -->
<div class="row mt-4" id="kpi-asesor-section">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white d-flex flex-wrap align-items-center gap-2 justify-content-between">
<h5 class="mb-0"><i class="fas fa-stopwatch text-danger me-2"></i>Tiempo de Respuesta de Asesores</h5>
<div class="d-flex flex-wrap gap-2 align-items-center">
<div class="btn-group btn-group-sm" role="group">
<button class="btn btn-outline-secondary kpi-asesor-quick" data-days="7">7 días</button>
<button class="btn btn-secondary kpi-asesor-quick active" data-days="30">30 días</button>
<button class="btn btn-outline-secondary kpi-asesor-quick" data-days="90">90 días</button>
</div>
<input type="date" id="kpia-desde" class="form-control form-control-sm" style="width:140px">
<span class="text-muted small"></span>
<input type="date" id="kpia-hasta" class="form-control form-control-sm" style="width:140px">
<button class="btn btn-danger btn-sm" onclick="cargarKPIAsesor()">
<i class="fas fa-filter me-1"></i>Filtrar
</button>
</div>
</div>
<div class="card-body">
<!-- Tarjetas de totales -->
<div class="row g-3 mb-4">
<div class="col-6 col-md-2">
<div class="card border-0 bg-danger bg-opacity-10 text-center py-3">
<div class="fs-2 fw-bold text-danger" id="kpia-total-sesiones">—</div>
<div class="small text-muted mt-1"><i class="fas fa-headset me-1"></i>Atenciones</div>
</div>
</div>
<div class="col-6 col-md-2">
<div class="card border-0 bg-warning bg-opacity-10 text-center py-3">
<div class="fs-2 fw-bold text-warning" id="kpia-prom-resp">—</div>
<div class="small text-muted mt-1"><i class="fas fa-clock me-1"></i>Prom. 1ª respuesta</div>
</div>
</div>
<div class="col-6 col-md-2">
<div class="card border-0 bg-info bg-opacity-10 text-center py-3">
<div class="fs-2 fw-bold text-info" id="kpia-prom-dur">—</div>
<div class="small text-muted mt-1"><i class="fas fa-hourglass-half me-1"></i>Prom. duración</div>
</div>
</div>
<div class="col-6 col-md-2">
<div class="card border-0 bg-success bg-opacity-10 text-center py-3">
<div class="fs-2 fw-bold text-success" id="kpia-pct-rapidas">—</div>
<div class="small text-muted mt-1"><i class="fas fa-bolt me-1"></i>Resp. &lt; 3 min</div>
</div>
</div>
<div class="col-6 col-md-2">
<div class="card border-0 bg-secondary bg-opacity-10 text-center py-3">
<div class="fs-2 fw-bold text-secondary" id="kpia-sin-resp">—</div>
<div class="small text-muted mt-1"><i class="fas fa-comment-slash me-1"></i>Sin respuesta</div>
</div>
</div>
</div>
<!-- Gráficas -->
<div class="row g-3 mb-3">
<div class="col-lg-8">
<div class="card border-0 bg-light">
<div class="card-body">
<h6 class="text-muted mb-3"><i class="fas fa-chart-line me-1"></i>Promedio de respuesta por día (minutos)</h6>
<div style="position:relative; height:200px">
<canvas id="kpiaChartDia"></canvas>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card border-0 bg-light">
<div class="card-body">
<h6 class="text-muted mb-3"><i class="fas fa-chart-pie me-1"></i>Distribución de tiempos</h6>
<div style="position:relative; height:200px">
<canvas id="kpiaChartHist"></canvas>
</div>
</div>
</div>
</div>
</div>
<!-- Tabla por asesor -->
<div class="card border-0 bg-light">
<div class="card-body p-0">
<h6 class="text-muted mb-0 px-3 pt-3"><i class="fas fa-user-tie me-1"></i>Detalle por asesor</h6>
<div class="table-responsive">
<table class="table table-sm table-hover mb-0 mt-2" id="kpia-tabla-asesores">
<thead class="table-light">
<tr>
<th>Asesor</th>
<th class="text-center">Atenciones</th>
<th class="text-center">Prom. 1ª respuesta</th>
<th class="text-center">Prom. duración</th>
<th>Velocidad</th>
</tr>
</thead>
<tbody id="kpia-body-asesores">
<tr><td colspan="5" class="text-center text-muted py-3"><i class="fas fa-spinner fa-spin"></i></td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ════════════════════════════════════════════════════════════ -->
<!-- Rate Limiting Monitor -->
<div class="row mt-4">
<div class="col-12">
@@ -2312,7 +2500,320 @@ try {
});
})();
</script>
<!-- ── / Historial Envíos Masivos JS ────────────────────────────────── -->
<!-- ── KPI Actividad de Pacientes JS ────────────────────────────────── -->
<script>
(function () {
'use strict';
let chartDia = null;
let chartSemana = null;
const fmt = n => Number(n).toLocaleString('es-CO');
// ── Inicializar fechas en los inputs ──────────────────────────────
function initFechas(days = 30) {
const hoy = new Date();
const desde = new Date(hoy); desde.setDate(hoy.getDate() - (days - 1));
document.getElementById('kpi-hasta').value = hoy.toISOString().slice(0, 10);
document.getElementById('kpi-desde').value = desde.toISOString().slice(0, 10);
}
// ── Carga y renderiza ─────────────────────────────────────────────
window.cargarKPI = async function () {
const desde = document.getElementById('kpi-desde').value;
const hasta = document.getElementById('kpi-hasta').value;
if (!desde || !hasta) return;
try {
const r = await fetch(`api/kpi_actividad.php?desde=${desde}&hasta=${hasta}`);
const d = await r.json();
if (!d.ok) throw new Error(d.error || 'Error');
// Tarjetas
document.getElementById('kpi-total-usuarios').textContent = fmt(d.totales.usuarios);
document.getElementById('kpi-total-mensajes').textContent = fmt(d.totales.mensajes);
document.getElementById('kpi-total-agend').textContent = fmt(d.totales.agendamientos);
document.getElementById('kpi-pico-dia').textContent = d.totales.pico_semana;
// Gráfica por día
const labsDia = d.por_dia.map(x => {
const f = new Date(x.fecha + 'T00:00:00');
return f.toLocaleDateString('es-CO', { day:'2-digit', month:'short' });
});
const dataUsuarios = d.por_dia.map(x => x.usuarios);
const dataAgend = d.por_dia.map(x => x.agendamientos);
if (chartDia) chartDia.destroy();
chartDia = new Chart(document.getElementById('kpiChartDia'), {
type: 'bar',
data: {
labels: labsDia,
datasets: [
{
label: 'Usuarios que escriben',
data: dataUsuarios,
backgroundColor: 'rgba(13,110,253,0.6)',
borderRadius: 4,
order: 1,
},
{
label: 'Agendamientos',
data: dataAgend,
backgroundColor: 'rgba(255,193,7,0.75)',
borderRadius: 4,
order: 2,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { position: 'top' } },
scales: {
x: { ticks: { maxRotation: 45, font: { size: 10 } } },
y: { beginAtZero: true, ticks: { precision: 0 } },
},
},
});
// Gráfica por día de semana
const labsSem = d.por_dia_semana.map(x => x.dia);
const dataSemU = d.por_dia_semana.map(x => x.usuarios);
const dataSemA = d.por_dia_semana.map(x => x.agendamientos);
if (chartSemana) chartSemana.destroy();
chartSemana = new Chart(document.getElementById('kpiChartSemana'), {
type: 'bar',
data: {
labels: labsSem,
datasets: [
{
label: 'Usuarios',
data: dataSemU,
backgroundColor: 'rgba(13,110,253,0.6)',
borderRadius: 4,
},
{
label: 'Agendamientos',
data: dataSemA,
backgroundColor: 'rgba(255,193,7,0.75)',
borderRadius: 4,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { position: 'top' } },
scales: {
y: { beginAtZero: true, ticks: { precision: 0 } },
},
},
});
} catch (e) {
console.error('KPI error:', e);
}
};
// ── Botones de rango rápido ───────────────────────────────────────
document.querySelectorAll('.kpi-quick').forEach(btn => {
btn.addEventListener('click', function () {
document.querySelectorAll('.kpi-quick').forEach(b => {
b.classList.remove('btn-secondary', 'active');
b.classList.add('btn-outline-secondary');
});
this.classList.remove('btn-outline-secondary');
this.classList.add('btn-secondary', 'active');
initFechas(parseInt(this.dataset.days, 10));
cargarKPI();
});
});
// ── Carga inicial (30 días por defecto) ───────────────────────────
// Solo se ejecuta cuando la pestaña dashboard está visible
function kpiInit() {
initFechas(30);
cargarKPI();
}
// Esperar a que el tab dashboard esté activo
const observer = new MutationObserver(() => {
const sec = document.getElementById('kpi-actividad-section');
if (sec && sec.closest('.tab-content')?.style.display !== 'none') {
kpiInit();
observer.disconnect();
}
});
// Si la sección ya es visible al cargar, inicializar de inmediato
document.addEventListener('DOMContentLoaded', () => {
const sec = document.getElementById('kpi-actividad-section');
if (sec) kpiInit();
});
})();
</script>
<!-- ── KPI Tiempo de Respuesta Asesores JS ──────────────────────────── -->
<script>
(function () {
'use strict';
let chartDiaA = null;
let chartHistA = null;
const fmtMin = m => m === null ? '—' : (m < 60 ? m + ' min' : Math.floor(m/60) + 'h ' + (m%60) + 'm');
const escA = s => String(s||'').replace(/[<>&"]/g, c => ({'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;'}[c]));
function initFechasA(days = 30) {
const hoy = new Date();
const desde = new Date(hoy); desde.setDate(hoy.getDate() - (days - 1));
document.getElementById('kpia-hasta').value = hoy.toISOString().slice(0, 10);
document.getElementById('kpia-desde').value = desde.toISOString().slice(0, 10);
}
function velocidadBadge(min) {
if (min === null) return '<span class="badge bg-secondary">Sin datos</span>';
if (min < 3) return '<span class="badge bg-success">Muy rápido</span>';
if (min < 10) return '<span class="badge bg-primary">Normal</span>';
if (min < 20) return '<span class="badge bg-warning text-dark">Lento</span>';
return '<span class="badge bg-danger">Muy lento</span>';
}
window.cargarKPIAsesor = async function () {
const desde = document.getElementById('kpia-desde').value;
const hasta = document.getElementById('kpia-hasta').value;
if (!desde || !hasta) return;
try {
const r = await fetch(`api/kpi_respuesta_asesor.php?desde=${desde}&hasta=${hasta}`);
const d = await r.json();
if (!d.ok) throw new Error(d.error || 'Error');
const t = d.totales;
document.getElementById('kpia-total-sesiones').textContent = t.sesiones;
document.getElementById('kpia-prom-resp').textContent = fmtMin(t.prom_respuesta);
document.getElementById('kpia-prom-dur').textContent = fmtMin(t.prom_duracion);
document.getElementById('kpia-pct-rapidas').textContent = t.pct_rapidas + '%';
document.getElementById('kpia-sin-resp').textContent = t.sin_respuesta;
// Gráfica por día
const labsDia = d.serie_dia.map(x => {
const f = new Date(x.fecha + 'T00:00:00');
return f.toLocaleDateString('es-CO', { day:'2-digit', month:'short' });
});
const dataResp = d.serie_dia.map(x => x.prom_respuesta);
const dataSes = d.serie_dia.map(x => x.sesiones);
if (chartDiaA) chartDiaA.destroy();
chartDiaA = new Chart(document.getElementById('kpiaChartDia'), {
data: {
labels: labsDia,
datasets: [
{
type: 'line',
label: 'Prom. respuesta (min)',
data: dataResp,
borderColor: 'rgba(220,53,69,0.9)',
backgroundColor: 'rgba(220,53,69,0.1)',
fill: true,
tension: 0.3,
pointRadius: 3,
yAxisID: 'y',
},
{
type: 'bar',
label: 'Atenciones',
data: dataSes,
backgroundColor: 'rgba(108,117,125,0.3)',
borderRadius: 3,
yAxisID: 'y2',
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { position: 'top' } },
scales: {
x: { ticks: { maxRotation: 45, font: { size: 10 } } },
y: { beginAtZero: true, ticks: { precision: 0 }, title: { display: true, text: 'Min' } },
y2: { beginAtZero: true, position: 'right', grid: { drawOnChartArea: false }, ticks: { precision: 0 }, title: { display: true, text: 'Atenciones' } },
},
},
});
// Histograma de distribución
const labsH = d.histograma.map(x => x.rango);
const dataH = d.histograma.map(x => x.sesiones);
const colores = [
'rgba(25,135,84,0.7)', // < 1 min verde
'rgba(13,110,253,0.7)', // 1-3 min azul
'rgba(255,193,7,0.7)', // 3-5 min amarillo
'rgba(253,126,20,0.7)', // 5-10 min naranja
'rgba(220,53,69,0.7)', // 10-20 rojo
'rgba(108,17,17,0.7)', // >20 rojo oscuro
'rgba(108,117,125,0.7)', // Sin resp gris
];
if (chartHistA) chartHistA.destroy();
chartHistA = new Chart(document.getElementById('kpiaChartHist'), {
type: 'doughnut',
data: {
labels: labsH,
datasets: [{ data: dataH, backgroundColor: colores, borderWidth: 1 }],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'right', labels: { font: { size: 11 } } },
},
},
});
// Tabla de asesores
const tbody = document.getElementById('kpia-body-asesores');
if (!d.asesores.length) {
tbody.innerHTML = '<tr><td colspan="5" class="text-center text-muted py-2">Sin datos</td></tr>';
} else {
tbody.innerHTML = d.asesores.map(a => `
<tr>
<td class="fw-semibold">${escA(a.operador)}</td>
<td class="text-center">${a.sesiones}</td>
<td class="text-center">${fmtMin(a.prom_respuesta)}</td>
<td class="text-center">${fmtMin(a.prom_duracion)}</td>
<td>${velocidadBadge(a.prom_respuesta)}</td>
</tr>
`).join('');
}
} catch (e) {
console.error('KPI Asesores error:', e);
}
};
// Botones rápidos
document.querySelectorAll('.kpi-asesor-quick').forEach(btn => {
btn.addEventListener('click', function () {
document.querySelectorAll('.kpi-asesor-quick').forEach(b => {
b.classList.remove('btn-secondary', 'active');
b.classList.add('btn-outline-secondary');
});
this.classList.remove('btn-outline-secondary');
this.classList.add('btn-secondary', 'active');
initFechasA(parseInt(this.dataset.days, 10));
cargarKPIAsesor();
});
});
// Carga inicial
document.addEventListener('DOMContentLoaded', () => {
const sec = document.getElementById('kpi-asesor-section');
if (sec) { initFechasA(30); cargarKPIAsesor(); }
});
})();
</script>
<!-- ── / KPI Tiempo de Respuesta Asesores JS ─────────────────────────── -->
</body>
</html>