From 25fff9b1c3a261c84829f830bbd9cc6bff03cd7b Mon Sep 17 00:00:00 2001 From: seaznCode Date: Sat, 11 Oct 2025 19:47:07 +0200 Subject: [PATCH 1/6] feat: Implement user status management with custom hook - Added `useUserStatus` hook to manage user status fetching and state. - Integrated user status in Quick Action Dashboard and related pages. - Enhanced error handling and loading states for user status. - Updated profile completion and document upload flows to refresh user status after actions. - Created a centralized API utility for handling requests and responses. - Refactored authentication token management to use session storage. --- src/app/ClientWrapper.tsx | 9 +- src/app/components/AuthInitializer.tsx | 26 + src/app/components/dashboard/QuickActions.tsx | 870 ++++++++++++++++++ src/app/dashboard/page.tsx | 36 +- src/app/debug-auth/page.tsx | 112 +++ src/app/hooks/useUserStatus.ts | 161 ++++ src/app/quickaction-dashboard/page.tsx | 92 +- .../company/page.tsx | 58 +- .../personal/page.tsx | 59 +- .../register-email-verify/page.tsx | 64 +- .../register-sign-contract/company/page.tsx | 66 +- .../register-sign-contract/personal/page.tsx | 64 +- .../register-upload-id/company/page.tsx | 52 +- .../register-upload-id/personal/page.tsx | 57 +- src/app/store/authStore.ts | 43 +- src/app/utils/api.ts | 241 +++++ 16 files changed, 1933 insertions(+), 77 deletions(-) create mode 100644 src/app/components/AuthInitializer.tsx create mode 100644 src/app/components/dashboard/QuickActions.tsx create mode 100644 src/app/debug-auth/page.tsx create mode 100644 src/app/hooks/useUserStatus.ts create mode 100644 src/app/utils/api.ts diff --git a/src/app/ClientWrapper.tsx b/src/app/ClientWrapper.tsx index 76d850b..b7e849f 100644 --- a/src/app/ClientWrapper.tsx +++ b/src/app/ClientWrapper.tsx @@ -1,7 +1,14 @@ 'use client'; import { I18nProvider } from './i18n/useTranslation'; +import AuthInitializer from './components/AuthInitializer'; export default function ClientWrapper({ children }: { children: React.ReactNode }) { - return {children}; + return ( + + + {children} + + + ); } \ No newline at end of file diff --git a/src/app/components/AuthInitializer.tsx b/src/app/components/AuthInitializer.tsx new file mode 100644 index 0000000..5f26164 --- /dev/null +++ b/src/app/components/AuthInitializer.tsx @@ -0,0 +1,26 @@ +'use client' + +import { useEffect } from 'react' +import useAuthStore from '../store/authStore' + +export default function AuthInitializer({ children }: { children: React.ReactNode }) { + const { refreshAuthToken, setAuthReady } = useAuthStore() + + useEffect(() => { + const initializeAuth = async () => { + try { + // Try to refresh token from httpOnly cookie + await refreshAuthToken() + } catch (error) { + console.log('No valid refresh token found') + } finally { + // Set auth as ready regardless of success/failure + setAuthReady(true) + } + } + + initializeAuth() + }, [refreshAuthToken, setAuthReady]) + + return <>{children} +} \ No newline at end of file diff --git a/src/app/components/dashboard/QuickActions.tsx b/src/app/components/dashboard/QuickActions.tsx new file mode 100644 index 0000000..8283ab5 --- /dev/null +++ b/src/app/components/dashboard/QuickActions.tsx @@ -0,0 +1,870 @@ +'use client' + +import { useState, useEffect } from 'react' +import { + EnvelopeIcon, + DocumentTextIcon, + UserPlusIcon, + DocumentCheckIcon, + CheckCircleIcon, + XCircleIcon, + ExclamationTriangleIcon +} from '@heroicons/react/24/outline' +import useAuthStore from '../../store/authStore' +import { useUserStatus } from '../../hooks/useUserStatus' + +interface QuickAction { + id: string + title: string + description: string + icon: any + color: string + status: 'pending' | 'completed' | 'unavailable' + onClick: () => void +} + +// UserStatus interface is now imported from useUserStatus hook + +export default function QuickActions() { + const { userStatus, loading, error, refreshStatus } = useUserStatus() + const [showEmailVerification, setShowEmailVerification] = useState(false) + const [showDocumentUpload, setShowDocumentUpload] = useState(false) + const [showProfileCompletion, setShowProfileCompletion] = useState(false) + const [showContractSigning, setShowContractSigning] = useState(false) + const [isClient, setIsClient] = useState(false) + + const user = useAuthStore(state => state.user) + + // Handle SSR hydration + useEffect(() => { + setIsClient(true) + }, []) + + // Debug logging (can be removed in production) + useEffect(() => { + if (isClient && process.env.NODE_ENV === 'development') { + console.log('🔍 [QuickActions] userStatus changed:', userStatus) + console.log('🔍 [QuickActions] loading state:', loading) + console.log('🔍 [QuickActions] error state:', error) + } + }, [isClient, userStatus, loading, error]) + + // Don't render until client-side hydration is complete + if (!isClient) { + return ( +
+
+

Account Setup

+
+
+ {[1, 2, 3, 4].map((i) => ( +
+
+
+
+
+
+
+
+
+ ))} +
+
+ ) + } + + const getActionStatus = (action: string): 'pending' | 'completed' | 'unavailable' => { + // If loading or no userStatus, show initial states (email is pending, others unavailable) + if (!userStatus) { + if (process.env.NODE_ENV === 'development') { + console.log(`🔍 [getActionStatus] No userStatus for action: ${action}`) + } + switch (action) { + case 'email': + return 'pending' + default: + return 'unavailable' + } + } + + if (process.env.NODE_ENV === 'development') { + console.log(`🔍 [getActionStatus] ${action}:`, { + email_verified: userStatus.email_verified, + documents_uploaded: userStatus.documents_uploaded, + profile_completed: userStatus.profile_completed, + contract_signed: userStatus.contract_signed + }) + } + + switch (action) { + case 'email': + return userStatus.email_verified ? 'completed' : 'pending' + case 'documents': + return !userStatus.email_verified ? 'unavailable' : + userStatus.documents_uploaded ? 'completed' : 'pending' + case 'profile': + return !userStatus.documents_uploaded ? 'unavailable' : + userStatus.profile_completed ? 'completed' : 'pending' + case 'contract': + return !userStatus.profile_completed ? 'unavailable' : + userStatus.contract_signed ? 'completed' : 'pending' + default: + return 'pending' + } + } + + const quickActions: QuickAction[] = [ + { + id: 'email', + title: 'Verify Email', + description: 'Confirm your email address to activate your account', + icon: EnvelopeIcon, + color: 'bg-blue-500', + status: getActionStatus('email'), + onClick: () => setShowEmailVerification(true) + }, + { + id: 'documents', + title: 'Upload ID Documents', + description: user?.userType === 'company' ? 'Upload company registration documents' : 'Upload personal identification documents', + icon: DocumentTextIcon, + color: 'bg-green-500', + status: getActionStatus('documents'), + onClick: () => setShowDocumentUpload(true) + }, + { + id: 'profile', + title: 'Complete Profile', + description: 'Add additional information to complete your profile', + icon: UserPlusIcon, + color: 'bg-purple-500', + status: getActionStatus('profile'), + onClick: () => setShowProfileCompletion(true) + }, + { + id: 'contract', + title: 'Sign Contract', + description: 'Review and sign your service agreement', + icon: DocumentCheckIcon, + color: 'bg-orange-500', + status: getActionStatus('contract'), + onClick: () => setShowContractSigning(true) + } + ] + + const getStatusIcon = (status: string) => { + switch (status) { + case 'completed': + return + case 'unavailable': + return + default: + return + } + } + + const getStatusText = (status: string) => { + switch (status) { + case 'completed': + return 'Completed' + case 'unavailable': + return 'Locked' + default: + return 'Pending' + } + } + + // Show error state if there's an error + if (error && !userStatus) { + return ( +
+

Account Setup

+
+
+ +
+

Error loading account status

+
+

{error}

+
+
+ +
+
+
+
+
+ ) + } + + return ( + <> +
+
+

Account Setup

+ {loading && ( +
+
+ Updating status... +
+ )} +
+
+ {quickActions.map((action) => ( + + ))} +
+
+ + {/* Modals */} + {showEmailVerification && ( + setShowEmailVerification(false)} + onSuccess={refreshStatus} + /> + )} + + {showDocumentUpload && ( + setShowDocumentUpload(false)} + onSuccess={refreshStatus} + /> + )} + + {showProfileCompletion && ( + setShowProfileCompletion(false)} + onSuccess={refreshStatus} + /> + )} + + {showContractSigning && ( + setShowContractSigning(false)} + onSuccess={refreshStatus} + /> + )} + + ) +} + +// Email Verification Modal Component +function EmailVerificationModal({ onClose, onSuccess }: { onClose: () => void, onSuccess: () => void }) { + const [verificationCode, setVerificationCode] = useState('') + const [loading, setLoading] = useState(false) + const [message, setMessage] = useState('') + const [error, setError] = useState('') + const token = useAuthStore(state => state.accessToken) + + const sendVerificationEmail = async () => { + if (!token) return + + setLoading(true) + setError('') + setMessage('') + + try { + const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/send-verification-email`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }) + + const data = await response.json() + + if (response.ok) { + setMessage('Verification email sent! Check your inbox.') + } else { + setError(data.message || 'Failed to send verification email') + } + } catch (error) { + setError('Network error occurred') + } finally { + setLoading(false) + } + } + + const verifyCode = async () => { + if (!token || !verificationCode.trim()) return + + setLoading(true) + setError('') + + try { + const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/verify-email-code`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ code: verificationCode }) + }) + + const data = await response.json() + + if (response.ok) { + setMessage('Email verified successfully!') + setTimeout(() => { + onSuccess() + onClose() + }, 1500) + } else { + setError(data.message || 'Invalid verification code') + } + } catch (error) { + setError('Network error occurred') + } finally { + setLoading(false) + } + } + + return ( +
+
+
+

