Files
whatsapp/modules/turnero/views/configuracion.php
Lizandro GuarnizoandClaude Opus 5 ccaf6f22cd Configuración: miniatura cuadrada en la lista de medios del televisor
La miniatura era de 90x60 con recorte, así que el material cuadrado se veía
achatado en Configuración y no coincidía con lo que después salía al aire.
Ahora es cuadrada, igual que la pantalla.

La carga en sí no necesitaba cambios: no valida proporciones ni dimensiones,
solo formato y tamaño, así que un video cuadrado siempre entró bien.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 08:28:19 -05:00

1857 lines
102 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; }
require_once __DIR__ . '/../_acceso.php';
turneroExigirRol(['supervisor']);
$db = Database::getInstance();
$pdo = $db->getConnection();
$_myIp = trim(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR'] ?? '')[0]);
// ── 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 = []; }
$dispositivos = [];
try {
$dispositivos = $pdo->query(
"SELECT d.*, l.nombre AS lugar_nombre
FROM turnero_dispositivos d
LEFT JOIN turnero_lugares l ON l.id = d.lugar_id
ORDER BY d.activo DESC, l.sort_order ASC, d.nombre ASC"
)->fetchAll(PDO::FETCH_ASSOC);
} catch (\Throwable $e) { $_loadErrors[] = 'turnero_dispositivos: ' . $e->getMessage(); }
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(); }
$tvMedia = [];
try {
$tvMedia = $pdo->query(
"SELECT id, tipo, url, orden, duracion_segundos FROM turnero_tv_media ORDER BY orden ASC"
)->fetchAll(PDO::FETCH_ASSOC);
} catch (\Throwable $e) { $_loadErrors[] = 'turnero_tv_media: ' . $e->getMessage(); }
$plantillasAprobadas = [];
$plantillasHabilitadas = [];
try {
$plantillasAprobadas = $pdo->query(
"SELECT id, name, template_name, language_code, body_text
FROM message_templates WHERE status='approved' ORDER BY name ASC"
)->fetchAll(PDO::FETCH_ASSOC);
$pdo->exec("CREATE TABLE IF NOT EXISTS turnero_chat_plantillas (
template_id INT NOT NULL, PRIMARY KEY (template_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$plantillasHabilitadas = array_column(
$pdo->query("SELECT template_id FROM turnero_chat_plantillas")->fetchAll(PDO::FETCH_ASSOC),
'template_id'
);
} catch (\Throwable $e) { $_loadErrors[] = 'plantillas_chat: ' . $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:flex-start; flex-wrap:wrap; gap:6px 8px; padding:10px 0; border-bottom:1px solid #f1f5f9; }
.item-row:last-child { border-bottom:none; }
.item-row .item-name { flex:1; min-width:200px; font-weight:500; color:#1e293b; }
.item-row .item-actions { display:flex; gap:4px; margin-left:auto; flex-shrink:0; }
.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:2px 8px; white-space:normal; word-break:break-word; max-width:260px; }
.plantilla-chk-row { display:flex; align-items:flex-start; gap:.65rem; padding:.6rem .8rem; border:1.5px solid #e2e8f0; border-radius:8px; cursor:pointer; background:#fff; transition:border-color .15s,background .15s; }
.plantilla-chk-row:hover { background:#f8fafc; border-color:#cbd5e1; }
.plantilla-chk-row.is-checked { border-color:#3b82f6; background:#eff6ff; }
.plantilla-chk-row input[type=checkbox] { margin-top:.2rem; flex-shrink:0; accent-color:#3b82f6; }
.plantilla-chk-body { display:flex; flex-direction:column; gap:.1rem; min-width:0; }
.plantilla-chk-name { font-size:.875rem; font-weight:600; color:#1e293b; }
.plantilla-chk-meta { font-size:.75rem; color:#64748b; }
.plantilla-chk-preview { font-size:.75rem; color:#94a3b8; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:480px; }
</style>
</head>
<body>
<div class="page-header">
<a href="<?= BASE_URL ?>erp.php?m=turnero&v=dashboard" 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','<?= $lu['formulario_modo'] ?? 'link' ?>')" 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','<?= $lu['formulario_modo'] ?? 'link' ?>')" 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>
Formularios de consentimiento
</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 class="mt-2">
<label class="form-label small fw-semibold mb-1">Modo de presentación del formulario</label>
<div class="d-flex gap-3">
<div class="form-check">
<input class="form-check-input" type="radio" name="edit-lu-form-modo" id="edit-lu-modo-link" value="link" checked>
<label class="form-check-label small" for="edit-lu-modo-link">
<i class="fas fa-link me-1 text-primary"></i>Enviar por link / WA
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="edit-lu-form-modo" id="edit-lu-modo-embebido" value="embebido">
<label class="form-check-label small" for="edit-lu-modo-embebido">
<i class="fas fa-window-maximize me-1 text-success"></i>Embebido en pantalla
</label>
</div>
</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="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>
<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>
<?php if (!empty($lugaresRec)): ?>
<div class="mb-3">
<small class="fw-semibold text-muted d-block mb-2"><i class="fas fa-user-tie me-1"></i>Gestión — Escritorios de Recepción</small>
<?php foreach ($lugaresRec 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-rec-<?= $lu['id'] ?>"
data-url="<?= htmlspecialchars(BASE_URL . 'erp.php?m=turnero&v=recepcion&desk_id=' . $lu['id']) ?>"
><?= htmlspecialchars(BASE_URL . 'erp.php?m=turnero&v=recepcion&desk_id=' . $lu['id']) ?></div>
<button class="btn btn-outline-secondary btn-sm" onclick="copiarUrl('url-rec-<?= $lu['id'] ?>')"><i class="fas fa-copy"></i></button>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (!empty($lugaresMues)): ?>
<div class="mb-3">
<small class="fw-semibold text-muted d-block mb-2"><i class="fas fa-flask me-1"></i>Gestión — Estaciones de Toma de Muestras</small>
<?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-mue-<?= $lu['id'] ?>"
data-url="<?= htmlspecialchars(BASE_URL . 'erp.php?m=turnero&v=lugar&lugar_id=' . $lu['id']) ?>"
><?= htmlspecialchars(BASE_URL . 'erp.php?m=turnero&v=lugar&lugar_id=' . $lu['id']) ?></div>
<button class="btn btn-outline-secondary btn-sm" onclick="copiarUrl('url-mue-<?= $lu['id'] ?>')"><i class="fas fa-copy"></i></button>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<!-- ── Dispositivos / Tablets ──────────────── -->
<hr class="my-4">
<div class="d-flex align-items-center gap-2 mb-2">
<i class="fas fa-tablet-alt text-secondary"></i>
<p class="section-title mb-0">Dispositivos / Tablets autorizadas</p>
<span class="badge bg-secondary-subtle text-secondary"><?= count($dispositivos) ?></span>
</div>
<p class="text-muted small mb-3">
Cada equipo queda bloqueado a su lugar asignado por token de navegador.
Si el equipo no aparece aquí, verá todos los lugares disponibles.
</p>
<div class="table-responsive mb-3">
<table class="table table-sm table-hover align-middle" style="font-size:.82rem">
<thead class="table-light">
<tr>
<th>Token / ID equipo</th>
<th>Nombre / descripción</th>
<th>Lugar asignado</th>
<th class="text-center">Estado</th>
<th></th>
</tr>
</thead>
<tbody id="tabla-dispositivos">
<?php foreach ($dispositivos as $dv): ?>
<tr data-dv-id="<?= $dv['id'] ?>">
<td>
<?php if (!empty($dv['token'])): ?>
<code title="<?= htmlspecialchars($dv['token']) ?>"><?= htmlspecialchars(substr($dv['token'], 0, 8)) ?>…</code>
<?php else: ?>
<code class="text-muted"><?= htmlspecialchars($dv['ip'] ?: '—') ?></code>
<?php endif; ?>
</td>
<td><?= htmlspecialchars($dv['nombre']) ?></td>
<td><?= htmlspecialchars($dv['lugar_nombre'] ?? '—') ?></td>
<td class="text-center">
<span class="badge <?= $dv['activo'] ? 'bg-success-subtle text-success' : 'bg-secondary-subtle text-secondary' ?>">
<?= $dv['activo'] ? 'Activo' : 'Inactivo' ?>
</span>
</td>
<td class="text-end" style="white-space:nowrap">
<button class="btn-icon text-primary"
onclick="editarDispositivo(<?= $dv['id'] ?>, '<?= addslashes($dv['ip'] ?? '') ?>', '<?= addslashes($dv['nombre']) ?>', <?= (int)$dv['lugar_id'] ?>, <?= (int)$dv['activo'] ?>, '<?= addslashes($dv['token'] ?? '') ?>')"
title="Editar">
<i class="fas fa-pencil-alt"></i>
</button>
<button class="btn-icon text-danger"
onclick="eliminarDispositivo(<?= $dv['id'] ?>, '<?= addslashes($dv['nombre']) ?>')"
title="Eliminar">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($dispositivos)): ?>
<tr><td colspan="5" class="text-muted text-center py-3">No hay dispositivos registrados</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
<div class="alert alert-primary py-2 px-3 mb-3 d-flex align-items-center gap-2 flex-wrap" style="font-size:.85rem">
<i class="fas fa-fingerprint"></i>
Token de <strong>este equipo</strong>: <code id="cfg-my-token" style="font-size:.8rem">cargando…</code>
<button class="btn btn-sm btn-primary ms-auto" onclick="registrarEsteEquipo()">
<i class="fas fa-plus me-1"></i>Registrar este equipo
</button>
</div>
<div class="form-add p-3" style="background:#f8fafc;border-radius:10px;border:1px solid #e2e8f0">
<p class="section-title mb-3"><i class="fas fa-plus me-1"></i>Agregar dispositivo</p>
<input type="hidden" id="dv-token">
<div class="row g-2 align-items-end">
<div class="col-md-4">
<label class="form-label small fw-semibold">Nombre / descripción <span class="text-danger">*</span></label>
<input type="text" id="dv-nombre" class="form-control form-control-sm" placeholder="Recepción 1 — María López">
</div>
<div class="col-md-4">
<label class="form-label small fw-semibold">Lugar asignado <span class="text-danger">*</span></label>
<select id="dv-lugar" class="form-select form-select-sm">
<option value="">— Seleccionar —</option>
<?php foreach ($lugares as $lu): ?>
<option value="<?= $lu['id'] ?>">[<?= $lu['tipo'] === 'recepcion' ? 'Rec' : 'TM' ?>] <?= htmlspecialchars($lu['nombre']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<button class="btn btn-secondary btn-sm w-100" onclick="guardarDispositivo()">
<i class="fas fa-save me-1"></i>Guardar
</button>
</div>
</div>
</div>
<!-- Modal editar dispositivo -->
<div class="modal fade" id="modalEditarDispositivo" 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 dispositivo</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="edit-dv-id">
<input type="hidden" id="edit-dv-token">
<div class="mb-2">
<label class="form-label small fw-semibold">Nombre / descripción</label>
<input type="text" id="edit-dv-nombre" class="form-control form-control-sm">
</div>
<div class="mb-2">
<label class="form-label small fw-semibold">Lugar asignado</label>
<select id="edit-dv-lugar" class="form-select form-select-sm">
<option value="">— Seleccionar —</option>
<?php foreach ($lugares as $lu): ?>
<option value="<?= $lu['id'] ?>">[<?= $lu['tipo'] === 'recepcion' ? 'Rec' : 'TM' ?>] <?= htmlspecialchars($lu['nombre']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="edit-dv-activo" checked>
<label class="form-check-label small" for="edit-dv-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="guardarEditarDispositivo()">Guardar</button>
</div>
</div>
</div>
</div>
<!-- ════════════════════════════════════════
TAB 2 — EXÁMENES Y CONSENTIMIENTOS
════════════════════════════════════════ -->
<?php elseif ($tab === 'examenes'): ?>
<div class="d-flex align-items-center gap-3 mb-3">
<p class="section-title mb-0">Tipos de examen y sus formularios de consentimiento</p>
<div class="ms-auto" style="min-width:220px">
<input type="search" id="exam-buscar" class="form-control form-control-sm"
placeholder="Buscar examen…" autocomplete="off">
</div>
</div>
<?php
// Agrupar por categoría
$categorias = [];
foreach ($examTipos as $et) {
$cat = $et['categoria'] ?: 'Sin categoría';
$categorias[$cat][] = $et;
}
?>
<div id="exam-lista">
<?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; ?>
<span class="item-actions">
<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>
</span>
</div>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
</div><!-- #exam-lista -->
<div id="exam-pager"></div>
<?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">Consentimientos</label>
<div id="ex-forms-wrap" class="border rounded p-2" style="max-height:120px;overflow-y:auto;background:#fffbeb">
<?php foreach ($formulariosCons as $fc): ?>
<div class="form-check mb-1">
<input class="form-check-input ex-form-chk" type="checkbox"
value="<?= $fc['id'] ?>" id="ex-fc-<?= $fc['id'] ?>">
<label class="form-check-label small" for="ex-fc-<?= $fc['id'] ?>">
<?= htmlspecialchars($fc['nombre']) ?>
</label>
</div>
<?php endforeach; ?>
<?php if (empty($formulariosCons)): ?>
<span class="text-muted small">Sin formularios activos</span>
<?php endif; ?>
</div>
</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">Consentimientos</label>
<div id="edit-ex-forms-wrap" class="border rounded p-2" style="max-height:120px;overflow-y:auto;background:#fffbeb">
<?php foreach ($formulariosCons as $fc): ?>
<div class="form-check mb-1">
<input class="form-check-input edit-ex-form-chk" type="checkbox"
value="<?= $fc['id'] ?>" id="edit-ex-fc-<?= $fc['id'] ?>">
<label class="form-check-label small" for="edit-ex-fc-<?= $fc['id'] ?>">
<?= htmlspecialchars($fc['nombre']) ?>
</label>
</div>
<?php endforeach; ?>
<?php if (empty($formulariosCons)): ?>
<span class="text-muted small">Sin formularios activos</span>
<?php endif; ?>
</div>
</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-5">
<label class="form-label small fw-semibold">Plantilla con consentimiento</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">Consulta, toma de muestra, etc. — incluye link de firma</small>
</div>
<div class="col-md-4">
<label class="form-label small fw-semibold">Plantilla muestra pendiente</label>
<input type="text" id="wa-template-muestra" class="form-control form-control-sm"
value="<?= htmlspecialchars($cfg['turnero_wa_template_muestra'] ?? 'turno_muestra_pendiente') ?>"
placeholder="turno_muestra_pendiente">
<small class="text-muted">Solo notifica turno, sin consentimiento</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>
<div class="d-flex justify-content-end mt-2">
<button class="btn btn-primary btn-sm" onclick="guardarWhatsApp()">
<i class="fas fa-save me-1"></i>Guardar configuración
</button>
</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 con consentimiento debe contener una variable de URL
(<code>{{1}}</code>) para el enlace de firma.
El sistema usará texto plano como fallback si la plantilla falla.
</div>
</div>
<!-- Perfil de WhatsApp Business -->
<div class="mt-4">
<p class="section-title"><i class="fab fa-whatsapp me-1" style="color:#25d366"></i>Perfil de WhatsApp Business</p>
<div class="d-flex align-items-center gap-3 mb-3">
<!-- Foto de perfil -->
<div style="position:relative;flex-shrink:0">
<img id="wa-perfil-foto" src="" alt="Foto perfil"
style="width:80px;height:80px;border-radius:50%;object-fit:cover;
border:2px solid #dee2e6;background:#f1f5f9;display:none">
<div id="wa-perfil-foto-placeholder"
style="width:80px;height:80px;border-radius:50%;background:#e2e8f0;
display:flex;align-items:center;justify-content:center;font-size:2rem">
<i class="fab fa-whatsapp" style="color:#25d366"></i>
</div>
<label title="Cambiar foto" style="position:absolute;bottom:0;right:0;
background:#25d366;color:#fff;border-radius:50%;width:26px;height:26px;
display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:.8rem">
<i class="fas fa-camera"></i>
<input type="file" id="wa-foto-input" accept="image/jpeg,image/png"
style="display:none" onchange="subirFotoPerfil(this)">
</label>
</div>
<div class="flex-grow-1">
<div class="fw-semibold" id="wa-perfil-nombre" style="font-size:.95rem">—</div>
<div class="text-muted small" id="wa-perfil-about">—</div>
<button class="btn btn-outline-secondary btn-sm mt-1" onclick="cargarPerfilWA()">
<i class="fas fa-sync-alt me-1"></i>Cargar perfil actual
</button>
</div>
</div>
<div class="row g-2">
<div class="col-12">
<label class="form-label small fw-semibold">Descripción corta (About) <small class="text-muted fw-normal">máx. 139 chars</small></label>
<input type="text" id="wa-about" maxlength="139" class="form-control form-control-sm"
placeholder="Ej: Laboratorio clínico · Lunes a Sábado 7am-5pm">
</div>
<div class="col-12">
<label class="form-label small fw-semibold">Descripción del negocio</label>
<textarea id="wa-description" rows="2" class="form-control form-control-sm"
placeholder="Descripción completa del laboratorio…"></textarea>
</div>
<div class="col-md-6">
<label class="form-label small fw-semibold">Dirección</label>
<input type="text" id="wa-address" class="form-control form-control-sm"
placeholder="Calle 5 #12-34, Cúcuta">
</div>
<div class="col-md-6">
<label class="form-label small fw-semibold">Email</label>
<input type="email" id="wa-email" class="form-control form-control-sm"
placeholder="contacto@laboratorio.com">
</div>
<div class="col-md-6">
<label class="form-label small fw-semibold">Sitio web 1</label>
<input type="url" id="wa-web1" class="form-control form-control-sm"
placeholder="https://www.laboratorio.com">
</div>
<div class="col-md-6">
<label class="form-label small fw-semibold">Sitio web 2 <small class="text-muted fw-normal">(opcional)</small></label>
<input type="url" id="wa-web2" class="form-control form-control-sm"
placeholder="https://instagram.com/laboratorio">
</div>
<div class="col-md-4">
<label class="form-label small fw-semibold">Categoría</label>
<select id="wa-vertical" class="form-select form-select-sm">
<option value="">— sin especificar —</option>
<option value="HEALTH">Salud</option>
<option value="MEDICAL_AND_HEALTH">Médico y Salud</option>
<option value="BEAUTY">Belleza</option>
<option value="EDUCATION">Educación</option>
<option value="OTHER">Otro</option>
</select>
</div>
</div>
<div class="d-flex justify-content-end mt-2">
<button class="btn btn-success btn-sm" onclick="guardarPerfilWA()">
<i class="fas fa-save me-1"></i>Guardar perfil
</button>
</div>
</div>
<!-- Plantillas habilitadas en el chat turnero -->
<div class="mt-4">
<p class="section-title"><i class="fas fa-comment-dots me-1"></i>Plantillas habilitadas en el chat</p>
<p class="text-muted small mb-3">Marca las plantillas aprobadas disponibles en el chat del turnero.</p>
<?php if (empty($plantillasAprobadas)): ?>
<div class="alert alert-warning py-2 px-3" style="font-size:.85rem">
<i class="fas fa-exclamation-triangle me-1"></i>
No hay plantillas aprobadas. Sincroniza primero desde WhatsApp Business.
</div>
<?php else: ?>
<div id="plantillas-chat-list" style="display:grid;gap:.4rem;margin-bottom:.75rem">
<?php foreach ($plantillasAprobadas as $pt):
$checked = in_array((int)$pt['id'], array_map('intval', $plantillasHabilitadas));
?>
<label class="plantilla-chk-row<?= $checked ? ' is-checked' : '' ?>">
<input type="checkbox" value="<?= (int)$pt['id'] ?>" <?= $checked ? 'checked' : '' ?>
onchange="this.closest('label').classList.toggle('is-checked',this.checked)">
<div class="plantilla-chk-body">
<span class="plantilla-chk-name"><?= htmlspecialchars($pt['name']) ?></span>
<span class="plantilla-chk-meta"><?= htmlspecialchars($pt['template_name']) ?> &bull; <?= htmlspecialchars($pt['language_code']) ?></span>
<?php if (!empty($pt['body_text'])): ?>
<span class="plantilla-chk-preview"><?= htmlspecialchars(mb_substr($pt['body_text'], 0, 100)) . (mb_strlen($pt['body_text']) > 100 ? '…' : '') ?></span>
<?php endif; ?>
</div>
</label>
<?php endforeach; ?>
</div>
<div class="d-flex justify-content-end">
<button class="btn btn-primary btn-sm" onclick="guardarPlantillasChat()">
<i class="fas fa-save me-1"></i>Guardar plantillas habilitadas
</button>
</div>
<?php endif; ?>
</div>
<!-- ════════════════════════════════════════
TAB 5 — PANTALLA TV
════════════════════════════════════════ -->
<?php elseif ($tab === 'tv'): ?>
<p class="section-title"><i class="fas fa-film me-1"></i>Playlist de la Pantalla TV</p>
<p class="text-muted" style="font-size:.85rem">Videos e imágenes se reproducen en bucle, uno detrás de otro, sin sonido, como fondo en la pantalla TV del turnero. Formatos: MP4, WebM, JPG, PNG, WEBP — máximo 500 MB por archivo.</p>
<!-- Lista actual -->
<div id="tv-media-list" class="mb-3 d-flex flex-column gap-2">
<?php foreach ($tvMedia as $m): ?>
<div class="tv-media-item d-flex align-items-center gap-3 p-2" data-id="<?= (int)$m['id'] ?>"
style="border:1px solid #e2e8f0;border-radius:10px">
<i class="fas fa-grip-vertical text-muted" style="cursor:grab"></i>
<?php /* Miniatura cuadrada, igual que la pantalla del televisor: con la
antigua de 90x60 el material cuadrado se veía recortado aquí y
no coincidía con lo que después salía al aire. */ ?>
<?php if ($m['tipo'] === 'video'): ?>
<video muted style="width:64px;height:64px;object-fit:cover;border-radius:6px;background:#000" src="<?= htmlspecialchars($m['url']) ?>"></video>
<?php else: ?>
<img style="width:64px;height:64px;object-fit:cover;border-radius:6px;background:#000" src="<?= htmlspecialchars($m['url']) ?>" alt="">
<?php endif; ?>
<div class="flex-grow-1">
<div class="small fw-semibold"><i class="fas fa-<?= $m['tipo']==='video'?'film':'image' ?> me-1"></i><?= $m['tipo']==='video'?'Video':'Imagen' ?></div>
<div class="text-muted small text-truncate" style="max-width:300px"><?= htmlspecialchars(basename($m['url'])) ?></div>
</div>
<?php if ($m['tipo'] === 'imagen'): ?>
<div class="d-flex align-items-center gap-1">
<input type="number" min="1" class="form-control form-control-sm tv-media-dur"
style="width:70px" value="<?= (int)$m['duracion_segundos'] ?>">
<span class="small text-muted">seg</span>
</div>
<?php endif; ?>
<button class="btn btn-outline-danger btn-sm" onclick="borrarTvMedia(<?= (int)$m['id'] ?>)">
<i class="fas fa-trash"></i>
</button>
</div>
<?php endforeach; ?>
<?php if (!$tvMedia): ?>
<div class="text-muted small" id="tv-media-empty">Sin videos ni imágenes en la playlist.</div>
<?php endif; ?>
</div>
<div class="mb-3">
<button class="btn btn-outline-primary btn-sm" onclick="document.getElementById('tv-file-input').click()">
<i class="fas fa-plus me-1"></i>Agregar video o imagen
</button>
<button class="btn btn-primary btn-sm" onclick="guardarOrdenTv()">
<i class="fas fa-save me-1"></i>Guardar orden / duración
</button>
</div>
<input type="file" id="tv-file-input" accept="video/mp4,video/webm,image/jpeg,image/png,image/webp" class="d-none" multiple>
<!-- 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 formModo = esEdicion
? (document.querySelector('input[name="edit-lu-form-modo"]:checked')?.value || 'link')
: 'link';
const body = { nombre, tipo, descripcion: desc, sort_order: orden, activo, formulario_ids: formularioIds, formulario_modo: formModo };
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', formModo = 'link') {
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));
});
// Modo de presentación
const modoEl = document.querySelector(`input[name="edit-lu-form-modo"][value="${formModo}"]`);
if (modoEl) modoEl.checked = true;
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'); }
}
// ─────────────────────────────────────────────────────────────
// DISPOSITIVOS / TABLETS
// ─────────────────────────────────────────────────────────────
// Mostrar token de este equipo en el banner
(function() {
function setTokenDisplay() {
var t = window._turneroDeviceToken || localStorage.getItem('turnero_device_token');
if (!t) return;
var el = document.getElementById('cfg-my-token');
if (el) el.textContent = t.substring(0, 8) + '… (' + t + ')';
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', setTokenDisplay);
else setTokenDisplay();
// Reintentar si el sidebar aún no corrió
setTimeout(setTokenDisplay, 500);
})();
function registrarEsteEquipo() {
var t = window._turneroDeviceToken || localStorage.getItem('turnero_device_token');
if (!t) { toast('Token no disponible aún, espera un momento', 'error'); return; }
document.getElementById('dv-token').value = t;
document.getElementById('dv-nombre').focus();
document.getElementById('dv-nombre').scrollIntoView({ behavior: 'smooth', block: 'center' });
}
async function guardarDispositivo(id = null) {
const pfx = id ? 'edit-dv-' : 'dv-';
const token = document.getElementById(pfx + 'token')?.value.trim() || null;
const nombre = document.getElementById(pfx + 'nombre')?.value.trim();
const lugarId = parseInt(document.getElementById(pfx + 'lugar')?.value);
const activo = id ? (document.getElementById('edit-dv-activo')?.checked ? 1 : 0) : 1;
if (!nombre) { toast('El nombre es requerido', 'error'); return; }
if (!lugarId) { toast('Selecciona un lugar', 'error'); return; }
if (!token) { toast('Haz clic en "Registrar este equipo" primero', 'error'); return; }
try {
const body = { token, nombre, lugar_id: lugarId, activo, ip: '' };
if (id) body.id = id;
const res = await fetch(API + 'save_dispositivo.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 ? 'Dispositivo actualizado' : 'Dispositivo registrado');
bootstrap.Modal.getInstance(document.getElementById('modalEditarDispositivo'))?.hide();
setTimeout(() => location.reload(), 600);
} catch (e) { toast('Error de conexión', 'error'); }
}
function editarDispositivo(id, ip, nombre, lugarId, activo, token) {
document.getElementById('edit-dv-id').value = id;
document.getElementById('edit-dv-token').value = token || '';
document.getElementById('edit-dv-nombre').value = nombre;
document.getElementById('edit-dv-lugar').value = lugarId;
document.getElementById('edit-dv-activo').checked = !!activo;
new bootstrap.Modal(document.getElementById('modalEditarDispositivo')).show();
}
function guardarEditarDispositivo() {
guardarDispositivo(parseInt(document.getElementById('edit-dv-id').value));
}
async function eliminarDispositivo(id, nombre) {
if (!confirm(`¿Eliminar el dispositivo "${nombre}"?`)) return;
try {
const res = await fetch(API + 'save_dispositivo.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('Dispositivo eliminado');
setTimeout(() => location.reload(), 600);
} catch (e) { toast('Error de conexión', 'error'); }
}
// ─────────────────────────────────────────────────────────────
// TAB 2: EXÁMENES
// ─────────────────────────────────────────────────────────────
async function guardarExamen(id = null) {
const prefix = id ? 'edit-ex' : 'ex';
const codigo = document.getElementById(prefix + '-codigo')?.value.trim().toUpperCase();
const nombre = document.getElementById(prefix + '-nombre')?.value.trim();
const categoria = document.getElementById(prefix + '-categoria')?.value.trim();
const activo = id ? (document.getElementById('edit-ex-activo')?.checked ? 1 : 0) : 1;
const chkClass = id ? 'edit-ex-form-chk' : 'ex-form-chk';
const form_ids = [...document.querySelectorAll(`.${chkClass}:checked`)].map(c => parseInt(c.value));
if (!codigo || !nombre) { toast('Código y nombre son requeridos', 'error'); return; }
const body = { codigo, nombre, categoria, form_ids, 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();
// Limpiar checkboxes del formulario de agregar
if (!id) document.querySelectorAll('.ex-form-chk').forEach(c => c.checked = false);
setTimeout(() => location.reload(), 600);
} catch (e) { toast('Error de conexión', 'error'); }
}
function editarExamen(id) {
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-activo').checked = !!d.activo;
// Marcar consentimientos vinculados
document.querySelectorAll('.edit-ex-form-chk').forEach(chk => {
chk.checked = (d.form_ids || []).includes(parseInt(chk.value));
});
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'); }
}
// ─────────────────────────────────────────────────────────────
// BUSCADOR + PAGINADOR — Tab examenes
// ─────────────────────────────────────────────────────────────
(function () {
const PAGE = 15;
let page = 1, q = '';
function update() {
const lista = document.getElementById('exam-lista');
if (!lista) return;
const rows = Array.from(lista.querySelectorAll('.item-row'));
const term = q.toLowerCase();
const matched = rows.filter(r => r.textContent.toLowerCase().includes(term));
rows.forEach(r => r.hidden = true);
const start = (page - 1) * PAGE;
matched.slice(start, start + PAGE).forEach(r => r.hidden = false);
// Ocultar categorías vacías
lista.querySelectorAll('.mb-3').forEach(cat => {
cat.hidden = Array.from(cat.querySelectorAll('.item-row')).every(r => r.hidden);
});
renderPager(matched.length);
}
function renderPager(total) {
const el = document.getElementById('exam-pager');
if (!el) return;
const pages = Math.ceil(total / PAGE);
if (pages <= 1) { el.innerHTML = total ? `<small class="text-muted">${total} resultado${total!==1?'s':''}</small>` : ''; return; }
const prev = `<button class="btn btn-sm btn-outline-secondary" ${page===1?'disabled':''} onclick="examPag(${page-1})"><i class="fas fa-chevron-left"></i></button>`;
const next = `<button class="btn btn-sm btn-outline-secondary" ${page===pages?'disabled':''} onclick="examPag(${page+1})"><i class="fas fa-chevron-right"></i></button>`;
el.innerHTML = `<div class="d-flex align-items-center gap-2 mt-2">
<small class="text-muted">${total} resultados</small>
<div class="ms-auto d-flex align-items-center gap-1">
${prev}
<span class="small px-1">Página ${page} de ${pages}</span>
${next}
</div>
</div>`;
}
window.examPag = function (p) { page = p; update(); };
document.addEventListener('DOMContentLoaded', () => {
const inp = document.getElementById('exam-buscar');
if (inp) inp.addEventListener('input', () => { q = inp.value; page = 1; update(); });
update();
});
})();
// ─────────────────────────────────────────────────────────────
// 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 — playlist de videos e imágenes
// ─────────────────────────────────────────────────────────────
(function () {
const inp = document.getElementById('tv-file-input');
if (!inp) return;
inp.addEventListener('change', () => {
Array.from(inp.files).forEach(subirTvMedia);
inp.value = '';
});
// Reordenar arrastrando (drag & drop nativo, sin librerías)
const list = document.getElementById('tv-media-list');
let dragEl = null;
list.addEventListener('dragstart', e => {
dragEl = e.target.closest('.tv-media-item');
e.dataTransfer.effectAllowed = 'move';
});
list.querySelectorAll('.tv-media-item').forEach(it => it.setAttribute('draggable', 'true'));
list.addEventListener('dragover', e => {
e.preventDefault();
const target = e.target.closest('.tv-media-item');
if (!target || target === dragEl) return;
const rect = target.getBoundingClientRect();
const after = (e.clientY - rect.top) > rect.height / 2;
list.insertBefore(dragEl, after ? target.nextSibling : target);
});
})();
function subirTvMedia(file) {
const allowedMime = ['video/mp4', 'video/webm', 'video/ogg', 'image/jpeg', 'image/png', 'image/webp'];
if (!allowedMime.includes(file.type)) { toast(file.name + ': formato no admitido', 'error'); return; }
if (file.size > 500 * 1024 * 1024) { toast(file.name + ': 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('media', file);
const xhr = new XMLHttpRequest();
xhr.open('POST', API + 'save_tv_media.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(file.name + ' agregado a la playlist');
agregarItemTvMedia(json.item);
} 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);
}
function agregarItemTvMedia(item) {
document.getElementById('tv-media-empty')?.remove();
const list = document.getElementById('tv-media-list');
const div = document.createElement('div');
div.className = 'tv-media-item d-flex align-items-center gap-3 p-2';
div.dataset.id = item.id;
div.setAttribute('draggable', 'true');
div.style.cssText = 'border:1px solid #e2e8f0;border-radius:10px';
const preview = item.tipo === 'video'
? `<video muted style="width:90px;height:60px;object-fit:cover;border-radius:6px" src="${item.url}"></video>`
: `<img style="width:90px;height:60px;object-fit:cover;border-radius:6px" src="${item.url}" alt="">`;
const durInput = item.tipo === 'imagen'
? `<div class="d-flex align-items-center gap-1">
<input type="number" min="1" class="form-control form-control-sm tv-media-dur" style="width:70px" value="${item.duracion_segundos}">
<span class="small text-muted">seg</span>
</div>` : '';
div.innerHTML = `
<i class="fas fa-grip-vertical text-muted" style="cursor:grab"></i>
${preview}
<div class="flex-grow-1">
<div class="small fw-semibold"><i class="fas fa-${item.tipo==='video'?'film':'image'} me-1"></i>${item.tipo==='video'?'Video':'Imagen'}</div>
<div class="text-muted small text-truncate" style="max-width:300px">${item.url.split('/').pop()}</div>
</div>
${durInput}
<button class="btn btn-outline-danger btn-sm" onclick="borrarTvMedia(${item.id})"><i class="fas fa-trash"></i></button>`;
list.appendChild(div);
}
async function borrarTvMedia(id) {
if (!confirm('¿Quitar este elemento de la playlist?')) return;
try {
const res = await fetch(API + 'delete_tv_media.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id })
});
const json = await res.json();
if (!json.ok) { toast(json.error || 'Error', 'error'); return; }
document.querySelector(`.tv-media-item[data-id="${id}"]`)?.remove();
const list = document.getElementById('tv-media-list');
if (!list.querySelector('.tv-media-item')) {
list.innerHTML = '<div class="text-muted small" id="tv-media-empty">Sin videos ni imágenes en la playlist.</div>';
}
toast('Elemento eliminado');
} catch(e) { toast('Error de conexión', 'error'); }
}
async function guardarOrdenTv() {
const items = Array.from(document.querySelectorAll('.tv-media-item')).map(el => ({
id: parseInt(el.dataset.id),
duracion_segundos: parseInt(el.querySelector('.tv-media-dur')?.value) || 8,
}));
if (!items.length) { toast('No hay elementos para guardar', 'error'); return; }
try {
const res = await fetch(API + 'reorder_tv_media.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items })
});
const json = await res.json();
if (!json.ok) { toast(json.error || 'Error', 'error'); return; }
toast('Orden y duración guardados');
} catch(e) { toast('Error de conexión', 'error'); }
}
async function cargarPerfilWA() {
try {
const res = await fetch(API + 'wa_profile.php');
const json = await res.json();
if (!json.ok) { toast(json.error || 'Error al cargar perfil', 'error'); return; }
const p = json.perfil || {};
if (p.profile_picture_url) {
document.getElementById('wa-perfil-foto').src = p.profile_picture_url;
document.getElementById('wa-perfil-foto').style.display = '';
document.getElementById('wa-perfil-foto-placeholder').style.display = 'none';
}
document.getElementById('wa-perfil-about').textContent = p.about || '—';
document.getElementById('wa-about').value = p.about || '';
document.getElementById('wa-description').value = p.description || '';
document.getElementById('wa-address').value = p.address || '';
document.getElementById('wa-email').value = p.email || '';
const webs = p.websites || [];
document.getElementById('wa-web1').value = webs[0] || '';
document.getElementById('wa-web2').value = webs[1] || '';
if (p.vertical) document.getElementById('wa-vertical').value = p.vertical;
toast('Perfil cargado');
} catch (e) { toast('Error de conexión', 'error'); }
}
async function guardarPerfilWA() {
const body = {
accion: 'guardar',
about: document.getElementById('wa-about').value.trim(),
description: document.getElementById('wa-description').value.trim(),
address: document.getElementById('wa-address').value.trim(),
email: document.getElementById('wa-email').value.trim(),
vertical: document.getElementById('wa-vertical').value,
websites: [document.getElementById('wa-web1').value.trim(),
document.getElementById('wa-web2').value.trim()].filter(Boolean),
};
try {
const res = await fetch(API + 'wa_profile.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('Perfil de WhatsApp actualizado');
document.getElementById('wa-perfil-about').textContent = body.about || '—';
} catch (e) { toast('Error de conexión', 'error'); }
}
async function subirFotoPerfil(input) {
if (!input.files[0]) return;
const form = new FormData();
form.append('accion', 'foto');
form.append('foto', input.files[0]);
const btn = input.closest('label');
const orig = btn.innerHTML;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
try {
const res = await fetch(API + 'wa_profile.php', { method: 'POST', body: form });
const json = await res.json();
if (!json.ok) { toast(json.error || 'Error al subir foto', 'error'); return; }
// Preview local
const reader = new FileReader();
reader.onload = e => {
document.getElementById('wa-perfil-foto').src = e.target.result;
document.getElementById('wa-perfil-foto').style.display = '';
document.getElementById('wa-perfil-foto-placeholder').style.display = 'none';
};
reader.readAsDataURL(input.files[0]);
toast('Foto de perfil actualizada');
} catch (e) { toast('Error al subir foto', 'error'); } finally {
btn.innerHTML = orig;
input.value = '';
}
}
async function guardarPlantillasChat() {
const ids = [...document.querySelectorAll('#plantillas-chat-list input[type=checkbox]:checked')]
.map(el => parseInt(el.value, 10));
try {
const res = await fetch(API + 'chat_save_plantillas.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids })
});
const json = await res.json();
if (!json.ok) { toast(json.error || 'Error al guardar', 'error'); return; }
toast('Plantillas guardadas (' + ids.length + ')');
} catch (e) { toast('Error de conexión', 'error'); }
}
async function guardarWhatsApp() {
const template = document.getElementById('wa-template')?.value.trim();
const templateMuestra = document.getElementById('wa-template-muestra')?.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, template_muestra: templateMuestra, 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>
<script src="<?= defined('APP_URL') ? rtrim(APP_URL,'/') : '' ?>/assets/js/lab-sidebar.js"></script>
</body>
</html>