feat: ciclos configurables + gestión de exámenes en Tomas Prolongadas
Feature 1 — Ciclos configurables por paciente: - ver_formulario_enviado.php: pantalla de configuración aparece la primera vez que el profesional abre el formulario (antes de firmar); permite seleccionar qué tomas realizar para cada protocolo. $_mpSkippedSepIds oculta las secciones no seleccionadas en render. $_mpMap se reconstruye desde los grupos filtrados para que next_firma_id salte tomas omitidas. - configurar_tomas.php (nuevo): guarda _tomas_config en datos_respuestas del consentimiento; valida que cada grupo tenga al menos una toma. - guardar_toma.php: aplica el filtro _tomas_config al array firmasCampos para que el conteo de tomas firmadas/pendientes sea correcto. Feature 2 — Gestión de tipos de examen (admin): - lab_tomas_config.php (nuevo): página admin que muestra los exámenes actuales con sus tomas y permite añadir nuevos tipos con su esquema completo (separador + hora + resultado + firma_profesional por toma). - api/lab/save_tomas_config.php (nuevo): modifica el esquema JSON del formulario id=15; add_exam agrega opción al selector y los campos de cada toma; delete_exam elimina la opción y los separadores exclusivos. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
3e110324a8
commit
2eefe3f5c8
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
/**
|
||||
* POST /api/lab/save_tomas_config.php
|
||||
* Gestiona los tipos de examen del formulario de Tomas Prolongadas (lab_formularios.id=15).
|
||||
*
|
||||
* Body JSON:
|
||||
* action string 'add_exam' | 'delete_exam'
|
||||
* exam_name string Nombre del nuevo tipo de examen
|
||||
* tomas array [{label, tipo:'minutos'|'hora_fija', valor}] (solo add_exam)
|
||||
*/
|
||||
require_once __DIR__ . '/_helpers.php';
|
||||
requireAdmin();
|
||||
requireMethod('POST');
|
||||
|
||||
$body = inputJson();
|
||||
$action = trim($body['action'] ?? '');
|
||||
|
||||
if (!in_array($action, ['add_exam', 'delete_exam'], true)) jsonError('action inválida.');
|
||||
|
||||
$db = Database::getInstance();
|
||||
$row = $db->fetch("SELECT id, esquema FROM lab_formularios WHERE id = 15 LIMIT 1");
|
||||
if (!$row) jsonError('Formulario de tomas no encontrado.', 404);
|
||||
|
||||
$esquema = json_decode($row['esquema'], true);
|
||||
if (!is_array($esquema)) jsonError('Esquema del formulario no válido.', 500);
|
||||
|
||||
// ── Localizar el campo selector de tipo de examen ────────────
|
||||
$idxSelector = null;
|
||||
foreach ($esquema as $i => $c) {
|
||||
if (($c['id'] ?? '') === '_c8j2g16') { $idxSelector = $i; break; }
|
||||
}
|
||||
if ($idxSelector === null) jsonError('Campo selector de examen (_c8j2g16) no encontrado.', 500);
|
||||
|
||||
// ── ADD EXAM ─────────────────────────────────────────────────
|
||||
if ($action === 'add_exam') {
|
||||
$examName = trim($body['exam_name'] ?? '');
|
||||
$tomas = $body['tomas'] ?? [];
|
||||
|
||||
if (!$examName) jsonError('exam_name requerido.');
|
||||
if (strlen($examName) > 80) jsonError('exam_name demasiado largo (máx 80 chars).');
|
||||
if (!is_array($tomas) || empty($tomas)) jsonError('tomas requeridas.');
|
||||
if (count($tomas) > 20) jsonError('Máximo 20 tomas por examen.');
|
||||
|
||||
// Verificar que el examen no exista ya
|
||||
$currentOptions = $esquema[$idxSelector]['options'] ?? [];
|
||||
if (in_array($examName, $currentOptions, true)) {
|
||||
jsonError("El tipo de examen '$examName' ya existe.");
|
||||
}
|
||||
|
||||
// Validar tomas
|
||||
foreach ($tomas as $i => $t) {
|
||||
$tipo = $t['tipo'] ?? '';
|
||||
$valor = $t['valor'] ?? '';
|
||||
$label = trim($t['label'] ?? '');
|
||||
if (!in_array($tipo, ['minutos', 'hora_fija'], true)) jsonError("Toma $i: tipo inválido.");
|
||||
if (!$label) jsonError("Toma $i: label requerido.");
|
||||
if ($tipo === 'minutos' && (!is_numeric($valor) || (int)$valor < 0))
|
||||
jsonError("Toma $i: valor de minutos inválido.");
|
||||
if ($tipo === 'hora_fija' && !preg_match('/^\d{1,2}:\d{2}$/', $valor))
|
||||
jsonError("Toma $i: hora_fija debe ser HH:MM.");
|
||||
}
|
||||
|
||||
// Prefijo corto para IDs (basado en nombre del examen, sanitizado)
|
||||
$prefix = '_' . substr(preg_replace('/[^a-z0-9]/i', '', strtolower($examName)), 0, 8) . '_';
|
||||
$uid = substr(md5($examName . microtime()), 0, 4);
|
||||
|
||||
// 1. Agregar opción al campo selector
|
||||
$esquema[$idxSelector]['options'][] = $examName;
|
||||
|
||||
// 2. Agregar campos al final del esquema
|
||||
$condCampoId = '_c8j2g16';
|
||||
foreach ($tomas as $idx => $t) {
|
||||
$label = trim($t['label']);
|
||||
$tipo = $t['tipo'];
|
||||
$valor = $t['valor'];
|
||||
|
||||
// Construir label del separador
|
||||
if ($tipo === 'minutos') {
|
||||
$sepLabel = "$examName · Minuto $valor";
|
||||
} else {
|
||||
// hora_fija: convertir HH:MM a "H:MM a.m./p.m."
|
||||
[$hh, $mm] = explode(':', $valor);
|
||||
$h = (int)$hh; $ampm = $h >= 12 ? 'p.m.' : 'a.m.';
|
||||
$h12 = $h > 12 ? $h - 12 : ($h === 0 ? 12 : $h);
|
||||
$sepLabel = "$examName · {$h12}:{$mm} {$ampm}";
|
||||
}
|
||||
|
||||
$sepId = $prefix . 's' . $idx . $uid;
|
||||
$horaId = $prefix . 'h' . $idx . $uid;
|
||||
$resId = $prefix . 'r' . $idx . $uid;
|
||||
$firmaId = $prefix . 'f' . $idx . $uid;
|
||||
|
||||
$esquema[] = [
|
||||
'id' => $sepId,
|
||||
'tipo' => 'separador',
|
||||
'label' => $sepLabel,
|
||||
'condicion'=> ['campo_id' => $condCampoId, 'valores' => [$examName]],
|
||||
];
|
||||
$esquema[] = [
|
||||
'id' => $horaId,
|
||||
'tipo' => 'hora',
|
||||
'label' => 'Hora de toma',
|
||||
];
|
||||
$esquema[] = [
|
||||
'id' => $resId,
|
||||
'tipo' => 'numero',
|
||||
'label' => 'Resultado',
|
||||
];
|
||||
$esquema[] = [
|
||||
'id' => $firmaId,
|
||||
'tipo' => 'firma_profesional',
|
||||
'label' => 'Firma del profesional',
|
||||
];
|
||||
}
|
||||
|
||||
$db->getConnection()->prepare(
|
||||
"UPDATE lab_formularios SET esquema = ? WHERE id = 15"
|
||||
)->execute([json_encode($esquema, JSON_UNESCAPED_UNICODE)]);
|
||||
|
||||
jsonOk([
|
||||
'exam_name' => $examName,
|
||||
'tomas_count' => count($tomas),
|
||||
'options_count'=> count($esquema[$idxSelector]['options']),
|
||||
], "Examen '$examName' agregado con " . count($tomas) . " tomas.");
|
||||
}
|
||||
|
||||
// ── DELETE EXAM ──────────────────────────────────────────────
|
||||
if ($action === 'delete_exam') {
|
||||
$examName = trim($body['exam_name'] ?? '');
|
||||
if (!$examName) jsonError('exam_name requerido.');
|
||||
|
||||
$options = $esquema[$idxSelector]['options'] ?? [];
|
||||
if (!in_array($examName, $options, true)) jsonError("El examen '$examName' no existe.");
|
||||
|
||||
// Eliminar opción del selector
|
||||
$esquema[$idxSelector]['options'] = array_values(array_filter($options, fn($o) => $o !== $examName));
|
||||
|
||||
// Eliminar separadores condicionados únicamente a este examen
|
||||
$esquema = array_values(array_filter($esquema, function($c) use ($examName) {
|
||||
$cond = $c['condicion'] ?? null;
|
||||
if (!$cond) return true;
|
||||
$vals = $cond['valores'] ?? [];
|
||||
return !(count($vals) === 1 && $vals[0] === $examName);
|
||||
}));
|
||||
|
||||
$db->getConnection()->prepare(
|
||||
"UPDATE lab_formularios SET esquema = ? WHERE id = 15"
|
||||
)->execute([json_encode($esquema, JSON_UNESCAPED_UNICODE)]);
|
||||
|
||||
jsonOk(['exam_name' => $examName], "Examen '$examName' eliminado.");
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
<?php
|
||||
/**
|
||||
* lab_tomas_config.php — Configuración de tipos de examen en Tomas Prolongadas (F-LAB-28)
|
||||
* Admin: añadir / eliminar tipos de examen y sus esquemas de tomas.
|
||||
*/
|
||||
require_once 'config/config.php';
|
||||
if (!defined('APP_ROOT')) { define('APP_ROOT', __DIR__); }
|
||||
if (!isUserLoggedIn()) { header('Location: login.php'); exit; }
|
||||
if (isEnfermero()) { header('Location: index.php'); exit; }
|
||||
|
||||
$db = Database::getInstance();
|
||||
$row = $db->fetch("SELECT esquema FROM lab_formularios WHERE id = 15 LIMIT 1");
|
||||
if (!$row) { die('Formulario de tomas no encontrado (id=15).'); }
|
||||
|
||||
$esquema = json_decode($row['esquema'], true) ?? [];
|
||||
|
||||
// Leer campo selector de examen
|
||||
$selectorCampo = null;
|
||||
foreach ($esquema as $c) {
|
||||
if (($c['id'] ?? '') === '_c8j2g16') { $selectorCampo = $c; break; }
|
||||
}
|
||||
$examOptions = $selectorCampo['options'] ?? [];
|
||||
|
||||
// Agrupar tomas por tipo de examen (leer la misma lógica del $_mpMap)
|
||||
$gruposActuales = [];
|
||||
$curLabel = null; $curCond = null; $curSepId = null;
|
||||
$curMin = null; $curHoraFija = null; $curHora = null;
|
||||
foreach ($esquema as $c) {
|
||||
$tipo = $c['tipo'] ?? '';
|
||||
if ($tipo === 'separador') {
|
||||
$curLabel = $c['label'] ?? '';
|
||||
$curSepId = $c['id'] ?? null;
|
||||
$curCond = $c['condicion'] ?? null;
|
||||
$curMin = $curHoraFija = $curHora = null;
|
||||
if (preg_match('/[Mm]inuto\s+(\d+)/u', $curLabel, $mx)) {
|
||||
$curMin = (int)$mx[1];
|
||||
} elseif (preg_match('/(\d{1,2}):(\d{2})\s*(a\.?m\.?|p\.?m\.?)/i', $curLabel, $mx)) {
|
||||
$h = (int)$mx[1]; $m2 = (int)$mx[2];
|
||||
$pm = strtolower(preg_replace('/[^apm]/i', '', $mx[3])) === 'pm';
|
||||
if ($pm && $h < 12) $h += 12;
|
||||
$curHoraFija = sprintf('%02d:%02d', $h, $m2);
|
||||
$curMin = PHP_INT_MAX;
|
||||
}
|
||||
} elseif ($tipo === 'hora' && ($curMin !== null || $curHoraFija !== null)) {
|
||||
$curHora = $c['id'] ?? null;
|
||||
} elseif ($tipo === 'firma_profesional' && $curHora !== null && ($curMin !== null || $curHoraFija !== null)) {
|
||||
// Determinar a qué examen pertenece
|
||||
$examKey = 'Sin condición';
|
||||
if ($curCond) {
|
||||
$vals = $curCond['valores'] ?? [];
|
||||
$examKey = count($vals) === 1 ? $vals[0] : implode(' / ', $vals);
|
||||
} else {
|
||||
// Label-based: prefijo antes de "·"
|
||||
$examKey = trim(explode('·', $curLabel)[0]);
|
||||
}
|
||||
if (!isset($gruposActuales[$examKey])) $gruposActuales[$examKey] = [];
|
||||
$gruposActuales[$examKey][] = [
|
||||
'label' => $curLabel,
|
||||
'sep_id' => $curSepId,
|
||||
'hora_fija' => $curHoraFija,
|
||||
'min' => $curMin === PHP_INT_MAX ? null : $curMin,
|
||||
'firma_id' => $c['id'],
|
||||
];
|
||||
$curHora = null;
|
||||
}
|
||||
}
|
||||
|
||||
Layout::open('Configurar Tomas Prolongadas', 'fas fa-vials');
|
||||
?>
|
||||
|
||||
<div class="row g-4">
|
||||
<!-- ── Exámenes actuales ── -->
|
||||
<div class="col-lg-7">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<span><i class="fas fa-list me-2 text-primary"></i>Tipos de examen configurados</span>
|
||||
<span class="badge bg-secondary"><?= count($examOptions) ?> tipos</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<?php if (empty($gruposActuales)): ?>
|
||||
<p class="text-muted text-center py-4">No hay grupos de tomas configurados.</p>
|
||||
<?php else: ?>
|
||||
<div class="accordion accordion-flush" id="acc-examenes">
|
||||
<?php foreach ($gruposActuales as $examNombre => $tomas): ?>
|
||||
<?php
|
||||
$accId = 'acc-' . md5($examNombre);
|
||||
$isCond = in_array($examNombre, $examOptions, true);
|
||||
?>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed py-2 px-3" type="button"
|
||||
data-bs-toggle="collapse" data-bs-target="#<?= $accId ?>">
|
||||
<i class="fas fa-syringe me-2 text-primary small"></i>
|
||||
<strong><?= htmlspecialchars($examNombre) ?></strong>
|
||||
<span class="badge bg-light text-dark border ms-2"><?= count($tomas) ?> tomas</span>
|
||||
<?php if (!$isCond): ?>
|
||||
<span class="badge bg-info text-white ms-1 small">Sin condición</span>
|
||||
<?php endif; ?>
|
||||
</button>
|
||||
</h2>
|
||||
<div id="<?= $accId ?>" class="accordion-collapse collapse">
|
||||
<div class="accordion-body p-0">
|
||||
<table class="table table-sm mb-0 small">
|
||||
<thead class="table-light">
|
||||
<tr><th>#</th><th>Label</th><th>Tiempo</th><th>Firma ID</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($tomas as $i => $t): ?>
|
||||
<tr>
|
||||
<td class="text-muted"><?= $i + 1 ?></td>
|
||||
<td><?= htmlspecialchars($t['label']) ?></td>
|
||||
<td>
|
||||
<?php if ($t['hora_fija']): ?>
|
||||
<span class="badge bg-warning text-dark">Fija <?= htmlspecialchars($t['hora_fija']) ?></span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-light text-dark border">Min <?= (int)$t['min'] ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="font-monospace text-muted small"><?= htmlspecialchars($t['firma_id']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php if ($isCond): ?>
|
||||
<div class="p-2 text-end">
|
||||
<button class="btn btn-sm btn-outline-danger btn-delete-exam"
|
||||
data-exam="<?= htmlspecialchars($examNombre, ENT_QUOTES) ?>">
|
||||
<i class="fas fa-trash me-1"></i>Eliminar examen
|
||||
</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Agregar nuevo examen ── -->
|
||||
<div class="col-lg-5">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-plus-circle me-2 text-success"></i>Agregar nuevo tipo de examen
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Nombre del examen</label>
|
||||
<input type="text" id="new-exam-name" class="form-control"
|
||||
placeholder="Ej: Insulina Basal y Post-Carga" maxlength="80">
|
||||
<div class="form-text">Debe ser único. Aparecerá en el selector de examen del formulario.</div>
|
||||
</div>
|
||||
|
||||
<label class="form-label fw-semibold">Tomas</label>
|
||||
<div id="tomas-container">
|
||||
<!-- Tomas se agregan aquí dinámicamente -->
|
||||
</div>
|
||||
<button class="btn btn-outline-secondary btn-sm mt-1" id="btn-add-toma">
|
||||
<i class="fas fa-plus me-1"></i>Agregar toma
|
||||
</button>
|
||||
|
||||
<hr>
|
||||
<button class="btn btn-success w-100" id="btn-save-exam">
|
||||
<i class="fas fa-save me-2"></i>Guardar examen
|
||||
</button>
|
||||
<div id="save-msg" class="mt-2 small text-center"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-info-circle me-2 text-info"></i>Instrucciones
|
||||
</div>
|
||||
<div class="card-body small text-muted">
|
||||
<ul class="mb-0 ps-3">
|
||||
<li>Defina el nombre exacto del examen (aparecerá en el checkbox del formulario).</li>
|
||||
<li><strong>Minutos</strong>: tiempo en minutos desde la primera toma (ej: 0, 30, 60).</li>
|
||||
<li><strong>Hora fija</strong>: hora exacta de toma (ej: 15:00 para 3pm).</li>
|
||||
<li>No mezcle minutos y hora fija en el mismo examen.</li>
|
||||
<li>Para <strong>eliminar</strong> un examen: expanda el grupo y use el botón Eliminar.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template id="tpl-toma">
|
||||
<div class="toma-row border rounded p-2 mb-2 bg-light position-relative">
|
||||
<button type="button" class="btn-close position-absolute top-0 end-0 m-1 btn-remove-toma" style="font-size:.65rem"></button>
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-5">
|
||||
<label class="form-label small mb-1">Label</label>
|
||||
<input type="text" class="form-control form-control-sm toma-label"
|
||||
placeholder="Ej: Minuto 0">
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<label class="form-label small mb-1">Tipo</label>
|
||||
<select class="form-select form-select-sm toma-tipo">
|
||||
<option value="minutos">Minutos</option>
|
||||
<option value="hora_fija">Hora fija</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label class="form-label small mb-1 toma-valor-label">Minutos</label>
|
||||
<input type="text" class="form-control form-control-sm toma-valor"
|
||||
placeholder="0">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
var BASE_URL = '<?= defined('APP_URL') ? rtrim(APP_URL, '/') : '' ?>';
|
||||
|
||||
function addToma(label, tipo, valor) {
|
||||
var tpl = document.getElementById('tpl-toma').content.cloneNode(true);
|
||||
var row = tpl.querySelector('.toma-row');
|
||||
if (label) row.querySelector('.toma-label').value = label;
|
||||
if (tipo) row.querySelector('.toma-tipo').value = tipo;
|
||||
if (valor !== undefined) row.querySelector('.toma-valor').value = valor;
|
||||
row.querySelector('.toma-tipo').addEventListener('change', function() {
|
||||
var lbl = this.closest('.toma-row').querySelector('.toma-valor-label');
|
||||
var inp = this.closest('.toma-row').querySelector('.toma-valor');
|
||||
if (this.value === 'hora_fija') {
|
||||
lbl.textContent = 'HH:MM'; inp.placeholder = '15:00';
|
||||
} else {
|
||||
lbl.textContent = 'Minutos'; inp.placeholder = '0';
|
||||
}
|
||||
});
|
||||
row.querySelector('.btn-remove-toma').addEventListener('click', function() {
|
||||
this.closest('.toma-row').remove();
|
||||
});
|
||||
document.getElementById('tomas-container').appendChild(tpl);
|
||||
}
|
||||
|
||||
document.getElementById('btn-add-toma').addEventListener('click', function() { addToma(); });
|
||||
// Iniciar con una toma vacía
|
||||
addToma('Minuto 0', 'minutos', '0');
|
||||
|
||||
document.getElementById('btn-save-exam').addEventListener('click', function() {
|
||||
var examName = document.getElementById('new-exam-name').value.trim();
|
||||
if (!examName) { showMsg('Ingrese el nombre del examen.', 'danger'); return; }
|
||||
|
||||
var tomas = [];
|
||||
document.querySelectorAll('#tomas-container .toma-row').forEach(function(row) {
|
||||
tomas.push({
|
||||
label: row.querySelector('.toma-label').value.trim(),
|
||||
tipo: row.querySelector('.toma-tipo').value,
|
||||
valor: row.querySelector('.toma-valor').value.trim(),
|
||||
});
|
||||
});
|
||||
if (!tomas.length) { showMsg('Agregue al menos una toma.', 'danger'); return; }
|
||||
|
||||
var btn = this;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Guardando...';
|
||||
|
||||
fetch(BASE_URL + '/api/lab/save_tomas_config.php', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ action: 'add_exam', exam_name: examName, tomas: tomas })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
if (d.ok) {
|
||||
showMsg('Examen guardado. Recargando...', 'success');
|
||||
setTimeout(() => location.reload(), 1200);
|
||||
} else {
|
||||
showMsg('Error: ' + (d.error || 'Error desconocido'), 'danger');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-save me-2"></i>Guardar examen';
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
showMsg('Error de conexión.', 'danger');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-save me-2"></i>Guardar examen';
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.btn-delete-exam').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var name = this.dataset.exam;
|
||||
if (!confirm('¿Eliminar el examen "' + name + '" y todas sus tomas del formulario?\n\nEsta acción no se puede deshacer.')) return;
|
||||
fetch(BASE_URL + '/api/lab/save_tomas_config.php', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ action: 'delete_exam', exam_name: name })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
if (d.ok) location.reload();
|
||||
else alert('Error: ' + (d.error || 'Error desconocido'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function showMsg(msg, type) {
|
||||
document.getElementById('save-msg').innerHTML =
|
||||
'<span class="text-' + type + '">' + msg + '</span>';
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php Layout::close(); ?>
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
/**
|
||||
* POST /modules/turnero/api/configurar_tomas.php
|
||||
* Guarda la configuración de ciclos (tomas_config) para un consentimiento de toma progresiva.
|
||||
* Se llama desde la pantalla de configuración antes de iniciar las firmas.
|
||||
*
|
||||
* Body JSON:
|
||||
* token string requerido UUID del consentimiento
|
||||
* tomas_config object requerido {grupoKey: [firmaId, ...], ...}
|
||||
*/
|
||||
require_once __DIR__ . '/_helpers.php';
|
||||
requireMethod('POST');
|
||||
|
||||
$body = inputJson();
|
||||
$token = trim($body['token'] ?? '');
|
||||
$cfg = $body['tomas_config'] ?? null;
|
||||
|
||||
if (!$token) jsonError('token requerido.');
|
||||
if (!is_array($cfg) || empty($cfg)) jsonError('tomas_config requerido.');
|
||||
|
||||
foreach ($cfg as $grupo => $ids) {
|
||||
if (!is_array($ids) || empty($ids)) jsonError("El grupo '$grupo' no tiene tomas seleccionadas.");
|
||||
foreach ($ids as $id) {
|
||||
if (!preg_match('/^[a-zA-Z0-9_]+$/', $id)) jsonError("ID de firma inválido: $id");
|
||||
}
|
||||
}
|
||||
|
||||
$pdo = db();
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT id, datos_respuestas FROM turnero_consentimientos WHERE token = ? LIMIT 1"
|
||||
);
|
||||
$stmt->execute([$token]);
|
||||
$tc = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$tc) jsonError('Consentimiento no encontrado.', 404);
|
||||
|
||||
$dr = [];
|
||||
if ($tc['datos_respuestas']) {
|
||||
$d = json_decode($tc['datos_respuestas'], true);
|
||||
if (is_array($d)) $dr = $d;
|
||||
}
|
||||
|
||||
$dr['_tomas_config'] = $cfg;
|
||||
|
||||
$pdo->prepare("UPDATE turnero_consentimientos SET datos_respuestas = ? WHERE id = ?")
|
||||
->execute([json_encode($dr, JSON_UNESCAPED_UNICODE), $tc['id']]);
|
||||
|
||||
jsonOk([], 'Configuración de tomas guardada.');
|
||||
@@ -101,6 +101,16 @@ foreach ($esquema as $campo) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Aplicar filtro de ciclos configurados (tomas_config) ────
|
||||
$__tcFirmas = $datosMerge['_tomas_config'] ?? null;
|
||||
if (is_array($__tcFirmas)) {
|
||||
$__allowed = [];
|
||||
foreach ($__tcFirmas as $__g => $__ids) {
|
||||
if (is_array($__ids)) foreach ($__ids as $__id) $__allowed[$__id] = true;
|
||||
}
|
||||
$firmasCampos = array_values(array_filter($firmasCampos, fn($f) => isset($__allowed[$f['id']])));
|
||||
}
|
||||
|
||||
// ── Determinar estado y siguiente_toma_at ────────────────────
|
||||
// Acepta firma guardada con clave campo_id O campo_id_svg (compatibilidad registros históricos)
|
||||
$hasFirma = fn($f) => (isset($datosMerge[$f['id']]) && strlen($datosMerge[$f['id']]) > 10)
|
||||
|
||||
+161
-2
@@ -248,14 +248,19 @@ $_soloFirmaPro = ($modoTurnero && !empty($tcRow['es_toma_progresiva']))
|
||||
|
||||
// Muestras Prolongadas: detectar secciones hora+firma_profesional con intervalos de minutos.
|
||||
// Agrupa por condición del separador para no mezclar Glicemia y Cortisol (ambas usan prefijo "Toma").
|
||||
$_mpMap = [];
|
||||
$_mpMap = [];
|
||||
$_mpGroupsFull = []; // grupos sin filtrar (para la pantalla de configuración)
|
||||
$_mpSkippedSepIds = []; // separadores de tomas no seleccionadas (se ocultarán en render)
|
||||
$_tomasConfig = null; // configuración guardada por paciente ({grupo:[firmaId,...]})
|
||||
|
||||
if ($modoTurnero && $embebido && $_soloFirmaPro) {
|
||||
$_mpSecs = []; $_mpLabel = null; $_mpMin = null; $_mpHoraFija = null; $_mpHora = null;
|
||||
$_mpCondKey = null; $_mpCondDisplay = null;
|
||||
$_mpCondKey = null; $_mpCondDisplay = null; $_mpSepId = null;
|
||||
foreach ($esquema as $_c) {
|
||||
$_t = $_c['tipo'] ?? '';
|
||||
if ($_t === 'separador') {
|
||||
$_mpLabel = $_c['label'] ?? ''; $_mpMin = null; $_mpHoraFija = null; $_mpHora = null;
|
||||
$_mpSepId = $_c['id'] ?? null;
|
||||
$_mpCond = $_c['condicion'] ?? null;
|
||||
// Clave de agrupación: usa valores de condición para distinguir Glicemia vs Cortisol
|
||||
if ($_mpCond) {
|
||||
@@ -279,6 +284,7 @@ if ($modoTurnero && $embebido && $_soloFirmaPro) {
|
||||
} elseif ($_t === 'firma_profesional' && $_mpHora !== null && ($_mpMin !== null || $_mpHoraFija !== null)) {
|
||||
$_mpSecs[] = [
|
||||
'label' => $_mpLabel,
|
||||
'sep_id' => $_mpSepId,
|
||||
'min' => $_mpMin ?? PHP_INT_MAX,
|
||||
'hora_fija' => $_mpHoraFija,
|
||||
'hora' => $_mpHora,
|
||||
@@ -291,6 +297,26 @@ if ($modoTurnero && $embebido && $_soloFirmaPro) {
|
||||
}
|
||||
$_mpGroups = [];
|
||||
foreach ($_mpSecs as $_s) { $_mpGroups[$_s['tipo']][] = $_s; }
|
||||
$_mpGroupsFull = $_mpGroups;
|
||||
|
||||
// Aplicar filtro de ciclos si el profesional ya configuró las tomas para este paciente
|
||||
$__rawCfg = $datosCliente['_tomas_config'] ?? null;
|
||||
if (is_array($__rawCfg)) {
|
||||
$_tomasConfig = $__rawCfg;
|
||||
foreach ($_mpGroups as $__gk => &$__gsecs) {
|
||||
if (!isset($_tomasConfig[$__gk])) continue;
|
||||
$__allowed = array_flip($_tomasConfig[$__gk]);
|
||||
foreach ($__gsecs as $__s) {
|
||||
if (!isset($__allowed[$__s['firma']])) {
|
||||
$_mpSkippedSepIds[$__s['sep_id']] = true;
|
||||
}
|
||||
}
|
||||
$__gsecs = array_values(array_filter($__gsecs, fn($s) => isset($__allowed[$s['firma']])));
|
||||
}
|
||||
unset($__gsecs);
|
||||
}
|
||||
|
||||
// Construir $_mpMap desde grupos (posiblemente filtrados)
|
||||
foreach ($_mpGroups as $_tipo => $_secs) {
|
||||
for ($i = 0, $n = count($_secs); $i < $n; $i++) {
|
||||
$_s = $_secs[$i]; $_nx = $_secs[$i + 1] ?? null;
|
||||
@@ -513,6 +539,24 @@ function esc2(mixed $v): string {
|
||||
display:inline-flex; align-items:center; gap:5px; transition:all .15s; }
|
||||
.btn-topaz:hover { background:#e0f2fe; }
|
||||
@keyframes mp-pulse { 0%,100%{opacity:1} 50%{opacity:.65} }
|
||||
|
||||
/* ── Panel configuración de ciclos ─────────────────── */
|
||||
.mp-cfg-overlay { position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,.45);
|
||||
display:flex;align-items:flex-start;justify-content:center;
|
||||
padding:1rem;overflow-y:auto; }
|
||||
.mp-cfg-card { background:#fff;border-radius:14px;max-width:500px;width:100%;
|
||||
box-shadow:0 12px 48px rgba(0,0,0,.3);margin-top:.5rem; }
|
||||
.mp-cfg-head { background:<?= htmlspecialchars($docColor) ?>;color:#fff;
|
||||
padding:1rem 1.25rem;border-radius:14px 14px 0 0;
|
||||
font-size:1rem;font-weight:700; }
|
||||
.mp-cfg-body { padding:1.25rem; }
|
||||
.mp-cfg-foot { padding:0 1.25rem 1.25rem; }
|
||||
.mp-cfg-gtitle { font-weight:700;font-size:.88rem;color:#1e3a5f;margin-bottom:.5rem;
|
||||
border-left:3px solid <?= htmlspecialchars($docColor) ?>;padding-left:.5rem; }
|
||||
.mp-cfg-item { border:1.5px solid #e5e7eb;border-radius:8px;padding:.35rem .6rem;
|
||||
transition:all .15s;cursor:pointer;user-select:none; }
|
||||
.mp-cfg-item:has(.form-check-input:checked) { background:#f0fdf4;border-color:#86efac; }
|
||||
.mp-cfg-item label { cursor:pointer; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -601,6 +645,70 @@ function esc2(mixed $v): string {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// ── Panel de configuración de ciclos (se muestra antes de iniciar el protocolo) ──
|
||||
$_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;
|
||||
$__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];
|
||||
}
|
||||
}
|
||||
$_showCfgPanel = $modoTurnero && $embebido && !empty($_gruposConfig) && $_tomasConfig === null;
|
||||
?>
|
||||
<?php if ($_showCfgPanel): ?>
|
||||
<div class="mp-cfg-overlay" id="mp-cfg-overlay">
|
||||
<div class="mp-cfg-card">
|
||||
<div class="mp-cfg-head">
|
||||
<i class="fas fa-sliders-h me-2"></i>Configurar protocolo de tomas
|
||||
</div>
|
||||
<div class="mp-cfg-body">
|
||||
<p class="text-muted small mb-3">
|
||||
Seleccione las tomas a realizar para este paciente.
|
||||
Puede desmarcar las tomas que no apliquen según la indicación médica.
|
||||
</p>
|
||||
<?php foreach ($_gruposConfig as $_cgk => $_cg): ?>
|
||||
<div class="mb-3">
|
||||
<div class="mp-cfg-gtitle"><?= esc2($_cg['title']) ?></div>
|
||||
<div class="row g-2">
|
||||
<?php foreach ($_cg['secs'] as $_cs): ?>
|
||||
<div class="col-6 col-sm-4">
|
||||
<div class="mp-cfg-item form-check">
|
||||
<input class="form-check-input mp-cfg-chk" type="checkbox"
|
||||
id="cfg-<?= esc2($_cs['firma']) ?>"
|
||||
value="<?= esc2($_cs['firma']) ?>"
|
||||
data-group="<?= esc2($_cgk) ?>"
|
||||
checked>
|
||||
<label class="form-check-label small" for="cfg-<?= esc2($_cs['firma']) ?>">
|
||||
<?= esc2($_cs['label']) ?>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div class="mp-cfg-foot">
|
||||
<button class="btn btn-success btn-lg w-100" id="mp-cfg-iniciar">
|
||||
<i class="fas fa-play-circle me-2"></i>Iniciar protocolo
|
||||
</button>
|
||||
<div id="mp-cfg-msg" class="text-center mt-2 small"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="doc-body">
|
||||
|
||||
<!-- Info del paciente: omitir si el esquema ya tiene campos linked que la muestran -->
|
||||
@@ -674,6 +782,10 @@ function esc2(mixed $v): string {
|
||||
} else {
|
||||
$_saltarSeccion = false;
|
||||
}
|
||||
// Ocultar secciones de tomas excluidas por la configuración de ciclos del paciente
|
||||
if (!$_saltarSeccion && isset($_mpSkippedSepIds[$campo['id'] ?? ''])) {
|
||||
$_saltarSeccion = true;
|
||||
}
|
||||
if ($_saltarSeccion) continue; ?>
|
||||
<div class="esquema-sep<?= $compact ? ' campo-full' : '' ?>" data-campo-id="<?= esc2($campo['id'] ?? '') ?>"><?= esc2($campo['label'] ?? '') ?></div>
|
||||
<?php continue; endif;
|
||||
@@ -1095,6 +1207,53 @@ function esc2(mixed $v): string {
|
||||
|
||||
<script>
|
||||
<?php if (!empty($_mpMap)): ?>
|
||||
<?php if ($_showCfgPanel ?? false): ?>
|
||||
/* ── Panel de configuración de ciclos ── */
|
||||
(function() {
|
||||
var btn = document.getElementById('mp-cfg-iniciar');
|
||||
if (!btn) return;
|
||||
btn.addEventListener('click', function() {
|
||||
var byGroup = {};
|
||||
document.querySelectorAll('.mp-cfg-chk').forEach(function(chk) {
|
||||
var g = chk.dataset.group;
|
||||
if (!byGroup[g]) byGroup[g] = { total: 0, sel: [] };
|
||||
byGroup[g].total++;
|
||||
if (chk.checked) byGroup[g].sel.push(chk.value);
|
||||
});
|
||||
var config = {};
|
||||
for (var g in byGroup) {
|
||||
if (byGroup[g].sel.length === 0) {
|
||||
document.getElementById('mp-cfg-msg').innerHTML =
|
||||
'<span class="text-danger"><i class="fas fa-exclamation-circle me-1"></i>Seleccione al menos una toma por protocolo.</span>';
|
||||
return;
|
||||
}
|
||||
config[g] = byGroup[g].sel;
|
||||
}
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Guardando...';
|
||||
var base = window.location.href.split('/ver_formulario_enviado.php')[0];
|
||||
fetch(base + '/modules/turnero/api/configurar_tomas.php', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: <?= json_encode($tokenTurnero) ?>, tomas_config: config })
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
if (d.ok) { location.reload(); }
|
||||
else {
|
||||
document.getElementById('mp-cfg-msg').innerHTML =
|
||||
'<span class="text-danger">Error: ' + (d.error || 'Error desconocido') + '</span>';
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-play-circle me-2"></i>Iniciar protocolo';
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
document.getElementById('mp-cfg-msg').innerHTML = '<span class="text-danger">Error de conexión.</span>';
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-play-circle me-2"></i>Iniciar protocolo';
|
||||
});
|
||||
});
|
||||
})();
|
||||
<?php endif; ?>
|
||||
window._muestrasMap = <?= json_encode($_mpMap, JSON_UNESCAPED_UNICODE) ?>;
|
||||
window._mpSavedFirmas = {};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user