use axum::Json; use axum::Router; use axum::extract::{Path, State}; use axum::routing::{get, post, put}; use holzleitner_application::dto::{ SetDeliveryOrderRequest, SetDeliveryOrderResponse, SyncTourRequest, TourDetails, TourSummary, }; use serde::Serialize; use uuid::Uuid; use crate::error::ApiError; use crate::extractors::AuthenticatedUser; use crate::state::AppState; pub fn router() -> Router { Router::new() .route("/me/tours/today", get(list_my_tours_today)) .route("/tours/{tour_id}", get(get_tour)) .route("/tours/{tour_id}/delivery-order", put(set_delivery_order)) .route("/sync/tour", post(sync_tour)) } /// Antwort-Hülle für `GET /me/tours/today`. Eigenes Struct, weil /// utoipa für `Vec` als Top-Level-Response keinen sauberen /// Schemanamen vergibt — und ein Wrapper macht die Erweiterbarkeit /// (z. B. Paginierung in Zukunft) zur Nicht-Breaking-Change. #[derive(Debug, Serialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub struct TourSummaryList { pub tours: Vec, } /// Antwort-Hülle für `POST /sync/tour`. #[derive(Debug, Serialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub struct SyncTourResponse { pub tour_id: Uuid, } /// Listet heutige Touren des angemeldeten Fahrers (Filter aus dem JWT). #[utoipa::path( get, path = "/me/tours/today", tag = "tours", responses( (status = 200, description = "Liste der heutigen Touren", body = TourSummaryList), (status = 401, description = "Authentifizierung fehlgeschlagen") ), security( ("bearer_auth" = []) ) )] pub async fn list_my_tours_today( State(state): State, AuthenticatedUser(claims): AuthenticatedUser, ) -> Result, ApiError> { tracing::debug!(personalnummer = claims.personalnummer, "list_my_tours_today"); let tours = state .list_my_tours_today .execute(claims.personalnummer) .await?; Ok(Json(TourSummaryList { tours })) } /// Lädt eine Tour mit allen Lieferungen, Positionen und referenzierten /// Stammdaten — die App nutzt das als einzigen großen Read. #[utoipa::path( get, path = "/tours/{tour_id}", tag = "tours", params( ("tour_id" = Uuid, Path, description = "Eindeutige Tour-Id (UUID)") ), responses( (status = 200, description = "Tour-Aggregat gefunden", body = TourDetails), (status = 401, description = "Authentifizierung fehlgeschlagen"), (status = 404, description = "Keine Tour mit dieser Id") ), security( ("bearer_auth" = []) ) )] pub async fn get_tour( State(state): State, AuthenticatedUser(claims): AuthenticatedUser, Path(tour_id): Path, ) -> Result, ApiError> { tracing::debug!(personalnummer = claims.personalnummer, %tour_id, "get_tour"); let details = state.get_tour.execute(tour_id).await?; Ok(Json(details)) } /// Schreibt die Sortier-Reihenfolge aller Lieferungen einer Tour neu. /// Der Client schickt die **vollständige** neue Reihenfolge; fehlende /// oder fremde Lieferungs-Ids werden mit `400 validation` abgelehnt. #[utoipa::path( put, path = "/tours/{tour_id}/delivery-order", tag = "tours", params(("tour_id" = Uuid, Path)), request_body = SetDeliveryOrderRequest, responses( (status = 200, description = "Neue Reihenfolge gespeichert", body = SetDeliveryOrderResponse), (status = 400, description = "Mengen-Mismatch oder Duplikate"), (status = 401, description = "Authentifizierung fehlgeschlagen"), (status = 404, description = "Tour nicht gefunden") ), security(("bearer_auth" = [])) )] pub async fn set_delivery_order( State(state): State, AuthenticatedUser(claims): AuthenticatedUser, Path(tour_id): Path, Json(request): Json, ) -> Result, ApiError> { tracing::info!( actor = claims.personalnummer, %tour_id, count = request.delivery_ids.len(), "set_delivery_order", ); let response = state.set_delivery_order.execute(tour_id, request).await?; Ok(Json(response)) } /// Sync-Endpoint für das ERP: legt eine Tagestour samt Lieferungen und /// Positionen idempotent an. Identität pro Tour /// `(driver_personalnummer, tour_date)`, pro Lieferung /// `(belegart_id, belegnummer)`. #[utoipa::path( post, path = "/sync/tour", tag = "sync", request_body = SyncTourRequest, responses( (status = 200, description = "Tour gespeichert", body = SyncTourResponse), (status = 400, description = "Validierungsfehler im Sync-Payload"), (status = 401, description = "Authentifizierung fehlgeschlagen") ), security( ("bearer_auth" = []) ) )] pub async fn sync_tour( State(state): State, AuthenticatedUser(claims): AuthenticatedUser, Json(request): Json, ) -> Result, ApiError> { tracing::info!( caller = claims.personalnummer, driver = request.driver_personalnummer, date = %request.tour_date, deliveries = request.deliveries.len(), "sync_tour", ); let tour_id = state.sync_tour.execute(request).await?; Ok(Json(SyncTourResponse { tour_id })) }