La miniatura era de 90x60 con recorte, así que el material cuadrado se veía achatado en Configuración y no coincidía con lo que después salía al aire. Ahora es cuadrada, igual que la pantalla. La carga en sí no necesitaba cambios: no valida proporciones ni dimensiones, solo formato y tamaño, así que un video cuadrado siempre entró bien. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1857 lines
102 KiB
PHP
1857 lines
102 KiB
PHP
<?php
|
|
/**
|
|
* modules/turnero/views/configuracion.php
|
|
* Panel de configuración del módulo Turnero — 4 pestañas:
|
|
* TAB 1: Lugares / estaciones
|
|
* TAB 2: Exámenes y Consentimientos
|
|
* TAB 3: Prioridades
|
|
* TAB 4: Sesión y WhatsApp
|
|
* TAB 5: Pantalla TV (video de fondo)
|
|
*/
|
|
require_once __DIR__ . '/../../../config/config.php';
|
|
|
|
if (!isUserLoggedIn()) { header('Location: ' . BASE_URL . 'login.php'); exit; }
|
|
|
|
require_once __DIR__ . '/../_acceso.php';
|
|
turneroExigirRol(['supervisor']);
|
|
|
|
$db = Database::getInstance();
|
|
$pdo = $db->getConnection();
|
|
|
|
$_myIp = trim(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR'] ?? '')[0]);
|
|
|
|
// ── Cargar datos para los tabs (con fallback para tablas aún no migradas) ──
|
|
$lugares = [];
|
|
$prioridades = [];
|
|
$examTipos = [];
|
|
$formulariosCons= [];
|
|
$sesiones = [];
|
|
$sesionHoy = false;
|
|
$cfg = [];
|
|
$_loadErrors = [];
|
|
|
|
try {
|
|
$lugares = $pdo->query(
|
|
'SELECT * FROM turnero_lugares ORDER BY sort_order ASC, id ASC'
|
|
)->fetchAll(PDO::FETCH_ASSOC);
|
|
// Cargar formularios de consentimiento vinculados a cada lugar
|
|
$lugarFormIds = [];
|
|
foreach ($lugares as $lu) {
|
|
$stmtLcf = $pdo->prepare(
|
|
"SELECT formulario_id FROM turnero_lugar_consentimientos WHERE lugar_id = ?"
|
|
);
|
|
$stmtLcf->execute([$lu['id']]);
|
|
$lugarFormIds[(int)$lu['id']] = $stmtLcf->fetchAll(PDO::FETCH_COLUMN);
|
|
}
|
|
} catch (\Throwable $e) { $_loadErrors[] = 'turnero_lugares: ' . $e->getMessage(); $lugarFormIds = []; }
|
|
|
|
$dispositivos = [];
|
|
try {
|
|
$dispositivos = $pdo->query(
|
|
"SELECT d.*, l.nombre AS lugar_nombre
|
|
FROM turnero_dispositivos d
|
|
LEFT JOIN turnero_lugares l ON l.id = d.lugar_id
|
|
ORDER BY d.activo DESC, l.sort_order ASC, d.nombre ASC"
|
|
)->fetchAll(PDO::FETCH_ASSOC);
|
|
} catch (\Throwable $e) { $_loadErrors[] = 'turnero_dispositivos: ' . $e->getMessage(); }
|
|
|
|
try {
|
|
$prioridades = $pdo->query(
|
|
'SELECT * FROM turnero_prioridades ORDER BY orden_peso ASC'
|
|
)->fetchAll(PDO::FETCH_ASSOC);
|
|
} catch (\Throwable $e) { $_loadErrors[] = 'turnero_prioridades: ' . $e->getMessage(); }
|
|
|
|
try {
|
|
$examTipos = $pdo->query(
|
|
'SELECT et.*, GROUP_CONCAT(etc.formulario_id) AS form_ids
|
|
FROM exam_tipos et
|
|
LEFT JOIN exam_tipo_consentimientos etc ON etc.exam_tipo_id = et.id
|
|
GROUP BY et.id
|
|
ORDER BY et.categoria ASC, et.nombre ASC'
|
|
)->fetchAll(PDO::FETCH_ASSOC);
|
|
} catch (\Throwable $e) { $_loadErrors[] = 'exam_tipos: ' . $e->getMessage(); }
|
|
|
|
try {
|
|
$formulariosCons = $pdo->query(
|
|
"SELECT id, nombre FROM lab_formularios WHERE is_active=1 ORDER BY nombre ASC"
|
|
)->fetchAll(PDO::FETCH_ASSOC);
|
|
} catch (\Throwable $e) { $_loadErrors[] = 'lab_formularios: ' . $e->getMessage(); }
|
|
|
|
try {
|
|
$sesiones = $pdo->query(
|
|
'SELECT s.*,
|
|
a1.full_name AS abierto_nombre,
|
|
a2.full_name AS cerrado_nombre
|
|
FROM turnero_sesiones s
|
|
LEFT JOIN admin_users a1 ON a1.id = s.abierto_por
|
|
LEFT JOIN admin_users a2 ON a2.id = s.cerrado_por
|
|
ORDER BY s.fecha DESC
|
|
LIMIT 30'
|
|
)->fetchAll(PDO::FETCH_ASSOC);
|
|
} catch (\Throwable $e) { $_loadErrors[] = 'turnero_sesiones: ' . $e->getMessage(); }
|
|
|
|
try {
|
|
$stmtHoy = $pdo->prepare(
|
|
"SELECT s.*,
|
|
(SELECT COUNT(*) FROM turnero_turnos WHERE sesion_id = s.id) AS total_turnos
|
|
FROM turnero_sesiones s
|
|
WHERE s.fecha = CURDATE()"
|
|
);
|
|
$stmtHoy->execute();
|
|
$sesionHoy = $stmtHoy->fetch(PDO::FETCH_ASSOC);
|
|
} catch (\Throwable $e) { $_loadErrors[] = 'sesion_hoy: ' . $e->getMessage(); }
|
|
|
|
try {
|
|
$cfgRows = $pdo->query("SELECT clave, valor FROM lab_config WHERE clave LIKE 'turnero_%'")->fetchAll(PDO::FETCH_ASSOC);
|
|
foreach ($cfgRows as $r) { $cfg[$r['clave']] = $r['valor']; }
|
|
} catch (\Throwable $e) { $_loadErrors[] = 'lab_config: ' . $e->getMessage(); }
|
|
|
|
$tvMedia = [];
|
|
try {
|
|
$tvMedia = $pdo->query(
|
|
"SELECT id, tipo, url, orden, duracion_segundos FROM turnero_tv_media ORDER BY orden ASC"
|
|
)->fetchAll(PDO::FETCH_ASSOC);
|
|
} catch (\Throwable $e) { $_loadErrors[] = 'turnero_tv_media: ' . $e->getMessage(); }
|
|
|
|
$plantillasAprobadas = [];
|
|
$plantillasHabilitadas = [];
|
|
try {
|
|
$plantillasAprobadas = $pdo->query(
|
|
"SELECT id, name, template_name, language_code, body_text
|
|
FROM message_templates WHERE status='approved' ORDER BY name ASC"
|
|
)->fetchAll(PDO::FETCH_ASSOC);
|
|
$pdo->exec("CREATE TABLE IF NOT EXISTS turnero_chat_plantillas (
|
|
template_id INT NOT NULL, PRIMARY KEY (template_id)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
|
$plantillasHabilitadas = array_column(
|
|
$pdo->query("SELECT template_id FROM turnero_chat_plantillas")->fetchAll(PDO::FETCH_ASSOC),
|
|
'template_id'
|
|
);
|
|
} catch (\Throwable $e) { $_loadErrors[] = 'plantillas_chat: ' . $e->getMessage(); }
|
|
|
|
$tab = $_GET['tab'] ?? 'lugares';
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Configuración Turnero</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
|
<style>
|
|
body { background: #f1f5f9; }
|
|
.page-header { background:#fff; border-bottom:1px solid #e2e8f0; padding:16px 24px; display:flex; align-items:center; gap:12px; }
|
|
.page-header h1 { font-size:1.25rem; font-weight:700; color:#1e293b; margin:0; }
|
|
.page-header .back-btn { color:#64748b; text-decoration:none; font-size:.9rem; }
|
|
.page-header .back-btn:hover { color:#0f172a; }
|
|
.content-wrap { max-width:1000px; margin:24px auto; padding:0 16px; }
|
|
.nav-tabs .nav-link { color:#475569; font-weight:500; }
|
|
.nav-tabs .nav-link.active { color:#1565c0; border-bottom:2px solid #1565c0; }
|
|
.tab-card { background:#fff; border:1px solid #e2e8f0; border-top:none; border-radius:0 0 10px 10px; padding:24px; }
|
|
.section-title { font-size:.75rem; font-weight:700; text-transform:uppercase; letter-spacing:.08em; color:#64748b; margin:0 0 14px; }
|
|
.item-row { display:flex; align-items:flex-start; flex-wrap:wrap; gap:6px 8px; padding:10px 0; border-bottom:1px solid #f1f5f9; }
|
|
.item-row:last-child { border-bottom:none; }
|
|
.item-row .item-name { flex:1; min-width:200px; font-weight:500; color:#1e293b; }
|
|
.item-row .item-actions { display:flex; gap:4px; margin-left:auto; flex-shrink:0; }
|
|
.btn-icon { border:none; background:none; color:#94a3b8; cursor:pointer; padding:4px 6px; border-radius:6px; }
|
|
.btn-icon:hover { background:#f1f5f9; color:#475569; }
|
|
.badge-color { width:14px; height:14px; border-radius:50%; display:inline-block; flex-shrink:0; }
|
|
.form-add { background:#f8fafc; border:1.5px dashed #cbd5e1; border-radius:8px; padding:16px; margin-top:16px; }
|
|
.url-box { font-family:monospace; font-size:.78rem; background:#f0f4ff; border:1px solid #c7d2fe; border-radius:6px; padding:6px 10px; color:#3730a3; word-break:break-all; }
|
|
.url-row { display:flex; align-items:center; gap:8px; }
|
|
.toast-stack { position:fixed; bottom:24px; right:24px; z-index:9999; display:flex; flex-direction:column; gap:8px; }
|
|
.session-badge { font-size:.75rem; padding:2px 8px; border-radius:99px; font-weight:600; }
|
|
.session-open { background:#dcfce7; color:#15803d; }
|
|
.session-closed{ background:#f1f5f9; color:#64748b; }
|
|
.consent-tag { font-size:.7rem; background:#ede9fe; color:#5b21b6; border-radius:99px; padding:2px 8px; white-space:normal; word-break:break-word; max-width:260px; }
|
|
.plantilla-chk-row { display:flex; align-items:flex-start; gap:.65rem; padding:.6rem .8rem; border:1.5px solid #e2e8f0; border-radius:8px; cursor:pointer; background:#fff; transition:border-color .15s,background .15s; }
|
|
.plantilla-chk-row:hover { background:#f8fafc; border-color:#cbd5e1; }
|
|
.plantilla-chk-row.is-checked { border-color:#3b82f6; background:#eff6ff; }
|
|
.plantilla-chk-row input[type=checkbox] { margin-top:.2rem; flex-shrink:0; accent-color:#3b82f6; }
|
|
.plantilla-chk-body { display:flex; flex-direction:column; gap:.1rem; min-width:0; }
|
|
.plantilla-chk-name { font-size:.875rem; font-weight:600; color:#1e293b; }
|
|
.plantilla-chk-meta { font-size:.75rem; color:#64748b; }
|
|
.plantilla-chk-preview { font-size:.75rem; color:#94a3b8; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:480px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="page-header">
|
|
<a href="<?= BASE_URL ?>erp.php?m=turnero&v=dashboard" class="back-btn">
|
|
<i class="fas fa-arrow-left me-1"></i>Volver
|
|
</a>
|
|
<h1><i class="fas fa-cogs me-2 text-primary"></i>Configuración del Turnero</h1>
|
|
</div>
|
|
|
|
<div class="content-wrap">
|
|
<?php if (!empty($_loadErrors)): ?>
|
|
<div class="alert alert-warning mb-3">
|
|
<strong><i class="fas fa-exclamation-triangle me-1"></i>Algunas tablas aún no existen (ejecuta las migraciones pendientes):</strong>
|
|
<ul class="mb-0 mt-1">
|
|
<?php foreach ($_loadErrors as $le): ?>
|
|
<li><code><?= htmlspecialchars($le) ?></code></li>
|
|
<?php endforeach; ?>
|
|
</ul>
|
|
</div>
|
|
<?php endif; ?>
|
|
<ul class="nav nav-tabs" id="cfgTabs">
|
|
<li class="nav-item">
|
|
<a class="nav-link <?= $tab==='lugares'?'active':'' ?>" href="erp.php?m=turnero&v=configuracion&tab=lugares">
|
|
<i class="fas fa-map-marker-alt me-1"></i>Lugares
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link <?= $tab==='examenes'?'active':'' ?>" href="erp.php?m=turnero&v=configuracion&tab=examenes">
|
|
<i class="fas fa-flask me-1"></i>Exámenes y Consentimientos
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link <?= $tab==='prioridades'?'active':'' ?>" href="erp.php?m=turnero&v=configuracion&tab=prioridades">
|
|
<i class="fas fa-sort-amount-up me-1"></i>Prioridades
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link <?= $tab==='sesion'?'active':'' ?>" href="erp.php?m=turnero&v=configuracion&tab=sesion">
|
|
<i class="fas fa-calendar-day me-1"></i>Sesión y WhatsApp
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link <?= $tab==='tv'?'active':'' ?>" href="erp.php?m=turnero&v=configuracion&tab=tv">
|
|
<i class="fas fa-tv me-1"></i>Pantalla TV
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
|
|
<div class="tab-card">
|
|
|
|
<!-- ════════════════════════════════════════
|
|
TAB 1 — LUGARES
|
|
════════════════════════════════════════ -->
|
|
<?php if ($tab === 'lugares'):
|
|
$lugaresRec = array_values(array_filter($lugares, fn($l) => ($l['tipo'] ?? 'muestras') === 'recepcion'));
|
|
$lugaresMues = array_values(array_filter($lugares, fn($l) => ($l['tipo'] ?? 'muestras') === 'muestras'));
|
|
?>
|
|
|
|
<!-- ── Escritorios de Recepción ─────────────────── -->
|
|
<div class="d-flex align-items-center gap-2 mb-2">
|
|
<i class="fas fa-door-open text-primary"></i>
|
|
<p class="section-title mb-0">Escritorios de Recepción</p>
|
|
<span class="badge bg-primary-subtle text-primary"><?= count($lugaresRec) ?></span>
|
|
</div>
|
|
<p class="text-muted small mb-3">Cada escritorio es una ventanilla donde un recepcionista atiende pacientes. Aparecerá en la columna izquierda de la pantalla TV global.</p>
|
|
|
|
<div id="lista-recepcion">
|
|
<?php foreach ($lugaresRec as $lu): ?>
|
|
<div class="item-row" data-id="<?= $lu['id'] ?>">
|
|
<span class="drag-handle text-muted me-1" style="cursor:grab"><i class="fas fa-grip-vertical"></i></span>
|
|
<i class="fas fa-door-open text-primary me-1" style="font-size:.8rem"></i>
|
|
<span class="item-name"><?= htmlspecialchars($lu['nombre']) ?></span>
|
|
<?php if ($lu['descripcion']): ?>
|
|
<small class="text-muted d-none d-md-inline"><?= htmlspecialchars($lu['descripcion']) ?></small>
|
|
<?php endif; ?>
|
|
<span class="badge <?= $lu['activo'] ? 'bg-success-subtle text-success' : 'bg-secondary-subtle text-secondary' ?> ms-1">
|
|
<?= $lu['activo'] ? 'Activo' : 'Inactivo' ?>
|
|
</span>
|
|
<button class="btn-icon text-primary" onclick="editarLugar(<?= $lu['id'] ?>,'<?= addslashes($lu['nombre']) ?>','<?= addslashes($lu['descripcion'] ?? '') ?>',<?= $lu['activo'] ?>,<?= $lu['sort_order'] ?>,'recepcion','<?= $lu['formulario_modo'] ?? 'link' ?>')" title="Editar">
|
|
<i class="fas fa-pencil-alt"></i>
|
|
</button>
|
|
<button class="btn-icon text-danger" onclick="eliminarLugar(<?= $lu['id'] ?>, '<?= addslashes($lu['nombre']) ?>')" title="Eliminar">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
<?php if (empty($lugaresRec)): ?>
|
|
<p class="text-muted text-center py-2 small">No hay escritorios de recepción configurados</p>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="form-add mt-2 mb-4 p-3" style="background:#eff6ff;border-radius:10px;border:1px solid #bfdbfe">
|
|
<p class="section-title mb-3"><i class="fas fa-plus me-1"></i>Agregar escritorio de recepción</p>
|
|
<div class="row g-2 align-items-end">
|
|
<div class="col-md-5">
|
|
<label class="form-label small fw-semibold">Nombre <span class="text-danger">*</span></label>
|
|
<input type="text" id="rec-nombre" class="form-control form-control-sm" placeholder="Recepción 1, Ventanilla 2…">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label small fw-semibold">Descripción</label>
|
|
<input type="text" id="rec-desc" class="form-control form-control-sm" placeholder="Opcional">
|
|
</div>
|
|
<div class="col-md-1">
|
|
<label class="form-label small fw-semibold">Orden</label>
|
|
<input type="number" id="rec-orden" class="form-control form-control-sm" value="<?= count($lugaresRec) + 1 ?>" min="1">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<button class="btn btn-primary btn-sm w-100" onclick="guardarLugar('recepcion')">
|
|
<i class="fas fa-save me-1"></i>Guardar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<hr class="my-3">
|
|
|
|
<!-- ── Estaciones de Toma de Muestras ──────────── -->
|
|
<div class="d-flex align-items-center gap-2 mb-2">
|
|
<i class="fas fa-vials text-success"></i>
|
|
<p class="section-title mb-0">Estaciones de Toma de Muestras</p>
|
|
<span class="badge bg-success-subtle text-success"><?= count($lugaresMues) ?></span>
|
|
</div>
|
|
<p class="text-muted small mb-3">Cada estación es donde se toman muestras o se realizan pruebas. Aparecerá en la columna derecha de la pantalla TV global.</p>
|
|
|
|
<div id="lista-muestras">
|
|
<?php foreach ($lugaresMues as $lu): ?>
|
|
<div class="item-row" data-id="<?= $lu['id'] ?>">
|
|
<span class="drag-handle text-muted me-1" style="cursor:grab"><i class="fas fa-grip-vertical"></i></span>
|
|
<i class="fas fa-flask text-success me-1" style="font-size:.8rem"></i>
|
|
<span class="item-name"><?= htmlspecialchars($lu['nombre']) ?></span>
|
|
<?php if ($lu['descripcion']): ?>
|
|
<small class="text-muted d-none d-md-inline"><?= htmlspecialchars($lu['descripcion']) ?></small>
|
|
<?php endif; ?>
|
|
<span class="badge <?= $lu['activo'] ? 'bg-success-subtle text-success' : 'bg-secondary-subtle text-secondary' ?> ms-1">
|
|
<?= $lu['activo'] ? 'Activo' : 'Inactivo' ?>
|
|
</span>
|
|
<button class="btn-icon" onclick="copiarUrlDisplay(<?= $lu['id'] ?>)" title="Copiar URL pantalla TV de este lugar">
|
|
<i class="fas fa-tv"></i>
|
|
</button>
|
|
<button class="btn-icon text-primary" onclick="editarLugar(<?= $lu['id'] ?>,'<?= addslashes($lu['nombre']) ?>','<?= addslashes($lu['descripcion'] ?? '') ?>',<?= $lu['activo'] ?>,<?= $lu['sort_order'] ?>,'muestras','<?= $lu['formulario_modo'] ?? 'link' ?>')" title="Editar">
|
|
<i class="fas fa-pencil-alt"></i>
|
|
</button>
|
|
<button class="btn-icon text-danger" onclick="eliminarLugar(<?= $lu['id'] ?>, '<?= addslashes($lu['nombre']) ?>')" title="Eliminar">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
<?php if (empty($lugaresMues)): ?>
|
|
<p class="text-muted text-center py-2 small">No hay estaciones de muestras configuradas</p>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="form-add mt-2 mb-4 p-3" style="background:#f0fdf4;border-radius:10px;border:1px solid #bbf7d0">
|
|
<p class="section-title mb-3"><i class="fas fa-plus me-1"></i>Agregar estación de toma de muestras</p>
|
|
<div class="row g-2 align-items-end">
|
|
<div class="col-md-5">
|
|
<label class="form-label small fw-semibold">Nombre <span class="text-danger">*</span></label>
|
|
<input type="text" id="mue-nombre" class="form-control form-control-sm" placeholder="Toma de Muestras 1, Rayos X…">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label small fw-semibold">Descripción</label>
|
|
<input type="text" id="mue-desc" class="form-control form-control-sm" placeholder="Opcional">
|
|
</div>
|
|
<div class="col-md-1">
|
|
<label class="form-label small fw-semibold">Orden</label>
|
|
<input type="number" id="mue-orden" class="form-control form-control-sm" value="<?= count($lugaresMues) + 1 ?>" min="1">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<button class="btn btn-success btn-sm w-100" onclick="guardarLugar('muestras')">
|
|
<i class="fas fa-save me-1"></i>Guardar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal editar lugar -->
|
|
<div class="modal fade" id="modalEditarLugar" tabindex="-1">
|
|
<div class="modal-dialog modal-sm">
|
|
<div class="modal-content">
|
|
<div class="modal-header py-2 px-3">
|
|
<h6 class="modal-title" id="modal-lugar-titulo">Editar lugar</h6>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<input type="hidden" id="edit-lu-id">
|
|
<input type="hidden" id="edit-lu-tipo" value="muestras">
|
|
<div class="mb-2">
|
|
<label class="form-label small fw-semibold">Nombre</label>
|
|
<input type="text" id="edit-lu-nombre" class="form-control form-control-sm">
|
|
</div>
|
|
<div class="mb-2">
|
|
<label class="form-label small fw-semibold">Descripción</label>
|
|
<input type="text" id="edit-lu-desc" class="form-control form-control-sm">
|
|
</div>
|
|
<div class="mb-2">
|
|
<label class="form-label small fw-semibold">Orden</label>
|
|
<input type="number" id="edit-lu-orden" class="form-control form-control-sm" min="1">
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="edit-lu-activo">
|
|
<label class="form-check-label small" for="edit-lu-activo">Activo</label>
|
|
</div>
|
|
<div class="mb-2 mt-3">
|
|
<label class="form-label small fw-semibold">
|
|
<i class="fas fa-file-signature me-1 text-warning"></i>
|
|
Formularios de consentimiento
|
|
</label>
|
|
<div id="edit-lu-consents" class="border rounded p-2" style="max-height:160px;overflow-y:auto;background:#fffbeb">
|
|
<?php foreach ($formulariosCons as $fc): ?>
|
|
<div class="form-check">
|
|
<input class="form-check-input edit-lu-consent-chk"
|
|
type="checkbox"
|
|
value="<?= (int)$fc['id'] ?>"
|
|
id="edit-lu-fc-<?= (int)$fc['id'] ?>">
|
|
<label class="form-check-label small" for="edit-lu-fc-<?= (int)$fc['id'] ?>">
|
|
<?= htmlspecialchars($fc['nombre']) ?>
|
|
</label>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
<?php if (empty($formulariosCons)): ?>
|
|
<small class="text-muted">No hay formularios activos configurados.</small>
|
|
<?php endif; ?>
|
|
</div>
|
|
<div class="mt-2">
|
|
<label class="form-label small fw-semibold mb-1">Modo de presentación del formulario</label>
|
|
<div class="d-flex gap-3">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="radio" name="edit-lu-form-modo" id="edit-lu-modo-link" value="link" checked>
|
|
<label class="form-check-label small" for="edit-lu-modo-link">
|
|
<i class="fas fa-link me-1 text-primary"></i>Enviar por link / WA
|
|
</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="radio" name="edit-lu-form-modo" id="edit-lu-modo-embebido" value="embebido">
|
|
<label class="form-check-label small" for="edit-lu-modo-embebido">
|
|
<i class="fas fa-window-maximize me-1 text-success"></i>Embebido en pantalla
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer py-2 px-3">
|
|
<button class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancelar</button>
|
|
<button class="btn btn-primary btn-sm" onclick="guardarEditarLugar()">Guardar</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- URLs de acceso -->
|
|
<hr class="my-4">
|
|
<p class="section-title"><i class="fas fa-desktop me-1"></i>URLs de pantalla TV</p>
|
|
|
|
<div class="mb-3 p-3" style="background:#fffbeb;border:1px solid #fde68a;border-radius:10px">
|
|
<div class="fw-semibold small mb-2"><i class="fas fa-star me-1 text-warning"></i>Pantalla Global (sala de espera — recomendada)</div>
|
|
<p class="text-muted small mb-2">Muestra TODOS los escritorios de recepción y estaciones de muestras en una sola pantalla.</p>
|
|
<div class="url-row">
|
|
<div class="url-box flex-1" id="url-global"
|
|
data-url="<?= htmlspecialchars(BASE_URL . 'erp.php?m=turnero&v=display_global') ?>"
|
|
><?= htmlspecialchars(BASE_URL . 'erp.php?m=turnero&v=display_global') ?></div>
|
|
<button class="btn btn-outline-secondary btn-sm" onclick="copiarUrl('url-global')"><i class="fas fa-copy"></i></button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-2">
|
|
<small class="fw-semibold text-muted d-block mb-1"><i class="fas fa-tablet-alt me-1"></i>Kiosko de turnos (tablet entrada)</small>
|
|
<div class="url-row">
|
|
<div class="url-box flex-1" id="url-kiosko"
|
|
data-url="<?= htmlspecialchars(BASE_URL . 'modules/turnero/views/kiosko.php') ?>"
|
|
><?= htmlspecialchars(BASE_URL . 'modules/turnero/views/kiosko.php') ?></div>
|
|
<button class="btn btn-outline-secondary btn-sm" onclick="copiarUrl('url-kiosko')"><i class="fas fa-copy"></i></button>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if (!empty($lugaresRec)): ?>
|
|
<div class="mb-3">
|
|
<small class="fw-semibold text-muted d-block mb-2"><i class="fas fa-user-tie me-1"></i>Gestión — Escritorios de Recepción</small>
|
|
<?php foreach ($lugaresRec as $lu): ?>
|
|
<div class="mb-2">
|
|
<small class="d-block mb-1 text-muted"><?= htmlspecialchars($lu['nombre']) ?></small>
|
|
<div class="url-row">
|
|
<div class="url-box flex-1" id="url-rec-<?= $lu['id'] ?>"
|
|
data-url="<?= htmlspecialchars(BASE_URL . 'erp.php?m=turnero&v=recepcion&desk_id=' . $lu['id']) ?>"
|
|
><?= htmlspecialchars(BASE_URL . 'erp.php?m=turnero&v=recepcion&desk_id=' . $lu['id']) ?></div>
|
|
<button class="btn btn-outline-secondary btn-sm" onclick="copiarUrl('url-rec-<?= $lu['id'] ?>')"><i class="fas fa-copy"></i></button>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if (!empty($lugaresMues)): ?>
|
|
<div class="mb-3">
|
|
<small class="fw-semibold text-muted d-block mb-2"><i class="fas fa-flask me-1"></i>Gestión — Estaciones de Toma de Muestras</small>
|
|
<?php foreach ($lugaresMues as $lu): ?>
|
|
<div class="mb-2">
|
|
<small class="d-block mb-1 text-muted"><?= htmlspecialchars($lu['nombre']) ?></small>
|
|
<div class="url-row">
|
|
<div class="url-box flex-1" id="url-mue-<?= $lu['id'] ?>"
|
|
data-url="<?= htmlspecialchars(BASE_URL . 'erp.php?m=turnero&v=lugar&lugar_id=' . $lu['id']) ?>"
|
|
><?= htmlspecialchars(BASE_URL . 'erp.php?m=turnero&v=lugar&lugar_id=' . $lu['id']) ?></div>
|
|
<button class="btn btn-outline-secondary btn-sm" onclick="copiarUrl('url-mue-<?= $lu['id'] ?>')"><i class="fas fa-copy"></i></button>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<!-- ── Dispositivos / Tablets ──────────────── -->
|
|
<hr class="my-4">
|
|
<div class="d-flex align-items-center gap-2 mb-2">
|
|
<i class="fas fa-tablet-alt text-secondary"></i>
|
|
<p class="section-title mb-0">Dispositivos / Tablets autorizadas</p>
|
|
<span class="badge bg-secondary-subtle text-secondary"><?= count($dispositivos) ?></span>
|
|
</div>
|
|
<p class="text-muted small mb-3">
|
|
Cada equipo queda bloqueado a su lugar asignado por token de navegador.
|
|
Si el equipo no aparece aquí, verá todos los lugares disponibles.
|
|
</p>
|
|
|
|
<div class="table-responsive mb-3">
|
|
<table class="table table-sm table-hover align-middle" style="font-size:.82rem">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Token / ID equipo</th>
|
|
<th>Nombre / descripción</th>
|
|
<th>Lugar asignado</th>
|
|
<th class="text-center">Estado</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="tabla-dispositivos">
|
|
<?php foreach ($dispositivos as $dv): ?>
|
|
<tr data-dv-id="<?= $dv['id'] ?>">
|
|
<td>
|
|
<?php if (!empty($dv['token'])): ?>
|
|
<code title="<?= htmlspecialchars($dv['token']) ?>"><?= htmlspecialchars(substr($dv['token'], 0, 8)) ?>…</code>
|
|
<?php else: ?>
|
|
<code class="text-muted"><?= htmlspecialchars($dv['ip'] ?: '—') ?></code>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td><?= htmlspecialchars($dv['nombre']) ?></td>
|
|
<td><?= htmlspecialchars($dv['lugar_nombre'] ?? '—') ?></td>
|
|
<td class="text-center">
|
|
<span class="badge <?= $dv['activo'] ? 'bg-success-subtle text-success' : 'bg-secondary-subtle text-secondary' ?>">
|
|
<?= $dv['activo'] ? 'Activo' : 'Inactivo' ?>
|
|
</span>
|
|
</td>
|
|
<td class="text-end" style="white-space:nowrap">
|
|
<button class="btn-icon text-primary"
|
|
onclick="editarDispositivo(<?= $dv['id'] ?>, '<?= addslashes($dv['ip'] ?? '') ?>', '<?= addslashes($dv['nombre']) ?>', <?= (int)$dv['lugar_id'] ?>, <?= (int)$dv['activo'] ?>, '<?= addslashes($dv['token'] ?? '') ?>')"
|
|
title="Editar">
|
|
<i class="fas fa-pencil-alt"></i>
|
|
</button>
|
|
<button class="btn-icon text-danger"
|
|
onclick="eliminarDispositivo(<?= $dv['id'] ?>, '<?= addslashes($dv['nombre']) ?>')"
|
|
title="Eliminar">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php if (empty($dispositivos)): ?>
|
|
<tr><td colspan="5" class="text-muted text-center py-3">No hay dispositivos registrados</td></tr>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="alert alert-primary py-2 px-3 mb-3 d-flex align-items-center gap-2 flex-wrap" style="font-size:.85rem">
|
|
<i class="fas fa-fingerprint"></i>
|
|
Token de <strong>este equipo</strong>: <code id="cfg-my-token" style="font-size:.8rem">cargando…</code>
|
|
<button class="btn btn-sm btn-primary ms-auto" onclick="registrarEsteEquipo()">
|
|
<i class="fas fa-plus me-1"></i>Registrar este equipo
|
|
</button>
|
|
</div>
|
|
|
|
<div class="form-add p-3" style="background:#f8fafc;border-radius:10px;border:1px solid #e2e8f0">
|
|
<p class="section-title mb-3"><i class="fas fa-plus me-1"></i>Agregar dispositivo</p>
|
|
<input type="hidden" id="dv-token">
|
|
<div class="row g-2 align-items-end">
|
|
<div class="col-md-4">
|
|
<label class="form-label small fw-semibold">Nombre / descripción <span class="text-danger">*</span></label>
|
|
<input type="text" id="dv-nombre" class="form-control form-control-sm" placeholder="Recepción 1 — María López">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label small fw-semibold">Lugar asignado <span class="text-danger">*</span></label>
|
|
<select id="dv-lugar" class="form-select form-select-sm">
|
|
<option value="">— Seleccionar —</option>
|
|
<?php foreach ($lugares as $lu): ?>
|
|
<option value="<?= $lu['id'] ?>">[<?= $lu['tipo'] === 'recepcion' ? 'Rec' : 'TM' ?>] <?= htmlspecialchars($lu['nombre']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<button class="btn btn-secondary btn-sm w-100" onclick="guardarDispositivo()">
|
|
<i class="fas fa-save me-1"></i>Guardar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal editar dispositivo -->
|
|
<div class="modal fade" id="modalEditarDispositivo" tabindex="-1">
|
|
<div class="modal-dialog modal-sm">
|
|
<div class="modal-content">
|
|
<div class="modal-header py-2 px-3">
|
|
<h6 class="modal-title">Editar dispositivo</h6>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<input type="hidden" id="edit-dv-id">
|
|
<input type="hidden" id="edit-dv-token">
|
|
<div class="mb-2">
|
|
<label class="form-label small fw-semibold">Nombre / descripción</label>
|
|
<input type="text" id="edit-dv-nombre" class="form-control form-control-sm">
|
|
</div>
|
|
<div class="mb-2">
|
|
<label class="form-label small fw-semibold">Lugar asignado</label>
|
|
<select id="edit-dv-lugar" class="form-select form-select-sm">
|
|
<option value="">— Seleccionar —</option>
|
|
<?php foreach ($lugares as $lu): ?>
|
|
<option value="<?= $lu['id'] ?>">[<?= $lu['tipo'] === 'recepcion' ? 'Rec' : 'TM' ?>] <?= htmlspecialchars($lu['nombre']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="edit-dv-activo" checked>
|
|
<label class="form-check-label small" for="edit-dv-activo">Activo</label>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer py-2 px-3">
|
|
<button class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancelar</button>
|
|
<button class="btn btn-primary btn-sm" onclick="guardarEditarDispositivo()">Guardar</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ════════════════════════════════════════
|
|
TAB 2 — EXÁMENES Y CONSENTIMIENTOS
|
|
════════════════════════════════════════ -->
|
|
<?php elseif ($tab === 'examenes'): ?>
|
|
<div class="d-flex align-items-center gap-3 mb-3">
|
|
<p class="section-title mb-0">Tipos de examen y sus formularios de consentimiento</p>
|
|
<div class="ms-auto" style="min-width:220px">
|
|
<input type="search" id="exam-buscar" class="form-control form-control-sm"
|
|
placeholder="Buscar examen…" autocomplete="off">
|
|
</div>
|
|
</div>
|
|
<?php
|
|
// Agrupar por categoría
|
|
$categorias = [];
|
|
foreach ($examTipos as $et) {
|
|
$cat = $et['categoria'] ?: 'Sin categoría';
|
|
$categorias[$cat][] = $et;
|
|
}
|
|
?>
|
|
<div id="exam-lista">
|
|
<?php foreach ($categorias as $cat => $items): ?>
|
|
<div class="mb-3">
|
|
<div class="fw-semibold text-uppercase small text-secondary mb-1" style="font-size:.7rem;letter-spacing:.07em">
|
|
<i class="fas fa-tag me-1"></i><?= htmlspecialchars($cat) ?>
|
|
</div>
|
|
<?php foreach ($items as $et):
|
|
$formIds = $et['form_ids'] ? array_map('intval', explode(',', $et['form_ids'])) : [];
|
|
?>
|
|
<div class="item-row" data-exam-id="<?= $et['id'] ?>">
|
|
<span class="item-name">
|
|
<span class="badge bg-primary-subtle text-primary me-2" style="font-size:.7rem"><?= htmlspecialchars($et['codigo']) ?></span>
|
|
<?= htmlspecialchars($et['nombre']) ?>
|
|
</span>
|
|
<?php if (!$et['activo']): ?>
|
|
<span class="badge bg-secondary-subtle text-secondary" style="font-size:.65rem">Inactivo</span>
|
|
<?php endif; ?>
|
|
<?php
|
|
// Etiquetas de consentimientos vinculados
|
|
foreach ($formIds as $fid):
|
|
$fNombre = '';
|
|
foreach ($formulariosCons as $fc) {
|
|
if ($fc['id'] == $fid) { $fNombre = $fc['nombre']; break; }
|
|
}
|
|
?>
|
|
<span class="consent-tag"><i class="fas fa-file-signature me-1"></i><?= htmlspecialchars($fNombre ?: 'Form #'.$fid) ?></span>
|
|
<?php endforeach; ?>
|
|
<span class="item-actions">
|
|
<button class="btn-icon text-primary" onclick="editarExamen(<?= $et['id'] ?>)" title="Editar">
|
|
<i class="fas fa-pencil-alt"></i>
|
|
</button>
|
|
<button class="btn-icon text-danger" onclick="eliminarExamen(<?= $et['id'] ?>, '<?= addslashes($et['nombre']) ?>')" title="Eliminar">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</span>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div><!-- #exam-lista -->
|
|
<div id="exam-pager"></div>
|
|
<?php if (empty($examTipos)): ?>
|
|
<p class="text-muted text-center py-3">No hay tipos de examen configurados</p>
|
|
<?php endif; ?>
|
|
|
|
<div class="form-add mt-4">
|
|
<p class="section-title mb-3"><i class="fas fa-plus me-1"></i>Agregar tipo de examen</p>
|
|
<div class="row g-2">
|
|
<div class="col-md-2">
|
|
<label class="form-label small fw-semibold">Código <span class="text-danger">*</span></label>
|
|
<input type="text" id="ex-codigo" class="form-control form-control-sm" placeholder="HEM" maxlength="20">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label small fw-semibold">Nombre <span class="text-danger">*</span></label>
|
|
<input type="text" id="ex-nombre" class="form-control form-control-sm" placeholder="Hemograma completo">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label small fw-semibold">Categoría</label>
|
|
<input type="text" id="ex-categoria" class="form-control form-control-sm" placeholder="Hematología">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label small fw-semibold">Consentimientos</label>
|
|
<div id="ex-forms-wrap" class="border rounded p-2" style="max-height:120px;overflow-y:auto;background:#fffbeb">
|
|
<?php foreach ($formulariosCons as $fc): ?>
|
|
<div class="form-check mb-1">
|
|
<input class="form-check-input ex-form-chk" type="checkbox"
|
|
value="<?= $fc['id'] ?>" id="ex-fc-<?= $fc['id'] ?>">
|
|
<label class="form-check-label small" for="ex-fc-<?= $fc['id'] ?>">
|
|
<?= htmlspecialchars($fc['nombre']) ?>
|
|
</label>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
<?php if (empty($formulariosCons)): ?>
|
|
<span class="text-muted small">Sin formularios activos</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
<div class="col-12 text-end">
|
|
<button class="btn btn-primary btn-sm" onclick="guardarExamen()">
|
|
<i class="fas fa-save me-1"></i>Guardar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if (!empty($formulariosCons)): ?>
|
|
<div class="mt-4">
|
|
<p class="section-title"><i class="fas fa-file-signature me-1"></i>Vista inversa: por consentimiento</p>
|
|
<?php foreach ($formulariosCons as $fc):
|
|
$exsVinculados = [];
|
|
foreach ($examTipos as $et) {
|
|
$fids = $et['form_ids'] ? array_map('intval', explode(',', $et['form_ids'])) : [];
|
|
if (in_array((int)$fc['id'], $fids)) $exsVinculados[] = $et;
|
|
}
|
|
?>
|
|
<div class="mb-2">
|
|
<span class="fw-semibold small"><?= htmlspecialchars($fc['nombre']) ?></span>
|
|
<span class="text-muted small ms-2">
|
|
<?= empty($exsVinculados)
|
|
? '<em>Sin exámenes vinculados</em>'
|
|
: implode(', ', array_map(fn($e)=>'<code>'.$e['codigo'].'</code>', $exsVinculados))
|
|
?>
|
|
</span>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<!-- Modal editar examen -->
|
|
<div class="modal fade" id="modalEditarExamen" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header py-2 px-3">
|
|
<h6 class="modal-title">Editar tipo de examen</h6>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<input type="hidden" id="edit-ex-id">
|
|
<div class="row g-2">
|
|
<div class="col-md-3">
|
|
<label class="form-label small fw-semibold">Código</label>
|
|
<input type="text" id="edit-ex-codigo" class="form-control form-control-sm" maxlength="20">
|
|
</div>
|
|
<div class="col-md-9">
|
|
<label class="form-label small fw-semibold">Nombre</label>
|
|
<input type="text" id="edit-ex-nombre" class="form-control form-control-sm">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label small fw-semibold">Categoría</label>
|
|
<input type="text" id="edit-ex-categoria" class="form-control form-control-sm">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label small fw-semibold">Consentimientos</label>
|
|
<div id="edit-ex-forms-wrap" class="border rounded p-2" style="max-height:120px;overflow-y:auto;background:#fffbeb">
|
|
<?php foreach ($formulariosCons as $fc): ?>
|
|
<div class="form-check mb-1">
|
|
<input class="form-check-input edit-ex-form-chk" type="checkbox"
|
|
value="<?= $fc['id'] ?>" id="edit-ex-fc-<?= $fc['id'] ?>">
|
|
<label class="form-check-label small" for="edit-ex-fc-<?= $fc['id'] ?>">
|
|
<?= htmlspecialchars($fc['nombre']) ?>
|
|
</label>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
<?php if (empty($formulariosCons)): ?>
|
|
<span class="text-muted small">Sin formularios activos</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
<div class="col-12">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="edit-ex-activo">
|
|
<label class="form-check-label small" for="edit-ex-activo">Activo</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer py-2 px-3">
|
|
<button class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancelar</button>
|
|
<button class="btn btn-primary btn-sm" onclick="guardarEditarExamen()">Guardar</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ════════════════════════════════════════
|
|
TAB 3 — PRIORIDADES
|
|
════════════════════════════════════════ -->
|
|
<?php elseif ($tab === 'prioridades'): ?>
|
|
<p class="section-title">Códigos de prioridad (menor orden_peso = mayor prioridad)</p>
|
|
|
|
<div id="lista-prioridades">
|
|
<?php foreach ($prioridades as $pr): ?>
|
|
<div class="item-row" data-id="<?= $pr['id'] ?>">
|
|
<span class="badge-color me-2" style="background:<?= htmlspecialchars($pr['color']) ?>"></span>
|
|
<span class="fw-bold me-2" style="min-width:28px; color:<?= htmlspecialchars($pr['color']) ?>"><?= htmlspecialchars($pr['codigo']) ?></span>
|
|
<span class="item-name"><?= htmlspecialchars($pr['nombre']) ?></span>
|
|
<small class="text-muted me-2">Orden: <?= $pr['orden_peso'] ?></small>
|
|
<span class="badge <?= $pr['activo'] ? 'bg-success-subtle text-success' : 'bg-secondary-subtle text-secondary' ?>">
|
|
<?= $pr['activo'] ? 'Activo' : 'Inactivo' ?>
|
|
</span>
|
|
<button class="btn-icon text-primary" onclick="editarPrioridad(<?= $pr['id'] ?>,'<?= addslashes($pr['codigo']) ?>','<?= addslashes($pr['nombre']) ?>','<?= htmlspecialchars($pr['color']) ?>','<?= htmlspecialchars($pr['icono'] ?? '') ?>','<?= addslashes($pr['descripcion'] ?? '') ?>',<?= $pr['orden_peso'] ?>,<?= $pr['activo'] ?>)" title="Editar">
|
|
<i class="fas fa-pencil-alt"></i>
|
|
</button>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<p class="text-muted small mt-2">
|
|
<i class="fas fa-info-circle me-1"></i>
|
|
Los códigos de prioridad no se pueden eliminar (están vinculados a turnos). Solo puede editarlos.
|
|
</p>
|
|
|
|
<!-- Modal editar prioridad -->
|
|
<div class="modal fade" id="modalEditarPrioridad" tabindex="-1">
|
|
<div class="modal-dialog modal-sm">
|
|
<div class="modal-content">
|
|
<div class="modal-header py-2 px-3">
|
|
<h6 class="modal-title">Editar prioridad</h6>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<input type="hidden" id="edit-pr-id">
|
|
<div class="mb-2">
|
|
<label class="form-label small fw-semibold">Código</label>
|
|
<input type="text" id="edit-pr-codigo" class="form-control form-control-sm" maxlength="1" readonly>
|
|
<small class="text-muted">El código no puede cambiarse</small>
|
|
</div>
|
|
<div class="mb-2">
|
|
<label class="form-label small fw-semibold">Nombre</label>
|
|
<input type="text" id="edit-pr-nombre" class="form-control form-control-sm">
|
|
</div>
|
|
<div class="row g-2">
|
|
<div class="col-6">
|
|
<label class="form-label small fw-semibold">Color</label>
|
|
<div class="input-group input-group-sm">
|
|
<input type="color" id="edit-pr-color" class="form-control form-control-color" style="width:40px;padding:2px">
|
|
<input type="text" id="edit-pr-color-text" class="form-control form-control-sm" maxlength="7" placeholder="#6b7280">
|
|
</div>
|
|
</div>
|
|
<div class="col-6">
|
|
<label class="form-label small fw-semibold">Orden</label>
|
|
<input type="number" id="edit-pr-orden" class="form-control form-control-sm" min="1">
|
|
</div>
|
|
</div>
|
|
<div class="mb-2">
|
|
<label class="form-label small fw-semibold">Icono</label>
|
|
<select id="edit-pr-icono" class="form-select form-select-sm">
|
|
<option value="">— Sin icono —</option>
|
|
<option value="fas fa-child">👶 Niños (fas fa-child)</option>
|
|
<option value="fa-solid fa-person-pregnant">🤰 Embarazada (fa-person-pregnant)</option>
|
|
<option value="fas fa-heart">❤ Embarazada alt (fas fa-heart)</option>
|
|
<option value="fas fa-person-cane">🦯 Adulto mayor (fas fa-person-cane)</option>
|
|
<option value="fas fa-wheelchair">♿ Discapacidad (fas fa-wheelchair)</option>
|
|
<option value="fas fa-user">👤 Paciente general (fas fa-user)</option>
|
|
<option value="fas fa-vial">🧪 Muestra (fas fa-vial)</option>
|
|
<option value="fas fa-heartbeat">💓 Embarazada alt (fas fa-heartbeat)</option>
|
|
<option value="fas fa-venus">♀ Mujer (fas fa-venus)</option>
|
|
<option value="fas fa-baby">🍼 Bebé (fas fa-baby)</option>
|
|
<option value="fas fa-star">⭐ VIP (fas fa-star)</option>
|
|
</select>
|
|
</div>
|
|
<div class="mb-2">
|
|
<label class="form-label small fw-semibold">Descripción</label>
|
|
<textarea id="edit-pr-descripcion" class="form-control form-control-sm" rows="2" maxlength="200" placeholder="Descripción para el kiosko"></textarea>
|
|
<small class="text-muted">Se muestra bajo el nombre en el kiosko de turnos</small>
|
|
</div>
|
|
<div class="form-check mt-2">
|
|
<input class="form-check-input" type="checkbox" id="edit-pr-activo">
|
|
<label class="form-check-label small" for="edit-pr-activo">Activo</label>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer py-2 px-3">
|
|
<button class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancelar</button>
|
|
<button class="btn btn-primary btn-sm" onclick="guardarPrioridad()">Guardar</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ════════════════════════════════════════
|
|
TAB 4 — SESIÓN Y WHATSAPP
|
|
════════════════════════════════════════ -->
|
|
<?php elseif ($tab === 'sesion'): ?>
|
|
|
|
<!-- Sesión de hoy -->
|
|
<div class="mb-4">
|
|
<p class="section-title"><i class="fas fa-calendar-day me-1"></i>Sesión de hoy</p>
|
|
<?php if ($sesionHoy): ?>
|
|
<div class="d-flex align-items-center gap-3 flex-wrap">
|
|
<div>
|
|
<span class="session-badge <?= $sesionHoy['fin_at'] ? 'session-closed' : 'session-open' ?>">
|
|
<?= $sesionHoy['fin_at'] ? 'Cerrada' : 'Abierta' ?>
|
|
</span>
|
|
</div>
|
|
<div class="small text-muted">
|
|
Fecha: <strong><?= htmlspecialchars($sesionHoy['fecha']) ?></strong> •
|
|
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>Notificaciones WhatsApp</p>
|
|
|
|
<div class="mb-3 p-3" style="background:#fffbeb;border:1px solid #fde68a;border-radius:10px">
|
|
<div class="form-check form-switch mb-2">
|
|
<input class="form-check-input" type="checkbox" id="chk-wa-kiosko"
|
|
<?= !empty($cfg['turnero_wa_kiosko_enabled']) ? 'checked' : '' ?>>
|
|
<label class="form-check-label" for="chk-wa-kiosko">
|
|
<strong>Enviar notificación de turno desde el kiosko</strong>
|
|
<small class="d-block text-muted">Cuando un paciente toma turno en el kiosko, se le envía un WhatsApp con la plantilla configurada.</small>
|
|
</label>
|
|
</div>
|
|
<div class="form-check form-switch">
|
|
<input class="form-check-input" type="checkbox" id="chk-wa-consent"
|
|
<?= !empty($cfg['turnero_wa_consent_enabled']) ? 'checked' : '' ?>>
|
|
<label class="form-check-label" for="chk-wa-consent">
|
|
<strong>Enviar consentimientos por WhatsApp</strong>
|
|
<small class="d-block text-muted">Permite enviar consentimientos informados desde recepción por WhatsApp.</small>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-2">
|
|
<div class="col-md-5">
|
|
<label class="form-label small fw-semibold">Plantilla con consentimiento</label>
|
|
<input type="text" id="wa-template" class="form-control form-control-sm"
|
|
value="<?= htmlspecialchars($cfg['turnero_wa_template'] ?? 'consentimiento_turno') ?>"
|
|
placeholder="consentimiento_turno">
|
|
<small class="text-muted">Consulta, toma de muestra, etc. — incluye link de firma</small>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label small fw-semibold">Plantilla muestra pendiente</label>
|
|
<input type="text" id="wa-template-muestra" class="form-control form-control-sm"
|
|
value="<?= htmlspecialchars($cfg['turnero_wa_template_muestra'] ?? 'turno_muestra_pendiente') ?>"
|
|
placeholder="turno_muestra_pendiente">
|
|
<small class="text-muted">Solo notifica turno, sin consentimiento</small>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label small fw-semibold">Código de idioma</label>
|
|
<input type="text" id="wa-lang" class="form-control form-control-sm"
|
|
value="<?= htmlspecialchars($cfg['turnero_wa_lang'] ?? 'es_CO') ?>"
|
|
placeholder="es_CO">
|
|
</div>
|
|
</div>
|
|
<div class="d-flex justify-content-end mt-2">
|
|
<button class="btn btn-primary btn-sm" onclick="guardarWhatsApp()">
|
|
<i class="fas fa-save me-1"></i>Guardar configuración
|
|
</button>
|
|
</div>
|
|
<div class="alert alert-info py-2 px-3 mt-3" style="font-size:.82rem">
|
|
<i class="fas fa-info-circle me-1"></i>
|
|
La plantilla con consentimiento debe contener una variable de URL
|
|
(<code>{{1}}</code>) para el enlace de firma.
|
|
El sistema usará texto plano como fallback si la plantilla falla.
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Perfil de WhatsApp Business -->
|
|
<div class="mt-4">
|
|
<p class="section-title"><i class="fab fa-whatsapp me-1" style="color:#25d366"></i>Perfil de WhatsApp Business</p>
|
|
<div class="d-flex align-items-center gap-3 mb-3">
|
|
<!-- Foto de perfil -->
|
|
<div style="position:relative;flex-shrink:0">
|
|
<img id="wa-perfil-foto" src="" alt="Foto perfil"
|
|
style="width:80px;height:80px;border-radius:50%;object-fit:cover;
|
|
border:2px solid #dee2e6;background:#f1f5f9;display:none">
|
|
<div id="wa-perfil-foto-placeholder"
|
|
style="width:80px;height:80px;border-radius:50%;background:#e2e8f0;
|
|
display:flex;align-items:center;justify-content:center;font-size:2rem">
|
|
<i class="fab fa-whatsapp" style="color:#25d366"></i>
|
|
</div>
|
|
<label title="Cambiar foto" style="position:absolute;bottom:0;right:0;
|
|
background:#25d366;color:#fff;border-radius:50%;width:26px;height:26px;
|
|
display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:.8rem">
|
|
<i class="fas fa-camera"></i>
|
|
<input type="file" id="wa-foto-input" accept="image/jpeg,image/png"
|
|
style="display:none" onchange="subirFotoPerfil(this)">
|
|
</label>
|
|
</div>
|
|
<div class="flex-grow-1">
|
|
<div class="fw-semibold" id="wa-perfil-nombre" style="font-size:.95rem">—</div>
|
|
<div class="text-muted small" id="wa-perfil-about">—</div>
|
|
<button class="btn btn-outline-secondary btn-sm mt-1" onclick="cargarPerfilWA()">
|
|
<i class="fas fa-sync-alt me-1"></i>Cargar perfil actual
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-2">
|
|
<div class="col-12">
|
|
<label class="form-label small fw-semibold">Descripción corta (About) <small class="text-muted fw-normal">máx. 139 chars</small></label>
|
|
<input type="text" id="wa-about" maxlength="139" class="form-control form-control-sm"
|
|
placeholder="Ej: Laboratorio clínico · Lunes a Sábado 7am-5pm">
|
|
</div>
|
|
<div class="col-12">
|
|
<label class="form-label small fw-semibold">Descripción del negocio</label>
|
|
<textarea id="wa-description" rows="2" class="form-control form-control-sm"
|
|
placeholder="Descripción completa del laboratorio…"></textarea>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label small fw-semibold">Dirección</label>
|
|
<input type="text" id="wa-address" class="form-control form-control-sm"
|
|
placeholder="Calle 5 #12-34, Cúcuta">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label small fw-semibold">Email</label>
|
|
<input type="email" id="wa-email" class="form-control form-control-sm"
|
|
placeholder="contacto@laboratorio.com">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label small fw-semibold">Sitio web 1</label>
|
|
<input type="url" id="wa-web1" class="form-control form-control-sm"
|
|
placeholder="https://www.laboratorio.com">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label small fw-semibold">Sitio web 2 <small class="text-muted fw-normal">(opcional)</small></label>
|
|
<input type="url" id="wa-web2" class="form-control form-control-sm"
|
|
placeholder="https://instagram.com/laboratorio">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label small fw-semibold">Categoría</label>
|
|
<select id="wa-vertical" class="form-select form-select-sm">
|
|
<option value="">— sin especificar —</option>
|
|
<option value="HEALTH">Salud</option>
|
|
<option value="MEDICAL_AND_HEALTH">Médico y Salud</option>
|
|
<option value="BEAUTY">Belleza</option>
|
|
<option value="EDUCATION">Educación</option>
|
|
<option value="OTHER">Otro</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="d-flex justify-content-end mt-2">
|
|
<button class="btn btn-success btn-sm" onclick="guardarPerfilWA()">
|
|
<i class="fas fa-save me-1"></i>Guardar perfil
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Plantillas habilitadas en el chat turnero -->
|
|
<div class="mt-4">
|
|
<p class="section-title"><i class="fas fa-comment-dots me-1"></i>Plantillas habilitadas en el chat</p>
|
|
<p class="text-muted small mb-3">Marca las plantillas aprobadas disponibles en el chat del turnero.</p>
|
|
<?php if (empty($plantillasAprobadas)): ?>
|
|
<div class="alert alert-warning py-2 px-3" style="font-size:.85rem">
|
|
<i class="fas fa-exclamation-triangle me-1"></i>
|
|
No hay plantillas aprobadas. Sincroniza primero desde WhatsApp Business.
|
|
</div>
|
|
<?php else: ?>
|
|
<div id="plantillas-chat-list" style="display:grid;gap:.4rem;margin-bottom:.75rem">
|
|
<?php foreach ($plantillasAprobadas as $pt):
|
|
$checked = in_array((int)$pt['id'], array_map('intval', $plantillasHabilitadas));
|
|
?>
|
|
<label class="plantilla-chk-row<?= $checked ? ' is-checked' : '' ?>">
|
|
<input type="checkbox" value="<?= (int)$pt['id'] ?>" <?= $checked ? 'checked' : '' ?>
|
|
onchange="this.closest('label').classList.toggle('is-checked',this.checked)">
|
|
<div class="plantilla-chk-body">
|
|
<span class="plantilla-chk-name"><?= htmlspecialchars($pt['name']) ?></span>
|
|
<span class="plantilla-chk-meta"><?= htmlspecialchars($pt['template_name']) ?> • <?= htmlspecialchars($pt['language_code']) ?></span>
|
|
<?php if (!empty($pt['body_text'])): ?>
|
|
<span class="plantilla-chk-preview"><?= htmlspecialchars(mb_substr($pt['body_text'], 0, 100)) . (mb_strlen($pt['body_text']) > 100 ? '…' : '') ?></span>
|
|
<?php endif; ?>
|
|
</div>
|
|
</label>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<div class="d-flex justify-content-end">
|
|
<button class="btn btn-primary btn-sm" onclick="guardarPlantillasChat()">
|
|
<i class="fas fa-save me-1"></i>Guardar plantillas habilitadas
|
|
</button>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<!-- ════════════════════════════════════════
|
|
TAB 5 — PANTALLA TV
|
|
════════════════════════════════════════ -->
|
|
<?php elseif ($tab === 'tv'): ?>
|
|
|
|
<p class="section-title"><i class="fas fa-film me-1"></i>Playlist de la Pantalla TV</p>
|
|
<p class="text-muted" style="font-size:.85rem">Videos e imágenes se reproducen en bucle, uno detrás de otro, sin sonido, como fondo en la pantalla TV del turnero. Formatos: MP4, WebM, JPG, PNG, WEBP — máximo 500 MB por archivo.</p>
|
|
|
|
<!-- Lista actual -->
|
|
<div id="tv-media-list" class="mb-3 d-flex flex-column gap-2">
|
|
<?php foreach ($tvMedia as $m): ?>
|
|
<div class="tv-media-item d-flex align-items-center gap-3 p-2" data-id="<?= (int)$m['id'] ?>"
|
|
style="border:1px solid #e2e8f0;border-radius:10px">
|
|
<i class="fas fa-grip-vertical text-muted" style="cursor:grab"></i>
|
|
<?php /* Miniatura cuadrada, igual que la pantalla del televisor: con la
|
|
antigua de 90x60 el material cuadrado se veía recortado aquí y
|
|
no coincidía con lo que después salía al aire. */ ?>
|
|
<?php if ($m['tipo'] === 'video'): ?>
|
|
<video muted style="width:64px;height:64px;object-fit:cover;border-radius:6px;background:#000" src="<?= htmlspecialchars($m['url']) ?>"></video>
|
|
<?php else: ?>
|
|
<img style="width:64px;height:64px;object-fit:cover;border-radius:6px;background:#000" src="<?= htmlspecialchars($m['url']) ?>" alt="">
|
|
<?php endif; ?>
|
|
<div class="flex-grow-1">
|
|
<div class="small fw-semibold"><i class="fas fa-<?= $m['tipo']==='video'?'film':'image' ?> me-1"></i><?= $m['tipo']==='video'?'Video':'Imagen' ?></div>
|
|
<div class="text-muted small text-truncate" style="max-width:300px"><?= htmlspecialchars(basename($m['url'])) ?></div>
|
|
</div>
|
|
<?php if ($m['tipo'] === 'imagen'): ?>
|
|
<div class="d-flex align-items-center gap-1">
|
|
<input type="number" min="1" class="form-control form-control-sm tv-media-dur"
|
|
style="width:70px" value="<?= (int)$m['duracion_segundos'] ?>">
|
|
<span class="small text-muted">seg</span>
|
|
</div>
|
|
<?php endif; ?>
|
|
<button class="btn btn-outline-danger btn-sm" onclick="borrarTvMedia(<?= (int)$m['id'] ?>)">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
<?php if (!$tvMedia): ?>
|
|
<div class="text-muted small" id="tv-media-empty">Sin videos ni imágenes en la playlist.</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<button class="btn btn-outline-primary btn-sm" onclick="document.getElementById('tv-file-input').click()">
|
|
<i class="fas fa-plus me-1"></i>Agregar video o imagen
|
|
</button>
|
|
<button class="btn btn-primary btn-sm" onclick="guardarOrdenTv()">
|
|
<i class="fas fa-save me-1"></i>Guardar orden / duración
|
|
</button>
|
|
</div>
|
|
<input type="file" id="tv-file-input" accept="video/mp4,video/webm,image/jpeg,image/png,image/webp" class="d-none" multiple>
|
|
|
|
<!-- Barra de progreso -->
|
|
<div id="tv-progress-wrap" class="mt-3" style="display:none">
|
|
<div class="d-flex align-items-center gap-2 mb-1">
|
|
<span class="small fw-semibold" id="tv-progress-lbl">Subiendo…</span>
|
|
<span class="small text-muted" id="tv-progress-pct">0%</span>
|
|
</div>
|
|
<div class="progress" style="height:8px">
|
|
<div class="progress-bar progress-bar-striped progress-bar-animated bg-primary"
|
|
id="tv-progress-bar" role="progressbar" style="width:0%"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php endif; ?>
|
|
</div><!-- /tab-card -->
|
|
</div><!-- /content-wrap -->
|
|
|
|
<!-- Toast stack -->
|
|
<div class="toast-stack" id="toastStack"></div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script>
|
|
/* ═══════════════════════════════════════════════════════════
|
|
Configuración Turnero — JS
|
|
═══════════════════════════════════════════════════════════ */
|
|
const API = '<?= BASE_URL ?>modules/turnero/api/';
|
|
|
|
// Mapa de formularios de consentimiento por lugar (cargado desde PHP)
|
|
const LUGAR_FORM_IDS = <?= json_encode($lugarFormIds ?? []) ?>;
|
|
|
|
// ── Toast helper ────────────────────────────────────────────
|
|
function toast(msg, type = 'success') {
|
|
const stack = document.getElementById('toastStack');
|
|
const el = document.createElement('div');
|
|
el.className = `toast align-items-center text-white bg-${type === 'error' ? 'danger' : 'success'} border-0`;
|
|
el.setAttribute('role', 'alert');
|
|
el.innerHTML = `<div class="d-flex"><div class="toast-body">${msg}</div>
|
|
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button></div>`;
|
|
stack.appendChild(el);
|
|
new bootstrap.Toast(el, { delay: 3500 }).show();
|
|
el.addEventListener('hidden.bs.toast', () => el.remove());
|
|
}
|
|
|
|
// ── Copiar URL al portapapeles ───────────────────────────────
|
|
function copiarUrl(elId) {
|
|
const url = document.getElementById(elId)?.dataset?.url || document.getElementById(elId)?.textContent || '';
|
|
navigator.clipboard.writeText(url.trim()).then(() => toast('URL copiada al portapapeles'));
|
|
}
|
|
function copiarUrlDisplay(lugarId) {
|
|
copiarUrl('url-lugar-' + lugarId);
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────
|
|
// TAB 1: LUGARES
|
|
// ─────────────────────────────────────────────────────────────
|
|
async function guardarLugar(tipoOrId = 'muestras', id = null) {
|
|
// tipoOrId puede ser 'recepcion', 'muestras', o un número (id al editar)
|
|
const esEdicion = typeof tipoOrId === 'number';
|
|
if (esEdicion) { id = tipoOrId; tipoOrId = null; }
|
|
|
|
const prefijo = esEdicion ? 'edit-lu' : (tipoOrId === 'recepcion' ? 'rec' : 'mue');
|
|
const nombre = document.getElementById(esEdicion ? 'edit-lu-nombre' : `${prefijo}-nombre`)?.value.trim();
|
|
const desc = document.getElementById(esEdicion ? 'edit-lu-desc' : `${prefijo}-desc`)?.value.trim();
|
|
const orden = parseInt(document.getElementById(esEdicion ? 'edit-lu-orden' : `${prefijo}-orden`)?.value) || 99;
|
|
const activo = esEdicion ? (document.getElementById('edit-lu-activo')?.checked ? 1 : 0) : 1;
|
|
const tipo = esEdicion ? (document.getElementById('edit-lu-tipo')?.value || 'muestras') : tipoOrId;
|
|
|
|
if (!nombre) { toast('El nombre es requerido', 'error'); return; }
|
|
|
|
// Consentimientos: solo aplica al editar (el modal tiene los checkboxes)
|
|
const formularioIds = [];
|
|
if (esEdicion) {
|
|
document.querySelectorAll('.edit-lu-consent-chk:checked').forEach(chk => {
|
|
formularioIds.push(parseInt(chk.value));
|
|
});
|
|
}
|
|
|
|
const formModo = esEdicion
|
|
? (document.querySelector('input[name="edit-lu-form-modo"]:checked')?.value || 'link')
|
|
: 'link';
|
|
const body = { nombre, tipo, descripcion: desc, sort_order: orden, activo, formulario_ids: formularioIds, formulario_modo: formModo };
|
|
if (id) body.id = id;
|
|
|
|
try {
|
|
const res = await fetch(API + 'save_lugar.php', {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body)
|
|
});
|
|
const json = await res.json();
|
|
if (!json.ok) { toast(json.error || 'Error', 'error'); return; }
|
|
toast(id ? 'Lugar actualizado' : 'Lugar creado');
|
|
if (id) bootstrap.Modal.getInstance(document.getElementById('modalEditarLugar'))?.hide();
|
|
setTimeout(() => location.reload(), 600);
|
|
} catch (e) { toast('Error de conexión', 'error'); }
|
|
}
|
|
|
|
function editarLugar(id, nombre, desc, activo, orden, tipo = 'muestras', formModo = 'link') {
|
|
document.getElementById('edit-lu-id').value = id;
|
|
document.getElementById('edit-lu-tipo').value = tipo;
|
|
document.getElementById('edit-lu-nombre').value = nombre;
|
|
document.getElementById('edit-lu-desc').value = desc;
|
|
document.getElementById('edit-lu-orden').value = orden;
|
|
document.getElementById('edit-lu-activo').checked = !!activo;
|
|
const titulo = tipo === 'recepcion' ? 'Editar escritorio de recepción' : 'Editar estación de muestras';
|
|
document.getElementById('modal-lugar-titulo').textContent = titulo;
|
|
|
|
// Marcar checkboxes de consentimientos del lugar
|
|
const formIds = (LUGAR_FORM_IDS[id] || []).map(Number);
|
|
document.querySelectorAll('.edit-lu-consent-chk').forEach(chk => {
|
|
chk.checked = formIds.includes(parseInt(chk.value));
|
|
});
|
|
|
|
// Modo de presentación
|
|
const modoEl = document.querySelector(`input[name="edit-lu-form-modo"][value="${formModo}"]`);
|
|
if (modoEl) modoEl.checked = true;
|
|
|
|
new bootstrap.Modal(document.getElementById('modalEditarLugar')).show();
|
|
}
|
|
|
|
function guardarEditarLugar() {
|
|
guardarLugar(parseInt(document.getElementById('edit-lu-id').value));
|
|
}
|
|
|
|
async function eliminarLugar(id, nombre) {
|
|
if (!confirm(`¿Eliminar el lugar "${nombre}"?\nSolo se puede si no tiene turnos asignados.`)) return;
|
|
try {
|
|
const res = await fetch(API + 'save_lugar.php', {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ id, _delete: true })
|
|
});
|
|
const json = await res.json();
|
|
if (!json.ok) { toast(json.error || 'Error al eliminar', 'error'); return; }
|
|
toast('Lugar eliminado');
|
|
setTimeout(() => location.reload(), 600);
|
|
} catch (e) { toast('Error de conexión', 'error'); }
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────
|
|
// DISPOSITIVOS / TABLETS
|
|
// ─────────────────────────────────────────────────────────────
|
|
// Mostrar token de este equipo en el banner
|
|
(function() {
|
|
function setTokenDisplay() {
|
|
var t = window._turneroDeviceToken || localStorage.getItem('turnero_device_token');
|
|
if (!t) return;
|
|
var el = document.getElementById('cfg-my-token');
|
|
if (el) el.textContent = t.substring(0, 8) + '… (' + t + ')';
|
|
}
|
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', setTokenDisplay);
|
|
else setTokenDisplay();
|
|
// Reintentar si el sidebar aún no corrió
|
|
setTimeout(setTokenDisplay, 500);
|
|
})();
|
|
|
|
function registrarEsteEquipo() {
|
|
var t = window._turneroDeviceToken || localStorage.getItem('turnero_device_token');
|
|
if (!t) { toast('Token no disponible aún, espera un momento', 'error'); return; }
|
|
document.getElementById('dv-token').value = t;
|
|
document.getElementById('dv-nombre').focus();
|
|
document.getElementById('dv-nombre').scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
}
|
|
|
|
async function guardarDispositivo(id = null) {
|
|
const pfx = id ? 'edit-dv-' : 'dv-';
|
|
const token = document.getElementById(pfx + 'token')?.value.trim() || null;
|
|
const nombre = document.getElementById(pfx + 'nombre')?.value.trim();
|
|
const lugarId = parseInt(document.getElementById(pfx + 'lugar')?.value);
|
|
const activo = id ? (document.getElementById('edit-dv-activo')?.checked ? 1 : 0) : 1;
|
|
|
|
if (!nombre) { toast('El nombre es requerido', 'error'); return; }
|
|
if (!lugarId) { toast('Selecciona un lugar', 'error'); return; }
|
|
if (!token) { toast('Haz clic en "Registrar este equipo" primero', 'error'); return; }
|
|
|
|
try {
|
|
const body = { token, nombre, lugar_id: lugarId, activo, ip: '' };
|
|
if (id) body.id = id;
|
|
const res = await fetch(API + 'save_dispositivo.php', {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body)
|
|
});
|
|
const json = await res.json();
|
|
if (!json.ok) { toast(json.error || 'Error', 'error'); return; }
|
|
toast(id ? 'Dispositivo actualizado' : 'Dispositivo registrado');
|
|
bootstrap.Modal.getInstance(document.getElementById('modalEditarDispositivo'))?.hide();
|
|
setTimeout(() => location.reload(), 600);
|
|
} catch (e) { toast('Error de conexión', 'error'); }
|
|
}
|
|
|
|
function editarDispositivo(id, ip, nombre, lugarId, activo, token) {
|
|
document.getElementById('edit-dv-id').value = id;
|
|
document.getElementById('edit-dv-token').value = token || '';
|
|
document.getElementById('edit-dv-nombre').value = nombre;
|
|
document.getElementById('edit-dv-lugar').value = lugarId;
|
|
document.getElementById('edit-dv-activo').checked = !!activo;
|
|
new bootstrap.Modal(document.getElementById('modalEditarDispositivo')).show();
|
|
}
|
|
|
|
function guardarEditarDispositivo() {
|
|
guardarDispositivo(parseInt(document.getElementById('edit-dv-id').value));
|
|
}
|
|
|
|
async function eliminarDispositivo(id, nombre) {
|
|
if (!confirm(`¿Eliminar el dispositivo "${nombre}"?`)) return;
|
|
try {
|
|
const res = await fetch(API + 'save_dispositivo.php', {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ id, _delete: true })
|
|
});
|
|
const json = await res.json();
|
|
if (!json.ok) { toast(json.error || 'Error', 'error'); return; }
|
|
toast('Dispositivo eliminado');
|
|
setTimeout(() => location.reload(), 600);
|
|
} catch (e) { toast('Error de conexión', 'error'); }
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────
|
|
// TAB 2: EXÁMENES
|
|
// ─────────────────────────────────────────────────────────────
|
|
async function guardarExamen(id = null) {
|
|
const prefix = id ? 'edit-ex' : 'ex';
|
|
const codigo = document.getElementById(prefix + '-codigo')?.value.trim().toUpperCase();
|
|
const nombre = document.getElementById(prefix + '-nombre')?.value.trim();
|
|
const categoria = document.getElementById(prefix + '-categoria')?.value.trim();
|
|
const activo = id ? (document.getElementById('edit-ex-activo')?.checked ? 1 : 0) : 1;
|
|
const chkClass = id ? 'edit-ex-form-chk' : 'ex-form-chk';
|
|
const form_ids = [...document.querySelectorAll(`.${chkClass}:checked`)].map(c => parseInt(c.value));
|
|
|
|
if (!codigo || !nombre) { toast('Código y nombre son requeridos', 'error'); return; }
|
|
|
|
const body = { codigo, nombre, categoria, form_ids, activo };
|
|
if (id) body.id = id;
|
|
|
|
try {
|
|
const res = await fetch(API + 'save_ex_tipo.php', {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body)
|
|
});
|
|
const json = await res.json();
|
|
if (!json.ok) { toast(json.error || 'Error', 'error'); return; }
|
|
toast(id ? 'Examen actualizado' : 'Examen creado');
|
|
if (id) bootstrap.Modal.getInstance(document.getElementById('modalEditarExamen'))?.hide();
|
|
// Limpiar checkboxes del formulario de agregar
|
|
if (!id) document.querySelectorAll('.ex-form-chk').forEach(c => c.checked = false);
|
|
setTimeout(() => location.reload(), 600);
|
|
} catch (e) { toast('Error de conexión', 'error'); }
|
|
}
|
|
|
|
function editarExamen(id) {
|
|
fetch(API + 'save_ex_tipo.php?id=' + id)
|
|
.then(r => r.json())
|
|
.then(json => {
|
|
if (!json.ok) return;
|
|
const d = json.data;
|
|
document.getElementById('edit-ex-id').value = d.id;
|
|
document.getElementById('edit-ex-codigo').value = d.codigo;
|
|
document.getElementById('edit-ex-nombre').value = d.nombre;
|
|
document.getElementById('edit-ex-categoria').value = d.categoria || '';
|
|
document.getElementById('edit-ex-activo').checked = !!d.activo;
|
|
// Marcar consentimientos vinculados
|
|
document.querySelectorAll('.edit-ex-form-chk').forEach(chk => {
|
|
chk.checked = (d.form_ids || []).includes(parseInt(chk.value));
|
|
});
|
|
new bootstrap.Modal(document.getElementById('modalEditarExamen')).show();
|
|
})
|
|
.catch(() => toast('Error cargando datos', 'error'));
|
|
}
|
|
|
|
function guardarEditarExamen() {
|
|
guardarExamen(parseInt(document.getElementById('edit-ex-id').value));
|
|
}
|
|
|
|
async function eliminarExamen(id, nombre) {
|
|
if (!confirm(`¿Eliminar el examen "${nombre}"?`)) return;
|
|
try {
|
|
const res = await fetch(API + 'save_ex_tipo.php', {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ id, _delete: true })
|
|
});
|
|
const json = await res.json();
|
|
if (!json.ok) { toast(json.error || 'Error', 'error'); return; }
|
|
toast('Examen eliminado');
|
|
setTimeout(() => location.reload(), 600);
|
|
} catch (e) { toast('Error de conexión', 'error'); }
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────
|
|
// BUSCADOR + PAGINADOR — Tab examenes
|
|
// ─────────────────────────────────────────────────────────────
|
|
(function () {
|
|
const PAGE = 15;
|
|
let page = 1, q = '';
|
|
|
|
function update() {
|
|
const lista = document.getElementById('exam-lista');
|
|
if (!lista) return;
|
|
const rows = Array.from(lista.querySelectorAll('.item-row'));
|
|
const term = q.toLowerCase();
|
|
const matched = rows.filter(r => r.textContent.toLowerCase().includes(term));
|
|
|
|
rows.forEach(r => r.hidden = true);
|
|
const start = (page - 1) * PAGE;
|
|
matched.slice(start, start + PAGE).forEach(r => r.hidden = false);
|
|
|
|
// Ocultar categorías vacías
|
|
lista.querySelectorAll('.mb-3').forEach(cat => {
|
|
cat.hidden = Array.from(cat.querySelectorAll('.item-row')).every(r => r.hidden);
|
|
});
|
|
|
|
renderPager(matched.length);
|
|
}
|
|
|
|
function renderPager(total) {
|
|
const el = document.getElementById('exam-pager');
|
|
if (!el) return;
|
|
const pages = Math.ceil(total / PAGE);
|
|
if (pages <= 1) { el.innerHTML = total ? `<small class="text-muted">${total} resultado${total!==1?'s':''}</small>` : ''; return; }
|
|
const prev = `<button class="btn btn-sm btn-outline-secondary" ${page===1?'disabled':''} onclick="examPag(${page-1})"><i class="fas fa-chevron-left"></i></button>`;
|
|
const next = `<button class="btn btn-sm btn-outline-secondary" ${page===pages?'disabled':''} onclick="examPag(${page+1})"><i class="fas fa-chevron-right"></i></button>`;
|
|
el.innerHTML = `<div class="d-flex align-items-center gap-2 mt-2">
|
|
<small class="text-muted">${total} resultados</small>
|
|
<div class="ms-auto d-flex align-items-center gap-1">
|
|
${prev}
|
|
<span class="small px-1">Página ${page} de ${pages}</span>
|
|
${next}
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
window.examPag = function (p) { page = p; update(); };
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const inp = document.getElementById('exam-buscar');
|
|
if (inp) inp.addEventListener('input', () => { q = inp.value; page = 1; update(); });
|
|
update();
|
|
});
|
|
})();
|
|
|
|
// ─────────────────────────────────────────────────────────────
|
|
// TAB 3: PRIORIDADES
|
|
// ─────────────────────────────────────────────────────────────
|
|
function editarPrioridad(id, codigo, nombre, color, icono, descripcion, orden, activo) {
|
|
document.getElementById('edit-pr-id').value = id;
|
|
document.getElementById('edit-pr-codigo').value = codigo;
|
|
document.getElementById('edit-pr-nombre').value = nombre;
|
|
document.getElementById('edit-pr-color').value = color;
|
|
document.getElementById('edit-pr-color-text').value = color;
|
|
document.getElementById('edit-pr-icono').value = icono || '';
|
|
document.getElementById('edit-pr-descripcion').value = descripcion || '';
|
|
document.getElementById('edit-pr-orden').value = orden;
|
|
document.getElementById('edit-pr-activo').checked = !!activo;
|
|
new bootstrap.Modal(document.getElementById('modalEditarPrioridad')).show();
|
|
}
|
|
|
|
// Sincronizar input color ↔ texto hex
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const colorPicker = document.getElementById('edit-pr-color');
|
|
const colorText = document.getElementById('edit-pr-color-text');
|
|
if (colorPicker && colorText) {
|
|
colorPicker.addEventListener('input', () => { colorText.value = colorPicker.value; });
|
|
colorText.addEventListener('input', () => {
|
|
if (/^#[0-9a-f]{6}$/i.test(colorText.value)) {
|
|
colorPicker.value = colorText.value;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
async function guardarPrioridad() {
|
|
const id = parseInt(document.getElementById('edit-pr-id').value);
|
|
const nombre = document.getElementById('edit-pr-nombre').value.trim();
|
|
const color = document.getElementById('edit-pr-color-text').value.trim() || document.getElementById('edit-pr-color').value;
|
|
const icono = document.getElementById('edit-pr-icono').value.trim() || null;
|
|
const descripcion = document.getElementById('edit-pr-descripcion').value.trim() || null;
|
|
const orden = parseInt(document.getElementById('edit-pr-orden').value) || 1;
|
|
const activo = document.getElementById('edit-pr-activo').checked ? 1 : 0;
|
|
|
|
if (!nombre) { toast('El nombre es requerido', 'error'); return; }
|
|
if (!id) { toast('ID no válido', 'error'); return; }
|
|
|
|
try {
|
|
const res = await fetch(API + 'save_prioridad.php', {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ id, nombre, color, icono, descripcion, orden_peso: orden, activo })
|
|
});
|
|
const json = await res.json();
|
|
if (!json.ok) { toast(json.error || 'Error', 'error'); return; }
|
|
toast('Prioridad actualizada');
|
|
bootstrap.Modal.getInstance(document.getElementById('modalEditarPrioridad'))?.hide();
|
|
setTimeout(() => location.reload(), 600);
|
|
} catch (e) { toast('Error de conexión', 'error'); }
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────
|
|
// TAB 4: SESIÓN Y WHATSAPP
|
|
// ─────────────────────────────────────────────────────────────
|
|
async function cambiarSesion(accion, sesionId) {
|
|
const msgs = {
|
|
cerrar: '¿Confirma el cierre de la sesión del día?',
|
|
reabrir: '¿Reabrir la sesión de hoy?',
|
|
abrir: '¿Abrir una nueva sesión para hoy?',
|
|
};
|
|
if (!confirm(msgs[accion] || '¿Confirmar?')) return;
|
|
|
|
try {
|
|
const res = await fetch(API + 'sesion_turno.php', {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ accion, sesion_id: sesionId })
|
|
});
|
|
const json = await res.json();
|
|
if (!json.ok) { toast(json.error || 'Error', 'error'); return; }
|
|
toast(json.message || 'Operación completada');
|
|
setTimeout(() => location.reload(), 600);
|
|
} catch (e) { toast('Error de conexión', 'error'); }
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────
|
|
// TAB 5: PANTALLA TV — playlist de videos e imágenes
|
|
// ─────────────────────────────────────────────────────────────
|
|
(function () {
|
|
const inp = document.getElementById('tv-file-input');
|
|
if (!inp) return;
|
|
inp.addEventListener('change', () => {
|
|
Array.from(inp.files).forEach(subirTvMedia);
|
|
inp.value = '';
|
|
});
|
|
|
|
// Reordenar arrastrando (drag & drop nativo, sin librerías)
|
|
const list = document.getElementById('tv-media-list');
|
|
let dragEl = null;
|
|
list.addEventListener('dragstart', e => {
|
|
dragEl = e.target.closest('.tv-media-item');
|
|
e.dataTransfer.effectAllowed = 'move';
|
|
});
|
|
list.querySelectorAll('.tv-media-item').forEach(it => it.setAttribute('draggable', 'true'));
|
|
list.addEventListener('dragover', e => {
|
|
e.preventDefault();
|
|
const target = e.target.closest('.tv-media-item');
|
|
if (!target || target === dragEl) return;
|
|
const rect = target.getBoundingClientRect();
|
|
const after = (e.clientY - rect.top) > rect.height / 2;
|
|
list.insertBefore(dragEl, after ? target.nextSibling : target);
|
|
});
|
|
})();
|
|
|
|
function subirTvMedia(file) {
|
|
const allowedMime = ['video/mp4', 'video/webm', 'video/ogg', 'image/jpeg', 'image/png', 'image/webp'];
|
|
if (!allowedMime.includes(file.type)) { toast(file.name + ': formato no admitido', 'error'); return; }
|
|
if (file.size > 500 * 1024 * 1024) { toast(file.name + ': supera el límite de 500 MB', 'error'); return; }
|
|
|
|
const wrap = document.getElementById('tv-progress-wrap');
|
|
const bar = document.getElementById('tv-progress-bar');
|
|
const pct = document.getElementById('tv-progress-pct');
|
|
const lbl = document.getElementById('tv-progress-lbl');
|
|
wrap.style.display = '';
|
|
|
|
const fd = new FormData();
|
|
fd.append('media', file);
|
|
|
|
const xhr = new XMLHttpRequest();
|
|
xhr.open('POST', API + 'save_tv_media.php');
|
|
|
|
xhr.upload.addEventListener('progress', e => {
|
|
if (!e.lengthComputable) return;
|
|
const p = Math.round(e.loaded / e.total * 100);
|
|
bar.style.width = p + '%';
|
|
pct.textContent = p + '%';
|
|
});
|
|
|
|
xhr.addEventListener('load', () => {
|
|
wrap.style.display = 'none';
|
|
try {
|
|
const json = JSON.parse(xhr.responseText);
|
|
if (!json.ok) { toast(json.error || 'Error al subir', 'error'); return; }
|
|
toast(file.name + ' agregado a la playlist');
|
|
agregarItemTvMedia(json.item);
|
|
} catch(e) { toast('Respuesta inesperada del servidor', 'error'); }
|
|
});
|
|
|
|
xhr.addEventListener('error', () => { wrap.style.display = 'none'; toast('Error de conexión', 'error'); });
|
|
|
|
lbl.textContent = 'Subiendo ' + file.name + '…';
|
|
bar.style.width = '0%';
|
|
xhr.send(fd);
|
|
}
|
|
|
|
function agregarItemTvMedia(item) {
|
|
document.getElementById('tv-media-empty')?.remove();
|
|
const list = document.getElementById('tv-media-list');
|
|
const div = document.createElement('div');
|
|
div.className = 'tv-media-item d-flex align-items-center gap-3 p-2';
|
|
div.dataset.id = item.id;
|
|
div.setAttribute('draggable', 'true');
|
|
div.style.cssText = 'border:1px solid #e2e8f0;border-radius:10px';
|
|
const preview = item.tipo === 'video'
|
|
? `<video muted style="width:90px;height:60px;object-fit:cover;border-radius:6px" src="${item.url}"></video>`
|
|
: `<img style="width:90px;height:60px;object-fit:cover;border-radius:6px" src="${item.url}" alt="">`;
|
|
const durInput = item.tipo === 'imagen'
|
|
? `<div class="d-flex align-items-center gap-1">
|
|
<input type="number" min="1" class="form-control form-control-sm tv-media-dur" style="width:70px" value="${item.duracion_segundos}">
|
|
<span class="small text-muted">seg</span>
|
|
</div>` : '';
|
|
div.innerHTML = `
|
|
<i class="fas fa-grip-vertical text-muted" style="cursor:grab"></i>
|
|
${preview}
|
|
<div class="flex-grow-1">
|
|
<div class="small fw-semibold"><i class="fas fa-${item.tipo==='video'?'film':'image'} me-1"></i>${item.tipo==='video'?'Video':'Imagen'}</div>
|
|
<div class="text-muted small text-truncate" style="max-width:300px">${item.url.split('/').pop()}</div>
|
|
</div>
|
|
${durInput}
|
|
<button class="btn btn-outline-danger btn-sm" onclick="borrarTvMedia(${item.id})"><i class="fas fa-trash"></i></button>`;
|
|
list.appendChild(div);
|
|
}
|
|
|
|
async function borrarTvMedia(id) {
|
|
if (!confirm('¿Quitar este elemento de la playlist?')) return;
|
|
try {
|
|
const res = await fetch(API + 'delete_tv_media.php', {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ id })
|
|
});
|
|
const json = await res.json();
|
|
if (!json.ok) { toast(json.error || 'Error', 'error'); return; }
|
|
document.querySelector(`.tv-media-item[data-id="${id}"]`)?.remove();
|
|
const list = document.getElementById('tv-media-list');
|
|
if (!list.querySelector('.tv-media-item')) {
|
|
list.innerHTML = '<div class="text-muted small" id="tv-media-empty">Sin videos ni imágenes en la playlist.</div>';
|
|
}
|
|
toast('Elemento eliminado');
|
|
} catch(e) { toast('Error de conexión', 'error'); }
|
|
}
|
|
|
|
async function guardarOrdenTv() {
|
|
const items = Array.from(document.querySelectorAll('.tv-media-item')).map(el => ({
|
|
id: parseInt(el.dataset.id),
|
|
duracion_segundos: parseInt(el.querySelector('.tv-media-dur')?.value) || 8,
|
|
}));
|
|
if (!items.length) { toast('No hay elementos para guardar', 'error'); return; }
|
|
try {
|
|
const res = await fetch(API + 'reorder_tv_media.php', {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ items })
|
|
});
|
|
const json = await res.json();
|
|
if (!json.ok) { toast(json.error || 'Error', 'error'); return; }
|
|
toast('Orden y duración guardados');
|
|
} catch(e) { toast('Error de conexión', 'error'); }
|
|
}
|
|
|
|
async function cargarPerfilWA() {
|
|
try {
|
|
const res = await fetch(API + 'wa_profile.php');
|
|
const json = await res.json();
|
|
if (!json.ok) { toast(json.error || 'Error al cargar perfil', 'error'); return; }
|
|
const p = json.perfil || {};
|
|
if (p.profile_picture_url) {
|
|
document.getElementById('wa-perfil-foto').src = p.profile_picture_url;
|
|
document.getElementById('wa-perfil-foto').style.display = '';
|
|
document.getElementById('wa-perfil-foto-placeholder').style.display = 'none';
|
|
}
|
|
document.getElementById('wa-perfil-about').textContent = p.about || '—';
|
|
document.getElementById('wa-about').value = p.about || '';
|
|
document.getElementById('wa-description').value = p.description || '';
|
|
document.getElementById('wa-address').value = p.address || '';
|
|
document.getElementById('wa-email').value = p.email || '';
|
|
const webs = p.websites || [];
|
|
document.getElementById('wa-web1').value = webs[0] || '';
|
|
document.getElementById('wa-web2').value = webs[1] || '';
|
|
if (p.vertical) document.getElementById('wa-vertical').value = p.vertical;
|
|
toast('Perfil cargado');
|
|
} catch (e) { toast('Error de conexión', 'error'); }
|
|
}
|
|
|
|
async function guardarPerfilWA() {
|
|
const body = {
|
|
accion: 'guardar',
|
|
about: document.getElementById('wa-about').value.trim(),
|
|
description: document.getElementById('wa-description').value.trim(),
|
|
address: document.getElementById('wa-address').value.trim(),
|
|
email: document.getElementById('wa-email').value.trim(),
|
|
vertical: document.getElementById('wa-vertical').value,
|
|
websites: [document.getElementById('wa-web1').value.trim(),
|
|
document.getElementById('wa-web2').value.trim()].filter(Boolean),
|
|
};
|
|
try {
|
|
const res = await fetch(API + 'wa_profile.php', {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
});
|
|
const json = await res.json();
|
|
if (!json.ok) { toast(json.error || 'Error', 'error'); return; }
|
|
toast('Perfil de WhatsApp actualizado');
|
|
document.getElementById('wa-perfil-about').textContent = body.about || '—';
|
|
} catch (e) { toast('Error de conexión', 'error'); }
|
|
}
|
|
|
|
async function subirFotoPerfil(input) {
|
|
if (!input.files[0]) return;
|
|
const form = new FormData();
|
|
form.append('accion', 'foto');
|
|
form.append('foto', input.files[0]);
|
|
const btn = input.closest('label');
|
|
const orig = btn.innerHTML;
|
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
|
try {
|
|
const res = await fetch(API + 'wa_profile.php', { method: 'POST', body: form });
|
|
const json = await res.json();
|
|
if (!json.ok) { toast(json.error || 'Error al subir foto', 'error'); return; }
|
|
// Preview local
|
|
const reader = new FileReader();
|
|
reader.onload = e => {
|
|
document.getElementById('wa-perfil-foto').src = e.target.result;
|
|
document.getElementById('wa-perfil-foto').style.display = '';
|
|
document.getElementById('wa-perfil-foto-placeholder').style.display = 'none';
|
|
};
|
|
reader.readAsDataURL(input.files[0]);
|
|
toast('Foto de perfil actualizada');
|
|
} catch (e) { toast('Error al subir foto', 'error'); } finally {
|
|
btn.innerHTML = orig;
|
|
input.value = '';
|
|
}
|
|
}
|
|
|
|
async function guardarPlantillasChat() {
|
|
const ids = [...document.querySelectorAll('#plantillas-chat-list input[type=checkbox]:checked')]
|
|
.map(el => parseInt(el.value, 10));
|
|
try {
|
|
const res = await fetch(API + 'chat_save_plantillas.php', {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ ids })
|
|
});
|
|
const json = await res.json();
|
|
if (!json.ok) { toast(json.error || 'Error al guardar', 'error'); return; }
|
|
toast('Plantillas guardadas (' + ids.length + ')');
|
|
} catch (e) { toast('Error de conexión', 'error'); }
|
|
}
|
|
|
|
async function guardarWhatsApp() {
|
|
const template = document.getElementById('wa-template')?.value.trim();
|
|
const templateMuestra = document.getElementById('wa-template-muestra')?.value.trim();
|
|
const lang = document.getElementById('wa-lang')?.value.trim();
|
|
const waKiosko = document.getElementById('chk-wa-kiosko')?.checked ? 1 : 0;
|
|
const waConsent = document.getElementById('chk-wa-consent')?.checked ? 1 : 0;
|
|
if (!template) { toast('El nombre de plantilla es requerido', 'error'); return; }
|
|
|
|
try {
|
|
const res = await fetch(API + 'sesion_turno.php', {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ accion: 'config_wa', template, template_muestra: templateMuestra, lang, wa_kiosko: waKiosko, wa_consent: waConsent })
|
|
});
|
|
const json = await res.json();
|
|
if (!json.ok) { toast(json.error || 'Error', 'error'); return; }
|
|
toast('Configuración WhatsApp guardada');
|
|
} catch (e) { toast('Error de conexión', 'error'); }
|
|
}
|
|
</script>
|
|
<script src="<?= defined('APP_URL') ? rtrim(APP_URL,'/') : '' ?>/assets/js/lab-sidebar.js"></script>
|
|
</body>
|
|
</html>
|