Compare commits
No commits in common. "1bdfd38ef577f1c9ea8b4222fac104e565e58370" and "bc89babc139e3f135042e1cd454293ffef4a5db5" have entirely different histories.
1bdfd38ef5
...
bc89babc13
@ -1,14 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { I18nProvider } from './i18n/useTranslation';
|
import { I18nProvider } from './i18n/useTranslation';
|
||||||
import AuthInitializer from './components/AuthInitializer';
|
|
||||||
|
|
||||||
export default function ClientWrapper({ children }: { children: React.ReactNode }) {
|
export default function ClientWrapper({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return <I18nProvider>{children}</I18nProvider>;
|
||||||
<I18nProvider>
|
|
||||||
<AuthInitializer>
|
|
||||||
{children}
|
|
||||||
</AuthInitializer>
|
|
||||||
</I18nProvider>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
@ -1,26 +0,0 @@
|
|||||||
'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}</>
|
|
||||||
}
|
|
||||||
@ -1,870 +0,0 @@
|
|||||||
'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 (
|
|
||||||
<div className="mb-8">
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900">Account Setup</h2>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
||||||
{[1, 2, 3, 4].map((i) => (
|
|
||||||
<div key={i} className="bg-white rounded-lg p-6 shadow-sm border border-gray-200">
|
|
||||||
<div className="flex items-start">
|
|
||||||
<div className="bg-gray-200 rounded-lg p-3 w-12 h-12"></div>
|
|
||||||
<div className="ml-4 flex-1">
|
|
||||||
<div className="h-4 bg-gray-200 rounded mb-2"></div>
|
|
||||||
<div className="h-3 bg-gray-200 rounded"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 <CheckCircleIcon className="h-5 w-5 text-green-500" />
|
|
||||||
case 'unavailable':
|
|
||||||
return <XCircleIcon className="h-5 w-5 text-gray-400" />
|
|
||||||
default:
|
|
||||||
return <ExclamationTriangleIcon className="h-5 w-5 text-yellow-500" />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<div className="mb-8">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Account Setup</h2>
|
|
||||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
|
||||||
<div className="flex">
|
|
||||||
<ExclamationTriangleIcon className="h-5 w-5 text-red-400" />
|
|
||||||
<div className="ml-3">
|
|
||||||
<h3 className="text-sm font-medium text-red-800">Error loading account status</h3>
|
|
||||||
<div className="mt-2 text-sm text-red-700">
|
|
||||||
<p>{error}</p>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={refreshStatus}
|
|
||||||
className="bg-red-100 px-2 py-1 text-xs font-semibold text-red-800 hover:bg-red-200 rounded"
|
|
||||||
>
|
|
||||||
Try again
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="mb-8">
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900">Account Setup</h2>
|
|
||||||
{loading && (
|
|
||||||
<div className="flex items-center text-sm text-gray-500">
|
|
||||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-[#8D6B1D] mr-2"></div>
|
|
||||||
Updating status...
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
||||||
{quickActions.map((action) => (
|
|
||||||
<button
|
|
||||||
key={action.id}
|
|
||||||
onClick={action.onClick}
|
|
||||||
disabled={action.status === 'unavailable' || loading}
|
|
||||||
className={`
|
|
||||||
bg-white rounded-lg p-6 shadow-sm border text-left group transition-all duration-200
|
|
||||||
${loading ? 'opacity-75' : ''}
|
|
||||||
${action.status === 'unavailable'
|
|
||||||
? 'border-gray-200 cursor-not-allowed opacity-60'
|
|
||||||
: action.status === 'completed'
|
|
||||||
? 'border-green-200 hover:border-green-300'
|
|
||||||
: 'border-gray-200 hover:shadow-md hover:border-gray-300'
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<div className="flex items-start">
|
|
||||||
<div className={`
|
|
||||||
${loading ? 'animate-pulse bg-gray-300' : action.color} rounded-lg p-3 transition-transform
|
|
||||||
${action.status === 'unavailable' ? 'opacity-60' : 'group-hover:scale-105'}
|
|
||||||
${action.status === 'completed' && !loading ? 'bg-green-500' : ''}
|
|
||||||
`}>
|
|
||||||
<action.icon className="h-6 w-6 text-white" />
|
|
||||||
</div>
|
|
||||||
<div className="ml-4 flex-1">
|
|
||||||
<div className="flex items-center justify-between mb-1">
|
|
||||||
<h3 className={`
|
|
||||||
text-lg font-medium transition-colors
|
|
||||||
${loading ? 'text-gray-400' : ''}
|
|
||||||
${action.status === 'unavailable'
|
|
||||||
? 'text-gray-400'
|
|
||||||
: action.status === 'completed'
|
|
||||||
? 'text-green-700'
|
|
||||||
: 'text-gray-900 group-hover:text-[#8D6B1D]'
|
|
||||||
}
|
|
||||||
`}>
|
|
||||||
{action.title}
|
|
||||||
</h3>
|
|
||||||
{loading ? (
|
|
||||||
<div className="animate-pulse bg-gray-300 rounded-full h-5 w-5"></div>
|
|
||||||
) : (
|
|
||||||
getStatusIcon(action.status)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className={`
|
|
||||||
text-sm mt-1
|
|
||||||
${loading ? 'text-gray-400' : ''}
|
|
||||||
${action.status === 'unavailable' ? 'text-gray-400' : 'text-gray-600'}
|
|
||||||
`}>
|
|
||||||
{action.description}
|
|
||||||
</p>
|
|
||||||
<div className="mt-2">
|
|
||||||
{loading ? (
|
|
||||||
<div className="animate-pulse bg-gray-200 rounded-full h-5 w-16"></div>
|
|
||||||
) : (
|
|
||||||
<span className={`
|
|
||||||
inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
|
||||||
${action.status === 'completed'
|
|
||||||
? 'bg-green-100 text-green-800'
|
|
||||||
: action.status === 'unavailable'
|
|
||||||
? 'bg-gray-100 text-gray-800'
|
|
||||||
: 'bg-yellow-100 text-yellow-800'
|
|
||||||
}
|
|
||||||
`}>
|
|
||||||
{getStatusText(action.status)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Modals */}
|
|
||||||
{showEmailVerification && (
|
|
||||||
<EmailVerificationModal
|
|
||||||
onClose={() => setShowEmailVerification(false)}
|
|
||||||
onSuccess={refreshStatus}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showDocumentUpload && (
|
|
||||||
<DocumentUploadModal
|
|
||||||
userType={user?.userType || 'personal'}
|
|
||||||
onClose={() => setShowDocumentUpload(false)}
|
|
||||||
onSuccess={refreshStatus}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showProfileCompletion && (
|
|
||||||
<ProfileCompletionModal
|
|
||||||
userType={user?.userType || 'personal'}
|
|
||||||
onClose={() => setShowProfileCompletion(false)}
|
|
||||||
onSuccess={refreshStatus}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showContractSigning && (
|
|
||||||
<ContractSigningModal
|
|
||||||
userType={user?.userType || 'personal'}
|
|
||||||
onClose={() => 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 (
|
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
||||||
<div className="bg-white rounded-lg p-6 w-full max-w-md mx-4">
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900">Email Verification</h3>
|
|
||||||
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
|
|
||||||
<XCircleIcon className="h-6 w-6" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-600 mb-4">
|
|
||||||
We'll send a verification code to your email address.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={sendVerificationEmail}
|
|
||||||
disabled={loading}
|
|
||||||
className="w-full bg-[#8D6B1D] text-white py-2 px-4 rounded-md hover:bg-[#7A5E1A] disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200"
|
|
||||||
>
|
|
||||||
{loading ? 'Sending...' : 'Send Verification Email'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="code" className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
Verification Code
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="code"
|
|
||||||
value={verificationCode}
|
|
||||||
onChange={(e) => 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}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={verifyCode}
|
|
||||||
disabled={loading || !verificationCode.trim()}
|
|
||||||
className="w-full bg-green-600 text-white py-2 px-4 rounded-md hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200"
|
|
||||||
>
|
|
||||||
{loading ? 'Verifying...' : 'Verify Email'}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{message && (
|
|
||||||
<div className="text-green-600 text-sm text-center">{message}</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="text-red-600 text-sm text-center">{error}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Document Upload Modal Component
|
|
||||||
function DocumentUploadModal({ userType, onClose, onSuccess }: {
|
|
||||||
userType: string,
|
|
||||||
onClose: () => void,
|
|
||||||
onSuccess: () => void
|
|
||||||
}) {
|
|
||||||
const [frontFile, setFrontFile] = useState<File | null>(null)
|
|
||||||
const [backFile, setBackFile] = useState<File | null>(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 (
|
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
||||||
<div className="bg-white rounded-lg p-6 w-full max-w-md mx-4">
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900">
|
|
||||||
Upload {userType === 'company' ? 'Company' : 'Personal'} Documents
|
|
||||||
</h3>
|
|
||||||
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
|
|
||||||
<XCircleIcon className="h-6 w-6" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
{userType === 'company' ? 'Company Registration (Front)' : 'ID Document (Front)'}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
accept="image/*,.pdf"
|
|
||||||
onChange={(e) => 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
{userType === 'company' ? 'Additional Documents (Optional)' : 'ID Document (Back)'}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
accept="image/*,.pdf"
|
|
||||||
onChange={(e) => 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
{userType === 'company' ? 'Document Type' : 'ID Type'} <span className="text-red-500">*</span>
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={idType}
|
|
||||||
onChange={(e) => 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"
|
|
||||||
>
|
|
||||||
<option value="">Select Document Type</option>
|
|
||||||
{userType === 'company' ? (
|
|
||||||
<>
|
|
||||||
<option value="business_registration">Business Registration</option>
|
|
||||||
<option value="tax_certificate">Tax Certificate</option>
|
|
||||||
<option value="business_license">Business License</option>
|
|
||||||
<option value="other">Other</option>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<option value="passport">Passport</option>
|
|
||||||
<option value="driver_license">Driver's License</option>
|
|
||||||
<option value="national_id">National ID</option>
|
|
||||||
<option value="other">Other</option>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
{userType === 'company' ? 'Registration/Document Number' : 'ID Number'} <span className="text-red-500">*</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={idNumber}
|
|
||||||
onChange={(e) => 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'}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Expiry Date <span className="text-red-500">*</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
value={expiryDate}
|
|
||||||
onChange={(e) => 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={handleUpload}
|
|
||||||
disabled={loading || !frontFile || !idType || !idNumber || !expiryDate}
|
|
||||||
className="w-full bg-[#8D6B1D] text-white py-2 px-4 rounded-md hover:bg-[#7A5E1A] disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200"
|
|
||||||
>
|
|
||||||
{loading ? 'Uploading...' : 'Upload Documents'}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{message && (
|
|
||||||
<div className="text-green-600 text-sm text-center">{message}</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="text-red-600 text-sm text-center">{error}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Profile Completion Modal Component
|
|
||||||
function ProfileCompletionModal({ userType, onClose, onSuccess }: {
|
|
||||||
userType: string,
|
|
||||||
onClose: () => void,
|
|
||||||
onSuccess: () => void
|
|
||||||
}) {
|
|
||||||
const [formData, setFormData] = useState<any>({})
|
|
||||||
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 (
|
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
||||||
<div className="bg-white rounded-lg p-6 w-full max-w-md mx-4">
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900">Complete Profile</h3>
|
|
||||||
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
|
|
||||||
<XCircleIcon className="h-6 w-6" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
{userType === 'company' ? (
|
|
||||||
<>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Company Name</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={formData.companyName || ''}
|
|
||||||
onChange={(e) => 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Industry</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={formData.industry || ''}
|
|
||||||
onChange={(e) => 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Phone Number</label>
|
|
||||||
<input
|
|
||||||
type="tel"
|
|
||||||
value={formData.phone || ''}
|
|
||||||
onChange={(e) => 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Address</label>
|
|
||||||
<textarea
|
|
||||||
value={formData.address || ''}
|
|
||||||
onChange={(e) => setFormData({...formData, address: 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"
|
|
||||||
rows={3}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={handleSubmit}
|
|
||||||
disabled={loading}
|
|
||||||
className="w-full bg-[#8D6B1D] text-white py-2 px-4 rounded-md hover:bg-[#7A5E1A] disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200"
|
|
||||||
>
|
|
||||||
{loading ? 'Saving...' : 'Complete Profile'}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{message && (
|
|
||||||
<div className="text-green-600 text-sm text-center">{message}</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="text-red-600 text-sm text-center">{error}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Contract Signing Modal Component
|
|
||||||
function ContractSigningModal({ userType, onClose, onSuccess }: {
|
|
||||||
userType: string,
|
|
||||||
onClose: () => void,
|
|
||||||
onSuccess: () => void
|
|
||||||
}) {
|
|
||||||
const [contractFile, setContractFile] = useState<File | null>(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 (
|
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
||||||
<div className="bg-white rounded-lg p-6 w-full max-w-md mx-4">
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900">Sign Contract</h3>
|
|
||||||
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
|
|
||||||
<XCircleIcon className="h-6 w-6" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-600 mb-4">
|
|
||||||
Please review and upload your signed service agreement.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Signed Contract Document
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
accept="image/*,.pdf"
|
|
||||||
onChange={(e) => 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-start">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="agreement"
|
|
||||||
checked={agreed}
|
|
||||||
onChange={(e) => setAgreed(e.target.checked)}
|
|
||||||
className="mt-1 h-4 w-4 text-[#8D6B1D] focus:ring-[#8D6B1D] border-gray-300 rounded"
|
|
||||||
/>
|
|
||||||
<label htmlFor="agreement" className="ml-2 text-sm text-gray-600">
|
|
||||||
I have read, understood, and agree to the terms and conditions of this service agreement.
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={handleUpload}
|
|
||||||
disabled={loading || !contractFile || !agreed}
|
|
||||||
className="w-full bg-[#8D6B1D] text-white py-2 px-4 rounded-md hover:bg-[#7A5E1A] disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200"
|
|
||||||
>
|
|
||||||
{loading ? 'Uploading...' : 'Upload Signed Contract'}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{message && (
|
|
||||||
<div className="text-green-600 text-sm text-center">{message}</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="text-red-600 text-sm text-center">{error}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -18,17 +18,16 @@ import {
|
|||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const user = useAuthStore(state => state.user)
|
const user = useAuthStore(state => state.user)
|
||||||
const isAuthReady = useAuthStore(state => state.isAuthReady)
|
|
||||||
|
|
||||||
// Redirect if not logged in (only after auth is ready)
|
// Redirect if not logged in
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isAuthReady && !user) {
|
if (!user) {
|
||||||
router.push('/login')
|
router.push('/login')
|
||||||
}
|
}
|
||||||
}, [isAuthReady, user, router])
|
}, [user, router])
|
||||||
|
|
||||||
// Show loading until auth is ready or user is confirmed
|
// Don't render if no user
|
||||||
if (!isAuthReady || !user) {
|
if (!user) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center">
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
@ -105,31 +104,6 @@ export default function DashboardPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Account setup note */}
|
|
||||||
<div className="mb-8 bg-blue-50 border border-blue-200 rounded-lg p-4">
|
|
||||||
<div className="flex">
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<UserCircleIcon className="h-5 w-5 text-blue-400" aria-hidden="true" />
|
|
||||||
</div>
|
|
||||||
<div className="ml-3">
|
|
||||||
<h3 className="text-sm font-medium text-blue-800">
|
|
||||||
Complete your account setup
|
|
||||||
</h3>
|
|
||||||
<div className="mt-2 text-sm text-blue-700">
|
|
||||||
<p>
|
|
||||||
Complete your verification process to unlock all features.{' '}
|
|
||||||
<button
|
|
||||||
onClick={() => router.push('/quickaction-dashboard')}
|
|
||||||
className="font-medium underline text-blue-800 hover:text-blue-900"
|
|
||||||
>
|
|
||||||
Start verification →
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Stats Grid - Tailwind UI Plus "With brand icon" */}
|
{/* Stats Grid - Tailwind UI Plus "With brand icon" */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4">
|
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
|||||||
@ -1,112 +0,0 @@
|
|||||||
'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<any>({})
|
|
||||||
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 (
|
|
||||||
<div className="min-h-screen bg-gray-100 p-8">
|
|
||||||
<div className="max-w-4xl mx-auto">
|
|
||||||
<h1 className="text-3xl font-bold mb-8">Auth Debug Page</h1>
|
|
||||||
|
|
||||||
<div className="grid gap-6 md:grid-cols-2">
|
|
||||||
{/* Auth Store State */}
|
|
||||||
<div className="bg-white rounded-lg p-6 shadow">
|
|
||||||
<h2 className="text-xl font-semibold mb-4">Auth Store State</h2>
|
|
||||||
<pre className="text-xs bg-gray-100 p-4 rounded overflow-auto">
|
|
||||||
{JSON.stringify(debugInfo, null, 2)}
|
|
||||||
</pre>
|
|
||||||
<div className="mt-4 space-x-2">
|
|
||||||
<button
|
|
||||||
onClick={handleRefreshToken}
|
|
||||||
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
|
||||||
>
|
|
||||||
Refresh Token
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleTestApiCall}
|
|
||||||
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
|
|
||||||
>
|
|
||||||
Test API Call
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* User Status */}
|
|
||||||
<div className="bg-white rounded-lg p-6 shadow">
|
|
||||||
<h2 className="text-xl font-semibold mb-4">User Status Hook</h2>
|
|
||||||
<div className="space-y-2 text-sm">
|
|
||||||
<p><strong>Loading:</strong> {loading ? 'Yes' : 'No'}</p>
|
|
||||||
<p><strong>Error:</strong> {error || 'None'}</p>
|
|
||||||
<p><strong>Status:</strong></p>
|
|
||||||
<pre className="text-xs bg-gray-100 p-4 rounded overflow-auto">
|
|
||||||
{JSON.stringify(userStatus, null, 2)}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Environment */}
|
|
||||||
<div className="bg-white rounded-lg p-6 shadow md:col-span-2">
|
|
||||||
<h2 className="text-xl font-semibold mb-4">Environment</h2>
|
|
||||||
<div className="text-sm space-y-1">
|
|
||||||
<p><strong>API Base URL:</strong> {process.env.NEXT_PUBLIC_API_BASE_URL}</p>
|
|
||||||
<p><strong>Node Env:</strong> {process.env.NODE_ENV}</p>
|
|
||||||
<p><strong>Current URL:</strong> {typeof window !== 'undefined' ? window.location.href : 'SSR'}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,161 +0,0 @@
|
|||||||
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<UserStatus | null>(null)
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
const [error, setError] = useState<string | null>(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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,10 +1,8 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState, useCallback, useEffect } from 'react'
|
import { useState, useCallback } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
|
||||||
import PageLayout from '../components/PageLayout'
|
import PageLayout from '../components/PageLayout'
|
||||||
import useAuthStore from '../store/authStore'
|
import useAuthStore from '../store/authStore'
|
||||||
import { useUserStatus } from '../hooks/useUserStatus'
|
|
||||||
import {
|
import {
|
||||||
CheckCircleIcon,
|
CheckCircleIcon,
|
||||||
XCircleIcon,
|
XCircleIcon,
|
||||||
@ -26,20 +24,13 @@ interface StatusItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function QuickActionDashboardPage() {
|
export default function QuickActionDashboardPage() {
|
||||||
const router = useRouter()
|
|
||||||
const user = useAuthStore(s => s.user)
|
const user = useAuthStore(s => s.user)
|
||||||
const { userStatus, loading, error, refreshStatus } = useUserStatus()
|
|
||||||
const [isClient, setIsClient] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
// Mock status derivation (replace with real user flags)
|
||||||
setIsClient(true)
|
const [emailVerified, setEmailVerified] = useState(!!user?.emailVerified)
|
||||||
}, [])
|
const [idUploaded, setIdUploaded] = useState(false)
|
||||||
|
const [additionalInfo, setAdditionalInfo] = useState(false)
|
||||||
// Derive status from real backend data
|
const [contractSigned, setContractSigned] = useState(false)
|
||||||
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[] = [
|
const statusItems: StatusItem[] = [
|
||||||
{
|
{
|
||||||
@ -72,25 +63,30 @@ export default function QuickActionDashboardPage() {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
// Action handlers - navigate to proper QuickAction pages
|
// Action handlers (placeholder async simulations)
|
||||||
const handleVerifyEmail = useCallback(() => {
|
const handleVerifyEmail = useCallback(async () => {
|
||||||
router.push('/quickaction-dashboard/register-email-verify')
|
// TODO: trigger backend email verification flow
|
||||||
}, [router])
|
await new Promise(r => setTimeout(r, 600))
|
||||||
|
setEmailVerified(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const handleUploadId = useCallback(() => {
|
const handleUploadId = useCallback(async () => {
|
||||||
const userType = user?.userType || 'personal'
|
// TODO: open upload modal / navigate
|
||||||
router.push(`/quickaction-dashboard/register-upload-id/${userType}`)
|
await new Promise(r => setTimeout(r, 600))
|
||||||
}, [router, user])
|
setIdUploaded(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const handleCompleteInfo = useCallback(() => {
|
const handleCompleteInfo = useCallback(async () => {
|
||||||
const userType = user?.userType || 'personal'
|
// TODO: navigate to profile completion
|
||||||
router.push(`/quickaction-dashboard/register-additional-information/${userType}`)
|
await new Promise(r => setTimeout(r, 600))
|
||||||
}, [router, user])
|
setAdditionalInfo(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const handleSignContract = useCallback(() => {
|
const handleSignContract = useCallback(async () => {
|
||||||
const userType = user?.userType || 'personal'
|
// TODO: open contract signing flow
|
||||||
router.push(`/quickaction-dashboard/register-sign-contract/${userType}`)
|
await new Promise(r => setTimeout(r, 600))
|
||||||
}, [router, user])
|
setContractSigned(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const canUploadId = emailVerified
|
const canUploadId = emailVerified
|
||||||
const canCompleteInfo = emailVerified && idUploaded
|
const canCompleteInfo = emailVerified && idUploaded
|
||||||
@ -137,41 +133,11 @@ export default function QuickActionDashboardPage() {
|
|||||||
{/* Welcome */}
|
{/* Welcome */}
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
<h1 className="text-3xl sm:text-4xl font-bold text-white tracking-tight">
|
<h1 className="text-3xl sm:text-4xl font-bold text-white tracking-tight">
|
||||||
Welcome{isClient && user?.firstName ? `, ${user.firstName}` : ''}!
|
Welcome{user?.firstName ? `, ${user.firstName}` : ''}!
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm sm:text-base text-blue-100 font-medium mt-2">
|
<p className="text-sm sm:text-base text-blue-100 font-medium mt-2">
|
||||||
{isClient && user?.userType === 'company' ? 'Company Account' : 'Personal Account'}
|
Personal Account
|
||||||
</p>
|
</p>
|
||||||
{loading && (
|
|
||||||
<p className="text-xs text-blue-200 mt-1">Loading status...</p>
|
|
||||||
)}
|
|
||||||
{error && (
|
|
||||||
<div className="mt-4 max-w-md mx-auto rounded-md bg-red-50 border border-red-200 px-4 py-3">
|
|
||||||
<div className="flex">
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z" clipRule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div className="ml-3">
|
|
||||||
<h3 className="text-sm font-medium text-red-800">
|
|
||||||
Error loading account status
|
|
||||||
</h3>
|
|
||||||
<div className="mt-2 text-sm text-red-700">
|
|
||||||
<p>{error}</p>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4">
|
|
||||||
<button
|
|
||||||
onClick={() => refreshStatus()}
|
|
||||||
className="text-sm bg-red-100 text-red-800 px-3 py-1 rounded-md hover:bg-red-200 transition-colors"
|
|
||||||
>
|
|
||||||
Try again
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Outer container card mimic (like screenshot) */}
|
{/* Outer container card mimic (like screenshot) */}
|
||||||
|
|||||||
@ -1,10 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
|
||||||
import PageLayout from '../../../components/PageLayout'
|
import PageLayout from '../../../components/PageLayout'
|
||||||
import useAuthStore from '../../../store/authStore'
|
|
||||||
import { useUserStatus } from '../../../hooks/useUserStatus'
|
|
||||||
|
|
||||||
interface CompanyProfileData {
|
interface CompanyProfileData {
|
||||||
companyName: string
|
companyName: string
|
||||||
@ -21,16 +18,6 @@ interface CompanyProfileData {
|
|||||||
emergencyPhone: string
|
emergencyPhone: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Common countries list
|
|
||||||
const COUNTRIES = [
|
|
||||||
'Germany', 'Austria', 'Switzerland', 'Italy', 'France', 'Spain', 'Portugal', 'Netherlands',
|
|
||||||
'Belgium', 'Poland', 'Czech Republic', 'Hungary', 'Croatia', 'Slovenia', 'Slovakia',
|
|
||||||
'United Kingdom', 'Ireland', 'Sweden', 'Norway', 'Denmark', 'Finland', 'Russia',
|
|
||||||
'Turkey', 'Greece', 'Romania', 'Bulgaria', 'Serbia', 'Albania', 'Bosnia and Herzegovina',
|
|
||||||
'United States', 'Canada', 'Brazil', 'Argentina', 'Mexico', 'China', 'Japan',
|
|
||||||
'India', 'Pakistan', 'Australia', 'South Africa', 'Other'
|
|
||||||
]
|
|
||||||
|
|
||||||
const init: CompanyProfileData = {
|
const init: CompanyProfileData = {
|
||||||
companyName: '',
|
companyName: '',
|
||||||
vatNumber: '',
|
vatNumber: '',
|
||||||
@ -47,16 +34,12 @@ const init: CompanyProfileData = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function CompanyAdditionalInformationPage() {
|
export default function CompanyAdditionalInformationPage() {
|
||||||
const router = useRouter()
|
|
||||||
const { accessToken } = useAuthStore()
|
|
||||||
const { refreshStatus } = useUserStatus()
|
|
||||||
|
|
||||||
const [form, setForm] = useState(init)
|
const [form, setForm] = useState(init)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [success, setSuccess] = useState(false)
|
const [success, setSuccess] = useState(false)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const { name, value } = e.target
|
const { name, value } = e.target
|
||||||
setForm(p => ({ ...p, [name]: value }))
|
setForm(p => ({ ...p, [name]: value }))
|
||||||
setError('')
|
setError('')
|
||||||
@ -84,56 +67,13 @@ export default function CompanyAdditionalInformationPage() {
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (loading || success) return
|
if (loading || success) return
|
||||||
if (!validate()) return
|
if (!validate()) return
|
||||||
|
|
||||||
if (!accessToken) {
|
|
||||||
setError('Not authenticated. Please log in again.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Prepare data for backend with correct field names
|
// TODO: POST to backend
|
||||||
const profileData = {
|
await new Promise(r => setTimeout(r, 1300))
|
||||||
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)
|
setSuccess(true)
|
||||||
|
} catch {
|
||||||
// Refresh user status to update profile completion state
|
setError('Speichern fehlgeschlagen.')
|
||||||
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 {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
@ -241,20 +181,13 @@ export default function CompanyAdditionalInformationPage() {
|
|||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Country *
|
Country *
|
||||||
</label>
|
</label>
|
||||||
<select
|
<input
|
||||||
name="country"
|
name="country"
|
||||||
value={form.country}
|
value={form.country}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||||
required
|
required
|
||||||
>
|
/>
|
||||||
<option value="">Select country...</option>
|
|
||||||
{COUNTRIES.map(country => (
|
|
||||||
<option key={country} value={country}>
|
|
||||||
{country}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@ -1,10 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
|
||||||
import PageLayout from '../../../components/PageLayout'
|
import PageLayout from '../../../components/PageLayout'
|
||||||
import useAuthStore from '../../../store/authStore'
|
|
||||||
import { useUserStatus } from '../../../hooks/useUserStatus'
|
|
||||||
|
|
||||||
interface PersonalProfileData {
|
interface PersonalProfileData {
|
||||||
dob: string
|
dob: string
|
||||||
@ -20,26 +17,6 @@ interface PersonalProfileData {
|
|||||||
emergencyPhone: string
|
emergencyPhone: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Common nationalities list
|
|
||||||
const NATIONALITIES = [
|
|
||||||
'German', 'Austrian', 'Swiss', 'Italian', 'French', 'Spanish', 'Portuguese', 'Dutch',
|
|
||||||
'Belgian', 'Polish', 'Czech', 'Hungarian', 'Croatian', 'Slovenian', 'Slovak',
|
|
||||||
'British', 'Irish', 'Swedish', 'Norwegian', 'Danish', 'Finnish', 'Russian',
|
|
||||||
'Turkish', 'Greek', 'Romanian', 'Bulgarian', 'Serbian', 'Albanian', 'Bosnian',
|
|
||||||
'American', 'Canadian', 'Brazilian', 'Argentinian', 'Mexican', 'Chinese',
|
|
||||||
'Japanese', 'Indian', 'Pakistani', 'Australian', 'South African', 'Other'
|
|
||||||
]
|
|
||||||
|
|
||||||
// Common countries list
|
|
||||||
const COUNTRIES = [
|
|
||||||
'Germany', 'Austria', 'Switzerland', 'Italy', 'France', 'Spain', 'Portugal', 'Netherlands',
|
|
||||||
'Belgium', 'Poland', 'Czech Republic', 'Hungary', 'Croatia', 'Slovenia', 'Slovakia',
|
|
||||||
'United Kingdom', 'Ireland', 'Sweden', 'Norway', 'Denmark', 'Finland', 'Russia',
|
|
||||||
'Turkey', 'Greece', 'Romania', 'Bulgaria', 'Serbia', 'Albania', 'Bosnia and Herzegovina',
|
|
||||||
'United States', 'Canada', 'Brazil', 'Argentina', 'Mexico', 'China', 'Japan',
|
|
||||||
'India', 'Pakistan', 'Australia', 'South Africa', 'Other'
|
|
||||||
]
|
|
||||||
|
|
||||||
const initialData: PersonalProfileData = {
|
const initialData: PersonalProfileData = {
|
||||||
dob: '',
|
dob: '',
|
||||||
nationality: '',
|
nationality: '',
|
||||||
@ -55,46 +32,17 @@ const initialData: PersonalProfileData = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function PersonalAdditionalInformationPage() {
|
export default function PersonalAdditionalInformationPage() {
|
||||||
const router = useRouter()
|
|
||||||
const { accessToken } = useAuthStore()
|
|
||||||
const { refreshStatus } = useUserStatus()
|
|
||||||
|
|
||||||
const [form, setForm] = useState(initialData)
|
const [form, setForm] = useState(initialData)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [success, setSuccess] = useState(false)
|
const [success, setSuccess] = useState(false)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const { name, value } = e.target
|
const { name, value } = e.target
|
||||||
setForm(p => ({ ...p, [name]: value }))
|
setForm(p => ({ ...p, [name]: value }))
|
||||||
setError('')
|
setError('')
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateDateOfBirth = (dob: string) => {
|
|
||||||
if (!dob) return false
|
|
||||||
|
|
||||||
const birthDate = new Date(dob)
|
|
||||||
const today = new Date()
|
|
||||||
|
|
||||||
// Check if date is valid
|
|
||||||
if (isNaN(birthDate.getTime())) return false
|
|
||||||
|
|
||||||
// Check if birth date is not in the future
|
|
||||||
if (birthDate > today) return false
|
|
||||||
|
|
||||||
// Check minimum age (18 years)
|
|
||||||
const minDate = new Date()
|
|
||||||
minDate.setFullYear(today.getFullYear() - 18)
|
|
||||||
if (birthDate > minDate) return false
|
|
||||||
|
|
||||||
// Check maximum age (120 years)
|
|
||||||
const maxDate = new Date()
|
|
||||||
maxDate.setFullYear(today.getFullYear() - 120)
|
|
||||||
if (birthDate < maxDate) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const validate = () => {
|
const validate = () => {
|
||||||
const requiredKeys: (keyof PersonalProfileData)[] = [
|
const requiredKeys: (keyof PersonalProfileData)[] = [
|
||||||
'dob','nationality','street','postalCode','city','country','accountHolder','iban'
|
'dob','nationality','street','postalCode','city','country','accountHolder','iban'
|
||||||
@ -105,13 +53,6 @@ export default function PersonalAdditionalInformationPage() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Date of birth validation
|
|
||||||
if (!validateDateOfBirth(form.dob)) {
|
|
||||||
setError('Ungültiges Geburtsdatum. Sie müssen mindestens 18 Jahre alt sein.')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// very loose IBAN check
|
// very loose IBAN check
|
||||||
if (!/^([A-Z]{2}\d{2}[A-Z0-9]{10,30})$/i.test(form.iban.replace(/\s+/g,''))) {
|
if (!/^([A-Z]{2}\d{2}[A-Z0-9]{10,30})$/i.test(form.iban.replace(/\s+/g,''))) {
|
||||||
setError('Ungültige IBAN.')
|
setError('Ungültige IBAN.')
|
||||||
@ -125,57 +66,13 @@ export default function PersonalAdditionalInformationPage() {
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (loading || success) return
|
if (loading || success) return
|
||||||
if (!validate()) return
|
if (!validate()) return
|
||||||
|
|
||||||
if (!accessToken) {
|
|
||||||
setError('Not authenticated. Please log in again.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Prepare data for backend with correct field names
|
// TODO: POST to backend
|
||||||
const profileData = {
|
await new Promise(r => setTimeout(r, 1200))
|
||||||
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)
|
setSuccess(true)
|
||||||
|
} catch {
|
||||||
// Refresh user status to update profile completion state
|
setError('Speichern fehlgeschlagen. Bitte erneut versuchen.')
|
||||||
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 {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
@ -227,8 +124,6 @@ export default function PersonalAdditionalInformationPage() {
|
|||||||
name="dob"
|
name="dob"
|
||||||
value={form.dob}
|
value={form.dob}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
min={new Date(new Date().getFullYear() - 120, 0, 1).toISOString().split('T')[0]}
|
|
||||||
max={new Date(new Date().getFullYear() - 18, 11, 31).toISOString().split('T')[0]}
|
|
||||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@ -237,20 +132,14 @@ export default function PersonalAdditionalInformationPage() {
|
|||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Nationality *
|
Nationality *
|
||||||
</label>
|
</label>
|
||||||
<select
|
<input
|
||||||
name="nationality"
|
name="nationality"
|
||||||
value={form.nationality}
|
value={form.nationality}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
placeholder="e.g. German"
|
||||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||||
required
|
required
|
||||||
>
|
/>
|
||||||
<option value="">Select nationality...</option>
|
|
||||||
{NATIONALITIES.map(nationality => (
|
|
||||||
<option key={nationality} value={nationality}>
|
|
||||||
{nationality}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:col-span-2 lg:col-span-3">
|
<div className="sm:col-span-2 lg:col-span-3">
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
@ -295,20 +184,14 @@ export default function PersonalAdditionalInformationPage() {
|
|||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Country *
|
Country *
|
||||||
</label>
|
</label>
|
||||||
<select
|
<input
|
||||||
name="country"
|
name="country"
|
||||||
value={form.country}
|
value={form.country}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
placeholder="e.g. Germany"
|
||||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||||
required
|
required
|
||||||
>
|
/>
|
||||||
<option value="">Select country...</option>
|
|
||||||
{COUNTRIES.map(country => (
|
|
||||||
<option key={country} value={country}>
|
|
||||||
{country}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@ -3,50 +3,16 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||||
import PageLayout from '../../components/PageLayout'
|
import PageLayout from '../../components/PageLayout'
|
||||||
import useAuthStore from '../../store/authStore'
|
import useAuthStore from '../../store/authStore'
|
||||||
import { useUserStatus } from '../../hooks/useUserStatus'
|
|
||||||
|
|
||||||
export default function EmailVerifyPage() {
|
export default function EmailVerifyPage() {
|
||||||
const user = useAuthStore(s => s.user)
|
const user = useAuthStore(s => s.user)
|
||||||
const token = useAuthStore(s => s.accessToken)
|
|
||||||
const { refreshStatus } = useUserStatus()
|
|
||||||
const [code, setCode] = useState(['', '', '', '', '', ''])
|
const [code, setCode] = useState(['', '', '', '', '', ''])
|
||||||
const [submitting, setSubmitting] = useState(false)
|
const [submitting, setSubmitting] = useState(false)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [success, setSuccess] = useState(false)
|
const [success, setSuccess] = useState(false)
|
||||||
const [resendCooldown, setResendCooldown] = useState(0)
|
const [resendCooldown, setResendCooldown] = useState(0)
|
||||||
const [initialEmailSent, setInitialEmailSent] = useState(false)
|
|
||||||
const inputsRef = useRef<Array<HTMLInputElement | null>>([])
|
const inputsRef = useRef<Array<HTMLInputElement | null>>([])
|
||||||
|
|
||||||
// Send verification email automatically on page load
|
|
||||||
useEffect(() => {
|
|
||||||
if (!token || initialEmailSent) return
|
|
||||||
|
|
||||||
const sendInitialEmail = async () => {
|
|
||||||
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) {
|
|
||||||
setInitialEmailSent(true)
|
|
||||||
setResendCooldown(30) // Start cooldown after initial send
|
|
||||||
} else {
|
|
||||||
console.error('Failed to send initial verification email:', data.message)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error sending initial verification email:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sendInitialEmail()
|
|
||||||
}, [token, initialEmailSent])
|
|
||||||
|
|
||||||
// Cooldown timer
|
// Cooldown timer
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!resendCooldown) return
|
if (!resendCooldown) return
|
||||||
@ -88,68 +54,25 @@ export default function EmailVerifyPage() {
|
|||||||
setError('Bitte 6-stelligen Code eingeben.')
|
setError('Bitte 6-stelligen Code eingeben.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!token) {
|
|
||||||
setError('Nicht authentifiziert. Bitte erneut einloggen.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setSubmitting(true)
|
setSubmitting(true)
|
||||||
setError('')
|
setError('')
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/verify-email-code`, {
|
// TODO: call backend verify endpoint
|
||||||
method: 'POST',
|
await new Promise(r => setTimeout(r, 1000))
|
||||||
headers: {
|
setSuccess(true)
|
||||||
'Authorization': `Bearer ${token}`,
|
} catch {
|
||||||
'Content-Type': 'application/json'
|
setError('Verifizierung fehlgeschlagen. Bitte erneut versuchen.')
|
||||||
},
|
|
||||||
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 {
|
} finally {
|
||||||
setSubmitting(false)
|
setSubmitting(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleResend = useCallback(async () => {
|
const handleResend = useCallback(async () => {
|
||||||
if (resendCooldown || !token) return
|
if (resendCooldown) return
|
||||||
setError('')
|
setError('')
|
||||||
|
// TODO: call resend endpoint
|
||||||
try {
|
setResendCooldown(30)
|
||||||
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/send-verification-email`, {
|
}, [resendCooldown])
|
||||||
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 (
|
return (
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
@ -197,23 +120,12 @@ export default function EmailVerifyPage() {
|
|||||||
E-Mail verifizieren
|
E-Mail verifizieren
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-3 text-gray-300 text-sm sm:text-base">
|
<p className="mt-3 text-gray-300 text-sm sm:text-base">
|
||||||
{initialEmailSent ? (
|
Gib den 6-stelligen Code ein, den wir an
|
||||||
<>
|
{' '}
|
||||||
Wir haben einen 6-stelligen Code an{' '}
|
<span className="text-indigo-300 font-medium">
|
||||||
<span className="text-indigo-300 font-medium">
|
{user?.email || 'deine E-Mail'}
|
||||||
{user?.email || 'deine E-Mail'}
|
</span>{' '}
|
||||||
</span>{' '}
|
gesendet haben.
|
||||||
gesendet. Gib ihn unten ein.
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
E-Mail wird gesendet an{' '}
|
|
||||||
<span className="text-indigo-300 font-medium">
|
|
||||||
{user?.email || 'deine E-Mail'}
|
|
||||||
</span>
|
|
||||||
...
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,16 +1,9 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
|
||||||
import PageLayout from '../../../components/PageLayout'
|
import PageLayout from '../../../components/PageLayout'
|
||||||
import useAuthStore from '../../../store/authStore'
|
|
||||||
import { useUserStatus } from '../../../hooks/useUserStatus'
|
|
||||||
|
|
||||||
export default function CompanySignContractPage() {
|
export default function CompanySignContractPage() {
|
||||||
const router = useRouter()
|
|
||||||
const { accessToken } = useAuthStore()
|
|
||||||
const { refreshStatus } = useUserStatus()
|
|
||||||
|
|
||||||
const [companyName, setCompanyName] = useState('')
|
const [companyName, setCompanyName] = useState('')
|
||||||
const [repName, setRepName] = useState('')
|
const [repName, setRepName] = useState('')
|
||||||
const [repTitle, setRepTitle] = useState('')
|
const [repTitle, setRepTitle] = useState('')
|
||||||
@ -28,94 +21,29 @@ export default function CompanySignContractPage() {
|
|||||||
setDate(new Date().toISOString().slice(0,10))
|
setDate(new Date().toISOString().slice(0,10))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const valid = () => {
|
const valid = () =>
|
||||||
const companyValid = companyName.trim().length >= 3 // Min 3 characters for company name
|
companyName.trim().length > 2 &&
|
||||||
const repNameValid = repName.trim().length >= 3 // Min 3 characters for representative name
|
repName.trim().length > 4 &&
|
||||||
const repTitleValid = repTitle.trim().length >= 2 // Min 2 characters for title
|
repTitle.trim().length > 1 &&
|
||||||
const locationValid = location.trim().length >= 2 // Min 2 characters for location
|
location.trim().length > 1 &&
|
||||||
const contractChecked = agreeContract
|
agreeContract &&
|
||||||
const dataChecked = agreeData
|
agreeData &&
|
||||||
const signatureChecked = confirmSignature
|
confirmSignature
|
||||||
|
|
||||||
return companyValid && repNameValid && repTitleValid && locationValid && contractChecked && dataChecked && signatureChecked
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!valid()) {
|
if (!valid()) {
|
||||||
// Detailed error message to help debug
|
setError('Bitte alle Pflichtfelder & Bestätigungen ausfüllen.')
|
||||||
const issues = []
|
|
||||||
if (companyName.trim().length < 3) issues.push('Firmenname (mindestens 3 Zeichen)')
|
|
||||||
if (repName.trim().length < 3) issues.push('Vertreter Name (mindestens 3 Zeichen)')
|
|
||||||
if (repTitle.trim().length < 2) issues.push('Vertretertitel (mindestens 2 Zeichen)')
|
|
||||||
if (location.trim().length < 2) issues.push('Ort (mindestens 2 Zeichen)')
|
|
||||||
if (!agreeContract) issues.push('Vertrag gelesen und verstanden')
|
|
||||||
if (!agreeData) issues.push('Datenschutzerklärung zugestimmt')
|
|
||||||
if (!confirmSignature) issues.push('Elektronische Signatur bestätigt')
|
|
||||||
|
|
||||||
setError(`Bitte vervollständigen: ${issues.join(', ')}`)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!accessToken) {
|
|
||||||
setError('Not authenticated. Please log in again.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setError('')
|
setError('')
|
||||||
setSubmitting(true)
|
setSubmitting(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const contractData = {
|
// TODO: POST /contracts/company/sign { companyName, repName, repTitle, location, date, note }
|
||||||
companyName: companyName.trim(),
|
await new Promise(r => setTimeout(r, 1400))
|
||||||
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 PDF file since the backend expects one (electronic signature)
|
|
||||||
const dummyPdfContent = '%PDF-1.4\n1 0 obj\n<<\n/Type /Catalog\n/Pages 2 0 R\n>>\nendobj\n2 0 obj\n<<\n/Type /Pages\n/Kids [3 0 R]\n/Count 1\n>>\nendobj\n3 0 obj\n<<\n/Type /Page\n/Parent 2 0 R\n/MediaBox [0 0 612 792]\n/Contents 4 0 R\n>>\nendobj\n4 0 obj\n<<\n/Length 44\n>>\nstream\nBT\n/F1 12 Tf\n100 700 Td\n(Electronic Signature) Tj\nET\nendstream\nendobj\nxref\n0 5\n0000000000 65535 f \n0000000010 00000 n \n0000000079 00000 n \n0000000136 00000 n \n0000000225 00000 n \ntrailer\n<<\n/Size 5\n/Root 1 0 R\n>>\nstartxref\n319\n%%EOF'
|
|
||||||
const dummyFile = new Blob([dummyPdfContent], { type: 'application/pdf' })
|
|
||||||
formData.append('contract', dummyFile, 'electronic_signature.pdf')
|
|
||||||
|
|
||||||
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)
|
setSuccess(true)
|
||||||
|
} catch {
|
||||||
// Refresh user status to update contract signed state
|
setError('Signatur fehlgeschlagen. Bitte erneut versuchen.')
|
||||||
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 {
|
} finally {
|
||||||
setSubmitting(false)
|
setSubmitting(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,9 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
|
||||||
import PageLayout from '../../../components/PageLayout'
|
import PageLayout from '../../../components/PageLayout'
|
||||||
import useAuthStore from '../../../store/authStore'
|
|
||||||
import { useUserStatus } from '../../../hooks/useUserStatus'
|
|
||||||
|
|
||||||
export default function PersonalSignContractPage() {
|
export default function PersonalSignContractPage() {
|
||||||
const router = useRouter()
|
|
||||||
const { accessToken } = useAuthStore()
|
|
||||||
const { refreshStatus } = useUserStatus()
|
|
||||||
|
|
||||||
const [fullName, setFullName] = useState('')
|
const [fullName, setFullName] = useState('')
|
||||||
const [location, setLocation] = useState('')
|
const [location, setLocation] = useState('')
|
||||||
const [date, setDate] = useState('')
|
const [date, setDate] = useState('')
|
||||||
@ -26,88 +19,27 @@ export default function PersonalSignContractPage() {
|
|||||||
setDate(new Date().toISOString().slice(0, 10))
|
setDate(new Date().toISOString().slice(0, 10))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const valid = () => {
|
const valid = () =>
|
||||||
const nameValid = fullName.trim().length >= 3 // Min 3 characters for name
|
fullName.trim().length > 4 &&
|
||||||
const locationValid = location.trim().length >= 2 // Min 2 characters for location
|
location.trim().length > 1 &&
|
||||||
const contractChecked = agreeContract
|
agreeContract &&
|
||||||
const dataChecked = agreeData
|
agreeData &&
|
||||||
const signatureChecked = confirmSignature
|
confirmSignature
|
||||||
|
|
||||||
return nameValid && locationValid && contractChecked && dataChecked && signatureChecked
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!valid()) {
|
if (!valid()) {
|
||||||
// Detailed error message to help debug
|
setError('Bitte alle Pflichtfelder & Bestätigungen ausfüllen.')
|
||||||
const issues = []
|
|
||||||
if (fullName.trim().length < 3) issues.push('Vollständiger Name (mindestens 3 Zeichen)')
|
|
||||||
if (location.trim().length < 2) issues.push('Ort (mindestens 2 Zeichen)')
|
|
||||||
if (!agreeContract) issues.push('Vertrag gelesen und verstanden')
|
|
||||||
if (!agreeData) issues.push('Datenschutzerklärung zugestimmt')
|
|
||||||
if (!confirmSignature) issues.push('Elektronische Signatur bestätigt')
|
|
||||||
|
|
||||||
setError(`Bitte vervollständigen: ${issues.join(', ')}`)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!accessToken) {
|
|
||||||
setError('Not authenticated. Please log in again.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setError('')
|
setError('')
|
||||||
setSubmitting(true)
|
setSubmitting(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const contractData = {
|
// TODO: POST /contracts/personal/sign { fullName, location, date, note }
|
||||||
fullName: fullName.trim(),
|
await new Promise(r => setTimeout(r, 1200))
|
||||||
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 PDF file since the backend expects one (electronic signature)
|
|
||||||
const dummyPdfContent = '%PDF-1.4\n1 0 obj\n<<\n/Type /Catalog\n/Pages 2 0 R\n>>\nendobj\n2 0 obj\n<<\n/Type /Pages\n/Kids [3 0 R]\n/Count 1\n>>\nendobj\n3 0 obj\n<<\n/Type /Page\n/Parent 2 0 R\n/MediaBox [0 0 612 792]\n/Contents 4 0 R\n>>\nendobj\n4 0 obj\n<<\n/Length 44\n>>\nstream\nBT\n/F1 12 Tf\n100 700 Td\n(Electronic Signature) Tj\nET\nendstream\nendobj\nxref\n0 5\n0000000000 65535 f \n0000000010 00000 n \n0000000079 00000 n \n0000000136 00000 n \n0000000225 00000 n \ntrailer\n<<\n/Size 5\n/Root 1 0 R\n>>\nstartxref\n319\n%%EOF'
|
|
||||||
const dummyFile = new Blob([dummyPdfContent], { type: 'application/pdf' })
|
|
||||||
formData.append('contract', dummyFile, 'electronic_signature.pdf')
|
|
||||||
|
|
||||||
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)
|
setSuccess(true)
|
||||||
|
} catch {
|
||||||
// Refresh user status to update contract signed state
|
setError('Signatur fehlgeschlagen. Bitte erneut versuchen.')
|
||||||
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 {
|
} finally {
|
||||||
setSubmitting(false)
|
setSubmitting(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,12 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState, useRef } from 'react'
|
import { useState, useRef } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
|
||||||
import PageLayout from '../../../components/PageLayout'
|
import PageLayout from '../../../components/PageLayout'
|
||||||
import { DocumentArrowUpIcon, XMarkIcon } from '@heroicons/react/24/outline'
|
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']
|
const DOC_TYPES = ['Handelsregisterauszug', 'Gewerbeanmeldung', 'Steuerbescheid', 'Sonstiges']
|
||||||
|
|
||||||
export default function CompanyIdUploadPage() {
|
export default function CompanyIdUploadPage() {
|
||||||
const router = useRouter()
|
|
||||||
const { accessToken } = useAuthStore()
|
|
||||||
const { refreshStatus } = useUserStatus()
|
|
||||||
|
|
||||||
const [docNumber, setDocNumber] = useState('')
|
const [docNumber, setDocNumber] = useState('')
|
||||||
const [docType, setDocType] = useState('')
|
const [docType, setDocType] = useState('')
|
||||||
const [issueDate, setIssueDate] = useState('')
|
const [issueDate, setIssueDate] = useState('')
|
||||||
@ -46,51 +39,14 @@ export default function CompanyIdUploadPage() {
|
|||||||
setError('Bitte alle Pflichtfelder (mit *) ausfüllen.')
|
setError('Bitte alle Pflichtfelder (mit *) ausfüllen.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!accessToken) {
|
|
||||||
setError('Not authenticated. Please log in again.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setError('')
|
setError('')
|
||||||
setSubmitting(true)
|
setSubmitting(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const formData = new FormData()
|
// TODO: API upload
|
||||||
formData.append('frontFile', frontFile)
|
await new Promise(r => setTimeout(r, 1200))
|
||||||
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)
|
setSuccess(true)
|
||||||
|
} catch {
|
||||||
// Refresh user status to update verification state
|
setError('Upload fehlgeschlagen.')
|
||||||
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 {
|
} finally {
|
||||||
setSubmitting(false)
|
setSubmitting(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,23 +2,14 @@
|
|||||||
|
|
||||||
import { useState, useRef, useCallback } from 'react'
|
import { useState, useRef, useCallback } from 'react'
|
||||||
import PageLayout from '../../../components/PageLayout'
|
import PageLayout from '../../../components/PageLayout'
|
||||||
import useAuthStore from '../../../store/authStore'
|
|
||||||
import { useUserStatus } from '../../../hooks/useUserStatus'
|
|
||||||
import {
|
import {
|
||||||
DocumentArrowUpIcon,
|
DocumentArrowUpIcon,
|
||||||
XMarkIcon
|
XMarkIcon
|
||||||
} from '@heroicons/react/24/outline'
|
} from '@heroicons/react/24/outline'
|
||||||
|
|
||||||
const ID_TYPES = [
|
const ID_TYPES = ['Personalausweis', 'Reisepass', 'Führerschein', 'Aufenthaltstitel']
|
||||||
{ value: 'national_id', label: 'Personalausweis' },
|
|
||||||
{ value: 'passport', label: 'Reisepass' },
|
|
||||||
{ value: 'driver_license', label: 'Führerschein' },
|
|
||||||
{ value: 'other', label: 'Aufenthaltstitel' }
|
|
||||||
]
|
|
||||||
|
|
||||||
export default function PersonalIdUploadPage() {
|
export default function PersonalIdUploadPage() {
|
||||||
const token = useAuthStore(s => s.accessToken)
|
|
||||||
const { refreshStatus } = useUserStatus()
|
|
||||||
const [idNumber, setIdNumber] = useState('')
|
const [idNumber, setIdNumber] = useState('')
|
||||||
const [idType, setIdType] = useState('')
|
const [idType, setIdType] = useState('')
|
||||||
const [expiry, setExpiry] = useState('')
|
const [expiry, setExpiry] = useState('')
|
||||||
@ -67,47 +58,13 @@ export default function PersonalIdUploadPage() {
|
|||||||
const submit = async (e: React.FormEvent) => {
|
const submit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!validate()) return
|
if (!validate()) return
|
||||||
if (!token) {
|
|
||||||
setError('Nicht authentifiziert. Bitte erneut einloggen.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setSubmitting(true)
|
setSubmitting(true)
|
||||||
setError('')
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const formData = new FormData()
|
// TODO: Upload logic (multipart/form-data)
|
||||||
formData.append('front', frontFile!)
|
await new Promise(r => setTimeout(r, 1200))
|
||||||
if (hasBack && backFile) {
|
setSuccess(true)
|
||||||
formData.append('back', backFile)
|
} catch {
|
||||||
}
|
setError('Upload fehlgeschlagen. Bitte erneut versuchen.')
|
||||||
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 {
|
} finally {
|
||||||
setSubmitting(false)
|
setSubmitting(false)
|
||||||
}
|
}
|
||||||
@ -189,7 +146,7 @@ export default function PersonalIdUploadPage() {
|
|||||||
required
|
required
|
||||||
>
|
>
|
||||||
<option value="">Select ID type</option>
|
<option value="">Select ID type</option>
|
||||||
{ID_TYPES.map(t => <option key={t.value} value={t.value}>{t.label}</option>)}
|
{ID_TYPES.map(t => <option key={t} value={t}>{t}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -27,28 +27,6 @@ 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 {
|
interface User {
|
||||||
email?: string;
|
email?: string;
|
||||||
companyName?: string;
|
companyName?: string;
|
||||||
@ -72,7 +50,7 @@ interface AuthStore {
|
|||||||
|
|
||||||
const useAuthStore = create<AuthStore>((set, get) => ({
|
const useAuthStore = create<AuthStore>((set, get) => ({
|
||||||
// Initialize with SSR-safe defaults
|
// Initialize with SSR-safe defaults
|
||||||
accessToken: typeof window !== 'undefined' ? getStoredToken() : null,
|
accessToken: null,
|
||||||
user: typeof window !== 'undefined' ? getStoredUser() : null,
|
user: typeof window !== 'undefined' ? getStoredUser() : null,
|
||||||
isAuthReady: false,
|
isAuthReady: false,
|
||||||
isRefreshing: false,
|
isRefreshing: false,
|
||||||
@ -85,20 +63,11 @@ const useAuthStore = create<AuthStore>((set, get) => ({
|
|||||||
|
|
||||||
setAccessToken: (token) => {
|
setAccessToken: (token) => {
|
||||||
log("🔑 Zustand: Setting access token in memory:", token ? `${token.substring(0, 20)}...` : null);
|
log("🔑 Zustand: Setting access token in memory:", token ? `${token.substring(0, 20)}...` : null);
|
||||||
if (typeof window !== 'undefined') {
|
if (token) {
|
||||||
try {
|
const expiry = getTokenExpiry(token);
|
||||||
if (token) {
|
log("⏳ Zustand: Token expiry:", expiry ? expiry.toLocaleString() : "Unknown");
|
||||||
const expiry = getTokenExpiry(token);
|
} else {
|
||||||
log("⏳ Zustand: Token expiry:", expiry ? expiry.toLocaleString() : "Unknown");
|
log("🗑️ Zustand: Clearing in-memory access token");
|
||||||
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 });
|
set({ accessToken: token });
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,241 +0,0 @@
|
|||||||
// 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<Response> {
|
|
||||||
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<Response> {
|
|
||||||
return this.makeRequest(endpoint, { method: 'GET' }, token)
|
|
||||||
}
|
|
||||||
|
|
||||||
static async post(
|
|
||||||
endpoint: string,
|
|
||||||
data?: any,
|
|
||||||
token?: string
|
|
||||||
): Promise<Response> {
|
|
||||||
return this.makeRequest(
|
|
||||||
endpoint,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
body: data ? JSON.stringify(data) : undefined,
|
|
||||||
},
|
|
||||||
token
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
static async postFormData(
|
|
||||||
endpoint: string,
|
|
||||||
formData: FormData,
|
|
||||||
token?: string
|
|
||||||
): Promise<Response> {
|
|
||||||
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<Response> {
|
|
||||||
return this.makeRequest(
|
|
||||||
endpoint,
|
|
||||||
{
|
|
||||||
method: 'PUT',
|
|
||||||
body: data ? JSON.stringify(data) : undefined,
|
|
||||||
},
|
|
||||||
token
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
static async patch(
|
|
||||||
endpoint: string,
|
|
||||||
data?: any,
|
|
||||||
token?: string
|
|
||||||
): Promise<Response> {
|
|
||||||
return this.makeRequest(
|
|
||||||
endpoint,
|
|
||||||
{
|
|
||||||
method: 'PATCH',
|
|
||||||
body: data ? JSON.stringify(data) : undefined,
|
|
||||||
},
|
|
||||||
token
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
static async delete(endpoint: string, token?: string): Promise<Response> {
|
|
||||||
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<T = any> {
|
|
||||||
success: boolean
|
|
||||||
message?: string
|
|
||||||
data?: T
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user