diff --git a/crates/api/src/config.rs b/crates/api/src/config.rs index d10886b..f37355b 100644 --- a/crates/api/src/config.rs +++ b/crates/api/src/config.rs @@ -460,7 +460,7 @@ pub struct GsdConfig { #[derive(Debug, Clone, Deserialize, Default)] #[serde(deny_unknown_fields)] pub struct DevConfig { - /// Überschreibt das serverseitige „heute" für `GET /me/tours/today`. + /// Überschreibt das serverseitige „heute" für `GET /me/tours`. /// Als **quotierter** String `"YYYY-MM-DD"` angeben — ein bloßes /// `2026-06-01` würde TOML als nativen Datums-Typ lesen, den chrono /// nicht direkt akzeptiert. `None`/weglassen = echte Uhr. Nur zum diff --git a/crates/api/src/openapi.rs b/crates/api/src/openapi.rs index 5991da9..13b6953 100644 --- a/crates/api/src/openapi.rs +++ b/crates/api/src/openapi.rs @@ -22,7 +22,7 @@ use utoipa::openapi::security::{ paths( crate::routes::health::health, crate::routes::accounts::get_account, - crate::routes::tours::list_my_tours_today, + crate::routes::tours::list_my_tours, crate::routes::tours::get_tour, crate::routes::tours::set_delivery_order, crate::routes::tours::sync_tour, diff --git a/crates/api/src/routes/tours.rs b/crates/api/src/routes/tours.rs index f95c771..635f5a8 100644 --- a/crates/api/src/routes/tours.rs +++ b/crates/api/src/routes/tours.rs @@ -1,11 +1,13 @@ use axum::Json; use axum::Router; -use axum::extract::{Path, State}; +use axum::extract::{Path, Query, State}; use axum::routing::{get, post, put}; +use chrono::NaiveDate; use holzleitner_application::dto::{ SetDeliveryOrderRequest, SetDeliveryOrderResponse, SyncTourRequest, TourDetails, TourSummary, }; -use serde::Serialize; +use holzleitner_application::error::ApplicationError; +use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::error::ApiError; @@ -14,7 +16,7 @@ use crate::state::AppState; pub fn router() -> Router { Router::new() - .route("/me/tours/today", get(list_my_tours_today)) + .route("/me/tours", get(list_my_tours)) .route("/tours/{tour_id}", get(get_tour)) .route("/tours/{tour_id}/delivery-order", put(set_delivery_order)) .route("/sync/tour", post(sync_tour)) @@ -37,27 +39,52 @@ pub struct SyncTourResponse { pub tour_id: Uuid, } -/// Listet heutige Touren des angemeldeten Fahrers (Filter aus dem JWT). +/// Query für `GET /me/tours`. Optionales Ziel-Datum; fehlt es, gilt +/// serverseitig „heute" (siehe `ListMyToursTodayUseCase`). +#[derive(Debug, Deserialize)] +pub struct ListToursQuery { + /// Ziel-Tourdatum `YYYY-MM-DD`. Fehlt der Parameter, wird **heute** + /// (Serverzeit bzw. Dev-Override) verwendet. + #[serde(default)] + pub date: Option, +} + +/// Listet die Touren des angemeldeten Fahrers (Filter aus dem JWT) für ein +/// Ziel-Datum. Ohne `?date=` gilt „heute". Der Fahrer wählt das Datum aktiv +/// im Kopf-Kalender der App (z. B. um die Tour von morgen vorab zu sehen). #[utoipa::path( get, - path = "/me/tours/today", + path = "/me/tours", tag = "tours", + params( + ("date" = Option, Query, description = "Ziel-Tourdatum YYYY-MM-DD (Default: heute)") + ), responses( - (status = 200, description = "Liste der heutigen Touren", body = TourSummaryList), + (status = 200, description = "Liste der Touren am Ziel-Datum", body = TourSummaryList), + (status = 400, description = "Ungültiges Datum"), (status = 401, description = "Authentifizierung fehlgeschlagen") ), security( ("bearer_auth" = []) ) )] -pub async fn list_my_tours_today( +pub async fn list_my_tours( State(state): State, AuthenticatedUser(claims): AuthenticatedUser, + Query(query): Query, ) -> Result, ApiError> { - tracing::debug!(personalnummer = claims.personalnummer, "list_my_tours_today"); + let date = match query.date { + Some(s) => Some(NaiveDate::parse_from_str(s.trim(), "%Y-%m-%d").map_err(|e| { + ApiError(ApplicationError::Validation(format!( + "ungültiges Datum '{s}' (erwartet YYYY-MM-DD): {e}" + ))) + })?), + None => None, + }; + tracing::debug!(personalnummer = claims.personalnummer, ?date, "list_my_tours"); let tours = state .list_my_tours_today - .execute(claims.personalnummer) + .execute(claims.personalnummer, date) .await?; Ok(Json(TourSummaryList { tours })) } diff --git a/crates/application/src/dto/tour_summary.rs b/crates/application/src/dto/tour_summary.rs index b59f933..eb37225 100644 --- a/crates/application/src/dto/tour_summary.rs +++ b/crates/application/src/dto/tour_summary.rs @@ -1,4 +1,4 @@ -//! Liste-Antwort für `GET /me/tours/today` — leichtgewichtig, ohne +//! Liste-Antwort für `GET /me/tours` — leichtgewichtig, ohne //! Lieferpositionen. Die App nutzt diese Liste für die //! Fahrzeug/Tour-Auswahl-Page und lädt erst nach der Auswahl die //! vollständige Tour über `GET /tours/{id}`. diff --git a/crates/application/src/usecases/list_my_tours_today.rs b/crates/application/src/usecases/list_my_tours_today.rs index 4999383..9ae8d14 100644 --- a/crates/application/src/usecases/list_my_tours_today.rs +++ b/crates/application/src/usecases/list_my_tours_today.rs @@ -6,9 +6,15 @@ use crate::dto::TourSummary; use crate::error::ApplicationError; use crate::ports::TourRepository; -/// Liste der heutigen Touren des angemeldeten Fahrers. Das "heute" -/// liegt **bewusst im Backend**: die App-Uhr ist nicht autoritativ -/// (Zeitzone, Falsch-Stand, Manipulation). +/// Liste der Touren des angemeldeten Fahrers für ein Ziel-Datum. +/// +/// Ohne explizites Datum gilt "heute" — und das liegt **bewusst im +/// Backend**: die App-Uhr ist nicht autoritativ (Zeitzone, Falsch-Stand, +/// Manipulation). Übergibt die App ein Datum (Fahrer wählt es aktiv im +/// Kopf-Kalender, z. B. um die Tour von morgen vorab zu sehen), gewinnt +/// dieses vor der Uhr. +/// +/// Präzedenz: **`requested_date` > `today_override` > echte Uhr**. /// /// `today_override` ist eine **DEV-ONLY**-Hintertür zum Testen mit /// historischen/importierten Touren: ist sie gesetzt, wird statt der echten @@ -26,17 +32,21 @@ impl ListMyToursTodayUseCase { } } - pub async fn execute(&self, personalnummer: i64) -> Result, ApplicationError> { + pub async fn execute( + &self, + personalnummer: i64, + requested_date: Option, + ) -> Result, ApplicationError> { // Bewusst Local statt Utc: der Tagesumschlag muss sich nach der // Ortszeit des Fahrers richten. Mit Utc wechselt "heute" erst um // 02:00 Ortszeit (CEST = UTC+2) — bis dahin sähe der Fahrer nach // Mitternacht noch die Touren von gestern. Der Server läuft in // Europe/Vienna, daher ist Local hier korrekt. - let today = self - .today_override + let date = requested_date + .or(self.today_override) .unwrap_or_else(|| Local::now().date_naive()); self.repository - .find_today_for_driver(personalnummer, today) + .find_today_for_driver(personalnummer, date) .await } }