feat(tours): GET /me/tours mit optionalem ?date= (statt /me/tours/today)
Fahrer können jetzt die Tour eines beliebigen Datums abrufen (z. B. morgen vorab). Ohne date-Param bleibt serverseitig 'heute' (Local::now bzw. Dev-today_override); Praezedenz: requested_date > today_override > Uhr. Endpoint umbenannt /me/tours/today -> /me/tours (semantisch sauberer). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -460,7 +460,7 @@ pub struct GsdConfig {
|
|||||||
#[derive(Debug, Clone, Deserialize, Default)]
|
#[derive(Debug, Clone, Deserialize, Default)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct DevConfig {
|
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
|
/// Als **quotierter** String `"YYYY-MM-DD"` angeben — ein bloßes
|
||||||
/// `2026-06-01` würde TOML als nativen Datums-Typ lesen, den chrono
|
/// `2026-06-01` würde TOML als nativen Datums-Typ lesen, den chrono
|
||||||
/// nicht direkt akzeptiert. `None`/weglassen = echte Uhr. Nur zum
|
/// nicht direkt akzeptiert. `None`/weglassen = echte Uhr. Nur zum
|
||||||
|
|||||||
@ -22,7 +22,7 @@ use utoipa::openapi::security::{
|
|||||||
paths(
|
paths(
|
||||||
crate::routes::health::health,
|
crate::routes::health::health,
|
||||||
crate::routes::accounts::get_account,
|
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::get_tour,
|
||||||
crate::routes::tours::set_delivery_order,
|
crate::routes::tours::set_delivery_order,
|
||||||
crate::routes::tours::sync_tour,
|
crate::routes::tours::sync_tour,
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
use axum::Json;
|
use axum::Json;
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use axum::extract::{Path, State};
|
use axum::extract::{Path, Query, State};
|
||||||
use axum::routing::{get, post, put};
|
use axum::routing::{get, post, put};
|
||||||
|
use chrono::NaiveDate;
|
||||||
use holzleitner_application::dto::{
|
use holzleitner_application::dto::{
|
||||||
SetDeliveryOrderRequest, SetDeliveryOrderResponse, SyncTourRequest, TourDetails, TourSummary,
|
SetDeliveryOrderRequest, SetDeliveryOrderResponse, SyncTourRequest, TourDetails, TourSummary,
|
||||||
};
|
};
|
||||||
use serde::Serialize;
|
use holzleitner_application::error::ApplicationError;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::error::ApiError;
|
use crate::error::ApiError;
|
||||||
@ -14,7 +16,7 @@ use crate::state::AppState;
|
|||||||
|
|
||||||
pub fn router() -> Router<AppState> {
|
pub fn router() -> Router<AppState> {
|
||||||
Router::new()
|
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}", get(get_tour))
|
||||||
.route("/tours/{tour_id}/delivery-order", put(set_delivery_order))
|
.route("/tours/{tour_id}/delivery-order", put(set_delivery_order))
|
||||||
.route("/sync/tour", post(sync_tour))
|
.route("/sync/tour", post(sync_tour))
|
||||||
@ -37,27 +39,52 @@ pub struct SyncTourResponse {
|
|||||||
pub tour_id: Uuid,
|
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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
path = "/me/tours/today",
|
path = "/me/tours",
|
||||||
tag = "tours",
|
tag = "tours",
|
||||||
|
params(
|
||||||
|
("date" = Option<String>, Query, description = "Ziel-Tourdatum YYYY-MM-DD (Default: heute)")
|
||||||
|
),
|
||||||
responses(
|
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")
|
(status = 401, description = "Authentifizierung fehlgeschlagen")
|
||||||
),
|
),
|
||||||
security(
|
security(
|
||||||
("bearer_auth" = [])
|
("bearer_auth" = [])
|
||||||
)
|
)
|
||||||
)]
|
)]
|
||||||
pub async fn list_my_tours_today(
|
pub async fn list_my_tours(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
AuthenticatedUser(claims): AuthenticatedUser,
|
AuthenticatedUser(claims): AuthenticatedUser,
|
||||||
|
Query(query): Query<ListToursQuery>,
|
||||||
) -> Result<Json<TourSummaryList>, ApiError> {
|
) -> Result<Json<TourSummaryList>, 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
|
let tours = state
|
||||||
.list_my_tours_today
|
.list_my_tours_today
|
||||||
.execute(claims.personalnummer)
|
.execute(claims.personalnummer, date)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(TourSummaryList { tours }))
|
Ok(Json(TourSummaryList { tours }))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
//! Lieferpositionen. Die App nutzt diese Liste für die
|
||||||
//! Fahrzeug/Tour-Auswahl-Page und lädt erst nach der Auswahl die
|
//! Fahrzeug/Tour-Auswahl-Page und lädt erst nach der Auswahl die
|
||||||
//! vollständige Tour über `GET /tours/{id}`.
|
//! vollständige Tour über `GET /tours/{id}`.
|
||||||
|
|||||||
@ -6,9 +6,15 @@ use crate::dto::TourSummary;
|
|||||||
use crate::error::ApplicationError;
|
use crate::error::ApplicationError;
|
||||||
use crate::ports::TourRepository;
|
use crate::ports::TourRepository;
|
||||||
|
|
||||||
/// Liste der heutigen Touren des angemeldeten Fahrers. Das "heute"
|
/// Liste der Touren des angemeldeten Fahrers für ein Ziel-Datum.
|
||||||
/// liegt **bewusst im Backend**: die App-Uhr ist nicht autoritativ
|
///
|
||||||
/// (Zeitzone, Falsch-Stand, Manipulation).
|
/// 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
|
/// `today_override` ist eine **DEV-ONLY**-Hintertür zum Testen mit
|
||||||
/// historischen/importierten Touren: ist sie gesetzt, wird statt der echten
|
/// historischen/importierten Touren: ist sie gesetzt, wird statt der echten
|
||||||
@ -26,17 +32,21 @@ impl ListMyToursTodayUseCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn execute(&self, personalnummer: i64) -> Result<Vec<TourSummary>, ApplicationError> {
|
pub async fn execute(
|
||||||
|
&self,
|
||||||
|
personalnummer: i64,
|
||||||
|
requested_date: Option<NaiveDate>,
|
||||||
|
) -> Result<Vec<TourSummary>, ApplicationError> {
|
||||||
// Bewusst Local statt Utc: der Tagesumschlag muss sich nach der
|
// Bewusst Local statt Utc: der Tagesumschlag muss sich nach der
|
||||||
// Ortszeit des Fahrers richten. Mit Utc wechselt "heute" erst um
|
// Ortszeit des Fahrers richten. Mit Utc wechselt "heute" erst um
|
||||||
// 02:00 Ortszeit (CEST = UTC+2) — bis dahin sähe der Fahrer nach
|
// 02:00 Ortszeit (CEST = UTC+2) — bis dahin sähe der Fahrer nach
|
||||||
// Mitternacht noch die Touren von gestern. Der Server läuft in
|
// Mitternacht noch die Touren von gestern. Der Server läuft in
|
||||||
// Europe/Vienna, daher ist Local hier korrekt.
|
// Europe/Vienna, daher ist Local hier korrekt.
|
||||||
let today = self
|
let date = requested_date
|
||||||
.today_override
|
.or(self.today_override)
|
||||||
.unwrap_or_else(|| Local::now().date_naive());
|
.unwrap_or_else(|| Local::now().date_naive());
|
||||||
self.repository
|
self.repository
|
||||||
.find_today_for_driver(personalnummer, today)
|
.find_today_for_driver(personalnummer, date)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user