eur czk can be disabled from now on
This commit is contained in:
@@ -476,8 +476,9 @@ bank-amount = Amount
|
|||||||
admin-shipping = Shipping
|
admin-shipping = Shipping
|
||||||
admin-shipping-desc = set the price and availability of each delivery option.
|
admin-shipping-desc = set the price and availability of each delivery option.
|
||||||
shipping-enabled = Active
|
shipping-enabled = Active
|
||||||
admin-currency = Currency
|
admin-currency = Exchange rate
|
||||||
admin-currency-desc = set the exchange rate for the currencies customers can switch between. You always enter prices in EUR.
|
admin-currency-desc = set the exchange rate for the currencies customers can switch between. You always enter prices in EUR.
|
||||||
|
currency-rate = Rate
|
||||||
exchange-rate = Exchange rate
|
exchange-rate = Exchange rate
|
||||||
exchange-rate-hint = { $code } prices are the { $base } price recalculated at this rate.
|
exchange-rate-hint = { $code } prices are the { $base } price recalculated at this rate.
|
||||||
currency-enabled = Available to customers
|
currency-enabled = Available to customers
|
||||||
|
|||||||
@@ -476,8 +476,9 @@ bank-amount = Suma
|
|||||||
admin-shipping = Doprava
|
admin-shipping = Doprava
|
||||||
admin-shipping-desc = nastaviť cenu a dostupnosť jednotlivých možností dopravy.
|
admin-shipping-desc = nastaviť cenu a dostupnosť jednotlivých možností dopravy.
|
||||||
shipping-enabled = Aktívne
|
shipping-enabled = Aktívne
|
||||||
admin-currency = Mena
|
admin-currency = Kurz
|
||||||
admin-currency-desc = nastaviť výmenný kurz pre meny, medzi ktorými môžu zákazníci prepínať. Ceny zadávate vždy v EUR.
|
admin-currency-desc = nastaviť výmenný kurz pre meny, medzi ktorými môžu zákazníci prepínať. Ceny zadávate vždy v EUR.
|
||||||
|
currency-rate = Kurz
|
||||||
exchange-rate = Výmenný kurz
|
exchange-rate = Výmenný kurz
|
||||||
exchange-rate-hint = ceny v { $code } sa prepočítajú z ceny v { $base } týmto kurzom.
|
exchange-rate-hint = ceny v { $code } sa prepočítajú z ceny v { $base } týmto kurzom.
|
||||||
currency-enabled = Dostupná pre zákazníkov
|
currency-enabled = Dostupná pre zákazníkov
|
||||||
|
|||||||
@@ -104,8 +104,19 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<!-- right side: cart + settings + mobile toggle -->
|
<!-- right side: kurz + cart + settings + mobile toggle -->
|
||||||
<div class="ml-auto flex items-center gap-3">
|
<div class="ml-auto flex items-center gap-3">
|
||||||
|
<!-- exchange-rate ("kurz") display: the admin-set EUR→alt rate(s).
|
||||||
|
Hidden when the store is EUR-only (no enabled alternatives). -->
|
||||||
|
{% set nav_cc = currencies() %}
|
||||||
|
{% if nav_cc.alts | length > 0 %}
|
||||||
|
<div class="hidden items-center gap-2 text-xs text-on-surface/70 dark:text-on-surface-dark/70 sm:flex">
|
||||||
|
<span class="font-semibold uppercase tracking-wide">{{ t(key="currency-rate", lang=lang | default(value='sk')) }}</span>
|
||||||
|
{% for a in nav_cc.alts %}
|
||||||
|
<span class="tabular-nums">1 {{ nav_cc.base.symbol }} = {{ a.rate }} {{ a.symbol }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<!-- customer profile dropdown (avatar + name + account type) -->
|
<!-- customer profile dropdown (avatar + name + account type) -->
|
||||||
{% if logged_in_customer %}
|
{% if logged_in_customer %}
|
||||||
{% include "partials/profile_menu.html" %}
|
{% include "partials/profile_menu.html" %}
|
||||||
|
|||||||
@@ -35,27 +35,32 @@
|
|||||||
{% if lang | default(value='sk') == "sk" %}<span class="text-primary dark:text-primary-dark">✓</span>{% endif %}
|
{% if lang | default(value='sk') == "sk" %}<span class="text-primary dark:text-primary-dark">✓</span>{% endif %}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{# Currency switcher. The active code is read from the `currency` cookie
|
{# Currency switcher. Only enabled (buyer-available) currencies are listed,
|
||||||
client-side (Alpine), so this partial needs no per-page server data; posting
|
from the `currencies()` snapshot; the whole section is hidden when the store
|
||||||
to /currency sets the cookie and reloads. EUR is the base; CZK prices are the
|
is EUR-only (no enabled alternatives). The active code is read from the
|
||||||
EUR price recalculated at the admin-set rate. #}
|
`currency` cookie client-side (Alpine); posting to /currency sets it. #}
|
||||||
|
{% set cc = currencies() %}
|
||||||
|
{% if cc.alts | length > 0 %}
|
||||||
<p class="mt-1 px-4 py-1.5 text-xs font-semibold uppercase tracking-wide text-on-surface/60 dark:text-on-surface-dark/60">
|
<p class="mt-1 px-4 py-1.5 text-xs font-semibold uppercase tracking-wide text-on-surface/60 dark:text-on-surface-dark/60">
|
||||||
{{ t(key="settings-currency", lang=lang | default(value='sk')) }}
|
{{ t(key="settings-currency", lang=lang | default(value='sk')) }}
|
||||||
</p>
|
</p>
|
||||||
<form method="post" action="/currency" hx-boost="false"
|
<form method="post" action="/currency" hx-boost="false"
|
||||||
x-data="{ cur: ((document.cookie.split('; ').find(function (c) { return c.indexOf('currency=') === 0 }) || 'currency=EUR').split('=')[1]) }">
|
x-data="{ cur: ((document.cookie.split('; ').find(function (c) { return c.indexOf('currency=') === 0 }) || 'currency={{ cc.base.code }}').split('=')[1]) }">
|
||||||
<input type="hidden" name="_csrf" value="{{ csrf_token() }}">
|
<input type="hidden" name="_csrf" value="{{ csrf_token() }}">
|
||||||
<button type="submit" name="currency" value="EUR" role="menuitem"
|
<button type="submit" name="currency" value="{{ cc.base.code }}" role="menuitem"
|
||||||
class="flex w-full items-center justify-between px-4 py-2 text-sm text-on-surface transition hover:bg-primary/5 hover:text-on-surface-strong focus-visible:bg-primary/10 focus-visible:text-on-surface-strong focus-visible:outline-hidden dark:text-on-surface-dark dark:hover:bg-primary-dark/5 dark:hover:text-on-surface-dark-strong">
|
class="flex w-full items-center justify-between px-4 py-2 text-sm text-on-surface transition hover:bg-primary/5 hover:text-on-surface-strong focus-visible:bg-primary/10 focus-visible:text-on-surface-strong focus-visible:outline-hidden dark:text-on-surface-dark dark:hover:bg-primary-dark/5 dark:hover:text-on-surface-dark-strong">
|
||||||
<span>EUR (€)</span>
|
<span>{{ cc.base.code }} ({{ cc.base.symbol }})</span>
|
||||||
<span x-cloak x-show="cur === 'EUR'" class="text-primary dark:text-primary-dark">✓</span>
|
<span x-cloak x-show="cur === '{{ cc.base.code }}'" class="text-primary dark:text-primary-dark">✓</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="currency" value="CZK" role="menuitem"
|
{% for a in cc.alts %}
|
||||||
|
<button type="submit" name="currency" value="{{ a.code }}" role="menuitem"
|
||||||
class="flex w-full items-center justify-between px-4 py-2 text-sm text-on-surface transition hover:bg-primary/5 hover:text-on-surface-strong focus-visible:bg-primary/10 focus-visible:text-on-surface-strong focus-visible:outline-hidden dark:text-on-surface-dark dark:hover:bg-primary-dark/5 dark:hover:text-on-surface-dark-strong">
|
class="flex w-full items-center justify-between px-4 py-2 text-sm text-on-surface transition hover:bg-primary/5 hover:text-on-surface-strong focus-visible:bg-primary/10 focus-visible:text-on-surface-strong focus-visible:outline-hidden dark:text-on-surface-dark dark:hover:bg-primary-dark/5 dark:hover:text-on-surface-dark-strong">
|
||||||
<span>CZK (Kč)</span>
|
<span>{{ a.code }} ({{ a.symbol }})</span>
|
||||||
<span x-cloak x-show="cur === 'CZK'" class="text-primary dark:text-primary-dark">✓</span>
|
<span x-cloak x-show="cur === '{{ a.code }}'" class="text-primary dark:text-primary-dark">✓</span>
|
||||||
</button>
|
</button>
|
||||||
|
{% endfor %}
|
||||||
</form>
|
</form>
|
||||||
|
{% endif %}
|
||||||
<p class="mt-1 px-4 py-1.5 text-xs font-semibold uppercase tracking-wide text-on-surface/60 dark:text-on-surface-dark/60">
|
<p class="mt-1 px-4 py-1.5 text-xs font-semibold uppercase tracking-wide text-on-surface/60 dark:text-on-surface-dark/60">
|
||||||
{{ t(key="settings-theme", lang=lang | default(value='sk')) }}
|
{{ t(key="settings-theme", lang=lang | default(value='sk')) }}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ async fn update(
|
|||||||
active.rate_e4 = Set(currency::parse_rate(&form.rate)?);
|
active.rate_e4 = Set(currency::parse_rate(&form.rate)?);
|
||||||
active.enabled = Set(is_checked(&form.enabled));
|
active.enabled = Set(is_checked(&form.enabled));
|
||||||
active.update(&ctx.db).await?;
|
active.update(&ctx.db).await?;
|
||||||
|
// Keep the navbar/settings chrome snapshot in sync with the new rate/state.
|
||||||
|
currency::refresh_snapshot(&ctx.db).await?;
|
||||||
format::redirect("/admin/currencies")
|
format::redirect("/admin/currencies")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use loco_rs::prelude::*;
|
|||||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, Set};
|
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, Set};
|
||||||
|
|
||||||
use crate::models::currencies;
|
use crate::models::currencies;
|
||||||
use crate::shared::currency::SCALE;
|
use crate::shared::currency::{self, SCALE};
|
||||||
|
|
||||||
/// `(code, symbol, default_rate_e4)` — default rate is a placeholder the admin
|
/// `(code, symbol, default_rate_e4)` — default rate is a placeholder the admin
|
||||||
/// is expected to update from the live FX rate.
|
/// is expected to update from the live FX rate.
|
||||||
@@ -45,6 +45,8 @@ impl Initializer for CurrencySeeder {
|
|||||||
.await?;
|
.await?;
|
||||||
tracing::info!(currency = code, "seeded display currency");
|
tracing::info!(currency = code, "seeded display currency");
|
||||||
}
|
}
|
||||||
|
// Prime the process-wide snapshot used by the navbar/settings chrome.
|
||||||
|
currency::refresh_snapshot(&ctx.db).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,12 @@ impl Initializer for ViewEngineInitializer {
|
|||||||
crate::shared::csrf::current_token().unwrap_or_default(),
|
crate::shared::csrf::current_token().unwrap_or_default(),
|
||||||
))
|
))
|
||||||
});
|
});
|
||||||
|
// `currencies()`: the EUR base plus enabled alternative currencies
|
||||||
|
// (from the process-wide snapshot), used by the global chrome — the
|
||||||
|
// settings-menu switcher and the navbar exchange-rate display.
|
||||||
|
tera.register_function("currencies", |_args: &HashMap<String, serde_json::Value>| {
|
||||||
|
Ok(crate::shared::currency::selectable_json())
|
||||||
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,11 @@
|
|||||||
//! [`Currency`] resolved per request converts EUR cents into the chosen currency
|
//! [`Currency`] resolved per request converts EUR cents into the chosen currency
|
||||||
//! for display only — the cart logic, orders and admin stay in EUR.
|
//! for display only — the cart logic, orders and admin stay in EUR.
|
||||||
|
|
||||||
|
use std::sync::RwLock;
|
||||||
|
|
||||||
use axum_extra::extract::cookie::CookieJar;
|
use axum_extra::extract::cookie::CookieJar;
|
||||||
use loco_rs::prelude::*;
|
use loco_rs::prelude::*;
|
||||||
|
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder};
|
||||||
|
|
||||||
use crate::models::currencies;
|
use crate::models::currencies;
|
||||||
use crate::shared::money::format_price;
|
use crate::shared::money::format_price;
|
||||||
@@ -99,6 +102,65 @@ pub async fn resolve(ctx: &AppContext, jar: &CookieJar) -> Currency {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One enabled, buyer-selectable alternative currency in the process-wide
|
||||||
|
/// snapshot below.
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Selectable {
|
||||||
|
code: String,
|
||||||
|
symbol: String,
|
||||||
|
rate_e4: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process-wide snapshot of the enabled alternative currencies, so the global
|
||||||
|
/// chrome (the settings-menu switcher and the navbar rate) can be rendered via a
|
||||||
|
/// Tera function without a per-request DB hit. Loaded at boot by
|
||||||
|
/// `initializers::currency_seeder` and refreshed by the admin on every edit (see
|
||||||
|
/// [`refresh_snapshot`]). EUR (the base) is implicit and never listed here.
|
||||||
|
static ENABLED: RwLock<Vec<Selectable>> = RwLock::new(Vec::new());
|
||||||
|
|
||||||
|
/// Reload the [`ENABLED`] snapshot from the database. Call at boot and after any
|
||||||
|
/// admin change to a currency's rate/enabled state.
|
||||||
|
pub async fn refresh_snapshot<C: sea_orm::ConnectionTrait>(db: &C) -> Result<()> {
|
||||||
|
let rows = currencies::Entity::find()
|
||||||
|
.filter(currencies::Column::Enabled.eq(true))
|
||||||
|
.order_by_asc(currencies::Column::Code)
|
||||||
|
.all(db)
|
||||||
|
.await?;
|
||||||
|
let list = rows
|
||||||
|
.into_iter()
|
||||||
|
.map(|m| Selectable {
|
||||||
|
code: m.code,
|
||||||
|
symbol: m.symbol,
|
||||||
|
rate_e4: m.rate_e4,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
*ENABLED.write().unwrap() = list;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The selectable currencies for templates (the Tera `currencies()` function):
|
||||||
|
/// the EUR base plus every enabled alternative, each with a human rate string.
|
||||||
|
/// `alts` is empty when the store is effectively EUR-only.
|
||||||
|
#[must_use]
|
||||||
|
pub fn selectable_json() -> serde_json::Value {
|
||||||
|
let alts: Vec<serde_json::Value> = ENABLED
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.map(|s| {
|
||||||
|
serde_json::json!({
|
||||||
|
"code": s.code,
|
||||||
|
"symbol": s.symbol,
|
||||||
|
"rate": format_rate(s.rate_e4),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
serde_json::json!({
|
||||||
|
"base": { "code": BASE_CODE, "symbol": BASE_SYMBOL },
|
||||||
|
"alts": alts,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse an exchange rate typed in major units ("25", "25.3", "25,30",
|
/// Parse an exchange rate typed in major units ("25", "25.3", "25,30",
|
||||||
/// "25.3045") into `rate_e4` (×10000). Rejects negatives and >4 decimals.
|
/// "25.3045") into `rate_e4` (×10000). Rejects negatives and >4 decimals.
|
||||||
pub fn parse_rate(value: &str) -> Result<i64> {
|
pub fn parse_rate(value: &str) -> Result<i64> {
|
||||||
|
|||||||
Reference in New Issue
Block a user