Compare commits

..

5 Commits

5 changed files with 189 additions and 38 deletions

View File

@ -21,6 +21,16 @@ interface CompanyProfileData {
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 = {
companyName: '',
vatNumber: '',
@ -46,7 +56,7 @@ export default function CompanyAdditionalInformationPage() {
const [success, setSuccess] = useState(false)
const [error, setError] = useState('')
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target
setForm(p => ({ ...p, [name]: value }))
setError('')
@ -231,13 +241,20 @@ export default function CompanyAdditionalInformationPage() {
<label className="block text-sm font-medium text-gray-700 mb-1">
Country *
</label>
<input
<select
name="country"
value={form.country}
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"
required
/>
>
<option value="">Select country...</option>
{COUNTRIES.map(country => (
<option key={country} value={country}>
{country}
</option>
))}
</select>
</div>
</div>
</section>

View File

@ -20,6 +20,26 @@ interface PersonalProfileData {
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 = {
dob: '',
nationality: '',
@ -44,12 +64,37 @@ export default function PersonalAdditionalInformationPage() {
const [success, setSuccess] = useState(false)
const [error, setError] = useState('')
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target
setForm(p => ({ ...p, [name]: value }))
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 requiredKeys: (keyof PersonalProfileData)[] = [
'dob','nationality','street','postalCode','city','country','accountHolder','iban'
@ -60,6 +105,13 @@ export default function PersonalAdditionalInformationPage() {
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
if (!/^([A-Z]{2}\d{2}[A-Z0-9]{10,30})$/i.test(form.iban.replace(/\s+/g,''))) {
setError('Ungültige IBAN.')
@ -175,6 +227,8 @@ export default function PersonalAdditionalInformationPage() {
name="dob"
value={form.dob}
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"
required
/>
@ -183,14 +237,20 @@ export default function PersonalAdditionalInformationPage() {
<label className="block text-sm font-medium text-gray-700 mb-1">
Nationality *
</label>
<input
<select
name="nationality"
value={form.nationality}
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"
required
/>
>
<option value="">Select nationality...</option>
{NATIONALITIES.map(nationality => (
<option key={nationality} value={nationality}>
{nationality}
</option>
))}
</select>
</div>
<div className="sm:col-span-2 lg:col-span-3">
<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">
Country *
</label>
<input
<select
name="country"
value={form.country}
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"
required
/>
>
<option value="">Select country...</option>
{COUNTRIES.map(country => (
<option key={country} value={country}>
{country}
</option>
))}
</select>
</div>
</div>
</section>

View File

@ -14,8 +14,39 @@ export default function EmailVerifyPage() {
const [error, setError] = useState('')
const [success, setSuccess] = useState(false)
const [resendCooldown, setResendCooldown] = useState(0)
const [initialEmailSent, setInitialEmailSent] = useState(false)
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
useEffect(() => {
if (!resendCooldown) return
@ -166,12 +197,23 @@ export default function EmailVerifyPage() {
E-Mail verifizieren
</h1>
<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">
{user?.email || 'deine E-Mail'}
</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>
</div>

View File

@ -28,19 +28,32 @@ export default function CompanySignContractPage() {
setDate(new Date().toISOString().slice(0,10))
}, [])
const valid = () =>
companyName.trim().length > 2 &&
repName.trim().length > 4 &&
repTitle.trim().length > 1 &&
location.trim().length > 1 &&
agreeContract &&
agreeData &&
confirmSignature
const valid = () => {
const companyValid = companyName.trim().length >= 3 // Min 3 characters for company name
const repNameValid = repName.trim().length >= 3 // Min 3 characters for representative name
const repTitleValid = repTitle.trim().length >= 2 // Min 2 characters for title
const locationValid = location.trim().length >= 2 // Min 2 characters for location
const contractChecked = agreeContract
const dataChecked = agreeData
const signatureChecked = confirmSignature
return companyValid && repNameValid && repTitleValid && locationValid && contractChecked && dataChecked && signatureChecked
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
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
}
@ -71,9 +84,10 @@ export default function CompanySignContractPage() {
// Create FormData for the existing backend endpoint
const formData = new FormData()
formData.append('contractData', JSON.stringify(contractData))
// Create a dummy file since the backend expects one (we'll update backend later)
const dummyFile = new Blob(['Electronic signature data'], { type: 'text/plain' })
formData.append('contract', dummyFile, 'electronic_signature.txt')
// 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',

View File

@ -26,17 +26,28 @@ export default function PersonalSignContractPage() {
setDate(new Date().toISOString().slice(0, 10))
}, [])
const valid = () =>
fullName.trim().length > 4 &&
location.trim().length > 1 &&
agreeContract &&
agreeData &&
confirmSignature
const valid = () => {
const nameValid = fullName.trim().length >= 3 // Min 3 characters for name
const locationValid = location.trim().length >= 2 // Min 2 characters for location
const contractChecked = agreeContract
const dataChecked = agreeData
const signatureChecked = confirmSignature
return nameValid && locationValid && contractChecked && dataChecked && signatureChecked
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
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
}
@ -65,9 +76,10 @@ export default function PersonalSignContractPage() {
// Create FormData for the existing backend endpoint
const formData = new FormData()
formData.append('contractData', JSON.stringify(contractData))
// Create a dummy file since the backend expects one (we'll update backend later)
const dummyFile = new Blob(['Electronic signature data'], { type: 'text/plain' })
formData.append('contract', dummyFile, 'electronic_signature.txt')
// 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',