From aac192eb975b36a6f26ba911014f707635222632 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Tue, 29 Aug 2023 21:34:00 +0200 Subject: [PATCH] add more stuff --- rust/Cargo.toml | 4 + rust/migrations/20230523190551_product.sql | 14 +- rust/migrations/20230523212341_finalize.sql | 0 rust/migrations/20230523221247_finalize2.sql | 3 + rust/sqlx-data.json | 200 ++++++++- rust/src/components/inventory.rs | 55 ++- rust/src/components/mod.rs | 6 +- rust/src/components/trip/mod.rs | 443 +++++++++++-------- rust/src/components/trip/types.rs | 9 +- rust/src/main.rs | 148 ++++++- rust/src/models.rs | 204 ++++++++- 11 files changed, 872 insertions(+), 214 deletions(-) create mode 100644 rust/migrations/20230523212341_finalize.sql create mode 100644 rust/migrations/20230523221247_finalize2.sql diff --git a/rust/Cargo.toml b/rust/Cargo.toml index e5253a9..a873429 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -3,6 +3,10 @@ name = "packager" version = "0.1.0" edition = "2021" +[profile.dev] +opt-level = 0 +lto = "off" + [dependencies.clap] version = "4.3.0" features = ["derive"] diff --git a/rust/migrations/20230523190551_product.sql b/rust/migrations/20230523190551_product.sql index 4e9e092..c5fa78d 100644 --- a/rust/migrations/20230523190551_product.sql +++ b/rust/migrations/20230523190551_product.sql @@ -7,7 +7,7 @@ CREATE TABLE "inventory_products" ( UNIQUE (name) ); -CREATE TABLE "inventory_items_tmp" +CREATE TABLE "inventory_items_tmp" ( id VARCHAR(36) NOT NULL, name TEXT NOT NULL, description TEXT, @@ -16,8 +16,12 @@ CREATE TABLE "inventory_items_tmp" product_id VARCHAR(36), PRIMARY KEY (id), FOREIGN KEY (category_id) REFERENCES inventory_items_categories(id) - FOREIGN KEY (product_id) REFERENCES inventory_products(id); -) + FOREIGN KEY (product_id) REFERENCES inventory_products(id) +); + +INSERT INTO inventory_items_tmp SELECT *, NULL as product_id FROM inventory_items; + +/* DROP TABLE inventory_items; */ + +/* ALTER TABLE "inventory_items_tmp" RENAME TO inventory_items; */ -ALTER TABLE "inventory_items" - FOREIGN KEY (product_id) REFERENCES inventory_products(id); diff --git a/rust/migrations/20230523212341_finalize.sql b/rust/migrations/20230523212341_finalize.sql new file mode 100644 index 0000000..e69de29 diff --git a/rust/migrations/20230523221247_finalize2.sql b/rust/migrations/20230523221247_finalize2.sql new file mode 100644 index 0000000..5f1668c --- /dev/null +++ b/rust/migrations/20230523221247_finalize2.sql @@ -0,0 +1,3 @@ +DROP TABLE inventory_items; + +ALTER TABLE "inventory_items_tmp" RENAME TO inventory_items; diff --git a/rust/sqlx-data.json b/rust/sqlx-data.json index 2b7c0ad..0fbb4a1 100644 --- a/rust/sqlx-data.json +++ b/rust/sqlx-data.json @@ -120,6 +120,48 @@ }, "query": "INSERT INTO trips_to_trips_types\n (trip_id, trip_type_id) VALUES (?, ?)" }, + "53c45b9447118c8b448df6836e68a38ff78d0b7f4d5344a1e5064d31e70396b3": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "name", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "weight", + "ordinal": 2, + "type_info": "Int64" + }, + { + "name": "description", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "category_id", + "ordinal": 4, + "type_info": "Text" + } + ], + "nullable": [ + false, + false, + false, + true, + false + ], + "parameters": { + "Right": 1 + } + }, + "query": "SELECT\n id,\n name,\n weight,\n description,\n category_id\n FROM inventory_items AS item\n WHERE item.id = ?" + }, "68304c19a0bee12c0b3ce9740d53389620b20e47973b41975678dbd13bd30c7f": { "describe": { "columns": [], @@ -344,6 +386,24 @@ }, "query": "INSERT INTO trips\n (id, name, date_start, date_end, state)\n VALUES\n (?, ?, ?, ?, ?)" }, + "9c4c2eb24747c3d8157ad17bacbe074d906d2891e8c68160becd8e10d045cfbc": { + "describe": { + "columns": [ + { + "name": "weight", + "ordinal": 0, + "type_info": "Int" + } + ], + "nullable": [ + true + ], + "parameters": { + "Right": 1 + } + }, + "query": "\n SELECT COALESCE(MAX(i_item.weight), 0) as weight\n FROM inventory_items_categories as category\n INNER JOIN inventory_items as i_item\n ON i_item.category_id = category.id\n WHERE category_id = (\n SELECT category_id\n FROM inventory_items\n WHERE inventory_items.id = ?\n )\n " + }, "a81bcbeb11260e3b4363e19c26b71b489e326b08bfacb6e11b4c4fc068dc7806": { "describe": { "columns": [ @@ -384,7 +444,17 @@ }, "query": "DELETE FROM inventory_items\n WHERE id = ?" }, - "b916db63913aa222cef4552dffdcda0f26f16612fbb4c1e839bfd0162888fdc3": { + "ded3be1c8894a64e3b5f749461db7261d9224abb8a54da980db8262733d08205": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 2 + } + }, + "query": "INSERT INTO trips_types\n (id, name)\n VALUES\n (?, ?)" + }, + "efcf56aacc622556fc10220edb57ea69822eed50cdf9ef54bc48a7fb04d4ee9a": { "describe": { "columns": [ { @@ -411,6 +481,36 @@ "name": "category_id", "ordinal": 4, "type_info": "Text" + }, + { + "name": "category_name", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "category_description", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "product_id", + "ordinal": 7, + "type_info": "Text" + }, + { + "name": "product_name", + "ordinal": 8, + "type_info": "Text" + }, + { + "name": "product_description", + "ordinal": 9, + "type_info": "Text" + }, + { + "name": "product_comment", + "ordinal": 10, + "type_info": "Text" } ], "nullable": [ @@ -418,23 +518,19 @@ false, true, false, - false + false, + false, + true, + true, + true, + true, + true ], "parameters": { "Right": 1 } }, - "query": "SELECT * FROM inventory_items AS item\n WHERE item.id = ?" - }, - "ded3be1c8894a64e3b5f749461db7261d9224abb8a54da980db8262733d08205": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Right": 2 - } - }, - "query": "INSERT INTO trips_types\n (id, name)\n VALUES\n (?, ?)" + "query": "SELECT\n item.id AS id,\n item.name AS name,\n item.description AS description,\n weight,\n category.id AS category_id,\n category.name AS category_name,\n category.description AS category_description,\n product.id AS product_id,\n product.name AS product_name,\n product.description AS product_description,\n product.comment AS product_comment\n FROM inventory_items AS item\n INNER JOIN inventory_items_categories as category\n ON item.category_id = category.id\n LEFT JOIN inventory_products AS product\n ON item.product_id = product.id\n WHERE item.id = ?" }, "f2038d75ff5ff10d4baeb30b9dc4cc1c991da1facdb1f05e16f271372eee0c7a": { "describe": { @@ -524,6 +620,84 @@ }, "query": "\n SELECT\n category.id as category_id,\n category.name as category_name,\n category.description AS category_description,\n inner.trip_id AS trip_id,\n inner.item_id AS item_id,\n inner.item_name AS item_name,\n inner.item_description AS item_description,\n inner.item_weight AS item_weight,\n inner.item_is_picked AS item_is_picked,\n inner.item_is_packed AS item_is_packed,\n inner.item_is_new AS item_is_new\n FROM inventory_items_categories AS category\n LEFT JOIN (\n SELECT\n trip.trip_id AS trip_id,\n category.id as category_id,\n category.name as category_name,\n category.description as category_description,\n item.id as item_id,\n item.name as item_name,\n item.description as item_description,\n item.weight as item_weight,\n trip.pick as item_is_picked,\n trip.pack as item_is_packed,\n trip.new as item_is_new\n FROM trips_items as trip\n INNER JOIN inventory_items as item\n ON item.id = trip.item_id\n INNER JOIN inventory_items_categories as category\n ON category.id = item.category_id\n WHERE trip.trip_id = ?\n ) AS inner\n ON inner.category_id = category.id\n " }, + "f3fd58ae5e462c354d76fcfce1e86ffcdab80d507cc6599adebcdaea8bb2bc6f": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "picked", + "ordinal": 1, + "type_info": "Bool" + }, + { + "name": "packed", + "ordinal": 2, + "type_info": "Bool" + }, + { + "name": "new", + "ordinal": 3, + "type_info": "Bool" + }, + { + "name": "name", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "description", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "weight", + "ordinal": 6, + "type_info": "Int64" + }, + { + "name": "category_id", + "ordinal": 7, + "type_info": "Text" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + true, + false, + false + ], + "parameters": { + "Right": 2 + } + }, + "query": "\n SELECT\n t_item.item_id AS id,\n t_item.pick AS picked,\n t_item.pack AS packed,\n t_item.new AS new,\n i_item.name AS name,\n i_item.description AS description,\n i_item.weight AS weight,\n i_item.category_id AS category_id\n FROM trips_items AS t_item\n INNER JOIN inventory_items AS i_item\n ON i_item.id = t_item.item_id\n WHERE t_item.item_id = ?\n AND t_item.trip_id = ?\n " + }, + "f9d080a5b8710c7d6a497bb1f5cf4839ad1589fd7d6a06d3faf1163d6981d8a0": { + "describe": { + "columns": [ + { + "name": "weight", + "ordinal": 0, + "type_info": "Int" + } + ], + "nullable": [ + true + ], + "parameters": { + "Right": 1 + } + }, + "query": "\n SELECT COALESCE(SUM(i_item.weight), 0) as weight\n FROM inventory_items_categories as category\n INNER JOIN inventory_items as i_item\n ON i_item.category_id = category.id\n INNER JOIN trips_items as t_item\n ON i_item.id = t_item.item_id\n WHERE category_id = ?\n AND t_item.pick = 1\n " + }, "ff260eef6f95a3c1f8e2f822808ac250925dc0971b9bddd9015b8b24643357c9": { "describe": { "columns": [ diff --git a/rust/src/components/inventory.rs b/rust/src/components/inventory.rs index fbe8a26..ee90095 100644 --- a/rust/src/components/inventory.rs +++ b/rust/src/components/inventory.rs @@ -1,5 +1,6 @@ use maud::{html, Markup, PreEscaped}; +use crate::models; use crate::models::*; use crate::ClientState; use uuid::{uuid, Uuid}; @@ -221,7 +222,8 @@ impl InventoryItemList { ."m-auto" ."mdi" ."mdi-content-save" - ."text-xl"; + ."text-xl" + {} } } td @@ -243,7 +245,8 @@ impl InventoryItemList { ."m-auto" ."mdi" ."mdi-cancel" - ."text-xl"; + ."text-xl" + {} } } } @@ -282,7 +285,7 @@ impl InventoryItemList { ."w-full" href=(format!("?edit_item={id}", id = item.id)) { - span ."m-auto" ."mdi" ."mdi-pencil" ."text-xl"; + span ."m-auto" ."mdi" ."mdi-pencil" ."text-xl" {} } } td @@ -299,7 +302,7 @@ impl InventoryItemList { ."w-full" href=(format!("/inventory/item/{id}/delete", id = item.id)) { - span ."m-auto" ."mdi" ."mdi-delete" ."text-xl"; + span ."m-auto" ."mdi" ."mdi-delete" ."text-xl" {} } } } @@ -348,7 +351,7 @@ impl InventoryNewItemFormName { ."focus:bg-white" ."focus:border-purple-500"[!error] value=[value] - ; + {} @if error { div ."col-start-2" @@ -549,3 +552,45 @@ impl InventoryNewCategoryForm { ) } } + +pub struct InventoryItem; + +impl InventoryItem { + pub fn build(_state: &ClientState, item: &models::InventoryItem) -> Markup { + html!( + div ."p-8" { + table + ."table" + ."table-auto" + ."border-collapse" + ."border-spacing-0" + ."border" + ."w-full" + { + tbody { + tr ."h-10" ."even:bg-gray-100" ."hover:bg-purple-100" ."h-full" { + td ."border" ."p-2" { "Name" } + td ."border" ."p-2" { (item.name) } + } + tr ."h-10" ."even:bg-gray-100" ."hover:bg-purple-100" ."h-full" { + td ."border" ."p-2" { "Description" } + td ."border" ."p-2" { (item.description.clone().unwrap_or("".to_string())) } + } + tr ."h-10" ."even:bg-gray-100" ."hover:bg-purple-100" ."h-full" { + td ."border" ."p-2" { "Weight" } + td ."border" ."p-2" { (item.weight.to_string()) } + } + tr ."h-10" ."even:bg-gray-100" ."hover:bg-purple-100" ."h-full" { + td ."border" ."p-2" { "Category" } + td ."border" ."p-2" { (item.category.name) } + } + } + } + @match item.product { + Some(ref product) => p { "this item is part of product" (product.name) }, + None => p { "this item is not part of a product" }, + } + } + ) + } +} diff --git a/rust/src/components/mod.rs b/rust/src/components/mod.rs index 99012ff..38c123a 100644 --- a/rust/src/components/mod.rs +++ b/rust/src/components/mod.rs @@ -27,8 +27,8 @@ impl Root { 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"; - link rel="shortcut icon" type="image/svg+xml" href="/favicon.svg"; + link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css" {} + link rel="shortcut icon" type="image/svg+xml" href="/favicon.svg" {} script { (PreEscaped(include_str!(concat!(env!("CARGO_MANIFEST_DIR"),"/js/app.js")))) } } body @@ -53,7 +53,7 @@ impl Root { ."items-center" ."gap-3" { - img ."h-12" src="/assets/luggage.svg"; + img ."h-12" src="/assets/luggage.svg" {} a #home href="/" { "Packager" } } nav diff --git a/rust/src/components/trip/mod.rs b/rust/src/components/trip/mod.rs index 7adf87a..7573301 100644 --- a/rust/src/components/trip/mod.rs +++ b/rust/src/components/trip/mod.rs @@ -237,7 +237,7 @@ impl Trip { name="new-value" form="edit-trip" value=(trip.name) - ; + {} a href="." ."bg-red-200" @@ -250,7 +250,7 @@ impl Trip { ."mdi-cancel" ."text-xl" ."m-auto" - ; + {} } button type="submit" @@ -263,7 +263,7 @@ impl Trip { ."mdi" ."mdi-content-save" ."text-xl" - ; + {} } } } @@ -277,7 +277,7 @@ impl Trip { ."mdi-pencil" ."text-xl" ."opacity-50" - ; + {} } } } @@ -308,10 +308,9 @@ impl TripInfoRow { name="edit-trip" id="edit-trip" action=(format!("edit/{key}/submit", key=(to_variant_name(&attribute_key).unwrap()) )) - htmx-push-url="true" target="_self" method="post" - ; + {} } tr .h-full { @if edit { @@ -324,7 +323,7 @@ impl TripInfoRow { name="new-value" form="edit-trip" value=(value.map_or(String::new(), |v| v.to_string())) - ; + {} } } td @@ -347,7 +346,8 @@ impl TripInfoRow { ."m-auto" ."mdi" ."mdi-cancel" - ."text-xl"; + ."text-xl" + {} } } td @@ -370,7 +370,8 @@ impl TripInfoRow { ."m-auto" ."mdi" ."mdi-content-save" - ."text-xl"; + ."text-xl" + {} } } } @else { @@ -395,7 +396,8 @@ impl TripInfoRow { ."m-auto" ."mdi" ."mdi-pencil" - ."text-xl"; + ."text-xl" + {} } } } @@ -493,7 +495,8 @@ impl TripInfo { ."m-auto" ."mdi" ."mdi-step-backward" - ."text-xl"; + ."text-xl" + {} } } } @@ -525,7 +528,8 @@ impl TripInfo { ."m-auto" ."mdi" ."mdi-step-forward" - ."text-xl"; + ."text-xl" + {} } } } @@ -658,7 +662,7 @@ impl TripComment { action="comment/submit" target="_self" method="post" - ; + {} // https://stackoverflow.com/a/48460773 textarea @@ -689,7 +693,7 @@ impl TripComment { ."gap-2" ."items-center" { - span ."mdi" ."mdi-content-save" ."text-xl"; + span ."mdi" ."mdi-content-save" ."text-xl" {} span { "Save" } } } @@ -735,6 +739,105 @@ impl TripItems { } } +pub struct TripCategoryListRow; + +impl TripCategoryListRow { + pub fn build( + category: &TripCategory, + active: bool, + has_new_items: bool, + biggest_category_weight: i64, + ) -> Markup { + html!( + tr + id={"category-" (category.category.id)} + ."h-10" + ."hover:bg-purple-100" + ."m-3" + ."h-full" + ."outline"[active] + ."outline-2"[active] + ."outline-indigo-300"[active] + { + + td + + ."border" + ."m-0" + + { + div + ."p-0" + ."flex" + ."flex-row" + ."items-center" + ."group" + { + a + id="select-category" + href=( + format!( + "?category={id}", + id=category.category.id + ) + ) + ."inline-block" + ."p-2" + ."m-0" + ."w-full" + ."grow" + ."font-bold"[active] + { + (category.category.name.clone()) + } + @if has_new_items { + div + ."mr-2" + ."flex" + ."flex-row" + ."items-center" + { + p + ."hidden" + ."group-hover:inline" + ."text-sm" + ."text-gray-500" + ."grow" + { + "new items" + } + span + ."mdi" + ."mdi-exclamation-thick" + ."text-xl" + ."text-yellow-400" + ."grow-0" + {} + } + } + } + } + td ."border" ."m-0" ."p-2" style="position:relative;" { + p { + (category.total_picked_weight().to_string()) + } + div ."bg-blue-600" ."h-1.5" + style=( + format!( + "width: {width}%;position:absolute;left:0;bottom:0;right:0;", + width=( + (category.total_picked_weight() as f64) + / (biggest_category_weight as f64) + * 100.0 + ) + ) + ) {} + } + } + ) + } +} + pub struct TripCategoryList; impl TripCategoryList { @@ -771,90 +874,7 @@ impl TripCategoryList { @for category in trip.categories() { @let has_new_items = category.items.as_ref().unwrap().iter().any(|item| item.new); @let active = state.active_category_id.map_or(false, |id| category.category.id == id); - tr - ."h-10" - ."hover:bg-purple-100" - ."m-3" - ."h-full" - ."outline"[active] - ."outline-2"[active] - ."outline-indigo-300"[active] - { - - td - - ."border" - ."m-0" - - { - div - ."p-0" - ."flex" - ."flex-row" - ."items-center" - ."group" - { - a - id="select-category" - href=( - format!( - "?category={id}", - id=category.category.id - ) - ) - ."inline-block" - ."p-2" - ."m-0" - ."w-full" - ."grow" - ."font-bold"[active] - { - (category.category.name.clone()) - } - @if has_new_items { - div - ."mr-2" - ."flex" - ."flex-row" - ."items-center" - { - p - ."hidden" - ."group-hover:inline" - ."text-sm" - ."text-gray-500" - ."grow" - { - "new items" - } - span - ."mdi" - ."mdi-exclamation-thick" - ."text-xl" - ."text-yellow-400" - ."grow-0" - ; - } - } - } - } - td ."border" ."m-0" ."p-2" style="position:relative;" { - p { - (category.total_picked_weight().to_string()) - } - div ."bg-blue-600" ."h-1.5" - style=( - format!( - "width: {width}%;position:absolute;left:0;bottom:0;right:0;", - width=( - (category.total_picked_weight() as f64) - / (biggest_category_weight as f64) - * 100.0 - ) - ) - ) {} - } - } + (TripCategoryListRow::build(category, active, has_new_items, biggest_category_weight)) } tr ."h-10" ."hover:bg-purple-200" ."bg-gray-300" ."font-bold" { td ."border" ."p-0" ."m-0" { @@ -894,7 +914,7 @@ impl TripItemList { table ."table" ."table-auto" - .table-fixed + ."table-fixed" ."border-collapse" ."border-spacing-0" ."border" @@ -910,86 +930,7 @@ impl TripItemList { } tbody { @for item in items { - tr ."h-10" ."even:bg-gray-100" ."hover:bg-purple-100" { - td { - a - href={ - "/trip/" (trip.id) - "/items/" (item.item.id) - "/" (if item.picked { "unpick" } else { "pick" }) } - ."inline-block" - ."p-2" - ."m-0" - ."w-full" - ."justify-center" - ."content-center" - ."flex" - { - input - type="checkbox" - checked[item.picked] - autocomplete="off" - ; - } - } - td { - a - href={ - "/trip/" (trip.id) - "/items/" (item.item.id) - "/" (if item.packed { "unpack" } else { "pack" }) } - ."inline-block" - ."p-2" - ."m-0" - ."w-full" - ."justify-center" - ."content-center" - ."flex" - { - input - type="checkbox" - checked[item.packed] - autocomplete="off" - ; - } - } - td ."border" ."p-0" { - div - ."flex" - ."flex-row" - ."items-center" - { - a - ."p-2" ."w-full" ."inline-block" - href=( - format!("/inventory/item/{id}/", id=item.item.id) - ) - { - (item.item.name.clone()) - } - @if item.new { - div ."mr-2" { - span - ."mdi" - ."mdi-exclamation-thick" - ."text-xl" - ."text-yellow-400" - ."grow-0" - ; - } - } - } - } - td ."border" ."p-2" style="position:relative;" { - p { (item.item.weight.to_string()) } - div ."bg-blue-600" ."h-1.5" style=(format!(" - width: {width}%; - position:absolute; - left:0; - bottom:0; - right:0;", width=((item.item.weight as f64) / (biggest_item_weight as f64) * 100.0))) {} - } - } + (TripItemListRow::build(trip.id, item, biggest_item_weight)) } } } @@ -997,3 +938,147 @@ impl TripItemList { ) } } + +pub struct TripItemListRow; + +impl TripItemListRow { + pub fn build(trip_id: Uuid, item: &models::TripItem, biggest_item_weight: i64) -> Markup { + html!( + tr ."h-10" { + td + ."border" + ."p-0" + { + a + href={ + "/trip/" (trip_id) + "/items/" (item.item.id) + "/" (if item.picked { "unpick" } else { "pick" }) } + hx-post={ + "/trip/" (trip_id) + "/items/" (item.item.id) + "/" (if item.picked { "unpick" } else { "pick" }) } + hx-target="closest tr" + hx-swap="outerHTML" + ."inline-block" + ."p-2" + ."m-0" + ."w-full" + ."justify-center" + ."content-center" + ."flex" + ."bg-green-200"[item.picked] + ."hover:bg-green-100"[!item.picked] + ."hover:bg-red-100"[item.picked] + { + @if item.picked { + span + ."mdi" + ."mdi-clipboard-text-outline" + ."text-2xl" + {} + } @else { + span + ."mdi" + ."mdi-clipboard-text-off-outline" + ."text-2xl" + {} + } + } + } + td + ."border" + ."p-0" + { + @if item.picked { + a + href={ + "/trip/" (trip_id) + "/items/" (item.item.id) + "/" (if item.packed { "unpack" } else { "pack" }) } + hx-post={ + "/trip/" (trip_id) + "/items/" (item.item.id) + "/" (if item.packed { "unpack" } else { "pack" }) } + hx-target="closest tr" + hx-swap="outerHTML" + ."inline-block" + ."p-2" + ."m-0" + ."w-full" + ."justify-center" + ."content-center" + ."flex" + ."bg-green-200"[item.packed] + ."hover:bg-green-100"[!item.packed] + ."hover:bg-red-100"[item.packed] + { + @if item.packed { + span + ."mdi" + ."mdi-bag-personal-outline" + ."text-2xl" + {} + } @else { + span + ."mdi" + ."mdi-bag-personal-off-outline" + ."text-2xl" + {} + } + } + } @else { + div + ."flex" + ."justify-center" + ."items-center" + { + span + ."mdi" + ."mdi-bag-personal-outline" + ."text-2xl" + ."text-gray-300" + {} + } + } + } + td ."border" ."p-0" { + div + ."flex" + ."flex-row" + ."items-center" + { + a + ."p-2" ."w-full" ."inline-block" + href=( + format!("/inventory/item/{id}/", id=item.item.id) + ) + { + (item.item.name.clone()) + } + @if item.new { + div ."mr-2" { + span + ."mdi" + ."mdi-exclamation-thick" + ."text-xl" + ."text-yellow-400" + ."grow-0" + {} + } + } + } + } + td ."border" ."p-2" style="position:relative;" { + p { (item.item.weight.to_string()) } + div ."bg-blue-600" ."h-1.5" style=(format!(" + width: {width}%; + position:absolute; + left:0; + bottom:0; + right:0;", width=((item.item.weight as f64) / (biggest_item_weight as f64) * 100.0))) {} + } + } + ) + } +} diff --git a/rust/src/components/trip/types.rs b/rust/src/components/trip/types.rs index 3a041d1..8a45dee 100644 --- a/rust/src/components/trip/types.rs +++ b/rust/src/components/trip/types.rs @@ -48,7 +48,7 @@ impl TypeList { name="new-value" form="edit-trip-type" value=(trip_type.name) - ; + {} } div ."flex" @@ -66,7 +66,7 @@ impl TypeList { ."mdi-cancel" ."text-xl" ."m-auto" - ; + {} } button type="submit" @@ -79,7 +79,7 @@ impl TypeList { ."mdi" ."mdi-content-save" ."text-xl" - ; + {} } } } @else { @@ -105,7 +105,8 @@ impl TypeList { ."m-auto" ."mdi" ."mdi-pencil" - ."text-xl"; + ."text-xl" + {} } } } diff --git a/rust/src/main.rs b/rust/src/main.rs index 9c960b5..7ca8dbe 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -9,6 +9,8 @@ use axum::{ Form, Router, }; +use maud::html; + use std::str::FromStr; use serde_variant::to_variant_name; @@ -128,12 +130,25 @@ async fn main() -> Result<(), sqlx::Error> { "/trip/:id/edit/:attribute/submit", post(trip_edit_attribute), ) - .route("/trip/:id/items/:id/pick", get(trip_item_set_pick)) - .route("/trip/:id/items/:id/unpick", get(trip_item_set_unpick)) - .route("/trip/:id/items/:id/pack", get(trip_item_set_pack)) - .route("/trip/:id/items/:id/unpack", get(trip_item_set_unpack)) + .route( + "/trip/:id/items/:id/pick", + get(trip_item_set_pick).post(trip_item_set_pick_htmx), + ) + .route( + "/trip/:id/items/:id/unpick", + get(trip_item_set_unpick).post(trip_item_set_unpick_htmx), + ) + .route( + "/trip/:id/items/:id/pack", + get(trip_item_set_pack).post(trip_item_set_pack_htmx), + ) + .route( + "/trip/:id/items/:id/unpack", + get(trip_item_set_unpack).post(trip_item_set_unpack_htmx), + ) .route("/inventory/", get(inventory_inactive)) .route("/inventory/category/", post(inventory_category_create)) + .route("/inventory/item/:id/", get(inventory_item)) .route("/inventory/item/", post(inventory_item_create)) .route( "/inventory/item/name/validate", @@ -1007,6 +1022,14 @@ async fn trip_item_set_pick( )? } +async fn trip_item_set_pick_htmx( + State(state): State, + Path((trip_id, item_id)): Path<(Uuid, Uuid)>, +) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { + trip_item_set_state(&state, trip_id, item_id, TripItemStateKey::Pick, true).await?; + Ok((StatusCode::OK, trip_row(&state, trip_id, item_id).await?)) +} + async fn trip_item_set_unpick( State(state): State, Path((trip_id, item_id)): Path<(Uuid, Uuid)>, @@ -1033,6 +1056,51 @@ async fn trip_item_set_unpick( )? } +async fn trip_row( + state: &AppState, + trip_id: Uuid, + item_id: Uuid, +) -> Result { + let item: TripItem = TripItem::find(&state.database_pool, trip_id, item_id) + .await + .map_err(|error| { + ( + StatusCode::BAD_REQUEST, + ErrorPage::build(&error.to_string()), + ) + })? + .ok_or_else(|| { + ( + StatusCode::NOT_FOUND, + ErrorPage::build(&format!( + "item with id {} not found for trip {}", + item_id, trip_id + )), + ) + })?; + + Ok(components::trip::TripItemListRow::build( + trip_id, + &item, + Item::get_category_max_weight(&state.database_pool, item.item.category_id) + .await + .map_err(|error| { + ( + StatusCode::BAD_REQUEST, + ErrorPage::build(&error.to_string()), + ) + })?, + )) +} + +async fn trip_item_set_unpick_htmx( + State(state): State, + Path((trip_id, item_id)): Path<(Uuid, Uuid)>, +) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { + trip_item_set_state(&state, trip_id, item_id, TripItemStateKey::Pick, false).await?; + Ok((StatusCode::OK, trip_row(&state, trip_id, item_id).await?)) +} + async fn trip_item_set_pack( State(state): State, Path((trip_id, item_id)): Path<(Uuid, Uuid)>, @@ -1059,6 +1127,14 @@ async fn trip_item_set_pack( )? } +async fn trip_item_set_pack_htmx( + State(state): State, + Path((trip_id, item_id)): Path<(Uuid, Uuid)>, +) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { + trip_item_set_state(&state, trip_id, item_id, TripItemStateKey::Pack, true).await?; + Ok((StatusCode::OK, trip_row(&state, trip_id, item_id).await?)) +} + async fn trip_item_set_unpack( State(state): State, Path((trip_id, item_id)): Path<(Uuid, Uuid)>, @@ -1085,6 +1161,14 @@ async fn trip_item_set_unpack( )? } +async fn trip_item_set_unpack_htmx( + State(state): State, + Path((trip_id, item_id)): Path<(Uuid, Uuid)>, +) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { + trip_item_set_state(&state, trip_id, item_id, TripItemStateKey::Pack, false).await?; + Ok((StatusCode::OK, trip_row(&state, trip_id, item_id).await?)) +} + #[derive(Deserialize)] struct NewCategory { #[serde(rename = "new-category-name")] @@ -1339,3 +1423,59 @@ async fn trips_types_edit_name( Ok(Redirect::to("/trips/types/")) } } + +async fn inventory_item( + State(state): State, + Path(id): Path, +) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { + let id_param = id.to_string(); + let item: models::InventoryItem = 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_one(&state.database_pool) + .map_ok(|row| row.try_into()) + .await + .map_err(|e: sqlx::Error| match e { + sqlx::Error::RowNotFound => ( + StatusCode::NOT_FOUND, + ErrorPage::build(&format!("item with id {} not found", id)), + ), + _ => ( + StatusCode::INTERNAL_SERVER_ERROR, + ErrorPage::build(&e.to_string()), + ), + })? + .map_err(|e: Error| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + ErrorPage::build(&e.to_string()), + ) + })?; + + Ok(( + StatusCode::OK, + Root::build( + &components::InventoryItem::build(&state.client_state, &item), + &TopLevelPage::Inventory, + ), + )) +} diff --git a/rust/src/models.rs b/rust/src/models.rs index 2e90946..755c39c 100644 --- a/rust/src/models.rs +++ b/rust/src/models.rs @@ -224,6 +224,79 @@ pub struct TripItem { pub new: bool, } +pub struct DbTripsItemsRow { + picked: bool, + packed: bool, + new: bool, + id: String, + name: String, + weight: i64, + description: Option, + category_id: String, +} + +impl TryFrom for TripItem { + type Error = Error; + + fn try_from(row: DbTripsItemsRow) -> Result { + Ok(TripItem { + picked: row.picked, + packed: row.packed, + new: row.new, + item: 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, + trip_id: Uuid, + item_id: Uuid, + ) -> Result, Error> { + let item_id_param = item_id.to_string(); + let trip_id_param = trip_id.to_string(); + let item: Result, sqlx::Error> = 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_one(pool) + .map_ok(|row| row.try_into()) + .await; + + match item { + Err(e) => match e { + sqlx::Error::RowNotFound => Ok(None), + _ => Err(e.into()), + }, + Ok(v) => Ok(Some(v?)), + } + } +} + pub struct DbTripRow { pub id: String, pub name: String, @@ -652,7 +725,13 @@ impl Item { let id_param = id.to_string(); let item: Result, sqlx::Error> = sqlx::query_as!( DbInventoryItemsRow, - "SELECT * FROM inventory_items AS item + "SELECT + id, + name, + weight, + description, + category_id + FROM inventory_items AS item WHERE item.id = ?", id_param, ) @@ -705,6 +784,69 @@ impl Item { Ok(v) => Ok(Some(v?)), } } + + pub async fn get_category_max_weight( + pool: &sqlx::Pool, + id: Uuid, + ) -> Result { + let id_param = id.to_string(); + let weight: Result = 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 = ( + SELECT category_id + FROM inventory_items + WHERE inventory_items.id = ? + ) + ", + 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?) + } + + pub async fn _get_category_total_picked_weight( + pool: &sqlx::Pool, + category_id: Uuid, + ) -> Result { + let category_id_param = category_id.to_string(); + let weight: Result = 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; + + Ok(weight?) + } } pub struct DbTripsTypesRow { @@ -733,3 +875,63 @@ impl TryFrom for TripsType { }) } } + +pub struct Product { + pub id: Uuid, + pub name: String, + pub description: Option, + pub comment: Option, +} + +pub struct InventoryItem { + pub id: Uuid, + pub name: String, + pub description: Option, + pub weight: i64, + pub category: Category, + pub product: Option, +} + +pub struct DbInventoryItemRow { + pub id: String, + pub name: String, + pub description: Option, + pub weight: i64, + pub category_id: String, + pub category_name: String, + pub category_description: Option, + pub product_id: Option, + pub product_name: Option, + pub product_description: Option, + pub product_comment: Option, +} + +impl TryFrom for InventoryItem { + type Error = Error; + + fn try_from(row: DbInventoryItemRow) -> Result { + 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 { + Ok(Product { + id: Uuid::try_parse(&id)?, + name: row.product_name.unwrap(), + description: row.product_description, + comment: row.product_comment, + }) + }) + .transpose()?, + }) + } +}