Files
whatsapp/modules/lab_examenes/views/index.php
T
Lizandro GuarnizoandClaude Sonnet 4.6 fb0e77523c feat: módulo lab_examenes — CRUD catálogo de exámenes, items y tarifas
- 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>
2026-07-07 23:06:54 -05:00

168 lines
8.1 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
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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;')}
</script>
<?php Layout::close(); ?>