Email Verification

+ +
+ +
+
+

+ We'll send a verification code to your email address. +

+ + +
+ +
+ + setVerificationCode(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent" + placeholder="Enter 6-digit code" + maxLength={6} + /> +
+ + + + {message && ( +
{message}
+ )} + + {error && ( +
{error}
+ )} +
+
+
+ ) +} + +// Document Upload Modal Component +function DocumentUploadModal({ userType, onClose, onSuccess }: { + userType: string, + onClose: () => void, + onSuccess: () => void +}) { + const [frontFile, setFrontFile] = useState(null) + const [backFile, setBackFile] = useState(null) + const [idType, setIdType] = useState('') + const [idNumber, setIdNumber] = useState('') + const [expiryDate, setExpiryDate] = useState('') + const [loading, setLoading] = useState(false) + const [error, setError] = useState('') + const [message, setMessage] = useState('') + const token = useAuthStore(state => state.accessToken) + + const handleUpload = async () => { + if (!token || !frontFile || !idType || !idNumber || !expiryDate) { + setError('Please fill in all required fields and select front image') + return + } + + setLoading(true) + setError('') + setMessage('') + + const formData = new FormData() + formData.append('front', frontFile) + if (backFile) { + formData.append('back', backFile) + } + formData.append('idType', idType) + formData.append('idNumber', idNumber) + formData.append('expiryDate', expiryDate) + + try { + const endpoint = userType === 'company' ? '/api/upload/company-id' : '/api/upload/personal-id' + const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}${endpoint}`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}` + }, + body: formData + }) + + const data = await response.json() + + if (response.ok) { + setMessage('Documents uploaded successfully!') + setTimeout(() => { + onSuccess() + onClose() + }, 1500) + } else { + setError(data.message || 'Failed to upload documents') + } + } catch (error) { + setError('Network error occurred') + } finally { + setLoading(false) + } + } + + return ( +
+
+
+

