From f6a3fb9c4d8dd86f78c9f75a23c1ac35bf35d4eb Mon Sep 17 00:00:00 2001 From: Benedikt Peetz Date: Thu, 19 Mar 2026 07:45:14 +0100 Subject: feat(treewide): Commit MVP --- src/api/mod.rs | 221 +++++++++++++++++-- src/components/async_fetch.rs | 12 +- src/components/buy.rs | 1 + src/components/catch_errors.rs | 40 ++++ src/components/checkbox_placeholder.rs | 54 +++++ src/components/container.rs | 52 ++--- src/components/inventory.rs | 4 +- src/components/login_wall.rs | 42 ++++ src/components/mod.rs | 7 + src/components/product_overview.rs | 19 +- src/components/product_parent_overview.rs | 37 ++++ src/components/recipies.rs | 20 +- src/components/textarea_placeholder.rs | 60 ++++++ src/components/unit_overview.rs | 37 +++- src/lib.rs | 60 +++++- src/pages/associate_barcode.rs | 96 +++++---- src/pages/buy.rs | 63 ++++-- src/pages/create_product.rs | 109 +++++++--- src/pages/create_product_parent.rs | 126 +++++++++++ src/pages/create_recipe.rs | 108 ++++++++++ src/pages/home.rs | 46 ++-- src/pages/inventory.rs | 68 +++--- src/pages/login.rs | 81 +++++++ src/pages/mod.rs | 83 ++++++- src/pages/not_found.rs | 7 +- src/pages/product.rs | 52 +++++ src/pages/products.rs | 68 ++++++ src/pages/provision.rs | 93 ++++++++ src/pages/recipe.rs | 344 ++++++++++++++++++++++++++++++ src/pages/recipies.rs | 74 ++++++- src/pages/units.rs | 78 +++++++ 31 files changed, 1932 insertions(+), 230 deletions(-) create mode 100644 src/components/catch_errors.rs create mode 100644 src/components/checkbox_placeholder.rs create mode 100644 src/components/login_wall.rs create mode 100644 src/components/product_parent_overview.rs create mode 100644 src/components/textarea_placeholder.rs create mode 100644 src/pages/create_product_parent.rs create mode 100644 src/pages/create_recipe.rs create mode 100644 src/pages/login.rs create mode 100644 src/pages/product.rs create mode 100644 src/pages/products.rs create mode 100644 src/pages/provision.rs create mode 100644 src/pages/recipe.rs create mode 100644 src/pages/units.rs (limited to 'src') diff --git a/src/api/mod.rs b/src/api/mod.rs index 3bc870c..eb9ca3a 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,20 +1,38 @@ use leptos::error::Error; use rocie_client::{ apis::{ - api_get_inventory_api::amount_by_id, - api_get_product_api::{ - product_by_id, product_by_name, product_suggestion_by_name, products_in_storage, - products_registered, + api_get_auth_inventory_api::amount_by_id, + api_get_auth_product_api::{ + product_by_id, product_by_name, product_suggestion_by_name, + products_by_product_parent_id_direct, products_by_product_parent_id_indirect, + products_in_storage, products_registered, products_without_product_parent, }, - api_get_unit_api::unit_by_id, - api_get_unit_property_api::{unit_properties, unit_property_by_id}, - api_set_barcode_api::buy_barcode, - api_set_product_api::{associate_barcode, register_product}, + api_get_auth_product_parent_api::{ + product_parents, product_parents_toplevel, product_parents_under, + }, + api_get_auth_recipe_api::{ + recipe_by_id, recipe_by_name, recipes, recipes_by_recipe_parent_id_direct, + recipes_by_recipe_parent_id_indirect, recipes_without_recipe_parent, + }, + api_get_auth_recipe_parent_api::{ + recipe_parents, recipe_parents_toplevel, recipe_parents_under, + }, + api_get_auth_unit_api::{unit_by_id, units, units_by_property_id}, + api_get_auth_unit_property_api::{unit_properties, unit_property_by_id}, + api_get_no_auth_state_api::{can_be_provisioned, is_logged_in}, + api_set_auth_barcode_api::buy_barcode, + api_set_auth_product_api::{associate_barcode, register_product}, + api_set_auth_product_parent_api::register_product_parent, + api_set_auth_recipe_api::add_recipe, + api_set_auth_recipe_parent_api::register_recipe_parent, + api_set_no_auth_user_api::{login, provision}, configuration::Configuration, }, models::{ - Barcode, BarcodeId, Product, ProductAmount, ProductId, ProductStub, Unit, UnitId, - UnitProperty, UnitPropertyId, + Barcode, BarcodeId, LoginInfo, Product, ProductAmount, ProductId, ProductParent, + ProductParentId, ProductParentStub, ProductStub, ProvisionInfo, Recipe, RecipeId, + RecipeParent, RecipeParentId, RecipeParentStub, RecipeStub, Unit, UnitId, UnitProperty, + UnitPropertyId, UserId, UserStub, }, }; @@ -89,22 +107,179 @@ macro_rules! mk_wrapper { } } -mk_wrapper!(product_by_id(product_id: ProductId) -> Product as product_by_id_wrapped); +mk_wrapper!( + is_logged_in() -> bool + as is_logged_in_wrapped +); +mk_wrapper!( + can_be_provisioned() -> bool + as can_be_provisioned_wrapped +); + +mk_wrapper!( + @external_config + login(&config, login_info: LoginInfo) -> () + as login_external_wrapped +); +mk_wrapper!( + @external_config + provision(&config, provsion_info: ProvisionInfo) -> UserId + as provision_external_wrapped +); + +mk_wrapper!( + product_by_id(product_id: ProductId) -> Product + as product_by_id_wrapped +); + +mk_wrapper!( + product_by_name(name: &str) -> Product + as product_by_name_wrapped +); +mk_wrapper!( + @treat_404_as_None + product_by_name(name: &str) -> Option + as product_by_name_404_wrapped +); +mk_wrapper!( + @external_config + product_by_name(&config, name: &str) -> Product + as product_by_name_external_wrapped +); + +mk_wrapper!( + product_suggestion_by_name(part_name: &str) -> Vec + as product_suggestion_by_name_wrapped +); + +mk_wrapper!( + units() -> Vec + as units_wrapped +); +mk_wrapper!( + units_by_property_id(id: UnitPropertyId) -> Vec + as units_by_property_id_wrapped +); +mk_wrapper!( + unit_by_id(unit_id: UnitId) -> Unit + as unit_by_id_wrapped +); +mk_wrapper!( + unit_property_by_id(unit_id: UnitPropertyId) -> UnitProperty + as unit_property_by_id_wrapped +); +mk_wrapper!( + unit_properties() -> Vec + as unit_properties_wrapped +); -mk_wrapper!(@treat_404_as_None product_by_name(name: &str) -> Option as product_by_name_404_wrapped); -mk_wrapper!(@external_config product_by_name(&config, name: &str) -> Product as product_by_name_external_wrapped); +mk_wrapper!( + @treat_404_as_None + amount_by_id(product_id: ProductId) -> Option + as amount_by_id_404_wrapped +); -mk_wrapper!(product_suggestion_by_name(part_name: &str) -> Vec as product_suggestion_by_name_wrapped); +mk_wrapper!( + products_registered() -> Vec + as products_registered_wrapped +); +mk_wrapper!( + products_in_storage() -> Vec + as products_in_storage_wrapped +); +mk_wrapper!( + products_without_product_parent() -> Vec + as products_without_product_parent_wrapped +); +mk_wrapper!( + products_by_product_parent_id_indirect(product_parent_id: ProductParentId) -> Vec + as products_by_product_parent_id_indirect_wrapped +); +mk_wrapper!( + products_by_product_parent_id_direct(product_parent_id: ProductParentId) -> Vec + as products_by_product_parent_id_direct_wrapped +); -mk_wrapper!(unit_by_id(unit_id: UnitId) -> Unit as unit_by_id_wrapped); -mk_wrapper!(unit_property_by_id(unit_id: UnitPropertyId) -> UnitProperty as unit_property_by_id_wrapped); -mk_wrapper!(unit_properties() -> Vec as unit_properties_wrapped); +mk_wrapper!( + @external_config + register_product_parent(&config, product_parent_stub: ProductParentStub) -> ProductParentId + as register_product_parent_external_wrapped +); +mk_wrapper!( + product_parents_toplevel() -> Vec + as product_parents_toplevel_wrapped +); +mk_wrapper!( + @treat_404_as_None + product_parents_under(id: ProductParentId) -> Option> + as product_parents_under_404_wrapped +); +mk_wrapper!( + product_parents() -> Vec + as product_parents_wrapped +); -mk_wrapper!(amount_by_id(product_id: ProductId) -> ProductAmount as amount_by_id_wrapped); +mk_wrapper!( + recipes() -> Vec + as recipes_wrapped +); +mk_wrapper!( + recipes_without_recipe_parent() -> Vec + as recipes_without_recipe_parent_wrapped +); +mk_wrapper!( + @external_config + add_recipe(&config, stub: RecipeStub) -> RecipeId + as add_recipe_external_wrapped +); +mk_wrapper!( + recipes_by_recipe_parent_id_indirect(recipe_parent_id: RecipeParentId) -> Vec + as recipes_by_recipe_parent_id_indirect_wrapped +); +mk_wrapper!( + recipes_by_recipe_parent_id_direct(recipe_parent_id: RecipeParentId) -> Vec + as recipes_by_recipe_parent_id_direct_wrapped +); +mk_wrapper!( + recipe_by_name(name: &str) -> Recipe + as recipe_by_name_wrapped +); +mk_wrapper!( + recipe_by_id(id: RecipeId) -> Recipe + as recipe_by_id_wrapped +); -mk_wrapper!(products_registered() -> Vec as products_registered_wrapped); -mk_wrapper!(products_in_storage() -> Vec as products_in_storage_wrapped); +mk_wrapper!( + @external_config + register_recipe_parent(&config, recipe_parent_stub: RecipeParentStub) -> RecipeParentId + as register_recipe_parent_external_wrapped +); +mk_wrapper!( + recipe_parents_toplevel() -> Vec + as recipe_parents_toplevel_wrapped +); +mk_wrapper!( + @treat_404_as_None + recipe_parents_under(id: RecipeParentId) -> Option> + as recipe_parents_under_404_wrapped +); +mk_wrapper!( + recipe_parents() -> Vec + as recipe_parents_wrapped +); -mk_wrapper!(@external_config buy_barcode(&config, barcode_number: BarcodeId, times: u32) -> () as buy_barcode_external_wrapped); -mk_wrapper!(@external_config register_product(&config, product_stub: ProductStub) -> ProductId as register_product_external_wrapped); -mk_wrapper!(@external_config associate_barcode(&config, id: ProductId, barcode: Barcode) -> () as associate_barcode_external_wrapped); +mk_wrapper!( + @external_config + buy_barcode(&config, barcode_number: BarcodeId, times: u32) -> () + as buy_barcode_external_wrapped +); +mk_wrapper!( + @external_config + register_product(&config, product_stub: ProductStub) -> ProductId + as register_product_external_wrapped +); +mk_wrapper!( + @external_config + associate_barcode(&config, id: ProductId, barcode: Barcode) -> () + as associate_barcode_external_wrapped +); diff --git a/src/components/async_fetch.rs b/src/components/async_fetch.rs index 7bf44a0..43469a7 100644 --- a/src/components/async_fetch.rs +++ b/src/components/async_fetch.rs @@ -37,11 +37,13 @@ macro_rules! AsyncFetch { "Loading..."

} }> - {move || Suspend::new(async move { - $resource - .await - .map($producer) - })} + { + Suspend::new(async move { + $resource + .await + .map($producer) + }) + }
} }}; diff --git a/src/components/buy.rs b/src/components/buy.rs index e69de29..8b13789 100644 --- a/src/components/buy.rs +++ b/src/components/buy.rs @@ -0,0 +1 @@ + diff --git a/src/components/catch_errors.rs b/src/components/catch_errors.rs new file mode 100644 index 0000000..d5a452d --- /dev/null +++ b/src/components/catch_errors.rs @@ -0,0 +1,40 @@ +use leptos::{ + IntoView, component, + error::ErrorBoundary, + prelude::{Children, ClassAttribute, CollectView, ElementChild, Get}, + view, +}; + +use crate::components::site_header::SiteHeader; + +#[component] +pub(crate) fn CatchErrors(children: Children) -> impl IntoView { + view! { + + +

