summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-10-25 02:15:31 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-10-25 02:15:31 +0200
commit3a0e91bda1e93afa33dd182c2e820c94b3e94593 (patch)
tree436fb1b1e1b0bbe450436ccb5e105b8bea8db17f /src
parentfeat(treewide): Make usage more intuitive (diff)
downloadweb-client-3a0e91bda1e93afa33dd182c2e820c94b3e94593.zip
feat(treewide): Add further buttons
The register product > associate barcode > buy barcode workflow is now
usable.

The only missing features for an MVP are unit and unit property
creation.
Diffstat (limited to 'src')
-rw-r--r--src/api/mod.rs171
-rw-r--r--src/components/buy.rs0
-rw-r--r--src/components/container.rs2
-rw-r--r--src/components/inventory.rs43
-rw-r--r--src/components/mod.rs2
-rw-r--r--src/components/product_overview.rs25
-rw-r--r--src/components/unit_overview.rs21
-rw-r--r--src/lib.rs20
-rw-r--r--src/pages/associate_barcode.rs163
-rw-r--r--src/pages/buy.rs156
-rw-r--r--src/pages/create_product.rs96
-rw-r--r--src/pages/home.rs7
-rw-r--r--src/pages/inventory.rs19
-rw-r--r--src/pages/mod.rs2
14 files changed, 476 insertions, 251 deletions
diff --git a/src/api/mod.rs b/src/api/mod.rs
index bc800fb..3bc870c 100644
--- a/src/api/mod.rs
+++ b/src/api/mod.rs
@@ -1,103 +1,110 @@
-use leptos::{
-    error::Error,
-    prelude::{Read, expect_context},
-};
-use reactive_stores::Store;
+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,
+            product_by_id, product_by_name, product_suggestion_by_name, products_in_storage,
+            products_registered,
         },
         api_get_unit_api::unit_by_id,
-        api_get_unit_property_api::unit_property_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},
         configuration::Configuration,
     },
     models::{
-        BarcodeId, Product, ProductAmount, ProductId, Unit, UnitId, UnitProperty, UnitPropertyId,
+        Barcode, BarcodeId, Product, ProductAmount, ProductId, ProductStub, Unit, UnitId,
+        UnitProperty, UnitPropertyId,
     },
 };
 
