diff --git a/modules/turnero/api/save_dispositivo.php b/modules/turnero/api/save_dispositivo.php
new file mode 100644
index 0000000..46997de
--- /dev/null
+++ b/modules/turnero/api/save_dispositivo.php
@@ -0,0 +1,44 @@
+prepare('DELETE FROM turnero_dispositivos WHERE id = ?')->execute([$id]);
+ jsonOk([], 'Dispositivo eliminado');
+}
+
+$ip = trim($input['ip'] ?? '');
+$nombre = trim($input['nombre'] ?? '');
+$lugarId = (int)($input['lugar_id'] ?? 0);
+$activo = (int)($input['activo'] ?? 1);
+
+if ($ip === '') jsonError('La IP es requerida');
+if ($nombre === '') jsonError('El nombre es requerido');
+if ($lugarId <= 0) jsonError('Debes asignar un lugar');
+
+if ($id) {
+ $stmt = db()->prepare(
+ 'UPDATE turnero_dispositivos SET ip=?, nombre=?, lugar_id=?, activo=? WHERE id=?'
+ );
+ $stmt->execute([$ip, $nombre, $lugarId, $activo, $id]);
+ jsonOk(['id' => $id], 'Dispositivo actualizado');
+} else {
+ $stmt = db()->prepare(
+ 'INSERT INTO turnero_dispositivos (ip, nombre, lugar_id, activo) VALUES (?, ?, ?, ?)
+ ON DUPLICATE KEY UPDATE nombre=VALUES(nombre), lugar_id=VALUES(lugar_id), activo=VALUES(activo)'
+ );
+ $stmt->execute([$ip, $nombre, $lugarId, $activo]);
+ jsonOk(['id' => (int)db()->lastInsertId()], 'Dispositivo creado');
+}
diff --git a/modules/turnero/views/configuracion.php b/modules/turnero/views/configuracion.php
index 42c7425..627c5a3 100644
--- a/modules/turnero/views/configuracion.php
+++ b/modules/turnero/views/configuracion.php
@@ -40,6 +40,16 @@ try {
}
} 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'
@@ -462,6 +472,129 @@ $tab = $_GET['tab'] ?? 'lugares';
+
+
+
+
+
Dispositivos / Tablets autorizadas
+
= count($dispositivos) ?>
+
+
+ Cada tablet queda bloqueada a su lugar asignado según la IP de red.
+ Si la IP no aparece aquí, el dispositivo verá todos los lugares disponibles.
+
+
+
+
+
+
+ | IP |
+ Nombre / descripción |
+ Lugar asignado |
+ Estado |
+ |
+
+
+
+
+
+ = htmlspecialchars($dv['ip']) ?> |
+ = htmlspecialchars($dv['nombre']) ?> |
+ = htmlspecialchars($dv['lugar_nombre'] ?? '—') ?> |
+
+
+ = $dv['activo'] ? 'Activo' : 'Inactivo' ?>
+
+ |
+
+
+
+ |
+
+
+
+ | No hay dispositivos registrados |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1147,6 +1280,62 @@ async function eliminarLugar(id, nombre) {
} catch (e) { toast('Error de conexión', 'error'); }
}
+// ─────────────────────────────────────────────────────────────
+// DISPOSITIVOS / TABLETS
+// ─────────────────────────────────────────────────────────────
+async function guardarDispositivo(id = null) {
+ const pfx = id ? 'edit-dv-' : 'dv-';
+ const ip = document.getElementById(pfx + 'ip')?.value.trim();
+ 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 (!ip) { toast('La IP es requerida', 'error'); return; }
+ if (!nombre) { toast('El nombre es requerido', 'error'); return; }
+ if (!lugarId) { toast('Selecciona un lugar', 'error'); return; }
+
+ try {
+ const body = { ip, nombre, lugar_id: lugarId, activo };
+ 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 creado');
+ 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) {
+ document.getElementById('edit-dv-id').value = id;
+ document.getElementById('edit-dv-ip').value = ip;
+ 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
// ─────────────────────────────────────────────────────────────