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 %}
+
+ {{ t(key="currency-rate", lang=lang | default(value='sk')) }}
+ {% for a in nav_cc.alts %}
+ 1 {{ nav_cc.base.symbol }} = {{ a.rate }} {{ a.symbol }}
+ {% endfor %}
+
+ {% 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')) }}
+ {% 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 {