frontend_nyla/src/hooks/useSettings.ts

389 lines
15 KiB
TypeScript

import { useState, useEffect, useCallback, useRef } from 'react';
import { useUser, useCurrentUser } from './useUsers';
import { useApiRequest } from './useApi';
import { GenericDataHook } from '../core/PageManager/pageInterface';
import type { SettingsFieldConfig } from '../core/PageManager/pageInterface';
// Interface for unified settings data
export interface SettingsData {
// User data (from API)
username?: string;
fullName?: string;
email?: string;
language?: string;
privilege?: string;
enabled?: boolean;
authenticationAuthority?: string;
// Phone name (localStorage)
phoneName?: string;
// Theme (localStorage)
theme?: string;
// Speech data (localStorage)
speechData?: any;
// Nested speech fields
mandate_general?: {
company_name?: string;
industry?: string;
contact_info?: {
email?: string;
phone?: string;
street?: string;
postal_code?: string;
city?: string;
country?: string;
};
business_hours?: string;
timezone?: string;
};
setup_contacts?: boolean;
}
// Create settings hook factory
export function createSettingsHook(): () => GenericDataHook {
return function useSettings() {
const { user: currentUser } = useCurrentUser();
const { getUser, updateUser } = useUser();
const { request } = useApiRequest();
const [settingsData, setSettingsData] = useState<SettingsData>({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [settingsFields, setSettingsFields] = useState<Record<string, SettingsFieldConfig[]>>({});
const [settingsLoading, setSettingsLoading] = useState<Record<string, boolean>>({});
const [settingsErrors, setSettingsErrors] = useState<Record<string, string | null>>({});
// Track if we've loaded data initially to prevent infinite loops
const hasLoadedRef = useRef(false);
const currentUserIdRef = useRef<string | undefined>(currentUser?.id);
// Load phone name from localStorage
const _loadPhoneName = useCallback((): string => {
try {
return localStorage.getItem('userPhoneName') || '';
} catch (error) {
console.error('Failed to load phone name from localStorage:', error);
return '';
}
}, []);
void _loadPhoneName; // Intentionally unused, reserved for future use
// Load theme from localStorage
const _loadTheme = useCallback((): string => {
try {
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
return savedTheme;
}
// Default to system preference
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
} catch (error) {
console.error('Failed to load theme from localStorage:', error);
return 'light';
}
}, []);
void _loadTheme; // Intentionally unused, reserved for future use
// Load speech data from localStorage
const _loadSpeechData = useCallback((): any | null => {
try {
const savedData = localStorage.getItem('speechSignUpData');
const timestamp = localStorage.getItem('speechSignUpTimestamp');
if (savedData && timestamp) {
const parsedData = JSON.parse(savedData);
const savedTime = parseInt(timestamp);
const now = Date.now();
const twentyFourHours = 24 * 60 * 60 * 1000;
// Check if data is still valid (within 24 hours)
if (now - savedTime < twentyFourHours) {
return parsedData;
} else {
// Data expired, clear it
localStorage.removeItem('speechSignUpData');
localStorage.removeItem('speechSignUpTimestamp');
return null;
}
}
return null;
} catch (error) {
console.error('Error loading speech data:', error);
return null;
}
}, []);
void _loadSpeechData; // Intentionally unused, reserved for future use
// Fetch user data from API
const _fetchUserData = useCallback(async () => {
if (!currentUser?.id) return null;
try {
const userData = await getUser(currentUser.id);
return userData;
} catch (error) {
console.error('Failed to fetch user data:', error);
throw error;
}
}, [currentUser?.id, getUser]);
void _fetchUserData; // Intentionally unused, reserved for future use
// Fetch field definitions from backend
const _fetchFieldsForSection = useCallback(async (sectionId: string): Promise<SettingsFieldConfig[]> => {
try {
setSettingsLoading(prev => ({ ...prev, [sectionId]: true }));
setSettingsErrors(prev => ({ ...prev, [sectionId]: null }));
// TODO: Replace with actual backend endpoint
// For now, return empty array - fields will come from backend later
const response = await request({
url: `/api/settings/fields?section=${sectionId}`,
method: 'get'
});
const fields = response?.fields || [];
setSettingsFields(prev => ({ ...prev, [sectionId]: fields }));
return fields;
} catch (error: any) {
const errorMessage = error.message || `Failed to load fields for ${sectionId}`;
console.error(`Error fetching fields for section ${sectionId}:`, error);
setSettingsErrors(prev => ({ ...prev, [sectionId]: errorMessage }));
return [];
} finally {
setSettingsLoading(prev => ({ ...prev, [sectionId]: false }));
}
}, [request]);
void _fetchFieldsForSection; // Intentionally unused, reserved for future use
// Load all settings data
const loadSettingsData = useCallback(async () => {
// Prevent multiple simultaneous loads
if (loading && hasLoadedRef.current) return;
try {
setLoading(true);
setError(null);
// Load from different sources - call functions directly to avoid dependency issues
const userData = currentUser?.id ? await getUser(currentUser.id) : null;
const phoneName = localStorage.getItem('userPhoneName') || '';
const savedTheme = localStorage.getItem('theme');
const theme = savedTheme || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
// Load speech data
let speechData: any | null = null;
try {
const savedData = localStorage.getItem('speechSignUpData');
const timestamp = localStorage.getItem('speechSignUpTimestamp');
if (savedData && timestamp) {
const parsedData = JSON.parse(savedData);
const savedTime = parseInt(timestamp);
const now = Date.now();
const twentyFourHours = 24 * 60 * 60 * 1000;
if (now - savedTime < twentyFourHours) {
speechData = parsedData;
} else {
localStorage.removeItem('speechSignUpData');
localStorage.removeItem('speechSignUpTimestamp');
}
}
} catch (error) {
console.error('Error loading speech data:', error);
}
// Merge all data into unified object
const unifiedData: SettingsData = {
...(userData || {}),
phoneName,
theme,
speechData,
// Flatten speech data if it exists
...(speechData?.mandate_general && {
mandate_general: speechData.mandate_general
}),
...(speechData?.setup_contacts !== undefined && {
setup_contacts: speechData.setup_contacts
})
};
setSettingsData(unifiedData);
hasLoadedRef.current = true;
} catch (error: any) {
console.error('Error loading settings data:', error);
setError(error.message || 'Failed to load settings');
} finally {
setLoading(false);
}
}, [currentUser?.id, getUser]); // Only depend on currentUser?.id and getUser
// Save section data
const saveSection = useCallback(async (sectionId: string, data: any) => {
try {
setSettingsLoading(prev => ({ ...prev, [sectionId]: true }));
setSettingsErrors(prev => ({ ...prev, [sectionId]: null }));
if (sectionId === 'user-info') {
// Separate user API data from localStorage data
const { phoneName, id, mandateId, ...userApiData } = data;
// Build clean update object with only allowed fields (explicitly exclude id and mandateId)
// Allowed fields: username, email, fullName, language, enabled, privilege, authenticationAuthority
const userUpdateData: Record<string, any> = {};
const allowedFields = ['username', 'email', 'fullName', 'language', 'enabled', 'privilege', 'authenticationAuthority'];
allowedFields.forEach(field => {
if (userApiData.hasOwnProperty(field)) {
userUpdateData[field] = userApiData[field];
}
});
// Save user data via API (only allowed fields, no id or mandateId)
if (currentUser?.id && Object.keys(userUpdateData).length > 0) {
const updatedUser = await updateUser(currentUser.id, userUpdateData);
// Update local state with API data
setSettingsData(prev => ({ ...prev, ...updatedUser }));
// Update localStorage cache
localStorage.setItem('currentUser', JSON.stringify(updatedUser));
// Dispatch event to notify other components
window.dispatchEvent(new CustomEvent('userInfoUpdated'));
}
// Save phone name to localStorage separately
if (phoneName !== undefined) {
if (phoneName?.trim()) {
localStorage.setItem('userPhoneName', phoneName.trim());
} else {
localStorage.removeItem('userPhoneName');
}
// Update local state with phone name
setSettingsData(prev => ({ ...prev, phoneName }));
}
} else if (sectionId === 'theme') {
// Save theme to localStorage
localStorage.setItem('theme', data.theme);
// Apply theme immediately
document.documentElement.setAttribute('data-theme', data.theme);
document.documentElement.classList.remove('light-theme', 'dark-theme');
document.documentElement.classList.add(`${data.theme}-theme`);
// Update local state
setSettingsData(prev => ({ ...prev, theme: data.theme }));
} else if (sectionId === 'speech-settings') {
// Save speech data to localStorage
localStorage.setItem('speechSignUpData', JSON.stringify(data));
localStorage.setItem('speechSignUpTimestamp', Date.now().toString());
// Update local state
setSettingsData(prev => ({
...prev,
speechData: data,
...(data.mandate_general && { mandate_general: data.mandate_general }),
...(data.setup_contacts !== undefined && { setup_contacts: data.setup_contacts })
}));
// Dispatch event to notify other components
window.dispatchEvent(new CustomEvent('speechSignUpChanged'));
} else if (sectionId === 'phone-name') {
// Save phone name to localStorage
if (data.phoneName?.trim()) {
localStorage.setItem('userPhoneName', data.phoneName.trim());
} else {
localStorage.removeItem('userPhoneName');
}
// Update local state
setSettingsData(prev => ({ ...prev, phoneName: data.phoneName }));
}
} catch (error: any) {
const errorMessage = error.message || `Failed to save ${sectionId}`;
console.error(`Error saving section ${sectionId}:`, error);
setSettingsErrors(prev => ({ ...prev, [sectionId]: errorMessage }));
throw error;
} finally {
setSettingsLoading(prev => ({ ...prev, [sectionId]: false }));
}
}, [currentUser?.id, updateUser]);
// Initial load - only when currentUser?.id changes or on first mount
useEffect(() => {
// Only load if:
// 1. We haven't loaded yet, OR
// 2. The user ID changed
if (!hasLoadedRef.current || currentUserIdRef.current !== currentUser?.id) {
currentUserIdRef.current = currentUser?.id;
// Load data directly to avoid dependency issues
const loadData = async () => {
if (loading && hasLoadedRef.current) return;
try {
setLoading(true);
setError(null);
// Load from different sources
const userData = currentUser?.id ? await getUser(currentUser.id) : null;
const phoneName = localStorage.getItem('userPhoneName') || '';
const savedTheme = localStorage.getItem('theme');
const theme = savedTheme || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
// Load speech data
let speechData: any | null = null;
try {
const savedData = localStorage.getItem('speechSignUpData');
const timestamp = localStorage.getItem('speechSignUpTimestamp');
if (savedData && timestamp) {
const parsedData = JSON.parse(savedData);
const savedTime = parseInt(timestamp);
const now = Date.now();
const twentyFourHours = 24 * 60 * 60 * 1000;
if (now - savedTime < twentyFourHours) {
speechData = parsedData;
} else {
localStorage.removeItem('speechSignUpData');
localStorage.removeItem('speechSignUpTimestamp');
}
}
} catch (error) {
console.error('Error loading speech data:', error);
}
// Merge all data into unified object
const unifiedData: SettingsData = {
...(userData || {}),
phoneName,
theme,
speechData,
...(speechData?.mandate_general && {
mandate_general: speechData.mandate_general
}),
...(speechData?.setup_contacts !== undefined && {
setup_contacts: speechData.setup_contacts
})
};
setSettingsData(unifiedData);
hasLoadedRef.current = true;
} catch (error: any) {
console.error('Error loading settings data:', error);
setError(error.message || 'Failed to load settings');
} finally {
setLoading(false);
}
};
loadData();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentUser?.id]); // Only depend on currentUser?.id
return {
data: [], // Not used for settings
loading,
error,
settingsData,
settingsFields,
settingsLoading,
settingsErrors,
saveSection,
refetch: loadSettingsData
} as GenericDataHook;
};
}