From ce3821b94e2a5f457530a4bf5f2cadb5c0d30112 Mon Sep 17 00:00:00 2001 From: Greg Burri Date: Mon, 10 Feb 2025 15:02:20 +0100 Subject: [PATCH] Ingredients can now be added to shopping list when a recipe is scheduled --- Cargo.lock | 8 ++--- backend/Cargo.toml | 4 +-- backend/sql/version_1.sql | 5 ++- backend/src/data/db/recipe.rs | 41 ++++++++++++++++++++---- backend/src/services/ron.rs | 8 ++++- backend/src/translation.rs | 1 + backend/templates/recipe_view.html | 10 ++++++ backend/translation.ron | 2 ++ common/src/ron_api.rs | 1 + frontend/Cargo.toml | 1 - frontend/src/recipe_scheduler.rs | 5 +-- frontend/src/recipe_view.rs | 51 +++++++++++++++++------------- 12 files changed, 98 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5963baa..2ee9bcf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2604,15 +2604,15 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.26.3" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +checksum = "ce1475c515a4f03a8a7129bb5228b81a781a86cb0b3fbbc19e1c556d491a401f" [[package]] name = "strum_macros" -version = "0.26.4" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +checksum = "9688894b43459159c82bfa5a5fa0435c19cbe3c9b427fa1dd7b1ce0c279b18a7" dependencies = [ "heck", "proc-macro2", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index abaa4d6..0e40b12 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -32,8 +32,8 @@ rinja = { version = "0.3" } argon2 = { version = "0.5", features = ["default", "std"] } rand_core = { version = "0.9", features = ["std"] } rand = "0.9" -strum = "0.26" -strum_macros = "0.26" +strum = "0.27" +strum_macros = "0.27" lettre = { version = "0.11", default-features = false, features = [ "smtp-transport", diff --git a/backend/sql/version_1.sql b/backend/sql/version_1.sql index 07b67fe..d4adac9 100644 --- a/backend/sql/version_1.sql +++ b/backend/sql/version_1.sql @@ -186,6 +186,8 @@ CREATE TABLE [ShoppingEntry] ( -- In both cases [name], [quantity_value] and [quantity_unit] are used to display -- the entry instead of [Ingredient] data. [ingredient_id] INTEGER, + [recipe_scheduled_id] INTEGER, -- Can be null when manually added. + [is_checked] INTEGER NOT NULL DEFAULT FALSE, [name] TEXT NOT NULL DEFAULT '', @@ -194,7 +196,8 @@ CREATE TABLE [ShoppingEntry] ( [servings] INTEGER, FOREIGN KEY([user_id]) REFERENCES [User]([id]) ON DELETE CASCADE, - FOREIGN KEY([ingredient_id]) REFERENCES [Ingredient]([id]) ON DELETE SET NULL + FOREIGN KEY([ingredient_id]) REFERENCES [Ingredient]([id]) ON DELETE SET NULL, + FOREIGN KEY([recipe_scheduled_id]) REFERENCES [RecipeScheduled]([id]) ON DELETE SET NULL ); -- When an ingredient is deleted, its values are copied to any shopping entry diff --git a/backend/src/data/db/recipe.rs b/backend/src/data/db/recipe.rs index 3737114..5000dc3 100644 --- a/backend/src/data/db/recipe.rs +++ b/backend/src/data/db/recipe.rs @@ -795,7 +795,10 @@ VALUES ($1, $2) recipe_id: i64, date: NaiveDate, servings: u32, + add_ingredients_element: bool, ) -> Result { + let mut tx = self.tx().await?; + match sqlx::query( r#" INSERT INTO [RecipeScheduled] (user_id, recipe_id, date, servings) @@ -806,7 +809,7 @@ VALUES ($1, $2, $3, $4) .bind(recipe_id) .bind(date) .bind(servings) - .execute(&self.pool) + .execute(&mut *tx) .await { Err(Error::Database(error)) @@ -815,7 +818,33 @@ VALUES ($1, $2, $3, $4) { Ok(AddScheduledRecipeResult::RecipeAlreadyScheduledAtThisDate) } - _ => Ok(AddScheduledRecipeResult::Ok), + Err(error) => { + Err(DBError::from(error)) + } + Ok(insert_result) => { + + if add_ingredients_element { + sqlx::query( + r#" +INSERT INTO [ShoppingEntry] ([ingredient_id], [user_id], [recipe_scheduled_id], [servings]) + SELECT [Ingredient].[id], $2, $3, $4 FROM [Ingredient] + INNER JOIN [Step] ON [Step].[id] = [Ingredient].[step_id] + INNER JOIN [Group] ON [Group].[id] = [Step].[group_id] + INNER JOIN [Recipe] ON [Recipe].[id] = [Group].[recipe_id] + WHERE [Recipe].[id] = $1 + "#) + .bind(recipe_id) + .bind(user_id) + .bind(insert_result.last_insert_rowid()) + .bind(servings) + .execute(&mut *tx) + .await?; + } + + tx.commit().await?; + + Ok(AddScheduledRecipeResult::Ok) + } } } @@ -1009,13 +1038,13 @@ VALUES let tomorrow = today + Days::new(1); connection - .add_scheduled_recipe(user_id, recipe_id_1, today, 4) + .add_scheduled_recipe(user_id, recipe_id_1, today, 4, false) .await?; connection - .add_scheduled_recipe(user_id, recipe_id_2, yesterday, 4) + .add_scheduled_recipe(user_id, recipe_id_2, yesterday, 4, false) .await?; connection - .add_scheduled_recipe(user_id, recipe_id_1, tomorrow, 4) + .add_scheduled_recipe(user_id, recipe_id_1, tomorrow, 4, false) .await?; assert_eq!( @@ -1054,7 +1083,7 @@ VALUES // Recipe scheduled at the same date is forbidden. let Ok(AddScheduledRecipeResult::RecipeAlreadyScheduledAtThisDate) = connection - .add_scheduled_recipe(user_id, recipe_id_1, today, 4) + .add_scheduled_recipe(user_id, recipe_id_1, today, 4, false) .await else { panic!("DBError::RecipeAlreadyScheduledAtThisDate must be returned"); diff --git a/backend/src/services/ron.rs b/backend/src/services/ron.rs index 3c60ba0..d5be0b9 100644 --- a/backend/src/services/ron.rs +++ b/backend/src/services/ron.rs @@ -660,7 +660,13 @@ pub async fn schedule_recipe( check_user_rights_recipe(&connection, &user, ron.recipe_id).await?; if let Some(user) = user { connection - .add_scheduled_recipe(user.id, ron.recipe_id, ron.date, ron.servings) + .add_scheduled_recipe( + user.id, + ron.recipe_id, + ron.date, + ron.servings, + ron.add_ingredients_to_shopping_list, + ) .await .map(|res| { ron_response_ok(common::ron_api::ScheduleRecipeResult::from(res)).into_response() diff --git a/backend/src/translation.rs b/backend/src/translation.rs index 472e83a..72bd392 100644 --- a/backend/src/translation.rs +++ b/backend/src/translation.rs @@ -145,6 +145,7 @@ pub enum Sentence { CalendarAddToPlannerSuccess, CalendarAddToPlannerAlreadyExists, CalendarDateFormat, // See https://docs.rs/chrono/latest/chrono/format/strftime/index.html. + CalendarAddIngredientsToShoppingList, } pub const DEFAULT_LANGUAGE_CODE: &str = "en"; diff --git a/backend/templates/recipe_view.html b/backend/templates/recipe_view.html index 42fee49..3afa746 100644 --- a/backend/templates/recipe_view.html +++ b/backend/templates/recipe_view.html @@ -84,6 +84,7 @@ {# To create a modal dialog to choose a date and and servings #}
{% include "calendar.html" %} + + + +
{{ tr.t(Sentence::CalendarAddToPlannerSuccess) }} diff --git a/backend/translation.ron b/backend/translation.ron index a24c95b..8d1f028 100644 --- a/backend/translation.ron +++ b/backend/translation.ron @@ -129,6 +129,7 @@ (CalendarAddToPlannerSuccess, "Recipe {title} has been scheduled for {date}"), (CalendarAddToPlannerAlreadyExists, "Recipe {title} has already been scheduled for {date}"), (CalendarDateFormat, "%A, %-d %B, %C%y"), // See https://docs.rs/chrono/latest/chrono/format/strftime/index.html. + (CalendarAddIngredientsToShoppingList, "Add ingredients to shopping list"), ] ), ( @@ -261,6 +262,7 @@ (CalendarAddToPlannerSuccess, "La recette {title} a été agendée pour le {date}"), (CalendarAddToPlannerAlreadyExists, "La recette {title} a été déjà été agendée pour le {date}"), (CalendarDateFormat, "%A %-d %B %C%y"), // See https://docs.rs/chrono/latest/chrono/format/strftime/index.html. + (CalendarAddIngredientsToShoppingList, "Ajouter les ingrédients à la liste de course"), ] ) ] \ No newline at end of file diff --git a/common/src/ron_api.rs b/common/src/ron_api.rs index 1dc3b05..d86b7cb 100644 --- a/common/src/ron_api.rs +++ b/common/src/ron_api.rs @@ -194,6 +194,7 @@ pub struct ScheduleRecipe { pub recipe_id: i64, pub date: NaiveDate, pub servings: u32, + pub add_ingredients_to_shopping_list: bool, } #[derive(Serialize, Deserialize, Clone, Debug)] diff --git a/frontend/Cargo.toml b/frontend/Cargo.toml index 780b299..44e2476 100644 --- a/frontend/Cargo.toml +++ b/frontend/Cargo.toml @@ -31,7 +31,6 @@ web-sys = { version = "0.3", features = [ "NodeList", "Window", "Location", - "Storage", "EventTarget", "DragEvent", "DataTransfer", diff --git a/frontend/src/recipe_scheduler.rs b/frontend/src/recipe_scheduler.rs index 5a27e6e..f9789fd 100644 --- a/frontend/src/recipe_scheduler.rs +++ b/frontend/src/recipe_scheduler.rs @@ -115,10 +115,10 @@ impl RecipeScheduler { recipe_id: i64, date: NaiveDate, servings: u32, + add_ingredients_to_shopping_list: bool, ) -> Result { if self.is_local { - // storage.get(format("scheduled_recipes-{}-{}", ) - // storage.set("asd", "hello").unwrap(); + // TODO: use 'add_ingredients_to_shopping_list'. let mut recipe_ids_and_dates = load_scheduled_recipes(date.year(), date.month0()); for recipe in recipe_ids_and_dates.iter() { if recipe.recipe_id == recipe_id && recipe.date == date { @@ -135,6 +135,7 @@ impl RecipeScheduler { recipe_id, date, servings, + add_ingredients_to_shopping_list, }, ) .await diff --git a/frontend/src/recipe_view.rs b/frontend/src/recipe_view.rs index 575be95..a66104f 100644 --- a/frontend/src/recipe_view.rs +++ b/frontend/src/recipe_view.rs @@ -21,30 +21,37 @@ pub fn setup_page(recipe_id: i64, is_user_logged: bool) -> Result<(), JsValue> { EventListener::new(&add_to_planner, "click", move |_event| { spawn_local(async move { - if let Some((date, servings)) = modal_dialog::show_and_initialize_with_ok( - "#hidden-templates .date-and-servings", - async |element| { - calendar::setup( - element.selector(".calendar"), - calendar::CalendarOptions { - can_select_date: true, - with_link_and_remove: false, - }, - recipe_scheduler, - ) - }, - |element, calendar_state| { - let servings_element: HtmlInputElement = element.selector("#input-servings"); - ( - calendar_state.get_selected_date(), - servings_element.value_as_number() as u32, - ) - }, - ) - .await + if let Some((date, servings, add_ingredients_to_shopping_list)) = + modal_dialog::show_and_initialize_with_ok( + "#hidden-templates .date-and-servings", + async |element| { + calendar::setup( + element.selector(".calendar"), + calendar::CalendarOptions { + can_select_date: true, + with_link_and_remove: false, + }, + recipe_scheduler, + ) + }, + |element, calendar_state| { + let servings_element: HtmlInputElement = + element.selector("#input-servings"); + + let add_ingredients_element: HtmlInputElement = + element.selector("#input-add-ingredients-to-shopping-list"); + + ( + calendar_state.get_selected_date(), + servings_element.value_as_number() as u32, + add_ingredients_element.checked(), + ) + }, + ) + .await { if let Ok(result) = recipe_scheduler - .shedule_recipe(recipe_id, date, servings) + .shedule_recipe(recipe_id, date, servings, add_ingredients_to_shopping_list) .await { toast::show_element_and_initialize( -- 2.49.0