diff --git a/assets/i18n/en/main.ftl b/assets/i18n/en/main.ftl index 2fc1023..a36e86a 100644 --- a/assets/i18n/en/main.ftl +++ b/assets/i18n/en/main.ftl @@ -476,8 +476,9 @@ bank-amount = Amount admin-shipping = Shipping admin-shipping-desc = set the price and availability of each delivery option. 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. +currency-rate = Rate exchange-rate = Exchange rate exchange-rate-hint = { $code } prices are the { $base } price recalculated at this rate. currency-enabled = Available to customers diff --git a/assets/i18n/sk/main.ftl b/assets/i18n/sk/main.ftl index 1e03de6..4916649 100644 --- a/assets/i18n/sk/main.ftl +++ b/assets/i18n/sk/main.ftl @@ -476,8 +476,9 @@ bank-amount = Suma admin-shipping = Doprava admin-shipping-desc = nastaviť cenu a dostupnosť jednotlivých možností dopravy. 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. +currency-rate = Kurz exchange-rate = Výmenný kurz exchange-rate-hint = ceny v { $code } sa prepočítajú z ceny v { $base } týmto kurzom. currency-enabled = Dostupná pre zákazníkov diff --git a/assets/views/base.html b/assets/views/base.html index 4db41b4..f18d2f2 100644 --- a/assets/views/base.html +++ b/assets/views/base.html @@ -104,8 +104,19 @@ {% endif %} - +
+ + {% set nav_cc = currencies() %} + {% if nav_cc.alts | length > 0 %} + + {% endif %} {% if logged_in_customer %} {% include "partials/profile_menu.html" %} diff --git a/assets/views/partials/settings_dropdown.html b/assets/views/partials/settings_dropdown.html index 788a7f0..be9ec32 100644 --- a/assets/views/partials/settings_dropdown.html +++ b/assets/views/partials/settings_dropdown.html @@ -35,27 +35,32 @@ {% if lang | default(value='sk') == "sk" %}{% endif %} - {# Currency switcher. The active code is read from the `currency` cookie - client-side (Alpine), so this partial needs no per-page server data; posting - to /currency sets the cookie and reloads. EUR is the base; CZK prices are the - EUR price recalculated at the admin-set rate. #} + {# Currency switcher. Only enabled (buyer-available) currencies are listed, + from the `currencies()` snapshot; the whole section is hidden when the store + is EUR-only (no enabled alternatives). The active code is read from the + `currency` cookie client-side (Alpine); posting to /currency sets it. #} + {% set cc = currencies() %} + {% if cc.alts | length > 0 %}

{{ t(key="settings-currency", lang=lang | default(value='sk')) }}

+ x-data="{ cur: ((document.cookie.split('; ').find(function (c) { return c.indexOf('currency=') === 0 }) || 'currency={{ cc.base.code }}').split('=')[1]) }"> - - + {% endfor %}
+ {% endif %}

{{ t(key="settings-theme", lang=lang | default(value='sk')) }}

diff --git a/src/controllers/admin_currencies.rs b/src/controllers/admin_currencies.rs index e881236..c5d8cd0 100644 --- a/src/controllers/admin_currencies.rs +++ b/src/controllers/admin_currencies.rs @@ -82,6 +82,8 @@ async fn update( active.rate_e4 = Set(currency::parse_rate(&form.rate)?); active.enabled = Set(is_checked(&form.enabled)); 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") } diff --git a/src/initializers/currency_seeder.rs b/src/initializers/currency_seeder.rs index bd6c868..4d7b932 100644 --- a/src/initializers/currency_seeder.rs +++ b/src/initializers/currency_seeder.rs @@ -10,7 +10,7 @@ use loco_rs::prelude::*; use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, Set}; 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 /// is expected to update from the live FX rate. @@ -45,6 +45,8 @@ impl Initializer for CurrencySeeder { .await?; 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(()) } } diff --git a/src/initializers/view_engine.rs b/src/initializers/view_engine.rs index 1c19ac2..3295705 100644 --- a/src/initializers/view_engine.rs +++ b/src/initializers/view_engine.rs @@ -54,6 +54,12 @@ impl Initializer for ViewEngineInitializer { 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| { + Ok(crate::shared::currency::selectable_json()) + }); Ok(()) })?; diff --git a/src/shared/currency.rs b/src/shared/currency.rs index 0d110af..c27c4ad 100644 --- a/src/shared/currency.rs +++ b/src/shared/currency.rs @@ -7,8 +7,11 @@ //! [`Currency`] resolved per request converts EUR cents into the chosen currency //! for display only — the cart logic, orders and admin stay in EUR. +use std::sync::RwLock; + use axum_extra::extract::cookie::CookieJar; use loco_rs::prelude::*; +use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder}; use crate::models::currencies; 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> = 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(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 = 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", /// "25.3045") into `rate_e4` (×10000). Rejects negatives and >4 decimals. pub fn parse_rate(value: &str) -> Result {