properly implement trip init & copying trips

This commit is contained in:
2023-09-11 19:39:16 +02:00
parent 5adf39c9e3
commit 008076f6df
6 changed files with 169 additions and 14 deletions

View File

@@ -1,6 +1,12 @@
#!/usr/bin/env bash
if [[ -n "$1" ]] ; then
export DATABASE_URL="sqlite://${1}"
else
db="$(mktemp)" db="$(mktemp)"
export DATABASE_URL="sqlite://${db}" export DATABASE_URL="sqlite://${db}"
fi
cargo sqlx database create cargo sqlx database create
cargo sqlx migrate run cargo sqlx migrate run

View File

@@ -532,7 +532,7 @@ impl Trip {
#[tracing::instrument] #[tracing::instrument]
pub async fn all(ctx: &Context, pool: &sqlite::Pool) -> Result<Vec<Trip>, Error> { pub async fn all(ctx: &Context, pool: &sqlite::Pool) -> Result<Vec<Trip>, Error> {
let user_id = ctx.user.id.to_string(); let user_id = ctx.user.id.to_string();
crate::query_all!( let mut trips = crate::query_all!(
&sqlite::QueryClassification { &sqlite::QueryClassification {
query_type: sqlite::QueryType::Select, query_type: sqlite::QueryType::Select,
component: sqlite::Component::Trips, component: sqlite::Component::Trips,
@@ -554,7 +554,10 @@ impl Trip {
WHERE user_id = ?", WHERE user_id = ?",
user_id user_id
) )
.await .await?;
trips.sort_by_key(|trip| trip.date_start);
Ok(trips)
} }
#[tracing::instrument] #[tracing::instrument]
@@ -842,6 +845,7 @@ impl Trip {
name: &str, name: &str,
date_start: time::Date, date_start: time::Date,
date_end: time::Date, date_end: time::Date,
copy_from: Option<Uuid>,
) -> Result<Uuid, Error> { ) -> Result<Uuid, Error> {
let user_id = ctx.user.id.to_string(); let user_id = ctx.user.id.to_string();
let id = Uuid::new_v4(); let id = Uuid::new_v4();
@@ -851,12 +855,14 @@ impl Trip {
let trip_state = TripState::new(); let trip_state = TripState::new();
let mut transaction = pool.begin().await?;
crate::execute!( crate::execute!(
&sqlite::QueryClassification { &sqlite::QueryClassification {
query_type: sqlite::QueryType::Insert, query_type: sqlite::QueryType::Insert,
component: sqlite::Component::Trips, component: sqlite::Component::Trips,
}, },
pool, &mut *transaction,
"INSERT INTO trips "INSERT INTO trips
(id, name, date_start, date_end, state, user_id) (id, name, date_start, date_end, state, user_id)
VALUES VALUES
@@ -870,6 +876,70 @@ impl Trip {
) )
.await?; .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) Ok(id)
} }
@@ -1019,7 +1089,7 @@ impl Trip {
// //
// * if the trip is new (it's state is INITIAL), we can just forward // * if the trip is new (it's state is INITIAL), we can just forward
// as-is // 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 // visible so the user knows that there might be new items to
// consider // consider
let user_id = ctx.user.id.to_string(); let user_id = ctx.user.id.to_string();

View File

@@ -6,8 +6,10 @@ use axum::{
routing::{get, post}, routing::{get, post},
BoxError, Router, BoxError, Router,
}; };
use serde::de;
use uuid::Uuid;
use std::time::Duration; use std::{fmt, time::Duration};
use tower::{timeout::TimeoutLayer, ServiceBuilder}; use tower::{timeout::TimeoutLayer, ServiceBuilder};
use crate::{AppState, Error, RequestError, TopLevelPage}; 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<Option<Uuid>, D::Error>
where
D: serde::Deserializer<'de>,
{
struct NoneVisitor;
impl<'vi> de::Visitor<'vi> for NoneVisitor {
type Value = Option<Uuid>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "invalid input")
}
fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
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] #[tracing::instrument]
pub fn router(state: AppState) -> Router { pub fn router(state: AppState) -> Router {
Router::new() Router::new()

View File

@@ -18,11 +18,13 @@ use crate::{AppState, Context, Error, RequestError, TopLevelPage};
use super::{get_referer, html}; use super::{get_referer, html};
#[derive(Deserialize, Default, Debug)] #[derive(Deserialize, Default, Debug)]
#[serde(deny_unknown_fields)]
pub struct InventoryQuery { pub struct InventoryQuery {
edit_item: Option<Uuid>, edit_item: Option<Uuid>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct NewItem { pub struct NewItem {
#[serde(rename = "new-item-name")] #[serde(rename = "new-item-name")]
name: String, name: String,
@@ -35,12 +37,14 @@ pub struct NewItem {
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct NewItemName { pub struct NewItemName {
#[serde(rename = "new-item-name")] #[serde(rename = "new-item-name")]
name: String, name: String,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct EditItem { pub struct EditItem {
#[serde(rename = "edit-item-name")] #[serde(rename = "edit-item-name")]
name: String, name: String,
@@ -49,6 +53,7 @@ pub struct EditItem {
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct NewTrip { pub struct NewTrip {
#[serde(rename = "new-trip-name")] #[serde(rename = "new-trip-name")]
name: String, name: String,
@@ -56,44 +61,56 @@ pub struct NewTrip {
date_start: time::Date, date_start: time::Date,
#[serde(rename = "new-trip-end-date")] #[serde(rename = "new-trip-end-date")]
date_end: time::Date, date_end: time::Date,
#[serde(
rename = "new-trip-copy-from",
deserialize_with = "super::uuid_or_empty"
)]
copy_from: Option<Uuid>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TripQuery { pub struct TripQuery {
edit: Option<models::trips::TripAttribute>, edit: Option<models::trips::TripAttribute>,
category: Option<Uuid>, category: Option<Uuid>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct CommentUpdate { pub struct CommentUpdate {
#[serde(rename = "new-comment")] #[serde(rename = "new-comment")]
new_comment: String, new_comment: String,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct TripUpdate { pub struct TripUpdate {
#[serde(rename = "new-value")] #[serde(rename = "new-value")]
new_value: String, new_value: String,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct NewCategory { pub struct NewCategory {
#[serde(rename = "new-category-name")] #[serde(rename = "new-category-name")]
name: String, name: String,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TripTypeQuery { pub struct TripTypeQuery {
edit: Option<Uuid>, edit: Option<Uuid>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct NewTripType { pub struct NewTripType {
#[serde(rename = "new-trip-type-name")] #[serde(rename = "new-trip-type-name")]
name: String, name: String,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct TripTypeUpdate { pub struct TripTypeUpdate {
#[serde(rename = "new-value")] #[serde(rename = "new-value")]
new_value: String, new_value: String,
@@ -367,6 +384,7 @@ pub async fn trip_create(
&new_trip.name, &new_trip.name,
new_trip.date_start, new_trip.date_start,
new_trip.date_end, new_trip.date_end,
new_trip.copy_from,
) )
.await?; .await?;

View File

@@ -64,6 +64,7 @@ impl<'a> Component for HeaderLink<'a> {
hx-get=(self.args.item.path()) hx-get=(self.args.item.path())
hx-target={ "#" (self.htmx.target().html_id()) } hx-target={ "#" (self.htmx.target().html_id()) }
hx-swap="outerHtml" hx-swap="outerHtml"
hx-push-url="true"
#{"header-link-" (self.args.item.id())} #{"header-link-" (self.args.item.id())}
."px-5" ."px-5"
."flex" ."flex"

View File

@@ -22,8 +22,8 @@ impl TripManager {
."gap-8" ."gap-8"
{ {
h1 ."text-2xl" {"Trips"} h1 ."text-2xl" {"Trips"}
(TripTable::build(trips)) (TripTable::build(&trips))
(NewTrip::build()) (NewTrip::build(&trips))
} }
) )
} }
@@ -62,7 +62,7 @@ pub struct TripTable;
impl TripTable { impl TripTable {
#[tracing::instrument] #[tracing::instrument]
pub fn build(trips: Vec<models::trips::Trip>) -> Markup { pub fn build(trips: &Vec<models::trips::Trip>) -> Markup {
html!( html!(
table table
."table" ."table"
@@ -125,8 +125,8 @@ impl TripTableRow {
pub struct NewTrip; pub struct NewTrip;
impl NewTrip { impl NewTrip {
#[tracing::instrument] #[tracing::instrument(skip(trips))]
pub fn build() -> Markup { pub fn build(trips: &Vec<models::trips::Trip>) -> Markup {
html!( html!(
form form
name="new_trip" name="new_trip"
@@ -161,7 +161,7 @@ impl NewTrip {
} }
div ."mx-auto" ."pb-8" { div ."mx-auto" ."pb-8" {
div ."flex" ."flex-row" ."justify-center" { 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" { span ."w-1/2" {
input input
type="date" type="date"
@@ -183,7 +183,7 @@ impl NewTrip {
} }
div ."mx-auto" ."pb-8" { div ."mx-auto" ."pb-8" {
div ."flex" ."flex-row" ."justify-center" { 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" { span ."w-1/2" {
input input
type="date" 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 input
type="submit" type="submit"
value="Add" value="Add"