-use crate::{ConfigState, ConfigStateStoreFields};
+macro_rules! get_config {
+    () => {{
+        use crate::ConfigStateStoreFields;
+        use leptos::prelude::Read;
 
-pub(crate) async fn get_amount_by_id(product_id: ProductId) -> Result<ProductAmount, Error> {
-    let config = expect_context::<Store<ConfigState>>();
-    amount_by_id(&config.config().read(), product_id)
-        .await
-        .map_err(Into::<Error>::into)
-}
-pub(crate) async fn get_product_by_id(product_id: ProductId) -> Result<Product, Error> {
-    let config = expect_context::<Store<ConfigState>>();
-    product_by_id(&config.config().read(), product_id)
-        .await
-        .map_err(Into::<Error>::into)
-}
-pub(crate) async fn get_product_by_name(
-    name: String,
-) -> Result<
-    Product,
-    rocie_client::apis::Error<rocie_client::apis::api_get_product_api::ProductByNameError>,
-> {
-    let config = expect_context::<Store<ConfigState>>();
-    product_by_name(&config.config().read(), &name).await
-}
-pub(crate) async fn get_products_by_part_name(part_name: String) -> Result<Vec<Product>, Error> {
-    let config = expect_context::<Store<ConfigState>>();
-    product_suggestion_by_name(&config.config().read(), &part_name)
-        .await
-        .map_err(Into::<Error>::into)
-}
-pub(crate) async fn get_unit_by_id(unit_id: UnitId) -> Result<Unit, Error> {
-    let config = expect_context::<Store<ConfigState>>();
-    unit_by_id(&config.config().read(), unit_id)
-        .await
-        .map_err(Into::<Error>::into)
-}
-pub(crate) async fn get_unit_property_by_id(
-    unit_id: UnitPropertyId,
-) -> Result<UnitProperty, Error> {
-    let config = expect_context::<Store<ConfigState>>();
-    unit_property_by_id(&config.config().read(), unit_id)
-        .await
-        .map_err(Into::<Error>::into)
+        let config =
+            leptos::prelude::expect_context::<reactive_stores::Store<crate::ConfigState>>();
+        config.config().read()
+    }};
 }
 
-pub(crate) async fn get_full_product_by_id(
-    id: ProductId,
-) -> Result<(Product, ProductAmount, Unit), Error> {
-    let amount = get_amount_by_id(id).await?;
-    let product = get_product_by_id(id).await?;
-    let unit = get_unit_by_id(amount.amount.unit).await?;
+pub(crate) use get_config;
 
-    Ok::<_, Error>((product, amount, unit))
-}
-pub(crate) async fn get_product_unit_by_id(
-    id: ProductId,
-) -> Result<(Product, UnitProperty), Error> {
-    let product = get_product_by_id(id).await?;
-    let unit = get_unit_property_by_id(product.unit_property).await?;
+macro_rules! mk_wrapper {
+    (
+        $orig:ident($($arg_name:ident : $arg_type:ty),*) -> $output_type:ty
+        as $new_name:ident
+    ) => {
+        pub(crate) async fn $new_name(
+            $($arg_name : $arg_type),*
+        ) -> Result<$output_type, Error> {
+            let config = get_config!();
 
-    Ok::<_, Error>((product, unit))
-}
+            $orig(&config, $($arg_name),*)
+                .await
+                .map_err(Into::<Error>::into)
+        }
+    };
 
-pub(crate) async fn get_products() -> Result<Vec<Product>, Error> {
-    let config = expect_context::<Store<ConfigState>>();
-    products(&config.config().read())
-        .await
-        .map_err(Into::<Error>::into)
-}
+    (
+        @treat_404_as_None
+        $orig:ident($($arg_name:ident : $arg_type:ty),*) -> Option<$output_type:ty>
+        as $new_name:ident
+    ) => {
+        pub(crate) async fn $new_name(
+            $($arg_name : $arg_type),*
+        ) -> Result<Option<$output_type>, Error> {
+            let config = get_config!();
 
-pub(crate) async fn buy_barcode_wrapper(
-    config: &Configuration,
-    barcode_number: u32,
-) -> Result<(), Error> {
-    buy_barcode(
-        config,
-        BarcodeId {
-            value: barcode_number,
-        },
-    )
-    .await
-    .map_err(Into::<Error>::into)
+            match $orig(&config, $($arg_name),*).await
+            {
+                Ok(ok) => Ok::<_, leptos::error::Error>(Some(ok)),
+                Err(err) => match err {
+                    rocie_client::apis::Error::ResponseError(ref response_content) => {
+                        match response_content.status.as_u16() {
+                            404 => Ok(None),
+                            _ => Err(err.into()),
+                        }
+                    }
+                    err => Err(err.into()),
+                },
+            }
+        }
+    };
+
+    (
+        @external_config
+        $orig:ident(&config, $($arg_name:ident : $arg_type:ty),*) -> $output_type:ty
+        as $new_name:ident
+    ) => {
+        pub(crate) async fn $new_name(
+            config: &Configuration,
+            $($arg_name : $arg_type),*
+        ) -> Result<$output_type, Error> {
+            $orig(config, $($arg_name),*)
+                .await
+                .map_err(Into::<Error>::into)
+        }
+    }
 }
+
+mk_wrapper!(product_by_id(product_id: ProductId) -> Product as product_by_id_wrapped);
+
+mk_wrapper!(@treat_404_as_None product_by_name(name: &str) -> Option<Product> 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<Product> as product_suggestion_by_name_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<UnitProperty> as unit_properties_wrapped);
+
+mk_wrapper!(amount_by_id(product_id: ProductId) -> ProductAmount as amount_by_id_wrapped);
+
+mk_wrapper!(products_registered() -> Vec<Product> as products_registered_wrapped);
+mk_wrapper!(products_in_storage() -> Vec<Product> as products_in_storage_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/buy.rs b/src/components/buy.rs
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/components/buy.rs
diff --git a/src/components/container.rs b/src/components/container.rs
index 83b9584..d6d2f03 100644
--- a/src/components/container.rs
+++ b/src/components/container.rs
@@ -31,7 +31,7 @@ pub fn Container(
                         .into_iter()
                         .map(|(name, path)| {
                             view! {
-                                <li class="bg-green-400/40 p-2 first:rounded-l-full last:rounded-r-full">
+                                <li class="bg-green-400/40 p-2 text-nowrap first:rounded-l-full last:rounded-r-full">
                                     <button on:click=move |_| {
                                         use_navigate()(path, NavigateOptions::default());
                                     }>{name}</button>
diff --git a/src/components/inventory.rs b/src/components/inventory.rs
new file mode 100644
index 0000000..275dd0b
--- /dev/null
+++ b/src/components/inventory.rs
@@ -0,0 +1,43 @@
+use leptos::{IntoView, component, view};
+
+use crate::{
+    api::products_in_storage_wrapped,
+    components::{async_fetch::AsyncFetch, container::Container, icon_p::IconP},
+};
+
+#[component]
+pub fn Inventory() -> impl IntoView {
+    view! {
+        <Container
+            header="Inventory"
+            buttons=vec![
+                (view! { <IconP icon=icondata_io::IoClipboard text="Inventory" /> }, "inventory"),
+                (view! { <IconP icon=icondata_io::IoPricetags text="Consume" /> }, "consume"),
+                (view! { <IconP icon=icondata_io::IoStorefront text="Buy" /> }, "buy"),
+            ]
+        >
+            {
+                AsyncFetch! {
+                    @map_error_in_producer
+                    fetcher = products_in_storage_wrapped(),
+                    producer = |products| {
+                        let products_num = products.len();
+                        let plural_s = if products_num == 1 { "" } else { "s" };
+                        let products_value = 2;
+                        let products_currency = "EUR";
+
+                        view! {
+                            <p>
+                                {format!(
+                                    "You have {products_num} product{plural_s} \
+                                         in stock with a value \
+                                         of {products_value} {products_currency}.",
+                                )}
+                            </p>
+                        }
+                    }
+                }
+            }
+        </Container>
+    }
+}
diff --git a/src/components/mod.rs b/src/components/mod.rs
index efc4842..2c3d79a 100644
--- a/src/components/mod.rs
+++ b/src/components/mod.rs
@@ -10,9 +10,11 @@ pub mod input_placeholder;
 pub mod select_placeholder;
 
 // Specific
+pub mod inventory;
 pub mod product_overview;
 pub mod recipies;
 pub mod site_header;
+pub mod unit_overview;
 
 fn get_id() -> u32 {
     static ID: AtomicU32 = AtomicU32::new(0);
diff --git a/src/components/product_overview.rs b/src/components/product_overview.rs
index 5413dc1..bf81624 100644
--- a/src/components/product_overview.rs
+++ b/src/components/product_overview.rs
@@ -1,7 +1,7 @@
 use leptos::{IntoView, component, view};
 
 use crate::{
-    api::get_products,
+    api::products_registered_wrapped,
     components::{async_fetch::AsyncFetch, container::Container, icon_p::IconP},
 };
 
@@ -9,31 +9,20 @@ use crate::{
 pub fn ProductOverview() -> impl IntoView {
     view! {
         <Container
-            header="Inventory"
+            header="Products"
             buttons=vec![
-                (view! { <IconP icon=icondata_io::IoClipboard text="Inventory" /> }, "inventory"),
-                (view! { <IconP icon=icondata_io::IoPricetags text="Consume" /> }, "consume"),
-                (view! { <IconP icon=icondata_io::IoStorefront text="Buy" /> }, "buy"),
+                (view! { <IconP icon=icondata_io::IoClipboard text="Show products" /> }, "products"),
+                (view! { <IconP icon=icondata_io::IoPricetags text="Create product" /> }, "create-product"),
+                (view! { <IconP icon=icondata_io::IoPricetags text="Associate barcode with product" /> }, "associate-barcode-product"),
             ]
         >
             {
                 AsyncFetch! {
                     @map_error_in_producer
-                    fetcher = get_products(),
+                    fetcher = products_registered_wrapped(),
                     producer = |products| {
-                        let products_num = products.len();
-                        let plural_s = if products_num == 1 { "" } else { "s" };
-                        let products_value = 2;
-                        let products_currency = "EUR";
-
                         view! {
-                            <p>
-                                {format!(
-                                    "You have {products_num} product{plural_s} \
-                                         in stock with a value \
-                                         of {products_value} {products_currency}.",
-                                )}
-                            </p>
+                            <p>{format!("You have {} products", products.len())}</p>
                         }
                     }
                 }
diff --git a/src/components/unit_overview.rs b/src/components/unit_overview.rs
new file mode 100644
index 0000000..25e5675
--- /dev/null
+++ b/src/components/unit_overview.rs
@@ -0,0 +1,21 @@
+use leptos::{IntoView, component, view};
+
+use crate::components::{container::Container, icon_p::IconP};
+
+#[component]
+pub fn UnitOverview() -> impl IntoView {
+    view! {
+        <Container
+            header="Units"
+            buttons=vec![
+                (view! { <IconP icon=icondata_io::IoClipboard text="Show unit" /> }, "units"),
+                (view! { <IconP icon=icondata_io::IoClipboard text="Create unit" /> }, "create-unit"),
+                (view! { <IconP icon=icondata_io::IoPricetags text="Create unit property" /> }, "create-unit-property"),
+            ]
+        >
+            {
+                "You have units"
+            }
+        </Container>
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index b5ca65a..36210e7 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -31,7 +31,7 @@ use reactive_stores::Store;
 use rocie_client::apis::configuration::Configuration;
 
 use crate::pages::{
-    buy::Buy, home::Home, inventory::Inventory, not_found::NotFound, recipies::Recipies,
+    associate_barcode::AssociateBarcode, buy::Buy, create_product::CreateProduct, home::Home, inventory::Inventory, not_found::NotFound, recipies::Recipies
 };
 
 #[derive(Debug, Clone, Store)]
@@ -71,6 +71,8 @@ pub fn App() -> impl IntoView {
                         view! { <Home /> }
                     }
                 />
+
+                // Inventory
                 <Route
                     path=path!("/inventory")
                     view=move || {
@@ -83,12 +85,28 @@ pub fn App() -> impl IntoView {
                         view! { <Buy /> }
                     }
                 />
+
+                // Recipes
                 <Route
                     path=path!("/recipies")
                     view=move || {
                         view! { <Recipies /> }
                     }
                 />
+
+                // Products
+                <Route
+                    path=path!("/create-product")
+                    view=move || {
+                        view! { <CreateProduct /> }
+                    }
+                />
+                <Route
+                    path=path!("/associate-barcode-product")
+                    view=move || {
+                        view! { <AssociateBarcode /> }
+                    }
+                />
             </Routes>
         </Router>
     }
diff --git a/src/pages/associate_barcode.rs b/src/pages/associate_barcode.rs
new file mode 100644
index 0000000..20714ff
--- /dev/null
+++ b/src/pages/associate_barcode.rs
@@ -0,0 +1,163 @@
+use leptos::{
+    IntoView, component,
+    prelude::{Get, Show, WriteSignal, signal},
+    task::spawn_local,
+    view,
+};
+use rocie_client::models::{Barcode, BarcodeId, Product, Unit, UnitAmount, UnitId};
+use rocie_macros::Form;
+use uuid::Uuid;
+
+use crate::{
+    api::{
+        associate_barcode_external_wrapped, get_config, product_by_id_wrapped,
+        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},
+};
+
+#[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! {
+        <SiteHeader logo=icondata_io::IoPricetag back_location="/" name="Buy" />
+
+        <Show when=move || errors.get().is_some()>
+            <Banner text=move || errors.get().expect("Was some") />
+        </Show>
+
+        {
+            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}")
+                                    )
+                                );
+                            },
+                        }
+                    });
+                };
+
+                <Input
+                    name=barcode_id,
+                    rust_type=u32,
+                    html_type="number",
+                    label="Barcode number",
+                />
+
+                <Input
+                    name=product_name,
+                    rust_type=String,
+                    html_type="text",
+                    label="Product Name",
+                    reactive=product_name_signal
+                    auto_complete=generate_suggest_products
+                />
+
+                <Show
+                    when=move || show_units.get(),
+                >
+                    <Select
+                        name=unit_id,
+                        rust_type=Uuid,
+                        label="Unit",
+                        options=AsyncResource! {
+                            (
+                                product_name: Option<String> = product_name_signal(),
+                                show_units_set: WriteSignal<bool> = show_units_set
+                            ) -> Result<Vec<(String, String)>, leptos::error::Error> {
+                                let units = product_unit_fetcher(product_name).await?;
+
+                                show_units_set.set(units.is_some());
+                                if let Some(units) = units {
+                                    Ok(
+                                        units
+                                            .into_iter()
+                                            .map(|unit| (unit.full_name_singular, unit.id.to_string()))
+                                            .collect()
+                                    )
+                                } else {
+                                    Ok(vec![])
+                                }
+                            }
+                        },
+                    />
+                </Show>
+
+                <Input
+                    name=amount,
+                    rust_type=u16,
+                    html_type="number",
+                    label="Amount"
+                />
+            }
+        }
+    }
+}
+
+async fn generate_suggest_products(
+    optional_product_name: Option<String>,
+) -> Result<Option<Vec<String>>, leptos::error::Error> {
+    if let Some(product_name) = optional_product_name
+        && !product_name.is_empty()
+    {
+        let products = product_suggestion_by_name_wrapped(&product_name).await?;
+        Ok(Some(products.into_iter().map(|prod| prod.name).collect()))
+    } else {
+        Ok(None)
+    }
+}
+
+async fn product_unit_fetcher(
+    optinal_product_name: Option<String>,
+) -> Result<Option<Vec<Unit>>, leptos::error::Error> {
+    if let Some(product_name) = optinal_product_name
+        && !product_name.is_empty()
+    {
+        let maybe_product: Option<Product> = product_by_name_404_wrapped(&product_name).await?;
+
+        if let Some(product) = maybe_product {
+            let unit_property =
+                unit_property_by_id_wrapped(product_by_id_wrapped(product.id).await?.unit_property)
+                    .await?;
+
+            let mut units = Vec::with_capacity(unit_property.units.len());
+            for unit_id in unit_property.units {
+                units.push(unit_by_id_wrapped(unit_id).await?);
+            }
+
+            Ok(Some(units))
+        } else {
+            Ok(None)
+        }
+    } else {
+        Ok(None)
+    }
+}
diff --git a/src/pages/buy.rs b/src/pages/buy.rs
index cb4cff4..f3335f6 100644
--- a/src/pages/buy.rs
+++ b/src/pages/buy.rs
@@ -1,25 +1,15 @@
 use leptos::{
     IntoView, component,
-    prelude::{Get, Read, Show, WriteSignal, expect_context, signal},
+    prelude::{Get, Show, signal},
     task::spawn_local,
     view,
 };
-use leptos_router::{NavigateOptions, hooks::use_navigate};
 use log::info;
-use reactive_stores::Store;
-use rocie_client::{
-    apis::Error,
-    models::{Product, Unit},
-};
-use uuid::Uuid;
+use rocie_client::models::BarcodeId;
 
 use crate::{
-    ConfigState, ConfigStateStoreFields,
-    api::{
-        buy_barcode_wrapper, get_product_by_name, get_product_unit_by_id,
-        get_products_by_part_name, get_unit_by_id,
-    },
-    components::{async_fetch::AsyncResource, banner::Banner, form::Form, site_header::SiteHeader},
+    api::{buy_barcode_external_wrapped, get_config},
+    components::{banner::Banner, form::Form, site_header::SiteHeader},
 };
 
 #[component]
@@ -35,20 +25,20 @@ pub fn Buy() -> impl IntoView {
 
         {
             Form! {
-                on_submit = |barcode_number, amount| {
-                    let config = expect_context::<Store<ConfigState>>();
-                    let config = config.config().read();
+                on_submit = |barcode_number, times| {
+                    let config = get_config!();
 
                     spawn_local(async move {
-                        if let Err(err) = buy_barcode_wrapper(&config, barcode_number).await {
+                        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}");
+
                             on_submit_errored_set.set(Some(error));
                         } else {
                             on_submit_errored_set.set(None);
-
-                            info!("Bought barcode {barcode_number} {amount} times");
                         }
 
+
+                        info!("Bought barcode {barcode_number} {times} times");
                     });
                 };
 
@@ -60,134 +50,12 @@ pub fn Buy() -> impl IntoView {
                 />
 
                 <Input
-                    name=amount,
-                    rust_type=u16,
-                    html_type="number",
-                    label="Amount"
-                />
-            }
-        }
-    }
-}
-
-#[component]
-pub fn AssociateBarcode() -> impl IntoView {
-    let product_name_signal;
-
-    let (show_units, show_units_set) = signal(false);
-
-    view! {
-        <SiteHeader logo=icondata_io::IoPricetag back_location="/" name="Buy" />
-
-        {
-            Form! {
-                on_submit = |product_name, amount, unit_id| {
-                    spawn_local(async move {
-                        let navigate = use_navigate();
-
-                        info!("Got product barcode: {product_name} with amount: {amount}, and {unit_id}");
-
-                        navigate("/", NavigateOptions::default());
-                    });
-                };
-
-                <Input
-                    name=product_name,
-                    rust_type=String,
-                    html_type="text",
-                    label="Product Name",
-                    reactive=product_name_signal
-                    auto_complete=generate_suggest_products
-                />
-
-                <Show
-                    when=move || show_units.get(),
-                >
-                    <Select
-                        name=unit_id,
-                        rust_type=Uuid,
-                        label="Unit",
-                        options=AsyncResource! {
-                            (
-                                product_name: Option<String> = product_name_signal(),
-                                show_units_set: WriteSignal<bool> = show_units_set
-                            ) -> Result<Vec<(String, String)>, leptos::error::Error> {
-                                let units = product_unit_fetcher(product_name).await?;
-
-                                show_units_set.set(units.is_some());
-                                if let Some(units) = units {
-                                    Ok(
-                                        units
-                                            .into_iter()
-                                            .map(|unit| (unit.full_name_singular, unit.id.to_string()))
-                                            .collect()
-                                    )
-                                } else {
-                                    Ok(vec![])
-                                }
-                            }
-                        },
-                    />
-                </Show>
-
-                <Input
-                    name=amount,
+                    name=times,
                     rust_type=u16,
                     html_type="number",
-                    label="Amount"
+                    label="Times"
                 />
             }
         }
     }
 }
-
-async fn generate_suggest_products(
-    optional_product_name: Option<String>,
-) -> Result<Option<Vec<String>>, leptos::error::Error> {
-    if let Some(product_name) = optional_product_name
-        && !product_name.is_empty()
-    {
-        let products = get_products_by_part_name(product_name).await?;
-        Ok(Some(products.into_iter().map(|prod| prod.name).collect()))
-    } else {
-        Ok(None)
-    }
-}
-
-async fn product_unit_fetcher(
-    optinal_product_name: Option<String>,
-) -> Result<Option<Vec<Unit>>, leptos::error::Error> {
-    if let Some(product_name) = optinal_product_name
-        && !product_name.is_empty()
-    {
-        let value: Option<Product> = {
-            match get_product_by_name(product_name).await {
-                Ok(ok) => Ok::<_, leptos::error::Error>(Some(ok)),
-                Err(err) => match err {
-                    Error::ResponseError(ref response_content) => {
-                        match response_content.status.as_u16() {
-                            404 => Ok(None),
-                            _ => Err(err.into()),
-                        }
-                    }
-                    err => Err(err.into()),
-                },
-            }?
-        };
-
-        if let Some(value) = value {
-            let (_, unit_property) = get_product_unit_by_id(value.id).await?;
-
-            let mut units = Vec::with_capacity(unit_property.units.len());
-            for unit_id in unit_property.units {
-                units.push(get_unit_by_id(unit_id).await?);
-            }
-
-            Ok(Some(units))
-        } else {
-            Ok(None)
-        }
-    } else {
-        Ok(None)
-    }
-}
diff --git a/src/pages/create_product.rs b/src/pages/create_product.rs
new file mode 100644
index 0000000..fcd3b0b
--- /dev/null
+++ b/src/pages/create_product.rs
@@ -0,0 +1,96 @@
+use std::{convert::Infallible, str::FromStr};
+
+use leptos::{
+    IntoView, component,
+    prelude::{Get, Show, signal},
+    task::spawn_local,
+    view,
+};
+use rocie_client::models::{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},
+};
+
+struct OptionalString(Option<String>);
+
+impl FromStr for OptionalString {
+    type Err = Infallible;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        if s.is_empty() {
+            Ok(Self(None))
+        } else {
+            Ok(Self(Some(s.to_owned())))
+        }
+    }
+}
+
+#[component]
+pub fn CreateProduct() -> impl IntoView {
+    let (error_message, error_message_set) = signal(None);
+
+    view! {
+        <SiteHeader logo=icondata_io::IoArrowBack back_location="/" name="Create Product" />
+
+        <Show when=move || error_message.get().is_some()>
+            <Banner text=move || error_message.get().expect("Is some") />
+        </Show>
+
+        {
+            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 },
+                            }
+                        ).await {
+                            Ok(_id) => {}
+                            Err(err) => error_message_set.set(Some(format!("Failed to create product: {err}"))),
+                        }
+                    });
+                };
+
+                <Input
+                    name=product_name,
+                    rust_type=String,
+                    html_type="text",
+                    label="Product Name",
+                />
+
+                <Input
+                    name=product_description,
+                    rust_type=OptionalString,
+                    html_type="text",
+                    label="Product Description"
+                />
+
+                <Select
+                    name=unit_property_id,
+                    rust_type=Uuid,
+                    label="Unit property",
+                    options=AsyncResource! {
+                        () -> Result<Vec<(String, String)>, leptos::error::Error> {
+                            let unit_properties = unit_properties_wrapped().await?;
+
+                            Ok(
+                                unit_properties
+                                    .into_iter()
+                                    .map(|prop| (prop.name, prop.id.to_string()))
+                                    .collect()
+                            )
+                        }
+                    },
+                />
+            }
+        }
+    }
+}
diff --git a/src/pages/home.rs b/src/pages/home.rs
index 387562e..b9dba64 100644
--- a/src/pages/home.rs
+++ b/src/pages/home.rs
@@ -10,7 +10,8 @@ use leptos_router::{
 };
 
 use crate::components::{
-    product_overview::ProductOverview, recipies::Recipies, site_header::SiteHeader,
+    inventory::Inventory, product_overview::ProductOverview, recipies::Recipies,
+    site_header::SiteHeader, unit_overview::UnitOverview,
 };
 
 #[component]
