Files
Lizandro GuarnizoandClaude Sonnet 4.6 ecc9b74247 fix: mostrar consentimientos del turnero en lab_formularios.php
- fecha usa COALESCE(firmado_at, enviado_at, t.creado_at) porque
  turnero siempre deja enviado_at en NULL
- filtro por fecha también usa ese COALESCE para no excluir registros
- total_envios y total_completados suman turnero_consentimientos

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-21 07:44:16 -05:00

277 lines
14 KiB
PHP

<?php
/**
* Formulario — Gestión de plantillas y envíos de formularios clínicos.
*/
require_once __DIR__ . '/../../classes/Database.php';
require_once __DIR__ . '/ActividadAdmin.php';
class Formulario {
private Database $db;
private ActividadAdmin $log;
public function __construct() {
$this->db = Database::getInstance();
$this->log = new ActividadAdmin();
}
// ─────────────────────────────────────────────────────────────────────────
// PLANTILLAS (admin)
// ─────────────────────────────────────────────────────────────────────────
public function listar(bool $soloActivos = true): array {
$w = $soloActivos ? 'WHERE f.is_active = 1' : '';
return $this->db->fetchAll("
SELECT f.*, u.full_name AS creado_por_nombre,
(SELECT COUNT(*) FROM lab_form_envios e WHERE e.formulario_id = f.id) +
(SELECT COUNT(*) FROM turnero_consentimientos tc WHERE tc.formulario_id = f.id) AS total_envios,
(SELECT COUNT(*) FROM lab_form_envios e WHERE e.formulario_id = f.id AND e.estado IN ('completado','firmado')) +
(SELECT COUNT(*) FROM turnero_consentimientos tc WHERE tc.formulario_id = f.id AND tc.estado = 'firmado') AS total_completados
FROM lab_formularios f
LEFT JOIN admin_users u ON u.id = f.creado_por
$w
ORDER BY f.created_at DESC
");
}
public function obtener(int $id): ?array {
$row = $this->db->fetch(
"SELECT f.*, u.full_name AS creado_por_nombre
FROM lab_formularios f
LEFT JOIN admin_users u ON u.id = f.creado_por
WHERE f.id = ?",
[$id]
);
if ($row) {
$row['esquema_decoded'] = json_decode($row['esquema'], true) ?? [];
}
return $row ?: null;
}
public function crear(array $datos, int $adminId): int {
$this->validarEsquema($datos['esquema'] ?? '[]');
$id = $this->db->insert('lab_formularios', [
'nombre' => trim($datos['nombre']),
'descripcion' => $datos['descripcion'] ?? null,
'categoria' => $datos['categoria'] ?? 'otro',
'esquema' => $datos['esquema'] ?? '[]',
'permite_firma' => !empty($datos['permite_firma']) ? 1 : 0,
'requiere_firma' => !empty($datos['requiere_firma']) ? 1 : 0,
'firma_modos' => $datos['firma_modos'] ?? 'canvas,foto',
'doc_encabezado' => $datos['doc_encabezado'] ?? null,
'doc_subtitulo' => $datos['doc_subtitulo'] ?? null,
'doc_logo_base64' => $datos['doc_logo_base64'] ?? null,
'doc_color' => $datos['doc_color'] ?? null,
'doc_pie_pagina' => $datos['doc_pie_pagina'] ?? null,
'creado_por' => $adminId,
]);
$this->log->registrar($adminId, 'formularios', 'crear', $id, ['nombre' => $datos['nombre']]);
return $id;
}
public function actualizar(int $id, array $datos, int $adminId): bool {
$permitidos = [
'nombre','descripcion','categoria','esquema',
'permite_firma','requiere_firma','firma_modos','is_active',
'doc_encabezado','doc_subtitulo','doc_logo_base64','doc_color','doc_pie_pagina',
];
$campos = array_intersect_key($datos, array_flip($permitidos));
if (isset($campos['esquema'])) {
$this->validarEsquema($campos['esquema']);
$row = $this->db->fetch('SELECT version FROM lab_formularios WHERE id = ?', [$id]);
$campos['version'] = (int)($row['version'] ?? 0) + 1;
}
$ok = $this->db->update('lab_formularios', $campos, 'id = ?', [$id]);
$this->log->registrar($adminId, 'formularios', 'editar', $id, $campos);
// rowCount() = 0 cuando los datos son idénticos a los guardados (no es un error)
return true;
}
// ─────────────────────────────────────────────────────────────────────────
// ENVÍOS
// ─────────────────────────────────────────────────────────────────────────
/**
* Crear un envío con datos pre-llenados, genera token único.
*/
public function crearEnvio(array $datos, int $enviadoPor): int {
$token = bin2hex(random_bytes(32));
$expira = null; // El enlace no expira
$id = $this->db->insert('lab_form_envios', [
'formulario_id' => (int)$datos['formulario_id'],
'paciente_id' => !empty($datos['paciente_id']) ? (int)$datos['paciente_id'] : null,
'domicilio_id' => !empty($datos['domicilio_id']) ? (int)$datos['domicilio_id'] : null,
'token' => $token,
'datos_prefilled' => $datos['datos_prefilled'] ?? '{}',
'estado' => 'pendiente',
'enviado_por' => $enviadoPor,
'enviado_via' => $datos['enviado_via'] ?? 'whatsapp',
'expira_en' => $expira,
]);
$this->log->registrar($enviadoPor, 'formularios', 'enviar', $id, [
'formulario_id' => $datos['formulario_id'],
'via' => $datos['enviado_via'] ?? 'whatsapp',
]);
return $id;
}
/**
* Obtener envío por token (uso público del cliente).
*/
public function obtenerPorToken(string $token): ?array {
$envio = $this->db->fetch(
"SELECT e.*, f.nombre AS form_nombre, f.esquema, f.permite_firma, f.requiere_firma, f.descripcion AS form_descripcion,
f.doc_color, f.doc_logo_base64, f.doc_encabezado, f.doc_subtitulo, f.doc_pie_pagina, f.firma_modos,
p.nombre_completo AS paciente_nombre, p.numero_documento, p.telefono AS paciente_telefono
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
WHERE e.token = ?",
[$token]
);
if (!$envio) return null;
if ($envio['estado'] === 'expirado') return null;
if ($envio['expira_en'] && strtotime($envio['expira_en']) < time()) {
$this->db->update('lab_form_envios', ['estado' => 'expirado'], 'id = ?', [$envio['id']]);
return null;
}
$envio['esquema_decoded'] = json_decode($envio['esquema'], true) ?? [];
$envio['prefilled_decoded'] = json_decode($envio['datos_prefilled'], true) ?? [];
return $envio;
}
/**
* Guardar respuesta del cliente (firma + datos).
*/
public function guardarRespuesta(string $token, array $datosCliente, ?string $firmaSvg, string $ip, string $ua): bool {
$envio = $this->db->fetch('SELECT id, estado FROM lab_form_envios WHERE token = ?', [$token]);
if (!$envio || $envio['estado'] === 'expirado') return false;
$tieneCanvas = !empty($firmaSvg);
$tieneFoto = !empty($datosCliente['__firma_foto']);
$estado = ($tieneCanvas || $tieneFoto) ? 'firmado' : 'completado';
// Hash de verificación SHA-256 (vincula contenido + firma + timestamp)
$payloadHash = json_encode($datosCliente, JSON_UNESCAPED_UNICODE)
. ($firmaSvg ?? '')
. $envio['id']
. $envio['token']
. date('Y-m-d H:i:s');
$hash = hash('sha256', $payloadHash);
$this->db->update('lab_form_envios', [
'datos_cliente' => json_encode($datosCliente, JSON_UNESCAPED_UNICODE),
'firma_svg' => $firmaSvg,
'hash_verificacion' => $hash,
'ip_cliente' => $ip,
'user_agent' => substr($ua, 0, 512),
'estado' => $estado,
'completado_en' => date('Y-m-d H:i:s'),
], 'token = ?', [$token]);
return true;
}
/**
* Listar envíos (admin/enfermero).
*/
public function listarEnvios(array $filtros = [], int $pagina = 1, int $porPagina = 20): array {
$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(COALESCE(tc.firmado_at, tc.enviado_at, t.creado_at)) >= ?'; $paramsB[] = $filtros['fecha_desde']; }
if (!empty($filtros['fecha_hasta'])) { $whereB[] = 'DATE(COALESCE(tc.firmado_at, tc.enviado_at, t.creado_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.firmado_at, tc.enviado_at, t.creado_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 ($sql) AS _u", $params
)['n'] ?? 0;
$rows = $this->db->fetchAll(
"SELECT * FROM ($sql) AS _u ORDER BY fecha DESC LIMIT ? OFFSET ?",
array_merge($params, [$porPagina, $offset])
);
return [
'data' => $rows,
'total' => (int)$total,
'pagina' => $pagina,
'por_pagina' => $porPagina,
'paginas' => (int)ceil($total / max(1, $porPagina)),
];
}
// ─────────────────────────────────────────────────────────────────────────
// Helper
// ─────────────────────────────────────────────────────────────────────────
private function validarEsquema(string $json): void {
$decoded = json_decode($json, true);
if (!is_array($decoded)) {
throw new InvalidArgumentException('El esquema del formulario no es un JSON válido');
}
}
}