summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/api/mod.rs56
-rw-r--r--src/components/async_fetch.rs67
-rw-r--r--src/components/banner.rs17
-rw-r--r--src/components/buy.rs210
-rw-r--r--src/components/input_placeholder.rs154
-rw-r--r--src/components/mod.rs1
-rw-r--r--src/components/product_overview.rs8
-rw-r--r--src/components/select_placeholder.rs40
8 files changed, 456 insertions, 97 deletions
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<Product,
.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)
+}
+
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<Vec<Product>, Error> {
let config = expect_context::<Store<ConfigState>>();
@@ -48,3 +86,17 @@ pub(crate) async fn get_products() -> Result<Vec<Product>, Error> {
.await
.map_err(Into::<Error>::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::<Error>::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 {
</Transition>
}
}};
+ (
+ @map_error_in_producer
+ from_resource = $resource:ident
+ producer = |$bound_variable:pat_param| $producer:block
+ ) => {{
+ use leptos::prelude::{ElementChild, Suspend, Transition};
+
+ leptos::view! {
+ <Transition fallback=|| {
+ view! { <p>"Loading..."</p> }
+ }>
+ {move || Suspend::new(async move {
+ $resource
+ .await
+ .map(|$bound_variable| $producer)
+ })}
+ </Transition>
+ }
+ }};
}
pub(crate) use AsyncFetch;
-
-// #[component]
-// pub fn AsyncFetch<P, V, T, Fut>(
-// fetcher: impl Fn() -> Fut + 'static + Send + Sync,
-// producer: P,
-// ) -> impl IntoView
-// where
-// V: IntoView + 'static,
-// P: Fn(T) -> V + 'static + Send + Sync,
-// Fut: Future<Output = Result<T, Error>> + 'static,
-// T: 'static,
-// LocalResource<Result<T, Error>>: IntoFuture<Output = Result<T, Error>> + Send,
-// {
-// view! {
-// <Transition fallback=|| {
-// view! { <p>"Loading..."</p> }
-// }>
-// { || Suspend::new(async {
-// let value_resource = LocalResource::new( || fetcher());
-// value_resource
-// .await
-// .map(|value| {
-// producer(value)
-// })
-// })}
-// </Transition>
-// }
-// }
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<T>(mut text: T) -> impl IntoView
+where
+ T: FnMut() -> String + Send + 'static,
+{
+ view! {
+ <p class="text-white rounded-lg m-2 p-2 bg-red-600">
+ {move || text()}
+ </p>
+ }
+}
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! {
+ <SiteHeader logo=icondata_io::IoPricetag back_location="/" name="Buy" />
+
+ <Show when=move || on_submit_errored.get().is_some()>
+ <Banner text=move || on_submit_errored.get().expect("Should be some") />
+ </Show>
+
+ {
+ Form! {
+ on_submit = |barcode_number, amount| {
+ let config = expect_context::<Store<ConfigState>>();
+ 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");
+ }
+
+ });
+ };
+
+ <Input
+ name=barcode_number,
+ rust_type=u32,
+ html_type="number",
+ label="Barcode Number",
+ />
+
+ <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_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());
+ });
+ };
+
+ <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 = 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?);
+ }
- <Input
- name=product_barcode,
- rust_type=u32,
- html_type="number",
- label="Product Barcode"
- />
- <Select
- name=unit_id,
- rust_type=Uuid,
- label="Unit",
- options=[
- ("Kilogram", Uuid::new_v4()),
- ("Gram", Uuid::new_v4())
- ]
- />
- <Input
- name=amount,
- rust_type=u16,
- html_type="number",
- label="Amount"
- />
- }}
+ Ok(Some(units))
+ } else {
+ Ok(None)
+ }
+ } else {
+ Ok(None)
}
}
diff --git a/src/components/input_placeholder.rs b/src/components/input_placeholder.rs
index aeef838..99b3196 100644
--- a/src/components/input_placeholder.rs
+++ b/src/components/input_placeholder.rs
@@ -1,27 +1,40 @@
use leptos::{
IntoView, component,
+ error::Error,
html::Input,
- prelude::{ClassAttribute, ElementChild, GlobalAttributes, NodeRef, NodeRefAttribute},
+ prelude::{
+ ClassAttribute, CollectView, ElementChild, Get, GlobalAttributes, LocalResource, NodeRef,
+ NodeRefAttribute, OnAttribute, OnTargetAttribute, PropAttribute, Set, Show, WriteSignal,
+ signal,
+ },
view,
};
+use log::{error, info};
use crate::components::get_id;
-
#[component]
+#[expect(clippy::too_many_lines)]
pub fn InputPlaceholder(
input_type: &'static str,
label: &'static str,
node_ref: NodeRef<Input>,
#[prop(default = None)] initial_value: Option<String>,
+ #[prop(default = None)] reactive: Option<WriteSignal<Option<String>>>,
+ #[prop(default = None)] auto_complete: Option<
+ LocalResource<Result<Option<Vec<String>>, Error>>,
+ >,
) -> impl IntoView {
let id = get_id();
+ let (autocomplete_signal, autocomplete_set) = signal(String::new());
+
view! {
<div class="relative h-14">
<input
id=id.to_string()
type=input_type
+ autocomplete="off"
class="\
absolute \
bottom-0 \
@@ -32,7 +45,8 @@ pub fn InputPlaceholder(
border-gray-200 \
focus:outline-none \
h-10 \
- peer \
+ peer/input \
+ group/input \
placeholder-transparent \
rounded-t-lg \
text-gray-900 \
@@ -41,16 +55,23 @@ pub fn InputPlaceholder(
placeholder="sentinel value"
node_ref=node_ref
value=initial_value
+ on:input:target=move |ev| {
+ if let Some(signal) = reactive {
+ signal.set(Some(ev.target().value()));
+ autocomplete_set.set(ev.target().value());
+ }
+ }
+ prop:value=autocomplete_signal
/>
// TODO: Reference `var(--tw-border-2)` instead of the `2 px` <2025-10-01>
- <div class="
+ <div class="\
absolute \
bottom-0 \
h-[2px] \
w-full \
bg-gray-300 \
- peer-focus:bg-indigo-600 \
+ peer-focus/input:bg-indigo-600 \
" />
<label
@@ -62,18 +83,125 @@ pub fn InputPlaceholder(
text-gray-700 \
text-sm \
transition-all \
- peer-focus:bottom-10 \
- peer-focus:left-0 \
- peer-focus:text-gray-700 \
- peer-focus:text-sm \
- peer-placeholder-shown:text-base \
- peer-placeholder-shown:text-gray-400 \
- peer-placeholder-shown:bottom-2 \
- peer-placeholder-shown:left-2 \
+ peer-focus/input:bottom-10 \
+ peer-focus/input:left-0 \
+ peer-focus/input:text-gray-700 \
+ peer-focus/input:text-sm \
+ peer-placeholder-shown/input:text-base \
+ peer-placeholder-shown/input:text-gray-400 \
+ peer-placeholder-shown/input:bottom-2 \
+ peer-placeholder-shown/input:left-2 \
"
>
{label}
</label>
+
+ <Show
+ when=move || {
+ !autocomplete_signal.get().is_empty()
+ }
+ fallback=move || ()
+ >
+ <div class="\
+ absolute \
+ top-0 \
+ left-0 \
+ invisible \
+ peer-focus/input:visible \
+ in-focus:visible \
+ ">
+ <div class="\
+ flex \
+ flex-row \
+ g-0 \
+ ">
+ // TODO: Reference `var(--tw-border-8)` instead of the `8 px` <2025-10-11>
+ <div class="w-[8px] h-full" />
+ <div class="\
+ flex \
+ flex-col \
+ g-0 \
+ ">
+ <div class="h-14 w-full peer/div" />
+ <div class="\
+ bg-white \
+ shadow \
+ outline \
+ outline-black/5 \
+ rounded-lg \
+ z-50 \
+ p-2 \
+ visible \
+ ">
+ {move || {
+ auto_complete
+ .map(|auto_complete| {
+ provide_auto_completion(
+ auto_complete,
+ autocomplete_set,
+ reactive
+ )
+ })
+ }}
+ </div>
+ </div>
+ </div>
+ </div>
+ </Show>
</div>
}
}
+
+fn provide_auto_completion(
+ auto_complete: LocalResource<Result<Option<Vec<String>>, Error>>,
+ autocomplete_set: WriteSignal<String>,
+ reactive: Option<WriteSignal<Option<String>>>,
+) -> impl IntoView {
+ match auto_complete.get() {
+ Some(resource_result) => match resource_result {
+ Ok(resource_fetch) => resource_fetch.map(|_| {
+ view! {
+ <div class="flex flex-col g-1">
+ {move || {
+ auto_complete
+ .get()
+ .expect("Worked before")
+ .unwrap()
+ .unwrap()
+ .into_iter()
+ .map(|item| {
+ let item2 = item.clone();
+ view! {
+ <button
+ type="button"
+ on:click=move |_| {
+ autocomplete_set.set(item2.clone());
+ reactive
+ .expect(
+ "Should be set, \
+ when autocomplete is used")
+ .set(Some(item2.clone()));
+
+ info!("Set autocomplete to {item2}.");
+ }
+ >
+ {item}
+ </button>
+ }
+ })
+ .collect_view()
+ }}
+ </div>
+ }
+ }),
+ Err(err) => {
+ error!(
+ "Error while loading \
+ autocompletion: {err}"
+ );
+ None
+ }
+ },
+ None => None,
+ }
+}
diff --git a/src/components/mod.rs b/src/components/mod.rs
index 1ee37d5..ca2ac10 100644
--- a/src/components/mod.rs
+++ b/src/components/mod.rs
@@ -7,6 +7,7 @@ pub mod form;
pub mod icon_p;
pub mod input_placeholder;
pub mod select_placeholder;
+pub mod banner;
// Specific
pub mod buy;
diff --git a/src/components/product_overview.rs b/src/components/product_overview.rs
index d86c04d..777baef 100644
--- a/src/components/product_overview.rs
+++ b/src/components/product_overview.rs
@@ -19,8 +19,9 @@ pub fn ProductOverview() -> impl IntoView {
(view! { <IconP icon=icondata_io::IoStorefront text="Buy" /> }, "buy"),
]
>
- {AsyncFetch!(
- fetcher = {get_products()}
+ {
+ AsyncFetch! {
+ fetcher = {get_products()}
producer = |products| {
let products_num = products.len();
let plural_s = if products_num == 1 { "" } else { "s" };
@@ -37,7 +38,8 @@ pub fn ProductOverview() -> impl IntoView {
</p>
}
}
- )}
+ }
+ }
</Container>
}
}
diff --git a/src/components/select_placeholder.rs b/src/components/select_placeholder.rs
index 947931c..2e0f783 100644
--- a/src/components/select_placeholder.rs
+++ b/src/components/select_placeholder.rs
@@ -1,37 +1,24 @@
use leptos::{
- IntoView,
- attr::{AttributeValue, IntoAttributeValue},
- component,
+ IntoView, component,
+ error::Error,
html::Select,
prelude::{
ClassAttribute, CollectView, ElementChild, GlobalAttributes, NodeRef, NodeRefAttribute,
},
+ server::LocalResource,
view,
};
-use crate::components::get_id;
+use crate::components::{async_fetch::AsyncFetch, get_id};
#[component]
-pub fn SelectPlaceholder<T>(
+pub fn SelectPlaceholder(
label: &'static str,
node_ref: NodeRef<Select>,
- options: Vec<(&'static str, T)>,
-) -> impl IntoView
-where
- T: IntoAttributeValue,
- <T as IntoAttributeValue>::Output: Send + AttributeValue,
-{
+ options: LocalResource<Result<Vec<(String, String)>, Error>>,
+) -> impl IntoView {
let id = get_id();
- let options = options
- .into_iter()
- .map(|(label, value)| {
- view! {
- <option value=value>{label}</option>
- }
- })
- .collect_view();
-
view! {
<div class="relative h-14">
<select
@@ -54,7 +41,18 @@ where
"
node_ref=node_ref
>
- {options}
+ {move || AsyncFetch! {
+ @map_error_in_producer
+ from_resource = options
+ producer = |options| {
+ options
+ .into_iter()
+ .map(|(label, value)| {
+ view! { <option value=value>{label}</option> }
+ })
+ .collect_view()
+ }
+ }}
</select>
// TODO: Reference `var(--tw-border-2)` instead of the `2 px` <2025-10-01>