"Uh oh! Something went wrong!"

+ +

"Errors: "

+
    + {move || { + errors + .get() + .into_iter() + .map(|(_, e)| { + view! { +
  • {e.to_string()}
  • + } + }) + .collect_view() + }} +
+ } + }>{children()}
+ } +} diff --git a/src/components/checkbox_placeholder.rs b/src/components/checkbox_placeholder.rs new file mode 100644 index 0000000..a1aaa0c --- /dev/null +++ b/src/components/checkbox_placeholder.rs @@ -0,0 +1,54 @@ +use leptos::{ + IntoView, component, + html::Input, + prelude::{ClassAttribute, ElementChild, GlobalAttributes, NodeRef, NodeRefAttribute}, + view, +}; + +use crate::components::get_id; + +#[component] +pub fn CheckboxPlaceholder( + label: &'static str, + node_ref: NodeRef, +) -> impl IntoView { + let id = get_id(); + + view! { +
+ + + +
+ } +} diff --git a/src/components/container.rs b/src/components/container.rs index d6d2f03..3b56713 100644 --- a/src/components/container.rs +++ b/src/components/container.rs @@ -1,46 +1,38 @@ use leptos::{ IntoView, component, - prelude::{Children, ClassAttribute, ElementChild, OnAttribute}, + prelude::{Children, ClassAttribute, ElementChild}, view, }; -use leptos_router::{NavigateOptions, hooks::use_navigate}; +use leptos_router::components::A; #[component] pub fn Container( - header: impl IntoView, - buttons: Vec<(impl IntoView, &'static str)>, + header: impl IntoView + 'static, + buttons: Vec<(impl IntoView + 'static, &'static str)>, children: Children, ) -> impl IntoView { assert!(!buttons.is_empty()); - let first_button_path = buttons.first().expect("Should have at least on button").1; + // TODO: Add the direct link to the first button back. <2026-02-15> + // let first_button_path = buttons.first().expect("Should have at least on button").1; view! { - - - } - }) - .collect::>()} - - - +
    + {buttons + .into_iter() + .map(|(name, path)| { + view! { +
  • + {name} +
  • + } + }) + .collect::>()} +
