up
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
-- Agrega columnas de solicitud de transferencia a turnero_lugar_ocupacion
|
||||
ALTER TABLE turnero_lugar_ocupacion
|
||||
ADD COLUMN solicitud_user_id INT DEFAULT NULL,
|
||||
ADD COLUMN solicitud_nombre VARCHAR(150) DEFAULT NULL,
|
||||
ADD COLUMN solicitud_at DATETIME DEFAULT NULL,
|
||||
ADD COLUMN solicitud_resultado ENUM('pendiente','denegada') DEFAULT NULL;
|
||||
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
/**
|
||||
* POST /modules/turnero/api/ocupar_lugar.php
|
||||
*
|
||||
* Gestiona la ocupación exclusiva de un puesto (recepción o muestras).
|
||||
* Un puesto solo puede tener UN usuario activo.
|
||||
*
|
||||
* Actions:
|
||||
* claim → reclamar el puesto (o renovar si ya es mío)
|
||||
* ping → renovar heartbeat (cada 60 s)
|
||||
* release → liberar el puesto
|
||||
* solicitar → usuario B pide transferencia al ocupante A
|
||||
* check_solicitud → A consulta si hay una solicitud pendiente
|
||||
* denegar → A rechaza la solicitud
|
||||
* aceptar → A acepta y libera el puesto
|
||||
* check_transfer → B consulta si el puesto quedó libre o fue denegado
|
||||
* disponibles → lista puestos del mismo tipo sin ocupante activo
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/_helpers.php';
|
||||
requireMethod('POST');
|
||||
|
||||
$datos = inputJson();
|
||||
$lugarId = (int)($datos['lugar_id'] ?? 0);
|
||||
$action = $datos['action'] ?? 'claim';
|
||||
|
||||
if ($lugarId <= 0) jsonError('lugar_id inválido.');
|
||||
|
||||
$validActions = ['claim','ping','release','solicitar','check_solicitud','denegar','aceptar','check_transfer','disponibles'];
|
||||
if (!in_array($action, $validActions, true)) jsonError('action inválida.');
|
||||
|
||||
$pdo = db();
|
||||
$userId = (int)adminId();
|
||||
$nombre = $_SESSION['admin_user']['full_name']
|
||||
?? $_SESSION['admin_user']['username']
|
||||
?? 'Usuario';
|
||||
|
||||
$timeout = 'NOW() - INTERVAL 2 MINUTE';
|
||||
|
||||
/* ── release ─────────────────────────────────────────────────── */
|
||||
if ($action === 'release') {
|
||||
$pdo->prepare(
|
||||
"DELETE FROM turnero_lugar_ocupacion WHERE lugar_id = ? AND user_id = ?"
|
||||
)->execute([$lugarId, $userId]);
|
||||
jsonOk(['liberado' => true]);
|
||||
}
|
||||
|
||||
/* ── ping ────────────────────────────────────────────────────── */
|
||||
if ($action === 'ping') {
|
||||
$pdo->prepare(
|
||||
"UPDATE turnero_lugar_ocupacion SET ultimo_ping = NOW()
|
||||
WHERE lugar_id = ? AND user_id = ?"
|
||||
)->execute([$lugarId, $userId]);
|
||||
jsonOk(['ok' => true]);
|
||||
}
|
||||
|
||||
/* ── aceptar (ocupante A acepta la solicitud y libera) ───────── */
|
||||
if ($action === 'aceptar') {
|
||||
$pdo->prepare(
|
||||
"DELETE FROM turnero_lugar_ocupacion WHERE lugar_id = ? AND user_id = ?"
|
||||
)->execute([$lugarId, $userId]);
|
||||
jsonOk(['liberado' => true]);
|
||||
}
|
||||
|
||||
/* ── denegar (ocupante A rechaza) ────────────────────────────── */
|
||||
if ($action === 'denegar') {
|
||||
$pdo->prepare(
|
||||
"UPDATE turnero_lugar_ocupacion
|
||||
SET solicitud_resultado = 'denegada'
|
||||
WHERE lugar_id = ? AND user_id = ?"
|
||||
)->execute([$lugarId, $userId]);
|
||||
jsonOk(['ok' => true]);
|
||||
}
|
||||
|
||||
/* ── check_solicitud (A consulta si hay solicitud pendiente) ─── */
|
||||
if ($action === 'check_solicitud') {
|
||||
$st = $pdo->prepare(
|
||||
"SELECT solicitud_user_id, solicitud_nombre, solicitud_at, solicitud_resultado
|
||||
FROM turnero_lugar_ocupacion
|
||||
WHERE lugar_id = ? AND user_id = ?"
|
||||
);
|
||||
$st->execute([$lugarId, $userId]);
|
||||
$row = $st->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$row || $row['solicitud_resultado'] !== 'pendiente') {
|
||||
jsonOk(['hay_solicitud' => false]);
|
||||
}
|
||||
jsonOk([
|
||||
'hay_solicitud' => true,
|
||||
'solicitante' => $row['solicitud_nombre'],
|
||||
'solicitud_at' => $row['solicitud_at'],
|
||||
]);
|
||||
}
|
||||
|
||||
/* ── check_transfer (B consulta si el puesto quedó libre) ───── */
|
||||
if ($action === 'check_transfer') {
|
||||
$st = $pdo->prepare(
|
||||
"SELECT user_id, solicitud_user_id, solicitud_resultado
|
||||
FROM turnero_lugar_ocupacion WHERE lugar_id = ?"
|
||||
);
|
||||
$st->execute([$lugarId]);
|
||||
$row = $st->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$row) {
|
||||
jsonOk(['disponible' => true]);
|
||||
}
|
||||
if ($row['solicitud_resultado'] === 'denegada') {
|
||||
jsonOk(['disponible' => false, 'denegado' => true]);
|
||||
}
|
||||
jsonOk(['disponible' => false, 'denegado' => false]);
|
||||
}
|
||||
|
||||
/* ── solicitar (B pide transferencia) ───────────────────────── */
|
||||
if ($action === 'solicitar') {
|
||||
// Limpiar expirados primero
|
||||
$pdo->prepare(
|
||||
"DELETE FROM turnero_lugar_ocupacion WHERE ultimo_ping < $timeout"
|
||||
)->execute();
|
||||
|
||||
$st = $pdo->prepare(
|
||||
"SELECT user_id, user_nombre, solicitud_at, solicitud_user_id
|
||||
FROM turnero_lugar_ocupacion WHERE lugar_id = ?"
|
||||
);
|
||||
$st->execute([$lugarId]);
|
||||
$ocupado = $st->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$ocupado || (int)$ocupado['user_id'] === $userId) {
|
||||
// Ya libre o soy yo — reclamar directamente
|
||||
$pdo->prepare(
|
||||
"INSERT INTO turnero_lugar_ocupacion (lugar_id, user_id, user_nombre, ultimo_ping)
|
||||
VALUES (?, ?, ?, NOW())
|
||||
ON DUPLICATE KEY UPDATE user_id=?, user_nombre=?, ultimo_ping=NOW(),
|
||||
solicitud_user_id=NULL, solicitud_nombre=NULL, solicitud_at=NULL, solicitud_resultado=NULL"
|
||||
)->execute([$lugarId, $userId, $nombre, $userId, $nombre]);
|
||||
jsonOk(['transferido' => true]);
|
||||
}
|
||||
|
||||
// Guardar solicitud (o renovarla si ya existía de este mismo B)
|
||||
$pdo->prepare(
|
||||
"UPDATE turnero_lugar_ocupacion
|
||||
SET solicitud_user_id = ?,
|
||||
solicitud_nombre = ?,
|
||||
solicitud_at = NOW(),
|
||||
solicitud_resultado = 'pendiente'
|
||||
WHERE lugar_id = ?"
|
||||
)->execute([$userId, $nombre, $lugarId]);
|
||||
|
||||
jsonOk(['solicitado' => true, 'ocupado_por' => $ocupado['user_nombre']]);
|
||||
}
|
||||
|
||||
/* ── disponibles (puestos del mismo tipo sin ocupante) ───────── */
|
||||
if ($action === 'disponibles') {
|
||||
$stLugar = $pdo->prepare("SELECT tipo FROM turnero_lugares WHERE id = ?");
|
||||
$stLugar->execute([$lugarId]);
|
||||
$lugar = $stLugar->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$lugar) jsonError('Lugar no encontrado.', 404);
|
||||
|
||||
$stDisp = $pdo->prepare(
|
||||
"SELECT l.id, l.nombre
|
||||
FROM turnero_lugares l
|
||||
WHERE l.tipo = ? AND l.activo = 1 AND l.id != ?
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM turnero_lugar_ocupacion o
|
||||
WHERE o.lugar_id = l.id
|
||||
AND o.ultimo_ping >= $timeout
|
||||
)
|
||||
ORDER BY l.sort_order ASC"
|
||||
);
|
||||
$stDisp->execute([$lugar['tipo'], $lugarId]);
|
||||
jsonOk(['disponibles' => $stDisp->fetchAll(PDO::FETCH_ASSOC)]);
|
||||
}
|
||||
|
||||
/* ── claim ───────────────────────────────────────────────────── */
|
||||
// Limpiar ocupaciones expiradas
|
||||
$pdo->prepare(
|
||||
"DELETE FROM turnero_lugar_ocupacion WHERE ultimo_ping < $timeout"
|
||||
)->execute();
|
||||
|
||||
// Verificar si hay un ocupante
|
||||
$st = $pdo->prepare(
|
||||
"SELECT user_id, user_nombre, solicitud_user_id, solicitud_at, solicitud_resultado
|
||||
FROM turnero_lugar_ocupacion WHERE lugar_id = ?"
|
||||
);
|
||||
$st->execute([$lugarId]);
|
||||
$ocupado = $st->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($ocupado && (int)$ocupado['user_id'] !== $userId) {
|
||||
// Si hay una solicitud de este usuario pendiente hace más de 6s → auto-liberar
|
||||
$esMiSolicitud = (int)($ocupado['solicitud_user_id'] ?? 0) === $userId;
|
||||
$solicitudVieja = $ocupado['solicitud_at'] && (strtotime($ocupado['solicitud_at']) < time() - 6);
|
||||
if ($esMiSolicitud && $solicitudVieja) {
|
||||
$pdo->prepare("DELETE FROM turnero_lugar_ocupacion WHERE lugar_id = ?")
|
||||
->execute([$lugarId]);
|
||||
$ocupado = null; // caer al INSERT/UPDATE de abajo
|
||||
} else {
|
||||
jsonOk([
|
||||
'disponible' => false,
|
||||
'ocupado_por' => $ocupado['user_nombre'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Reclamar o renovar
|
||||
$pdo->prepare(
|
||||
"INSERT INTO turnero_lugar_ocupacion (lugar_id, user_id, user_nombre, ultimo_ping)
|
||||
VALUES (?, ?, ?, NOW())
|
||||
ON DUPLICATE KEY UPDATE user_id=?, user_nombre=?, ultimo_ping=NOW(),
|
||||
solicitud_user_id=NULL, solicitud_nombre=NULL, solicitud_at=NULL, solicitud_resultado=NULL"
|
||||
)->execute([$lugarId, $userId, $nombre, $userId, $nombre]);
|
||||
|
||||
jsonOk(['disponible' => true]);
|
||||
@@ -856,6 +856,183 @@ function mostrarToast(msg, type = 'info', duration = 2500) {
|
||||
}
|
||||
function mostrarError(msg) { mostrarToast(msg, 'error', 4000); }
|
||||
function mostrarLlamando(cod) { mostrarToast('Llamando turno ' + cod + '…', 'info', 2000); }
|
||||
|
||||
/* ── Ocupación exclusiva del puesto ──────────────────────────── */
|
||||
(function(_ID, _NOMBRE, _VIEW, _PARAM) {
|
||||
if (!_ID) return;
|
||||
|
||||
function _e(s) {
|
||||
const d = document.createElement('div');
|
||||
d.appendChild(document.createTextNode(String(s ?? '')));
|
||||
return d.innerHTML;
|
||||
}
|
||||
function _post(body) {
|
||||
return fetch(API + 'ocupar_lugar.php', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(Object.assign({ lugar_id: _ID }, body)),
|
||||
}).then(r => r.json());
|
||||
}
|
||||
function _redirigirSiguiente(disponibles) {
|
||||
if (disponibles && disponibles.length) {
|
||||
window.location.href = BASE_WA + 'erp.php?m=turnero&v=' + _VIEW + '&' + _PARAM + '=' + disponibles[0].id;
|
||||
} else {
|
||||
window.location.href = BASE_WA + 'erp.php?m=turnero';
|
||||
}
|
||||
}
|
||||
|
||||
/* Pantalla de bloqueo */
|
||||
function _mostrarBloqueado(ocupadoPor) {
|
||||
document.body.innerHTML =
|
||||
'<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;' +
|
||||
'min-height:100vh;background:#f8faff;font-family:sans-serif;gap:14px;text-align:center;padding:28px">' +
|
||||
'<div style="font-size:4rem">🔒</div>' +
|
||||
'<h2 style="color:#1565c0;margin:0">' + _e(_NOMBRE) + ' está abierta en otro equipo</h2>' +
|
||||
'<p style="color:#555;font-size:1.05rem;max-width:440px;line-height:1.6">' +
|
||||
'Actualmente usada por <strong>' + _e(ocupadoPor) + '</strong>.<br>' +
|
||||
'Puedes solicitar que cierre su sesión o ir al siguiente puesto disponible.</p>' +
|
||||
'<div style="display:flex;gap:12px;flex-wrap:wrap;justify-content:center;margin-top:4px">' +
|
||||
'<button id="_btn-solicitar" onclick="window._ocToma()" ' +
|
||||
'style="padding:12px 26px;background:#1565c0;color:#fff;border:none;border-radius:8px;font-size:1rem;cursor:pointer;font-weight:600">' +
|
||||
'📢 Solicitar cierre en ese equipo</button>' +
|
||||
'<button id="_btn-siguiente" onclick="window._ocSiguiente()" ' +
|
||||
'style="padding:12px 26px;background:#f0f4ff;color:#1565c0;border:2px solid #1565c0;border-radius:8px;font-size:1rem;cursor:pointer;font-weight:600">' +
|
||||
'➡ Ir al siguiente disponible</button>' +
|
||||
'</div>' +
|
||||
'<div id="_oc-estado" style="min-height:24px;color:#777;font-size:.95rem;margin-top:4px"></div>' +
|
||||
'</div>';
|
||||
}
|
||||
function _setEstado(msg, color) {
|
||||
const el = document.getElementById('_oc-estado');
|
||||
if (el) el.innerHTML = '<span style="color:' + (color||'#555') + '">' + msg + '</span>';
|
||||
}
|
||||
|
||||
/* Solicitar toma de control */
|
||||
let _checkTransferTimer = null;
|
||||
window._ocToma = async function() {
|
||||
const btn = document.getElementById('_btn-solicitar');
|
||||
if (btn) btn.disabled = true;
|
||||
_setEstado('Enviando solicitud…', '#555');
|
||||
try {
|
||||
const j = await _post({ action: 'solicitar' });
|
||||
if (!j.ok) { _setEstado('Error al enviar solicitud.', '#c62828'); if (btn) btn.disabled = false; return; }
|
||||
if (j.transferido) { location.reload(); return; }
|
||||
_setEstado('✉ Solicitud enviada. El otro equipo tiene 5 segundos para responder…', '#1565c0');
|
||||
clearInterval(_checkTransferTimer);
|
||||
_checkTransferTimer = setInterval(async () => {
|
||||
try {
|
||||
const r = await _post({ action: 'check_transfer' });
|
||||
if (r.disponible) {
|
||||
clearInterval(_checkTransferTimer);
|
||||
_setEstado('✅ Puesto liberado. Tomando control…', '#2e7d32');
|
||||
const rc = await _post({ action: 'claim' });
|
||||
if (rc.disponible) location.reload();
|
||||
else _setEstado('No se pudo tomar el control. Recargue la página.', '#c62828');
|
||||
} else if (r.denegado) {
|
||||
clearInterval(_checkTransferTimer);
|
||||
_setEstado('❌ La solicitud fue denegada.', '#c62828');
|
||||
if (btn) { btn.disabled = false; btn.textContent = '📢 Volver a solicitar'; }
|
||||
}
|
||||
} catch(_) {}
|
||||
}, 1500);
|
||||
} catch(e) {
|
||||
_setEstado('Error de red: ' + e.message, '#c62828');
|
||||
if (btn) btn.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
/* Ir al siguiente disponible */
|
||||
window._ocSiguiente = async function() {
|
||||
const btn = document.getElementById('_btn-siguiente');
|
||||
if (btn) btn.disabled = true;
|
||||
_setEstado('Buscando puesto disponible…', '#555');
|
||||
try {
|
||||
const j = await _post({ action: 'disponibles' });
|
||||
if (!j.ok || !j.disponibles.length) {
|
||||
_setEstado('No hay otros puestos disponibles.', '#c62828');
|
||||
if (btn) btn.disabled = false;
|
||||
return;
|
||||
}
|
||||
_redirigirSiguiente(j.disponibles);
|
||||
} catch(e) {
|
||||
_setEstado('Error de red.', '#c62828');
|
||||
if (btn) btn.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
/* Modal de solicitud entrante (para el ocupante actual) */
|
||||
let _modalActivo = false;
|
||||
function _mostrarModalSolicitud(solicitante) {
|
||||
if (_modalActivo) return;
|
||||
_modalActivo = true;
|
||||
const modal = document.createElement('div');
|
||||
modal.style.cssText =
|
||||
'position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:99999;display:flex;align-items:center;justify-content:center';
|
||||
modal.innerHTML =
|
||||
'<div style="background:#fff;border-radius:14px;padding:32px 28px;max-width:420px;width:92%;text-align:center;box-shadow:0 8px 32px rgba(0,0,0,.25)">' +
|
||||
'<div style="font-size:2.5rem">⚠️</div>' +
|
||||
'<h3 style="color:#c62828;margin:10px 0 6px">Solicitud de transferencia</h3>' +
|
||||
'<p style="color:#333;margin:0 0 6px"><strong>' + _e(solicitante) + '</strong> quiere usar este puesto.</p>' +
|
||||
'<p style="color:#555;font-size:.95rem">Si no responde, se cerrará automáticamente en <strong id="_sol-cnt">5</strong> segundos.</p>' +
|
||||
'<div style="display:flex;gap:10px;justify-content:center;margin-top:18px">' +
|
||||
'<button id="_sol-aceptar" style="padding:10px 22px;background:#c62828;color:#fff;border:none;border-radius:8px;font-size:1rem;cursor:pointer;font-weight:600">Aceptar y ceder</button>' +
|
||||
'<button id="_sol-denegar" style="padding:10px 22px;background:#f5f5f5;color:#333;border:1px solid #ccc;border-radius:8px;font-size:1rem;cursor:pointer">Denegar</button>' +
|
||||
'</div></div>';
|
||||
document.body.appendChild(modal);
|
||||
|
||||
let secs = 5;
|
||||
const tick = setInterval(() => {
|
||||
secs--;
|
||||
const el = document.getElementById('_sol-cnt');
|
||||
if (el) el.textContent = secs;
|
||||
if (secs <= 0) { clearInterval(tick); _liberar(); }
|
||||
}, 1000);
|
||||
|
||||
async function _liberar() {
|
||||
modal.remove();
|
||||
await _post({ action: 'aceptar' }).catch(() => {});
|
||||
try {
|
||||
const j = await _post({ action: 'disponibles' });
|
||||
_redirigirSiguiente(j.disponibles || []);
|
||||
} catch(_) {
|
||||
window.location.href = BASE_WA + 'erp.php?m=turnero';
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('_sol-aceptar').onclick = () => { clearInterval(tick); _liberar(); };
|
||||
document.getElementById('_sol-denegar').onclick = async () => {
|
||||
clearInterval(tick);
|
||||
modal.remove();
|
||||
_modalActivo = false;
|
||||
await _post({ action: 'denegar' }).catch(() => {});
|
||||
};
|
||||
}
|
||||
|
||||
/* Arranque */
|
||||
async function _iniciar() {
|
||||
try {
|
||||
const j = await _post({ action: 'claim' });
|
||||
if (!j.ok) return;
|
||||
if (!j.disponible) { _mostrarBloqueado(j.ocupado_por); return; }
|
||||
// Heartbeat cada 60 s
|
||||
setInterval(() => _post({ action: 'ping' }).catch(() => {}), 60000);
|
||||
// Polling de solicitudes entrantes cada 2 s
|
||||
setInterval(async () => {
|
||||
if (_modalActivo) return;
|
||||
try {
|
||||
const r = await _post({ action: 'check_solicitud' });
|
||||
if (r.hay_solicitud) _mostrarModalSolicitud(r.solicitante);
|
||||
} catch(_) {}
|
||||
}, 2000);
|
||||
} catch(_) {}
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
navigator.sendBeacon(API + 'ocupar_lugar.php',
|
||||
new Blob([JSON.stringify({ lugar_id: _ID, action: 'release' })], { type: 'application/json' }));
|
||||
});
|
||||
|
||||
_iniciar();
|
||||
})(lugarId, LUGAR_NOMBRE, 'lugar', 'lugar_id');
|
||||
</script>
|
||||
|
||||
<div class="rec-toast" id="lug-toast">
|
||||
|
||||
@@ -525,7 +525,8 @@ let pollingColaId = null;
|
||||
const API = '<?= BASE_URL ?>modules/turnero/api/';
|
||||
const API_PAC = '<?= BASE_URL ?>api/lab/get_pacientes.php';
|
||||
const BASE_WA = '<?= BASE_URL ?>';
|
||||
const DESK_ID = <?= $deskId ?: 'null' ?>;
|
||||
const DESK_ID = <?= $deskId ?: 'null' ?>;
|
||||
const DESK_NOMBRE = '<?= addslashes($deskNombre ?? 'Recepción') ?>';
|
||||
|
||||
// ── Arranque ──────────────────────────────────────────────────
|
||||
let pollingConsentimientosId = null;
|
||||
@@ -1365,6 +1366,183 @@ function mostrarLlamando(codigo) {
|
||||
mostrarToast('Llamando turno ' + codigo + '…', 'info', 2000);
|
||||
}
|
||||
|
||||
/* ── Ocupación exclusiva del escritorio ──────────────────────── */
|
||||
(function(_ID, _NOMBRE, _VIEW, _PARAM) {
|
||||
if (!_ID) return;
|
||||
|
||||
function _e(s) {
|
||||
const d = document.createElement('div');
|
||||
d.appendChild(document.createTextNode(String(s ?? '')));
|
||||
return d.innerHTML;
|
||||
}
|
||||
function _post(body) {
|
||||
return fetch(API + 'ocupar_lugar.php', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(Object.assign({ lugar_id: _ID }, body)),
|
||||
}).then(r => r.json());
|
||||
}
|
||||
function _redirigirSiguiente(disponibles) {
|
||||
if (disponibles && disponibles.length) {
|
||||
window.location.href = BASE_WA + 'erp.php?m=turnero&v=' + _VIEW + '&' + _PARAM + '=' + disponibles[0].id;
|
||||
} else {
|
||||
window.location.href = BASE_WA + 'erp.php?m=turnero';
|
||||
}
|
||||
}
|
||||
|
||||
/* Pantalla de bloqueo */
|
||||
function _mostrarBloqueado(ocupadoPor) {
|
||||
document.body.innerHTML =
|
||||
'<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;' +
|
||||
'min-height:100vh;background:#f8faff;font-family:sans-serif;gap:14px;text-align:center;padding:28px">' +
|
||||
'<div style="font-size:4rem">🔒</div>' +
|
||||
'<h2 style="color:#1565c0;margin:0">' + _e(_NOMBRE) + ' está abierta en otro equipo</h2>' +
|
||||
'<p style="color:#555;font-size:1.05rem;max-width:440px;line-height:1.6">' +
|
||||
'Actualmente usada por <strong>' + _e(ocupadoPor) + '</strong>.<br>' +
|
||||
'Puedes solicitar que cierre su sesión o ir al siguiente escritorio disponible.</p>' +
|
||||
'<div style="display:flex;gap:12px;flex-wrap:wrap;justify-content:center;margin-top:4px">' +
|
||||
'<button id="_btn-solicitar" onclick="window._ocToma()" ' +
|
||||
'style="padding:12px 26px;background:#1565c0;color:#fff;border:none;border-radius:8px;font-size:1rem;cursor:pointer;font-weight:600">' +
|
||||
'📢 Solicitar cierre en ese equipo</button>' +
|
||||
'<button id="_btn-siguiente" onclick="window._ocSiguiente()" ' +
|
||||
'style="padding:12px 26px;background:#f0f4ff;color:#1565c0;border:2px solid #1565c0;border-radius:8px;font-size:1rem;cursor:pointer;font-weight:600">' +
|
||||
'➡ Ir al siguiente disponible</button>' +
|
||||
'</div>' +
|
||||
'<div id="_oc-estado" style="min-height:24px;color:#777;font-size:.95rem;margin-top:4px"></div>' +
|
||||
'</div>';
|
||||
}
|
||||
function _setEstado(msg, color) {
|
||||
const el = document.getElementById('_oc-estado');
|
||||
if (el) el.innerHTML = '<span style="color:' + (color||'#555') + '">' + msg + '</span>';
|
||||
}
|
||||
|
||||
/* Solicitar toma de control */
|
||||
let _checkTransferTimer = null;
|
||||
window._ocToma = async function() {
|
||||
const btn = document.getElementById('_btn-solicitar');
|
||||
if (btn) btn.disabled = true;
|
||||
_setEstado('Enviando solicitud…', '#555');
|
||||
try {
|
||||
const j = await _post({ action: 'solicitar' });
|
||||
if (!j.ok) { _setEstado('Error al enviar solicitud.', '#c62828'); if (btn) btn.disabled = false; return; }
|
||||
if (j.transferido) { location.reload(); return; }
|
||||
_setEstado('✉ Solicitud enviada. El otro equipo tiene 5 segundos para responder…', '#1565c0');
|
||||
clearInterval(_checkTransferTimer);
|
||||
_checkTransferTimer = setInterval(async () => {
|
||||
try {
|
||||
const r = await _post({ action: 'check_transfer' });
|
||||
if (r.disponible) {
|
||||
clearInterval(_checkTransferTimer);
|
||||
_setEstado('✅ Escritorio liberado. Tomando control…', '#2e7d32');
|
||||
const rc = await _post({ action: 'claim' });
|
||||
if (rc.disponible) location.reload();
|
||||
else _setEstado('No se pudo tomar el control. Recargue la página.', '#c62828');
|
||||
} else if (r.denegado) {
|
||||
clearInterval(_checkTransferTimer);
|
||||
_setEstado('❌ La solicitud fue denegada.', '#c62828');
|
||||
if (btn) { btn.disabled = false; btn.textContent = '📢 Volver a solicitar'; }
|
||||
}
|
||||
} catch(_) {}
|
||||
}, 1500);
|
||||
} catch(e) {
|
||||
_setEstado('Error de red: ' + e.message, '#c62828');
|
||||
if (btn) btn.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
/* Ir al siguiente disponible */
|
||||
window._ocSiguiente = async function() {
|
||||
const btn = document.getElementById('_btn-siguiente');
|
||||
if (btn) btn.disabled = true;
|
||||
_setEstado('Buscando escritorio disponible…', '#555');
|
||||
try {
|
||||
const j = await _post({ action: 'disponibles' });
|
||||
if (!j.ok || !j.disponibles.length) {
|
||||
_setEstado('No hay otros escritorios disponibles.', '#c62828');
|
||||
if (btn) btn.disabled = false;
|
||||
return;
|
||||
}
|
||||
_redirigirSiguiente(j.disponibles);
|
||||
} catch(e) {
|
||||
_setEstado('Error de red.', '#c62828');
|
||||
if (btn) btn.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
/* Modal de solicitud entrante (para el ocupante actual) */
|
||||
let _modalActivo = false;
|
||||
function _mostrarModalSolicitud(solicitante) {
|
||||
if (_modalActivo) return;
|
||||
_modalActivo = true;
|
||||
const modal = document.createElement('div');
|
||||
modal.style.cssText =
|
||||
'position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:99999;display:flex;align-items:center;justify-content:center';
|
||||
modal.innerHTML =
|
||||
'<div style="background:#fff;border-radius:14px;padding:32px 28px;max-width:420px;width:92%;text-align:center;box-shadow:0 8px 32px rgba(0,0,0,.25)">' +
|
||||
'<div style="font-size:2.5rem">⚠️</div>' +
|
||||
'<h3 style="color:#c62828;margin:10px 0 6px">Solicitud de transferencia</h3>' +
|
||||
'<p style="color:#333;margin:0 0 6px"><strong>' + _e(solicitante) + '</strong> quiere usar este escritorio.</p>' +
|
||||
'<p style="color:#555;font-size:.95rem">Si no responde, se cerrará automáticamente en <strong id="_sol-cnt">5</strong> segundos.</p>' +
|
||||
'<div style="display:flex;gap:10px;justify-content:center;margin-top:18px">' +
|
||||
'<button id="_sol-aceptar" style="padding:10px 22px;background:#c62828;color:#fff;border:none;border-radius:8px;font-size:1rem;cursor:pointer;font-weight:600">Aceptar y ceder</button>' +
|
||||
'<button id="_sol-denegar" style="padding:10px 22px;background:#f5f5f5;color:#333;border:1px solid #ccc;border-radius:8px;font-size:1rem;cursor:pointer">Denegar</button>' +
|
||||
'</div></div>';
|
||||
document.body.appendChild(modal);
|
||||
|
||||
let secs = 5;
|
||||
const tick = setInterval(() => {
|
||||
secs--;
|
||||
const el = document.getElementById('_sol-cnt');
|
||||
if (el) el.textContent = secs;
|
||||
if (secs <= 0) { clearInterval(tick); _liberar(); }
|
||||
}, 1000);
|
||||
|
||||
async function _liberar() {
|
||||
modal.remove();
|
||||
await _post({ action: 'aceptar' }).catch(() => {});
|
||||
try {
|
||||
const j = await _post({ action: 'disponibles' });
|
||||
_redirigirSiguiente(j.disponibles || []);
|
||||
} catch(_) {
|
||||
window.location.href = BASE_WA + 'erp.php?m=turnero';
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('_sol-aceptar').onclick = () => { clearInterval(tick); _liberar(); };
|
||||
document.getElementById('_sol-denegar').onclick = async () => {
|
||||
clearInterval(tick);
|
||||
modal.remove();
|
||||
_modalActivo = false;
|
||||
await _post({ action: 'denegar' }).catch(() => {});
|
||||
};
|
||||
}
|
||||
|
||||
/* Arranque */
|
||||
async function _iniciar() {
|
||||
try {
|
||||
const j = await _post({ action: 'claim' });
|
||||
if (!j.ok) return;
|
||||
if (!j.disponible) { _mostrarBloqueado(j.ocupado_por); return; }
|
||||
// Heartbeat cada 60 s
|
||||
setInterval(() => _post({ action: 'ping' }).catch(() => {}), 60000);
|
||||
// Polling de solicitudes entrantes cada 2 s
|
||||
setInterval(async () => {
|
||||
if (_modalActivo) return;
|
||||
try {
|
||||
const r = await _post({ action: 'check_solicitud' });
|
||||
if (r.hay_solicitud) _mostrarModalSolicitud(r.solicitante);
|
||||
} catch(_) {}
|
||||
}, 2000);
|
||||
} catch(_) {}
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
navigator.sendBeacon(API + 'ocupar_lugar.php',
|
||||
new Blob([JSON.stringify({ lugar_id: _ID, action: 'release' })], { type: 'application/json' }));
|
||||
});
|
||||
|
||||
_iniciar();
|
||||
})(DESK_ID, DESK_NOMBRE, 'recepcion', 'desk_id');
|
||||
|
||||
// ── Toast ────────────────────────────────────────────────────
|
||||
let toastTimer = null;
|
||||
function mostrarToast(msg, type = 'info', duration = 2500) {
|
||||
|
||||
Reference in New Issue
Block a user