From e0c9bc542af4bbbc5cb5f71744acb141dc8e1343 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Tue, 29 Aug 2023 21:33:59 +0200 Subject: [PATCH] some htmx and alpine --- rust/src/components/inventory.rs | 224 +++++++++++----- rust/src/components/mod.rs | 1 + rust/src/components/{trip.rs => trip/mod.rs} | 20 +- rust/src/components/trip/types.rs | 163 ++++++++++++ rust/src/main.rs | 254 ++++++++++++++++++- rust/src/models.rs | 139 ++-------- 6 files changed, 612 insertions(+), 189 deletions(-) rename rust/src/components/{trip.rs => trip/mod.rs} (98%) create mode 100644 rust/src/components/trip/types.rs diff --git a/rust/src/components/inventory.rs b/rust/src/components/inventory.rs index 27f73ad..27e64af 100644 --- a/rust/src/components/inventory.rs +++ b/rust/src/components/inventory.rs @@ -1,4 +1,4 @@ -use maud::{html, Markup}; +use maud::{html, Markup, PreEscaped}; use crate::models::*; use crate::ClientState; @@ -312,12 +312,154 @@ impl InventoryItemList { } } +pub struct InventoryNewItemFormName; + +impl InventoryNewItemFormName { + pub fn build(value: Option<&str>, error: bool) -> Markup { + html!( + div + ."grid" + ."grid-cols-[2fr,3fr]" + ."justify-items-center" + ."items-center" + hx-post="/inventory/item/name/validate" + hx-trigger="input delay:1s, every 5s" + hx-params="new-item-name" + hx-swap="outerHTML" + { + label for="name" .font-bold { "Name" } + input + type="text" + id="new-item-name" + name="new-item-name" + x-on:input="(e) => {save_active = inventory_new_item_check_input()}" + ."block" + ."w-full" + ."p-2" + ."bg-gray-50" + ."border-2" + ."border-red-500"[error] + ."border-gray-300"[!error] + ."rounded" + ."focus:outline-none" + ."focus:bg-white" + ."focus:border-purple-500"[!error] + value=[value] + ; + @if error { + div + ."col-start-2" + ."text-sm" + ."text-red-500" + { "name already exists" } + } + } + ) + } +} + +pub struct InventoryNewItemFormWeight; + +impl InventoryNewItemFormWeight { + pub fn build() -> Markup { + html!( + div + ."grid" + ."grid-cols-[2fr,3fr]" + ."justify-items-center" + ."items-center" + { + label for="weight" .font-bold { "Weight" } + input + type="number" + id="new-item-weight" + name="new-item-weight" + min="0" + x-on:input="(e) => { + save_active = inventory_new_item_check_input(); + weight_error = !check_weight(); + }" + x-bind:class="weight_error && 'border-red-500' || 'border-gray-300 focus:border-purple-500'" + ."block" + ."w-full" + ."p-2" + ."bg-gray-50" + ."border-2" + ."rounded" + ."focus:outline-none" + ."focus:bg-white" + {} + span + // x-on produces some errors, this works as well + x-bind:class="!weight_error && 'hidden'" + ."col-start-2" + ."text-sm" + ."text-red-500" + { "invalid input" } + } + ) + } +} + +pub struct InventoryNewItemFormCategory; + +impl InventoryNewItemFormCategory { + pub fn build(state: &ClientState, categories: &Vec) -> Markup { + html!( + div + ."grid" + ."grid-cols-[2fr,3fr]" + ."justify-items-center" + ."items-center" + { + label for="item-category" .font-bold ."w-1/2" .text-center { "Category" } + select + id="new-item-category-id" + name="new-item-category-id" + ."block" + ."w-full" + ."p-2" + ."bg-gray-50" + ."border-2" + ."border-gray-300" + ."rounded" + ."focus:outline-none" + ."focus:bg-white" + ."focus:border-purple-500" + autocomplete="off" // https://stackoverflow.com/a/10096033 + { + @for category in categories { + option value=(category.id) selected[state.active_category_id.map_or(false, |id| id == category.id)] { + (category.name) + } + } + } + } + ) + } +} + pub struct InventoryNewItemForm; impl InventoryNewItemForm { pub fn build(state: &ClientState, categories: &Vec) -> Markup { html!( + script { + (PreEscaped(" + function inventory_new_item_check_input() { + return document.getElementById('new-item-name').value.length != 0 + && is_positive_integer(document.getElementById('new-item-weight').value) + } + function check_weight() { + return document.getElementById('new-item-weight').validity.valid; + } + ")) + } form + x-data="{ + save_active: inventory_new_item_check_input(), + weight_error: !check_weight(), + }" name="new-item" id="new-item" action="/inventory/item/" @@ -328,74 +470,14 @@ impl InventoryNewItemForm { span ."mdi" ."mdi-playlist-plus" ."text-2xl" ."mr-4" {} p ."inline" ."text-xl" { "Add new item" } } - div ."w-11/12" ."mx-auto" { - div ."pb-8" { - div ."flex" ."flex-row" ."justify-center" ."items-start"{ - label for="name" .font-bold ."w-1/2" ."p-2" ."text-center" { "Name" } - span ."w-1/2" { - input type="text" id="new-item-name" name="new-item-name" - ."block" - ."w-full" - ."p-2" - ."bg-gray-50" - ."border-2" - ."rounded" - ."focus:outline-none" - ."focus:bg-white" - ."focus:border-purple-500" - { - } - } - } - } - div ."flex" ."flex-row" ."justify-center" ."items-center" ."pb-8" { - label for="weight" .font-bold ."w-1/2" .text-center { "Weight" } - span ."w-1/2" { - input - type="text" - id="new-item-weight" - name="new-item-weight" - ."block" - ."w-full" - ."p-2" - ."bg-gray-50" - ."border-2" - ."border-gray-300" - ."rounded" - ."focus:outline-none" - ."focus:bg-white" - ."focus:border-purple-500" - { - } - } - } - div ."flex" ."flex-row" ."justify-center" ."items-center" ."pb-8" { - label for="item-category" .font-bold ."w-1/2" .text-center { "Category" } - span ."w-1/2" { - select - id="new-item-category-id" - name="new-item-category-id" - ."block" - ."w-full" - ."p-2" - ."bg-gray-50" - ."border-2" - ."border-gray-300" - ."rounded" - ."focus:outline-none" - ."focus:bg-white" - ."focus:border-purple-500" - autocomplete="off" // https://stackoverflow.com/a/10096033 - { - @for category in categories { - option value=(category.id) selected[state.active_category_id.map_or(false, |id| id == category.id)] { - (category.name) - } - } - } - } - } + div ."w-11/12" ."mx-auto" ."flex" ."flex-col" ."gap-8" { + (InventoryNewItemFormName::build(None, false)) + (InventoryNewItemFormWeight::build()) + (InventoryNewItemFormCategory::build(&state, categories)) input type="submit" value="Add" + x-bind:disabled="!save_active" + ."enabled:cursor-pointer" + ."disabled:opacity-50" ."py-2" ."border-2" ."rounded" @@ -415,6 +497,7 @@ impl InventoryNewCategoryForm { pub fn build() -> Markup { html!( form + x-data="{ save_active: document.getElementById('new-category-name').value.length != 0 }" name="new-category" id="new-category" action="/inventory/category/" @@ -431,11 +514,13 @@ impl InventoryNewCategoryForm { label for="name" .font-bold ."w-1/2" ."p-2" ."text-center" { "Name" } span ."w-1/2" { input type="text" id="new-category-name" name="new-category-name" + x-on:input="(e) => {save_active = e.target.value.length != 0 }" ."block" ."w-full" ."p-2" ."bg-gray-50" ."border-2" + ."border-gray-300" ."rounded" ."focus:outline-none" ."focus:bg-white" @@ -446,6 +531,9 @@ impl InventoryNewCategoryForm { } } input type="submit" value="Add" + x-bind:disabled="!save_active" + ."enabled:cursor-pointer" + ."disabled:opacity-50" ."py-2" ."border-2" ."rounded" diff --git a/rust/src/components/mod.rs b/rust/src/components/mod.rs index 15897d9..15a9a52 100644 --- a/rust/src/components/mod.rs +++ b/rust/src/components/mod.rs @@ -24,6 +24,7 @@ impl Root { head { title { "Packager" } script src="https://unpkg.com/htmx.org@1.9.2" {} + script src="https://unpkg.com/alpinejs@3.12.1" defer {} script src="https://cdn.tailwindcss.com" {} script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.js" defer {} link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css"; diff --git a/rust/src/components/trip.rs b/rust/src/components/trip/mod.rs similarity index 98% rename from rust/src/components/trip.rs rename to rust/src/components/trip/mod.rs index 825a211..11b8030 100644 --- a/rust/src/components/trip.rs +++ b/rust/src/components/trip/mod.rs @@ -9,6 +9,9 @@ use serde_variant::to_variant_name; use crate::ClientState; pub struct TripManager; +pub mod types; +pub use types::*; + impl TripManager { pub fn build(trips: Vec) -> Markup { html!( @@ -557,7 +560,7 @@ impl TripInfo { ."gap-1" { span { (triptype.name) } - span ."mdi" ."mdi-delete" ."text-sm" {} + span ."mdi" ."mdi-close" ."text-sm" {} } } } @@ -584,6 +587,7 @@ impl TripInfo { ."flex-column" ."items-center" ."hover:bg-green-200" + ."hover:opacity-100" ."gap-1" ."opacity-60" { @@ -623,7 +627,9 @@ pub struct TripComment; impl TripComment { pub fn build(trip: &models::Trip) -> Markup { html!( - div { + div + x-data="{ save_active: false }" + { h1 ."text-xl" ."mb-5" { "Comments" } form @@ -636,6 +642,7 @@ impl TripComment { // https://stackoverflow.com/a/48460773 textarea #"comment" + x-on:input="save_active=true" ."border" ."w-full" ."h-48" name="new-comment" form="edit-comment" @@ -647,11 +654,14 @@ impl TripComment { button type="submit" form="edit-comment" + x-bind:disabled="!save_active" + ."enabled:bg-green-200" + ."enabled:hover:bg-green-400" + ."enabled:cursor-pointer" + ."disabled:opacity-50" + ."disabled:bg-gray-300" ."mt-2" ."border" - ."bg-green-200" - ."hover:bg-green-400" - ."cursor-pointer" ."flex" ."flex-column" ."p-2" diff --git a/rust/src/components/trip/types.rs b/rust/src/components/trip/types.rs new file mode 100644 index 0000000..3a041d1 --- /dev/null +++ b/rust/src/components/trip/types.rs @@ -0,0 +1,163 @@ +use crate::models; +use crate::ClientState; +use maud::{html, Markup}; + +pub struct TypeList; + +impl TypeList { + pub fn build(state: &ClientState, trip_types: Vec) -> Markup { + html!( + div ."p-8" ."flex" ."flex-col" ."gap-8" { + h1 ."text-2xl" {"Trip Types"} + + ul + ."flex" + ."flex-col" + ."items-stretch" + ."border-t" + ."border-l" + ."h-full" + { + @for trip_type in trip_types { + li + ."border-b" + ."border-r" + ."flex" + ."flex-row" + ."justify-between" + ."items-stretch" + { + @if state.trip_type_edit.map_or(false, |id| id == trip_type.id) { + form + ."hidden" + id="edit-trip-type" + action={ (trip_type.id) "/edit/name/submit" } + target="_self" + method="post" + {} + div + ."bg-blue-200" + ."p-2" + ."grow" + { + input + ."bg-blue-100" + ."hover:bg-white" + ."w-full" + type="text" + name="new-value" + form="edit-trip-type" + value=(trip_type.name) + ; + } + div + ."flex" + ."flex-row" + { + a + href="." + ."bg-red-200" + ."hover:bg-red-300" + ."w-8" + ."flex" + { + span + ."mdi" + ."mdi-cancel" + ."text-xl" + ."m-auto" + ; + } + button + type="submit" + form="edit-trip-type" + ."bg-green-200" + ."hover:bg-green-300" + ."w-8" + { + span + ."mdi" + ."mdi-content-save" + ."text-xl" + ; + } + } + } @else { + span + ."p-2" + { + (trip_type.name) + } + + div + ."bg-blue-100" + ."hover:bg-blue-200" + ."p-0" + ."w-8" + { + a + .flex + ."w-full" + ."h-full" + href={ "?edit=" (trip_type.id) } + { + span + ."m-auto" + ."mdi" + ."mdi-pencil" + ."text-xl"; + } + } + } + } + } + } + + form + name="new-trip-type" + action="/trips/types/" + target="_self" + method="post" + ."mt-8" ."p-5" ."border-2" ."border-gray-200" + { + div ."mb-5" ."flex" ."flex-row" { + span ."mdi" ."mdi-playlist-plus" ."text-2xl" ."mr-4" {} + p ."inline" ."text-xl" { "Add new trip type" } + } + div ."w-11/12" ."m-auto" { + div ."mx-auto" ."pb-8" { + div ."flex" ."flex-row" ."justify-center" { + label for="new-trip-type-name" ."font-bold" ."w-1/2" ."p-2" ."text-center" { "Name" } + span ."w-1/2" { + input + type="text" + id="new-trip-type-name" + name="new-trip-type-name" + ."block" + ."w-full" + ."p-2" + ."bg-gray-50" + ."border-2" + ."rounded" + ."focus:outline-none" + ."focus:bg-white" + {} + } + } + } + input + type="submit" + value="Add" + ."py-2" + ."border-2" + ."rounded" + ."border-gray-300" + ."mx-auto" + ."w-full" + {} + } + } + } + ) + } +} diff --git a/rust/src/main.rs b/rust/src/main.rs index 2326f23..440e04e 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -47,6 +47,7 @@ pub struct ClientState { pub active_category_id: Option, pub edit_item: Option, pub trip_edit_attribute: Option, + pub trip_type_edit: Option, } impl ClientState { @@ -55,6 +56,7 @@ impl ClientState { active_category_id: None, edit_item: None, trip_edit_attribute: None, + trip_type_edit: None, } } } @@ -102,6 +104,11 @@ async fn main() -> Result<(), sqlx::Error> { .route("/assets/luggage.svg", get(icon_handler)) .route("/", get(root)) .route("/trips/", get(trips)) + .route("/trips/types/", get(trips_types).post(trip_type_create)) + .route( + "/trips/types/:id/edit/name/submit", + post(trips_types_edit_name), + ) .route("/trip/", post(trip_create)) .route("/trip/:id/", get(trip)) .route("/trip/:id/comment/submit", post(trip_comment_set)) @@ -119,6 +126,10 @@ async fn main() -> Result<(), sqlx::Error> { .route("/inventory/", get(inventory_inactive)) .route("/inventory/category/", post(inventory_category_create)) .route("/inventory/item/", post(inventory_item_create)) + .route( + "/inventory/item/name/validate", + post(inventory_item_validate_name), + ) .route("/inventory/category/:id/", get(inventory_active)) .route("/inventory/item/:id/delete", get(inventory_item_delete)) .route("/inventory/item/:id/edit", post(inventory_item_edit)) @@ -250,10 +261,62 @@ struct NewItem { category_id: Uuid, } +#[derive(Deserialize)] +struct NewItemName { + #[serde(rename = "new-item-name")] + name: String, +} + +async fn inventory_item_validate_name( + State(state): State, + Form(new_item): Form, +) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { + let results = query!( + "SELECT id + FROM inventory_items + WHERE name = ?", + new_item.name, + ) + .fetch(&state.database_pool) + .map_ok(|_| Ok(())) + .try_collect::>>() + .await + // we have two error handling lines here. these are distinct errors + // this one is the SQL error that may arise during the query + .map_err(|e| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + ErrorPage::build(&e.to_string()), + ) + })? + .into_iter() + .collect::, models::Error>>() + // and this one is the model mapping error that may arise e.g. during + // reading of the rows + .map_err(|e| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + ErrorPage::build(&e.to_string()), + ) + })?; + + Ok(( + StatusCode::OK, + InventoryNewItemFormName::build(Some(&new_item.name), !results.is_empty()), + )) +} + async fn inventory_item_create( State(state): State, Form(new_item): Form, ) -> Result { + if new_item.name.len() == 0 { + return Err(( + StatusCode::UNPROCESSABLE_ENTITY, + "name cannot be empty".to_string(), + )); + } + let id = Uuid::new_v4(); let id_param = id.to_string(); let name = &new_item.name; @@ -411,6 +474,13 @@ async fn inventory_item_edit( Path(id): Path, Form(edit_item): Form, ) -> Result { + if edit_item.name.len() == 0 { + return Err(( + StatusCode::UNPROCESSABLE_ENTITY, + ErrorPage::build("name cannot be empty"), + )); + } + let id = Item::update( &state.database_pool, id, @@ -469,6 +539,13 @@ async fn trip_create( State(state): State, Form(new_trip): Form, ) -> Result { + if new_trip.name.len() == 0 { + return Err(( + StatusCode::UNPROCESSABLE_ENTITY, + "name cannot be empty".to_string(), + )); + } + let id = Uuid::new_v4(); let id_param = id.to_string(); let date_start = new_trip @@ -532,8 +609,6 @@ async fn trip_create( async fn trips( State(state): State, ) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { - tracing::info!("receiving trips"); - let trips: Vec = query_as!( DbTripRow, "SELECT @@ -571,8 +646,6 @@ async fn trips( ) })?; - tracing::info!("received trips"); - Ok(( StatusCode::OK, Root::build(TripManager::build(trips), &TopLevelPage::Trips), @@ -811,6 +884,14 @@ async fn trip_edit_attribute( Path((trip_id, attribute)): Path<(Uuid, TripAttribute)>, Form(trip_update): Form, ) -> Result { + if let TripAttribute::Name = attribute { + if trip_update.new_value.len() == 0 { + return Err(( + StatusCode::UNPROCESSABLE_ENTITY, + ErrorPage::build("name cannot be empty"), + )); + } + } let result = query(&format!( "UPDATE trips SET {attribute} = ? @@ -980,6 +1061,13 @@ async fn inventory_category_create( State(state): State, Form(new_category): Form, ) -> Result { + if new_category.name.len() == 0 { + return Err(( + StatusCode::UNPROCESSABLE_ENTITY, + ErrorPage::build("name cannot be empty"), + )); + } + let id = Uuid::new_v4(); let id_param = id.to_string(); query!( @@ -1059,3 +1147,161 @@ async fn trip_state_set( Ok(Redirect::to(&format!("/trip/{id}/", id = trip_id))) } } + +#[derive(Debug, Deserialize)] +struct TripTypeQuery { + edit: Option, +} + +async fn trips_types( + State(mut state): State, + Query(trip_type_query): Query, +) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { + state.client_state.trip_type_edit = trip_type_query.edit; + + let trip_types: Vec = query_as!( + DbTripsTypesRow, + "SELECT + id, + name + FROM trips_types", + ) + .fetch(&state.database_pool) + .map_ok(|row| row.try_into()) + .try_collect::>>() + .await + // we have two error handling lines here. these are distinct errors + // this one is the SQL error that may arise during the query + .map_err(|e| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + ErrorPage::build(&e.to_string()), + ) + })? + .into_iter() + .collect::, models::Error>>() + // and this one is the model mapping error that may arise e.g. during + // reading of the rows + .map_err(|e| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + ErrorPage::build(&e.to_string()), + ) + })?; + + Ok(( + StatusCode::OK, + Root::build( + components::trip::TypeList::build(&state.client_state, trip_types), + &TopLevelPage::Trips, + ), + )) +} + +#[derive(Deserialize)] +struct NewTripType { + #[serde(rename = "new-trip-type-name")] + name: String, +} + +async fn trip_type_create( + State(state): State, + Form(new_trip_type): Form, +) -> Result { + if new_trip_type.name.len() == 0 { + return Err(( + StatusCode::UNPROCESSABLE_ENTITY, + "name cannot be empty".to_string(), + )); + } + + let id = Uuid::new_v4(); + let id_param = id.to_string(); + query!( + "INSERT INTO trips_types + (id, name) + VALUES + (?, ?)", + id_param, + new_trip_type.name, + ) + .execute(&state.database_pool) + .await + .map_err(|e| match e { + sqlx::Error::Database(ref error) => { + let sqlite_error = error.downcast_ref::(); + if let Some(code) = sqlite_error.code() { + match &*code { + "2067" => { + // SQLITE_CONSTRAINT_UNIQUE + ( + StatusCode::BAD_REQUEST, + format!( + "trip type with name \"{name}\" already exists", + name = new_trip_type.name, + ), + ) + } + _ => ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("got error with unknown code: {}", sqlite_error.to_string()), + ), + } + } else { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("got error without code: {}", sqlite_error.to_string()), + ) + } + } + _ => ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("got unknown error: {}", e.to_string()), + ), + })?; + + Ok(Redirect::to("/trips/types/")) +} + +#[derive(Deserialize)] +struct TripTypeUpdate { + #[serde(rename = "new-value")] + new_value: String, +} + +async fn trips_types_edit_name( + State(state): State, + Path(trip_type_id): Path, + Form(trip_update): Form, +) -> Result { + if trip_update.new_value.len() == 0 { + return Err(( + StatusCode::UNPROCESSABLE_ENTITY, + ErrorPage::build("name cannot be empty"), + )); + } + + let id_param = trip_type_id.to_string(); + let result = query!( + "UPDATE trips_types + SET name = ? + WHERE id = ?", + trip_update.new_value, + id_param, + ) + .execute(&state.database_pool) + .await + .map_err(|e| (StatusCode::BAD_REQUEST, ErrorPage::build(&e.to_string())))?; + + if result.rows_affected() == 0 { + Err(( + StatusCode::NOT_FOUND, + ErrorPage::build(&format!( + "tript type with id {id} not found", + id = trip_type_id + )), + )) + } else { + Ok(Redirect::to("/trips/types/")) + } +} diff --git a/rust/src/models.rs b/rust/src/models.rs index 8126c7e..473dbc2 100644 --- a/rust/src/models.rs +++ b/rust/src/models.rs @@ -288,69 +288,6 @@ pub enum TripAttribute { TempMax, } -// impl std::convert::Into<&'static str> for TripAttribute { -// fn into(self) -> &'static str { -// match self { -// Self::DateStart => "date_start", -// Self::DateEnd => "date_end", -// Self::Location => "location", -// Self::TempMin => "temp_min", -// Self::TempMax => "temp_max", -// } -// } -// } - -// impl std::convert::TryFrom<&str> for TripAttribute { -// type Error = Error; - -// fn try_from(value: &str) -> Result { -// Ok(match value { -// "date_start" => Self::DateStart, -// "date_end" => Self::DateEnd, -// "location" => Self::Location, -// "temp_min" => Self::TempMin, -// "temp_max" => Self::TempMax, -// _ => { -// return Err(Error::UnknownAttributeValue { -// attribute: value.to_string(), -// }) -// } -// }) -// } -// } - -// impl TryFrom for Trip { -// type Error = Error; - -// fn try_from(row: SqliteRow) -> Result { -// let name: &str = row.try_get("name")?; -// let id: &str = row.try_get("id")?; -// let date_start: time::Date = row.try_get("date_start")?; -// let date_end: time::Date = row.try_get("date_end")?; -// let state: TripState = row.try_get("state")?; -// let location = row.try_get("location")?; -// let temp_min = row.try_get("temp_min")?; -// let temp_max = row.try_get("temp_max")?; -// let comment = row.try_get("comment")?; - -// let id: Uuid = Uuid::try_parse(id)?; - -// Ok(Trip { -// id, -// name: name.to_string(), -// date_start, -// date_end, -// state, -// location, -// temp_min, -// temp_max, -// comment, -// types: None, -// categories: None, -// }) -// } -// } - impl<'a> Trip { pub fn types(&'a self) -> &Vec { self.types @@ -614,18 +551,6 @@ pub struct TripType { pub active: bool, } -// impl TryFrom for TripType { -// type Error = Error; - -// fn try_from(row: SqliteRow) -> Result { -// let id: Uuid = Uuid::try_parse(row.try_get("id")?)?; -// let name: String = row.try_get::<&str, _>("name")?.to_string(); -// let active: bool = row.try_get("active")?; - -// Ok(Self { id, name, active }) -// } -// } - pub struct DbCategoryRow { pub id: String, pub name: String, @@ -653,23 +578,6 @@ impl TryFrom for Category { } } -// impl TryFrom for Category { -// type Error = Error; - -// fn try_from(row: SqliteRow) -> Result { -// let name: &str = row.try_get("name")?; -// let description: &str = row.try_get("description")?; -// let id: Uuid = Uuid::try_parse(row.try_get("id")?)?; - -// Ok(Category { -// id, -// name: name.to_string(), -// description: description.to_string(), -// items: None, -// }) -// } -// } - pub struct DbInventoryItemsRow { id: String, name: String, @@ -741,26 +649,6 @@ impl TryFrom for Item { } } -// impl TryFrom for Item { -// type Error = Error; - -// fn try_from(row: SqliteRow) -> Result { -// let name: &str = row.try_get("name")?; -// let description: &str = row.try_get("description")?; -// let weight: i64 = row.try_get("weight")?; -// let id: Uuid = Uuid::try_parse(row.try_get("id")?)?; -// let category_id: Uuid = Uuid::try_parse(row.try_get("category_id")?)?; - -// Ok(Item { -// id, -// name: name.to_string(), -// weight, -// description: description.to_string(), -// category_id, -// }) -// } -// } - impl Item { pub async fn find(pool: &sqlx::Pool, id: Uuid) -> Result, Error> { let id_param = id.to_string(); @@ -820,3 +708,30 @@ impl Item { } } } + +pub 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 for TripsType { + type Error = Error; + + fn try_from(row: DbTripsTypesRow) -> Result { + Ok(TripsType { + id: Uuid::try_parse(&row.id)?, + name: row.name, + }) + } +}