Demo auth, seed import/reset, dashboard, vehicle/booking list+detail, audit trail. Backend: 19 tests passing, ruff clean. Frontend: React Router shell, typed API client, responsive pages. Verified end-to-end via curl and browser.
165 lines
5.5 KiB
Python
165 lines
5.5 KiB
Python
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from sqlalchemy import 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.customer import Customer
|
|
from app.models.data_quality import DataQualityIssue
|
|
from app.models.inspection import Inspection
|
|
from app.models.maintenance import MaintenanceRecord
|
|
from app.models.vehicle import Vehicle
|
|
from app.schemas import (
|
|
BookingSummaryOut,
|
|
CurrentUser,
|
|
DataQualityIssueOut,
|
|
InspectionOut,
|
|
MaintenanceOut,
|
|
VehicleDetailOut,
|
|
VehicleOut,
|
|
)
|
|
|
|
router = APIRouter(prefix="/api/v1/vehicles", tags=["vehicles"])
|
|
|
|
|
|
def _attention_vehicle_ids(db: Session) -> set:
|
|
rows = db.scalars(
|
|
select(DataQualityIssue.entity_id).where(
|
|
DataQualityIssue.entity_type == "vehicle",
|
|
DataQualityIssue.status == "open",
|
|
)
|
|
).all()
|
|
return set(rows)
|
|
|
|
|
|
@router.get("", response_model=list[VehicleOut])
|
|
def list_vehicles(
|
|
status: str | None = Query(default=None),
|
|
attention_only: bool = Query(default=False),
|
|
db: Session = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
) -> list[VehicleOut]:
|
|
stmt = select(Vehicle).order_by(Vehicle.public_ref)
|
|
if status:
|
|
stmt = stmt.where(Vehicle.operational_status == status)
|
|
vehicles = db.scalars(stmt).all()
|
|
attention_ids = _attention_vehicle_ids(db)
|
|
out = [
|
|
VehicleOut(
|
|
public_ref=v.public_ref,
|
|
make=v.make,
|
|
model=v.model,
|
|
model_year=v.model_year,
|
|
registration_number=v.registration_number,
|
|
location=v.location,
|
|
operational_status=v.operational_status,
|
|
odometer_km=v.odometer_km,
|
|
next_service_km=v.next_service_km,
|
|
active=v.active,
|
|
attention=v.id in attention_ids or v.operational_status == "blocked",
|
|
)
|
|
for v in vehicles
|
|
]
|
|
if attention_only:
|
|
out = [v for v in out if v.attention]
|
|
return out
|
|
|
|
|
|
@router.get("/{public_ref}", response_model=VehicleDetailOut)
|
|
def get_vehicle(
|
|
public_ref: str,
|
|
db: Session = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
) -> VehicleDetailOut:
|
|
vehicle = db.scalar(select(Vehicle).where(Vehicle.public_ref == public_ref))
|
|
if vehicle is None:
|
|
raise HTTPException(status_code=404, detail="Vehicle not found")
|
|
|
|
bookings = db.scalars(
|
|
select(Booking).where(Booking.vehicle_id == vehicle.id).order_by(Booking.starts_at.desc())
|
|
).all()
|
|
customer_ref_by_id = {c.id: c.public_ref for c in db.scalars(select(Customer)).all()}
|
|
inspections = db.scalars(
|
|
select(Inspection)
|
|
.where(Inspection.vehicle_id == vehicle.id)
|
|
.order_by(Inspection.completed_at.desc())
|
|
).all()
|
|
maintenance = db.scalars(
|
|
select(MaintenanceRecord)
|
|
.where(MaintenanceRecord.vehicle_id == vehicle.id)
|
|
.order_by(MaintenanceRecord.occurred_at.desc())
|
|
).all()
|
|
issues = db.scalars(
|
|
select(DataQualityIssue)
|
|
.where(DataQualityIssue.entity_type == "vehicle", DataQualityIssue.entity_id == vehicle.id)
|
|
.order_by(DataQualityIssue.detected_at.desc())
|
|
).all()
|
|
|
|
booking_by_id = {b.id: b.public_ref for b in bookings}
|
|
|
|
attention_ids = _attention_vehicle_ids(db)
|
|
return VehicleDetailOut(
|
|
public_ref=vehicle.public_ref,
|
|
make=vehicle.make,
|
|
model=vehicle.model,
|
|
model_year=vehicle.model_year,
|
|
registration_number=vehicle.registration_number,
|
|
location=vehicle.location,
|
|
operational_status=vehicle.operational_status,
|
|
odometer_km=vehicle.odometer_km,
|
|
next_service_km=vehicle.next_service_km,
|
|
active=vehicle.active,
|
|
attention=vehicle.id in attention_ids or vehicle.operational_status == "blocked",
|
|
bookings=[
|
|
BookingSummaryOut(
|
|
public_ref=b.public_ref,
|
|
customer_ref=customer_ref_by_id.get(b.customer_id, ""),
|
|
vehicle_ref=vehicle.public_ref,
|
|
starts_at=b.starts_at,
|
|
ends_at=b.ends_at,
|
|
status=b.status,
|
|
)
|
|
for b in bookings
|
|
],
|
|
inspections=[
|
|
InspectionOut(
|
|
public_ref=i.public_ref,
|
|
booking_ref=booking_by_id.get(i.booking_id, ""),
|
|
type=i.type,
|
|
fuel_level_percent=i.fuel_level_percent,
|
|
cleanliness_ok=i.cleanliness_ok,
|
|
damage_reported=i.damage_reported,
|
|
technical_warning=i.technical_warning,
|
|
odometer_km=i.odometer_km,
|
|
completed_at=i.completed_at,
|
|
)
|
|
for i in inspections
|
|
],
|
|
maintenance=[
|
|
MaintenanceOut(
|
|
public_ref=m.public_ref,
|
|
occurred_at=m.occurred_at,
|
|
odometer_km=m.odometer_km,
|
|
category=m.category,
|
|
summary=m.summary,
|
|
)
|
|
for m in maintenance
|
|
],
|
|
quality_issues=[
|
|
DataQualityIssueOut(
|
|
public_ref=q.public_ref,
|
|
rule_type=q.rule_type,
|
|
entity_type=q.entity_type,
|
|
entity_ref=vehicle.public_ref,
|
|
severity=q.severity,
|
|
status=q.status,
|
|
evidence=q.evidence_json,
|
|
detected_at=q.detected_at,
|
|
resolved_at=q.resolved_at,
|
|
)
|
|
for q in issues
|
|
],
|
|
)
|