From 7bff22756beec82b4a1470e2d325b706dc56e5f2 Mon Sep 17 00:00:00 2001
From: Benedikt Peetz
Date: Thu, 23 Oct 2025 01:36:39 +0200
Subject: feat(buy): Provide basic buy interface
---
src/api/mod.rs | 56 ++++++++-
src/components/async_fetch.rs | 67 ++++++-----
src/components/banner.rs | 17 +++
src/components/buy.rs | 212 ++++++++++++++++++++++++++++++-----
src/components/input_placeholder.rs | 154 ++++++++++++++++++++++---
src/components/mod.rs | 1 +
src/components/product_overview.rs | 8 +-
src/components/select_placeholder.rs | 40 ++++---
8 files changed, 457 insertions(+), 98 deletions(-)
create mode 100644 src/components/banner.rs
(limited to 'src')
diff --git a/src/api/mod.rs b/src/api/mod.rs
index 8b9e77d..3879223 100644
--- a/src/api/mod.rs
+++ b/src/api/mod.rs
@@ -6,10 +6,16 @@ use reactive_stores::Store;
use rocie_client::{
apis::{
api_get_inventory_api::amount_by_id,
- api_get_product_api::{product_by_id, products},
+ api_get_product_api::{
+ product_by_id, product_by_name, product_suggestion_by_name, products,
+ },
api_get_unit_api::unit_by_id,
+ api_get_unit_property_api::unit_property_by_id,
+ api_set_barcode_api::buy_barcode, configuration::Configuration,
+ },
+ models::{
+ BarcodeId, Product, ProductAmount, ProductId, Unit, UnitId, UnitProperty, UnitPropertyId,
},
- models::{Product, ProductAmount, ProductId, Unit, UnitId},
};
use crate::{ConfigState, ConfigStateStoreFields};
@@ -26,12 +32,36 @@ pub(crate) async fn get_product_by_id(product_id: ProductId) -> Result::into)
}
+pub(crate) async fn get_product_by_name(
+ name: String,
+) -> Result<
+ Product,
+ rocie_client::apis::Error,
+> {
+ let config = expect_context::>();
+ product_by_name(&config.config().read(), &name).await
+}
+pub(crate) async fn get_products_by_part_name(part_name: String) -> Result, Error> {
+ let config = expect_context::>();
+ product_suggestion_by_name(&config.config().read(), &part_name)
+ .await
+ .map_err(Into::::into)
+}
pub(crate) async fn get_unit_by_id(unit_id: UnitId) -> Result {
let config = expect_context::>();
unit_by_id(&config.config().read(), unit_id)
.await
.map_err(Into::::into)
}
+pub(crate) async fn get_unit_property_by_id(
+ unit_id: UnitPropertyId,
+) -> Result {
+ let config = expect_context::>();
+ unit_property_by_id(&config.config().read(), unit_id)
+ .await
+ .map_err(Into::::into)
+}
+
pub(crate) async fn get_full_product_by_id(
id: ProductId,
) -> Result<(Product, ProductAmount, Unit), Error> {
@@ -41,6 +71,14 @@ pub(crate) async fn get_full_product_by_id(
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?;
+
+ Ok::<_, Error>((product, unit))
+}
pub(crate) async fn get_products() -> Result, Error> {
let config = expect_context::>();
@@ -48,3 +86,17 @@ pub(crate) async fn get_products() -> Result, Error> {
.await
.map_err(Into::::into)
}
+
+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::::into)
+}
diff --git a/src/components/async_fetch.rs b/src/components/async_fetch.rs
index 7105c6f..f24e3a5 100644
--- a/src/components/async_fetch.rs
+++ b/src/components/async_fetch.rs
@@ -1,5 +1,23 @@
+macro_rules! AsyncResource {
+ (
+ (
+ $(
+ $input_name:ident : $input_type:ty = $input:expr
+ ),*
+ ) -> $output:ty $fetcher:block
+ ) => {{
+ async fn fetcher($($input_name : $input_type),*) -> $output $fetcher
+
+ leptos::prelude::LocalResource::new(move || fetcher($($input),*))
+ }}
+}
+pub(crate) use AsyncResource;
+
macro_rules! AsyncFetch {
- (fetcher = $fetcher:block producer = |$bound_variable:pat_param| $producer:block) => {{
+ (
+ fetcher = $fetcher:block
+ producer = |$bound_variable:pat_param| $producer:block
+ ) => {{
use leptos::{
prelude::{ElementChild, LocalResource, Suspend, Transition},
view,
@@ -18,33 +36,24 @@ macro_rules! AsyncFetch {
}
}};
+ (
+ @map_error_in_producer
+ from_resource = $resource:ident
+ producer = |$bound_variable:pat_param| $producer:block
+ ) => {{
+ use leptos::prelude::{ElementChild, Suspend, Transition};
+
+ leptos::view! {
+ "Loading..."
}
+ }>
+ {move || Suspend::new(async move {
+ $resource
+ .await
+ .map(|$bound_variable| $producer)
+ })}
+
+ }
+ }};
}
pub(crate) use AsyncFetch;
-
-// #[component]
-// pub fn AsyncFetch(
-// fetcher: impl Fn() -> Fut + 'static + Send + Sync,
-// producer: P,
-// ) -> impl IntoView
-// where
-// V: IntoView + 'static,
-// P: Fn(T) -> V + 'static + Send + Sync,
-// Fut: Future
}
-// }>
-// { || Suspend::new(async {
-// let value_resource = LocalResource::new( || fetcher());
-// value_resource
-// .await
-// .map(|value| {
-// producer(value)
-// })
-// })}
-//
-// }
-// }
diff --git a/src/components/banner.rs b/src/components/banner.rs
new file mode 100644
index 0000000..acaaf62
--- /dev/null
+++ b/src/components/banner.rs
@@ -0,0 +1,17 @@
+use leptos::{
+ IntoView, component,
+ prelude::{ClassAttribute, ElementChild},
+ view,
+};
+
+#[component]
+pub fn Banner(mut text: T) -> impl IntoView
+where
+ T: FnMut() -> String + Send + 'static,
+{
+ view! {
+
+ {move || text()}
+
+ }
+}
diff --git a/src/components/buy.rs b/src/components/buy.rs
index 6d9402e..cb4cff4 100644
--- a/src/components/buy.rs
+++ b/src/components/buy.rs
@@ -1,41 +1,193 @@
-use leptos::{IntoView, component, view};
+use leptos::{
+ IntoView, component,
+ prelude::{Get, Read, Show, WriteSignal, expect_context, signal},
+ task::spawn_local,
+ view,
+};
+use leptos_router::{NavigateOptions, hooks::use_navigate};
use log::info;
-use rocie_client::models::UnitId;
+use reactive_stores::Store;
+use rocie_client::{
+ apis::Error,
+ models::{Product, Unit},
+};
use uuid::Uuid;
-use crate::components::{form::Form, site_header::SiteHeader};
+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},
+};
#[component]
pub fn Buy() -> impl IntoView {
+ let (on_submit_errored, on_submit_errored_set) = signal(None);
+
+ view! {
+
+
+
+
+
+
+ {
+ Form! {
+ on_submit = |barcode_number, amount| {
+ let config = expect_context::>();
+ let config = config.config().read();
+
+ spawn_local(async move {
+ if let Err(err) = buy_barcode_wrapper(&config, barcode_number).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");
+ }
+
+ });
+ };
+
+
+
+
+ }
+ }
+ }
+}
+
+#[component]
+pub fn AssociateBarcode() -> impl IntoView {
+ let product_name_signal;
+
+ let (show_units, show_units_set) = signal(false);
+
view! {
- {Form! {
- on_submit = |product_barcode, amount, unit_id| {
- info!("Got product barcode: {product_barcode} with amount: {amount}, {unit_id}");
- };
-
-
-
-
- }}
+ {
+ 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());
+ });
+ };
+
+
+
+
+
+
+
+ }
+ }
+ }
+}
+
+async fn generate_suggest_products(
+ optional_product_name: Option,
+) -> Result