/** * Settings Store (Zustand) * Manages account and module settings */ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import { fetchAccountSettings, fetchAccountSetting, createAccountSetting, updateAccountSetting, deleteAccountSetting, fetchModuleSettings, createModuleSetting, updateModuleSetting, fetchModuleEnableSettings, updateModuleEnableSettings, AccountSetting, ModuleSetting, ModuleEnableSettings, AccountSettingsError, } from '../services/api'; const getAccountSettingsErrorMessage = (error: AccountSettingsError): string => { switch (error.type) { case 'ACCOUNT_SETTINGS_NOT_FOUND': return 'No account-level settings have been created yet.'; case 'ACCOUNT_SETTINGS_VALIDATION_ERROR': return error.message || 'The account settings request contained invalid data.'; default: return error.message || 'Unexpected error while loading account settings.'; } }; interface SettingsState { accountSettings: Record; moduleSettings: Record>; moduleEnableSettings: ModuleEnableSettings | null; loading: boolean; error: string | null; // Actions loadAccountSettings: () => Promise; loadAccountSetting: (key: string) => Promise; updateAccountSetting: (key: string, value: any) => Promise; loadModuleSettings: (moduleName: string) => Promise; updateModuleSetting: (moduleName: string, key: string, value: any) => Promise; loadModuleEnableSettings: () => Promise; updateModuleEnableSettings: (data: Partial) => Promise; isModuleEnabled: (moduleName: string) => boolean; reset: () => void; } export const useSettingsStore = create()( persist( (set, get) => ({ accountSettings: {}, moduleSettings: {}, moduleEnableSettings: null, loading: false, error: null, _moduleEnableLastFetched: 0 as number | undefined, _moduleEnableInFlight: null as Promise | null, loadAccountSettings: async () => { set({ loading: true, error: null }); try { const response = await fetchAccountSettings(); const settingsMap: Record = {}; response.results.forEach(setting => { settingsMap[setting.key] = setting; }); set({ accountSettings: settingsMap, loading: false }); } catch (error: any) { if (error instanceof AccountSettingsError) { const message = getAccountSettingsErrorMessage(error); // Not found should not be treated as failure – just indicate no settings yet if (error.type === 'ACCOUNT_SETTINGS_NOT_FOUND') { set({ accountSettings: {}, loading: false, error: null }); } else { set({ error: message, loading: false }); } } else { set({ error: error.message, loading: false }); } } }, loadAccountSetting: async (key: string) => { try { const setting = await fetchAccountSetting(key); set(state => ({ accountSettings: { ...state.accountSettings, [key]: setting } })); } catch (error: any) { if (error instanceof AccountSettingsError) { if (error.type === 'ACCOUNT_SETTINGS_NOT_FOUND') { // Silently ignore missing setting – it just hasn't been created yet return; } set({ error: getAccountSettingsErrorMessage(error) }); return; } set({ error: error.message }); } }, updateAccountSetting: async (key: string, value: any) => { set({ loading: true, error: null }); try { const existing = get().accountSettings[key]; let setting: AccountSetting; if (existing) { setting = await updateAccountSetting(key, { config: value }); } else { setting = await createAccountSetting({ key, config: value }); } set(state => ({ accountSettings: { ...state.accountSettings, [key]: setting }, loading: false })); } catch (error: any) { if (error instanceof AccountSettingsError) { const message = getAccountSettingsErrorMessage(error); set({ error: message, loading: false }); throw error; } set({ error: error.message, loading: false }); throw error; } }, loadModuleSettings: async (moduleName: string) => { set({ loading: true, error: null }); try { const settings = await fetchModuleSettings(moduleName); const settingsMap: Record = {}; settings.forEach(setting => { settingsMap[setting.key] = setting; }); set(state => ({ moduleSettings: { ...state.moduleSettings, [moduleName]: settingsMap }, loading: false })); } catch (error: any) { set({ error: error.message, loading: false }); } }, updateModuleSetting: async (moduleName: string, key: string, value: any) => { set({ loading: true, error: null }); try { const existing = get().moduleSettings[moduleName]?.[key]; let setting: ModuleSetting; if (existing) { setting = await updateModuleSetting(moduleName, key, { config: value }); } else { setting = await createModuleSetting({ module_name: moduleName, key, config: value }); } set(state => ({ moduleSettings: { ...state.moduleSettings, [moduleName]: { ...(state.moduleSettings[moduleName] || {}), [key]: setting } }, loading: false })); } catch (error: any) { set({ error: error.message, loading: false }); throw error; } }, loadModuleEnableSettings: async () => { const state = get() as any; const now = Date.now(); // Use cached value if fetched within last 60s if (state.moduleEnableSettings && state._moduleEnableLastFetched && now - state._moduleEnableLastFetched < 60000) { return; } // Coalesce concurrent calls if (state._moduleEnableInFlight) { await state._moduleEnableInFlight; return; } set({ loading: true, error: null }); try { const inFlight = fetchModuleEnableSettings(); (state as any)._moduleEnableInFlight = inFlight; const settings = await inFlight; set({ moduleEnableSettings: settings, loading: false, _moduleEnableLastFetched: Date.now() }); } catch (error: any) { // On 429/403, avoid loops; cache the failure timestamp and do not retry automatically if (error?.status === 429 || error?.status === 403) { set({ loading: false, _moduleEnableLastFetched: Date.now() }); return; } set({ error: error.message, loading: false, _moduleEnableLastFetched: Date.now() }); } finally { (get() as any)._moduleEnableInFlight = null; } }, updateModuleEnableSettings: async (data: Partial) => { set({ loading: true, error: null }); try { const settings = await updateModuleEnableSettings(data); set({ moduleEnableSettings: settings, loading: false }); } catch (error: any) { set({ error: error.message, loading: false }); throw error; } }, isModuleEnabled: (moduleName: string): boolean => { const settings = get().moduleEnableSettings; if (!settings) return true; // Default to enabled if not loaded const enabledKey = `${moduleName}_enabled` as keyof ModuleEnableSettings; return settings[enabledKey] !== false; // Default to true if not set }, reset: () => { set({ accountSettings: {}, moduleSettings: {}, moduleEnableSettings: null, loading: false, error: null, }); }, }), { name: 'settings-storage', partialize: (state) => ({ accountSettings: state.accountSettings, moduleSettings: state.moduleSettings, moduleEnableSettings: state.moduleEnableSettings, }), } ) );