update: Add Global Search on Header, Add Global Audit log for all actions.

This commit is contained in:
2026-04-19 15:41:29 +03:00
parent 4f35bef6e3
commit 6a958a8d7d
27 changed files with 2086 additions and 267 deletions

View File

163
backend/search/router.py Normal file
View 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}