Compare commits

..

5 Commits

5 changed files with 189 additions and 38 deletions

View File

@ -21,6 +21,16 @@ 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: '',
@ -46,7 +56,7 @@ export default function CompanyAdditionalInformationPage() {
const [success, setSuccess] = useState(false) const [success, setSuccess] = useState(false)
const [error, setError] = useState('') const [error, setError] = useState('')
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target const { name, value } = e.target
setForm(p => ({ ...p, [name]: value })) setForm(p => ({ ...p, [name]: value }))
setError('') setError('')
@ -231,13 +241,20 @@ 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>
<input <select
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>

View File

@ -20,6 +20,26 @@ 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: '',
@ -44,12 +64,37 @@ export default function PersonalAdditionalInformationPage() {
const [success, setSuccess] = useState(false) const [success, setSuccess] = useState(false)
const [error, setError] = useState('') const [error, setError] = useState('')
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
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'
@ -60,6 +105,13 @@ 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.')
@ -175,6 +227,8 @@ 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
/> />
@ -183,14 +237,20 @@ 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>
<input <select
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">
@ -235,14 +295,20 @@ 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>
<input <select
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>

View File

@ -14,8 +14,39 @@ export default function EmailVerifyPage() {
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
@ -166,12 +197,23 @@ 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">
Gib den 6-stelligen Code ein, den wir an {initialEmailSent ? (
{' '} <>
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>

View File

@ -28,19 +28,32 @@ export default function CompanySignContractPage() {
setDate(new Date().toISOString().slice(0,10)) setDate(new Date().toISOString().slice(0,10))
}, []) }, [])
const valid = () => const valid = () => {
companyName.trim().length > 2 && const companyValid = companyName.trim().length >= 3 // Min 3 characters for company name
repName.trim().length > 4 && const repNameValid = repName.trim().length >= 3 // Min 3 characters for representative name
repTitle.trim().length > 1 && const repTitleValid = repTitle.trim().length >= 2 // Min 2 characters for title
location.trim().length > 1 && const locationValid = location.trim().length >= 2 // Min 2 characters for location
agreeContract && const contractChecked = agreeContract
agreeData && const dataChecked = agreeData
confirmSignature const signatureChecked = 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()) {
setError('Bitte alle Pflichtfelder & Bestätigungen ausfüllen.') // Detailed error message to help debug
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
} }
@ -71,9 +84,10 @@ export default function CompanySignContractPage() {
// Create FormData for the existing backend endpoint // Create FormData for the existing backend endpoint
const formData = new FormData() const formData = new FormData()
formData.append('contractData', JSON.stringify(contractData)) formData.append('contractData', JSON.stringify(contractData))
// Create a dummy file since the backend expects one (we'll update backend later) // Create a dummy PDF file since the backend expects one (electronic signature)
const dummyFile = new Blob(['Electronic signature data'], { type: 'text/plain' }) 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'
formData.append('contract', dummyFile, 'electronic_signature.txt') 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`, { const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/upload/contract/company`, {
method: 'POST', method: 'POST',

View File

@ -26,17 +26,28 @@ export default function PersonalSignContractPage() {
setDate(new Date().toISOString().slice(0, 10)) setDate(new Date().toISOString().slice(0, 10))
}, []) }, [])
const valid = () => const valid = () => {
fullName.trim().length > 4 && const nameValid = fullName.trim().length >= 3 // Min 3 characters for name
location.trim().length > 1 && const locationValid = location.trim().length >= 2 // Min 2 characters for location
agreeContract && const contractChecked = agreeContract
agreeData && const dataChecked = agreeData
confirmSignature const signatureChecked = 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()) {
setError('Bitte alle Pflichtfelder & Bestätigungen ausfüllen.') // Detailed error message to help debug
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
} }
@ -65,9 +76,10 @@ export default function PersonalSignContractPage() {
// Create FormData for the existing backend endpoint // Create FormData for the existing backend endpoint
const formData = new FormData() const formData = new FormData()
formData.append('contractData', JSON.stringify(contractData)) formData.append('contractData', JSON.stringify(contractData))
// Create a dummy file since the backend expects one (we'll update backend later) // Create a dummy PDF file since the backend expects one (electronic signature)
const dummyFile = new Blob(['Electronic signature data'], { type: 'text/plain' }) 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'
formData.append('contract', dummyFile, 'electronic_signature.txt') 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`, { const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/upload/contract/personal`, {
method: 'POST', method: 'POST',