feat: Konto-Sicherheit gebündelt + Einstellungen aufgeräumt
- Neue Seite „Mein Konto" (/konto, für alle Nutzer): eigenes Passwort ändern (inkl. Richtlinien-Anzeige) + Zwei-Faktor verwalten; erreichbar über den Nutzer-Chip in der Seitenleisten-Fußzeile - Einstellungen: 2FA-Karte entfernt (liegt jetzt bei „Mein Konto"), Karten in ein mehrspaltiges Raster gruppiert - Menüpunkt „Einstellungen" unter die Überschrift „System" verschoben (für alle Nutzer sichtbar; „Benutzer" bleibt Admin-only) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+83
-91
@@ -1,5 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSettings } from "../settings";
|
||||
import { useAuth } from "../auth";
|
||||
@@ -53,7 +52,7 @@ export default function Settings() {
|
||||
<h1>{t("settings.title")}</h1>
|
||||
|
||||
{flagged.length > 0 && (
|
||||
<div className="card" style={{ maxWidth: 520, marginBottom: "var(--sp-4)", borderColor: "var(--danger)" }}>
|
||||
<div className="card" style={{ marginBottom: "var(--sp-4)", borderColor: "var(--danger)" }}>
|
||||
<h2 style={{ display: "flex", alignItems: "center", gap: "var(--sp-2)", marginTop: 0 }}>
|
||||
<Icon name="alert" size={18} /> {t("settings.keywordReview")}
|
||||
</h2>
|
||||
@@ -76,101 +75,94 @@ export default function Settings() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="card" style={{ maxWidth: 520 }}>
|
||||
<label>
|
||||
<span>{t("settings.baseCurrency")}</span>
|
||||
<select value={s.base_currency} onChange={(e) => set("base_currency", e.target.value)}>
|
||||
<option value="EUR">Euro (EUR)</option>
|
||||
<option value="CHF">Schweizer Franken (CHF)</option>
|
||||
</select>
|
||||
</label>
|
||||
<p className="muted small" style={{ marginTop: -8 }}>{t("settings.baseCurrencyHint")}</p>
|
||||
<div className="settings-grid">
|
||||
<div className="card">
|
||||
<h2 style={{ marginTop: 0 }}>{t("settings.general")}</h2>
|
||||
<label>
|
||||
<span>{t("settings.baseCurrency")}</span>
|
||||
<select value={s.base_currency} onChange={(e) => set("base_currency", e.target.value)}>
|
||||
<option value="EUR">Euro (EUR)</option>
|
||||
<option value="CHF">Schweizer Franken (CHF)</option>
|
||||
</select>
|
||||
</label>
|
||||
<p className="muted small" style={{ marginTop: -8 }}>{t("settings.baseCurrencyHint")}</p>
|
||||
|
||||
<label>
|
||||
<span>{t("settings.locale")}</span>
|
||||
<select value={s.locale} onChange={(e) => set("locale", e.target.value)}>
|
||||
<option value="de">{t("settings.languageDe")}</option>
|
||||
<option value="en">{t("settings.languageEn")}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>{t("settings.locale")}</span>
|
||||
<select value={s.locale} onChange={(e) => set("locale", e.target.value)}>
|
||||
<option value="de">{t("settings.languageDe")}</option>
|
||||
<option value="en">{t("settings.languageEn")}</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span>{t("settings.theme")}</span>
|
||||
<select value={s.theme} onChange={(e) => set("theme", e.target.value)}>
|
||||
<option value="system">{t("settings.themeSystem")}</option>
|
||||
<option value="light">{t("settings.themeLight")}</option>
|
||||
<option value="dark">{t("settings.themeDark")}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>{t("settings.theme")}</span>
|
||||
<select value={s.theme} onChange={(e) => set("theme", e.target.value)}>
|
||||
<option value="system">{t("settings.themeSystem")}</option>
|
||||
<option value="light">{t("settings.themeLight")}</option>
|
||||
<option value="dark">{t("settings.themeDark")}</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span>{t("settings.budgetStartDay")}</span>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="31"
|
||||
value={s.budget_start_day}
|
||||
onChange={(e) => {
|
||||
const day = Math.max(1, Math.min(31, Number(e.target.value) || 1));
|
||||
set("budget_start_day", String(day));
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<p className="muted small" style={{ marginTop: -8 }}>{t("settings.budgetStartDayHint")}</p>
|
||||
</div>
|
||||
|
||||
<div className="card" style={{ maxWidth: 520, marginTop: "var(--sp-4)" }}>
|
||||
<h2 style={{ display: "flex", alignItems: "center", gap: "var(--sp-2)", marginTop: 0 }}>
|
||||
<Icon name="shield" size={18} /> {t("settings.security")}
|
||||
</h2>
|
||||
<p className="muted small">{t("settings.securityHint")}</p>
|
||||
<Link className="btn" to="/zwei-faktor">
|
||||
<Icon name="smartphone" size={15} /> {t("settings.twoFactor")}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{isAdmin && <PolicyCard />}
|
||||
|
||||
<div className="card" style={{ maxWidth: 520, marginTop: "var(--sp-4)" }}>
|
||||
<h2>{t("settings.backup")}</h2>
|
||||
<p className="muted small">{t("settings.backupHint")}</p>
|
||||
<button className="btn primary" onClick={() => dl(api.exportBackupJson)}>
|
||||
<Icon name="download" size={15} /> {t("settings.exportBackup")}
|
||||
</button>
|
||||
|
||||
<p className="muted small" style={{ marginTop: "var(--sp-4)" }}>{t("settings.exportHint")}</p>
|
||||
<div className="field-inline" style={{ flexWrap: "wrap" }}>
|
||||
<button className="btn" onClick={() => dl(api.exportTransactionsCsv)}>
|
||||
<Icon name="download" size={15} /> {t("settings.exportTxnCsv")}
|
||||
</button>
|
||||
<button className="btn" onClick={() => dl(api.exportTransactionsJson)}>
|
||||
<Icon name="download" size={15} /> {t("settings.exportTxnJson")}
|
||||
</button>
|
||||
<button className="btn" onClick={() => dl(api.exportAllJson)}>
|
||||
<Icon name="download" size={15} /> {t("settings.exportAll")}
|
||||
</button>
|
||||
<label>
|
||||
<span>{t("settings.budgetStartDay")}</span>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="31"
|
||||
value={s.budget_start_day}
|
||||
onChange={(e) => {
|
||||
const day = Math.max(1, Math.min(31, Number(e.target.value) || 1));
|
||||
set("budget_start_day", String(day));
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<p className="muted small" style={{ marginTop: -8 }}>{t("settings.budgetStartDayHint")}</p>
|
||||
</div>
|
||||
|
||||
{isAdmin && (
|
||||
<>
|
||||
<p className="muted small" style={{ marginTop: "var(--sp-4)" }}>{t("settings.dbBackupHint")}</p>
|
||||
<div className="field-inline" style={{ flexWrap: "wrap" }}>
|
||||
<button className="btn" onClick={() => dl(api.downloadDbBackup)}>
|
||||
<Icon name="download" size={15} /> {t("settings.dbBackup")}
|
||||
</button>
|
||||
<button className="btn" onClick={() => dl(api.exportSystemBackupJson)}>
|
||||
<Icon name="download" size={15} /> {t("settings.systemBackup")}
|
||||
</button>
|
||||
</div>
|
||||
<div className="alert warn" style={{ marginTop: "var(--sp-3)" }}>
|
||||
<Icon name="alert" size={16} />
|
||||
<span>{t("settings.systemBackupWarning")}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{isAdmin && <PolicyCard />}
|
||||
|
||||
<RestoreCard />
|
||||
<div className="card">
|
||||
<h2 style={{ marginTop: 0 }}>{t("settings.backup")}</h2>
|
||||
<p className="muted small">{t("settings.backupHint")}</p>
|
||||
<button className="btn primary" onClick={() => dl(api.exportBackupJson)}>
|
||||
<Icon name="download" size={15} /> {t("settings.exportBackup")}
|
||||
</button>
|
||||
|
||||
<p className="muted small" style={{ marginTop: "var(--sp-4)" }}>{t("settings.exportHint")}</p>
|
||||
<div className="field-inline" style={{ flexWrap: "wrap" }}>
|
||||
<button className="btn" onClick={() => dl(api.exportTransactionsCsv)}>
|
||||
<Icon name="download" size={15} /> {t("settings.exportTxnCsv")}
|
||||
</button>
|
||||
<button className="btn" onClick={() => dl(api.exportTransactionsJson)}>
|
||||
<Icon name="download" size={15} /> {t("settings.exportTxnJson")}
|
||||
</button>
|
||||
<button className="btn" onClick={() => dl(api.exportAllJson)}>
|
||||
<Icon name="download" size={15} /> {t("settings.exportAll")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isAdmin && (
|
||||
<>
|
||||
<p className="muted small" style={{ marginTop: "var(--sp-4)" }}>{t("settings.dbBackupHint")}</p>
|
||||
<div className="field-inline" style={{ flexWrap: "wrap" }}>
|
||||
<button className="btn" onClick={() => dl(api.downloadDbBackup)}>
|
||||
<Icon name="download" size={15} /> {t("settings.dbBackup")}
|
||||
</button>
|
||||
<button className="btn" onClick={() => dl(api.exportSystemBackupJson)}>
|
||||
<Icon name="download" size={15} /> {t("settings.systemBackup")}
|
||||
</button>
|
||||
</div>
|
||||
<div className="alert warn" style={{ marginTop: "var(--sp-3)" }}>
|
||||
<Icon name="alert" size={16} />
|
||||
<span>{t("settings.systemBackupWarning")}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<RestoreCard />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user