feat(turnero): gestión de dispositivos/tablets en configuración
Sección nueva en tab Lugares con tabla CRUD de turnero_dispositivos: IP, nombre, lugar asignado, estado activo/inactivo. API save_dispositivo.php para crear/editar/eliminar. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
fb0e77523c
commit
bad3d42a92
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
/**
|
||||
* modules/turnero/api/save_dispositivo.php
|
||||
* POST {ip, nombre, lugar_id, activo} → crear
|
||||
* POST {id, ip, nombre, lugar_id, activo} → actualizar
|
||||
* POST {id, _delete: true} → eliminar
|
||||
*/
|
||||
require_once __DIR__ . '/_helpers.php';
|
||||
requireTurnero();
|
||||
requireMethod('POST');
|
||||
|
||||
$input = inputJson();
|
||||
$id = (int)($input['id'] ?? 0);
|
||||
$delete = !empty($input['_delete']);
|
||||
|
||||
if ($delete) {
|
||||
if (!$id) jsonError('ID requerido');
|
||||
db()->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');
|
||||
}
|
||||
@@ -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';
|
||||
</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 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.
|
||||
</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>IP</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><code><?= htmlspecialchars($dv['ip']) ?></code></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'] ?>)"
|
||||
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="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>
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small fw-semibold">IP <span class="text-danger">*</span></label>
|
||||
<input type="text" id="dv-ip" class="form-control form-control-sm" placeholder="192.168.1.10">
|
||||
</div>
|
||||
<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-3">
|
||||
<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">
|
||||
<div class="mb-2">
|
||||
<label class="form-label small fw-semibold">IP</label>
|
||||
<input type="text" id="edit-dv-ip" class="form-control form-control-sm">
|
||||
</div>
|
||||
<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
|
||||
════════════════════════════════════════ -->
|
||||
@@ -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
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user