+ } } diff --git a/src/components/inventory.rs b/src/components/inventory.rs index 275dd0b..31b1c12 100644 --- a/src/components/inventory.rs +++ b/src/components/inventory.rs @@ -23,8 +23,8 @@ pub fn Inventory() -> impl IntoView { producer = |products| { let products_num = products.len(); let plural_s = if products_num == 1 { "" } else { "s" }; - let products_value = 2; - let products_currency = "EUR"; + let products_value = -1; + let products_currency = "TODO"; view! {

diff --git a/src/components/login_wall.rs b/src/components/login_wall.rs new file mode 100644 index 0000000..fd5c64f --- /dev/null +++ b/src/components/login_wall.rs @@ -0,0 +1,42 @@ +use leptos::{ + IntoView, component, + error::Error, + prelude::{Children, IntoAny}, + view, +}; +use leptos_router::{NavigateOptions, hooks::use_navigate}; + +use crate::{ + api::{can_be_provisioned_wrapped, is_logged_in_wrapped}, + components::async_fetch::{AsyncFetch, AsyncResource}, +}; + +#[component] +pub fn LoginWall( + back: impl Fn() -> String + Send + Sync + 'static, + children: Children, +) -> impl IntoView { + view! { + { + AsyncFetch! { + @map_error_in_producer + from_resource = AsyncResource!( + () -> Result<(bool, bool), Error> { + Ok((can_be_provisioned_wrapped().await?, is_logged_in_wrapped().await?)) + } + ), + producer = |(can_be_provisioned, is_logged_in)| { + if is_logged_in { + children() + } else if can_be_provisioned { + use_navigate()(format!("/provision/?back={}", back()).as_str(), NavigateOptions::default()); + ().into_any() + } else { + use_navigate()(format!("/login/?back={}", back()).as_str(), NavigateOptions::default()); + ().into_any() + } + } + } + } + } +} diff --git a/src/components/mod.rs b/src/components/mod.rs index 2c3d79a..2a3a0b1 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -3,15 +3,22 @@ use std::sync::atomic::{AtomicU32, Ordering}; // Generic pub mod async_fetch; pub mod banner; +pub mod catch_errors; pub mod container; pub mod form; pub mod icon_p; +pub mod login_wall; + +// placeholders +pub mod checkbox_placeholder; pub mod input_placeholder; pub mod select_placeholder; +pub mod textarea_placeholder; // Specific pub mod inventory; pub mod product_overview; +pub mod product_parent_overview; pub mod recipies; pub mod site_header; pub mod unit_overview; diff --git a/src/components/product_overview.rs b/src/components/product_overview.rs index bf81624..233b8a7 100644 --- a/src/components/product_overview.rs +++ b/src/components/product_overview.rs @@ -11,9 +11,20 @@ pub fn ProductOverview() -> impl IntoView { }, "products"), - (view! { }, "create-product"), - (view! { }, "associate-barcode-product"), + (view! { }, "products"), + ( + view! { }, + "create-product", + ), + ( + view! { + + }, + "associate-barcode-product", + ), ] > { @@ -22,7 +33,7 @@ pub fn ProductOverview() -> impl IntoView { fetcher = products_registered_wrapped(), producer = |products| { view! { -

{format!("You have {} products", products.len())}

+

{format!("You have {} products.", products.len())}

} } } diff --git a/src/components/product_parent_overview.rs b/src/components/product_parent_overview.rs new file mode 100644 index 0000000..4aa2a0f --- /dev/null +++ b/src/components/product_parent_overview.rs @@ -0,0 +1,37 @@ +use leptos::{IntoView, component, view}; + +use crate::{ + api::product_parents_wrapped, + components::{async_fetch::AsyncFetch, container::Container, icon_p::IconP}, +}; + +#[component] +pub fn ProductParentOverview() -> impl IntoView { + view! { + }, + "products", + ), + ( + view! { }, + "create-product-parent", + ), + ] + > + { + AsyncFetch! { + @map_error_in_producer + fetcher = product_parents_wrapped(), + producer = |product_parents| { + view! { +

