diff --git a/rust/js/app.js b/rust/js/app.js index f59befa..52c6fa9 100644 --- a/rust/js/app.js +++ b/rust/js/app.js @@ -9,3 +9,11 @@ window.onload = function() { function is_positive_integer(val) { return /^\d+$/.test(val); } + +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; +} diff --git a/rust/query.sql b/rust/query.sql index b09655b..f3f3151 100644 --- a/rust/query.sql +++ b/rust/query.sql @@ -63,22 +63,44 @@ /* ON inner.category_id = category.id */ /* WHERE category.id = '1293c6b6-eef5-4269-bf10-a1ac20549dac' */ -SELECT - trip.id AS id, - trip.name AS name, - CAST (date_start AS TEXT) date_start, - CAST (date_end AS TEXT) date_end, - state, - location, - temp_min, - temp_max, - comment, - SUM(i_item.weight) 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 = '0535193c-7b47-4ba4-bca5-40e54c15c2d0' - AND t_item.pick = true +/* SELECT */ +/* trip.id AS id, */ +/* trip.name AS name, */ +/* CAST (date_start AS TEXT) date_start, */ +/* CAST (date_end AS TEXT) date_end, */ +/* state, */ +/* location, */ +/* temp_min, */ +/* temp_max, */ +/* comment, */ +/* SUM(i_item.weight) 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 = '0535193c-7b47-4ba4-bca5-40e54c15c2d0' */ +/* AND t_item.pick = true */ + + /* UPDATE trips_items */ + /* SET pick = true */ + /* WHERE trip_id = '0535193c-7b47-4ba4-bca5-40e54c15c2d0' */ + /* AND item_id = '2418ab2d-4e11-4a68-8761-265d442742f6' */ + /* RETURNING */ + /* trips_items.item_id */ + +SELECT * +FROM inventory_items +WHERE id = ( + UPDATE trips_items + SET pick = true + WHERE trip_id = '0535193c-7b47-4ba4-bca5-40e54c15c2d0' + AND item_id = '2418ab2d-4e11-4a68-8761-265d442742f6' + RETURNING + trips_items.item_id + /* SELECT trips_items.item_id */ + /* FROM trips_items */ + /* WHERE trip_id = '0535193c-7b47-4ba4-bca5-40e54c15c2d0' */ + /* AND item_id = '2418ab2d-4e11-4a68-8761-265d442742f6' */ + ) diff --git a/rust/src/components/home.rs b/rust/src/components/home.rs index 2f9b872..0470da0 100644 --- a/rust/src/components/home.rs +++ b/rust/src/components/home.rs @@ -5,13 +5,327 @@ pub struct Home; impl Home { pub fn build() -> Markup { html!( - div id="home" class={"p-8" "max-w-xl"} { - p { - a href="/inventory/" { "Inventory" } + div + id="home" + hx-boost="true" + ."p-8" + ."flex" + ."flex-col" + ."gap-8" + ."flex-nowrap" + { + h1 + ."text-2xl" + ."m-auto" + ."my-4" + { + "Welcome!" } - p { - a href="/trips/" { "Trips" } + section + ."border-2" + ."border-gray-200" + ."rounded-md" + ."flex" + ."flex-row" + { + a + href="/inventory/" + hx-boost="true" + ."p-8" + ."w-1/5" + ."flex" + ."hover:bg-gray-200" + { + span + ."m-auto" + ."text-xl" + { "Inventory" } + } + div + ."p-8" + ."w-4/5" + ."flex" + ."flex-col" + ."gap-3" + { + p { + "The inventory contains all the items that you own." + } + p { + "It is effectively a list of items, sectioned into + arbitrary categories" + } + p { + "Each item has some important data attached to it, + like its weight" + } + } } + section + ."border-2" + ."border-gray-200" + ."rounded-md" + ."flex" + ."flex-row" + { + a + href="/trips/" + hx-boost="true" + ."p-8" + ."w-1/5" + ."flex" + ."hover:bg-gray-200" + { + span + ."m-auto" + ."text-xl" + { "Trips" } + } + div + ."p-8" + ."w-4/5" + ."flex" + ."flex-col" + ."gap-6" + { + div + ."flex" + ."flex-col" + ."gap-3" + { + p { + "Trips is where it gets interesting, as you can put + your inventory to good use" + } + p { + r#"With trips, you record any trips you plan to do. A + "trip" can be anything you want it to be. Anything + from a multi-week hike, a high altitude mountaineering + tour or just a visit to the library. Whenever it makes + sense to do some planning, creating a trip makes sense."# + } + p { + "Each trip has some metadata attached to it, like start- + and end dates or the expected temperature." + } + } + div + ."flex" + ."flex-col" + ."gap-3" + { + div + ."flex" + ."flex-row" + ."gap-2" + ."items-center" + ."justify-start" + { + span + ."mdi" + ."mdi-pound" + ."text-lg" + ."text-gray-300" + {} + h3 ."text-lg" { + "States" + } + } + p { + "One of the most important parts of each trip is + its " em{"state"} ", which determines certain + actions on the trip and can have the following values:" + } + table + ."table" + ."table-auto" + ."border-collapse" + { + tr + ."border-b-2" + ."last:border-b-0" + { + td ."py-2" ."pr-4" ."border-r-2" { + "Init" + } + td ."py-2" ."w-full" ."pl-4" { + "The new trip was just created" + } + } + tr + ."border-b-2" + ."last:border-b-0" + { + td ."py-2" ."pr-4" ."border-r-2" { + "Planning" + } + td ."py-2" ."w-full" ."pl-4" { + "Now, you actually start planning the trip. + Setting the location, going through your + items to decide what to take with you." + } + } + tr + ."border-b-2" + ."last:border-b-0" + { + td ."py-2" ."pr-4" ."border-r-2" { + "Planned" + } + td ."py-2" ."w-full" ."pl-4" { + "You are done with the planning. It's time + to pack up your stuff and get going." + } + } + tr + ."border-b-2" + ."last:border-b-0" + { + td ."py-2" ."pr-4" ."border-r-2" { + "Active" + } + td ."py-2" ."w-full" ."pl-4" { + "The trip is finally underway!" + } + } + tr + ."border-b-2" + ."last:border-b-0" + { + td ."py-2" ."pr-4" ."border-r-2" { + "Review" + } + td ."py-2" ."w-full" ."pl-4" { + div + ."flex" + ."flex-col" + ."gap-2" + { + p { + "You returned from your trip. It may make + sense to take a look back and see what + went well and what went not so well." + } + p { + "Anything you missed? Any items that you + took with you that turned out to be useless? + Record it and you will remember on your next + trip" + } + } + } + } + tr + ."border-b-2" + ."last:border-b-0" + { + td ."py-2" ."pr-4" ."border-r-2" { + "Done" + } + td ."py-2" ."w-full" ."pl-4" { + "Your review is done and the trip can be laid to rest" + } + } + } + } + div + ."flex" + ."flex-col" + ."gap-3" + { + div + ."flex" + ."flex-row" + ."gap-2" + ."items-center" + ."justify-start" + { + span + ."mdi" + ."mdi-pound" + ."text-lg" + ."text-gray-300" + {} + h3 ."text-lg" { + "Items" + } + } + p { + "Of course, you can use items defined in your + inventory in your trips" + } + p { + "Generally, all items are available to you in + the same way as the inventory. For each item, + there are two specific states for the trip: An + item can be " em{"picked"} ", which means that + you plan to take it on the trip, and it can + be " em{"packed"} ", which means that you actually + packed it into your bag (and therefore, you cannot + forget it any more)" + } + } + div + ."flex" + ."flex-col" + ."gap-3" + { + div + ."flex" + ."flex-row" + ."gap-2" + ."items-center" + ."justify-start" + { + span + ."mdi" + ."mdi-pound" + ."text-lg" + ."text-gray-300" + {} + h3 ."text-lg" { + "Types & Presets" + } + } + p { + "Often, you will take a certain set of items to + certain trips. Whenever you plan to sleep outdoors, + it makes sense to take your sleeping bag and mat + with you" + } + p { + "To reflect this, you can attach " em {"types"} " " + "to your trips. Types define arbitrary characteristics + about a trip and reference a certain set of items." + } + p { + "Here are some examples of types that might make sense:" + } + ul + ."list-disc" + ."list-inside" + { + li { + r#""Biking": Make sure to pack your helmet and + some repair tools"# + } + li { + r#""Climbing": You certainly don't want to forget + your climbing shoes"# + } + li { + r#""Rainy": Pack a rain jacket and some waterproof + shoes"# + } + } + p { + "Types are super flexible, it's up to you how to use + them" + } + } + } + } + } ) } diff --git a/rust/src/components/inventory.rs b/rust/src/components/inventory.rs index e1c8ef2..3eff763 100644 --- a/rust/src/components/inventory.rs +++ b/rust/src/components/inventory.rs @@ -89,7 +89,7 @@ impl InventoryCategoryList { id="select-category" href={ "/inventory/category/" - (category.id) + (category.id) "/" } hx-post={ "/inventory/categories/" @@ -152,6 +152,7 @@ impl InventoryItemList { name="edit-item" id="edit-item" action={"/inventory/item/" (edit_item_id) "/edit"} + hx-boost="true" target="_self" method="post" {} @@ -230,12 +231,13 @@ impl InventoryItemList { ."h-full" { a + href=(format!("/inventory/item/{id}/cancel", id = item.id)) + hx-boost="true" ."aspect-square" ."flex" ."w-full" ."h-full" ."p-0" - href=(format!("/inventory/item/{id}/cancel", id = item.id)) { span ."m-auto" @@ -253,10 +255,11 @@ impl InventoryItemList { ."p-2" ."w-full" ."inline-block" href=( format!("/inventory/item/{id}/", id=item.id) - ) { - - (item.name.clone()) - } + ) + hx-boost="true" + { + (item.name.clone()) + } } td ."border" ."p-2" style="position:relative;" { p { (item.weight.to_string()) } @@ -276,10 +279,11 @@ impl InventoryItemList { ."h-full" { a + href=(format!("?edit_item={id}", id = item.id)) + hx-boost="true" ."aspect-square" ."flex" ."w-full" - href=(format!("?edit_item={id}", id = item.id)) { span ."m-auto" ."mdi" ."mdi-pencil" ."text-xl" {} } @@ -293,10 +297,11 @@ impl InventoryItemList { ."h-full" { a + href=(format!("/inventory/item/{id}/delete", id = item.id)) + hx-boost="true" ."aspect-square" ."flex" ."w-full" - href=(format!("/inventory/item/{id}/delete", id = item.id)) { span ."m-auto" ."mdi" ."mdi-delete" ."text-xl" {} } @@ -317,17 +322,6 @@ pub struct InventoryNewItemFormName; impl InventoryNewItemFormName { pub fn build(value: Option<&str>, error: bool) -> 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; - } - ")) - } div ."grid" ."grid-cols-[2fr,3fr]" @@ -376,17 +370,6 @@ pub struct InventoryNewItemFormWeight; impl InventoryNewItemFormWeight { pub fn build() -> 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; - } - ")) - } div ."grid" ."grid-cols-[2fr,3fr]" @@ -468,17 +451,6 @@ pub struct InventoryNewItemForm; impl InventoryNewItemForm { pub fn build(active_category: Option<&Category>, 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(), diff --git a/rust/src/components/mod.rs b/rust/src/components/mod.rs index e817d9e..fd44e07 100644 --- a/rust/src/components/mod.rs +++ b/rust/src/components/mod.rs @@ -10,6 +10,7 @@ pub use trip::*; pub struct Root; +#[derive(PartialEq, Eq)] pub enum TopLevelPage { Inventory, Trips, @@ -36,25 +37,30 @@ impl Root { { header #header - ."h-full" + ."h-16" ."bg-gray-200" - ."p-5" ."flex" ."flex-row" ."flex-nowrap" ."justify-between" - ."items-center" + ."items-stretch" { - span - ."text-xl" - ."font-semibold" + a + #home + hx-boost="true" + href="/" ."flex" ."flex-row" ."items-center" ."gap-3" + ."px-5" + ."hover:bg-gray-300" { img ."h-12" src="/assets/luggage.svg" {} - a #home href="/" { "Packager" } + span + ."text-xl" + ."font-semibold" + { "Packager" } } nav ."grow" @@ -62,22 +68,44 @@ impl Root { ."flex-row" ."justify-center" ."gap-x-10" - ."content-stretch" + ."items-stretch" { - a href="/inventory/" + a + href="/inventory/" + hx-boost="true" #header-link-inventory + ."px-5" + ."flex" ."h-full" ."text-lg" - ."font-bold"[matches!(active_page, TopLevelPage::Inventory)] - ."underline"[matches!(active_page, TopLevelPage::Inventory)] - { "Inventory" } - a href="/trips/" + ."hover:bg-gray-300" + + // invisible top border to fix alignment + ."border-t-gray-200"[active_page == &TopLevelPage::Inventory] + ."hover:border-t-gray-300"[active_page == &TopLevelPage::Inventory] + + ."border-b-gray-500"[active_page == &TopLevelPage::Inventory] + ."border-y-4"[active_page == &TopLevelPage::Inventory] + ."font-bold"[active_page == &TopLevelPage::Inventory] + { span ."m-auto" ."font-semibold" { "Inventory" }} + a + href="/trips/" + hx-boost="true" #header-link-trips + ."px-5" + ."flex" ."h-full" ."text-lg" - ."font-bold"[matches!(active_page, TopLevelPage::Trips)] - ."underline"[matches!(active_page, TopLevelPage::Trips)] - { "Trips" } + ."hover:bg-gray-300" + + // invisible top border to fix alignment + ."border-t-gray-200"[active_page == &TopLevelPage::Trips] + ."hover:border-t-gray-300"[active_page == &TopLevelPage::Trips] + + ."border-gray-500"[active_page == &TopLevelPage::Trips] + ."border-y-4"[active_page == &TopLevelPage::Trips] + ."font-bold"[active_page == &TopLevelPage::Trips] + { span ."m-auto" ."font-semibold" { "Trips" }} } } (body) diff --git a/rust/src/components/trip/mod.rs b/rust/src/components/trip/mod.rs index 876c898..9464ddb 100644 --- a/rust/src/components/trip/mod.rs +++ b/rust/src/components/trip/mod.rs @@ -9,7 +9,9 @@ use serde_variant::to_variant_name; use crate::ClientState; pub struct TripManager; +pub mod packagelist; pub mod types; + pub use types::*; impl TripManager { @@ -105,8 +107,14 @@ impl TripTableRow { pub fn build(trip_id: Uuid, value: impl maud::Render) -> Markup { html!( td ."border" ."p-0" ."m-0" { - a ."inline-block" ."p-2" ."m-0" ."w-full" - href=(format!("/trips/{id}/", id=trip_id)) + a + href={"/trips/" (trip_id) "/"} + hx-boost="true" + ."inline-block" + ."p-2" + ."m-0" + ."w-full" + { (value) } } ) @@ -123,6 +131,7 @@ impl NewTrip { action="/trips/" target="_self" method="post" + hx-boost="true" ."p-5" ."border-2" ."border-gray-200" { div ."mb-5" ."flex" ."flex-row" ."trips-center" { @@ -225,93 +234,116 @@ impl Trip { div ."flex" ."flex-row" - ."items-stretch" - ."gap-x-5" + ."justify-between" { - a - href="/trips/" - ."text-sm" - ."text-gray-500" + div ."flex" + ."flex-row" + ."items-stretch" + ."gap-x-5" { - div - ."m-auto" + a + href="/trips/" + hx-boost="true" + ."text-sm" + ."text-gray-500" + ."flex" { - span - ."mdi" - ."mdi-arrow-left" - {} - "back" + div + ."m-auto" + { + span + ."mdi" + ."mdi-arrow-left" + {} + "back" + } + } + div ."flex" ."flex-row" ."items-center" ."gap-x-3" { + @if trip_edit_attribute.as_ref().map_or(false, |a| *a == TripAttribute::Name) { + form + id="edit-trip" + action=(format!("edit/{}/submit", to_variant_name(&TripAttribute::Name).unwrap())) + hx-boost="true" + target="_self" + method="post" + { + div + ."flex" + ."flex-row" + ."items-center" + ."gap-x-3" + ."items-stretch" + { + input + ."bg-blue-200" + ."w-full" + ."text-2xl" + ."font-semibold" + type=(>::into(InputType::Text)) + name="new-value" + form="edit-trip" + value=(trip.name) + {} + a + href="." + hx-boost="true" + ."bg-red-200" + ."hover:bg-red-300" + ."w-8" + ."flex" + { + span + ."mdi" + ."mdi-cancel" + ."text-xl" + ."m-auto" + {} + } + button + type="submit" + form="edit-trip" + ."bg-green-200" + ."hover:bg-green-300" + ."w-8" + { + span + ."mdi" + ."mdi-content-save" + ."text-xl" + {} + } + } + } + } @else { + h1 ."text-2xl" { (trip.name) } + span { + a + href={"?edit=" (to_variant_name(&TripAttribute::Name).unwrap())} + hx-boost="true" + { + span + ."mdi" + ."mdi-pencil" + ."text-xl" + ."opacity-50" + {} + } + } + } } } - div ."flex" ."flex-row" ."items-center" ."gap-x-3" { - @if trip_edit_attribute.as_ref().map_or(false, |a| *a == TripAttribute::Name) { - form - id="edit-trip" - action=(format!("edit/{}/submit", to_variant_name(&TripAttribute::Name).unwrap())) - target="_self" - method="post" - { - div - ."flex" - ."flex-row" - ."items-center" - ."gap-x-3" - ."items-stretch" - { - input - ."bg-blue-200" - ."w-full" - ."text-2xl" - ."font-semibold" - type=(>::into(InputType::Text)) - name="new-value" - form="edit-trip" - value=(trip.name) - {} - 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" - ."bg-green-200" - ."hover:bg-green-300" - ."w-8" - { - span - ."mdi" - ."mdi-content-save" - ."text-xl" - {} - } - } - } - } @else { - h1 ."text-2xl" { (trip.name) } - span { - a href=(format!("?edit={}", to_variant_name(&TripAttribute::Name).unwrap())) - { - span - ."mdi" - ."mdi-pencil" - ."text-xl" - ."opacity-50" - {} - } - } - } + a + href={"/trips/" (trip.id) "/packagelist/"} + hx-boost="true" + ."p-2" + ."border-2" + ."border-gray-500" + ."rounded-md" + ."bg-blue-200" + ."hover:bg-blue-200" + { + "Show Package List" } } (TripInfo::build(trip_edit_attribute, trip)) @@ -339,6 +371,7 @@ impl TripInfoRow { name="edit-trip" id="edit-trip" action=(format!("edit/{key}/submit", key=(to_variant_name(&attribute_key).unwrap()) )) + hx-boost="true" target="_self" method="post" {} @@ -377,11 +410,12 @@ impl TripInfoRow { ."h-full" { a + href="." // strips query parameters + hx-boost="true" ."flex" ."w-full" ."h-full" ."p-0" - href="." // strips query parameters { span ."m-auto" @@ -426,10 +460,11 @@ impl TripInfoRow { ."h-full" { a - .flex + href={ "?edit=" (to_variant_name(&attribute_key).unwrap()) } + hx-boost="true" + ."flex" ."w-full" ."h-full" - href={ "?edit=" (to_variant_name(&attribute_key).unwrap()) } { span ."m-auto" @@ -649,7 +684,10 @@ impl TripInfo { ."justify-start" { @for triptype in active_triptypes { - a href=(format!("type/{}/remove", triptype.id)) { + a + href={"type/" (triptype.id) "/remove"} + hx-boost="true" + { li ."border" ."rounded-2xl" @@ -679,7 +717,10 @@ impl TripInfo { ."justify-start" { @for triptype in inactive_triptypes { - a href=(format!("type/{}/add", triptype.id)) { + a + href={"type/" (triptype.id) "/add"} + hx-boost="true" + { li ."border" ."rounded-2xl" @@ -703,7 +744,9 @@ impl TripInfo { } } } - a href="/trips/types/" + a + href="/trips/types/" + hx-boost="true" ."text-sm" ."text-gray-500" ."mr-2" @@ -742,6 +785,7 @@ impl TripComment { form id="edit-comment" action="comment/submit" + hx-boost="true" target="_self" method="post" {} @@ -1118,6 +1162,7 @@ impl TripItemListRow { href=( format!("/inventory/item/{id}/", id=item.item.id) ) + hx-boost="true" { (item.item.name.clone()) } diff --git a/rust/src/components/trip/types.rs b/rust/src/components/trip/types.rs index 8a45dee..0b1bbd2 100644 --- a/rust/src/components/trip/types.rs +++ b/rust/src/components/trip/types.rs @@ -32,6 +32,7 @@ impl TypeList { ."hidden" id="edit-trip-type" action={ (trip_type.id) "/edit/name/submit" } + hx-boost="true" target="_self" method="post" {} @@ -56,6 +57,7 @@ impl TypeList { { a href="." + hx-boost="true" ."bg-red-200" ."hover:bg-red-300" ."w-8" @@ -96,10 +98,11 @@ impl TypeList { ."w-8" { a + href={ "?edit=" (trip_type.id) } + hx-boost="true" .flex ."w-full" ."h-full" - href={ "?edit=" (trip_type.id) } { span ."m-auto" @@ -119,6 +122,7 @@ impl TypeList { action="/trips/types/" target="_self" method="post" + hx-boost="true" ."mt-8" ."p-5" ."border-2" ."border-gray-200" { div ."mb-5" ."flex" ."flex-row" { diff --git a/rust/src/main.rs b/rust/src/main.rs index 63a7e3c..2c441d8 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -166,13 +166,21 @@ async fn main() -> Result<(), sqlx::Error> { .nest( "/trips/", Router::new() - .route("/", get(trips)) + .route("/", get(trips).post(trip_create)) .route("/types/", get(trips_types).post(trip_type_create)) .route("/types/:id/edit/name/submit", post(trips_types_edit_name)) - .route("/", post(trip_create)) .route("/:id/", get(trip)) .route("/:id/comment/submit", post(trip_comment_set)) .route("/:id/categories/:id/select", post(trip_category_select)) + .route("/:id/packagelist/", get(trip_packagelist)) + .route( + "/:id/packagelist/item/:id/pack", + post(trip_item_packagelist_set_pack_htmx), + ) + .route( + "/:id/packagelist/item/:id/unpack", + post(trip_item_packagelist_set_unpack_htmx), + ) .route("/:id/state/:id", post(trip_state_set)) .route("/:id/total_weight", get(trip_total_weight_htmx)) .route("/:id/type/:id/add", get(trip_type_add)) @@ -199,18 +207,15 @@ async fn main() -> Result<(), sqlx::Error> { "/inventory/", Router::new() .route("/", get(inventory_inactive)) - .route("/category/", post(inventory_category_create)) - .route("/item/:id/", get(inventory_item)) .route("/categories/:id/select", post(inventory_category_select)) - .route("/item/", post(inventory_item_create)) - .route("/item/name/validate", post(inventory_item_validate_name)) + .route("/category/", post(inventory_category_create)) .route("/category/:id/", get(inventory_active)) + .route("/item/", post(inventory_item_create)) + .route("/item/:id/", get(inventory_item)) + .route("/item/:id/cancel", get(inventory_item_cancel)) .route("/item/:id/delete", get(inventory_item_delete)) .route("/item/:id/edit", post(inventory_item_edit)) - .route("/item/:id/cancel", get(inventory_item_cancel)), // .route( - // "/inventory/category/:id/items", - // post(htmx_inventory_category_items), - // ); + .route("/item/name/validate", post(inventory_item_validate_name)), ) .fallback(|| async { (StatusCode::NOT_FOUND, "not found") }) .with_state(state); @@ -1777,7 +1782,7 @@ async fn inventory_category_select( let mut headers = HeaderMap::new(); headers.insert::( HtmxResponseHeaders::PushUrl.into(), - format!("/inventory/category/{category_id}") + format!("/inventory/category/{category_id}/") .parse() .unwrap(), ); @@ -1792,3 +1797,90 @@ async fn inventory_category_select( ), )) } + +async fn trip_packagelist( + State(state): State, + Path(trip_id): Path, +) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { + let mut trip = models::Trip::find(&state.database_pool, trip_id) + .await + .map_err(|e| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + ErrorPage::build(&e.to_string()), + ) + })? + .ok_or(( + StatusCode::NOT_FOUND, + ErrorPage::build(&format!("trip with id {trip_id} not found")), + ))?; + + trip.load_categories(&state.database_pool) + .await + .map_err(|e| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + ErrorPage::build(&e.to_string()), + ) + })?; + + Ok(( + StatusCode::OK, + Root::build( + &components::packagelist::TripPackageList::build(&trip), + &TopLevelPage::None, + ), + )) +} + +async fn trip_item_packagelist_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?; + + let item = models::TripItem::find(&state.database_pool, trip_id, item_id) + .await + .map_err(|error| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + ErrorPage::build(&error.to_string()), + ) + })? + .ok_or(( + StatusCode::NOT_FOUND, + ErrorPage::build(&format!("an item with id {item_id} does not exist")), + ))?; + + Ok(( + StatusCode::OK, + components::packagelist::TripPackageListRow::build(trip_id, &item), + )) +} + +async fn trip_item_packagelist_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?; + + // note that this cannot fail due to a missing item, as trip_item_set_state would already + // return 404. but error handling cannot hurt ;) + let item = models::TripItem::find(&state.database_pool, trip_id, item_id) + .await + .map_err(|error| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + ErrorPage::build(&error.to_string()), + ) + })? + .ok_or(( + StatusCode::NOT_FOUND, + ErrorPage::build(&format!("an item with id {item_id} does not exist")), + ))?; + + Ok(( + StatusCode::OK, + components::packagelist::TripPackageListRow::build(trip_id, &item), + )) +}