+ Upload {userType === 'company' ? 'Company' : 'Personal'} Documents +

+ +
+ +
+
+ + setFrontFile(e.target.files?.[0] || null)} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent" + /> +
+ +
+ + setBackFile(e.target.files?.[0] || null)} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent" + /> +
+ +
+ + +
+ +
+ + setIdNumber(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent" + placeholder={userType === 'company' ? 'Enter registration number' : 'Enter ID number'} + /> +
+ +
+ + setExpiryDate(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent" + /> +
+ + + + {message && ( +
{message}
+ )} + + {error && ( +
{error}
+ )} +
+
+
+ ) +} + +// Profile Completion Modal Component +function ProfileCompletionModal({ userType, onClose, onSuccess }: { + userType: string, + onClose: () => void, + onSuccess: () => void +}) { + const [formData, setFormData] = useState({}) + const [loading, setLoading] = useState(false) + const [error, setError] = useState('') + const [message, setMessage] = useState('') + const token = useAuthStore(state => state.accessToken) + + const handleSubmit = async () => { + if (!token) return + + setLoading(true) + setError('') + setMessage('') + + try { + const endpoint = userType === 'company' ? '/api/profile/company/complete' : '/api/profile/personal/complete' + const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}${endpoint}`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(formData) + }) + + const data = await response.json() + + if (response.ok) { + setMessage('Profile completed successfully!') + setTimeout(() => { + onSuccess() + onClose() + }, 1500) + } else { + setError(data.message || 'Failed to complete profile') + } + } catch (error) { + setError('Network error occurred') + } finally { + setLoading(false) + } + } + + return ( +
+
+
+

Complete Profile

+ +
+ +
+ {userType === 'company' ? ( + <> +
+ + setFormData({...formData, companyName: e.target.value})} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent" + /> +
+
+ + setFormData({...formData, industry: e.target.value})} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent" + /> +
+ + ) : ( + <> +
+ + setFormData({...formData, phone: e.target.value})} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent" + /> +
+
+ +