'use client' import { useEffect, useMemo, useRef, useState, useCallback } from 'react' import { useRouter } from 'next/navigation' import PageLayout from '../../../components/PageLayout' import useAuthStore from '../../../store/authStore' import { useUserStatus } from '../../../hooks/useUserStatus' import { useToast } from '../../../components/toast/toastComponent' import { useTranslation } from '../../../i18n/useTranslation' import { ChevronDownIcon } from '@heroicons/react/20/solid' import TelephoneInput, { TelephoneInputHandle } from '../../../components/phone/telephoneInput' interface PersonalProfileData { firstName: string lastName: string email: string phone: string dob: string nationality: string street: string postalCode: string city: string country: string accountHolder: string iban: string secondPhone: string emergencyName: string emergencyPhone: string } const NATIONALITY_CODES = [ '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', 'southAfrican', 'other' ] as const const COUNTRY_CODES = [ 'germany', 'austria', 'switzerland', 'italy', 'france', 'spain', 'portugal', 'netherlands', 'belgium', 'poland', 'czechRepublic', 'hungary', 'croatia', 'slovenia', 'slovakia', 'unitedKingdom', 'ireland', 'sweden', 'norway', 'denmark', 'finland', 'russia', 'turkey', 'greece', 'romania', 'bulgaria', 'serbia', 'albania', 'bosniaHerzegovina', 'unitedStates', 'canada', 'brazil', 'argentina', 'mexico', 'china', 'japan', 'india', 'pakistan', 'australia', 'southAfrica', 'other' ] as const const NATIONALITY_VALUE_BY_CODE: Record<(typeof NATIONALITY_CODES)[number], string> = { german: 'German', austrian: 'Austrian', swiss: 'Swiss', italian: 'Italian', french: 'French', spanish: 'Spanish', portuguese: 'Portuguese', dutch: 'Dutch', belgian: 'Belgian', polish: 'Polish', czech: 'Czech', hungarian: 'Hungarian', croatian: 'Croatian', slovenian: 'Slovenian', slovak: 'Slovak', british: 'British', irish: 'Irish', swedish: 'Swedish', norwegian: 'Norwegian', danish: 'Danish', finnish: 'Finnish', russian: 'Russian', turkish: 'Turkish', greek: 'Greek', romanian: 'Romanian', bulgarian: 'Bulgarian', serbian: 'Serbian', albanian: 'Albanian', bosnian: 'Bosnian', american: 'American', canadian: 'Canadian', brazilian: 'Brazilian', argentinian: 'Argentinian', mexican: 'Mexican', chinese: 'Chinese', japanese: 'Japanese', indian: 'Indian', pakistani: 'Pakistani', australian: 'Australian', southAfrican: 'South African', other: 'Other' } const COUNTRY_VALUE_BY_CODE: Record<(typeof COUNTRY_CODES)[number], string> = { germany: 'Germany', austria: 'Austria', switzerland: 'Switzerland', italy: 'Italy', france: 'France', spain: 'Spain', portugal: 'Portugal', netherlands: 'Netherlands', belgium: 'Belgium', poland: 'Poland', czechRepublic: 'Czech Republic', hungary: 'Hungary', croatia: 'Croatia', slovenia: 'Slovenia', slovakia: 'Slovakia', unitedKingdom: 'United Kingdom', ireland: 'Ireland', sweden: 'Sweden', norway: 'Norway', denmark: 'Denmark', finland: 'Finland', russia: 'Russia', turkey: 'Turkey', greece: 'Greece', romania: 'Romania', bulgaria: 'Bulgaria', serbia: 'Serbia', albania: 'Albania', bosniaHerzegovina: 'Bosnia and Herzegovina', unitedStates: 'United States', canada: 'Canada', brazil: 'Brazil', argentina: 'Argentina', mexico: 'Mexico', china: 'China', japan: 'Japan', india: 'India', pakistan: 'Pakistan', australia: 'Australia', southAfrica: 'South Africa', other: 'Other' } const NATIONALITIES = NATIONALITY_CODES.map(code => NATIONALITY_VALUE_BY_CODE[code]) const COUNTRIES = COUNTRY_CODES.map(code => COUNTRY_VALUE_BY_CODE[code]) const normalizeOptionValue = (value: string) => value.toLowerCase().replace(/[^a-z]/g, '') const initialData: PersonalProfileData = { firstName: '', lastName: '', email: '', phone: '', dob: '', nationality: '', street: '', postalCode: '', city: '', country: '', accountHolder: '', iban: '', secondPhone: '', emergencyName: '', emergencyPhone: '' } type SelectOption = { value: string; label: string } function ModernSelect({ label, placeholder, searchPlaceholder = 'Search…', noResults = 'No results', value, onChange, options, }: { label: string placeholder?: string searchPlaceholder?: string noResults?: string value: string onChange: (next: string) => void options: SelectOption[] }) { const { t } = useTranslation(); const [open, setOpen] = useState(false) const [query, setQuery] = useState('') const btnRef = useRef(null) const [pos, setPos] = useState({ left: 16, top: 0, width: 320 }) const resolvedPlaceholder = placeholder ?? t('autofix.ka5bf342b') const selected = useMemo( () => options.find(o => o.value === value) || null, [options, value] ) const filtered = useMemo(() => { const q = query.trim().toLowerCase() if (!q) return options return options.filter(o => o.label.toLowerCase().includes(q)) }, [options, query]) useEffect(() => { if (!open) return const update = () => { const el = btnRef.current if (!el) return const r = el.getBoundingClientRect() const padding = 16 const width = Math.min(r.width, window.innerWidth - padding * 2) const left = Math.max(padding, Math.min(r.left, window.innerWidth - width - padding)) const top = r.bottom + 8 setPos({ left, top, width }) } update() window.addEventListener('resize', update) window.addEventListener('scroll', update, true) return () => { window.removeEventListener('resize', update) window.removeEventListener('scroll', update, true) } }, [open]) // close on escape useEffect(() => { if (!open) return const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') setOpen(false) } window.addEventListener('keydown', onKey) return () => window.removeEventListener('keydown', onKey) }, [open]) return (
{open && ( <> {/* click-away overlay */}
setOpen(false)} aria-hidden /> {/* dropdown (fixed so it “pops out under” even on mobile) */}
setQuery(e.target.value)} placeholder={searchPlaceholder} className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent" autoFocus />
{filtered.length === 0 ? (
{noResults}
) : ( filtered.map(o => { const active = o.value === value return ( ) }) )}
)}
) } export default function PersonalAdditionalInformationPage() { const { t } = useTranslation(); const router = useRouter() const user = useAuthStore(s => s.user) // NEW const isAuthReady = useAuthStore(s => (s as any).isAuthReady) // NEW const { accessToken } = useAuthStore() const { userStatus, loading: statusLoading, refreshStatus } = useUserStatus() const { showToast } = useToast() const phoneRef = useRef(null) const secondPhoneRef = useRef(null) const emergencyPhoneRef = useRef(null) const [form, setForm] = useState(initialData) const [loading, setLoading] = useState(false) const [success, setSuccess] = useState(false) const [error, setError] = useState('') const nationalityOptions = useMemo( () => NATIONALITY_CODES.map(code => ({ value: code, label: t(`quickactionDashboard.additionalInfo.nationalities.${code}`) })), [t] ) const countryOptions = useMemo( () => COUNTRY_CODES.map(code => ({ value: code, label: t(`quickactionDashboard.additionalInfo.countries.${code}`) })), [t] ) // Prefill form if profile already exists useEffect(() => { let abort = false async function loadProfile() { if (!accessToken) return try { const res = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/me`, { method: 'GET', headers: { Authorization: `Bearer ${accessToken}` } }) if (!res.ok) return const data = await res.json().catch(() => null) const profile = data?.profile const user = data?.user if ((!profile && !user) || abort) return const toDateInput = (d?: string) => { if (!d) return '' const dt = new Date(d) if (Number.isNaN(dt.getTime())) return '' return dt.toISOString().split('T')[0] } setForm(prev => ({ ...prev, firstName: user?.firstName || profile?.first_name || prev.firstName, lastName: user?.lastName || profile?.last_name || prev.lastName, email: user?.email || prev.email, phone: user?.phone || profile?.phone || prev.phone, dob: toDateInput(profile?.date_of_birth || profile?.dateOfBirth), nationality: Object.entries(NATIONALITY_VALUE_BY_CODE).find(([, label]) => normalizeOptionValue(label) === normalizeOptionValue(profile?.nationality || ''))?.[0] || prev.nationality, street: profile?.address || prev.street, postalCode: profile?.zip_code || profile?.zipCode || prev.postalCode, city: profile?.city || prev.city, country: Object.entries(COUNTRY_VALUE_BY_CODE).find(([, label]) => normalizeOptionValue(label) === normalizeOptionValue(profile?.country || ''))?.[0] || prev.country, accountHolder: profile?.account_holder_name || profile?.accountHolderName || prev.accountHolder, // Prefer IBAN from users table (data.user.iban), fallback to profile if any iban: (user?.iban ?? profile?.iban ?? prev.iban) as string, secondPhone: profile?.phone_secondary || profile?.phoneSecondary || prev.secondPhone, emergencyName: profile?.emergency_contact_name || profile?.emergencyContactName || prev.emergencyName, emergencyPhone: profile?.emergency_contact_phone || profile?.emergencyContactPhone || prev.emergencyPhone, })) } catch (_) { // ignore prefill errors; user can still fill manually } } loadProfile() return () => { abort = true } }, [accessToken]) const handleChange = (e: React.ChangeEvent) => { const { name, value } = e.target setForm(p => ({ ...p, [name]: value })) setError('') } const handlePhoneInput = (e: React.FormEvent) => { const { name, value } = e.currentTarget 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)[] = [ 'firstName','lastName','email','phone', 'dob','nationality','street','postalCode','city','country','accountHolder','iban' ] for (const k of requiredKeys) { if (!form[k].trim()) { const msg = t('quickactionDashboard.additionalInfo.fillRequiredFields') setError(msg) showToast({ variant: 'error', title: t('quickactionDashboard.uploadId.missingInfoTitle'), message: msg, }) return false } } // Date of birth validation if (!validateDateOfBirth(form.dob)) { const msg = t('quickactionDashboard.additionalInfo.invalidDateOfBirthMessage') setError(msg) showToast({ variant: 'error', title: t('quickactionDashboard.additionalInfo.invalidDateOfBirthTitle'), message: msg, }) return false } // very loose IBAN check if (!/^([A-Z]{2}\d{2}[A-Z0-9]{10,30})$/i.test(form.iban.replace(/\s+/g,''))) { const msg = t('quickactionDashboard.additionalInfo.invalidIbanMessage') setError(msg) showToast({ variant: 'error', title: t('quickactionDashboard.additionalInfo.invalidIbanTitle'), message: msg, }) return false } const phoneApi = phoneRef.current const dialCode = phoneApi?.getDialCode?.() const intlNumber = phoneApi?.getNumber() || '' const valid = phoneApi?.isValid() ?? false if (!dialCode) { const msg = t('quickactionDashboard.additionalInfo.missingCountryCodeMessage') setError(msg) showToast({ variant: 'error', title: t('quickactionDashboard.additionalInfo.missingCountryCodeTitle'), message: msg, }) return false } if (!intlNumber) { const msg = t('quickactionDashboard.additionalInfo.phoneNumberMissingMessage') setError(msg) showToast({ variant: 'error', title: t('quickactionDashboard.additionalInfo.missingPhoneNumberTitle'), message: msg, }) return false } if (!valid) { const msg = t('quickactionDashboard.additionalInfo.validPhoneNumberMessage') setError(msg) showToast({ variant: 'error', title: t('quickactionDashboard.additionalInfo.invalidPhoneNumberTitle'), message: msg, }) return false } const optionalSecond = form.secondPhone.trim() if (optionalSecond) { const secondApi = secondPhoneRef.current const ok = secondApi?.isValid?.() ?? false if (!ok) { const msg = t('quickactionDashboard.additionalInfo.validSecondPhoneNumberMessage') setError(msg) showToast({ variant: 'error', title: t('quickactionDashboard.additionalInfo.invalidPhoneNumberTitle'), message: msg, }) return false } } const optionalEmergency = form.emergencyPhone.trim() if (optionalEmergency) { const emergencyApi = emergencyPhoneRef.current const ok = emergencyApi?.isValid?.() ?? false if (!ok) { const msg = t('quickactionDashboard.additionalInfo.validEmergencyPhoneNumberMessage') setError(msg) showToast({ variant: 'error', title: t('quickactionDashboard.additionalInfo.invalidPhoneNumberTitle'), message: msg, }) return false } } setError('') return true } const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() if (loading || success) return if (!validate()) return if (!accessToken) { const msg = t('quickactionDashboard.additionalInfo.authErrorMessage') setError(msg) showToast({ variant: 'error', title: t('quickactionDashboard.additionalInfo.authErrorTitle'), message: msg, }) return } setLoading(true) try { const normalizedPhone = phoneRef.current?.getNumber() || form.phone const normalizedSecondPhone = secondPhoneRef.current?.getNumber() || form.secondPhone const normalizedEmergencyPhone = emergencyPhoneRef.current?.getNumber() || form.emergencyPhone // Prepare data for backend with correct field names const profileData = { firstName: form.firstName, lastName: form.lastName, phone: normalizedPhone, dateOfBirth: form.dob, nationality: NATIONALITY_VALUE_BY_CODE[form.nationality as keyof typeof NATIONALITY_VALUE_BY_CODE] || form.nationality, address: form.street, // Backend expects 'address', not nested object zip_code: form.postalCode, // Backend expects 'zip_code' city: form.city, country: COUNTRY_VALUE_BY_CODE[form.country as keyof typeof COUNTRY_VALUE_BY_CODE] || form.country, phoneSecondary: normalizedSecondPhone || null, // Backend expects 'phoneSecondary' emergencyContactName: form.emergencyName || null, emergencyContactPhone: normalizedEmergencyPhone || 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: t('autofix.k481c2be7') })) throw new Error(errorData.message || 'Save failed') } setSuccess(true) showToast({ variant: 'success', title: t('quickactionDashboard.additionalInfo.additionalInfoSuccessTitle'), message: t('quickactionDashboard.additionalInfo.personalSuccessMessage'), }) // Refresh user status to update profile completion state suppressAutoRedirectRef.current = true await refreshStatus() // Redirect back to tutorial modal after short delay setTimeout(() => { smoothReplace('/quickaction-dashboard?tutorial=true') }, 1500) } catch (error: any) { console.error('Personal profile save error:', error) const msg = error.message || t('quickactionDashboard.additionalInfo.saveFailedMessage') setError(msg) showToast({ variant: 'error', title: t('quickactionDashboard.additionalInfo.saveFailedTitle'), message: msg, }) } finally { setLoading(false) } } const setField = (name: keyof PersonalProfileData, value: string) => { setForm(p => ({ ...p, [name]: value })) setError('') } // NEW: smooth redirect const [redirectTo, setRedirectTo] = useState(null) const redirectOnceRef = useRef(false) const suppressAutoRedirectRef = useRef(false) const smoothReplace = useCallback((to: string) => { if (redirectOnceRef.current) return redirectOnceRef.current = true setRedirectTo(to) window.setTimeout(() => router.replace(to), 200) }, [router]) // NEW: hard block if step already done OR all steps done useEffect(() => { if (statusLoading || !userStatus) return if (suppressAutoRedirectRef.current) return const allDone = !!userStatus.email_verified && !!userStatus.documents_uploaded && !!userStatus.profile_completed && !!userStatus.contract_signed if (allDone) { smoothReplace('/dashboard') // CHANGED } else if (userStatus.profile_completed) { smoothReplace('/quickaction-dashboard') // CHANGED } }, [statusLoading, userStatus, smoothReplace]) // NEW: must be logged in useEffect(() => { if (!isAuthReady) return if (!user || !accessToken) smoothReplace('/login') }, [isAuthReady, user, accessToken, smoothReplace]) return ( {/* NEW: smooth redirect overlay */} {redirectTo && (
{t('quickactionDashboard.redirecting')}
{t('quickactionDashboard.pleaseWait')}
)}
{/* Animated background (same as dashboard) */}
{/* Soft gradient blobs */}
{/* Subtle radial highlight */}

{t('quickactionDashboard.additionalInfo.title')}

{/* Personal Information */}

{t('quickactionDashboard.additionalInfo.personalInformation')}

setField('nationality', v)} options={NATIONALITIES.map(n => ({ value: n, label: n }))} />
setField('country', v)} options={COUNTRIES.map(c => ({ value: c, label: c }))} />

{/* Bank Details */}

{t('quickactionDashboard.additionalInfo.bankDetails')}


{/* Additional Information */}

{t('quickactionDashboard.additionalInfo.additionalInformation')}

{error && (
{error}
)} {success && (
{t('quickactionDashboard.additionalInfo.dataSavedRedirecting')}
)}
) }