From 129c3c59a4a1c7ea8fbc92bf3fb05c4ce96e2068 Mon Sep 17 00:00:00 2001 From: traxys Date: Sat, 2 Mar 2024 17:18:12 +0100 Subject: [PATCH] Migrate ingredients --- src/app/ingredients.rs | 245 +++++++++++++++++++++++++++++++++++++++++ src/app/mod.rs | 2 + 2 files changed, 247 insertions(+) create mode 100644 src/app/ingredients.rs diff --git a/src/app/ingredients.rs b/src/app/ingredients.rs new file mode 100644 index 0000000..0978997 --- /dev/null +++ b/src/app/ingredients.rs @@ -0,0 +1,245 @@ +use axum::{ + extract::{Path, State}, + response::Redirect, + routing::{get, post}, + Form, Router, +}; +use maud::Markup; +use sea_orm::{prelude::*, ActiveValue, DatabaseConnection, IntoActiveModel, QueryOrder}; +use serde::Deserialize; + +use crate::entity::{ingredient, prelude::*}; + +use super::{ + confirm_danger_modal, error_alert, + household::CurrentHousehold, + sidebar::{sidebar, SidebarLocation}, + AppState, AuthenticatedUser, RouteError, +}; + +pub(super) fn routes() -> Router { + Router::new() + .route("/", get(ingredients)) + .route("/add", post(add_ingredient)) + .route("/delete/:ig", post(delete_ingredient)) + .route("/edit/:ig", post(edit_ingredient)) +} + +fn edit_ingredient_modal(id: &str, ingredient: &ingredient::Model) -> Markup { + maud::html! { + .modal.fade #(id) tabindex="-1" aria-labelledby={(id) "Label"} aria-hidden="true" { + .modal-dialog.modal-dialog-centered { + .modal-content { + .modal-header { + h1 .modal-title."fs-5" #{(id) "Label"} { "Edit ingredient " (ingredient.name) } + input + type="reset" + form={(id) "Form"} + .btn-close + data-bs-dismiss="modal" + aria-label="Close" + value=""; + } + .modal-body { + form #{(id) "Form"} method="post" action={"/ingredients/edit/" (ingredient.id)} { + .form-floating { + input .form-control + #{(id) "Name"} + placeholder="Ingrendient name" + name="name" + value=(ingredient.name); + label for={(id) "Name"} { "Ingredient name" } + } + .form-floating { + input .form-control + #{(id) "Unit"} + placeholder="Ingrendient unit" + name="unit" + value=(ingredient.unit.as_deref().unwrap_or_default()); + label for={(id) "Unit"} { "Ingredient unit" } + } + } + } + .modal-footer { + input type="reset" form={(id) "Form"} .btn.btn-danger data-bs-dismiss="modal" value="Cancel"; + input type="submit" form={(id) "Form"} .btn.btn-primary data-bs-dismiss="modal" value="Edit"; + } + } + } + } + } +} + +fn render_ingredients(error: Option, ingredient_list: &[ingredient::Model]) -> Markup { + maud::html! { + .d-flex.align-items-center.justify-content-center."w-100" { + .container.text-center.rounded.border."pt-2"."m-2" { + (error_alert(error)) + form method="post" action="/ingredients/add" { + .form-floating { + input .form-control name="name" id="newIgName" placeholder="Ingredient name"; + label for="newIgName" { "Ingredient name" } + } + .form-floating."my-1" { + input .form-control name="unit" id="newIgUnit" placeholder="Ingredient unit"; + label for="newIgUnit" { "Ingredient unit" } + } + input type="submit" .btn.btn-primary."mt-2" value="Add Ingredient"; + } + hr; + ul .list-group.list-group-flush.text-start { + @for ig in ingredient_list { + li .list-group-item.d-flex.align-items-center { + @let delete_id = format!("deleteIg{}", ig.id); + @let add_id = format!("addIg{}", ig.id); + p .flex-fill.m-auto { (ig.name) @if let Some(unit) = &ig.unit { {" (unit: " (unit) ")"}} } + button .btn.btn-primary + type="button" data-bs-toggle="modal" data-bs-target={"#" (add_id)} { + i .bi-pencil-fill {} + } + (edit_ingredient_modal(&add_id, ig)) + button .btn.btn-danger."ms-1" + type="button" data-bs-toggle="modal" data-bs-target={"#" (delete_id)} { + i .bi-trash-fill {} + } + (confirm_danger_modal( + &delete_id, + &format!("Are you sure you want to delete {}", ig.name), + &format!("/ingredients/delete/{}", ig.id), + "Delete ingredient", + )) + } + } + } + } + } + } +} + +async fn ingredients_view( + error: Option, + household: &CurrentHousehold, + user: &AuthenticatedUser, + db: &DatabaseConnection, +) -> Result { + let list = household + .0 + .find_related(Ingredient) + .order_by_asc(ingredient::Column::Id) + .all(db) + .await?; + + Ok(sidebar( + SidebarLocation::Ingredients, + household, + user, + render_ingredients(error, &list), + )) +} + +async fn ingredients( + state: State, + user: AuthenticatedUser, + household: CurrentHousehold, +) -> Result { + ingredients_view(None, &household, &user, &state.db).await +} + +async fn do_ingredient_delete( + household: &CurrentHousehold, + db: &DatabaseConnection, + id: i64, +) -> Result, RouteError> { + if household + .0 + .find_related(Ingredient) + .filter(ingredient::Column::Id.eq(id)) + .count(db) + .await? + == 0 + { + return Err(RouteError::RessourceNotFound); + } + + // Maybe we are racing with another deletion + let ingredient = Ingredient::find_by_id(id) + .one(db) + .await? + .ok_or(RouteError::RessourceNotFound)?; + + let recipe_count = ingredient.find_related(Recipe).count(db).await?; + if recipe_count != 0 { + return Ok(Some(format!( + "Could not delete {}, ingredient is used in {recipe_count} recipes", + ingredient.name + ))); + } + + ingredient.delete(db).await?; + + Ok(None) +} + +async fn delete_ingredient( + state: State, + user: AuthenticatedUser, + household: CurrentHousehold, + ig: Path, +) -> Result { + ingredients_view( + do_ingredient_delete(&household, &state.db, ig.0).await?, + &household, + &user, + &state.db, + ) + .await +} + +#[derive(Deserialize)] +struct IngredientDesc { + name: String, + unit: String, +} + +async fn edit_ingredient( + state: State, + household: CurrentHousehold, + ig: Path, + form: Form, +) -> Result { + let ingredient = household + .0 + .find_related(Ingredient) + .filter(ingredient::Column::Id.eq(ig.0)) + .one(&state.db) + .await? + .ok_or(RouteError::RessourceNotFound)?; + + let mut ingredient = ingredient.into_active_model(); + ingredient.name = ActiveValue::Set(form.0.name); + ingredient.unit = ActiveValue::Set(match form.0.unit.is_empty() { + true => None, + false => Some(form.0.unit), + }); + ingredient.update(&state.db).await?; + + Ok(Redirect::to("/ingredients")) +} + +async fn add_ingredient( + state: State, + household: CurrentHousehold, + form: Form, +) -> Result { + let mut ingredient = ingredient::ActiveModel::new(); + ingredient.name = ActiveValue::Set(form.0.name); + ingredient.unit = ActiveValue::Set(match form.0.unit.is_empty() { + true => None, + false => Some(form.0.unit), + }); + ingredient.household = ActiveValue::Set(household.0.id); + + ingredient.insert(&state.db).await?; + + Ok(Redirect::to("/ingredients")) +} diff --git a/src/app/mod.rs b/src/app/mod.rs index 9299e4d..5ef9aed 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -23,6 +23,7 @@ use self::{household::CurrentHousehold, sidebar::SidebarLocation}; mod household; mod recipe; mod sidebar; +mod ingredients; type AppState = Arc; @@ -372,5 +373,6 @@ pub(crate) fn router() -> Router { .route("/login/redirect/:id", get(oidc_login_finish)) .nest("/household", household::routes()) .nest("/recipe", recipe::routes()) + .nest("/ingredients", ingredients::routes()) .fallback_service(ServeDir::new(public).fallback((|| async { not_found() }).into_service())) }