update: Add Global Search on Header, Add Global Audit log for all actions.
This commit is contained in:
163
backend/search/router.py
Normal file
163
backend/search/router.py
Normal file
@@ -0,0 +1,163 @@
|
||||
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}
|
||||
Reference in New Issue
Block a user