mirror of
https://github.com/zaphar/kitchen.git
synced 2025-07-21 19:29:49 -04:00
Store inventory details
This commit is contained in:
parent
a5e8575ef9
commit
60a1945fe8
3
kitchen/migrations/20221119220732_inventory.down.sql
Normal file
3
kitchen/migrations/20221119220732_inventory.down.sql
Normal file
@ -0,0 +1,3 @@
|
||||
-- Add down migration script here
|
||||
delete table filtered_ingredients;
|
||||
delete table modified_amts;
|
17
kitchen/migrations/20221119220732_inventory.up.sql
Normal file
17
kitchen/migrations/20221119220732_inventory.up.sql
Normal file
@ -0,0 +1,17 @@
|
||||
-- Add up migration script here
|
||||
create table filtered_ingredients(
|
||||
user_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
form TEXT NOT NULL,
|
||||
measure_type TEXT NOT NULL,
|
||||
primary key(user_id, name, form, measure_type)
|
||||
);
|
||||
|
||||
create table modified_amts(
|
||||
user_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
form TEXT NOT NULL,
|
||||
measure_type TEXT NOT NULL,
|
||||
amt TEXT NOT NULL,
|
||||
primary key(user_id, name, form, measure_type)
|
||||
);
|
@ -1,5 +1,15 @@
|
||||
{
|
||||
"db": "SQLite",
|
||||
"0e06f6e072e2c55769feda0d5f998509139097fee640caf8fb38c7087669bee4": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
}
|
||||
},
|
||||
"query": "insert into filtered_ingredients(user_id, name, form, measure_type)\n values (?, ?, ?, ?)"
|
||||
},
|
||||
"104f07472670436d3eee1733578bbf0c92dc4f965d3d13f9bf4bfbc92958c5b6": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@ -48,7 +58,7 @@
|
||||
{
|
||||
"name": "plan_date: NaiveDate",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
"type_info": "Date"
|
||||
},
|
||||
{
|
||||
"name": "recipe_id",
|
||||
@ -82,6 +92,16 @@
|
||||
},
|
||||
"query": "insert into recipes (user_id, recipe_id, recipe_text) values (?, ?, ?)\n on conflict(user_id, recipe_id) do update set recipe_text=excluded.recipe_text"
|
||||
},
|
||||
"512003bd6ef47567b243bbcaefcbd220acf36efab4cbd43b1a8debe59593577c": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Right": 5
|
||||
}
|
||||
},
|
||||
"query": "insert into modified_amts(user_id, name, form, measure_type, amt)\n values (?, ?, ?, ?, ?)"
|
||||
},
|
||||
"5d743897fb0d8fd54c3708f1b1c6e416346201faa9e28823c1ba5a421472b1fa": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@ -170,7 +190,7 @@
|
||||
{
|
||||
"name": "plan_date: NaiveDate",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
"type_info": "Date"
|
||||
},
|
||||
{
|
||||
"name": "recipe_id",
|
||||
@ -222,6 +242,36 @@
|
||||
},
|
||||
"query": "select category_text from categories where user_id = ?"
|
||||
},
|
||||
"d7d94a87b0153d1436eac0f6db820f25594e94decc8d740037c10802aa49157f": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "form",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "measure_type",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
}
|
||||
},
|
||||
"query": "select name, form, measure_type from filtered_ingredients where user_id = ?"
|
||||
},
|
||||
"d84685a82585c5e4ae72c86ba1fe6e4a7241c4c3c9e948213e5849d956132bad": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@ -231,5 +281,41 @@
|
||||
}
|
||||
},
|
||||
"query": "delete from sessions"
|
||||
},
|
||||
"fc294739374d2a791214f747095e0bf9378989d1ff07d96a5431dbb208f21951": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "form",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "measure_type",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "amt",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
}
|
||||
},
|
||||
"query": "select name, form, measure_type, amt from modified_amts where user_id = ?;"
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
use std::collections::BTreeMap;
|
||||
// Copyright 2022 Jeremy Wall
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@ -11,9 +12,9 @@
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::{collections::BTreeSet, net::SocketAddr};
|
||||
|
||||
use axum::{
|
||||
body::{boxed, Full},
|
||||
@ -23,11 +24,11 @@ use axum::{
|
||||
routing::{get, Router},
|
||||
};
|
||||
use mime_guess;
|
||||
use recipes::RecipeEntry;
|
||||
use recipes::{IngredientKey, RecipeEntry};
|
||||
use rust_embed::RustEmbed;
|
||||
use tower::ServiceBuilder;
|
||||
use tower_http::trace::TraceLayer;
|
||||
use tracing::{debug, info, instrument};
|
||||
use tracing::{debug, error, info, instrument};
|
||||
|
||||
use storage::{APIStore, AuthStore};
|
||||
|
||||
@ -263,6 +264,56 @@ async fn api_save_plan(
|
||||
}
|
||||
}
|
||||
|
||||
async fn api_inventory(
|
||||
Extension(app_store): Extension<Arc<storage::SqliteStore>>,
|
||||
session: storage::UserIdFromSession,
|
||||
) -> impl IntoResponse {
|
||||
use storage::{UserId, UserIdFromSession::FoundUserId};
|
||||
if let FoundUserId(UserId(id)) = session {
|
||||
match app_store.fetch_inventory_data(id).await {
|
||||
Ok(tpl) => Ok(axum::Json::from(tpl)),
|
||||
Err(e) => {
|
||||
error!(err=?e);
|
||||
Err((StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", e)))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Err((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"You must be authorized to use this API call".to_owned(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
async fn api_save_inventory(
|
||||
Extension(app_store): Extension<Arc<storage::SqliteStore>>,
|
||||
session: storage::UserIdFromSession,
|
||||
Json((filtered_ingredients, modified_amts)): Json<(
|
||||
BTreeSet<IngredientKey>,
|
||||
BTreeMap<IngredientKey, String>,
|
||||
)>,
|
||||
) -> impl IntoResponse {
|
||||
use storage::{UserId, UserIdFromSession::FoundUserId};
|
||||
if let FoundUserId(UserId(id)) = session {
|
||||
if let Err(e) = app_store
|
||||
.save_inventory_data(id, filtered_ingredients, modified_amts)
|
||||
.await
|
||||
{
|
||||
error!(err=?e);
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", e));
|
||||
}
|
||||
(
|
||||
StatusCode::OK,
|
||||
"Successfully saved inventory data".to_owned(),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"You must be authorized to use this API call".to_owned(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(fields(recipe_dir=?recipe_dir_path,listen=?listen_socket), skip_all)]
|
||||
pub async fn ui_main(recipe_dir_path: PathBuf, store_path: PathBuf, listen_socket: SocketAddr) {
|
||||
let store = Arc::new(storage::file_store::AsyncFileStore::new(
|
||||
@ -281,9 +332,16 @@ pub async fn ui_main(recipe_dir_path: PathBuf, store_path: PathBuf, listen_socke
|
||||
.route("/api/v1/recipes", get(api_recipes).post(api_save_recipes))
|
||||
// recipe entry api path route
|
||||
.route("/api/v1/recipe/:recipe_id", get(api_recipe_entry))
|
||||
// TODO(jwall): We should use route_layer to enforce the authorization
|
||||
// requirements here.
|
||||
// mealplan api path routes
|
||||
.route("/api/v1/plan", get(api_plan).post(api_save_plan))
|
||||
.route("/api/v1/plan/:date", get(api_plan_since))
|
||||
// Inventory api path route
|
||||
.route(
|
||||
"/api/v1/inventory",
|
||||
get(api_inventory).post(api_save_inventory),
|
||||
)
|
||||
// categories api path route
|
||||
.route(
|
||||
"/api/v1/categories",
|
||||
|
@ -0,0 +1 @@
|
||||
select name, form, measure_type from filtered_ingredients where user_id = ?
|
@ -0,0 +1 @@
|
||||
select name, form, measure_type, amt from modified_amts where user_id = ?;
|
@ -12,6 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
use async_std::sync::Arc;
|
||||
use std::collections::BTreeSet;
|
||||
use std::str::FromStr;
|
||||
use std::{collections::BTreeMap, path::Path};
|
||||
|
||||
@ -28,7 +29,7 @@ use axum::{
|
||||
};
|
||||
use chrono::NaiveDate;
|
||||
use ciborium;
|
||||
use recipes::RecipeEntry;
|
||||
use recipes::{IngredientKey, RecipeEntry};
|
||||
use secrecy::{ExposeSecret, Secret};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{
|
||||
@ -119,6 +120,18 @@ pub trait APIStore {
|
||||
recipe_counts: &Vec<(String, i32)>,
|
||||
date: NaiveDate,
|
||||
) -> Result<()>;
|
||||
|
||||
async fn fetch_inventory_data<S: AsRef<str> + Send>(
|
||||
&self,
|
||||
user_id: S,
|
||||
) -> Result<(BTreeSet<IngredientKey>, BTreeMap<IngredientKey, String>)>;
|
||||
|
||||
async fn save_inventory_data<S: AsRef<str> + Send>(
|
||||
&self,
|
||||
user_id: S,
|
||||
filtered_ingredients: BTreeSet<IngredientKey>,
|
||||
modified_amts: BTreeMap<IngredientKey, String>,
|
||||
) -> Result<()>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@ -479,4 +492,106 @@ impl APIStore for SqliteStore {
|
||||
}
|
||||
Ok(Some(result))
|
||||
}
|
||||
|
||||
async fn fetch_inventory_data<S: AsRef<str> + Send>(
|
||||
&self,
|
||||
user_id: S,
|
||||
) -> Result<(BTreeSet<IngredientKey>, BTreeMap<IngredientKey, String>)> {
|
||||
let user_id = user_id.as_ref();
|
||||
struct FilteredIngredientRow {
|
||||
name: String,
|
||||
form: String,
|
||||
measure_type: String,
|
||||
}
|
||||
let filtered_ingredient_rows: Vec<FilteredIngredientRow> = sqlx::query_file_as!(
|
||||
FilteredIngredientRow,
|
||||
"src/web/storage/fetch_inventory_filtered_ingredients.sql",
|
||||
user_id
|
||||
)
|
||||
.fetch_all(self.pool.as_ref())
|
||||
.await?;
|
||||
let mut filtered_ingredients = BTreeSet::new();
|
||||
for row in filtered_ingredient_rows {
|
||||
filtered_ingredients.insert(IngredientKey::new(
|
||||
row.name,
|
||||
if row.form.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(row.form)
|
||||
},
|
||||
row.measure_type,
|
||||
));
|
||||
}
|
||||
struct ModifiedAmtRow {
|
||||
name: String,
|
||||
form: String,
|
||||
measure_type: String,
|
||||
amt: String,
|
||||
}
|
||||
let modified_amt_rows = sqlx::query_file_as!(
|
||||
ModifiedAmtRow,
|
||||
"src/web/storage/fetch_inventory_modified_amts.sql",
|
||||
user_id,
|
||||
)
|
||||
.fetch_all(self.pool.as_ref())
|
||||
.await?;
|
||||
let mut modified_amts = BTreeMap::new();
|
||||
for row in modified_amt_rows {
|
||||
modified_amts.insert(
|
||||
IngredientKey::new(
|
||||
row.name,
|
||||
if row.form.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(row.form)
|
||||
},
|
||||
row.measure_type,
|
||||
),
|
||||
row.amt,
|
||||
);
|
||||
}
|
||||
Ok((filtered_ingredients, modified_amts))
|
||||
}
|
||||
|
||||
async fn save_inventory_data<S: AsRef<str> + Send>(
|
||||
&self,
|
||||
user_id: S,
|
||||
filtered_ingredients: BTreeSet<IngredientKey>,
|
||||
modified_amts: BTreeMap<IngredientKey, String>,
|
||||
) -> Result<()> {
|
||||
let user_id = user_id.as_ref();
|
||||
let mut transaction = self.pool.as_ref().begin().await?;
|
||||
for key in filtered_ingredients {
|
||||
let name = key.name();
|
||||
let form = key.form();
|
||||
let measure_type = key.measure_type();
|
||||
sqlx::query_file!(
|
||||
"src/web/storage/save_inventory_filtered_ingredients.sql",
|
||||
user_id,
|
||||
name,
|
||||
form,
|
||||
measure_type,
|
||||
)
|
||||
.execute(&mut transaction)
|
||||
.await?;
|
||||
}
|
||||
for (key, amt) in modified_amts {
|
||||
let name = key.name();
|
||||
let form = key.form();
|
||||
let measure_type = key.measure_type();
|
||||
let amt = &amt;
|
||||
sqlx::query_file!(
|
||||
"src/web/storage/save_inventory_modified_amts.sql",
|
||||
user_id,
|
||||
name,
|
||||
form,
|
||||
measure_type,
|
||||
amt,
|
||||
)
|
||||
.execute(&mut transaction)
|
||||
.await?;
|
||||
}
|
||||
transaction.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,2 @@
|
||||
insert into filtered_ingredients(user_id, name, form, measure_type)
|
||||
values (?, ?, ?, ?)
|
2
kitchen/src/web/storage/save_inventory_modified_amts.sql
Normal file
2
kitchen/src/web/storage/save_inventory_modified_amts.sql
Normal file
@ -0,0 +1,2 @@
|
||||
insert into modified_amts(user_id, name, form, measure_type, amt)
|
||||
values (?, ?, ?, ?, ?)
|
@ -202,9 +202,27 @@ impl Step {
|
||||
|
||||
/// Unique identifier for an Ingredient. Ingredients are identified by name, form,
|
||||
/// and measurement type. (Volume, Count, Weight)
|
||||
#[derive(PartialEq, PartialOrd, Eq, Ord, Clone, Hash, Debug)]
|
||||
#[derive(PartialEq, PartialOrd, Eq, Ord, Clone, Hash, Debug, Deserialize, Serialize)]
|
||||
pub struct IngredientKey(String, Option<String>, String);
|
||||
|
||||
impl IngredientKey {
|
||||
pub fn new(name: String, form: Option<String>, measure_type: String) -> Self {
|
||||
Self(name, form, measure_type)
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &String {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn form(&self) -> String {
|
||||
self.1.clone().unwrap_or_else(|| String::new())
|
||||
}
|
||||
|
||||
pub fn measure_type(&self) -> &String {
|
||||
&self.2
|
||||
}
|
||||
}
|
||||
|
||||
/// Ingredient in a recipe. The `name` and `form` fields with the measurement type
|
||||
/// uniquely identify an ingredient.
|
||||
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord)]
|
||||
|
@ -11,14 +11,14 @@
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use reqwasm;
|
||||
use serde_json::{from_str, to_string};
|
||||
use sycamore::prelude::*;
|
||||
use tracing::{debug, error, info, instrument, warn};
|
||||
|
||||
use recipes::{parse, Recipe, RecipeEntry};
|
||||
use recipes::{parse, IngredientKey, Recipe, RecipeEntry};
|
||||
use wasm_bindgen::JsValue;
|
||||
|
||||
use crate::{app_state, js_lib};
|
||||
@ -92,6 +92,16 @@ pub async fn init_page_state(store: &HttpStore, state: &app_state::State) -> Res
|
||||
error!("{:?}", e);
|
||||
}
|
||||
}
|
||||
info!("Synchronizing inventory data");
|
||||
match store.get_inventory_data().await {
|
||||
Ok((filtered_ingredients, modified_amts)) => {
|
||||
state.reset_modified_amts(modified_amts);
|
||||
state.filtered_ingredients.set(filtered_ingredients);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("{:?}", e);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -371,4 +381,41 @@ impl HttpStore {
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_inventory_data(
|
||||
&self,
|
||||
) -> Result<(BTreeSet<IngredientKey>, BTreeMap<IngredientKey, String>), Error> {
|
||||
let mut path = self.root.clone();
|
||||
path.push_str("/inventory");
|
||||
let resp = reqwasm::http::Request::get(&path).send().await?;
|
||||
if resp.status() != 200 {
|
||||
Err(format!("Status: {}", resp.status()).into())
|
||||
} else {
|
||||
debug!("We got a valid response back");
|
||||
let inventory = resp.json().await.map_err(|e| format!("{}", e))?;
|
||||
Ok(inventory)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn save_inventory_data(
|
||||
&self,
|
||||
filtered_ingredients: BTreeSet<IngredientKey>,
|
||||
modified_amts: BTreeMap<IngredientKey, String>,
|
||||
) -> Result<(), Error> {
|
||||
let mut path = self.root.clone();
|
||||
let serialized_inventory = to_string(&(filtered_ingredients, modified_amts))
|
||||
.expect("Unable to encode plan as json");
|
||||
path.push_str("/inventory");
|
||||
let resp = reqwasm::http::Request::post(&path)
|
||||
.body(&serialized_inventory)
|
||||
.header("content-type", "application/json")
|
||||
.send()
|
||||
.await?;
|
||||
if resp.status() != 200 {
|
||||
Err(format!("Status: {}", resp.status()).into())
|
||||
} else {
|
||||
debug!("We got a valid response back!");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||
use sycamore::prelude::*;
|
||||
use tracing::{debug, instrument, warn};
|
||||
|
||||
use recipes::{Ingredient, IngredientAccumulator, Recipe};
|
||||
use recipes::{Ingredient, IngredientAccumulator, IngredientKey, Recipe};
|
||||
|
||||
pub struct State {
|
||||
pub recipe_counts: RcSignal<BTreeMap<String, RcSignal<usize>>>,
|
||||
@ -24,6 +24,8 @@ pub struct State {
|
||||
pub staples: RcSignal<Option<Recipe>>,
|
||||
pub recipes: RcSignal<BTreeMap<String, Recipe>>,
|
||||
pub category_map: RcSignal<BTreeMap<String, String>>,
|
||||
pub filtered_ingredients: RcSignal<BTreeSet<IngredientKey>>,
|
||||
pub modified_amts: RcSignal<BTreeMap<IngredientKey, RcSignal<String>>>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
@ -34,6 +36,8 @@ impl State {
|
||||
staples: create_rc_signal(None),
|
||||
recipes: create_rc_signal(BTreeMap::new()),
|
||||
category_map: create_rc_signal(BTreeMap::new()),
|
||||
filtered_ingredients: create_rc_signal(BTreeSet::new()),
|
||||
modified_amts: create_rc_signal(BTreeMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,7 +107,7 @@ impl State {
|
||||
}
|
||||
|
||||
pub fn reset_recipe_counts(&self) {
|
||||
for (key, count) in self.recipe_counts.get_untracked().iter() {
|
||||
for (_, count) in self.recipe_counts.get_untracked().iter() {
|
||||
count.set(0);
|
||||
}
|
||||
}
|
||||
@ -119,4 +123,23 @@ impl State {
|
||||
self.recipe_counts.set(counts);
|
||||
self.recipe_counts.get_untracked().get(key).unwrap().clone()
|
||||
}
|
||||
|
||||
pub fn get_current_modified_amts(&self) -> BTreeMap<IngredientKey, String> {
|
||||
let mut modified_amts = BTreeMap::new();
|
||||
for (key, amt) in self.modified_amts.get_untracked().iter() {
|
||||
modified_amts.insert(key.clone(), amt.get_untracked().as_ref().clone());
|
||||
}
|
||||
modified_amts
|
||||
}
|
||||
|
||||
pub fn reset_modified_amts(&self, modified_amts: BTreeMap<IngredientKey, String>) {
|
||||
let mut modified_amts_copy = self.modified_amts.get().as_ref().clone();
|
||||
for (key, amt) in modified_amts {
|
||||
modified_amts_copy
|
||||
.entry(key)
|
||||
.and_modify(|amt_signal| amt_signal.set(amt.clone()))
|
||||
.or_insert_with(|| create_rc_signal(amt));
|
||||
}
|
||||
self.modified_amts.set(modified_amts_copy);
|
||||
}
|
||||
}
|
||||
|
@ -14,13 +14,13 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use recipes::{Ingredient, IngredientKey};
|
||||
use sycamore::prelude::*;
|
||||
use sycamore::{futures::spawn_local_scoped, prelude::*};
|
||||
use tracing::{debug, instrument};
|
||||
|
||||
fn make_ingredients_rows<'ctx, G: Html>(
|
||||
cx: Scope<'ctx>,
|
||||
ingredients: &'ctx ReadSignal<Vec<(IngredientKey, (Ingredient, BTreeSet<String>))>>,
|
||||
modified_amts: &'ctx Signal<BTreeMap<IngredientKey, RcSignal<String>>>,
|
||||
modified_amts: RcSignal<BTreeMap<IngredientKey, RcSignal<String>>>,
|
||||
filtered_keys: RcSignal<BTreeSet<IngredientKey>>,
|
||||
) -> View<G> {
|
||||
view!(
|
||||
@ -111,7 +111,7 @@ fn make_extras_rows<'ctx, G: Html>(
|
||||
fn make_shopping_table<'ctx, G: Html>(
|
||||
cx: Scope<'ctx>,
|
||||
ingredients: &'ctx ReadSignal<Vec<(IngredientKey, (Ingredient, BTreeSet<String>))>>,
|
||||
modified_amts: &'ctx Signal<BTreeMap<IngredientKey, RcSignal<String>>>,
|
||||
modified_amts: RcSignal<BTreeMap<IngredientKey, RcSignal<String>>>,
|
||||
extras: RcSignal<Vec<(usize, (RcSignal<String>, RcSignal<String>))>>,
|
||||
filtered_keys: RcSignal<BTreeSet<IngredientKey>>,
|
||||
) -> View<G> {
|
||||
@ -137,10 +137,12 @@ fn make_shopping_table<'ctx, G: Html>(
|
||||
#[instrument]
|
||||
#[component]
|
||||
pub fn ShoppingList<G: Html>(cx: Scope) -> View<G> {
|
||||
let filtered_keys: RcSignal<BTreeSet<IngredientKey>> = create_rc_signal(BTreeSet::new());
|
||||
let state = crate::app_state::State::get_from_context(cx);
|
||||
// FIXME(jwall): We need to init this state for the page at some point.
|
||||
let filtered_keys: RcSignal<BTreeSet<IngredientKey>> = state.filtered_ingredients.clone();
|
||||
let ingredients_map = create_rc_signal(BTreeMap::new());
|
||||
let modified_amts = create_signal(cx, BTreeMap::new());
|
||||
let show_staples = create_signal(cx, true);
|
||||
let save_click = create_signal(cx, ());
|
||||
create_effect(cx, {
|
||||
let state = crate::app_state::State::get_from_context(cx);
|
||||
let ingredients_map = ingredients_map.clone();
|
||||
@ -174,7 +176,7 @@ pub fn ShoppingList<G: Html>(cx: Scope) -> View<G> {
|
||||
table_view.set(make_shopping_table(
|
||||
cx,
|
||||
ingredients,
|
||||
modified_amts.clone(),
|
||||
state.modified_amts.clone(),
|
||||
state.extras.clone(),
|
||||
filtered_keys.clone(),
|
||||
));
|
||||
@ -183,6 +185,22 @@ pub fn ShoppingList<G: Html>(cx: Scope) -> View<G> {
|
||||
}
|
||||
}
|
||||
});
|
||||
create_effect(cx, move || {
|
||||
save_click.track();
|
||||
spawn_local_scoped(cx, {
|
||||
let state = crate::app_state::State::get_from_context(cx);
|
||||
let store = crate::api::HttpStore::get_from_context(cx);
|
||||
async move {
|
||||
store
|
||||
.save_inventory_data(
|
||||
state.filtered_ingredients.get_untracked().as_ref().clone(),
|
||||
state.get_current_modified_amts(),
|
||||
)
|
||||
.await
|
||||
.expect("Unable to save inventory data");
|
||||
}
|
||||
})
|
||||
});
|
||||
let state = crate::app_state::State::get_from_context(cx);
|
||||
view! {cx,
|
||||
h1 { "Shopping List " }
|
||||
@ -201,9 +219,12 @@ pub fn ShoppingList<G: Html>(cx: Scope) -> View<G> {
|
||||
ingredients_map.set(state.get_shopping_list(*show_staples.get()));
|
||||
// clear the filter_signal
|
||||
filtered_keys.set(BTreeSet::new());
|
||||
modified_amts.set(BTreeMap::new());
|
||||
state.modified_amts.set(BTreeMap::new());
|
||||
state.extras.set(Vec::new());
|
||||
}
|
||||
})
|
||||
input(type="button", value="Save", class="no-print", on:click=|_| {
|
||||
save_click.trigger_subscribers();
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user