/* biome-ignore-all lint/a11y/noLabelWithoutControl: settings rows use label-styled text with adjacent custom toggle controls */ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { ConfirmModal, ExportModal } from "../components"; import { useAppContext } from "../context"; import { getSystemLocale } from "../utils/formatters"; export function SettingsPage() { const { t, i18n } = useTranslation(); const [apiKeyToken, setApiKeyToken] = useState(""); const [apiKeyGenerating, setApiKeyGenerating] = useState(false); const [apiKeyCopied, setApiKeyCopied] = useState(false); const [apiKeyError, setApiKeyError] = useState(null); const { settings, savedSettings, setSettings, settingsLoading, settingsSaving, settingsSaved, settingsLoadError, // Email testing testEmail, testingEmail, testEmailResult, // Shoutrrr testing testShoutrrr, testingShoutrrr, testShoutrrrResult, // Export/Import exporting, importing, showExportModal, setShowExportModal, handleExport, handleImportFileSelect, showImportConfirm, setShowImportConfirm, setPendingImportData, handleImportConfirm, importResult, setImportResult, meds, } = useAppContext(); const [timezoneTouched, setTimezoneTouched] = useState(false); const [timezoneDraft, setTimezoneDraft] = useState(""); const hasExistingData = meds.length > 0; let emailUnavailableReason: string | null = null; if (settingsLoadError === "auth") { emailUnavailableReason = t("settings.email.loadErrorAuth"); } else if (settingsLoadError === "forbidden") { emailUnavailableReason = t("settings.email.loadErrorForbidden"); } else if (settingsLoadError === "request") { emailUnavailableReason = t("settings.email.loadErrorGeneric"); } else if (!settings.smtpHost) { emailUnavailableReason = t("settings.email.serverNotConfigured"); } const generateApiKey = async () => { setApiKeyGenerating(true); setApiKeyError(null); setApiKeyCopied(false); try { const response = await fetch("/api/auth/api-keys", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ name: "Default API Key", scope: "write", }), }); const data = await response.json().catch(() => ({})); if (!response.ok || typeof data?.token !== "string" || !data.token) { setApiKeyError(t("settings.apiKey.generateError")); return; } setApiKeyToken(data.token); } catch { setApiKeyError(t("settings.apiKey.generateError")); } finally { setApiKeyGenerating(false); } }; const copyApiKeyToken = async () => { if (!apiKeyToken) return; const markCopied = () => { setApiKeyCopied(true); setTimeout(() => setApiKeyCopied(false), 2000); }; if (navigator.clipboard?.writeText) { try { await navigator.clipboard.writeText(apiKeyToken); markCopied(); return; } catch { // Fall back to textarea-based copy. } } const textarea = document.createElement("textarea"); textarea.value = apiKeyToken; textarea.style.position = "fixed"; textarea.style.opacity = "0"; document.body.appendChild(textarea); textarea.select(); try { document.execCommand("copy"); markCopied(); } finally { document.body.removeChild(textarea); } }; const automaticStockCalculationId = "settings-stock-calculation-automatic"; const manualStockCalculationId = "settings-stock-calculation-manual"; useEffect(() => { setTimezoneDraft(settings.timezone); }, [settings.timezone]); const commitTimezoneDraft = () => { if (timezoneDraft === settings.timezone) { return; } setTimezoneTouched(true); setSettings((prev) => ({ ...prev, timezone: timezoneDraft })); }; const savedTimezone = savedSettings?.timezone ?? settings.timezone; const timezoneChanged = settings.timezone !== savedTimezone; const showTimezoneSaving = timezoneTouched && timezoneChanged && settingsSaving; const showTimezoneSaved = timezoneTouched && !timezoneChanged && settingsSaved; let timezoneStatusText = ""; if (showTimezoneSaving) { timezoneStatusText = t("settings.timezone.saving"); } else if (showTimezoneSaved) { timezoneStatusText = t("settings.timezone.saved"); } const timezoneStatusClassName = showTimezoneSaved ? "timezone-status timezone-status-saved" : "timezone-status"; const availableTimezones = Array.isArray(settings.availableTimezones) ? settings.availableTimezones : []; const timezoneSuggestions = availableTimezones.length > 0 ? 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 (
{settingsLoading ? (
{t("settings.loading")}
) : (
{/* Language */}

{t("settings.language.title")}

{t("settings.timezone.select")} ⓘ
{ setTimezoneDraft(e.target.value); }} onBlur={commitTimezoneDraft} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); (e.currentTarget as HTMLInputElement).blur(); } }} list="settings-timezone-suggestions" placeholder={settings.serverTimezone || "UTC"} /> {timezoneSuggestions.map((zone) => (

{timezoneStatusText || " "}

{t("settings.timezone.currentServerTz", { timezone: settings.serverTimezone || "UTC" })}

{t("settings.apiKey.title")}

{t("settings.apiKey.generateTitle")} {t("settings.apiKey.generateDesc")}
{apiKeyToken ? (
{t("settings.apiKey.currentToken")}
(e.target as HTMLInputElement).select()} />

{t("settings.apiKey.copyHint")}

) : null} {apiKeyError ?

{apiKeyError}

: null}
{/* Notifications */}

{t("settings.notifications.title")}

{t("settings.notifications.channels")}

{t("settings.notifications.email")}
{t("settings.notifications.push")}
{t("settings.notifications.stockReminders")}
{t("settings.notifications.intakeReminders")}
{t("settings.notifications.prescriptionReminders")}
{!settings.emailEnabled && !settings.shoutrrrEnabled && (

{t("settings.notifications.enableHint")}

)} {/* Skip reminders for taken doses */}
{/* Repeat reminders for missed doses */}
{/* Reminder interval (only shown when repeat is enabled) */} {settings.repeatRemindersEnabled && ( <>
setSettings({ ...settings, reminderRepeatIntervalMinutes: parseInt(e.target.value, 10) || 30 }) } style={{ width: "80px", textAlign: "center" }} />
{ const val = parseInt(e.target.value, 10); if (!Number.isNaN(val)) { setSettings({ ...settings, maxNaggingReminders: Math.max(1, Math.min(20, val)) }); } }} style={{ width: "80px", textAlign: "center" }} />
)}

