Files
whatsapp/modules/turnero/views/historial.php
T
Lizandro GuarnizoandClaude Sonnet 4.6 f738607738 feat: historial turnos — mostrar recepcionista, desk, bacteriólogo y exámenes
Columnas nuevas en tabla principal:
- Recepción: módulo de desk + nombre del recepcionista
- Lugar / Bacteriólogo: lugar destino + quien atendió toma de muestras
- Exámenes: badges con código de cada examen solicitado

Los datos ya venían del API (get_historial.php), solo se exponen en la tabla.
Se elimina columna Teléfono y Entrada de la vista principal (siguen en detalle).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 20:22:07 -05:00

661 lines
29 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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; }
/* ── Exámenes y comentarios ── */
.detail-extra { margin-top:10px; padding-top:10px; border-top:1px solid #e2e8f0; }
.detail-extra .d-lbl { font-size:.67rem; text-transform:uppercase; letter-spacing:.07em; color:#94a3b8; margin-bottom:5px; }
.detail-comment { padding:5px 0; border-bottom:1px solid #f1f5f9; font-size:.82rem; }
.detail-comment:last-child { border-bottom:none; }
/* ── Documentos firmados ── */
.docs-section { margin-top:10px; padding-top:10px; border-top:1px solid #e2e8f0; }
.docs-lbl { font-size:.65rem; font-weight:700; text-transform:uppercase; letter-spacing:.07em; color:#94a3b8; margin-bottom:5px; }
.doc-pill { display:inline-flex; align-items:center; gap:4px; padding:3px 10px; border-radius:99px;
font-size:.73rem; font-weight:600; margin:2px 2px; text-decoration:none; transition:opacity .15s; }
.doc-pill:hover { opacity:.8; }
.doc-pill.firmado { background:#dcfce7; color:#166534; border:1px solid #bbf7d0; }
.doc-pill.enviado { background:#fef9c3; color:#78350f; border:1px solid #fde68a; }
.doc-pill.pendiente{ background:#f1f5f9; color:#64748b; border:1px solid #e2e8f0; }
.docs-empty { font-size:.78rem; color:#94a3b8; font-style:italic; }
</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>Recepción</th>
<th>Lugar / Bacteriólogo</th>
<th>Exámenes</th>
<th>Estado</th>
<th>Espera</th>
<th>Servicio</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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
.replace(/"/g,'&quot;').replace(/'/g,'&#39;');
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}`;
// Recepción: desk + recepcionista
const desk = esc(t.recepcion_desk_nombre || '—');
const recep = esc(t.atendido_recepcion_nombre || '—');
const bacterio = esc(t.atendido_lugar_nombre || '—');
// Exámenes: badges con código
const examsHtml = (t.examenes && t.examenes.length)
? t.examenes.map(e => `<span class="badge bg-success-subtle text-success me-1 mb-1" style="font-size:.65rem">${esc(e.codigo)}</span>`).join('')
: '<span class="text-muted small">—</span>';
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="small">
<div class="text-muted" style="font-size:.72rem"><i class="fas fa-desktop me-1 opacity-50"></i>${desk}</div>
<div><i class="fas fa-user-tie me-1 opacity-50" style="font-size:.72rem"></i>${recep}</div>
</td>
<td class="small">
<div class="fw-semibold">${lugar}</div>
<div class="text-muted" style="font-size:.72rem"><i class="fas fa-microscope me-1 opacity-50"></i>${bacterio}</div>
</td>
<td style="max-width:140px">${examsHtml}</td>
<td><span class="eb ${cls}">${lbl}</span></td>
<td class="text-nowrap">${espMin}</td>
<td class="text-nowrap">${srvMin}</td>
<td>
<button class="btn btn-sm btn-outline-secondary py-0 px-2"
onclick="toggleDetalle('${rowId}', this, ${t.id})"
title="Ver detalles">
<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 || '—') },
{ lbl:'Recepción (desk)', val: esc(t.recepcion_desk_nombre || '—') },
{ lbl:'Atendió recepción', val: esc(t.atendido_recepcion_nombre || '—') },
{ lbl:'Atendió muestras', val: esc(t.atendido_lugar_nombre || '—') },
{ lbl:'Notas', val: esc(t.notas || '—') },
];
// Exámenes
let examHtml = '';
if (t.examenes && t.examenes.length) {
examHtml = `<div class="detail-extra">
<div class="d-lbl"><i class="fas fa-vials me-1 text-success"></i>Exámenes (${t.examenes.length})</div>
<div class="d-val">${t.examenes.map(e => `<span class="badge bg-success-subtle text-success me-1 mb-1">${esc(e.nombre)}</span>`).join('')}</div>
</div>`;
}
// Comentarios
let comHtml = '';
if (t.comentarios && t.comentarios.length) {
comHtml = `<div class="detail-extra">
<div class="d-lbl"><i class="fas fa-comments me-1 text-primary"></i>Comentarios</div>
<div>${t.comentarios.map(c => `
<div class="detail-comment">
<span class="fw-semibold">${esc(c.usuario_nombre)}</span>
<span class="text-muted small ms-1">${c.creado_at ? new Date(c.creado_at.replace(' ','T')).toLocaleString('es-CO',{dateStyle:'short',timeStyle:'short'}) : ''}</span>
<div class="text-secondary">${esc(c.comentario)}</div>
</div>`).join('')}
</div>
</div>`;
}
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>
${examHtml}${comHtml}
<div class="docs-section" id="docs-${t.id}">
<div class="docs-lbl"><i class="fas fa-file-signature me-1"></i>Documentos</div>
<span class="text-muted small"><i class="fas fa-spinner fa-spin me-1"></i>Cargando…</span>
</div>`;
}
function toggleDetalle(rowId, btn, turnoId) {
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';
if (!visible && turnoId) {
const docsEl = document.getElementById(`docs-${turnoId}`);
if (docsEl && !docsEl.dataset.loaded) {
docsEl.dataset.loaded = '1';
cargarDocumentosTurno(turnoId, docsEl);
}
}
}
const BASE_URL_ROOT = '<?= BASE_URL ?>';
async function cargarDocumentosTurno(turnoId, container) {
try {
const res = await fetch(`${API}get_consentimientos.php?turno_id=${turnoId}`);
const json = await res.json();
if (!json.ok) { container.querySelector('span').textContent = 'Error al cargar'; return; }
const list = json.consentimientos || [];
if (!list.length) {
container.innerHTML = '<div class="docs-lbl"><i class="fas fa-file-signature me-1"></i>Documentos</div>'
+ '<span class="docs-empty">Sin documentos para este turno</span>';
return;
}
const LBLS = { firmado:'Firmado', enviado:'Enviado', visto:'Visto', pendiente:'Pendiente' };
const pills = list.map(c => {
const est = c.estado || 'pendiente';
const nombre = esc(c.formulario_nombre || 'Documento');
if (est === 'firmado' && c.token) {
const url = BASE_URL_ROOT + 'ver_formulario_enviado.php?token=' + encodeURIComponent(c.token);
return `<a class="doc-pill firmado" href="${url}" target="_blank">
<i class="fas fa-check-circle"></i>${nombre}
<i class="fas fa-external-link-alt ms-1" style="font-size:.6rem;opacity:.6"></i>
</a>`;
}
const cls = est === 'enviado' || est === 'visto' ? 'enviado' : 'pendiente';
const ico = est === 'enviado' ? 'fa-paper-plane' : (est === 'visto' ? 'fa-eye' : 'fa-clock');
return `<span class="doc-pill ${cls}"><i class="fas ${ico}"></i>${nombre}</span>`;
}).join('');
container.innerHTML = '<div class="docs-lbl"><i class="fas fa-file-signature me-1"></i>Documentos</div>' + pills;
} catch(_) {
const sp = container.querySelector('span');
if (sp) sp.textContent = 'Error de conexión';
}
}
// ── 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(); ?>