feat(recepcion): Tom Select para selección de exámenes con badge de consentimiento
Reemplaza la lista de checkboxes por Tom Select (sin jQuery): - Búsqueda integrada mientras se escribe - Exámenes agrupados por categoría (optgroup) - Cada examen con consentimiento asociado muestra badge ⚠ Consentimiento en el dropdown y marcador visual en el tag seleccionado - Solo entrega de muestras deshabilita el select automáticamente - Remove_button plugin para quitar ítems individualmente - Elimina filtrarExamenes / toggleCategoria / actualizarCatChk (ya innecesarios) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
c566c1f33f
commit
56b8fc98df
@@ -30,7 +30,13 @@ try {
|
||||
$lugarMuestras = $lugaresDestino[0] ?? null;
|
||||
|
||||
$examenes = $pdo->query(
|
||||
"SELECT id, codigo, nombre, categoria FROM exam_tipos WHERE activo = 1 ORDER BY categoria, nombre"
|
||||
"SELECT et.id, et.codigo, et.nombre, et.categoria,
|
||||
IF(COUNT(etc.formulario_id) > 0, 1, 0) AS tiene_consentimiento
|
||||
FROM exam_tipos et
|
||||
LEFT JOIN exam_tipo_consentimientos etc ON etc.exam_tipo_id = et.id
|
||||
WHERE et.activo = 1
|
||||
GROUP BY et.id
|
||||
ORDER BY et.categoria, et.nombre"
|
||||
)->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Agrupar exámenes por categoría
|
||||
@@ -79,6 +85,8 @@ $adminNombre = $_SESSION['admin_user']['full_name'] ?? $_SESSION['admin_user']['
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||
<link href="<?= BASE_URL ?>assets/css/styles.css?v=12" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/tom-select@2.4.3/dist/css/tom-select.bootstrap5.min.css" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/tom-select@2.4.3/dist/js/tom-select.complete.min.js"></script>
|
||||
<style>
|
||||
/* ── Layout de dos columnas ── */
|
||||
.rec-layout {
|
||||
@@ -172,16 +180,22 @@ $adminNombre = $_SESSION['admin_user']['full_name'] ?? $_SESSION['admin_user']['
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
}
|
||||
|
||||
/* ── Checkboxes de exámenes ── */
|
||||
.exam-group-title { font-size: .75rem; color: #94a3b8;
|
||||
text-transform: uppercase; letter-spacing: .08em; margin: .6rem 0 .3rem; }
|
||||
.exam-cat-label { display: flex; align-items: center; gap: .35rem; cursor: pointer; }
|
||||
.exam-cat-label input[type=checkbox] { cursor: pointer; }
|
||||
.exam-check-item { display: flex; align-items: center; gap: .5rem;
|
||||
padding: .3rem .4rem; border-radius: 6px; cursor: pointer;
|
||||
transition: background .1s; font-size: .88rem; }
|
||||
.exam-check-item:hover { background: #f1f5f9; }
|
||||
.exam-check-item input[type=checkbox] { cursor: pointer; }
|
||||
/* ── Tom Select — exámenes ── */
|
||||
#wrap-examenes .ts-wrapper { font-size: .88rem; }
|
||||
#wrap-examenes .ts-control { min-height: 44px; border-radius: 8px; }
|
||||
#wrap-examenes .ts-dropdown { font-size: .88rem; }
|
||||
#wrap-examenes .ts-dropdown .optgroup-header {
|
||||
font-size: .7rem; font-weight: 700; color: #94a3b8;
|
||||
text-transform: uppercase; letter-spacing: .08em;
|
||||
}
|
||||
.ts-consent-badge {
|
||||
font-size: .65rem; font-weight: 700; border-radius: 4px;
|
||||
padding: 1px 5px; margin-left: 5px;
|
||||
background: #fef3c7; color: #92400e; border: 1px solid #fde68a;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ts-consent-dot { margin-left: 3px; color: #d97706; font-size: .75rem; }
|
||||
#wrap-examenes.disabled { opacity: .4; pointer-events: none; }
|
||||
|
||||
/* ── Consentimientos ── */
|
||||
.consent-item {
|
||||
@@ -322,7 +336,6 @@ $adminNombre = $_SESSION['admin_user']['full_name'] ?? $_SESSION['admin_user']['
|
||||
.btn-accion-sec.danger:hover { background: #fef2f2; }
|
||||
|
||||
/* ── Mejoras de touch target (tablet) ── */
|
||||
.exam-check-item { min-height: 40px; }
|
||||
.consent-item { min-height: 46px; }
|
||||
.pac-resultado { min-height: 50px; }
|
||||
.cola-card { min-height: 62px; }
|
||||
@@ -574,43 +587,23 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
<i class="fas fa-vial me-1"></i>Solo entrega de muestras
|
||||
</label>
|
||||
</div>
|
||||
<div class="input-group input-group-sm mb-2">
|
||||
<span class="input-group-text text-muted" style="background:#f8fafc"><i class="fas fa-search"></i></span>
|
||||
<input type="text" id="inp-filtrar-exam" class="form-control"
|
||||
placeholder="Filtrar exámenes…" autocomplete="off"
|
||||
oninput="filtrarExamenes(this.value)">
|
||||
<button class="btn btn-outline-secondary" type="button"
|
||||
onclick="document.getElementById('inp-filtrar-exam').value='';filtrarExamenes('')"
|
||||
title="Limpiar filtro">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
<div id="wrap-examenes">
|
||||
<select id="sel-examenes" multiple placeholder="Buscar y seleccionar exámenes…">
|
||||
<?php foreach ($examenesAgrupados as $cat => $items): ?>
|
||||
<optgroup label="<?= htmlspecialchars($cat) ?>">
|
||||
<?php foreach ($items as $ex): ?>
|
||||
<option value="<?= (int)$ex['id'] ?>"
|
||||
data-consent="<?= (int)$ex['tiene_consentimiento'] ?>">
|
||||
<?= htmlspecialchars($ex['codigo']) ?> — <?= htmlspecialchars($ex['nombre']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</optgroup>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div id="lista-examenes">
|
||||
<?php foreach ($examenesAgrupados as $cat => $items): ?>
|
||||
<div class="exam-group-title">
|
||||
<label class="exam-cat-label">
|
||||
<input type="checkbox" class="exam-cat-chk"
|
||||
data-cat="<?= htmlspecialchars($cat) ?>"
|
||||
onchange="toggleCategoria(this)">
|
||||
<?= htmlspecialchars($cat) ?>
|
||||
</label>
|
||||
</div>
|
||||
<?php foreach ($items as $ex): ?>
|
||||
<label class="exam-check-item">
|
||||
<input type="checkbox" class="exam-chk"
|
||||
value="<?= (int)$ex['id'] ?>"
|
||||
data-nombre="<?= htmlspecialchars($ex['nombre']) ?>"
|
||||
data-cat="<?= htmlspecialchars($cat) ?>"
|
||||
onchange="actualizarCatChk('<?= htmlspecialchars($cat, ENT_QUOTES) ?>')">
|
||||
<span><strong><?= htmlspecialchars($ex['codigo']) ?></strong>
|
||||
— <?= htmlspecialchars($ex['nombre']) ?></span>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div class="mt-2 d-flex gap-2 flex-wrap">
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="toggleTodosExamenes(false)">
|
||||
Limpiar
|
||||
<div class="mt-2">
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="examTS && examTS.clear()">
|
||||
<i class="fas fa-times me-1"></i>Limpiar selección
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -905,14 +898,33 @@ const DESK_NOMBRE = '<?= addslashes($deskNombre ?? 'Recepción') ?>';
|
||||
// ── Arranque ──────────────────────────────────────────────────
|
||||
let pollingConsentimientosId = null;
|
||||
|
||||
let examTS;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
examTS = new TomSelect('#sel-examenes', {
|
||||
plugins: ['remove_button'],
|
||||
maxOptions: null,
|
||||
render: {
|
||||
option(data, escape) {
|
||||
const consent = data.element?.dataset.consent === '1';
|
||||
return `<div class="d-flex align-items-center justify-content-between">
|
||||
<span>${escape(data.text)}</span>
|
||||
${consent ? '<span class="ts-consent-badge"><i class="fas fa-file-signature me-1"></i>Consentimiento</span>' : ''}
|
||||
</div>`;
|
||||
},
|
||||
item(data, escape) {
|
||||
const consent = data.element?.dataset.consent === '1';
|
||||
return `<div>${escape(data.text)}${consent ? '<span class="ts-consent-dot" title="Requiere consentimiento"> ⚠</span>' : ''}</div>`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
cargarCola();
|
||||
pollingColaId = setInterval(cargarCola, 3000);
|
||||
document.getElementById('inp-buscar-pac')
|
||||
.addEventListener('keydown', e => { if (e.key === 'Enter') buscarPaciente(); });
|
||||
document.getElementById('sel-lugar')
|
||||
.addEventListener('change', onLugarChange);
|
||||
// Refrescar consentimientos al volver a la pestaña (firma en otra pestaña)
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (!document.hidden && turnoActivo) refrescarConsentimientosLugar();
|
||||
});
|
||||
@@ -1443,7 +1455,7 @@ async function guardarSolicitud() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const examIds = Array.from(document.querySelectorAll('.exam-chk:checked')).map(c => parseInt(c.value));
|
||||
const examIds = examTS ? examTS.getValue().map(v => parseInt(v)) : [];
|
||||
if (!soloMuestras && !examIds.length) { mostrarError('Seleccione al menos un examen.'); return; }
|
||||
|
||||
const btn = document.getElementById('btn-guardar');
|
||||
@@ -1864,16 +1876,15 @@ function escJs(s) { return String(s||'').replace(/\\/g,'\\\\').replace(/'/g,"\\'
|
||||
|
||||
function toggleSoloMuestras() {
|
||||
const solo = document.getElementById('chk-solo-muestras').checked;
|
||||
document.querySelectorAll('.exam-chk').forEach(c => { c.checked = false; c.disabled = solo; });
|
||||
document.getElementById('lista-examenes').style.opacity = solo ? '0.4' : '';
|
||||
const wrap = document.getElementById('wrap-examenes');
|
||||
if (examTS) { examTS.clear(); solo ? examTS.disable() : examTS.enable(); }
|
||||
wrap.classList.toggle('disabled', solo);
|
||||
}
|
||||
|
||||
function resetCheckboxes() {
|
||||
document.getElementById('chk-solo-muestras').checked = false;
|
||||
document.querySelectorAll('.exam-chk').forEach(c => { c.checked = false; c.disabled = false; });
|
||||
document.getElementById('lista-examenes').style.opacity = '';
|
||||
const fi = document.getElementById('inp-filtrar-exam');
|
||||
if (fi) { fi.value = ''; filtrarExamenes(''); }
|
||||
if (examTS) { examTS.clear(); examTS.enable(); }
|
||||
document.getElementById('wrap-examenes').classList.remove('disabled');
|
||||
document.getElementById('sel-pago').value = '';
|
||||
document.getElementById('inp-total').value = '';
|
||||
document.getElementById('inp-obs').value = '';
|
||||
@@ -1881,43 +1892,6 @@ function resetCheckboxes() {
|
||||
resetMedico();
|
||||
}
|
||||
|
||||
function filtrarExamenes(q) {
|
||||
const query = q.toLowerCase().trim();
|
||||
document.querySelectorAll('#lista-examenes .exam-check-item').forEach(item => {
|
||||
item.style.display = (!query || item.textContent.toLowerCase().includes(query)) ? '' : 'none';
|
||||
});
|
||||
document.querySelectorAll('#lista-examenes .exam-group-title').forEach(title => {
|
||||
let next = title.nextElementSibling;
|
||||
let anyVisible = false;
|
||||
while (next && !next.classList.contains('exam-group-title')) {
|
||||
if (next.style.display !== 'none') anyVisible = true;
|
||||
next = next.nextElementSibling;
|
||||
}
|
||||
title.style.display = anyVisible ? '' : 'none';
|
||||
});
|
||||
}
|
||||
function toggleTodosExamenes(val) {
|
||||
document.querySelectorAll('.exam-chk').forEach(c => c.checked = val);
|
||||
document.querySelectorAll('.exam-cat-chk').forEach(c => c.checked = val);
|
||||
}
|
||||
|
||||
function toggleCategoria(chk) {
|
||||
const cat = chk.dataset.cat;
|
||||
document.querySelectorAll(`.exam-chk[data-cat="${CSS.escape(cat)}"]`).forEach(c => {
|
||||
if (c.closest('.exam-check-item')?.style.display !== 'none') c.checked = chk.checked;
|
||||
});
|
||||
}
|
||||
|
||||
function actualizarCatChk(cat) {
|
||||
const catChk = document.querySelector(`.exam-cat-chk[data-cat="${CSS.escape(cat)}"]`);
|
||||
if (!catChk) return;
|
||||
const items = Array.from(document.querySelectorAll(`.exam-chk[data-cat="${CSS.escape(cat)}"]`))
|
||||
.filter(c => c.closest('.exam-check-item')?.style.display !== 'none');
|
||||
const checked = items.filter(c => c.checked).length;
|
||||
catChk.checked = checked === items.length && items.length > 0;
|
||||
catChk.indeterminate = checked > 0 && checked < items.length;
|
||||
}
|
||||
|
||||
function escHtml(str) {
|
||||
const d = document.createElement('div');
|
||||
d.appendChild(document.createTextNode(String(str)));
|
||||
|
||||
Reference in New Issue
Block a user