From 376c1084ab54364c124c31b9097662eceacc7e42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Wo=C5=BAniak?= Date: Mon, 28 Nov 2022 17:00:19 +0100 Subject: [PATCH] Add categories --- .../migrations/202204131841_init.sql | 1 + crates/cart_manager/src/actions.rs | 2 + .../src/db/shopping_cart_items.rs | 95 ++-- crates/channels/src/carts.rs | 2 + crates/channels/src/stocks/load.rs | 4 +- crates/channels/src/stocks/mod.rs | 2 + crates/channels/src/stocks/product.rs | 1 + crates/channels/src/stocks/product_variant.rs | 2 + crates/model/src/api.rs | 111 ++-- crates/model/src/lib.rs | 485 ++++++++++++------ .../migrations/202204131841_init.sql | 9 + crates/stock_manager/src/actions/load.rs | 95 +++- crates/stock_manager/src/actions/product.rs | 10 +- .../src/actions/product_photo.rs | 1 + .../src/actions/product_stock.rs | 1 + .../src/actions/product_variant.rs | 6 + crates/stock_manager/src/db/categories.rs | 124 +++++ crates/stock_manager/src/db/mod.rs | 2 + crates/stock_manager/src/db/photos.rs | 1 + crates/stock_manager/src/db/product_photos.rs | 1 + .../stock_manager/src/db/product_variants.rs | 29 +- crates/stock_manager/src/db/stocks.rs | 1 + crates/web/src/api/public.rs | 8 +- crates/web/src/model.rs | 19 +- crates/web/src/pages/public.rs | 51 +- crates/web/src/pages/public/checkout.rs | 50 +- crates/web/src/pages/public/listing.rs | 65 ++- crates/web/src/pages/public/product.rs | 81 +-- crates/web/src/pages/public/shopping_cart.rs | 48 +- crates/web/src/pages/public/sign_in.rs | 2 +- crates/web/src/pages/public/sign_up.rs | 2 +- crates/web/src/shared/view.rs | 50 +- crates/web/src/shopping_cart.rs | 14 +- 33 files changed, 941 insertions(+), 434 deletions(-) create mode 100644 crates/stock_manager/src/db/categories.rs diff --git a/crates/cart_manager/migrations/202204131841_init.sql b/crates/cart_manager/migrations/202204131841_init.sql index 161e5b4..1820be8 100644 --- a/crates/cart_manager/migrations/202204131841_init.sql +++ b/crates/cart_manager/migrations/202204131841_init.sql @@ -26,6 +26,7 @@ CREATE TABLE shopping_carts ( CREATE TABLE shopping_cart_items ( id serial NOT NULL, product_variant_id integer NOT NULL, + product_id integer NOT NULL, shopping_cart_id integer, quantity integer DEFAULT 0 NOT NULL, quantity_unit "QuantityUnit" NOT NULL, diff --git a/crates/cart_manager/src/actions.rs b/crates/cart_manager/src/actions.rs index cd08139..bb060f8 100644 --- a/crates/cart_manager/src/actions.rs +++ b/crates/cart_manager/src/actions.rs @@ -83,6 +83,7 @@ pub async fn modify_item(msg: modify_item::Input, db: Database) -> modify_item:: Some(item) => { let dbm = UpdateShoppingCartItem { id: item.id, + product_id: msg.product_id, product_variant_id: msg.product_variant_id, shopping_cart_id: cart.id, quantity: msg.quantity, @@ -99,6 +100,7 @@ pub async fn modify_item(msg: modify_item::Input, db: Database) -> modify_item:: } None => { let dbm = CreateShoppingCartItem { + product_id: msg.product_id, product_variant_id: msg.product_variant_id, shopping_cart_id: cart.id, quantity: msg.quantity, diff --git a/crates/cart_manager/src/db/shopping_cart_items.rs b/crates/cart_manager/src/db/shopping_cart_items.rs index 1cf2ea3..57fc2f4 100644 --- a/crates/cart_manager/src/db/shopping_cart_items.rs +++ b/crates/cart_manager/src/db/shopping_cart_items.rs @@ -37,6 +37,7 @@ impl AllShoppingCartItems { sqlx::query_as( r#" SELECT shopping_cart_items.id, + shopping_cart_items.product_id, shopping_cart_items.product_variant_id, shopping_cart_items.shopping_cart_id, shopping_cart_items.quantity, @@ -73,6 +74,7 @@ impl AccountShoppingCartItems { Some(shopping_cart_id) => sqlx::query_as( r#" SELECT shopping_cart_items.id as id, + shopping_cart_items.product_id as product_id, shopping_cart_items.product_variant_id as product_variant_id, shopping_cart_items.shopping_cart_id as shopping_cart_id, shopping_cart_items.quantity as quantity, @@ -89,6 +91,7 @@ ORDER BY shopping_cart_items.id None => sqlx::query_as( r#" SELECT shopping_cart_items.id as id, + shopping_cart_items.product_id as product_id, shopping_cart_items.product_variant_id as product_variant_id, shopping_cart_items.shopping_cart_id as shopping_cart_id, shopping_cart_items.quantity as quantity, @@ -113,6 +116,7 @@ ORDER BY shopping_cart_items.id #[derive(Debug)] pub struct CreateShoppingCartItem { + pub product_id: ProductId, pub product_variant_id: ProductVariantId, pub shopping_cart_id: ShoppingCartId, pub quantity: Quantity, @@ -127,12 +131,13 @@ impl CreateShoppingCartItem { let msg = self; sqlx::query_as( r#" -INSERT INTO shopping_cart_items (product_variant_id, shopping_cart_id, quantity, quantity_unit) -VALUES ($1, $2, $3, $4) -RETURNING id, product_variant_id, shopping_cart_id, quantity, quantity_unit +INSERT INTO shopping_cart_items (product_variant_id, product_id, shopping_cart_id, quantity, quantity_unit) +VALUES ($1, $2, $3, $4, $5) +RETURNING id, product_id, product_variant_id, shopping_cart_id, quantity, quantity_unit "#, ) .bind(msg.product_variant_id) + .bind(msg.product_id) .bind(msg.shopping_cart_id) .bind(msg.quantity) .bind(msg.quantity_unit) @@ -149,6 +154,7 @@ RETURNING id, product_variant_id, shopping_cart_id, quantity, quantity_unit #[derive(Debug)] pub struct UpdateShoppingCartItem { pub id: ShoppingCartItemId, + pub product_id: ProductId, pub product_variant_id: ProductVariantId, pub shopping_cart_id: ShoppingCartId, pub quantity: Quantity, @@ -164,12 +170,13 @@ impl UpdateShoppingCartItem { sqlx::query_as( r#" UPDATE shopping_cart_items -SET product_variant_id = $2, shopping_cart_id = $3, quantity = $4, quantity_unit = $5 +SET product_variant_id = $2, product_id = $3, shopping_cart_id = $4, quantity = $5, quantity_unit = $6 WHERE id = $1 -RETURNING id, product_variant_id, shopping_cart_id, quantity, quantity_unit +RETURNING id, product_id, product_variant_id, shopping_cart_id, quantity, quantity_unit "#, ) .bind(msg.id) + .bind(msg.product_id) .bind(msg.product_variant_id) .bind(msg.shopping_cart_id) .bind(msg.quantity) @@ -198,7 +205,7 @@ impl DeleteShoppingCartItem { r#" DELETE FROM shopping_cart_items WHERE id = $1 -RETURNING id, product_variant_id, shopping_cart_id, quantity, quantity_unit +RETURNING id, product_id, product_variant_id, shopping_cart_id, quantity, quantity_unit "#, ) .bind(msg.id) @@ -224,7 +231,7 @@ impl FindShoppingCartItem { let msg = self; sqlx::query_as( r#" -SELECT id, product_variant_id, shopping_cart_id, quantity, quantity_unit +SELECT id, product_id, product_variant_id, shopping_cart_id, quantity, quantity_unit FROM shopping_cart_items WHERE id = $1 "#, @@ -253,6 +260,7 @@ impl ActiveCartItemByProduct { sqlx::query_as( r#" SELECT shopping_cart_items.id, + shopping_cart_items.product_id, shopping_cart_items.product_variant_id, shopping_cart_items.shopping_cart_id, shopping_cart_items.quantity, @@ -291,6 +299,7 @@ impl CartItems { sqlx::query_as( r#" SELECT id, + product_id, product_variant_id, shopping_cart_id, quantity, @@ -328,7 +337,7 @@ impl RemoveCartItem { r#" DELETE FROM shopping_cart_items WHERE shopping_cart_id = $1 AND id = $2 -RETURNING id, product_variant_id, shopping_cart_id, quantity, quantity_unit +RETURNING id, product_id, product_variant_id, shopping_cart_id, quantity, quantity_unit "#, ) .bind(msg.shopping_cart_id) @@ -337,7 +346,7 @@ RETURNING id, product_variant_id, shopping_cart_id, quantity, quantity_unit r#" DELETE FROM shopping_cart_items WHERE shopping_cart_id = $1 AND id = $2 AND product_variant_id = $3 -RETURNING id, product_variant_id, shopping_cart_id, quantity, quantity_unit +RETURNING id, product_id, product_id, product_variant_id, shopping_cart_id, quantity, quantity_unit "#, ) .bind(msg.shopping_cart_id) @@ -347,7 +356,7 @@ RETURNING id, product_variant_id, shopping_cart_id, quantity, quantity_unit r#" DELETE FROM shopping_cart_items WHERE shopping_cart_id = $1 AND product_variant_id = $2 -RETURNING id, product_variant_id, shopping_cart_id, quantity, quantity_unit +RETURNING id, product_id, product_variant_id, shopping_cart_id, quantity, quantity_unit "#, ) .bind(msg.shopping_cart_id) @@ -425,6 +434,7 @@ WHERE buyer_id = $1 t: &mut sqlx::Transaction<'_, sqlx::Postgres>, shopping_cart_id: Option, product_variant_id: Option, + product_id: Option, ) -> ShoppingCartItem { let shopping_cart_id = match shopping_cart_id { Some(id) => id, @@ -438,7 +448,12 @@ WHERE buyer_id = $1 Some(id) => id, _ => 1.into(), }; + let product_id = match product_id { + Some(id) => id, + _ => 1.into(), + }; CreateShoppingCartItem { + product_id, product_variant_id, shopping_cart_id, quantity: Quantity::from_u32(496879), @@ -453,7 +468,7 @@ WHERE buyer_id = $1 async fn create() { testx::db_t_ref!(t); - test_shopping_cart_item(&mut t, None, None).await; + test_shopping_cart_item(&mut t, None, None, None).await; testx::db_rollback!(t); } @@ -467,19 +482,19 @@ WHERE buyer_id = $1 let mut items = Vec::with_capacity(9); let cart1 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Closed).await; - items.push(test_shopping_cart_item(&mut t, Some(cart1.id), None).await); - items.push(test_shopping_cart_item(&mut t, Some(cart1.id), None).await); - items.push(test_shopping_cart_item(&mut t, Some(cart1.id), None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart1.id), None, None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart1.id), None, None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart1.id), None, None).await); let cart2 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Active).await; - items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None).await); - items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None).await); - items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None, None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None, None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None, None).await); let cart3 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Closed).await; - items.push(test_shopping_cart_item(&mut t, Some(cart3.id), None).await); - items.push(test_shopping_cart_item(&mut t, Some(cart3.id), None).await); - items.push(test_shopping_cart_item(&mut t, Some(cart3.id), None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart3.id), None, None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart3.id), None, None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart3.id), None, None).await); let all = AllShoppingCartItems.run(&mut t).await.unwrap(); @@ -496,19 +511,19 @@ WHERE buyer_id = $1 let mut items = Vec::with_capacity(9); let cart1 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Closed).await; - test_shopping_cart_item(&mut t, Some(cart1.id), None).await; - test_shopping_cart_item(&mut t, Some(cart1.id), None).await; - test_shopping_cart_item(&mut t, Some(cart1.id), None).await; + test_shopping_cart_item(&mut t, Some(cart1.id), None, None).await; + test_shopping_cart_item(&mut t, Some(cart1.id), None, None).await; + test_shopping_cart_item(&mut t, Some(cart1.id), None, None).await; let cart2 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Active).await; - items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None).await); - items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None).await); - items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None, None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None, None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None, None).await); let cart3 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Closed).await; - test_shopping_cart_item(&mut t, Some(cart3.id), None).await; - test_shopping_cart_item(&mut t, Some(cart3.id), None).await; - test_shopping_cart_item(&mut t, Some(cart3.id), None).await; + test_shopping_cart_item(&mut t, Some(cart3.id), None, None).await; + test_shopping_cart_item(&mut t, Some(cart3.id), None, None).await; + test_shopping_cart_item(&mut t, Some(cart3.id), None, None).await; let all = AccountShoppingCartItems { account_id, @@ -531,19 +546,19 @@ WHERE buyer_id = $1 let mut items = Vec::with_capacity(9); let cart1 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Closed).await; - items.push(test_shopping_cart_item(&mut t, Some(cart1.id), None).await); - items.push(test_shopping_cart_item(&mut t, Some(cart1.id), None).await); - items.push(test_shopping_cart_item(&mut t, Some(cart1.id), None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart1.id), None, None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart1.id), None, None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart1.id), None, None).await); let cart2 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Active).await; - items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None).await); - items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None).await); - items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None, None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None, None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None, None).await); let cart3 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Closed).await; - items.push(test_shopping_cart_item(&mut t, Some(cart3.id), None).await); - items.push(test_shopping_cart_item(&mut t, Some(cart3.id), None).await); - items.push(test_shopping_cart_item(&mut t, Some(cart3.id), None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart3.id), None, None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart3.id), None, None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart3.id), None, None).await); let all = AccountShoppingCartItems { account_id, @@ -562,10 +577,11 @@ WHERE buyer_id = $1 testx::db_t_ref!(t); let account_id = 1.into(); let cart1 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Closed).await; - let item = test_shopping_cart_item(&mut t, Some(cart1.id), None).await; + let item = test_shopping_cart_item(&mut t, Some(cart1.id), None, None).await; let updated = UpdateShoppingCartItem { id: item.id, + product_id: item.product_id, product_variant_id: item.product_variant_id, shopping_cart_id: item.shopping_cart_id, quantity: Quantity::from_u32(987979879), @@ -581,6 +597,7 @@ WHERE buyer_id = $1 ShoppingCartItem { id: item.id, product_variant_id: item.product_variant_id, + product_id: item.product_id, shopping_cart_id: item.shopping_cart_id, quantity: Quantity::from_u32(987979879), quantity_unit: QuantityUnit::Kilogram, diff --git a/crates/channels/src/carts.rs b/crates/channels/src/carts.rs index 0373c78..f1acee2 100644 --- a/crates/channels/src/carts.rs +++ b/crates/channels/src/carts.rs @@ -41,6 +41,7 @@ pub mod remove_product { pub mod modify_item { use model::v2::ProductVariantId; + use model::ProductId; use super::Error; @@ -48,6 +49,7 @@ pub mod modify_item { pub struct Input { pub buyer_id: model::AccountId, pub product_variant_id: ProductVariantId, + pub product_id: ProductId, pub quantity: model::Quantity, pub quantity_unit: model::QuantityUnit, } diff --git a/crates/channels/src/stocks/load.rs b/crates/channels/src/stocks/load.rs index 9d1af76..deec807 100644 --- a/crates/channels/src/stocks/load.rs +++ b/crates/channels/src/stocks/load.rs @@ -9,11 +9,11 @@ pub mod detailed_product { } #[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)] - pub struct Output2 { + pub struct Details { pub product: DetailedProduct, } - pub type Output = Result; + pub type Output = Result; } pub mod detailed_products { diff --git a/crates/channels/src/stocks/mod.rs b/crates/channels/src/stocks/mod.rs index 33da68d..cfb8b62 100644 --- a/crates/channels/src/stocks/mod.rs +++ b/crates/channels/src/stocks/mod.rs @@ -54,6 +54,8 @@ pub enum Error { FindProducts(Vec), #[error("Failed to load product variants {0:?}")] FindProductVariants(Vec), + #[error("Failed to load all categories")] + Categories, } pub mod rpc { diff --git a/crates/channels/src/stocks/product.rs b/crates/channels/src/stocks/product.rs index 4c59c98..e4fc41b 100644 --- a/crates/channels/src/stocks/product.rs +++ b/crates/channels/src/stocks/product.rs @@ -18,6 +18,7 @@ pub mod create_product { pub long_description: ProductLongDesc, pub category: Option, pub price: Price, + pub quantity_unit: QuantityUnit, pub deliver_days_flag: Days, } diff --git a/crates/channels/src/stocks/product_variant.rs b/crates/channels/src/stocks/product_variant.rs index 873d379..ea96d03 100644 --- a/crates/channels/src/stocks/product_variant.rs +++ b/crates/channels/src/stocks/product_variant.rs @@ -15,6 +15,7 @@ pub mod create_product_variant { pub short_description: ProductShortDesc, pub long_description: ProductLongDesc, pub price: Price, + pub quantity_unit: QuantityUnit, } #[derive(Debug, serde::Serialize, serde::Deserialize)] @@ -38,6 +39,7 @@ pub mod update_product_variant { pub short_description: ProductShortDesc, pub long_description: ProductLongDesc, pub price: Price, + pub quantity_unit: QuantityUnit, } #[derive(Debug, serde::Serialize, serde::Deserialize)] diff --git a/crates/model/src/api.rs b/crates/model/src/api.rs index d2843b6..48904d0 100644 --- a/crates/model/src/api.rs +++ b/crates/model/src/api.rs @@ -2,6 +2,7 @@ use chrono::NaiveDateTime; use derive_more::Deref; use serde::{Deserialize, Serialize}; +use crate::v2::{CategoryId, CategoryKey, CategoryName, CategorySvg, DetailedProduct}; use crate::*; #[derive(Serialize, Deserialize, Debug)] @@ -174,6 +175,7 @@ pub struct Order { pub struct ShoppingCartItem { pub id: ShoppingCartItemId, pub product_variant_id: ProductVariantId, + pub product_id: ProductId, pub shopping_cart_id: ShoppingCartId, pub quantity: Quantity, pub quantity_unit: QuantityUnit, @@ -184,6 +186,7 @@ impl From for ShoppingCartItem { crate::ShoppingCartItem { id, product_variant_id, + product_id, shopping_cart_id, quantity, quantity_unit, @@ -192,6 +195,7 @@ impl From for ShoppingCartItem { Self { id, product_variant_id, + product_id, shopping_cart_id, quantity, quantity_unit, @@ -234,11 +238,13 @@ impl From<(crate::ShoppingCart, Vec)> for ShoppingCart |crate::ShoppingCartItem { id, product_variant_id, + product_id, shopping_cart_id, quantity, quantity_unit, }| ShoppingCartItem { id, + product_id, product_variant_id, shopping_cart_id, quantity, @@ -250,43 +256,47 @@ impl From<(crate::ShoppingCart, Vec)> for ShoppingCart } } -#[derive(Serialize, Deserialize, Debug, Hash)] +#[derive(Debug, Hash, PartialEq, Serialize, Deserialize)] pub struct Photo { - pub id: crate::PhotoId, - pub file_name: crate::FileName, + pub id: PhotoId, + pub file_name: FileName, pub url: String, - pub unique_name: crate::UniqueName, + pub unique_name: UniqueName, } #[derive(Clone, Debug, Hash, PartialOrd, PartialEq, Eq, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub struct Category { - pub name: String, - pub key: String, - pub svg: String, + pub id: CategoryId, + pub parent_id: Option, + pub name: CategoryName, + pub key: CategoryKey, + pub svg: CategorySvg, } impl From<&crate::Category> for Category { - fn from(crate::Category { name, key, svg }: &crate::Category) -> Self { + fn from(&crate::Category { name, key, svg }: &crate::Category) -> Self { Self { - name: (*name).into(), - key: (*key).into(), - svg: (*svg).into(), + id: Default::default(), + parent_id: None, + name: CategoryName::from(name), + key: CategoryKey::from(key), + svg: CategorySvg::from(svg), } } } #[derive(Serialize, Deserialize, Debug, Hash)] pub struct Product { - pub id: crate::ProductId, - pub name: crate::ProductName, - pub short_description: crate::ProductShortDesc, - pub long_description: crate::ProductLongDesc, + pub id: ProductId, + pub name: ProductName, + pub short_description: ProductShortDesc, + pub long_description: ProductLongDesc, pub category: Option, - pub price: crate::Price, + pub price: Price, pub available: bool, - pub quantity_unit: crate::QuantityUnit, - pub deliver_days_flag: crate::Days, + pub quantity_unit: QuantityUnit, + pub deliver_days_flag: Days, pub photos: Vec, } @@ -315,7 +325,7 @@ impl<'path> ): ( crate::Product, &mut Vec, - &mut Vec, + &mut Vec, &'path str, ), ) -> Self { @@ -325,21 +335,13 @@ impl<'path> let (available, quantity_unit) = pos .map(|idx| product_stocks.remove(idx)) .map(|stock| (**stock.quantity > 0, stock.quantity_unit)) - .unwrap_or_else(|| (false, crate::QuantityUnit::Piece)); + .unwrap_or_else(|| (false, QuantityUnit::Piece)); Self { id, name, short_description, long_description, - category: category.and_then(|name| { - crate::CATEGORIES.iter().find_map(|c| { - if c.name == name.as_str() { - Some(Category::from(c)) - } else { - None - } - }) - }), + category: category.and_then(CategoryMapper::api_from_product_category), price, available, quantity_unit, @@ -367,32 +369,32 @@ impl<'path> #[derive(Serialize, Deserialize, Debug, Deref)] #[serde(transparent)] -pub struct Products(pub Vec); +pub struct Products(pub Vec); -impl - From<( - Vec, - Vec, - Vec, - String, - )> for Products -{ - fn from( - (products, mut photos, mut products_stock, public_path): ( - Vec, - Vec, - Vec, - String, - ), - ) -> Self { - Self( - products - .into_iter() - .map(|p| (p, &mut photos, &mut products_stock, public_path.as_str()).into()) - .collect(), - ) - } -} +// impl +// From<( +// Vec, +// Vec, +// Vec, +// String, +// )> for Products +// { +// fn from( +// (products, mut photos, mut products_stock, public_path): ( +// Vec, +// Vec, +// Vec, +// String, +// ), +// ) -> Self { +// Self( +// products +// .into_iter() +// .map(|p| (p, &mut photos, &mut products_stock, +// public_path.as_str()).into()) .collect(), +// ) +// } +// } #[derive(Serialize, Deserialize, Debug)] pub struct SignInInput { @@ -453,6 +455,7 @@ pub struct DeleteItemOutput { #[derive(Serialize, Deserialize, Debug)] pub struct UpdateItemInput { pub product_variant_id: ProductVariantId, + pub product_id: ProductId, pub quantity: Quantity, pub quantity_unit: QuantityUnit, } diff --git a/crates/model/src/lib.rs b/crates/model/src/lib.rs index a516403..de6ba42 100644 --- a/crates/model/src/lib.rs +++ b/crates/model/src/lib.rs @@ -14,7 +14,7 @@ use serde::de::{Error, Visitor}; use serde::{Deserialize, Deserializer, Serialize}; pub use crate::encrypt::*; -use crate::v2::ProductVariantId; +use crate::v2::{CategoryKey, CategoryName, CategorySvg, ProductVariantId}; #[derive(Debug, Hash, thiserror::Error)] pub enum TransformError { @@ -71,53 +71,94 @@ macro_rules! category_svg { }; } -pub const CATEGORIES: [Category; 9] = [ - Category { - name: Category::CAMERAS_NAME, - key: Category::CAMERAS_KEY, - svg: category_svg!("cameras"), - }, - Category { - name: Category::DRUGSTORE_NAME, - key: Category::DRUGSTORE_KEY, - svg: category_svg!("drugstore"), - }, - Category { - name: Category::SPEAKERS_NAME, - key: Category::SPEAKERS_KEY, - svg: category_svg!("speakers"), - }, - Category { - name: Category::PHONES_NAME, - key: Category::PHONES_KEY, - svg: category_svg!("phones"), - }, - Category { - name: Category::SWEETS_NAME, - key: Category::SWEETS_KEY, - svg: category_svg!("sweets"), - }, - Category { - name: Category::MEMORY_NAME, - key: Category::MEMORY_KEY, - svg: category_svg!("memory"), - }, - Category { - name: Category::PANTS_NAME, - key: Category::PANTS_KEY, - svg: category_svg!("pants"), - }, - Category { - name: Category::CLOTHES_NAME, - key: Category::CLOTHES_KEY, - svg: category_svg!("clothes"), - }, - Category { - name: Category::PLATES_NAME, - key: Category::PLATES_KEY, - svg: category_svg!("plates"), - }, -]; +pub struct CategoryMapper; + +impl CategoryMapper { + pub const CATEGORIES: [Category; 9] = [ + Category { + name: Category::CAMERAS_NAME, + key: Category::CAMERAS_KEY, + svg: category_svg!("cameras"), + }, + Category { + name: Category::DRUGSTORE_NAME, + key: Category::DRUGSTORE_KEY, + svg: category_svg!("drugstore"), + }, + Category { + name: Category::SPEAKERS_NAME, + key: Category::SPEAKERS_KEY, + svg: category_svg!("speakers"), + }, + Category { + name: Category::PHONES_NAME, + key: Category::PHONES_KEY, + svg: category_svg!("phones"), + }, + Category { + name: Category::SWEETS_NAME, + key: Category::SWEETS_KEY, + svg: category_svg!("sweets"), + }, + Category { + name: Category::MEMORY_NAME, + key: Category::MEMORY_KEY, + svg: category_svg!("memory"), + }, + Category { + name: Category::PANTS_NAME, + key: Category::PANTS_KEY, + svg: category_svg!("pants"), + }, + Category { + name: Category::CLOTHES_NAME, + key: Category::CLOTHES_KEY, + svg: category_svg!("clothes"), + }, + Category { + name: Category::PLATES_NAME, + key: Category::PLATES_KEY, + svg: category_svg!("plates"), + }, + ]; + + pub fn api_from_product_category(name: ProductCategory) -> Option { + Self::CATEGORIES + .iter() + .find(|category| category.name == name.as_str()) + .map(|&Category { name, key, svg }| crate::api::Category { + id: Default::default(), + parent_id: None, + name: CategoryName::from(name), + key: CategoryKey::from(key), + svg: CategorySvg::from(svg), + }) + } + + pub fn db_into_api( + name: ProductCategory, + categories: &[crate::v2::Category], + ) -> Option { + categories + .iter() + .find(|category| category.name.as_str() == name.as_str()) + .map( + |crate::v2::Category { + id, + parent_id, + name, + key, + svg, + }| crate::api::Category { + id: id.clone(), + parent_id: parent_id.clone(), + name: name.clone(), + key: key.clone(), + svg: svg.clone(), + }, + ) + } +} #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(rename_all = "snake_case"))] @@ -943,7 +984,7 @@ impl ProductLongDesc { #[cfg_attr(feature = "db", sqlx(transparent))] #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Deref, Display, From)] #[serde(transparent)] -pub struct ProductCategory(String); +pub struct ProductCategory(pub String); impl ProductCategory { pub fn new>(s: S) -> Self { @@ -955,121 +996,6 @@ impl ProductCategory { } } -pub mod v2 { - use derive_more::{Deref, Display, From}; - use serde::{Deserialize, Serialize}; - - pub use crate::{ - Day, Days, FileName, Limit, LocalPath, Offset, PhotoId, Price, ProductCategory, ProductId, - ProductLongDesc, ProductName, ProductPhotoId, ProductShortDesc, Quantity, QuantityUnit, - RecordId, ShoppingCartId, StockId, UniqueName, - }; - - #[cfg_attr(feature = "db", derive(sqlx::Type))] - #[cfg_attr(feature = "db", sqlx(transparent))] - #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Deref, Display, From)] - #[serde(transparent)] - pub struct ProductVariantName(String); - - impl ProductVariantName { - pub fn new>(s: S) -> Self { - Self(s.into()) - } - - pub fn into_inner(self) -> String { - self.0 - } - - pub fn as_str(&self) -> &str { - &self.0 - } - } - - #[cfg_attr(feature = "db", derive(sqlx::Type))] - #[cfg_attr(feature = "db", sqlx(transparent))] - #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Deref, From)] - #[serde(transparent)] - pub struct ProductVariantId(pub RecordId); - - #[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] - pub struct DetailedProductVariant { - pub id: ProductVariantId, - pub name: ProductVariantName, - pub short_description: ProductShortDesc, - pub long_description: ProductLongDesc, - pub price: Price, - - pub stocks: Vec, - - pub photos: Vec, - } - - #[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] - pub struct DetailedProduct { - pub id: ProductId, - pub name: ProductName, - pub category: Option, - pub deliver_days_flag: Days, - pub variants: Vec, - } - - #[cfg_attr(feature = "db", derive(sqlx::FromRow))] - #[derive(Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] - pub struct Product { - pub id: ProductId, - pub name: ProductName, - pub category: Option, - pub deliver_days_flag: Days, - } - - #[cfg_attr(feature = "db", derive(sqlx::FromRow))] - #[derive(Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] - pub struct ProductVariant { - pub id: ProductVariantId, - pub product_id: ProductId, - pub name: ProductVariantName, - pub short_description: ProductShortDesc, - pub long_description: ProductLongDesc, - pub price: Price, - } - - #[cfg_attr(feature = "db", derive(sqlx::FromRow))] - #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] - pub struct Stock { - pub id: StockId, - pub product_variant_id: ProductVariantId, - pub quantity: Quantity, - pub quantity_unit: QuantityUnit, - } - - #[cfg_attr(feature = "db", derive(sqlx::FromRow))] - #[derive(Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] - pub struct Photo { - pub id: PhotoId, - pub local_path: LocalPath, - pub file_name: FileName, - pub unique_name: UniqueName, - } - - #[cfg_attr(feature = "db", derive(sqlx::FromRow))] - #[derive(Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] - pub struct ProductLinkedPhoto { - pub photo_id: PhotoId, - pub local_path: LocalPath, - pub file_name: FileName, - pub unique_name: UniqueName, - pub product_variant_id: ProductVariantId, - } - - #[cfg_attr(feature = "db", derive(sqlx::FromRow))] - #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] - pub struct ProductPhoto { - pub id: ProductPhotoId, - pub product_variant_id: ProductVariantId, - pub photo_id: PhotoId, - } -} - #[cfg_attr(feature = "db", derive(sqlx::FromRow))] #[derive(Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] pub struct Product { @@ -1216,6 +1142,7 @@ pub struct ShoppingCartItemId(RecordId); #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct ShoppingCartItem { pub id: ShoppingCartItemId, + pub product_id: ProductId, pub product_variant_id: ProductVariantId, pub shopping_cart_id: ShoppingCartId, pub quantity: Quantity, @@ -1568,3 +1495,233 @@ pub struct OrderAddress { pub zip: Zip, pub phone: Phone, } + +pub mod v2 { + use derive_more::{Deref, Display, From}; + use serde::{Deserialize, Serialize}; + + use crate::NonNegative; + pub use crate::{ + Day, Days, FileName, Limit, LocalPath, Offset, PhotoId, Price, ProductCategory, ProductId, + ProductLongDesc, ProductName, ProductPhotoId, ProductShortDesc, Quantity, QuantityUnit, + RecordId, ShoppingCartId, StockId, UniqueName, + }; + + #[cfg_attr(feature = "db", derive(sqlx::Type))] + #[cfg_attr(feature = "db", sqlx(transparent))] + #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Deref, Display, From)] + #[serde(transparent)] + pub struct ProductVariantName(String); + + impl ProductVariantName { + pub fn new>(s: S) -> Self { + Self(s.into()) + } + + pub fn into_inner(self) -> String { + self.0 + } + + pub fn as_str(&self) -> &str { + &self.0 + } + } + + #[cfg_attr(feature = "db", derive(sqlx::Type))] + #[cfg_attr(feature = "db", sqlx(transparent))] + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Deref, From)] + #[serde(transparent)] + pub struct ProductVariantId(pub RecordId); + + #[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)] + pub struct DetailedProductVariant { + pub id: ProductVariantId, + pub name: ProductVariantName, + pub short_description: ProductShortDesc, + pub long_description: ProductLongDesc, + pub price: Price, + pub quantity_unit: QuantityUnit, + + pub stocks: Vec, + + pub photos: Vec, + + pub available: bool, + } + + #[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)] + pub struct DetailedProduct { + pub id: ProductId, + pub name: ProductName, + pub category: Option, + pub deliver_days_flag: Days, + pub variants: Vec, + } + + impl DetailedProduct { + pub fn variant_for(&self, variant_id: ProductVariantId) -> Option<&DetailedProductVariant> { + self.variants.iter().find(|v| v.id == variant_id) + } + } + + #[cfg_attr(feature = "db", derive(sqlx::FromRow))] + #[derive(Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] + pub struct Product { + pub id: ProductId, + pub name: ProductName, + pub category: Option, + pub deliver_days_flag: Days, + } + + #[cfg_attr(feature = "db", derive(sqlx::FromRow))] + #[derive(Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] + pub struct ProductVariant { + pub id: ProductVariantId, + pub product_id: ProductId, + pub name: ProductVariantName, + pub short_description: ProductShortDesc, + pub long_description: ProductLongDesc, + pub price: Price, + pub quantity_unit: QuantityUnit, + } + + #[cfg_attr(feature = "db", derive(sqlx::FromRow))] + #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] + pub struct Stock { + pub id: StockId, + pub product_variant_id: ProductVariantId, + pub quantity: Quantity, + pub quantity_unit: QuantityUnit, + } + + #[cfg_attr(feature = "db", derive(sqlx::FromRow))] + #[derive(Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] + pub struct Photo { + pub id: PhotoId, + pub local_path: LocalPath, + pub file_name: FileName, + pub unique_name: UniqueName, + } + + #[cfg_attr(feature = "db", derive(sqlx::FromRow))] + #[derive(Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] + pub struct ProductLinkedPhoto { + pub photo_id: PhotoId, + pub local_path: LocalPath, + pub file_name: FileName, + pub unique_name: UniqueName, + pub product_variant_id: ProductVariantId, + } + + #[cfg_attr(feature = "db", derive(sqlx::FromRow))] + #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] + pub struct ProductPhoto { + pub id: ProductPhotoId, + pub product_variant_id: ProductVariantId, + pub photo_id: PhotoId, + } + + #[cfg_attr(feature = "db", derive(sqlx::Type))] + #[cfg_attr(feature = "db", sqlx(transparent))] + #[derive( + Debug, + Clone, + Default, + PartialOrd, + PartialEq, + Eq, + Hash, + Serialize, + Deserialize, + Deref, + From, + Display, + )] + #[serde(transparent)] + pub struct CategoryId(NonNegative); + + #[cfg_attr(feature = "db", derive(sqlx::Type))] + #[cfg_attr(feature = "db", sqlx(transparent))] + #[derive( + Debug, + Clone, + Default, + PartialOrd, + PartialEq, + Eq, + Hash, + Serialize, + Deserialize, + Deref, + From, + Display, + )] + #[serde(transparent)] + pub struct CategoryName(pub String); + + impl<'s> From<&'s str> for CategoryName { + fn from(value: &'s str) -> Self { + Self(value.into()) + } + } + + #[cfg_attr(feature = "db", derive(sqlx::Type))] + #[cfg_attr(feature = "db", sqlx(transparent))] + #[derive( + Debug, + Clone, + Default, + PartialOrd, + PartialEq, + Eq, + Hash, + Serialize, + Deserialize, + Deref, + From, + Display, + )] + #[serde(transparent)] + pub struct CategoryKey(pub String); + + impl<'s> From<&'s str> for CategoryKey { + fn from(value: &'s str) -> Self { + Self(value.into()) + } + } + + #[cfg_attr(feature = "db", derive(sqlx::Type))] + #[cfg_attr(feature = "db", sqlx(transparent))] + #[derive( + Debug, + Clone, + Default, + PartialOrd, + PartialEq, + Eq, + Hash, + Serialize, + Deserialize, + Deref, + From, + Display, + )] + #[serde(transparent)] + pub struct CategorySvg(pub String); + + impl<'s> From<&'s str> for CategorySvg { + fn from(value: &'s str) -> Self { + Self(value.into()) + } + } + + #[cfg_attr(feature = "db", derive(sqlx::FromRow))] + #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] + pub struct Category { + pub id: CategoryId, + pub parent_id: Option, + pub name: CategoryName, + pub key: CategoryKey, + pub svg: CategorySvg, + } +} diff --git a/crates/stock_manager/migrations/202204131841_init.sql b/crates/stock_manager/migrations/202204131841_init.sql index a78d16d..8cd7425 100644 --- a/crates/stock_manager/migrations/202204131841_init.sql +++ b/crates/stock_manager/migrations/202204131841_init.sql @@ -28,6 +28,7 @@ CREATE TABLE product_variants ( short_description character varying NOT NULL, long_description character varying NOT NULL, price integer NOT NULL, + quantity_unit "QuantityUnit" NOT NULL, CONSTRAINT non_negative CHECK ((price >= 0)) ); @@ -44,3 +45,11 @@ CREATE TABLE product_photos ( product_variant_id integer REFERENCES product_variants(id) ON DELETE CASCADE NOT NULL, photo_id integer REFERENCES photos(id) NOT NULL ); + +CREATE TABLE categories ( + id serial NOT NULL PRIMARY KEY, + parent_id int references categories (id) ON DELETE CASCADE, + "name" varchar not null, + "key" varchar not null, + "svg" varchar not null +); diff --git a/crates/stock_manager/src/actions/load.rs b/crates/stock_manager/src/actions/load.rs index f58044e..e1e0bf2 100644 --- a/crates/stock_manager/src/actions/load.rs +++ b/crates/stock_manager/src/actions/load.rs @@ -2,9 +2,11 @@ use channels::stocks::{detailed_product, detailed_products, Error}; use channels::AsyncClient; use config::SharedAppConfig; use db_utils::PgT; -use model::Limit; +use model::{Limit, Offset}; -use crate::db::{Database, PhotosForProductVariants, ProductVariantsStock, ProductsVariants}; +use crate::db::{ + AllCategories, Database, PhotosForProductVariants, ProductVariantsStock, ProductsVariants, +}; use crate::{begin_t, dbm_run}; pub async fn detailed_product( @@ -26,7 +28,7 @@ async fn inner_detailed_product( input: detailed_product::Input, t: &mut PgT<'_>, _mqtt: Option, - _config: Option, + config: Option, ) -> detailed_product::Output { let dbm = crate::db::FindProduct { product_id: input.product_id, @@ -55,13 +57,29 @@ async fn inner_detailed_product( Error::VariantPhotos(variants.into_iter().map(|p| p.id).collect()) ); + let dbm = AllCategories { + limit: Limit::from_u32(2000), + offset: Offset::from_u32(0), + }; + let categories = dbm_run!(dbm, t, Error::Categories); + let mut variants = utils::vec_to_hash_vec(variants, 10, |p| p.product_id); let mut stocks = utils::vec_to_hash_vec(stocks, 10, |s| s.product_variant_id); let mut photos = utils::vec_to_hash_vec(photos, 10, |p| p.product_variant_id); - let product = utils::map_product(product, &mut variants, &mut stocks, &mut photos); + let product = utils::map_product( + product, + &mut variants, + &mut stocks, + &mut photos, + &categories, + &config + .as_ref() + .map(|c: &SharedAppConfig| c.lock().files().public_path()) + .unwrap_or_else(|| "https://example.com/".into()), + ); - Ok(detailed_product::Output2 { product }) + Ok(detailed_product::Details { product }) } pub async fn detailed_products( @@ -83,7 +101,7 @@ async fn inner_detailed_products( input: detailed_products::Input, t: &mut PgT<'_>, _mqtt: Option, - _config: Option, + config: Option, ) -> detailed_products::Output { let dbm = crate::db::AllProducts { limit: input.limit, @@ -124,6 +142,11 @@ async fn inner_detailed_products( )); } }; + let dbm = AllCategories { + limit: Limit::from_u32(2000), + offset: Offset::from_u32(0), + }; + let categories = dbm_run!(dbm, t, Error::Categories); let mut variants = utils::vec_to_hash_vec(variants, 10, |p| p.product_id); let mut stocks = utils::vec_to_hash_vec(stocks, 10, |s| s.product_variant_id); @@ -131,7 +154,19 @@ async fn inner_detailed_products( let products = products .into_iter() - .map(|product| utils::map_product(product, &mut variants, &mut stocks, &mut photos)) + .map(|product| { + utils::map_product( + product, + &mut variants, + &mut stocks, + &mut photos, + &categories, + &config + .as_ref() + .map(|config: &SharedAppConfig| config.lock().files().public_path()) + .unwrap_or_else(|| "https:///example.com".into()), + ) + }) .collect(); Ok(detailed_products::Details { products }) @@ -142,6 +177,7 @@ mod utils { use std::hash::Hash; use model::v2::*; + use model::CategoryMapper; pub fn vec_to_hash_vec Id>( v: Vec, @@ -162,6 +198,8 @@ mod utils { variants: &mut HashMap>, stocks: &mut HashMap>, photos: &mut HashMap>, + categories: &[Category], + public_path: &str, ) -> DetailedProduct { let Product { id, @@ -172,7 +210,7 @@ mod utils { DetailedProduct { id, name, - category, + category: category.and_then(|name| CategoryMapper::db_into_api(name, categories)), deliver_days_flag, variants: variants .remove(&id) @@ -186,14 +224,38 @@ mod utils { short_description, long_description, price, - }| DetailedProductVariant { - id, - name, - short_description, - long_description, - price, - stocks: stocks.remove(&id).unwrap_or_default(), - photos: photos.remove(&id).unwrap_or_default(), + quantity_unit, + }| { + let stocks = stocks.remove(&id).unwrap_or_default(); + DetailedProductVariant { + id, + name, + short_description, + long_description, + price, + quantity_unit, + available: !stocks.is_empty(), + stocks, + photos: photos + .remove(&id) + .unwrap_or_default() + .into_iter() + .map( + |ProductLinkedPhoto { + photo_id, + local_path, + file_name, + unique_name, + product_variant_id: _, + }| model::api::Photo { + id: photo_id, + file_name, + url: format!("{public_path}/{unique_name}"), + unique_name, + }, + ) + .collect(), + } }, ) .collect(), @@ -233,6 +295,7 @@ mod tests { short_description: ProductShortDesc::new(format!("{}", Uuid::new_v4())), long_description: ProductLongDesc::new(format!("{}", Uuid::new_v4())), price: Default::default(), + quantity_unit: QuantityUnit::Gram, } .run(t) .await diff --git a/crates/stock_manager/src/actions/product.rs b/crates/stock_manager/src/actions/product.rs index 0e73897..66e44d6 100644 --- a/crates/stock_manager/src/actions/product.rs +++ b/crates/stock_manager/src/actions/product.rs @@ -3,6 +3,7 @@ use channels::AsyncClient; use config::SharedAppConfig; use db_utils::PgT; use model::v2::*; +use model::CategoryMapper; use crate::begin_t; use crate::db::Database; @@ -61,6 +62,7 @@ async fn inner_create_product( short_description: input.product.short_description, long_description: input.product.long_description, price: input.product.price, + quantity_unit: input.product.quantity_unit, }; let variant = match dbm.run(&mut *t).await { Ok(variant) => variant, @@ -87,7 +89,9 @@ async fn inner_create_product( product: DetailedProduct { id: product.id, name: product.name, - category: product.category, + category: product + .category + .and_then(CategoryMapper::api_from_product_category), deliver_days_flag: product.deliver_days_flag, variants: vec![DetailedProductVariant { id: variant.id, @@ -95,8 +99,10 @@ async fn inner_create_product( short_description: variant.short_description, long_description: variant.long_description, price: variant.price, + quantity_unit: QuantityUnit::Gram, stocks: vec![stock], photos: vec![], + available: false, }], }, }) @@ -259,6 +265,7 @@ mod tests { long_description: ProductLongDesc::new("long description"), category: None, price: Default::default(), + quantity_unit: QuantityUnit::Gram, deliver_days_flag: Days(vec![]), }, stock: create_product::StockInput { @@ -286,6 +293,7 @@ mod tests { long_description: ProductLongDesc::new("long description"), category: None, price: Default::default(), + quantity_unit: QuantityUnit::Gram, deliver_days_flag: Days(vec![]), }, stock: create_product::StockInput { diff --git a/crates/stock_manager/src/actions/product_photo.rs b/crates/stock_manager/src/actions/product_photo.rs index ce6c9ba..616ef1a 100644 --- a/crates/stock_manager/src/actions/product_photo.rs +++ b/crates/stock_manager/src/actions/product_photo.rs @@ -215,6 +215,7 @@ mod tests { short_description: ProductShortDesc::new(fakeit::words::sentence(4)), long_description: ProductLongDesc::new(fakeit::words::sentence(16)), price: Price::from_u32(650), + quantity_unit: QuantityUnit::Gram, } .run(t) .await diff --git a/crates/stock_manager/src/actions/product_stock.rs b/crates/stock_manager/src/actions/product_stock.rs index d8e4dcf..68c5135 100644 --- a/crates/stock_manager/src/actions/product_stock.rs +++ b/crates/stock_manager/src/actions/product_stock.rs @@ -128,6 +128,7 @@ mod tests { short_description: ProductShortDesc::new(fakeit::words::sentence(4)), long_description: ProductLongDesc::new(fakeit::words::sentence(16)), price: Price::from_u32(650), + quantity_unit: QuantityUnit::Gram, } .run(t) .await diff --git a/crates/stock_manager/src/actions/product_variant.rs b/crates/stock_manager/src/actions/product_variant.rs index fc39dd1..4f05d04 100644 --- a/crates/stock_manager/src/actions/product_variant.rs +++ b/crates/stock_manager/src/actions/product_variant.rs @@ -47,6 +47,7 @@ async fn inner_create_product_variant( short_description: input.short_description, long_description: input.long_description, price: input.price, + quantity_unit: input.quantity_unit, }; match dbm.run(t).await { Ok(variant) => Ok(create_product_variant::Details { @@ -96,6 +97,7 @@ async fn inner_update_product_variant( short_description: input.short_description, long_description: input.long_description, price: input.price, + quantity_unit: input.quantity_unit, }; match dbm.run(t).await { Ok(product_variant) => Ok(update_product_variant::Details { product_variant }), @@ -233,6 +235,7 @@ mod test { short_description: ProductShortDesc::new(fakeit::words::sentence(4)), long_description: ProductLongDesc::new(fakeit::words::sentence(16)), price: Price::from_u32(650), + quantity_unit: QuantityUnit::Gram, }, t, ) @@ -259,6 +262,7 @@ mod test { short_description: short_description.clone(), long_description: long_description.clone(), price, + quantity_unit: QuantityUnit::Gram, }, &mut t, ) @@ -296,6 +300,7 @@ mod test { short_description: short_description.clone(), long_description: long_description.clone(), price, + quantity_unit: QuantityUnit::Gram, }, &mut t, ) @@ -312,6 +317,7 @@ mod test { short_description, long_description, price, + quantity_unit: QuantityUnit::Gram, }; assert_eq!(res.product_variant, expected); } diff --git a/crates/stock_manager/src/db/categories.rs b/crates/stock_manager/src/db/categories.rs new file mode 100644 index 0000000..2672f8e --- /dev/null +++ b/crates/stock_manager/src/db/categories.rs @@ -0,0 +1,124 @@ +use db_utils::PgT; +use model::v2::{Category, CategoryId, CategoryKey, CategoryName, CategorySvg}; +use model::{Limit, Offset}; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Failed to load all categories")] + All, + #[error("Failed to create category")] + Create, +} + +pub type Result = std::result::Result; + +pub struct CreateCategory { + pub parent_id: Option, + pub name: CategoryName, + pub key: CategoryKey, + pub svg: CategorySvg, +} + +impl CreateCategory { + pub async fn run(self, t: &mut PgT<'_>) -> Result { + sqlx::query_as( + r#" +INSERT INTO categories (parent_id, name, key, svg) +VALUES ($1, $2, $3, $4) +RETURNING id, + parent_id, + name, + key, + svg + "#, + ) + .bind(&self.parent_id) + .bind(&self.name) + .bind(&self.key) + .bind(&self.svg) + .fetch_one(t) + .await + .map_err(|e| { + tracing::warn!("{e:?}"); + dbg!(e); + Error::Create + }) + } +} + +pub struct AllCategories { + pub limit: Limit, + pub offset: Offset, +} + +impl AllCategories { + pub async fn run(self, t: &mut PgT<'_>) -> Result> { + sqlx::query_as( + r#" +SELECT id, + parent_id, + name, + key, + svg +FROM categories +ORDER BY id ASC +LIMIT $1 OFFSET $2 +"#, + ) + .bind(self.limit) + .bind(self.offset) + .fetch_all(t) + .await + .map_err(|e| { + tracing::warn!("{e:?}"); + dbg!(e); + Error::All + }) + } +} + +#[cfg(test)] +mod tests { + use config::UpdateConfig; + use db_utils::PgT; + use model::v2::{Category, CategoryName}; + use model::{Limit, Offset}; + + use crate::db::{AllCategories, CreateCategory, Database}; + + struct NoOpts; + + impl UpdateConfig for NoOpts {} + + async fn test_category(name: CategoryName, t: &mut PgT<'_>) -> Category { + CreateCategory { + parent_id: None, + name, + key: Default::default(), + svg: Default::default(), + } + .run(t) + .await + .unwrap() + } + + #[tokio::test] + async fn load_all() { + testx::db_t_ref!(t); + + test_category("Electronics".into(), &mut t).await; + test_category("Shoes".into(), &mut t).await; + test_category("Pants".into(), &mut t).await; + + let res = AllCategories { + limit: Limit::from_u32(2000), + offset: Offset::from_u32(0), + } + .run(&mut t) + .await; + + testx::db_rollback!(t); + + assert_eq!(res.unwrap().len(), 3); + } +} diff --git a/crates/stock_manager/src/db/mod.rs b/crates/stock_manager/src/db/mod.rs index c1c5511..08aa25c 100644 --- a/crates/stock_manager/src/db/mod.rs +++ b/crates/stock_manager/src/db/mod.rs @@ -2,12 +2,14 @@ use config::SharedAppConfig; use sqlx_core::pool::Pool; use sqlx_core::postgres::Postgres; +pub mod categories; pub mod photos; pub mod product_photos; pub mod product_variants; pub mod products; pub mod stocks; +pub use categories::*; pub use photos::*; pub use product_photos::*; pub use product_variants::*; diff --git a/crates/stock_manager/src/db/photos.rs b/crates/stock_manager/src/db/photos.rs index 50663c9..490eb67 100644 --- a/crates/stock_manager/src/db/photos.rs +++ b/crates/stock_manager/src/db/photos.rs @@ -184,6 +184,7 @@ mod tests { short_description: ProductShortDesc::new(format!("{}", Uuid::new_v4())), long_description: ProductLongDesc::new(format!("{}", Uuid::new_v4())), price: Default::default(), + quantity_unit: QuantityUnit::Gram, } .run(t) .await diff --git a/crates/stock_manager/src/db/product_photos.rs b/crates/stock_manager/src/db/product_photos.rs index 047ef5d..601e17d 100644 --- a/crates/stock_manager/src/db/product_photos.rs +++ b/crates/stock_manager/src/db/product_photos.rs @@ -126,6 +126,7 @@ mod tests { short_description: ProductShortDesc::new(format!("{}", Uuid::new_v4())), long_description: ProductLongDesc::new(format!("{}", Uuid::new_v4())), price: Default::default(), + quantity_unit: QuantityUnit::Gram, } .run(t) .await diff --git a/crates/stock_manager/src/db/product_variants.rs b/crates/stock_manager/src/db/product_variants.rs index d9441e5..0110078 100644 --- a/crates/stock_manager/src/db/product_variants.rs +++ b/crates/stock_manager/src/db/product_variants.rs @@ -33,7 +33,8 @@ SELECT id, name, short_description, long_description, - price + price, + quantity_unit FROM product_variants WHERE "#, @@ -63,6 +64,7 @@ pub struct CreateProductVariant { pub short_description: ProductShortDesc, pub long_description: ProductLongDesc, pub price: Price, + pub quantity_unit: QuantityUnit, } impl CreateProductVariant { @@ -74,14 +76,16 @@ INSERT INTO product_variants ( name, short_description, long_description, - price -) VALUES ($1, $2, $3, $4, $5) + price, + quantity_unit +) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, product_id, name, short_description, long_description, - price + price, + quantity_unit "#, ) .bind(self.product_id) @@ -89,6 +93,7 @@ RETURNING id, .bind(self.short_description) .bind(self.long_description) .bind(self.price) + .bind(self.quantity_unit) .fetch_one(pool) .await .map_err(|e| { @@ -107,6 +112,7 @@ pub struct UpdateProductVariant { pub short_description: ProductShortDesc, pub long_description: ProductLongDesc, pub price: Price, + pub quantity_unit: QuantityUnit, } impl UpdateProductVariant { @@ -118,14 +124,16 @@ SET product_id = $2, name = $3, short_description = $4, long_description = $5, - price = $6 + price = $6, + quantity_unit = $7 WHERE id = $1 RETURNING id, product_id, name, short_description, long_description, - price + price, + quantity_unit "#, ) .bind(self.product_variant_id) @@ -134,6 +142,7 @@ RETURNING id, .bind(self.short_description) .bind(self.long_description) .bind(self.price) + .bind(self.quantity_unit) .fetch_one(pool) .await .map_err(|e| { @@ -161,7 +170,8 @@ SELECT pv.id, pv.name, pv.short_description, pv.long_description, - pv.price + pv.price, + pv.quantity_unit FROM product_variants pv INNER JOIN products ps ON pv.product_id = ps.id @@ -203,7 +213,8 @@ RETURNING id, name, short_description, long_description, - price + price, + quantity_unit "#, ) .bind(self.product_variant_id) @@ -234,6 +245,7 @@ mod tests { short_description: ProductShortDesc::new(format!("{}", Uuid::new_v4())), long_description: ProductLongDesc::new(format!("{}", Uuid::new_v4())), price: Default::default(), + quantity_unit: QuantityUnit::Gram, } .run(t) .await @@ -268,6 +280,7 @@ mod tests { short_description: ProductShortDesc::new("aosdjajsodjaoisdjoajs"), long_description: ProductLongDesc::new("jsa a98dh 9ahsd ha89shd 98aus 98asu "), price: Default::default(), + quantity_unit: QuantityUnit::Gram, }; let res = dbm.run(&mut t).await; diff --git a/crates/stock_manager/src/db/stocks.rs b/crates/stock_manager/src/db/stocks.rs index 1ab3d32..f554e14 100644 --- a/crates/stock_manager/src/db/stocks.rs +++ b/crates/stock_manager/src/db/stocks.rs @@ -223,6 +223,7 @@ mod tests { short_description: ProductShortDesc::new(format!("{}", Uuid::new_v4())), long_description: ProductLongDesc::new(format!("{}", Uuid::new_v4())), price: Default::default(), + quantity_unit: QuantityUnit::Gram, } .run(t) .await diff --git a/crates/web/src/api/public.rs b/crates/web/src/api/public.rs index d841b54..0f0da3e 100644 --- a/crates/web/src/api/public.rs +++ b/crates/web/src/api/public.rs @@ -1,6 +1,6 @@ use model::api::OrderAddressInput; use model::v2::ProductVariantId; -use model::{AccessTokenString, AddressId, PaymentMethod, RefreshTokenString}; +use model::{AccessTokenString, AddressId, PaymentMethod, ProductId, RefreshTokenString}; use seed::fetch::{Header, Method, Request}; use crate::api::perform; @@ -68,11 +68,13 @@ pub async fn sign_up(input: model::api::CreateAccountInput) -> NetRes NetRes { let input = model::api::UpdateItemInput { product_variant_id, + product_id, quantity, quantity_unit, }; @@ -90,7 +92,7 @@ pub async fn update_cart( access_token: AccessTokenString, items: Vec, notes: String, - payment_method: Option, + payment_method: Option, ) -> NetRes { let input = model::api::UpdateCartInput { notes, @@ -99,10 +101,12 @@ pub async fn update_cart( .map( |crate::shopping_cart::Item { product_variant_id, + product_id, quantity, quantity_unit, }| model::api::UpdateItemInput { product_variant_id, + product_id, quantity, quantity_unit, }, diff --git a/crates/web/src/model.rs b/crates/web/src/model.rs index fef8671..1903ae3 100644 --- a/crates/web/src/model.rs +++ b/crates/web/src/model.rs @@ -1,6 +1,6 @@ use std::collections::{HashMap, HashSet}; -use model::v2::{DetailedProduct, ProductVariantId}; +use model::v2::{DetailedProduct, DetailedProductVariant}; use model::ProductId; use seed::Url; @@ -104,7 +104,7 @@ impl Products { }; } - pub fn filter_product_ids(&self, filter: F) -> Vec + pub fn filter_product_ids(&self, filter: F) -> Vec where F: Fn(&DetailedProduct) -> bool, { @@ -113,4 +113,19 @@ impl Products { .filter_map(|id| self.products.get(id).filter(|&p| filter(p)).map(|p| p.id)) .collect() } + + pub fn for_item<'item, 'products>( + &'products self, + item: &'item shopping_cart::Item, + ) -> Option<( + &'item shopping_cart::Item, + &'products DetailedProduct, + &'products DetailedProductVariant, + )> { + self.products.get(&item.product_id).and_then(|product| { + product + .variant_for(item.product_variant_id) + .map(|variant| (item, product, variant)) + }) + } } diff --git a/crates/web/src/pages/public.rs b/crates/web/src/pages/public.rs index 1fd5611..19f0314 100644 --- a/crates/web/src/pages/public.rs +++ b/crates/web/src/pages/public.rs @@ -28,22 +28,37 @@ pub mod layout { use seed::prelude::*; use seed::*; - pub fn view( + pub fn view_with_categories<'category, Categories>( model: &crate::Model, content: Node, - categories: Option<&[model::api::Category]>, - ) -> Node { - let sidebar = match categories { - Some(categories) => { - let sidebar = super::sidebar::view(model, categories); - div![ - C!["flex flex-col w-64 h-screen px-4 py-8 overflow-y-auto border-r"], - sidebar - ] - } - _ => empty![], - }; + categories: Categories, + ) -> Node + where + Categories: Iterator, + { + let sidebar = div![ + C!["flex flex-col w-64 h-screen px-4 py-8 overflow-y-auto border-r"], + super::sidebar::view(model, categories) + ]; let notifications = crate::shared::notification::view(model); + + view(notifications, sidebar, content) + } + + pub fn view_without_categories( + model: &crate::Model, + content: Node, + ) -> Node { + let notifications = crate::shared::notification::view(model); + + view(notifications, empty![], content) + } + + fn view( + notifications: Node, + sidebar: Node, + content: Node, + ) -> Node { div![ C!["flex"], sidebar, @@ -59,8 +74,14 @@ pub mod sidebar { use crate::pages::Urls; - pub fn view(model: &crate::Model, categories: &[model::api::Category]) -> Node { - let categories = categories.iter().map(|category| item(model, category)); + pub fn view<'category, Msg, Categories>( + model: &crate::Model, + categories: Categories, + ) -> Node + where + Categories: Iterator, + { + let categories = categories.map(|category| item(model, category)); div![ C!["flex flex-col justify-between mt-6"], diff --git a/crates/web/src/pages/public/checkout.rs b/crates/web/src/pages/public/checkout.rs index 5efd820..325ea1e 100644 --- a/crates/web/src/pages/public/checkout.rs +++ b/crates/web/src/pages/public/checkout.rs @@ -149,11 +149,12 @@ pub fn view(model: &crate::Model, page: &CheckoutPage) -> Node { div![ crate::shared::view::public_navbar::view(model, &page.products), - super::layout::view(model, content, None) + super::layout::view_without_categories(model, content) ] } mod left_side { + use model::v2::{DetailedProduct, DetailedProductVariant}; use rusty_money::Money; use seed::prelude::*; use seed::*; @@ -177,11 +178,23 @@ mod left_side { .values() .filter_map(|item: &Item| { page.products - .product_variants - .get(&item.product_variant_id) - .map(|product| (item, product)) + .products + .get(&item.product_id) + .map(|product: &DetailedProduct| (item, product)) }) - .map(|(item, variant)| product_view(model, variant, item)); + .filter_map(|(item, product): (&Item, &DetailedProduct)| { + product + .variants + .iter() + .find(|v| v.id == item.product_variant_id) + .zip(Some((item, product))) + }) + .map( + |(variant, (item, product)): ( + &DetailedProductVariant, + (&Item, &DetailedProduct), + )| { product_view(model, product, variant, item) }, + ); div![ C!["w-full mx-auto text-gray-800 font-light mb-6 border-b border-gray-200 pb-6"], products @@ -190,17 +203,18 @@ mod left_side { fn product_view( model: &crate::Model, - product: &model::api::v2::ProductVariant, + _product: &DetailedProduct, + variant: &DetailedProductVariant, item: &Item, ) -> Node { - let img = product + let img = variant .photos .first() .as_ref() .map(|photo| photo.url.as_str()) .unwrap_or_default(); let price = Money::from_minor( - **(product.price * item.quantity) as i64, + **(variant.price * item.quantity) as i64, model.config.currency, ) .to_string(); @@ -217,7 +231,7 @@ mod left_side { C!["flex-grow pl-3"], h6![ C!["font-semibold uppercase text-gray-600"], - product.name.as_str() + variant.name.as_str() ], p![C!["text-gray-400"], "x ", **item.quantity] ], @@ -242,14 +256,22 @@ mod left_side { .values() .filter_map(|item: &Item| { page.products - .product_variants - .get(&item.product_variant_id) + .products + .get(&item.product_id) .map(|product| (item, product)) }) + .filter_map(|(item, product): (&Item, &DetailedProduct)| { + product + .variants + .iter() + .find(|v| v.id == item.product_variant_id) + .zip(Some((item, product))) + }) .map( - |(item, product): (&Item, &model::api::v2::ProductVariant)| { - **(product.price * item.quantity) - }, + |(variant, (item, _product)): ( + &DetailedProductVariant, + (&Item, &DetailedProduct), + )| { **(variant.price * item.quantity) }, ) .sum::() as i64; diff --git a/crates/web/src/pages/public/listing.rs b/crates/web/src/pages/public/listing.rs index 6dde10a..b56b872 100644 --- a/crates/web/src/pages/public/listing.rs +++ b/crates/web/src/pages/public/listing.rs @@ -1,7 +1,7 @@ use std::collections::HashSet; -use model::v2::ProductVariantId; -use model::Quantity; +use model::v2::{DetailedProduct, DetailedProductVariant}; +use model::{ProductId, Quantity}; use seed::app::Orders; use seed::prelude::*; use seed::*; @@ -15,7 +15,7 @@ use crate::shopping_cart::CartMsg; pub struct ListingPage { pub errors: Vec, pub filters: HashSet, - pub visible_product_variants: Vec, + pub visible_products: Vec, pub products: Products, } @@ -32,7 +32,7 @@ pub fn init(url: Url, orders: &mut impl Orders) -> ListingPage { products: Products::default(), filters: url_to_filters(url), errors: vec![], - visible_product_variants: vec![], + visible_products: vec![], } } @@ -55,16 +55,16 @@ pub fn page_changed(url: Url, model: &mut ListingPage) { let ids = { model .products - .filter_product_variant_ids(|product| filter_product(&*model, product)) + .filter_product_ids(|product| filter_product(&*model, product)) }; - model.visible_product_variants = ids; + model.visible_products = ids; } -fn filter_product(model: &ListingPage, product: &model::api::v2::ProductVariant) -> bool { - product +fn filter_product(model: &ListingPage, variant: &DetailedProduct) -> bool { + variant .category .as_ref() - .filter(|c| model.filters.contains(c.key.as_str())) + .filter(|c| model.filters.contains(c.name.as_str())) .is_some() } @@ -84,8 +84,8 @@ pub fn update(msg: ListingMsg, model: &mut ListingPage, orders: &mut impl Orders model.products.update(products.0); let ids = model .products - .filter_product_variant_ids(|product| filter_product(model, product)); - model.visible_product_variants = ids; + .filter_product_ids(|product| filter_product(model, product)); + model.visible_products = ids; } ListingMsg::ProductsFetched(NetRes::Error(_)) | ListingMsg::ProductsFetched(NetRes::Http(_)) => { @@ -95,17 +95,17 @@ pub fn update(msg: ListingMsg, model: &mut ListingPage, orders: &mut impl Orders } pub fn view(model: &crate::Model, page: &ListingPage) -> Node { - let products: Vec> = if page.visible_product_variants.is_empty() { + let products: Vec>> = if page.visible_products.is_empty() { page.products - .product_variant_ids + .product_ids .iter() - .filter_map(|id| page.products.product_variants.get(id)) + .filter_map(|id| page.products.products.get(id)) .map(|p| product(model, p)) .collect() } else { - page.visible_product_variants + page.visible_products .iter() - .filter_map(|id| page.products.product_variants.get(id)) + .filter_map(|id| page.products.products.get(id)) .map(|p| product(model, p)) .collect() }; @@ -118,17 +118,28 @@ pub fn view(model: &crate::Model, page: &ListingPage) -> Node { div![ crate::shared::view::public_navbar::view(model, &page.products), - super::layout::view(model, content, Some(&page.products.categories)) + super::layout::view_with_categories(model, content, page.products.categories.iter()) ] } -fn product(model: &crate::Model, product: &model::api::v2::ProductVariant) -> Node { - use rusty_money::Money; +fn product(model: &crate::Model, product: &DetailedProduct) -> Vec> { + product + .variants + .iter() + .map(|variant| product_variant(model, product, variant)) + .collect() +} - let price = Money::from_minor(**product.price as i64, model.config.currency).to_string(); - let _description = product.short_description.as_str(); - let name = product.name.as_str(); - let img = product +fn product_variant( + model: &crate::Model, + product: &DetailedProduct, + variant: &DetailedProductVariant, +) -> Node { + use rusty_money::Money; + let price = Money::from_minor(**variant.price as i64, model.config.currency).to_string(); + let _description = variant.short_description.as_str(); + let name = variant.name.as_str(); + let img = variant .photos .first() .map(|photo| photo.url.as_str()) @@ -136,10 +147,11 @@ fn product(model: &crate::Model, product: &model::api::v2::ProductVariant) -> No let url = Urls::new(&model.url) .product() - .add_path_part((*product.id as i32).to_string()); + .add_path_part((*variant.id as i32).to_string()); - let quantity_unit = product.quantity_unit; - let product_variant_id = product.id; + let quantity_unit = variant.quantity_unit; + let product_variant_id = variant.id; + let product_id = product.id; div![ C!["w-full px-4 lg:px-0"], @@ -176,6 +188,7 @@ fn product(model: &crate::Model, product: &model::api::v2::ProductVariant) -> No ev.stop_propagation(); crate::Msg::from(CartMsg::AddItem { product_variant_id, + product_id, quantity_unit, quantity: Quantity::try_from(1).unwrap_or_default() }) diff --git a/crates/web/src/pages/public/product.rs b/crates/web/src/pages/public/product.rs index 7277c52..dd3988f 100644 --- a/crates/web/src/pages/public/product.rs +++ b/crates/web/src/pages/public/product.rs @@ -1,3 +1,4 @@ +use model::v2::{DetailedProduct, DetailedProductVariant, ProductVariantId}; use seed::prelude::*; use seed::*; @@ -12,6 +13,7 @@ pub enum ProductMsg { #[derive(Debug, Default)] pub struct ProductPage { pub product_id: Option, + pub product_variant_id: Option, pub products: crate::model::Products, pub selected_image: usize, } @@ -28,6 +30,7 @@ pub fn init(mut url: Url, orders: &mut impl Orders) -> ProductPage { }); ProductPage { product_id: Some(product_id), + product_variant_id: None, products: Default::default(), selected_image: 0, } @@ -61,16 +64,24 @@ pub fn view(model: &crate::Model, page: &ProductPage) -> Node { None => return empty!(), Some(product) => product, }; - let large_photo = product + let variant: &DetailedProductVariant = match page + .product_variant_id + .and_then(|id| product.variant_for(id)) + { + Some(v) => v, + _ => return empty![], + }; + let large_photo = variant .photos .get(page.selected_image) .map(image) .unwrap_or_else(|| empty![]); + let small_photos = { - if product.photos.len() <= 1 { + if variant.photos.len() <= 1 { empty![] } else { - let photos = product + let photos = variant .photos .iter() .enumerate() @@ -79,13 +90,13 @@ pub fn view(model: &crate::Model, page: &ProductPage) -> Node { } }; - let description = product + let description = variant .long_description .as_str() .split('\n') .map(|s| div![s]); - let delivery = delivery_available(product, model); + let delivery = delivery_available(product, variant, model); let content = div![ C!["max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-6"], @@ -106,11 +117,11 @@ pub fn view(model: &crate::Model, page: &ProductPage) -> Node { C!["mb-2 leading-tight tracking-tight font-bold text-gray-800 text-2xl md:text-3xl"], product.name.as_str() ], - div![model.i18n.t("Price per"), " ", model.i18n.t(product.quantity_unit.name()).to_lowercase()], + div![model.i18n.t("Price per"), " ", model.i18n.t(variant.quantity_unit.name()).to_lowercase()], div![ delivery ], - action_section(product, model) + action_section(product, variant, model) ], ], div![ @@ -122,39 +133,47 @@ pub fn view(model: &crate::Model, page: &ProductPage) -> Node { div![ crate::shared::view::public_navbar::view(model, &page.products), - super::layout::view(model, content, Some(&page.products.categories)) + super::layout::view_with_categories(model, content, page.products.categories.iter()) ] } -fn action_section(product: &model::api::Product, model: &crate::Model) -> Node { - if product.available { +fn action_section( + _product: &DetailedProduct, + variant: &DetailedProductVariant, + model: &crate::Model, +) -> Node { + if variant.available { div![ - C!["flex py-4 space-x-4"], - div![ - C!["relative"], - label![ - C!["text-center left-0 pt-2 right-0 absolute block text-xs uppercase text-gray-400 tracking-wide font-semibold"], - attrs!["for" => "quantity"], - model.i18n.t("Qty") - ], - input![C![""], attrs!["id" => "quantity", "type" => "number"]] - ], - button![ - C!["px-6 py-3 text-sm text-white bg-indigo-500 rounded-lg outline-none hover:bg-indigo-600 ring-indigo-300"], - model.i18n.t("Add to Cart"), - ev("click", move |ev| { - ev.prevent_default(); - ev.stop_propagation(); - None as Option - }) - ] - ] + C!["flex py-4 space-x-4"], + div![ + C!["relative"], + label![ + C!["text-center left-0 pt-2 right-0 absolute block text-xs uppercase text-gray-400 tracking-wide font-semibold"], + attrs!["for" => "quantity"], + model.i18n.t("Qty") + ], + input![C![""], attrs!["id" => "quantity", "type" => "number"]] + ], + button![ + C!["px-6 py-3 text-sm text-white bg-indigo-500 rounded-lg outline-none hover:bg-indigo-600 ring-indigo-300"], + model.i18n.t("Add to Cart"), + ev("click", move |ev| { + ev.prevent_default(); + ev.stop_propagation(); + None as Option + }) + ] + ] } else { div![C!["text-sm text-gray-400 "], model.i18n.t("Out of stock")] } } -fn delivery_available(product: &model::api::Product, model: &crate::Model) -> Node { +fn delivery_available( + product: &DetailedProduct, + _variant: &DetailedProductVariant, + model: &crate::Model, +) -> Node { match product.deliver_days_flag.len() { 0 => return empty![], 7 => return div![model.i18n.t("Delivery all week")], diff --git a/crates/web/src/pages/public/shopping_cart.rs b/crates/web/src/pages/public/shopping_cart.rs index 1f49619..99c89e5 100644 --- a/crates/web/src/pages/public/shopping_cart.rs +++ b/crates/web/src/pages/public/shopping_cart.rs @@ -1,3 +1,4 @@ +use model::v2::{DetailedProduct, DetailedProductVariant}; use model::Quantity; use seed::prelude::*; use seed::*; @@ -63,7 +64,7 @@ pub fn view(model: &crate::Model, page: &ShoppingCartPage) -> Node { div![ crate::shared::view::public_navbar::view(model, &page.products), - super::layout::view(model, content, None) + super::layout::view_without_categories(model, content) ] } @@ -153,6 +154,7 @@ mod summary_left { } mod summary_right { + use model::v2::{DetailedProduct, DetailedProductVariant}; use rusty_money::Money; use seed::prelude::*; use seed::*; @@ -165,17 +167,13 @@ mod summary_right { .cart .items .values() - .filter_map(|item: &crate::shopping_cart::Item| { - page.products - .product_variants - .get(&item.product_variant_id) - .map(|product| (item, product)) - }) + .filter_map(|item: &crate::shopping_cart::Item| page.products.for_item(item)) .map( - |(item, product): ( + |(item, _product, variant): ( &crate::shopping_cart::Item, - &model::api::v2::ProductVariant, - )| { **(product.price * item.quantity) }, + &DetailedProduct, + &DetailedProductVariant, + )| { **(variant.price * item.quantity) }, ) .sum::() as i64; div![ @@ -390,16 +388,13 @@ fn products_body(model: &crate::Model, page: &ShoppingCartPage) -> Node Node Node { use rusty_money::Money; - let img = product + let img = variant .photos .first() .map(|photo| photo.url.as_str()) .unwrap_or_default(); - let product_variant_id = product.id; - let quantity_unit = product.quantity_unit; + let product_variant_id = variant.id; + let product_id = product.id; + let quantity_unit = variant.quantity_unit; let product_url = Urls::new(&model.url) .product() .add_path_part(product.id.to_string()); @@ -476,6 +473,7 @@ fn item_view( Some(crate::Msg::from(CartMsg::ModifyItem { product_variant_id, + product_id, quantity_unit, quantity, })) @@ -488,7 +486,7 @@ fn item_view( C!["hidden text-right md:table-cell"], span![ C!["text-sm lg:text-base font-medium"], - Money::from_minor(**product.price as i64, model.config.currency).to_string() + Money::from_minor(**variant.price as i64, model.config.currency).to_string() ] ], td![ @@ -496,7 +494,7 @@ fn item_view( span![ C!["text-sm lg:text-base font-medium"], Money::from_minor( - **(product.price * item.quantity) as i64, + **(variant.price * item.quantity) as i64, model.config.currency ) .to_string() diff --git a/crates/web/src/pages/public/sign_in.rs b/crates/web/src/pages/public/sign_in.rs index 5167e23..3653c9e 100644 --- a/crates/web/src/pages/public/sign_in.rs +++ b/crates/web/src/pages/public/sign_in.rs @@ -62,7 +62,7 @@ pub fn view(model: &crate::Model, page: &SignInPage) -> Node { ] .map_msg(Into::into); - div![super::layout::view(model, content, None)] + div![super::layout::view_without_categories(model, content)] } fn sign_in_form(model: &crate::Model, _page: &SignInPage) -> Node { diff --git a/crates/web/src/pages/public/sign_up.rs b/crates/web/src/pages/public/sign_up.rs index 2b2487b..0ad9f24 100644 --- a/crates/web/src/pages/public/sign_up.rs +++ b/crates/web/src/pages/public/sign_up.rs @@ -114,7 +114,7 @@ pub fn view(model: &crate::Model, page: &SignUpPage) -> Node { ] ] .map_msg(Into::into); - div![super::layout::view(model, content, None)] + div![super::layout::view_without_categories(model, content)] } fn sign_up_form(model: &crate::Model, _page: &SignUpPage) -> Node { diff --git a/crates/web/src/shared/view.rs b/crates/web/src/shared/view.rs index d920907..525c32c 100644 --- a/crates/web/src/shared/view.rs +++ b/crates/web/src/shared/view.rs @@ -108,36 +108,26 @@ pub mod public_navbar { } pub mod cart_dropdown { - use model::ProductId; + use model::v2::{DetailedProduct, DetailedProductVariant}; use seed::prelude::*; use seed::*; use crate::shopping_cart::Item; use crate::{Model, Msg}; - macro_rules! filter_products { - ($model: expr, $products: expr) => { - $model - .cart - .items - .values() - .filter_map(|item: &Item| filter_product(item, $products, item.product_id)) - }; - } - macro_rules! filter_product_variants { ($model: expr, $products: expr) => { $model .cart .items .values() - .filter_map(|item: &Item| filter_product(item, $products, item.product_variant_id)) + .filter_map(|item: &Item| $products.for_item(item)) }; } pub fn view(model: &Model, products: &crate::model::Products) -> Node { - let items = - filter_product_variants!(model, products).map(|(it, product)| item(model, it, product)); + let items = filter_product_variants!(model, products) + .map(|(it, product, variant)| item(model, it, product, variant)); div![ C![ "absolute w-full rounded-b border-t-0 z-10", @@ -151,15 +141,20 @@ pub mod cart_dropdown { ] } - fn item(model: &Model, item: &Item, product: &model::api::Product) -> Node { - let img = product + fn item( + model: &Model, + item: &Item, + product: &DetailedProduct, + variant: &DetailedProductVariant, + ) -> Node { + let img = variant .photos .first() - .map(|photo| photo.url.as_str()) + .map(|photo| photo.unique_name.as_str()) .unwrap_or_default(); let price = rusty_money::Money::from_minor( - **(product.price * item.quantity) as i64, + **(variant.price * item.quantity) as i64, model.config.currency, ) .to_string(); @@ -169,7 +164,7 @@ pub mod cart_dropdown { div![ C!["flex-auto text-sm w-32"], div![C!["font-bold"], product.name.as_str()], - div![C!["truncate"], product.short_description.as_str()], + div![C!["truncate"], variant.short_description.as_str()], div![ C!["text-gray-400"], model.i18n.t("Qty:"), @@ -213,7 +208,11 @@ pub mod cart_dropdown { fn checkout(model: &Model, products: &crate::model::Products) -> Node { let sum: i32 = filter_product_variants!(model, products) - .map(|(item, product): (&Item, &model::api::Product)| **item.quantity * **product.price) + .map( + |(item, _product, variant): (&Item, &DetailedProduct, &DetailedProductVariant)| { + **item.quantity * **variant.price + }, + ) .sum(); let sum = rusty_money::Money::from_minor(sum as i64, model.config.currency); @@ -230,15 +229,4 @@ pub mod cart_dropdown { ] ] } - - fn filter_product<'item, 'product>( - item: &'item Item, - products: &'product crate::model::Products, - product_id: ProductId, - ) -> Option<(&'item Item, &'product model::api::Product)> { - products - .products - .get(&product_id) - .map(|product| (item, product)) - } } diff --git a/crates/web/src/shopping_cart.rs b/crates/web/src/shopping_cart.rs index f89a867..98907b5 100644 --- a/crates/web/src/shopping_cart.rs +++ b/crates/web/src/shopping_cart.rs @@ -12,11 +12,13 @@ pub enum CartMsg { quantity: Quantity, quantity_unit: QuantityUnit, product_variant_id: ProductVariantId, + product_id: ProductId, }, ModifyItem { quantity: Quantity, quantity_unit: QuantityUnit, product_variant_id: ProductVariantId, + product_id: ProductId, }, Remove(ProductVariantId), Hover, @@ -34,12 +36,14 @@ impl From for Msg { } } -pub type Items = indexmap::IndexMap; +pub type Items = indexmap::IndexMap; #[derive(Debug, Copy, Clone, Serialize, Deserialize)] pub struct Item { - #[serde(rename = "i")] + #[serde(rename = "v")] pub product_variant_id: ProductVariantId, + #[serde(rename = "p")] + pub product_id: ProductId, #[serde(rename = "q")] pub quantity: Quantity, #[serde(rename = "u")] @@ -70,6 +74,7 @@ pub fn update(msg: CartMsg, model: &mut Model, orders: &mut impl Orders) { quantity, quantity_unit, product_variant_id, + product_id, } => { { let items: &mut Items = &mut model.cart.items; @@ -77,6 +82,7 @@ pub fn update(msg: CartMsg, model: &mut Model, orders: &mut impl Orders) { quantity: Quantity::from_u32(0), quantity_unit, product_variant_id, + product_id, }); entry.quantity = entry.quantity + quantity; entry.quantity_unit = quantity_unit; @@ -85,6 +91,7 @@ pub fn update(msg: CartMsg, model: &mut Model, orders: &mut impl Orders) { sync_cart(model, orders); } CartMsg::ModifyItem { + product_id, product_variant_id, quantity_unit, quantity, @@ -97,6 +104,7 @@ pub fn update(msg: CartMsg, model: &mut Model, orders: &mut impl Orders) { quantity, quantity_unit, product_variant_id, + product_id, }); entry.quantity = quantity; entry.quantity_unit = quantity_unit; @@ -127,6 +135,7 @@ pub fn update(msg: CartMsg, model: &mut Model, orders: &mut impl Orders) { model::api::ShoppingCartItem { id: _, product_variant_id, + product_id, shopping_cart_id: _, quantity, quantity_unit, @@ -135,6 +144,7 @@ pub fn update(msg: CartMsg, model: &mut Model, orders: &mut impl Orders) { product_variant_id, Item { product_variant_id, + product_id, quantity, quantity_unit, },