feat(admin): completed-deliveries um from/to-Range erweitern
/admin/completed-deliveries akzeptiert jetzt zusätzlich zu `day` (Kurzform from=to) optionale `from`/`to` (DD-MM-YYYY, inklusive, je offen). Damit lassen sich Halb-Grenzen abfragen: `?from=` bzw. `?to=` liefern je eine Belegnummern- Menge, deren SQL-AND-Schnitt einen Zeitraum ergibt (Range-Filter über zwei unabhängige ERPframe-Filter). Response gibt statt `day` nun `from`/`to` zurück. Mindestens eine Grenze ist Pflicht (kein Full-Table-Dump). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -192,9 +192,16 @@ pub async fn delivered_belegnummern(
|
|||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct CompletedDeliveriesQuery {
|
pub struct CompletedDeliveriesQuery {
|
||||||
/// Ziel-Tag im Format `DD-MM-YYYY`. **Pflicht** — der Endpunkt liefert nur
|
/// Einzeltag `DD-MM-YYYY` — Kurzform für `from == to == day`. Hat Vorrang
|
||||||
/// Abschlüsse genau dieses (Berliner) Kalendertages.
|
/// vor `from`/`to`, wenn gesetzt.
|
||||||
pub day: String,
|
#[serde(default)]
|
||||||
|
pub day: Option<String>,
|
||||||
|
/// Untere Bereichsgrenze `DD-MM-YYYY` (inklusive). Ohne Angabe offen.
|
||||||
|
#[serde(default)]
|
||||||
|
pub from: Option<String>,
|
||||||
|
/// Obere Bereichsgrenze `DD-MM-YYYY` (inklusive). Ohne Angabe offen.
|
||||||
|
#[serde(default)]
|
||||||
|
pub to: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Eine abgeschlossene Lieferung im Tagesabruf.
|
/// Eine abgeschlossene Lieferung im Tagesabruf.
|
||||||
@ -210,30 +217,48 @@ pub struct CompletedDeliveryItem {
|
|||||||
|
|
||||||
#[derive(Debug, Serialize, ToSchema)]
|
#[derive(Debug, Serialize, ToSchema)]
|
||||||
pub struct CompletedDeliveriesResponse {
|
pub struct CompletedDeliveriesResponse {
|
||||||
/// Tag, nach dem gefiltert wurde (ISO `YYYY-MM-DD`).
|
/// Wirksame untere Grenze (ISO `YYYY-MM-DD`) oder `null` (offen).
|
||||||
pub day: String,
|
pub from: Option<String>,
|
||||||
/// Anzahl der abgeschlossenen Lieferungen an diesem Tag.
|
/// Wirksame obere Grenze (ISO `YYYY-MM-DD`) oder `null` (offen).
|
||||||
|
pub to: Option<String>,
|
||||||
|
/// Anzahl der abgeschlossenen Lieferungen im Bereich.
|
||||||
pub count: usize,
|
pub count: usize,
|
||||||
/// Die abgeschlossenen Lieferungen, aufsteigend nach Abschluss-Zeitpunkt.
|
/// Die abgeschlossenen Lieferungen, aufsteigend nach Abschluss-Zeitpunkt.
|
||||||
pub deliveries: Vec<CompletedDeliveryItem>,
|
pub deliveries: Vec<CompletedDeliveryItem>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Liefert **alle** an einem Tag abgeschlossenen (ausgelieferten) Lieferungen —
|
/// Parst einen `DD-MM-YYYY`-Tag in ein `NaiveDate` oder liefert `400`.
|
||||||
/// unabhängig vom Mail-Versand-Status. Der Tag (`day`, `DD-MM-YYYY`) ist Pflicht
|
fn parse_ddmmyyyy(s: &str) -> Result<NaiveDate, ApiError> {
|
||||||
/// und wird als Berliner Kalendertag auf den Abschluss-Zeitpunkt
|
NaiveDate::parse_from_str(s.trim(), "%d-%m-%Y").map_err(|e| {
|
||||||
/// (`completed_at`) angewandt. Pro Lieferung: Belegnummer und ein Flag, ob
|
ApiError(ApplicationError::Validation(format!(
|
||||||
/// Positionen verändert wurden (Menge reduziert/Zeile entfernt oder
|
"ungültiges Datum '{s}' (erwartet DD-MM-YYYY): {e}"
|
||||||
/// Geld-Gutschrift).
|
)))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Liefert **alle** abgeschlossenen (ausgelieferten) Lieferungen in einem
|
||||||
|
/// Datumsbereich — unabhängig vom Mail-Versand-Status. Gefiltert wird über den
|
||||||
|
/// **Berliner** Kalendertag des Abschluss-Zeitpunkts (`completed_at`).
|
||||||
|
///
|
||||||
|
/// Parameter (alle `DD-MM-YYYY`): `day` = Einzeltag (Kurzform `from=to=day`),
|
||||||
|
/// sonst `from`/`to` als **inklusive** Bereichsgrenzen (je optional/offen).
|
||||||
|
/// Mindestens einer von `day`/`from`/`to` ist erforderlich. Pro Lieferung:
|
||||||
|
/// Belegnummer + `positions_modified` (Menge reduziert/Zeile entfernt oder
|
||||||
|
/// Geld-Gutschrift). Die Halb-Grenzen-Variante ist für Range-Filter gedacht:
|
||||||
|
/// `?from=…` und `?to=…` liefern je eine Menge, deren SQL-`AND`-Schnitt den
|
||||||
|
/// Zeitraum ergibt.
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
path = "/admin/completed-deliveries",
|
path = "/admin/completed-deliveries",
|
||||||
tag = "admin",
|
tag = "admin",
|
||||||
params(
|
params(
|
||||||
("day" = String, Query, description = "Tag DD-MM-YYYY (Pflicht)")
|
("day" = Option<String>, Query, description = "Einzeltag DD-MM-YYYY (Kurzform from=to)"),
|
||||||
|
("from" = Option<String>, Query, description = "Untere Grenze DD-MM-YYYY (inklusive)"),
|
||||||
|
("to" = Option<String>, Query, description = "Obere Grenze DD-MM-YYYY (inklusive)")
|
||||||
),
|
),
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Abgeschlossene Lieferungen des Tages", body = CompletedDeliveriesResponse),
|
(status = 200, description = "Abgeschlossene Lieferungen im Bereich", body = CompletedDeliveriesResponse),
|
||||||
(status = 400, description = "Ungültiger/fehlender Tag"),
|
(status = 400, description = "Ungültiges Datum oder keine Grenze angegeben"),
|
||||||
(status = 401, description = "Admin-API-Key fehlt/ungültig")
|
(status = 401, description = "Admin-API-Key fehlt/ungültig")
|
||||||
),
|
),
|
||||||
security(("admin_api_key" = []))
|
security(("admin_api_key" = []))
|
||||||
@ -242,17 +267,31 @@ pub async fn completed_deliveries(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Query(query): Query<CompletedDeliveriesQuery>,
|
Query(query): Query<CompletedDeliveriesQuery>,
|
||||||
) -> Result<Json<CompletedDeliveriesResponse>, ApiError> {
|
) -> Result<Json<CompletedDeliveriesResponse>, ApiError> {
|
||||||
let day = NaiveDate::parse_from_str(query.day.trim(), "%d-%m-%Y").map_err(|e| {
|
// `day` ist die Kurzform und hat Vorrang; sonst freie Halb-/Vollgrenzen.
|
||||||
ApiError(ApplicationError::Validation(format!(
|
let (from, to) = match query.day.as_deref() {
|
||||||
"ungültiger Tag '{}' (erwartet DD-MM-YYYY): {e}",
|
Some(day) => {
|
||||||
query.day
|
let d = parse_ddmmyyyy(day)?;
|
||||||
)))
|
(Some(d), Some(d))
|
||||||
})?;
|
}
|
||||||
tracing::info!(?day, "admin.completed_deliveries");
|
None => (
|
||||||
let summaries = state.list_completed_deliveries.execute(day).await?;
|
query.from.as_deref().map(parse_ddmmyyyy).transpose()?,
|
||||||
tracing::info!(?day, count = summaries.len(), "admin.completed_deliveries.done");
|
query.to.as_deref().map(parse_ddmmyyyy).transpose()?,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Unbegrenzt (alles) wäre ein versehentlicher Full-Table-Dump → ablehnen.
|
||||||
|
if from.is_none() && to.is_none() {
|
||||||
|
return Err(ApiError(ApplicationError::Validation(
|
||||||
|
"mindestens einer von `day`, `from`, `to` (DD-MM-YYYY) ist erforderlich".into(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::info!(?from, ?to, "admin.completed_deliveries");
|
||||||
|
let summaries = state.list_completed_deliveries.execute(from, to).await?;
|
||||||
|
tracing::info!(?from, ?to, count = summaries.len(), "admin.completed_deliveries.done");
|
||||||
Ok(Json(CompletedDeliveriesResponse {
|
Ok(Json(CompletedDeliveriesResponse {
|
||||||
day: day.format("%Y-%m-%d").to_string(),
|
from: from.map(|d| d.format("%Y-%m-%d").to_string()),
|
||||||
|
to: to.map(|d| d.format("%Y-%m-%d").to_string()),
|
||||||
count: summaries.len(),
|
count: summaries.len(),
|
||||||
deliveries: summaries
|
deliveries: summaries
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
@ -132,16 +132,20 @@ pub trait DeliveryCompletionRepository: Send + Sync {
|
|||||||
day: Option<NaiveDate>,
|
day: Option<NaiveDate>,
|
||||||
) -> Result<Vec<String>, ApplicationError>;
|
) -> Result<Vec<String>, ApplicationError>;
|
||||||
|
|
||||||
/// Liefert alle **abgeschlossenen** (ausgelieferten) Lieferungen, deren
|
/// Liefert alle **abgeschlossenen** (ausgelieferten) Lieferungen im
|
||||||
/// Abschluss-Zeitpunkt (`completed_at`) auf den Berliner Kalendertag `day`
|
/// angegebenen Datumsbereich — gefiltert über den Berliner Kalendertag von
|
||||||
/// fällt — unabhängig vom Mail-Versand-Status. Pro Lieferung Belegnummer
|
/// `completed_at`, unabhängig vom Mail-Versand-Status. Pro Lieferung
|
||||||
/// und das `positions_modified`-Flag (siehe [`CompletedDeliverySummary`]).
|
/// Belegnummer und das `positions_modified`-Flag (siehe
|
||||||
|
/// [`CompletedDeliverySummary`]).
|
||||||
///
|
///
|
||||||
/// `completed_at` ist ein UTC-Zeitstempel; der Kalendertag wird in
|
/// `from`/`to` sind **inklusive** Grenzen; `None` lässt die jeweilige Seite
|
||||||
/// **Europe/Berlin** bestimmt. Sortierung: aufsteigend nach Abschluss-Zeit.
|
/// offen (`from == to` ⇒ genau ein Tag, `None`/`None` ⇒ alle). `completed_at`
|
||||||
async fn list_completed_deliveries_for_day(
|
/// ist ein UTC-Zeitstempel; der Kalendertag wird in **Europe/Berlin**
|
||||||
|
/// bestimmt. Sortierung: aufsteigend nach Abschluss-Zeit.
|
||||||
|
async fn list_completed_deliveries(
|
||||||
&self,
|
&self,
|
||||||
day: NaiveDate,
|
from: Option<NaiveDate>,
|
||||||
|
to: Option<NaiveDate>,
|
||||||
) -> Result<Vec<CompletedDeliverySummary>, ApplicationError>;
|
) -> Result<Vec<CompletedDeliverySummary>, ApplicationError>;
|
||||||
|
|
||||||
/// Markiert die Liefermail der angegebenen Belegnummern als **versendet**
|
/// Markiert die Liefermail der angegebenen Belegnummern als **versendet**
|
||||||
|
|||||||
@ -23,11 +23,13 @@ impl ListCompletedDeliveriesUseCase {
|
|||||||
Self { completions }
|
Self { completions }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Liefert alle am `day` (Berliner Kalendertag) abgeschlossenen Lieferungen.
|
/// Liefert alle im Bereich `[from, to]` (Berliner Kalendertag, inklusive,
|
||||||
|
/// `None` = offene Seite) abgeschlossenen Lieferungen.
|
||||||
pub async fn execute(
|
pub async fn execute(
|
||||||
&self,
|
&self,
|
||||||
day: NaiveDate,
|
from: Option<NaiveDate>,
|
||||||
|
to: Option<NaiveDate>,
|
||||||
) -> Result<Vec<CompletedDeliverySummary>, ApplicationError> {
|
) -> Result<Vec<CompletedDeliverySummary>, ApplicationError> {
|
||||||
self.completions.list_completed_deliveries_for_day(day).await
|
self.completions.list_completed_deliveries(from, to).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -473,12 +473,14 @@ impl DeliveryCompletionRepository for PgDeliveryCompletionRepository {
|
|||||||
Ok(belegnummern)
|
Ok(belegnummern)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_completed_deliveries_for_day(
|
async fn list_completed_deliveries(
|
||||||
&self,
|
&self,
|
||||||
day: NaiveDate,
|
from: Option<NaiveDate>,
|
||||||
|
to: Option<NaiveDate>,
|
||||||
) -> Result<Vec<CompletedDeliverySummary>, ApplicationError> {
|
) -> Result<Vec<CompletedDeliverySummary>, ApplicationError> {
|
||||||
// Alle an `day` (Berliner Kalendertag von completed_at) abgeschlossenen
|
// Alle im Bereich [from, to] (Berliner Kalendertag von completed_at)
|
||||||
// Lieferungen — ohne Mail-Filter. `positions_modified` ist true, wenn
|
// abgeschlossenen Lieferungen — ohne Mail-Filter. `from`/`to` inklusive,
|
||||||
|
// NULL = offene Seite. `positions_modified` ist true, wenn
|
||||||
// (a) irgendeine Belegzeile eine Stück-Gutschrift trägt
|
// (a) irgendeine Belegzeile eine Stück-Gutschrift trägt
|
||||||
// (credited_quantity > 0 ⇒ Zeile ganz entfernt oder Menge reduziert),
|
// (credited_quantity > 0 ⇒ Zeile ganz entfernt oder Menge reduziert),
|
||||||
// ODER
|
// ODER
|
||||||
@ -506,11 +508,15 @@ impl DeliveryCompletionRepository for PgDeliveryCompletionRepository {
|
|||||||
) AS positions_modified
|
) AS positions_modified
|
||||||
FROM deliveries d
|
FROM deliveries d
|
||||||
JOIN delivery_completions c ON c.delivery_id = d.id
|
JOIN delivery_completions c ON c.delivery_id = d.id
|
||||||
WHERE (c.completed_at AT TIME ZONE 'Europe/Berlin')::date = $1
|
WHERE ( $1::date IS NULL
|
||||||
|
OR (c.completed_at AT TIME ZONE 'Europe/Berlin')::date >= $1 )
|
||||||
|
AND ( $2::date IS NULL
|
||||||
|
OR (c.completed_at AT TIME ZONE 'Europe/Berlin')::date <= $2 )
|
||||||
ORDER BY c.completed_at
|
ORDER BY c.completed_at
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(day)
|
.bind(from)
|
||||||
|
.bind(to)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(db)?;
|
.map_err(db)?;
|
||||||
|
|||||||
Reference in New Issue
Block a user