import React, { useEffect, useRef, useState } from "react"; import { Box, Alert, Button, Checkbox, FormControl, FormControlLabel, InputLabel, MenuItem, Select, Skeleton, Tab, Tabs, Typography, } from "@mui/material"; import { useNavigate } from "react-router-dom"; import { JobTableColumns } from "./JobTable"; import ImportExportJobs from "./ImportExportJobs"; import RulesSettingsCard, { RULE_FIELDS, RuleDraft, RuleField, RuleSettings } from "./RulesSettingsCard"; import BackupCard from "./BackupCard"; import QuickCaptureCard from "./QuickCaptureCard"; import AiUsageCard from "./AiUsageCard"; import AiPrivacySettingsCard from "./AiPrivacySettingsCard"; import { ThemeModePref } from "../themePrefs"; import { useI18n } from "../i18n/I18nProvider"; import { api, getApiErrorMessage } from "../api"; import { useToast } from "../toast"; import { PageSection } from "../layout/PageLayout"; interface Props { pageSize: 15 | 20 | 25; onPageSizeChange: (n: 15 | 20 | 25) => void; columns: JobTableColumns; onColumnsChange: (next: JobTableColumns) => void; themeMode: ThemeModePref; onThemeModeChange: (v: ThemeModePref) => void; } function TabPanel({ value, index, children }: { value: number; index: number; children: React.ReactNode }) { if (value !== index) return null; return {children}; } type NotificationPrefs = { emailFollowUpRemindersEnabled: boolean; }; function ruleDraft(settings: RuleSettings): RuleDraft { return Object.fromEntries(RULE_FIELDS.map(field => [field, String(settings[field])])) as RuleDraft; } export default function SettingsView({ pageSize, onPageSizeChange, columns, onColumnsChange, themeMode, onThemeModeChange, }: Props) { const navigate = useNavigate(); const [tab, setTab] = useState(0); const { language, setLanguage, t } = useI18n(); const { toast } = useToast(); const [notificationPrefs, setNotificationPrefs] = useState(null); const [notificationError, setNotificationError] = useState(null); const [savingNotifications, setSavingNotifications] = useState(false); const [savedRules, setSavedRules] = useState(null); const [rulesDraft, setRulesDraft] = useState(null); const [rulesLoadError, setRulesLoadError] = useState(null); const [rulesSaveError, setRulesSaveError] = useState(false); const [invalidRules, setInvalidRules] = useState([]); const [savingRules, setSavingRules] = useState(false); const rulesSession = useRef({ mounted: true, loading: false, saving: false }); const rulesDirty = Boolean(rulesDraft && savedRules && RULE_FIELDS.some(field => rulesDraft[field] !== String(savedRules[field]))); useEffect(() => { const session = rulesSession.current; session.mounted = true; return () => { session.mounted = false; }; }, []); const loadRules = async () => { const session = rulesSession.current; if (rulesDraft || session.loading) return; session.loading = true; setRulesLoadError(null); try { const { data } = await api.get("/rules"); if (!session.mounted) return; setSavedRules(data); setRulesDraft(ruleDraft(data)); } catch (error) { if (session.mounted) setRulesLoadError(getApiErrorMessage(error, t("rulesLoadFailed"))); } finally { session.loading = false; } }; const selectSection = (next: number) => { setTab(next); if (next === 1) void loadRules(); }; const saveRules = async () => { const session = rulesSession.current; if (!rulesDraft || !savedRules || session.saving) return; const invalid = RULE_FIELDS.filter(field => !rulesDraft[field].trim() || !Number.isInteger(Number(rulesDraft[field])) || Number(rulesDraft[field]) < 1 || Number(rulesDraft[field]) > 365); setInvalidRules(invalid); if (invalid.length) return; const submittedDraft = rulesDraft; const submitted = { id: savedRules.id, ...Object.fromEntries(RULE_FIELDS.map(field => [field, Number(submittedDraft[field])])) } as RuleSettings; session.saving = true; setSavingRules(true); setRulesSaveError(false); try { await api.put("/rules", submitted); if (!session.mounted) return; setSavedRules(submitted); // A completed save acknowledges its snapshot, not edits made while it was pending. setRulesDraft(current => current && RULE_FIELDS.every(field => current[field] === submittedDraft[field]) ? ruleDraft(submitted) : current); toast(t("rulesSaved"), "success"); } catch { if (session.mounted) setRulesSaveError(true); } finally { session.saving = false; if (session.mounted) setSavingRules(false); } }; const discardRules = () => { if (!savedRules || savingRules) return; setRulesDraft(ruleDraft(savedRules)); setInvalidRules([]); setRulesSaveError(false); }; useEffect(() => { let active = true; api.get("/notification-settings") .then(({ data }) => { if (active) setNotificationPrefs(data); }) .catch((error) => { if (active) setNotificationError(getApiErrorMessage(error, t("settingsNotificationsLoadFailed"))); }); return () => { active = false; }; }, [t]); const saveNotifications = async () => { if (!notificationPrefs) return; setSavingNotifications(true); setNotificationError(null); try { const { data } = await api.put("/notification-settings", notificationPrefs); setNotificationPrefs(data); toast(t("settingsNotificationsSaved"), "success"); } catch (error) { setNotificationError(getApiErrorMessage(error, t("settingsNotificationsSaveFailed"))); } finally { setSavingNotifications(false); } }; return ( {t("settingsNavigation")} selectSection(v)} variant="scrollable" scrollButtons="auto" sx={{ display: { xs: "none", sm: "flex" }, mb: 1 }}> {rulesDirty ? {t("assetsUnsavedChanges")}. {t("rulesDraftScope")} : null} {t("settingsTheme")} {t("settingsPreferredLanguage")} {t("settingsPagination")} {t("settingsRowsPerPage")} {t("settingsColumns")} {( [ ["status", t("settingsColumnStatus")], ["dateApplied", t("settingsColumnDateApplied")], ["daysSince", t("settingsColumnDays")], ["jobUrl", t("settingsColumnJobUrl")], ] as const ).map(([key, label]) => ( onColumnsChange({ ...columns, [key]: !columns[key] })} />} label={label} /> ))} navigate("/settings/connected-accounts")}>{t("settingsManageConnectedAccounts")}} /> void saveRules()} onRetry={() => void loadRules()} onDiscard={discardRules} onChange={(field, value) => { setRulesDraft(current => current ? { ...current, [field]: value } : current); setInvalidRules(current => current.filter(key => key !== field)); }} /> {notificationError ? {notificationError} : null} {!notificationPrefs ? : setNotificationPrefs({ emailFollowUpRemindersEnabled: e.target.checked })} />} label={t("settingsNotificationsFollowUpReminders")} /> {t("settingsNotificationsEmailBody")} } {t("settingsNotificationsDelivery")} ); }