Shopping list (WIP)
This commit is contained in:
parent
ce3821b94e
commit
084be9fb00
16 changed files with 296 additions and 90 deletions
|
|
@ -124,10 +124,10 @@ body {
|
|||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
#hidden-templates {
|
||||
display: none;
|
||||
}
|
||||
#hidden-templates {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#recipe-edit {
|
||||
|
|
|
|||
|
|
@ -177,6 +177,7 @@ CREATE TABLE [RecipeScheduled] (
|
|||
FOREIGN KEY([recipe_id]) REFERENCES [Recipe]([id]) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX [RecipeScheduled_user_id_index] ON [RecipeScheduled]([user_id]);
|
||||
CREATE INDEX [RecipeScheduled_date_index] ON [RecipeScheduled]([date]);
|
||||
|
||||
CREATE TABLE [ShoppingEntry] (
|
||||
|
|
@ -200,6 +201,8 @@ CREATE TABLE [ShoppingEntry] (
|
|||
FOREIGN KEY([recipe_scheduled_id]) REFERENCES [RecipeScheduled]([id]) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX [ShoppingEntry_user_id_index] ON [ShoppingEntry]([user_id]);
|
||||
|
||||
-- When an ingredient is deleted, its values are copied to any shopping entry
|
||||
-- that referenced it.
|
||||
CREATE TRIGGER [Ingredient_trigger_delete]
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ use crate::consts;
|
|||
|
||||
pub mod recipe;
|
||||
pub mod settings;
|
||||
pub mod shopping_list;
|
||||
pub mod user;
|
||||
|
||||
const CURRENT_DB_VERSION: u32 = 1;
|
||||
|
|
|
|||
38
backend/src/data/db/shopping_list.rs
Normal file
38
backend/src/data/db/shopping_list.rs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
use sqlx;
|
||||
|
||||
use super::{Connection, DBError, Result};
|
||||
use crate::data::model;
|
||||
|
||||
impl Connection {
|
||||
pub async fn get_shopping_list(&self, user_id: i64) -> Result<Vec<model::ShoppingListItem>> {
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
SELECT [ShoppingEntry].[id],
|
||||
CASE [ShoppingEntry].[name]
|
||||
WHEN '' THEN [Ingredient].[name]
|
||||
ELSE [ShoppingEntry].[name]
|
||||
END AS [name],
|
||||
CASE WHEN [ShoppingEntry].[quantity_value] IS NOT NULL THEN [ShoppingEntry].[quantity_value]
|
||||
ELSE [Ingredient].[quantity_value]
|
||||
END AS [quantity_value],
|
||||
CASE [ShoppingEntry].[quantity_unit] WHEN '' THEN [Ingredient].[quantity_unit]
|
||||
ELSE [ShoppingEntry].[quantity_unit]
|
||||
END AS [quantity_unit],
|
||||
[Recipe].[id] AS [recipe_id],
|
||||
[Recipe].[title] AS [recipe_title],
|
||||
[RecipeScheduled].[date],
|
||||
[is_checked]
|
||||
FROM [ShoppingEntry]
|
||||
LEFT JOIN [Ingredient] ON [Ingredient].[id] = [ShoppingEntry].[ingredient_id]
|
||||
LEFT JOIN [RecipeScheduled] ON [RecipeScheduled].[id] = [ShoppingEntry].[recipe_scheduled_id]
|
||||
LEFT JOIN [Recipe] ON [Recipe].[id] = [RecipeScheduled].[recipe_id]
|
||||
WHERE [ShoppingEntry].[user_id] = $1
|
||||
ORDER BY [is_checked], [recipe_id], [name]
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(DBError::from)
|
||||
}
|
||||
}
|
||||
|
|
@ -72,3 +72,15 @@ pub struct Ingredient {
|
|||
pub quantity_value: Option<f64>,
|
||||
pub quantity_unit: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, FromRow)]
|
||||
pub struct ShoppingListItem {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub quantity_value: Option<f64>,
|
||||
pub quantity_unit: String,
|
||||
pub recipe_id: Option<i64>,
|
||||
pub recipe_title: Option<String>,
|
||||
pub date: Option<NaiveDate>,
|
||||
pub is_checked: bool,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -190,6 +190,10 @@ async fn main() {
|
|||
"/calendar/remove_scheduled_recipe",
|
||||
delete(services::ron::rm_scheduled_recipe),
|
||||
)
|
||||
.route(
|
||||
"/shopping_list/get_list",
|
||||
get(services::ron::get_shopping_list),
|
||||
)
|
||||
.fallback(services::ron::not_found);
|
||||
|
||||
let fragments_routes = Router::new().route(
|
||||
|
|
|
|||
|
|
@ -618,7 +618,7 @@ pub async fn set_ingredients_order(
|
|||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
/// Calendar ///
|
||||
/*** Calendar ***/
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn get_scheduled_recipes(
|
||||
|
|
@ -692,7 +692,46 @@ pub async fn rm_scheduled_recipe(
|
|||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
/// 404 ///
|
||||
/*** Shopping list ***/
|
||||
|
||||
impl From<model::ShoppingListItem> for common::ron_api::ShoppingListItem {
|
||||
fn from(item: model::ShoppingListItem) -> Self {
|
||||
Self {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
quantity_value: item.quantity_value,
|
||||
quantity_unit: item.quantity_unit,
|
||||
recipe_id: item.recipe_id,
|
||||
recipe_title: item.recipe_title,
|
||||
date: item.date,
|
||||
is_checked: item.is_checked,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn get_shopping_list(
|
||||
State(connection): State<db::Connection>,
|
||||
Extension(user): Extension<Option<model::User>>,
|
||||
) -> Result<impl IntoResponse> {
|
||||
if let Some(user) = user {
|
||||
Ok(ron_response_ok(
|
||||
connection
|
||||
.get_shopping_list(user.id)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(common::ron_api::ShoppingListItem::from)
|
||||
.collect::<Vec<_>>(),
|
||||
))
|
||||
} else {
|
||||
Err(ErrorResponse::from(ron_error(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
NOT_AUTHORIZED_MESSAGE,
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/*** 404 ***/
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn not_found(Extension(_user): Extension<Option<model::User>>) -> impl IntoResponse {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,20 @@
|
|||
|
||||
<div class="content" id="home">
|
||||
{% include "calendar.html" %}
|
||||
|
||||
<div id="shopping-list">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="hidden-templates">
|
||||
<div class="shopping-item">
|
||||
<input class="item-is-checked" type="checkbox"/>
|
||||
<div class="item-quantity"></div>
|
||||
<div class="item-name"></div>
|
||||
<div class="item-scheduled-recipe"><a></a></div>
|
||||
<div class="item-delete"></div>
|
||||
</div>
|
||||
<span class="calendar-date-format">{{ tr.t(Sentence::CalendarDateFormat) }}</span>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -74,7 +74,7 @@
|
|||
{%+ if recipe.is_published %}
|
||||
checked
|
||||
{% endif %}
|
||||
>
|
||||
/>
|
||||
<label for="input-is-published">{{ tr.t(Sentence::RecipeIsPublished) }}</label>
|
||||
|
||||
<input id="input-delete" type="button" value="{{ tr.t(Sentence::RecipeDelete) }}" />
|
||||
|
|
@ -83,63 +83,63 @@
|
|||
</div>
|
||||
|
||||
<input id="input-add-group" type="button" value="{{ tr.t(Sentence::RecipeAddAGroup) }}" />
|
||||
</div>
|
||||
|
||||
<div id="hidden-templates">
|
||||
<div class="group">
|
||||
<span class="drag-handle"></span>
|
||||
<div id="hidden-templates">
|
||||
<div class="group">
|
||||
<span class="drag-handle"></span>
|
||||
|
||||
<label for="input-group-name">{{ tr.t(Sentence::RecipeGroupName) }}</label>
|
||||
<input class="input-group-name" type="text" />
|
||||
<label for="input-group-name">{{ tr.t(Sentence::RecipeGroupName) }}</label>
|
||||
<input class="input-group-name" type="text" />
|
||||
|
||||
<label for="input-group-comment">{{ tr.t(Sentence::RecipeGroupComment) }}</label>
|
||||
<input class="input-group-comment" type="text" />
|
||||
<label for="input-group-comment">{{ tr.t(Sentence::RecipeGroupComment) }}</label>
|
||||
<input class="input-group-comment" type="text" />
|
||||
|
||||
<input class="input-group-delete" type="button" value="{{ tr.t(Sentence::RecipeRemoveGroup) }}" />
|
||||
<input class="input-group-delete" type="button" value="{{ tr.t(Sentence::RecipeRemoveGroup) }}" />
|
||||
|
||||
<div class="steps">
|
||||
</div>
|
||||
|
||||
<input class="input-add-step" type="button" value="{{ tr.t(Sentence::RecipeAddAStep) }}" />
|
||||
<div class="steps">
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<span class="drag-handle"></span>
|
||||
|
||||
<label for="text-area-step-action">{{ tr.t(Sentence::RecipeStepAction) }}</label>
|
||||
<textarea class="text-area-step-action"></textarea>
|
||||
|
||||
<input class="input-step-delete" type="button" value="{{ tr.t(Sentence::RecipeRemoveStep) }}" />
|
||||
|
||||
<div class="ingredients"></div>
|
||||
|
||||
<input class="input-add-ingredient" type="button" value="{{ tr.t(Sentence::RecipeAddAnIngredient) }}"/>
|
||||
</div>
|
||||
|
||||
<div class="ingredient">
|
||||
<span class="drag-handle"></span>
|
||||
|
||||
<label for="input-ingredient-name">{{ tr.t(Sentence::RecipeIngredientName) }}</label>
|
||||
<input class="input-ingredient-name" type="text" />
|
||||
|
||||
<label for="input-ingredient-quantity">{{ tr.t(Sentence::RecipeIngredientQuantity) }}</label>
|
||||
<input class="input-ingredient-quantity" type="number" step="0.1" min="0" max="10000" />
|
||||
|
||||
<label for="input-ingredient-unit">{{ tr.t(Sentence::RecipeIngredientUnit) }}</label>
|
||||
<input class="input-ingredient-unit" type="text" />
|
||||
|
||||
<label for="input-ingredient-comment">{{ tr.t(Sentence::RecipeIngredientComment) }}</label>
|
||||
<input class="input-ingredient-comment" type="text" />
|
||||
|
||||
<input class="input-ingredient-delete" type="button" value="{{ tr.t(Sentence::RecipeRemoveIngredient) }}" />
|
||||
</div>
|
||||
|
||||
<div class="dropzone"></div>
|
||||
|
||||
<span class="recipe-delete-confirmation">{{ tr.t(Sentence::RecipeDeleteConfirmation) }}</span>
|
||||
<span class="recipe-group-delete-confirmation">{{ tr.t(Sentence::RecipeGroupDeleteConfirmation) }}</span>
|
||||
<span class="recipe-step-delete-confirmation">{{ tr.t(Sentence::RecipeStepDeleteConfirmation) }}</span>
|
||||
<span class="recipe-ingredient-delete-confirmation">{{ tr.t(Sentence::RecipeIngredientDeleteConfirmation) }}</span>
|
||||
<input class="input-add-step" type="button" value="{{ tr.t(Sentence::RecipeAddAStep) }}" />
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<span class="drag-handle"></span>
|
||||
|
||||
<label for="text-area-step-action">{{ tr.t(Sentence::RecipeStepAction) }}</label>
|
||||
<textarea class="text-area-step-action"></textarea>
|
||||
|
||||
<input class="input-step-delete" type="button" value="{{ tr.t(Sentence::RecipeRemoveStep) }}" />
|
||||
|
||||
<div class="ingredients"></div>
|
||||
|
||||
<input class="input-add-ingredient" type="button" value="{{ tr.t(Sentence::RecipeAddAnIngredient) }}"/>
|
||||
</div>
|
||||
|
||||
<div class="ingredient">
|
||||
<span class="drag-handle"></span>
|
||||
|
||||
<label for="input-ingredient-name">{{ tr.t(Sentence::RecipeIngredientName) }}</label>
|
||||
<input class="input-ingredient-name" type="text" />
|
||||
|
||||
<label for="input-ingredient-quantity">{{ tr.t(Sentence::RecipeIngredientQuantity) }}</label>
|
||||
<input class="input-ingredient-quantity" type="number" step="0.1" min="0" max="10000" />
|
||||
|
||||
<label for="input-ingredient-unit">{{ tr.t(Sentence::RecipeIngredientUnit) }}</label>
|
||||
<input class="input-ingredient-unit" type="text" />
|
||||
|
||||
<label for="input-ingredient-comment">{{ tr.t(Sentence::RecipeIngredientComment) }}</label>
|
||||
<input class="input-ingredient-comment" type="text" />
|
||||
|
||||
<input class="input-ingredient-delete" type="button" value="{{ tr.t(Sentence::RecipeRemoveIngredient) }}" />
|
||||
</div>
|
||||
|
||||
<div class="dropzone"></div>
|
||||
|
||||
<span class="recipe-delete-confirmation">{{ tr.t(Sentence::RecipeDeleteConfirmation) }}</span>
|
||||
<span class="recipe-group-delete-confirmation">{{ tr.t(Sentence::RecipeGroupDeleteConfirmation) }}</span>
|
||||
<span class="recipe-step-delete-confirmation">{{ tr.t(Sentence::RecipeStepDeleteConfirmation) }}</span>
|
||||
<span class="recipe-ingredient-delete-confirmation">{{ tr.t(Sentence::RecipeIngredientDeleteConfirmation) }}</span>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -79,39 +79,39 @@
|
|||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div id="hidden-templates">
|
||||
{# To create a modal dialog to choose a date and and servings #}
|
||||
<div class="date-and-servings" >
|
||||
{% include "calendar.html" %}
|
||||
<div id="hidden-templates">
|
||||
{# 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"
|
||||
type="number"
|
||||
step="1" min="1" max="100"
|
||||
value="
|
||||
{% if let Some(user) = user %}
|
||||
{{ user.default_servings }}
|
||||
{% else %}
|
||||
4
|
||||
{% endif %}
|
||||
"/>
|
||||
<label for="input-servings">{{ tr.t(Sentence::RecipeServings) }}</label>
|
||||
<input
|
||||
id="input-servings"
|
||||
type="number"
|
||||
step="1" min="1" max="100"
|
||||
value="
|
||||
{% if let Some(user) = user %}
|
||||
{{ user.default_servings }}
|
||||
{% else %}
|
||||
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>
|
||||
<span class="calendar-add-to-planner-already-exists">{{ tr.t(Sentence::CalendarAddToPlannerAlreadyExists) }}</span>
|
||||
<span class="calendar-date-format">{{ tr.t(Sentence::CalendarDateFormat) }}</span>
|
||||
<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>
|
||||
<span class="calendar-add-to-planner-already-exists">{{ tr.t(Sentence::CalendarAddToPlannerAlreadyExists) }}</span>
|
||||
<span class="calendar-date-format">{{ tr.t(Sentence::CalendarDateFormat) }}</span>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
Loading…
Add table
Add a link
Reference in a new issue