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) AS total_envios, (SELECT COUNT(*) FROM lab_form_envios e WHERE e.formulario_id = f.id AND e.estado IN ('completado','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, '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','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 = date('Y-m-d H:i:s', strtotime('+7 days')); $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, 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 = []): 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']; } $w = implode(' AND ', $where); return $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 200 ", $params); } // ───────────────────────────────────────────────────────────────────────── // 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'); } } }