Compare commits
5
Commits
a0189a61eb
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9749d726fa | ||
|
|
339ca8a40d | ||
|
|
3dbf40e4fa | ||
|
|
1e2cfbaa0b | ||
|
|
b399c3710e |
@@ -64,10 +64,60 @@ try {
|
|||||||
|
|
||||||
$estadoActual = $turno['estado'];
|
$estadoActual = $turno['estado'];
|
||||||
|
|
||||||
// Estados finales no modificables
|
// Estados finales — solo admin (sin role_id) puede reabrir
|
||||||
if (in_array($estadoActual, ['finalizado', 'ausente', 'cancelado'], true)) {
|
if (in_array($estadoActual, ['finalizado', 'ausente', 'cancelado'], true)) {
|
||||||
$pdo->rollBack();
|
$roleId = $_SESSION['admin_user']['role_id'] ?? null;
|
||||||
jsonError("El turno ya está en estado '{$estadoActual}' y no puede modificarse.", 422);
|
if (!empty($roleId)) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
jsonError("El turno ya está en estado '{$estadoActual}' y no puede modificarse.", 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$permitidosAdmin = ['en_servicio', 'en_espera_lugar', 'en_recepcion', 'espera'];
|
||||||
|
if (!in_array($nuevoEstado, $permitidosAdmin, true)) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
jsonError("Estado destino inválido para reapertura. Opciones: " . implode(', ', $permitidosAdmin), 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sets = ['estado = ?'];
|
||||||
|
$binds = [$nuevoEstado];
|
||||||
|
|
||||||
|
if ($nuevoEstado === 'en_servicio') {
|
||||||
|
$sets[] = 'fin_lugar_at = NULL';
|
||||||
|
$sets[] = 'inicio_lugar_at = COALESCE(inicio_lugar_at, NOW())';
|
||||||
|
$sets[] = 'atendido_lugar_por = COALESCE(atendido_lugar_por, ?)';
|
||||||
|
$binds[] = adminId();
|
||||||
|
} elseif ($nuevoEstado === 'en_espera_lugar') {
|
||||||
|
$sets[] = 'fin_lugar_at = NULL';
|
||||||
|
$sets[] = 'inicio_lugar_at = NULL';
|
||||||
|
$sets[] = 'llamado_lugar_at = NULL';
|
||||||
|
} elseif ($nuevoEstado === 'espera') {
|
||||||
|
$sets[] = 'llamado_recepcion_at = NULL';
|
||||||
|
$sets[] = 'inicio_recepcion_at = NULL';
|
||||||
|
$sets[] = 'fin_recepcion_at = NULL';
|
||||||
|
$sets[] = 'llamado_lugar_at = NULL';
|
||||||
|
$sets[] = 'inicio_lugar_at = NULL';
|
||||||
|
$sets[] = 'fin_lugar_at = NULL';
|
||||||
|
$sets[] = 'atendido_recepcion_por = NULL';
|
||||||
|
$sets[] = 'atendido_lugar_por = NULL';
|
||||||
|
$sets[] = 'recepcion_desk_id = NULL';
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = 'UPDATE turnero_turnos SET ' . implode(', ', $sets) . ' WHERE id = ?';
|
||||||
|
$binds[] = $turnoId;
|
||||||
|
$pdo->prepare($sql)->execute($binds);
|
||||||
|
$pdo->commit();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
'SELECT t.*, p.codigo AS prioridad_codigo, p.nombre AS prioridad_nombre, p.color AS prioridad_color
|
||||||
|
FROM turnero_turnos t
|
||||||
|
JOIN turnero_prioridades p ON p.id = t.prioridad_id
|
||||||
|
WHERE t.id = ?'
|
||||||
|
);
|
||||||
|
$stmt->execute([$turnoId]);
|
||||||
|
$turnoActualizado = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
notificarSSE((int) $turnoActualizado['sesion_id']);
|
||||||
|
jsonOk(['turno' => $turnoActualizado], "Turno reabierto: estado actualizado a '{$nuevoEstado}'");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verificar que la transición sea válida
|
// Verificar que la transición sea válida
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ $body = inputJson();
|
|||||||
$turnoId = (int)($body['turno_id'] ?? 0);
|
$turnoId = (int)($body['turno_id'] ?? 0);
|
||||||
$formularioId = (int)($body['formulario_id'] ?? 0);
|
$formularioId = (int)($body['formulario_id'] ?? 0);
|
||||||
$svg = $body['svg'] ?? '';
|
$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 (!$turnoId) jsonError('turno_id requerido.');
|
||||||
if (!$formularioId) jsonError('formulario_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);
|
if (!$tc) jsonError('Consentimiento no encontrado.', 404);
|
||||||
|
|
||||||
$stmt = $pdo->prepare(
|
if ($soloPro) {
|
||||||
"UPDATE turnero_consentimientos
|
// El profesional es el firmante final: guardar datos + marcar como firmado
|
||||||
SET firma_profesional_svg = ?, firmado_profesional_at = NOW()
|
$pdo->prepare(
|
||||||
WHERE turno_id = ? AND formulario_id = ?"
|
"UPDATE turnero_consentimientos
|
||||||
);
|
SET firma_profesional_svg = ?, firmado_profesional_at = NOW(),
|
||||||
$stmt->execute([$svg, $turnoId, $formularioId]);
|
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']);
|
notificarSSE((int)$tc['sesion_id']);
|
||||||
|
|
||||||
|
|||||||
@@ -111,6 +111,21 @@ Layout::open('Historial de Turnos', 'fas fa-history');
|
|||||||
.doc-pill.enviado { background:#fef9c3; color:#78350f; border:1px solid #fde68a; }
|
.doc-pill.enviado { background:#fef9c3; color:#78350f; border:1px solid #fde68a; }
|
||||||
.doc-pill.pendiente{ background:#f1f5f9; color:#64748b; border:1px solid #e2e8f0; }
|
.doc-pill.pendiente{ background:#f1f5f9; color:#64748b; border:1px solid #e2e8f0; }
|
||||||
.docs-empty { font-size:.78rem; color:#94a3b8; font-style:italic; }
|
.docs-empty { font-size:.78rem; color:#94a3b8; font-style:italic; }
|
||||||
|
|
||||||
|
/* ── Modal cambio de estado ── */
|
||||||
|
.estado-modal-backdrop {
|
||||||
|
position:fixed; inset:0; background:rgba(0,0,0,.45); z-index:1050;
|
||||||
|
display:flex; align-items:center; justify-content:center;
|
||||||
|
}
|
||||||
|
.estado-modal {
|
||||||
|
background:#fff; border-radius:14px; padding:24px 28px; width:340px;
|
||||||
|
box-shadow:0 20px 60px rgba(0,0,0,.2);
|
||||||
|
}
|
||||||
|
.estado-modal h5 { font-size:.95rem; font-weight:700; margin-bottom:4px; }
|
||||||
|
.estado-modal .sub { font-size:.78rem; color:#64748b; margin-bottom:16px; }
|
||||||
|
.estado-modal select { width:100%; padding:8px 10px; border:1px solid #e2e8f0;
|
||||||
|
border-radius:8px; font-size:.85rem; margin-bottom:16px; }
|
||||||
|
.estado-modal .actions { display:flex; gap:8px; justify-content:flex-end; }
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<!-- ── Encabezado ────────────────────────────────────────────── -->
|
<!-- ── Encabezado ────────────────────────────────────────────── -->
|
||||||
@@ -445,12 +460,17 @@ function renderTabla(turnos) {
|
|||||||
<td><span class="eb ${cls}">${lbl}</span></td>
|
<td><span class="eb ${cls}">${lbl}</span></td>
|
||||||
<td class="text-nowrap">${espMin}</td>
|
<td class="text-nowrap">${espMin}</td>
|
||||||
<td class="text-nowrap">${srvMin}</td>
|
<td class="text-nowrap">${srvMin}</td>
|
||||||
<td>
|
<td class="text-nowrap">
|
||||||
<button class="btn btn-sm btn-outline-secondary py-0 px-2"
|
<button class="btn btn-sm btn-outline-secondary py-0 px-2 me-1"
|
||||||
onclick="toggleDetalle('${rowId}', this, ${t.id})"
|
onclick="toggleDetalle('${rowId}', this, ${t.id})"
|
||||||
title="Ver detalles">
|
title="Ver detalles">
|
||||||
<i class="fas fa-chevron-down" style="font-size:.65rem"></i>
|
<i class="fas fa-chevron-down" style="font-size:.65rem"></i>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-warning py-0 px-2"
|
||||||
|
onclick="abrirModalEstado(${t.id}, '${t.estado}', '${nombre.replace(/'/g,\'\\\'')}')"
|
||||||
|
title="Cambiar estado">
|
||||||
|
<i class="fas fa-exchange-alt" style="font-size:.65rem"></i>
|
||||||
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr id="${rowId}" class="detail-row" style="display:none">
|
<tr id="${rowId}" class="detail-row" style="display:none">
|
||||||
@@ -641,6 +661,82 @@ function exportarCSV() {
|
|||||||
window.open(`${API}export_historial.php?${params}`, '_blank');
|
window.open(`${API}export_historial.php?${params}`, '_blank');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Cambio de estado ─────────────────────────────────────────
|
||||||
|
const ESTADOS_OPCIONES = {
|
||||||
|
espera : [{v:'en_recepcion', l:'En recepción'}, {v:'ausente', l:'Ausente'}, {v:'cancelado', l:'Cancelado'}],
|
||||||
|
en_recepcion : [{v:'en_espera_lugar', l:'Esp. lugar'}, {v:'espera', l:'Espera'}, {v:'ausente', l:'Ausente'}, {v:'cancelado', l:'Cancelado'}],
|
||||||
|
en_espera_lugar : [{v:'en_servicio', l:'En servicio'}, {v:'ausente', l:'Ausente'}, {v:'cancelado', l:'Cancelado'}],
|
||||||
|
en_servicio : [{v:'finalizado', l:'Finalizado'}, {v:'en_espera_lugar', l:'Esp. lugar'}, {v:'ausente', l:'Ausente'}, {v:'cancelado', l:'Cancelado'}],
|
||||||
|
// estados finales: reapertura (admin)
|
||||||
|
finalizado : [{v:'en_servicio', l:'Reabrir → En servicio'}, {v:'en_espera_lugar', l:'Reabrir → Esp. lugar'}, {v:'espera', l:'Reabrir → Espera'}],
|
||||||
|
ausente : [{v:'espera', l:'Reabrir → Espera'}, {v:'en_servicio', l:'Reabrir → En servicio'}],
|
||||||
|
cancelado : [{v:'espera', l:'Reabrir → Espera'}, {v:'en_servicio', l:'Reabrir → En servicio'}],
|
||||||
|
};
|
||||||
|
|
||||||
|
let _modalTurnoId = null;
|
||||||
|
|
||||||
|
function abrirModalEstado(turnoId, estadoActual, nombre) {
|
||||||
|
_modalTurnoId = turnoId;
|
||||||
|
const opciones = ESTADOS_OPCIONES[estadoActual] || [];
|
||||||
|
if (!opciones.length) { alert('No hay transiciones disponibles para este estado.'); return; }
|
||||||
|
|
||||||
|
const sel = opciones.map(o => `<option value="${o.v}">${o.l}</option>`).join('');
|
||||||
|
const esReopetura = ['finalizado','ausente','cancelado'].includes(estadoActual);
|
||||||
|
|
||||||
|
const backdrop = document.createElement('div');
|
||||||
|
backdrop.className = 'estado-modal-backdrop';
|
||||||
|
backdrop.id = 'estadoModalBackdrop';
|
||||||
|
backdrop.innerHTML = `
|
||||||
|
<div class="estado-modal" onclick="event.stopPropagation()">
|
||||||
|
<h5><i class="fas fa-exchange-alt me-2 text-warning"></i>Cambiar estado del turno</h5>
|
||||||
|
<div class="sub">${nombre}${esReopetura ? ' <span class="badge bg-warning text-dark ms-1">Reapertura admin</span>' : ''}</div>
|
||||||
|
<select id="selectNuevoEstado">${sel}</select>
|
||||||
|
<div class="actions">
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" onclick="cerrarModalEstado()">Cancelar</button>
|
||||||
|
<button class="btn btn-sm btn-warning" onclick="confirmarCambioEstado()">
|
||||||
|
<i class="fas fa-check me-1"></i>Confirmar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
backdrop.addEventListener('click', cerrarModalEstado);
|
||||||
|
document.body.appendChild(backdrop);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cerrarModalEstado() {
|
||||||
|
document.getElementById('estadoModalBackdrop')?.remove();
|
||||||
|
_modalTurnoId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmarCambioEstado() {
|
||||||
|
const nuevoEstado = document.getElementById('selectNuevoEstado').value;
|
||||||
|
if (!_modalTurnoId || !nuevoEstado) return;
|
||||||
|
|
||||||
|
const btn = document.querySelector('#estadoModalBackdrop .btn-warning');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Guardando…';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API}cambiar_estado.php`, {
|
||||||
|
method : 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body : JSON.stringify({ turno_id: _modalTurnoId, nuevo_estado: nuevoEstado }),
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
if (json.ok) {
|
||||||
|
cerrarModalEstado();
|
||||||
|
buscar(_state.page);
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + (json.error || 'No se pudo cambiar el estado.'));
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = '<i class="fas fa-check me-1"></i>Confirmar';
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
alert('Error de conexión');
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = '<i class="fas fa-check me-1"></i>Confirmar';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Helpers visuales ──────────────────────────────────────────
|
// ── Helpers visuales ──────────────────────────────────────────
|
||||||
function mostrarSpinner() {
|
function mostrarSpinner() {
|
||||||
document.getElementById('tbody').innerHTML =
|
document.getElementById('tbody').innerHTML =
|
||||||
|
|||||||
@@ -321,14 +321,7 @@ require_once __DIR__ . '/../../../shared/components/sidebar.php';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── Sección: Consentimientos ── -->
|
|
||||||
<!-- ── Botón firma embebida (modo=embebido) ── -->
|
|
||||||
<div class="ficha-sec d-none" id="sec-form-embebido">
|
|
||||||
<h6><i class="fas fa-file-signature me-1"></i>Consentimiento informado</h6>
|
|
||||||
<button class="btn btn-primary w-100" onclick="abrirModalConsentimiento()">
|
|
||||||
<i class="fas fa-pen me-2"></i>Abrir formulario de firma
|
|
||||||
</button>
|
|
||||||
</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>
|
||||||
@@ -753,29 +746,40 @@ function renderConsentimientos(lista) {
|
|||||||
const idJs = parseInt(c.id) || 0;
|
const idJs = parseInt(c.id) || 0;
|
||||||
const tId = parseInt(c.turno_id) || (turnoActivo?.id) || 0;
|
const tId = parseInt(c.turno_id) || (turnoActivo?.id) || 0;
|
||||||
|
|
||||||
// Firmar aquí: solo si no completado y hay token
|
// Botón ver (firmado)
|
||||||
const btnFirmar = (!ya && c.token)
|
const btnVer = (ya && c.token)
|
||||||
? `<button class="btn btn-outline-primary" title="Firmar aquí"
|
? `<a href="${BASE_WA}ver_formulario_enviado.php?token=${token}" target="_blank"
|
||||||
onclick="abrirFirmaPresencial('${token}', ${idJs}, ${nomJs.replace(/"/g,'"')})">
|
class="btn btn-outline-secondary" title="Ver firmado">
|
||||||
<i class="fas fa-signature"></i> Firmar
|
<i class="fas fa-eye"></i> Ver
|
||||||
</button>` : '';
|
</a>` : '';
|
||||||
|
|
||||||
// WA / Ver: según estado
|
// Acción principal según modo y estado
|
||||||
const btnWa = ya
|
let btnAccion = '';
|
||||||
? (c.token ? `<a href="${BASE_WA}ver_formulario_enviado.php?token=${token}" target="_blank"
|
if (!ya && c.token) {
|
||||||
class="btn btn-outline-secondary" title="Ver firmado">
|
if (LUGAR_FORM_MODO === 'embebido') {
|
||||||
<i class="fas fa-eye"></i> Ver
|
// Modo embebido: abrir modal directamente
|
||||||
</a>` : '')
|
btnAccion = `<button class="btn btn-primary" title="Abrir formulario"
|
||||||
: `<button class="btn btn-outline-success" title="Reenviar por WhatsApp"
|
onclick="abrirModalConsentimientoPorToken('${token}')">
|
||||||
onclick="reenviarConsentimiento(${tId})">
|
<i class="fas fa-pen me-1"></i>Firmar
|
||||||
<i class="fab fa-whatsapp"></i> WA
|
</button>`;
|
||||||
</button>`;
|
} else {
|
||||||
|
// Modo link: firmar presencial o reenviar WA
|
||||||
|
btnAccion = `<button class="btn btn-outline-primary" title="Firmar aquí"
|
||||||
|
onclick="abrirFirmaPresencial('${token}', ${idJs}, ${nomJs.replace(/"/g,'"')})">
|
||||||
|
<i class="fas fa-signature"></i> Firmar
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-success" title="Reenviar por WhatsApp"
|
||||||
|
onclick="reenviarConsentimiento(${tId})">
|
||||||
|
<i class="fab fa-whatsapp"></i> WA
|
||||||
|
</button>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return `<div class="consent-row ${c.estado}" data-consent-id="${c.id}">
|
return `<div class="consent-row ${c.estado}" data-consent-id="${c.id}">
|
||||||
<i class="fas ${ico}"></i>
|
<i class="fas ${ico}"></i>
|
||||||
<span class="nom-form">${escHtml(c.formulario_nombre || 'Consentimiento')}</span>
|
<span class="nom-form">${escHtml(c.formulario_nombre || 'Consentimiento')}</span>
|
||||||
<span class="c-badge">${label}</span>
|
<span class="c-badge">${label}</span>
|
||||||
<div class="acciones-consent">${btnFirmar}${btnWa}</div>
|
<div class="acciones-consent">${btnAccion}${btnVer}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
@@ -889,27 +893,10 @@ async function cambiarEstadoTurno(nuevoEstado) {
|
|||||||
// ── Embebido como modal ───────────────────────────────────────
|
// ── Embebido como modal ───────────────────────────────────────
|
||||||
let _consentTokenCache = null; // { turnoId, token }
|
let _consentTokenCache = null; // { turnoId, token }
|
||||||
|
|
||||||
async function cargarFormEmbebido(turnoId) {
|
// cargarFormEmbebido ya no necesita hacer nada (el token viene de la fila de consentimiento)
|
||||||
const sec = document.getElementById('sec-form-embebido');
|
async function cargarFormEmbebido(turnoId) { /* token llega vía renderConsentimientos */ }
|
||||||
if (sec) sec.classList.remove('d-none');
|
|
||||||
// Pre-cargar token para que el modal abra rápido
|
|
||||||
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) {
|
|
||||||
_consentTokenCache = { turnoId, token: json.token };
|
|
||||||
} else {
|
|
||||||
_consentTokenCache = null;
|
|
||||||
if (sec) sec.innerHTML = `<h6><i class="fas fa-file-signature me-1"></i>Consentimiento informado</h6>
|
|
||||||
<div class="text-danger small"><i class="fas fa-exclamation-triangle me-1"></i>${json.error || 'Sin formulario configurado'}</div>`;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
_consentTokenCache = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function abrirModalConsentimiento() {
|
function _abrirModalConToken(token) {
|
||||||
if (!_consentTokenCache) { mostrarError('Token de consentimiento no disponible.'); return; }
|
|
||||||
const modal = document.getElementById('modal-consentimiento');
|
const modal = document.getElementById('modal-consentimiento');
|
||||||
const iframe = document.getElementById('modal-consent-iframe');
|
const iframe = document.getElementById('modal-consent-iframe');
|
||||||
const load = document.getElementById('modal-consent-loading');
|
const load = document.getElementById('modal-consent-loading');
|
||||||
@@ -917,12 +904,20 @@ function abrirModalConsentimiento() {
|
|||||||
iframe.src = '';
|
iframe.src = '';
|
||||||
load.style.display = '';
|
load.style.display = '';
|
||||||
modal.style.display = 'flex';
|
modal.style.display = 'flex';
|
||||||
// Pequeño delay para que el modal sea visible antes de cargar el iframe
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
iframe.src = `${BASE_WA}ver_formulario_enviado.php?token=${encodeURIComponent(_consentTokenCache.token)}&embed=1`;
|
iframe.src = `${BASE_WA}ver_formulario_enviado.php?token=${encodeURIComponent(token)}&embed=1`;
|
||||||
}, 80);
|
}, 80);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function abrirModalConsentimiento() {
|
||||||
|
if (!_consentTokenCache?.token) { mostrarError('Token de consentimiento no disponible.'); return; }
|
||||||
|
_abrirModalConToken(_consentTokenCache.token);
|
||||||
|
}
|
||||||
|
|
||||||
|
function abrirModalConsentimientoPorToken(token) {
|
||||||
|
_abrirModalConToken(token);
|
||||||
|
}
|
||||||
|
|
||||||
function cerrarModalConsentimiento() {
|
function cerrarModalConsentimiento() {
|
||||||
const modal = document.getElementById('modal-consentimiento');
|
const modal = document.getElementById('modal-consentimiento');
|
||||||
const iframe = document.getElementById('modal-consent-iframe');
|
const iframe = document.getElementById('modal-consent-iframe');
|
||||||
@@ -952,17 +947,7 @@ function resetFicha() {
|
|||||||
tieneConsent = false;
|
tieneConsent = false;
|
||||||
hayPendientes = false;
|
hayPendientes = false;
|
||||||
_consentTokenCache = null;
|
_consentTokenCache = null;
|
||||||
|
|
||||||
// Limpiar modal embebido
|
|
||||||
cerrarModalConsentimiento();
|
cerrarModalConsentimiento();
|
||||||
const sec = document.getElementById('sec-form-embebido');
|
|
||||||
if (sec) {
|
|
||||||
sec.classList.add('d-none');
|
|
||||||
sec.innerHTML = `<h6><i class="fas fa-file-signature me-1"></i>Consentimiento informado</h6>
|
|
||||||
<button class="btn btn-primary w-100" onclick="abrirModalConsentimiento()">
|
|
||||||
<i class="fas fa-pen me-2"></i>Abrir formulario de firma
|
|
||||||
</button>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
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');
|
||||||
|
|||||||
+63
-24
@@ -1,4 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
ini_set('display_errors', 1); error_reporting(E_ALL); // ponytail: quitar después de depurar
|
||||||
/**
|
/**
|
||||||
* ver_formulario_enviado.php — Ver respuesta de un formulario (imprimible / PDF)
|
* ver_formulario_enviado.php — Ver respuesta de un formulario (imprimible / PDF)
|
||||||
* Acceso admin/enfermero: ver_formulario_enviado.php?id=ENVIO_ID (requiere sesión)
|
* Acceso admin/enfermero: ver_formulario_enviado.php?id=ENVIO_ID (requiere sesión)
|
||||||
@@ -189,6 +190,9 @@ $datosPrefilled = json_decode($envio['datos_prefilled'] ?? '{}', true) ?? [];
|
|||||||
$todos = array_merge($datosPrefilled, $datosCliente);
|
$todos = array_merge($datosPrefilled, $datosCliente);
|
||||||
$modoEditar = $modoTurnero && $envio['estado'] !== 'firmado';
|
$modoEditar = $modoTurnero && $envio['estado'] !== 'firmado';
|
||||||
$embebido = isset($_GET['embed']) && $_GET['embed'] === '1';
|
$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
|
// Mapa id → label
|
||||||
$labelMap = [];
|
$labelMap = [];
|
||||||
@@ -482,6 +486,7 @@ function esc2(mixed $v): string {
|
|||||||
$_renderedFirmaProfesional = false;
|
$_renderedFirmaProfesional = false;
|
||||||
$_renderedFirmaPaciente = false;
|
$_renderedFirmaPaciente = false;
|
||||||
$_esquemaTieneFirmaPaciente = false;
|
$_esquemaTieneFirmaPaciente = false;
|
||||||
|
$_esquemaTieneFirmaAlguna = false; // true si hay cualquier campo firma/firma_profesional
|
||||||
$_firmaGlobalPacienteUsada = false; // la firma global solo va al primer campo firma
|
$_firmaGlobalPacienteUsada = false; // la firma global solo va al primer campo firma
|
||||||
|
|
||||||
foreach ($esquema as $campo):
|
foreach ($esquema as $campo):
|
||||||
@@ -496,7 +501,8 @@ function esc2(mixed $v): string {
|
|||||||
if (!$cid) continue;
|
if (!$cid) continue;
|
||||||
$isPro = ($tipo === 'firma_profesional');
|
$isPro = ($tipo === 'firma_profesional');
|
||||||
|
|
||||||
// Marcar siempre que el esquema tiene campo firma paciente
|
// Marcar presencia de campos firma en el esquema
|
||||||
|
$_esquemaTieneFirmaAlguna = true;
|
||||||
if (!$isPro) {
|
if (!$isPro) {
|
||||||
$_esquemaTieneFirmaPaciente = true;
|
$_esquemaTieneFirmaPaciente = true;
|
||||||
}
|
}
|
||||||
@@ -555,14 +561,19 @@ function esc2(mixed $v): string {
|
|||||||
<div class="turnero-msg mt-2 small"></div>
|
<div class="turnero-msg mt-2 small"></div>
|
||||||
</div>
|
</div>
|
||||||
<?php elseif ($isPro): ?>
|
<?php elseif ($isPro): ?>
|
||||||
<?php if (!$modoTurnero && !$modoPublico && !$yaHayCanvasPro): ?>
|
<?php
|
||||||
<!-- Canvas del profesional: solo aparece una vez -->
|
// 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) ?>"
|
<div class="firma-pro-widget no-print" id="fpw-<?= htmlspecialchars($cid) ?>"
|
||||||
data-envio="<?= (int)$envio['id'] ?>" data-campo="<?= htmlspecialchars($cid) ?>"
|
data-envio="<?= (int)$envio['id'] ?>" data-campo="<?= htmlspecialchars($cid) ?>"
|
||||||
<?php if ($modoTurnero): ?>
|
data-solo-pro="<?= ($modoTurnero && $_soloFirmaPro) ? '1' : '0' ?>"
|
||||||
data-turno="<?= (int)$tcRow['turno_id'] ?>"
|
data-turno="<?= isset($tcRow) ? (int)$tcRow['turno_id'] : '' ?>"
|
||||||
data-formulario="<?= (int)$tcRow['formulario_id'] ?>"
|
data-formulario="<?= isset($tcRow) ? (int)$tcRow['formulario_id'] : '' ?>">
|
||||||
<?php endif; ?>>
|
|
||||||
<p class="text-muted small mb-2"><i class="fas fa-pen me-1"></i>Dibuje su firma en el recuadro:</p>
|
<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>
|
<canvas class="fpw-canvas" width="500" height="150"></canvas>
|
||||||
<div class="mt-2 d-flex gap-2">
|
<div class="mt-2 d-flex gap-2">
|
||||||
@@ -789,8 +800,8 @@ function esc2(mixed $v): string {
|
|||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<!-- ── Widget firma turnero (fallback si el esquema no tiene campo firma) ── -->
|
<!-- ── Widget firma turnero (fallback solo si el esquema no tiene NINGÚN campo firma) ── -->
|
||||||
<?php if ($modoTurnero && $envio['estado'] !== 'firmado' && !$_esquemaTieneFirmaPaciente): ?>
|
<?php if ($modoTurnero && $envio['estado'] !== 'firmado' && !$_esquemaTieneFirmaAlguna): ?>
|
||||||
<div class="mt-4 no-print turnero-firma-item" data-cid="__firma_global">
|
<div class="mt-4 no-print turnero-firma-item" data-cid="__firma_global">
|
||||||
<div class="section-title" style="color:#1565c0">
|
<div class="section-title" style="color:#1565c0">
|
||||||
<i class="fas fa-pen me-1"></i>Firma del paciente / responsable
|
<i class="fas fa-pen me-1"></i>Firma del paciente / responsable
|
||||||
@@ -889,7 +900,6 @@ function esc2(mixed $v): string {
|
|||||||
|
|
||||||
btnSave.addEventListener('click', function() {
|
btnSave.addEventListener('click', function() {
|
||||||
const svg = canvas.toDataURL('image/png');
|
const svg = canvas.toDataURL('image/png');
|
||||||
// Verificar que no esté vacío (al menos 1000 bytes de data)
|
|
||||||
if (svg.length < 1000) {
|
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>';
|
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;
|
return;
|
||||||
@@ -900,12 +910,32 @@ function esc2(mixed $v): string {
|
|||||||
|
|
||||||
const turnoId = widget.dataset.turno;
|
const turnoId = widget.dataset.turno;
|
||||||
const formularioId = widget.dataset.formulario;
|
const formularioId = widget.dataset.formulario;
|
||||||
|
const soloPro = widget.dataset.soloPro === '1';
|
||||||
const isTurnero = !!(turnoId && formularioId);
|
const isTurnero = !!(turnoId && formularioId);
|
||||||
const saveUrl = isTurnero
|
const saveUrl = isTurnero
|
||||||
? 'modules/turnero/api/firmar_profesional_consentimiento.php'
|
? 'modules/turnero/api/firmar_profesional_consentimiento.php'
|
||||||
: 'api/lab/firmar_profesional.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 };
|
: { envio_id: envioId, campo_id: campoId, svg: svg };
|
||||||
|
|
||||||
fetch(saveUrl, {
|
fetch(saveUrl, {
|
||||||
@@ -916,18 +946,27 @@ function esc2(mixed $v): string {
|
|||||||
.then(function(r){ return r.json(); })
|
.then(function(r){ return r.json(); })
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
if (data.ok) {
|
if (data.ok) {
|
||||||
// Reemplazar el canvas con la imagen firmada
|
if (soloPro) {
|
||||||
const img = document.createElement('img');
|
// Modo solo-profesional: mostrar éxito y notificar al padre
|
||||||
img.src = svg;
|
document.querySelectorAll('.firma-pro-widget, .campo-edit, .section-title').forEach(function(el) {
|
||||||
img.alt = 'Firma profesional';
|
el.style.display = 'none';
|
||||||
img.style.maxHeight = '140px';
|
});
|
||||||
img.style.maxWidth = '340px';
|
var ok = document.createElement('div');
|
||||||
img.style.display = 'block';
|
ok.className = 'alert alert-success mt-4 d-flex align-items-center gap-2 no-print';
|
||||||
const box = document.createElement('div');
|
ok.innerHTML = '<i class="fas fa-check-circle fs-4"></i><div><strong>Firmado correctamente.</strong><br>Puede cerrar esta ventana.</div>';
|
||||||
box.className = 'firma-box';
|
widget.parentNode.insertBefore(ok, widget.nextSibling);
|
||||||
box.style.borderColor = '#198754';
|
widget.style.display = 'none';
|
||||||
box.appendChild(img);
|
try { window.parent.postMessage({ type: 'turneroFirmado' }, '*'); } catch(e) {}
|
||||||
widget.replaceWith(box);
|
} 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 {
|
} else {
|
||||||
msg.innerHTML = '<span class="text-danger"><i class="fas fa-times me-1"></i>' + (data.error||'Error al guardar') + '</span>';
|
msg.innerHTML = '<span class="text-danger"><i class="fas fa-times me-1"></i>' + (data.error||'Error al guardar') + '</span>';
|
||||||
btnSave.disabled = false;
|
btnSave.disabled = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user