Add searchable timezone settings override for reminder scheduling

This commit is contained in:
Daniel Volz
2026-04-10 21:08:16 +02:00
parent 0d2b21199e
commit 401228699f
16 changed files with 183 additions and 13 deletions
+7
View File
@@ -7,6 +7,9 @@ import { useTranslation } from "react-i18next";
import { log } from "../utils/logger";
export interface Settings {
timezone: string;
availableTimezones: string[];
serverTimezone: string;
emailEnabled: boolean;
notificationEmail: string;
reminderDaysBefore: number;
@@ -58,6 +61,9 @@ export interface Settings {
export type SettingsLoadError = "auth" | "forbidden" | "request" | null;
const defaultSettings: Settings = {
timezone: "",
availableTimezones: [],
serverTimezone: "UTC",
emailEnabled: false,
notificationEmail: "",
reminderDaysBefore: 7,
@@ -243,6 +249,7 @@ export function useSettings(): UseSettingsReturn {
const repeatDailyReminders = hasAnyStockReminder ? settingsToSave.repeatDailyReminders : false;
return {
timezone: settingsToSave.timezone,
emailEnabled: effectiveEmailEnabled,
notificationEmail: settingsToSave.notificationEmail,
reminderDaysBefore: settingsToSave.reminderDaysBefore,
+6
View File
@@ -389,6 +389,12 @@
"title": "Sprache",
"select": "Sprache auswählen"
},
"timezone": {
"select": "Zeitzone",
"hint": "IANA-Zeitzone wählen. Wenn gesetzt, überschreibt sie die Server-TZ für deine Reminder-Zeitpunkte.",
"useServerDefault": "Server-Standard nutzen",
"currentServerTz": "Server-Standardzeitzone: {{timezone}}"
},
"apiKey": {
"title": "API-Zugriff",
"generateTitle": "API-Key erzeugen",
+6
View File
@@ -389,6 +389,12 @@
"title": "Language",
"select": "Select language"
},
"timezone": {
"select": "Timezone",
"hint": "Select an IANA timezone. When set, this overrides server TZ for your reminder timing.",
"useServerDefault": "Use server default",
"currentServerTz": "Server default timezone: {{timezone}}"
},
"apiKey": {
"title": "API Access",
"generateTitle": "Generate API key",
+46
View File
@@ -116,6 +116,23 @@ export function SettingsPage() {
const automaticStockCalculationId = "settings-stock-calculation-automatic";
const manualStockCalculationId = "settings-stock-calculation-manual";
const timezoneSuggestions =
settings.availableTimezones.length > 0
? settings.availableTimezones
: (() => {
try {
type IntlWithSupportedValuesOf = typeof Intl & {
supportedValuesOf?: (key: string) => string[];
};
const intlWithSupportedValues = Intl as IntlWithSupportedValuesOf;
if (typeof intlWithSupportedValues.supportedValuesOf === "function") {
return intlWithSupportedValues.supportedValuesOf("timeZone");
}
} catch {
// fall through
}
return [settings.serverTimezone || "UTC", "UTC"];
})();
return (
<section className="grid">
@@ -160,6 +177,35 @@ export function SettingsPage() {
<option value="de">🇩🇪 Deutsch</option>
</select>
</label>
<div className="setting-row compact" style={{ marginTop: "12px" }}>
<div className="setting-label">
<span>{t("settings.timezone.select")}</span>
<span className="info-tooltip small" data-tooltip={t("settings.timezone.hint")}>
</span>
</div>
<div className="setting-actions" style={{ margin: 0, flexWrap: "nowrap", gap: "8px" }}>
<input
type="text"
className="select-field language-select"
value={settings.timezone}
onChange={(e) => setSettings({ ...settings, timezone: e.target.value })}
list="settings-timezone-suggestions"
placeholder={settings.serverTimezone || "UTC"}
/>
<datalist id="settings-timezone-suggestions">
{timezoneSuggestions.map((zone) => (
<option key={zone} value={zone} />
))}
</datalist>
<button type="button" className="ghost" onClick={() => setSettings({ ...settings, timezone: "" })}>
{t("settings.timezone.useServerDefault")}
</button>
</div>
</div>
<p className="hint-text" style={{ marginTop: "8px" }}>
{t("settings.timezone.currentServerTz", { timezone: settings.serverTimezone || "UTC" })}
</p>
</article>
<article className="card" data-testid="settings-notification-card">