effective price is only highlighted if changed
This commit is contained in:
@@ -242,7 +242,11 @@ negotiated-prices = Negotiated prices
|
|||||||
negotiated-prices-hint = Set a price for a specific product for this business account. The customer always pays the lower of the public and negotiated price.
|
negotiated-prices-hint = Set a price for a specific product for this business account. The customer always pays the lower of the public and negotiated price.
|
||||||
manage-prices = Manage prices
|
manage-prices = Manage prices
|
||||||
public-price = Public price
|
public-price = Public price
|
||||||
|
business-price = Business price
|
||||||
negotiated-price = Negotiated price
|
negotiated-price = Negotiated price
|
||||||
|
set-negotiated-price = Set price
|
||||||
|
negotiated-price-hint = Set a negotiated price for this product for this business account. The customer always pays the lowest of the public, business and negotiated price.
|
||||||
|
negotiated-remove-confirm = Remove this negotiated price?
|
||||||
effective-price = Effective price
|
effective-price = Effective price
|
||||||
admin-discount-profiles = Discount profiles
|
admin-discount-profiles = Discount profiles
|
||||||
admin-discount-profiles-desc = Create reusable discount layers (a % over chosen products) and assign them to business accounts.
|
admin-discount-profiles-desc = Create reusable discount layers (a % over chosen products) and assign them to business accounts.
|
||||||
|
|||||||
@@ -242,7 +242,11 @@ negotiated-prices = Dohodnuté ceny
|
|||||||
negotiated-prices-hint = Nastavte cenu pre konkrétny produkt pre tento firemný účet. Zákazník vždy zaplatí najnižšiu z verejnej a dohodnutej ceny.
|
negotiated-prices-hint = Nastavte cenu pre konkrétny produkt pre tento firemný účet. Zákazník vždy zaplatí najnižšiu z verejnej a dohodnutej ceny.
|
||||||
manage-prices = Spravovať ceny
|
manage-prices = Spravovať ceny
|
||||||
public-price = Verejná cena
|
public-price = Verejná cena
|
||||||
|
business-price = Firemná cena
|
||||||
negotiated-price = Dohodnutá cena
|
negotiated-price = Dohodnutá cena
|
||||||
|
set-negotiated-price = Nastaviť cenu
|
||||||
|
negotiated-price-hint = Nastavte dohodnutú cenu tohto produktu pre tento firemný účet. Zákazník vždy zaplatí najnižšiu z verejnej, firemnej a dohodnutej ceny.
|
||||||
|
negotiated-remove-confirm = Zrušiť túto dohodnutú cenu?
|
||||||
effective-price = Výsledná cena
|
effective-price = Výsledná cena
|
||||||
admin-discount-profiles = Zľavové profily
|
admin-discount-profiles = Zľavové profily
|
||||||
admin-discount-profiles-desc = Vytvorte opakovane použiteľné zľavové vrstvy (% na vybrané produkty) a priraďte ich firemným účtom.
|
admin-discount-profiles-desc = Vytvorte opakovane použiteľné zľavové vrstvy (% na vybrané produkty) a priraďte ich firemným účtom.
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
97
assets/views/admin/customers/price_form.html
Normal file
97
assets/views/admin/customers/price_form.html
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
{% extends "admin/base.html" %}
|
||||||
|
{% import "macros/ui.html" as ui %}
|
||||||
|
|
||||||
|
{% block title %}{{ t(key="set-negotiated-price", lang=lang | default(value='sk')) }}{% endblock title %}
|
||||||
|
{% block crumb %}{{ t(key="admin-customers", lang=lang | default(value='sk')) }}{% endblock crumb %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="flex items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<h1 class="text-2xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">{{ product.name }}</h1>
|
||||||
|
{{ ui::badge(label=t(key="negotiated-price", lang=lang | default(value='sk')), variant="info") }}
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-on-surface/70 dark:text-on-surface-dark/70">{{ customer.name }}</p>
|
||||||
|
</div>
|
||||||
|
{{ ui::button(variant="outline-secondary", label=t(key="back", lang=lang | default(value='sk')), href="/admin/customers/" ~ customer.id, size="px-3 py-2 text-sm") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
<div class="mt-4 max-w-md">{{ ui::alert_danger(message=t(key=error, lang=lang | default(value='sk'))) }}</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post" action="/admin/customers/{{ customer.id }}/prices/{{ product.id }}"
|
||||||
|
x-data="{
|
||||||
|
price: '{{ negotiated }}',
|
||||||
|
regular: {{ product.regular_cents }},
|
||||||
|
num(v) { let n = parseFloat(String(v).replace(',', '.')); return isFinite(n) ? n : null; },
|
||||||
|
get afterCents() { let f = this.num(this.price); return f === null ? null : Math.round(f * 100); },
|
||||||
|
money(c) { return (c / 100).toFixed(2); },
|
||||||
|
get valid() { let a = this.afterCents; return a !== null && a > 0; }
|
||||||
|
}"
|
||||||
|
class="mt-6 max-w-md space-y-5 rounded-radius border-2 border-secondary/60 bg-surface p-6 dark:border-secondary-dark/60 dark:bg-surface-dark-alt">
|
||||||
|
{{ ui::csrf_field() }}
|
||||||
|
|
||||||
|
<p class="text-sm text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="negotiated-price-hint", lang=lang | default(value='sk')) }}</p>
|
||||||
|
|
||||||
|
<!-- reference prices -->
|
||||||
|
<div class="space-y-2 rounded-radius bg-surface-alt px-4 py-3 dark:bg-surface-dark/40">
|
||||||
|
<div class="flex items-center justify-between gap-3 text-sm">
|
||||||
|
<span class="text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="price", lang=lang | default(value='sk')) }}</span>
|
||||||
|
<span class="tabular-nums text-on-surface-strong dark:text-on-surface-dark-strong">{{ product.regular_price }} {{ product.currency }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between gap-3 text-sm">
|
||||||
|
<span class="text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="business-price", lang=lang | default(value='sk')) }}</span>
|
||||||
|
<span class="tabular-nums {% if product.business_reduced %}font-medium text-danger{% else %}text-on-surface-strong dark:text-on-surface-dark-strong{% endif %}">{{ product.business_price }} {{ product.currency }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between gap-3 text-sm">
|
||||||
|
<span class="text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="effective-price", lang=lang | default(value='sk')) }}</span>
|
||||||
|
<span class="tabular-nums font-medium {% if product.effective_differs %}text-primary dark:text-primary-dark{% else %}text-on-surface-strong dark:text-on-surface-dark-strong{% endif %}">{{ product.effective_price }} {{ product.currency }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- negotiated price input -->
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<label for="price" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="negotiated-price", lang=lang | default(value='sk')) }}</label>
|
||||||
|
{{ ui::input(name="price", id="price", value=negotiated, placeholder="0.00", attrs='inputmode="decimal" x-model="price"') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- live preview -->
|
||||||
|
<div x-show="afterCents !== null" x-cloak
|
||||||
|
class="space-y-2 rounded-radius border border-outline bg-surface-alt px-4 py-3 dark:border-outline-dark dark:bg-surface-dark/40">
|
||||||
|
<div class="flex items-center justify-between gap-3">
|
||||||
|
<span class="text-sm text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="negotiated-price", lang=lang | default(value='sk')) }}</span>
|
||||||
|
<span class="text-lg font-semibold tabular-nums" :class="valid ? 'text-secondary dark:text-secondary-dark' : 'text-on-surface/40 dark:text-on-surface-dark/40'">
|
||||||
|
<span x-text="money(afterCents)"></span> {{ product.currency }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p x-show="!valid" class="text-xs text-danger">{{ t(key="discount-must-be-positive", lang=lang | default(value='sk')) }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-3 pt-2">
|
||||||
|
{{ ui::button(variant="secondary", label=t(key="save", lang=lang | default(value='sk')), type="submit") }}
|
||||||
|
{% if has_negotiated %}
|
||||||
|
{{ ui::button(variant="outline-danger", label=t(key="remove", lang=lang | default(value='sk')), type="submit", attrs=`formaction="/admin/customers/` ~ customer.id ~ `/prices/` ~ product.id ~ `/remove" onclick="return confirm('` ~ t(key="negotiated-remove-confirm", lang=lang | default(value='sk')) ~ `')"`) }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if collision %}
|
||||||
|
<!-- collision resolution: two assigned profiles cover this product -->
|
||||||
|
<section class="mt-4 max-w-md rounded-radius border border-warning/60 bg-surface p-6 dark:bg-surface-dark-alt">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<h2 class="text-lg font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="automated-price", lang=lang | default(value='sk')) }}</h2>
|
||||||
|
{{ ui::badge(label=t(key="collision", lang=lang | default(value='sk')), variant="warning") }}
|
||||||
|
</div>
|
||||||
|
<form method="post" action="/admin/customers/{{ customer.id }}/resolutions/{{ product.id }}" class="mt-3 flex items-center gap-2">
|
||||||
|
{{ ui::csrf_field() }}
|
||||||
|
<select name="profile_id" class="rounded-radius border border-outline bg-surface-alt px-2 py-1.5 text-sm dark:border-outline-dark dark:bg-surface-dark-alt/50 dark:text-on-surface-dark">
|
||||||
|
{% for c in covering %}
|
||||||
|
<option value="{{ c.id }}" {% if c.id == auto_profile_id %}selected{% endif %}>{{ c.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
{{ ui::button(label=t(key="resolve", lang=lang | default(value='sk')), type="submit", size="px-3 py-1.5 text-sm") }}
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock content %}
|
||||||
@@ -49,10 +49,9 @@
|
|||||||
<thead class="{{ ui::thead_cls() }}">
|
<thead class="{{ ui::thead_cls() }}">
|
||||||
<tr>
|
<tr>
|
||||||
{{ ui::th(label=t(key="product", lang=lang | default(value='sk'))) }}
|
{{ ui::th(label=t(key="product", lang=lang | default(value='sk'))) }}
|
||||||
{{ ui::th(label=t(key="public-price", lang=lang | default(value='sk'))) }}
|
{{ ui::th(label=t(key="business-price", lang=lang | default(value='sk'))) }}
|
||||||
{{ ui::th(label=t(key="automated-price", lang=lang | default(value='sk'))) }}
|
{{ ui::th(label=t(key="effective-price", lang=lang | default(value='sk'))) }}
|
||||||
{{ ui::th(label=t(key="negotiated-price", lang=lang | default(value='sk'))) }}
|
{{ ui::th(label=t(key="actions", lang=lang | default(value='sk')), align="text-right") }}
|
||||||
{{ ui::th(label=t(key="effective-price", lang=lang | default(value='sk')), align="text-right") }}
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="{{ ui::tbody_cls() }}">
|
<tbody class="{{ ui::tbody_cls() }}">
|
||||||
@@ -60,46 +59,28 @@
|
|||||||
<tr class="{{ ui::row_cls() }}">
|
<tr class="{{ ui::row_cls() }}">
|
||||||
<td class="px-4 py-3 font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ product.name }}</td>
|
<td class="px-4 py-3 font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ product.name }}</td>
|
||||||
<td class="px-4 py-3 tabular-nums">
|
<td class="px-4 py-3 tabular-nums">
|
||||||
{% if product.on_public_sale %}
|
{% if product.business_reduced %}
|
||||||
<span class="font-medium text-danger">{{ product.public_price }} {{ product.currency }}</span>
|
<span class="font-medium text-danger">{{ product.business_price }} {{ product.currency }}</span>
|
||||||
<span class="ml-1 text-xs text-on-surface/50 line-through dark:text-on-surface-dark/50">{{ product.regular_price }}</span>
|
<span class="ml-1 text-xs text-on-surface/50 line-through dark:text-on-surface-dark/50">{{ product.regular_price }}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ product.public_price }} {{ product.currency }}
|
{{ product.business_price }} {{ product.currency }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 tabular-nums">
|
<td class="px-4 py-3 tabular-nums">
|
||||||
{% if product.auto_price %}
|
<span class="font-medium {% if product.effective_differs %}text-primary dark:text-primary-dark{% else %}text-on-surface-strong dark:text-on-surface-dark-strong{% endif %}">{{ product.effective_price }} {{ product.currency }}</span>
|
||||||
<div>{{ product.auto_price }} {{ product.currency }}</div>
|
{% if product.collision %}<span class="ml-1">{{ ui::badge(label=t(key="collision", lang=lang | default(value='sk')), variant="warning") }}</span>{% endif %}
|
||||||
{% if product.collision %}
|
|
||||||
<div class="mt-1">{{ ui::badge(label=t(key="collision", lang=lang | default(value='sk')), variant="warning") }}</div>
|
|
||||||
<form method="post" action="/admin/customers/{{ customer.id }}/resolutions/{{ product.product_id }}" class="mt-1 flex items-center gap-1">
|
|
||||||
{{ ui::csrf_field() }}
|
|
||||||
<select name="profile_id" class="rounded-radius border border-outline bg-surface-alt px-2 py-1 text-xs dark:border-outline-dark dark:bg-surface-dark-alt/50 dark:text-on-surface-dark">
|
|
||||||
{% for c in product.covering %}
|
|
||||||
<option value="{{ c.id }}" {% if c.id == product.auto_profile_id %}selected{% endif %}>{{ c.name }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
{{ ui::button(label=t(key="resolve", lang=lang | default(value='sk')), type="submit", size="px-2 py-1 text-xs") }}
|
|
||||||
</form>
|
|
||||||
{% elif product.auto_profile_name %}
|
|
||||||
<div class="text-xs text-on-surface/60 dark:text-on-surface-dark/60">{{ product.auto_profile_name }}</div>
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
<span class="text-on-surface/40 dark:text-on-surface-dark/40">—</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3">
|
<td class="px-4 py-3">
|
||||||
<form method="post" action="/admin/customers/{{ customer.id }}/prices/{{ product.product_id }}" class="flex items-center gap-2">
|
<div class="flex flex-wrap justify-end gap-2">
|
||||||
{{ ui::csrf_field() }}
|
{{ ui::button(variant="outline-secondary", label=t(key="set-negotiated-price", lang=lang | default(value='sk')), href="/admin/customers/" ~ customer.id ~ "/prices/" ~ product.product_id ~ "/edit", size="px-3 py-1.5 text-xs") }}
|
||||||
{{ ui::input(name="price", value=product.manual_price | default(value=""), placeholder="0.00", width="w-28", attrs='inputmode="decimal"') }}
|
{% if product.has_negotiated %}
|
||||||
{{ ui::button(label=t(key="save", lang=lang | default(value='sk')), type="submit", size="px-3 py-1.5 text-xs") }}
|
<form method="post" action="/admin/customers/{{ customer.id }}/prices/{{ product.product_id }}/remove"
|
||||||
{% if product.manual_price %}
|
onsubmit="return confirm('{{ t(key="negotiated-remove-confirm", lang=lang | default(value='sk')) }}')">
|
||||||
{{ ui::button(variant="outline-danger", label=t(key="remove", lang=lang | default(value='sk')), type="submit", size="px-3 py-1.5 text-xs", attrs='formaction="/admin/customers/' ~ customer.id ~ '/prices/' ~ product.product_id ~ '/remove"') }}
|
{{ ui::csrf_field() }}
|
||||||
|
{{ ui::button(variant="outline-danger", label=t(key="remove", lang=lang | default(value='sk')), type="submit", size="px-3 py-1.5 text-xs") }}
|
||||||
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</form>
|
</div>
|
||||||
</td>
|
|
||||||
<td class="px-4 py-3 text-right tabular-nums">
|
|
||||||
<span class="font-medium {% if product.is_business %}text-primary dark:text-primary-dark{% else %}text-on-surface-strong dark:text-on-surface-dark-strong{% endif %}">{{ product.effective_price }} {{ product.currency }}</span>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
const COMPANY: &str = "company";
|
const COMPANY: &str = "company";
|
||||||
|
const BUSINESS_AUDIENCE: &str = "business";
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct PriceForm {
|
struct PriceForm {
|
||||||
@@ -117,8 +118,6 @@ async fn show(
|
|||||||
.all(&ctx.db)
|
.all(&ctx.db)
|
||||||
.await?;
|
.await?;
|
||||||
let assigned = assigned_profile_ids(&ctx, company.id).await?;
|
let assigned = assigned_profile_ids(&ctx, company.id).await?;
|
||||||
let profile_name: HashMap<i32, String> =
|
|
||||||
all_profiles.iter().map(|p| (p.id, p.name.clone())).collect();
|
|
||||||
let profiles_json: Vec<serde_json::Value> = all_profiles
|
let profiles_json: Vec<serde_json::Value> = all_profiles
|
||||||
.iter()
|
.iter()
|
||||||
.map(|p| {
|
.map(|p| {
|
||||||
@@ -136,32 +135,30 @@ async fn show(
|
|||||||
.order_by_asc(products::Column::Name)
|
.order_by_asc(products::Column::Name)
|
||||||
.all(&ctx.db)
|
.all(&ctx.db)
|
||||||
.await?;
|
.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?;
|
let details = pricing::detail_many(&ctx, &list, Some(&company)).await?;
|
||||||
|
|
||||||
let rows: Vec<serde_json::Value> = list
|
let rows: Vec<serde_json::Value> = list
|
||||||
.iter()
|
.iter()
|
||||||
|
.zip(business.iter())
|
||||||
.zip(details.iter())
|
.zip(details.iter())
|
||||||
.map(|(product, d)| {
|
.map(|((product, b), d)| {
|
||||||
let covering: Vec<serde_json::Value> = d
|
|
||||||
.covering_profile_ids
|
|
||||||
.iter()
|
|
||||||
.map(|pid| json!({ "id": pid, "name": profile_name.get(pid) }))
|
|
||||||
.collect();
|
|
||||||
json!({
|
json!({
|
||||||
"product_id": product.id,
|
"product_id": product.id,
|
||||||
"name": product.name,
|
"name": product.name,
|
||||||
"currency": product.currency,
|
"currency": product.currency,
|
||||||
"regular_price": format_price(d.regular_cents),
|
"regular_price": format_price(d.regular_cents),
|
||||||
"public_price": format_price(d.public_cents),
|
"business_price": format_price(b.price_cents),
|
||||||
"on_public_sale": product.on_sale(),
|
"business_reduced": b.price_cents < d.regular_cents,
|
||||||
"manual_price": d.manual_cents.map(format_price),
|
"has_negotiated": d.manual_cents.is_some(),
|
||||||
"auto_price": d.auto_cents.map(format_price),
|
|
||||||
"auto_profile_name": d.auto_profile_id.and_then(|pid| profile_name.get(&pid)),
|
|
||||||
"auto_profile_id": d.auto_profile_id,
|
|
||||||
"collision": d.collision,
|
"collision": d.collision,
|
||||||
"covering": covering,
|
|
||||||
"effective_price": format_price(d.price_cents),
|
"effective_price": format_price(d.price_cents),
|
||||||
"is_business": d.is_business,
|
"effective_differs": d.price_cents != b.price_cents,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@@ -179,6 +176,75 @@ async fn show(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Dedicated per-product page for the negotiated price (and, when two assigned
|
||||||
|
/// profiles collide, the resolution selector). Mirrors the catalog "Set discount"
|
||||||
|
/// page but for a single company.
|
||||||
|
#[debug_handler]
|
||||||
|
async fn price_edit(
|
||||||
|
auth: auth::JWT,
|
||||||
|
jar: CookieJar,
|
||||||
|
ViewEngine(v): ViewEngine<TeraView>,
|
||||||
|
Path((id, product_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)
|
||||||
|
.one(&ctx.db)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| Error::NotFound)?;
|
||||||
|
|
||||||
|
let business =
|
||||||
|
pricing::audience_price_many(&ctx, std::slice::from_ref(&product), 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 d = &detail[0];
|
||||||
|
|
||||||
|
// Names for the covering profiles, used by the collision resolution selector.
|
||||||
|
let covering: Vec<serde_json::Value> = if d.covering_profile_ids.is_empty() {
|
||||||
|
Vec::new()
|
||||||
|
} else {
|
||||||
|
let profiles = discount_profiles::Entity::find()
|
||||||
|
.filter(discount_profiles::Column::Id.is_in(d.covering_profile_ids.clone()))
|
||||||
|
.all(&ctx.db)
|
||||||
|
.await?;
|
||||||
|
let name: HashMap<i32, String> =
|
||||||
|
profiles.iter().map(|p| (p.id, p.name.clone())).collect();
|
||||||
|
d.covering_profile_ids
|
||||||
|
.iter()
|
||||||
|
.map(|pid| json!({ "id": pid, "name": name.get(pid) }))
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
format::view(
|
||||||
|
&v,
|
||||||
|
"admin/customers/price_form.html",
|
||||||
|
json!({
|
||||||
|
"customer": { "id": company.id, "name": company.name },
|
||||||
|
"product": {
|
||||||
|
"id": product.id,
|
||||||
|
"name": product.name,
|
||||||
|
"currency": product.currency,
|
||||||
|
"regular_price": format_price(d.regular_cents),
|
||||||
|
"regular_cents": d.regular_cents,
|
||||||
|
"business_price": format_price(business_cents),
|
||||||
|
"business_reduced": business_cents < d.regular_cents,
|
||||||
|
"effective_price": format_price(d.price_cents),
|
||||||
|
"effective_differs": d.price_cents != business_cents,
|
||||||
|
},
|
||||||
|
"negotiated": d.manual_cents.map(format_price).unwrap_or_default(),
|
||||||
|
"has_negotiated": d.manual_cents.is_some(),
|
||||||
|
"collision": d.collision,
|
||||||
|
"covering": covering,
|
||||||
|
"auto_profile_id": d.auto_profile_id,
|
||||||
|
"error": params.get("error"),
|
||||||
|
"lang": current_lang(&jar),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
async fn set_price(
|
async fn set_price(
|
||||||
auth: auth::JWT,
|
auth: auth::JWT,
|
||||||
@@ -199,7 +265,7 @@ async fn set_price(
|
|||||||
Ok(cents) if cents > 0 => cents,
|
Ok(cents) if cents > 0 => cents,
|
||||||
_ => {
|
_ => {
|
||||||
return format::redirect(&format!(
|
return format::redirect(&format!(
|
||||||
"/admin/customers/{id}?error=discount-must-be-positive"
|
"/admin/customers/{id}/prices/{product_id}/edit?error=discount-must-be-positive"
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -288,6 +354,10 @@ pub fn routes() -> Routes {
|
|||||||
.add("/admin/customers", get(index))
|
.add("/admin/customers", get(index))
|
||||||
.add("/admin/customers/{id}", get(show))
|
.add("/admin/customers/{id}", get(show))
|
||||||
.add("/admin/customers/{id}/profiles", post(sync_profiles))
|
.add("/admin/customers/{id}/profiles", post(sync_profiles))
|
||||||
|
.add(
|
||||||
|
"/admin/customers/{id}/prices/{product_id}/edit",
|
||||||
|
get(price_edit),
|
||||||
|
)
|
||||||
.add("/admin/customers/{id}/prices/{product_id}", post(set_price))
|
.add("/admin/customers/{id}/prices/{product_id}", post(set_price))
|
||||||
.add(
|
.add(
|
||||||
"/admin/customers/{id}/prices/{product_id}/remove",
|
"/admin/customers/{id}/prices/{product_id}/remove",
|
||||||
|
|||||||
Reference in New Issue
Block a user