save discount profile is now working perfectly well
Some checks failed
CI / Check Style (push) Has been cancelled
CI / Run Clippy (push) Has been cancelled
CI / Run Tests (push) Has been cancelled

This commit is contained in:
Priec
2026-06-22 13:51:40 +02:00
parent 88074c1871
commit 29854a972b
5 changed files with 121 additions and 6 deletions

View 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 %}

View File

@@ -40,6 +40,9 @@
</p> </p>
{% if profiles | length > 0 %} {% if profiles | length > 0 %}
<form method="post" action="/admin/catalog/products/profiles?audience={{ audience }}" class="mt-3 space-y-3" <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="{ x-data="{
orig: { {% for p in profiles %}'{{ p.id }}': {% if p.assigned %}true{% else %}false{% endif %}{% if not loop.last %}, {% endif %}{% endfor %} }, 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 %} }, 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 %} {% endif %}
</td> </td>
<td class="px-4 py-3 tabular-nums"> <td class="px-4 py-3 tabular-nums">
{% if product.effective_reduced %} <span id="eff-{{ product.id }}">{{ ui::eff_price(p=product) }}</span>
<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 %}
</td> </td>
<td class="px-4 py-3 tabular-nums">{{ product.stock }}</td> <td class="px-4 py-3 tabular-nums">{{ product.stock }}</td>
<td class="px-4 py-3"> <td class="px-4 py-3">

View File

@@ -122,6 +122,21 @@
{%- endif %} {%- endif %}
{%- endmacro badge %} {%- 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 {# ---- Form controls. Verbatim Penguin classes from
penguinui/{text-input,text-area,select,checkbox,file-input}/default-*.html. penguinui/{text-input,text-area,select,checkbox,file-input}/default-*.html.
These macros emit only the control (callers keep their own <label>/layout), so These macros emit only the control (callers keep their own <label>/layout), so

View File

@@ -432,6 +432,62 @@ fn percent_off(regular_cents: i64, sale_cents: i64) -> i64 {
off.round() as 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(&params);
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 /// Replace the profiles applied to this audience with the submitted checkbox set
/// (`profile_ids`, a repeated field parsed directly from the body). /// (`profile_ids`, a repeated field parsed directly from the body).
#[debug_handler] #[debug_handler]
@@ -631,6 +687,10 @@ pub fn routes() -> Routes {
post(create).layer(image_limit.clone()), post(create).layer(image_limit.clone()),
) )
.add("/admin/catalog/products/profiles", post(sync_profiles)) .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}/edit", get(edit))
.add( .add(
"/admin/catalog/products/{id}", "/admin/catalog/products/{id}",

View File

@@ -370,6 +370,40 @@ pub async fn audience_price_many(
Ok(list.iter().map(|p| detail_for(p, &pc).priced()).collect()) 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). /// Price one product for `user` (`None` = anonymous/public).
pub async fn price_for( pub async fn price_for(
ctx: &AppContext, ctx: &AppContext,