- Nuevo módulo lab_examenes con lista paginada, buscador y filtro por categoría - Vista de detalle/edición con campos completos de exam_tipos (CUPS, protocolo, tipo muestra, nivel, abreviatura, seremite, ayuno, instrucciones) - Sub-tabla inline de items de resultado (lab_items_resultado) con edición por fila - Sub-tabla de precios por tarifa (lab_tarifas) con modal de edición - APIs: list, get, save, save_item, save_tarifa, get_tarifas - Script ETL Firebird→MySQL (scripts/etl_examenes.php) para la migración inicial Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
168 lines
8.1 KiB
PHP
168 lines
8.1 KiB
PHP
<?php
|
||
require_once APP_ROOT . '/config/config.php';
|
||
if (!isUserLoggedIn()) { header('Location: ' . BASE_URL . 'login.php'); exit; }
|
||
Layout::open('Catálogo de Exámenes', 'fas fa-flask');
|
||
|
||
$pdo = Database::getInstance()->getConnection();
|
||
$categorias = $pdo->query(
|
||
"SELECT DISTINCT categoria FROM exam_tipos WHERE categoria IS NOT NULL ORDER BY categoria"
|
||
)->fetchAll(PDO::FETCH_COLUMN);
|
||
|
||
$API = BASE_URL . 'modules/lab_examenes/api/';
|
||
$DETAIL = BASE_URL . 'erp.php?m=lab_examenes&v=examen&id=';
|
||
?>
|
||
<style>
|
||
body { background:#f1f5f9 }
|
||
.page-header { background:#fff; border-bottom:1px solid #e2e8f0; padding:14px 24px;
|
||
display:flex; align-items:center; gap:12px; flex-wrap:wrap }
|
||
.page-header h1 { font-size:1.18rem; font-weight:700; color:#1e293b; margin:0; flex:1 }
|
||
.content-wrap { max-width:1200px; margin:24px auto; padding:0 16px 48px }
|
||
.card-box { background:#fff; border:1px solid #e2e8f0; border-radius:12px; overflow:hidden }
|
||
.toolbar { padding:12px 16px; display:flex; gap:10px; align-items:center;
|
||
border-bottom:1px solid #e2e8f0; flex-wrap:wrap }
|
||
.toolbar input, .toolbar select { font-size:.85rem }
|
||
table { width:100%; border-collapse:collapse; font-size:.88rem }
|
||
thead th { background:#f8fafc; padding:9px 14px; font-size:.7rem; text-transform:uppercase;
|
||
letter-spacing:.07em; color:#64748b; border-bottom:1px solid #e2e8f0; white-space:nowrap }
|
||
tbody td { padding:9px 14px; border-bottom:1px solid #f1f5f9; color:#1e293b; vertical-align:middle }
|
||
tbody tr:last-child td { border-bottom:none }
|
||
tbody tr:hover td { background:#f8fafc }
|
||
.badge-cat { background:#eff6ff; color:#1d4ed8; border-radius:20px; padding:2px 9px;
|
||
font-size:.72rem; font-weight:600; white-space:nowrap }
|
||
.badge-remite { background:#dcfce7; color:#166534; border-radius:20px; padding:2px 9px; font-size:.72rem }
|
||
.badge-off { background:#f1f5f9; color:#94a3b8; border-radius:20px; padding:2px 9px; font-size:.72rem }
|
||
.pag-bar { display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:.5rem;
|
||
padding:10px 16px; border-top:1px solid #e2e8f0; background:#f8fafc }
|
||
.pag-info { font-size:.8rem; color:#64748b }
|
||
.pag-btn { min-width:32px; height:32px; padding:0 8px; border:1px solid #e2e8f0; border-radius:7px;
|
||
background:#fff; font-size:.82rem; color:#374151; cursor:pointer;
|
||
display:inline-flex; align-items:center; justify-content:center }
|
||
.pag-btn:hover:not(:disabled) { background:#eff6ff; border-color:#bfdbfe; color:#1d4ed8 }
|
||
.pag-btn.active { background:#2563eb; color:#fff; border-color:#2563eb }
|
||
.pag-btn:disabled { opacity:.4; cursor:not-allowed }
|
||
.toggle-activo { cursor:pointer; font-size:1.05rem }
|
||
</style>
|
||
|
||
<div class="page-header">
|
||
<i class="fas fa-flask" style="font-size:1.3rem;color:#6366f1"></i>
|
||
<h1>Catálogo de Exámenes</h1>
|
||
<a href="<?= BASE_URL ?>erp.php?m=lab_examenes&v=examen" class="btn btn-primary btn-sm ms-auto">
|
||
<i class="fas fa-plus me-1"></i>Nuevo examen
|
||
</a>
|
||
</div>
|
||
|
||
<div class="content-wrap">
|
||
<div class="card-box">
|
||
<div class="toolbar">
|
||
<input type="search" id="inp-q" class="form-control form-control-sm"
|
||
placeholder="Buscar por nombre, código o CUPS…" style="max-width:280px" oninput="buscar()">
|
||
<select id="sel-cat" class="form-select form-select-sm" style="max-width:220px" onchange="cargar(1)">
|
||
<option value="">Todas las categorías</option>
|
||
<?php foreach ($categorias as $c): ?>
|
||
<option value="<?= htmlspecialchars($c) ?>"><?= htmlspecialchars($c) ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<span class="text-muted small ms-auto" id="lbl-total"></span>
|
||
</div>
|
||
<div style="overflow-x:auto">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Código</th>
|
||
<th>Nombre</th>
|
||
<th>Categoría</th>
|
||
<th>CUPS</th>
|
||
<th>Protocolo</th>
|
||
<th>Se remite</th>
|
||
<th>Activo</th>
|
||
<th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="tbl-body">
|
||
<tr><td colspan="8" class="text-center text-muted py-4">Cargando…</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="pag-bar" id="pag-bar" style="display:none">
|
||
<span class="pag-info" id="pag-info"></span>
|
||
<div id="pag-ctrl" style="display:flex;gap:4px"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const API = '<?= $API ?>';
|
||
const DETAIL = '<?= $DETAIL ?>';
|
||
let _page=1, _limit=50, _total=0, _q='', _timer;
|
||
|
||
document.addEventListener('DOMContentLoaded', () => cargar(1));
|
||
|
||
function buscar() {
|
||
clearTimeout(_timer);
|
||
_timer = setTimeout(() => cargar(1), 260);
|
||
}
|
||
|
||
async function cargar(page=_page) {
|
||
_page = page;
|
||
_q = document.getElementById('inp-q').value.trim();
|
||
const cat = document.getElementById('sel-cat').value;
|
||
const p = new URLSearchParams({page, limit:_limit});
|
||
if (_q) p.set('q', _q);
|
||
if (cat) p.set('categoria', cat);
|
||
const r = await fetch(API+'list.php?'+p).then(r=>r.json());
|
||
if (!r.ok) return;
|
||
_total = r.total;
|
||
renderTabla(r.data);
|
||
renderPag(Math.ceil(_total/_limit));
|
||
const d=(_page-1)*_limit+1, h=Math.min(_page*_limit,_total);
|
||
document.getElementById('lbl-total').textContent = _total ? `${d}–${h} de ${_total}` : '0 exámenes';
|
||
}
|
||
|
||
function renderTabla(rows) {
|
||
const b = document.getElementById('tbl-body');
|
||
if (!rows.length) { b.innerHTML='<tr><td colspan="8" class="text-center text-muted py-4">Sin resultados.</td></tr>'; return; }
|
||
b.innerHTML = rows.map(r => `
|
||
<tr>
|
||
<td><code class="text-primary">${esc(r.codigo)}</code></td>
|
||
<td><a href="${DETAIL+r.id}" class="text-decoration-none fw-semibold text-dark">${esc(r.nombre)}</a></td>
|
||
<td>${r.categoria ? `<span class="badge-cat">${esc(r.categoria)}</span>` : '<span class="text-muted">—</span>'}</td>
|
||
<td><small class="text-muted">${esc(r.cups||'—')}</small></td>
|
||
<td><small class="text-muted">${esc(r.cod_protocolo||'—')}</small></td>
|
||
<td>${+r.seremite ? '<span class="badge-remite">Sí</span>' : '<span class="badge-off">No</span>'}</td>
|
||
<td><span class="toggle-activo" title="Clic para cambiar" onclick="toggleActivo(${r.id},${r.activo})">${+r.activo?'✅':'⬜'}</span></td>
|
||
<td><a href="${DETAIL+r.id}" class="btn btn-outline-primary btn-sm py-0 px-2"><i class="fas fa-pencil-alt"></i></a></td>
|
||
</tr>`).join('');
|
||
}
|
||
|
||
async function toggleActivo(id, actual) {
|
||
const r = await fetch(API+'save.php', {
|
||
method:'POST', headers:{'Content-Type':'application/json'},
|
||
body: JSON.stringify({id, activo: actual=='1'||actual===1 ? 0 : 1})
|
||
}).then(r=>r.json());
|
||
if (r.ok) cargar(_page);
|
||
}
|
||
|
||
function renderPag(total) {
|
||
const bar = document.getElementById('pag-bar');
|
||
const ctrl = document.getElementById('pag-ctrl');
|
||
if (total<=1) { bar.style.display='none'; return; }
|
||
bar.style.display='flex';
|
||
document.getElementById('pag-info').textContent = `Página ${_page} de ${total}`;
|
||
const pages = new Set([1,total,_page]);
|
||
for(let i=_page-2;i<=_page+2;i++) if(i>0&&i<=total) pages.add(i);
|
||
const sorted=[...pages].sort((a,b)=>a-b);
|
||
let h=`<button class="pag-btn" onclick="cargar(${_page-1})" ${_page===1?'disabled':''}><i class="fas fa-chevron-left"></i></button>`;
|
||
let prev=0;
|
||
for(const p of sorted){
|
||
if(prev&&p-prev>1) h+=`<span style="padding:0 4px;line-height:32px;font-size:.82rem;color:#94a3b8">…</span>`;
|
||
h+=`<button class="pag-btn ${p===_page?'active':''}" onclick="cargar(${p})">${p}</button>`;
|
||
prev=p;
|
||
}
|
||
h+=`<button class="pag-btn" onclick="cargar(${_page+1})" ${_page===total?'disabled':''}><i class="fas fa-chevron-right"></i></button>`;
|
||
ctrl.innerHTML=h;
|
||
}
|
||
|
||
function esc(s){if(!s)return'';return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"')}
|
||
</script>
|
||
<?php Layout::close(); ?>
|