up
This commit is contained in:
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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>";
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user