now products have different options, like different parameters
This commit is contained in:
@@ -21,7 +21,7 @@ use crate::{
|
||||
controllers::i18n::current_lang,
|
||||
models::{
|
||||
account_discount_profiles, account_product_prices, account_product_resolutions,
|
||||
categories, discount_profiles, products, _entities::users,
|
||||
categories, discount_profiles, product_variants, products, _entities::users,
|
||||
},
|
||||
shared::{
|
||||
guard,
|
||||
@@ -142,16 +142,9 @@ async fn show(
|
||||
.order_by_asc(products::Column::Name)
|
||||
.all(&ctx.db)
|
||||
.await?;
|
||||
// Two prices per product:
|
||||
// - the generic business price a freshly-registered company sees (business
|
||||
// baseline + business-audience profiles, no per-company deals), and
|
||||
// - this company's effective price (its negotiated price + assigned profiles).
|
||||
// The effective price is highlighted only when it differs from the generic one.
|
||||
let business = pricing::audience_price_many(&ctx, &list, BUSINESS_AUDIENCE).await?;
|
||||
let details = pricing::detail_many(&ctx, &list, Some(&company)).await?;
|
||||
|
||||
// Category sidebar tree (counts over the full, unfiltered list) plus the
|
||||
// active `?category=` filter applied to the rows.
|
||||
// Category sidebar tree (counts over the full, unfiltered product list) plus
|
||||
// the active `?category=` filter applied to the rows.
|
||||
let category_ids: Vec<Option<i32>> = list.iter().map(|p| p.category_id).collect();
|
||||
let category_groups = view::admin_category_groups(&all_categories, &category_ids);
|
||||
let selected_category = params
|
||||
@@ -161,15 +154,43 @@ async fn show(
|
||||
.to_string();
|
||||
let filter = view::category_filter_ids(&all_categories, &selected_category);
|
||||
|
||||
let rows: Vec<serde_json::Value> = list
|
||||
// Pricing is per variant. Flatten the (filtered) products into their variants
|
||||
// in product-name then variant-position order, carrying each variant's
|
||||
// product for the row's display name.
|
||||
let product_ids: Vec<i32> = list.iter().map(|p| p.id).collect();
|
||||
let grouped = product_variants::Entity::grouped_for_products(&ctx.db, &product_ids).await?;
|
||||
let mut variant_rows: Vec<(&products::Model, product_variants::Model)> = Vec::new();
|
||||
for product in &list {
|
||||
if !view::category_filter_keep(&filter, product.category_id) {
|
||||
continue;
|
||||
}
|
||||
if let Some(variants) = grouped.get(&product.id) {
|
||||
for variant in variants {
|
||||
variant_rows.push((product, variant.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Two prices per variant:
|
||||
// - the generic business price a freshly-registered company sees (business
|
||||
// baseline + business-audience profiles, no per-company deals), and
|
||||
// - this company's effective price (its negotiated price + assigned profiles).
|
||||
// The effective price is highlighted only when it differs from the generic one.
|
||||
let variants_only: Vec<product_variants::Model> =
|
||||
variant_rows.iter().map(|(_, v)| v.clone()).collect();
|
||||
let business = pricing::audience_price_variants(&ctx, &variants_only, BUSINESS_AUDIENCE).await?;
|
||||
let details = pricing::detail_variants(&ctx, &variants_only, Some(&company)).await?;
|
||||
|
||||
let rows: Vec<serde_json::Value> = variant_rows
|
||||
.iter()
|
||||
.zip(business.iter())
|
||||
.zip(details.iter())
|
||||
.filter(|((product, _), _)| view::category_filter_keep(&filter, product.category_id))
|
||||
.map(|((product, b), d)| {
|
||||
.map(|(((product, variant), b), d)| {
|
||||
json!({
|
||||
"product_id": product.id,
|
||||
"variant_id": variant.id,
|
||||
"name": product.name,
|
||||
"variant_label": variant.label,
|
||||
"currency": product.currency,
|
||||
"regular_price": format_price(d.regular_cents),
|
||||
"business_price": format_price(b.price_cents),
|
||||
@@ -207,22 +228,27 @@ async fn price_edit(
|
||||
auth: auth::JWT,
|
||||
jar: CookieJar,
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
Path((id, product_id)): Path<(i32, i32)>,
|
||||
Path((id, variant_id)): Path<(i32, i32)>,
|
||||
Query(params): Query<HashMap<String, String>>,
|
||||
State(ctx): State<AppContext>,
|
||||
) -> Result<Response> {
|
||||
guard::current_admin(auth, &ctx).await?;
|
||||
let company = company_by_id(&ctx, id).await?;
|
||||
let product = products::Entity::find_by_id(product_id)
|
||||
let variant = product_variants::Entity::find_by_id(variant_id)
|
||||
.one(&ctx.db)
|
||||
.await?
|
||||
.ok_or_else(|| Error::NotFound)?;
|
||||
let product = products::Entity::find_by_id(variant.product_id)
|
||||
.one(&ctx.db)
|
||||
.await?
|
||||
.ok_or_else(|| Error::NotFound)?;
|
||||
|
||||
let business =
|
||||
pricing::audience_price_many(&ctx, std::slice::from_ref(&product), BUSINESS_AUDIENCE)
|
||||
pricing::audience_price_variants(&ctx, std::slice::from_ref(&variant), BUSINESS_AUDIENCE)
|
||||
.await?;
|
||||
let business_cents = business[0].price_cents;
|
||||
let detail = pricing::detail_many(&ctx, std::slice::from_ref(&product), Some(&company)).await?;
|
||||
let detail =
|
||||
pricing::detail_variants(&ctx, std::slice::from_ref(&variant), Some(&company)).await?;
|
||||
let d = &detail[0];
|
||||
|
||||
// Names for the covering profiles, used by the collision resolution selector.
|
||||
@@ -248,7 +274,9 @@ async fn price_edit(
|
||||
"customer": { "id": company.id, "name": company.name },
|
||||
"product": {
|
||||
"id": product.id,
|
||||
"variant_id": variant.id,
|
||||
"name": product.name,
|
||||
"variant_label": variant.label,
|
||||
"currency": product.currency,
|
||||
"regular_price": format_price(d.regular_cents),
|
||||
"regular_cents": d.regular_cents,
|
||||
@@ -271,7 +299,7 @@ async fn price_edit(
|
||||
#[debug_handler]
|
||||
async fn set_price(
|
||||
auth: auth::JWT,
|
||||
Path((id, product_id)): Path<(i32, i32)>,
|
||||
Path((id, variant_id)): Path<(i32, i32)>,
|
||||
State(ctx): State<AppContext>,
|
||||
Form(form): Form<PriceForm>,
|
||||
) -> Result<Response> {
|
||||
@@ -280,7 +308,7 @@ async fn set_price(
|
||||
|
||||
let entered = form.price.trim().to_string();
|
||||
if entered.is_empty() {
|
||||
account_product_prices::Model::clear(&ctx.db, company.id, product_id).await?;
|
||||
account_product_prices::Model::clear(&ctx.db, company.id, variant_id).await?;
|
||||
return format::redirect(&format!("/admin/customers/{id}"));
|
||||
}
|
||||
|
||||
@@ -288,23 +316,23 @@ async fn set_price(
|
||||
Ok(cents) if cents > 0 => cents,
|
||||
_ => {
|
||||
return format::redirect(&format!(
|
||||
"/admin/customers/{id}/prices/{product_id}/edit?error=discount-must-be-positive"
|
||||
"/admin/customers/{id}/prices/{variant_id}/edit?error=discount-must-be-positive"
|
||||
))
|
||||
}
|
||||
};
|
||||
account_product_prices::Model::upsert(&ctx.db, company.id, product_id, cents).await?;
|
||||
account_product_prices::Model::upsert(&ctx.db, company.id, variant_id, cents).await?;
|
||||
format::redirect(&format!("/admin/customers/{id}"))
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn remove_price(
|
||||
auth: auth::JWT,
|
||||
Path((id, product_id)): Path<(i32, i32)>,
|
||||
Path((id, variant_id)): Path<(i32, i32)>,
|
||||
State(ctx): State<AppContext>,
|
||||
) -> Result<Response> {
|
||||
guard::current_admin(auth, &ctx).await?;
|
||||
let company = company_by_id(&ctx, id).await?;
|
||||
account_product_prices::Model::clear(&ctx.db, company.id, product_id).await?;
|
||||
account_product_prices::Model::clear(&ctx.db, company.id, variant_id).await?;
|
||||
format::redirect(&format!("/admin/customers/{id}"))
|
||||
}
|
||||
|
||||
@@ -347,7 +375,7 @@ async fn sync_profiles(
|
||||
#[debug_handler]
|
||||
async fn set_resolution(
|
||||
auth: auth::JWT,
|
||||
Path((id, product_id)): Path<(i32, i32)>,
|
||||
Path((id, variant_id)): Path<(i32, i32)>,
|
||||
State(ctx): State<AppContext>,
|
||||
Form(form): Form<ResolutionForm>,
|
||||
) -> Result<Response> {
|
||||
@@ -356,14 +384,14 @@ async fn set_resolution(
|
||||
|
||||
let existing = account_product_resolutions::Entity::find()
|
||||
.filter(account_product_resolutions::Column::UserId.eq(company.id))
|
||||
.filter(account_product_resolutions::Column::ProductId.eq(product_id))
|
||||
.filter(account_product_resolutions::Column::VariantId.eq(variant_id))
|
||||
.one(&ctx.db)
|
||||
.await?;
|
||||
let mut active = match existing {
|
||||
Some(row) => row.into_active_model(),
|
||||
None => account_product_resolutions::ActiveModel {
|
||||
user_id: Set(company.id),
|
||||
product_id: Set(product_id),
|
||||
variant_id: Set(variant_id),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
@@ -378,16 +406,16 @@ pub fn routes() -> Routes {
|
||||
.add("/admin/customers/{id}", get(show))
|
||||
.add("/admin/customers/{id}/profiles", post(sync_profiles))
|
||||
.add(
|
||||
"/admin/customers/{id}/prices/{product_id}/edit",
|
||||
"/admin/customers/{id}/prices/{variant_id}/edit",
|
||||
get(price_edit),
|
||||
)
|
||||
.add("/admin/customers/{id}/prices/{product_id}", post(set_price))
|
||||
.add("/admin/customers/{id}/prices/{variant_id}", post(set_price))
|
||||
.add(
|
||||
"/admin/customers/{id}/prices/{product_id}/remove",
|
||||
"/admin/customers/{id}/prices/{variant_id}/remove",
|
||||
post(remove_price),
|
||||
)
|
||||
.add(
|
||||
"/admin/customers/{id}/resolutions/{product_id}",
|
||||
"/admin/customers/{id}/resolutions/{variant_id}",
|
||||
post(set_resolution),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user