164 lines
5.1 KiB
Python
164 lines
5.1 KiB
Python
import asyncio
|
|
import json
|
|
from fastapi import APIRouter, Depends, Query
|
|
from auth.dependencies import get_current_user
|
|
from auth.models import TokenPayload
|
|
from devices import service as devices_service
|
|
from users import service as users_service
|
|
from crm import service as crm_service
|
|
from melodies import service as melodies_service
|
|
|
|
router = APIRouter(prefix="/api/search", tags=["search"])
|
|
|
|
LIMIT = 5
|
|
|
|
|
|
def _truncate(s: str, n: int = 48) -> str:
|
|
if not s:
|
|
return ""
|
|
return s if len(s) <= n else s[:n - 1] + "…"
|
|
|
|
|
|
def _search_devices(q: str) -> list[dict]:
|
|
try:
|
|
results = devices_service.list_devices(search=q)
|
|
except Exception:
|
|
return []
|
|
out = []
|
|
for d in results[:LIMIT]:
|
|
label = d.device_name or d.serial_number or d.device_id or d.id
|
|
sublabel = d.serial_number if d.device_name else None
|
|
out.append({
|
|
"type": "device",
|
|
"id": d.id,
|
|
"label": _truncate(label),
|
|
"sublabel": _truncate(sublabel) if sublabel else None,
|
|
"url": f"/devices/{d.id}",
|
|
})
|
|
return out
|
|
|
|
|
|
def _search_users(q: str) -> list[dict]:
|
|
try:
|
|
results = users_service.list_users(search=q)
|
|
except Exception:
|
|
return []
|
|
out = []
|
|
for u in results[:LIMIT]:
|
|
label = u.display_name or u.email or u.id
|
|
sublabel = u.email if u.display_name else None
|
|
out.append({
|
|
"type": "user",
|
|
"id": u.id,
|
|
"label": _truncate(label),
|
|
"sublabel": _truncate(sublabel) if sublabel else None,
|
|
"url": f"/users/{u.id}",
|
|
})
|
|
return out
|
|
|
|
|
|
def _search_customers(q: str) -> list[dict]:
|
|
try:
|
|
results = crm_service.list_customers(search=q)
|
|
except Exception:
|
|
return []
|
|
out = []
|
|
for c in results[:LIMIT]:
|
|
name_parts = [c.name, c.surname]
|
|
label = " ".join(p for p in name_parts if p) or c.organization or c.id
|
|
sublabel_parts = []
|
|
if c.organization and (c.name or c.surname):
|
|
sublabel_parts.append(c.organization)
|
|
if c.location:
|
|
if c.location.city:
|
|
sublabel_parts.append(c.location.city)
|
|
if c.location.country:
|
|
sublabel_parts.append(c.location.country)
|
|
out.append({
|
|
"type": "customer",
|
|
"id": c.id,
|
|
"label": _truncate(label),
|
|
"sublabel": _truncate(" · ".join(sublabel_parts)) if sublabel_parts else None,
|
|
"url": f"/crm/customers/{c.id}",
|
|
})
|
|
return out
|
|
|
|
|
|
def _search_products(q: str) -> list[dict]:
|
|
try:
|
|
results = crm_service.list_products(search=q)
|
|
except Exception:
|
|
return []
|
|
out = []
|
|
for p in results[:LIMIT]:
|
|
sublabel_parts = []
|
|
if p.category:
|
|
sublabel_parts.append(p.category.value.replace("_", " ").title())
|
|
if p.sku:
|
|
sublabel_parts.append(p.sku)
|
|
out.append({
|
|
"type": "product",
|
|
"id": p.id,
|
|
"label": _truncate(p.name or p.id),
|
|
"sublabel": _truncate(" · ".join(sublabel_parts)) if sublabel_parts else None,
|
|
"url": f"/crm/products/{p.id}",
|
|
})
|
|
return out
|
|
|
|
|
|
async def _search_melodies(q: str) -> list[dict]:
|
|
try:
|
|
results = await melodies_service.list_melodies(search=q)
|
|
except Exception:
|
|
return []
|
|
out = []
|
|
for m in results[:LIMIT]:
|
|
try:
|
|
name_dict = json.loads(m.information.name) if m.information.name else {}
|
|
label = name_dict.get("en") or name_dict.get("gr") or next(iter(name_dict.values()), None) or m.id
|
|
except Exception:
|
|
label = m.information.name or m.id
|
|
sublabel_parts = []
|
|
if m.pid:
|
|
sublabel_parts.append(m.pid)
|
|
if m.information.melodyTone:
|
|
sublabel_parts.append(m.information.melodyTone.value.title())
|
|
if m.information.totalActiveBells:
|
|
sublabel_parts.append(f"{m.information.totalActiveBells} bells")
|
|
out.append({
|
|
"type": "melody",
|
|
"id": m.id,
|
|
"label": _truncate(label),
|
|
"sublabel": _truncate(" · ".join(sublabel_parts)) if sublabel_parts else None,
|
|
"url": f"/melodies/{m.id}",
|
|
})
|
|
return out
|
|
|
|
|
|
@router.get("")
|
|
async def global_search(
|
|
q: str = Query(..., min_length=1, max_length=100),
|
|
_user: TokenPayload = Depends(get_current_user),
|
|
):
|
|
q = q.strip()
|
|
if not q:
|
|
return {"results": []}
|
|
|
|
# Run sync searches in a thread pool, melody search is already async
|
|
loop = asyncio.get_event_loop()
|
|
devices_fut = loop.run_in_executor(None, _search_devices, q)
|
|
users_fut = loop.run_in_executor(None, _search_users, q)
|
|
customers_fut = loop.run_in_executor(None, _search_customers, q)
|
|
products_fut = loop.run_in_executor(None, _search_products, q)
|
|
melodies_task = _search_melodies(q)
|
|
|
|
devices, users, customers, products, melodies = await asyncio.gather(
|
|
devices_fut, users_fut, customers_fut, products_fut, melodies_task
|
|
)
|
|
|
|
results = []
|
|
for group in (devices, users, customers, products, melodies):
|
|
results.extend(group)
|
|
|
|
return {"results": results}
|