From 008076f6df75b4756d4d2e9f0e65297dbb0ea979 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Mon, 11 Sep 2023 19:39:16 +0200 Subject: [PATCH] properly implement trip init & copying trips --- rust/rebuild-schema-data.sh | 10 ++++- rust/src/models/trips.rs | 78 +++++++++++++++++++++++++++++++++++-- rust/src/routing/mod.rs | 31 ++++++++++++++- rust/src/routing/routes.rs | 18 +++++++++ rust/src/view/root.rs | 1 + rust/src/view/trip/mod.rs | 45 +++++++++++++++++---- 6 files changed, 169 insertions(+), 14 deletions(-) diff --git a/rust/rebuild-schema-data.sh b/rust/rebuild-schema-data.sh index cc8bbac..09eea6f 100755 --- a/rust/rebuild-schema-data.sh +++ b/rust/rebuild-schema-data.sh @@ -1,6 +1,12 @@ -db="$(mktemp)" +#!/usr/bin/env bash -export DATABASE_URL="sqlite://${db}" +if [[ -n "$1" ]] ; then + export DATABASE_URL="sqlite://${1}" +else + db="$(mktemp)" + + export DATABASE_URL="sqlite://${db}" +fi cargo sqlx database create cargo sqlx migrate run diff --git a/rust/src/models/trips.rs b/rust/src/models/trips.rs index be42251..5f8eb3c 100644 --- a/rust/src/models/trips.rs +++ b/rust/src/models/trips.rs @@ -532,7 +532,7 @@ impl Trip { #[tracing::instrument] pub async fn all(ctx: &Context, pool: &sqlite::Pool) -> Result, Error> { let user_id = ctx.user.id.to_string(); - crate::query_all!( + let mut trips = crate::query_all!( &sqlite::QueryClassification { query_type: sqlite::QueryType::Select, component: sqlite::Component::Trips, @@ -554,7 +554,10 @@ impl Trip { WHERE user_id = ?", user_id ) - .await + .await?; + + trips.sort_by_key(|trip| trip.date_start); + Ok(trips) } #[tracing::instrument] @@ -842,6 +845,7 @@ impl Trip { name: &str, date_start: time::Date, date_end: time::Date, + copy_from: Option, ) -> Result { let user_id = ctx.user.id.to_string(); let id = Uuid::new_v4(); @@ -851,12 +855,14 @@ impl Trip { let trip_state = TripState::new(); + let mut transaction = pool.begin().await?; + crate::execute!( &sqlite::QueryClassification { query_type: sqlite::QueryType::Insert, component: sqlite::Component::Trips, }, - pool, + &mut *transaction, "INSERT INTO trips (id, name, date_start, date_end, state, user_id) VALUES @@ -870,6 +876,70 @@ impl Trip { ) .await?; + if let Some(copy_from_trip_id) = copy_from { + let copy_from_trip_id_param = copy_from_trip_id.to_string(); + crate::execute!( + &sqlite::QueryClassification { + query_type: sqlite::QueryType::Insert, + component: sqlite::Component::Trips, + }, + &mut *transaction, + r#"INSERT INTO trips_items ( + item_id, + trip_id, + pick, + pack, + ready, + new, + user_id + ) SELECT + item_id, + $1 as trip_id, + pick, + false as pack, + false as ready, + false as new, + user_id + FROM trips_items + WHERE trip_id = $2 AND user_id = $3"#, + id_param, + copy_from_trip_id_param, + user_id, + ) + .await?; + } else { + crate::execute!( + &sqlite::QueryClassification { + query_type: sqlite::QueryType::Insert, + component: sqlite::Component::Trips, + }, + &mut *transaction, + r#"INSERT INTO trips_items ( + item_id, + trip_id, + pick, + pack, + ready, + new, + user_id + ) SELECT + id as item_id, + $1 as trip_id, + false as pick, + false as pack, + false as ready, + false as new, + user_id + FROM inventory_items + WHERE user_id = $2"#, + id_param, + user_id, + ) + .await?; + } + + transaction.commit().await?; + Ok(id) } @@ -1019,7 +1089,7 @@ impl Trip { // // * if the trip is new (it's state is INITIAL), we can just forward // as-is - // * if the trip is new, we have to make these new items prominently + // * if the trip is not new, we have to make these new items prominently // visible so the user knows that there might be new items to // consider let user_id = ctx.user.id.to_string(); diff --git a/rust/src/routing/mod.rs b/rust/src/routing/mod.rs index ed9ac23..ea6ead2 100644 --- a/rust/src/routing/mod.rs +++ b/rust/src/routing/mod.rs @@ -6,8 +6,10 @@ use axum::{ routing::{get, post}, BoxError, Router, }; +use serde::de; +use uuid::Uuid; -use std::time::Duration; +use std::{fmt, time::Duration}; use tower::{timeout::TimeoutLayer, ServiceBuilder}; use crate::{AppState, Error, RequestError, TopLevelPage}; @@ -31,6 +33,33 @@ fn get_referer(headers: &HeaderMap) -> Result<&str, Error> { }) } +fn uuid_or_empty<'de, D>(input: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + struct NoneVisitor; + + impl<'vi> de::Visitor<'vi> for NoneVisitor { + type Value = Option; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "invalid input") + } + + fn visit_str(self, value: &str) -> Result { + if value.is_empty() { + Ok(None) + } else { + Ok(Some(Uuid::try_from(value).map_err(|e| { + E::custom(format!("UUID parsing failed: {}", e)) + })?)) + } + } + } + + input.deserialize_str(NoneVisitor) +} + #[tracing::instrument] pub fn router(state: AppState) -> Router { Router::new() diff --git a/rust/src/routing/routes.rs b/rust/src/routing/routes.rs index 8aea033..c83f291 100644 --- a/rust/src/routing/routes.rs +++ b/rust/src/routing/routes.rs @@ -18,11 +18,13 @@ use crate::{AppState, Context, Error, RequestError, TopLevelPage}; use super::{get_referer, html}; #[derive(Deserialize, Default, Debug)] +#[serde(deny_unknown_fields)] pub struct InventoryQuery { edit_item: Option, } #[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields)] pub struct NewItem { #[serde(rename = "new-item-name")] name: String, @@ -35,12 +37,14 @@ pub struct NewItem { } #[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields)] pub struct NewItemName { #[serde(rename = "new-item-name")] name: String, } #[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields)] pub struct EditItem { #[serde(rename = "edit-item-name")] name: String, @@ -49,6 +53,7 @@ pub struct EditItem { } #[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields)] pub struct NewTrip { #[serde(rename = "new-trip-name")] name: String, @@ -56,44 +61,56 @@ pub struct NewTrip { date_start: time::Date, #[serde(rename = "new-trip-end-date")] date_end: time::Date, + #[serde( + rename = "new-trip-copy-from", + deserialize_with = "super::uuid_or_empty" + )] + copy_from: Option, } #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] pub struct TripQuery { edit: Option, category: Option, } #[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields)] pub struct CommentUpdate { #[serde(rename = "new-comment")] new_comment: String, } #[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields)] pub struct TripUpdate { #[serde(rename = "new-value")] new_value: String, } #[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields)] pub struct NewCategory { #[serde(rename = "new-category-name")] name: String, } #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] pub struct TripTypeQuery { edit: Option, } #[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields)] pub struct NewTripType { #[serde(rename = "new-trip-type-name")] name: String, } #[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields)] pub struct TripTypeUpdate { #[serde(rename = "new-value")] new_value: String, @@ -367,6 +384,7 @@ pub async fn trip_create( &new_trip.name, new_trip.date_start, new_trip.date_end, + new_trip.copy_from, ) .await?; diff --git a/rust/src/view/root.rs b/rust/src/view/root.rs index 6c55f65..038579a 100644 --- a/rust/src/view/root.rs +++ b/rust/src/view/root.rs @@ -64,6 +64,7 @@ impl<'a> Component for HeaderLink<'a> { hx-get=(self.args.item.path()) hx-target={ "#" (self.htmx.target().html_id()) } hx-swap="outerHtml" + hx-push-url="true" #{"header-link-" (self.args.item.id())} ."px-5" ."flex" diff --git a/rust/src/view/trip/mod.rs b/rust/src/view/trip/mod.rs index c79f8ff..110fa83 100644 --- a/rust/src/view/trip/mod.rs +++ b/rust/src/view/trip/mod.rs @@ -22,8 +22,8 @@ impl TripManager { ."gap-8" { h1 ."text-2xl" {"Trips"} - (TripTable::build(trips)) - (NewTrip::build()) + (TripTable::build(&trips)) + (NewTrip::build(&trips)) } ) } @@ -62,7 +62,7 @@ pub struct TripTable; impl TripTable { #[tracing::instrument] - pub fn build(trips: Vec) -> Markup { + pub fn build(trips: &Vec) -> Markup { html!( table ."table" @@ -125,8 +125,8 @@ impl TripTableRow { pub struct NewTrip; impl NewTrip { - #[tracing::instrument] - pub fn build() -> Markup { + #[tracing::instrument(skip(trips))] + pub fn build(trips: &Vec) -> Markup { html!( form name="new_trip" @@ -161,7 +161,7 @@ impl NewTrip { } div ."mx-auto" ."pb-8" { div ."flex" ."flex-row" ."justify-center" { - label for="trip-name" ."font-bold" ."w-1/2" ."p-2" ."text-center" { "Start date" } + label for="start-date" ."font-bold" ."w-1/2" ."p-2" ."text-center" { "Start date" } span ."w-1/2" { input type="date" @@ -183,7 +183,7 @@ impl NewTrip { } div ."mx-auto" ."pb-8" { div ."flex" ."flex-row" ."justify-center" { - label for="trip-name" ."font-bold" ."w-1/2" ."p-2" ."text-center" { "Start date" } + label for="end-date" ."font-bold" ."w-1/2" ."p-2" ."text-center" { "End date" } span ."w-1/2" { input type="date" @@ -203,6 +203,37 @@ impl NewTrip { } } } + div ."mx-auto" ."pb-8" { + div ."flex" ."flex-row" ."justify-center" { + label for="copy-from" ."font-bold" ."w-1/2" ."p-2" ."text-center" { "Reuse trip" } + span ."w-1/2" { + select + id="copy-from" + name="new-trip-copy-from" + ."block" + ."w-full" + ."p-2" + ."bg-gray-50" + ."border-2" + ."border-gray-300" + ."focus:outline-none" + ."focus:bg-white" + ."focus:border-purple-500" + { + option value="" { "[None]" } + @for trip in trips.iter().rev() { + option value=(trip.id) { + (format!("{year}-{month:02} {name}", + year = trip.date_start.year(), + month = trip.date_start.month() as u8, + name = trip.name + )) + } + } + } + } + } + } input type="submit" value="Add"