253 lines
8.6 KiB
TypeScript
253 lines
8.6 KiB
TypeScript
/**
|
||
* 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<string, AccountSetting>;
|
||
moduleSettings: Record<string, Record<string, ModuleSetting>>;
|
||
moduleEnableSettings: ModuleEnableSettings | null;
|
||
loading: boolean;
|
||
error: string | null;
|
||
|
||
// Actions
|
||
loadAccountSettings: () => Promise<void>;
|
||
loadAccountSetting: (key: string) => Promise<void>;
|
||
updateAccountSetting: (key: string, value: any) => Promise<void>;
|
||
loadModuleSettings: (moduleName: string) => Promise<void>;
|
||
updateModuleSetting: (moduleName: string, key: string, value: any) => Promise<void>;
|
||
loadModuleEnableSettings: () => Promise<void>;
|
||
updateModuleEnableSettings: (data: Partial<ModuleEnableSettings>) => Promise<void>;
|
||
isModuleEnabled: (moduleName: string) => boolean;
|
||
reset: () => void;
|
||
}
|
||
|
||
export const useSettingsStore = create<SettingsState>()(
|
||
persist<SettingsState>(
|
||
(set, get) => ({
|
||
accountSettings: {},
|
||
moduleSettings: {},
|
||
moduleEnableSettings: null,
|
||
loading: false,
|
||
error: null,
|
||
_moduleEnableLastFetched: 0 as number | undefined,
|
||
_moduleEnableInFlight: null as Promise<ModuleEnableSettings> | null,
|
||
|
||
loadAccountSettings: async () => {
|
||
set({ loading: true, error: null });
|
||
try {
|
||
const response = await fetchAccountSettings();
|
||
const settingsMap: Record<string, AccountSetting> = {};
|
||
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<string, ModuleSetting> = {};
|
||
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<ModuleEnableSettings>) => {
|
||
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,
|
||
}),
|
||
}
|
||
)
|
||
);
|
||
|