profit-planet-frontend/src/app/quickaction-dashboard/register-additional-information/company/page.tsx
2026-01-14 18:15:48 +01:00

585 lines
22 KiB
TypeScript

'use client'
import { useState, useMemo, useRef, useEffect, 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 { ChevronDownIcon } from '@heroicons/react/20/solid' // NEW
interface CompanyProfileData {
companyName: string
vatNumber: string
street: string
postalCode: string
city: string
country: string
accountHolder: string
iban: string
bic: string
secondPhone: string
emergencyName: 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 = {
companyName: '',
vatNumber: '',
street: '',
postalCode: '',
city: '',
country: '',
accountHolder: '',
iban: '',
bic: '',
secondPhone: '',
emergencyName: '',
emergencyPhone: ''
}
function ModernSelect({
label,
placeholder = 'Select…',
value,
onChange,
options,
}: {
label: string
placeholder?: string
value: string
onChange: (next: string) => void
options: { value: string; label: string }[]
}) {
const [open, setOpen] = useState(false)
const [query, setQuery] = useState('')
const btnRef = useRef<HTMLButtonElement | null>(null)
const [pos, setPos] = useState({ left: 16, top: 0, width: 320 })
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])
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 (
<div className="relative">
<label className="block text-sm font-medium text-gray-700 mb-1">{label} *</label>
<button
ref={btnRef}
type="button"
onClick={() => setOpen(v => !v)}
className="w-full rounded-lg border border-gray-300 bg-white/70 px-3 py-2 text-sm text-left
shadow-sm hover:border-gray-400
focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent
inline-flex items-center justify-between gap-3"
aria-haspopup="listbox"
aria-expanded={open}
>
<span className={selected ? 'text-gray-900' : 'text-gray-500'}>
{selected ? selected.label : placeholder}
</span>
<ChevronDownIcon className={`h-5 w-5 text-gray-500 transition-transform ${open ? 'rotate-180' : ''}`} />
</button>
{open && (
<>
<div className="fixed inset-0 z-[90]" onClick={() => setOpen(false)} aria-hidden />
<div
className="fixed z-[100] overflow-hidden rounded-xl border border-gray-200 bg-white shadow-2xl"
style={{ left: pos.left, top: pos.top, width: pos.width }}
>
<div className="p-2 border-b border-gray-100">
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search…"
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
/>
</div>
<div className="max-h-[42vh] overflow-auto p-1">
{filtered.length === 0 ? (
<div className="px-3 py-2 text-sm text-gray-500">No results</div>
) : (
filtered.map(o => {
const active = o.value === value
return (
<button
key={o.value}
type="button"
onClick={() => { onChange(o.value); setQuery(''); setOpen(false) }}
className={`w-full text-left px-3 py-2 rounded-lg text-sm transition-colors
${active ? 'bg-[#8D6B1D]/10 text-[#7A5E1A] font-semibold' : 'text-gray-800 hover:bg-gray-50'}`}
role="option"
aria-selected={active}
>
{o.label}
</button>
)
})
)}
</div>
</div>
</>
)}
</div>
)
}
export default function CompanyAdditionalInformationPage() {
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 [form, setForm] = useState(init)
const [loading, setLoading] = useState(false)
const [success, setSuccess] = useState(false)
const [error, setError] = useState('')
// NEW: smooth redirect
const [redirectTo, setRedirectTo] = useState<string | null>(null)
const redirectOnceRef = 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
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])
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target
setForm(p => ({ ...p, [name]: value }))
setError('')
}
const validate = () => {
const required: (keyof CompanyProfileData)[] = [
'companyName','vatNumber','street','postalCode','city','country','accountHolder','iban'
]
for (const k of required) {
if (!form[k].trim()) {
const msg = 'Bitte alle Pflichtfelder ausfüllen.'
setError(msg)
showToast({
variant: 'error',
title: 'Missing information',
message: msg,
})
return false
}
}
if (!/^([A-Z]{2}\d{2}[A-Z0-9]{10,30})$/i.test(form.iban.replace(/\s+/g,''))) {
const msg = 'Ungültige IBAN.'
setError(msg)
showToast({
variant: 'error',
title: 'Invalid IBAN',
message: msg,
})
return false
}
setError('')
return true
}
const submit = async (e: React.FormEvent) => {
e.preventDefault()
if (loading || success) return
if (!validate()) return
if (!accessToken) {
const msg = 'Not authenticated. Please log in again.'
setError(msg)
showToast({
variant: 'error',
title: 'Authentication error',
message: msg,
})
return
}
setLoading(true)
try {
// Prepare data for backend with correct field names
const profileData = {
address: form.street, // Backend expects 'address', not nested object
zip_code: form.postalCode, // Backend expects 'zip_code'
city: form.city,
country: form.country,
registrationNumber: form.vatNumber, // Map VAT number to registration number
businessType: 'company', // Default business type
branch: null, // Not collected in form, set to null
numberOfEmployees: null, // Not collected in form, set to null
accountHolderName: form.accountHolder, // Backend expects 'accountHolderName'
iban: form.iban.replace(/\s+/g, '') // Remove spaces from IBAN
}
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/profile/company/complete`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(profileData)
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({ message: 'Save failed' }))
throw new Error(errorData.message || 'Save failed')
}
setSuccess(true)
showToast({
variant: 'success',
title: 'Profile saved',
message: 'Your company profile has been saved successfully.',
})
// Refresh user status to update profile completion state
await refreshStatus()
// Redirect to next step after short delay
setTimeout(() => {
// Check if we came from tutorial
const urlParams = new URLSearchParams(window.location.search)
const fromTutorial = urlParams.get('tutorial') === 'true'
if (fromTutorial) {
router.push('/quickaction-dashboard?tutorial=true')
} else {
router.push('/quickaction-dashboard/register-sign-contract/company')
}
}, 1500)
} catch (error: any) {
console.error('Company profile save error:', error)
const msg = error.message || 'Speichern fehlgeschlagen.'
setError(msg)
showToast({
variant: 'error',
title: 'Save failed',
message: msg,
})
} finally {
setLoading(false)
}
}
const setField = (name: keyof CompanyProfileData, value: string) => {
setForm(p => ({ ...p, [name]: value }))
setError('')
}
return (
<PageLayout>
{/* NEW: smooth redirect overlay */}
{redirectTo && (
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-white/70 backdrop-blur-sm transition-opacity duration-200 opacity-100">
<div className="rounded-xl bg-white px-5 py-4 shadow ring-1 ring-black/5">
<div className="text-sm font-medium text-gray-900">Redirecting</div>
<div className="mt-1 text-xs text-gray-600">Please wait</div>
</div>
</div>
)}
<div className="relative min-h-screen overflow-hidden bg-slate-50">
{/* Animated background (same as dashboard) */}
<div className="pointer-events-none absolute inset-0 z-0">
{/* Soft gradient blobs */}
<div className="absolute -top-32 -left-20 h-80 w-80 rounded-full bg-blue-400/25 blur-3xl animate-pulse" />
<div className="absolute top-1/4 -right-24 h-96 w-96 rounded-full bg-emerald-400/20 blur-3xl animate-pulse" />
<div className="absolute bottom-[-6rem] left-1/4 h-96 w-96 rounded-full bg-indigo-400/20 blur-3xl animate-pulse" />
{/* Subtle radial highlight */}
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(59,130,246,0.14),transparent_60%)]" />
</div>
<main className="relative z-10 flex flex-col flex-1 w-full px-5 sm:px-8 lg:px-12 py-12">
<form
onSubmit={submit}
className="relative max-w-6xl w-full mx-auto bg-white rounded-2xl shadow-xl ring-1 ring-black/10"
>
<div className="px-6 py-8 sm:px-10 lg:px-16">
<h1 className="text-center text-xl sm:text-2xl font-semibold text-[#0F172A] mb-6">
Complete Company Profile
</h1>
{/* Company Details */}
<section>
<h2 className="text-sm font-semibold text-[#0F2460] mb-4">
Company Details
</h2>
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
<div className="sm:col-span-2 lg:col-span-3">
<label className="block text-sm font-medium text-gray-700 mb-1">
Company Name *
</label>
<input
name="companyName"
value={form.companyName}
onChange={handleChange}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
VAT / Reg No. *
</label>
<input
name="vatNumber"
value={form.vatNumber}
onChange={handleChange}
placeholder="e.g. DE123456789"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm uppercase focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
required
/>
</div>
<div className="sm:col-span-2 lg:col-span-3">
<label className="block text-sm font-medium text-gray-700 mb-1">
Street & Number *
</label>
<input
name="street"
value={form.street}
onChange={handleChange}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Postal Code *
</label>
<input
name="postalCode"
value={form.postalCode}
onChange={handleChange}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
City *
</label>
<input
name="city"
value={form.city}
onChange={handleChange}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
required
/>
</div>
<div>
<ModernSelect
label="Country"
placeholder="Select country..."
value={form.country}
onChange={(v) => setField('country', v)}
options={COUNTRIES.map(c => ({ value: c, label: c }))}
/>
</div>
</div>
</section>
<hr className="my-8 border-gray-200" />
{/* Bank Details */}
<section>
<h2 className="text-sm font-semibold text-[#0F2460] mb-4">
Bank Details
</h2>
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
<div className="sm:col-span-2 lg:col-span-3">
<label className="block text-sm font-medium text-gray-700 mb-1">
Account Holder *
</label>
<input
name="accountHolder"
value={form.accountHolder}
onChange={handleChange}
placeholder="Company / Holder name"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
required
/>
</div>
<div className="sm:col-span-2 lg:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
IBAN *
</label>
<input
name="iban"
value={form.iban}
onChange={handleChange}
placeholder="DE89 3704 0044 0532 0130 00"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm tracking-wide uppercase focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
BIC (optional)
</label>
<input
name="bic"
value={form.bic}
onChange={handleChange}
placeholder="GENODEF1XXX"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm uppercase focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
/>
</div>
</div>
</section>
<hr className="my-8 border-gray-200" />
{/* Additional Information */}
<section>
<h2 className="text-sm font-semibold text-[#0F2460] mb-4">
Additional Information
</h2>
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
<div className="sm:col-span-2 lg:col-span-3">
<label className="block text-sm font-medium text-gray-700 mb-1">
Second Phone (optional)
</label>
<input
name="secondPhone"
value={form.secondPhone}
onChange={handleChange}
placeholder="+49 123 456 7890"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Emergency Contact Name
</label>
<input
name="emergencyName"
value={form.emergencyName}
onChange={handleChange}
placeholder="Contact name"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Emergency Contact Phone
</label>
<input
name="emergencyPhone"
value={form.emergencyPhone}
onChange={handleChange}
placeholder="+49 123 456 7890"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
/>
</div>
<div className="hidden lg:block" />
</div>
</section>
{error && (
<div className="mt-6 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-xs text-red-600">
{error}
</div>
)}
{success && (
<div className="mt-6 rounded-md border border-green-300 bg-green-50 px-4 py-3 text-xs text-green-700">
Data saved. Redirecting shortly
</div>
)}
<div className="mt-10 flex items-center justify-between">
<button
type="button"
onClick={() => router.push('/quickaction-dashboard')}
className="inline-flex items-center rounded-md border border-[#8D6B1D]/40 px-4 py-2 text-sm font-semibold text-[#8D6B1D] bg-white hover:bg-[#8D6B1D]/10"
>
Back to Dashboard
</button>
<button
type="submit"
disabled={loading || success}
className="inline-flex items-center rounded-md bg-[#8D6B1D] px-6 py-2.5 text-sm font-semibold text-white shadow hover:bg-[#7A5E1A] focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:ring-offset-2 disabled:bg-gray-400 disabled:cursor-not-allowed"
>
{loading ? 'Speichern…' : success ? 'Gespeichert' : 'Save & Continue'}
</button>
</div>
</div>
</form>
</main>
</div>
</PageLayout>
)
}