{format!("You have {} product parents.", product_parents.len())}

+ } + } + } + } +
+ } +} diff --git a/src/components/recipies.rs b/src/components/recipies.rs index f7903e4..755954e 100644 --- a/src/components/recipies.rs +++ b/src/components/recipies.rs @@ -1,6 +1,9 @@ -use leptos::{IntoView, component, prelude::ElementChild, view}; +use leptos::{IntoView, component, view}; -use crate::components::{container::Container, icon_p::IconP}; +use crate::{ + api::recipes_wrapped, + components::{async_fetch::AsyncFetch, container::Container, icon_p::IconP}, +}; #[component] pub fn Recipies() -> impl IntoView { @@ -9,10 +12,21 @@ pub fn Recipies() -> impl IntoView { header="Recipies" buttons=vec![ (view! { }, "recipies"), + (view! { }, "create-recipe"), (view! { }, "mealplan"), ] > -

"You have 0 recipies."

+ { + AsyncFetch! { + @map_error_in_producer + fetcher = recipes_wrapped(), + producer = |recipes| { + view! { +

{format!("You have {} recipies.", recipes.len())}

+ } + } + } + } } } diff --git a/src/components/textarea_placeholder.rs b/src/components/textarea_placeholder.rs new file mode 100644 index 0000000..a0bae6d --- /dev/null +++ b/src/components/textarea_placeholder.rs @@ -0,0 +1,60 @@ +use leptos::{ + IntoView, component, + html::Textarea, + prelude::{ClassAttribute, ElementChild, GlobalAttributes, NodeRef, NodeRefAttribute}, + view, +}; + +use crate::components::get_id; + +#[component] +pub fn TextareaPlaceholder( + label: &'static str, + node_ref: NodeRef + + + + } +} diff --git a/src/components/unit_overview.rs b/src/components/unit_overview.rs index 25e5675..0ea3825 100644 --- a/src/components/unit_overview.rs +++ b/src/components/unit_overview.rs @@ -1,6 +1,10 @@ use leptos::{IntoView, component, view}; +use rocie_client::models::{Unit, UnitProperty}; -use crate::components::{container::Container, icon_p::IconP}; +use crate::{ + api::{unit_properties_wrapped, units_wrapped}, + components::{async_fetch::AsyncFetch, container::Container, icon_p::IconP}, +}; #[component] pub fn UnitOverview() -> impl IntoView { @@ -8,14 +12,37 @@ pub fn UnitOverview() -> impl IntoView { }, "units"), - (view! { }, "create-unit"), - (view! { }, "create-unit-property"), + (view! { }, "units"), + ( + view! { }, + "create-unit", + ), + ( + view! { }, + "create-unit-property", + ), ] > { - "You have units" + AsyncFetch! { + @map_error_in_producer + fetcher = get_units_and_unit_properties(), + producer = |(units, unit_properties)| { + view! { +

