""" Provider végpontok – Szolgáltató keresés és gyors felvétel (crowdsourced). 4-Level Category Architecture (2026-06-17): ============================================ - GET /categories/tree — Teljes hierarchikus fa a frontend checkboxai számára - GET /categories/autocomplete — Szöveges kereső Szint 2 és Szint 3 címkékhez - GET /categories — Meglévő endpoint (ExpertiseTag lista) - GET /search — Szolgáltató keresés - POST /quick-add — Gyors szolgáltató felvétel - PUT /{provider_id} — Szolgáltató szerkesztés (hibrid mentés) """ import logging from typing import Optional, List from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy import select, or_ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import joinedload from app.db.session import get_db from app.api.deps import get_current_user from app.models.identity import User from app.models.marketplace.service import ExpertiseTag from app.schemas.provider import ( ProviderSearchResponse, ProviderQuickAddIn, ProviderQuickAddResponse, ProviderUpdateIn, ProviderUpdateResponse, ExpertiseCategoryOut, CategoryTreeNode, CategoryAutocompleteItem, ) from app.services.provider_service import search_providers, quick_add_provider, update_provider router = APIRouter() logger = logging.getLogger(__name__) # ────────────────────────────────────────────── # 4-LEVEL CATEGORY ENDPOINTS (2026-06-17) # ────────────────────────────────────────────── @router.get("/categories/tree", response_model=List[CategoryTreeNode]) async def get_category_tree( db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """ Visszaadja a teljes kategória hierarchikus fát a frontend checkboxai számára. Level 0: Járműtípus (Vehicle Type) Level 1: Iparág/Főcsoport (Industry) Level 2: Szakma (Profession) Level 3: Specifikus feladat/címke (Specific Tag) Rekurzívan építi fel a fa struktúrát a parent_id és path mezők alapján. """ try: # Összes tag betöltése stmt = select(ExpertiseTag).order_by(ExpertiseTag.level, ExpertiseTag.id) result = await db.execute(stmt) all_tags = result.scalars().all() # Indexelés ID alapján tag_map = {t.id: t for t in all_tags} # Fa építése: csak a gyökér elemeket (parent_id IS NULL) adjuk vissza def build_tree(parent_id: Optional[int] = None) -> List[CategoryTreeNode]: nodes = [] for tag in all_tags: if tag.parent_id == parent_id: children = build_tree(tag.id) nodes.append(CategoryTreeNode( id=tag.id, key=tag.key, name_hu=tag.name_hu, name_en=tag.name_en, level=tag.level, path=tag.path, is_official=tag.is_official, children=children, )) return nodes tree = build_tree(parent_id=None) return tree except Exception as e: logger.error(f"Hiba a kategória fa lekérése során: {e}", exc_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Hiba történt a kategória fa lekérése során.", ) @router.get("/categories/autocomplete", response_model=List[CategoryAutocompleteItem]) async def autocomplete_categories( q: str = Query(..., min_length=2, max_length=100, description="Keresőszó"), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """ Szöveges kereső a Szint 2 (Szakma) és Szint 3 (Specifikus feladat) címkékhez. Csak azokat listázza, ahol is_official=True. A keresés a name_hu, name_en és key mezőkben történik (ILIKE). """ try: stmt = select(ExpertiseTag).where( ExpertiseTag.level >= 2, ExpertiseTag.is_official == True, or_( ExpertiseTag.name_hu.ilike(f"%{q}%"), ExpertiseTag.name_en.ilike(f"%{q}%"), ExpertiseTag.key.ilike(f"%{q}%"), ), ).order_by(ExpertiseTag.level, ExpertiseTag.name_hu).limit(20) result = await db.execute(stmt) tags = result.scalars().all() return [ CategoryAutocompleteItem( id=t.id, key=t.key, name_hu=t.name_hu, name_en=t.name_en, level=t.level, path=t.path, parent_id=t.parent_id, ) for t in tags ] except Exception as e: logger.error(f"Hiba a kategória autocomplete során: {e}", exc_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Hiba történt a kategória keresés során.", ) # ────────────────────────────────────────────── # EXISTING ENDPOINTS (extended) # ────────────────────────────────────────────── @router.get("/categories", response_model=List[ExpertiseCategoryOut]) async def list_expertise_categories( db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """ Visszaadja az összes expertise kategóriát a frontend dropdown számára. Kibővítve a 4-level hierarchy mezőkkel (level, parent_id, path). """ try: stmt = select(ExpertiseTag).order_by(ExpertiseTag.level, ExpertiseTag.id) result = await db.execute(stmt) tags = result.scalars().all() return [ ExpertiseCategoryOut( id=t.id, key=t.key, name_hu=t.name_hu, name_en=t.name_en, category=t.category, level=t.level, parent_id=t.parent_id, path=t.path, ) for t in tags ] except Exception as e: logger.error(f"Hiba a kategóriák lekérése során: {e}", exc_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Hiba történt a kategóriák lekérése során.", ) @router.get("/search", response_model=ProviderSearchResponse) async def search_service_providers( q: Optional[str] = Query(None, min_length=2, max_length=200, description="Keresőszó"), category: Optional[str] = Query(None, max_length=50, description="Kategória szűrő (expertise_tags.key)"), city: Optional[str] = Query(None, max_length=100, description="Város szűrő"), category_ids: Optional[str] = Query(None, description="Kategória ID-k vesszővel elválasztva (pl. '201,205') — hierarchikus szűrés"), limit: int = Query(20, ge=1, le=100, description="Találatok száma oldalanként"), offset: int = Query(0, ge=0, description="Lapozási offset"), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """ Szolgáltatók keresése egyesített forrásokból. Három forrásból keres: - **verified_org**: Verifikált szervezetek (fleet.organizations, org_type='service_provider') - **staged_data**: Robotok által gyűjtött adatok (marketplace.service_staging) - **crowd_added**: Közösség által hozzáadott adatok (marketplace.service_providers) A találatok forrás szerint csoportosítva, lapozva érkeznek. **category_ids**: Ha meg van adva, csak azok a szervezetek jelennek meg, amelyek ServiceProfile-jához tartozik legalább egy megadott ExpertiseTag (közvetlenül vagy hierarchikusan a path LIKE segítségével). """ try: # Parse category_ids: vesszővel elválasztott string -> List[int] parsed_category_ids: Optional[List[int]] = None if category_ids: try: parsed_category_ids = [int(x.strip()) for x in category_ids.split(",") if x.strip()] except (ValueError, TypeError): raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Érvénytelen category_ids formátum. Vesszővel elválasztott számok szükségesek (pl. '201,205').", ) result = await search_providers( db=db, q=q, category=category, city=city, category_ids=parsed_category_ids, limit=limit, offset=offset, ) return result except HTTPException: raise except Exception as e: logger.error(f"Hiba a szolgáltató keresés során: {e}", exc_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Hiba történt a keresés során.", ) @router.post("/quick-add", response_model=ProviderQuickAddResponse, status_code=status.HTTP_201_CREATED) async def quick_add_service_provider( data: ProviderQuickAddIn, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """ Gyors szolgáltató felvétel crowdsourcingból. Létrehoz egy új Organization-t (is_verified=False, org_type='service_provider'), hozzáköt egy ServiceProfile-t, a megadott kategóriát (expertise_tags), létrehoz egy Branch-t a címmel, és a felhasználót beállítja OWNER-nek. A szolgáltató 'pending_verification' státuszba kerül, amíg a robotok vagy az adminok nem ellenőrzik. """ try: result = await quick_add_provider( db=db, data=data, user_id=current_user.id, ) return result except ValueError as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e), ) except Exception as e: logger.error(f"Hiba a gyors szolgáltató felvétel során: {e}", exc_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Hiba történt a szolgáltató rögzítése során.", ) @router.put("/{provider_id}", response_model=ProviderUpdateResponse) async def update_service_provider( provider_id: int, data: ProviderUpdateIn, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """ Meglévő szolgáltató (Organization) adatainak szerkesztése. Frissíti a szervezet alapadatait (név, cím) és a kapcsolódó ServiceProfile kapcsolati mezőit (telefon, email, weboldal, címkék). 4-Level Category Hybrid Save (2026-06-17): - category_ids: A kiválasztott kategória ID-k (Szint 0-3 checkboxokból) -> ServiceExpertise kapcsolatok frissítése - new_tags: A user által gépelt új címkenevek -> Létrehozás ExpertiseTag-ban (is_official=False, level=3) -> ServiceExpertise kapcsolat létrehozása - tags: Meglévő specialization_tags frissítése """ try: result = await update_provider( db=db, provider_id=provider_id, data=data, user_id=current_user.id, ) return result except ValueError as e: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(e), ) except PermissionError as e: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=str(e), ) except Exception as e: logger.error(f"Hiba a szolgáltató frissítése során (id={provider_id}): {e}", exc_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Hiba történt a szolgáltató frissítése során.", )