save discount profile is now working perfectly well
This commit is contained in:
8
assets/views/admin/catalog/_price_preview.html
Normal file
8
assets/views/admin/catalog/_price_preview.html
Normal file
@@ -0,0 +1,8 @@
|
||||
{# OOB fragment: effective-price cells recomputed from the unsaved profile
|
||||
selection on the products page. Each span replaces the matching #eff-<id>
|
||||
span in the table via htmx out-of-band swap. Rendered by
|
||||
admin_products::profiles_preview. #}
|
||||
{% import "macros/ui.html" as ui %}
|
||||
{% for product in products %}
|
||||
<span id="eff-{{ product.id }}" hx-swap-oob="true">{{ ui::eff_price(p=product, preview=true) }}</span>
|
||||
{% endfor %}
|
||||
@@ -40,6 +40,9 @@
|
||||
</p>
|
||||
{% if profiles | length > 0 %}
|
||||
<form method="post" action="/admin/catalog/products/profiles?audience={{ audience }}" class="mt-3 space-y-3"
|
||||
hx-post="/admin/catalog/products/profiles/preview?audience={{ audience }}&category={{ selected_category }}"
|
||||
hx-trigger="change"
|
||||
hx-swap="none"
|
||||
x-data="{
|
||||
orig: { {% for p in profiles %}'{{ p.id }}': {% if p.assigned %}true{% else %}false{% endif %}{% if not loop.last %}, {% endif %}{% endfor %} },
|
||||
sel: { {% for p in profiles %}'{{ p.id }}': {% if p.assigned %}true{% else %}false{% endif %}{% if not loop.last %}, {% endif %}{% endfor %} },
|
||||
@@ -113,12 +116,7 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-4 py-3 tabular-nums">
|
||||
{% if product.effective_reduced %}
|
||||
<span class="font-medium text-primary dark:text-primary-dark">{{ product.effective_price }} {{ product.currency }}</span>
|
||||
<span class="ml-1 text-xs text-on-surface/60 dark:text-on-surface-dark/60">(−{{ product.effective_percent_off }}%)</span>
|
||||
{% else %}
|
||||
{{ product.effective_price }} {{ product.currency }}
|
||||
{% endif %}
|
||||
<span id="eff-{{ product.id }}">{{ ui::eff_price(p=product) }}</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 tabular-nums">{{ product.stock }}</td>
|
||||
<td class="px-4 py-3">
|
||||
|
||||
@@ -122,6 +122,21 @@
|
||||
{%- endif %}
|
||||
{%- endmacro badge %}
|
||||
|
||||
{# Effective-price cell content for the admin products table. The value is
|
||||
colored only when it differs from the regular price (effective_reduced);
|
||||
when equal it renders in the plain text color, unified with the Price column.
|
||||
`preview=true` uses the info color (an unsaved profile-toggle preview) instead
|
||||
of the saved primary color. No t() calls, so it is safe inside a macro. #}
|
||||
{% macro eff_price(p, preview=false) -%}
|
||||
{%- if preview -%}{% set strong = "text-info" %}{%- else -%}{% set strong = "text-primary dark:text-primary-dark" %}{%- endif -%}
|
||||
{% if p.effective_reduced %}
|
||||
<span class="font-medium {{ strong }}">{{ p.effective_price }} {{ p.currency }}</span>
|
||||
<span class="ml-1 text-xs text-on-surface/60 dark:text-on-surface-dark/60">(−{{ p.effective_percent_off }}%)</span>
|
||||
{% else %}
|
||||
{{ p.effective_price }} {{ p.currency }}
|
||||
{% endif %}
|
||||
{%- endmacro eff_price %}
|
||||
|
||||
{# ---- Form controls. Verbatim Penguin classes from
|
||||
penguinui/{text-input,text-area,select,checkbox,file-input}/default-*.html.
|
||||
These macros emit only the control (callers keep their own <label>/layout), so
|
||||
|
||||
@@ -432,6 +432,62 @@ fn percent_off(regular_cents: i64, sale_cents: i64) -> i64 {
|
||||
off.round() as i64
|
||||
}
|
||||
|
||||
/// Preview the effective prices that the submitted (unsaved) checkbox set would
|
||||
/// produce, without persisting anything. Returns OOB `<span>`s that htmx swaps
|
||||
/// into the effective-price column so the admin sees the effect before Save.
|
||||
#[debug_handler]
|
||||
async fn profiles_preview(
|
||||
auth: auth::JWT,
|
||||
jar: CookieJar,
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
Query(params): Query<HashMap<String, String>>,
|
||||
State(ctx): State<AppContext>,
|
||||
body: String,
|
||||
) -> Result<Response> {
|
||||
guard::current_admin(auth, &ctx).await?;
|
||||
let audience = read_audience(¶ms);
|
||||
|
||||
let profile_ids: Vec<i32> = form_urlencoded::parse(body.as_bytes())
|
||||
.filter(|(k, _)| k == "profile_ids")
|
||||
.filter_map(|(_, value)| value.parse::<i32>().ok())
|
||||
.collect();
|
||||
|
||||
let all_categories = categories::Entity::find()
|
||||
.order_by_asc(categories::Column::Position)
|
||||
.order_by_asc(categories::Column::Name)
|
||||
.all(&ctx.db)
|
||||
.await?;
|
||||
let list = products::Entity::find()
|
||||
.order_by_desc(products::Column::CreatedAt)
|
||||
.all(&ctx.db)
|
||||
.await?;
|
||||
let effective =
|
||||
pricing::audience_price_many_preview(&ctx, &list, audience, profile_ids).await?;
|
||||
|
||||
let selected_category = params.get("category").map(String::as_str).unwrap_or("all");
|
||||
let filter = view::category_filter_ids(&all_categories, selected_category);
|
||||
|
||||
let mut rows = Vec::new();
|
||||
for (product, priced) in list.iter().zip(effective.iter()) {
|
||||
if !view::category_filter_keep(&filter, product.category_id) {
|
||||
continue;
|
||||
}
|
||||
rows.push(json!({
|
||||
"id": product.id,
|
||||
"currency": product.currency,
|
||||
"effective_price": format_price(priced.price_cents),
|
||||
"effective_reduced": priced.is_reduced(),
|
||||
"effective_percent_off": percent_off(product.price_cents, priced.price_cents),
|
||||
}));
|
||||
}
|
||||
|
||||
format::view(
|
||||
&v,
|
||||
"admin/catalog/_price_preview.html",
|
||||
json!({ "products": rows, "lang": current_lang(&jar) }),
|
||||
)
|
||||
}
|
||||
|
||||
/// Replace the profiles applied to this audience with the submitted checkbox set
|
||||
/// (`profile_ids`, a repeated field parsed directly from the body).
|
||||
#[debug_handler]
|
||||
@@ -631,6 +687,10 @@ pub fn routes() -> Routes {
|
||||
post(create).layer(image_limit.clone()),
|
||||
)
|
||||
.add("/admin/catalog/products/profiles", post(sync_profiles))
|
||||
.add(
|
||||
"/admin/catalog/products/profiles/preview",
|
||||
post(profiles_preview),
|
||||
)
|
||||
.add("/admin/catalog/products/{id}/edit", get(edit))
|
||||
.add(
|
||||
"/admin/catalog/products/{id}",
|
||||
|
||||
@@ -370,6 +370,40 @@ pub async fn audience_price_many(
|
||||
Ok(list.iter().map(|p| detail_for(p, &pc).priced()).collect())
|
||||
}
|
||||
|
||||
/// Like [`audience_price_many`], but prices against an *unsaved* set of profile
|
||||
/// ids for the active audience instead of the persisted assignment. Used by the
|
||||
/// products page to preview effective prices as the admin toggles profile
|
||||
/// checkboxes, before they hit Save. For the business tab the personal layer
|
||||
/// stays the persisted one (businesses get the lower of personal/business), and
|
||||
/// only the business layer is replaced by the previewed selection.
|
||||
pub async fn audience_price_many_preview(
|
||||
ctx: &AppContext,
|
||||
list: &[products::Model],
|
||||
audience: &str,
|
||||
selected_profile_ids: Vec<i32>,
|
||||
) -> Result<Vec<PricedProduct>> {
|
||||
let preview = load_profiles_by_ids(ctx, selected_profile_ids).await?;
|
||||
let (personal, business, b2b) = if audience == AUDIENCE_BUSINESS {
|
||||
(
|
||||
load_audience(ctx, AUDIENCE_PERSONAL).await?,
|
||||
preview,
|
||||
Some(B2bContext {
|
||||
manual: HashMap::new(),
|
||||
profiles: LoadedProfiles::empty(),
|
||||
resolutions: HashMap::new(),
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
(preview, LoadedProfiles::empty(), None)
|
||||
};
|
||||
let pc = PricingCtx {
|
||||
personal,
|
||||
business,
|
||||
b2b,
|
||||
};
|
||||
Ok(list.iter().map(|p| detail_for(p, &pc).priced()).collect())
|
||||
}
|
||||
|
||||
/// Price one product for `user` (`None` = anonymous/public).
|
||||
pub async fn price_for(
|
||||
ctx: &AppContext,
|
||||
|
||||
Reference in New Issue
Block a user