db = getDBConnection(); } /** WHERE entre fechas (columna datetime/timestamp) */ private function dateFilterClause(?string $from, ?string $to, string $colName, array &$params): string { $where = []; if (!empty($from)) { $where[] = "$colName >= ?"; $params[] = $from . ' 00:00:00'; } if (!empty($to)) { $where[] = "$colName <= ?"; $params[] = $to . ' 23:59:59'; } return $where ? (' WHERE ' . implode(' AND ', $where)) : ''; } /** Ingresos y pedidos por mes (pedidos) */ public function getIngresosPedidosMensuales(?string $from = null, ?string $to = null): array { $params = []; $where = $this->dateFilterClause($from, $to, 'fecha_pedido', $params); $sql = " SELECT YEAR(fecha_pedido) AS año, MONTH(fecha_pedido) AS mes, COUNT(id_pedido) AS total_pedidos, COALESCE(SUM(total_pedido),0) AS total_ingresos FROM pedidos " . ($where ? $where . " AND id_estado_pedido != 3" : " WHERE id_estado_pedido != 3") . " GROUP BY YEAR(fecha_pedido), MONTH(fecha_pedido) ORDER BY año ASC, mes ASC "; $st = $this->db->prepare($sql); $st->execute($params); return $st->fetchAll(PDO::FETCH_ASSOC); } /** Productos vendidos por mes (detalles_pedido + pedidos) */ public function getProductosVendidosMensuales(?string $from = null, ?string $to = null): array { $params = []; // Ojo: la fecha está en pedidos.fecha_pedido $where = $this->dateFilterClause($from, $to, 'p.fecha_pedido', $params); $sql = " SELECT YEAR(p.fecha_pedido) AS año, MONTH(p.fecha_pedido) AS mes, COALESCE(SUM(dp.cantidad),0) AS productos_vendidos FROM detalles_pedido dp INNER JOIN pedidos p ON dp.id_pedido = p.id_pedido " . ($where ? $where . " AND p.id_estado_pedido != 3" : " WHERE p.id_estado_pedido != 3") . " GROUP BY YEAR(p.fecha_pedido), MONTH(p.fecha_pedido) ORDER BY año ASC, mes ASC "; $st = $this->db->prepare($sql); $st->execute($params); return $st->fetchAll(PDO::FETCH_ASSOC); } /** Top productos (detalles_pedido + productos + categorias) */ public function getProductosMasVendidos(int $limite = 5, ?string $from = null, ?string $to = null): array { $params = []; $where = $this->dateFilterClause($from, $to, 'p.fecha_pedido', $params); $sql = " SELECT pr.nombre_producto AS nombre_producto, c.nombre_categoria AS categoria, COALESCE(SUM(dp.cantidad),0) AS total_vendido FROM detalles_pedido dp INNER JOIN productos pr ON dp.id_producto = pr.id_producto LEFT JOIN categorias c ON pr.id_categoria = c.id_categoria INNER JOIN pedidos p ON dp.id_pedido = p.id_pedido " . ($where ? $where . " AND p.id_estado_pedido != 3" : " WHERE p.id_estado_pedido != 3") . " GROUP BY pr.id_producto, c.id_categoria ORDER BY total_vendido DESC LIMIT ? "; $st = $this->db->prepare($sql); $params[] = $limite; $st->execute($params); return $st->fetchAll(PDO::FETCH_ASSOC); } /** Clientes nuevos por mes (clientes.fecha_creacion) */ public function getClientesNuevosMensuales(?string $from = null, ?string $to = null): array { $params = []; $where = $this->dateFilterClause($from, $to, 'fecha_creacion', $params); $sql = " SELECT YEAR(fecha_creacion) AS año, MONTH(fecha_creacion) AS mes, COUNT(id_cliente) AS clientes_nuevos FROM clientes $where GROUP BY YEAR(fecha_creacion), MONTH(fecha_creacion) ORDER BY año ASC, mes ASC "; $st = $this->db->prepare($sql); $st->execute($params); return $st->fetchAll(PDO::FETCH_ASSOC); } /** Top clientes por nº de pedidos (alias apellidos -> apellido para el JS) */ public function getClientesMasPedidos(int $limite = 5, ?string $from = null, ?string $to = null): array { $params = []; $where = $this->dateFilterClause($from, $to, 'p.fecha_pedido', $params); $sql = " SELECT c.nombre, c.apellidos AS apellido, COUNT(p.id_pedido) AS total_pedidos FROM pedidos p INNER JOIN clientes c ON p.id_cliente = c.id_cliente " . ($where ? $where . " AND p.id_estado_pedido != 3" : " WHERE p.id_estado_pedido != 3") . " GROUP BY p.id_cliente ORDER BY total_pedidos DESC LIMIT ? "; $st = $this->db->prepare($sql); $params[] = $limite; $st->execute($params); return $st->fetchAll(PDO::FETCH_ASSOC); } /** Proveedores con más entregas (movimientos_inventario) */ public function getProveedoresMasEntregas(?string $from = null, ?string $to = null): array { $params = []; // La fecha está en movimientos_inventario.fecha_movimiento $baseWhere = $this->dateFilterClause($from, $to, 'mi.fecha_movimiento', $params); // Añadimos además tipo_movimiento = 'entrada' $where = $baseWhere ? ($baseWhere . " AND mi.tipo_movimiento = 'entrada'") : " WHERE mi.tipo_movimiento = 'entrada'"; $sql = " SELECT pr.nombre AS nombre_proveedor, COUNT(mi.id_movimiento) AS total_entregas FROM movimientos_inventario mi LEFT JOIN proveedores pr ON mi.id_proveedor = pr.id_proveedor $where GROUP BY pr.id_proveedor, pr.nombre ORDER BY total_entregas DESC "; $st = $this->db->prepare($sql); $st->execute($params); return $st->fetchAll(PDO::FETCH_ASSOC); } /** Distribución de ventas por categorías (sum(subtotal) o cantidad*precio) */ public function getDistribucionCategorias(?string $from = null, ?string $to = null): array { $params = []; $where = $this->dateFilterClause($from, $to, 'p.fecha_pedido', $params); // detalles_pedido tiene precio_unitario y cantidad; usamos el cálculo robusto si subtotal puede venir NULL $sql = " SELECT COALESCE(c.nombre_categoria, 'Sin categoría') AS nombre_categoria, COALESCE(SUM( CASE WHEN dp.subtotal IS NOT NULL THEN dp.subtotal ELSE dp.cantidad * dp.precio_unitario END ),0) AS total_ventas FROM detalles_pedido dp INNER JOIN productos pr ON dp.id_producto = pr.id_producto LEFT JOIN categorias c ON pr.id_categoria = c.id_categoria INNER JOIN pedidos p ON dp.id_pedido = p.id_pedido " . ($where ? $where . " AND p.id_estado_pedido != 3" : " WHERE p.id_estado_pedido != 3") . " GROUP BY c.id_categoria, c.nombre_categoria ORDER BY total_ventas DESC "; $st = $this->db->prepare($sql); $st->execute($params); return $st->fetchAll(PDO::FETCH_ASSOC); } }