up
This commit is contained in:
@@ -0,0 +1,19 @@
|
|||||||
|
-- ════════════════════════════════════════════════════════════════
|
||||||
|
-- Migration 20260422 — Consentimientos por lugar/servicio del turnero
|
||||||
|
-- Permite asociar formularios de consentimiento directamente a un lugar
|
||||||
|
-- (p.ej. "Toma de Muestras 2") independientemente de los exámenes.
|
||||||
|
-- ════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS turnero_lugar_consentimientos (
|
||||||
|
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
lugar_id INT UNSIGNED NOT NULL COMMENT 'FK turnero_lugares.id',
|
||||||
|
formulario_id INT UNSIGNED NOT NULL COMMENT 'FK lab_formularios.id',
|
||||||
|
creado_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY uq_lugar_form (lugar_id, formulario_id),
|
||||||
|
INDEX idx_lugar (lugar_id),
|
||||||
|
INDEX idx_form (formulario_id),
|
||||||
|
CONSTRAINT fk_tlc_lugar FOREIGN KEY (lugar_id) REFERENCES turnero_lugares (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT fk_tlc_form FOREIGN KEY (formulario_id) REFERENCES lab_formularios (id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||||
|
COMMENT='Formularios de consentimiento que aplican para todo turno en un lugar/servicio dado';
|
||||||
@@ -18,6 +18,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
|||||||
$row->execute([$id]);
|
$row->execute([$id]);
|
||||||
$data = $row->fetch(PDO::FETCH_ASSOC);
|
$data = $row->fetch(PDO::FETCH_ASSOC);
|
||||||
if (!$data) jsonError('Lugar no encontrado', 404);
|
if (!$data) jsonError('Lugar no encontrado', 404);
|
||||||
|
// Incluir formularios de consentimiento vinculados
|
||||||
|
$stmtFc = db()->prepare('SELECT formulario_id FROM turnero_lugar_consentimientos WHERE lugar_id = ?');
|
||||||
|
$stmtFc->execute([$id]);
|
||||||
|
$data['formulario_ids'] = $stmtFc->fetchAll(PDO::FETCH_COLUMN);
|
||||||
jsonOk(['data' => $data]);
|
jsonOk(['data' => $data]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,20 +52,44 @@ $sortOrder = (int)($input['sort_order'] ?? 99);
|
|||||||
$activo = (int)($input['activo'] ?? 1);
|
$activo = (int)($input['activo'] ?? 1);
|
||||||
$tipo = in_array($input['tipo'] ?? '', ['recepcion', 'muestras'], true)
|
$tipo = in_array($input['tipo'] ?? '', ['recepcion', 'muestras'], true)
|
||||||
? $input['tipo'] : 'muestras';
|
? $input['tipo'] : 'muestras';
|
||||||
|
$formularioIds = isset($input['formulario_ids']) && is_array($input['formulario_ids'])
|
||||||
|
? array_filter(array_map('intval', $input['formulario_ids']), fn($v) => $v > 0)
|
||||||
|
: [];
|
||||||
|
|
||||||
if ($nombre === '') jsonError('El nombre es requerido');
|
if ($nombre === '') jsonError('El nombre es requerido');
|
||||||
if ($sortOrder < 1) jsonError('El orden debe ser mayor a 0');
|
if ($sortOrder < 1) jsonError('El orden debe ser mayor a 0');
|
||||||
|
|
||||||
if ($id) {
|
$pdo = db();
|
||||||
$stmt = db()->prepare(
|
$pdo->beginTransaction();
|
||||||
'UPDATE turnero_lugares SET nombre=?, tipo=?, descripcion=?, sort_order=?, activo=? WHERE id=?'
|
try {
|
||||||
);
|
if ($id) {
|
||||||
$stmt->execute([$nombre, $tipo, $desc ?: null, $sortOrder, $activo, $id]);
|
$stmt = $pdo->prepare(
|
||||||
jsonOk(['id' => $id], 'Lugar actualizado');
|
'UPDATE turnero_lugares SET nombre=?, tipo=?, descripcion=?, sort_order=?, activo=? WHERE id=?'
|
||||||
} else {
|
);
|
||||||
$stmt = db()->prepare(
|
$stmt->execute([$nombre, $tipo, $desc ?: null, $sortOrder, $activo, $id]);
|
||||||
'INSERT INTO turnero_lugares (nombre, tipo, descripcion, sort_order, activo) VALUES (?, ?, ?, ?, ?)'
|
$lugarId = $id;
|
||||||
);
|
} else {
|
||||||
$stmt->execute([$nombre, $tipo, $desc ?: null, $sortOrder, $activo]);
|
$stmt = $pdo->prepare(
|
||||||
jsonOk(['id' => (int)db()->lastInsertId()], 'Lugar creado');
|
'INSERT INTO turnero_lugares (nombre, tipo, descripcion, sort_order, activo) VALUES (?, ?, ?, ?, ?)'
|
||||||
|
);
|
||||||
|
$stmt->execute([$nombre, $tipo, $desc ?: null, $sortOrder, $activo]);
|
||||||
|
$lugarId = (int) $pdo->lastInsertId();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sincronizar formularios de consentimiento
|
||||||
|
$pdo->prepare('DELETE FROM turnero_lugar_consentimientos WHERE lugar_id = ?')->execute([$lugarId]);
|
||||||
|
if (!empty($formularioIds)) {
|
||||||
|
$stmtIns = $pdo->prepare(
|
||||||
|
'INSERT IGNORE INTO turnero_lugar_consentimientos (lugar_id, formulario_id) VALUES (?, ?)'
|
||||||
|
);
|
||||||
|
foreach ($formularioIds as $fid) {
|
||||||
|
$stmtIns->execute([$lugarId, $fid]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
jsonOk(['id' => $lugarId], $id ? 'Lugar actualizado' : 'Lugar creado');
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
jsonError('Error al guardar lugar: ' . $e->getMessage(), 500);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ $stmt = $pdo->prepare(
|
|||||||
t.estado,
|
t.estado,
|
||||||
s.id AS solicitud_id,
|
s.id AS solicitud_id,
|
||||||
s.paciente_id,
|
s.paciente_id,
|
||||||
|
s.lugar_id,
|
||||||
p.telefono AS pac_telefono,
|
p.telefono AS pac_telefono,
|
||||||
p.telefono AS pac_celular,
|
p.telefono AS pac_celular,
|
||||||
p.nombre_completo AS pac_nombre
|
p.nombre_completo AS pac_nombre
|
||||||
@@ -77,6 +78,7 @@ if (empty($examIds)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── 3. Obtener formularios de consentimiento (deduplicados) ───
|
// ── 3. Obtener formularios de consentimiento (deduplicados) ───
|
||||||
|
// 3a. Consentimientos por tipo de examen
|
||||||
$in = implode(',', array_fill(0, count($examIds), '?'));
|
$in = implode(',', array_fill(0, count($examIds), '?'));
|
||||||
$stmt = $pdo->prepare(
|
$stmt = $pdo->prepare(
|
||||||
"SELECT DISTINCT etc.formulario_id, f.nombre AS formulario_nombre
|
"SELECT DISTINCT etc.formulario_id, f.nombre AS formulario_nombre
|
||||||
@@ -87,8 +89,36 @@ $stmt = $pdo->prepare(
|
|||||||
$stmt->execute($examIds);
|
$stmt->execute($examIds);
|
||||||
$formulariosRequeridos = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$formulariosRequeridos = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// 3b. Consentimientos por lugar/servicio (de la solicitud)
|
||||||
|
$lugarIdSol = (int)($turno['lugar_id'] ?? 0);
|
||||||
|
if (!$lugarIdSol) {
|
||||||
|
// Intentar leer lugar_id directo de la solicitud si no vino en el join anterior
|
||||||
|
$stmtLugar = $pdo->prepare("SELECT lugar_id FROM turnero_solicitudes WHERE id = ?");
|
||||||
|
$stmtLugar->execute([$turno['solicitud_id']]);
|
||||||
|
$lugarIdSol = (int)($stmtLugar->fetchColumn() ?: 0);
|
||||||
|
}
|
||||||
|
if ($lugarIdSol > 0) {
|
||||||
|
$stmtLc = $pdo->prepare(
|
||||||
|
"SELECT tlc.formulario_id, f.nombre AS formulario_nombre
|
||||||
|
FROM turnero_lugar_consentimientos tlc
|
||||||
|
JOIN lab_formularios f ON f.id = tlc.formulario_id
|
||||||
|
WHERE tlc.lugar_id = ?"
|
||||||
|
);
|
||||||
|
$stmtLc->execute([$lugarIdSol]);
|
||||||
|
foreach ($stmtLc->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||||
|
// Deduplicar: solo agregar si aún no está en la lista
|
||||||
|
$yaExiste = false;
|
||||||
|
foreach ($formulariosRequeridos as $fr) {
|
||||||
|
if ((int)$fr['formulario_id'] === (int)$row['formulario_id']) { $yaExiste = true; break; }
|
||||||
|
}
|
||||||
|
if (!$yaExiste) {
|
||||||
|
$formulariosRequeridos[] = $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (empty($formulariosRequeridos)) {
|
if (empty($formulariosRequeridos)) {
|
||||||
jsonOk(['consentimientos' => [], 'mensaje' => 'Ningún examen requiere consentimiento.']);
|
jsonOk(['consentimientos' => [], 'mensaje' => 'Ningún examen ni servicio requiere consentimiento.']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 4. Generar / recuperar tokens UUID ────────────────────────
|
// ── 4. Generar / recuperar tokens UUID ────────────────────────
|
||||||
|
|||||||
@@ -29,7 +29,16 @@ try {
|
|||||||
$lugares = $pdo->query(
|
$lugares = $pdo->query(
|
||||||
'SELECT * FROM turnero_lugares ORDER BY sort_order ASC, id ASC'
|
'SELECT * FROM turnero_lugares ORDER BY sort_order ASC, id ASC'
|
||||||
)->fetchAll(PDO::FETCH_ASSOC);
|
)->fetchAll(PDO::FETCH_ASSOC);
|
||||||
} catch (\Throwable $e) { $_loadErrors[] = 'turnero_lugares: ' . $e->getMessage(); }
|
// Cargar formularios de consentimiento vinculados a cada lugar
|
||||||
|
$lugarFormIds = [];
|
||||||
|
foreach ($lugares as $lu) {
|
||||||
|
$stmtLcf = $pdo->prepare(
|
||||||
|
"SELECT formulario_id FROM turnero_lugar_consentimientos WHERE lugar_id = ?"
|
||||||
|
);
|
||||||
|
$stmtLcf->execute([$lu['id']]);
|
||||||
|
$lugarFormIds[(int)$lu['id']] = $stmtLcf->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) { $_loadErrors[] = 'turnero_lugares: ' . $e->getMessage(); $lugarFormIds = []; }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$prioridades = $pdo->query(
|
$prioridades = $pdo->query(
|
||||||
@@ -321,6 +330,28 @@ $tab = $_GET['tab'] ?? 'lugares';
|
|||||||
<input class="form-check-input" type="checkbox" id="edit-lu-activo">
|
<input class="form-check-input" type="checkbox" id="edit-lu-activo">
|
||||||
<label class="form-check-label small" for="edit-lu-activo">Activo</label>
|
<label class="form-check-label small" for="edit-lu-activo">Activo</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-2 mt-3">
|
||||||
|
<label class="form-label small fw-semibold">
|
||||||
|
<i class="fas fa-file-signature me-1 text-warning"></i>
|
||||||
|
Consentimientos requeridos para este lugar
|
||||||
|
</label>
|
||||||
|
<div id="edit-lu-consents" class="border rounded p-2" style="max-height:160px;overflow-y:auto;background:#fffbeb">
|
||||||
|
<?php foreach ($formulariosCons as $fc): ?>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input edit-lu-consent-chk"
|
||||||
|
type="checkbox"
|
||||||
|
value="<?= (int)$fc['id'] ?>"
|
||||||
|
id="edit-lu-fc-<?= (int)$fc['id'] ?>">
|
||||||
|
<label class="form-check-label small" for="edit-lu-fc-<?= (int)$fc['id'] ?>">
|
||||||
|
<?= htmlspecialchars($fc['nombre']) ?>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php if (empty($formulariosCons)): ?>
|
||||||
|
<small class="text-muted">No hay formularios activos configurados.</small>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer py-2 px-3">
|
<div class="modal-footer py-2 px-3">
|
||||||
<button class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancelar</button>
|
<button class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancelar</button>
|
||||||
@@ -772,6 +803,9 @@ $tab = $_GET['tab'] ?? 'lugares';
|
|||||||
═══════════════════════════════════════════════════════════ */
|
═══════════════════════════════════════════════════════════ */
|
||||||
const API = '<?= BASE_URL ?>modules/turnero/api/';
|
const API = '<?= BASE_URL ?>modules/turnero/api/';
|
||||||
|
|
||||||
|
// Mapa de formularios de consentimiento por lugar (cargado desde PHP)
|
||||||
|
const LUGAR_FORM_IDS = <?= json_encode($lugarFormIds ?? []) ?>;
|
||||||
|
|
||||||
// ── Toast helper ────────────────────────────────────────────
|
// ── Toast helper ────────────────────────────────────────────
|
||||||
function toast(msg, type = 'success') {
|
function toast(msg, type = 'success') {
|
||||||
const stack = document.getElementById('toastStack');
|
const stack = document.getElementById('toastStack');
|
||||||
@@ -811,7 +845,15 @@ async function guardarLugar(tipoOrId = 'muestras', id = null) {
|
|||||||
|
|
||||||
if (!nombre) { toast('El nombre es requerido', 'error'); return; }
|
if (!nombre) { toast('El nombre es requerido', 'error'); return; }
|
||||||
|
|
||||||
const body = { nombre, tipo, descripcion: desc, sort_order: orden, activo };
|
// Consentimientos: solo aplica al editar (el modal tiene los checkboxes)
|
||||||
|
const formularioIds = [];
|
||||||
|
if (esEdicion) {
|
||||||
|
document.querySelectorAll('.edit-lu-consent-chk:checked').forEach(chk => {
|
||||||
|
formularioIds.push(parseInt(chk.value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = { nombre, tipo, descripcion: desc, sort_order: orden, activo, formulario_ids: formularioIds };
|
||||||
if (id) body.id = id;
|
if (id) body.id = id;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -835,6 +877,13 @@ function editarLugar(id, nombre, desc, activo, orden, tipo = 'muestras') {
|
|||||||
document.getElementById('edit-lu-activo').checked = !!activo;
|
document.getElementById('edit-lu-activo').checked = !!activo;
|
||||||
const titulo = tipo === 'recepcion' ? 'Editar escritorio de recepción' : 'Editar estación de muestras';
|
const titulo = tipo === 'recepcion' ? 'Editar escritorio de recepción' : 'Editar estación de muestras';
|
||||||
document.getElementById('modal-lugar-titulo').textContent = titulo;
|
document.getElementById('modal-lugar-titulo').textContent = titulo;
|
||||||
|
|
||||||
|
// Marcar checkboxes de consentimientos del lugar
|
||||||
|
const formIds = (LUGAR_FORM_IDS[id] || []).map(Number);
|
||||||
|
document.querySelectorAll('.edit-lu-consent-chk').forEach(chk => {
|
||||||
|
chk.checked = formIds.includes(parseInt(chk.value));
|
||||||
|
});
|
||||||
|
|
||||||
new bootstrap.Modal(document.getElementById('modalEditarLugar')).show();
|
new bootstrap.Modal(document.getElementById('modalEditarLugar')).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -112,7 +112,9 @@ unset($p);
|
|||||||
height: 48px;
|
height: 48px;
|
||||||
max-width: 140px;
|
max-width: 140px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
filter: brightness(0) invert(1);
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 4px 6px;
|
||||||
}
|
}
|
||||||
.kiosko-topbar .logo-fallback {
|
.kiosko-topbar .logo-fallback {
|
||||||
width: 46px; height: 46px;
|
width: 46px; height: 46px;
|
||||||
@@ -137,6 +139,19 @@ unset($p);
|
|||||||
font-size: .8rem;
|
font-size: .8rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
.btn-fullscreen {
|
||||||
|
background: rgba(255,255,255,.15);
|
||||||
|
border: 1.5px solid rgba(255,255,255,.3);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background .15s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.btn-fullscreen:hover { background: rgba(255,255,255,.28); }
|
||||||
|
|
||||||
/* ── Pantalla / paso ── */
|
/* ── Pantalla / paso ── */
|
||||||
.screen {
|
.screen {
|
||||||
@@ -351,6 +366,9 @@ unset($p);
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<div class="lab-nombre"><?= $_kNombre ?></div>
|
<div class="lab-nombre"><?= $_kNombre ?></div>
|
||||||
<div class="turno-badge"><i class="fas fa-ticket-alt me-1"></i>Turnero</div>
|
<div class="turno-badge"><i class="fas fa-ticket-alt me-1"></i>Turnero</div>
|
||||||
|
<button class="btn-fullscreen" id="btn-fs" onclick="toggleFullscreen()" title="Pantalla completa">
|
||||||
|
<i class="fas fa-expand" id="fs-icon"></i>
|
||||||
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
||||||
@@ -518,6 +536,22 @@ unset($p);
|
|||||||
|
|
||||||
function setSpinner(on) { document.getElementById('spinner').classList.toggle('active', on); }
|
function setSpinner(on) { document.getElementById('spinner').classList.toggle('active', on); }
|
||||||
|
|
||||||
|
function toggleFullscreen() {
|
||||||
|
if (!document.fullscreenElement) {
|
||||||
|
document.documentElement.requestFullscreen().catch(() => {});
|
||||||
|
} else {
|
||||||
|
document.exitFullscreen().catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('fullscreenchange', () => {
|
||||||
|
const icon = document.getElementById('fs-icon');
|
||||||
|
if (document.fullscreenElement) {
|
||||||
|
icon.classList.replace('fa-expand', 'fa-compress');
|
||||||
|
} else {
|
||||||
|
icon.classList.replace('fa-compress', 'fa-expand');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function mostrarError(msg) {
|
function mostrarError(msg) {
|
||||||
const el = document.getElementById('toast-error');
|
const el = document.getElementById('toast-error');
|
||||||
el.textContent = msg;
|
el.textContent = msg;
|
||||||
|
|||||||
Reference in New Issue
Block a user