553 lines
23 KiB
PHP
553 lines
23 KiB
PHP
<?php
|
||
/**
|
||
* modules/turnero/views/historial.php
|
||
* Historial consultable de turnos con filtros multi-día y paginación.
|
||
*/
|
||
require_once APP_ROOT . '/config/config.php';
|
||
if (!isUserLoggedIn()) { header('Location: ' . BASE_URL . 'login.php'); exit; }
|
||
|
||
Layout::open('Historial de Turnos', 'fas fa-history');
|
||
?>
|
||
<style>
|
||
body { background:#f1f5f9; }
|
||
|
||
/* ── Encabezado ── */
|
||
.page-header {
|
||
background:#fff; border-bottom:1px solid #e2e8f0;
|
||
padding:12px 22px; display:flex; align-items:center; gap:10px; flex-wrap:wrap;
|
||
}
|
||
.page-header h1 { font-size:1.12rem; font-weight:700; color:#1e293b; margin:0; flex:1; min-width:150px; }
|
||
|
||
/* ── Filtros ── */
|
||
.filter-bar {
|
||
background:#fff; border-bottom:1px solid #e2e8f0;
|
||
padding:12px 22px; display:flex; align-items:flex-end; gap:10px; flex-wrap:wrap;
|
||
}
|
||
.filter-group { display:flex; flex-direction:column; gap:3px; }
|
||
.filter-group label { font-size:.7rem; font-weight:600; text-transform:uppercase;
|
||
letter-spacing:.06em; color:#64748b; }
|
||
.filter-group .form-control,
|
||
.filter-group .form-select { font-size:.82rem; min-width:120px; }
|
||
.filter-group.wide .form-control { min-width:200px; }
|
||
|
||
/* ── Contenido ── */
|
||
.content-wrap { max-width:1200px; margin:22px auto; padding:0 16px 60px; }
|
||
|
||
/* ── KPIs resumen ── */
|
||
.kpi-strip {
|
||
display:flex; flex-wrap:wrap; gap:10px; margin-bottom:18px;
|
||
}
|
||
.kpi-chip {
|
||
display:flex; align-items:center; gap:7px;
|
||
background:#fff; border:1px solid #e2e8f0; border-radius:99px;
|
||
padding:5px 14px; font-size:.8rem;
|
||
}
|
||
.kpi-chip .chip-val { font-weight:800; font-size:1rem; }
|
||
.kpi-chip .chip-lbl { color:#64748b; }
|
||
|
||
/* ── Tabla ── */
|
||
.section-card { background:#fff; border:1px solid #e2e8f0; border-radius:12px;
|
||
padding:0; overflow:hidden; }
|
||
.section-card .table { margin:0; }
|
||
.section-card .table thead th {
|
||
background:#f8fafc; font-size:.7rem; text-transform:uppercase;
|
||
letter-spacing:.07em; color:#475569; font-weight:700;
|
||
padding:10px 12px; white-space:nowrap; border-bottom:2px solid #e2e8f0;
|
||
}
|
||
.section-card .table tbody td { padding:9px 12px; font-size:.82rem; vertical-align:middle; }
|
||
.section-card .table tbody tr:hover { background:#f8fafc; }
|
||
|
||
/* ── Estado badges ── */
|
||
.eb { font-size:.65rem; padding:2px 8px; border-radius:99px; font-weight:700; white-space:nowrap; }
|
||
.eb-espera { background:#fef9c3; color:#78350f; }
|
||
.eb-en_recepcion { background:#dbeafe; color:#1e40af; }
|
||
.eb-en_espera_lugar { background:#fde68a; color:#92400e; }
|
||
.eb-en_servicio { background:#dcfce7; color:#166534; }
|
||
.eb-finalizado { background:#f1f5f9; color:#334155; }
|
||
.eb-ausente { background:#fee2e2; color:#991b1b; }
|
||
.eb-cancelado { background:#f1f5f9; color:#9ca3af; }
|
||
|
||
/* ── Paginación ── */
|
||
.pagination-wrap {
|
||
display:flex; align-items:center; justify-content:space-between;
|
||
padding:12px 18px; border-top:1px solid #e2e8f0; flex-wrap:wrap; gap:8px;
|
||
}
|
||
.pagination-info { font-size:.78rem; color:#64748b; }
|
||
.page-btns { display:flex; gap:4px; }
|
||
.page-btns button { font-size:.78rem; padding:3px 10px; border-radius:6px;
|
||
border:1px solid #e2e8f0; background:#fff; cursor:pointer; }
|
||
.page-btns button.active { background:var(--brand,#2563eb); color:#fff; border-color:var(--brand,#2563eb); }
|
||
.page-btns button:disabled { opacity:.4; cursor:default; }
|
||
|
||
/* ── Estado vacío ── */
|
||
.empty-state { text-align:center; color:#94a3b8; padding:60px 20px; font-size:.9rem; }
|
||
.empty-state i { font-size:2.5rem; display:block; margin-bottom:10px; }
|
||
|
||
/* ── Spinner ── */
|
||
.spinner-row td { text-align:center; padding:40px; color:#94a3b8; }
|
||
|
||
/* ── Detalle expandible ── */
|
||
.detail-row { background:#f8fafc; }
|
||
.detail-row td { padding:0 !important; }
|
||
.detail-body { padding:10px 18px 14px; }
|
||
.detail-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(160px,1fr)); gap:8px; }
|
||
.detail-item { background:#fff; border:1px solid #e2e8f0; border-radius:8px; padding:8px 12px; }
|
||
.detail-item .d-lbl { font-size:.67rem; text-transform:uppercase; letter-spacing:.07em; color:#94a3b8; }
|
||
.detail-item .d-val { font-size:.85rem; font-weight:600; color:#1e293b; }
|
||
</style>
|
||
|
||
<!-- ── Encabezado ────────────────────────────────────────────── -->
|
||
<div class="page-header">
|
||
<a href="<?= BASE_URL ?>/erp.php?m=turnero&v=dashboard" class="text-muted text-decoration-none small">
|
||
<i class="fas fa-arrow-left me-1"></i>Dashboard
|
||
</a>
|
||
<h1><i class="fas fa-history me-2 text-primary"></i>Historial de Turnos</h1>
|
||
<button class="btn btn-outline-success btn-sm" id="btnExport" title="Exportar CSV">
|
||
<i class="fas fa-file-csv me-1"></i>Exportar CSV
|
||
</button>
|
||
</div>
|
||
|
||
<!-- ── Barra de filtros ──────────────────────────────────────── -->
|
||
<div class="filter-bar">
|
||
<div class="filter-group">
|
||
<label>Desde</label>
|
||
<input type="date" id="fDesde" class="form-control form-control-sm">
|
||
</div>
|
||
<div class="filter-group">
|
||
<label>Hasta</label>
|
||
<input type="date" id="fHasta" class="form-control form-control-sm">
|
||
</div>
|
||
<div class="filter-group">
|
||
<label>Estado</label>
|
||
<select id="fEstado" class="form-select form-select-sm">
|
||
<option value="">Todos</option>
|
||
<option value="finalizado">Finalizado</option>
|
||
<option value="ausente">Ausente</option>
|
||
<option value="cancelado">Cancelado</option>
|
||
<option value="en_servicio">En servicio</option>
|
||
<option value="en_espera_lugar">Esp. lugar</option>
|
||
<option value="en_recepcion">En recepción</option>
|
||
<option value="espera">En espera</option>
|
||
</select>
|
||
</div>
|
||
<div class="filter-group" id="wrapLugar">
|
||
<label>Lugar</label>
|
||
<select id="fLugar" class="form-select form-select-sm">
|
||
<option value="0">Todos</option>
|
||
</select>
|
||
</div>
|
||
<div class="filter-group" id="wrapPrioridad">
|
||
<label>Prioridad</label>
|
||
<select id="fPrioridad" class="form-select form-select-sm">
|
||
<option value="0">Todas</option>
|
||
</select>
|
||
</div>
|
||
<div class="filter-group wide">
|
||
<label>Buscar paciente / código</label>
|
||
<input type="text" id="fQ" class="form-control form-control-sm" placeholder="Nombre o código…">
|
||
</div>
|
||
<button class="btn btn-primary btn-sm align-self-end" id="btnBuscar">
|
||
<i class="fas fa-search me-1"></i>Buscar
|
||
</button>
|
||
<button class="btn btn-outline-secondary btn-sm align-self-end" id="btnHoy" title="Solo hoy">
|
||
Hoy
|
||
</button>
|
||
<button class="btn btn-outline-secondary btn-sm align-self-end" id="btn7d" title="Últimos 7 días">
|
||
7 días
|
||
</button>
|
||
<button class="btn btn-outline-secondary btn-sm align-self-end" id="btn30d" title="Últimos 30 días">
|
||
30 días
|
||
</button>
|
||
</div>
|
||
|
||
<!-- ── Contenido ──────────────────────────────────────────────── -->
|
||
<div class="content-wrap">
|
||
|
||
<!-- Resumen por estado del rango -->
|
||
<div class="kpi-strip" id="kpiStrip" style="display:none"></div>
|
||
|
||
<!-- Tabla principal -->
|
||
<div class="section-card">
|
||
<div class="table-responsive">
|
||
<table class="table table-hover align-middle" id="tablaHistorial">
|
||
<thead>
|
||
<tr>
|
||
<th>Fecha</th>
|
||
<th>Código</th>
|
||
<th>Prioridad</th>
|
||
<th>Paciente</th>
|
||
<th>Teléfono</th>
|
||
<th>Lugar</th>
|
||
<th>Estado</th>
|
||
<th>Espera</th>
|
||
<th>Servicio</th>
|
||
<th>Entrada</th>
|
||
<th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="tbody">
|
||
<tr class="spinner-row">
|
||
<td colspan="11"><i class="fas fa-spinner fa-spin me-2"></i>Cargando…</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- Paginación -->
|
||
<div class="pagination-wrap" id="paginacionWrap" style="display:none">
|
||
<div class="pagination-info" id="paginacionInfo"></div>
|
||
<div class="page-btns" id="pagBtns"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
/* ═══════════════════════════════════════════════════════════
|
||
Historial de Turnos
|
||
═══════════════════════════════════════════════════════════ */
|
||
const API = '<?= BASE_URL ?>/modules/turnero/api/';
|
||
|
||
let _state = {
|
||
fechaDesde : '',
|
||
fechaHasta : '',
|
||
estado : '',
|
||
lugarId : 0,
|
||
prioridadId: 0,
|
||
q : '',
|
||
page : 1,
|
||
perPage : 50,
|
||
totalPages : 1,
|
||
total : 0,
|
||
};
|
||
|
||
// ── Utilidades ────────────────────────────────────────────────
|
||
const esc = s => (s == null ? '' : String(s))
|
||
.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
|
||
.replace(/"/g,'"').replace(/'/g,''');
|
||
|
||
function hoy() { return new Date().toISOString().slice(0,10); }
|
||
function hace(n){ return new Date(Date.now()-n*86400000).toISOString().slice(0,10); }
|
||
|
||
// ── Inicialización ────────────────────────────────────────────
|
||
document.addEventListener('DOMContentLoaded', async () => {
|
||
// Fecha por defecto: últimos 7 días
|
||
document.getElementById('fDesde').value = hace(6);
|
||
document.getElementById('fHasta').value = hoy();
|
||
document.getElementById('fHasta').max = hoy();
|
||
|
||
// Cargar catálogos (lugares + prioridades)
|
||
await cargarMeta();
|
||
|
||
// Primera carga
|
||
buscar();
|
||
|
||
// Eventos
|
||
document.getElementById('btnBuscar').addEventListener('click', buscar);
|
||
document.getElementById('fQ').addEventListener('keydown', e => { if (e.key==='Enter') buscar(); });
|
||
document.getElementById('btnExport').addEventListener('click', exportarCSV);
|
||
|
||
document.getElementById('btnHoy').addEventListener('click', () => {
|
||
document.getElementById('fDesde').value = hoy();
|
||
document.getElementById('fHasta').value = hoy();
|
||
buscar();
|
||
});
|
||
document.getElementById('btn7d').addEventListener('click', () => {
|
||
document.getElementById('fDesde').value = hace(6);
|
||
document.getElementById('fHasta').value = hoy();
|
||
buscar();
|
||
});
|
||
document.getElementById('btn30d').addEventListener('click', () => {
|
||
document.getElementById('fDesde').value = hace(29);
|
||
document.getElementById('fHasta').value = hoy();
|
||
buscar();
|
||
});
|
||
});
|
||
|
||
// ── Catálogos ─────────────────────────────────────────────────
|
||
async function cargarMeta() {
|
||
try {
|
||
const res = await fetch(`${API}get_historial.php?meta=1&fecha_desde=${hoy()}&fecha_hasta=${hoy()}&per_page=1`);
|
||
const json = await res.json();
|
||
if (!json.ok) return;
|
||
|
||
const selLugar = document.getElementById('fLugar');
|
||
(json.meta.lugares || []).forEach(l => {
|
||
const o = document.createElement('option');
|
||
o.value = l.id; o.textContent = l.nombre;
|
||
selLugar.appendChild(o);
|
||
});
|
||
|
||
const selPri = document.getElementById('fPrioridad');
|
||
(json.meta.prioridades || []).forEach(p => {
|
||
const o = document.createElement('option');
|
||
o.value = p.id;
|
||
o.textContent = `${p.codigo} — ${p.nombre}`;
|
||
selPri.appendChild(o);
|
||
});
|
||
} catch(_) {}
|
||
}
|
||
|
||
// ── Búsqueda principal ────────────────────────────────────────
|
||
function buscar(pageNum) {
|
||
_state.fechaDesde = document.getElementById('fDesde').value || hace(6);
|
||
_state.fechaHasta = document.getElementById('fHasta').value || hoy();
|
||
_state.estado = document.getElementById('fEstado').value;
|
||
_state.lugarId = parseInt(document.getElementById('fLugar').value) || 0;
|
||
_state.prioridadId = parseInt(document.getElementById('fPrioridad').value)|| 0;
|
||
_state.q = document.getElementById('fQ').value.trim();
|
||
_state.page = pageNum || 1;
|
||
|
||
cargarPagina();
|
||
}
|
||
|
||
async function cargarPagina() {
|
||
mostrarSpinner();
|
||
const params = new URLSearchParams({
|
||
fecha_desde : _state.fechaDesde,
|
||
fecha_hasta : _state.fechaHasta,
|
||
estado : _state.estado,
|
||
lugar_id : _state.lugarId,
|
||
prioridad_id : _state.prioridadId,
|
||
q : _state.q,
|
||
page : _state.page,
|
||
per_page : _state.perPage,
|
||
});
|
||
|
||
try {
|
||
const res = await fetch(`${API}get_historial.php?${params}`);
|
||
const json = await res.json();
|
||
if (!json.ok) { mostrarError(json.error || 'Error al cargar historial'); return; }
|
||
|
||
_state.totalPages = json.total_pages;
|
||
_state.total = json.total;
|
||
_state.page = json.page;
|
||
|
||
renderKpis(json.resumen_estados);
|
||
renderTabla(json.turnos);
|
||
renderPaginacion();
|
||
} catch(e) {
|
||
mostrarError('Error de conexión');
|
||
}
|
||
}
|
||
|
||
// ── KPIs de resumen ───────────────────────────────────────────
|
||
const ESTADO_META = {
|
||
finalizado : { lbl:'Finalizados', bg:'#f1f5f9', color:'#334155' },
|
||
ausente : { lbl:'Ausentes', bg:'#fee2e2', color:'#991b1b' },
|
||
cancelado : { lbl:'Cancelados', bg:'#f1f5f9', color:'#9ca3af' },
|
||
en_servicio : { lbl:'En servicio', bg:'#dcfce7', color:'#166534' },
|
||
en_espera_lugar : { lbl:'Esp. lugar', bg:'#fde68a', color:'#92400e' },
|
||
en_recepcion : { lbl:'En recepción', bg:'#dbeafe', color:'#1e40af' },
|
||
espera : { lbl:'En espera', bg:'#fef9c3', color:'#78350f' },
|
||
};
|
||
|
||
function renderKpis(resumen) {
|
||
const strip = document.getElementById('kpiStrip');
|
||
let total = Object.values(resumen).reduce((a,b)=>a+(+b),0);
|
||
if (!total) { strip.style.display='none'; return; }
|
||
|
||
strip.style.display = 'flex';
|
||
strip.innerHTML = `
|
||
<div class="kpi-chip">
|
||
<span class="chip-val">${total}</span>
|
||
<span class="chip-lbl">turnos (rango)</span>
|
||
</div>
|
||
${Object.entries(resumen).map(([est,cnt]) => {
|
||
const m = ESTADO_META[est] || {lbl:est, bg:'#f1f5f9', color:'#334155'};
|
||
return `<div class="kpi-chip" style="border-color:${m.color}22">
|
||
<span class="chip-val" style="color:${m.color}">${cnt}</span>
|
||
<span class="chip-lbl">${m.lbl}</span>
|
||
</div>`;
|
||
}).join('')}
|
||
`;
|
||
}
|
||
|
||
// ── Tabla ─────────────────────────────────────────────────────
|
||
const ESTADO_CLS = {
|
||
espera:'eb-espera', en_recepcion:'eb-en_recepcion', en_espera_lugar:'eb-en_espera_lugar',
|
||
en_servicio:'eb-en_servicio', finalizado:'eb-finalizado', ausente:'eb-ausente', cancelado:'eb-cancelado',
|
||
};
|
||
const ESTADO_LBL = {
|
||
espera:'Espera', en_recepcion:'Recepción', en_espera_lugar:'Esp. lugar',
|
||
en_servicio:'En servicio', finalizado:'Finalizado', ausente:'Ausente', cancelado:'Cancelado',
|
||
};
|
||
|
||
function renderTabla(turnos) {
|
||
const tbody = document.getElementById('tbody');
|
||
if (!turnos.length) {
|
||
tbody.innerHTML = `<tr><td colspan="11">
|
||
<div class="empty-state"><i class="fas fa-search"></i>Sin resultados para los filtros aplicados</div>
|
||
</td></tr>`;
|
||
return;
|
||
}
|
||
|
||
tbody.innerHTML = turnos.map((t,i) => {
|
||
const nombre = esc(t.paciente_bd || t.paciente_nombre || '—');
|
||
const lugar = esc(t.lugar_nombre || '—');
|
||
const fecha = t.fecha_sesion || '—';
|
||
const horaEnt = t.creado_at
|
||
? new Date(t.creado_at.replace(' ','T'))
|
||
.toLocaleString('es-CO',{hour:'2-digit',minute:'2-digit'})
|
||
: '—';
|
||
const espMin = t.espera_min != null ? t.espera_min + ' m' : '—';
|
||
const srvMin = t.servicio_min != null ? t.servicio_min + ' m' : '—';
|
||
const tel = esc(t.paciente_cel || '—');
|
||
const priCls = esc(t.prioridad_color || '#888');
|
||
const cls = ESTADO_CLS[t.estado] || 'eb-finalizado';
|
||
const lbl = ESTADO_LBL[t.estado] || t.estado;
|
||
const rowId = `det-${t.id}`;
|
||
|
||
return `<tr data-id="${t.id}">
|
||
<td class="text-nowrap text-muted small">${esc(fecha)}</td>
|
||
<td><strong style="color:${priCls}">${esc(t.codigo)}</strong></td>
|
||
<td>
|
||
<span class="eb" style="background:${priCls}22;color:${priCls}">
|
||
${esc(t.prioridad_codigo)}
|
||
</span>
|
||
</td>
|
||
<td>${nombre}</td>
|
||
<td class="text-muted small">${tel}</td>
|
||
<td>${lugar}</td>
|
||
<td><span class="eb ${cls}">${lbl}</span></td>
|
||
<td class="text-nowrap">${espMin}</td>
|
||
<td class="text-nowrap">${srvMin}</td>
|
||
<td class="text-nowrap small text-muted">${horaEnt}</td>
|
||
<td>
|
||
<button class="btn btn-sm btn-outline-secondary py-0 px-2"
|
||
onclick="toggleDetalle('${rowId}', this)"
|
||
title="Ver detalles de timestamps">
|
||
<i class="fas fa-chevron-down" style="font-size:.65rem"></i>
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
<tr id="${rowId}" class="detail-row" style="display:none">
|
||
<td colspan="11">
|
||
<div class="detail-body">
|
||
${renderDetalle(t)}
|
||
</div>
|
||
</td>
|
||
</tr>`;
|
||
}).join('');
|
||
}
|
||
|
||
function renderDetalle(t) {
|
||
const fmt = dt => {
|
||
if (!dt) return '—';
|
||
const d = new Date(dt.replace(' ','T'));
|
||
return d.toLocaleString('es-CO',{dateStyle:'short',timeStyle:'short'});
|
||
};
|
||
const items = [
|
||
{ lbl:'Creado', val: fmt(t.creado_at) },
|
||
{ lbl:'Inicio recepción', val: fmt(t.inicio_recepcion_at) },
|
||
{ lbl:'Fin recepción', val: fmt(t.fin_recepcion_at) },
|
||
{ lbl:'Inicio servicio', val: fmt(t.inicio_lugar_at) },
|
||
{ lbl:'Fin servicio', val: fmt(t.fin_lugar_at) },
|
||
{ lbl:'Código completo', val: esc(t.codigo) },
|
||
{ lbl:'Número en cola', val: t.numero ?? '—' },
|
||
{ lbl:'Teléfono', val: esc(t.paciente_cel || '—') },
|
||
];
|
||
return `<div class="detail-grid">
|
||
${items.map(it=>`
|
||
<div class="detail-item">
|
||
<div class="d-lbl">${it.lbl}</div>
|
||
<div class="d-val">${it.val}</div>
|
||
</div>`).join('')}
|
||
</div>`;
|
||
}
|
||
|
||
function toggleDetalle(rowId, btn) {
|
||
const row = document.getElementById(rowId);
|
||
const icon = btn.querySelector('i');
|
||
if (!row) return;
|
||
const visible = row.style.display !== 'none';
|
||
row.style.display = visible ? 'none' : '';
|
||
icon.className = visible ? 'fas fa-chevron-down' : 'fas fa-chevron-up';
|
||
icon.style.fontSize = '.65rem';
|
||
}
|
||
|
||
// ── Paginación ────────────────────────────────────────────────
|
||
function renderPaginacion() {
|
||
const wrap = document.getElementById('paginacionWrap');
|
||
const info = document.getElementById('paginacionInfo');
|
||
const btns = document.getElementById('pagBtns');
|
||
|
||
wrap.style.display = '';
|
||
|
||
const desde = (_state.page - 1) * _state.perPage + 1;
|
||
const hasta = Math.min(_state.page * _state.perPage, _state.total);
|
||
info.textContent = `Mostrando ${desde}–${hasta} de ${_state.total} turnos`;
|
||
|
||
// Generar botones de página (máximo 7 visibles)
|
||
const total = _state.totalPages;
|
||
const current = _state.page;
|
||
let pages = [];
|
||
|
||
if (total <= 7) {
|
||
pages = Array.from({length: total}, (_,i) => i+1);
|
||
} else {
|
||
pages = [1];
|
||
if (current > 3) pages.push('…');
|
||
for (let p = Math.max(2, current-1); p <= Math.min(total-1, current+1); p++) pages.push(p);
|
||
if (current < total-2) pages.push('…');
|
||
pages.push(total);
|
||
}
|
||
|
||
btns.innerHTML = `
|
||
<button ${current<=1?'disabled':''} onclick="irPagina(${current-1})">
|
||
<i class="fas fa-chevron-left"></i>
|
||
</button>
|
||
${pages.map(p =>
|
||
p === '…'
|
||
? `<button disabled>…</button>`
|
||
: `<button class="${p===current?'active':''}" onclick="irPagina(${p})">${p}</button>`
|
||
).join('')}
|
||
<button ${current>=total?'disabled':''} onclick="irPagina(${current+1})">
|
||
<i class="fas fa-chevron-right"></i>
|
||
</button>
|
||
`;
|
||
}
|
||
|
||
function irPagina(n) {
|
||
if (n < 1 || n > _state.totalPages) return;
|
||
_state.page = n;
|
||
cargarPagina();
|
||
window.scrollTo({top:0, behavior:'smooth'});
|
||
}
|
||
|
||
// ── Exportar CSV ──────────────────────────────────────────────
|
||
function exportarCSV() {
|
||
const params = new URLSearchParams({
|
||
fecha : _state.fechaDesde, // export_csv actual usa 'fecha' — usamos fecha_desde
|
||
m : 'historial', // flag para que export_csv sepa que es multi-día
|
||
fecha_desde : _state.fechaDesde,
|
||
fecha_hasta : _state.fechaHasta,
|
||
estado : _state.estado,
|
||
lugar_id : _state.lugarId,
|
||
prioridad_id : _state.prioridadId,
|
||
q : _state.q,
|
||
per_page : 9999,
|
||
page : 1,
|
||
});
|
||
// Generamos CSV en cliente a partir de los datos actuales si son pocos,
|
||
// o mandamos al endpoint get_historial con per_page alto para descarga
|
||
window.open(`${API}export_historial.php?${params}`, '_blank');
|
||
}
|
||
|
||
// ── Helpers visuales ──────────────────────────────────────────
|
||
function mostrarSpinner() {
|
||
document.getElementById('tbody').innerHTML =
|
||
'<tr class="spinner-row"><td colspan="11"><i class="fas fa-spinner fa-spin me-2"></i>Buscando…</td></tr>';
|
||
document.getElementById('paginacionWrap').style.display = 'none';
|
||
document.getElementById('kpiStrip').style.display = 'none';
|
||
}
|
||
|
||
function mostrarError(msg) {
|
||
document.getElementById('tbody').innerHTML =
|
||
`<tr><td colspan="11"><div class="empty-state text-danger">
|
||
<i class="fas fa-exclamation-circle"></i>${esc(msg)}
|
||
</div></td></tr>`;
|
||
}
|
||
</script>
|
||
|
||
<?php Layout::close(); ?>
|