properly implement trip init & copying trips
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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?;
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user