Ingredients can now be added to shopping list when a recipe is scheduled
authorGreg Burri <greg.burri@gmail.com>
Mon, 10 Feb 2025 14:02:20 +0000 (15:02 +0100)
committerGreg Burri <greg.burri@gmail.com>
Mon, 10 Feb 2025 14:02:20 +0000 (15:02 +0100)
12 files changed:
Cargo.lock
backend/Cargo.toml
backend/sql/version_1.sql
backend/src/data/db/recipe.rs
backend/src/services/ron.rs
backend/src/translation.rs
backend/templates/recipe_view.html
backend/translation.ron
common/src/ron_api.rs
frontend/Cargo.toml
frontend/src/recipe_scheduler.rs
frontend/src/recipe_view.rs

index 5963baa..2ee9bcf 100644 (file)
@@ -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",
index abaa4d6..0e40b12 100644 (file)
@@ -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",
index 07b67fe..d4adac9 100644 (file)
@@ -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
index 3737114..5000dc3 100644 (file)
@@ -795,7 +795,10 @@ VALUES ($1, $2)
         recipe_id: i64,
         date: NaiveDate,
         servings: u32,
+        add_ingredients_element: bool,
     ) -> Result<AddScheduledRecipeResult> {
+        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");
index 3c60ba0..d5be0b9 100644 (file)
@@ -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()
index 472e83a..72bd392 100644 (file)
@@ -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";
index 42fee49..3afa746 100644 (file)
@@ -84,6 +84,7 @@
         {# To create a modal dialog to choose a date and and servings #}
         <div class="date-and-servings" >
             {% include "calendar.html" %}
+
             <label for="input-servings">{{ tr.t(Sentence::RecipeServings) }}</label>
             <input
                 id="input-servings"
                     4
                 {% endif %}
                 "/>
+
+            <input
+                id="input-add-ingredients-to-shopping-list"
+                type="checkbox"
+                checked
+            >
+            <label for="input-add-ingredients-to-shopping-list">
+                {{ tr.t(Sentence::CalendarAddIngredientsToShoppingList) }}
+            </label>
         </div>
 
         <span class="calendar-add-to-planner-success">{{ tr.t(Sentence::CalendarAddToPlannerSuccess) }}</span>
index a24c95b..8d1f028 100644 (file)
             (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"),
         ]
     ),
     (
             (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
index 1dc3b05..d86b7cb 100644 (file)
@@ -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)]
index 780b299..44e2476 100644 (file)
@@ -31,7 +31,6 @@ web-sys = { version = "0.3", features = [
     "NodeList",
     "Window",
     "Location",
-    "Storage",
     "EventTarget",
     "DragEvent",
     "DataTransfer",
index 5a27e6e..f9789fd 100644 (file)
@@ -115,10 +115,10 @@ impl RecipeScheduler {
         recipe_id: i64,
         date: NaiveDate,
         servings: u32,
+        add_ingredients_to_shopping_list: bool,
     ) -> Result<ScheduleRecipeResult> {
         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
index 575be95..a66104f 100644 (file)
@@ -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(