from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy import or_, select from sqlalchemy.orm import Session, joinedload from app.api.deps import AuthSession, get_auth_session from app.db.session import get_db from app.models.mix import Mix from app.models.product import Product from app.schemas.editor import EditorMixUpdate, EditorProductRow, EditorProductUpdate from app.services.client_access_service import has_access_level router = APIRouter(prefix="/api/editor", tags=["editor"]) def _serialize_row(product: Product) -> dict: return { "id": product.id, "tenant_id": product.tenant_id, "client_name": product.client_name, "item_id": product.item_id, "name": product.name, "mix_id": product.mix_id, "mix_client_name": product.mix.client_name if product.mix else "", "mix_name": product.mix.name if product.mix else "", "sale_type": product.sale_type, "unit_of_measure": product.unit_of_measure, "visible": product.visible, "product_notes": product.notes, "mix_notes": product.mix.notes if product.mix else None, } def _require_editor_session( session: AuthSession = Depends(get_auth_session), db: Session = Depends(get_db), ) -> AuthSession: if session.role == "internal": permissions = session.module_permissions or {} if not has_access_level(permissions.get("client_access"), "manage"): raise HTTPException(status_code=403, detail="Lean access is required") if not has_access_level(permissions.get("products"), "edit"): raise HTTPException(status_code=403, detail="products edit access is required") if not has_access_level(permissions.get("mix_master"), "edit"): raise HTTPException(status_code=403, detail="mix_master edit access is required") if not session.tenant_id: raise HTTPException(status_code=403, detail="Internal user context is missing") return session raise HTTPException(status_code=403, detail="Lean access is required") @router.get("/products", response_model=list[EditorProductRow]) def list_editor_products( q: str | None = Query(default=None, max_length=255), client_name: str | None = Query(default=None, max_length=255), limit: int = Query(default=500, ge=1, le=1000), session: AuthSession = Depends(_require_editor_session), db: Session = Depends(get_db), ): statement = ( select(Product) .where(Product.tenant_id == session.tenant_id) .options(joinedload(Product.mix)) .join(Product.mix) .order_by(Product.client_name, Product.name, Product.id) .limit(limit) ) if client_name: statement = statement.where(Product.client_name == client_name) if q: term = f"%{q.strip()}%" statement = statement.where( or_( Product.client_name.ilike(term), Product.name.ilike(term), Product.item_id.ilike(term), Product.unit_of_measure.ilike(term), Mix.name.ilike(term), ) ) return [_serialize_row(product) for product in db.scalars(statement).all()] @router.patch("/products/{product_id}", response_model=EditorProductRow) def update_editor_product( product_id: int, payload: EditorProductUpdate, session: AuthSession = Depends(_require_editor_session), db: Session = Depends(get_db), ): product = db.scalar( select(Product) .where(Product.id == product_id, Product.tenant_id == session.tenant_id) .options(joinedload(Product.mix)) ) if product is None: raise HTTPException(status_code=404, detail="Product not found") if payload.mix_id is not None: mix = db.scalar(select(Mix).where(Mix.id == payload.mix_id, Mix.tenant_id == session.tenant_id)) if mix is None: raise HTTPException(status_code=404, detail="Mix not found") for field, value in payload.model_dump(exclude_unset=True).items(): setattr(product, field, value) db.commit() db.refresh(product) return _serialize_row(product) @router.patch("/mixes/{mix_id}", response_model=list[EditorProductRow]) def update_editor_mix( mix_id: int, payload: EditorMixUpdate, session: AuthSession = Depends(_require_editor_session), db: Session = Depends(get_db), ): mix = db.scalar(select(Mix).where(Mix.id == mix_id, Mix.tenant_id == session.tenant_id)) if mix is None: raise HTTPException(status_code=404, detail="Mix not found") for field, value in payload.model_dump(exclude_unset=True).items(): setattr(mix, field, value) db.commit() products = db.scalars( select(Product) .where(Product.tenant_id == session.tenant_id, Product.mix_id == mix_id) .options(joinedload(Product.mix)) .order_by(Product.client_name, Product.name, Product.id) ).all() return [_serialize_row(product) for product in products]