@@ -43,8 +44,10 @@ pub fn Home() -> impl IntoView {
 
             <div class="flex flex-col content-start">
                 <SiteHeader logo=icondata_io::IoRoseSharp back_location="/" name="Rocie" />
-                <ProductOverview />
+                <Inventory />
                 <Recipies />
+                <ProductOverview />
+                <UnitOverview />
             </div>
         </ErrorBoundary>
     }
diff --git a/src/pages/inventory.rs b/src/pages/inventory.rs
index e5ff6ae..b2ce4a1 100644
--- a/src/pages/inventory.rs
+++ b/src/pages/inventory.rs
@@ -3,10 +3,13 @@ use leptos::{
     prelude::{ClassAttribute, ElementChild},
     view,
 };
-use rocie_client::models::{Product, ProductAmount, Unit};
+use rocie_client::models::{Product, ProductAmount, ProductId, Unit};
 
 use crate::{
-    api::{get_full_product_by_id, get_products},
+    api::{
+        amount_by_id_wrapped, product_by_id_wrapped, products_in_storage_wrapped,
+        unit_by_id_wrapped,
+    },
     components::{async_fetch::AsyncFetch, site_header::SiteHeader},
 };
 
@@ -19,7 +22,7 @@ pub fn Inventory() -> impl IntoView {
             {
                 AsyncFetch! {
                     @map_error_in_producer
-                    fetcher = get_products(),
+                    fetcher = products_in_storage_wrapped(),
                     producer = render_products,
                 }
             }
@@ -40,6 +43,16 @@ fn render_products(products: Vec<Product>) -> impl IntoView {
         .collect::<Vec<_>>()
 }
 
+async fn get_full_product_by_id(
+    id: ProductId,
+) -> Result<(Product, ProductAmount, Unit), leptos::error::Error> {
+    let product = product_by_id_wrapped(id).await?;
+    let amount = amount_by_id_wrapped(id).await?;
+    let unit = unit_by_id_wrapped(amount.amount.unit).await?;
+
+    Ok((product, amount, unit))
+}
+
 fn format_full_product((product, amount, unit): (Product, ProductAmount, Unit)) -> impl IntoView {
     view! {
         <ul class="my-3">
diff --git a/src/pages/mod.rs b/src/pages/mod.rs
index a6057cd..b8a68c7 100644
--- a/src/pages/mod.rs
+++ b/src/pages/mod.rs
@@ -3,3 +3,5 @@ pub mod home;
 pub mod inventory;
 pub mod not_found;
 pub mod recipies;
+pub mod create_product;
+pub mod associate_barcode;