db = Database::getInstance(); $this->log = new ActividadAdmin(); } // ───────────────────────────────────────────────────────────────────────── // CRUD // ───────────────────────────────────────────────────────────────────────── /** * Lista paginada de órdenes con filtros. * * @param array $filtros estado, paciente_id, desde, hasta, busqueda */ public function listar(array $filtros = [], int $pagina = 1, int $porPagina = 30): array { $wheres = []; $params = []; if (!empty($filtros['estado'])) { $wheres[] = 'o.estado = ?'; $params[] = $filtros['estado']; } if (!empty($filtros['paciente_id'])) { $wheres[] = 'o.paciente_id = ?'; $params[] = $filtros['paciente_id']; } if (!empty($filtros['desde'])) { $wheres[] = 'DATE(o.created_at) >= ?'; $params[] = $filtros['desde']; } if (!empty($filtros['hasta'])) { $wheres[] = 'DATE(o.created_at) <= ?'; $params[] = $filtros['hasta']; } if (!empty($filtros['busqueda'])) { $like = '%' . $filtros['busqueda'] . '%'; $wheres[] = '(p.nombre_completo LIKE ? OR p.numero_documento LIKE ?)'; $params[] = $like; $params[] = $like; } $where = $wheres ? 'WHERE ' . implode(' AND ', $wheres) : ''; $offset = ($pagina - 1) * $porPagina; $total = $this->db->fetch( "SELECT COUNT(*) AS n FROM lab_ordenes_medicas o JOIN lab_pacientes p ON p.id = o.paciente_id $where", $params )['n'] ?? 0; $rows = $this->db->fetchAll(" SELECT o.*, p.nombre_completo AS paciente_nombre, p.numero_documento, p.telefono AS paciente_telefono, rev.full_name AS revisada_por_nombre, aut.full_name AS autorizada_por_nombre FROM lab_ordenes_medicas o JOIN lab_pacientes p ON p.id = o.paciente_id LEFT JOIN admin_users rev ON rev.id = o.revisada_por LEFT JOIN admin_users aut ON aut.id = o.autorizada_por $where ORDER BY o.created_at DESC LIMIT ? OFFSET ? ", array_merge($params, [$porPagina, $offset])); return [ 'data' => $rows, 'total' => (int)$total, 'pagina' => $pagina, 'por_pagina' => $porPagina, 'paginas' => (int)ceil($total / $porPagina), ]; } /** * Obtener una orden por ID (con todos los datos relacionados). */ public function obtener(int $id): ?array { $orden = $this->db->fetch(" SELECT o.*, p.nombre_completo AS paciente_nombre, p.numero_documento, p.telefono AS paciente_telefono, p.eps AS paciente_eps, p.user_id, u.phone_number, rev.full_name AS revisada_por_nombre, aut.full_name AS autorizada_por_nombre, c.local_file AS conv_local_file, c.whatsapp_media_id AS conv_media_id FROM lab_ordenes_medicas o JOIN lab_pacientes p ON p.id = o.paciente_id LEFT JOIN users u ON u.id = p.user_id LEFT JOIN admin_users rev ON rev.id = o.revisada_por LEFT JOIN admin_users aut ON aut.id = o.autorizada_por LEFT JOIN conversations c ON c.id = o.conversation_id WHERE o.id = ? ", [$id]); if (!$orden) { return null; } // Historial de cambios de estado $orden['historial'] = $this->db->fetchAll(" SELECT la.*, au.full_name AS admin_nombre FROM lab_autorizaciones la LEFT JOIN admin_users au ON au.id = la.realizada_por WHERE la.orden_id = ? ORDER BY la.created_at ASC ", [$id]); return $orden; } /** * Crear nueva orden médica. */ public function crear(array $datos, ?int $adminId = null): int { $campos = $this->filtrarCampos($datos); $id = $this->db->insert('lab_ordenes_medicas', $campos); // Registrar en historial $this->registrarCambioEstado($id, null, 'pendiente', $adminId, 'Orden creada', 'creada'); $this->log->registrar($adminId, 'ordenes', 'crear', $id, $campos); return $id; } /** * Actualizar datos de la orden (sin cambiar estado). */ public function actualizar(int $id, array $datos, ?int $adminId = null): bool { $campos = $this->filtrarCampos($datos); unset($campos['estado']); // el estado se cambia con cambiarEstado() $ok = $this->db->update('lab_ordenes_medicas', $campos, 'id = ?', [$id]); if ($ok) { $this->registrarCambioEstado($id, null, null, $adminId, 'Datos actualizados', 'editada'); $this->log->registrar($adminId, 'ordenes', 'editar', $id, $campos); } return $ok > 0; } // ───────────────────────────────────────────────────────────────────────── // Gestión de estados (flujo de revisión) // ───────────────────────────────────────────────────────────────────────── /** * Cambia el estado de la orden con trazabilidad completa. * * @param int $id * @param string $nuevoEstado Debe estar en self::ESTADOS * @param int $adminId * @param string $comentario */ public function cambiarEstado( int $id, string $nuevoEstado, int $adminId, string $comentario = '' ): bool { if (!in_array($nuevoEstado, self::ESTADOS)) { throw new InvalidArgumentException("Estado inválido: $nuevoEstado"); } $orden = $this->obtener($id); if (!$orden) { throw new RuntimeException("Orden #$id no encontrada"); } $estadoAnterior = $orden['estado']; $ahora = date('Y-m-d H:i:s'); $campos = ['estado' => $nuevoEstado]; // Campos adicionales según el nuevo estado if ($nuevoEstado === 'en_revision') { $campos['revisada_por'] = $adminId; $campos['revisada_at'] = $ahora; } elseif (in_array($nuevoEstado, ['autorizada', 'rechazada'])) { $campos['autorizada_por'] = $adminId; $campos['autorizada_at'] = $ahora; $campos['comentario_revision'] = $comentario; } $ok = $this->db->update('lab_ordenes_medicas', $campos, 'id = ?', [$id]); if ($ok) { $this->registrarCambioEstado($id, $estadoAnterior, $nuevoEstado, $adminId, $comentario); $this->log->registrar($adminId, 'ordenes', $nuevoEstado, $id, [ 'estado_anterior' => $estadoAnterior, 'comentario' => $comentario, ]); } return $ok > 0; } // Alias de conveniencia public function autorizar(int $id, int $adminId, string $comentario = ''): bool { return $this->cambiarEstado($id, 'autorizada', $adminId, $comentario); } public function rechazar(int $id, int $adminId, string $motivo): bool { return $this->cambiarEstado($id, 'rechazada', $adminId, $motivo); } public function iniciarRevision(int $id, int $adminId): bool { return $this->cambiarEstado($id, 'en_revision', $adminId); } // ───────────────────────────────────────────────────────────────────────── // Estadísticas / Dashboard // ───────────────────────────────────────────────────────────────────────── /** * Conteo de órdenes por estado. */ public function contadorPorEstado(): array { $rows = $this->db->fetchAll(" SELECT estado, COUNT(*) AS total FROM lab_ordenes_medicas GROUP BY estado "); $result = array_fill_keys(self::ESTADOS, 0); foreach ($rows as $row) { $result[$row['estado']] = (int)$row['total']; } return $result; } /** * Órdenes pendientes más antiguas (sin revisar). */ public function pendientesAntiguos(int $limit = 10): array { return $this->db->fetchAll(" SELECT o.*, p.nombre_completo, TIMESTAMPDIFF(HOUR, o.created_at, NOW()) AS horas_espera FROM lab_ordenes_medicas o JOIN lab_pacientes p ON p.id = o.paciente_id WHERE o.estado = 'pendiente' ORDER BY o.created_at ASC LIMIT ? ", [$limit]); } // ───────────────────────────────────────────────────────────────────────── // Helpers privados // ───────────────────────────────────────────────────────────────────────── private function registrarCambioEstado( int $ordenId, ?string $estadoAnterior, ?string $estadoNuevo, ?int $adminId, string $comentario = '', string $accion = '' ): void { $accion = $accion ?: ($estadoNuevo ?? 'editada'); $this->db->insert('lab_autorizaciones', [ 'orden_id' => $ordenId, 'accion' => $accion, 'estado_anterior'=> $estadoAnterior, 'estado_nuevo' => $estadoNuevo, 'realizada_por' => $adminId, 'comentario' => $comentario, ]); } private function filtrarCampos(array $datos): array { $permitidos = [ 'paciente_id', 'conversation_id', 'whatsapp_media_id', 'local_file', 'estado', 'medico_nombre', 'medico_registro', 'fecha_orden', 'diagnostico', 'examenes_solicitados', 'requiere_ayuno', 'horas_ayuno', 'indicaciones', 'revisada_por', 'revisada_at', 'autorizada_por', 'autorizada_at', 'comentario_revision', 'notas_admin', ]; return array_intersect_key($datos, array_flip($permitidos)); } }