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
+
+
+
+ Try again
+
+
+
+
+
+
+ )
+ }
+
+ return (
+ <>
+
+
+
Account Setup
+ {loading && (
+
+ )}
+
+
+ {quickActions.map((action) => (
+
+
+
+
+
+
+ {action.title}
+
+ {loading ? (
+
+ ) : (
+ getStatusIcon(action.status)
+ )}
+
+
+ {action.description}
+
+
+ {loading ? (
+
+ ) : (
+
+ {getStatusText(action.status)}
+
+ )}
+
+
+
+
+ ))}
+
+
+
+ {/* 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.
+
+
+
+ {loading ? 'Sending...' : 'Send Verification Email'}
+
+
+
+
+
+ Verification Code
+
+ 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}
+ />
+
+
+
+ {loading ? 'Verifying...' : 'Verify Email'}
+
+
+ {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
+
+
+
+
+
+
+
+
+
+ {userType === 'company' ? 'Company Registration (Front)' : 'ID Document (Front)'}
+
+ 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"
+ />
+
+
+
+
+ {userType === 'company' ? 'Additional Documents (Optional)' : 'ID Document (Back)'}
+
+ 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"
+ />
+
+
+
+
+ {userType === 'company' ? 'Document Type' : 'ID Type'} *
+
+ setIdType(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"
+ >
+ Select Document Type
+ {userType === 'company' ? (
+ <>
+ Business Registration
+ Tax Certificate
+ Business License
+ Other
+ >
+ ) : (
+ <>
+ Passport
+ Driver's License
+ National ID
+ Other
+ >
+ )}
+
+
+
+
+
+ {userType === 'company' ? 'Registration/Document Number' : 'ID Number'} *
+
+ 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'}
+ />
+
+
+
+
+ Expiry Date *
+
+ 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"
+ />
+
+
+
+ {loading ? 'Uploading...' : 'Upload Documents'}
+
+
+ {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' ? (
+ <>
+
+ Company Name
+ 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"
+ />
+
+
+ Industry
+ 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"
+ />
+
+ >
+ ) : (
+ <>
+
+ Phone Number
+ 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"
+ />
+
+
+ Address
+
+ >
+ )}
+
+
+ {loading ? 'Saving...' : 'Complete Profile'}
+
+
+ {message && (
+
{message}
+ )}
+
+ {error && (
+
{error}
+ )}
+
+
+
+ )
+}
+
+// Contract Signing Modal Component
+function ContractSigningModal({ userType, onClose, onSuccess }: {
+ userType: string,
+ onClose: () => void,
+ onSuccess: () => void
+}) {
+ const [contractFile, setContractFile] = useState(null)
+ const [agreed, setAgreed] = useState(false)
+ const [loading, setLoading] = useState(false)
+ const [error, setError] = useState('')
+ const [message, setMessage] = useState('')
+ const token = useAuthStore(state => state.accessToken)
+
+ const handleUpload = async () => {
+ if (!token || !contractFile || !agreed) return
+
+ setLoading(true)
+ setError('')
+ setMessage('')
+
+ const formData = new FormData()
+ formData.append('contract', contractFile)
+
+ try {
+ const endpoint = userType === 'company' ? '/api/upload/contract/company' : '/api/upload/contract/personal'
+ 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('Contract signed successfully!')
+ setTimeout(() => {
+ onSuccess()
+ onClose()
+ }, 1500)
+ } else {
+ setError(data.message || 'Failed to upload contract')
+ }
+ } catch (error) {
+ setError('Network error occurred')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+
+
+
Sign Contract
+
+
+
+
+
+
+
+
+ Please review and upload your signed service agreement.
+
+
+
+ Signed Contract Document
+
+
setContractFile(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"
+ />
+
+
+
+ setAgreed(e.target.checked)}
+ className="mt-1 h-4 w-4 text-[#8D6B1D] focus:ring-[#8D6B1D] border-gray-300 rounded"
+ />
+
+ I have read, understood, and agree to the terms and conditions of this service agreement.
+
+
+
+
+ {loading ? 'Uploading...' : 'Upload Signed Contract'}
+
+
+ {message && (
+
{message}
+ )}
+
+ {error && (
+
{error}
+ )}
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx
index 757b894..654e5d4 100644
--- a/src/app/dashboard/page.tsx
+++ b/src/app/dashboard/page.tsx
@@ -18,16 +18,17 @@ import {
export default function DashboardPage() {
const router = useRouter()
const user = useAuthStore(state => state.user)
+ const isAuthReady = useAuthStore(state => state.isAuthReady)
- // Redirect if not logged in
+ // Redirect if not logged in (only after auth is ready)
useEffect(() => {
- if (!user) {
+ if (isAuthReady && !user) {
router.push('/login')
}
- }, [user, router])
+ }, [isAuthReady, user, router])
- // Don't render if no user
- if (!user) {
+ // Show loading until auth is ready or user is confirmed
+ if (!isAuthReady || !user) {
return (
@@ -104,6 +105,31 @@ export default function DashboardPage() {
+ {/* Account setup note */}
+
+
+
+
+
+
+
+ Complete your account setup
+
+
+
+ Complete your verification process to unlock all features.{' '}
+ router.push('/quickaction-dashboard')}
+ className="font-medium underline text-blue-800 hover:text-blue-900"
+ >
+ Start verification →
+
+
+
+
+
+
+
{/* Stats Grid - Tailwind UI Plus "With brand icon" */}
diff --git a/src/app/debug-auth/page.tsx b/src/app/debug-auth/page.tsx
new file mode 100644
index 0000000..458f60b
--- /dev/null
+++ b/src/app/debug-auth/page.tsx
@@ -0,0 +1,112 @@
+'use client'
+
+import { useEffect, useState } from 'react'
+import useAuthStore from '../store/authStore'
+import { useUserStatus } from '../hooks/useUserStatus'
+
+export default function DebugAuthPage() {
+ const [debugInfo, setDebugInfo] = useState
({})
+ const { accessToken, user, isAuthReady, refreshAuthToken, getAuthState } = useAuthStore()
+ const { userStatus, loading, error } = useUserStatus()
+
+ useEffect(() => {
+ const updateDebugInfo = () => {
+ const authState = getAuthState()
+ setDebugInfo({
+ ...authState,
+ timestamp: new Date().toISOString(),
+ sessionStorageUser: typeof window !== 'undefined' ? sessionStorage.getItem('user') : null,
+ sessionStorageToken: typeof window !== 'undefined' ? sessionStorage.getItem('accessToken') : null,
+ apiBaseUrl: process.env.NEXT_PUBLIC_API_BASE_URL
+ })
+ }
+
+ updateDebugInfo()
+ const interval = setInterval(updateDebugInfo, 1000)
+ return () => clearInterval(interval)
+ }, [getAuthState])
+
+ const handleRefreshToken = async () => {
+ console.log('Manual token refresh...')
+ const result = await refreshAuthToken()
+ console.log('Refresh result:', result)
+ }
+
+ const handleTestApiCall = async () => {
+ if (!accessToken) {
+ console.error('No access token available')
+ return
+ }
+
+ try {
+ const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/user/status`, {
+ method: 'GET',
+ headers: {
+ 'Authorization': `Bearer ${accessToken}`,
+ 'Content-Type': 'application/json'
+ }
+ })
+
+ console.log('API Response Status:', response.status)
+ const data = await response.json().catch(() => null)
+ console.log('API Response Data:', data)
+ } catch (error) {
+ console.error('API Test Error:', error)
+ }
+ }
+
+ return (
+
+
+
Auth Debug Page
+
+
+ {/* Auth Store State */}
+
+
Auth Store State
+
+ {JSON.stringify(debugInfo, null, 2)}
+
+
+
+ Refresh Token
+
+
+ Test API Call
+
+
+
+
+ {/* User Status */}
+
+
User Status Hook
+
+
Loading: {loading ? 'Yes' : 'No'}
+
Error: {error || 'None'}
+
Status:
+
+ {JSON.stringify(userStatus, null, 2)}
+
+
+
+
+ {/* Environment */}
+
+
Environment
+
+
API Base URL: {process.env.NEXT_PUBLIC_API_BASE_URL}
+
Node Env: {process.env.NODE_ENV}
+
Current URL: {typeof window !== 'undefined' ? window.location.href : 'SSR'}
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/app/hooks/useUserStatus.ts b/src/app/hooks/useUserStatus.ts
new file mode 100644
index 0000000..7c818d5
--- /dev/null
+++ b/src/app/hooks/useUserStatus.ts
@@ -0,0 +1,161 @@
+import { useState, useEffect, useCallback } from 'react'
+import useAuthStore from '../store/authStore'
+
+export interface UserStatus {
+ email_verified: boolean
+ documents_uploaded: boolean
+ profile_completed: boolean
+ contract_signed: boolean
+ is_admin_verified?: boolean
+ email_verified_at?: string
+ documents_uploaded_at?: string
+ profile_completed_at?: string
+ contract_signed_at?: string
+}
+
+export interface UserStatusResponse {
+ success: boolean
+ status: UserStatus
+ message?: string
+}
+
+export const useUserStatus = () => {
+ const [userStatus, setUserStatus] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
+ const [isClient, setIsClient] = useState(false)
+ const { accessToken, user } = useAuthStore()
+
+ // Debug logging
+ useEffect(() => {
+ console.log('🔍 useUserStatus debug:', {
+ isClient,
+ hasToken: !!accessToken,
+ tokenPrefix: accessToken ? accessToken.substring(0, 20) + '...' : null,
+ hasUser: !!user,
+ userEmail: user?.email || user?.companyName
+ })
+ }, [isClient, accessToken, user])
+
+ // Handle SSR hydration
+ useEffect(() => {
+ setIsClient(true)
+ }, [])
+
+ const fetchUserStatus = useCallback(async () => {
+ if (!isClient) {
+ setLoading(false)
+ return
+ }
+
+ // If no user, clear status and don't show error
+ if (!user) {
+ setUserStatus(null)
+ setLoading(false)
+ setError(null)
+ return
+ }
+
+ try {
+ setLoading(true)
+ setError(null)
+
+ // Get current token - it might be null initially
+ let currentToken = accessToken
+
+ // If no token, try to refresh first
+ if (!currentToken) {
+ console.log('No access token, attempting refresh...')
+ const { refreshAuthToken } = useAuthStore.getState()
+ const refreshed = await refreshAuthToken()
+ if (refreshed) {
+ currentToken = useAuthStore.getState().accessToken
+ }
+ }
+
+ // If still no token after refresh attempt, this is an auth error
+ if (!currentToken) {
+ throw new Error('Not authenticated - please log in again')
+ }
+
+ const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/user/status`, {
+ method: 'GET',
+ headers: {
+ 'Authorization': `Bearer ${currentToken}`,
+ 'Content-Type': 'application/json'
+ }
+ })
+
+ // If 401, try token refresh once
+ if (response.status === 401) {
+ console.log('Got 401, attempting token refresh...')
+ const { refreshAuthToken } = useAuthStore.getState()
+ const refreshed = await refreshAuthToken()
+
+ if (refreshed) {
+ const newToken = useAuthStore.getState().accessToken
+ if (newToken) {
+ // Retry with new token
+ const retryResponse = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/user/status`, {
+ method: 'GET',
+ headers: {
+ 'Authorization': `Bearer ${newToken}`,
+ 'Content-Type': 'application/json'
+ }
+ })
+
+ if (!retryResponse.ok) {
+ throw new Error(`Failed to fetch user status: ${retryResponse.status}`)
+ }
+
+ const retryData: UserStatusResponse = await retryResponse.json()
+ if (retryData.success) {
+ setUserStatus(retryData.status)
+ return
+ } else {
+ throw new Error(retryData.message || 'Failed to fetch user status')
+ }
+ }
+ }
+
+ throw new Error('Authentication failed - please log in again')
+ }
+
+ if (!response.ok) {
+ throw new Error(`Failed to fetch user status: ${response.status}`)
+ }
+
+ const data: UserStatusResponse = await response.json()
+
+ if (data.success) {
+ setUserStatus(data.status)
+ } else {
+ throw new Error(data.message || 'Failed to fetch user status')
+ }
+ } catch (err) {
+ console.error('Error fetching user status:', err)
+ setError(err instanceof Error ? err.message : 'Unknown error occurred')
+ } finally {
+ setLoading(false)
+ }
+ }, [isClient, accessToken, user])
+
+ // Fetch status on mount and when auth changes
+ useEffect(() => {
+ if (isClient) {
+ fetchUserStatus()
+ }
+ }, [isClient, fetchUserStatus])
+
+ // Refresh function for manual updates
+ const refreshStatus = useCallback(() => {
+ return fetchUserStatus()
+ }, [fetchUserStatus])
+
+ return {
+ userStatus,
+ loading,
+ error,
+ refreshStatus
+ }
+}
\ No newline at end of file
diff --git a/src/app/quickaction-dashboard/page.tsx b/src/app/quickaction-dashboard/page.tsx
index 75d1b4a..59e0f68 100644
--- a/src/app/quickaction-dashboard/page.tsx
+++ b/src/app/quickaction-dashboard/page.tsx
@@ -1,8 +1,10 @@
'use client'
-import { useState, useCallback } from 'react'
+import { useState, useCallback, useEffect } from 'react'
+import { useRouter } from 'next/navigation'
import PageLayout from '../components/PageLayout'
import useAuthStore from '../store/authStore'
+import { useUserStatus } from '../hooks/useUserStatus'
import {
CheckCircleIcon,
XCircleIcon,
@@ -24,13 +26,20 @@ interface StatusItem {
}
export default function QuickActionDashboardPage() {
+ const router = useRouter()
const user = useAuthStore(s => s.user)
+ const { userStatus, loading, error, refreshStatus } = useUserStatus()
+ const [isClient, setIsClient] = useState(false)
- // Mock status derivation (replace with real user flags)
- const [emailVerified, setEmailVerified] = useState(!!user?.emailVerified)
- const [idUploaded, setIdUploaded] = useState(false)
- const [additionalInfo, setAdditionalInfo] = useState(false)
- const [contractSigned, setContractSigned] = useState(false)
+ useEffect(() => {
+ setIsClient(true)
+ }, [])
+
+ // Derive status from real backend data
+ const emailVerified = userStatus?.email_verified || false
+ const idUploaded = userStatus?.documents_uploaded || false
+ const additionalInfo = userStatus?.profile_completed || false
+ const contractSigned = userStatus?.contract_signed || false
const statusItems: StatusItem[] = [
{
@@ -63,30 +72,25 @@ export default function QuickActionDashboardPage() {
}
]
- // Action handlers (placeholder async simulations)
- const handleVerifyEmail = useCallback(async () => {
- // TODO: trigger backend email verification flow
- await new Promise(r => setTimeout(r, 600))
- setEmailVerified(true)
- }, [])
+ // Action handlers - navigate to proper QuickAction pages
+ const handleVerifyEmail = useCallback(() => {
+ router.push('/quickaction-dashboard/register-email-verify')
+ }, [router])
- const handleUploadId = useCallback(async () => {
- // TODO: open upload modal / navigate
- await new Promise(r => setTimeout(r, 600))
- setIdUploaded(true)
- }, [])
+ const handleUploadId = useCallback(() => {
+ const userType = user?.userType || 'personal'
+ router.push(`/quickaction-dashboard/register-upload-id/${userType}`)
+ }, [router, user])
- const handleCompleteInfo = useCallback(async () => {
- // TODO: navigate to profile completion
- await new Promise(r => setTimeout(r, 600))
- setAdditionalInfo(true)
- }, [])
+ const handleCompleteInfo = useCallback(() => {
+ const userType = user?.userType || 'personal'
+ router.push(`/quickaction-dashboard/register-additional-information/${userType}`)
+ }, [router, user])
- const handleSignContract = useCallback(async () => {
- // TODO: open contract signing flow
- await new Promise(r => setTimeout(r, 600))
- setContractSigned(true)
- }, [])
+ const handleSignContract = useCallback(() => {
+ const userType = user?.userType || 'personal'
+ router.push(`/quickaction-dashboard/register-sign-contract/${userType}`)
+ }, [router, user])
const canUploadId = emailVerified
const canCompleteInfo = emailVerified && idUploaded
@@ -133,11 +137,41 @@ export default function QuickActionDashboardPage() {
{/* Welcome */}
- Welcome{user?.firstName ? `, ${user.firstName}` : ''}!
+ Welcome{isClient && user?.firstName ? `, ${user.firstName}` : ''}!
- Personal Account
+ {isClient && user?.userType === 'company' ? 'Company Account' : 'Personal Account'}
+ {loading && (
+
Loading status...
+ )}
+ {error && (
+
+
+
+
+
+ Error loading account status
+
+
+
+ refreshStatus()}
+ className="text-sm bg-red-100 text-red-800 px-3 py-1 rounded-md hover:bg-red-200 transition-colors"
+ >
+ Try again
+
+
+
+
+
+ )}
{/* Outer container card mimic (like screenshot) */}
diff --git a/src/app/quickaction-dashboard/register-additional-information/company/page.tsx b/src/app/quickaction-dashboard/register-additional-information/company/page.tsx
index e28b882..fc79b50 100644
--- a/src/app/quickaction-dashboard/register-additional-information/company/page.tsx
+++ b/src/app/quickaction-dashboard/register-additional-information/company/page.tsx
@@ -1,7 +1,10 @@
'use client'
import { useState } from 'react'
+import { useRouter } from 'next/navigation'
import PageLayout from '../../../components/PageLayout'
+import useAuthStore from '../../../store/authStore'
+import { useUserStatus } from '../../../hooks/useUserStatus'
interface CompanyProfileData {
companyName: string
@@ -34,6 +37,10 @@ const init: CompanyProfileData = {
}
export default function CompanyAdditionalInformationPage() {
+ const router = useRouter()
+ const { accessToken } = useAuthStore()
+ const { refreshStatus } = useUserStatus()
+
const [form, setForm] = useState(init)
const [loading, setLoading] = useState(false)
const [success, setSuccess] = useState(false)
@@ -67,13 +74,56 @@ export default function CompanyAdditionalInformationPage() {
e.preventDefault()
if (loading || success) return
if (!validate()) return
+
+ if (!accessToken) {
+ setError('Not authenticated. Please log in again.')
+ return
+ }
+
setLoading(true)
+
try {
- // TODO: POST to backend
- await new Promise(r => setTimeout(r, 1300))
+ // Prepare data for backend with correct field names
+ const profileData = {
+ address: form.street, // Backend expects 'address', not nested object
+ zip_code: form.postalCode, // Backend expects 'zip_code'
+ city: form.city,
+ country: form.country,
+ registrationNumber: form.vatNumber, // Map VAT number to registration number
+ businessType: 'company', // Default business type
+ branch: null, // Not collected in form, set to null
+ numberOfEmployees: null, // Not collected in form, set to null
+ accountHolderName: form.accountHolder, // Backend expects 'accountHolderName'
+ iban: form.iban.replace(/\s+/g, '') // Remove spaces from IBAN
+ }
+
+ const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/profile/company/complete`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${accessToken}`,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(profileData)
+ })
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({ message: 'Save failed' }))
+ throw new Error(errorData.message || 'Save failed')
+ }
+
setSuccess(true)
- } catch {
- setError('Speichern fehlgeschlagen.')
+
+ // Refresh user status to update profile completion state
+ await refreshStatus()
+
+ // Redirect to next step after short delay
+ setTimeout(() => {
+ router.push('/quickaction-dashboard/register-sign-contract/company')
+ }, 1500)
+
+ } catch (error: any) {
+ console.error('Company profile save error:', error)
+ setError(error.message || 'Speichern fehlgeschlagen.')
} finally {
setLoading(false)
}
diff --git a/src/app/quickaction-dashboard/register-additional-information/personal/page.tsx b/src/app/quickaction-dashboard/register-additional-information/personal/page.tsx
index c969101..7727bda 100644
--- a/src/app/quickaction-dashboard/register-additional-information/personal/page.tsx
+++ b/src/app/quickaction-dashboard/register-additional-information/personal/page.tsx
@@ -1,7 +1,10 @@
'use client'
import { useState } from 'react'
+import { useRouter } from 'next/navigation'
import PageLayout from '../../../components/PageLayout'
+import useAuthStore from '../../../store/authStore'
+import { useUserStatus } from '../../../hooks/useUserStatus'
interface PersonalProfileData {
dob: string
@@ -32,6 +35,10 @@ const initialData: PersonalProfileData = {
}
export default function PersonalAdditionalInformationPage() {
+ const router = useRouter()
+ const { accessToken } = useAuthStore()
+ const { refreshStatus } = useUserStatus()
+
const [form, setForm] = useState(initialData)
const [loading, setLoading] = useState(false)
const [success, setSuccess] = useState(false)
@@ -66,13 +73,57 @@ export default function PersonalAdditionalInformationPage() {
e.preventDefault()
if (loading || success) return
if (!validate()) return
+
+ if (!accessToken) {
+ setError('Not authenticated. Please log in again.')
+ return
+ }
+
setLoading(true)
+
try {
- // TODO: POST to backend
- await new Promise(r => setTimeout(r, 1200))
+ // Prepare data for backend with correct field names
+ const profileData = {
+ dateOfBirth: form.dob,
+ nationality: form.nationality,
+ address: form.street, // Backend expects 'address', not nested object
+ zip_code: form.postalCode, // Backend expects 'zip_code'
+ city: form.city,
+ country: form.country,
+ phoneSecondary: form.secondPhone || null, // Backend expects 'phoneSecondary'
+ emergencyContactName: form.emergencyName || null,
+ emergencyContactPhone: form.emergencyPhone || null,
+ accountHolderName: form.accountHolder, // Backend expects 'accountHolderName'
+ iban: form.iban.replace(/\s+/g, '') // Remove spaces from IBAN
+ }
+
+ const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/profile/personal/complete`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${accessToken}`,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(profileData)
+ })
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({ message: 'Save failed' }))
+ throw new Error(errorData.message || 'Save failed')
+ }
+
setSuccess(true)
- } catch {
- setError('Speichern fehlgeschlagen. Bitte erneut versuchen.')
+
+ // Refresh user status to update profile completion state
+ await refreshStatus()
+
+ // Redirect to next step after short delay
+ setTimeout(() => {
+ router.push('/quickaction-dashboard/register-sign-contract/personal')
+ }, 1500)
+
+ } catch (error: any) {
+ console.error('Personal profile save error:', error)
+ setError(error.message || 'Speichern fehlgeschlagen. Bitte erneut versuchen.')
} finally {
setLoading(false)
}
diff --git a/src/app/quickaction-dashboard/register-email-verify/page.tsx b/src/app/quickaction-dashboard/register-email-verify/page.tsx
index 574303d..09f76f8 100644
--- a/src/app/quickaction-dashboard/register-email-verify/page.tsx
+++ b/src/app/quickaction-dashboard/register-email-verify/page.tsx
@@ -3,9 +3,12 @@
import { useState, useEffect, useCallback, useRef } from 'react'
import PageLayout from '../../components/PageLayout'
import useAuthStore from '../../store/authStore'
+import { useUserStatus } from '../../hooks/useUserStatus'
export default function EmailVerifyPage() {
const user = useAuthStore(s => s.user)
+ const token = useAuthStore(s => s.accessToken)
+ const { refreshStatus } = useUserStatus()
const [code, setCode] = useState(['', '', '', '', '', ''])
const [submitting, setSubmitting] = useState(false)
const [error, setError] = useState('')
@@ -54,25 +57,68 @@ export default function EmailVerifyPage() {
setError('Bitte 6-stelligen Code eingeben.')
return
}
+ if (!token) {
+ setError('Nicht authentifiziert. Bitte erneut einloggen.')
+ return
+ }
+
setSubmitting(true)
setError('')
try {
- // TODO: call backend verify endpoint
- await new Promise(r => setTimeout(r, 1000))
- setSuccess(true)
- } catch {
- setError('Verifizierung fehlgeschlagen. Bitte erneut versuchen.')
+ 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: fullCode })
+ })
+
+ const data = await response.json()
+
+ if (response.ok && data.success) {
+ setSuccess(true)
+ await refreshStatus() // Refresh user status
+ // Redirect after 2 seconds
+ setTimeout(() => {
+ window.location.href = '/quickaction-dashboard'
+ }, 2000)
+ } else {
+ setError(data.error || 'Verifizierung fehlgeschlagen. Bitte erneut versuchen.')
+ }
+ } catch (error) {
+ console.error('Email verification error:', error)
+ setError('Netzwerkfehler. Bitte erneut versuchen.')
} finally {
setSubmitting(false)
}
}
const handleResend = useCallback(async () => {
- if (resendCooldown) return
+ if (resendCooldown || !token) return
setError('')
- // TODO: call resend endpoint
- setResendCooldown(30)
- }, [resendCooldown])
+
+ 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 && data.success) {
+ setResendCooldown(30)
+ } else {
+ setError(data.message || 'Fehler beim Senden der E-Mail.')
+ }
+ } catch (error) {
+ console.error('Resend email error:', error)
+ setError('Netzwerkfehler beim Senden der E-Mail.')
+ }
+ }, [resendCooldown, token])
return (
diff --git a/src/app/quickaction-dashboard/register-sign-contract/company/page.tsx b/src/app/quickaction-dashboard/register-sign-contract/company/page.tsx
index 169b319..cb0ef21 100644
--- a/src/app/quickaction-dashboard/register-sign-contract/company/page.tsx
+++ b/src/app/quickaction-dashboard/register-sign-contract/company/page.tsx
@@ -1,9 +1,16 @@
'use client'
import { useState, useEffect } from 'react'
+import { useRouter } from 'next/navigation'
import PageLayout from '../../../components/PageLayout'
+import useAuthStore from '../../../store/authStore'
+import { useUserStatus } from '../../../hooks/useUserStatus'
export default function CompanySignContractPage() {
+ const router = useRouter()
+ const { accessToken } = useAuthStore()
+ const { refreshStatus } = useUserStatus()
+
const [companyName, setCompanyName] = useState('')
const [repName, setRepName] = useState('')
const [repTitle, setRepTitle] = useState('')
@@ -36,14 +43,65 @@ export default function CompanySignContractPage() {
setError('Bitte alle Pflichtfelder & Bestätigungen ausfüllen.')
return
}
+
+ if (!accessToken) {
+ setError('Not authenticated. Please log in again.')
+ return
+ }
+
setError('')
setSubmitting(true)
+
try {
- // TODO: POST /contracts/company/sign { companyName, repName, repTitle, location, date, note }
- await new Promise(r => setTimeout(r, 1400))
+ const contractData = {
+ companyName: companyName.trim(),
+ representativeName: repName.trim(),
+ representativeTitle: repTitle.trim(),
+ location: location.trim(),
+ date,
+ note: note.trim() || null,
+ contractType: 'company',
+ confirmations: {
+ agreeContract,
+ agreeData,
+ confirmSignature
+ }
+ }
+
+ // Create FormData for the existing backend endpoint
+ const formData = new FormData()
+ formData.append('contractData', JSON.stringify(contractData))
+ // Create a dummy file since the backend expects one (we'll update backend later)
+ const dummyFile = new Blob(['Electronic signature data'], { type: 'text/plain' })
+ formData.append('contract', dummyFile, 'electronic_signature.txt')
+
+ const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/upload/contract/company`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${accessToken}`
+ // Don't set Content-Type, let browser set it for FormData
+ },
+ body: formData
+ })
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({ message: 'Contract signing failed' }))
+ throw new Error(errorData.message || 'Contract signing failed')
+ }
+
setSuccess(true)
- } catch {
- setError('Signatur fehlgeschlagen. Bitte erneut versuchen.')
+
+ // Refresh user status to update contract signed state
+ await refreshStatus()
+
+ // Redirect to main dashboard after short delay
+ setTimeout(() => {
+ router.push('/quickaction-dashboard')
+ }, 2000)
+
+ } catch (error: any) {
+ console.error('Contract signing error:', error)
+ setError(error.message || 'Signatur fehlgeschlagen. Bitte erneut versuchen.')
} finally {
setSubmitting(false)
}
diff --git a/src/app/quickaction-dashboard/register-sign-contract/personal/page.tsx b/src/app/quickaction-dashboard/register-sign-contract/personal/page.tsx
index bdd6e58..2288167 100644
--- a/src/app/quickaction-dashboard/register-sign-contract/personal/page.tsx
+++ b/src/app/quickaction-dashboard/register-sign-contract/personal/page.tsx
@@ -1,9 +1,16 @@
'use client'
import { useState, useEffect } from 'react'
+import { useRouter } from 'next/navigation'
import PageLayout from '../../../components/PageLayout'
+import useAuthStore from '../../../store/authStore'
+import { useUserStatus } from '../../../hooks/useUserStatus'
export default function PersonalSignContractPage() {
+ const router = useRouter()
+ const { accessToken } = useAuthStore()
+ const { refreshStatus } = useUserStatus()
+
const [fullName, setFullName] = useState('')
const [location, setLocation] = useState('')
const [date, setDate] = useState('')
@@ -32,14 +39,63 @@ export default function PersonalSignContractPage() {
setError('Bitte alle Pflichtfelder & Bestätigungen ausfüllen.')
return
}
+
+ if (!accessToken) {
+ setError('Not authenticated. Please log in again.')
+ return
+ }
+
setError('')
setSubmitting(true)
+
try {
- // TODO: POST /contracts/personal/sign { fullName, location, date, note }
- await new Promise(r => setTimeout(r, 1200))
+ const contractData = {
+ fullName: fullName.trim(),
+ location: location.trim(),
+ date,
+ note: note.trim() || null,
+ contractType: 'personal',
+ confirmations: {
+ agreeContract,
+ agreeData,
+ confirmSignature
+ }
+ }
+
+ // Create FormData for the existing backend endpoint
+ const formData = new FormData()
+ formData.append('contractData', JSON.stringify(contractData))
+ // Create a dummy file since the backend expects one (we'll update backend later)
+ const dummyFile = new Blob(['Electronic signature data'], { type: 'text/plain' })
+ formData.append('contract', dummyFile, 'electronic_signature.txt')
+
+ const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/upload/contract/personal`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${accessToken}`
+ // Don't set Content-Type, let browser set it for FormData
+ },
+ body: formData
+ })
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({ message: 'Contract signing failed' }))
+ throw new Error(errorData.message || 'Contract signing failed')
+ }
+
setSuccess(true)
- } catch {
- setError('Signatur fehlgeschlagen. Bitte erneut versuchen.')
+
+ // Refresh user status to update contract signed state
+ await refreshStatus()
+
+ // Redirect to main dashboard after short delay
+ setTimeout(() => {
+ router.push('/quickaction-dashboard')
+ }, 2000)
+
+ } catch (error: any) {
+ console.error('Contract signing error:', error)
+ setError(error.message || 'Signatur fehlgeschlagen. Bitte erneut versuchen.')
} finally {
setSubmitting(false)
}
diff --git a/src/app/quickaction-dashboard/register-upload-id/company/page.tsx b/src/app/quickaction-dashboard/register-upload-id/company/page.tsx
index 6984bb7..270e7da 100644
--- a/src/app/quickaction-dashboard/register-upload-id/company/page.tsx
+++ b/src/app/quickaction-dashboard/register-upload-id/company/page.tsx
@@ -1,12 +1,19 @@
'use client'
import { useState, useRef } from 'react'
+import { useRouter } from 'next/navigation'
import PageLayout from '../../../components/PageLayout'
import { DocumentArrowUpIcon, XMarkIcon } from '@heroicons/react/24/outline'
+import useAuthStore from '../../../store/authStore'
+import { useUserStatus } from '../../../hooks/useUserStatus'
const DOC_TYPES = ['Handelsregisterauszug', 'Gewerbeanmeldung', 'Steuerbescheid', 'Sonstiges']
export default function CompanyIdUploadPage() {
+ const router = useRouter()
+ const { accessToken } = useAuthStore()
+ const { refreshStatus } = useUserStatus()
+
const [docNumber, setDocNumber] = useState('')
const [docType, setDocType] = useState('')
const [issueDate, setIssueDate] = useState('')
@@ -39,14 +46,51 @@ export default function CompanyIdUploadPage() {
setError('Bitte alle Pflichtfelder (mit *) ausfüllen.')
return
}
+
+ if (!accessToken) {
+ setError('Not authenticated. Please log in again.')
+ return
+ }
+
setError('')
setSubmitting(true)
+
try {
- // TODO: API upload
- await new Promise(r => setTimeout(r, 1200))
+ const formData = new FormData()
+ formData.append('frontFile', frontFile)
+ if (extraFile) {
+ formData.append('backFile', extraFile)
+ }
+ formData.append('docType', docType)
+ formData.append('docNumber', docNumber.trim())
+ formData.append('issueDate', issueDate)
+
+ const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/upload/company-id`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${accessToken}`
+ },
+ body: formData
+ })
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({ message: 'Upload failed' }))
+ throw new Error(errorData.message || 'Upload failed')
+ }
+
setSuccess(true)
- } catch {
- setError('Upload fehlgeschlagen.')
+
+ // Refresh user status to update verification state
+ await refreshStatus()
+
+ // Redirect to next step after short delay
+ setTimeout(() => {
+ router.push('/quickaction-dashboard/register-additional-information')
+ }, 1500)
+
+ } catch (error: any) {
+ console.error('Company ID upload error:', error)
+ setError(error.message || 'Upload fehlgeschlagen.')
} finally {
setSubmitting(false)
}
diff --git a/src/app/quickaction-dashboard/register-upload-id/personal/page.tsx b/src/app/quickaction-dashboard/register-upload-id/personal/page.tsx
index 59a8439..907c091 100644
--- a/src/app/quickaction-dashboard/register-upload-id/personal/page.tsx
+++ b/src/app/quickaction-dashboard/register-upload-id/personal/page.tsx
@@ -2,14 +2,23 @@
import { useState, useRef, useCallback } from 'react'
import PageLayout from '../../../components/PageLayout'
+import useAuthStore from '../../../store/authStore'
+import { useUserStatus } from '../../../hooks/useUserStatus'
import {
DocumentArrowUpIcon,
XMarkIcon
} from '@heroicons/react/24/outline'
-const ID_TYPES = ['Personalausweis', 'Reisepass', 'Führerschein', 'Aufenthaltstitel']
+const ID_TYPES = [
+ { value: 'national_id', label: 'Personalausweis' },
+ { value: 'passport', label: 'Reisepass' },
+ { value: 'driver_license', label: 'Führerschein' },
+ { value: 'other', label: 'Aufenthaltstitel' }
+]
export default function PersonalIdUploadPage() {
+ const token = useAuthStore(s => s.accessToken)
+ const { refreshStatus } = useUserStatus()
const [idNumber, setIdNumber] = useState('')
const [idType, setIdType] = useState('')
const [expiry, setExpiry] = useState('')
@@ -58,13 +67,47 @@ export default function PersonalIdUploadPage() {
const submit = async (e: React.FormEvent) => {
e.preventDefault()
if (!validate()) return
+ if (!token) {
+ setError('Nicht authentifiziert. Bitte erneut einloggen.')
+ return
+ }
+
setSubmitting(true)
+ setError('')
+
try {
- // TODO: Upload logic (multipart/form-data)
- await new Promise(r => setTimeout(r, 1200))
- setSuccess(true)
- } catch {
- setError('Upload fehlgeschlagen. Bitte erneut versuchen.')
+ const formData = new FormData()
+ formData.append('front', frontFile!)
+ if (hasBack && backFile) {
+ formData.append('back', backFile)
+ }
+ formData.append('idType', idType)
+ formData.append('idNumber', idNumber)
+ formData.append('expiryDate', expiry)
+
+ const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/upload/personal-id`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${token}`
+ },
+ body: formData
+ })
+
+ const data = await response.json()
+
+ if (response.ok && data.success) {
+ setSuccess(true)
+ await refreshStatus() // Refresh user status
+ // Redirect after 2 seconds
+ setTimeout(() => {
+ window.location.href = '/quickaction-dashboard'
+ }, 2000)
+ } else {
+ setError(data.message || 'Upload fehlgeschlagen. Bitte erneut versuchen.')
+ }
+ } catch (error) {
+ console.error('Upload error:', error)
+ setError('Netzwerkfehler. Bitte erneut versuchen.')
} finally {
setSubmitting(false)
}
@@ -146,7 +189,7 @@ export default function PersonalIdUploadPage() {
required
>
Select ID type
- {ID_TYPES.map(t => {t} )}
+ {ID_TYPES.map(t => {t.label} )}
diff --git a/src/app/store/authStore.ts b/src/app/store/authStore.ts
index 2d43c9f..67f5ab2 100644
--- a/src/app/store/authStore.ts
+++ b/src/app/store/authStore.ts
@@ -27,6 +27,28 @@ const getStoredUser = () => {
}
};
+const getStoredToken = () => {
+ if (typeof window === 'undefined') return null; // SSR check
+ try {
+ const token = sessionStorage.getItem('accessToken');
+ if (token) {
+ const expiry = getTokenExpiry(token);
+ if (expiry && expiry.getTime() > Date.now()) {
+ log("🔑 Retrieved valid token from sessionStorage");
+ return token;
+ } else {
+ log("⏰ Stored token expired, removing");
+ sessionStorage.removeItem('accessToken');
+ return null;
+ }
+ }
+ return null;
+ } catch (error) {
+ log("❌ Error retrieving token from sessionStorage:", error);
+ return null;
+ }
+};
+
interface User {
email?: string;
companyName?: string;
@@ -50,7 +72,7 @@ interface AuthStore {
const useAuthStore = create
((set, get) => ({
// Initialize with SSR-safe defaults
- accessToken: null,
+ accessToken: typeof window !== 'undefined' ? getStoredToken() : null,
user: typeof window !== 'undefined' ? getStoredUser() : null,
isAuthReady: false,
isRefreshing: false,
@@ -63,11 +85,20 @@ const useAuthStore = create((set, get) => ({
setAccessToken: (token) => {
log("🔑 Zustand: Setting access token in memory:", token ? `${token.substring(0, 20)}...` : null);
- if (token) {
- const expiry = getTokenExpiry(token);
- log("⏳ Zustand: Token expiry:", expiry ? expiry.toLocaleString() : "Unknown");
- } else {
- log("🗑️ Zustand: Clearing in-memory access token");
+ if (typeof window !== 'undefined') {
+ try {
+ if (token) {
+ const expiry = getTokenExpiry(token);
+ log("⏳ Zustand: Token expiry:", expiry ? expiry.toLocaleString() : "Unknown");
+ sessionStorage.setItem('accessToken', token);
+ log("✅ Token stored in sessionStorage");
+ } else {
+ sessionStorage.removeItem('accessToken');
+ log("🗑️ Token removed from sessionStorage");
+ }
+ } catch (error) {
+ log("❌ Error storing token in sessionStorage:", error);
+ }
}
set({ accessToken: token });
},
diff --git a/src/app/utils/api.ts b/src/app/utils/api.ts
new file mode 100644
index 0000000..c31d2c1
--- /dev/null
+++ b/src/app/utils/api.ts
@@ -0,0 +1,241 @@
+// API Configuration
+export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001'
+
+// API Endpoints
+export const API_ENDPOINTS = {
+ // Auth
+ LOGIN: '/api/login',
+ REFRESH: '/api/refresh',
+ LOGOUT: '/api/logout',
+ ME: '/api/me',
+
+ // User Status & Settings
+ USER_STATUS: '/api/user/status',
+ USER_STATUS_PROGRESS: '/api/user/status-progress',
+ USER_SETTINGS: '/api/user/settings',
+
+ // Email Verification
+ SEND_VERIFICATION_EMAIL: '/api/send-verification-email',
+ VERIFY_EMAIL_CODE: '/api/verify-email-code',
+
+ // Document Upload
+ UPLOAD_PERSONAL_ID: '/api/upload/personal-id',
+ UPLOAD_COMPANY_ID: '/api/upload/company-id',
+
+ // Profile Completion
+ COMPLETE_PERSONAL_PROFILE: '/api/profile/personal/complete',
+ COMPLETE_COMPANY_PROFILE: '/api/profile/company/complete',
+
+ // Contract Signing
+ UPLOAD_PERSONAL_CONTRACT: '/api/upload/contract/personal',
+ UPLOAD_COMPANY_CONTRACT: '/api/upload/contract/company',
+
+ // Documents
+ USER_DOCUMENTS: '/api/users/:id/documents',
+
+ // Admin
+ ADMIN_USERS: '/api/admin/users/:id/full',
+}
+
+// API Helper Functions
+export class ApiClient {
+ static async makeRequest(
+ endpoint: string,
+ options: RequestInit = {},
+ token?: string
+ ): Promise {
+ const url = `${API_BASE_URL}${endpoint}`
+
+ const defaultHeaders: HeadersInit = {
+ 'Content-Type': 'application/json',
+ }
+
+ if (token) {
+ defaultHeaders['Authorization'] = `Bearer ${token}`
+ }
+
+ const config: RequestInit = {
+ ...options,
+ headers: {
+ ...defaultHeaders,
+ ...options.headers,
+ },
+ }
+
+ return fetch(url, config)
+ }
+
+ static async get(endpoint: string, token?: string): Promise {
+ return this.makeRequest(endpoint, { method: 'GET' }, token)
+ }
+
+ static async post(
+ endpoint: string,
+ data?: any,
+ token?: string
+ ): Promise {
+ return this.makeRequest(
+ endpoint,
+ {
+ method: 'POST',
+ body: data ? JSON.stringify(data) : undefined,
+ },
+ token
+ )
+ }
+
+ static async postFormData(
+ endpoint: string,
+ formData: FormData,
+ token?: string
+ ): Promise {
+ const url = `${API_BASE_URL}${endpoint}`
+
+ const headers: HeadersInit = {}
+ if (token) {
+ headers['Authorization'] = `Bearer ${token}`
+ }
+
+ return fetch(url, {
+ method: 'POST',
+ headers,
+ body: formData,
+ })
+ }
+
+ static async put(
+ endpoint: string,
+ data?: any,
+ token?: string
+ ): Promise {
+ return this.makeRequest(
+ endpoint,
+ {
+ method: 'PUT',
+ body: data ? JSON.stringify(data) : undefined,
+ },
+ token
+ )
+ }
+
+ static async patch(
+ endpoint: string,
+ data?: any,
+ token?: string
+ ): Promise {
+ return this.makeRequest(
+ endpoint,
+ {
+ method: 'PATCH',
+ body: data ? JSON.stringify(data) : undefined,
+ },
+ token
+ )
+ }
+
+ static async delete(endpoint: string, token?: string): Promise {
+ return this.makeRequest(endpoint, { method: 'DELETE' }, token)
+ }
+}
+
+// Specific API Functions for QuickActions
+export class QuickActionsAPI {
+ static async getUserStatus(token: string) {
+ const response = await ApiClient.get(API_ENDPOINTS.USER_STATUS, token)
+ if (!response.ok) {
+ throw new Error('Failed to fetch user status')
+ }
+ return response.json()
+ }
+
+ static async sendVerificationEmail(token: string) {
+ const response = await ApiClient.post(API_ENDPOINTS.SEND_VERIFICATION_EMAIL, {}, token)
+ if (!response.ok) {
+ const error = await response.json()
+ throw new Error(error.message || 'Failed to send verification email')
+ }
+ return response.json()
+ }
+
+ static async verifyEmailCode(token: string, code: string) {
+ const response = await ApiClient.post(
+ API_ENDPOINTS.VERIFY_EMAIL_CODE,
+ { code },
+ token
+ )
+ if (!response.ok) {
+ const error = await response.json()
+ throw new Error(error.message || 'Invalid verification code')
+ }
+ return response.json()
+ }
+
+ static async uploadDocuments(token: string, userType: string, files: { front: File, back?: File }) {
+ const formData = new FormData()
+ formData.append('front', files.front)
+ if (files.back) {
+ formData.append('back', files.back)
+ }
+
+ const endpoint = userType === 'company'
+ ? API_ENDPOINTS.UPLOAD_COMPANY_ID
+ : API_ENDPOINTS.UPLOAD_PERSONAL_ID
+
+ const response = await ApiClient.postFormData(endpoint, formData, token)
+ if (!response.ok) {
+ const error = await response.json()
+ throw new Error(error.message || 'Failed to upload documents')
+ }
+ return response.json()
+ }
+
+ static async completeProfile(token: string, userType: string, profileData: any) {
+ const endpoint = userType === 'company'
+ ? API_ENDPOINTS.COMPLETE_COMPANY_PROFILE
+ : API_ENDPOINTS.COMPLETE_PERSONAL_PROFILE
+
+ const response = await ApiClient.post(endpoint, profileData, token)
+ if (!response.ok) {
+ const error = await response.json()
+ throw new Error(error.message || 'Failed to complete profile')
+ }
+ return response.json()
+ }
+
+ static async uploadContract(token: string, userType: string, contractFile: File) {
+ const formData = new FormData()
+ formData.append('contract', contractFile)
+
+ const endpoint = userType === 'company'
+ ? API_ENDPOINTS.UPLOAD_COMPANY_CONTRACT
+ : API_ENDPOINTS.UPLOAD_PERSONAL_CONTRACT
+
+ const response = await ApiClient.postFormData(endpoint, formData, token)
+ if (!response.ok) {
+ const error = await response.json()
+ throw new Error(error.message || 'Failed to upload contract')
+ }
+ return response.json()
+ }
+}
+
+// Error Types
+export interface ApiError {
+ message: string
+ status?: number
+ code?: string
+}
+
+// Response Types
+export interface UserStatus {
+ emailVerified: boolean
+ documentsUploaded: boolean
+ profileCompleted: boolean
+ contractSigned: boolean
+}
+
+export interface ApiResponse {
+ success: boolean
+ message?: string
+ data?: T
+}
\ No newline at end of file