up
This commit is contained in:
@@ -0,0 +1,857 @@
|
||||
<?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> •
|
||||
Turnos: <strong><?= $sesionHoy['total_turnos'] ?></strong>
|
||||
<?php if ($sesionHoy['inicio_at']): ?>
|
||||
• Inicio: <strong><?= date('H:i', strtotime($sesionHoy['inicio_at'])) ?></strong>
|
||||
<?php endif; ?>
|
||||
<?php if ($sesionHoy['fin_at']): ?>
|
||||
• 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>
|
||||
@@ -0,0 +1,374 @@
|
||||
<?php
|
||||
/**
|
||||
* modules/turnero/views/dashboard.php
|
||||
* Panel de resumen diario del Turnero.
|
||||
* Carga datos vía fetch a get_dashboard.php (permite cambiar fecha sin recargar).
|
||||
*/
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
|
||||
if (!isUserLoggedIn()) { header('Location: ' . BASE_URL . 'login.php'); exit; }
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Dashboard 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:14px 24px; display:flex; align-items:center; gap:12px; flex-wrap:wrap;
|
||||
}
|
||||
.page-header h1 { font-size:1.18rem; font-weight:700; color:#1e293b; margin:0; flex:1; }
|
||||
.page-header .back-btn { color:#64748b; text-decoration:none; font-size:.9rem; }
|
||||
.content-wrap { max-width:1100px; margin:24px auto; padding:0 16px 48px; }
|
||||
|
||||
/* ── KPI cards ── */
|
||||
.kpi-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(160px,1fr)); gap:14px; margin-bottom:24px; }
|
||||
.kpi-card {
|
||||
background:#fff; border:1px solid #e2e8f0; border-radius:12px;
|
||||
padding:16px 18px; text-align:center;
|
||||
}
|
||||
.kpi-card .kpi-val { font-size:2rem; font-weight:800; line-height:1.1; }
|
||||
.kpi-card .kpi-lbl { font-size:.72rem; text-transform:uppercase; letter-spacing:.07em; color:#64748b; margin-top:4px; }
|
||||
.kpi-card .kpi-sub { font-size:.75rem; color:#94a3b8; margin-top:2px; }
|
||||
|
||||
/* ── Charts / tables section ── */
|
||||
.section-card { background:#fff; border:1px solid #e2e8f0; border-radius:12px; padding:20px; margin-bottom:20px; }
|
||||
.section-title { font-size:.72rem; font-weight:700; text-transform:uppercase; letter-spacing:.08em; color:#64748b; margin:0 0 14px; }
|
||||
|
||||
/* ── Priority bar ── */
|
||||
.pri-row { display:flex; align-items:center; gap:8px; margin-bottom:8px; }
|
||||
.pri-dot { width:10px; height:10px; border-radius:50%; flex-shrink:0; }
|
||||
.pri-name { width:160px; font-size:.82rem; color:#334155; }
|
||||
.pri-bar-wrap { flex:1; background:#f1f5f9; border-radius:99px; height:10px; overflow:hidden; }
|
||||
.pri-bar-fill { height:100%; border-radius:99px; transition:width .5s; }
|
||||
.pri-nums { font-size:.78rem; color:#64748b; white-space:nowrap; }
|
||||
|
||||
/* ── Lugar cards ── */
|
||||
.lugar-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(200px,1fr)); gap:12px; }
|
||||
.lugar-card { border:1px solid #e2e8f0; border-radius:10px; padding:14px; }
|
||||
.lugar-card .lc-name { font-weight:600; font-size:.9rem; color:#1e293b; margin-bottom:8px; }
|
||||
.lugar-card .lc-row { display:flex; justify-content:space-between; font-size:.8rem; color:#475569; margin-bottom:4px; }
|
||||
.lugar-card .lc-row span:last-child { font-weight:600; color:#0f172a; }
|
||||
|
||||
/* ── Tabla de turnos ── */
|
||||
.estado-badge { font-size:.65rem; padding:2px 7px; border-radius:99px; font-weight:600; white-space:nowrap; }
|
||||
.eb-espera { background:#fef9c3; color:#92400e; }
|
||||
.eb-en_recepcion { background:#dbeafe; color:#1e40af; }
|
||||
.eb-en_espera_lugar { background:#fde68a; color:#92400e; }
|
||||
.eb-en_servicio { background:#dcfce7; color:#166534; }
|
||||
.eb-finalizado { background:#f1f5f9; color:#475569; }
|
||||
.eb-ausente { background:#fee2e2; color:#991b1b; }
|
||||
.eb-cancelado { background:#f1f5f9; color:#9ca3af; }
|
||||
|
||||
/* ── Sesión badge ── */
|
||||
.sesion-open { background:#dcfce7; color:#15803d; }
|
||||
.sesion-closed { background:#f1f5f9; color:#64748b; }
|
||||
|
||||
/* ── Spinner / empty ── */
|
||||
#loadingOverlay { min-height:180px; display:flex; align-items:center; justify-content:center; }
|
||||
.empty-msg { color:#94a3b8; font-size:.9rem; text-align:center; padding:40px 0; }
|
||||
</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-chart-bar me-2 text-primary"></i>Dashboard Turnero</h1>
|
||||
<!-- Selector de fecha -->
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<label class="small fw-semibold text-muted mb-0">Fecha:</label>
|
||||
<input type="date" id="fechaInput" class="form-control form-control-sm" style="width:150px"
|
||||
value="<?= date('Y-m-d') ?>" max="<?= date('Y-m-d') ?>">
|
||||
</div>
|
||||
<button class="btn btn-outline-success btn-sm" id="btnExport" title="Exportar CSV del día">
|
||||
<i class="fas fa-file-csv me-1"></i>Exportar CSV
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="cargarDatos()" title="Refrescar">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
</button>
|
||||
<a href="<?= BASE_URL ?>modules/turnero/views/configuracion.php" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="fas fa-cogs"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="content-wrap">
|
||||
<!-- Estado sesión -->
|
||||
<div id="sesionInfo" class="mb-3 small text-muted"></div>
|
||||
|
||||
<!-- KPIs -->
|
||||
<div class="kpi-grid" id="kpiGrid">
|
||||
<div id="loadingOverlay" class="col-span-full">
|
||||
<div class="text-center text-muted"><i class="fas fa-spinner fa-spin me-2"></i>Cargando...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Consentimientos (solo si hay) -->
|
||||
<div class="section-card" id="sectionConsent" style="display:none">
|
||||
<div class="section-title"><i class="fas fa-file-signature me-1"></i>Consentimientos del día</div>
|
||||
<div id="consentKpis" class="d-flex gap-4 flex-wrap"></div>
|
||||
</div>
|
||||
|
||||
<!-- Desglose por prioridad -->
|
||||
<div class="section-card" id="sectionPrioridad" style="display:none">
|
||||
<div class="section-title"><i class="fas fa-sort-amount-up me-1"></i>Por prioridad</div>
|
||||
<div id="barrasPrioridad"></div>
|
||||
</div>
|
||||
|
||||
<!-- Desglose por lugar -->
|
||||
<div class="section-card" id="sectionLugar" style="display:none">
|
||||
<div class="section-title"><i class="fas fa-map-marker-alt me-1"></i>Por lugar / estación</div>
|
||||
<div class="lugar-grid" id="lugarGrid"></div>
|
||||
</div>
|
||||
|
||||
<!-- Tabla detalle de turnos -->
|
||||
<div class="section-card">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3 flex-wrap gap-2">
|
||||
<div class="section-title mb-0"><i class="fas fa-list me-1"></i>Detalle de turnos</div>
|
||||
<input type="text" id="filtroBusqueda" class="form-control form-control-sm"
|
||||
style="max-width:220px" placeholder="Buscar paciente / código...">
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover small align-middle" id="tablaTurnos">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Código</th>
|
||||
<th>Prioridad</th>
|
||||
<th>Paciente</th>
|
||||
<th>Lugar</th>
|
||||
<th>Estado</th>
|
||||
<th>Espera</th>
|
||||
<th>Servicio</th>
|
||||
<th>Entrada</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tbodyTurnos">
|
||||
<tr><td colspan="8" class="empty-msg">Cargando...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="<?= BASE_URL ?>assets/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
Dashboard Turnero
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
const API = '<?= BASE_URL ?>modules/turnero/api/';
|
||||
const BASE_WA = '<?= BASE_URL ?>';
|
||||
let _turnos = []; // cache para búsqueda local
|
||||
let _autoReloadId = null;
|
||||
|
||||
// ── Entrada ──────────────────────────────────────────────────
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
cargarDatos();
|
||||
document.getElementById('fechaInput').addEventListener('change', cargarDatos);
|
||||
document.getElementById('filtroBusqueda').addEventListener('input', filtrarTabla);
|
||||
document.getElementById('btnExport').addEventListener('click', exportarCSV);
|
||||
|
||||
// Auto-refresh cada 30s solo si la fecha es hoy
|
||||
_autoReloadId = setInterval(() => {
|
||||
const hoy = new Date().toISOString().slice(0, 10);
|
||||
if (document.getElementById('fechaInput').value === hoy) cargarDatos();
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
// ── Carga principal ──────────────────────────────────────────
|
||||
async function cargarDatos() {
|
||||
const fecha = document.getElementById('fechaInput').value;
|
||||
document.getElementById('kpiGrid').innerHTML =
|
||||
'<div id="loadingOverlay"><div class="text-center text-muted"><i class="fas fa-spinner fa-spin me-2"></i>Cargando...</div></div>';
|
||||
document.getElementById('sesionInfo').textContent = '';
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API}get_dashboard.php?fecha=${fecha}`);
|
||||
const json = await res.json();
|
||||
if (!json.ok) { mostrarError(json.error || 'Error'); return; }
|
||||
renderDashboard(json);
|
||||
} catch (e) {
|
||||
mostrarError('Error de conexión');
|
||||
}
|
||||
}
|
||||
|
||||
function mostrarError(msg) {
|
||||
document.getElementById('kpiGrid').innerHTML =
|
||||
`<div class="empty-msg text-danger"><i class="fas fa-exclamation-circle me-1"></i>${escHtml(msg)}</div>`;
|
||||
}
|
||||
|
||||
// ── Render ───────────────────────────────────────────────────
|
||||
function renderDashboard(json) {
|
||||
const { sesion, resumen, por_prioridad, por_lugar, consent_stats, turnos } = json;
|
||||
|
||||
// ── Sesión info ────────────────────────────────────────
|
||||
const si = document.getElementById('sesionInfo');
|
||||
if (!sesion) {
|
||||
si.innerHTML = '<span class="badge bg-secondary-subtle text-secondary">Sin sesión para este día</span>';
|
||||
} else {
|
||||
const cls = sesion.abierta ? 'bg-success-subtle text-success' : 'bg-secondary-subtle text-secondary';
|
||||
const est = sesion.abierta ? 'Abierta' : 'Cerrada';
|
||||
si.innerHTML = `<span class="badge ${cls} me-2">${est}</span>
|
||||
<span>Inicio: <strong>${sesion.inicio || '—'}</strong></span>
|
||||
${sesion.fin ? ` • Cierre: <strong>${sesion.fin}</strong>` : ''}`;
|
||||
}
|
||||
|
||||
// ── KPIs ───────────────────────────────────────────────
|
||||
const total = parseInt(resumen.total || 0);
|
||||
const atendidos = parseInt(resumen.atendidos || 0);
|
||||
const enEspera = parseInt(resumen.en_espera || 0);
|
||||
const ausentes = parseInt(resumen.ausentes || 0);
|
||||
const cancelados= parseInt(resumen.cancelados || 0);
|
||||
const tEspera = resumen.tiempo_espera_promedio_min;
|
||||
const tServicio = resumen.tiempo_servicio_promedio_min;
|
||||
const pct = total > 0 ? Math.round(atendidos / total * 100) : 0;
|
||||
|
||||
document.getElementById('kpiGrid').innerHTML = `
|
||||
${kpiCard(total, '📋 Total turnos', '')}
|
||||
${kpiCard(atendidos,'✅ Atendidos', pct + '%')}
|
||||
${kpiCard(enEspera, '⏳ En espera', '')}
|
||||
${kpiCard(ausentes, '🚫 Ausentes', '')}
|
||||
${kpiCard(cancelados,'❌ Cancelados', '')}
|
||||
${kpiCard(tEspera !== null ? tEspera + ' min' : '—', '⏱ Espera prom.', 'hasta recepción')}
|
||||
${kpiCard(tServicio !== null ? tServicio + ' min' : '—', '⏱ Servicio prom.', 'en lugar')}
|
||||
`;
|
||||
|
||||
// ── Consentimientos ────────────────────────────────────
|
||||
const totalConsent = Object.values(consent_stats).reduce((a,b)=>a+(+b),0);
|
||||
const secCons = document.getElementById('sectionConsent');
|
||||
if (totalConsent > 0) {
|
||||
secCons.style.display = '';
|
||||
const labels = { pendiente:'Pendientes', enviado:'Enviados', visto:'Vistos', firmado:'Firmados', rechazado:'Rechazados' };
|
||||
const colors = { pendiente:'#fef9c3', enviado:'#dbeafe', visto:'#e0f2fe', firmado:'#dcfce7', rechazado:'#fee2e2' };
|
||||
const tcolors= { pendiente:'#92400e', enviado:'#1e40af', visto:'#0369a1', firmado:'#166534', rechazado:'#991b1b' };
|
||||
document.getElementById('consentKpis').innerHTML =
|
||||
Object.entries(consent_stats).map(([est, cnt]) =>
|
||||
`<div style="text-align:center">
|
||||
<div style="font-size:1.5rem;font-weight:800;color:${tcolors[est]||'#0f172a'}">${cnt}</div>
|
||||
<div style="font-size:.7rem;color:#64748b;text-transform:uppercase;letter-spacing:.05em">${labels[est]||est}</div>
|
||||
</div>`
|
||||
).join('');
|
||||
} else {
|
||||
secCons.style.display = 'none';
|
||||
}
|
||||
|
||||
// ── Barras por prioridad ───────────────────────────────
|
||||
const secPri = document.getElementById('sectionPrioridad');
|
||||
if (por_prioridad.length) {
|
||||
secPri.style.display = '';
|
||||
const maxTotal = Math.max(...por_prioridad.map(p => +p.total), 1);
|
||||
document.getElementById('barrasPrioridad').innerHTML = por_prioridad.map(p => {
|
||||
const pct2 = Math.round(+p.total / maxTotal * 100);
|
||||
return `<div class="pri-row">
|
||||
<div class="pri-dot" style="background:${p.color}"></div>
|
||||
<div class="pri-name">${escHtml(p.codigo + ' – ' + p.nombre)}</div>
|
||||
<div class="pri-bar-wrap">
|
||||
<div class="pri-bar-fill" style="width:${pct2}%;background:${p.color}"></div>
|
||||
</div>
|
||||
<div class="pri-nums">${p.total} (✅${p.atendidos} ⏳${p.pendientes} 🚫${p.ausentes})</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
} else {
|
||||
secPri.style.display = 'none';
|
||||
}
|
||||
|
||||
// ── Tarjetas por lugar ─────────────────────────────────
|
||||
const secLug = document.getElementById('sectionLugar');
|
||||
if (por_lugar.length) {
|
||||
secLug.style.display = '';
|
||||
document.getElementById('lugarGrid').innerHTML = por_lugar.map(l => `
|
||||
<div class="lugar-card">
|
||||
<div class="lc-name"><i class="fas fa-map-marker-alt text-primary me-1"></i>${escHtml(l.lugar)}</div>
|
||||
<div class="lc-row"><span>Total</span><span>${l.total}</span></div>
|
||||
<div class="lc-row"><span>Atendidos</span><span>${l.atendidos}</span></div>
|
||||
<div class="lc-row"><span>En espera</span><span>${l.en_espera}</span></div>
|
||||
<div class="lc-row"><span>T. servicio prom.</span><span>${l.tiempo_servicio_promedio_min !== null ? l.tiempo_servicio_promedio_min + ' min' : '—'}</span></div>
|
||||
</div>`).join('');
|
||||
} else {
|
||||
secLug.style.display = 'none';
|
||||
}
|
||||
|
||||
// ── Tabla de turnos ────────────────────────────────────
|
||||
_turnos = turnos;
|
||||
renderTabla(turnos);
|
||||
}
|
||||
|
||||
function kpiCard(val, lbl, sub) {
|
||||
return `<div class="kpi-card">
|
||||
<div class="kpi-val">${val !== null && val !== undefined ? escHtml(String(val)) : '—'}</div>
|
||||
<div class="kpi-lbl">${lbl}</div>
|
||||
${sub ? `<div class="kpi-sub">${escHtml(sub)}</div>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── Tabla detalle ─────────────────────────────────────────────
|
||||
function renderTabla(turnos) {
|
||||
const tbody = document.getElementById('tbodyTurnos');
|
||||
if (!turnos.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="8" class="empty-msg">Sin turnos para este día</td></tr>';
|
||||
return;
|
||||
}
|
||||
const mapEstado = {
|
||||
espera: 'eb-espera',
|
||||
en_recepcion: 'eb-en_recepcion',
|
||||
en_espera_lugar: 'eb-en_espera_lugar',
|
||||
en_servicio: 'eb-en_servicio',
|
||||
finalizado: 'eb-finalizado',
|
||||
ausente: 'eb-ausente',
|
||||
cancelado: 'eb-cancelado',
|
||||
};
|
||||
const labelEstado = {
|
||||
espera: 'Espera', en_recepcion: 'Recepción', en_espera_lugar: 'Esp. lugar',
|
||||
en_servicio: 'En servicio', finalizado: 'Finalizado', ausente: 'Ausente', cancelado: 'Cancelado',
|
||||
};
|
||||
tbody.innerHTML = turnos.map(t => {
|
||||
const nombrePac = escHtml(t.paciente_bd || t.paciente_nombre || '—');
|
||||
const lugar = escHtml(t.lugar_nombre || '—');
|
||||
const estadoCls = mapEstado[t.estado] || 'eb-finalizado';
|
||||
const estadoLbl = labelEstado[t.estado] || t.estado;
|
||||
const hora = t.creado_at ? new Date(t.creado_at.replace(' ', 'T')).toLocaleTimeString('es-CO', {hour:'2-digit',minute:'2-digit'}) : '—';
|
||||
const espMin = t.espera_min !== null ? t.espera_min + ' m' : '—';
|
||||
const srvMin = t.servicio_min !== null ? t.servicio_min + ' m' : '—';
|
||||
return `<tr>
|
||||
<td><span class="fw-bold" style="color:${escHtml(t.prioridad_color || '#333')}">${escHtml(t.codigo)}</span></td>
|
||||
<td><span class="estado-badge" style="background:${escHtml(t.prioridad_color+'22'||'#eee')};color:${escHtml(t.prioridad_color||'#333')}">${escHtml(t.prioridad_codigo)}</span></td>
|
||||
<td>${nombrePac}</td>
|
||||
<td>${lugar}</td>
|
||||
<td><span class="estado-badge ${estadoCls}">${estadoLbl}</span></td>
|
||||
<td>${espMin}</td>
|
||||
<td>${srvMin}</td>
|
||||
<td>${hora}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function filtrarTabla() {
|
||||
const q = document.getElementById('filtroBusqueda').value.toLowerCase().trim();
|
||||
if (!q) { renderTabla(_turnos); return; }
|
||||
renderTabla(_turnos.filter(t =>
|
||||
(t.codigo || '').toLowerCase().includes(q) ||
|
||||
(t.paciente_bd || '').toLowerCase().includes(q) ||
|
||||
(t.paciente_nombre || '').toLowerCase().includes(q)
|
||||
));
|
||||
}
|
||||
|
||||
// ── Exportar CSV ──────────────────────────────────────────────
|
||||
function exportarCSV() {
|
||||
const fecha = document.getElementById('fechaInput').value;
|
||||
window.location.href = `${API}export_csv.php?fecha=${fecha}`;
|
||||
}
|
||||
|
||||
// ── Helper ────────────────────────────────────────────────────
|
||||
function escHtml(str) {
|
||||
const d = document.createElement('div');
|
||||
d.appendChild(document.createTextNode(String(str ?? '')));
|
||||
return d.innerHTML;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,425 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Pantalla de Turnos</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>
|
||||
/* ── Base ─────────────────────────────────────────── */
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
html, body {
|
||||
margin: 0; padding: 0;
|
||||
height: 100%; width: 100%;
|
||||
overflow: hidden;
|
||||
background: #0a0f1e;
|
||||
color: #f0f4ff;
|
||||
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
/* ── Layout ───────────────────────────────────────── */
|
||||
.display-grid {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
height: 100vh;
|
||||
gap: 0;
|
||||
}
|
||||
.display-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: .75rem 2rem;
|
||||
background: #0f172a;
|
||||
border-bottom: 1px solid #1e293b;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.display-header .titulo {
|
||||
font-size: clamp(1rem, 2.5vw, 1.5rem);
|
||||
font-weight: 700; color: #94a3b8;
|
||||
display: flex; align-items: center; gap: .6rem;
|
||||
}
|
||||
.display-header .reloj {
|
||||
font-size: clamp(1.2rem, 3vw, 1.8rem);
|
||||
font-weight: 800; color: #e2e8f0;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.display-header .estado-chip {
|
||||
font-size: .8rem; padding: .25rem .75rem; border-radius: 20px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.estado-chip.conectado { background: #14532d; color: #86efac; }
|
||||
.estado-chip.conectando { background: #422006; color: #fdba74; }
|
||||
.estado-chip.error { background: #450a0a; color: #fca5a5; }
|
||||
|
||||
/* ── Panel central ────────────────────────────────── */
|
||||
.display-body {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 340px;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.display-body { grid-template-columns: 1fr; }
|
||||
.cola-panel { display: none !important; }
|
||||
}
|
||||
|
||||
/* ── Turno activo ─────────────────────────────────── */
|
||||
.turno-activo {
|
||||
display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center;
|
||||
padding: 2rem; position: relative;
|
||||
overflow: hidden;
|
||||
background: radial-gradient(ellipse at center, #111827 0%, #0a0f1e 70%);
|
||||
}
|
||||
.turno-activo .etiqueta {
|
||||
font-size: clamp(.9rem, 2vw, 1.2rem);
|
||||
color: #64748b; font-weight: 600;
|
||||
text-transform: uppercase; letter-spacing: 3px;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
.turno-activo .codigo-grande {
|
||||
font-size: clamp(5rem, 22vw, 16rem);
|
||||
font-weight: 900; line-height: 1;
|
||||
letter-spacing: -4px;
|
||||
transition: color .4s;
|
||||
}
|
||||
.turno-activo .nombre-pac {
|
||||
font-size: clamp(1rem, 3vw, 2rem);
|
||||
font-weight: 600; color: #cbd5e1;
|
||||
margin-top: .5rem; text-align: center;
|
||||
}
|
||||
.turno-activo .prio-badge {
|
||||
display: inline-flex; align-items: center; gap: .5rem;
|
||||
padding: .4rem 1.2rem; border-radius: 30px;
|
||||
font-size: clamp(.8rem, 1.8vw, 1.15rem); font-weight: 700;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.turno-activo .sin-turno {
|
||||
font-size: clamp(1.5rem, 4vw, 3rem);
|
||||
color: #334155; text-align: center;
|
||||
}
|
||||
|
||||
/* Anillo de animación al llamar turno */
|
||||
@keyframes ping-ring {
|
||||
0% { transform: scale(.8); opacity: .9; }
|
||||
100% { transform: scale(2.5); opacity: 0; }
|
||||
}
|
||||
.ring-anim {
|
||||
position: absolute; inset: 0;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
pointer-events: none; z-index: 0;
|
||||
}
|
||||
.ring-anim::before {
|
||||
content: '';
|
||||
width: 35vmin; height: 35vmin;
|
||||
border-radius: 50%;
|
||||
background: transparent;
|
||||
border: 4px solid currentColor;
|
||||
opacity: 0;
|
||||
}
|
||||
.ring-anim.animar::before {
|
||||
animation: ping-ring .7s ease-out 1;
|
||||
}
|
||||
.turno-activo > *:not(.ring-anim) { position: relative; z-index: 1; }
|
||||
|
||||
/* ── Panel de cola ────────────────────────────────── */
|
||||
.cola-panel {
|
||||
background: #0f172a;
|
||||
border-left: 1px solid #1e293b;
|
||||
display: flex; flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.cola-panel-header {
|
||||
padding: 1rem 1.2rem .6rem;
|
||||
font-size: .85rem; font-weight: 700;
|
||||
color: #64748b; text-transform: uppercase; letter-spacing: 2px;
|
||||
border-bottom: 1px solid #1e293b;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.cola-lista {
|
||||
flex: 1; overflow-y: auto;
|
||||
padding: .5rem;
|
||||
scrollbar-width: thin; scrollbar-color: #1e293b transparent;
|
||||
}
|
||||
.cola-item {
|
||||
display: flex; align-items: center; gap: .75rem;
|
||||
padding: .6rem .8rem; border-radius: 10px;
|
||||
margin-bottom: .35rem;
|
||||
background: #1e293b;
|
||||
transition: background .2s;
|
||||
}
|
||||
.cola-item:first-child { background: #1e3a5f; }
|
||||
.cola-item .cod-badge {
|
||||
font-size: 1.2rem; font-weight: 900;
|
||||
min-width: 52px; text-align: center;
|
||||
padding: .2rem .4rem; border-radius: 8px;
|
||||
}
|
||||
.cola-item .pac-info { flex: 1; min-width: 0; }
|
||||
.cola-item .pac-nombre {
|
||||
font-size: .85rem; color: #e2e8f0; font-weight: 600;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.cola-item .pac-hora { font-size: .72rem; color: #475569; }
|
||||
.cola-vacia { color: #334155; text-align: center; padding: 3rem 1rem; font-size: .95rem; }
|
||||
|
||||
/* ── Stats (footer) ──────────────────────────────── */
|
||||
.display-footer {
|
||||
display: flex; justify-content: center; gap: 2.5rem; flex-wrap: wrap;
|
||||
padding: .65rem 2rem;
|
||||
background: #0f172a;
|
||||
border-top: 1px solid #1e293b;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.stat-item { text-align: center; }
|
||||
.stat-item .val { font-size: 1.3rem; font-weight: 800; color: #e2e8f0; }
|
||||
.stat-item .lbl { font-size: .7rem; color: #64748b; text-transform: uppercase; letter-spacing: 1px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="display-grid">
|
||||
|
||||
<!-- ── Cabecera ─────────────────────────────────────── -->
|
||||
<header class="display-header">
|
||||
<div class="titulo">
|
||||
<i class="fas fa-ticket-alt"></i>
|
||||
<span id="lbl-area">Sistema de Turnos</span>
|
||||
</div>
|
||||
<span id="chip-estado" class="estado-chip conectando">
|
||||
<i class="fas fa-circle-notch fa-spin me-1"></i>Conectando…
|
||||
</span>
|
||||
<div class="reloj" id="reloj">--:--:--</div>
|
||||
</header>
|
||||
|
||||
<!-- ── Cuerpo ────────────────────────────────────────── -->
|
||||
<div class="display-body">
|
||||
|
||||
<!-- Turno activo -->
|
||||
<div class="turno-activo" id="zona-activo">
|
||||
<div class="ring-anim" id="ring" style="color:#3b82f6"></div>
|
||||
<div class="etiqueta">Turno en atención</div>
|
||||
<div class="codigo-grande" id="codigo-activo" style="color:#3b82f6">—</div>
|
||||
<div class="nombre-pac" id="nombre-activo"></div>
|
||||
<div class="prio-badge" id="prio-activo" style="background:#1e293b;color:#94a3b8">
|
||||
<i class="fas fa-ticket-alt"></i> Esperando turno
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lista de cola -->
|
||||
<div class="cola-panel">
|
||||
<div class="cola-panel-header">
|
||||
<i class="fas fa-list-ol me-1"></i>EN ESPERA
|
||||
</div>
|
||||
<div class="cola-lista" id="cola-lista">
|
||||
<div class="cola-vacia">
|
||||
<i class="fas fa-hourglass-start fa-2x mb-2 d-block"></i>
|
||||
Cola vacía
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ── Footer stats ──────────────────────────────────── -->
|
||||
<footer class="display-footer">
|
||||
<div class="stat-item">
|
||||
<div class="val" id="stat-espera">—</div>
|
||||
<div class="lbl">En espera</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="val" id="stat-atendidos">—</div>
|
||||
<div class="lbl">Atendidos hoy</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="val" id="stat-total">—</div>
|
||||
<div class="lbl">Total del día</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="val" id="stat-tiempo">—</div>
|
||||
<div class="lbl">Tiempo prom.</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ── Configuración desde URL ───────────────────────────────────
|
||||
const params = new URLSearchParams(location.search);
|
||||
const area = params.get('display') || 'recepcion';
|
||||
const lugarId = params.get('lugar_id') || '';
|
||||
const BASE_API = '../api/';
|
||||
|
||||
// Etiqueta de área
|
||||
document.getElementById('lbl-area').textContent =
|
||||
area === 'lugar' && lugarId ? `Lugar #${lugarId} — Turnos` : 'Recepción — Turnos';
|
||||
|
||||
// ── Reloj ─────────────────────────────────────────────────────
|
||||
function actualizarReloj() {
|
||||
document.getElementById('reloj').textContent =
|
||||
new Date().toLocaleTimeString('es-CO', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||
}
|
||||
actualizarReloj();
|
||||
setInterval(actualizarReloj, 1000);
|
||||
|
||||
// ── Audio (Web Audio API — sin archivos externos) ─────────────
|
||||
let audioCtx = null;
|
||||
function playBeep() {
|
||||
try {
|
||||
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
const osc = audioCtx.createOscillator();
|
||||
const gain = audioCtx.createGain();
|
||||
osc.connect(gain);
|
||||
gain.connect(audioCtx.destination);
|
||||
osc.type = 'sine';
|
||||
osc.frequency.setValueAtTime(880, audioCtx.currentTime);
|
||||
osc.frequency.setValueAtTime(660, audioCtx.currentTime + 0.12);
|
||||
gain.gain.setValueAtTime(0.25, audioCtx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.0001, audioCtx.currentTime + 0.5);
|
||||
osc.start(audioCtx.currentTime);
|
||||
osc.stop(audioCtx.currentTime + 0.5);
|
||||
} catch (_) { /* Sin audio si el contexto no está disponible */ }
|
||||
}
|
||||
// Desbloquear AudioContext ante primer tap/clic (requerido por navegadores)
|
||||
document.addEventListener('click', () => {
|
||||
if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
|
||||
}, { once: true });
|
||||
|
||||
// ── Estado previo ─────────────────────────────────────────────
|
||||
let lastCodigoActivo = null;
|
||||
|
||||
// ── Render ────────────────────────────────────────────────────
|
||||
function renderSnapshot(snap) {
|
||||
const activo = snap.activo;
|
||||
const cola = snap.cola || [];
|
||||
const stats = snap.stats || {};
|
||||
|
||||
// Turno activo
|
||||
const elCodigo = document.getElementById('codigo-activo');
|
||||
const elNombre = document.getElementById('nombre-activo');
|
||||
const elPrio = document.getElementById('prio-activo');
|
||||
const elRing = document.getElementById('ring');
|
||||
|
||||
if (activo) {
|
||||
const color = activo.prioridad_color || '#3b82f6';
|
||||
|
||||
// ¿Cambió el turno? → animar + sonido
|
||||
if (activo.codigo !== lastCodigoActivo) {
|
||||
playBeep();
|
||||
elRing.style.color = color;
|
||||
elRing.classList.remove('animar');
|
||||
// Forzar reflow para reiniciar animación
|
||||
void elRing.offsetWidth;
|
||||
elRing.classList.add('animar');
|
||||
lastCodigoActivo = activo.codigo;
|
||||
}
|
||||
|
||||
elCodigo.textContent = activo.codigo;
|
||||
elCodigo.style.color = color;
|
||||
elNombre.textContent = activo.paciente_nombre || '';
|
||||
elPrio.style.background = color + '33'; // 20% opacity
|
||||
elPrio.style.color = color;
|
||||
elPrio.innerHTML = `<i class="fas fa-ticket-alt"></i> ${activo.prioridad_codigo} — ${activo.prioridad_nombre}`;
|
||||
} else {
|
||||
elCodigo.textContent = '—';
|
||||
elCodigo.style.color = '#334155';
|
||||
elNombre.textContent = '';
|
||||
elPrio.style.background = '#1e293b';
|
||||
elPrio.style.color = '#94a3b8';
|
||||
elPrio.innerHTML = '<i class="fas fa-ticket-alt"></i> Esperando turno';
|
||||
lastCodigoActivo = null;
|
||||
}
|
||||
|
||||
// Lista de cola (excluir el activo si está ahí)
|
||||
const colaEspera = cola.filter(t =>
|
||||
t.estado === 'espera' || t.estado === 'en_espera_lugar'
|
||||
);
|
||||
const elLista = document.getElementById('cola-lista');
|
||||
|
||||
if (colaEspera.length === 0) {
|
||||
elLista.innerHTML = `
|
||||
<div class="cola-vacia">
|
||||
<i class="fas fa-hourglass-start fa-2x mb-2 d-block"></i>
|
||||
Cola vacía
|
||||
</div>`;
|
||||
} else {
|
||||
elLista.innerHTML = colaEspera.map((t, idx) => {
|
||||
const hora = t.creado_at ? new Date(t.creado_at).toLocaleTimeString('es-CO',
|
||||
{ hour: '2-digit', minute: '2-digit' }) : '';
|
||||
return `
|
||||
<div class="cola-item">
|
||||
<span class="cod-badge" style="background:${t.prioridad_color}22;color:${t.prioridad_color}">${t.codigo}</span>
|
||||
<div class="pac-info">
|
||||
<div class="pac-nombre">${escHtml(t.paciente_nombre || 'Paciente')}</div>
|
||||
<div class="pac-hora">${t.prioridad_nombre} • ${hora}</div>
|
||||
</div>
|
||||
${idx === 0 ? '<i class="fas fa-arrow-right" style="color:#3b82f6;font-size:.8rem"></i>' : ''}
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Stats
|
||||
const enEspera = parseInt(stats.en_espera || 0) + parseInt(stats.en_espera_lugar || 0);
|
||||
document.getElementById('stat-espera').textContent = enEspera;
|
||||
document.getElementById('stat-atendidos').textContent = stats.finalizados || 0;
|
||||
document.getElementById('stat-total').textContent = stats.total || 0;
|
||||
document.getElementById('stat-tiempo').textContent = stats.tiempo_promedio_atencion
|
||||
? stats.tiempo_promedio_atencion.substring(0, 5) : '—';
|
||||
}
|
||||
|
||||
function escHtml(str) {
|
||||
const d = document.createElement('div');
|
||||
d.appendChild(document.createTextNode(str));
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
// ── Carga inicial ─────────────────────────────────────────────
|
||||
async function cargarInicial() {
|
||||
const url = new URL(BASE_API + 'get_cola.php', location.href);
|
||||
url.searchParams.set('area', area);
|
||||
if (lugarId) url.searchParams.set('lugar_id', lugarId);
|
||||
|
||||
try {
|
||||
const res = await fetch(url.toString());
|
||||
const json = await res.json();
|
||||
if (json.ok) renderSnapshot(json.data);
|
||||
} catch (_) { /* SSE tomará el relevo */ }
|
||||
}
|
||||
|
||||
// ── SSE ───────────────────────────────────────────────────────
|
||||
const chipEstado = document.getElementById('chip-estado');
|
||||
let esSource = null;
|
||||
let reconnectDelay = 2000;
|
||||
|
||||
function conectarSSE() {
|
||||
const sseUrl = new URL(BASE_API + 'sse_turno.php', location.href);
|
||||
sseUrl.searchParams.set('area', area);
|
||||
if (lugarId) sseUrl.searchParams.set('lugar_id', lugarId);
|
||||
|
||||
esSource = new EventSource(sseUrl.toString());
|
||||
|
||||
esSource.addEventListener('open', () => {
|
||||
reconnectDelay = 2000;
|
||||
chipEstado.className = 'estado-chip conectado';
|
||||
chipEstado.innerHTML = '<i class="fas fa-circle me-1" style="font-size:.5rem"></i>En vivo';
|
||||
});
|
||||
|
||||
esSource.addEventListener('cola_update', (e) => {
|
||||
try {
|
||||
const snap = JSON.parse(e.data);
|
||||
renderSnapshot(snap);
|
||||
} catch (_) {}
|
||||
});
|
||||
|
||||
esSource.addEventListener('error', () => {
|
||||
chipEstado.className = 'estado-chip error';
|
||||
chipEstado.innerHTML = '<i class="fas fa-exclamation-circle me-1"></i>Reconectando…';
|
||||
esSource.close();
|
||||
setTimeout(conectarSSE, reconnectDelay);
|
||||
reconnectDelay = Math.min(reconnectDelay * 2, 30000);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Inicio ────────────────────────────────────────────────────
|
||||
cargarInicial();
|
||||
conectarSSE();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,394 @@
|
||||
<?php
|
||||
/**
|
||||
* Kiosko de turnos — pantalla táctil pública
|
||||
* Ruta: /modules/turnero/views/kiosko.php
|
||||
* Sin login requerido. NUNCA expone datos de otros pacientes.
|
||||
*/
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
|
||||
// Cargar prioridades activas desde BD
|
||||
$prioridades = [];
|
||||
try {
|
||||
$pdo = Database::getInstance()->getConnection();
|
||||
$stmt = $pdo->query(
|
||||
"SELECT id, codigo, nombre, color, icono, descripcion, orden_peso
|
||||
FROM turnero_prioridades
|
||||
WHERE activo = 1
|
||||
ORDER BY orden_peso ASC"
|
||||
);
|
||||
$prioridades = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (\Throwable $e) {
|
||||
// Fallback con prioridades por defecto si la BD no está disponible
|
||||
$prioridades = [
|
||||
['codigo'=>'A','nombre'=>'Niños', 'color'=>'#ef4444','icono'=>'fas fa-child', 'descripcion'=>'Pacientes menores de edad'],
|
||||
['codigo'=>'B','nombre'=>'Embarazadas', 'color'=>'#f97316','icono'=>'fas fa-heart', 'descripcion'=>'Mujeres en estado de embarazo'],
|
||||
['codigo'=>'C','nombre'=>'Adulto mayor', 'color'=>'#eab308','icono'=>'fas fa-person-cane', 'descripcion'=>'Mayores de 60 años'],
|
||||
['codigo'=>'D','nombre'=>'Discapacidad', 'color'=>'#8b5cf6','icono'=>'fas fa-wheelchair', 'descripcion'=>'Personas con discapacidad'],
|
||||
['codigo'=>'E','nombre'=>'Paciente general', 'color'=>'#3b82f6','icono'=>'fas fa-user', 'descripcion'=>'Atención general'],
|
||||
['codigo'=>'F','nombre'=>'Muestra pendiente', 'color'=>'#6b7280','icono'=>'fas fa-vial', 'descripcion'=>'Entrega de muestra tomada'],
|
||||
];
|
||||
}
|
||||
|
||||
// Iconos por defecto por código (si la BD no tiene el campo icono populado)
|
||||
$iconosDefecto = [
|
||||
'A' => 'fas fa-child',
|
||||
'B' => 'fas fa-heart',
|
||||
'C' => 'fas fa-person-cane',
|
||||
'D' => 'fas fa-wheelchair',
|
||||
'E' => 'fas fa-user',
|
||||
'F' => 'fas fa-vial',
|
||||
];
|
||||
foreach ($prioridades as &$p) {
|
||||
if (empty($p['icono'])) {
|
||||
$p['icono'] = $iconosDefecto[$p['codigo']] ?? 'fas fa-ticket-alt';
|
||||
}
|
||||
}
|
||||
unset($p);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>Kiosko de Turnos</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>
|
||||
/* ── Reset fullscreen ── */
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
html, body {
|
||||
margin: 0; padding: 0;
|
||||
height: 100%; width: 100%;
|
||||
overflow: hidden;
|
||||
background: #0f172a;
|
||||
color: #f8fafc;
|
||||
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||
touch-action: manipulation;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
/* ── Pantalla / paso ── */
|
||||
.screen {
|
||||
position: fixed; inset: 0;
|
||||
display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center;
|
||||
padding: 2rem;
|
||||
transition: opacity .35s ease, transform .35s ease;
|
||||
}
|
||||
.screen.hidden { opacity: 0; pointer-events: none; transform: scale(.97); }
|
||||
.screen.visible { opacity: 1; pointer-events: all; transform: scale(1); }
|
||||
|
||||
/* ── Logo / cabecera ── */
|
||||
.kiosko-header { text-align: center; margin-bottom: 2.5rem; }
|
||||
.kiosko-header h1 { font-size: clamp(1.6rem, 4vw, 3rem); font-weight: 700; letter-spacing: -.5px; }
|
||||
.kiosko-header p { font-size: clamp(.9rem, 2vw, 1.2rem); color: #94a3b8; margin: 0; }
|
||||
|
||||
/* ── Botones de prioridad ── */
|
||||
.prioridad-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
max-width: 900px;
|
||||
}
|
||||
.btn-prioridad {
|
||||
border: none; border-radius: 16px;
|
||||
padding: 1.6rem 1.2rem;
|
||||
cursor: pointer;
|
||||
display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center;
|
||||
gap: .6rem;
|
||||
transition: transform .12s, box-shadow .12s, filter .12s;
|
||||
color: #fff; font-weight: 600;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,.35);
|
||||
-webkit-user-select: none; user-select: none;
|
||||
}
|
||||
.btn-prioridad:active { transform: scale(.95); filter: brightness(.88); }
|
||||
.btn-prioridad .letra { font-size: clamp(2rem, 6vw, 4rem); line-height: 1; font-weight: 800; }
|
||||
.btn-prioridad .nombre { font-size: clamp(.85rem, 2vw, 1.1rem); text-align: center; line-height: 1.2; }
|
||||
.btn-prioridad .desc { font-size: clamp(.7rem, 1.5vw, .85rem); opacity: .8; text-align: center; }
|
||||
.btn-prioridad i { font-size: clamp(1.4rem, 3.5vw, 2.2rem); }
|
||||
|
||||
/* ── Formulario opcional ── */
|
||||
.form-kiosko { width: 100%; max-width: 520px; }
|
||||
.form-kiosko label { color: #cbd5e1; font-size: 1rem; margin-bottom: .35rem; }
|
||||
.form-kiosko .form-control {
|
||||
background: #1e293b; border: 1.5px solid #334155;
|
||||
color: #f8fafc; border-radius: 12px;
|
||||
padding: .8rem 1rem; font-size: 1.1rem;
|
||||
}
|
||||
.form-kiosko .form-control:focus {
|
||||
background: #1e293b; border-color: #60a5fa;
|
||||
box-shadow: 0 0 0 3px rgba(96,165,250,.2); color: #f8fafc;
|
||||
}
|
||||
.form-kiosko .hint { color: #64748b; font-size: .85rem; margin-top: .4rem; }
|
||||
.btn-kiosko-main {
|
||||
border: none; border-radius: 14px;
|
||||
padding: 1rem 2.5rem; font-size: 1.15rem; font-weight: 700;
|
||||
cursor: pointer; color: #fff;
|
||||
background: linear-gradient(135deg, #3b82f6, #6366f1);
|
||||
box-shadow: 0 4px 16px rgba(99,102,241,.4);
|
||||
transition: transform .12s, box-shadow .12s;
|
||||
width: 100%; margin-top: 1rem;
|
||||
}
|
||||
.btn-kiosko-main:active { transform: scale(.97); }
|
||||
.btn-kiosko-back {
|
||||
background: transparent; border: 1.5px solid #475569;
|
||||
color: #94a3b8; border-radius: 12px; padding: .7rem 1.5rem;
|
||||
font-size: .95rem; cursor: pointer; margin-top: .6rem; width: 100%;
|
||||
transition: background .12s;
|
||||
}
|
||||
.btn-kiosko-back:active { background: #1e293b; }
|
||||
|
||||
/* ── Pantalla de ticket ── */
|
||||
.ticket-box {
|
||||
background: #1e293b; border-radius: 24px;
|
||||
padding: 2.5rem 3rem; text-align: center;
|
||||
box-shadow: 0 8px 40px rgba(0,0,0,.5);
|
||||
max-width: 480px; width: 100%;
|
||||
}
|
||||
.ticket-codigo {
|
||||
font-size: clamp(4rem, 18vw, 9rem);
|
||||
font-weight: 900; line-height: 1;
|
||||
letter-spacing: -2px;
|
||||
}
|
||||
.ticket-subtitle { color: #94a3b8; font-size: 1rem; margin-top: .5rem; }
|
||||
.ticket-prioridad { font-size: 1.05rem; margin-top: .8rem; }
|
||||
.ticket-numero { font-size: 2rem; font-weight: 700; color: #e2e8f0; }
|
||||
.ticket-instruc {
|
||||
margin-top: 1.5rem; color: #64748b; font-size: .9rem; line-height: 1.5;
|
||||
}
|
||||
.btn-nuevo {
|
||||
margin-top: 2rem; border: none; border-radius: 14px;
|
||||
padding: .85rem 2rem; font-size: 1rem; font-weight: 600;
|
||||
cursor: pointer; color: #fff;
|
||||
background: #475569;
|
||||
transition: background .15s;
|
||||
}
|
||||
.btn-nuevo:hover { background: #64748b; }
|
||||
|
||||
/* ── Spinner ── */
|
||||
.spinner-overlay {
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(15,23,42,.75);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
z-index: 999; opacity: 0; pointer-events: none;
|
||||
transition: opacity .2s;
|
||||
}
|
||||
.spinner-overlay.active { opacity: 1; pointer-events: all; }
|
||||
.spinner { width: 56px; height: 56px; border: 5px solid #334155;
|
||||
border-top-color: #60a5fa; border-radius: 50%; animation: spin .8s linear infinite; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* ── Error toast ── */
|
||||
.toast-error {
|
||||
position: fixed; bottom: 2rem; left: 50%; transform: translateX(-50%);
|
||||
background: #ef4444; color: #fff; padding: .8rem 1.6rem;
|
||||
border-radius: 12px; font-size: .95rem; font-weight: 600;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,.4);
|
||||
opacity: 0; pointer-events: none; transition: opacity .25s;
|
||||
z-index: 1000; white-space: nowrap;
|
||||
}
|
||||
.toast-error.show { opacity: 1; }
|
||||
|
||||
/* ── Responsive: pantalla grande (TV táctil) ── */
|
||||
@media (min-height: 800px) {
|
||||
.prioridad-grid { gap: 1.4rem; }
|
||||
.btn-prioridad { padding: 2rem 1.5rem; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ══ PASO 1: Selección de prioridad ══════════════════════════ -->
|
||||
<div id="screen-prio" class="screen visible">
|
||||
<div class="kiosko-header">
|
||||
<i class="fas fa-ticket-alt fa-2x mb-3" style="color:#60a5fa"></i>
|
||||
<h1>Solicitar Turno</h1>
|
||||
<p>Seleccione su tipo de atención</p>
|
||||
</div>
|
||||
|
||||
<div class="prioridad-grid">
|
||||
<?php foreach ($prioridades as $prio): ?>
|
||||
<button
|
||||
class="btn-prioridad"
|
||||
style="background: <?= htmlspecialchars($prio['color']) ?>;"
|
||||
data-codigo="<?= htmlspecialchars($prio['codigo']) ?>"
|
||||
data-nombre="<?= htmlspecialchars($prio['nombre']) ?>"
|
||||
data-color="<?= htmlspecialchars($prio['color']) ?>"
|
||||
onclick="seleccionarPrioridad(this)"
|
||||
>
|
||||
<i class="<?= htmlspecialchars($prio['icono']) ?>"></i>
|
||||
<span class="letra"><?= htmlspecialchars($prio['codigo']) ?></span>
|
||||
<span class="nombre"><?= htmlspecialchars($prio['nombre']) ?></span>
|
||||
<?php if (!empty($prio['descripcion'])): ?>
|
||||
<span class="desc"><?= htmlspecialchars($prio['descripcion']) ?></span>
|
||||
<?php endif; ?>
|
||||
</button>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ PASO 2: Datos opcionales ════════════════════════════════ -->
|
||||
<div id="screen-datos" class="screen hidden">
|
||||
<div class="kiosko-header">
|
||||
<div id="badge-prio" style="display:inline-block; padding:.5rem 1.4rem; border-radius:40px; font-size:1.2rem; font-weight:700; margin-bottom:1rem;"></div>
|
||||
<h1>Datos de contacto</h1>
|
||||
<p>Opcional — para notificaciones por WhatsApp</p>
|
||||
</div>
|
||||
|
||||
<div class="form-kiosko">
|
||||
<div class="mb-4">
|
||||
<label for="inp-nombre">Nombre completo</label>
|
||||
<input type="text" id="inp-nombre" class="form-control" placeholder="Ej: Juan Pérez" maxlength="120" autocomplete="off">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="inp-cel">Celular (WhatsApp)</label>
|
||||
<input type="tel" id="inp-cel" class="form-control" placeholder="Ej: 3001234567" maxlength="20" autocomplete="off" inputmode="numeric">
|
||||
<div class="hint">Incluya código de país si es diferente a Colombia (+57)</div>
|
||||
</div>
|
||||
<button class="btn-kiosko-main" onclick="confirmarTurno()">
|
||||
<i class="fas fa-ticket-alt me-2"></i>Obtener mi turno
|
||||
</button>
|
||||
<button class="btn-kiosko-back" onclick="volverPrioridades()">
|
||||
<i class="fas fa-arrow-left me-1"></i>Cambiar tipo
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ PASO 3: Ticket asignado ══════════════════════════════════ -->
|
||||
<div id="screen-ticket" class="screen hidden">
|
||||
<div class="ticket-box">
|
||||
<div class="ticket-subtitle">Su número de turno es</div>
|
||||
<div id="tick-codigo" class="ticket-codigo">—</div>
|
||||
<div id="tick-prioridad" class="ticket-prioridad"></div>
|
||||
<div class="ticket-subtitle mt-2">Posición en cola</div>
|
||||
<div id="tick-posicion" class="ticket-numero">—</div>
|
||||
<div class="ticket-instruc">
|
||||
Por favor espere ser llamado.<br>
|
||||
Recuerde traer su documento de identidad y la orden médica.
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-nuevo" onclick="reiniciar()">
|
||||
<i class="fas fa-plus me-1"></i>Nuevo turno
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ══ Spinner de carga ════════════════════════════════════════ -->
|
||||
<div class="spinner-overlay" id="spinner">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
|
||||
<!-- ══ Toast de error ════════════════════════════════════════ -->
|
||||
<div class="toast-error" id="toast-error"></div>
|
||||
|
||||
<script>
|
||||
// ── Estado local ──────────────────────────────────────────
|
||||
let prioCodigo = '';
|
||||
let prioNombre = '';
|
||||
let prioColor = '';
|
||||
|
||||
const API_URL = '../api/create_turno.php';
|
||||
|
||||
// ── Navegación entre pasos ────────────────────────────────
|
||||
function mostrar(id) {
|
||||
document.querySelectorAll('.screen').forEach(s => {
|
||||
s.classList.toggle('visible', s.id === id);
|
||||
s.classList.toggle('hidden', s.id !== id);
|
||||
});
|
||||
}
|
||||
|
||||
function seleccionarPrioridad(btn) {
|
||||
prioCodigo = btn.dataset.codigo;
|
||||
prioNombre = btn.dataset.nombre;
|
||||
prioColor = btn.dataset.color;
|
||||
|
||||
document.getElementById('badge-prio').textContent = prioCodigo + ' — ' + prioNombre;
|
||||
document.getElementById('badge-prio').style.background = prioColor;
|
||||
|
||||
mostrar('screen-datos');
|
||||
document.getElementById('inp-nombre').focus();
|
||||
}
|
||||
|
||||
function volverPrioridades() {
|
||||
mostrar('screen-prio');
|
||||
}
|
||||
|
||||
// ── Crear turno ───────────────────────────────────────────
|
||||
async function confirmarTurno() {
|
||||
const nombre = document.getElementById('inp-nombre').value.trim();
|
||||
const cel = document.getElementById('inp-cel').value.trim();
|
||||
|
||||
// Validación mínima de celular
|
||||
if (cel && !/^\+?\d{7,15}$/.test(cel.replace(/\s/g, ''))) {
|
||||
mostrarError('Ingrese un número de celular válido (solo dígitos).');
|
||||
return;
|
||||
}
|
||||
|
||||
setSpinner(true);
|
||||
|
||||
try {
|
||||
const res = await fetch(API_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
prioridad_codigo: prioCodigo,
|
||||
paciente_nombre: nombre || null,
|
||||
paciente_cel: cel || null,
|
||||
}),
|
||||
});
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if (!json.ok) {
|
||||
throw new Error(json.error ?? 'Error al crear turno');
|
||||
}
|
||||
|
||||
const t = json.data.turno;
|
||||
mostrarTicket(t);
|
||||
|
||||
} catch (err) {
|
||||
mostrarError(err.message);
|
||||
} finally {
|
||||
setSpinner(false);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Mostrar ticket ────────────────────────────────────────
|
||||
function mostrarTicket(t) {
|
||||
document.getElementById('tick-codigo').textContent = t.codigo;
|
||||
document.getElementById('tick-codigo').style.color = prioColor;
|
||||
document.getElementById('tick-prioridad').innerHTML =
|
||||
'<span style="background:' + prioColor + ';padding:.3rem .9rem;border-radius:20px;font-weight:700;">'
|
||||
+ prioCodigo + ' — ' + prioNombre + '</span>';
|
||||
document.getElementById('tick-posicion').textContent = '#' + t.posicion_cola;
|
||||
mostrar('screen-ticket');
|
||||
|
||||
// Auto-reinicio tras 30 s para liberar el kiosko
|
||||
clearTimeout(window._reinicioTimer);
|
||||
window._reinicioTimer = setTimeout(reiniciar, 30000);
|
||||
}
|
||||
|
||||
// ── Reinicio ──────────────────────────────────────────────
|
||||
function reiniciar() {
|
||||
clearTimeout(window._reinicioTimer);
|
||||
document.getElementById('inp-nombre').value = '';
|
||||
document.getElementById('inp-cel').value = '';
|
||||
prioCodigo = prioNombre = prioColor = '';
|
||||
mostrar('screen-prio');
|
||||
}
|
||||
|
||||
// ── Utilidades ────────────────────────────────────────────
|
||||
function setSpinner(on) {
|
||||
document.getElementById('spinner').classList.toggle('active', on);
|
||||
}
|
||||
|
||||
function mostrarError(msg) {
|
||||
const el = document.getElementById('toast-error');
|
||||
el.textContent = msg;
|
||||
el.classList.add('show');
|
||||
setTimeout(() => el.classList.remove('show'), 4000);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,760 @@
|
||||
<?php
|
||||
/**
|
||||
* Puesto Lugar / Estación de Servicio — Módulo Turnero
|
||||
* Genérica: sirve para Toma de Muestras 1, 2, Rayos X, etc.
|
||||
* Requiere login + módulo turnero.
|
||||
*
|
||||
* URL: /modules/turnero/views/lugar.php?lugar_id=X
|
||||
* Si no se pasa lugar_id, muestra un selector al entrar.
|
||||
*/
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
if (!isUserLoggedIn()) {
|
||||
header('Location: ' . BASE_URL . 'login.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Cargar lista de lugares activos
|
||||
try {
|
||||
$pdo = Database::getInstance()->getConnection();
|
||||
$lugares = $pdo->query(
|
||||
"SELECT id, nombre, descripcion FROM turnero_lugares WHERE activo = 1 ORDER BY sort_order ASC"
|
||||
)->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (\Throwable) {
|
||||
$lugares = [];
|
||||
}
|
||||
|
||||
$lugarIdParam = isset($_GET['lugar_id']) ? (int)$_GET['lugar_id'] : 0;
|
||||
|
||||
// Resolver nombre del lugar para el título
|
||||
$lugarNombre = 'Estación de Servicio';
|
||||
foreach ($lugares as $l) {
|
||||
if ((int)$l['id'] === $lugarIdParam) {
|
||||
$lugarNombre = $l['nombre'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$adminNombre = $_SESSION['admin_user']['full_name'] ?? $_SESSION['admin_user']['username'] ?? 'Operador';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= htmlspecialchars($lugarNombre) ?> — 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">
|
||||
<link href="<?= BASE_URL ?>assets/css/styles.css?v=12" rel="stylesheet">
|
||||
<style>
|
||||
/* ── Layout ── */
|
||||
.lugar-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 300px 1fr;
|
||||
height: calc(100vh - 52px);
|
||||
overflow: hidden;
|
||||
}
|
||||
@media (max-width: 860px) {
|
||||
.lugar-layout { grid-template-columns: 1fr; }
|
||||
.lugar-cola { max-height: 240px; }
|
||||
}
|
||||
|
||||
/* ── Columna cola ── */
|
||||
.lugar-cola {
|
||||
background: #f8fafc;
|
||||
border-right: 1px solid #e2e8f0;
|
||||
display: flex; flex-direction: column; overflow: hidden;
|
||||
}
|
||||
.lugar-cola-hdr {
|
||||
padding: .85rem 1rem .5rem;
|
||||
background: #fff; border-bottom: 1px solid #e2e8f0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.cola-items { flex: 1; overflow-y: auto; padding: .4rem; }
|
||||
.cola-card {
|
||||
background: #fff; border: 1px solid #e2e8f0; border-radius: 10px;
|
||||
padding: .55rem .8rem; margin-bottom: .35rem;
|
||||
display: flex; align-items: center; gap: .55rem;
|
||||
}
|
||||
.cola-card.activo { border-color: #6366f1; box-shadow: 0 0 0 2px #c7d2fe; }
|
||||
.prio-dot {
|
||||
width: 34px; height: 34px; border-radius: 50%; flex-shrink: 0;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 1rem; font-weight: 800; color: #fff;
|
||||
}
|
||||
.turno-info .cod { font-size: .98rem; font-weight: 700; }
|
||||
.turno-info .pac { font-size: .75rem; color: #64748b;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.cola-vacia { text-align: center; padding: 2.5rem 1rem; color: #94a3b8; font-size: .88rem; }
|
||||
|
||||
/* ── Columna ficha ── */
|
||||
.lugar-ficha { overflow-y: auto; padding: 1.2rem 1.5rem; background: #fff; }
|
||||
.ficha-placeholder {
|
||||
height: 100%; display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center;
|
||||
color: #94a3b8; text-align: center;
|
||||
}
|
||||
|
||||
/* ── Secciones ── */
|
||||
.ficha-sec {
|
||||
background: #f8fafc; border: 1px solid #e2e8f0;
|
||||
border-radius: 12px; padding: 1.1rem 1.2rem; margin-bottom: 1rem;
|
||||
}
|
||||
.ficha-sec h6 {
|
||||
font-size: .75rem; text-transform: uppercase; letter-spacing: .08em;
|
||||
color: #475569; margin-bottom: .75rem;
|
||||
}
|
||||
|
||||
/* ── Datos paciente ── */
|
||||
.pac-dato { display: flex; gap: .5rem; align-items: baseline; margin-bottom: .3rem; }
|
||||
.pac-dato .lbl { font-size: .75rem; color: #94a3b8; min-width: 80px; }
|
||||
.pac-dato .val { font-size: .88rem; color: #1e293b; font-weight: 500; }
|
||||
|
||||
/* ── Exámenes ── */
|
||||
.exam-pill {
|
||||
display: inline-flex; align-items: center; gap: .3rem;
|
||||
background: #eff6ff; color: #1d4ed8; border-radius: 20px;
|
||||
padding: .2rem .65rem; font-size: .78rem; font-weight: 600;
|
||||
margin: .15rem;
|
||||
}
|
||||
|
||||
/* ── Consentimientos ── */
|
||||
.consent-row {
|
||||
display: flex; align-items: center; gap: .6rem;
|
||||
padding: .5rem .65rem; border-radius: 8px; margin-bottom: .35rem;
|
||||
font-size: .85rem;
|
||||
}
|
||||
.consent-row.firmado { background: #f0fdf4; color: #166534; }
|
||||
.consent-row.rechazado { background: #f1f5f9; color: #475569; }
|
||||
.consent-row.enviado { background: #eff6ff; color: #1e40af; }
|
||||
.consent-row.pendiente { background: #fffbeb; color: #92400e; }
|
||||
.consent-row.visto { background: #faf5ff; color: #6b21a8; }
|
||||
.consent-row .nom-form { flex: 1; }
|
||||
.consent-row .acciones-consent { display: flex; gap: .4rem; }
|
||||
|
||||
/* ── Barra de acciones ── */
|
||||
.ficha-acciones {
|
||||
position: sticky; bottom: 0;
|
||||
background: #fff; border-top: 1px solid #e2e8f0;
|
||||
padding: .85rem 0 .2rem; margin-top: 1rem;
|
||||
display: flex; gap: .5rem; flex-wrap: wrap; align-items: center;
|
||||
}
|
||||
|
||||
/* ── Modal de firma ── */
|
||||
.modal-firma-backdrop {
|
||||
position: fixed; inset: 0; background: rgba(0,0,0,.55);
|
||||
z-index: 1050; display: none; align-items: center; justify-content: center;
|
||||
}
|
||||
.modal-firma-backdrop.open { display: flex; }
|
||||
.modal-firma-box {
|
||||
background: #fff; border-radius: 16px;
|
||||
width: min(96vw, 800px); height: min(90vh, 700px);
|
||||
display: flex; flex-direction: column;
|
||||
overflow: hidden; box-shadow: 0 20px 60px rgba(0,0,0,.35);
|
||||
}
|
||||
.modal-firma-hdr {
|
||||
padding: .75rem 1rem; border-bottom: 1px solid #e2e8f0;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.modal-firma-box iframe {
|
||||
flex: 1; border: none; width: 100%;
|
||||
}
|
||||
|
||||
/* ── Selector de lugar (pantalla inicial) ── */
|
||||
.selector-lugar-overlay {
|
||||
position: fixed; inset: 0; background: #f1f5f9;
|
||||
z-index: 200; display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.selector-lugar-box {
|
||||
background: #fff; border-radius: 20px; padding: 2.5rem;
|
||||
width: min(90vw, 480px); box-shadow: 0 8px 40px rgba(0,0,0,.12); text-align: center;
|
||||
}
|
||||
.selector-lugar-box h2 { font-size: 1.4rem; font-weight: 700; margin-bottom: 1.5rem; }
|
||||
|
||||
/* ── Indicador de bloqueo ── */
|
||||
.bloqueo-banner {
|
||||
background: #fef2f2; border: 1px solid #fca5a5; border-radius: 10px;
|
||||
padding: .65rem 1rem; font-size: .85rem; color: #991b1b;
|
||||
display: flex; align-items: center; gap: .5rem; margin-bottom: .75rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<?php
|
||||
$SIDEBAR_TITLE = 'Turnero';
|
||||
$SIDEBAR_ICON = 'fas fa-ticket-alt';
|
||||
require_once __DIR__ . '/../../../shared/components/sidebar.php';
|
||||
?>
|
||||
|
||||
<!-- ══ Selector inicial de lugar (si no viene en URL) ════════ -->
|
||||
<div id="overlay-selector" class="selector-lugar-overlay" <?= $lugarIdParam ? 'style="display:none"' : '' ?>>
|
||||
<div class="selector-lugar-box">
|
||||
<i class="fas fa-map-marker-alt fa-2x text-primary mb-3"></i>
|
||||
<h2>Seleccione su estación</h2>
|
||||
<select id="sel-lugar-init" class="form-select mb-3">
|
||||
<option value="">— Elija un lugar —</option>
|
||||
<?php foreach ($lugares as $l): ?>
|
||||
<option value="<?= (int)$l['id'] ?>"><?= htmlspecialchars($l['nombre']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button class="btn btn-primary w-100" onclick="confirmarLugar()">
|
||||
<i class="fas fa-check me-1"></i>Confirmar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main class="main-content" style="padding:0">
|
||||
|
||||
<!-- ── Top bar ──────────────────────────────────────────── -->
|
||||
<div class="d-flex align-items-center justify-content-between px-3 py-2 border-bottom bg-white"
|
||||
style="position:sticky;top:0;z-index:100;min-height:52px">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<i class="fas fa-map-marker-alt text-primary"></i>
|
||||
<strong id="lbl-lugar-titulo"><?= htmlspecialchars($lugarNombre) ?></strong>
|
||||
<span id="badge-turno-activo" class="badge bg-primary ms-1 d-none" id="badge-cod-activo"></span>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="cambiarLugar()">
|
||||
<i class="fas fa-exchange-alt me-1"></i>Cambiar lugar
|
||||
</button>
|
||||
<span class="text-muted small"><?= htmlspecialchars($adminNombre) ?></span>
|
||||
<button class="btn btn-primary btn-sm" id="btn-llamar" onclick="llamarSiguiente()">
|
||||
<i class="fas fa-bell me-1"></i>Llamar siguiente
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lugar-layout">
|
||||
|
||||
<!-- ══ Cola ═══════════════════════════════════════════ -->
|
||||
<div class="lugar-cola">
|
||||
<div class="lugar-cola-hdr">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<span class="fw-semibold text-secondary" style="font-size:.78rem;text-transform:uppercase;letter-spacing:.05em">
|
||||
<i class="fas fa-list-ol me-1"></i>EN ESPERA
|
||||
</span>
|
||||
<span class="badge bg-primary rounded-pill" id="badge-count">0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cola-items" id="cola-items">
|
||||
<div class="cola-vacia">
|
||||
<i class="fas fa-hourglass-start fa-2x mb-2 d-block"></i>
|
||||
Cola vacía
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ Ficha del turno ════════════════════════════════ -->
|
||||
<div class="lugar-ficha" id="lugar-ficha">
|
||||
|
||||
<div class="ficha-placeholder" id="ficha-placeholder">
|
||||
<i class="fas fa-stethoscope fa-3x mb-3" style="color:#cbd5e1"></i>
|
||||
<p class="fw-semibold mb-1">Sin turno activo</p>
|
||||
<small class="text-muted">Pulse "Llamar siguiente" o espere a que llegue el turno</small>
|
||||
</div>
|
||||
|
||||
<div id="ficha-turno" class="d-none">
|
||||
|
||||
<!-- Encabezado turno -->
|
||||
<div class="d-flex align-items-center gap-3 mb-3">
|
||||
<div id="ficha-prio-dot" class="prio-dot"
|
||||
style="width:52px;height:52px;border-radius:12px;font-size:1.6rem;font-weight:900">—</div>
|
||||
<div>
|
||||
<div class="h4 mb-0 fw-bold" id="ficha-codigo">—</div>
|
||||
<div class="text-muted small" id="ficha-prio-nombre"></div>
|
||||
</div>
|
||||
<span class="ms-auto badge bg-success-subtle text-success border border-success-subtle"
|
||||
id="ficha-estado-badge">en espera</span>
|
||||
</div>
|
||||
|
||||
<!-- ── Sección: Paciente ── -->
|
||||
<div class="ficha-sec">
|
||||
<h6><i class="fas fa-user me-1"></i>Paciente</h6>
|
||||
<div id="bloque-pac-info">
|
||||
<div class="pac-dato"><span class="lbl">Nombre</span><span class="val" id="pac-nombre">—</span></div>
|
||||
<div class="pac-dato"><span class="lbl">Documento</span><span class="val" id="pac-doc">—</span></div>
|
||||
<div class="pac-dato"><span class="lbl">Fecha nac.</span><span class="val" id="pac-fec">—</span></div>
|
||||
<div class="pac-dato"><span class="lbl">Celular</span><span class="val" id="pac-cel">—</span></div>
|
||||
</div>
|
||||
<div id="bloque-pac-sin" class="text-muted small">
|
||||
<i class="fas fa-info-circle me-1"></i>Sin paciente vinculado
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Sección: Exámenes ── -->
|
||||
<div class="ficha-sec">
|
||||
<h6><i class="fas fa-vial me-1"></i>Exámenes solicitados</h6>
|
||||
<div id="lista-examenes-ficha">
|
||||
<span class="text-muted small">Sin exámenes registrados</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Sección: Consentimientos ── -->
|
||||
<div class="ficha-sec" id="sec-consent">
|
||||
<h6><i class="fas fa-file-signature me-1"></i>Consentimientos informados</h6>
|
||||
|
||||
<div id="bloqueo-banner" class="bloqueo-banner d-none">
|
||||
<i class="fas fa-lock"></i>
|
||||
<span>Hay consentimientos pendientes. No puede iniciar la atención hasta que estén <strong>firmados</strong> o <strong>rechazados</strong>.</span>
|
||||
</div>
|
||||
|
||||
<div id="lista-consent"></div>
|
||||
|
||||
<div id="sin-consent" class="text-muted small">
|
||||
<i class="fas fa-check-circle text-success me-1"></i>No se requieren consentimientos
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Barra de acciones ── -->
|
||||
<div class="ficha-acciones">
|
||||
<button class="btn btn-success" id="btn-iniciar" onclick="iniciarAtencion()">
|
||||
<i class="fas fa-play me-1"></i>Iniciar atención
|
||||
</button>
|
||||
<button class="btn btn-primary d-none" id="btn-finalizar" onclick="finalizarAtencion()">
|
||||
<i class="fas fa-flag-checkered me-1"></i>Finalizar
|
||||
</button>
|
||||
<button class="btn btn-outline-warning d-none" id="btn-regresar" onclick="regresarCola()">
|
||||
<i class="fas fa-undo me-1"></i>Regresar a cola
|
||||
</button>
|
||||
<button class="btn btn-secondary ms-auto" id="btn-ausente" onclick="marcarAusente()">
|
||||
<i class="fas fa-user-slash me-1"></i>Ausente
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div><!-- /ficha-turno -->
|
||||
</div><!-- /lugar-ficha -->
|
||||
|
||||
</div><!-- /lugar-layout -->
|
||||
</main>
|
||||
|
||||
<!-- ══ Modal de firma presencial ══════════════════════════════ -->
|
||||
<div class="modal-firma-backdrop" id="modal-firma">
|
||||
<div class="modal-firma-box">
|
||||
<div class="modal-firma-hdr">
|
||||
<strong id="modal-firma-titulo">Firmar consentimiento</strong>
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="cerrarModalFirma()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<iframe id="firma-iframe" src="" title="Formulario de consentimiento"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ── Estado global ─────────────────────────────────────────────
|
||||
let lugarId = <?= $lugarIdParam ?: 0 ?>;
|
||||
let turnoActivo = null;
|
||||
let tieneConsent = false;
|
||||
let hayPendientes = false;
|
||||
let pollingColaId = null;
|
||||
let pollingConsentId = null;
|
||||
|
||||
const API = '../api/';
|
||||
const BASE_WA = '<?= BASE_URL ?>';
|
||||
|
||||
// ── Arranque ──────────────────────────────────────────────────
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (lugarId) {
|
||||
iniciarPuesto();
|
||||
}
|
||||
});
|
||||
|
||||
function iniciarPuesto() {
|
||||
cargarCola();
|
||||
pollingColaId = setInterval(cargarCola, 7000);
|
||||
}
|
||||
|
||||
// ── Selector de lugar ─────────────────────────────────────────
|
||||
function confirmarLugar() {
|
||||
const sel = document.getElementById('sel-lugar-init');
|
||||
const id = parseInt(sel.value);
|
||||
if (!id) return alert('Seleccione un lugar.');
|
||||
lugarId = id;
|
||||
const txt = sel.options[sel.selectedIndex].text;
|
||||
document.getElementById('lbl-lugar-titulo').textContent = txt;
|
||||
document.getElementById('overlay-selector').style.display = 'none';
|
||||
document.title = txt + ' — Turnero';
|
||||
iniciarPuesto();
|
||||
}
|
||||
|
||||
function cambiarLugar() {
|
||||
clearInterval(pollingColaId);
|
||||
clearInterval(pollingConsentId);
|
||||
turnoActivo = null;
|
||||
resetFicha();
|
||||
document.getElementById('overlay-selector').style.display = 'flex';
|
||||
}
|
||||
|
||||
// ── Cola ──────────────────────────────────────────────────────
|
||||
async function cargarCola() {
|
||||
if (!lugarId) return;
|
||||
try {
|
||||
const res = await fetch(`${API}get_cola.php?area=lugar&lugar_id=${lugarId}`);
|
||||
const json = await res.json();
|
||||
if (!json.ok) return;
|
||||
renderCola(json.data);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function renderCola(snap) {
|
||||
const espera = (snap.cola || []).filter(t =>
|
||||
t.estado === 'en_espera_lugar'
|
||||
);
|
||||
document.getElementById('badge-count').textContent = espera.length;
|
||||
|
||||
const lista = document.getElementById('cola-items');
|
||||
if (!espera.length) {
|
||||
lista.innerHTML = `<div class="cola-vacia">
|
||||
<i class="fas fa-hourglass-start fa-2x mb-2 d-block"></i>Cola vacía</div>`;
|
||||
return;
|
||||
}
|
||||
lista.innerHTML = espera.map(t => `
|
||||
<div class="cola-card ${turnoActivo?.id === t.id ? 'activo' : ''}">
|
||||
<div class="prio-dot" style="background:${t.prioridad_color}">${t.prioridad_codigo}</div>
|
||||
<div class="turno-info">
|
||||
<div class="cod">${escHtml(t.codigo)}</div>
|
||||
<div class="pac">${escHtml(t.paciente_nombre || 'Paciente')}</div>
|
||||
</div>
|
||||
</div>`).join('');
|
||||
}
|
||||
|
||||
// ── Llamar siguiente ──────────────────────────────────────────
|
||||
async function llamarSiguiente() {
|
||||
if (!lugarId) { alert('Primero seleccione un lugar.'); return; }
|
||||
const btn = document.getElementById('btn-llamar');
|
||||
btn.disabled = true;
|
||||
try {
|
||||
const res = await fetch(API + 'llamar_turno.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ area: 'lugar', lugar_id: lugarId }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!json.ok) { alert('Error: ' + json.error); return; }
|
||||
|
||||
if (!json.data.turno) {
|
||||
alert('Cola del lugar vacía.');
|
||||
return;
|
||||
}
|
||||
await abrirFicha(json.data.turno);
|
||||
cargarCola();
|
||||
} catch (err) {
|
||||
alert('Error: ' + err.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Abrir ficha ───────────────────────────────────────────────
|
||||
async function abrirFicha(turno) {
|
||||
turnoActivo = turno;
|
||||
|
||||
document.getElementById('ficha-placeholder').classList.add('d-none');
|
||||
document.getElementById('ficha-turno').classList.remove('d-none');
|
||||
|
||||
// Header
|
||||
document.getElementById('ficha-codigo').textContent = turno.codigo;
|
||||
document.getElementById('ficha-prio-nombre').textContent = turno.prioridad_nombre || '';
|
||||
const dot = document.getElementById('ficha-prio-dot');
|
||||
dot.textContent = turno.prioridad_codigo || '—';
|
||||
dot.style.background = turno.prioridad_color || '#6366f1';
|
||||
|
||||
const badgeCod = document.getElementById('badge-turno-activo');
|
||||
badgeCod.textContent = turno.codigo;
|
||||
badgeCod.classList.remove('d-none');
|
||||
|
||||
actualizarEstadoBadge(turno.estado);
|
||||
|
||||
// Cargar ficha del paciente/exámenes desde la solicitud
|
||||
await cargarFichaSolicitud(turno.id);
|
||||
|
||||
// Iniciar polling de consentimientos
|
||||
clearInterval(pollingConsentId);
|
||||
pollingConsentId = setInterval(() => actualizarConsentimientos(turnoActivo?.id), 5000);
|
||||
}
|
||||
|
||||
// ── Cargar datos de la solicitud del turno ────────────────────
|
||||
async function cargarFichaSolicitud(turnoId) {
|
||||
try {
|
||||
const res = await fetch(`${API}get_consentimientos.php?turno_id=${turnoId}&incluir_solicitud=1`);
|
||||
const json = await res.json();
|
||||
if (!json.ok) return;
|
||||
|
||||
const sol = json.data.solicitud;
|
||||
const pac = json.data.paciente;
|
||||
const exams = json.data.examenes || [];
|
||||
const consts = json.data.consentimientos || [];
|
||||
|
||||
// Datos del paciente
|
||||
if (pac) {
|
||||
document.getElementById('bloque-pac-info').style.display = '';
|
||||
document.getElementById('bloque-pac-sin').classList.add('d-none');
|
||||
document.getElementById('pac-nombre').textContent =
|
||||
(pac.full_name || (pac.nombre||'') + ' ' + (pac.apellido||'')).trim() || '—';
|
||||
document.getElementById('pac-doc').textContent =
|
||||
(pac.tipo_documento||'') + ' ' + (pac.documento||'') || '—';
|
||||
document.getElementById('pac-fec').textContent = pac.fecha_nacimiento || '—';
|
||||
document.getElementById('pac-cel').textContent = pac.celular || pac.telefono || '—';
|
||||
} else {
|
||||
document.getElementById('bloque-pac-info').style.display = 'none';
|
||||
document.getElementById('bloque-pac-sin').classList.remove('d-none');
|
||||
}
|
||||
|
||||
// Exámenes
|
||||
const listaEx = document.getElementById('lista-examenes-ficha');
|
||||
if (exams.length) {
|
||||
listaEx.innerHTML = exams.map(e =>
|
||||
`<span class="exam-pill"><i class="fas fa-vial"></i>${escHtml(e.codigo)} ${escHtml(e.nombre)}</span>`
|
||||
).join('');
|
||||
} else {
|
||||
listaEx.innerHTML = '<span class="text-muted small">Sin exámenes registrados</span>';
|
||||
}
|
||||
|
||||
// Consentimientos
|
||||
renderConsentimientos(consts);
|
||||
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// ── Consentimientos ───────────────────────────────────────────
|
||||
function renderConsentimientos(lista) {
|
||||
tieneConsent = lista.length > 0;
|
||||
hayPendientes = lista.some(c => !['firmado', 'rechazado'].includes(c.estado));
|
||||
|
||||
const sec = document.getElementById('sec-consent');
|
||||
const listEl = document.getElementById('lista-consent');
|
||||
const sinEl = document.getElementById('sin-consent');
|
||||
const banner = document.getElementById('bloqueo-banner');
|
||||
const btnIni = document.getElementById('btn-iniciar');
|
||||
|
||||
if (!tieneConsent) {
|
||||
listEl.innerHTML = '';
|
||||
sinEl.classList.remove('d-none');
|
||||
banner.classList.add('d-none');
|
||||
btnIni.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
sinEl.classList.add('d-none');
|
||||
banner.classList.toggle('d-none', !hayPendientes);
|
||||
btnIni.disabled = hayPendientes;
|
||||
|
||||
listEl.innerHTML = lista.map(c => {
|
||||
const cls = c.estado;
|
||||
const ico = c.estado === 'firmado' ? 'fa-check-circle'
|
||||
: c.estado === 'rechazado' ? 'fa-ban'
|
||||
: c.estado === 'enviado' ? 'fa-envelope'
|
||||
: c.estado === 'visto' ? 'fa-eye'
|
||||
: 'fa-clock';
|
||||
const label = c.estado === 'firmado' ? 'Firmado'
|
||||
: c.estado === 'rechazado' ? 'Rechazado'
|
||||
: c.estado === 'enviado' ? 'Enviado'
|
||||
: c.estado === 'visto' ? 'Visto'
|
||||
: 'Pendiente';
|
||||
|
||||
const botonesAccion = !['firmado','rechazado'].includes(c.estado) ? `
|
||||
<div class="acciones-consent">
|
||||
<button class="btn btn-outline-secondary btn-sm py-0" title="Reenviar WhatsApp"
|
||||
onclick="reenviarConsentimiento(${c.turno_id})">
|
||||
<i class="fas fa-paper-plane" style="font-size:.75rem"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-primary btn-sm py-0" title="Firmar presencialmente"
|
||||
onclick="abrirFirmaPresencial('${c.token}', ${c.id}, ${JSON.stringify(c.formulario_nombre).replace(/"/g,'"')})">
|
||||
<i class="fas fa-signature" style="font-size:.75rem"></i> Firmar aquí
|
||||
</button>
|
||||
</div>` : '';
|
||||
|
||||
return `<div class="consent-row ${cls}" data-consent-id="${c.id}">
|
||||
<i class="fas ${ico}"></i>
|
||||
<span class="nom-form">${escHtml(c.formulario_nombre || 'Consentimiento')}</span>
|
||||
<span class="badge">${escHtml(label)}</span>
|
||||
${botonesAccion}
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function actualizarConsentimientos(turnoId) {
|
||||
if (!turnoId) return;
|
||||
try {
|
||||
const res = await fetch(`${API}get_consentimientos.php?turno_id=${turnoId}`);
|
||||
const json = await res.json();
|
||||
if (!json.ok) return;
|
||||
renderConsentimientos(json.data.consentimientos || []);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
async function reenviarConsentimiento(turnoId) {
|
||||
try {
|
||||
const res = await fetch(API + 'send_consentimiento.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ turno_id: turnoId }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!json.ok) { alert('Error: ' + json.error); return; }
|
||||
await actualizarConsentimientos(turnoId);
|
||||
} catch (err) {
|
||||
alert('Error: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Modal de firma presencial ─────────────────────────────────
|
||||
function abrirFirmaPresencial(token, consentId, nombreForm) {
|
||||
const url = BASE_WA + 'ver_formulario_enviado.php?token=' + encodeURIComponent(token);
|
||||
document.getElementById('modal-firma-titulo').textContent = 'Firmar: ' + nombreForm;
|
||||
document.getElementById('firma-iframe').src = url;
|
||||
document.getElementById('modal-firma').classList.add('open');
|
||||
|
||||
// Al cerrar, refrescar consentimientos
|
||||
window._firmaConsentId = consentId;
|
||||
}
|
||||
|
||||
function cerrarModalFirma() {
|
||||
document.getElementById('modal-firma').classList.remove('open');
|
||||
document.getElementById('firma-iframe').src = '';
|
||||
if (turnoActivo) actualizarConsentimientos(turnoActivo.id);
|
||||
}
|
||||
|
||||
// Cerrar modal al hacer clic fuera del cuadro
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.getElementById('modal-firma').addEventListener('click', function(e) {
|
||||
if (e.target === this) cerrarModalFirma();
|
||||
});
|
||||
|
||||
// Escuchar mensaje del iframe (firma completada desde ver_formulario_enviado.php)
|
||||
window.addEventListener('message', function(e) {
|
||||
if (e.data && e.data.type === 'turneroFirmado') {
|
||||
cerrarModalFirma();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ── Acciones del turno ────────────────────────────────────────
|
||||
async function iniciarAtencion() {
|
||||
if (!turnoActivo) return;
|
||||
if (hayPendientes) return; // botón ya deshabilitado, doble seguro
|
||||
|
||||
const btn = document.getElementById('btn-iniciar');
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
const res = await fetch(API + 'cambiar_estado.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ turno_id: turnoActivo.id, nuevo_estado: 'en_servicio' }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!json.ok) {
|
||||
alert('Error: ' + json.error);
|
||||
btn.disabled = hayPendientes;
|
||||
return;
|
||||
}
|
||||
turnoActivo = json.data.turno;
|
||||
actualizarEstadoBadge('en_servicio');
|
||||
|
||||
// Mostrar "Finalizar" y "Regresar", ocultar "Iniciar"
|
||||
btn.classList.add('d-none');
|
||||
document.getElementById('btn-finalizar').classList.remove('d-none');
|
||||
document.getElementById('btn-regresar').classList.remove('d-none');
|
||||
cargarCola();
|
||||
} catch (err) {
|
||||
alert('Error: ' + err.message);
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function finalizarAtencion() {
|
||||
if (!turnoActivo) return;
|
||||
if (!confirm(`¿Finalizar atención del turno ${turnoActivo.codigo}?`)) return;
|
||||
|
||||
await cambiarEstadoTurno('finalizado');
|
||||
}
|
||||
|
||||
async function marcarAusente() {
|
||||
if (!turnoActivo) return;
|
||||
if (!confirm(`¿Marcar turno ${turnoActivo.codigo} como AUSENTE?`)) return;
|
||||
|
||||
await cambiarEstadoTurno('ausente');
|
||||
}
|
||||
|
||||
async function regresarCola() {
|
||||
if (!turnoActivo) return;
|
||||
if (!confirm(`¿Regresar turno ${turnoActivo.codigo} a la cola del lugar?`)) return;
|
||||
|
||||
// Regresar a en_espera_lugar
|
||||
try {
|
||||
const res = await fetch(API + 'cambiar_estado.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
turno_id: turnoActivo.id,
|
||||
nuevo_estado: 'en_espera_lugar',
|
||||
lugar_id: lugarId,
|
||||
}),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!json.ok) { alert('Error: ' + json.error); return; }
|
||||
resetFicha();
|
||||
cargarCola();
|
||||
} catch (err) {
|
||||
alert('Error: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function cambiarEstadoTurno(nuevoEstado) {
|
||||
try {
|
||||
const res = await fetch(API + 'cambiar_estado.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ turno_id: turnoActivo.id, nuevo_estado: nuevoEstado }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!json.ok) { alert('Error: ' + json.error); return; }
|
||||
resetFicha();
|
||||
cargarCola();
|
||||
} catch (err) {
|
||||
alert('Error: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Reset ─────────────────────────────────────────────────────
|
||||
function resetFicha() {
|
||||
clearInterval(pollingConsentId);
|
||||
turnoActivo = null;
|
||||
tieneConsent = false;
|
||||
hayPendientes = false;
|
||||
|
||||
document.getElementById('ficha-turno').classList.add('d-none');
|
||||
document.getElementById('ficha-placeholder').classList.remove('d-none');
|
||||
document.getElementById('badge-turno-activo').classList.add('d-none');
|
||||
|
||||
// Botones al estado inicial
|
||||
document.getElementById('btn-iniciar').classList.remove('d-none');
|
||||
document.getElementById('btn-iniciar').disabled = false;
|
||||
document.getElementById('btn-finalizar').classList.add('d-none');
|
||||
document.getElementById('btn-regresar').classList.add('d-none');
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────
|
||||
function actualizarEstadoBadge(estado) {
|
||||
const el = document.getElementById('ficha-estado-badge');
|
||||
const mapa = {
|
||||
'en_espera_lugar': ['bg-warning-subtle text-warning border border-warning-subtle', 'En espera'],
|
||||
'en_servicio': ['bg-success-subtle text-success border border-success-subtle', 'En servicio'],
|
||||
'finalizado': ['bg-secondary-subtle text-secondary border', 'Finalizado'],
|
||||
'ausente': ['bg-danger-subtle text-danger border border-danger-subtle', 'Ausente'],
|
||||
};
|
||||
const [cls, lbl] = mapa[estado] || ['bg-secondary-subtle text-secondary border', estado];
|
||||
el.className = 'ms-auto badge ' + cls;
|
||||
el.textContent = lbl;
|
||||
}
|
||||
|
||||
function escHtml(str) {
|
||||
const d = document.createElement('div');
|
||||
d.appendChild(document.createTextNode(String(str ?? '')));
|
||||
return d.innerHTML;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,706 @@
|
||||
<?php
|
||||
/**
|
||||
* Puesto de Recepción — Módulo Turnero
|
||||
* Requiere login + módulo turnero.
|
||||
*/
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
if (!isUserLoggedIn()) {
|
||||
header('Location: ' . BASE_URL . 'login.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Cargar catálogos para la ficha
|
||||
try {
|
||||
$pdo = Database::getInstance()->getConnection();
|
||||
|
||||
$lugares = $pdo->query(
|
||||
"SELECT id, nombre FROM turnero_lugares WHERE activo = 1 ORDER BY sort_order ASC"
|
||||
)->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$examenes = $pdo->query(
|
||||
"SELECT id, codigo, nombre, categoria FROM exam_tipos WHERE activo = 1 ORDER BY categoria, nombre"
|
||||
)->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Agrupar exámenes por categoría
|
||||
$examenesAgrupados = [];
|
||||
foreach ($examenes as $ex) {
|
||||
$cat = $ex['categoria'] ?: 'General';
|
||||
$examenesAgrupados[$cat][] = $ex;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$lugares = [];
|
||||
$examenesAgrupados = [];
|
||||
}
|
||||
|
||||
$adminNombre = $_SESSION['admin_user']['full_name'] ?? $_SESSION['admin_user']['username'] ?? 'Operador';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Recepció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">
|
||||
<link href="<?= BASE_URL ?>assets/css/styles.css?v=12" rel="stylesheet">
|
||||
<style>
|
||||
/* ── Layout de dos columnas ── */
|
||||
.rec-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 320px 1fr;
|
||||
height: calc(100vh - 56px);
|
||||
overflow: hidden;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.rec-layout { grid-template-columns: 1fr; }
|
||||
.rec-cola { max-height: 260px; }
|
||||
}
|
||||
|
||||
/* ── Columna cola ── */
|
||||
.rec-cola {
|
||||
background: #f8fafc;
|
||||
border-right: 1px solid #e2e8f0;
|
||||
display: flex; flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.rec-cola-header {
|
||||
padding: 1rem 1.2rem .6rem;
|
||||
background: #fff; border-bottom: 1px solid #e2e8f0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.cola-items { flex: 1; overflow-y: auto; padding: .5rem; }
|
||||
.cola-card {
|
||||
background: #fff; border: 1px solid #e2e8f0;
|
||||
border-radius: 10px; padding: .65rem .9rem;
|
||||
margin-bottom: .4rem; cursor: default;
|
||||
display: flex; align-items: center; gap: .6rem;
|
||||
transition: box-shadow .15s;
|
||||
}
|
||||
.cola-card.activo { border-color: #3b82f6; box-shadow: 0 0 0 2px #bfdbfe; }
|
||||
.cola-card .prio-dot {
|
||||
width: 36px; height: 36px; border-radius: 50%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 1.1rem; font-weight: 800; color: #fff; flex-shrink: 0;
|
||||
}
|
||||
.cola-card .turno-info { flex: 1; min-width: 0; }
|
||||
.cola-card .turno-info .cod { font-size: 1rem; font-weight: 700; }
|
||||
.cola-card .turno-info .pac { font-size: .78rem; color: #64748b;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.cola-vacia { text-align: center; padding: 3rem 1rem; color: #94a3b8; }
|
||||
|
||||
/* ── Columna ficha ── */
|
||||
.rec-ficha {
|
||||
overflow-y: auto; padding: 1.5rem;
|
||||
background: #fff;
|
||||
}
|
||||
.ficha-placeholder {
|
||||
display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center;
|
||||
height: 100%; color: #94a3b8; text-align: center;
|
||||
}
|
||||
|
||||
/* ── Secciones de la ficha ── */
|
||||
.ficha-section {
|
||||
background: #f8fafc; border: 1px solid #e2e8f0;
|
||||
border-radius: 12px; padding: 1.2rem; margin-bottom: 1rem;
|
||||
}
|
||||
.ficha-section h6 { color: #475569; font-size: .78rem;
|
||||
text-transform: uppercase; letter-spacing: .08em; margin-bottom: .8rem; }
|
||||
|
||||
/* ── Búsqueda de paciente ── */
|
||||
.pac-resultado {
|
||||
border: 1px solid #e2e8f0; border-radius: 8px; padding: .5rem .75rem;
|
||||
cursor: pointer; margin-top: .25rem; transition: background .12s;
|
||||
}
|
||||
.pac-resultado:hover { background: #f1f5f9; }
|
||||
.pac-resultado .pac-nombre { font-weight: 600; font-size: .9rem; }
|
||||
.pac-resultado .pac-doc { font-size: .78rem; color: #64748b; }
|
||||
.pac-seleccionado {
|
||||
background: #eff6ff; border: 1px solid #93c5fd;
|
||||
border-radius: 8px; padding: .65rem .9rem;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
}
|
||||
|
||||
/* ── Checkboxes de exámenes ── */
|
||||
.exam-group-title { font-size: .75rem; color: #94a3b8;
|
||||
text-transform: uppercase; letter-spacing: .08em; margin: .6rem 0 .3rem; }
|
||||
.exam-check-item { display: flex; align-items: center; gap: .5rem;
|
||||
padding: .3rem .4rem; border-radius: 6px; cursor: pointer;
|
||||
transition: background .1s; font-size: .88rem; }
|
||||
.exam-check-item:hover { background: #f1f5f9; }
|
||||
.exam-check-item input[type=checkbox] { cursor: pointer; }
|
||||
|
||||
/* ── Consentimientos ── */
|
||||
.consent-item {
|
||||
display: flex; align-items: center; gap: .6rem;
|
||||
padding: .4rem .6rem; border-radius: 8px;
|
||||
font-size: .85rem; margin-bottom: .3rem;
|
||||
}
|
||||
.consent-item.firmado { background: #f0fdf4; color: #166534; }
|
||||
.consent-item.pendiente{ background: #fffbeb; color: #92400e; }
|
||||
.consent-item.enviado { background: #eff6ff; color: #1e40af; }
|
||||
|
||||
/* ── Barra de acciones ── */
|
||||
.ficha-acciones {
|
||||
position: sticky; bottom: 0;
|
||||
background: #fff; border-top: 1px solid #e2e8f0;
|
||||
padding: 1rem 0 .25rem; margin-top: 1rem;
|
||||
display: flex; gap: .5rem; flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* ── Badge de turno activo en topbar ── */
|
||||
.badge-turno-activo {
|
||||
background: #eff6ff; color: #1d4ed8;
|
||||
border: 1px solid #bfdbfe; border-radius: 8px;
|
||||
padding: .25rem .75rem; font-size: .82rem; font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<?php
|
||||
$SIDEBAR_TITLE = 'Turnero';
|
||||
$SIDEBAR_ICON = 'fas fa-ticket-alt';
|
||||
require_once __DIR__ . '/../../../shared/components/sidebar.php';
|
||||
?>
|
||||
|
||||
<main class="main-content" style="padding:0">
|
||||
|
||||
<!-- ── Top bar ──────────────────────────────────────────── -->
|
||||
<div class="d-flex align-items-center justify-content-between px-3 py-2 border-bottom bg-white"
|
||||
style="position:sticky;top:0;z-index:100">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<i class="fas fa-ticket-alt text-primary"></i>
|
||||
<strong>Recepción</strong>
|
||||
<span id="badge-turno-activo" class="badge-turno-activo d-none">
|
||||
Turno: <span id="badge-codigo">—</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="text-muted small"><?= htmlspecialchars($adminNombre) ?></span>
|
||||
<button class="btn btn-primary btn-sm" id="btn-llamar" onclick="llamarSiguiente()">
|
||||
<i class="fas fa-bell me-1"></i>Llamar siguiente
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rec-layout">
|
||||
|
||||
<!-- ══ Columna izquierda: cola ════════════════════════ -->
|
||||
<div class="rec-cola">
|
||||
<div class="rec-cola-header">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<span class="fw-semibold text-secondary small">
|
||||
<i class="fas fa-list-ol me-1"></i>EN ESPERA
|
||||
</span>
|
||||
<span class="badge bg-primary rounded-pill" id="badge-count">0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cola-items" id="cola-items">
|
||||
<div class="cola-vacia">
|
||||
<i class="fas fa-hourglass-start fa-2x mb-2 d-block"></i>
|
||||
Cola vacía
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ Columna derecha: ficha del turno ═══════════════ -->
|
||||
<div class="rec-ficha" id="rec-ficha">
|
||||
<div class="ficha-placeholder" id="ficha-placeholder">
|
||||
<i class="fas fa-ticket-alt fa-3x mb-3" style="color:#cbd5e1"></i>
|
||||
<p class="mb-0 fw-semibold">Sin turno activo</p>
|
||||
<small class="text-muted">Haga clic en "Llamar siguiente" o seleccione un turno de la cola</small>
|
||||
</div>
|
||||
|
||||
<!-- Ficha de turno (oculta hasta llamar) -->
|
||||
<div id="ficha-turno" class="d-none">
|
||||
|
||||
<!-- Encabezado del turno -->
|
||||
<div class="d-flex align-items-center gap-3 mb-3">
|
||||
<div id="ficha-prio-dot" class="prio-dot"
|
||||
style="width:52px;height:52px;border-radius:12px;display:flex;align-items:center;
|
||||
justify-content:center;font-size:1.6rem;font-weight:900;color:#fff;background:#3b82f6">
|
||||
—
|
||||
</div>
|
||||
<div>
|
||||
<div class="h4 mb-0 fw-bold" id="ficha-codigo">—</div>
|
||||
<div class="text-muted small" id="ficha-prio-nombre">—</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Sección 1: Paciente ── -->
|
||||
<div class="ficha-section">
|
||||
<h6><i class="fas fa-user me-1"></i>Paciente</h6>
|
||||
|
||||
<!-- Si el kiosko capturó un nombre -->
|
||||
<div id="bloque-nombre-kiosko" class="d-none mb-2">
|
||||
<small class="text-muted">Nombre del kiosko:</small>
|
||||
<div class="fw-semibold" id="lbl-nombre-kiosko"></div>
|
||||
</div>
|
||||
|
||||
<div id="bloque-pac-no-vinculado">
|
||||
<div class="input-group mb-2">
|
||||
<input type="text" id="inp-buscar-pac"
|
||||
class="form-control form-control-sm"
|
||||
placeholder="Buscar por nombre o documento…"
|
||||
autocomplete="off">
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="buscarPaciente()" type="button">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="lista-pacientes-res"></div>
|
||||
</div>
|
||||
|
||||
<div id="bloque-pac-seleccionado" class="d-none">
|
||||
<div class="pac-seleccionado">
|
||||
<div>
|
||||
<div class="fw-semibold" id="lbl-pac-nombre">—</div>
|
||||
<div class="text-muted small" id="lbl-pac-doc">—</div>
|
||||
<div class="text-muted small" id="lbl-pac-cel"></div>
|
||||
</div>
|
||||
<button class="btn btn-link btn-sm text-secondary p-0" onclick="desvincularPaciente()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Sección 2: Lugar destino ── -->
|
||||
<div class="ficha-section">
|
||||
<h6><i class="fas fa-map-marker-alt me-1"></i>Lugar destino</h6>
|
||||
<select id="sel-lugar" class="form-select form-select-sm">
|
||||
<option value="">— Seleccione lugar —</option>
|
||||
<?php foreach ($lugares as $l): ?>
|
||||
<option value="<?= (int)$l['id'] ?>"><?= htmlspecialchars($l['nombre']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- ── Sección 3: Exámenes ── -->
|
||||
<div class="ficha-section">
|
||||
<h6><i class="fas fa-vial me-1"></i>Exámenes solicitados</h6>
|
||||
<div id="lista-examenes">
|
||||
<?php foreach ($examenesAgrupados as $cat => $items): ?>
|
||||
<div class="exam-group-title"><?= htmlspecialchars($cat) ?></div>
|
||||
<?php foreach ($items as $ex): ?>
|
||||
<label class="exam-check-item">
|
||||
<input type="checkbox" class="exam-chk"
|
||||
value="<?= (int)$ex['id'] ?>"
|
||||
data-nombre="<?= htmlspecialchars($ex['nombre']) ?>">
|
||||
<span><strong><?= htmlspecialchars($ex['codigo']) ?></strong>
|
||||
— <?= htmlspecialchars($ex['nombre']) ?></span>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div class="mt-2 d-flex gap-2 flex-wrap">
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="toggleTodosExamenes(false)">
|
||||
Limpiar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Sección 4: Pago ── -->
|
||||
<div class="ficha-section">
|
||||
<h6><i class="fas fa-dollar-sign me-1"></i>Cobro (opcional)</h6>
|
||||
<div class="row g-2">
|
||||
<div class="col-6">
|
||||
<input type="number" id="inp-total" class="form-control form-control-sm"
|
||||
placeholder="Total $" step="100" min="0">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<select id="sel-pago" class="form-select form-select-sm">
|
||||
<option value="">— Forma de pago —</option>
|
||||
<option value="efectivo">Efectivo</option>
|
||||
<option value="transferencia">Transferencia</option>
|
||||
<option value="tarjeta">Tarjeta</option>
|
||||
<option value="eps">EPS / Convenio</option>
|
||||
<option value="cortesia">Cortesía</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<textarea id="inp-obs" class="form-control form-control-sm mt-2"
|
||||
rows="2" placeholder="Observaciones…" maxlength="500"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- ── Sección 5: Consentimientos ── -->
|
||||
<div class="ficha-section" id="sec-consentimientos" style="display:none!important">
|
||||
<h6><i class="fas fa-file-signature me-1"></i>Consentimientos</h6>
|
||||
<div id="lista-consentimientos"></div>
|
||||
<button class="btn btn-outline-primary btn-sm mt-2"
|
||||
id="btn-reenviar-consent" onclick="enviarConsentimientos()">
|
||||
<i class="fas fa-paper-plane me-1"></i>Enviar / Reenviar por WhatsApp
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ── Acciones ── -->
|
||||
<div class="ficha-acciones">
|
||||
<button class="btn btn-success" id="btn-guardar" onclick="guardarSolicitud()">
|
||||
<i class="fas fa-save me-1"></i>Guardar solicitud
|
||||
</button>
|
||||
<button class="btn btn-primary d-none" id="btn-pasar-lugar" onclick="pasarALugar()">
|
||||
<i class="fas fa-arrow-right me-1"></i>Pasar a lugar
|
||||
</button>
|
||||
<button class="btn btn-warning d-none" id="btn-enviar-consent" onclick="enviarConsentimientos()">
|
||||
<i class="fas fa-paper-plane me-1"></i>Enviar consentimientos
|
||||
</button>
|
||||
<button class="btn btn-secondary ms-auto" onclick="marcarAusente()">
|
||||
<i class="fas fa-user-slash me-1"></i>Ausente
|
||||
</button>
|
||||
</div>
|
||||
</div><!-- /ficha-turno -->
|
||||
</div><!-- /rec-ficha -->
|
||||
|
||||
</div><!-- /rec-layout -->
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// ── Estado ────────────────────────────────────────────────────
|
||||
let turnoActivo = null;
|
||||
let pacienteActivo = null;
|
||||
let solicitudActiva = null;
|
||||
let consentimientos = [];
|
||||
let pollingColaId = null;
|
||||
|
||||
const API = '../api/';
|
||||
const API_PAC = '<?= BASE_URL ?>api/lab/get_pacientes.php';
|
||||
|
||||
// ── Arranque ──────────────────────────────────────────────────
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
cargarCola();
|
||||
pollingColaId = setInterval(cargarCola, 8000);
|
||||
document.getElementById('inp-buscar-pac')
|
||||
.addEventListener('keydown', e => { if (e.key === 'Enter') buscarPaciente(); });
|
||||
});
|
||||
|
||||
// ── Cola ──────────────────────────────────────────────────────
|
||||
async function cargarCola() {
|
||||
try {
|
||||
const res = await fetch(API + 'get_cola.php?area=recepcion');
|
||||
const json = await res.json();
|
||||
if (!json.ok) return;
|
||||
renderCola(json.data);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function renderCola(snap) {
|
||||
const lista = document.getElementById('cola-items');
|
||||
const espera = (snap.cola || []).filter(t => t.estado === 'espera');
|
||||
const badge = document.getElementById('badge-count');
|
||||
badge.textContent = espera.length;
|
||||
|
||||
if (espera.length === 0) {
|
||||
lista.innerHTML = `<div class="cola-vacia">
|
||||
<i class="fas fa-hourglass-start fa-2x mb-2 d-block"></i>Cola vacía</div>`;
|
||||
return;
|
||||
}
|
||||
lista.innerHTML = espera.map(t => `
|
||||
<div class="cola-card ${turnoActivo?.id === t.id ? 'activo' : ''}"
|
||||
onclick="seleccionarTurno(${t.id})">
|
||||
<div class="prio-dot" style="background:${t.prioridad_color}">${t.prioridad_codigo}</div>
|
||||
<div class="turno-info">
|
||||
<div class="cod">${escHtml(t.codigo)}</div>
|
||||
<div class="pac">${escHtml(t.paciente_nombre || 'Paciente')}</div>
|
||||
</div>
|
||||
</div>`).join('');
|
||||
}
|
||||
|
||||
// ── Llamar siguiente ──────────────────────────────────────────
|
||||
async function llamarSiguiente() {
|
||||
const btn = document.getElementById('btn-llamar');
|
||||
btn.disabled = true;
|
||||
try {
|
||||
const res = await fetch(API + 'llamar_turno.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ area: 'recepcion' }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!json.ok) { mostrarError(json.error); return; }
|
||||
|
||||
if (!json.data.turno) {
|
||||
mostrarAviso('Cola vacía', 'No hay turnos en espera.');
|
||||
return;
|
||||
}
|
||||
abrirFicha(json.data.turno);
|
||||
cargarCola();
|
||||
} catch (err) {
|
||||
mostrarError(err.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Seleccionar turno de la cola (para ver ficha de uno ya llamado) ─
|
||||
async function seleccionarTurno(turnoId) {
|
||||
// No abrir si ya hay uno en recepción (confuso)
|
||||
if (turnoActivo && turnoActivo.estado === 'en_recepcion') return;
|
||||
}
|
||||
|
||||
// ── Abrir ficha ───────────────────────────────────────────────
|
||||
function abrirFicha(turno) {
|
||||
turnoActivo = turno;
|
||||
pacienteActivo = null;
|
||||
solicitudActiva = null;
|
||||
|
||||
document.getElementById('ficha-placeholder').classList.add('d-none');
|
||||
document.getElementById('ficha-turno').classList.remove('d-none');
|
||||
|
||||
// Datos del turno
|
||||
document.getElementById('ficha-codigo').textContent = turno.codigo;
|
||||
document.getElementById('ficha-prio-nombre').textContent = turno.prioridad_nombre || '';
|
||||
const dot = document.getElementById('ficha-prio-dot');
|
||||
dot.textContent = turno.prioridad_codigo || '—';
|
||||
dot.style.background = turno.prioridad_color || '#3b82f6';
|
||||
|
||||
// Badge top
|
||||
document.getElementById('badge-turno-activo').classList.remove('d-none');
|
||||
document.getElementById('badge-codigo').textContent = turno.codigo;
|
||||
|
||||
// Nombre del kiosko
|
||||
if (turno.paciente_nombre) {
|
||||
document.getElementById('bloque-nombre-kiosko').classList.remove('d-none');
|
||||
document.getElementById('lbl-nombre-kiosko').textContent = turno.paciente_nombre;
|
||||
document.getElementById('inp-buscar-pac').value = turno.paciente_nombre;
|
||||
}
|
||||
|
||||
// Reset secciones
|
||||
resetCheckboxes();
|
||||
document.getElementById('sec-consentimientos').style.removeProperty('display');
|
||||
document.getElementById('sec-consentimientos').style.display = 'none';
|
||||
document.getElementById('btn-pasar-lugar').classList.add('d-none');
|
||||
document.getElementById('btn-enviar-consent').classList.add('d-none');
|
||||
document.getElementById('btn-guardar').classList.remove('d-none');
|
||||
document.getElementById('btn-guardar').disabled = false;
|
||||
desvincularPaciente();
|
||||
|
||||
// Buscar consentimientos si ya tiene solicitud
|
||||
verificarSolicitudExistente(turno.id);
|
||||
}
|
||||
|
||||
async function verificarSolicitudExistente(turnoId) {
|
||||
try {
|
||||
const res = await fetch(API + 'get_cola.php?area=recepcion');
|
||||
// La solicitud se carga solo después de guardar
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// ── Buscador de paciente ──────────────────────────────────────
|
||||
async function buscarPaciente() {
|
||||
const q = document.getElementById('inp-buscar-pac').value.trim();
|
||||
if (q.length < 2) return;
|
||||
try {
|
||||
const res = await fetch(`${API_PAC}?busqueda=${encodeURIComponent(q)}&limit=8`);
|
||||
const json = await res.json();
|
||||
const lista = document.getElementById('lista-pacientes-res');
|
||||
const datos = json.data || json.registros || [];
|
||||
if (!datos.length) {
|
||||
lista.innerHTML = `<div class="text-muted small mt-1">Sin resultados.
|
||||
<a href="#" onclick="abrirNuevoPaciente('${escHtml(q)}');return false">Crear nuevo</a></div>`;
|
||||
return;
|
||||
}
|
||||
lista.innerHTML = datos.map(p => `
|
||||
<div class="pac-resultado" onclick="seleccionarPaciente(${JSON.stringify(p).replace(/"/g,'"')})">
|
||||
<div class="pac-nombre">${escHtml((p.full_name||p.nombre||'') + ' ' + (p.apellido||''))}</div>
|
||||
<div class="pac-doc">${escHtml(p.tipo_documento||'')} ${escHtml(p.documento||p.numero_documento||'')}
|
||||
${p.telefono||p.celular ? '· ' + escHtml(p.telefono||p.celular) : ''}</div>
|
||||
</div>`).join('');
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function seleccionarPaciente(pac) {
|
||||
pacienteActivo = pac;
|
||||
document.getElementById('bloque-pac-no-vinculado').classList.add('d-none');
|
||||
document.getElementById('bloque-pac-seleccionado').classList.remove('d-none');
|
||||
document.getElementById('lbl-pac-nombre').textContent =
|
||||
(pac.full_name || (pac.nombre||'') + ' ' + (pac.apellido||'')).trim();
|
||||
document.getElementById('lbl-pac-doc').textContent =
|
||||
(pac.tipo_documento||'') + ' ' + (pac.documento||pac.numero_documento||'');
|
||||
document.getElementById('lbl-pac-cel').textContent =
|
||||
pac.telefono || pac.celular || '';
|
||||
document.getElementById('lista-pacientes-res').innerHTML = '';
|
||||
}
|
||||
|
||||
function desvincularPaciente() {
|
||||
pacienteActivo = null;
|
||||
document.getElementById('bloque-pac-no-vinculado').classList.remove('d-none');
|
||||
document.getElementById('bloque-pac-seleccionado').classList.add('d-none');
|
||||
document.getElementById('lista-pacientes-res').innerHTML = '';
|
||||
document.getElementById('inp-buscar-pac').value = '';
|
||||
}
|
||||
|
||||
function abrirNuevoPaciente(nombre) {
|
||||
window.open('<?= BASE_URL ?>lab_pacientes.php?nuevo=1&nombre=' + encodeURIComponent(nombre), '_blank');
|
||||
}
|
||||
|
||||
// ── Guardar solicitud ─────────────────────────────────────────
|
||||
async function guardarSolicitud() {
|
||||
if (!turnoActivo) return;
|
||||
if (!pacienteActivo) { mostrarError('Seleccione un paciente antes de guardar.'); return; }
|
||||
|
||||
const lugarId = parseInt(document.getElementById('sel-lugar').value);
|
||||
if (!lugarId) { mostrarError('Seleccione el lugar destino.'); return; }
|
||||
|
||||
const examIds = Array.from(document.querySelectorAll('.exam-chk:checked')).map(c => parseInt(c.value));
|
||||
if (!examIds.length) { mostrarError('Seleccione al menos un examen.'); return; }
|
||||
|
||||
const btn = document.getElementById('btn-guardar');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Guardando…';
|
||||
|
||||
try {
|
||||
const res = await fetch(API + 'create_solicitud.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
turno_id: turnoActivo.id,
|
||||
paciente_id: pacienteActivo.id,
|
||||
lugar_id: lugarId,
|
||||
exam_tipo_ids: examIds,
|
||||
total_cobrado: parseFloat(document.getElementById('inp-total').value) || null,
|
||||
metodo_pago: document.getElementById('sel-pago').value || null,
|
||||
observaciones: document.getElementById('inp-obs').value.trim() || null,
|
||||
}),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!json.ok) { mostrarError(json.error); return; }
|
||||
|
||||
solicitudActiva = json.data.solicitud;
|
||||
consentimientos = json.data.consentimientos_requeridos || [];
|
||||
|
||||
btn.innerHTML = '<i class="fas fa-check me-1"></i>Guardado';
|
||||
|
||||
// Mostrar sección de consentimientos si aplica
|
||||
if (consentimientos.length > 0) {
|
||||
renderConsentimientos(consentimientos);
|
||||
document.getElementById('sec-consentimientos').style.display = '';
|
||||
document.getElementById('btn-enviar-consent').classList.remove('d-none');
|
||||
}
|
||||
|
||||
document.getElementById('btn-pasar-lugar').classList.remove('d-none');
|
||||
document.getElementById('btn-guardar').classList.add('d-none');
|
||||
|
||||
} catch (err) {
|
||||
mostrarError(err.message);
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-save me-1"></i>Guardar solicitud';
|
||||
}
|
||||
}
|
||||
|
||||
function renderConsentimientos(lista) {
|
||||
document.getElementById('lista-consentimientos').innerHTML = lista.map(c => {
|
||||
const cls = c.estado === 'firmado' ? 'firmado'
|
||||
: c.estado === 'enviado' ? 'enviado'
|
||||
: 'pendiente';
|
||||
const ico = c.estado === 'firmado' ? 'fa-check-circle'
|
||||
: c.estado === 'enviado' ? 'fa-envelope'
|
||||
: 'fa-clock';
|
||||
return `<div class="consent-item ${cls}">
|
||||
<i class="fas ${ico}"></i>
|
||||
<span class="flex-1">${escHtml(c.formulario_nombre || 'Consentimiento')}</span>
|
||||
<span class="badge">${escHtml(c.estado)}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// ── Enviar consentimientos ────────────────────────────────────
|
||||
async function enviarConsentimientos() {
|
||||
if (!turnoActivo || !solicitudActiva) return;
|
||||
const btn = document.getElementById('btn-enviar-consent');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Enviando…';
|
||||
try {
|
||||
const res = await fetch(API + 'send_consentimiento.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ turno_id: turnoActivo.id }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!json.ok) { mostrarError(json.error); return; }
|
||||
|
||||
consentimientos = json.data.consentimientos || consentimientos;
|
||||
renderConsentimientos(consentimientos);
|
||||
btn.innerHTML = '<i class="fas fa-check me-1"></i>Enviado';
|
||||
} catch (err) {
|
||||
mostrarError(err.message);
|
||||
btn.innerHTML = '<i class="fas fa-paper-plane me-1"></i>Enviar consentimientos';
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Pasar a lugar ─────────────────────────────────────────────
|
||||
async function pasarALugar() {
|
||||
if (!turnoActivo || !solicitudActiva) return;
|
||||
|
||||
const btn = document.getElementById('btn-pasar-lugar');
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
const res = await fetch(API + 'cambiar_estado.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
turno_id: turnoActivo.id,
|
||||
nuevo_estado: 'en_espera_lugar',
|
||||
lugar_id: solicitudActiva.lugar_id,
|
||||
}),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!json.ok) { mostrarError(json.error); btn.disabled = false; return; }
|
||||
|
||||
// Reiniciar ficha
|
||||
turnoActivo = null;
|
||||
solicitudActiva = null;
|
||||
document.getElementById('ficha-turno').classList.add('d-none');
|
||||
document.getElementById('ficha-placeholder').classList.remove('d-none');
|
||||
document.getElementById('badge-turno-activo').classList.add('d-none');
|
||||
cargarCola();
|
||||
|
||||
} catch (err) {
|
||||
mostrarError(err.message);
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Ausente ───────────────────────────────────────────────────
|
||||
async function marcarAusente() {
|
||||
if (!turnoActivo) return;
|
||||
if (!confirm(`¿Marcar turno ${turnoActivo.codigo} como AUSENTE?`)) return;
|
||||
|
||||
await fetch(API + 'cambiar_estado.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ turno_id: turnoActivo.id, nuevo_estado: 'ausente' }),
|
||||
});
|
||||
|
||||
turnoActivo = null;
|
||||
solicitudActiva = null;
|
||||
document.getElementById('ficha-turno').classList.add('d-none');
|
||||
document.getElementById('ficha-placeholder').classList.remove('d-none');
|
||||
document.getElementById('badge-turno-activo').classList.add('d-none');
|
||||
cargarCola();
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────
|
||||
function resetCheckboxes() {
|
||||
document.querySelectorAll('.exam-chk').forEach(c => c.checked = false);
|
||||
}
|
||||
function toggleTodosExamenes(val) {
|
||||
document.querySelectorAll('.exam-chk').forEach(c => c.checked = val);
|
||||
}
|
||||
|
||||
function escHtml(str) {
|
||||
const d = document.createElement('div');
|
||||
d.appendChild(document.createTextNode(String(str)));
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
function mostrarError(msg) {
|
||||
alert('Error: ' + msg);
|
||||
}
|
||||
function mostrarAviso(titulo, msg) {
|
||||
alert(titulo + '\n' + msg);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user