import React, { useState, useEffect, useRef } from 'react'; import { useForm } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; import * as yup from 'yup'; import { User, Mail, Phone, Save, CheckCircle, AlertCircle, Lock, Camera, Shield, ShieldCheck, ShieldOff, Copy, Eye, EyeOff, RefreshCw, KeyRound, LogOut, Monitor, Smartphone, Tablet } from 'lucide-react'; import { toast } from 'react-toastify'; import authService from '../../features/auth/services/authService'; import sessionService from '../../features/auth/services/sessionService'; import useAuthStore from '../../store/useAuthStore'; import Loading from '../../shared/components/Loading'; import EmptyState from '../../shared/components/EmptyState'; import { useAsync } from '../../shared/hooks/useAsync'; import { useGlobalLoading } from '../../shared/contexts/LoadingContext'; import { normalizeImageUrl } from '../../shared/utils/imageUtils'; import { formatDate } from '../../shared/utils/format'; import { UserSession } from '../../features/auth/services/sessionService'; const profileValidationSchema = yup.object().shape({ name: yup .string() .required('Full name is required') .min(2, 'Full name must be at least 2 characters') .max(100, 'Full name cannot exceed 100 characters'), email: yup .string() .required('Email is required') .email('Invalid email address'), phone: yup .string() .required('Phone number is required') .matches( /^[\d\s\-\+\(\)]{5,}$/, 'Please enter a valid phone number' ), }); const passwordValidationSchema = yup.object().shape({ currentPassword: yup .string() .required('Current password is required'), newPassword: yup .string() .required('New password is required') .min(8, 'Password must be at least 8 characters') .matches(/[A-Z]/, 'Password must contain at least one uppercase letter') .matches(/[a-z]/, 'Password must contain at least one lowercase letter') .matches(/[0-9]/, 'Password must contain at least one number') .matches(/[!@#$%^&*(),.?":{}|<>]/, 'Password must contain at least one special character (!@#$%^&*(),.?":{}|<>)'), confirmPassword: yup .string() .required('Please confirm your password') .oneOf([yup.ref('newPassword')], 'Passwords must match'), }); type ProfileFormData = yup.InferType; type PasswordFormData = yup.InferType; const HousekeepingProfilePage: React.FC = () => { const { userInfo, setUser } = useAuthStore(); const { setLoading } = useGlobalLoading(); const [activeTab, setActiveTab] = useState<'profile' | 'password' | 'mfa' | 'sessions'>('profile'); const [avatarPreview, setAvatarPreview] = useState(null); const [avatarError, setAvatarError] = useState(false); const [showPassword, setShowPassword] = useState<{ current: boolean; new: boolean; confirm: boolean; }>({ current: false, new: false, confirm: false, }); const [mfaStatus, setMfaStatus] = useState<{mfa_enabled: boolean; backup_codes_count: number} | null>(null); const [mfaSecret, setMfaSecret] = useState(null); const [mfaQrCode, setMfaQrCode] = useState(null); const [mfaVerificationToken, setMfaVerificationToken] = useState(''); const [showBackupCodes, setShowBackupCodes] = useState(null); const [showMfaSecret, setShowMfaSecret] = useState(false); const mfaAbortControllerRef = useRef(null); const fetchProfile = async () => { const response = await authService.getProfile(); if (response.status === 'success' || response.success) { const user = response.data?.user || response.data; if (user) { setUser(user as any); return user; } } throw new Error('Failed to load profile'); }; const { data: profileData, loading: loadingProfile, error: profileError, execute: refetchProfile } = useAsync(fetchProfile, { immediate: true, onError: (error: any) => { toast.error(error.message || 'Unable to load profile'); } }); const { register: registerProfile, handleSubmit: handleSubmitProfile, formState: { errors: profileErrors }, reset: resetProfile, } = useForm({ resolver: yupResolver(profileValidationSchema), defaultValues: { name: ((userInfo && 'user' in userInfo ? userInfo.user : userInfo) as any)?.name || '', email: ((userInfo && 'user' in userInfo ? userInfo.user : userInfo) as any)?.email || '', phone: ((userInfo && 'user' in userInfo ? userInfo.user : userInfo) as any)?.phone || '', }, }); const { register: registerPassword, handleSubmit: handleSubmitPassword, formState: { errors: passwordErrors }, reset: resetPassword, } = useForm({ resolver: yupResolver(passwordValidationSchema), }); const fetchMFAStatus = async () => { try { const response = await authService.getMFAStatus(); if (response) { setMfaStatus({ mfa_enabled: response.mfa_enabled || false, backup_codes_count: response.backup_codes_count || 0, }); } } catch (error: any) { if (error.name === 'AbortError') return; } }; useEffect(() => { if (profileData || userInfo) { const data = profileData || (userInfo && 'user' in userInfo ? userInfo.user : userInfo); resetProfile({ name: (data as any)?.name || '', email: (data as any)?.email || '', phone: (data as any)?.phone || '', }); if ((data as any)?.avatar) { setAvatarPreview(normalizeImageUrl((data as any).avatar)); setAvatarError(false); } else { setAvatarPreview(null); setAvatarError(false); } } }, [profileData, userInfo, resetProfile]); useEffect(() => { if (activeTab === 'mfa') { if (mfaAbortControllerRef.current) { mfaAbortControllerRef.current.abort(); } mfaAbortControllerRef.current = new AbortController(); fetchMFAStatus(); } return () => { if (mfaAbortControllerRef.current) { mfaAbortControllerRef.current.abort(); } }; }, [activeTab]); const onSubmitProfile = async (data: ProfileFormData) => { try { setLoading(true, 'Updating profile...'); if ('updateProfile' in authService) { const response = await (authService as any).updateProfile({ full_name: data.name, email: data.email, phone_number: data.phone, }); if (response.status === 'success' || response.success) { const updatedUser = response.data?.user || response.data; if (updatedUser) { setUser(updatedUser); toast.success('Profile updated successfully!'); refetchProfile(); } } } else { const { updateUser } = await import('../../features/auth/services/userService'); const user = userInfo && 'user' in userInfo ? userInfo.user : userInfo; const response = await updateUser((user as any)?.id || (userInfo as any)?.id, { full_name: data.name, email: data.email, phone_number: data.phone, }); if ((response as any).success || (response as any).status === 'success') { const updatedUser = response.data?.user || response.data; if (updatedUser) { setUser({ id: updatedUser.id, name: (updatedUser as any).full_name || (updatedUser as any).name, email: updatedUser.email, phone: (updatedUser as any).phone_number || (updatedUser as any).phone, avatar: (updatedUser as any).avatar, role: updatedUser.role, }); toast.success('Profile updated successfully!'); refetchProfile(); } } } } catch (error: any) { const errorMessage = error.response?.data?.message || error.message || 'Failed to update profile'; toast.error(errorMessage); } finally { setLoading(false); } }; const onSubmitPassword = async (data: PasswordFormData) => { try { setLoading(true, 'Changing password...'); if ('updateProfile' in authService) { const response = await (authService as any).updateProfile({ currentPassword: data.currentPassword, password: data.newPassword, }); if (response.status === 'success' || response.success) { toast.success('Password changed successfully!'); resetPassword(); } } else { const { updateUser } = await import('../../features/auth/services/userService'); const user = userInfo && 'user' in userInfo ? userInfo.user : userInfo; const response = await updateUser((user as any)?.id || (userInfo as any)?.id, { password: data.newPassword, }); if ((response as any).success || (response as any).status === 'success') { toast.success('Password changed successfully!'); resetPassword(); } } } catch (error: any) { const errorMessage = error.response?.data?.message || error.message || 'Failed to change password'; toast.error(errorMessage); } finally { setLoading(false); } }; const handleAvatarChange = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; if (!file.type.startsWith('image/')) { toast.error('Please select an image file'); return; } if (file.size > 2 * 1024 * 1024) { toast.error('Image size must be less than 2MB'); return; } const reader = new FileReader(); reader.onloadend = () => { setAvatarPreview(reader.result as string); }; reader.readAsDataURL(file); try { setLoading(true, 'Uploading avatar...'); const response = await authService.uploadAvatar(file); if ((response.status === 'success' || response.success) && response.data) { const updatedUser = response.data.user || response.data; const avatarUrl = (response.data as any).full_url || (response.data as any).avatar_url || normalizeImageUrl((updatedUser as any)?.avatar); if (updatedUser) { setUser({ id: updatedUser.id, name: (updatedUser as any).name || (updatedUser as any).full_name, email: updatedUser.email, phone: (updatedUser as any).phone || (updatedUser as any).phone_number, avatar: avatarUrl, role: updatedUser.role, }); setAvatarPreview(avatarUrl || null); setAvatarError(false); toast.success(response.message || 'Avatar uploaded successfully!'); refetchProfile(); } else { throw new Error('Invalid response format'); } } else { throw new Error(response.message || 'Upload failed'); } } catch (error: any) { const errorMessage = error.response?.data?.detail || error.response?.data?.message || error.message || 'Failed to upload avatar. Please try again.'; toast.error(errorMessage); if (userInfo?.avatar) { setAvatarPreview(normalizeImageUrl(userInfo.avatar)); setAvatarError(true); } else { setAvatarPreview(null); } } finally { setLoading(false); e.target.value = ''; } }; if (loadingProfile) { return ; } if (profileError && !userInfo) { return (
); } return (
{/* Header */}

Housekeeping Profile

Manage your account information and security settings

{/* Tabs */}
{/* Profile Tab */} {activeTab === 'profile' && (
{/* Avatar Section */}
{(avatarPreview || userInfo?.avatar) && !avatarError ? ( Profile setAvatarError(true)} /> ) : (
)}
{/* Name Field */}
{profileErrors.name && (

{profileErrors.name.message}

)}
{/* Email Field */}
{profileErrors.email && (

{profileErrors.email.message}

)}
{/* Phone Field */}
{profileErrors.phone && (

{profileErrors.phone.message}

)}
{/* Submit Button */}
)} {/* Password Tab */} {activeTab === 'password' && (
{/* Password Requirements */}

Password Requirements

  • • At least 8 characters long
  • • Contains uppercase and lowercase letters
  • • Contains at least one number
  • • Contains at least one special character
{/* Current Password */}
{passwordErrors.currentPassword && (

{passwordErrors.currentPassword.message}

)}
{/* New Password */}
{passwordErrors.newPassword && (

{passwordErrors.newPassword.message}

)}
{/* Confirm Password */}
{passwordErrors.confirmPassword && (

{passwordErrors.confirmPassword.message}

)}
{/* Submit Button */}
)} {/* MFA Tab */} {activeTab === 'mfa' && ( )} {/* Sessions Tab */} {activeTab === 'sessions' && }
); }; // MFA Tab Component interface MFATabProps { mfaStatus: {mfa_enabled: boolean; backup_codes_count: number} | null; mfaSecret: string | null; mfaQrCode: string | null; mfaVerificationToken: string; showBackupCodes: string[] | null; showMfaSecret: boolean; setMfaSecret: (value: string | null) => void; setMfaQrCode: (value: string | null) => void; setMfaVerificationToken: (value: string) => void; setShowBackupCodes: (value: string[] | null) => void; setShowMfaSecret: (value: boolean) => void; fetchMFAStatus: () => Promise; setLoading: (loading: boolean, message?: string) => void; } const MFATab: React.FC = ({ mfaStatus, mfaSecret, mfaQrCode, mfaVerificationToken, showBackupCodes, showMfaSecret, setMfaSecret, setMfaQrCode, setMfaVerificationToken, setShowBackupCodes, setShowMfaSecret, fetchMFAStatus, setLoading, }) => { return (

