389 lines
15 KiB
TypeScript
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;
|
|
};
|
|
}
|
|
|