M1: implement operational core
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.
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
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
|
||||
],
|
||||
)
|
||||
Reference in New Issue
Block a user