feat(formulario): firma profesional directa en modal embebido (formularios solo-pro)
- ver_formulario_enviado.php: pre-scan $_soloFirmaPro; muestra canvas pro cuando modoTurnero+embebido+sesión activa+sin firma paciente - fpw-save JS: recopila campos del form y envía solo_profesional=true; al guardar muestra éxito y postMessage turneroFirmado - firmar_profesional_consentimiento.php: si solo_profesional=true guarda datos_respuestas y marca estado=firmado Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
1e2cfbaa0b
commit
3dbf40e4fa
@@ -16,6 +16,9 @@ $body = inputJson();
|
||||
$turnoId = (int)($body['turno_id'] ?? 0);
|
||||
$formularioId = (int)($body['formulario_id'] ?? 0);
|
||||
$svg = $body['svg'] ?? '';
|
||||
$soloPro = !empty($body['solo_profesional']);
|
||||
$datosResp = (isset($body['datos_respuestas']) && is_array($body['datos_respuestas']))
|
||||
? json_encode($body['datos_respuestas'], JSON_UNESCAPED_UNICODE) : null;
|
||||
|
||||
if (!$turnoId) jsonError('turno_id requerido.');
|
||||
if (!$formularioId) jsonError('formulario_id requerido.');
|
||||
@@ -37,12 +40,22 @@ $tc = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$tc) jsonError('Consentimiento no encontrado.', 404);
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
"UPDATE turnero_consentimientos
|
||||
SET firma_profesional_svg = ?, firmado_profesional_at = NOW()
|
||||
WHERE turno_id = ? AND formulario_id = ?"
|
||||
);
|
||||
$stmt->execute([$svg, $turnoId, $formularioId]);
|
||||
if ($soloPro) {
|
||||
// El profesional es el firmante final: guardar datos + marcar como firmado
|
||||
$pdo->prepare(
|
||||
"UPDATE turnero_consentimientos
|
||||
SET firma_profesional_svg = ?, firmado_profesional_at = NOW(),
|
||||
estado = 'firmado', firmado_at = NOW(),
|
||||
datos_respuestas = COALESCE(?, datos_respuestas)
|
||||
WHERE turno_id = ? AND formulario_id = ?"
|
||||
)->execute([$svg, $datosResp, $turnoId, $formularioId]);
|
||||
} else {
|
||||
$pdo->prepare(
|
||||
"UPDATE turnero_consentimientos
|
||||
SET firma_profesional_svg = ?, firmado_profesional_at = NOW()
|
||||
WHERE turno_id = ? AND formulario_id = ?"
|
||||
)->execute([$svg, $turnoId, $formularioId]);
|
||||
}
|
||||
|
||||
notificarSSE((int)$tc['sesion_id']);
|
||||
|
||||
|
||||
+57
-21
@@ -189,6 +189,9 @@ $datosPrefilled = json_decode($envio['datos_prefilled'] ?? '{}', true) ?? [];
|
||||
$todos = array_merge($datosPrefilled, $datosCliente);
|
||||
$modoEditar = $modoTurnero && $envio['estado'] !== 'firmado';
|
||||
$embebido = isset($_GET['embed']) && $_GET['embed'] === '1';
|
||||
// Pre-scan: formulario que solo requiere firma del profesional (sin firma paciente)
|
||||
$_soloFirmaPro = !empty(array_filter($esquema, fn($c) => ($c['tipo'] ?? '') === 'firma_profesional'))
|
||||
&& empty(array_filter($esquema, fn($c) => ($c['tipo'] ?? '') === 'firma'));
|
||||
|
||||
// Mapa id → label
|
||||
$labelMap = [];
|
||||
@@ -557,14 +560,19 @@ function esc2(mixed $v): string {
|
||||
<div class="turnero-msg mt-2 small"></div>
|
||||
</div>
|
||||
<?php elseif ($isPro): ?>
|
||||
<?php if (!$modoTurnero && !$modoPublico && !$yaHayCanvasPro): ?>
|
||||
<!-- Canvas del profesional: solo aparece una vez -->
|
||||
<?php
|
||||
// Mostrar canvas pro si: admin normal, O turnero+embebido+sesión activa+solo firma pro
|
||||
$_mostrarCanvasPro = !$yaHayCanvasPro && (
|
||||
(!$modoTurnero && !$modoPublico) ||
|
||||
($modoTurnero && $embebido && isUserLoggedIn() && $_soloFirmaPro && $modoEditar)
|
||||
);
|
||||
if ($_mostrarCanvasPro): ?>
|
||||
<!-- Canvas del profesional -->
|
||||
<div class="firma-pro-widget no-print" id="fpw-<?= htmlspecialchars($cid) ?>"
|
||||
data-envio="<?= (int)$envio['id'] ?>" data-campo="<?= htmlspecialchars($cid) ?>"
|
||||
<?php if ($modoTurnero): ?>
|
||||
data-turno="<?= (int)$tcRow['turno_id'] ?>"
|
||||
data-formulario="<?= (int)$tcRow['formulario_id'] ?>"
|
||||
<?php endif; ?>>
|
||||
data-solo-pro="<?= ($modoTurnero && $_soloFirmaPro) ? '1' : '0' ?>"
|
||||
data-turno="<?= isset($tcRow) ? (int)$tcRow['turno_id'] : '' ?>"
|
||||
data-formulario="<?= isset($tcRow) ? (int)$tcRow['formulario_id'] : '' ?>">
|
||||
<p class="text-muted small mb-2"><i class="fas fa-pen me-1"></i>Dibuje su firma en el recuadro:</p>
|
||||
<canvas class="fpw-canvas" width="500" height="150"></canvas>
|
||||
<div class="mt-2 d-flex gap-2">
|
||||
@@ -891,7 +899,6 @@ function esc2(mixed $v): string {
|
||||
|
||||
btnSave.addEventListener('click', function() {
|
||||
const svg = canvas.toDataURL('image/png');
|
||||
// Verificar que no esté vacío (al menos 1000 bytes de data)
|
||||
if (svg.length < 1000) {
|
||||
msg.innerHTML = '<span class="text-danger"><i class="fas fa-exclamation-triangle me-1"></i>Por favor dibuje su firma antes de guardar.</span>';
|
||||
return;
|
||||
@@ -902,12 +909,32 @@ function esc2(mixed $v): string {
|
||||
|
||||
const turnoId = widget.dataset.turno;
|
||||
const formularioId = widget.dataset.formulario;
|
||||
const soloPro = widget.dataset.soloPro === '1';
|
||||
const isTurnero = !!(turnoId && formularioId);
|
||||
const saveUrl = isTurnero
|
||||
? 'modules/turnero/api/firmar_profesional_consentimiento.php'
|
||||
: 'api/lab/firmar_profesional.php';
|
||||
const savePayload = isTurnero
|
||||
? { turno_id: parseInt(turnoId), formulario_id: parseInt(formularioId), svg: svg }
|
||||
|
||||
// Recopilar campos del formulario cuando el pro es el firmante final
|
||||
var datosRespuestas = {};
|
||||
if (soloPro) {
|
||||
document.querySelectorAll('[name]').forEach(function(el) {
|
||||
var raw = el.name;
|
||||
var isArr = raw.slice(-2) === '[]';
|
||||
var name = isArr ? raw.slice(0, -2) : raw;
|
||||
if (el.type === 'checkbox') {
|
||||
if (el.checked) { if (!Array.isArray(datosRespuestas[name])) datosRespuestas[name] = []; datosRespuestas[name].push(el.value); }
|
||||
} else if (el.type === 'radio') {
|
||||
if (el.checked) datosRespuestas[name] = el.value;
|
||||
} else if (el.value !== '') {
|
||||
datosRespuestas[name] = el.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const savePayload = isTurnero
|
||||
? { turno_id: parseInt(turnoId), formulario_id: parseInt(formularioId), svg: svg,
|
||||
...(soloPro ? { solo_profesional: true, datos_respuestas: datosRespuestas } : {}) }
|
||||
: { envio_id: envioId, campo_id: campoId, svg: svg };
|
||||
|
||||
fetch(saveUrl, {
|
||||
@@ -918,18 +945,27 @@ function esc2(mixed $v): string {
|
||||
.then(function(r){ return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.ok) {
|
||||
// Reemplazar el canvas con la imagen firmada
|
||||
const img = document.createElement('img');
|
||||
img.src = svg;
|
||||
img.alt = 'Firma profesional';
|
||||
img.style.maxHeight = '140px';
|
||||
img.style.maxWidth = '340px';
|
||||
img.style.display = 'block';
|
||||
const box = document.createElement('div');
|
||||
box.className = 'firma-box';
|
||||
box.style.borderColor = '#198754';
|
||||
box.appendChild(img);
|
||||
widget.replaceWith(box);
|
||||
if (soloPro) {
|
||||
// Modo solo-profesional: mostrar éxito y notificar al padre
|
||||
document.querySelectorAll('.firma-pro-widget, .campo-edit, .section-title').forEach(function(el) {
|
||||
el.style.display = 'none';
|
||||
});
|
||||
var ok = document.createElement('div');
|
||||
ok.className = 'alert alert-success mt-4 d-flex align-items-center gap-2 no-print';
|
||||
ok.innerHTML = '<i class="fas fa-check-circle fs-4"></i><div><strong>Firmado correctamente.</strong><br>Puede cerrar esta ventana.</div>';
|
||||
widget.parentNode.insertBefore(ok, widget.nextSibling);
|
||||
widget.style.display = 'none';
|
||||
try { window.parent.postMessage({ type: 'turneroFirmado' }, '*'); } catch(e) {}
|
||||
} else {
|
||||
// Reemplazar canvas con imagen firmada
|
||||
var img = document.createElement('img');
|
||||
img.src = svg; img.alt = 'Firma profesional';
|
||||
img.style.maxHeight = '140px'; img.style.maxWidth = '340px'; img.style.display = 'block';
|
||||
var box = document.createElement('div');
|
||||
box.className = 'firma-box'; box.style.borderColor = '#198754';
|
||||
box.appendChild(img);
|
||||
widget.replaceWith(box);
|
||||
}
|
||||
} else {
|
||||
msg.innerHTML = '<span class="text-danger"><i class="fas fa-times me-1"></i>' + (data.error||'Error al guardar') + '</span>';
|
||||
btnSave.disabled = false;
|
||||
|
||||
Reference in New Issue
Block a user