From 7660146175c3181ffe72433fbd59ddb09402f611 Mon Sep 17 00:00:00 2001 From: traxys Date: Sat, 17 Jun 2023 21:52:13 +0200 Subject: [PATCH] app: Handle ingredient management --- app/src/ingredients.rs | 358 +++++++++++++++++++++++++++++++++++++++++ app/src/main.rs | 3 +- 2 files changed, 360 insertions(+), 1 deletion(-) create mode 100644 app/src/ingredients.rs diff --git a/app/src/ingredients.rs b/app/src/ingredients.rs new file mode 100644 index 0000000..2c8dfd4 --- /dev/null +++ b/app/src/ingredients.rs @@ -0,0 +1,358 @@ +use api::{CreateIngredientRequest, EditIngredientRequest, IngredientInfo}; +use itertools::Itertools; +use uuid::Uuid; +use wasm_bindgen::JsCast; +use web_sys::HtmlInputElement; +use yew::{prelude::*, suspense::use_future_with_deps}; + +use crate::{ + api, + bootstrap::{bs, FormModal}, + RegaladeGlobalState, +}; + +async fn fetch_ingredients(token: String, household: Uuid) -> anyhow::Result { + let rsp = gloo_net::http::Request::get(api!("household/{household}/ingredients")) + .header("Authorization", &format!("Bearer {token}")) + .send() + .await?; + + if !rsp.ok() { + let body = rsp.body(); + match body { + None => anyhow::bail!("Could not fetch ingredients: {rsp:?}"), + Some(b) => anyhow::bail!("Could not fetch ingredients: {}", b.to_string()), + } + } + + Ok(rsp.json().await?) +} + +async fn do_edit_ingredient( + token: String, + household: Uuid, + id: i64, + new_name: String, + new_unit: String, +) -> anyhow::Result<()> { + let rsp = gloo_net::http::Request::patch(api!("household/{household}/ingredients/{id}")) + .header("Authorization", &format!("Bearer {token}")) + .json(&EditIngredientRequest { + name: Some(new_name), + has_unit: true, + unit: (!new_unit.is_empty()).then_some(new_unit), + })? + .send() + .await?; + + if !rsp.ok() { + let body = rsp.body(); + match body { + None => anyhow::bail!("Could not edit ingredients: {rsp:?}"), + Some(b) => anyhow::bail!("Could not edit ingredients: {}", b.to_string()), + } + } + + Ok(()) +} + +async fn do_delete_ingredient(token: String, household: Uuid, id: i64) -> anyhow::Result<()> { + let rsp = gloo_net::http::Request::delete(api!("household/{household}/ingredients/{id}")) + .header("Authorization", &format!("Bearer {token}")) + .send() + .await?; + + if !rsp.ok() { + let body = rsp.body(); + match body { + None => anyhow::bail!("Could not delete ingredient: {rsp:?}"), + Some(b) => anyhow::bail!("Could not delete ingredient: {}", b.to_string()), + } + } + + Ok(()) +} + +#[derive(Properties, PartialEq, Eq)] +struct IngredientListProps { + token: String, + household: Uuid, + render_id: u64, +} + +#[function_component] +fn IngredientList(props: &IngredientListProps) -> HtmlResult { + let fetch_id = use_state(|| 0u64); + let ingredients = use_future_with_deps( + |_| fetch_ingredients(props.token.clone(), props.household), + (*fetch_id as u128) << 64 | props.render_id as u128, + )?; + let error = use_state(|| None::); + + let edit_state = use_state(|| None); + + let es = edit_state.clone(); + let item_edit = |id, current: IngredientInfo| { + let es = es.clone(); + Callback::from(move |_| { + es.set(Some((id, current.clone()))); + }) + }; + + let es = edit_state.clone(); + let token = props.token.clone(); + let household = props.household; + let err = error.clone(); + let fetch = fetch_id.clone(); + let on_submit = Callback::from(move |()| { + if let Some((id, _)) = &*es { + let document = gloo_utils::document(); + + let name: HtmlInputElement = document + .get_element_by_id("editIgName") + .unwrap() + .dyn_into() + .expect("editIgName is not an input element"); + let name = name.value(); + + let unit: HtmlInputElement = document + .get_element_by_id("editIgUnit") + .unwrap() + .dyn_into() + .expect("editIgUnit is not an input element"); + let unit = unit.value(); + + let token = token.clone(); + let id = *id; + let err = err.clone(); + let fetch = fetch.clone(); + + wasm_bindgen_futures::spawn_local(async move { + match do_edit_ingredient(token, household, id, name, unit).await { + Ok(_) => { + let modal = bs::Modal::get_instance("#editIgModal"); + modal.hide(); + fetch.set(*fetch + 1); + } + Err(e) => err.set(Some(format!("Could not edit ingredient: {e:?}"))), + } + }); + } + }); + + let global_error = use_state(|| None::); + let token = props.token.clone(); + let err = global_error.clone(); + let item_delete = move |id| { + let fetch = fetch_id.clone(); + let err = err.clone(); + let token = token.clone(); + + Callback::from(move |_| { + let fetch = fetch.clone(); + let err = err.clone(); + let token = token.clone(); + + wasm_bindgen_futures::spawn_local(async move { + match do_delete_ingredient(token, household, id).await { + Ok(_) => { + fetch.set(*fetch + 1); + } + Err(e) => err.set(Some(format!("Could not edit ingredient: {e:?}"))), + } + }) + }) + }; + + Ok(match &*ingredients { + Ok(l) => html! {<> + if let Some(err) = &*global_error { + + } +
    + { for l.ingredients.iter().sorted_by_key(|(&k,_)| k).map(|(&k,i)| { + html! { +
  • +

    + {&i.name} + if let Some(unit) = &i.unit { + {format!(" (unit: {unit})")} + } +

    + + +
  • + } + }) + } +
