Files
whatsapp/classes/lab/Paciente.php
T
Lizandro GuarnizoandClaude Opus 5 57818b180a Estación de muestras: corregir la ficha del paciente desde el turno
Los campos del formulario F-LAB-08 son vinculados: leen de la ficha del
paciente. Si el dato está mal, no había forma de arreglarlo desde la estación
—el modal era de solo lectura— y corregirlo únicamente en el documento habría
dejado la ficha equivocada, reapareciendo en el próximo turno, orden o
consentimiento. Por eso se corrige la ficha y no el documento: el dato queda
bien en todo el sistema y el formulario, al ser automático, lo toma solo.

La edición tiene dos niveles de fricción. Teléfono, dirección y EPS se editan
directo: cambian seguido y el error es de bajo riesgo. Nombre, tipo y número
de documento y fecha de nacimiento quedan tras un botón aparte con
confirmación, porque identifican al paciente en toda su historia clínica.

Si el turno ya tiene documentos firmados se advierte que corregir la ficha no
los modifica y que hay que emitirlos de nuevo, para que nadie espere que un
PDF ya firmado se actualice solo.

Paciente::actualizar registraba en la bitácora solo el valor nuevo, lo que
impedía reconstruir el anterior si la corrección resultaba equivocada —justo
cuando hace falta consultarlo. Ahora guarda antes y después, y únicamente de
los campos que realmente cambiaron.

La validación del servidor ya cubría nombre, celular, documento y correo, y la
cédula tiene índice único, así que no hizo falta agregar validación.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 09:05:48 -05:00

302 lines
11 KiB
PHP

