Migrate ingredients

This commit is contained in:
traxys 2024-03-02 17:18:12 +01:00
parent faeaf43f80
commit 129c3c59a4
2 changed files with 247 additions and 0 deletions

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

@ -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<AppState> {
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<String>, 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<String>,
household: &CurrentHousehold,
user: &AuthenticatedUser,
db: &DatabaseConnection,
) -> Result<Markup, RouteError> {
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<AppState>,
user: AuthenticatedUser,
household: CurrentHousehold,
) -> Result<Markup, RouteError> {
ingredients_view(None, &household, &user, &state.db).await
}
async fn do_ingredient_delete(
household: &CurrentHousehold,
db: &DatabaseConnection,
id: i64,
) -> Result<Option<String>, 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<AppState>,
user: AuthenticatedUser,
household: CurrentHousehold,
ig: Path<i64>,
) -> Result<Markup, RouteError> {
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<AppState>,
household: CurrentHousehold,
ig: Path<i64>,
form: Form<IngredientDesc>,
) -> Result<Redirect, RouteError> {
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<AppState>,
household: CurrentHousehold,
form: Form<IngredientDesc>,
) -> Result<Redirect, RouteError> {
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"))
}

View file

@ -23,6 +23,7 @@ use self::{household::CurrentHousehold, sidebar::SidebarLocation};
mod household;
mod recipe;
mod sidebar;
mod ingredients;
type AppState = Arc<crate::AppState>;
@ -372,5 +373,6 @@ pub(crate) fn router() -> Router<AppState> {
.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()))
}