from typing import Any, Dict from fastapi import APIRouter, Depends, Request from sqlalchemy.orm import Session from adapters.db.session import get_db from app.core.auth_dependency import get_current_user, AuthContext from app.core.permissions import require_business_access from app.core.responses import success_response, ApiError from adapters.db.repositories.category_repository import CategoryRepository router = APIRouter(prefix="/categories", tags=["categories"]) @router.post("/business/{business_id}/tree") @require_business_access("business_id") def get_categories_tree( request: Request, business_id: int, body: Dict[str, Any] | None = None, ctx: AuthContext = Depends(get_current_user), db: Session = Depends(get_db), ) -> Dict[str, Any]: # اجازه مشاهده نیاز به view روی سکشن categories دارد if not ctx.can_read_section("categories"): raise ApiError("FORBIDDEN", "Missing business permission: categories.view", http_status=403) repo = CategoryRepository(db) # درخت سراسری: بدون فیلتر نوع tree = repo.get_tree(business_id, None) # تبدیل کلید title به label به صورت بازگشتی def _map_label(nodes: list[Dict[str, Any]]) -> list[Dict[str, Any]]: mapped: list[Dict[str, Any]] = [] for n in nodes: children = n.get("children") or [] mapped.append({ "id": n.get("id"), "parent_id": n.get("parent_id"), "label": n.get("title", ""), "translations": n.get("translations", {}), "children": _map_label(children) if isinstance(children, list) else [], }) return mapped items = _map_label(tree) return success_response({"items": items}, request) @router.post("/business/{business_id}") @require_business_access("business_id") def create_category( request: Request, business_id: int, body: Dict[str, Any], ctx: AuthContext = Depends(get_current_user), db: Session = Depends(get_db), ) -> Dict[str, Any]: if not ctx.has_business_permission("categories", "add"): raise ApiError("FORBIDDEN", "Missing business permission: categories.add", http_status=403) parent_id = body.get("parent_id") label: str = (body.get("label") or "").strip() # ساخت ترجمه‌ها از روی برچسب واحد translations: Dict[str, str] = {"fa": label, "en": label} if label else {} repo = CategoryRepository(db) obj = repo.create_category(business_id=business_id, parent_id=parent_id, translations=translations) item = { "id": obj.id, "parent_id": obj.parent_id, "label": (obj.title_translations or {}).get(ctx.language) or (obj.title_translations or {}).get("fa") or (obj.title_translations or {}).get("en"), "translations": obj.title_translations, } return success_response({"item": item}, request) @router.post("/business/{business_id}/update") @require_business_access("business_id") def update_category( request: Request, business_id: int, body: Dict[str, Any], ctx: AuthContext = Depends(get_current_user), db: Session = Depends(get_db), ) -> Dict[str, Any]: if not ctx.has_business_permission("categories", "edit"): raise ApiError("FORBIDDEN", "Missing business permission: categories.edit", http_status=403) category_id = body.get("category_id") label = body.get("label") translations = {"fa": label, "en": label} if isinstance(label, str) and label.strip() else None repo = CategoryRepository(db) obj = repo.update_category(category_id=category_id, translations=translations) if not obj: raise ApiError("NOT_FOUND", "Category not found", http_status=404) item = { "id": obj.id, "parent_id": obj.parent_id, "label": (obj.title_translations or {}).get(ctx.language) or (obj.title_translations or {}).get("fa") or (obj.title_translations or {}).get("en"), "translations": obj.title_translations, } return success_response({"item": item}, request) @router.post("/business/{business_id}/move") @require_business_access("business_id") def move_category( request: Request, business_id: int, body: Dict[str, Any], ctx: AuthContext = Depends(get_current_user), db: Session = Depends(get_db), ) -> Dict[str, Any]: if not ctx.has_business_permission("categories", "edit"): raise ApiError("FORBIDDEN", "Missing business permission: categories.edit", http_status=403) category_id = body.get("category_id") new_parent_id = body.get("new_parent_id") repo = CategoryRepository(db) obj = repo.move_category(category_id=category_id, new_parent_id=new_parent_id) if not obj: raise ApiError("NOT_FOUND", "Category not found", http_status=404) item = { "id": obj.id, "parent_id": obj.parent_id, "label": (obj.title_translations or {}).get(ctx.language) or (obj.title_translations or {}).get("fa") or (obj.title_translations or {}).get("en"), "translations": obj.title_translations, } return success_response({"item": item}, request) @router.post("/business/{business_id}/delete") @require_business_access("business_id") def delete_category( request: Request, business_id: int, body: Dict[str, Any], ctx: AuthContext = Depends(get_current_user), db: Session = Depends(get_db), ) -> Dict[str, Any]: if not ctx.has_business_permission("categories", "delete"): raise ApiError("FORBIDDEN", "Missing business permission: categories.delete", http_status=403) repo = CategoryRepository(db) category_id = body.get("category_id") ok = repo.delete_category(category_id=category_id) return success_response({"deleted": ok}, request) # Server-side search categories with breadcrumb path @router.post("/business/{business_id}/search") @require_business_access("business_id") def search_categories( request: Request, business_id: int, body: Dict[str, Any] | None = None, ctx: AuthContext = Depends(get_current_user), db: Session = Depends(get_db), ) -> Dict[str, Any]: if not ctx.can_read_section("categories"): raise ApiError("FORBIDDEN", "Missing business permission: categories.view", http_status=403) q = (body or {}).get("query") if isinstance(body, dict) else None limit = (body or {}).get("limit") if isinstance(body, dict) else None if not isinstance(q, str) or not q.strip(): return success_response({"items": []}, request) try: limit_int = int(limit) if isinstance(limit, int) or (isinstance(limit, str) and str(limit).isdigit()) else 50 limit_int = max(1, min(limit_int, 200)) except Exception: limit_int = 50 repo = CategoryRepository(db) items = repo.search_with_paths(business_id=business_id, query=q.strip(), limit=limit_int) # map label consistently mapped = [ { "id": it.get("id"), "parent_id": it.get("parent_id"), "label": it.get("title") or "", "translations": it.get("translations") or {}, "path": it.get("path") or [], } for it in items ] return success_response({"items": mapped}, request)