Files
whatsapp/modules/turnero/views/configuracion.php
T
2026-04-18 23:41:01 -05:00

858 lines
46 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
*/
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 ────────────────────────────────
$lugares = $pdo->query(
'SELECT * FROM turnero_lugares ORDER BY sort_order ASC, id ASC'
)->fetchAll(PDO::FETCH_ASSOC);
$prioridades = $pdo->query(
'SELECT * FROM turnero_prioridades ORDER BY orden_peso ASC'
)->fetchAll(PDO::FETCH_ASSOC);
$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);
$formulariosCons = $pdo->query(
"SELECT id, nombre FROM lab_formularios WHERE tipo='consentimiento' AND activo=1 ORDER BY nombre ASC"
)->fetchAll(PDO::FETCH_ASSOC);
// Historial de sesiones (últimas 30)
$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);
// Sesión abierta hoy
$sesionHoy = $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()"
);
$sesionHoy->execute();
$sesionHoy = $sesionHoy->fetch(PDO::FETCH_ASSOC);
// Config WhatsApp del turnero
$cfgRows = $pdo->query("SELECT clave, valor FROM lab_config WHERE clave LIKE 'turnero_%'")->fetchAll(PDO::FETCH_ASSOC);
$cfg = [];
foreach ($cfgRows as $r) { $cfg[$r['clave']] = $r['valor']; }
$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 rel="stylesheet" href="<?= BASE_URL ?>assets/css/bootstrap.min.css">
<link rel="stylesheet" href="<?= BASE_URL ?>assets/css/fontawesome.min.css">
<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">
<ul class="nav nav-tabs" id="cfgTabs">
<li class="nav-item">
<a class="nav-link <?= $tab==='lugares'?'active':'' ?>" href="?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="?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="?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="?tab=sesion">
<i class="fas fa-calendar-day me-1"></i>Sesión y WhatsApp
</a>
</li>
</ul>
<div class="tab-card">
<!-- ════════════════════════════════════════
TAB 1 — LUGARES
════════════════════════════════════════ -->
<?php if ($tab === 'lugares'): ?>
<p class="section-title">Estaciones de servicio configurables</p>
<div id="lista-lugares">
<?php foreach ($lugares as $lu): ?>
<div class="item-row" data-id="<?= $lu['id'] ?>">
<span class="drag-handle text-muted me-1" style="cursor:grab" title="Reordenar">
<i class="fas fa-grip-vertical"></i>
</span>
<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">
<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'] ?>)" 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($lugares)): ?>
<p class="text-muted text-center py-3">No hay lugares configurados</p>
<?php endif; ?>
</div>
<div class="form-add mt-4">
<p class="section-title mb-3"><i class="fas fa-plus me-1"></i>Agregar lugar</p>
<div class="row g-2 align-items-end">
<div class="col-md-4">
<label class="form-label small fw-semibold">Nombre <span class="text-danger">*</span></label>
<input type="text" id="lu-nombre" class="form-control form-control-sm" placeholder="Toma de Muestras 3">
</div>
<div class="col-md-4">
<label class="form-label small fw-semibold">Descripción</label>
<input type="text" id="lu-desc" class="form-control form-control-sm" placeholder="Opcional">
</div>
<div class="col-md-2">
<label class="form-label small fw-semibold">Orden</label>
<input type="number" id="lu-orden" class="form-control form-control-sm" value="<?= count($lugares) + 1 ?>" min="1">
</div>
<div class="col-md-2">
<button class="btn btn-primary btn-sm w-100" onclick="guardarLugar()">
<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">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">
<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>
<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>
<!-- URL panel TV por lugar -->
<div class="mt-4">
<p class="section-title"><i class="fas fa-desktop me-1"></i>URLs de pantalla TV por lugar</p>
<?php foreach ($lugares as $lu): ?>
<div class="mb-2">
<small class="fw-semibold text-muted d-block mb-1"><?= htmlspecialchars($lu['nombre']) ?></small>
<div class="url-row">
<div class="url-box flex-1"
id="url-lugar-<?= $lu['id'] ?>"
data-url="<?= htmlspecialchars(BASE_URL . 'modules/turnero/views/display.php?display=lugar&lugar_id=' . $lu['id']) ?>"
><?= htmlspecialchars(BASE_URL . 'modules/turnero/views/display.php?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 class="mb-2 mt-3">
<small class="fw-semibold text-muted d-block mb-1">Recepción (pantalla general)</small>
<div class="url-row">
<div class="url-box flex-1"
id="url-recepcion"
data-url="<?= htmlspecialchars(BASE_URL . 'modules/turnero/views/display.php?display=recepcion') ?>"
><?= htmlspecialchars(BASE_URL . 'modules/turnero/views/display.php?display=recepcion') ?></div>
<button class="btn btn-outline-secondary btn-sm" onclick="copiarUrl('url-recepcion')">
<i class="fas fa-copy"></i>
</button>
</div>
</div>
<div class="mb-2 mt-1">
<small class="fw-semibold text-muted d-block mb-1">Kiosko de turnos (tablet/pantalla 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>
</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']) ?>',<?= $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="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>Plantilla de consentimiento WhatsApp</p>
<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>
<?php endif; ?>
</div><!-- /tab-card -->
</div><!-- /content-wrap -->
<!-- Toast stack -->
<div class="toast-stack" id="toastStack"></div>
<script src="<?= BASE_URL ?>assets/js/bootstrap.bundle.min.js"></script>
<script>
/* ═══════════════════════════════════════════════════════════
Configuración Turnero — JS
═══════════════════════════════════════════════════════════ */
const API = '<?= BASE_URL ?>modules/turnero/api/';
// ── 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(id = null) {
const nombre = (id ? document.getElementById('edit-lu-nombre') : document.getElementById('lu-nombre'))?.value.trim();
const desc = (id ? document.getElementById('edit-lu-desc') : document.getElementById('lu-desc'))?.value.trim();
const orden = parseInt((id ? document.getElementById('edit-lu-orden') : document.getElementById('lu-orden'))?.value) || 99;
const activo = id ? (document.getElementById('edit-lu-activo')?.checked ? 1 : 0) : 1;
if (!nombre) { toast('El nombre es requerido', 'error'); return; }
const body = { nombre, descripcion: desc, sort_order: orden, activo };
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) {
document.getElementById('edit-lu-id').value = id;
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;
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, 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-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 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, 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'); }
}
async function guardarWhatsApp() {
const template = document.getElementById('wa-template')?.value.trim();
const lang = document.getElementById('wa-lang')?.value.trim();
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 })
});
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>