diff --git a/backend/app/models/product.py b/backend/app/models/product.py index e72a278..64b08e2 100644 --- a/backend/app/models/product.py +++ b/backend/app/models/product.py @@ -43,3 +43,8 @@ class Product(Base): return float(self.model.base_price) else: return 0.0 # Default fallback + + @property + def category_name(self): + """Return the name of the associated category, or None if not set.""" + return self.category.name if self.category else None diff --git a/backend/app/routers/products.py b/backend/app/routers/products.py index 28dea1b..90712a1 100644 --- a/backend/app/routers/products.py +++ b/backend/app/routers/products.py @@ -1,5 +1,6 @@ from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File from fastapi.responses import FileResponse +from sqlalchemy import func from sqlalchemy.orm import Session, joinedload from typing import List, Optional from pathlib import Path @@ -40,7 +41,7 @@ def list_products( limit: int = 20, db: Session = Depends(get_db), ): - query = db.query(Product).options(joinedload(Product.model)) + query = db.query(Product).options(joinedload(Product.model), joinedload(Product.category)) if category_id: query = query.filter(Product.category_id == category_id) @@ -101,9 +102,42 @@ def search(q: str, skip: int = 0, limit: int = 20, db: Session = Depends(get_db) return products +@router.get("/count") +def get_products_count( + category_id: Optional[int] = None, + model_id: Optional[int] = None, + brand: Optional[str] = None, + gender: Optional[str] = None, + min_price: Optional[float] = None, + max_price: Optional[float] = None, + on_sale: Optional[bool] = None, + featured: Optional[bool] = None, + db: Session = Depends(get_db), +): + """Return the total number of products matching the given filters.""" + query = db.query(func.count(Product.id)) + if category_id: + query = query.filter(Product.category_id == category_id) + if model_id: + query = query.filter(Product.model_id == model_id) + if brand: + query = query.filter(Product.brand == brand) + if gender: + query = query.filter(Product.gender == gender) + if min_price is not None: + query = query.filter(Product.price >= min_price) + if max_price is not None: + query = query.filter(Product.price <= max_price) + if on_sale is not None: + query = query.filter(Product.is_on_sale == on_sale) + if featured is not None: + query = query.filter(Product.is_featured == featured) + return {"count": query.scalar()} + + @router.get("/{product_id}", response_model=ProductResponse) def get_product(product_id: int, db: Session = Depends(get_db)): - product = get_product_by_id(db, product_id) + product = db.query(Product).options(joinedload(Product.model), joinedload(Product.category)).filter(Product.id == product_id).first() if not product: raise HTTPException(status_code=404, detail="Product not found") diff --git a/backend/app/schemas/product.py b/backend/app/schemas/product.py index 2ca4f7d..7644158 100644 --- a/backend/app/schemas/product.py +++ b/backend/app/schemas/product.py @@ -72,6 +72,7 @@ class ProductResponse(BaseModel): is_on_sale: bool override_price: Optional[Decimal] override_sizes: Optional[List[str]] + category_name: Optional[str] = None created_at: datetime class Config: diff --git a/backend/app/services/product.py b/backend/app/services/product.py index 6581b5f..6868beb 100644 --- a/backend/app/services/product.py +++ b/backend/app/services/product.py @@ -13,7 +13,7 @@ def get_products( skip: int = 0, limit: int = 10, ) -> List[Product]: - query = db.query(Product).options(joinedload(Product.model)) + query = db.query(Product).options(joinedload(Product.model), joinedload(Product.category)) if category_id: query = query.filter(Product.category_id == category_id) @@ -67,7 +67,7 @@ def delete_product(db: Session, product_id: int) -> bool: def search_products(db: Session, query: str, skip: int = 0, limit: int = 10) -> List[Product]: return ( db.query(Product) - .options(joinedload(Product.model)) + .options(joinedload(Product.model), joinedload(Product.category)) .filter( Product.name.ilike(f"%{query}%") | Product.brand.ilike(f"%{query}%") ) diff --git a/frontend/src/pages/Admin.jsx b/frontend/src/pages/Admin.jsx index dccbbe1..e5429ca 100644 --- a/frontend/src/pages/Admin.jsx +++ b/frontend/src/pages/Admin.jsx @@ -53,7 +53,8 @@ export default function Admin() { const [filterModel, setFilterModel] = useState('') const [brands, setBrands] = useState([]) const [allProducts, setAllProducts] = useState([]) // Store all products for filtering - const [itemsPerPage, setItemsPerPage] = useState(25) // Products per page in admin list + const [itemsPerPage, setItemsPerPage] = useState(25) // 25, 50, 100, or 'all' + const [currentPage, setCurrentPage] = useState(1) const [showBrandForm, setShowBrandForm] = useState(false) const [editingBrand, setEditingBrand] = useState(null) const [brandFormData, setBrandFormData] = useState({ name: '' }) @@ -97,7 +98,8 @@ export default function Admin() { const fetchProducts = async () => { try { - const response = await api.get('/products') + // Fetch all products so admin can see and filter the full catalogue + const response = await api.get('/products', { params: { limit: 10000, skip: 0 } }) setAllProducts(response.data) setProducts(response.data) } catch (error) { @@ -192,6 +194,7 @@ export default function Admin() { } setProducts(filtered) + setCurrentPage(1) // Reset to first page whenever filters change }, [searchQuery, filterBrand, filterCategory, filterModel, allProducts]) const handleChange = (e) => { @@ -1185,18 +1188,35 @@ export default function Admin() { )}
Loading...
) : ( + <>| ID | Name | Brand | +Category | Slug | Price | Stock | @@ -1220,11 +1242,14 @@ export default function Admin() { - {products.slice(0, itemsPerPage).map(product => ( + {pagedProducts.map(product => (
|---|---|---|---|---|
| {product.id} | {product.name} | {product.brand} | ++ {product.category_name || categories.find(c => c.id === product.category_id)?.name || product.category_id} + | {product.slug || '-'} | @@ -1251,7 +1276,47 @@ export default function Admin() {
No products found.