from __future__ import annotations from datetime import UTC, date, datetime from typing import Literal from fastapi import APIRouter, Depends from sqlalchemy import select from sqlalchemy.orm import Session from app.api.deps import get_current_user, get_db from app.core.config import get_settings from app.models.booking import Booking from app.models.customer import Customer from app.models.data_quality import DataQualityIssue from app.models.outbox import OutboxEvent from app.models.vehicle import Vehicle from app.schemas import ( AttentionItem, AutomationRunOut, CurrentUser, DashboardOut, TodayItem, ) from app.services.operations import compute_metrics router = APIRouter(prefix="/api/v1/dashboard", tags=["dashboard"]) settings = get_settings() _SEVERITY_ORDER = {"high": 0, "medium": 1, "low": 2} def _today() -> date: # Seeded dates are shifted to the real reset moment by `seed_loader.py`'s anchor # shift, so "today" must be real wall-clock time, not the frozen `demo_today` setting. return datetime.now(UTC).date() @router.get("", response_model=DashboardOut) def get_dashboard( db: Session = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ) -> DashboardOut: metrics = compute_metrics(db) vehicles_by_id = {v.id: v for v in db.scalars(select(Vehicle)).all()} customers_by_id = {c.id: c for c in db.scalars(select(Customer)).all()} issues = db.scalars( select(DataQualityIssue) .where(DataQualityIssue.status == "open") .order_by(DataQualityIssue.detected_at.asc()) ).all() attention_items = [] for issue in issues: entity: Vehicle | Customer | None link_type: Literal["vehicle", "customer"] if issue.entity_type == "vehicle": entity = vehicles_by_id.get(issue.entity_id) link_type = "vehicle" else: entity = customers_by_id.get(issue.entity_id) link_type = "customer" link_ref = entity.public_ref if entity else "" title = f"{issue.rule_type.replace('_', ' ').title()} — {link_ref}" attention_items.append( AttentionItem( kind="quality_issue", severity=issue.severity, title=title, detail=issue.evidence_json.get("summary", ""), link_type=link_type, link_ref=link_ref, issue_ref=issue.public_ref, ) ) attention_items.sort(key=lambda item: _SEVERITY_ORDER.get(item.severity, 3)) attention_items = attention_items[:8] today = _today() bookings = db.scalars(select(Booking)).all() today_items: list[TodayItem] = [] for b in bookings: vehicle = vehicles_by_id.get(b.vehicle_id) vehicle_ref = vehicle.public_ref if vehicle else "" if b.starts_at.date() == today and b.status in ("reserved", "active"): today_items.append( TodayItem( kind="departure", booking_ref=b.public_ref, vehicle_ref=vehicle_ref, scheduled_at=b.starts_at, ) ) if b.ends_at.date() == today and b.status in ("active", "returned"): today_items.append( TodayItem( kind="return", booking_ref=b.public_ref, vehicle_ref=vehicle_ref, scheduled_at=b.ends_at, ) ) today_items.sort(key=lambda item: item.scheduled_at) recent = db.scalars( select(OutboxEvent).order_by(OutboxEvent.occurred_at.desc()).limit(5) ).all() recent_automation = [ AutomationRunOut( event_id=str(r.event_id), event_type=r.event_type, aggregate_ref=r.payload_json.get("aggregate_ref", ""), status=r.delivery_status, attempts=r.attempts, last_error=r.last_error, occurred_at=r.occurred_at, ) for r in recent ] return DashboardOut( metrics=metrics, attention_items=attention_items, today=today_items, recent_automation=recent_automation, )