db = Database::getConnection(); } // MÉTODO PARA OBTENER ERRORES E INFORMARLOS A TRAVÉZ DEL CONTROLLER public function getError(): ?string { return $this->error; } // GUARDAR PEDIDO (retorna false o el ID del pedido generado) public function guardarPedido(array $productosFinales, array $proveedor, array $vendedor, float $total): int|false { // Validaciones (ya verifica el controller también) if (empty($productosFinales)) { $this->error = 'El pedido no tiene productos.'; return false; } if (empty($proveedor['id'])) { $this->error = 'Proveedor inválido.'; return false; } if (empty($vendedor['id'])) { $this->error = 'No se pudo vincular al usuario.'; return false; } if ($total <= 0) { $this->error = 'Total de pedido inválido.'; return false; } foreach ($productosFinales as $p) { if (empty($p['id_producto']) || empty($p['cantidad']) || empty($p['costo']) || empty($p['precio'])) { $this->error = 'Producto inválido en el pedido.'; return false; } if ($p['cantidad'] <= 0 || $p['costo'] < 0) { $this->error = 'Cantidad o costo inválido.'; return false; } } try { $this->db->beginTransaction(); // 1. INSERT PEDIDO $sqlPedido = " INSERT INTO pedidos (total, id_proveedor, id_usuario) VALUES (:total, :id_proveedor, :id_usuario) RETURNING id "; $stmtPedido = $this->db->prepare($sqlPedido); $stmtPedido->execute([ ':total' => $total, ':id_proveedor' => $proveedor['id'], ':id_usuario' => $vendedor['id'] ]); $idPedido = $stmtPedido->fetchColumn(); if (!$idPedido) { $this->error = 'No se pudo crear el pedido.'; $this->db->rollBack(); return false; } // 2. INSERT DETALLE (LOOP) $sqlDetalle = " INSERT INTO pedido_detalle (precio_unitario, cantidad, subtotal, id_pedido, id_producto) VALUES (:precio_unitario, :cantidad, :subtotal, :id_pedido, :id_producto) "; $stmtDetalle = $this->db->prepare($sqlDetalle); foreach ($productosFinales as $p) { $subtotal = $p['costo'] * $p['cantidad']; if (!$stmtDetalle->execute([ ':precio_unitario' => $p['costo'], ':cantidad' => $p['cantidad'], ':subtotal' => $subtotal, ':id_pedido' => $idPedido, ':id_producto' => $p['id_producto'], ]) ) { $this->error = 'Error al guardar el detalle del pedido.'; $this->db->rollBack(); return false; } } // 3. ACTUALIZAR DATOS DE PRODUCTO $sqlStock = " UPDATE productos SET stock = COALESCE(stock, 0) + :cantidad, precio = :precio WHERE id = :id_producto "; $stmtStock = $this->db->prepare($sqlStock); foreach ($productosFinales as $p) { if (!$stmtStock->execute([ ':cantidad' => $p['cantidad'], ':precio' => $p['precio'], ':id_producto' => $p['id_producto'], ]) ) { $this->error = 'Error al actualizar el producto.'; $this->db->rollBack(); return false; } } // 4. COMMIT $this->db->commit(); return $idPedido; } catch (Throwable $e) { $this->db->rollBack(); // Log interno error_log($e->getMessage()); $this->error = 'Error interno al guardar el pedido.'; return false; } } // LISTAR PEDIDOS public function listarPedidos(int $anio): ?array { $sql = "SELECT p.id, p.fecha, p.total, pe.razon_social, u.nombre AS vendedor_nombre, u.apellido AS vendedor_apellido FROM pedidos AS p INNER JOIN proveedores AS pe ON (p.id_proveedor = pe.id) INNER JOIN usuarios AS u ON (p.id_usuario = u.id) WHERE EXTRACT(YEAR FROM p.fecha) = :anio ORDER BY p.id ASC"; $stmt = $this->db->prepare($sql); $stmt->execute([':anio' => $anio]); return $stmt->fetchAll(PDO::FETCH_ASSOC); } // OBTENER DETALLE DE PEDIDO public function getDetallePedido(int $idPedido): ?array { $sql = "SELECT p.id AS id_producto, p.marca, p.modelo, pd.precio_unitario, pd.cantidad, pd.subtotal FROM pedido_detalle AS pd INNER JOIN productos AS p ON (pd.id_producto = p.id) WHERE (pd.id_pedido = :id_pedido) ORDER BY p.id ASC"; $stmt = $this->db->prepare($sql); $stmt->execute([':id_pedido' => $idPedido]); return $stmt->fetchAll(PDO::FETCH_ASSOC); } // OBTENER PREVIEW DE CODIGO DE REMITO (no se utiliza más) /* public function getNumeroRemitoPreview(): int { $sql = "SELECT nextval('remito_preview_seq')"; $stmt = $this->db->prepare($sql); $stmt->execute(); return (int) $stmt->fetchColumn(); } */ // OBTENER COSTO DE PRODUCTO public function obtenerCosto(int $idProducto): ?float { $sql = "SELECT pd.precio_unitario FROM pedido_detalle AS pd INNER JOIN productos AS p ON (pd.id_producto = p.id) WHERE (p.id = :idProducto) AND pd.id_pedido = ( SELECT MAX(pd2.id_pedido) FROM pedido_detalle AS pd2 WHERE pd2.id_producto = :idProducto2 )"; $stmt = $this->db->prepare($sql); $stmt->execute([ ':idProducto' => $idProducto, ':idProducto2' => $idProducto ]); $resultado = $stmt->fetchColumn(); return $resultado !== false ? (float) $resultado : null; } // OBTENER COSTOS DE PRODUCTOS EN BASE A UNA CATEGORIA public function obtenerCostosPorCategoria(int $idCategoria): array { // Uso de LATERAL en LEFT JOIN -> permite traer el costo mas reciente (actúa como un foreach, ejecutando la subconsulta por cada producto) $sql = "SELECT p.id AS id_producto, pd.precio_unitario AS costo FROM productos AS p LEFT JOIN LATERAL ( SELECT pd.precio_unitario FROM pedido_detalle AS pd INNER JOIN pedidos AS pe ON (pe.id = pd.id_pedido) WHERE (pd.id_producto = p.id) ORDER BY pe.fecha DESC, pd.id DESC LIMIT 1 ) pd ON true WHERE (p.id_categoria = :idCategoria)"; $stmt = $this->db->prepare($sql); $stmt->execute([':idCategoria' => $idCategoria]); return $stmt->fetchAll(PDO::FETCH_ASSOC); } }