This commit is contained in:
Lizandro Guarnizo
2026-06-16 12:06:26 -05:00
parent 275170f9d5
commit 415ea3ef56
9 changed files with 348 additions and 47 deletions
+1
View File
@@ -19,6 +19,7 @@ try {
if (!empty($_GET['fecha_desde'])) $filtros['fecha_desde'] = $_GET['fecha_desde'];
if (!empty($_GET['fecha_hasta'])) $filtros['fecha_hasta'] = $_GET['fecha_hasta'];
if (!empty($_GET['paciente'])) $filtros['paciente'] = trim($_GET['paciente']);
if (!empty($_GET['origen'])) $filtros['origen'] = $_GET['origen'];
// Enfermero solo ve los suyos
if (userRole() === 'enfermero') {
$filtros['enviado_por'] = adminId();
+73 -29
View File
@@ -174,40 +174,84 @@ class Formulario {
* Listar envíos (admin/enfermero).
*/
public function listarEnvios(array $filtros = [], int $pagina = 1, int $porPagina = 20): array {
$where = ['1=1'];
$params = [];
if (!empty($filtros['formulario_id'])) { $where[] = 'e.formulario_id = ?'; $params[] = $filtros['formulario_id']; }
if (!empty($filtros['paciente_id'])) { $where[] = 'e.paciente_id = ?'; $params[] = $filtros['paciente_id']; }
if (!empty($filtros['enviado_por'])) { $where[] = 'e.enviado_por = ?'; $params[] = $filtros['enviado_por']; }
if (!empty($filtros['estado'])) { $where[] = 'e.estado = ?'; $params[] = $filtros['estado']; }
if (!empty($filtros['fecha_desde'])) { $where[] = 'DATE(e.created_at) >= ?'; $params[] = $filtros['fecha_desde']; }
if (!empty($filtros['fecha_hasta'])) { $where[] = 'DATE(e.created_at) <= ?'; $params[] = $filtros['fecha_hasta']; }
if (!empty($filtros['paciente'])) { $where[] = 'p.nombre_completo LIKE ?'; $params[] = '%' . $filtros['paciente'] . '%'; }
$w = implode(' AND ', $where);
$origen = $filtros['origen'] ?? ''; // 'turnero' | 'otros' | '' (todos)
$offset = ($pagina - 1) * $porPagina;
// ── Rama 1: envíos normales (domicilios, envío manual, etc.) ──────────
$whereA = ['1=1'];
$paramsA = [];
if (!empty($filtros['formulario_id'])) { $whereA[] = 'e.formulario_id = ?'; $paramsA[] = $filtros['formulario_id']; }
if (!empty($filtros['paciente_id'])) { $whereA[] = 'e.paciente_id = ?'; $paramsA[] = $filtros['paciente_id']; }
if (!empty($filtros['enviado_por'])) { $whereA[] = 'e.enviado_por = ?'; $paramsA[] = $filtros['enviado_por']; }
if (!empty($filtros['estado'])) { $whereA[] = 'e.estado = ?'; $paramsA[] = $filtros['estado']; }
if (!empty($filtros['fecha_desde'])) { $whereA[] = 'DATE(e.created_at) >= ?'; $paramsA[] = $filtros['fecha_desde']; }
if (!empty($filtros['fecha_hasta'])) { $whereA[] = 'DATE(e.created_at) <= ?'; $paramsA[] = $filtros['fecha_hasta']; }
if (!empty($filtros['paciente'])) { $whereA[] = 'p.nombre_completo LIKE ?'; $paramsA[] = '%' . $filtros['paciente'] . '%'; }
$wA = implode(' AND ', $whereA);
$sqlA = "SELECT e.id, e.formulario_id, e.estado, e.created_at AS fecha,
f.nombre AS form_nombre, f.categoria,
p.nombre_completo AS paciente_nombre,
u.full_name AS enviado_por_nombre,
'otros' AS origen, e.token, NULL AS turno_codigo
FROM lab_form_envios e
JOIN lab_formularios f ON f.id = e.formulario_id
LEFT JOIN lab_pacientes p ON p.id = e.paciente_id
LEFT JOIN admin_users u ON u.id = e.enviado_por
WHERE $wA";
// ── Rama 2: consentimientos del turnero ───────────────────────────────
$whereB = ['1=1'];
$paramsB = [];
if (!empty($filtros['formulario_id'])) { $whereB[] = 'tc.formulario_id = ?'; $paramsB[] = $filtros['formulario_id']; }
if (!empty($filtros['estado'])) {
// Mapear estados: firmado=firmado, pendiente=pendiente/enviado/visto
if ($filtros['estado'] === 'firmado') {
$whereB[] = "tc.estado = 'firmado'";
} elseif ($filtros['estado'] === 'pendiente') {
$whereB[] = "tc.estado IN ('pendiente','enviado','visto')";
} else {
$whereB[] = '1=0'; // estados como 'completado'/'expirado' no aplican al turnero
}
}
if (!empty($filtros['fecha_desde'])) { $whereB[] = 'DATE(tc.enviado_at) >= ?'; $paramsB[] = $filtros['fecha_desde']; }
if (!empty($filtros['fecha_hasta'])) { $whereB[] = 'DATE(tc.enviado_at) <= ?'; $paramsB[] = $filtros['fecha_hasta']; }
if (!empty($filtros['paciente'])) { $whereB[] = 'p.nombre_completo LIKE ?'; $paramsB[] = '%' . $filtros['paciente'] . '%'; }
if (!empty($filtros['enviado_por'])) { $whereB[] = '1=0'; } // turnero no tiene enviado_por
$wB = implode(' AND ', $whereB);
$sqlB = "SELECT tc.id, tc.formulario_id, tc.estado, COALESCE(tc.enviado_at, tc.firmado_at) AS fecha,
f.nombre AS form_nombre, f.categoria,
p.nombre_completo AS paciente_nombre,
'Turnero' AS enviado_por_nombre,
'turnero' AS origen, tc.token, t.codigo AS turno_codigo
FROM turnero_consentimientos tc
JOIN lab_formularios f ON f.id = tc.formulario_id
JOIN turnero_turnos t ON t.id = tc.turno_id
LEFT JOIN turnero_solicitudes ts ON ts.turno_id = tc.turno_id
LEFT JOIN lab_pacientes p ON p.id = ts.paciente_id
WHERE $wB";
// ── Combinar según filtro origen ──────────────────────────────────────
if ($origen === 'turnero') {
$sql = $sqlB;
$params = $paramsB;
} elseif ($origen === 'otros') {
$sql = $sqlA;
$params = $paramsA;
} else {
$sql = "($sqlA) UNION ALL ($sqlB)";
$params = array_merge($paramsA, $paramsB);
}
$total = $this->db->fetch(
"SELECT COUNT(*) AS n
FROM lab_form_envios e
JOIN lab_formularios f ON f.id = e.formulario_id
LEFT JOIN lab_pacientes p ON p.id = e.paciente_id
LEFT JOIN admin_users u ON u.id = e.enviado_por
WHERE $w",
$params
"SELECT COUNT(*) AS n FROM ($sql) AS _u", $params
)['n'] ?? 0;
$rows = $this->db->fetchAll("
SELECT e.*, f.nombre AS form_nombre, f.categoria, f.esquema,
p.nombre_completo AS paciente_nombre,
u.full_name AS enviado_por_nombre
FROM lab_form_envios e
JOIN lab_formularios f ON f.id = e.formulario_id
LEFT JOIN lab_pacientes p ON p.id = e.paciente_id
LEFT JOIN admin_users u ON u.id = e.enviado_por
WHERE $w
ORDER BY e.created_at DESC
LIMIT ? OFFSET ?
", array_merge($params, [$porPagina, $offset]));
$rows = $this->db->fetchAll(
"SELECT * FROM ($sql) AS _u ORDER BY fecha DESC LIMIT ? OFFSET ?",
array_merge($params, [$porPagina, $offset])
);
return [
'data' => $rows,
+30 -10
View File
@@ -121,6 +121,14 @@ require_once __DIR__ . '/shared/components/sidebar.php';
<option value="expirado">⏰ Expirado</option>
</select>
</div>
<div class="col-6 col-sm-2">
<label class="form-label form-label-sm mb-1 text-muted">Origen</label>
<select id="env-f-origen" class="form-select form-select-sm" onchange="envios.filtrar()">
<option value="">Todos</option>
<option value="turnero">Turnero</option>
<option value="otros">Otros</option>
</select>
</div>
<div class="col-6 col-sm-3">
<label class="form-label form-label-sm mb-1 text-muted">Paciente</label>
<input type="text" id="env-f-paciente" class="form-control form-control-sm"
@@ -151,14 +159,14 @@ require_once __DIR__ . '/shared/components/sidebar.php';
<tr>
<th>Formulario</th>
<th>Paciente</th>
<th>Origen</th>
<th>Estado</th>
<th>Enviado</th>
<th>Expira</th>
<th>Fecha</th>
<th>Acciones</th>
</tr>
</thead>
<tbody id="tbody-envios">
<tr><td colspan="6" class="text-center text-muted py-4">Cargando…</td></tr>
<tr><td colspan="7" class="text-center text-muted py-4">Cargando…</td></tr>
</tbody>
</table>
</div>
@@ -496,6 +504,7 @@ const envios = {
paciente: $('env-f-paciente').value.trim() || '',
formulario_id: $('env-f-formulario').value || '',
estado: $('env-f-estado').value || '',
origen: $('env-f-origen').value || '',
});
},
@@ -514,7 +523,7 @@ const envios = {
this._render();
} catch(e) {
const tb = $('tbody-envios');
if (tb) tb.innerHTML = '<tr><td colspan="6" class="text-center text-danger py-4">Error al cargar envíos</td></tr>';
if (tb) tb.innerHTML = '<tr><td colspan="7" class="text-center text-danger py-4">Error al cargar envíos</td></tr>';
} finally {
if (spinner) spinner.style.display = 'none';
if (wrap) wrap.style.display = '';
@@ -532,6 +541,7 @@ const envios = {
$('env-f-paciente').value = '';
$('env-f-formulario').value = '';
$('env-f-estado').value = '';
$('env-f-origen').value = '';
this.cargar(1);
},
@@ -595,29 +605,39 @@ const envios = {
_render() {
const tb = $('tbody-envios');
if (!this._paginaData.length) {
tb.innerHTML = '<tr><td colspan="6" class="text-center text-muted py-4">Sin envíos para los filtros seleccionados</td></tr>';
tb.innerHTML = '<tr><td colspan="7" class="text-center text-muted py-4">Sin envíos para los filtros seleccionados</td></tr>';
$('env-pag-info').textContent = '';
$('env-paginacion').innerHTML = '';
return;
}
const inicio = (this._pagina - 1) * POR_PAGINA;
tb.innerHTML = this._paginaData.map(e => {
const info = ESTADO_ENV[e.estado] || {};
const info = ESTADO_ENV[e.estado] || {};
const esTurnero = e.origen === 'turnero';
const origenBadge = esTurnero
? `<span class="badge bg-info text-dark">Turnero${e.turno_codigo ? ' #'+esc(e.turno_codigo):''}</span>`
: `<span class="badge bg-secondary">Otros</span>`;
const viewHref = esTurnero
? `ver_formulario_enviado.php?token=${esc(e.token)}`
: `ver_formulario_enviado.php?id=${e.id}`;
const fecha = (e.fecha||'').slice(0,16).replace('T',' ');
return `<tr>
<td><span class="fw-semibold">${esc(e.form_nombre)}</span>
<br><small class="text-muted">${esc(e.categoria)}</small></td>
<td>${esc(e.paciente_nombre || '—')}</td>
<td>${origenBadge}</td>
<td><span class="badge text-white ${info.cls}">${info.icon} ${e.estado}</span></td>
<td><small>${esc((e.created_at||'').slice(0,16).replace('T',' '))}</small></td>
<td><small class="text-muted">${esc((e.expira_en||'').slice(0,10))}</small></td>
<td><small>${esc(fecha)}</small></td>
<td>
<button class="btn btn-xs btn-outline-secondary" title="Copiar link"
onclick="envios.copiarTokenLink('${esc(e.token)}')">
<i class="fas fa-copy"></i>
</button>
${['completado','firmado'].includes(e.estado)
? `<button class="btn btn-xs btn-outline-primary ms-1" onclick="verRespuesta.abrir(${e.id})"><i class="fas fa-eye"></i></button>
<a class="btn btn-xs btn-outline-danger ms-1" href="ver_formulario_enviado.php?id=${e.id}" target="_blank"><i class="fas fa-file-pdf"></i></a>`
? (esTurnero
? `<a class="btn btn-xs btn-outline-danger ms-1" href="${viewHref}" target="_blank"><i class="fas fa-file-pdf"></i></a>`
: `<button class="btn btn-xs btn-outline-primary ms-1" onclick="verRespuesta.abrir(${e.id})"><i class="fas fa-eye"></i></button>
<a class="btn btn-xs btn-outline-danger ms-1" href="${viewHref}" target="_blank"><i class="fas fa-file-pdf"></i></a>`)
: ''}
</td>
</tr>`;
@@ -0,0 +1,6 @@
-- Agrega columna para firma del enfermero en consentimientos del turnero
ALTER TABLE turnero_consentimientos
ADD COLUMN IF NOT EXISTS firma_profesional_svg MEDIUMTEXT DEFAULT NULL
COMMENT 'Firma del enfermero/profesional en formato SVG o PNG base64',
ADD COLUMN IF NOT EXISTS firmado_profesional_at DATETIME DEFAULT NULL
COMMENT 'Fecha y hora en que el profesional firmó';
@@ -0,0 +1,50 @@
<?php
/**
* POST /modules/turnero/api/firmar_profesional_consentimiento.php
* Guarda la firma del enfermero/profesional en un consentimiento del turnero.
*
* Body JSON:
* turno_id int requerido
* formulario_id int requerido
* svg string requerido (data URI SVG o PNG base64)
*/
require_once __DIR__ . '/_helpers.php';
requireMethod('POST');
requireTurnero();
$body = inputJson();
$turnoId = (int)($body['turno_id'] ?? 0);
$formularioId = (int)($body['formulario_id'] ?? 0);
$svg = $body['svg'] ?? '';
if (!$turnoId) jsonError('turno_id requerido.');
if (!$formularioId) jsonError('formulario_id requerido.');
if (strlen($svg) < 100) jsonError('Firma requerida.');
if (!preg_match('/^data:image\/(svg\+xml|png|jpeg|webp);base64,/i', $svg)) {
jsonError('Formato de firma no válido.');
}
$pdo = db();
$stmt = $pdo->prepare(
"SELECT tc.id, tc.estado, t.sesion_id
FROM turnero_consentimientos tc
JOIN turnero_turnos t ON t.id = tc.turno_id
WHERE tc.turno_id = ? AND tc.formulario_id = ?"
);
$stmt->execute([$turnoId, $formularioId]);
$tc = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$tc) jsonError('Consentimiento no encontrado.', 404);
if ($tc['estado'] !== 'firmado') jsonError('El paciente debe firmar primero antes de que el profesional pueda firmar.');
$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]);
notificarSSE((int)$tc['sesion_id']);
jsonOk(['mensaje' => 'Firma del profesional guardada correctamente.']);
@@ -35,6 +35,8 @@ $stmt = $pdo->prepare(
tc.estado,
tc.enviado_at,
tc.firmado_at,
(tc.firma_profesional_svg IS NOT NULL) AS tiene_firma_profesional,
tc.firmado_profesional_at,
f.nombre AS formulario_nombre
FROM turnero_consentimientos tc
LEFT JOIN lab_formularios f ON f.id = tc.formulario_id
+128 -7
View File
@@ -168,6 +168,9 @@ $adminNombre = $_SESSION['admin_user']['full_name'] ?? $_SESSION['admin_user']['
border-radius: 99px; border: 1px solid currentColor; opacity: .85; }
.consent-acciones { display: flex; gap: .3rem; flex-shrink: 0; }
.consent-acciones .btn { font-size: .72rem; padding: 2px 8px; border-radius: 7px; }
.consent-prof-row { display:flex; align-items:center; gap:.4rem; font-size:.75rem;
margin-top:.25rem; padding-top:.25rem; border-top:1px solid rgba(0,0,0,.07); }
.consent-prof-row .badge { font-size:.65rem; }
/* ── Barra de acciones ── */
.ficha-acciones {
@@ -425,6 +428,35 @@ require_once __DIR__ . '/../../../shared/components/sidebar.php';
</div><!-- /rec-layout -->
</main>
<!-- ═══ Modal firma enfermero ═══════════════════════════════════════ -->
<div class="modal fade" id="modalFirmaEnfermero" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header py-2">
<h6 class="modal-title mb-0"><i class="fas fa-pen-nib me-1"></i>Firma del enfermero</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body text-center">
<p class="text-muted small mb-2" id="fe-nombre-consentimiento"></p>
<canvas id="fe-canvas" width="440" height="180"
style="border:1px solid #cbd5e1;border-radius:8px;touch-action:none;cursor:crosshair;max-width:100%"></canvas>
<div class="d-flex justify-content-between mt-2">
<button class="btn btn-sm btn-outline-secondary" onclick="feCanvas.limpiar()">
<i class="fas fa-eraser me-1"></i>Limpiar
</button>
<span class="text-muted small align-self-center">Firme en el recuadro</span>
</div>
</div>
<div class="modal-footer py-2">
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">Cancelar</button>
<button type="button" class="btn btn-primary btn-sm" onclick="feGuardarFirma()">
<i class="fas fa-check me-1"></i>Guardar firma
</button>
</div>
</div>
</div>
</div>
<div class="rec-toast" id="rec-toast">
<span class="ico" id="toast-ico"><i class="fas fa-check-circle"></i></span>
<span id="toast-msg"></span>
@@ -875,10 +907,11 @@ function renderConsentimientos(lista) {
const token = escHtml(c.token || '');
const nom = escHtml(c.formulario_nombre || 'Consentimiento');
const fId = c.formulario_id;
const nomJs = (c.formulario_nombre || 'Consentimiento').replace(/'/g, "\\'");
// Botón de firma (solo si no está firmado/rechazado)
// Botón de firma paciente (solo si no está firmado/rechazado)
const btnFirmar = (!ya)
? `<button class="btn btn-outline-primary" onclick="firmarConsentimiento(${fId}, '${nom.replace(/'/g, "\\'")}')"
? `<button class="btn btn-outline-primary" onclick="firmarConsentimiento(${fId}, '${nomJs}')"
title="Abrir firma en nueva pestaña">
<i class="fas fa-external-link-alt"></i> Firmar
</button>` : '';
@@ -893,15 +926,103 @@ function renderConsentimientos(lista) {
<i class="fab fa-whatsapp"></i> WhatsApp
</button>`;
return `<div class="consent-item ${m.cls}">
<i class="fas ${m.ico}"></i>
<span class="c-nom">${nom}</span>
<span class="c-badge">${m.lbl}</span>
<div class="consent-acciones">${btnFirmar}${btnWa}</div>
// Fila de firma del profesional (solo visible si el paciente ya firmó)
let profRow = '';
if (c.estado === 'firmado') {
const tienePro = c.tiene_firma_profesional == 1 || c.tiene_firma_profesional === true;
if (tienePro) {
profRow = `<div class="consent-prof-row">
<i class="fas fa-user-nurse text-success"></i>
<span class="text-success">Firma enfermero</span>
<span class="badge bg-success ms-1">OK</span>
</div>`;
} else {
profRow = `<div class="consent-prof-row">
<i class="fas fa-user-nurse text-warning"></i>
<span class="text-warning fw-semibold">Falta firma enfermero</span>
<button class="btn btn-warning btn-sm ms-auto" style="font-size:.7rem;padding:1px 7px"
onclick="abrirFirmaEnfermero(${fId}, '${nomJs}')">
<i class="fas fa-pen-nib me-1"></i>Firmar
</button>
</div>`;
}
}
return `<div class="consent-item ${m.cls}" style="flex-wrap:wrap">
<div style="display:flex;align-items:center;gap:.5rem;width:100%">
<i class="fas ${m.ico}"></i>
<span class="c-nom">${nom}</span>
<span class="c-badge">${m.lbl}</span>
<div class="consent-acciones">${btnFirmar}${btnWa}</div>
</div>
${profRow}
</div>`;
}).join('');
}
// ── Firma del enfermero: canvas modal ─────────────────────────
let _feFormularioId = null;
const feCanvas = {
_el: null, _ctx: null, _drawing: false,
init() {
this._el = document.getElementById('fe-canvas');
this._ctx = this._el.getContext('2d');
this._ctx.strokeStyle = '#1e293b';
this._ctx.lineWidth = 2;
this._ctx.lineCap = 'round';
this._el.addEventListener('pointerdown', e => { this._drawing = true; this._ctx.beginPath(); this._move(e); });
this._el.addEventListener('pointermove', e => { if (!this._drawing) return; this._ctx.lineTo(...this._pos(e)); this._ctx.stroke(); });
['pointerup','pointerleave'].forEach(ev => this._el.addEventListener(ev, () => { this._drawing = false; }));
},
_pos(e) { const r = this._el.getBoundingClientRect(); return [e.clientX - r.left, e.clientY - r.top]; },
_move(e) { const [x,y] = this._pos(e); this._ctx.moveTo(x, y); },
limpiar() { this._ctx.clearRect(0, 0, this._el.width, this._el.height); },
vacio() {
const d = this._ctx.getImageData(0, 0, this._el.width, this._el.height).data;
return !d.some(v => v !== 0);
},
png() { return this._el.toDataURL('image/png'); },
};
function abrirFirmaEnfermero(formularioId, nombre) {
if (!turnoActivo) { mostrarError('No hay turno activo.'); return; }
_feFormularioId = formularioId;
document.getElementById('fe-nombre-consentimiento').textContent = nombre;
if (!feCanvas._el) feCanvas.init();
feCanvas.limpiar();
const modal = bootstrap.Modal.getOrCreate(document.getElementById('modalFirmaEnfermero'));
modal.show();
}
async function feGuardarFirma() {
if (!turnoActivo || !_feFormularioId) return;
if (feCanvas.vacio()) { mostrarError('Dibuja la firma antes de guardar.'); return; }
const png = feCanvas.png();
const btn = document.querySelector('#modalFirmaEnfermero .btn-primary');
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Guardando…';
try {
const res = await fetch(API + 'firmar_profesional_consentimiento.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ turno_id: turnoActivo.id, formulario_id: _feFormularioId, svg: png }),
});
const json = await res.json();
if (json.ok) {
bootstrap.Modal.getInstance(document.getElementById('modalFirmaEnfermero')).hide();
mostrarToast('Firma del enfermero guardada', 'success');
await refrescarConsentimientosLugar();
} else {
mostrarError(json.error || 'No se pudo guardar la firma.');
}
} catch(e) {
mostrarError('Error al guardar: ' + e.message);
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-check me-1"></i>Guardar firma';
}
}
// ── Firmar en nueva pestaña ──────────────────────────────────
async function firmarConsentimiento(formularioId, nombre) {
if (!turnoActivo) { mostrarError('No hay turno activo.'); return; }
@@ -0,0 +1,55 @@
<?php
/**
* Runner — Migración 20260616: firma_profesional en turnero_consentimientos
* Ejecutar una sola vez desde el navegador o CLI.
*/
require_once __DIR__ . '/config/config.php';
$sqlFile = __DIR__ . '/migrations/20260616_turnero_firma_profesional.sql';
if (!file_exists($sqlFile)) {
die("❌ Archivo no encontrado: $sqlFile\n");
}
try {
$pdo = Database::getInstance()->getConnection();
$sql = file_get_contents($sqlFile);
// Separar sentencias
$statements = array_filter(array_map('trim', explode(';', $sql)));
echo "<pre>🚀 Ejecutando migración: 20260616_turnero_firma_profesional\n\n";
foreach ($statements as $stmt) {
$clean = preg_replace('/--[^\n]*/', '', $stmt);
$clean = trim($clean);
if (empty($clean)) continue;
try {
$pdo->exec($clean);
$preview = substr(preg_replace('/\s+/', ' ', $clean), 0, 120);
echo "{$preview}\n";
} catch (PDOException $e) {
$msg = $e->getMessage();
// Ignorar si la columna ya existe
if (str_contains($msg, 'Duplicate column') || str_contains($msg, 'errno: 1060')) {
echo "️ (columna ya existe, omitida)\n";
} else {
throw $e;
}
}
}
echo "\n✅ Migración completada.\n";
echo " Columnas agregadas a <strong>turnero_consentimientos</strong>:\n";
echo " • firma_profesional_svg\n";
echo " • firmado_profesional_at\n</pre>";
// Verificación
$cols = $pdo->query("SHOW COLUMNS FROM turnero_consentimientos")->fetchAll(PDO::FETCH_COLUMN);
echo "<pre>📋 Columnas actuales: " . implode(', ', $cols) . "</pre>";
} catch (Throwable $e) {
echo "<pre>❌ Error: " . htmlspecialchars($e->getMessage()) . "\n</pre>";
}
+3 -1
View File
@@ -30,6 +30,7 @@ if ($modoTurnero) {
$tcRow = $db->fetch(
"SELECT tc.id, tc.turno_id, tc.formulario_id, tc.token, tc.estado,
tc.enviado_at, tc.firmado_at, tc.ip_firma, tc.ua_firma, tc.firma_svg,
tc.firma_profesional_svg, tc.firmado_profesional_at,
f.nombre AS form_nombre, f.categoria, f.descripcion AS form_descripcion,
f.esquema, f.doc_encabezado, f.doc_subtitulo, f.doc_logo_base64, f.doc_color, f.doc_pie_pagina,
p.nombre_completo AS paciente_nombre,
@@ -97,6 +98,7 @@ if ($modoTurnero) {
'eps' => $tcRow['eps'],
'enviado_por_nombre'=> 'Sistema Turnero',
'enviado_por_email' => null,
'firma_profesional_svg' => $tcRow['firma_profesional_svg'] ?? null,
'datos_cliente' => '{}',
'datos_prefilled' => json_encode(['__paciente' => [
'nombre_completo' => $tcRow['paciente_nombre'] ?? '',
@@ -425,7 +427,7 @@ function esc2(mixed $v): string {
// Si el paciente firmó una vez (cualquier campo o via turnero widget),
// esa firma se usa en TODOS los campos firma del mismo tipo.
$firmaSharedPaciente = $envio['firma_svg'] ?? null; // turnero widget
$firmaSharedProfesional = null;
$firmaSharedProfesional = $envio['firma_profesional_svg'] ?? null; // turnero profesional
foreach ($esquema as $_c) {
$_t = $_c['tipo'] ?? '';
$_id = $_c['id'] ?? null;