<?php
/**
* Paciente — Gestión de pacientes del módulo administrativo de laboratorio.
* Extiende la información del contacto WhatsApp (users.id).
*/
require_once __DIR__ . '/../../classes/Database.php';
require_once __DIR__ . '/ActividadAdmin.php';
class Paciente {
private Database $db;
private ActividadAdmin $log;
public function __construct() {
$this->db = Database::getInstance();
$this->log = new ActividadAdmin();
}
// ─────────────────────────────────────────────────────────────────────────
// CRUD
// ─────────────────────────────────────────────────────────────────────────
/**
* Lista paginada de pacientes con búsqueda.
*/
public function listar(
string $busqueda = '',
int $pagina = 1,
int $porPagina = 30,
string $origen = ''
): array {
$offset = ($pagina - 1) * $porPagina;
$like = "%$busqueda%";
$conditions = [];
$params = [];
if ($busqueda) {
$conditions[] = "(p.nombre_completo LIKE ? OR p.numero_documento LIKE ? OR p.telefono LIKE ? OR p.email LIKE ?)";
$params = [$like, $like, $like, $like];
}
if ($origen) {
$conditions[] = "p.origen = ?";
$params[] = $origen;
}
$where = $conditions ? 'WHERE ' . implode(' AND ', $conditions) : '';
$total = $this->db->fetch(
"SELECT COUNT(*) AS n FROM lab_pacientes p $where",
$params
)['n'] ?? 0;
$rows = $this->db->fetchAll("
SELECT
p.*,
u.phone_number,
u.name AS whatsapp_name,
(SELECT COUNT(*) FROM lab_ordenes_medicas o WHERE o.paciente_id = p.id) AS total_ordenes
FROM lab_pacientes p
LEFT JOIN users u ON u.id = p.user_id
$where
ORDER BY p.created_at DESC, p.nombre_completo ASC
LIMIT ? OFFSET ?
", array_merge($params, [$porPagina, $offset]));
return [
'data' => $rows,
'total' => (int)$total,
'pagina' => $pagina,
'por_pagina' => $porPagina,
'paginas' => (int)ceil($total / $porPagina),
];
}
public function stats(): array {
return $this->db->fetch("
SELECT
COUNT(*) AS total,
SUM(origen = 'lab') AS importados,
SUM(origen = 'manual') AS manuales,
SUM(origen = 'whatsapp') AS whatsapp,
SUM(user_id IS NOT NULL) AS con_wa
FROM lab_pacientes
WHERE is_active = 1
") ?: ['total' => 0, 'importados' => 0, 'manuales' => 0, 'whatsapp' => 0, 'con_wa' => 0];
}
/**
* Un paciente por ID (con datos del usuario WhatsApp).
*/
public function obtener(int $id): ?array {
return $this->db->fetch("
SELECT
p.*,
u.phone_number,
u.name AS whatsapp_name,
u.status AS whatsapp_status,
u.created_at AS whatsapp_desde
FROM lab_pacientes p
LEFT JOIN users u ON u.id = p.user_id
WHERE p.id = ?
", [$id]) ?: null;
}
/**
* Buscar por número de documento.
*/
public function porDocumento(string $documento): ?array {
return $this->db->fetch(
'SELECT * FROM lab_pacientes WHERE numero_documento = ?',
[$documento]
) ?: null;
}
/**
* Buscar por user_id (teléfono WhatsApp).
*/
public function porUserId(int $userId): ?array {
return $this->db->fetch(
'SELECT * FROM lab_pacientes WHERE user_id = ?',
[$userId]
) ?: null;
}
/**
* Crear paciente nuevo.
*
* @param array $datos Campos del formulario
* @param int|null $adminId Quien lo crea (para trazabilidad)
* @return int ID del paciente creado
*/
public function crear(array $datos, ?int $adminId = null): int {
$campos = $this->filtrarCampos($datos);
// Validar documento duplicado antes de insertar
if (!empty($campos['numero_documento'])) {
$existente = $this->db->fetch(
'SELECT id, nombre_completo FROM lab_pacientes WHERE numero_documento = ?',
[$campos['numero_documento']]
);
if ($existente) {
throw new Exception(
'Ya existe un paciente registrado con ese número de documento: ' .
$existente['nombre_completo']
);
}
}
$id = $this->db->insert('lab_pacientes', $campos);
$this->log->registrar($adminId, 'pacientes', 'crear', $id, $campos);
return $id;
}
/**
* Actualizar paciente existente.
*/
public function actualizar(int $id, array $datos, ?int $adminId = null): bool {
$campos = $this->filtrarCampos($datos);
// Validar que el documento no pertenezca a otro paciente
if (!empty($campos['numero_documento'])) {
$existente = $this->db->fetch(
'SELECT id, nombre_completo FROM lab_pacientes WHERE numero_documento = ? AND id != ?',
[$campos['numero_documento'], $id]
);
if ($existente) {
throw new Exception(
'Ese número de documento ya está registrado para: ' .
$existente['nombre_completo']
);
}
}
// Registrar antes y después de lo que realmente cambia. Guardar solo el
// valor nuevo impide reconstruir el dato anterior si la corrección
// resultó equivocada, que es justo cuando hace falta consultarlo.
$antes = $this->db->fetch('SELECT * FROM lab_pacientes WHERE id = ?', [$id]) ?: [];
$cambios = [];
foreach ($campos as $campo => $nuevo) {
$previo = $antes[$campo] ?? null;
if ((string)$previo !== (string)$nuevo) {
$cambios[$campo] = ['antes' => $previo, 'despues' => $nuevo];
}
}
$ok = $this->db->update('lab_pacientes', $campos, 'id = ?', [$id]);
if ($cambios) {
$this->log->registrar($adminId, 'pacientes', 'editar', $id, $cambios);
}
return $ok > 0;
}
/**
* Desactivar (soft-delete).
*/
public function desactivar(int $id, ?int $adminId = null): bool {
$ok = $this->db->update('lab_pacientes', ['is_active' => 0], 'id = ?', [$id]);
$this->log->registrar($adminId, 'pacientes', 'desactivar', $id);
return $ok > 0;
}
/**
* Vincular paciente con un usuario de WhatsApp.
*/
public function vincularUsuario(int $pacienteId, int $userId, ?int $adminId = null): bool {
$ok = $this->db->update('lab_pacientes', ['user_id' => $userId], 'id = ?', [$pacienteId]);
$this->log->registrar($adminId, 'pacientes', 'vincular_whatsapp', $pacienteId, [
'user_id' => $userId,
]);
return $ok > 0;
}
// ─────────────────────────────────────────────────────────────────────────
// Historial
// ─────────────────────────────────────────────────────────────────────────
/**
* Órdenes médicas del paciente.
*/
public function ordenes(int $pacienteId, int $limit = 20): array {
return $this->db->fetchAll("
SELECT
o.*,
au.full_name AS autorizada_por_nombre
FROM lab_ordenes_medicas o
LEFT JOIN admin_users au ON au.id = o.autorizada_por
WHERE o.paciente_id = ?
ORDER BY o.created_at DESC
LIMIT ?
", [$pacienteId, $limit]);
}
/**
* Historial de actividad de trazabilidad del paciente.
*/
public function actividad(int $pacienteId): array {
return $this->log->porEntidad('pacientes', $pacienteId);
}
// ─────────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────────
/**
* Crea automáticamente un paciente a partir de un users.id (whatsapp)
* si aún no existe. Devuelve el id del paciente.
*/
public function obtenerOCrearDesdeWhatsapp(int $userId): int {
$existente = $this->porUserId($userId);
if ($existente) {
return $existente['id'];
}
$user = $this->db->fetch(
'SELECT * FROM users WHERE id = ?',
[$userId]
);
return $this->crear([
'user_id' => $userId,
'nombre_completo'=> $user['name'] ?? ('Paciente ' . $user['phone_number']),
'telefono' => $user['phone_number'] ?? null,
]);
}
private function filtrarCampos(array $datos): array {
$permitidos = [
'user_id', 'numero_documento', 'tipo_documento',
'nombre_completo', 'telefono', 'email',
'fecha_nacimiento', 'genero', 'direccion',
'ciudad', 'barrio', 'eps', 'notas_admin', 'is_active', 'origen',
];
$campos = array_intersect_key($datos, array_flip($permitidos));
if (!empty($campos['telefono'])) {
$campos['telefono'] = $this->normalizarTelefono($campos['telefono']);
}
return $campos;
}
/**
* Normaliza un número de teléfono colombiano al formato 57XXXXXXXXXX.
* - Elimina espacios, guiones y paréntesis.
* - Si empieza por +57 → quita el +.
* - Si tiene 10 dígitos empezando por 3 → antepone 57.
* - Cualquier otro caso lo devuelve limpio de caracteres no numéricos.
*/
private function normalizarTelefono(string $tel): string {
$tel = preg_replace('/[^0-9+]/', '', $tel);
if (str_starts_with($tel, '+')) {
$tel = ltrim($tel, '+');
}
// Sin prefijo (10 dígitos empezando por 3) → asume Colombia 57
if (strlen($tel) === 10 && str_starts_with($tel, '3')) {
$tel = '57' . $tel;
}
// Si ya trae prefijo de 2 dígitos (57xx... o 58xx...) lo respetamos
return $tel;
}
}