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>
302 lines
11 KiB
PHP
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;
|
|
}
|
|
}
|