Tomas: wizard inline para crear tipo de examen con tomas configurables

- Botón "Añadir tipo de examen…" abre wizard directo en el formulario
- Paso 1: nombre del examen + cantidad de tomas (1-12) con +/-
- Cada toma: label, tipo (minutos / hora fija), valor
- Al guardar llama api/lab/save_tomas_config.php y recarga la página
- Auto-selecciona el nuevo examen tras recargar (sessionStorage)
- Restaura filtrado de $_gruposConfig por examen seleccionado

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-07-10 11:10:51 -05:00
co-authored by Claude Sonnet 4.6
parent be55a087aa
commit f0fb0c7492
+161 -79
View File
@@ -355,6 +355,45 @@ function esc2(mixed $v): string {
if (is_array($v)) return htmlspecialchars(implode(', ', $v), ENT_QUOTES);
return htmlspecialchars((string)($v ?? ''), ENT_QUOTES);
}
function _addExamWizardHtml(string $cid): string {
return '
<div class="mt-2" id="aew-wrap-' . $cid . '">
<button type="button" class="btn btn-link btn-sm p-0 text-muted" onclick="aewOpen(\'' . $cid . '\')">
<i class="fas fa-plus-circle me-1"></i>Añadir tipo de examen…
</button>
<div id="aew-box-' . $cid . '" class="d-none mt-2 border rounded p-3 bg-light" style="max-width:480px">
<!-- Paso 1: nombre -->
<div id="aew-s1-' . $cid . '">
<div class="fw-semibold small mb-2"><i class="fas fa-vials me-1 text-primary"></i>Nuevo tipo de examen</div>
<div class="mb-2">
<label class="form-label small mb-1">Nombre del examen</label>
<input type="text" id="aew-name-' . $cid . '" class="form-control form-control-sm"
placeholder="Ej: Curva de Insulina" maxlength="80">
</div>
<div class="mb-2">
<label class="form-label small mb-1">¿Cuántas tomas?</label>
<div class="d-flex align-items-center gap-2">
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="aewAddToma(\'' . $cid . '\',-1)"><i class="fas fa-minus"></i></button>
<span id="aew-count-' . $cid . '" class="fw-bold" style="min-width:20px;text-align:center">1</span>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="aewAddToma(\'' . $cid . '\',1)"><i class="fas fa-plus"></i></button>
</div>
</div>
<div id="aew-tomas-' . $cid . '" class="mb-2"></div>
<div class="d-flex gap-2">
<button type="button" class="btn btn-success btn-sm" id="aew-save-' . $cid . '"
onclick="aewSave(\'' . $cid . '\')">
<i class="fas fa-check me-1"></i>Crear examen
</button>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="aewClose(\'' . $cid . '\')">
Cancelar
</button>
</div>
<div id="aew-msg-' . $cid . '" class="small mt-2"></div>
</div>
</div>
</div>';
}
?>
<!DOCTYPE html>
<html lang="es">
@@ -647,11 +686,22 @@ function esc2(mixed $v): string {
<?php
// ── Panel de configuración de ciclos (se muestra antes de iniciar el protocolo) ──
// Con campo tipo texto libre, todos los grupos se muestran siempre
$_examOptions = [];
foreach ($esquema as $_oc) {
if (($_oc['id'] ?? '') === '_c8j2g16') { $_examOptions = $_oc['options'] ?? []; break; }
}
$_examSel = (array)($datosCliente['_c8j2g16'] ?? []);
$_gruposConfig = [];
foreach ($_mpGroupsFull as $_gk => $_gsecs) {
if (empty($_gsecs)) continue;
$_gruposConfig[$_gk] = ['title' => $_gsecs[0]['display'] ?? $_gk, 'secs' => $_gsecs];
$__parts = array_values(array_filter(explode('|', $_gk)));
$__isCond = !empty(array_intersect($__parts, $_examOptions));
$__matchSel = !empty(array_intersect($__parts, $_examSel));
if (!$__isCond || $__matchSel) {
$__title = implode(' / ', array_intersect($__parts, $_examSel));
if (!$__title) $__title = $_gsecs[0]['display'] ?? $_gk;
$_gruposConfig[$_gk] = ['title' => $__title, 'secs' => $_gsecs];
}
}
// Mostrar panel solo si al menos un grupo tiene >= 2 tomas (hay algo que configurar).
// Si todos los grupos tienen 1 sola toma, el JS auto-guarda la config sin preguntar.
@@ -995,26 +1045,7 @@ function esc2(mixed $v): string {
</div>
<?php endforeach; ?>
<?php if ($cid === '_c8j2g16'): ?>
<div class="mt-2" id="add-exam-wrap-<?= esc2($cid) ?>">
<button type="button" class="btn btn-link btn-sm p-0 text-muted"
onclick="toggleAddExam('<?= esc2($cid) ?>')">
<i class="fas fa-plus-circle me-1"></i>Añadir tipo de examen…
</button>
<div id="add-exam-box-<?= esc2($cid) ?>" class="d-none mt-1 d-flex gap-2">
<input type="text" id="add-exam-input-<?= esc2($cid) ?>"
class="form-control form-control-sm" style="max-width:260px"
placeholder="Nombre del tipo de examen">
<button type="button" class="btn btn-success btn-sm"
onclick="guardarNuevoExamen('<?= esc2($cid) ?>')">
<i class="fas fa-check me-1"></i>Guardar
</button>
<button type="button" class="btn btn-outline-secondary btn-sm"
onclick="toggleAddExam('<?= esc2($cid) ?>')">
<i class="fas fa-times"></i>
</button>
</div>
<div id="add-exam-msg-<?= esc2($cid) ?>" class="small mt-1"></div>
</div>
<?= _addExamWizardHtml($cid) ?>
<?php endif; ?>
</div>
<?php continue; endif;
@@ -1036,26 +1067,7 @@ function esc2(mixed $v): string {
<?php endforeach; ?>
</div>
<?php if ($cid === '_c8j2g16'): ?>
<div class="mt-2" id="add-exam-wrap-<?= esc2($cid) ?>">
<button type="button" class="btn btn-link btn-sm p-0 text-muted"
onclick="toggleAddExam('<?= esc2($cid) ?>')">
<i class="fas fa-plus-circle me-1"></i>Añadir tipo de examen…
</button>
<div id="add-exam-box-<?= esc2($cid) ?>" class="d-none mt-1 d-flex gap-2">
<input type="text" id="add-exam-input-<?= esc2($cid) ?>"
class="form-control form-control-sm" style="max-width:260px"
placeholder="Nombre del tipo de examen">
<button type="button" class="btn btn-success btn-sm"
onclick="guardarNuevoExamen('<?= esc2($cid) ?>')">
<i class="fas fa-check me-1"></i>Guardar
</button>
<button type="button" class="btn btn-outline-secondary btn-sm"
onclick="toggleAddExam('<?= esc2($cid) ?>')">
<i class="fas fa-times"></i>
</button>
</div>
<div id="add-exam-msg-<?= esc2($cid) ?>" class="small mt-1"></div>
</div>
<?= _addExamWizardHtml($cid) ?>
<?php endif; ?>
</div>
<?php continue; endif;
@@ -1327,57 +1339,127 @@ document.addEventListener('DOMContentLoaded', function() {
})();
<?php endif; ?>
<?php endif; ?>
/* ── Añadir tipo de examen inline ── */
function toggleAddExam(cid) {
var box = document.getElementById('add-exam-box-' + cid);
/* ── Wizard: añadir tipo de examen ── */
var _aewState = {}; // {cid: {count, rows}}
function aewOpen(cid) {
var box = document.getElementById('aew-box-' + cid);
if (!box) return;
box.classList.toggle('d-none');
if (!box.classList.contains('d-none')) {
var inp = document.getElementById('add-exam-input-' + cid);
if (inp) inp.focus();
}
box.classList.remove('d-none');
_aewState[cid] = { count: 1 };
aewRenderTomas(cid);
var ni = document.getElementById('aew-name-' + cid);
if (ni) ni.focus();
}
function guardarNuevoExamen(cid) {
var inp = document.getElementById('add-exam-input-' + cid);
var msg = document.getElementById('add-exam-msg-' + cid);
if (!inp) return;
var name = inp.value.trim();
if (!name) { if (msg) msg.innerHTML = '<span class="text-danger">Ingrese un nombre.</span>'; return; }
function aewClose(cid) {
var box = document.getElementById('aew-box-' + cid);
if (box) box.classList.add('d-none');
}
function aewAddToma(cid, delta) {
if (!_aewState[cid]) return;
var n = (_aewState[cid].count || 1) + delta;
if (n < 1) n = 1; if (n > 12) n = 12;
_aewState[cid].count = n;
var el = document.getElementById('aew-count-' + cid);
if (el) el.textContent = n;
aewRenderTomas(cid);
}
function aewRenderTomas(cid) {
var n = (_aewState[cid] || {}).count || 1;
var cnt = document.getElementById('aew-tomas-' + cid);
if (!cnt) return;
// Guardar valores actuales antes de re-renderizar
var prev = [];
cnt.querySelectorAll('.aew-row').forEach(function(r) {
prev.push({
lbl: r.querySelector('.aew-lbl') ? r.querySelector('.aew-lbl').value : '',
tipo: r.querySelector('.aew-tipo') ? r.querySelector('.aew-tipo').value : 'minutos',
val: r.querySelector('.aew-val') ? r.querySelector('.aew-val').value : '',
});
});
var html = '';
for (var i = 0; i < n; i++) {
var p = prev[i] || {};
var lbl = p.lbl || (i === 0 ? 'Minuto 0' : 'Minuto ' + (i * 30));
var tipo = p.tipo || 'minutos';
var val = p.val !== undefined ? p.val : (i === 0 ? '0' : String(i * 30));
html += '<div class="aew-row border rounded p-2 mb-1 d-flex gap-2 align-items-end flex-wrap">'
+ '<div style="flex:2;min-width:120px"><label class="form-label small mb-1">Label toma ' + (i+1) + '</label>'
+ '<input class="form-control form-control-sm aew-lbl" value="' + lbl.replace(/"/g,'&quot;') + '" placeholder="Ej: Minuto 0"></div>'
+ '<div style="flex:1;min-width:100px"><label class="form-label small mb-1">Tipo</label>'
+ '<select class="form-select form-select-sm aew-tipo" onchange="aewTipoChange(this)">'
+ '<option value="minutos"' + (tipo==='minutos'?' selected':'') + '>Minutos</option>'
+ '<option value="hora_fija"' + (tipo==='hora_fija'?' selected':'') + '>Hora fija</option>'
+ '</select></div>'
+ '<div style="flex:1;min-width:80px"><label class="form-label small mb-1 aew-val-lbl">' + (tipo==='hora_fija'?'HH:MM':'Minutos') + '</label>'
+ '<input class="form-control form-control-sm aew-val" value="' + String(val).replace(/"/g,'&quot;') + '" placeholder="' + (tipo==='hora_fija'?'08:00':'0') + '"></div>'
+ '</div>';
}
cnt.innerHTML = html;
}
function aewTipoChange(sel) {
var row = sel.closest('.aew-row');
var lbl = row.querySelector('.aew-val-lbl');
var inp = row.querySelector('.aew-val');
if (sel.value === 'hora_fija') { lbl.textContent = 'HH:MM'; inp.placeholder = '08:00'; }
else { lbl.textContent = 'Minutos'; inp.placeholder = '0'; }
}
function aewSave(cid) {
var name = (document.getElementById('aew-name-' + cid) || {}).value;
if (name) name = name.trim();
var msg = document.getElementById('aew-msg-' + cid);
function showErr(e) { if (msg) msg.innerHTML = '<span class="text-danger"><i class="fas fa-exclamation-circle me-1"></i>' + e + '</span>'; }
if (!name) { showErr('Ingrese el nombre del examen.'); return; }
var tomas = [];
document.querySelectorAll('#aew-tomas-' + cid + ' .aew-row').forEach(function(row) {
tomas.push({
label: (row.querySelector('.aew-lbl') || {}).value || '',
tipo: (row.querySelector('.aew-tipo') || {}).value || 'minutos',
valor: (row.querySelector('.aew-val') || {}).value || '0',
});
});
if (!tomas.length) { showErr('Configure al menos una toma.'); return; }
for (var i = 0; i < tomas.length; i++) {
if (!tomas[i].label.trim()) { showErr('Ingrese el label de la toma ' + (i+1) + '.'); return; }
}
var btn = document.getElementById('aew-save-' + cid);
if (btn) { btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Guardando…'; }
if (msg) msg.innerHTML = '';
var base = window.location.href.split('/ver_formulario_enviado.php')[0];
fetch(base + '/api/lab/add_exam_option.php', {
fetch(base + '/api/lab/save_tomas_config.php', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({exam_name: name})
body: JSON.stringify({ action: 'add_exam', exam_name: name, tomas: tomas })
})
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.ok) {
// Añadir radio button al grupo y seleccionarlo
var wrap = document.querySelector('[data-campo-id="' + cid + '"]');
if (wrap) {
var div = document.createElement('div');
div.className = 'form-check';
div.innerHTML = '<input class="form-check-input" type="radio" name="' + cid + '" value="' + name.replace(/"/g, '&quot;') + '" checked>'
+ '<label class="form-check-label">' + name.replace(/</g, '&lt;') + '</label>';
// Insertar antes del bloque add-exam-wrap
var addWrap = document.getElementById('add-exam-wrap-' + cid);
wrap.insertBefore(div, addWrap);
// Disparar evento change para condiciones de visibilidad
div.querySelector('input').dispatchEvent(new Event('change', {bubbles: true}));
}
inp.value = '';
if (msg) msg.innerHTML = '<span class="text-success"><i class="fas fa-check me-1"></i>Añadido.</span>';
setTimeout(function() {
toggleAddExam(cid);
if (msg) msg.innerHTML = '';
}, 1500);
if (msg) msg.innerHTML = '<span class="text-success"><i class="fas fa-check me-1"></i>Examen creado. Recargando…</span>';
// Guardar selección en sessionStorage para auto-seleccionar al recargar
try { sessionStorage.setItem('_aew_preselect', name); } catch(e) {}
setTimeout(function() { location.reload(); }, 900);
} else {
if (msg) msg.innerHTML = '<span class="text-danger">' + (d.error || 'Error') + '</span>';
showErr(d.error || 'Error desconocido');
if (btn) { btn.disabled = false; btn.innerHTML = '<i class="fas fa-check me-1"></i>Crear examen'; }
}
})
.catch(function() {
if (msg) msg.innerHTML = '<span class="text-danger">Error de conexión.</span>';
showErr('Error de conexión.');
if (btn) { btn.disabled = false; btn.innerHTML = '<i class="fas fa-check me-1"></i>Crear examen'; }
});
}
// Auto-seleccionar el examen recién creado tras recargar
(function() {
try {
var pre = sessionStorage.getItem('_aew_preselect');
if (!pre) return;
sessionStorage.removeItem('_aew_preselect');
var radio = document.querySelector('input[type="radio"][value="' + pre.replace(/"/g, '\\"') + '"]');
if (radio) { radio.checked = true; radio.dispatchEvent(new Event('change', {bubbles:true})); }
} catch(e) {}
})();
window._muestrasMap = <?= json_encode($_mpMap, JSON_UNESCAPED_UNICODE) ?>;
window._mpSavedFirmas = {};