céges meghívó kezelése,

This commit is contained in:
Roo
2026-06-17 22:07:55 +00:00
parent bf3a971ff1
commit 127b130401
28 changed files with 5806 additions and 1313 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,23 @@
"""
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
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
@@ -20,6 +30,8 @@ from app.schemas.provider import (
ProviderUpdateIn,
ProviderUpdateResponse,
ExpertiseCategoryOut,
CategoryTreeNode,
CategoryAutocompleteItem,
)
from app.services.provider_service import search_providers, quick_add_provider, update_provider
@@ -27,6 +39,114 @@ 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),
@@ -34,9 +154,10 @@ async def list_expertise_categories(
):
"""
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.id)
stmt = select(ExpertiseTag).order_by(ExpertiseTag.level, ExpertiseTag.id)
result = await db.execute(stmt)
tags = result.scalars().all()
return [
@@ -46,6 +167,9 @@ async def list_expertise_categories(
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
]
@@ -62,6 +186,7 @@ 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),
@@ -76,17 +201,35 @@ async def search_service_providers(
- **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(
@@ -143,6 +286,14 @@ async def update_service_provider(
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(
@@ -157,6 +308,11 @@ async def update_service_provider(
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(