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);
|
||||
$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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user