db = Database::getInstance(); $this->log = new ActividadAdmin(); } // ───────────────────────────────────────────────────────────────────────── // CRUD // ───────────────────────────────────────────────────────────────────────── /** * Lista de domicilios con filtros. * * @param array $filtros fecha, estado, enfermera_id, paciente_id */ public function listar(array $filtros = [], int $pagina = 1, int $porPagina = 30): array { $wheres = []; $params = []; if (!empty($filtros['fecha'])) { $wheres[] = 'd.fecha_programada = ?'; $params[] = $filtros['fecha']; } if (!empty($filtros['desde'])) { $wheres[] = 'd.fecha_programada >= ?'; $params[] = $filtros['desde']; } if (!empty($filtros['hasta'])) { $wheres[] = 'd.fecha_programada <= ?'; $params[] = $filtros['hasta']; } if (!empty($filtros['estado'])) { $wheres[] = 'd.estado = ?'; $params[] = $filtros['estado']; } if (!empty($filtros['enfermera_id'])) { $wheres[] = 'a.enfermera_id = ?'; $params[] = $filtros['enfermera_id']; } if (!empty($filtros['paciente_id'])) { $wheres[] = 'd.paciente_id = ?'; $params[] = $filtros['paciente_id']; } $where = $wheres ? 'WHERE ' . implode(' AND ', $wheres) : ''; $offset = ($pagina - 1) * $porPagina; $total = $this->db->fetch( "SELECT COUNT(*) AS n FROM lab_domicilios d LEFT JOIN lab_asignaciones a ON a.domicilio_id = d.id $where", $params )['n'] ?? 0; $rows = $this->db->fetchAll(" SELECT d.*, p.nombre_completo AS paciente_nombre, p.telefono AS paciente_telefono, e.nombre_completo AS enfermera_nombre, e.telefono AS enfermera_telefono, a.enfermera_id AS enfermera_lab_id, a.id AS asignacion_id, a.estado AS asignacion_estado, CASE WHEN d.hora_en_camino IS NULL OR d.hora_llegada IS NULL THEN NULL ELSE TIMESTAMPDIFF( MINUTE, CONCAT(d.fecha_programada, ' ', d.hora_en_camino), CONCAT(d.fecha_programada, ' ', d.hora_llegada) ) END AS tiempo_recorrido_min FROM lab_domicilios d JOIN lab_pacientes p ON p.id = d.paciente_id LEFT JOIN lab_asignaciones a ON a.domicilio_id = d.id LEFT JOIN lab_enfermeras e ON e.id = a.enfermera_id $where ORDER BY d.fecha_programada ASC, d.hora_programada ASC LIMIT ? OFFSET ? ", array_merge($params, [$porPagina, $offset])); return [ 'data' => $rows, 'total' => (int)$total, 'pagina' => $pagina, 'por_pagina' => $porPagina, 'paginas' => (int)ceil($total / $porPagina), ]; } /** * Domicilio por ID con todos los datos relacionados. */ public function obtener(int $id): ?array { $dom = $this->db->fetch(" SELECT d.*, p.nombre_completo AS paciente_nombre, p.telefono AS paciente_telefono, p.email AS paciente_email, p.fecha_nacimiento AS paciente_fecha_nacimiento, p.tipo_documento AS paciente_tipo_documento, p.numero_documento AS paciente_numero_documento, p.direccion AS paciente_direccion, o.examenes_solicitados, o.indicaciones AS indicaciones_orden, o.local_file AS orden_local_file, e.nombre_completo AS enfermera_nombre, e.telefono AS enfermera_telefono, a.enfermera_id AS enfermera_lab_id, a.id AS asignacion_id, a.estado AS asignacion_estado, adm.full_name AS creado_por_nombre, CASE WHEN d.hora_en_camino IS NULL OR d.hora_llegada IS NULL THEN NULL ELSE TIMESTAMPDIFF( MINUTE, CONCAT(d.fecha_programada, ' ', d.hora_en_camino), CONCAT(d.fecha_programada, ' ', d.hora_llegada) ) END AS tiempo_recorrido_min FROM lab_domicilios d JOIN lab_pacientes p ON p.id = d.paciente_id LEFT JOIN lab_ordenes_medicas o ON o.id = d.orden_id LEFT JOIN lab_asignaciones a ON a.domicilio_id = d.id LEFT JOIN lab_enfermeras e ON e.id = a.enfermera_id LEFT JOIN admin_users adm ON adm.id = d.creado_por WHERE d.id = ? ", [$id]); if (!$dom) return null; // Todos los archivos de órdenes vinculados a este domicilio $archivos = $this->db->fetchAll( "SELECT id AS orden_id, local_file FROM lab_ordenes_medicas WHERE domicilio_id = ? AND local_file IS NOT NULL ORDER BY id ASC", [$id] ); // Fallback para domicilios creados antes de la migración (usan orden_id FK) if (empty($archivos) && !empty($dom['orden_local_file'])) { $archivos = [['orden_id' => $dom['orden_id'], 'local_file' => $dom['orden_local_file']]]; } $dom['archivos_ordenes'] = $archivos; // Servicios extras registrados por el enfermero $dom['servicios_extra'] = $this->db->fetchAll( "SELECT id, descripcion, tipo, notas, requiere_pago, valor, created_at FROM lab_servicios_extra WHERE domicilio_id = ? ORDER BY created_at ASC", [$id] ); // Pagos parciales por método de pago $dom['pagos_detalle'] = $this->db->fetchAll( "SELECT modo, monto, notas FROM lab_domicilio_pagos WHERE domicilio_id = ? ORDER BY id ASC", [$id] ); // Vínculo: domicilio origen (este fue reprogramado desde otro) if (!empty($dom['domicilio_origen_id'])) { $dom['origen_info'] = $this->db->fetch( "SELECT id, fecha_programada, hora_programada, estado FROM lab_domicilios WHERE id = ?", [(int)$dom['domicilio_origen_id']] ); } // Vínculo: domicilio(s) reprogramado(s) desde este $dom['reprogramaciones'] = $this->db->fetchAll( "SELECT id, fecha_programada, hora_programada, estado FROM lab_domicilios WHERE domicilio_origen_id = ? ORDER BY id ASC", [$id] ); return $dom; } /** * Crear nuevo domicilio. */ public function crear(array $datos, ?int $adminId = null): int { $campos = $this->filtrarCampos($datos); $campos['creado_por'] = $adminId; // Actualizar orden médica si aplica if (!empty($campos['orden_id'])) { $this->db->update( 'lab_ordenes_medicas', ['estado' => 'en_domicilio'], 'id = ?', [$campos['orden_id']] ); } $id = (int)$this->db->insert('lab_domicilios', $campos); $this->log->registrar($adminId, 'domicilios', 'crear', $id, $campos); return $id; } /** * Actualizar domicilio. */ public function actualizar(int $id, array $datos, ?int $adminId = null): bool { $campos = $this->filtrarCampos($datos); $ok = $this->db->update('lab_domicilios', $campos, 'id = ?', [$id]); $this->log->registrar($adminId, 'domicilios', 'editar', $id, $campos); return $ok > 0; } /** * Cambiar estado del domicilio. */ public function cambiarEstado( int $id, string $nuevoEstado, ?int $adminId, array $extras = [] ): bool { if (!in_array($nuevoEstado, self::ESTADOS)) { throw new InvalidArgumentException("Estado inválido: $nuevoEstado"); } $campos = array_merge(['estado' => $nuevoEstado], $extras); $ok = $this->db->update('lab_domicilios', $campos, 'id = ?', [$id]); $this->log->registrar($adminId, 'domicilios', $nuevoEstado, $id, $extras); return $ok > 0; } /** * Reprogramar un domicilio: * 1. Marca el original como 'reprogramado' con fecha_reprogramada. * 2. Clona el domicilio para la nueva fecha/hora. * 3. Vincula el clon con domicilio_origen_id. * * @return int ID del nuevo domicilio creado. */ public function reprogramar( int $idOrigen, string $nuevaFecha, string $nuevaHora = '', string $motivo = '', ?int $adminId = null ): int { // 1. Obtener datos del original $origen = $this->db->fetch( 'SELECT * FROM lab_domicilios WHERE id = ?', [$idOrigen] ); if (!$origen) { throw new \RuntimeException('Domicilio no encontrado'); } // 2. Marcar original como reprogramado $extrasOrigen = [ 'fecha_reprogramada' => $nuevaFecha, ]; if ($motivo) $extrasOrigen['motivo_cancelacion'] = $motivo; $this->cambiarEstado($idOrigen, 'reprogramado', $adminId, $extrasOrigen); // 3. Clonar campos relevantes para el nuevo domicilio $campos = $this->filtrarCampos($origen); // Resetear campos operativos del original unset( $campos['estado'], $campos['motivo_cancelacion'], $campos['fecha_reprogramada'], $campos['hora_llegada'], $campos['hora_salida'], $campos['observaciones'], $campos['muestras_tomadas'], $campos['pago_estado'], $campos['pago_modo'], $campos['pago_monto'], $campos['pago_fecha'], $campos['pago_notas'], $campos['pago_registrado_por'], $campos['temperatura_inicio'], $campos['temperatura_salida'], $campos['domicilio_origen_id'] ); $campos['fecha_programada'] = $nuevaFecha; $campos['hora_programada'] = $nuevaHora ?: null; $campos['estado'] = 'programado'; $campos['domicilio_origen_id']= $idOrigen; $campos['creado_por'] = $adminId; $nuevoId = (int)$this->db->insert('lab_domicilios', $campos); $this->log->registrar($adminId, 'domicilios', 'reprogramar', $nuevoId, [ 'origen_id' => $idOrigen, 'nueva_fecha' => $nuevaFecha, ]); return $nuevoId; } // ───────────────────────────────────────────────────────────────────────── // Agenda del día // ───────────────────────────────────────────────────────────────────────── /** * Todos los domicilios de hoy (o la fecha indicada). */ public function agendaDia(string $fecha = ''): array { $fecha = $fecha ?: date('Y-m-d'); return $this->listar(['fecha' => $fecha], 1, 200)['data']; } /** * Domicilios sin enfermera asignada. */ public function sinAsignar(string $fecha = ''): array { $fecha = $fecha ?: date('Y-m-d'); return $this->db->fetchAll(" SELECT d.*, p.nombre_completo AS paciente_nombre FROM lab_domicilios d JOIN lab_pacientes p ON p.id = d.paciente_id LEFT JOIN lab_asignaciones a ON a.domicilio_id = d.id WHERE d.fecha_programada = ? AND a.id IS NULL AND d.estado NOT IN ('cancelado','completado') ORDER BY d.hora_programada ASC ", [$fecha]); } /** * Estadísticas: conteo de domicilios por estado hoy. */ public function estadisticasHoy(): array { $rows = $this->db->fetchAll(" SELECT estado, COUNT(*) AS total FROM lab_domicilios WHERE fecha_programada = CURDATE() GROUP BY estado "); $result = array_fill_keys(self::ESTADOS, 0); foreach ($rows as $row) { $result[$row['estado']] = (int)$row['total']; } return $result; } // ───────────────────────────────────────────────────────────────────────── // Helpers // ───────────────────────────────────────────────────────────────────────── private function filtrarCampos(array $datos): array { $permitidos = [ 'orden_id', 'paciente_id', 'direccion', 'ciudad', 'barrio', 'indicaciones_dir', 'fecha_programada', 'hora_programada', 'tipo_servicio', 'tipo_cliente', 'examenes_solicitados', 'estado', 'motivo_cancelacion', 'fecha_reprogramada', 'hora_llegada', 'hora_salida', 'observaciones', 'muestras_tomadas', 'notas_admin', 'seguro_nombre', 'autorizacion', 'copago_laboratorio', 'valor_domicilio', 'valor_copago', 'domicilio_origen_id', // pago 'pago_estado', 'pago_modo', 'pago_monto', 'pago_fecha', 'pago_notas', 'pago_registrado_por', ]; $campos = array_intersect_key($datos, array_flip($permitidos)); // Campos FK que deben ser NULL cuando vienen vacíos o cero foreach (['orden_id', 'paciente_id'] as $fk) { if (array_key_exists($fk, $campos) && ($campos[$fk] === '' || $campos[$fk] === '0' || $campos[$fk] === 0)) { $campos[$fk] = null; } } // Normalizar campos de fecha/hora vacíos a NULL para evitar error SQL (MySQL strict) foreach (['fecha_programada', 'fecha_reprogramada', 'hora_programada', 'hora_llegada', 'hora_salida', 'pago_fecha'] as $dt) { if (array_key_exists($dt, $campos) && trim((string)$campos[$dt]) === '') { $campos[$dt] = null; } } // Normalizar campos numéricos opcionales vacíos a NULL para evitar "Incorrect decimal value: ''" foreach (['valor_domicilio', 'valor_copago', 'copago_laboratorio', 'pago_monto'] as $num) { if (array_key_exists($num, $campos) && trim((string)$campos[$num]) === '') { $campos[$num] = null; } } return $campos; } }