{t("settings.stockReminder.title")}

{t("settings.notifications.email")}

{emailUnavailableReason && (
{emailUnavailableReason}
)} {settings.emailEnabled && ( <>
{t("settings.email.recipient")} ⓘ setSettings({ ...settings, notificationEmail: e.target.value })} placeholder="recipient address" pattern="[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}$" inputMode="email" autoComplete="off" autoCapitalize="none" autoCorrect="off" spellCheck={false} data-bwignore="true" data-lpignore="true" data-1p-ignore="true" />
{testEmailResult && ( {testEmailResult.message} )}
)}

{t("settings.notifications.push")}

{settings.shoutrrrEnabled && ( <>
{t("settings.push.url")} ⓘ setSettings({ ...settings, shoutrrrUrl: e.target.value })} placeholder={t("settings.push.urlPlaceholder")} />
{testShoutrrrResult && ( {testShoutrrrResult.message} )}
)}
{t("settings.schedule.title")} ⓘ
{t("settings.schedule.stockCheck")} {t("settings.schedule.dailyAtHour", { hour: settings.reminderHour })}
{t("settings.schedule.intakeCheck")} {t("settings.schedule.minutesBefore", { minutes: settings.reminderMinutesBefore })}
{settings.nextScheduledCheck && (
{t("settings.schedule.nextCheck")} {new Date(settings.nextScheduledCheck).toLocaleString(getSystemLocale(i18n.language), { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", })}
)} {settings.lastStockReminderSent && (
{t("settings.schedule.lastStockSent")} {new Date(settings.lastStockReminderSent).toLocaleString(getSystemLocale(i18n.language), { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", })}
)} {settings.lastAutoEmailSent && (
{t("settings.schedule.lastIntakeSent")} {new Date(settings.lastAutoEmailSent).toLocaleString(getSystemLocale(i18n.language), { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", })}
)} {settings.lastPrescriptionReminderSent && (
{t("settings.schedule.lastPrescriptionSent")} {new Date(settings.lastPrescriptionReminderSent).toLocaleString(getSystemLocale(i18n.language), { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", })}
)}
{/* Stock Settings */}

{t("settings.stock.title")}

{t("settings.stock.calculationMode")}

{t("settings.stock.thresholds")}

= settings.lowStockDays ? "threshold-invalid" : ""} data-testid="settings-threshold-critical" > {t("status.criticalStock")} ⓘ setSettings({ ...settings, reminderDaysBefore: Number(e.target.value) || 7 })} />
= settings.highStockDays ? "threshold-invalid" : "" } data-testid="settings-threshold-low" > {t("status.lowStock")} ⓘ setSettings({ ...settings, lowStockDays: Number(e.target.value) || 30 })} />
{t("status.highStock")} ⓘ setSettings({ ...settings, highStockDays: Number(e.target.value) || 180 })} />
{(settings.reminderDaysBefore >= settings.lowStockDays || settings.lowStockDays >= settings.highStockDays) && (

{t("settings.stock.thresholdValidation")}

)}

â„šī¸ {t("settings.stock.packageTypesNote")}

{/* General UI */}

{t("settings.timeline.title")}

{t("settings.timeline.dashboardSectionOrder")}

{t("settings.timeline.swapDashboardSections")} ⓘ

{t("settings.timeline.upcomingSection")}

{t("settings.timeline.upcomingTodayOnly")} ⓘ

{t("settings.timeline.sharedSection")}

{t("settings.timeline.shareMedicationOverview")} ⓘ
{t("settings.timeline.shareScheduleTodayOnly")} ⓘ
{/* Export/Import Section */}

{t("exportImport.title")} ⓘ

{/* Import Success Message */} {importResult && (
✓ {t("exportImport.importSuccess")} {t("exportImport.importSuccessDetails", { medications: importResult.medications, doses: importResult.doses, refills: importResult.refills, shares: importResult.shares, })}
)} {/* Export */}
{t("exportImport.exportTitle")} {t("exportImport.exportDesc")}
{/* Import */}
{t("exportImport.importTitle")} {t("exportImport.importDesc")}
)} {/* Import Confirmation Modal */} {showImportConfirm && (

{t("exportImport.confirmImportMessage")}

âš ī¸ {t("exportImport.confirmImportWarning")}

) : (

{t("exportImport.confirmImportEmptyMessage")}

) } confirmLabel={t(hasExistingData ? "exportImport.confirmButton" : "exportImport.confirmButtonEmpty")} cancelLabel={t("exportImport.cancelButton")} onConfirm={handleImportConfirm} onCancel={() => { setShowImportConfirm(false); setPendingImportData(null); }} confirmVariant={hasExistingData ? "danger" : "primary"} /> )} {/* Export Options Modal */} setShowExportModal(false)} onExport={handleExport} exporting={exporting} />
); }