more refactors

This commit is contained in:
2023-08-29 21:34:00 +02:00
parent ea40064cef
commit 7c6fe7b1b7
8 changed files with 1517 additions and 1474 deletions

View File

@@ -42,7 +42,7 @@ struct Args {
pub struct ClientState { pub struct ClientState {
pub active_category_id: Option<Uuid>, pub active_category_id: Option<Uuid>,
pub edit_item: Option<Uuid>, pub edit_item: Option<Uuid>,
pub trip_edit_attribute: Option<models::TripAttribute>, pub trip_edit_attribute: Option<models::trips::TripAttribute>,
pub trip_type_edit: Option<Uuid>, pub trip_type_edit: Option<Uuid>,
} }
@@ -279,9 +279,9 @@ async fn inventory_active(
state.client_state.edit_item = inventory_query.edit_item; state.client_state.edit_item = inventory_query.edit_item;
state.client_state.active_category_id = Some(id); state.client_state.active_category_id = Some(id);
let inventory = models::Inventory::load(&state.database_pool).await?; let inventory = models::inventory::Inventory::load(&state.database_pool).await?;
let active_category: Option<&models::Category> = state let active_category: Option<&models::inventory::Category> = state
.client_state .client_state
.active_category_id .active_category_id
.map(|id| { .map(|id| {
@@ -312,7 +312,7 @@ async fn inventory_inactive(
state.client_state.edit_item = inventory_query.edit_item; state.client_state.edit_item = inventory_query.edit_item;
state.client_state.active_category_id = None; state.client_state.active_category_id = None;
let inventory = models::Inventory::load(&state.database_pool).await?; let inventory = models::inventory::Inventory::load(&state.database_pool).await?;
Ok(view::Root::build( Ok(view::Root::build(
&view::inventory::Inventory::build( &view::inventory::Inventory::build(
@@ -346,7 +346,8 @@ async fn inventory_item_validate_name(
State(state): State<AppState>, State(state): State<AppState>,
Form(new_item): Form<NewItemName>, Form(new_item): Form<NewItemName>,
) -> Result<impl IntoResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let exists = models::InventoryItem::name_exists(&state.database_pool, &new_item.name).await?; let exists =
models::inventory::InventoryItem::name_exists(&state.database_pool, &new_item.name).await?;
Ok(view::inventory::InventoryNewItemFormName::build( Ok(view::inventory::InventoryNewItemFormName::build(
Some(&new_item.name), Some(&new_item.name),
@@ -365,7 +366,7 @@ async fn inventory_item_create(
})); }));
} }
let _new_id = models::InventoryItem::save( let _new_id = models::inventory::InventoryItem::save(
&state.database_pool, &state.database_pool,
&new_item.name, &new_item.name,
new_item.category_id, new_item.category_id,
@@ -374,11 +375,11 @@ async fn inventory_item_create(
.await?; .await?;
if is_htmx(&headers) { if is_htmx(&headers) {
let inventory = models::Inventory::load(&state.database_pool).await?; let inventory = models::inventory::Inventory::load(&state.database_pool).await?;
// it's impossible to NOT find the item here, as we literally just added // it's impossible to NOT find the item here, as we literally just added
// it. // it.
let active_category: Option<&models::Category> = Some( let active_category: Option<&models::inventory::Category> = Some(
inventory inventory
.categories .categories
.iter() .iter()
@@ -418,7 +419,7 @@ async fn inventory_item_delete(
headers: HeaderMap, headers: HeaderMap,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
) -> Result<Redirect, Error> { ) -> Result<Redirect, Error> {
let deleted = models::InventoryItem::delete(&state.database_pool, id).await?; let deleted = models::inventory::InventoryItem::delete(&state.database_pool, id).await?;
if !deleted { if !deleted {
Err(Error::Request(RequestError::NotFound { Err(Error::Request(RequestError::NotFound {
@@ -448,8 +449,13 @@ async fn inventory_item_edit(
})); }));
} }
let id = let id = models::inventory::InventoryItem::update(
models::Item::update(&state.database_pool, id, &edit_item.name, edit_item.weight).await?; &state.database_pool,
id,
&edit_item.name,
edit_item.weight,
)
.await?;
Ok(Redirect::to(&format!("/inventory/category/{id}/", id = id))) Ok(Redirect::to(&format!("/inventory/category/{id}/", id = id)))
} }
@@ -458,7 +464,7 @@ async fn inventory_item_cancel(
State(state): State<AppState>, State(state): State<AppState>,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
) -> Result<Redirect, Error> { ) -> Result<Redirect, Error> {
let id = models::Item::find(&state.database_pool, id) let id = models::inventory::InventoryItem::find(&state.database_pool, id)
.await? .await?
.ok_or(Error::Request(RequestError::NotFound { .ok_or(Error::Request(RequestError::NotFound {
message: format!("item with id {id} not found"), message: format!("item with id {id} not found"),
@@ -466,7 +472,7 @@ async fn inventory_item_cancel(
Ok(Redirect::to(&format!( Ok(Redirect::to(&format!(
"/inventory/category/{id}/", "/inventory/category/{id}/",
id = id.category_id id = id.category.id
))) )))
} }
@@ -490,7 +496,7 @@ async fn trip_create(
})); }));
} }
let new_id = models::Trip::save( let new_id = models::trips::Trip::save(
&state.database_pool, &state.database_pool,
&new_trip.name, &new_trip.name,
new_trip.date_start, new_trip.date_start,
@@ -502,7 +508,7 @@ async fn trip_create(
} }
async fn trips(State(state): State<AppState>) -> Result<impl IntoResponse, Error> { async fn trips(State(state): State<AppState>) -> Result<impl IntoResponse, Error> {
let trips = models::Trip::all(&state.database_pool).await?; let trips = models::trips::Trip::all(&state.database_pool).await?;
Ok(view::Root::build( Ok(view::Root::build(
&view::trip::TripManager::build(trips), &view::trip::TripManager::build(trips),
@@ -512,7 +518,7 @@ async fn trips(State(state): State<AppState>) -> Result<impl IntoResponse, Error
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct TripQuery { struct TripQuery {
edit: Option<models::TripAttribute>, edit: Option<models::trips::TripAttribute>,
category: Option<Uuid>, category: Option<Uuid>,
} }
@@ -524,12 +530,11 @@ async fn trip(
state.client_state.trip_edit_attribute = trip_query.edit; state.client_state.trip_edit_attribute = trip_query.edit;
state.client_state.active_category_id = trip_query.category; state.client_state.active_category_id = trip_query.category;
let mut trip: models::Trip = let mut trip: models::trips::Trip = models::trips::Trip::find(&state.database_pool, id)
models::Trip::find(&state.database_pool, id) .await?
.await? .ok_or(Error::Request(RequestError::NotFound {
.ok_or(Error::Request(RequestError::NotFound { message: format!("trip with id {id} not found"),
message: format!("trip with id {id} not found"), }))?;
}))?;
trip.load_trips_types(&state.database_pool).await?; trip.load_trips_types(&state.database_pool).await?;
@@ -538,7 +543,7 @@ async fn trip(
trip.load_categories(&state.database_pool).await?; trip.load_categories(&state.database_pool).await?;
let active_category: Option<&models::TripCategory> = state let active_category: Option<&models::trips::TripCategory> = state
.client_state .client_state
.active_category_id .active_category_id
.map(|id| { .map(|id| {
@@ -565,7 +570,8 @@ async fn trip_type_remove(
State(state): State<AppState>, State(state): State<AppState>,
Path((trip_id, type_id)): Path<(Uuid, Uuid)>, Path((trip_id, type_id)): Path<(Uuid, Uuid)>,
) -> Result<Redirect, Error> { ) -> Result<Redirect, Error> {
let found = models::Trip::trip_type_remove(&state.database_pool, trip_id, type_id).await?; let found =
models::trips::Trip::trip_type_remove(&state.database_pool, trip_id, type_id).await?;
if !found { if !found {
Err(Error::Request(RequestError::NotFound { Err(Error::Request(RequestError::NotFound {
@@ -580,7 +586,7 @@ async fn trip_type_add(
State(state): State<AppState>, State(state): State<AppState>,
Path((trip_id, type_id)): Path<(Uuid, Uuid)>, Path((trip_id, type_id)): Path<(Uuid, Uuid)>,
) -> Result<Redirect, Error> { ) -> Result<Redirect, Error> {
models::Trip::trip_type_add(&state.database_pool, trip_id, type_id).await?; models::trips::Trip::trip_type_add(&state.database_pool, trip_id, type_id).await?;
Ok(Redirect::to(&format!("/trips/{trip_id}/"))) Ok(Redirect::to(&format!("/trips/{trip_id}/")))
} }
@@ -596,9 +602,12 @@ async fn trip_comment_set(
Path(trip_id): Path<Uuid>, Path(trip_id): Path<Uuid>,
Form(comment_update): Form<CommentUpdate>, Form(comment_update): Form<CommentUpdate>,
) -> Result<Redirect, Error> { ) -> Result<Redirect, Error> {
let found = let found = models::trips::Trip::set_comment(
models::Trip::set_comment(&state.database_pool, trip_id, &comment_update.new_comment) &state.database_pool,
.await?; trip_id,
&comment_update.new_comment,
)
.await?;
if !found { if !found {
Err(Error::Request(RequestError::NotFound { Err(Error::Request(RequestError::NotFound {
@@ -617,17 +626,17 @@ struct TripUpdate {
async fn trip_edit_attribute( async fn trip_edit_attribute(
State(state): State<AppState>, State(state): State<AppState>,
Path((trip_id, attribute)): Path<(Uuid, models::TripAttribute)>, Path((trip_id, attribute)): Path<(Uuid, models::trips::TripAttribute)>,
Form(trip_update): Form<TripUpdate>, Form(trip_update): Form<TripUpdate>,
) -> Result<Redirect, Error> { ) -> Result<Redirect, Error> {
if attribute == models::TripAttribute::Name { if attribute == models::trips::TripAttribute::Name {
if trip_update.new_value.is_empty() { if trip_update.new_value.is_empty() {
return Err(Error::Request(RequestError::EmptyFormElement { return Err(Error::Request(RequestError::EmptyFormElement {
name: "name".to_string(), name: "name".to_string(),
})); }));
} }
} }
models::Trip::set_attribute( models::trips::Trip::set_attribute(
&state.database_pool, &state.database_pool,
trip_id, trip_id,
attribute, attribute,
@@ -642,10 +651,10 @@ async fn trip_item_set_state(
state: &AppState, state: &AppState,
trip_id: Uuid, trip_id: Uuid,
item_id: Uuid, item_id: Uuid,
key: models::TripItemStateKey, key: models::trips::TripItemStateKey,
value: bool, value: bool,
) -> Result<(), Error> { ) -> Result<(), Error> {
models::TripItem::set_state(&state.database_pool, trip_id, item_id, key, value).await?; models::trips::TripItem::set_state(&state.database_pool, trip_id, item_id, key, value).await?;
Ok(()) Ok(())
} }
@@ -654,7 +663,7 @@ async fn trip_row(
trip_id: Uuid, trip_id: Uuid,
item_id: Uuid, item_id: Uuid,
) -> Result<impl IntoResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let item = models::TripItem::find(&state.database_pool, trip_id, item_id) let item = models::trips::TripItem::find(&state.database_pool, trip_id, item_id)
.await? .await?
.ok_or_else(|| { .ok_or_else(|| {
Error::Request(RequestError::NotFound { Error::Request(RequestError::NotFound {
@@ -665,16 +674,21 @@ async fn trip_row(
let item_row = view::trip::TripItemListRow::build( let item_row = view::trip::TripItemListRow::build(
trip_id, trip_id,
&item, &item,
models::Item::get_category_max_weight(&state.database_pool, item.item.category_id).await?, models::inventory::Item::get_category_max_weight(
&state.database_pool,
item.item.category_id,
)
.await?,
); );
let category = models::TripCategory::find(&state.database_pool, trip_id, item.item.category_id) let category =
.await? models::trips::TripCategory::find(&state.database_pool, trip_id, item.item.category_id)
.ok_or_else(|| { .await?
Error::Request(RequestError::NotFound { .ok_or_else(|| {
message: format!("category with id {} not found", item.item.category_id), Error::Request(RequestError::NotFound {
}) message: format!("category with id {} not found", item.item.category_id),
})?; })
})?;
// TODO biggest_category_weight? // TODO biggest_category_weight?
let category_row = view::trip::TripCategoryListRow::build(trip_id, &category, true, 0, true); let category_row = view::trip::TripCategoryListRow::build(trip_id, &category, true, 0, true);
@@ -692,7 +706,7 @@ async fn trip_item_set_pick(
&state, &state,
trip_id, trip_id,
item_id, item_id,
models::TripItemStateKey::Pick, models::trips::TripItemStateKey::Pick,
true, true,
) )
.await?, .await?,
@@ -708,7 +722,7 @@ async fn trip_item_set_pick_htmx(
&state, &state,
trip_id, trip_id,
item_id, item_id,
models::TripItemStateKey::Pick, models::trips::TripItemStateKey::Pick,
true, true,
) )
.await?; .await?;
@@ -730,7 +744,7 @@ async fn trip_item_set_unpick(
&state, &state,
trip_id, trip_id,
item_id, item_id,
models::TripItemStateKey::Pick, models::trips::TripItemStateKey::Pick,
false, false,
) )
.await?, .await?,
@@ -746,7 +760,7 @@ async fn trip_item_set_unpick_htmx(
&state, &state,
trip_id, trip_id,
item_id, item_id,
models::TripItemStateKey::Pick, models::trips::TripItemStateKey::Pick,
false, false,
) )
.await?; .await?;
@@ -768,7 +782,7 @@ async fn trip_item_set_pack(
&state, &state,
trip_id, trip_id,
item_id, item_id,
models::TripItemStateKey::Pack, models::trips::TripItemStateKey::Pack,
true, true,
) )
.await?, .await?,
@@ -784,7 +798,7 @@ async fn trip_item_set_pack_htmx(
&state, &state,
trip_id, trip_id,
item_id, item_id,
models::TripItemStateKey::Pack, models::trips::TripItemStateKey::Pack,
true, true,
) )
.await?; .await?;
@@ -806,7 +820,7 @@ async fn trip_item_set_unpack(
&state, &state,
trip_id, trip_id,
item_id, item_id,
models::TripItemStateKey::Pack, models::trips::TripItemStateKey::Pack,
false, false,
) )
.await?, .await?,
@@ -822,7 +836,7 @@ async fn trip_item_set_unpack_htmx(
&state, &state,
trip_id, trip_id,
item_id, item_id,
models::TripItemStateKey::Pack, models::trips::TripItemStateKey::Pack,
false, false,
) )
.await?; .await?;
@@ -839,7 +853,7 @@ async fn trip_total_weight_htmx(
Path(trip_id): Path<Uuid>, Path(trip_id): Path<Uuid>,
) -> Result<impl IntoResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let total_weight = let total_weight =
models::Trip::find_total_picked_weight(&state.database_pool, trip_id).await?; models::trips::Trip::find_total_picked_weight(&state.database_pool, trip_id).await?;
Ok(view::trip::TripInfoTotalWeightRow::build( Ok(view::trip::TripInfoTotalWeightRow::build(
trip_id, trip_id,
total_weight, total_weight,
@@ -862,7 +876,8 @@ async fn inventory_category_create(
})); }));
} }
let _new_id = models::Category::save(&state.database_pool, &new_category.name).await?; let _new_id =
models::inventory::Category::save(&state.database_pool, &new_category.name).await?;
Ok(Redirect::to("/inventory/")) Ok(Redirect::to("/inventory/"))
} }
@@ -870,9 +885,9 @@ async fn inventory_category_create(
async fn trip_state_set( async fn trip_state_set(
State(state): State<AppState>, State(state): State<AppState>,
headers: HeaderMap, headers: HeaderMap,
Path((trip_id, new_state)): Path<(Uuid, models::TripState)>, Path((trip_id, new_state)): Path<(Uuid, models::trips::TripState)>,
) -> Result<impl IntoResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let exists = models::Trip::set_state(&state.database_pool, trip_id, &new_state).await?; let exists = models::trips::Trip::set_state(&state.database_pool, trip_id, &new_state).await?;
if !exists { if !exists {
return Err(Error::Request(RequestError::NotFound { return Err(Error::Request(RequestError::NotFound {
@@ -905,7 +920,8 @@ async fn trips_types(
) -> Result<impl IntoResponse, Error> { ) -> Result<impl IntoResponse, Error> {
state.client_state.trip_type_edit = trip_type_query.edit; state.client_state.trip_type_edit = trip_type_query.edit;
let trip_types: Vec<models::TripsType> = models::TripsType::all(&state.database_pool).await?; let trip_types: Vec<models::trips::TripsType> =
models::trips::TripsType::all(&state.database_pool).await?;
Ok(view::Root::build( Ok(view::Root::build(
&view::trip::types::TypeList::build(&state.client_state, trip_types), &view::trip::types::TypeList::build(&state.client_state, trip_types),
@@ -929,7 +945,7 @@ async fn trip_type_create(
})); }));
} }
let _new_id = models::TripsType::save(&state.database_pool, &new_trip_type.name).await?; let _new_id = models::trips::TripsType::save(&state.database_pool, &new_trip_type.name).await?;
Ok(Redirect::to("/trips/types/")) Ok(Redirect::to("/trips/types/"))
} }
@@ -951,9 +967,12 @@ async fn trips_types_edit_name(
})); }));
} }
let exists = let exists = models::trips::TripsType::set_name(
models::TripsType::set_name(&state.database_pool, trip_type_id, &trip_update.new_value) &state.database_pool,
.await?; trip_type_id,
&trip_update.new_value,
)
.await?;
if !exists { if !exists {
return Err(Error::Request(RequestError::NotFound { return Err(Error::Request(RequestError::NotFound {
@@ -968,7 +987,7 @@ async fn inventory_item(
State(state): State<AppState>, State(state): State<AppState>,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let item = models::InventoryItem::find(&state.database_pool, id) let item = models::inventory::InventoryItem::find(&state.database_pool, id)
.await? .await?
.ok_or(Error::Request(RequestError::NotFound { .ok_or(Error::Request(RequestError::NotFound {
message: format!("inventory item with id {id} not found"), message: format!("inventory item with id {id} not found"),
@@ -984,7 +1003,7 @@ async fn trip_category_select(
State(state): State<AppState>, State(state): State<AppState>,
Path((trip_id, category_id)): Path<(Uuid, Uuid)>, Path((trip_id, category_id)): Path<(Uuid, Uuid)>,
) -> Result<impl IntoResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let mut trip = models::Trip::find(&state.database_pool, trip_id) let mut trip = models::trips::Trip::find(&state.database_pool, trip_id)
.await? .await?
.ok_or(Error::Request(RequestError::NotFound { .ok_or(Error::Request(RequestError::NotFound {
message: format!("trip with id {trip_id} not found"), message: format!("trip with id {trip_id} not found"),
@@ -1016,9 +1035,9 @@ async fn inventory_category_select(
State(state): State<AppState>, State(state): State<AppState>,
Path(category_id): Path<Uuid>, Path(category_id): Path<Uuid>,
) -> Result<impl IntoResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let inventory = models::Inventory::load(&state.database_pool).await?; let inventory = models::inventory::Inventory::load(&state.database_pool).await?;
let active_category: Option<&models::Category> = Some( let active_category: Option<&models::inventory::Category> = Some(
inventory inventory
.categories .categories
.iter() .iter()
@@ -1050,7 +1069,7 @@ async fn trip_packagelist(
State(state): State<AppState>, State(state): State<AppState>,
Path(trip_id): Path<Uuid>, Path(trip_id): Path<Uuid>,
) -> Result<impl IntoResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let mut trip = models::Trip::find(&state.database_pool, trip_id) let mut trip = models::trips::Trip::find(&state.database_pool, trip_id)
.await? .await?
.ok_or(Error::Request(RequestError::NotFound { .ok_or(Error::Request(RequestError::NotFound {
message: format!("trip with id {trip_id} not found"), message: format!("trip with id {trip_id} not found"),
@@ -1072,12 +1091,12 @@ async fn trip_item_packagelist_set_pack_htmx(
&state, &state,
trip_id, trip_id,
item_id, item_id,
models::TripItemStateKey::Pack, models::trips::TripItemStateKey::Pack,
true, true,
) )
.await?; .await?;
let item = models::TripItem::find(&state.database_pool, trip_id, item_id) let item = models::trips::TripItem::find(&state.database_pool, trip_id, item_id)
.await? .await?
.ok_or(Error::Request(RequestError::NotFound { .ok_or(Error::Request(RequestError::NotFound {
message: format!("an item with id {item_id} does not exist"), message: format!("an item with id {item_id} does not exist"),
@@ -1096,14 +1115,14 @@ async fn trip_item_packagelist_set_unpack_htmx(
&state, &state,
trip_id, trip_id,
item_id, item_id,
models::TripItemStateKey::Pack, models::trips::TripItemStateKey::Pack,
false, false,
) )
.await?; .await?;
// note that this cannot fail due to a missing item, as trip_item_set_state would already // note that this cannot fail due to a missing item, as trip_item_set_state would already
// return 404. but error handling cannot hurt ;) // return 404. but error handling cannot hurt ;)
let item = models::TripItem::find(&state.database_pool, trip_id, item_id) let item = models::trips::TripItem::find(&state.database_pool, trip_id, item_id)
.await? .await?
.ok_or(Error::Request(RequestError::NotFound { .ok_or(Error::Request(RequestError::NotFound {
message: format!("an item with id {item_id} does not exist"), message: format!("an item with id {item_id} does not exist"),

View File

@@ -0,0 +1,412 @@
use super::Error;
use futures::{TryFutureExt, TryStreamExt};
use uuid::Uuid;
pub struct Inventory {
pub categories: Vec<Category>,
}
impl Inventory {
pub async fn load(pool: &sqlx::Pool<sqlx::Sqlite>) -> Result<Self, Error> {
let mut categories = sqlx::query_as!(
DbCategoryRow,
"SELECT id,name,description FROM inventory_items_categories"
)
.fetch(pool)
.map_ok(|row: DbCategoryRow| row.try_into())
.try_collect::<Vec<Result<Category, Error>>>()
.await?
.into_iter()
.collect::<Result<Vec<Category>, Error>>()?;
for category in &mut categories {
category.populate_items(pool).await?;
}
Ok(Self { categories })
}
}
#[derive(Debug)]
pub struct Category {
pub id: Uuid,
pub name: String,
pub description: Option<String>,
pub items: Option<Vec<Item>>,
}
pub struct DbCategoryRow {
pub id: String,
pub name: String,
pub description: Option<String>,
}
impl TryFrom<DbCategoryRow> for Category {
type Error = Error;
fn try_from(row: DbCategoryRow) -> Result<Self, Self::Error> {
Ok(Category {
id: Uuid::try_parse(&row.id)?,
name: row.name,
description: row.description,
items: None,
})
}
}
impl Category {
pub async fn _find(
pool: &sqlx::Pool<sqlx::Sqlite>,
id: Uuid,
) -> Result<Option<Category>, Error> {
let id_param = id.to_string();
sqlx::query_as!(
DbCategoryRow,
"SELECT
id,
name,
description
FROM inventory_items_categories AS category
WHERE category.id = ?",
id_param,
)
.fetch_optional(pool)
.await?
.map(|row| row.try_into())
.transpose()
}
pub async fn save(pool: &sqlx::Pool<sqlx::Sqlite>, name: &str) -> Result<Uuid, Error> {
let id = Uuid::new_v4();
let id_param = id.to_string();
sqlx::query!(
"INSERT INTO inventory_items_categories
(id, name)
VALUES
(?, ?)",
id_param,
name,
)
.execute(pool)
.await?;
Ok(id)
}
pub fn items(&self) -> &Vec<Item> {
self.items
.as_ref()
.expect("you need to call populate_items()")
}
pub fn total_weight(&self) -> i64 {
self.items().iter().map(|item| item.weight).sum()
}
pub async fn populate_items(&mut self, pool: &sqlx::Pool<sqlx::Sqlite>) -> Result<(), Error> {
let id = self.id.to_string();
let items = sqlx::query_as!(
DbInventoryItemsRow,
"SELECT
id,
name,
weight,
description,
category_id
FROM inventory_items
WHERE category_id = ?",
id
)
.fetch(pool)
.map_ok(|row| row.try_into())
.try_collect::<Vec<Result<Item, Error>>>()
.await?
.into_iter()
.collect::<Result<Vec<Item>, Error>>()?;
self.items = Some(items);
Ok(())
}
}
pub struct Product {
pub id: Uuid,
pub name: String,
pub description: Option<String>,
pub comment: Option<String>,
}
pub struct inventoryItem {
pub id: Uuid,
pub name: String,
pub description: Option<String>,
pub weight: i64,
pub category: Category,
pub product: Option<Product>,
}
struct DbInventoryItemRow {
pub id: String,
pub name: String,
pub description: Option<String>,
pub weight: i64,
pub category_id: String,
pub category_name: String,
pub category_description: Option<String>,
pub product_id: Option<String>,
pub product_name: Option<String>,
pub product_description: Option<String>,
pub product_comment: Option<String>,
}
impl TryFrom<DbInventoryItemRow> for InventoryItem {
type Error = Error;
fn try_from(row: DbInventoryItemRow) -> Result<Self, Self::Error> {
Ok(InventoryItem {
id: Uuid::try_parse(&row.id)?,
name: row.name,
description: row.description,
weight: row.weight,
category: Category {
id: Uuid::try_parse(&row.category_id)?,
name: row.category_name,
description: row.category_description,
items: None,
},
product: row
.product_id
.map(|id| -> Result<Product, Error> {
Ok(Product {
id: Uuid::try_parse(&id)?,
name: row.product_name.unwrap(),
description: row.product_description,
comment: row.product_comment,
})
})
.transpose()?,
})
}
}
impl InventoryItem {
pub async fn find(pool: &sqlx::Pool<sqlx::Sqlite>, id: Uuid) -> Result<Option<Self>, Error> {
let id_param = id.to_string();
sqlx::query_as!(
DbInventoryItemRow,
"SELECT
item.id AS id,
item.name AS name,
item.description AS description,
weight,
category.id AS category_id,
category.name AS category_name,
category.description AS category_description,
product.id AS product_id,
product.name AS product_name,
product.description AS product_description,
product.comment AS product_comment
FROM inventory_items AS item
INNER JOIN inventory_items_categories as category
ON item.category_id = category.id
LEFT JOIN inventory_products AS product
ON item.product_id = product.id
WHERE item.id = ?",
id_param,
)
.fetch_optional(pool)
.await?
.map(|row| row.try_into())
.transpose()
}
pub async fn name_exists(pool: &sqlx::Pool<sqlx::Sqlite>, name: &str) -> Result<bool, Error> {
Ok(sqlx::query!(
"SELECT id
FROM inventory_items
WHERE name = ?",
name,
)
.fetch_optional(pool)
.await?
.map(|_row| ())
.is_some())
}
pub async fn delete(pool: &sqlx::Pool<sqlx::Sqlite>, id: Uuid) -> Result<bool, Error> {
let id_param = id.to_string();
let results = sqlx::query!(
"DELETE FROM inventory_items
WHERE id = ?",
id_param
)
.execute(pool)
.await?;
Ok(results.rows_affected() != 0)
}
pub async fn update(
pool: &sqlx::Pool<sqlx::Sqlite>,
id: Uuid,
name: &str,
weight: u32,
) -> Result<Uuid, Error> {
let weight = i64::try_from(weight).unwrap();
let id_param = id.to_string();
Ok(sqlx::query!(
"UPDATE inventory_items AS item
SET
name = ?,
weight = ?
WHERE item.id = ?
RETURNING inventory_items.category_id AS id
",
name,
weight,
id_param,
)
.fetch_one(pool)
.map_ok(|row| Uuid::try_parse(&row.id.unwrap()))
.await??)
}
pub async fn save(
pool: &sqlx::Pool<sqlx::Sqlite>,
name: &str,
category_id: Uuid,
weight: u32,
) -> Result<Uuid, Error> {
let id = Uuid::new_v4();
let id_param = id.to_string();
let category_id_param = category_id.to_string();
sqlx::query!(
"INSERT INTO inventory_items
(id, name, description, weight, category_id)
VALUES
(?, ?, ?, ?, ?)",
id_param,
name,
"",
weight,
category_id_param
)
.execute(pool)
.await?;
Ok(id)
}
pub async fn get_category_max_weight(
pool: &sqlx::Pool<sqlx::Sqlite>,
category_id: Uuid,
) -> Result<i64, Error> {
let category_id_param = category_id.to_string();
let weight = sqlx::query!(
"
SELECT COALESCE(MAX(i_item.weight), 0) as weight
FROM inventory_items_categories as category
INNER JOIN inventory_items as i_item
ON i_item.category_id = category.id
WHERE category_id = ?
",
category_id_param
)
.fetch_one(pool)
.map_ok(|row| {
// convert to i64 because that the default integer type, but looks
// like COALESCE return i32?
//
// We can be certain that the row exists, as we COALESCE it
row.weight.unwrap() as i64
})
.await?;
Ok(weight)
}
}
// #[derive(Debug)]
// pub struct Item {
// pub id: Uuid,
// pub name: String,
// pub description: Option<String>,
// pub weight: i64,
// pub category_id: Uuid,
// }
// pub struct DbInventoryItemsRow {
// pub id: String,
// pub name: String,
// pub weight: i64,
// pub description: Option<String>,
// pub category_id: String,
// }
// impl TryFrom<DbInventoryItemsRow> for Item {
// type Error = Error;
// fn try_from(row: DbInventoryItemsRow) -> Result<Self, Self::Error> {
// Ok(Item {
// id: Uuid::try_parse(&row.id)?,
// name: row.name,
// description: row.description, // TODO
// weight: row.weight,
// category_id: Uuid::try_parse(&row.category_id)?,
// })
// }
// }
// impl Item {
// pub async fn find(pool: &sqlx::Pool<sqlx::Sqlite>, id: Uuid) -> Result<Option<Item>, Error> {
// let id_param = id.to_string();
// sqlx::query_as!(
// DbInventoryItemsRow,
// "SELECT
// id,
// name,
// weight,
// description,
// category_id
// FROM inventory_items AS item
// WHERE item.id = ?",
// id_param,
// )
// .fetch_optional(pool)
// .await?
// .map(|row| row.try_into())
// .transpose()
// }
// pub async fn _get_category_total_picked_weight(
// pool: &sqlx::Pool<sqlx::Sqlite>,
// category_id: Uuid,
// ) -> Result<i64, Error> {
// let category_id_param = category_id.to_string();
// Ok(sqlx::query!(
// "
// SELECT COALESCE(SUM(i_item.weight), 0) as weight
// FROM inventory_items_categories as category
// INNER JOIN inventory_items as i_item
// ON i_item.category_id = category.id
// INNER JOIN trips_items as t_item
// ON i_item.id = t_item.item_id
// WHERE category_id = ?
// AND t_item.pick = 1
// ",
// category_id_param
// )
// .fetch_one(pool)
// .map_ok(|row| {
// // convert to i64 because that the default integer type, but looks
// // like COALESCE return i32?
// //
// // We can be certain that the row exists, as we COALESCE it
// row.weight.unwrap() as i64
// })
// .await?)
// }
// }

File diff suppressed because it is too large Load Diff

951
rust/src/models/trips.rs Normal file
View File

@@ -0,0 +1,951 @@
use std::fmt;
use super::{
consts,
error::{DatabaseError, Error, QueryError},
inventory,
};
use futures::{TryFutureExt, TryStreamExt};
use serde::{Deserialize, Serialize};
use serde_variant::to_variant_name;
use time;
use uuid::Uuid;
#[derive(sqlx::Type, PartialEq, PartialOrd, Deserialize)]
pub enum TripState {
Init,
Planning,
Planned,
Active,
Review,
Done,
}
impl TripState {
pub fn new() -> Self {
TripState::Init
}
pub fn next(&self) -> Option<Self> {
match self {
Self::Init => Some(Self::Planning),
Self::Planning => Some(Self::Planned),
Self::Planned => Some(Self::Active),
Self::Active => Some(Self::Review),
Self::Review => Some(Self::Done),
Self::Done => None,
}
}
pub fn prev(&self) -> Option<Self> {
match self {
Self::Init => None,
Self::Planning => Some(Self::Init),
Self::Planned => Some(Self::Planning),
Self::Active => Some(Self::Planned),
Self::Review => Some(Self::Active),
Self::Done => Some(Self::Review),
}
}
}
impl fmt::Display for TripState {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}",
match self {
Self::Init => "Init",
Self::Planning => "Planning",
Self::Planned => "Planned",
Self::Active => "Active",
Self::Review => "Review",
Self::Done => "Done",
},
)
}
}
impl std::convert::TryFrom<&str> for TripState {
type Error = Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Ok(match value {
"Init" => Self::Init,
"Planning" => Self::Planning,
"Planned" => Self::Planned,
"Active" => Self::Active,
"Review" => Self::Review,
"Done" => Self::Done,
_ => {
return Err(Error::Database(DatabaseError::Enum {
description: format!("{value} is not a valid value for TripState"),
}))
}
})
}
}
#[derive(Serialize, Debug)]
pub enum TripItemStateKey {
Pick,
Pack,
}
impl fmt::Display for TripItemStateKey {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}",
match self {
Self::Pick => "pick",
Self::Pack => "pack",
},
)
}
}
#[derive(Debug)]
pub struct TripCategory {
pub category: inventory::Category,
pub items: Option<Vec<TripItem>>,
}
impl TripCategory {
pub fn total_picked_weight(&self) -> i64 {
self.items
.as_ref()
.unwrap()
.iter()
.filter(|item| item.picked)
.map(|item| item.item.weight)
.sum()
}
pub async fn find(
pool: &sqlx::Pool<sqlx::Sqlite>,
trip_id: Uuid,
category_id: Uuid,
) -> Result<Option<TripCategory>, Error> {
let mut category: Option<TripCategory> = None;
let trip_id_param = trip_id.to_string();
let category_id_param = category_id.to_string();
sqlx::query!(
"
SELECT
category.id as category_id,
category.name as category_name,
category.description AS category_description,
inner.trip_id AS trip_id,
inner.item_id AS item_id,
inner.item_name AS item_name,
inner.item_description AS item_description,
inner.item_weight AS item_weight,
inner.item_is_picked AS item_is_picked,
inner.item_is_packed AS item_is_packed,
inner.item_is_new AS item_is_new
FROM inventory_items_categories AS category
LEFT JOIN (
SELECT
trip.trip_id AS trip_id,
category.id as category_id,
category.name as category_name,
category.description as category_description,
item.id as item_id,
item.name as item_name,
item.description as item_description,
item.weight as item_weight,
trip.pick as item_is_picked,
trip.pack as item_is_packed,
trip.new as item_is_new
FROM trips_items as trip
INNER JOIN inventory_items as item
ON item.id = trip.item_id
INNER JOIN inventory_items_categories as category
ON category.id = item.category_id
WHERE trip.trip_id = ?
) AS inner
ON inner.category_id = category.id
WHERE category.id = ?
",
trip_id_param,
category_id_param
)
.fetch(pool)
.map_ok(|row| -> Result<(), Error> {
match &category {
Some(_) => (),
None => {
category = Some(TripCategory {
category: inventory::Category {
id: Uuid::try_parse(&row.category_id)?,
name: row.category_name,
description: row.category_description,
items: None,
},
items: None,
})
}
};
match row.item_id {
None => {
// we have an empty (unused) category which has NULL values
// for the item_id column
category.as_mut().unwrap().items = Some(vec![]);
category.as_mut().unwrap().category.items = Some(vec![]);
}
Some(item_id) => {
let item = TripItem {
item: inventory::Item {
id: Uuid::try_parse(&item_id)?,
name: row.item_name.unwrap(),
description: row.item_description,
weight: row.item_weight.unwrap(),
category_id: category.as_ref().unwrap().category.id,
},
picked: row.item_is_picked.unwrap(),
packed: row.item_is_packed.unwrap(),
new: row.item_is_new.unwrap(),
};
match &mut category.as_mut().unwrap().items {
None => category.as_mut().unwrap().items = Some(vec![item]),
Some(ref mut items) => items.push(item),
}
}
}
Ok(())
})
.try_collect::<Vec<Result<(), Error>>>()
.await?
.into_iter()
.collect::<Result<(), Error>>()?;
// this may be None if there are no results (which
// means that the category was not found)
Ok(category)
}
}
#[derive(Debug)]
pub struct TripItem {
pub item: inventory::Item,
pub picked: bool,
pub packed: bool,
pub new: bool,
}
pub(crate) struct DbTripsItemsRow {
pub(crate) picked: bool,
pub(crate) packed: bool,
pub(crate) new: bool,
pub(crate) id: String,
pub(crate) name: String,
pub(crate) weight: i64,
pub(crate) description: Option<String>,
pub(crate) category_id: String,
}
impl TryFrom<DbTripsItemsRow> for TripItem {
type Error = Error;
fn try_from(row: DbTripsItemsRow) -> Result<Self, Self::Error> {
Ok(TripItem {
picked: row.picked,
packed: row.packed,
new: row.new,
item: inventory::Item {
id: Uuid::try_parse(&row.id)?,
name: row.name,
description: row.description,
weight: row.weight,
category_id: Uuid::try_parse(&row.category_id)?,
},
})
}
}
impl TripItem {
pub async fn find(
pool: &sqlx::Pool<sqlx::Sqlite>,
trip_id: Uuid,
item_id: Uuid,
) -> Result<Option<Self>, Error> {
let item_id_param = item_id.to_string();
let trip_id_param = trip_id.to_string();
sqlx::query_as!(
DbTripsItemsRow,
"
SELECT
t_item.item_id AS id,
t_item.pick AS picked,
t_item.pack AS packed,
t_item.new AS new,
i_item.name AS name,
i_item.description AS description,
i_item.weight AS weight,
i_item.category_id AS category_id
FROM trips_items AS t_item
INNER JOIN inventory_items AS i_item
ON i_item.id = t_item.item_id
WHERE t_item.item_id = ?
AND t_item.trip_id = ?
",
item_id_param,
trip_id_param,
)
.fetch_optional(pool)
.await?
.map(|row| row.try_into())
.transpose()
}
pub async fn set_state(
pool: &sqlx::Pool<sqlx::Sqlite>,
trip_id: Uuid,
item_id: Uuid,
key: TripItemStateKey,
value: bool,
) -> Result<(), Error> {
let result = sqlx::query(&format!(
"UPDATE trips_items
SET {key} = ?
WHERE trip_id = ?
AND item_id = ?",
key = to_variant_name(&key).unwrap()
))
.bind(value)
.bind(trip_id.to_string())
.bind(item_id.to_string())
.execute(pool)
.await?;
(result.rows_affected() != 0).then_some(()).ok_or_else(|| {
Error::Query(QueryError::NotFound {
description: format!("item {item_id} not found for trip {trip_id}"),
})
})
}
}
pub(crate) struct DbTripRow {
pub id: String,
pub name: String,
pub date_start: String,
pub date_end: String,
pub state: String,
pub location: Option<String>,
pub temp_min: Option<i64>,
pub temp_max: Option<i64>,
pub comment: Option<String>,
}
impl TryFrom<DbTripRow> for Trip {
type Error = Error;
fn try_from(row: DbTripRow) -> Result<Self, Self::Error> {
Ok(Trip {
id: Uuid::try_parse(&row.id)?,
name: row.name,
date_start: time::Date::parse(&row.date_start, consts::DATE_FORMAT)?,
date_end: time::Date::parse(&row.date_end, consts::DATE_FORMAT)?,
state: row.state.as_str().try_into()?,
location: row.location,
temp_min: row.temp_min,
temp_max: row.temp_max,
comment: row.comment,
types: None,
categories: None,
})
}
}
pub struct Trip {
pub id: Uuid,
pub name: String,
pub date_start: time::Date,
pub date_end: time::Date,
pub state: TripState,
pub location: Option<String>,
pub temp_min: Option<i64>,
pub temp_max: Option<i64>,
pub comment: Option<String>,
pub(crate) types: Option<Vec<TripType>>,
pub(crate) categories: Option<Vec<TripCategory>>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum TripAttribute {
#[serde(rename = "name")]
Name,
#[serde(rename = "date_start")]
DateStart,
#[serde(rename = "date_end")]
DateEnd,
#[serde(rename = "location")]
Location,
#[serde(rename = "temp_min")]
TempMin,
#[serde(rename = "temp_max")]
TempMax,
}
pub(crate) struct DbTripWeightRow {
pub total_weight: Option<i32>,
}
impl Trip {
pub async fn all(pool: &sqlx::Pool<sqlx::Sqlite>) -> Result<Vec<Trip>, Error> {
sqlx::query_as!(
DbTripRow,
"SELECT
id,
name,
CAST (date_start AS TEXT) date_start,
CAST (date_end AS TEXT) date_end,
state,
location,
temp_min,
temp_max,
comment
FROM trips",
)
.fetch(pool)
.map_ok(|row| row.try_into())
.try_collect::<Vec<Result<Trip, Error>>>()
.await?
.into_iter()
.collect::<Result<Vec<Trip>, Error>>()
}
pub async fn find(
pool: &sqlx::Pool<sqlx::Sqlite>,
trip_id: Uuid,
) -> Result<Option<Self>, Error> {
let trip_id_param = trip_id.to_string();
sqlx::query_as!(
DbTripRow,
"SELECT
id,
name,
CAST (date_start AS TEXT) date_start,
CAST (date_end AS TEXT) date_end,
state,
location,
temp_min,
temp_max,
comment
FROM trips
WHERE id = ?",
trip_id_param
)
.fetch_optional(pool)
.await?
.map(|row| row.try_into())
.transpose()
}
pub async fn trip_type_remove(
pool: &sqlx::Pool<sqlx::Sqlite>,
id: Uuid,
type_id: Uuid,
) -> Result<bool, Error> {
let id_param = id.to_string();
let type_id_param = type_id.to_string();
let results = sqlx::query!(
"DELETE FROM trips_to_trips_types AS ttt
WHERE ttt.trip_id = ?
AND ttt.trip_type_id = ?
",
id_param,
type_id_param,
)
.execute(pool)
.await?;
Ok(results.rows_affected() != 0)
}
pub async fn trip_type_add(
pool: &sqlx::Pool<sqlx::Sqlite>,
id: Uuid,
type_id: Uuid,
) -> Result<(), Error> {
let trip_id_param = id.to_string();
let type_id_param = type_id.to_string();
sqlx::query!(
"INSERT INTO trips_to_trips_types
(trip_id, trip_type_id)
VALUES
(?, ?)
",
trip_id_param,
type_id_param,
)
.execute(pool)
.await?;
Ok(())
}
pub async fn set_state(
pool: &sqlx::Pool<sqlx::Sqlite>,
id: Uuid,
new_state: &TripState,
) -> Result<bool, Error> {
let trip_id_param = id.to_string();
let result = sqlx::query!(
"UPDATE trips
SET state = ?
WHERE id = ?",
new_state,
trip_id_param,
)
.execute(pool)
.await?;
Ok(result.rows_affected() != 0)
}
pub async fn set_comment(
pool: &sqlx::Pool<sqlx::Sqlite>,
id: Uuid,
new_comment: &str,
) -> Result<bool, Error> {
let trip_id_param = id.to_string();
let result = sqlx::query!(
"UPDATE trips
SET comment = ?
WHERE id = ?",
new_comment,
trip_id_param,
)
.execute(pool)
.await?;
Ok(result.rows_affected() != 0)
}
pub async fn set_attribute(
pool: &sqlx::Pool<sqlx::Sqlite>,
trip_id: Uuid,
attribute: TripAttribute,
value: &str,
) -> Result<(), Error> {
let result = sqlx::query(&format!(
"UPDATE trips
SET {attribute} = ?
WHERE id = ?",
attribute = to_variant_name(&attribute).unwrap()
))
.bind(value)
.bind(trip_id.to_string())
.execute(pool)
.await?;
(result.rows_affected() != 0).then_some(()).ok_or_else(|| {
Error::Query(QueryError::NotFound {
description: format!("trip {trip_id} not found"),
})
})
}
pub async fn save(
pool: &sqlx::Pool<sqlx::Sqlite>,
name: &str,
date_start: time::Date,
date_end: time::Date,
) -> Result<Uuid, Error> {
let id = Uuid::new_v4();
let id_param = id.to_string();
let date_start = date_start.format(consts::DATE_FORMAT)?;
let date_end = date_end.format(consts::DATE_FORMAT)?;
let trip_state = TripState::new();
sqlx::query!(
"INSERT INTO trips
(id, name, date_start, date_end, state)
VALUES
(?, ?, ?, ?, ?)",
id_param,
name,
date_start,
date_end,
trip_state,
)
.execute(pool)
.await?;
Ok(id)
}
pub async fn find_total_picked_weight(
pool: &sqlx::Pool<sqlx::Sqlite>,
trip_id: Uuid,
) -> Result<i64, Error> {
let trip_id_param = trip_id.to_string();
let weight = sqlx::query_as!(
DbTripWeightRow,
"
SELECT
CAST(IFNULL(SUM(i_item.weight), 0) AS INTEGER) AS total_weight
FROM trips AS trip
INNER JOIN trips_items AS t_item
ON t_item.trip_id = trip.id
INNER JOIN inventory_items AS i_item
ON t_item.item_id = i_item.id
WHERE
trip.id = ?
AND t_item.pick = true
",
trip_id_param
)
.fetch_one(pool)
.map_ok(|row| row.total_weight.unwrap() as i64)
.await?;
Ok(weight)
}
pub fn types(&self) -> &Vec<TripType> {
self.types
.as_ref()
.expect("you need to call load_trips_types()")
}
pub fn categories(&self) -> &Vec<TripCategory> {
self.categories
.as_ref()
.expect("you need to call load_trips_types()")
}
pub fn total_picked_weight(&self) -> i64 {
self.categories()
.iter()
.map(|category| -> i64 {
category
.items
.as_ref()
.unwrap()
.iter()
.filter_map(|item| Some(item.item.weight).filter(|_| item.picked))
.sum::<i64>()
})
.sum::<i64>()
}
pub async fn load_trips_types(&mut self, pool: &sqlx::Pool<sqlx::Sqlite>) -> Result<(), Error> {
let id = self.id.to_string();
let types = sqlx::query!(
"
SELECT
type.id as id,
type.name as name,
inner.id IS NOT NULL AS active
FROM trips_types AS type
LEFT JOIN (
SELECT type.id as id, type.name as name
FROM trips as trip
INNER JOIN trips_to_trips_types as ttt
ON ttt.trip_id = trip.id
INNER JOIN trips_types AS type
ON type.id == ttt.trip_type_id
WHERE trip.id = ?
) AS inner
ON inner.id = type.id
",
id
)
.fetch(pool)
.map_ok(|row| -> Result<TripType, Error> {
Ok(TripType {
id: Uuid::try_parse(&row.id)?,
name: row.name,
active: match row.active {
0 => false,
1 => true,
_ => unreachable!(),
},
})
})
.try_collect::<Vec<Result<TripType, Error>>>()
.await?
.into_iter()
.collect::<Result<Vec<TripType>, Error>>()?;
self.types = Some(types);
Ok(())
}
pub async fn sync_trip_items_with_inventory(
&mut self,
pool: &sqlx::Pool<sqlx::Sqlite>,
) -> Result<(), Error> {
// we need to get all items that are part of the inventory but not
// part of the trip items
//
// then, we know which items we need to sync. there are different
// states for them:
//
// * 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
// visible so the user knows that there might be new items to
// consider
let trip_id = self.id.to_string();
let unsynced_items: Vec<Uuid> = sqlx::query!(
"
SELECT
i_item.id AS item_id
FROM inventory_items AS i_item
LEFT JOIN (
SELECT t_item.item_id as item_id
FROM trips_items AS t_item
WHERE t_item.trip_id = ?
) AS t_item
ON t_item.item_id = i_item.id
WHERE t_item.item_id IS NULL
",
trip_id
)
.fetch(pool)
.map_ok(|row| -> Result<Uuid, Error> { Ok(Uuid::try_parse(&row.item_id)?) })
.try_collect::<Vec<Result<Uuid, Error>>>()
.await?
.into_iter()
.collect::<Result<Vec<Uuid>, Error>>()?;
// looks like there is currently no nice way to do multiple inserts
// with sqlx. whatever, this won't matter
// only mark as new when the trip is already underway
let mark_as_new = self.state != TripState::new();
for unsynced_item in &unsynced_items {
let item_id = unsynced_item.to_string();
sqlx::query!(
"
INSERT INTO trips_items
(
item_id,
trip_id,
pick,
pack,
new
)
VALUES (?, ?, ?, ?, ?)
",
item_id,
trip_id,
false,
false,
mark_as_new,
)
.execute(pool)
.await?;
}
tracing::info!("unsynced items: {:?}", &unsynced_items);
Ok(())
}
pub async fn load_categories(&mut self, pool: &sqlx::Pool<sqlx::Sqlite>) -> Result<(), Error> {
let mut categories: Vec<TripCategory> = vec![];
// we can ignore the return type as we collect into `categories`
// in the `map_ok()` closure
let id = self.id.to_string();
sqlx::query!(
"
SELECT
category.id as category_id,
category.name as category_name,
category.description AS category_description,
inner.trip_id AS trip_id,
inner.item_id AS item_id,
inner.item_name AS item_name,
inner.item_description AS item_description,
inner.item_weight AS item_weight,
inner.item_is_picked AS item_is_picked,
inner.item_is_packed AS item_is_packed,
inner.item_is_new AS item_is_new
FROM inventory_items_categories AS category
LEFT JOIN (
SELECT
trip.trip_id AS trip_id,
category.id as category_id,
category.name as category_name,
category.description as category_description,
item.id as item_id,
item.name as item_name,
item.description as item_description,
item.weight as item_weight,
trip.pick as item_is_picked,
trip.pack as item_is_packed,
trip.new as item_is_new
FROM trips_items as trip
INNER JOIN inventory_items as item
ON item.id = trip.item_id
INNER JOIN inventory_items_categories as category
ON category.id = item.category_id
WHERE trip.trip_id = ?
) AS inner
ON inner.category_id = category.id
",
id
)
.fetch(pool)
.map_ok(|row| -> Result<(), Error> {
let mut category = TripCategory {
category: inventory::Category {
id: Uuid::try_parse(&row.category_id)?,
name: row.category_name,
description: row.category_description,
items: None,
},
items: None,
};
match row.item_id {
None => {
// we have an empty (unused) category which has NULL values
// for the item_id column
category.items = Some(vec![]);
categories.push(category);
}
Some(item_id) => {
let item = TripItem {
item: inventory::Item {
id: Uuid::try_parse(&item_id)?,
name: row.item_name.unwrap(),
description: row.item_description,
weight: row.item_weight.unwrap(),
category_id: category.category.id,
},
picked: row.item_is_picked.unwrap(),
packed: row.item_is_packed.unwrap(),
new: row.item_is_new.unwrap(),
};
if let Some(&mut ref mut c) = categories
.iter_mut()
.find(|c| c.category.id == category.category.id)
{
// we always populate c.items when we add a new category, so
// it's safe to unwrap here
c.items.as_mut().unwrap().push(item);
} else {
category.items = Some(vec![item]);
categories.push(category);
}
}
}
Ok(())
})
.try_collect::<Vec<Result<(), Error>>>()
.await?
.into_iter()
.collect::<Result<(), Error>>()?;
self.categories = Some(categories);
Ok(())
}
}
pub struct TripType {
pub id: Uuid,
pub name: String,
pub active: bool,
}
impl TripsType {
pub async fn all(pool: &sqlx::Pool<sqlx::Sqlite>) -> Result<Vec<Self>, Error> {
sqlx::query_as!(
DbTripsTypesRow,
"SELECT
id,
name
FROM trips_types",
)
.fetch(pool)
.map_ok(|row| row.try_into())
.try_collect::<Vec<Result<Self, Error>>>()
.await?
.into_iter()
.collect::<Result<Vec<Self>, Error>>()
}
pub async fn save(pool: &sqlx::Pool<sqlx::Sqlite>, name: &str) -> Result<Uuid, Error> {
let id = Uuid::new_v4();
let id_param = id.to_string();
sqlx::query!(
"INSERT INTO trips_types
(id, name)
VALUES
(?, ?)",
id_param,
name,
)
.execute(pool)
.await?;
Ok(id)
}
pub async fn set_name(
pool: &sqlx::Pool<sqlx::Sqlite>,
id: Uuid,
new_name: &str,
) -> Result<bool, Error> {
let id_param = id.to_string();
let result = sqlx::query!(
"UPDATE trips_types
SET name = ?
WHERE id = ?",
new_name,
id_param,
)
.execute(pool)
.await?;
Ok(result.rows_affected() != 0)
}
}
pub(crate) struct DbTripsTypesRow {
pub id: String,
pub name: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum TripTypeAttribute {
#[serde(rename = "name")]
Name,
}
pub struct TripsType {
pub id: Uuid,
pub name: String,
}
impl TryFrom<DbTripsTypesRow> for TripsType {
type Error = Error;
fn try_from(row: DbTripsTypesRow) -> Result<Self, Self::Error> {
Ok(TripsType {
id: Uuid::try_parse(&row.id)?,
name: row.name,
})
}
}

View File

@@ -8,8 +8,8 @@ pub struct Inventory;
impl Inventory { impl Inventory {
pub fn build( pub fn build(
active_category: Option<&models::Category>, active_category: Option<&models::inventory::Category>,
categories: &Vec<models::Category>, categories: &Vec<models::inventory::Category>,
edit_item_id: Option<Uuid>, edit_item_id: Option<Uuid>,
) -> Markup { ) -> Markup {
html!( html!(
@@ -37,12 +37,12 @@ pub struct InventoryCategoryList;
impl InventoryCategoryList { impl InventoryCategoryList {
pub fn build( pub fn build(
active_category: Option<&models::Category>, active_category: Option<&models::inventory::Category>,
categories: &Vec<models::Category>, categories: &Vec<models::inventory::Category>,
) -> Markup { ) -> Markup {
let biggest_category_weight: i64 = categories let biggest_category_weight: i64 = categories
.iter() .iter()
.map(models::Category::total_weight) .map(models::inventory::Category::total_weight)
.max() .max()
.unwrap_or(1); .unwrap_or(1);
@@ -129,7 +129,7 @@ impl InventoryCategoryList {
} }
td ."border" ."p-0" ."m-0" { td ."border" ."p-0" ."m-0" {
p ."p-2" ."m-2" { p ."p-2" ."m-2" {
(categories.iter().map(models::Category::total_weight).sum::<i64>().to_string()) (categories.iter().map(models::inventory::Category::total_weight).sum::<i64>().to_string())
} }
} }
} }
@@ -142,7 +142,7 @@ impl InventoryCategoryList {
pub struct InventoryItemList; pub struct InventoryItemList;
impl InventoryItemList { impl InventoryItemList {
pub fn build(edit_item_id: Option<Uuid>, items: &Vec<models::Item>) -> Markup { pub fn build(edit_item_id: Option<Uuid>, items: &Vec<models::inventory::Item>) -> Markup {
let biggest_item_weight: i64 = items.iter().map(|item| item.weight).max().unwrap_or(1); let biggest_item_weight: i64 = items.iter().map(|item| item.weight).max().unwrap_or(1);
html!( html!(
div #items { div #items {
@@ -414,8 +414,8 @@ pub struct InventoryNewItemFormCategory;
impl InventoryNewItemFormCategory { impl InventoryNewItemFormCategory {
pub fn build( pub fn build(
active_category: Option<&models::Category>, active_category: Option<&models::inventory::Category>,
categories: &Vec<models::Category>, categories: &Vec<models::inventory::Category>,
) -> Markup { ) -> Markup {
html!( html!(
div div
@@ -455,8 +455,8 @@ pub struct InventoryNewItemForm;
impl InventoryNewItemForm { impl InventoryNewItemForm {
pub fn build( pub fn build(
active_category: Option<&models::Category>, active_category: Option<&models::inventory::Category>,
categories: &Vec<models::Category>, categories: &Vec<models::inventory::Category>,
) -> Markup { ) -> Markup {
html!( html!(
form form
@@ -558,7 +558,7 @@ impl InventoryNewCategoryForm {
pub struct InventoryItem; pub struct InventoryItem;
impl InventoryItem { impl InventoryItem {
pub fn build(_state: &ClientState, item: &models::InventoryItem) -> Markup { pub fn build(_state: &ClientState, item: &models::inventory::InventoryItem) -> Markup {
html!( html!(
div ."p-8" { div ."p-8" {
table table

View File

@@ -12,7 +12,7 @@ pub mod packagelist;
pub mod types; pub mod types;
impl TripManager { impl TripManager {
pub fn build(trips: Vec<models::Trip>) -> Markup { pub fn build(trips: Vec<models::trips::Trip>) -> Markup {
html!( html!(
div div
."p-8" ."p-8"
@@ -44,21 +44,21 @@ impl From<InputType> for &'static str {
} }
} }
fn trip_state_icon(state: &models::TripState) -> &'static str { fn trip_state_icon(state: &models::trips::TripState) -> &'static str {
match state { match state {
models::TripState::Init => "mdi-magic-staff", models::trips::TripState::Init => "mdi-magic-staff",
models::TripState::Planning => "mdi-text-box-outline", models::trips::TripState::Planning => "mdi-text-box-outline",
models::TripState::Planned => "mdi-clock-outline", models::trips::TripState::Planned => "mdi-clock-outline",
models::TripState::Active => "mdi-play", models::trips::TripState::Active => "mdi-play",
models::TripState::Review => "mdi-magnify", models::trips::TripState::Review => "mdi-magnify",
models::TripState::Done => "mdi-check", models::trips::TripState::Done => "mdi-check",
} }
} }
pub struct TripTable; pub struct TripTable;
impl TripTable { impl TripTable {
pub fn build(trips: Vec<models::Trip>) -> Markup { pub fn build(trips: Vec<models::trips::Trip>) -> Markup {
html!( html!(
table table
."table" ."table"
@@ -222,9 +222,9 @@ pub struct Trip;
impl Trip { impl Trip {
pub fn build( pub fn build(
trip: &models::Trip, trip: &models::trips::Trip,
trip_edit_attribute: Option<models::TripAttribute>, trip_edit_attribute: Option<models::trips::TripAttribute>,
active_category: Option<&models::TripCategory>, active_category: Option<&models::trips::TripCategory>,
) -> Markup { ) -> Markup {
html!( html!(
div ."p-8" ."flex" ."flex-col" ."gap-8" { div ."p-8" ."flex" ."flex-col" ."gap-8" {
@@ -257,10 +257,10 @@ impl Trip {
} }
} }
div ."flex" ."flex-row" ."items-center" ."gap-x-3" { div ."flex" ."flex-row" ."items-center" ."gap-x-3" {
@if trip_edit_attribute.as_ref().map_or(false, |a| *a == models::TripAttribute::Name) { @if trip_edit_attribute.as_ref().map_or(false, |a| *a == models::trips::TripAttribute::Name) {
form form
id="edit-trip" id="edit-trip"
action=(format!("edit/{}/submit", to_variant_name(&models::TripAttribute::Name).unwrap())) action=(format!("edit/{}/submit", to_variant_name(&models::trips::TripAttribute::Name).unwrap()))
hx-boost="true" hx-boost="true"
target="_self" target="_self"
method="post" method="post"
@@ -316,7 +316,7 @@ impl Trip {
h1 ."text-2xl" { (trip.name) } h1 ."text-2xl" { (trip.name) }
span { span {
a a
href={"?edit=" (to_variant_name(&models::TripAttribute::Name).unwrap())} href={"?edit=" (to_variant_name(&models::trips::TripAttribute::Name).unwrap())}
hx-boost="true" hx-boost="true"
{ {
span span
@@ -357,8 +357,8 @@ impl TripInfoRow {
pub fn build( pub fn build(
name: &str, name: &str,
value: Option<impl std::fmt::Display>, value: Option<impl std::fmt::Display>,
attribute_key: &models::TripAttribute, attribute_key: &models::trips::TripAttribute,
edit_attribute: Option<&models::TripAttribute>, edit_attribute: Option<&models::trips::TripAttribute>,
input_type: InputType, input_type: InputType,
) -> Markup { ) -> Markup {
let edit = edit_attribute.map_or(false, |a| a == attribute_key); let edit = edit_attribute.map_or(false, |a| a == attribute_key);
@@ -497,7 +497,7 @@ impl TripInfoTotalWeightRow {
pub struct TripInfoStateRow; pub struct TripInfoStateRow;
impl TripInfoStateRow { impl TripInfoStateRow {
pub fn build(trip_state: &models::TripState) -> Markup { pub fn build(trip_state: &models::trips::TripState) -> Markup {
let prev_state = trip_state.prev(); let prev_state = trip_state.prev();
let next_state = trip_state.next(); let next_state = trip_state.next();
html!( html!(
@@ -602,8 +602,8 @@ pub struct TripInfo;
impl TripInfo { impl TripInfo {
pub fn build( pub fn build(
trip_edit_attribute: Option<models::TripAttribute>, trip_edit_attribute: Option<models::trips::TripAttribute>,
trip: &models::Trip, trip: &models::trips::Trip,
) -> Markup { ) -> Markup {
html!( html!(
table table
@@ -617,31 +617,31 @@ impl TripInfo {
tbody { tbody {
(TripInfoRow::build("Location", (TripInfoRow::build("Location",
trip.location.as_ref(), trip.location.as_ref(),
&models::TripAttribute::Location, &models::trips::TripAttribute::Location,
trip_edit_attribute.as_ref(), trip_edit_attribute.as_ref(),
InputType::Text, InputType::Text,
)) ))
(TripInfoRow::build("Start date", (TripInfoRow::build("Start date",
Some(trip.date_start), Some(trip.date_start),
&models::TripAttribute::DateStart, &models::trips::TripAttribute::DateStart,
trip_edit_attribute.as_ref(), trip_edit_attribute.as_ref(),
InputType::Date, InputType::Date,
)) ))
(TripInfoRow::build("End date", (TripInfoRow::build("End date",
Some(trip.date_end), Some(trip.date_end),
&models::TripAttribute::DateEnd, &models::trips::TripAttribute::DateEnd,
trip_edit_attribute.as_ref(), trip_edit_attribute.as_ref(),
InputType::Date, InputType::Date,
)) ))
(TripInfoRow::build("Temp (min)", (TripInfoRow::build("Temp (min)",
trip.temp_min, trip.temp_min,
&models::TripAttribute::TempMin, &models::trips::TripAttribute::TempMin,
trip_edit_attribute.as_ref(), trip_edit_attribute.as_ref(),
InputType::Number, InputType::Number,
)) ))
(TripInfoRow::build("Temp (max)", (TripInfoRow::build("Temp (max)",
trip.temp_max, trip.temp_max,
&models::TripAttribute::TempMax, &models::trips::TripAttribute::TempMax,
trip_edit_attribute.as_ref(), trip_edit_attribute.as_ref(),
InputType::Number, InputType::Number,
)) ))
@@ -672,8 +672,8 @@ impl TripInfo {
// the margins // the margins
{ {
@let types = trip.types(); @let types = trip.types();
@let active_triptypes = types.iter().filter(|t| t.active).collect::<Vec<&models::TripType>>(); @let active_triptypes = types.iter().filter(|t| t.active).collect::<Vec<&models::trips::TripType>>();
@let inactive_triptypes = types.iter().filter(|t| !t.active).collect::<Vec<&models::TripType>>(); @let inactive_triptypes = types.iter().filter(|t| !t.active).collect::<Vec<&models::trips::TripType>>();
@if !active_triptypes.is_empty() { @if !active_triptypes.is_empty() {
div div
@@ -775,7 +775,7 @@ impl TripInfo {
pub struct TripComment; pub struct TripComment;
impl TripComment { impl TripComment {
pub fn build(trip: &models::Trip) -> Markup { pub fn build(trip: &models::trips::Trip) -> Markup {
html!( html!(
div div
x-data="{ save_active: false }" x-data="{ save_active: false }"
@@ -830,7 +830,10 @@ impl TripComment {
pub struct TripItems; pub struct TripItems;
impl TripItems { impl TripItems {
pub fn build(active_category: Option<&models::TripCategory>, trip: &models::Trip) -> Markup { pub fn build(
active_category: Option<&models::trips::TripCategory>,
trip: &models::trips::Trip,
) -> Markup {
html!( html!(
div #trip-items ."grid" ."grid-cols-4" ."gap-3" { div #trip-items ."grid" ."grid-cols-4" ."gap-3" {
div ."col-span-2" { div ."col-span-2" {
@@ -856,7 +859,7 @@ pub struct TripCategoryListRow;
impl TripCategoryListRow { impl TripCategoryListRow {
pub fn build( pub fn build(
trip_id: Uuid, trip_id: Uuid,
category: &models::TripCategory, category: &models::trips::TripCategory,
active: bool, active: bool,
biggest_category_weight: i64, biggest_category_weight: i64,
htmx_swap: bool, htmx_swap: bool,
@@ -962,12 +965,15 @@ impl TripCategoryListRow {
pub struct TripCategoryList; pub struct TripCategoryList;
impl TripCategoryList { impl TripCategoryList {
pub fn build(active_category: Option<&models::TripCategory>, trip: &models::Trip) -> Markup { pub fn build(
active_category: Option<&models::trips::TripCategory>,
trip: &models::trips::Trip,
) -> Markup {
let categories = trip.categories(); let categories = trip.categories();
let biggest_category_weight: i64 = categories let biggest_category_weight: i64 = categories
.iter() .iter()
.map(models::TripCategory::total_picked_weight) .map(models::trips::TripCategory::total_picked_weight)
.max() .max()
.unwrap_or(1); .unwrap_or(1);
@@ -1002,7 +1008,7 @@ impl TripCategoryList {
} }
td ."border" ."p-0" ."m-0" { td ."border" ."p-0" ."m-0" {
p ."p-2" ."m-2" { p ."p-2" ."m-2" {
(categories.iter().map(models::TripCategory::total_picked_weight).sum::<i64>().to_string()) (categories.iter().map(models::trips::TripCategory::total_picked_weight).sum::<i64>().to_string())
} }
} }
} }
@@ -1015,7 +1021,7 @@ impl TripCategoryList {
pub struct TripItemList; pub struct TripItemList;
impl TripItemList { impl TripItemList {
pub fn build(trip_id: Uuid, items: &Vec<models::TripItem>) -> Markup { pub fn build(trip_id: Uuid, items: &Vec<models::trips::TripItem>) -> Markup {
let biggest_item_weight: i64 = items.iter().map(|item| item.item.weight).max().unwrap_or(1); let biggest_item_weight: i64 = items.iter().map(|item| item.item.weight).max().unwrap_or(1);
html!( html!(
@@ -1053,7 +1059,11 @@ impl TripItemList {
pub struct TripItemListRow; pub struct TripItemListRow;
impl TripItemListRow { impl TripItemListRow {
pub fn build(trip_id: Uuid, item: &models::TripItem, biggest_item_weight: i64) -> Markup { pub fn build(
trip_id: Uuid,
item: &models::trips::TripItem,
biggest_item_weight: i64,
) -> Markup {
html!( html!(
tr ."h-10" { tr ."h-10" {
td td

View File

@@ -6,7 +6,7 @@ use crate::models;
pub struct TripPackageListRow; pub struct TripPackageListRow;
impl TripPackageListRow { impl TripPackageListRow {
pub fn build(trip_id: Uuid, item: &models::TripItem) -> Markup { pub fn build(trip_id: Uuid, item: &models::trips::TripItem) -> Markup {
html!( html!(
li li
."flex" ."flex"
@@ -82,7 +82,7 @@ impl TripPackageListRow {
pub struct TripPackageListCategoryBlock; pub struct TripPackageListCategoryBlock;
impl TripPackageListCategoryBlock { impl TripPackageListCategoryBlock {
pub fn build(trip: &models::Trip, category: &models::TripCategory) -> Markup { pub fn build(trip: &models::trips::Trip, category: &models::trips::TripCategory) -> Markup {
let empty = !category let empty = !category
.items .items
.as_ref() .as_ref()
@@ -138,7 +138,7 @@ impl TripPackageListCategoryBlock {
pub struct TripPackageList; pub struct TripPackageList;
impl TripPackageList { impl TripPackageList {
pub fn build(trip: &models::Trip) -> Markup { pub fn build(trip: &models::trips::Trip) -> Markup {
// let all_packed = trip.categories().iter().all(|category| { // let all_packed = trip.categories().iter().all(|category| {
// category // category
// .items // .items

View File

@@ -5,7 +5,7 @@ use maud::{html, Markup};
pub struct TypeList; pub struct TypeList;
impl TypeList { impl TypeList {
pub fn build(state: &ClientState, trip_types: Vec<models::TripsType>) -> Markup { pub fn build(state: &ClientState, trip_types: Vec<models::trips::TripsType>) -> Markup {
html!( html!(
div ."p-8" ."flex" ."flex-col" ."gap-8" { div ."p-8" ."flex" ."flex-col" ."gap-8" {
h1 ."text-2xl" {"Trip Types"} h1 ."text-2xl" {"Trip Types"}