Fetch stocks
This commit is contained in:
parent
ef903c6f31
commit
1e941a4122
@ -1,6 +1,7 @@
|
||||
use actix::{Actor, Context};
|
||||
use config::SharedAppConfig;
|
||||
use sqlx::PgPool;
|
||||
use sqlx_core::arguments::Arguments;
|
||||
|
||||
pub use crate::account_orders::*;
|
||||
pub use crate::accounts::*;
|
||||
@ -156,3 +157,97 @@ impl Database {
|
||||
impl Actor for Database {
|
||||
type Context = Context<Self>;
|
||||
}
|
||||
|
||||
/// Multi-query load for large amount of records to read
|
||||
///
|
||||
/// Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use database_manager::photos::Error;
|
||||
/// async fn load() {
|
||||
/// # let pool: sqlx::PgPool::connect("").await.unwrap();
|
||||
/// use database_manager::MultiLoad;
|
||||
/// let t = pool.begin().await.unwrap();
|
||||
/// let mut multi = MultiLoad::new(
|
||||
/// &mut t,
|
||||
/// "SELECT id, name FROM products WHERE ",
|
||||
/// " id = "
|
||||
/// );
|
||||
/// multi.load(4, vec![1,2,3,4], |_| Error::All.into());
|
||||
/// t.commit().await.unwrap();
|
||||
/// }
|
||||
/// ```
|
||||
pub struct MultiLoad<'transaction, 'transaction2, 'header, 'condition, T> {
|
||||
pool: &'transaction mut sqlx::Transaction<'transaction2, sqlx::Postgres>,
|
||||
header: &'header str,
|
||||
condition: &'condition str,
|
||||
__phantom: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<'transaction, 'transaction2, 'header, 'condition, T>
|
||||
MultiLoad<'transaction, 'transaction2, 'header, 'condition, T>
|
||||
where
|
||||
T: for<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow> + Send + Unpin,
|
||||
{
|
||||
pub fn new(
|
||||
pool: &'transaction mut sqlx::Transaction<'transaction2, sqlx::Postgres>,
|
||||
header: &'header str,
|
||||
condition: &'condition str,
|
||||
) -> Self {
|
||||
Self {
|
||||
pool,
|
||||
header,
|
||||
condition,
|
||||
__phantom: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn load<'query, Error, Ids>(
|
||||
&mut self,
|
||||
len: usize,
|
||||
items: Ids,
|
||||
on_error: Error,
|
||||
) -> std::result::Result<Vec<T>, crate::Error>
|
||||
where
|
||||
Ids: Iterator<Item = model::RecordId>,
|
||||
Error: Fn(sqlx::Error) -> crate::Error,
|
||||
{
|
||||
let mut res = Vec::new();
|
||||
|
||||
for ids in items.fold(
|
||||
Vec::<Vec<model::RecordId>>::with_capacity(len),
|
||||
|mut v, id| {
|
||||
if matches!(v.last().map(|v| v.len()), Some(20) | None) {
|
||||
v.push(Vec::with_capacity(20));
|
||||
}
|
||||
v.last_mut().unwrap().push(id);
|
||||
v
|
||||
},
|
||||
) {
|
||||
let query: String = self.header.into();
|
||||
let query = ids.iter().enumerate().fold(query, |mut q, (idx, _id)| {
|
||||
if idx != 0 {
|
||||
q.push_str(" OR");
|
||||
}
|
||||
q.push_str(&format!(" {} ${}", self.condition, idx + 1));
|
||||
q
|
||||
});
|
||||
let q = sqlx::query_as_with(
|
||||
query.as_str(),
|
||||
ids.into_iter()
|
||||
.fold(sqlx::postgres::PgArguments::default(), |mut args, id| {
|
||||
args.add(id);
|
||||
args
|
||||
}),
|
||||
);
|
||||
|
||||
let records: Vec<T> = match q.fetch_all(&mut *self.pool).await {
|
||||
Ok(rec) => rec,
|
||||
Err(e) => return Err(on_error(e)),
|
||||
};
|
||||
res.extend(records);
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use crate::Result;
|
||||
use crate::{MultiLoad, Result};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
@ -88,44 +88,23 @@ pub(crate) async fn photos_for_products(
|
||||
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
) -> Result<Vec<model::ProductLinkedPhoto>> {
|
||||
log::debug!("all product ids {:?}", msg.product_ids);
|
||||
let mut res: Vec<model::ProductLinkedPhoto> = Vec::with_capacity(100);
|
||||
|
||||
let len = msg.product_ids.len() / 20;
|
||||
for ids in msg.product_ids.into_iter().fold(
|
||||
Vec::<Vec<model::ProductId>>::with_capacity(len),
|
||||
|mut v, id| {
|
||||
if matches!(v.last().map(|v| v.len()), Some(20) | None) {
|
||||
v.push(Vec::with_capacity(20));
|
||||
}
|
||||
v.last_mut().unwrap().push(id);
|
||||
v
|
||||
},
|
||||
) {
|
||||
log::debug!("scoped product ids {:?}", ids);
|
||||
let query: String = r#"
|
||||
SELECT photos.id, photos.local_path, photos.file_name, product_photos.product_id, photos.unique_name
|
||||
FROM photos
|
||||
let res: Vec<model::ProductLinkedPhoto> = MultiLoad::new(
|
||||
pool,
|
||||
r#"
|
||||
SELECT photos.id, photos.local_path, photos.file_name,
|
||||
product_photos.product_id, photos.unique_name FROM photos
|
||||
INNER JOIN product_photos
|
||||
ON photos.id = product_photos.photo_id
|
||||
WHERE
|
||||
"#
|
||||
.into();
|
||||
let query = ids.iter().enumerate().fold(query, |mut q, (idx, _id)| {
|
||||
if idx != 0 {
|
||||
q.push_str(" OR");
|
||||
}
|
||||
q.push_str(&format!(" product_photos.product_id = ${}", idx + 1));
|
||||
q
|
||||
});
|
||||
let q = sqlx::query_as::<_, model::ProductLinkedPhoto>(query.as_str());
|
||||
let q = ids.into_iter().map(|id| *id).fold(q, |q, id| q.bind(id));
|
||||
|
||||
let records = q.fetch_all(&mut *pool).await.map_err(|e| {
|
||||
log::error!("{e:?}");
|
||||
crate::Error::Photo(Error::All)
|
||||
})?;
|
||||
res.extend(records);
|
||||
}
|
||||
"#,
|
||||
" product_photos.product_id =",
|
||||
)
|
||||
.load(
|
||||
msg.product_ids.len(),
|
||||
msg.product_ids.into_iter().map(|id| *id),
|
||||
|_e| crate::Error::Photo(Error::All),
|
||||
)
|
||||
.await?;
|
||||
log::debug!("product linked photos {:?}", res);
|
||||
Ok(res)
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ use actix::Message;
|
||||
use model::{ProductId, Quantity, QuantityUnit, Stock, StockId};
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::Result;
|
||||
use crate::{MultiLoad, Result};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
@ -14,6 +14,8 @@ pub enum Error {
|
||||
Update,
|
||||
#[error("Unable to delete stock")]
|
||||
Delete,
|
||||
#[error("Unable find stock for product")]
|
||||
ProductStock,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
@ -124,3 +126,37 @@ RETURNING id, product_id, quantity, quantity_unit
|
||||
crate::Error::Stock(Error::Delete)
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "Result<Vec<model::Stock>>")]
|
||||
pub struct ProductsStock {
|
||||
pub product_ids: Vec<ProductId>,
|
||||
}
|
||||
|
||||
crate::db_async_handler!(
|
||||
ProductsStock,
|
||||
product_stock,
|
||||
Vec<model::Stock>,
|
||||
inner_product_stock
|
||||
);
|
||||
|
||||
async fn product_stock(
|
||||
msg: ProductsStock,
|
||||
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
) -> Result<Vec<model::Stock>> {
|
||||
Ok(MultiLoad::new(
|
||||
pool,
|
||||
r#"
|
||||
SELECT id, product_id, quantity, quantity_unit
|
||||
FROM stocks
|
||||
WHERE
|
||||
"#,
|
||||
" product_id =",
|
||||
)
|
||||
.load(
|
||||
msg.product_ids.len(),
|
||||
msg.product_ids.into_iter().map(|id| *id),
|
||||
|_e| crate::Error::Stock(Error::ProductStock),
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
|
@ -30,14 +30,16 @@ async fn products(
|
||||
|
||||
let db = db.into_inner();
|
||||
|
||||
let products = admin_send_db!(db, database_manager::AllProducts);
|
||||
let products: Vec<model::Product> = admin_send_db!(db, database_manager::AllProducts);
|
||||
let product_ids: Vec<model::ProductId> = products.iter().map(|p| p.id).collect();
|
||||
let photos = admin_send_db!(
|
||||
db,
|
||||
database_manager::PhotosForProducts {
|
||||
product_ids: products.iter().map(|p| p.id).collect()
|
||||
product_ids: product_ids.clone()
|
||||
}
|
||||
);
|
||||
Ok(Json((products, photos, public_path).into()))
|
||||
let products_stock = admin_send_db!(db, database_manager::ProductsStock { product_ids });
|
||||
Ok(Json((products, photos, products_stock, public_path).into()))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
@ -23,14 +23,20 @@ async fn products(
|
||||
};
|
||||
|
||||
let products: Vec<model::Product> = public_send_db!(owned, db, database_manager::AllProducts);
|
||||
let product_ids: Vec<model::ProductId> = products.iter().map(|p| p.id).collect();
|
||||
let products_stock: Vec<model::Stock> = public_send_db!(
|
||||
owned,
|
||||
db,
|
||||
database_manager::ProductsStock {
|
||||
product_ids: product_ids.clone()
|
||||
}
|
||||
);
|
||||
let photos: Vec<model::ProductLinkedPhoto> = public_send_db!(
|
||||
owned,
|
||||
db,
|
||||
database_manager::PhotosForProducts {
|
||||
product_ids: products.iter().map(|p| p.id).collect()
|
||||
}
|
||||
database_manager::PhotosForProducts { product_ids }
|
||||
);
|
||||
Ok(Json((products, photos, public_path).into()))
|
||||
Ok(Json((products, photos, products_stock, public_path).into()))
|
||||
}
|
||||
|
||||
#[get("/product/{id}")]
|
||||
@ -48,6 +54,13 @@ async fn product(
|
||||
|
||||
let product: model::Product =
|
||||
public_send_db!(owned, db, database_manager::FindProduct { product_id });
|
||||
let mut products_stock = public_send_db!(
|
||||
owned,
|
||||
db,
|
||||
database_manager::ProductsStock {
|
||||
product_ids: vec![product.id]
|
||||
}
|
||||
);
|
||||
let mut photos: Vec<model::ProductLinkedPhoto> = public_send_db!(
|
||||
owned,
|
||||
db,
|
||||
@ -55,7 +68,15 @@ async fn product(
|
||||
product_ids: vec![product.id]
|
||||
}
|
||||
);
|
||||
Ok(Json((product, &mut photos, public_path.as_str()).into()))
|
||||
Ok(Json(
|
||||
(
|
||||
product,
|
||||
&mut photos,
|
||||
&mut products_stock,
|
||||
public_path.as_str(),
|
||||
)
|
||||
.into(),
|
||||
))
|
||||
}
|
||||
|
||||
#[get("/stocks")]
|
||||
|
@ -189,11 +189,20 @@ pub struct Product {
|
||||
pub long_description: crate::ProductLongDesc,
|
||||
pub category: Option<Category>,
|
||||
pub price: crate::Price,
|
||||
pub available: bool,
|
||||
pub quantity_unit: crate::QuantityUnit,
|
||||
pub deliver_days_flag: crate::Days,
|
||||
pub photos: Vec<Photo>,
|
||||
}
|
||||
|
||||
impl<'path> From<(crate::Product, &mut Vec<ProductLinkedPhoto>, &'path str)> for Product {
|
||||
impl<'path>
|
||||
From<(
|
||||
crate::Product,
|
||||
&mut Vec<ProductLinkedPhoto>,
|
||||
&mut Vec<crate::Stock>,
|
||||
&'path str,
|
||||
)> for Product
|
||||
{
|
||||
fn from(
|
||||
(
|
||||
crate::Product {
|
||||
@ -206,9 +215,22 @@ impl<'path> From<(crate::Product, &mut Vec<ProductLinkedPhoto>, &'path str)> for
|
||||
deliver_days_flag,
|
||||
},
|
||||
photos,
|
||||
product_stocks,
|
||||
public_path,
|
||||
): (crate::Product, &mut Vec<ProductLinkedPhoto>, &'path str),
|
||||
): (
|
||||
crate::Product,
|
||||
&mut Vec<ProductLinkedPhoto>,
|
||||
&mut Vec<crate::Stock>,
|
||||
&'path str,
|
||||
),
|
||||
) -> Self {
|
||||
let pos = product_stocks
|
||||
.iter()
|
||||
.position(|stock| stock.product_id == id);
|
||||
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));
|
||||
Self {
|
||||
id,
|
||||
name,
|
||||
@ -224,6 +246,8 @@ impl<'path> From<(crate::Product, &mut Vec<ProductLinkedPhoto>, &'path str)> for
|
||||
})
|
||||
}),
|
||||
price,
|
||||
available,
|
||||
quantity_unit,
|
||||
deliver_days_flag,
|
||||
photos: photos
|
||||
.drain_filter(|photo| photo.product_id == id)
|
||||
@ -251,14 +275,26 @@ impl<'path> From<(crate::Product, &mut Vec<ProductLinkedPhoto>, &'path str)> for
|
||||
#[serde(transparent)]
|
||||
pub struct Products(pub Vec<Product>);
|
||||
|
||||
impl From<(Vec<crate::Product>, Vec<ProductLinkedPhoto>, String)> for Products {
|
||||
impl
|
||||
From<(
|
||||
Vec<crate::Product>,
|
||||
Vec<ProductLinkedPhoto>,
|
||||
Vec<crate::Stock>,
|
||||
String,
|
||||
)> for Products
|
||||
{
|
||||
fn from(
|
||||
(products, mut photos, public_path): (Vec<crate::Product>, Vec<ProductLinkedPhoto>, String),
|
||||
(products, mut photos, mut products_stock, public_path): (
|
||||
Vec<crate::Product>,
|
||||
Vec<ProductLinkedPhoto>,
|
||||
Vec<crate::Stock>,
|
||||
String,
|
||||
),
|
||||
) -> Self {
|
||||
Self(
|
||||
products
|
||||
.into_iter()
|
||||
.map(|p| (p, &mut photos, public_path.as_str()).into())
|
||||
.map(|p| (p, &mut photos, &mut products_stock, public_path.as_str()).into())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
@ -184,6 +184,26 @@ pub enum QuantityUnit {
|
||||
Piece,
|
||||
}
|
||||
|
||||
impl QuantityUnit {
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Gram => "Gram",
|
||||
Self::Decagram => "Decagram",
|
||||
Self::Kilogram => "Kilogram",
|
||||
Self::Piece => "Piece",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn short_name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Gram => "g",
|
||||
Self::Decagram => "dkg",
|
||||
Self::Kilogram => "kg",
|
||||
Self::Piece => "piece",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
|
||||
#[cfg_attr(feature = "db", derive(sqlx::Type))]
|
||||
#[cfg_attr(feature = "db", sqlx(rename_all = "snake_case"))]
|
||||
|
@ -95,6 +95,7 @@ pub fn view(model: &crate::Model, page: &ProductPage) -> Node<crate::Msg> {
|
||||
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.short_name())],
|
||||
div![
|
||||
delivery
|
||||
],
|
||||
@ -135,6 +136,11 @@ pub fn view(model: &crate::Model, page: &ProductPage) -> Node<crate::Msg> {
|
||||
}
|
||||
|
||||
fn delivery_available(product: &model::api::Product, model: &crate::Model) -> Node<Msg> {
|
||||
match product.deliver_days_flag.len() {
|
||||
0 => return empty![],
|
||||
7 => return div![model.i18n.t("Delivery all week")],
|
||||
_ => {}
|
||||
};
|
||||
let days = product
|
||||
.deliver_days_flag
|
||||
.iter()
|
||||
@ -143,7 +149,7 @@ fn delivery_available(product: &model::api::Product, model: &crate::Model) -> No
|
||||
model.i18n.t(day.short_name())
|
||||
]);
|
||||
div![
|
||||
div![C![""], model.i18n.t("Delivery every")],
|
||||
div![model.i18n.t("Delivery every")],
|
||||
div![C!["flex py-4 space-x-4"], days]
|
||||
]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user