Two-Factor Authentication

Add extra security to your account with MFA.

{mfaStatus?.mfa_enabled ? (

MFA is Enabled

Your account is protected with two-factor authentication.

Remaining backup codes: {mfaStatus.backup_codes_count}

{showBackupCodes && (

Save Your Backup Codes

{showBackupCodes.map((code, index) => (
{code}
))}
)}
) : (
{!mfaSecret ? ( ) : (

Step 1: Scan QR Code

Use your authenticator app to scan this QR code.

{mfaQrCode && (
MFA QR Code
)}
Manual Entry Key:
{showMfaSecret ? mfaSecret : '••••••••••••••••'}

Step 2: Verify Setup

Enter the 6-digit code from your authenticator app.

setMfaVerificationToken(e.target.value.replace(/\D/g, '').slice(0, 6))} placeholder="000000" maxLength={6} className="luxury-input text-center text-xl tracking-widest font-mono" />
)}
)}
); }; // Sessions Tab Component const SessionsTab: React.FC = () => { const [sessions, setSessions] = useState([]); const [loading, setLoading] = useState(true); const abortControllerRef = useRef(null); useEffect(() => { if (abortControllerRef.current) { abortControllerRef.current.abort(); } abortControllerRef.current = new AbortController(); fetchSessions(); return () => { if (abortControllerRef.current) { abortControllerRef.current.abort(); } }; }, []); const fetchSessions = async () => { try { setLoading(true); const response = await sessionService.getMySessions(); setSessions(response.data?.sessions || []); } catch (error: any) { if (error.name === 'AbortError') return; toast.error(error.response?.data?.message || 'Unable to load sessions'); } finally { setLoading(false); } }; const getDeviceIcon = (userAgent?: string) => { if (!userAgent) return ; if (userAgent.includes('Mobile')) return ; if (userAgent.includes('Tablet')) return ; return ; }; const getDeviceName = (userAgent?: string) => { if (!userAgent) return 'Unknown Device'; if (userAgent.includes('Chrome')) return 'Chrome Browser'; if (userAgent.includes('Firefox')) return 'Firefox Browser'; if (userAgent.includes('Safari')) return 'Safari Browser'; if (userAgent.includes('Edge')) return 'Edge Browser'; if (userAgent.includes('Mobile')) return 'Mobile Device'; if (userAgent.includes('Tablet')) return 'Tablet Device'; return 'Web Browser'; }; const handleRevoke = async (sessionId: number) => { if (!confirm('Are you sure you want to revoke this session?')) return; try { const response = await sessionService.revokeSession(sessionId); if (response.data?.logout_required) { toast.warning('Your current session has been revoked. You will be logged out.'); setTimeout(() => { window.location.href = '/'; }, 2000); } else { toast.success('Session revoked successfully'); fetchSessions(); } } catch (error: any) { if (error.response?.status === 401) { toast.warning('Your session has been revoked. You will be logged out.'); setTimeout(() => { window.location.href = '/'; }, 2000); } else { toast.error(error.response?.data?.message || 'Unable to revoke session'); } } }; const handleRevokeAll = async () => { if (!confirm('Are you sure you want to revoke all other sessions?')) return; try { const response = await sessionService.revokeAllSessions(); if (response.data?.logout_required) { toast.warning('All sessions have been revoked. You will be logged out.'); setTimeout(() => { window.location.href = '/'; }, 2000); } else { toast.success('All sessions revoked'); fetchSessions(); } } catch (error: any) { if (error.response?.status === 401) { toast.warning('All sessions have been revoked. You will be logged out.'); setTimeout(() => { window.location.href = '/'; }, 2000); } else { toast.error(error.response?.data?.message || 'Unable to revoke sessions'); } } }; if (loading) { return (
); } return (

Active Sessions

Manage your active sessions across different devices

{sessions.length === 0 ? (

No active sessions

) : (
{sessions.length > 1 && (
)} {sessions.map((session) => (
{getDeviceIcon(session.user_agent)}

{getDeviceName(session.user_agent)}

IP Address: {session.ip_address || 'Unknown'}

Last Activity: {formatDate(session.last_activity)}

Expires: {formatDate(session.expires_at)}

))}
)}
); }; export default HousekeepingProfilePage;