+ + if let Some(err) = &*error { + + } +
+ + +
+
+ + +
+
+ }, + Err(e) => html! { + {format!("Error fetching ingredients: {e:?}")} + }, + }) +} + +async fn do_add_ingredient( + token: String, + household: Uuid, + name: String, + unit: String, +) -> anyhow::Result<()> { + let rsp = gloo_net::http::Request::post(api!("household/{household}/ingredients")) + .header("Authorization", &format!("Bearer {token}")) + .json(&CreateIngredientRequest { + name, + unit: (!unit.is_empty()).then_some(unit), + })? + .send() + .await?; + + if !rsp.ok() { + let body = rsp.body(); + match body { + None => anyhow::bail!("Could not add ingredient: {rsp:?}"), + Some(b) => anyhow::bail!("Could not add ingredient: {}", b.to_string()), + } + } + + Ok(()) +} + +#[function_component] +pub fn Ingredients() -> Html { + let fallback = html! { {"Loading..."} }; + let global_state = use_state(RegaladeGlobalState::get); + + let render_id = use_state(|| 0u64); + let error = use_state(|| None::); + + let token = global_state.token.token.clone(); + let household = global_state.household.id; + let err = error.clone(); + let render = render_id.clone(); + let onsubmit = Callback::from(move |e: SubmitEvent| { + e.prevent_default(); + + let document = gloo_utils::document(); + + let name_elem: HtmlInputElement = document + .get_element_by_id("newIgName") + .unwrap() + .dyn_into() + .expect("editIgName is not an input element"); + let name = name_elem.value(); + + let unit_elem: HtmlInputElement = document + .get_element_by_id("newIgUnit") + .unwrap() + .dyn_into() + .expect("editIgUnit is not an input element"); + let unit = unit_elem.value(); + + let token = token.clone(); + let err = err.clone(); + let render = render.clone(); + + wasm_bindgen_futures::spawn_local(async move { + match do_add_ingredient(token, household, name, unit).await { + Ok(_) => { + name_elem.set_value(""); + unit_elem.set_value(""); + render.set(*render + 1); + } + Err(e) => err.set(Some(format!("Could not add ingredient: {e:?}"))), + } + }); + }); + + html! { +
+
+
+ if let Some(err) = &*error { + + } +
+ + +
+
+ + +
+ +
+
+ + + +
+
+ } +} diff --git a/app/src/main.rs b/app/src/main.rs index 732afde..bfc73a5 100644 --- a/app/src/main.rs +++ b/app/src/main.rs @@ -20,6 +20,7 @@ use crate::{ mod bootstrap; mod sidebar; +mod ingredients; const API_ROUTE: &str = match option_env!("REGALADE_API_SERVER_BASE") { None => "http://localhost:8085", @@ -461,7 +462,7 @@ fn switch(route: Route) -> Html { }, Route::Ingredients => html! { - {"Ingredients"} + }, Route::HouseholdSelect => html! {