Fetch stocks
This commit is contained in:
parent
ef903c6f31
commit
1e941a4122
@ -1,6 +1,7 @@
|
|||||||
use actix::{Actor, Context};
|
use actix::{Actor, Context};
|
||||||
use config::SharedAppConfig;
|
use config::SharedAppConfig;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
use sqlx_core::arguments::Arguments;
|
||||||
|
|
||||||
pub use crate::account_orders::*;
|
pub use crate::account_orders::*;
|
||||||
pub use crate::accounts::*;
|
pub use crate::accounts::*;
|
||||||
@ -156,3 +157,97 @@ impl Database {
|
|||||||
impl Actor for Database {
|
impl Actor for Database {
|
||||||
type Context = Context<Self>;
|
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)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
@ -88,44 +88,23 @@ pub(crate) async fn photos_for_products(
|
|||||||
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||||
) -> Result<Vec<model::ProductLinkedPhoto>> {
|
) -> Result<Vec<model::ProductLinkedPhoto>> {
|
||||||
log::debug!("all product ids {:?}", msg.product_ids);
|
log::debug!("all product ids {:?}", msg.product_ids);
|
||||||
let mut res: Vec<model::ProductLinkedPhoto> = Vec::with_capacity(100);
|
let res: Vec<model::ProductLinkedPhoto> = MultiLoad::new(
|
||||||
|
pool,
|
||||||
let len = msg.product_ids.len() / 20;
|
r#"
|
||||||
for ids in msg.product_ids.into_iter().fold(
|
SELECT photos.id, photos.local_path, photos.file_name,
|
||||||
Vec::<Vec<model::ProductId>>::with_capacity(len),
|
product_photos.product_id, photos.unique_name FROM photos
|
||||||
|mut v, id| {
|
INNER JOIN product_photos
|
||||||
if matches!(v.last().map(|v| v.len()), Some(20) | None) {
|
ON photos.id = product_photos.photo_id
|
||||||
v.push(Vec::with_capacity(20));
|
WHERE
|
||||||
}
|
"#,
|
||||||
v.last_mut().unwrap().push(id);
|
" product_photos.product_id =",
|
||||||
v
|
)
|
||||||
},
|
.load(
|
||||||
) {
|
msg.product_ids.len(),
|
||||||
log::debug!("scoped product ids {:?}", ids);
|
msg.product_ids.into_iter().map(|id| *id),
|
||||||
let query: String = r#"
|
|_e| crate::Error::Photo(Error::All),
|
||||||
SELECT photos.id, photos.local_path, photos.file_name, product_photos.product_id, photos.unique_name
|
)
|
||||||
FROM photos
|
.await?;
|
||||||
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);
|
|
||||||
}
|
|
||||||
log::debug!("product linked photos {:?}", res);
|
log::debug!("product linked photos {:?}", res);
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ use actix::Message;
|
|||||||
use model::{ProductId, Quantity, QuantityUnit, Stock, StockId};
|
use model::{ProductId, Quantity, QuantityUnit, Stock, StockId};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
|
||||||
use crate::Result;
|
use crate::{MultiLoad, Result};
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
@ -14,6 +14,8 @@ pub enum Error {
|
|||||||
Update,
|
Update,
|
||||||
#[error("Unable to delete stock")]
|
#[error("Unable to delete stock")]
|
||||||
Delete,
|
Delete,
|
||||||
|
#[error("Unable find stock for product")]
|
||||||
|
ProductStock,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Message)]
|
#[derive(Message)]
|
||||||
@ -124,3 +126,37 @@ RETURNING id, product_id, quantity, quantity_unit
|
|||||||
crate::Error::Stock(Error::Delete)
|
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 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!(
|
let photos = admin_send_db!(
|
||||||
db,
|
db,
|
||||||
database_manager::PhotosForProducts {
|
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)]
|
#[derive(Deserialize)]
|
||||||
|
@ -23,14 +23,20 @@ async fn products(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let products: Vec<model::Product> = public_send_db!(owned, db, database_manager::AllProducts);
|
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!(
|
let photos: Vec<model::ProductLinkedPhoto> = public_send_db!(
|
||||||
owned,
|
owned,
|
||||||
db,
|
db,
|
||||||
database_manager::PhotosForProducts {
|
database_manager::PhotosForProducts { product_ids }
|
||||||
product_ids: products.iter().map(|p| p.id).collect()
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
Ok(Json((products, photos, public_path).into()))
|
Ok(Json((products, photos, products_stock, public_path).into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/product/{id}")]
|
#[get("/product/{id}")]
|
||||||
@ -48,6 +54,13 @@ async fn product(
|
|||||||
|
|
||||||
let product: model::Product =
|
let product: model::Product =
|
||||||
public_send_db!(owned, db, database_manager::FindProduct { product_id });
|
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!(
|
let mut photos: Vec<model::ProductLinkedPhoto> = public_send_db!(
|
||||||
owned,
|
owned,
|
||||||
db,
|
db,
|
||||||
@ -55,7 +68,15 @@ async fn product(
|
|||||||
product_ids: vec![product.id]
|
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")]
|
#[get("/stocks")]
|
||||||
|
@ -189,11 +189,20 @@ pub struct Product {
|
|||||||
pub long_description: crate::ProductLongDesc,
|
pub long_description: crate::ProductLongDesc,
|
||||||
pub category: Option<Category>,
|
pub category: Option<Category>,
|
||||||
pub price: crate::Price,
|
pub price: crate::Price,
|
||||||
|
pub available: bool,
|
||||||
|
pub quantity_unit: crate::QuantityUnit,
|
||||||
pub deliver_days_flag: crate::Days,
|
pub deliver_days_flag: crate::Days,
|
||||||
pub photos: Vec<Photo>,
|
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(
|
fn from(
|
||||||
(
|
(
|
||||||
crate::Product {
|
crate::Product {
|
||||||
@ -206,9 +215,22 @@ impl<'path> From<(crate::Product, &mut Vec<ProductLinkedPhoto>, &'path str)> for
|
|||||||
deliver_days_flag,
|
deliver_days_flag,
|
||||||
},
|
},
|
||||||
photos,
|
photos,
|
||||||
|
product_stocks,
|
||||||
public_path,
|
public_path,
|
||||||
): (crate::Product, &mut Vec<ProductLinkedPhoto>, &'path str),
|
): (
|
||||||
|
crate::Product,
|
||||||
|
&mut Vec<ProductLinkedPhoto>,
|
||||||
|
&mut Vec<crate::Stock>,
|
||||||
|
&'path str,
|
||||||
|
),
|
||||||
) -> Self {
|
) -> 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 {
|
Self {
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
@ -224,6 +246,8 @@ impl<'path> From<(crate::Product, &mut Vec<ProductLinkedPhoto>, &'path str)> for
|
|||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
price,
|
price,
|
||||||
|
available,
|
||||||
|
quantity_unit,
|
||||||
deliver_days_flag,
|
deliver_days_flag,
|
||||||
photos: photos
|
photos: photos
|
||||||
.drain_filter(|photo| photo.product_id == id)
|
.drain_filter(|photo| photo.product_id == id)
|
||||||
@ -251,14 +275,26 @@ impl<'path> From<(crate::Product, &mut Vec<ProductLinkedPhoto>, &'path str)> for
|
|||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
pub struct Products(pub Vec<Product>);
|
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(
|
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 {
|
||||||
Self(
|
Self(
|
||||||
products
|
products
|
||||||
.into_iter()
|
.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(),
|
.collect(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -184,6 +184,26 @@ pub enum QuantityUnit {
|
|||||||
Piece,
|
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 = "dummy", derive(fake::Dummy))]
|
||||||
#[cfg_attr(feature = "db", derive(sqlx::Type))]
|
#[cfg_attr(feature = "db", derive(sqlx::Type))]
|
||||||
#[cfg_attr(feature = "db", sqlx(rename_all = "snake_case"))]
|
#[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"],
|
C!["mb-2 leading-tight tracking-tight font-bold text-gray-800 text-2xl md:text-3xl"],
|
||||||
product.name.as_str()
|
product.name.as_str()
|
||||||
],
|
],
|
||||||
|
div![model.i18n.t("Price per"), ": ", model.i18n.t(product.quantity_unit.short_name())],
|
||||||
div![
|
div![
|
||||||
delivery
|
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> {
|
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
|
let days = product
|
||||||
.deliver_days_flag
|
.deliver_days_flag
|
||||||
.iter()
|
.iter()
|
||||||
@ -143,7 +149,7 @@ fn delivery_available(product: &model::api::Product, model: &crate::Model) -> No
|
|||||||
model.i18n.t(day.short_name())
|
model.i18n.t(day.short_name())
|
||||||
]);
|
]);
|
||||||
div![
|
div![
|
||||||
div![C![""], model.i18n.t("Delivery every")],
|
div![model.i18n.t("Delivery every")],
|
||||||
div![C!["flex py-4 space-x-4"], days]
|
div![C!["flex py-4 space-x-4"], days]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user