feat: modo embebido de formulario por lugar (link vs iframe)
- Nueva columna turnero_lugares.formulario_modo (link|embebido) - Config: radio en modal de edición para elegir el modo - lugar.php: muestra iframe con ver_formulario_enviado al activar turno - API get_consent_token.php: crea/obtiene token para turno+lugar Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
daa9827d93
commit
f8245b4d6f
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE turnero_lugares
|
||||||
|
ADD COLUMN IF NOT EXISTS formulario_modo ENUM('link','embebido') NOT NULL DEFAULT 'link';
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* GET ?turno_id=X&lugar_id=Y
|
||||||
|
* Devuelve (o crea) el token de consentimiento para un turno+lugar en modo embebido.
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/_helpers.php';
|
||||||
|
requireTurnero();
|
||||||
|
|
||||||
|
$turnoId = (int)($_GET['turno_id'] ?? 0);
|
||||||
|
$lugarId = (int)($_GET['lugar_id'] ?? 0);
|
||||||
|
if (!$turnoId || !$lugarId) jsonError('turno_id y lugar_id requeridos');
|
||||||
|
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
// Obtener formulario configurado para el lugar
|
||||||
|
$formRow = $pdo->prepare(
|
||||||
|
"SELECT tlc.formulario_id FROM turnero_lugar_consentimientos tlc
|
||||||
|
JOIN lab_formularios f ON f.id = tlc.formulario_id AND f.is_active = 1
|
||||||
|
WHERE tlc.lugar_id = ?
|
||||||
|
ORDER BY tlc.formulario_id ASC LIMIT 1"
|
||||||
|
);
|
||||||
|
$formRow->execute([$lugarId]);
|
||||||
|
$form = $formRow->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!$form) jsonError('No hay formulario configurado para este lugar', 404);
|
||||||
|
|
||||||
|
$formularioId = (int)$form['formulario_id'];
|
||||||
|
|
||||||
|
// Buscar token existente
|
||||||
|
$existing = $pdo->prepare(
|
||||||
|
"SELECT token FROM turnero_consentimientos WHERE turno_id = ? AND formulario_id = ? LIMIT 1"
|
||||||
|
);
|
||||||
|
$existing->execute([$turnoId, $formularioId]);
|
||||||
|
$row = $existing->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if ($row) {
|
||||||
|
jsonOk(['token' => $row['token'], 'nuevo' => false]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crear nuevo token
|
||||||
|
$token = sprintf(
|
||||||
|
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
|
||||||
|
mt_rand(0,0xffff), mt_rand(0,0xffff),
|
||||||
|
mt_rand(0,0xffff),
|
||||||
|
mt_rand(0,0x0fff)|0x4000,
|
||||||
|
mt_rand(0,0x3fff)|0x8000,
|
||||||
|
mt_rand(0,0xffff), mt_rand(0,0xffff), mt_rand(0,0xffff)
|
||||||
|
);
|
||||||
|
$pdo->prepare(
|
||||||
|
"INSERT IGNORE INTO turnero_consentimientos (turno_id, formulario_id, token, estado) VALUES (?, ?, ?, 'pendiente')"
|
||||||
|
)->execute([$turnoId, $formularioId, $token]);
|
||||||
|
|
||||||
|
jsonOk(['token' => $token, 'nuevo' => true]);
|
||||||
@@ -52,6 +52,8 @@ $sortOrder = (int)($input['sort_order'] ?? 99);
|
|||||||
$activo = (int)($input['activo'] ?? 1);
|
$activo = (int)($input['activo'] ?? 1);
|
||||||
$tipo = in_array($input['tipo'] ?? '', ['recepcion', 'muestras'], true)
|
$tipo = in_array($input['tipo'] ?? '', ['recepcion', 'muestras'], true)
|
||||||
? $input['tipo'] : 'muestras';
|
? $input['tipo'] : 'muestras';
|
||||||
|
$formModo = in_array($input['formulario_modo'] ?? '', ['link', 'embebido'], true)
|
||||||
|
? $input['formulario_modo'] : 'link';
|
||||||
$formularioIds = isset($input['formulario_ids']) && is_array($input['formulario_ids'])
|
$formularioIds = isset($input['formulario_ids']) && is_array($input['formulario_ids'])
|
||||||
? array_filter(array_map('intval', $input['formulario_ids']), fn($v) => $v > 0)
|
? array_filter(array_map('intval', $input['formulario_ids']), fn($v) => $v > 0)
|
||||||
: [];
|
: [];
|
||||||
@@ -64,15 +66,15 @@ $pdo->beginTransaction();
|
|||||||
try {
|
try {
|
||||||
if ($id) {
|
if ($id) {
|
||||||
$stmt = $pdo->prepare(
|
$stmt = $pdo->prepare(
|
||||||
'UPDATE turnero_lugares SET nombre=?, tipo=?, descripcion=?, sort_order=?, activo=? WHERE id=?'
|
'UPDATE turnero_lugares SET nombre=?, tipo=?, descripcion=?, sort_order=?, activo=?, formulario_modo=? WHERE id=?'
|
||||||
);
|
);
|
||||||
$stmt->execute([$nombre, $tipo, $desc ?: null, $sortOrder, $activo, $id]);
|
$stmt->execute([$nombre, $tipo, $desc ?: null, $sortOrder, $activo, $formModo, $id]);
|
||||||
$lugarId = $id;
|
$lugarId = $id;
|
||||||
} else {
|
} else {
|
||||||
$stmt = $pdo->prepare(
|
$stmt = $pdo->prepare(
|
||||||
'INSERT INTO turnero_lugares (nombre, tipo, descripcion, sort_order, activo) VALUES (?, ?, ?, ?, ?)'
|
'INSERT INTO turnero_lugares (nombre, tipo, descripcion, sort_order, activo, formulario_modo) VALUES (?, ?, ?, ?, ?, ?)'
|
||||||
);
|
);
|
||||||
$stmt->execute([$nombre, $tipo, $desc ?: null, $sortOrder, $activo]);
|
$stmt->execute([$nombre, $tipo, $desc ?: null, $sortOrder, $activo, $formModo]);
|
||||||
$lugarId = (int) $pdo->lastInsertId();
|
$lugarId = (int) $pdo->lastInsertId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ $tab = $_GET['tab'] ?? 'lugares';
|
|||||||
<span class="badge <?= $lu['activo'] ? 'bg-success-subtle text-success' : 'bg-secondary-subtle text-secondary' ?> ms-1">
|
<span class="badge <?= $lu['activo'] ? 'bg-success-subtle text-success' : 'bg-secondary-subtle text-secondary' ?> ms-1">
|
||||||
<?= $lu['activo'] ? 'Activo' : 'Inactivo' ?>
|
<?= $lu['activo'] ? 'Activo' : 'Inactivo' ?>
|
||||||
</span>
|
</span>
|
||||||
<button class="btn-icon text-primary" onclick="editarLugar(<?= $lu['id'] ?>,'<?= addslashes($lu['nombre']) ?>','<?= addslashes($lu['descripcion'] ?? '') ?>',<?= $lu['activo'] ?>,<?= $lu['sort_order'] ?>,'recepcion')" title="Editar">
|
<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>
|
<i class="fas fa-pencil-alt"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-icon text-danger" onclick="eliminarLugar(<?= $lu['id'] ?>, '<?= addslashes($lu['nombre']) ?>')" title="Eliminar">
|
<button class="btn-icon text-danger" onclick="eliminarLugar(<?= $lu['id'] ?>, '<?= addslashes($lu['nombre']) ?>')" title="Eliminar">
|
||||||
@@ -268,7 +268,7 @@ $tab = $_GET['tab'] ?? 'lugares';
|
|||||||
<button class="btn-icon" onclick="copiarUrlDisplay(<?= $lu['id'] ?>)" title="Copiar URL pantalla TV de este lugar">
|
<button class="btn-icon" onclick="copiarUrlDisplay(<?= $lu['id'] ?>)" title="Copiar URL pantalla TV de este lugar">
|
||||||
<i class="fas fa-tv"></i>
|
<i class="fas fa-tv"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-icon text-primary" onclick="editarLugar(<?= $lu['id'] ?>,'<?= addslashes($lu['nombre']) ?>','<?= addslashes($lu['descripcion'] ?? '') ?>',<?= $lu['activo'] ?>,<?= $lu['sort_order'] ?>,'muestras')" title="Editar">
|
<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>
|
<i class="fas fa-pencil-alt"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-icon text-danger" onclick="eliminarLugar(<?= $lu['id'] ?>, '<?= addslashes($lu['nombre']) ?>')" title="Eliminar">
|
<button class="btn-icon text-danger" onclick="eliminarLugar(<?= $lu['id'] ?>, '<?= addslashes($lu['nombre']) ?>')" title="Eliminar">
|
||||||
@@ -334,7 +334,7 @@ $tab = $_GET['tab'] ?? 'lugares';
|
|||||||
<div class="mb-2 mt-3">
|
<div class="mb-2 mt-3">
|
||||||
<label class="form-label small fw-semibold">
|
<label class="form-label small fw-semibold">
|
||||||
<i class="fas fa-file-signature me-1 text-warning"></i>
|
<i class="fas fa-file-signature me-1 text-warning"></i>
|
||||||
Consentimientos requeridos para este lugar
|
Formularios de consentimiento
|
||||||
</label>
|
</label>
|
||||||
<div id="edit-lu-consents" class="border rounded p-2" style="max-height:160px;overflow-y:auto;background:#fffbeb">
|
<div id="edit-lu-consents" class="border rounded p-2" style="max-height:160px;overflow-y:auto;background:#fffbeb">
|
||||||
<?php foreach ($formulariosCons as $fc): ?>
|
<?php foreach ($formulariosCons as $fc): ?>
|
||||||
@@ -352,6 +352,23 @@ $tab = $_GET['tab'] ?? 'lugares';
|
|||||||
<small class="text-muted">No hay formularios activos configurados.</small>
|
<small class="text-muted">No hay formularios activos configurados.</small>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<div class="modal-footer py-2 px-3">
|
<div class="modal-footer py-2 px-3">
|
||||||
@@ -986,7 +1003,10 @@ async function guardarLugar(tipoOrId = 'muestras', id = null) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = { nombre, tipo, descripcion: desc, sort_order: orden, activo, formulario_ids: formularioIds };
|
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;
|
if (id) body.id = id;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1001,7 +1021,7 @@ async function guardarLugar(tipoOrId = 'muestras', id = null) {
|
|||||||
} catch (e) { toast('Error de conexión', 'error'); }
|
} catch (e) { toast('Error de conexión', 'error'); }
|
||||||
}
|
}
|
||||||
|
|
||||||
function editarLugar(id, nombre, desc, activo, orden, tipo = 'muestras') {
|
function editarLugar(id, nombre, desc, activo, orden, tipo = 'muestras', formModo = 'link') {
|
||||||
document.getElementById('edit-lu-id').value = id;
|
document.getElementById('edit-lu-id').value = id;
|
||||||
document.getElementById('edit-lu-tipo').value = tipo;
|
document.getElementById('edit-lu-tipo').value = tipo;
|
||||||
document.getElementById('edit-lu-nombre').value = nombre;
|
document.getElementById('edit-lu-nombre').value = nombre;
|
||||||
@@ -1017,6 +1037,10 @@ function editarLugar(id, nombre, desc, activo, orden, tipo = 'muestras') {
|
|||||||
chk.checked = formIds.includes(parseInt(chk.value));
|
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();
|
new bootstrap.Modal(document.getElementById('modalEditarLugar')).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,13 +23,15 @@ try {
|
|||||||
$lugares = [];
|
$lugares = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
$lugarIdParam = isset($_GET['lugar_id']) ? (int)$_GET['lugar_id'] : 0;
|
$lugarIdParam = isset($_GET['lugar_id']) ? (int)$_GET['lugar_id'] : 0;
|
||||||
|
|
||||||
// Resolver nombre del lugar para el título
|
// Resolver nombre y modo del lugar para el título
|
||||||
$lugarNombre = 'Estación de Servicio';
|
$lugarNombre = 'Estación de Servicio';
|
||||||
|
$lugarFormModo = 'link'; // 'link' | 'embebido'
|
||||||
foreach ($lugares as $l) {
|
foreach ($lugares as $l) {
|
||||||
if ((int)$l['id'] === $lugarIdParam) {
|
if ((int)$l['id'] === $lugarIdParam) {
|
||||||
$lugarNombre = $l['nombre'];
|
$lugarNombre = $l['nombre'];
|
||||||
|
$lugarFormModo = $l['formulario_modo'] ?? 'link';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -320,6 +322,18 @@ require_once __DIR__ . '/../../../shared/components/sidebar.php';
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── Sección: Consentimientos ── -->
|
<!-- ── Sección: Consentimientos ── -->
|
||||||
|
<!-- ── Formulario embebido (modo=embebido) ── -->
|
||||||
|
<div class="ficha-sec d-none" id="sec-form-embebido">
|
||||||
|
<h6><i class="fas fa-file-alt me-1"></i>Formulario de consentimiento</h6>
|
||||||
|
<div id="form-embebido-loading" class="text-muted small py-2">
|
||||||
|
<i class="fas fa-spinner fa-spin me-1"></i>Cargando formulario...
|
||||||
|
</div>
|
||||||
|
<iframe id="form-embebido-iframe" src="" frameborder="0"
|
||||||
|
style="width:100%;min-height:520px;border-radius:8px;border:1px solid #e2e8f0;display:none"
|
||||||
|
onload="this.style.display='block';document.getElementById('form-embebido-loading').style.display='none'">
|
||||||
|
</iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="ficha-sec" id="sec-consent">
|
<div class="ficha-sec" id="sec-consent">
|
||||||
<h6><i class="fas fa-file-signature me-1"></i>Consentimientos informados</h6>
|
<h6><i class="fas fa-file-signature me-1"></i>Consentimientos informados</h6>
|
||||||
|
|
||||||
@@ -389,9 +403,10 @@ let hayPendientes = false;
|
|||||||
let pollingColaId = null;
|
let pollingColaId = null;
|
||||||
let pollingConsentId = null;
|
let pollingConsentId = null;
|
||||||
|
|
||||||
const API = '<?= BASE_URL ?>modules/turnero/api/';
|
const API = '<?= BASE_URL ?>modules/turnero/api/';
|
||||||
const BASE_WA = '<?= BASE_URL ?>';
|
const BASE_WA = '<?= BASE_URL ?>';
|
||||||
const LUGAR_NOMBRE = document.getElementById('lbl-lugar-titulo')?.textContent || '<?= addslashes($lugarNombre) ?>';
|
const LUGAR_NOMBRE = document.getElementById('lbl-lugar-titulo')?.textContent || '<?= addslashes($lugarNombre) ?>';
|
||||||
|
const LUGAR_FORM_MODO = '<?= $lugarFormModo ?>';
|
||||||
|
|
||||||
|
|
||||||
// ── Arranque ──────────────────────────────────────────────────
|
// ── Arranque ──────────────────────────────────────────────────
|
||||||
@@ -523,6 +538,9 @@ async function seleccionarSinLlamar(turnoId) {
|
|||||||
await cargarFichaSolicitud(t.id);
|
await cargarFichaSolicitud(t.id);
|
||||||
clearInterval(pollingConsentId);
|
clearInterval(pollingConsentId);
|
||||||
pollingConsentId = setInterval(() => actualizarConsentimientos(turnoActivo?.id), 5000);
|
pollingConsentId = setInterval(() => actualizarConsentimientos(turnoActivo?.id), 5000);
|
||||||
|
if (LUGAR_FORM_MODO === 'embebido' && lugarId) {
|
||||||
|
cargarFormEmbebido(t.id);
|
||||||
|
}
|
||||||
mostrarFichaMobile();
|
mostrarFichaMobile();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -596,6 +614,12 @@ async function abrirFicha(turno) {
|
|||||||
// Iniciar polling de consentimientos
|
// Iniciar polling de consentimientos
|
||||||
clearInterval(pollingConsentId);
|
clearInterval(pollingConsentId);
|
||||||
pollingConsentId = setInterval(() => actualizarConsentimientos(turnoActivo?.id), 5000);
|
pollingConsentId = setInterval(() => actualizarConsentimientos(turnoActivo?.id), 5000);
|
||||||
|
|
||||||
|
// Formulario embebido
|
||||||
|
if (LUGAR_FORM_MODO === 'embebido' && lugarId) {
|
||||||
|
cargarFormEmbebido(turno.id);
|
||||||
|
}
|
||||||
|
|
||||||
mostrarFichaMobile();
|
mostrarFichaMobile();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -835,12 +859,41 @@ async function cambiarEstadoTurno(nuevoEstado) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Reset ─────────────────────────────────────────────────────
|
// ── Reset ─────────────────────────────────────────────────────
|
||||||
|
async function cargarFormEmbebido(turnoId) {
|
||||||
|
const sec = document.getElementById('sec-form-embebido');
|
||||||
|
const iframe = document.getElementById('form-embebido-iframe');
|
||||||
|
const load = document.getElementById('form-embebido-loading');
|
||||||
|
if (!sec || !iframe) return;
|
||||||
|
sec.classList.remove('d-none');
|
||||||
|
iframe.style.display = 'none';
|
||||||
|
load.style.display = '';
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API}get_consent_token.php?turno_id=${turnoId}&lugar_id=${lugarId}`);
|
||||||
|
const json = await res.json();
|
||||||
|
if (json.ok && json.token) {
|
||||||
|
iframe.src = `${BASE_WA}ver_formulario_enviado.php?token=${encodeURIComponent(json.token)}`;
|
||||||
|
} else {
|
||||||
|
load.innerHTML = '<span class="text-danger small"><i class="fas fa-exclamation-triangle me-1"></i>' + (json.error || 'No hay formulario configurado') + '</span>';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
load.innerHTML = '<span class="text-danger small"><i class="fas fa-exclamation-triangle me-1"></i>Error al cargar formulario</span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function resetFicha() {
|
function resetFicha() {
|
||||||
clearInterval(pollingConsentId);
|
clearInterval(pollingConsentId);
|
||||||
turnoActivo = null;
|
turnoActivo = null;
|
||||||
tieneConsent = false;
|
tieneConsent = false;
|
||||||
hayPendientes = false;
|
hayPendientes = false;
|
||||||
|
|
||||||
|
// Limpiar iframe embebido
|
||||||
|
const iframe = document.getElementById('form-embebido-iframe');
|
||||||
|
const sec = document.getElementById('sec-form-embebido');
|
||||||
|
const load = document.getElementById('form-embebido-loading');
|
||||||
|
if (iframe) { iframe.src = ''; iframe.style.display = 'none'; }
|
||||||
|
if (sec) { sec.classList.add('d-none'); }
|
||||||
|
if (load) { load.style.display = ''; load.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Cargando formulario...'; }
|
||||||
|
|
||||||
document.getElementById('ficha-turno').classList.add('d-none');
|
document.getElementById('ficha-turno').classList.add('d-none');
|
||||||
document.getElementById('ficha-placeholder').classList.remove('d-none');
|
document.getElementById('ficha-placeholder').classList.remove('d-none');
|
||||||
document.getElementById('badge-turno-activo').classList.add('d-none');
|
document.getElementById('badge-turno-activo').classList.add('d-none');
|
||||||
|
|||||||
Reference in New Issue
Block a user