app: Handle ingredient management

This commit is contained in:
traxys 2023-06-17 21:52:13 +02:00
parent c64dcc1400
commit 7660146175
2 changed files with 360 additions and 1 deletions

358
app/src/ingredients.rs Normal file
View file

@ -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<api::IngredientList> {
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::<String>);
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::<String>);
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 {
<div class={classes!("alert", "alert-danger")} role="alert">
{err}
</div>
}
<ul class="list-group list-group-flush text-start">
{ for l.ingredients.iter().sorted_by_key(|(&k,_)| k).map(|(&k,i)| {
html! {
<li class="list-group-item d-flex align-items-center" key={k}>
<p class="flex-fill m-auto">
{&i.name}
if let Some(unit) = &i.unit {
{format!(" (unit: {unit})")}
}
</p>
<button
type="button"
class="btn btn-primary"
onclick={item_edit(k, i.clone())}
data-bs-toggle="modal"
data-bs-target="#editIgModal"
>
<i class={classes!("bi-pencil-fill")}></i>
</button>
<button
type="button"
class="btn btn-danger ms-1"
onclick={item_delete(k)}
>
<i class={classes!("bi-trash-fill")}></i>
</button>
</li>
}
})
}
</ul>
<FormModal
centered={true}
id="editIgModal"
submit_label="Edit"
title="Edit Ingredient"
{on_submit}
>
if let Some(err) = &*error {
<div class={classes!("alert", "alert-danger")} role="alert">
{err}
</div>
}
<div class="form-floating">
<input
id="editIgName"
class={classes!("form-control")}
placeholder="Ingredient Name"
value={edit_state.as_ref().map(|s| s.1.name.clone())}
/>
<label for="editIgName">{"Ingredient name"}</label>
</div>
<div class="form-floating">
<input
id="editIgUnit"
class={classes!("form-control")}
placeholder="Ingredient Unit"
value={edit_state.as_ref().map(|s| s.1.unit.clone().unwrap_or_default())}
/>
<label for="editIgUnit">{"Ingredient unit"}</label>
</div>
</FormModal>
</>},
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::<String>);
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! {
<div class="d-flex align-items-center justify-content-center w-100">
<div class={classes!("container", "text-center", "rounded", "border", "pt-2", "m-2")}>
<form {onsubmit}>
if let Some(err) = &*error {
<div class={classes!("alert", "alert-danger")} role="alert">
{err}
</div>
}
<div class="form-floating">
<input
type="text"
class="form-control"
placeholder="Ingredient Name"
id="newIgName"
/>
<label for="newIgName">{"Ingredient Name"}</label>
</div>
<div class="form-floating my-1">
<input
type="text"
class="form-control"
placeholder="Ingredient Unit"
id="newIgUnit"
/>
<label for="newIgUnit">{"Ingredient Unit"}</label>
</div>
<button class="btn btn-primary mt-2">
{"Add Ingredient"}
</button>
</form>
<hr />
<Suspense {fallback}>
<IngredientList
token={global_state.token.token.clone()}
household={global_state.household.id}
render_id={*render_id}
/>
</Suspense>
</div>
</div>
}
}

View file

@ -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! {
<GlobalStateRedirector {route}>
{"Ingredients"}
<ingredients::Ingredients />
</GlobalStateRedirector>
},
Route::HouseholdSelect => html! {