diff --git a/backend/app/routers/settings.py b/backend/app/routers/settings.py index 3259241..67fd3dc 100644 --- a/backend/app/routers/settings.py +++ b/backend/app/routers/settings.py @@ -20,6 +20,11 @@ BUDGET_START_DAY_KEY = "budget_start_day" LOCALE_KEY = "locale" THEME_KEY = "theme" DATE_FORMAT_KEY = "date_format" +# Welche Waehrungen in Auswahlfeldern erscheinen (Komma-getrennt). +# Beschraenkt nur die ANZEIGE: Gueltig bleibt alles, was einen +# EZB-Referenzkurs hat – sonst verloere ein Konto seine Waehrung, +# nur weil sie hier gerade nicht angehakt ist. +ACTIVE_CURRENCIES_KEY = "active_currencies" def known_defaults() -> dict[str, str]: @@ -29,6 +34,7 @@ def known_defaults() -> dict[str, str]: LOCALE_KEY: "de", THEME_KEY: "system", DATE_FORMAT_KEY: "de", + ACTIVE_CURRENCIES_KEY: "EUR,CHF,USD", } diff --git a/backend/tests/test_abos.py b/backend/tests/test_abos.py index c1a4214..a705a05 100644 --- a/backend/tests/test_abos.py +++ b/backend/tests/test_abos.py @@ -161,3 +161,20 @@ def test_fremdes_konto_wird_nicht_angenommen(admin): "name": "Fremdzugriff", "amount_cents": 100, "currency": "EUR", "interval_months": 1, "account_id": fremd["id"]}) assert r.status_code == 404, r.text + + +def test_angebotene_waehrungen_sind_eine_einstellung(admin): + """Die Liste beschraenkt nur die ANZEIGE. Gueltig bleibt alles mit Kurs – + sonst verloere ein Konto seine Waehrung, weil sie gerade nicht angehakt + ist.""" + rows = {r["key"]: r["value"] for r in admin.get("/api/settings").json()} + assert rows["active_currencies"] == "EUR,CHF,USD", "Vorgabe passt nicht" + + r = admin.put("/api/settings/active_currencies", params={"value": "EUR,CHF"}) + assert r.status_code == 200, r.text + try: + # Trotz Abwahl weiter anlegbar: Die Einstellung ist keine Sperre. + abo = _abo(admin, currency="USD", amount_cents=500) + assert abo["currency"] == "USD" + finally: + admin.put("/api/settings/active_currencies", params={"value": "EUR,CHF,USD"}) diff --git a/web/src/components/CurrencySelect.jsx b/web/src/components/CurrencySelect.jsx new file mode 100644 index 0000000..49d9e16 --- /dev/null +++ b/web/src/components/CurrencySelect.jsx @@ -0,0 +1,29 @@ +import { useSettings } from "../settings"; +import { angeboteneWaehrungen, waehrungLabel } from "../waehrungen"; + +/** + * Währungs-Auswahl. Zeigt die eingestellten Währungen – und immer die gerade + * gesetzte dazu. + * + * Das Letzte ist der Punkt: Ein Konto in Dollar vor einem Feld ohne Dollar + * wechselte beim nächsten Speichern stillschweigend die Währung, und der + * Saldo wäre danach still falsch. + */ +export default function CurrencySelect({ value, onChange, disabled = false, ...rest }) { + const { activeCurrencies } = useSettings(); + const liste = angeboteneWaehrungen(activeCurrencies, value); + return ( + + ); +} diff --git a/web/src/finance.css b/web/src/finance.css index a7342d3..d210af3 100644 --- a/web/src/finance.css +++ b/web/src/finance.css @@ -1646,27 +1646,38 @@ tr.row-dup { display: flex; flex-direction: column; } +/* Als Gitter und nicht als Reihe: Auf dem Telefon rutscht der Betrag unter den + Namen, statt vier Spalten in 375 Pixel zu quetschen. Mit Reihe blieb vom + Namen "Sal..." uebrig und von der Kategorie ein Punkt. */ .abo-zeile { - display: flex; + display: grid; + grid-template-columns: auto 1fr auto auto; + grid-template-areas: "symbol mitte betrag knoepfe"; align-items: center; gap: var(--sp-3); padding: var(--sp-2) 0; border-bottom: 1px solid var(--border); } +.abo-zeile > :nth-child(1) { grid-area: symbol; } +.abo-mitte { grid-area: mitte; } +.abo-betrag { grid-area: betrag; } +.abo-knoepfe { grid-area: knoepfe; } .abo-zeile:last-child { border-bottom: none; } /* Gekündigtes bleibt lesbar, tritt aber zurück – es zählt nicht mehr mit. */ .abo-beendet { opacity: 0.55; } -.abo-mitte { flex: 1; min-width: 0; } +.abo-mitte { min-width: 0; } .abo-kopf { display: flex; align-items: baseline; - gap: var(--sp-2); + flex-wrap: wrap; + gap: 2px var(--sp-2); min-width: 0; } .abo-unten { display: flex; - gap: 5px; + flex-wrap: wrap; + gap: 0 5px; min-width: 0; margin-top: 1px; } @@ -1688,3 +1699,53 @@ tr.row-dup { padding-left: var(--sp-3); margin-bottom: var(--sp-3); } + +@media (max-width: 620px) { + /* Betrag unter den Namen, Knoepfe bleiben rechts oben. So bekommt der Name + die volle Breite – auf dem Telefon ist er das Einzige, woran man die Zeile + erkennt. */ + .abo-zeile { + grid-template-columns: auto 1fr auto; + grid-template-areas: + "symbol mitte knoepfe" + "symbol betrag knoepfe"; + align-items: start; + row-gap: 2px; + padding: var(--sp-3) 0; + } + .abo-zeile > :nth-child(1), + .abo-knoepfe { align-self: center; } + /* Waagrecht statt untereinander: Unter dem Namen ist Breite da, Hoehe nicht. */ + .abo-betrag { + flex-direction: row; + align-items: baseline; + flex-wrap: wrap; + gap: 0 var(--sp-2); + } + .abo-summe { gap: var(--sp-3) var(--sp-5); } + .abo-summe-zahl { font-size: 1.2rem; } +} + +/* Waehrungs-Auswahl in den Einstellungen: ein Gitter aus Kacheln statt einer + Liste mit dreissig Zeilen. */ +.waehrung-gitter { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); + gap: 6px; + margin-bottom: var(--sp-2); +} +.waehrung-kachel { + display: flex; + align-items: center; + gap: 6px; + margin: 0; + padding: 6px 8px; + border: 1px solid var(--border); + border-radius: var(--radius-sm, 6px); + cursor: pointer; + font-size: 0.85rem; + white-space: nowrap; +} +.waehrung-kachel.an { border-color: var(--accent, var(--border-strong)); } +.waehrung-kachel input { width: auto; margin: 0; } +.waehrung-kachel:has(input:disabled) { opacity: 0.6; cursor: default; } diff --git a/web/src/locales/de.json b/web/src/locales/de.json index 0549adc..29a50e5 100644 --- a/web/src/locales/de.json +++ b/web/src/locales/de.json @@ -77,6 +77,8 @@ "general": "Darstellung & Sprache", "baseCurrency": "Hauptwährung", "baseCurrencyHint": "Währung für Budget-Summen und Dashboard-Gesamtwerte.", + "activeCurrencies": "Angebotene Währungen", + "activeCurrenciesHint": "Nur diese stehen in den Auswahlfeldern für Konten und Verträge. Alle übrigen bleiben gültig – wer eine davon schon benutzt, sieht sie dort weiterhin.", "locale": "Sprache", "theme": "Darstellung", "themeSystem": "Automatisch (System)", diff --git a/web/src/locales/en.json b/web/src/locales/en.json index 4200ce1..71e852b 100644 --- a/web/src/locales/en.json +++ b/web/src/locales/en.json @@ -77,6 +77,8 @@ "general": "Appearance & language", "baseCurrency": "Base currency", "baseCurrencyHint": "Currency for budget totals and dashboard aggregates.", + "activeCurrencies": "Offered currencies", + "activeCurrenciesHint": "Only these appear in the currency pickers for accounts and contracts. All others stay valid – if one is already in use, it still shows up there.", "locale": "Language", "theme": "Appearance", "themeSystem": "Automatic (system)", diff --git a/web/src/pages/Abos.jsx b/web/src/pages/Abos.jsx index 2483778..23de2e4 100644 --- a/web/src/pages/Abos.jsx +++ b/web/src/pages/Abos.jsx @@ -11,7 +11,7 @@ import IconPicker, { CATEGORY_ICONS } from "../components/IconPicker"; import CategorySelect from "../components/CategorySelect"; import AccountSelect from "../components/AccountSelect"; import { gruppenFuer } from "../kategorien"; -import { WAEHRUNGEN } from "../waehrungen"; +import CurrencySelect from "../components/CurrencySelect"; import { centsToInput, formatDate, formatMoney, parseAmountToCents } from "../format"; // Muss zu INTERVALL_MONATE im Server passen. @@ -334,16 +334,10 @@ export default function Abos() { diff --git a/web/src/pages/Accounts.jsx b/web/src/pages/Accounts.jsx index bfb8953..0f9a78b 100644 --- a/web/src/pages/Accounts.jsx +++ b/web/src/pages/Accounts.jsx @@ -11,7 +11,7 @@ import Modal from "../components/Modal"; import IconPicker, { ACCOUNT_ICONS } from "../components/IconPicker"; import CategorySelect from "../components/CategorySelect"; import { gruppenFuer } from "../kategorien"; -import { WAEHRUNGEN } from "../waehrungen"; +import CurrencySelect from "../components/CurrencySelect"; import { centsToInput, formatDate, formatMoney, parseAmountToCents } from "../format"; const ACCOUNT_TYPES = ["girokonto", "sparkonto", "bargeld", "kreditkarte", "depot", "sonstiges"]; @@ -470,9 +470,8 @@ export default function Accounts() {
{t("settings.baseCurrencyHint")}
+ +{t("settings.activeCurrenciesHint")}
+