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:
Lizandro Guarnizo
2026-07-03 11:35:45 -05:00
co-authored by Claude Sonnet 4.6
parent daa9827d93
commit f8245b4d6f
5 changed files with 149 additions and 16 deletions
@@ -0,0 +1,2 @@
ALTER TABLE turnero_lugares
ADD COLUMN IF NOT EXISTS formulario_modo ENUM('link','embebido') NOT NULL DEFAULT 'link';
+52
View File
@@ -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]);
+6 -4
View File
@@ -52,6 +52,8 @@ $sortOrder = (int)($input['sort_order'] ?? 99);
$activo = (int)($input['activo'] ?? 1);
$tipo = in_array($input['tipo'] ?? '', ['recepcion', 'muestras'], true)
? $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'])
? array_filter(array_map('intval', $input['formulario_ids']), fn($v) => $v > 0)
: [];
@@ -64,15 +66,15 @@ $pdo->beginTransaction();
try {
if ($id) {
$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;
} else {
$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();
}
+29 -5
View File
@@ -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">
<?= $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')" 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>
</button>
<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">
<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')" 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>
</button>
<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">
<label class="form-label small fw-semibold">
<i class="fas fa-file-signature me-1 text-warning"></i>
Consentimientos requeridos para este lugar
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): ?>
@@ -352,6 +352,23 @@ $tab = $_GET['tab'] ?? 'lugares';
<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">
@@ -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;
try {
@@ -1001,7 +1021,7 @@ async function guardarLugar(tipoOrId = 'muestras', id = null) {
} 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-tipo').value = tipo;
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));
});
// 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();
}
+54 -1
View File
@@ -25,11 +25,13 @@ try {
$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';
$lugarFormModo = 'link'; // 'link' | 'embebido'
foreach ($lugares as $l) {
if ((int)$l['id'] === $lugarIdParam) {
$lugarNombre = $l['nombre'];
$lugarFormModo = $l['formulario_modo'] ?? 'link';
break;
}
}
@@ -320,6 +322,18 @@ require_once __DIR__ . '/../../../shared/components/sidebar.php';
</div>
<!-- ── 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">
<h6><i class="fas fa-file-signature me-1"></i>Consentimientos informados</h6>
@@ -392,6 +406,7 @@ let pollingConsentId = null;
const API = '<?= BASE_URL ?>modules/turnero/api/';
const BASE_WA = '<?= BASE_URL ?>';
const LUGAR_NOMBRE = document.getElementById('lbl-lugar-titulo')?.textContent || '<?= addslashes($lugarNombre) ?>';
const LUGAR_FORM_MODO = '<?= $lugarFormModo ?>';
// ── Arranque ──────────────────────────────────────────────────
@@ -523,6 +538,9 @@ async function seleccionarSinLlamar(turnoId) {
await cargarFichaSolicitud(t.id);
clearInterval(pollingConsentId);
pollingConsentId = setInterval(() => actualizarConsentimientos(turnoActivo?.id), 5000);
if (LUGAR_FORM_MODO === 'embebido' && lugarId) {
cargarFormEmbebido(t.id);
}
mostrarFichaMobile();
}
@@ -596,6 +614,12 @@ async function abrirFicha(turno) {
// Iniciar polling de consentimientos
clearInterval(pollingConsentId);
pollingConsentId = setInterval(() => actualizarConsentimientos(turnoActivo?.id), 5000);
// Formulario embebido
if (LUGAR_FORM_MODO === 'embebido' && lugarId) {
cargarFormEmbebido(turno.id);
}
mostrarFichaMobile();
}
@@ -835,12 +859,41 @@ async function cambiarEstadoTurno(nuevoEstado) {
}
// ── 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() {
clearInterval(pollingConsentId);
turnoActivo = null;
tieneConsent = 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-placeholder').classList.remove('d-none');
document.getElementById('badge-turno-activo').classList.add('d-none');