{move || format!( + "You have {} units and {} unit properties.", + units.len(), + unit_properties.len() + )}

+ } + }, + } }
} } + +async fn get_units_and_unit_properties() +-> Result<(Vec, Vec), leptos::error::Error> { + Ok((units_wrapped().await?, unit_properties_wrapped().await?)) +} diff --git a/src/lib.rs b/src/lib.rs index 36210e7..a884201 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,7 +31,10 @@ use reactive_stores::Store; use rocie_client::apis::configuration::Configuration; use crate::pages::{ - associate_barcode::AssociateBarcode, buy::Buy, create_product::CreateProduct, home::Home, inventory::Inventory, not_found::NotFound, recipies::Recipies + associate_barcode::AssociateBarcode, buy::Buy, create_product::CreateProduct, + create_product_parent::CreateProductParent, create_recipe::CreateRecipe, home::Home, + inventory::Inventory, login::Login, not_found::NotFound, product::Product, products::Products, + provision::Provision, recipe::Recipe, recipies::Recipies, units::Units, }; #[derive(Debug, Clone, Store)] @@ -40,6 +43,7 @@ pub struct ConfigState { } #[component] +#[expect(clippy::too_many_lines)] pub fn App() -> impl IntoView { // Provides context that manages stylesheets, titles, meta tags, etc. provide_meta_context(); @@ -48,7 +52,7 @@ pub fn App() -> impl IntoView { let mut config = Configuration::new(); config.user_agent = Some("rocie-mobile".to_owned()); - "http://127.0.0.1:8080".clone_into(&mut config.base_path); + "/api/".clone_into(&mut config.base_path); config }; @@ -71,6 +75,18 @@ pub fn App() -> impl IntoView { view! { } } /> + } + } + /> + } + } + /> // Inventory impl IntoView { view! { } } /> + } + } + /> + } + } + /> // Products + } + } + /> impl IntoView { view! { } } /> + } + } + /> + + // Product Parents + } + } + /> + + // Units + } + } + /> } diff --git a/src/pages/associate_barcode.rs b/src/pages/associate_barcode.rs index 20714ff..0e1308d 100644 --- a/src/pages/associate_barcode.rs +++ b/src/pages/associate_barcode.rs @@ -1,9 +1,10 @@ use leptos::{ IntoView, component, - prelude::{Get, Show, WriteSignal, signal}, + prelude::{ElementExt, Get, Show, WriteSignal, signal}, task::spawn_local, view, }; +use leptos_router::{NavigateOptions, hooks::use_navigate}; use rocie_client::models::{Barcode, BarcodeId, Product, Unit, UnitAmount, UnitId}; use rocie_macros::Form; use uuid::Uuid; @@ -14,55 +15,62 @@ use crate::{ product_by_name_404_wrapped, product_by_name_external_wrapped, product_suggestion_by_name_wrapped, unit_by_id_wrapped, unit_property_by_id_wrapped, }, - components::{async_fetch::AsyncResource, banner::Banner, site_header::SiteHeader}, + components::{ + async_fetch::AsyncResource, banner::Banner, catch_errors::CatchErrors, + login_wall::LoginWall, site_header::SiteHeader, + }, }; #[component] pub fn AssociateBarcode() -> impl IntoView { - let product_name_signal; - let (errors, errors_set) = signal(None); let (show_units, show_units_set) = signal(false); view! { - - - - - - - { - Form! { - on_submit = |barcode_id, product_name, amount, unit_id| { - let config = get_config!(); - - spawn_local(async move { - let output = async { - let product = product_by_name_external_wrapped(&config, &product_name).await?; - - associate_barcode_external_wrapped(&config, product.id, Barcode { - amount:UnitAmount { - unit: UnitId { value: unit_id }, - value: u32::from(amount), - }, - id: BarcodeId { value: barcode_id }, - }).await?; - - Ok::<_, leptos::error::Error>(()) - }; - - match output.await { - Ok(()) => (), - Err(err) => { - errors_set.set( - Some( - format!("Could not associate barcode: {err}") - ) - ); - }, - } - }); + + + + + + + + + { + let product_name_signal; + Form! { + on_submit = |barcode_id, product_name, amount, unit_id| { + let config = get_config!(); + let navigate = use_navigate(); + + spawn_local(async move { + let output = async { + let product = product_by_name_external_wrapped(&config, product_name.trim()).await?; + + associate_barcode_external_wrapped(&config, product.id, Barcode { + amount:UnitAmount { + unit: UnitId { value: unit_id }, + value: u32::from(amount), + }, + id: BarcodeId { value: barcode_id }, + }).await?; + + Ok::<_, leptos::error::Error>(()) + }; + + match output.await { + Ok(()) => { + navigate("/associate-barcode-product", NavigateOptions::default()); + }, + Err(err) => { + errors_set.set( + Some( + format!("Could not associate barcode: {err}") + ) + ); + }, + } + }); }; impl IntoView { html_type="number", label="Amount" /> - } - } + } + } + + } } diff --git a/src/pages/buy.rs b/src/pages/buy.rs index f3335f6..e4cd599 100644 --- a/src/pages/buy.rs +++ b/src/pages/buy.rs @@ -4,12 +4,16 @@ use leptos::{ task::spawn_local, view, }; +use leptos_router::{NavigateOptions, hooks::use_navigate}; use log::info; use rocie_client::models::BarcodeId; use crate::{ api::{buy_barcode_external_wrapped, get_config}, - components::{banner::Banner, form::Form, site_header::SiteHeader}, + components::{ + banner::Banner, catch_errors::CatchErrors, form::Form, login_wall::LoginWall, + site_header::SiteHeader, + }, }; #[component] @@ -17,29 +21,44 @@ pub fn Buy() -> impl IntoView { let (on_submit_errored, on_submit_errored_set) = signal(None); view! { - + + + - - - + + + - { - Form! { - on_submit = |barcode_number, times| { - let config = get_config!(); + { + Form! { + on_submit = |barcode_number, times| { + let config = get_config!(); + let navigate = use_navigate(); - spawn_local(async move { - if let Err(err) = buy_barcode_external_wrapped(&config, BarcodeId { value: barcode_number }, u32::from(times)).await { - let error = format!("Error in form on-submit for barcode `{barcode_number}`: {err}"); + spawn_local(async move { + match buy_barcode_external_wrapped( + &config, + BarcodeId { value: barcode_number }, + u32::from(times) + ).await { + Ok(()) => { + navigate("/buy", NavigateOptions::default()); + on_submit_errored_set.set(None); + }, + Err(err) => { + let error = + format!( + "Error in form \ + on-submit for barcode \ + `{barcode_number}`: {err}" + ); + on_submit_errored_set.set(Some(error)); + }, + } - on_submit_errored_set.set(Some(error)); - } else { - on_submit_errored_set.set(None); - } - - info!("Bought barcode {barcode_number} {times} times"); - }); + info!("Bought barcode {barcode_number} {times} times"); + }); }; impl IntoView { html_type="number", label="Times" /> - } - } + } + } + + } } diff --git a/src/pages/create_product.rs b/src/pages/create_product.rs index fcd3b0b..fdf8f28 100644 --- a/src/pages/create_product.rs +++ b/src/pages/create_product.rs @@ -1,4 +1,4 @@ -use std::{convert::Infallible, str::FromStr}; +use std::{convert::Infallible, iter, str::FromStr}; use leptos::{ IntoView, component, @@ -6,16 +6,23 @@ use leptos::{ task::spawn_local, view, }; -use rocie_client::models::{ProductStub, UnitPropertyId}; +use leptos_router::{NavigateOptions, hooks::use_navigate}; +use rocie_client::models::{ProductParentId, ProductStub, UnitPropertyId}; use rocie_macros::Form; use uuid::Uuid; use crate::{ - api::{get_config, register_product_external_wrapped, unit_properties_wrapped}, - components::{async_fetch::AsyncResource, banner::Banner, site_header::SiteHeader}, + api::{ + get_config, product_parents_wrapped, register_product_external_wrapped, + unit_properties_wrapped, + }, + components::{ + async_fetch::AsyncResource, banner::Banner, catch_errors::CatchErrors, + login_wall::LoginWall, site_header::SiteHeader, + }, }; -struct OptionalString(Option); +pub(crate) struct OptionalString(pub(crate) Option); impl FromStr for OptionalString { type Err = Infallible; @@ -29,35 +36,54 @@ impl FromStr for OptionalString { } } +struct OptionalParentId(Option); + +impl FromStr for OptionalParentId { + type Err = uuid::Error; + + fn from_str(s: &str) -> Result { + if s.is_empty() { + Ok(Self(None)) + } else { + Ok(Self(Some(ProductParentId { value: s.parse()? }))) + } + } +} + #[component] pub fn CreateProduct() -> impl IntoView { let (error_message, error_message_set) = signal(None); view! { - - - - - - - { - Form! { - on_submit = |product_name, product_description, unit_property_id| { - let config = get_config!(); - - spawn_local(async move { - match register_product_external_wrapped(&config, ProductStub { - description: Some(product_description.0), - name: product_name, - parent: None, // TODO: Add this <2025-10-25> - unit_property: UnitPropertyId { value: unit_property_id }, + + + + + + + + + { + Form! { + on_submit = |product_name, product_description, unit_property_id, parent| { + let config = get_config!(); + let navigate = use_navigate(); + + spawn_local(async move { + match register_product_external_wrapped(&config, ProductStub { + description: product_description.0.map(|d| d.trim().to_owned()), + name: product_name.trim().to_owned(), + parent: parent.0, + unit_property: UnitPropertyId { value: unit_property_id }, + } + ).await { + Ok(_id) => { + navigate("/create-product", NavigateOptions::default()); + } + Err(err) => error_message_set.set(Some(format!("Failed to create product: {err}"))), } - ).await { - Ok(_id) => {} - Err(err) => error_message_set.set(Some(format!("Failed to create product: {err}"))), - } - }); - }; + }); + }; impl IntoView { label="Product Description" /> + impl IntoView { } }, /> - } - } + } + } + + } } diff --git a/src/pages/create_product_parent.rs b/src/pages/create_product_parent.rs new file mode 100644 index 0000000..152347a --- /dev/null +++ b/src/pages/create_product_parent.rs @@ -0,0 +1,126 @@ +use std::{convert::Infallible, iter, str::FromStr}; + +use leptos::{ + IntoView, component, + prelude::{Get, Show, signal}, + task::spawn_local, + view, +}; +use leptos_router::{NavigateOptions, hooks::use_navigate}; +use rocie_client::models::{ProductParentId, ProductParentStub}; +use rocie_macros::Form; + +use crate::{ + api::{get_config, product_parents_wrapped, register_product_parent_external_wrapped}, + components::{ + async_fetch::AsyncResource, banner::Banner, catch_errors::CatchErrors, + login_wall::LoginWall, site_header::SiteHeader, + }, +}; + +struct OptionalString(Option); + +impl FromStr for OptionalString { + type Err = Infallible; + + fn from_str(s: &str) -> Result { + if s.is_empty() { + Ok(Self(None)) + } else { + Ok(Self(Some(s.to_owned()))) + } + } +} + +struct OptionalParentId(Option); + +impl FromStr for OptionalParentId { + type Err = uuid::Error; + + fn from_str(s: &str) -> Result { + if s.is_empty() { + Ok(Self(None)) + } else { + Ok(Self(Some(ProductParentId { value: s.parse()? }))) + } + } +} + +#[component] +pub fn CreateProductParent() -> impl IntoView { + let (error_message, error_message_set) = signal(None); + + view! { + + + + + + + + + { + Form! { + on_submit = |name, description, parent| { + let config = get_config!(); + let navigate = use_navigate(); + + spawn_local(async move { + match register_product_parent_external_wrapped(&config, ProductParentStub { + description: description.0.map(|d| d.trim().to_owned()), + name: name.trim().to_owned(), + parent: parent.0, + } + ).await { + Ok(_id) => { + navigate("/create-product-parent", NavigateOptions::default()); + } + Err(err) => error_message_set.set(Some(format!("Failed to create product: {err}"))), + } + }); + }; + + + + + + + +