Files
whatsapp/modules/turnero/views/configuracion.php
T
Lizandro GuarnizoandClaude Sonnet 4.6 f363631b32 Turnero: mejoras UI, historial detallado, kiosko scanner, firma por turno
- Kiosko: detector scanner (gap < 80ms), iconos 90px, fa-person-pregnant, ticket Arial
- Recepción: vincular_paciente.php para persistir paciente_id antes de firmar consentimiento
- Historial y dashboard: recepción desk, empleado recepción/muestras, exámenes y comentarios
- display_global: turno más pequeño, lugar-nombre más grande
- lab_pacientes: modal compacta con form-sm
- Configuración: icono embarazada fa-person-pregnant en prioridades
- ver_formulario_enviado: firma por campo (fix doble firma)
- docker-compose.local.yml: override BD producción para dev local

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 22:07:09 -05:00

1192 lines
65 KiB
PHP

<?php
/**
* modules/turnero/views/configuracion.php
* Panel de configuración del módulo Turnero — 4 pestañas:
* TAB 1: Lugares / estaciones
* TAB 2: Exámenes y Consentimientos
* TAB 3: Prioridades
* TAB 4: Sesión y WhatsApp
* TAB 5: Pantalla TV (video de fondo)
*/
require_once __DIR__ . '/../../../config/config.php';
if (!isUserLoggedIn()) { header('Location: ' . BASE_URL . 'login.php'); exit; }
$db = Database::getInstance();
$pdo = $db->getConnection();
// ── Cargar datos para los tabs (con fallback para tablas aún no migradas) ──
$lugares = [];
$prioridades = [];
$examTipos = [];
$formulariosCons= [];
$sesiones = [];
$sesionHoy = false;
$cfg = [];
$_loadErrors = [];
try {
$lugares = $pdo->query(
'SELECT * FROM turnero_lugares ORDER BY sort_order ASC, id ASC'
)->fetchAll(PDO::FETCH_ASSOC);
// 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 {
$prioridades = $pdo->query(
'SELECT * FROM turnero_prioridades ORDER BY orden_peso ASC'
)->fetchAll(PDO::FETCH_ASSOC);
} catch (\Throwable $e) { $_loadErrors[] = 'turnero_prioridades: ' . $e->getMessage(); }
try {
$examTipos = $pdo->query(
'SELECT et.*, GROUP_CONCAT(etc.formulario_id) AS form_ids
FROM exam_tipos et
LEFT JOIN exam_tipo_consentimientos etc ON etc.exam_tipo_id = et.id
GROUP BY et.id
ORDER BY et.categoria ASC, et.nombre ASC'
)->fetchAll(PDO::FETCH_ASSOC);
} catch (\Throwable $e) { $_loadErrors[] = 'exam_tipos: ' . $e->getMessage(); }
try {
$formulariosCons = $pdo->query(
"SELECT id, nombre FROM lab_formularios WHERE is_active=1 ORDER BY nombre ASC"
)->fetchAll(PDO::FETCH_ASSOC);
} catch (\Throwable $e) { $_loadErrors[] = 'lab_formularios: ' . $e->getMessage(); }
try {
$sesiones = $pdo->query(
'SELECT s.*,
a1.full_name AS abierto_nombre,
a2.full_name AS cerrado_nombre
FROM turnero_sesiones s
LEFT JOIN admin_users a1 ON a1.id = s.abierto_por
LEFT JOIN admin_users a2 ON a2.id = s.cerrado_por
ORDER BY s.fecha DESC
LIMIT 30'
)->fetchAll(PDO::FETCH_ASSOC);
} catch (\Throwable $e) { $_loadErrors[] = 'turnero_sesiones: ' . $e->getMessage(); }
try {
$stmtHoy = $pdo->prepare(
"SELECT s.*,
(SELECT COUNT(*) FROM turnero_turnos WHERE sesion_id = s.id) AS total_turnos
FROM turnero_sesiones s
WHERE s.fecha = CURDATE()"
);
$stmtHoy->execute();
$sesionHoy = $stmtHoy->fetch(PDO::FETCH_ASSOC);
} catch (\Throwable $e) { $_loadErrors[] = 'sesion_hoy: ' . $e->getMessage(); }
try {
$cfgRows = $pdo->query("SELECT clave, valor FROM lab_config WHERE clave LIKE 'turnero_%'")->fetchAll(PDO::FETCH_ASSOC);
foreach ($cfgRows as $r) { $cfg[$r['clave']] = $r['valor']; }
} catch (\Throwable $e) { $_loadErrors[] = 'lab_config: ' . $e->getMessage(); }
$tab = $_GET['tab'] ?? 'lugares';
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Configuración Turnero</title>
<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">
<style>
body { background: #f1f5f9; }
.page-header { background:#fff; border-bottom:1px solid #e2e8f0; padding:16px 24px; display:flex; align-items:center; gap:12px; }
.page-header h1 { font-size:1.25rem; font-weight:700; color:#1e293b; margin:0; }
.page-header .back-btn { color:#64748b; text-decoration:none; font-size:.9rem; }
.page-header .back-btn:hover { color:#0f172a; }
.content-wrap { max-width:1000px; margin:24px auto; padding:0 16px; }
.nav-tabs .nav-link { color:#475569; font-weight:500; }
.nav-tabs .nav-link.active { color:#1565c0; border-bottom:2px solid #1565c0; }
.tab-card { background:#fff; border:1px solid #e2e8f0; border-top:none; border-radius:0 0 10px 10px; padding:24px; }
.section-title { font-size:.75rem; font-weight:700; text-transform:uppercase; letter-spacing:.08em; color:#64748b; margin:0 0 14px; }
.item-row { display:flex; align-items:center; gap:8px; padding:10px 0; border-bottom:1px solid #f1f5f9; }
.item-row:last-child { border-bottom:none; }
.item-row .item-name { flex:1; font-weight:500; color:#1e293b; }
.btn-icon { border:none; background:none; color:#94a3b8; cursor:pointer; padding:4px 6px; border-radius:6px; }
.btn-icon:hover { background:#f1f5f9; color:#475569; }
.badge-color { width:14px; height:14px; border-radius:50%; display:inline-block; flex-shrink:0; }
.form-add { background:#f8fafc; border:1.5px dashed #cbd5e1; border-radius:8px; padding:16px; margin-top:16px; }
.url-box { font-family:monospace; font-size:.78rem; background:#f0f4ff; border:1px solid #c7d2fe; border-radius:6px; padding:6px 10px; color:#3730a3; word-break:break-all; }
.url-row { display:flex; align-items:center; gap:8px; }
.toast-stack { position:fixed; bottom:24px; right:24px; z-index:9999; display:flex; flex-direction:column; gap:8px; }
.session-badge { font-size:.75rem; padding:2px 8px; border-radius:99px; font-weight:600; }
.session-open { background:#dcfce7; color:#15803d; }
.session-closed{ background:#f1f5f9; color:#64748b; }
.consent-tag { font-size:.7rem; background:#ede9fe; color:#5b21b6; border-radius:99px; padding:1px 7px; white-space:nowrap; }
</style>
</head>
<body>
<div class="page-header">
<a href="<?= BASE_URL ?>modules/turnero/views/recepcion.php" class="back-btn">
<i class="fas fa-arrow-left me-1"></i>Volver
</a>
<h1><i class="fas fa-cogs me-2 text-primary"></i>Configuración del Turnero</h1>
</div>
<div class="content-wrap">
<?php if (!empty($_loadErrors)): ?>
<div class="alert alert-warning mb-3">
<strong><i class="fas fa-exclamation-triangle me-1"></i>Algunas tablas aún no existen (ejecuta las migraciones pendientes):</strong>
<ul class="mb-0 mt-1">
<?php foreach ($_loadErrors as $le): ?>
<li><code><?= htmlspecialchars($le) ?></code></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<ul class="nav nav-tabs" id="cfgTabs">
<li class="nav-item">
<a class="nav-link <?= $tab==='lugares'?'active':'' ?>" href="erp.php?m=turnero&v=configuracion&tab=lugares">
<i class="fas fa-map-marker-alt me-1"></i>Lugares
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= $tab==='examenes'?'active':'' ?>" href="erp.php?m=turnero&v=configuracion&tab=examenes">
<i class="fas fa-flask me-1"></i>Exámenes y Consentimientos
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= $tab==='prioridades'?'active':'' ?>" href="erp.php?m=turnero&v=configuracion&tab=prioridades">
<i class="fas fa-sort-amount-up me-1"></i>Prioridades
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= $tab==='sesion'?'active':'' ?>" href="erp.php?m=turnero&v=configuracion&tab=sesion">
<i class="fas fa-calendar-day me-1"></i>Sesión y WhatsApp
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= $tab==='tv'?'active':'' ?>" href="erp.php?m=turnero&v=configuracion&tab=tv">
<i class="fas fa-tv me-1"></i>Pantalla TV
</a>
</li>
</ul>
<div class="tab-card">
<!-- ════════════════════════════════════════
TAB 1 — LUGARES
════════════════════════════════════════ -->
<?php if ($tab === 'lugares'):
$lugaresRec = array_values(array_filter($lugares, fn($l) => ($l['tipo'] ?? 'muestras') === 'recepcion'));
$lugaresMues = array_values(array_filter($lugares, fn($l) => ($l['tipo'] ?? 'muestras') === 'muestras'));
?>
<!-- ── Escritorios de Recepción ─────────────────── -->
<div class="d-flex align-items-center gap-2 mb-2">
<i class="fas fa-door-open text-primary"></i>
<p class="section-title mb-0">Escritorios de Recepción</p>
<span class="badge bg-primary-subtle text-primary"><?= count($lugaresRec) ?></span>
</div>
<p class="text-muted small mb-3">Cada escritorio es una ventanilla donde un recepcionista atiende pacientes. Aparecerá en la columna izquierda de la pantalla TV global.</p>
<div id="lista-recepcion">
<?php foreach ($lugaresRec as $lu): ?>
<div class="item-row" data-id="<?= $lu['id'] ?>">
<span class="drag-handle text-muted me-1" style="cursor:grab"><i class="fas fa-grip-vertical"></i></span>
<i class="fas fa-door-open text-primary me-1" style="font-size:.8rem"></i>
<span class="item-name"><?= htmlspecialchars($lu['nombre']) ?></span>
<?php if ($lu['descripcion']): ?>
<small class="text-muted d-none d-md-inline"><?= htmlspecialchars($lu['descripcion']) ?></small>
<?php endif; ?>
<span class="badge <?= $lu['activo'] ? 'bg-success-subtle text-success' : 'bg-secondary-subtle text-secondary' ?> ms-1">
<?= $lu['activo'] ? 'Activo' : 'Inactivo' ?>
</span>
<button class="btn-icon text-primary" onclick="editarLugar(<?= $lu['id'] ?>,'<?= addslashes($lu['nombre']) ?>','<?= addslashes($lu['descripcion'] ?? '') ?>',<?= $lu['activo'] ?>,<?= $lu['sort_order'] ?>,'recepcion')" title="Editar">
<i class="fas fa-pencil-alt"></i>
</button>
<button class="btn-icon text-danger" onclick="eliminarLugar(<?= $lu['id'] ?>, '<?= addslashes($lu['nombre']) ?>')" title="Eliminar">
<i class="fas fa-trash"></i>
</button>
</div>
<?php endforeach; ?>
<?php if (empty($lugaresRec)): ?>
<p class="text-muted text-center py-2 small">No hay escritorios de recepción configurados</p>
<?php endif; ?>
</div>
<div class="form-add mt-2 mb-4 p-3" style="background:#eff6ff;border-radius:10px;border:1px solid #bfdbfe">
<p class="section-title mb-3"><i class="fas fa-plus me-1"></i>Agregar escritorio de recepción</p>
<div class="row g-2 align-items-end">
<div class="col-md-5">
<label class="form-label small fw-semibold">Nombre <span class="text-danger">*</span></label>
<input type="text" id="rec-nombre" class="form-control form-control-sm" placeholder="Recepción 1, Ventanilla 2…">
</div>
<div class="col-md-4">
<label class="form-label small fw-semibold">Descripción</label>
<input type="text" id="rec-desc" class="form-control form-control-sm" placeholder="Opcional">
</div>
<div class="col-md-1">
<label class="form-label small fw-semibold">Orden</label>
<input type="number" id="rec-orden" class="form-control form-control-sm" value="<?= count($lugaresRec) + 1 ?>" min="1">
</div>
<div class="col-md-2">
<button class="btn btn-primary btn-sm w-100" onclick="guardarLugar('recepcion')">
<i class="fas fa-save me-1"></i>Guardar
</button>
</div>
</div>
</div>
<hr class="my-3">
<!-- ── Estaciones de Toma de Muestras ──────────── -->
<div class="d-flex align-items-center gap-2 mb-2">
<i class="fas fa-vials text-success"></i>
<p class="section-title mb-0">Estaciones de Toma de Muestras</p>
<span class="badge bg-success-subtle text-success"><?= count($lugaresMues) ?></span>
</div>
<p class="text-muted small mb-3">Cada estación es donde se toman muestras o se realizan pruebas. Aparecerá en la columna derecha de la pantalla TV global.</p>
<div id="lista-muestras">
<?php foreach ($lugaresMues as $lu): ?>
<div class="item-row" data-id="<?= $lu['id'] ?>">
<span class="drag-handle text-muted me-1" style="cursor:grab"><i class="fas fa-grip-vertical"></i></span>
<i class="fas fa-flask text-success me-1" style="font-size:.8rem"></i>
<span class="item-name"><?= htmlspecialchars($lu['nombre']) ?></span>
<?php if ($lu['descripcion']): ?>
<small class="text-muted d-none d-md-inline"><?= htmlspecialchars($lu['descripcion']) ?></small>
<?php endif; ?>
<span class="badge <?= $lu['activo'] ? 'bg-success-subtle text-success' : 'bg-secondary-subtle text-secondary' ?> ms-1">
<?= $lu['activo'] ? 'Activo' : 'Inactivo' ?>
</span>
<button class="btn-icon" onclick="copiarUrlDisplay(<?= $lu['id'] ?>)" title="Copiar URL pantalla TV de este lugar">
<i class="fas fa-tv"></i>
</button>
<button class="btn-icon text-primary" onclick="editarLugar(<?= $lu['id'] ?>,'<?= addslashes($lu['nombre']) ?>','<?= addslashes($lu['descripcion'] ?? '') ?>',<?= $lu['activo'] ?>,<?= $lu['sort_order'] ?>,'muestras')" title="Editar">
<i class="fas fa-pencil-alt"></i>
</button>
<button class="btn-icon text-danger" onclick="eliminarLugar(<?= $lu['id'] ?>, '<?= addslashes($lu['nombre']) ?>')" title="Eliminar">
<i class="fas fa-trash"></i>
</button>
</div>
<?php endforeach; ?>
<?php if (empty($lugaresMues)): ?>
<p class="text-muted text-center py-2 small">No hay estaciones de muestras configuradas</p>
<?php endif; ?>
</div>
<div class="form-add mt-2 mb-4 p-3" style="background:#f0fdf4;border-radius:10px;border:1px solid #bbf7d0">
<p class="section-title mb-3"><i class="fas fa-plus me-1"></i>Agregar estación de toma de muestras</p>
<div class="row g-2 align-items-end">
<div class="col-md-5">
<label class="form-label small fw-semibold">Nombre <span class="text-danger">*</span></label>
<input type="text" id="mue-nombre" class="form-control form-control-sm" placeholder="Toma de Muestras 1, Rayos X…">
</div>
<div class="col-md-4">
<label class="form-label small fw-semibold">Descripción</label>
<input type="text" id="mue-desc" class="form-control form-control-sm" placeholder="Opcional">
</div>
<div class="col-md-1">
<label class="form-label small fw-semibold">Orden</label>
<input type="number" id="mue-orden" class="form-control form-control-sm" value="<?= count($lugaresMues) + 1 ?>" min="1">
</div>
<div class="col-md-2">
<button class="btn btn-success btn-sm w-100" onclick="guardarLugar('muestras')">
<i class="fas fa-save me-1"></i>Guardar
</button>
</div>
</div>
</div>
<!-- Modal editar lugar -->
<div class="modal fade" id="modalEditarLugar" tabindex="-1">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header py-2 px-3">
<h6 class="modal-title" id="modal-lugar-titulo">Editar lugar</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="edit-lu-id">
<input type="hidden" id="edit-lu-tipo" value="muestras">
<div class="mb-2">
<label class="form-label small fw-semibold">Nombre</label>
<input type="text" id="edit-lu-nombre" class="form-control form-control-sm">
</div>
<div class="mb-2">
<label class="form-label small fw-semibold">Descripción</label>
<input type="text" id="edit-lu-desc" class="form-control form-control-sm">
</div>
<div class="mb-2">
<label class="form-label small fw-semibold">Orden</label>
<input type="number" id="edit-lu-orden" class="form-control form-control-sm" min="1">
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="edit-lu-activo">
<label class="form-check-label small" for="edit-lu-activo">Activo</label>
</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 class="modal-footer py-2 px-3">
<button class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancelar</button>
<button class="btn btn-primary btn-sm" onclick="guardarEditarLugar()">Guardar</button>
</div>
</div>
</div>
</div>
<!-- URLs de acceso -->
<hr class="my-4">
<p class="section-title"><i class="fas fa-desktop me-1"></i>URLs de pantalla TV</p>
<div class="mb-3 p-3" style="background:#fffbeb;border:1px solid #fde68a;border-radius:10px">
<div class="fw-semibold small mb-2"><i class="fas fa-star me-1 text-warning"></i>Pantalla Global (sala de espera — recomendada)</div>
<p class="text-muted small mb-2">Muestra TODOS los escritorios de recepción y estaciones de muestras en una sola pantalla.</p>
<div class="url-row">
<div class="url-box flex-1" id="url-global"
data-url="<?= htmlspecialchars(BASE_URL . 'erp.php?m=turnero&v=display_global') ?>"
><?= htmlspecialchars(BASE_URL . 'erp.php?m=turnero&v=display_global') ?></div>
<button class="btn btn-outline-secondary btn-sm" onclick="copiarUrl('url-global')"><i class="fas fa-copy"></i></button>
</div>
</div>
<?php if (!empty($lugaresMues)): ?>
<div class="mb-3">
<div class="fw-semibold small text-muted mb-2"><i class="fas fa-flask me-1"></i>Pantallas individuales por estación de muestras</div>
<?php foreach ($lugaresMues as $lu): ?>
<div class="mb-2">
<small class="d-block mb-1 text-muted"><?= htmlspecialchars($lu['nombre']) ?></small>
<div class="url-row">
<div class="url-box flex-1" id="url-lugar-<?= $lu['id'] ?>"
data-url="<?= htmlspecialchars(BASE_URL . 'erp.php?m=turnero&v=display&display=lugar&lugar_id=' . $lu['id']) ?>"
><?= htmlspecialchars(BASE_URL . 'erp.php?m=turnero&v=display&display=lugar&lugar_id=' . $lu['id']) ?></div>
<button class="btn btn-outline-secondary btn-sm" onclick="copiarUrl('url-lugar-<?= $lu['id'] ?>')"><i class="fas fa-copy"></i></button>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<div class="mb-2">
<small class="fw-semibold text-muted d-block mb-1"><i class="fas fa-tablet-alt me-1"></i>Kiosko de turnos (tablet entrada)</small>
<div class="url-row">
<div class="url-box flex-1" id="url-kiosko"
data-url="<?= htmlspecialchars(BASE_URL . 'modules/turnero/views/kiosko.php') ?>"
><?= htmlspecialchars(BASE_URL . 'modules/turnero/views/kiosko.php') ?></div>
<button class="btn btn-outline-secondary btn-sm" onclick="copiarUrl('url-kiosko')"><i class="fas fa-copy"></i></button>
</div>
</div>
<!-- ════════════════════════════════════════
TAB 2 — EXÁMENES Y CONSENTIMIENTOS
════════════════════════════════════════ -->
<?php elseif ($tab === 'examenes'): ?>
<p class="section-title">Tipos de examen y sus formularios de consentimiento</p>
<?php
// Agrupar por categoría
$categorias = [];
foreach ($examTipos as $et) {
$cat = $et['categoria'] ?: 'Sin categoría';
$categorias[$cat][] = $et;
}
?>
<?php foreach ($categorias as $cat => $items): ?>
<div class="mb-3">
<div class="fw-semibold text-uppercase small text-secondary mb-1" style="font-size:.7rem;letter-spacing:.07em">
<i class="fas fa-tag me-1"></i><?= htmlspecialchars($cat) ?>
</div>
<?php foreach ($items as $et):
$formIds = $et['form_ids'] ? array_map('intval', explode(',', $et['form_ids'])) : [];
?>
<div class="item-row" data-exam-id="<?= $et['id'] ?>">
<span class="item-name">
<span class="badge bg-primary-subtle text-primary me-2" style="font-size:.7rem"><?= htmlspecialchars($et['codigo']) ?></span>
<?= htmlspecialchars($et['nombre']) ?>
</span>
<?php if (!$et['activo']): ?>
<span class="badge bg-secondary-subtle text-secondary" style="font-size:.65rem">Inactivo</span>
<?php endif; ?>
<?php
// Etiquetas de consentimientos vinculados
foreach ($formIds as $fid):
$fNombre = '';
foreach ($formulariosCons as $fc) {
if ($fc['id'] == $fid) { $fNombre = $fc['nombre']; break; }
}
?>
<span class="consent-tag"><i class="fas fa-file-signature me-1"></i><?= htmlspecialchars($fNombre ?: 'Form #'.$fid) ?></span>
<?php endforeach; ?>
<button class="btn-icon text-primary" onclick="editarExamen(<?= $et['id'] ?>)" title="Editar">
<i class="fas fa-pencil-alt"></i>
</button>
<button class="btn-icon text-danger" onclick="eliminarExamen(<?= $et['id'] ?>, '<?= addslashes($et['nombre']) ?>')" title="Eliminar">
<i class="fas fa-trash"></i>
</button>
</div>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
<?php if (empty($examTipos)): ?>
<p class="text-muted text-center py-3">No hay tipos de examen configurados</p>
<?php endif; ?>
<div class="form-add mt-4">
<p class="section-title mb-3"><i class="fas fa-plus me-1"></i>Agregar tipo de examen</p>
<div class="row g-2">
<div class="col-md-2">
<label class="form-label small fw-semibold">Código <span class="text-danger">*</span></label>
<input type="text" id="ex-codigo" class="form-control form-control-sm" placeholder="HEM" maxlength="20">
</div>
<div class="col-md-4">
<label class="form-label small fw-semibold">Nombre <span class="text-danger">*</span></label>
<input type="text" id="ex-nombre" class="form-control form-control-sm" placeholder="Hemograma completo">
</div>
<div class="col-md-3">
<label class="form-label small fw-semibold">Categoría</label>
<input type="text" id="ex-categoria" class="form-control form-control-sm" placeholder="Hematología">
</div>
<div class="col-md-3">
<label class="form-label small fw-semibold">Consentimiento</label>
<select id="ex-form" class="form-select form-select-sm">
<option value="">— Sin consentimiento —</option>
<?php foreach ($formulariosCons as $fc): ?>
<option value="<?= $fc['id'] ?>"><?= htmlspecialchars($fc['nombre']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-12 text-end">
<button class="btn btn-primary btn-sm" onclick="guardarExamen()">
<i class="fas fa-save me-1"></i>Guardar
</button>
</div>
</div>
</div>
<?php if (!empty($formulariosCons)): ?>
<div class="mt-4">
<p class="section-title"><i class="fas fa-file-signature me-1"></i>Vista inversa: por consentimiento</p>
<?php foreach ($formulariosCons as $fc):
$exsVinculados = [];
foreach ($examTipos as $et) {
$fids = $et['form_ids'] ? array_map('intval', explode(',', $et['form_ids'])) : [];
if (in_array((int)$fc['id'], $fids)) $exsVinculados[] = $et;
}
?>
<div class="mb-2">
<span class="fw-semibold small"><?= htmlspecialchars($fc['nombre']) ?></span>
<span class="text-muted small ms-2">
<?= empty($exsVinculados)
? '<em>Sin exámenes vinculados</em>'
: implode(', ', array_map(fn($e)=>'<code>'.$e['codigo'].'</code>', $exsVinculados))
?>
</span>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<!-- Modal editar examen -->
<div class="modal fade" id="modalEditarExamen" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header py-2 px-3">
<h6 class="modal-title">Editar tipo de examen</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="edit-ex-id">
<div class="row g-2">
<div class="col-md-3">
<label class="form-label small fw-semibold">Código</label>
<input type="text" id="edit-ex-codigo" class="form-control form-control-sm" maxlength="20">
</div>
<div class="col-md-9">
<label class="form-label small fw-semibold">Nombre</label>
<input type="text" id="edit-ex-nombre" class="form-control form-control-sm">
</div>
<div class="col-md-6">
<label class="form-label small fw-semibold">Categoría</label>
<input type="text" id="edit-ex-categoria" class="form-control form-control-sm">
</div>
<div class="col-md-6">
<label class="form-label small fw-semibold">Consentimiento</label>
<select id="edit-ex-form" class="form-select form-select-sm">
<option value="">— Sin consentimiento —</option>
<?php foreach ($formulariosCons as $fc): ?>
<option value="<?= $fc['id'] ?>"><?= htmlspecialchars($fc['nombre']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="edit-ex-activo">
<label class="form-check-label small" for="edit-ex-activo">Activo</label>
</div>
</div>
</div>
</div>
<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-primary btn-sm" onclick="guardarEditarExamen()">Guardar</button>
</div>
</div>
</div>
</div>
<!-- ════════════════════════════════════════
TAB 3 — PRIORIDADES
════════════════════════════════════════ -->
<?php elseif ($tab === 'prioridades'): ?>
<p class="section-title">Códigos de prioridad (menor orden_peso = mayor prioridad)</p>
<div id="lista-prioridades">
<?php foreach ($prioridades as $pr): ?>
<div class="item-row" data-id="<?= $pr['id'] ?>">
<span class="badge-color me-2" style="background:<?= htmlspecialchars($pr['color']) ?>"></span>
<span class="fw-bold me-2" style="min-width:28px; color:<?= htmlspecialchars($pr['color']) ?>"><?= htmlspecialchars($pr['codigo']) ?></span>
<span class="item-name"><?= htmlspecialchars($pr['nombre']) ?></span>
<small class="text-muted me-2">Orden: <?= $pr['orden_peso'] ?></small>
<span class="badge <?= $pr['activo'] ? 'bg-success-subtle text-success' : 'bg-secondary-subtle text-secondary' ?>">
<?= $pr['activo'] ? 'Activo' : 'Inactivo' ?>
</span>
<button class="btn-icon text-primary" onclick="editarPrioridad(<?= $pr['id'] ?>,'<?= addslashes($pr['codigo']) ?>','<?= addslashes($pr['nombre']) ?>','<?= htmlspecialchars($pr['color']) ?>','<?= htmlspecialchars($pr['icono'] ?? '') ?>','<?= addslashes($pr['descripcion'] ?? '') ?>',<?= $pr['orden_peso'] ?>,<?= $pr['activo'] ?>)" title="Editar">
<i class="fas fa-pencil-alt"></i>
</button>
</div>
<?php endforeach; ?>
</div>
<p class="text-muted small mt-2">
<i class="fas fa-info-circle me-1"></i>
Los códigos de prioridad no se pueden eliminar (están vinculados a turnos). Solo puede editarlos.
</p>
<!-- Modal editar prioridad -->
<div class="modal fade" id="modalEditarPrioridad" tabindex="-1">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header py-2 px-3">
<h6 class="modal-title">Editar prioridad</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="edit-pr-id">
<div class="mb-2">
<label class="form-label small fw-semibold">Código</label>
<input type="text" id="edit-pr-codigo" class="form-control form-control-sm" maxlength="1" readonly>
<small class="text-muted">El código no puede cambiarse</small>
</div>
<div class="mb-2">
<label class="form-label small fw-semibold">Nombre</label>
<input type="text" id="edit-pr-nombre" class="form-control form-control-sm">
</div>
<div class="row g-2">
<div class="col-6">
<label class="form-label small fw-semibold">Color</label>
<div class="input-group input-group-sm">
<input type="color" id="edit-pr-color" class="form-control form-control-color" style="width:40px;padding:2px">
<input type="text" id="edit-pr-color-text" class="form-control form-control-sm" maxlength="7" placeholder="#6b7280">
</div>
</div>
<div class="col-6">
<label class="form-label small fw-semibold">Orden</label>
<input type="number" id="edit-pr-orden" class="form-control form-control-sm" min="1">
</div>
</div>
<div class="mb-2">
<label class="form-label small fw-semibold">Icono</label>
<select id="edit-pr-icono" class="form-select form-select-sm">
<option value="">— Sin icono —</option>
<option value="fas fa-child">👶 Niños (fas fa-child)</option>
<option value="fa-solid fa-person-pregnant">🤰 Embarazada (fa-person-pregnant)</option>
<option value="fas fa-heart">❤ Embarazada alt (fas fa-heart)</option>
<option value="fas fa-person-cane">🦯 Adulto mayor (fas fa-person-cane)</option>
<option value="fas fa-wheelchair">♿ Discapacidad (fas fa-wheelchair)</option>
<option value="fas fa-user">👤 Paciente general (fas fa-user)</option>
<option value="fas fa-vial">🧪 Muestra (fas fa-vial)</option>
<option value="fas fa-heartbeat">💓 Embarazada alt (fas fa-heartbeat)</option>
<option value="fas fa-venus">♀ Mujer (fas fa-venus)</option>
<option value="fas fa-baby">🍼 Bebé (fas fa-baby)</option>
<option value="fas fa-star">⭐ VIP (fas fa-star)</option>
</select>
</div>
<div class="mb-2">
<label class="form-label small fw-semibold">Descripción</label>
<textarea id="edit-pr-descripcion" class="form-control form-control-sm" rows="2" maxlength="200" placeholder="Descripción para el kiosko"></textarea>
<small class="text-muted">Se muestra bajo el nombre en el kiosko de turnos</small>
</div>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" id="edit-pr-activo">
<label class="form-check-label small" for="edit-pr-activo">Activo</label>
</div>
</div>
<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-primary btn-sm" onclick="guardarPrioridad()">Guardar</button>
</div>
</div>
</div>
</div>
<!-- ════════════════════════════════════════
TAB 4 — SESIÓN Y WHATSAPP
════════════════════════════════════════ -->
<?php elseif ($tab === 'sesion'): ?>
<!-- Sesión de hoy -->
<div class="mb-4">
<p class="section-title"><i class="fas fa-calendar-day me-1"></i>Sesión de hoy</p>
<?php if ($sesionHoy): ?>
<div class="d-flex align-items-center gap-3 flex-wrap">
<div>
<span class="session-badge <?= $sesionHoy['fin_at'] ? 'session-closed' : 'session-open' ?>">
<?= $sesionHoy['fin_at'] ? 'Cerrada' : 'Abierta' ?>
</span>
</div>
<div class="small text-muted">
Fecha: <strong><?= htmlspecialchars($sesionHoy['fecha']) ?></strong> &bull;
Turnos: <strong><?= $sesionHoy['total_turnos'] ?></strong>
<?php if ($sesionHoy['inicio_at']): ?>
&bull; Inicio: <strong><?= date('H:i', strtotime($sesionHoy['inicio_at'])) ?></strong>
<?php endif; ?>
<?php if ($sesionHoy['fin_at']): ?>
&bull; Cierre: <strong><?= date('H:i', strtotime($sesionHoy['fin_at'])) ?></strong>
<?php endif; ?>
</div>
<?php if (!$sesionHoy['fin_at']): ?>
<button class="btn btn-danger btn-sm" onclick="cambiarSesion('cerrar',<?= $sesionHoy['id'] ?>)">
<i class="fas fa-lock me-1"></i>Cerrar sesión del día
</button>
<?php else: ?>
<button class="btn btn-success btn-sm" onclick="cambiarSesion('reabrir',<?= $sesionHoy['id'] ?>)">
<i class="fas fa-lock-open me-1"></i>Reabrir sesión
</button>
<?php endif; ?>
</div>
<?php else: ?>
<p class="text-muted">No hay sesión creada para hoy.</p>
<button class="btn btn-success btn-sm" onclick="cambiarSesion('abrir',0)">
<i class="fas fa-play me-1"></i>Abrir sesión de hoy
</button>
<?php endif; ?>
</div>
<!-- Historial de sesiones -->
<div class="mb-4">
<p class="section-title"><i class="fas fa-history me-1"></i>Historial de sesiones (últimas 30)</p>
<?php if (empty($sesiones)): ?>
<p class="text-muted">Sin historial</p>
<?php else: ?>
<div class="table-responsive">
<table class="table table-sm table-hover small">
<thead class="table-light">
<tr>
<th>Fecha</th>
<th>Inicio</th>
<th>Cierre</th>
<th>Abierto por</th>
<th>Estado</th>
</tr>
</thead>
<tbody>
<?php foreach ($sesiones as $s): ?>
<tr>
<td><?= htmlspecialchars($s['fecha']) ?></td>
<td><?= $s['inicio_at'] ? date('H:i', strtotime($s['inicio_at'])) : '—' ?></td>
<td><?= $s['fin_at'] ? date('H:i', strtotime($s['fin_at'])) : '—' ?></td>
<td><?= htmlspecialchars($s['abierto_nombre'] ?? '—') ?></td>
<td>
<span class="session-badge <?= $s['fin_at'] ? 'session-closed' : 'session-open' ?>">
<?= $s['fin_at'] ? 'Cerrada' : 'Abierta' ?>
</span>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<!-- Config WhatsApp turnero -->
<div>
<p class="section-title"><i class="fab fa-whatsapp me-1" style="color:#25d366"></i>Notificaciones WhatsApp</p>
<div class="mb-3 p-3" style="background:#fffbeb;border:1px solid #fde68a;border-radius:10px">
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" id="chk-wa-kiosko"
<?= !empty($cfg['turnero_wa_kiosko_enabled']) ? 'checked' : '' ?>>
<label class="form-check-label" for="chk-wa-kiosko">
<strong>Enviar notificación de turno desde el kiosko</strong>
<small class="d-block text-muted">Cuando un paciente toma turno en el kiosko, se le envía un WhatsApp con la plantilla configurada.</small>
</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="chk-wa-consent"
<?= !empty($cfg['turnero_wa_consent_enabled']) ? 'checked' : '' ?>>
<label class="form-check-label" for="chk-wa-consent">
<strong>Enviar consentimientos por WhatsApp</strong>
<small class="d-block text-muted">Permite enviar consentimientos informados desde recepción por WhatsApp.</small>
</label>
</div>
</div>
<div class="row g-2">
<div class="col-md-6">
<label class="form-label small fw-semibold">Nombre de plantilla Meta aprobada</label>
<input type="text" id="wa-template" class="form-control form-control-sm"
value="<?= htmlspecialchars($cfg['turnero_wa_template'] ?? 'consentimiento_turno') ?>"
placeholder="consentimiento_turno">
<small class="text-muted">Nombre exacto en Meta Business Manager</small>
</div>
<div class="col-md-3">
<label class="form-label small fw-semibold">Código de idioma</label>
<input type="text" id="wa-lang" class="form-control form-control-sm"
value="<?= htmlspecialchars($cfg['turnero_wa_lang'] ?? 'es_CO') ?>"
placeholder="es_CO">
</div>
<div class="col-md-3 d-flex align-items-end">
<button class="btn btn-primary btn-sm w-100" onclick="guardarWhatsApp()">
<i class="fas fa-save me-1"></i>Guardar
</button>
</div>
</div>
<div class="alert alert-info py-2 px-3 mt-3" style="font-size:.82rem">
<i class="fas fa-info-circle me-1"></i>
La plantilla debe estar aprobada en Meta y contener una variable de URL
(<code>{{1}}</code>) para el enlace de firma del consentimiento.
El sistema usará texto plano como fallback si la plantilla falla.
</div>
</div>
<!-- ════════════════════════════════════════
TAB 5 — PANTALLA TV
════════════════════════════════════════ -->
<?php elseif ($tab === 'tv'): ?>
<?php $tvVideo = $cfg['turnero_tv_video'] ?? ''; ?>
<p class="section-title"><i class="fas fa-film me-1"></i>Video de fondo / publicidad</p>
<p class="text-muted" style="font-size:.85rem">El video se reproduce en bucle y sin sonido como fondo en la pantalla TV del turnero. Formatos admitidos: MP4, WebM. Máximo 500 MB.</p>
<!-- Vista previa actual -->
<div id="tv-preview-wrap" class="mb-4" <?= $tvVideo ? '' : 'style="display:none"' ?>>
<p class="fw-semibold small mb-2">Video actual:</p>
<video id="tv-preview" controls muted
style="max-width:480px;width:100%;border-radius:10px;border:1px solid #e2e8f0"
src="<?= htmlspecialchars($tvVideo) ?>"></video>
<div class="mt-2">
<button class="btn btn-outline-danger btn-sm" onclick="borrarTvVideo()">
<i class="fas fa-trash me-1"></i>Quitar video
</button>
</div>
</div>
<!-- Zona de subida -->
<div id="tv-upload-wrap" <?= $tvVideo ? 'style="display:none"' : '' ?>>
<label class="drop-zone" id="tv-drop" for="tv-file-input"
style="display:flex;flex-direction:column;align-items:center;justify-content:center;
gap:.6rem;padding:2.5rem 1rem;border:2px dashed #cbd5e1;border-radius:12px;
cursor:pointer;text-align:center;transition:border-color .2s">
<i class="fas fa-cloud-upload-alt fa-2x text-primary"></i>
<span class="fw-semibold text-primary">Haz clic o arrastra tu video aquí</span>
<span class="text-muted small">MP4 / WebM — máx 500 MB</span>
</label>
<input type="file" id="tv-file-input" accept="video/mp4,video/webm" class="d-none">
</div>
<!-- Barra de progreso -->
<div id="tv-progress-wrap" class="mt-3" style="display:none">
<div class="d-flex align-items-center gap-2 mb-1">
<span class="small fw-semibold" id="tv-progress-lbl">Subiendo…</span>
<span class="small text-muted" id="tv-progress-pct">0%</span>
</div>
<div class="progress" style="height:8px">
<div class="progress-bar progress-bar-striped progress-bar-animated bg-primary"
id="tv-progress-bar" role="progressbar" style="width:0%"></div>
</div>
</div>
<?php endif; ?>
</div><!-- /tab-card -->
</div><!-- /content-wrap -->
<!-- Toast stack -->
<div class="toast-stack" id="toastStack"></div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
/* ═══════════════════════════════════════════════════════════
Configuración Turnero — JS
═══════════════════════════════════════════════════════════ */
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 ────────────────────────────────────────────
function toast(msg, type = 'success') {
const stack = document.getElementById('toastStack');
const el = document.createElement('div');
el.className = `toast align-items-center text-white bg-${type === 'error' ? 'danger' : 'success'} border-0`;
el.setAttribute('role', 'alert');
el.innerHTML = `<div class="d-flex"><div class="toast-body">${msg}</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button></div>`;
stack.appendChild(el);
new bootstrap.Toast(el, { delay: 3500 }).show();
el.addEventListener('hidden.bs.toast', () => el.remove());
}
// ── Copiar URL al portapapeles ───────────────────────────────
function copiarUrl(elId) {
const url = document.getElementById(elId)?.dataset?.url || document.getElementById(elId)?.textContent || '';
navigator.clipboard.writeText(url.trim()).then(() => toast('URL copiada al portapapeles'));
}
function copiarUrlDisplay(lugarId) {
copiarUrl('url-lugar-' + lugarId);
}
// ─────────────────────────────────────────────────────────────
// TAB 1: LUGARES
// ─────────────────────────────────────────────────────────────
async function guardarLugar(tipoOrId = 'muestras', id = null) {
// tipoOrId puede ser 'recepcion', 'muestras', o un número (id al editar)
const esEdicion = typeof tipoOrId === 'number';
if (esEdicion) { id = tipoOrId; tipoOrId = null; }
const prefijo = esEdicion ? 'edit-lu' : (tipoOrId === 'recepcion' ? 'rec' : 'mue');
const nombre = document.getElementById(esEdicion ? 'edit-lu-nombre' : `${prefijo}-nombre`)?.value.trim();
const desc = document.getElementById(esEdicion ? 'edit-lu-desc' : `${prefijo}-desc`)?.value.trim();
const orden = parseInt(document.getElementById(esEdicion ? 'edit-lu-orden' : `${prefijo}-orden`)?.value) || 99;
const activo = esEdicion ? (document.getElementById('edit-lu-activo')?.checked ? 1 : 0) : 1;
const tipo = esEdicion ? (document.getElementById('edit-lu-tipo')?.value || 'muestras') : tipoOrId;
if (!nombre) { toast('El nombre es requerido', 'error'); return; }
// 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;
try {
const res = await fetch(API + 'save_lugar.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body)
});
const json = await res.json();
if (!json.ok) { toast(json.error || 'Error', 'error'); return; }
toast(id ? 'Lugar actualizado' : 'Lugar creado');
if (id) bootstrap.Modal.getInstance(document.getElementById('modalEditarLugar'))?.hide();
setTimeout(() => location.reload(), 600);
} catch (e) { toast('Error de conexión', 'error'); }
}
function editarLugar(id, nombre, desc, activo, orden, tipo = 'muestras') {
document.getElementById('edit-lu-id').value = id;
document.getElementById('edit-lu-tipo').value = tipo;
document.getElementById('edit-lu-nombre').value = nombre;
document.getElementById('edit-lu-desc').value = desc;
document.getElementById('edit-lu-orden').value = orden;
document.getElementById('edit-lu-activo').checked = !!activo;
const titulo = tipo === 'recepcion' ? 'Editar escritorio de recepción' : 'Editar estación de muestras';
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();
}
function guardarEditarLugar() {
guardarLugar(parseInt(document.getElementById('edit-lu-id').value));
}
async function eliminarLugar(id, nombre) {
if (!confirm(`¿Eliminar el lugar "${nombre}"?\nSolo se puede si no tiene turnos asignados.`)) return;
try {
const res = await fetch(API + 'save_lugar.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, _delete: true })
});
const json = await res.json();
if (!json.ok) { toast(json.error || 'Error al eliminar', 'error'); return; }
toast('Lugar eliminado');
setTimeout(() => location.reload(), 600);
} catch (e) { toast('Error de conexión', 'error'); }
}
// ─────────────────────────────────────────────────────────────
// TAB 2: EXÁMENES
// ─────────────────────────────────────────────────────────────
async function guardarExamen(id = null) {
const codigo = (id ? document.getElementById('edit-ex-codigo') : document.getElementById('ex-codigo'))?.value.trim().toUpperCase();
const nombre = (id ? document.getElementById('edit-ex-nombre') : document.getElementById('ex-nombre'))?.value.trim();
const categoria = (id ? document.getElementById('edit-ex-categoria'): document.getElementById('ex-categoria'))?.value.trim();
const formId = parseInt((id ? document.getElementById('edit-ex-form') : document.getElementById('ex-form'))?.value) || null;
const activo = id ? (document.getElementById('edit-ex-activo')?.checked ? 1 : 0) : 1;
if (!codigo || !nombre) { toast('Código y nombre son requeridos', 'error'); return; }
const body = { codigo, nombre, categoria, formulario_id: formId, activo };
if (id) body.id = id;
try {
const res = await fetch(API + 'save_ex_tipo.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body)
});
const json = await res.json();
if (!json.ok) { toast(json.error || 'Error', 'error'); return; }
toast(id ? 'Examen actualizado' : 'Examen creado');
if (id) bootstrap.Modal.getInstance(document.getElementById('modalEditarExamen'))?.hide();
setTimeout(() => location.reload(), 600);
} catch (e) { toast('Error de conexión', 'error'); }
}
function editarExamen(id) {
const row = document.querySelector(`[data-exam-id="${id}"]`);
const tags = row ? row.querySelectorAll('.consent-tag') : [];
// Leer datos de los atributos del servidor (via fetch de datos ya cargados)
fetch(API + 'save_ex_tipo.php?id=' + id)
.then(r => r.json())
.then(json => {
if (!json.ok) return;
const d = json.data;
document.getElementById('edit-ex-id').value = d.id;
document.getElementById('edit-ex-codigo').value = d.codigo;
document.getElementById('edit-ex-nombre').value = d.nombre;
document.getElementById('edit-ex-categoria').value= d.categoria || '';
document.getElementById('edit-ex-form').value = d.formulario_id || '';
document.getElementById('edit-ex-activo').checked = !!d.activo;
new bootstrap.Modal(document.getElementById('modalEditarExamen')).show();
})
.catch(() => toast('Error cargando datos', 'error'));
}
function guardarEditarExamen() {
guardarExamen(parseInt(document.getElementById('edit-ex-id').value));
}
async function eliminarExamen(id, nombre) {
if (!confirm(`¿Eliminar el examen "${nombre}"?`)) return;
try {
const res = await fetch(API + 'save_ex_tipo.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, _delete: true })
});
const json = await res.json();
if (!json.ok) { toast(json.error || 'Error', 'error'); return; }
toast('Examen eliminado');
setTimeout(() => location.reload(), 600);
} catch (e) { toast('Error de conexión', 'error'); }
}
// ─────────────────────────────────────────────────────────────
// TAB 3: PRIORIDADES
// ─────────────────────────────────────────────────────────────
function editarPrioridad(id, codigo, nombre, color, icono, descripcion, orden, activo) {
document.getElementById('edit-pr-id').value = id;
document.getElementById('edit-pr-codigo').value = codigo;
document.getElementById('edit-pr-nombre').value = nombre;
document.getElementById('edit-pr-color').value = color;
document.getElementById('edit-pr-color-text').value = color;
document.getElementById('edit-pr-icono').value = icono || '';
document.getElementById('edit-pr-descripcion').value = descripcion || '';
document.getElementById('edit-pr-orden').value = orden;
document.getElementById('edit-pr-activo').checked = !!activo;
new bootstrap.Modal(document.getElementById('modalEditarPrioridad')).show();
}
// Sincronizar input color ↔ texto hex
document.addEventListener('DOMContentLoaded', () => {
const colorPicker = document.getElementById('edit-pr-color');
const colorText = document.getElementById('edit-pr-color-text');
if (colorPicker && colorText) {
colorPicker.addEventListener('input', () => { colorText.value = colorPicker.value; });
colorText.addEventListener('input', () => {
if (/^#[0-9a-f]{6}$/i.test(colorText.value)) {
colorPicker.value = colorText.value;
}
});
}
});
async function guardarPrioridad() {
const id = parseInt(document.getElementById('edit-pr-id').value);
const nombre = document.getElementById('edit-pr-nombre').value.trim();
const color = document.getElementById('edit-pr-color-text').value.trim() || document.getElementById('edit-pr-color').value;
const icono = document.getElementById('edit-pr-icono').value.trim() || null;
const descripcion = document.getElementById('edit-pr-descripcion').value.trim() || null;
const orden = parseInt(document.getElementById('edit-pr-orden').value) || 1;
const activo = document.getElementById('edit-pr-activo').checked ? 1 : 0;
if (!nombre) { toast('El nombre es requerido', 'error'); return; }
if (!id) { toast('ID no válido', 'error'); return; }
try {
const res = await fetch(API + 'save_prioridad.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, nombre, color, icono, descripcion, orden_peso: orden, activo })
});
const json = await res.json();
if (!json.ok) { toast(json.error || 'Error', 'error'); return; }
toast('Prioridad actualizada');
bootstrap.Modal.getInstance(document.getElementById('modalEditarPrioridad'))?.hide();
setTimeout(() => location.reload(), 600);
} catch (e) { toast('Error de conexión', 'error'); }
}
// ─────────────────────────────────────────────────────────────
// TAB 4: SESIÓN Y WHATSAPP
// ─────────────────────────────────────────────────────────────
async function cambiarSesion(accion, sesionId) {
const msgs = {
cerrar: '¿Confirma el cierre de la sesión del día?',
reabrir: '¿Reabrir la sesión de hoy?',
abrir: '¿Abrir una nueva sesión para hoy?',
};
if (!confirm(msgs[accion] || '¿Confirmar?')) return;
try {
const res = await fetch(API + 'sesion_turno.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ accion, sesion_id: sesionId })
});
const json = await res.json();
if (!json.ok) { toast(json.error || 'Error', 'error'); return; }
toast(json.message || 'Operación completada');
setTimeout(() => location.reload(), 600);
} catch (e) { toast('Error de conexión', 'error'); }
}
// ─────────────────────────────────────────────────────────────
// TAB 5: PANTALLA TV
// ─────────────────────────────────────────────────────────────
(function () {
const inp = document.getElementById('tv-file-input');
const drop = document.getElementById('tv-drop');
if (!inp) return;
// Drag-and-drop visual
drop.addEventListener('dragover', e => { e.preventDefault(); drop.style.borderColor = '#3b82f6'; });
drop.addEventListener('dragleave', () => { drop.style.borderColor = '#cbd5e1'; });
drop.addEventListener('drop', e => {
e.preventDefault(); drop.style.borderColor = '#cbd5e1';
const f = e.dataTransfer.files[0];
if (f) subirTvVideo(f);
});
inp.addEventListener('change', () => { if (inp.files[0]) subirTvVideo(inp.files[0]); });
})();
function subirTvVideo(file) {
const allowedMime = ['video/mp4', 'video/webm', 'video/ogg'];
if (!allowedMime.includes(file.type)) { toast('Solo se permiten MP4 o WebM', 'error'); return; }
if (file.size > 500 * 1024 * 1024) { toast('El video supera el límite de 500 MB', 'error'); return; }
const wrap = document.getElementById('tv-progress-wrap');
const bar = document.getElementById('tv-progress-bar');
const pct = document.getElementById('tv-progress-pct');
const lbl = document.getElementById('tv-progress-lbl');
wrap.style.display = '';
const fd = new FormData();
fd.append('video', file);
const xhr = new XMLHttpRequest();
xhr.open('POST', API + 'save_tv_video.php');
xhr.upload.addEventListener('progress', e => {
if (!e.lengthComputable) return;
const p = Math.round(e.loaded / e.total * 100);
bar.style.width = p + '%';
pct.textContent = p + '%';
});
xhr.addEventListener('load', () => {
wrap.style.display = 'none';
try {
const json = JSON.parse(xhr.responseText);
if (!json.ok) { toast(json.error || 'Error al subir', 'error'); return; }
toast('Video guardado correctamente');
// Mostrar preview y ocultar zona de subida
document.getElementById('tv-preview').src = json.url;
document.getElementById('tv-preview-wrap').style.display = '';
document.getElementById('tv-upload-wrap').style.display = 'none';
} catch(e) { toast('Respuesta inesperada del servidor', 'error'); }
});
xhr.addEventListener('error', () => { wrap.style.display = 'none'; toast('Error de conexión', 'error'); });
lbl.textContent = 'Subiendo ' + file.name + '…';
bar.style.width = '0%';
xhr.send(fd);
}
async function borrarTvVideo() {
if (!confirm('¿Quitar el video de la pantalla TV?')) return;
try {
const res = await fetch(API + 'save_tv_video.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ accion: 'borrar' })
});
const json = await res.json();
if (!json.ok) { toast(json.error || 'Error', 'error'); return; }
toast('Video eliminado');
document.getElementById('tv-preview-wrap').style.display = 'none';
document.getElementById('tv-upload-wrap').style.display = '';
document.getElementById('tv-preview').src = '';
} catch(e) { toast('Error de conexión', 'error'); }
}
async function guardarWhatsApp() {
const template = document.getElementById('wa-template')?.value.trim();
const lang = document.getElementById('wa-lang')?.value.trim();
const waKiosko = document.getElementById('chk-wa-kiosko')?.checked ? 1 : 0;
const waConsent = document.getElementById('chk-wa-consent')?.checked ? 1 : 0;
if (!template) { toast('El nombre de plantilla es requerido', 'error'); return; }
try {
const res = await fetch(API + 'sesion_turno.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ accion: 'config_wa', template, lang, wa_kiosko: waKiosko, wa_consent: waConsent })
});
const json = await res.json();
if (!json.ok) { toast(json.error || 'Error', 'error'); return; }
toast('Configuración WhatsApp guardada');
} catch (e) { toast('Error de conexión', 'error'); }
}
</script>
</body>
</html>