from fastapi import APIRouter, Depends, Query, UploadFile, File, HTTPException from fastapi.responses import FileResponse from typing import Optional import os import shutil from auth.models import TokenPayload from auth.dependencies import require_permission from crm.models import ProductCreate, ProductUpdate, ProductInDB, ProductListResponse from crm import service router = APIRouter(prefix="/api/crm/products", tags=["crm-products"]) PHOTO_DIR = os.path.join(os.path.dirname(__file__), "..", "storage", "product_images") os.makedirs(PHOTO_DIR, exist_ok=True) @router.get("", response_model=ProductListResponse) def list_products( search: Optional[str] = Query(None), category: Optional[str] = Query(None), active_only: bool = Query(False), _user: TokenPayload = Depends(require_permission("crm", "view")), ): products = service.list_products(search=search, category=category, active_only=active_only) return ProductListResponse(products=products, total=len(products)) @router.get("/{product_id}", response_model=ProductInDB) def get_product( product_id: str, _user: TokenPayload = Depends(require_permission("crm", "view")), ): return service.get_product(product_id) @router.post("", response_model=ProductInDB, status_code=201) def create_product( body: ProductCreate, _user: TokenPayload = Depends(require_permission("crm", "edit")), ): return service.create_product(body) @router.put("/{product_id}", response_model=ProductInDB) def update_product( product_id: str, body: ProductUpdate, _user: TokenPayload = Depends(require_permission("crm", "edit")), ): return service.update_product(product_id, body) @router.delete("/{product_id}", status_code=204) def delete_product( product_id: str, _user: TokenPayload = Depends(require_permission("crm", "edit")), ): service.delete_product(product_id) @router.post("/{product_id}/photo", response_model=ProductInDB) async def upload_product_photo( product_id: str, file: UploadFile = File(...), _user: TokenPayload = Depends(require_permission("crm", "edit")), ): """Upload a product photo. Accepts JPG or PNG, stored on disk.""" if file.content_type not in ("image/jpeg", "image/png", "image/webp"): raise HTTPException(status_code=400, detail="Only JPG, PNG, or WebP images are accepted.") ext = {"image/jpeg": "jpg", "image/png": "png", "image/webp": "webp"}.get(file.content_type, "jpg") photo_path = os.path.join(PHOTO_DIR, f"{product_id}.{ext}") # Remove any old photo files for this product for old_ext in ("jpg", "png", "webp"): old_path = os.path.join(PHOTO_DIR, f"{product_id}.{old_ext}") if os.path.exists(old_path) and old_path != photo_path: os.remove(old_path) with open(photo_path, "wb") as f: shutil.copyfileobj(file.file, f) photo_url = f"/crm/products/{product_id}/photo" return service.update_product(product_id, ProductUpdate(photo_url=photo_url)) @router.get("/{product_id}/photo") def get_product_photo( product_id: str, ): """Serve a product photo from disk.""" for ext in ("jpg", "png", "webp"): photo_path = os.path.join(PHOTO_DIR, f"{product_id}.{ext}") if os.path.exists(photo_path): return FileResponse(photo_path) raise HTTPException(status_code=404, detail="No photo found for this product.")