import React, { useState, useEffect, ChangeEvent, FormEvent, useCallback, } from 'react'; import { useTranslation } from 'react-i18next'; import { InformationCircleIcon, EyeIcon, EyeSlashIcon, UserIcon, ClockIcon, ShieldCheckIcon, LightBulbIcon, CogIcon, ClipboardDocumentListIcon, BoltIcon, ChevronRightIcon, ExclamationTriangleIcon, FaceSmileIcon, CheckIcon, SunIcon, MoonIcon, } from '@heroicons/react/24/outline'; import TelegramIcon from '../Icons/TelegramIcon'; import { useToast } from '../Shared/ToastContext'; import { dispatchTelegramStatusChange } from '../../contexts/TelegramStatusContext'; import LanguageDropdown from '../Shared/LanguageDropdown'; interface ProfileSettingsProps { currentUser: { id: number; email: string }; isDarkMode?: boolean; toggleDarkMode?: () => void; } interface Profile { id: number; email: string; appearance: 'light' | 'dark'; language: string; timezone: string; avatar_image: string | null; telegram_bot_token: string | null; telegram_chat_id: string | null; task_summary_enabled: boolean; task_summary_frequency: string; task_intelligence_enabled: boolean; auto_suggest_next_actions_enabled: boolean; productivity_assistant_enabled: boolean; next_task_suggestion_enabled: boolean; pomodoro_enabled: boolean; } interface TelegramBotInfo { username: string; first_name?: string; polling_status: any; chat_url: string; } const formatFrequency = (frequency: string): string => { if (frequency.endsWith('h')) { const value = frequency.replace('h', ''); return `${value} ${parseInt(value) === 1 ? 'hour' : 'hours'}`; } else if (frequency === 'daily') { return '1 day'; } else if (frequency === 'weekly') { return '1 week'; } else if (frequency === 'weekdays') { return 'Weekdays'; } return frequency; }; const ProfileSettings: React.FC = ({ isDarkMode, toggleDarkMode, }) => { const { t, i18n } = useTranslation(); const { showSuccessToast, showErrorToast } = useToast(); const [activeTab, setActiveTab] = useState('general'); // Password visibility state const [showCurrentPassword, setShowCurrentPassword] = useState(false); const [showNewPassword, setShowNewPassword] = useState(false); const [showConfirmPassword, setShowConfirmPassword] = useState(false); const [profile, setProfile] = useState(null); const [formData, setFormData] = useState< Partial< Profile & { currentPassword: string; newPassword: string; confirmPassword: string; } > >({ appearance: isDarkMode ? 'dark' : 'light', language: 'en', timezone: 'UTC', avatar_image: '', telegram_bot_token: '', task_intelligence_enabled: true, task_summary_enabled: false, task_summary_frequency: 'daily', auto_suggest_next_actions_enabled: true, productivity_assistant_enabled: true, next_task_suggestion_enabled: true, pomodoro_enabled: true, currentPassword: '', newPassword: '', confirmPassword: '', }); const [loading, setLoading] = useState(true); const [updateKey, setUpdateKey] = useState(0); const [isChangingLanguage, setIsChangingLanguage] = useState(false); const [isPolling, setIsPolling] = useState(false); const [telegramSetupStatus, setTelegramSetupStatus] = useState< 'idle' | 'loading' | 'success' | 'error' >('idle'); const [telegramBotInfo, setTelegramBotInfo] = useState(null); const forceUpdate = useCallback(() => { setUpdateKey((prevKey) => prevKey + 1); }, []); // Password validation const validatePasswordForm = (): { valid: boolean; errors: { [key: string]: string }; } => { const errors: { [key: string]: string } = {}; // Only validate if user is trying to change password if ( formData.currentPassword || formData.newPassword || formData.confirmPassword ) { if (!formData.currentPassword) { errors.currentPassword = t( 'profile.currentPasswordRequired', 'Current password is required' ); } if (!formData.newPassword) { errors.newPassword = t( 'profile.newPasswordRequired', 'New password is required' ); } else if (formData.newPassword.length < 6) { errors.newPassword = t( 'profile.passwordTooShort', 'Password must be at least 6 characters' ); } if (formData.newPassword !== formData.confirmPassword) { errors.confirmPassword = t( 'profile.passwordMismatch', 'Passwords do not match' ); } } return { valid: Object.keys(errors).length === 0, errors }; }; const handleChange = ( e: ChangeEvent ) => { const { name, value } = e.target; setFormData((prev) => ({ ...prev, [name]: value })); }; const handleLanguageChange = async (value: string) => { try { setIsChangingLanguage(true); await i18n.changeLanguage(value); document.documentElement.lang = value; const resources = i18n.getResourceBundle(value, 'translation'); if (!resources || Object.keys(resources).length === 0) { const loadPath = `/locales/${value}/translation.json`; try { const response = await fetch(loadPath); if (response.ok) { const data = await response.json(); i18n.addResourceBundle( value, 'translation', data, true, true ); if (window.forceLanguageReload) { window.forceLanguageReload(value); } } } catch { // Ignore errors loading language resources } } setTimeout(() => { forceUpdate(); const checkAndLoadResources = i18n.getResourceBundle( value, 'translation' ); if ( !checkAndLoadResources || Object.keys(checkAndLoadResources).length === 0 ) { if (window.forceLanguageReload) { window.forceLanguageReload(value); } } setTimeout(() => { if (isChangingLanguage) { setIsChangingLanguage(false); } }, 800); }, 200); } catch { setIsChangingLanguage(false); } }; useEffect(() => { const fetchProfile = async () => { try { setLoading(true); const response = await fetch('/api/profile'); if (!response.ok) { throw new Error( t('profile.fetchError', 'Failed to fetch profile data.') ); } const data = await response.json(); setProfile(data); setFormData({ appearance: data.appearance || (isDarkMode ? 'dark' : 'light'), language: data.language || 'en', timezone: data.timezone || 'UTC', avatar_image: data.avatar_image || '', telegram_bot_token: data.telegram_bot_token || '', task_intelligence_enabled: data.task_intelligence_enabled !== undefined ? data.task_intelligence_enabled : true, task_summary_enabled: data.task_summary_enabled !== undefined ? data.task_summary_enabled : false, task_summary_frequency: data.task_summary_frequency || 'daily', auto_suggest_next_actions_enabled: data.auto_suggest_next_actions_enabled !== undefined ? data.auto_suggest_next_actions_enabled : true, productivity_assistant_enabled: data.productivity_assistant_enabled !== undefined ? data.productivity_assistant_enabled : true, next_task_suggestion_enabled: data.next_task_suggestion_enabled !== undefined ? data.next_task_suggestion_enabled : true, pomodoro_enabled: data.pomodoro_enabled !== undefined ? data.pomodoro_enabled : true, }); // Note: Task summary status checking functionality removed for now if (data.telegram_bot_token) { fetchPollingStatus(); } } catch (error) { showErrorToast((error as Error).message); } finally { setLoading(false); } }; const fetchPollingStatus = async () => { try { const response = await fetch('/api/telegram/polling-status'); if (!response.ok) { throw new Error( t( 'profile.pollingStatusError', 'Failed to fetch polling status.' ) ); } const data = await response.json(); setIsPolling(data.status?.running || false); // Auto-start polling if user has a bot token but polling is not running if (data.telegram_bot_token && !data.status?.running) { handleStartPolling(); } } catch { // Ignore errors fetching polling status } }; fetchProfile(); }, []); // Fetch Telegram bot info when profile loads useEffect(() => { const fetchTelegramInfo = async () => { if (profile?.telegram_bot_token) { try { // Fetch bot info const setupResponse = await fetch('/api/telegram/setup', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ token: profile.telegram_bot_token, }), }); if (setupResponse.ok) { const setupData = await setupResponse.json(); if (setupData.bot) { setTelegramBotInfo({ username: setupData.bot.username, first_name: setupData.bot.first_name, chat_url: `https://t.me/${setupData.bot.username}`, polling_status: null, }); } } // Also fetch and auto-start polling status const pollingResponse = await fetch( '/api/telegram/polling-status', { credentials: 'include', headers: { Accept: 'application/json', }, } ); if (pollingResponse.ok) { const pollingData = await pollingResponse.json(); setIsPolling(pollingData.status?.running || false); // Auto-start polling if not running if (!pollingData.status?.running) { setTimeout(() => { handleStartPolling(); }, 1000); } else { // Dispatch healthy status if already running dispatchTelegramStatusChange('healthy'); } } } catch (error) { console.error('Error fetching Telegram info:', error); } } }; fetchTelegramInfo(); }, [profile?.telegram_bot_token]); useEffect(() => {}, [updateKey, i18n.language]); useEffect(() => { setFormData((prev) => ({ ...prev, appearance: isDarkMode ? 'dark' : 'light', })); }, [isDarkMode]); useEffect(() => { const handleLanguageChanged = () => { forceUpdate(); }; const handleAppLanguageChanged = () => { forceUpdate(); setTimeout(() => { setIsChangingLanguage(false); }, 300); }; i18n.on('languageChanged', handleLanguageChanged); window.addEventListener( 'app-language-changed', handleAppLanguageChanged as EventListener ); return () => { i18n.off('languageChanged', handleLanguageChanged); window.removeEventListener( 'app-language-changed', handleAppLanguageChanged as EventListener ); }; }, []); const handleSetupTelegram = async () => { setTelegramSetupStatus('loading'); setTelegramBotInfo(null); try { if ( !formData.telegram_bot_token || !formData.telegram_bot_token.includes(':') ) { throw new Error(t('profile.invalidTelegramToken')); } const response = await fetch('/api/telegram/setup', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ token: formData.telegram_bot_token }), }); if (!response.ok) { const data = await response.json(); throw new Error(data.error || t('profile.telegramSetupFailed')); } const data = await response.json(); setTelegramSetupStatus('success'); // Extract bot info properly const bot = data.bot; let botDisplayName = 'Bot'; if (bot) { if (bot.first_name && bot.username) { botDisplayName = `${bot.first_name} (@${bot.username})`; } else if (bot.first_name) { botDisplayName = bot.first_name; } else if (bot.username) { botDisplayName = `@${bot.username}`; } } showSuccessToast( t( 'profile.telegramSetupSuccess', 'Telegram bot "{{botName}}" configured successfully!', { botName: botDisplayName } ) ); if (data.bot) { setTelegramBotInfo({ username: data.bot.username, first_name: data.bot.first_name, chat_url: `https://t.me/${data.bot.username}`, polling_status: null, }); setIsPolling(true); // Send welcome message on first setup if (profile?.telegram_chat_id) { try { await fetch('/api/telegram/send-welcome', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ chatId: profile.telegram_chat_id, }), }); } catch (error) { console.error('Error sending welcome message:', error); } } if (!data.bot.polling_status?.running) { setTimeout(() => { handleStartPolling(); }, 1000); } // Dispatch status change event dispatchTelegramStatusChange('healthy'); } } catch (error) { setTelegramSetupStatus('error'); showErrorToast((error as Error).message); } }; const handleStartPolling = async () => { try { const response = await fetch('/api/telegram/start-polling', { method: 'POST', headers: { 'Content-Type': 'application/json', }, }); if (!response.ok) { const data = await response.json(); throw new Error(data.error || t('profile.startPollingFailed')); } const data = await response.json(); setIsPolling(true); showSuccessToast(t('profile.pollingStarted')); // Dispatch status change event dispatchTelegramStatusChange('healthy'); if (telegramBotInfo) { setTelegramBotInfo({ ...telegramBotInfo, polling_status: data.status, }); } } catch { showErrorToast(t('profile.pollingError')); dispatchTelegramStatusChange('problem'); } }; const handleStopPolling = async () => { try { const response = await fetch('/api/telegram/stop-polling', { method: 'POST', headers: { 'Content-Type': 'application/json', }, }); if (!response.ok) { const data = await response.json(); throw new Error(data.error || t('profile.stopPollingFailed')); } const data = await response.json(); setIsPolling(false); showSuccessToast( t('profile.pollingStopped', 'Polling stopped successfully.') ); // Dispatch status change event dispatchTelegramStatusChange('problem'); if (telegramBotInfo) { setTelegramBotInfo({ ...telegramBotInfo, polling_status: data.status, }); } } catch { showErrorToast(t('profile.pollingError')); dispatchTelegramStatusChange('problem'); } }; const handleSubmit = async (e: FormEvent) => { e.preventDefault(); // Check if user is trying to change password const isPasswordChange = formData.currentPassword || formData.newPassword || formData.confirmPassword; // Only validate password if user is trying to change password if (isPasswordChange) { const passwordValidation = validatePasswordForm(); if (!passwordValidation.valid) { showErrorToast(Object.values(passwordValidation.errors)[0]); return; } } try { // Prepare data to send - exclude password fields if not changing password const dataToSend = { ...formData }; if (!isPasswordChange) { delete dataToSend.currentPassword; delete dataToSend.newPassword; delete dataToSend.confirmPassword; } const response = await fetch('/api/profile', { method: 'PATCH', credentials: 'include', headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, body: JSON.stringify(dataToSend), }); if (!response.ok) { const data = await response.json(); throw new Error(data.error || 'Failed to update profile.'); } const updatedProfile: Profile = await response.json(); setProfile(updatedProfile); // Update formData to reflect the saved changes, preserving any fields not in response setFormData((prev) => ({ ...prev, appearance: updatedProfile.appearance || prev.appearance || 'light', language: updatedProfile.language || prev.language || 'en', timezone: updatedProfile.timezone || prev.timezone || 'UTC', avatar_image: updatedProfile.avatar_image !== undefined ? updatedProfile.avatar_image : prev.avatar_image || '', telegram_bot_token: updatedProfile.telegram_bot_token !== undefined ? updatedProfile.telegram_bot_token : prev.telegram_bot_token || '', task_intelligence_enabled: updatedProfile.task_intelligence_enabled !== undefined ? updatedProfile.task_intelligence_enabled : prev.task_intelligence_enabled !== undefined ? prev.task_intelligence_enabled : true, task_summary_enabled: updatedProfile.task_summary_enabled !== undefined ? updatedProfile.task_summary_enabled : prev.task_summary_enabled !== undefined ? prev.task_summary_enabled : false, task_summary_frequency: updatedProfile.task_summary_frequency || prev.task_summary_frequency || 'daily', auto_suggest_next_actions_enabled: updatedProfile.auto_suggest_next_actions_enabled !== undefined ? updatedProfile.auto_suggest_next_actions_enabled : prev.auto_suggest_next_actions_enabled !== undefined ? prev.auto_suggest_next_actions_enabled : true, productivity_assistant_enabled: updatedProfile.productivity_assistant_enabled !== undefined ? updatedProfile.productivity_assistant_enabled : prev.productivity_assistant_enabled !== undefined ? prev.productivity_assistant_enabled : true, next_task_suggestion_enabled: updatedProfile.next_task_suggestion_enabled !== undefined ? updatedProfile.next_task_suggestion_enabled : prev.next_task_suggestion_enabled !== undefined ? prev.next_task_suggestion_enabled : true, pomodoro_enabled: updatedProfile.pomodoro_enabled !== undefined ? updatedProfile.pomodoro_enabled : prev.pomodoro_enabled !== undefined ? prev.pomodoro_enabled : true, })); // Apply appearance change after save if ( updatedProfile.appearance !== (isDarkMode ? 'dark' : 'light') && toggleDarkMode ) { toggleDarkMode(); } // Apply language change after save if (updatedProfile.language !== i18n.language) { await handleLanguageChange(updatedProfile.language); } // Notify other components about Pomodoro setting change if (updatedProfile.pomodoro_enabled !== undefined) { window.dispatchEvent( new CustomEvent('pomodoroSettingChanged', { detail: { enabled: updatedProfile.pomodoro_enabled }, }) ); } // Clear password fields on successful save if (isPasswordChange) { setFormData((prev) => ({ ...prev, currentPassword: '', newPassword: '', confirmPassword: '', })); } const successMessage = isPasswordChange ? t( 'profile.passwordChangeSuccess', 'Password changed successfully!' ) : t('profile.successMessage', 'Profile updated successfully!'); showSuccessToast(successMessage); } catch (err) { showErrorToast((err as Error).message); } }; if (loading) { return (
{t('common.loading')}
); } const tabs = [ { id: 'general', name: t('profile.tabs.general', 'General'), icon: 'user', }, { id: 'security', name: t('profile.tabs.security', 'Security'), icon: 'shield', }, { id: 'productivity', name: t('profile.tabs.productivity', 'Productivity'), icon: 'clock', }, { id: 'telegram', name: t('profile.tabs.telegram', 'Telegram'), icon: 'chat', }, { id: 'ai', name: t('profile.tabs.ai', 'AI Features'), icon: 'sparkles', }, ]; const renderTabIcon = (iconType: string) => { switch (iconType) { case 'user': return ; case 'clock': return ; case 'chat': return ; case 'shield': return ; case 'sparkles': return ; default: return null; } }; return (

{t('profile.title')}

{/* Navigation Tabs */}
{/* General Tab */} {activeTab === 'general' && (

{t( 'profile.accountSettings', 'Account & Preferences' )}

{ setFormData((prev) => ({ ...prev, language: languageCode, })); }} />
)} {/* Security Tab */} {activeTab === 'security' && (

{t('profile.security', 'Security Settings')}

{/* Password Change Section */}

{t('profile.changePassword', 'Change Password')}

{t( 'profile.passwordChangeOptional', 'Leave password fields empty to update other settings without changing your password.' )}

{t( 'profile.passwordChangeNote', 'Password changes will be saved when you click "Save Changes" at the bottom of the form.' )}
)} {/* Productivity Tab */} {activeTab === 'productivity' && (

{t( 'profile.productivityFeatures', 'Productivity Features' )}

{/* Pomodoro Timer */}

{t( 'profile.pomodoroDescription', 'Enable the Pomodoro timer in the navigation bar for focused work sessions.' )}

{ setFormData((prev) => ({ ...prev, pomodoro_enabled: !prev.pomodoro_enabled, })); }} >
)} {/* Telegram Tab */} {activeTab === 'telegram' && (

{t( 'profile.telegramIntegration', 'Telegram Integration' )}

{/* Bot Setup Subsection */}

{t('profile.botSetup', 'Bot Setup')}

{t( 'profile.telegramDescription', 'Connect your tududi account to a Telegram bot to add items to your inbox via Telegram messages.' )}

{t( 'profile.telegramTokenDescription', 'Create a bot with @BotFather on Telegram and paste the token here.' )}

{profile?.telegram_chat_id && (

{t( 'profile.telegramConnected', 'Your Telegram account is connected! Send messages to your bot to add items to your tududi inbox.' )}

)} {(telegramBotInfo || profile?.telegram_bot_token) && (

{t( 'profile.botConfigured', 'Bot configured successfully!' )}

{telegramBotInfo?.first_name && (

Bot Name:{' '} {telegramBotInfo.first_name}

)} {telegramBotInfo?.username && (

{t( 'profile.botUsername', 'Bot Username:' )}{' '} @{telegramBotInfo.username}

)}

{t( 'profile.pollingStatus', 'Polling Status:' )}{' '}

{isPolling ? t( 'profile.pollingActive' ) : t( 'profile.pollingInactive' )}

{t( 'profile.pollingNote', 'Polling periodically checks for new messages from Telegram and adds them to your inbox.' )}

{isPolling ? ( ) : ( )} {telegramBotInfo?.chat_url && ( {t( 'profile.openTelegram', 'Open in Telegram' )} )}
)} {/* Status indicator */} {telegramSetupStatus === 'success' && (
Bot configured successfully!
)} {telegramSetupStatus === 'error' && (
Setup failed. Please check your token.
)}
{/* Task Summary Notifications Subsection */}

{t( 'profile.taskSummaryNotifications', 'Task Summary Notifications' )}

{t( 'profile.taskSummaryDescription', 'Receive regular summaries of your tasks via Telegram. This feature requires your Telegram integration to be set up.' )}

{ setFormData((prev) => ({ ...prev, task_summary_enabled: !prev.task_summary_enabled, })); }} >
{[ '1h', '2h', '4h', '8h', '12h', 'daily', 'weekly', ].map((frequency) => ( ))}

{t( 'profile.frequencyHelp', 'Choose how often you want to receive task summaries.' )}

{(!profile?.telegram_bot_token || !profile?.telegram_chat_id) && (

{t( 'profile.telegramRequiredForSummaries', 'Telegram integration must be set up to use task summaries.' )}

)}
)} {/* AI Features Tab */} {activeTab === 'ai' && (

{t( 'profile.aiProductivityFeatures', 'AI & Productivity Features' )}

{/* Task Intelligence Subsection */}

{t( 'profile.taskIntelligence', 'Task Intelligence' )}

{t( 'profile.taskIntelligenceDescription', 'Get helpful suggestions to make your task names more descriptive and actionable.' )}

{ setFormData((prev) => ({ ...prev, task_intelligence_enabled: !prev.task_intelligence_enabled, })); }} >
{/* Auto-Suggest Next Actions Subsection */}

{t( 'profile.autoSuggestNextActions', 'Auto-Suggest Next Actions' )}

{t( 'profile.autoSuggestNextActionsDescription', 'When creating a project, automatically prompt for the very next physical action to take.' )}

{ setFormData((prev) => ({ ...prev, auto_suggest_next_actions_enabled: !prev.auto_suggest_next_actions_enabled, })); }} >
{/* Productivity Assistant Subsection */}

{t( 'profile.productivityAssistant', 'Productivity Assistant' )}

{t( 'profile.productivityAssistantDescription', 'Show productivity insights that help identify stalled projects, vague tasks, and workflow improvements on your Today page.' )}

{ setFormData((prev) => ({ ...prev, productivity_assistant_enabled: !prev.productivity_assistant_enabled, })); }} >
{/* Next Task Suggestion Subsection */}

{t( 'profile.nextTaskSuggestion', 'Next Task Suggestion' )}

{t( 'profile.nextTaskSuggestionDescription', 'Automatically suggest the next best task to work on when you have nothing in progress, prioritizing due today tasks, then suggested tasks, then next actions.' )}

{ setFormData((prev) => ({ ...prev, next_task_suggestion_enabled: !prev.next_task_suggestion_enabled, })); }} >
)} {/* Save Button */}
); }; export default ProfileSettings;