from __future__ import annotations from fastapi import APIRouter, Depends, Query from sqlalchemy import or_, select from sqlalchemy.orm import Session from app.api.deps import get_current_user, get_db from app.models.booking import Booking from app.models.data_quality import DataQualityIssue from app.models.vehicle import Vehicle from app.schemas import CurrentUser, SearchResponse, SearchResultItem router = APIRouter(prefix="/api/v1/search", tags=["search"]) # Static application sections. `id` is a stable code matching navigation.json's # `items.*` keys -- the frontend localizes both the section label and its one-line # detail from `id`, so no English prose is sent over the wire (search.sections. in # every locale; see docs/fleet-ops-correction/i18n-inventory.md). Manager-only sections # are filtered by role, mirroring the same nav visibility rule Layout.tsx applies -- # search must never surface a destination the current role can't actually reach. _SECTIONS: list[dict] = [ { "id": "overview", "link": "/dashboard", # Search terms deliberately span all three supported UI languages (not just # English) so a query never depends on the operator's selected locale. "terms": ["overview", "dashboard", "readiness", "overzicht", "aperçu", "tableau de bord"], }, { "id": "fleet", "link": "/vehicles", "terms": ["fleet", "vehicle", "vehicles", "wagenpark", "voertuig", "flotte", "véhicule"], }, { "id": "bookings", "link": "/bookings", "terms": [ "booking", "bookings", "rental", "boeking", "boekingen", "verhuur", "réservation", "réservations", "location", ], }, { "id": "quality", "link": "/data-quality", "terms": [ "quality", "data quality", "issues", "kwaliteit", "datakwaliteit", "problemen", "qualité", "problèmes", ], "role": "operations_manager", }, { "id": "knowledge", "link": "/knowledge", "terms": ["knowledge", "procedures", "kennis", "procedures", "connaissances", "procédures"], }, { "id": "integrations", "link": "/automation", "terms": [ "automation", "integrations", "systems", "n8n", "automatisering", "integraties", "systemen", "automatisation", "intégrations", "systèmes", ], "role": "operations_manager", }, { "id": "audit", "link": "/audit", "terms": ["audit", "history", "geschiedenis", "historique"], "role": "operations_manager", }, { "id": "privacy", "link": "/privacy", "terms": ["privacy", "retention", "anonymise", "anonimiseren", "confidentialité"], "role": "operations_manager", }, ] @router.get("", response_model=SearchResponse) def search( q: str = Query(min_length=1, max_length=100), db: Session = Depends(get_db), user: CurrentUser = Depends(get_current_user), ) -> SearchResponse: query = q.strip() normalized = query.lower() results: list[SearchResultItem] = [] for section in _SECTIONS: role = section.get("role") if role and user.role != role: continue terms: list[str] = section["terms"] if any(term in normalized or normalized in term for term in terms): results.append( SearchResultItem( type="section", label=section["id"], detail_code=section["id"], link=section["link"], ) ) like = f"%{query}%" for v in db.scalars( select(Vehicle) .where( or_( Vehicle.public_ref.ilike(like), Vehicle.make.ilike(like), Vehicle.model.ilike(like), Vehicle.registration_number.ilike(like), Vehicle.location.ilike(like), ) ) .order_by(Vehicle.public_ref) .limit(5) ).all(): results.append( SearchResultItem( type="vehicle", label=v.public_ref, detail_code="vehicleSummary", detail_params={"make": v.make, "model": v.model, "location": v.location}, link=f"/vehicles/{v.public_ref}", ) ) for b in db.scalars( select(Booking) .where(Booking.public_ref.ilike(like)) .order_by(Booking.starts_at.desc()) .limit(5) ).all(): results.append( SearchResultItem( type="booking", label=b.public_ref, detail_code=b.status, link=f"/bookings/{b.public_ref}", ) ) # No customer detail route exists in this proof of concept, so customers are # deliberately never returned here -- there is nowhere useful to send the user. if user.role == "operations_manager": for i in db.scalars( select(DataQualityIssue) .where(DataQualityIssue.public_ref.ilike(like)) .order_by(DataQualityIssue.detected_at.desc()) .limit(5) ).all(): results.append( SearchResultItem( type="data_quality_issue", label=i.public_ref, detail_code=i.rule_type, link=f"/data-quality/{i.public_ref}", ) ) return SearchResponse(query=query, results=results[:10])