update account creation fileds

This commit is contained in:
root
2026-05-25 14:05:37 -04:00
parent 813eb5404c
commit 914ae839a7
10 changed files with 448 additions and 50 deletions
@@ -8,7 +8,7 @@ import { useDashboardI18n } from '@/components/I18nProvider'
import { apiFetch } from '@/lib/api'
import PublicShell from '@/components/layout/PublicShell'
import { marketplaceUrl } from '@/lib/urls'
import { BilingualField, BilingualInput, bilingualPrimary, emptyBilingual } from '@/components/ui/BilingualInput'
import { BilingualField, BilingualInput, emptyBilingual } from '@/components/ui/BilingualInput'
type SignupForm = {
firstName: BilingualField
@@ -18,15 +18,30 @@ type SignupForm = {
confirmPassword: string
preferredLanguage: 'en' | 'fr' | 'ar' | ''
companyName: BilingualField
legalName: string
legalForm: string
registrationNumber: string
iceNumber: string
taxId: string
operatingLicenseNumber: string
operatingLicenseIssuedAt: string
operatingLicenseIssuedBy: string
streetAddress: BilingualField
city: BilingualField
country: BilingualField
zipCode: string
companyPhone: string
companyEmail: string
fax: string
yearsActive: string
representativeName: string
representativeTitle: string
responsibleName: string
responsibleRole: string
responsibleIdentityNumber: string
responsibleQualification: string
responsiblePhone: string
responsibleEmail: string
plan: 'STARTER' | 'GROWTH' | 'PRO'
billingPeriod: 'MONTHLY' | 'ANNUAL'
currency: 'MAD'
@@ -74,15 +89,30 @@ export default function SignUpPage() {
confirmPassword: '',
preferredLanguage: '',
companyName: emptyBilingual(),
legalName: '',
legalForm: '',
registrationNumber: '',
iceNumber: '',
taxId: '',
operatingLicenseNumber: '',
operatingLicenseIssuedAt: '',
operatingLicenseIssuedBy: '',
streetAddress: emptyBilingual(),
city: emptyBilingual(),
country: emptyBilingual(),
zipCode: '',
companyPhone: '',
companyEmail: '',
fax: '',
yearsActive: '',
representativeName: '',
representativeTitle: '',
responsibleName: '',
responsibleRole: '',
responsibleIdentityNumber: '',
responsibleQualification: '',
responsiblePhone: '',
responsibleEmail: '',
plan: initialPlan,
billingPeriod: initialBillingPeriod,
currency: initialCurrency,
@@ -107,19 +137,39 @@ export default function SignUpPage() {
firstName: 'Manager/Owner first name',
lastName: 'Manager/Owner last name',
ownerEmail: 'Manager/Owner email',
emailReuseHint: 'You can reuse the same email for the owner, company, and responsible person.',
password: 'Password',
confirmPassword: 'Confirm password',
passwordHint: 'Use at least 8 characters for your company owner account.',
companyName: 'Company Name',
companyName: 'Commercial name',
legalName: 'Legal company name',
legalForm: 'Legal form',
registrationNumber: 'Commercial Registry (RC)',
iceNumber: 'ICE number',
taxId: 'Fiscal ID (IF)',
operatingLicenseNumber: 'Operating license number',
operatingLicenseIssuedAt: 'License issue date',
operatingLicenseIssuedBy: 'Issuing authority',
streetAddress: 'Street Address',
city: 'City',
country: 'Country',
zipCode: 'Zip code',
companyPhone: 'Phone',
companyEmail: 'Company contact email',
fax: 'Fax (optional)',
yearsActive: 'Years active',
representativeName: 'Legal representative name',
representativeTitle: 'Legal representative title',
responsibleName: 'Responsible person name',
responsibleRole: 'Responsible person role',
responsibleIdentityNumber: 'Responsible person ID number',
responsibleQualification: 'Qualification / diploma / experience',
responsiblePhone: 'Responsible person phone',
responsibleEmail: 'Responsible person email',
identitySection: 'Legal identity',
contactSection: 'Company contact',
representativeSection: 'Legal representative',
responsibleSection: 'Responsible person',
continue: 'Continue',
back: 'Back',
working: 'Working…',
@@ -131,7 +181,10 @@ export default function SignUpPage() {
reviewWorkspace: 'Workspace',
reviewPlan: 'Plan',
reviewOwnerEmail: 'Owner email',
reviewCompanyEmail: 'Company email',
reviewPhone: 'Phone',
reviewLegalName: 'Legal name',
reviewRegistration: 'RC / ICE / IF',
invalidPassword: 'Choose a password with at least 8 characters.',
passwordMismatch: 'Passwords do not match.',
missingRequired: 'Fill in all required fields.',
@@ -186,19 +239,39 @@ export default function SignUpPage() {
firstName: 'Prénom du gérant/propriétaire',
lastName: 'Nom du gérant/propriétaire',
ownerEmail: 'E-mail du gérant/propriétaire',
emailReuseHint: 'Vous pouvez utiliser le même e-mail pour le propriétaire, lentreprise et le responsable.',
password: 'Mot de passe',
confirmPassword: 'Confirmer le mot de passe',
passwordHint: 'Utilisez au moins 8 caractères pour le compte propriétaire.',
companyName: 'Nom de lentreprise',
companyName: 'Nom commercial',
legalName: 'Raison sociale',
legalForm: 'Forme juridique',
registrationNumber: 'Registre du commerce (RC)',
iceNumber: 'Numéro ICE',
taxId: 'Identifiant fiscal (IF)',
operatingLicenseNumber: 'Numéro dagrément',
operatingLicenseIssuedAt: 'Date de délivrance',
operatingLicenseIssuedBy: 'Autorité délivrante',
streetAddress: 'Adresse',
city: 'Ville',
country: 'Pays',
zipCode: 'Code postal',
companyPhone: 'Téléphone',
companyEmail: 'E-mail de contact de lentreprise',
fax: 'Fax (optionnel)',
yearsActive: 'Années dactivité',
representativeName: 'Nom et prénom du représentant',
representativeTitle: 'Fonction du représentant',
responsibleName: 'Nom et prénom du responsable',
responsibleRole: 'Qualité du responsable',
responsibleIdentityNumber: 'N° de pièce didentité',
responsibleQualification: 'Qualification / diplôme / expérience',
responsiblePhone: 'Téléphone du responsable',
responsibleEmail: 'E-mail du responsable',
identitySection: 'Identité juridique',
contactSection: 'Coordonnées de lentreprise',
representativeSection: 'Représentant légal',
responsibleSection: 'Responsable désigné',
continue: 'Continuer',
back: 'Retour',
working: 'Traitement…',
@@ -210,7 +283,10 @@ export default function SignUpPage() {
reviewWorkspace: 'Espace',
reviewPlan: 'Formule',
reviewOwnerEmail: 'E-mail propriétaire',
reviewCompanyEmail: 'E-mail entreprise',
reviewPhone: 'Téléphone',
reviewLegalName: 'Raison sociale',
reviewRegistration: 'RC / ICE / IF',
invalidPassword: 'Choisissez un mot de passe dau moins 8 caractères.',
passwordMismatch: 'Les mots de passe ne correspondent pas.',
missingRequired: 'Renseignez tous les champs obligatoires.',
@@ -265,19 +341,39 @@ export default function SignUpPage() {
firstName: 'الاسم الأول للمدير/المالك',
lastName: 'اسم العائلة للمدير/المالك',
ownerEmail: 'بريد المدير/المالك الإلكتروني',
emailReuseHint: 'يمكنك استخدام نفس البريد الإلكتروني للمالك والشركة والمسؤول.',
password: 'كلمة المرور',
confirmPassword: 'تأكيد كلمة المرور',
passwordHint: 'استخدم 8 أحرف على الأقل لحساب مالك الشركة.',
companyName: 'اسم الشركة',
companyName: 'الاسم التجاري',
legalName: 'الاسم القانوني للشركة',
legalForm: 'الشكل القانوني',
registrationNumber: 'السجل التجاري (RC)',
iceNumber: 'رقم ICE',
taxId: 'المعرف الضريبي (IF)',
operatingLicenseNumber: 'رقم الترخيص',
operatingLicenseIssuedAt: 'تاريخ إصدار الترخيص',
operatingLicenseIssuedBy: 'الجهة المانحة',
streetAddress: 'عنوان الشارع',
city: 'المدينة',
country: 'الدولة',
zipCode: 'الرمز البريدي',
companyPhone: 'الهاتف',
companyEmail: 'بريد الشركة للتواصل',
fax: 'الفاكس (اختياري)',
yearsActive: 'سنوات النشاط',
representativeName: 'اسم الممثل القانوني',
representativeTitle: 'صفة الممثل القانوني',
responsibleName: 'اسم المسؤول',
responsibleRole: 'صفة المسؤول',
responsibleIdentityNumber: 'رقم وثيقة الهوية',
responsibleQualification: 'المؤهل / الشهادة / الخبرة',
responsiblePhone: 'هاتف المسؤول',
responsibleEmail: 'بريد المسؤول الإلكتروني',
identitySection: 'الهوية القانونية',
contactSection: 'بيانات الشركة',
representativeSection: 'الممثل القانوني',
responsibleSection: 'الشخص المسؤول',
continue: 'متابعة',
back: 'رجوع',
working: 'جارٍ التنفيذ…',
@@ -289,7 +385,10 @@ export default function SignUpPage() {
reviewWorkspace: 'المساحة',
reviewPlan: 'الخطة',
reviewOwnerEmail: 'بريد المالك',
reviewCompanyEmail: 'بريد الشركة',
reviewPhone: 'الهاتف',
reviewLegalName: 'الاسم القانوني',
reviewRegistration: 'RC / ICE / IF',
invalidPassword: 'اختر كلمة مرور من 8 أحرف على الأقل.',
passwordMismatch: 'كلمتا المرور غير متطابقتين.',
missingRequired: 'أكمل جميع الحقول الإلزامية.',
@@ -335,10 +434,25 @@ export default function SignUpPage() {
setError(dict.missingLanguage)
return
}
const ownerEmail = normalizeEmail(form.email)
setForm((current) => ({
...current,
companyEmail: current.companyEmail || ownerEmail,
responsibleEmail: current.responsibleEmail || ownerEmail,
}))
setError(null)
setStep(2)
}
function handleStep2Continue() {
if (!hasRequiredPartnerFields(form)) {
setError(dict.missingRequired)
return
}
setError(null)
setStep(3)
}
async function submitSignup() {
if (form.password.length < 8) {
setError(dict.invalidPassword)
@@ -370,9 +484,15 @@ export default function SignUpPage() {
password: form.password,
preferredLanguage: form.preferredLanguage || 'en',
companyName: form.companyName.fr || form.companyName.ar,
legalName: form.legalName,
companyNameAr: form.companyName.ar || undefined,
legalForm: form.legalForm,
registrationNumber: form.registrationNumber,
iceNumber: form.iceNumber,
taxId: form.taxId,
operatingLicenseNumber: form.operatingLicenseNumber,
operatingLicenseIssuedAt: form.operatingLicenseIssuedAt,
operatingLicenseIssuedBy: form.operatingLicenseIssuedBy,
streetAddress: form.streetAddress.fr || form.streetAddress.ar,
streetAddressAr: form.streetAddress.ar || undefined,
city: form.city.fr || form.city.ar,
@@ -381,8 +501,17 @@ export default function SignUpPage() {
countryAr: form.country.ar || undefined,
zipCode: form.zipCode,
companyPhone: form.companyPhone,
companyEmail: form.companyEmail,
fax: form.fax || undefined,
yearsActive: form.yearsActive,
representativeName: form.representativeName || undefined,
representativeTitle: form.representativeTitle || undefined,
responsibleName: form.responsibleName,
responsibleRole: form.responsibleRole,
responsibleIdentityNumber: form.responsibleIdentityNumber,
responsibleQualification: form.responsibleQualification || undefined,
responsiblePhone: form.responsiblePhone,
responsibleEmail: form.responsibleEmail,
plan: form.plan,
billingPeriod: form.billingPeriod,
currency: form.currency,
@@ -483,6 +612,7 @@ export default function SignUpPage() {
<BilingualInput label={dict.firstName} required maxLength={80} value={form.firstName} onChange={(value) => setForm((current) => ({ ...current, firstName: value }))} />
<BilingualInput label={dict.lastName} required maxLength={80} value={form.lastName} onChange={(current) => setForm((f) => ({ ...f, lastName: current }))} />
<LabeledInput label={dict.ownerEmail} required type="email" maxLength={254} value={form.email} onChange={(value) => setForm((current) => ({ ...current, email: normalizeEmail(value) }))} />
<p className="text-xs text-slate-500">{dict.emailReuseHint}</p>
<div className="grid gap-4 sm:grid-cols-2">
<LabeledInput label={dict.password} required type="password" maxLength={128} value={form.password} onChange={(value) => setForm((current) => ({ ...current, password: value }))} />
<LabeledInput label={dict.confirmPassword} required type="password" maxLength={128} value={form.confirmPassword} onChange={(value) => setForm((current) => ({ ...current, confirmPassword: value }))} />
@@ -522,12 +652,31 @@ export default function SignUpPage() {
<div className="space-y-5">
<SectionIntro title={dict.partnerTitle} body={dict.partnerBody} />
<div className="space-y-4">
<FormSubsection title={dict.identitySection} />
<BilingualInput label={dict.companyName} required maxLength={120} value={form.companyName} onChange={(value) => setForm((current) => ({ ...current, companyName: value }))} />
<LabeledInput label={dict.legalName} required maxLength={160} value={form.legalName} onChange={(value) => setForm((current) => ({ ...current, legalName: normalizeTrimmed(value) }))} />
<LabeledSelect label={dict.legalForm} required value={form.legalForm} options={['', ...LEGAL_FORM_OPTIONS]} labels={dict.legalFormOptions} onChange={(value) => setForm((current) => ({ ...current, legalForm: value }))} />
<LabeledInput label={dict.registrationNumber} required maxLength={120} value={form.registrationNumber} onChange={(value) => setForm((current) => ({ ...current, registrationNumber: normalizeTitleCase(value) }))} />
<div className="grid gap-4 sm:grid-cols-2">
<LabeledInput label={dict.registrationNumber} required maxLength={120} value={form.registrationNumber} onChange={(value) => setForm((current) => ({ ...current, registrationNumber: normalizeUpper(value) }))} />
<LabeledInput label={dict.iceNumber} required maxLength={120} value={form.iceNumber} onChange={(value) => setForm((current) => ({ ...current, iceNumber: normalizeUpper(value) }))} />
</div>
<div className="grid gap-4 sm:grid-cols-2">
<LabeledInput label={dict.taxId} required maxLength={120} value={form.taxId} onChange={(value) => setForm((current) => ({ ...current, taxId: normalizeUpper(value) }))} />
<LabeledInput label={dict.operatingLicenseNumber} required maxLength={120} value={form.operatingLicenseNumber} onChange={(value) => setForm((current) => ({ ...current, operatingLicenseNumber: normalizeUpper(value) }))} />
</div>
<div className="grid gap-4 sm:grid-cols-2">
<LabeledInput label={dict.operatingLicenseIssuedAt} required type="date" value={form.operatingLicenseIssuedAt} onChange={(value) => setForm((current) => ({ ...current, operatingLicenseIssuedAt: value }))} />
<LabeledInput label={dict.operatingLicenseIssuedBy} required maxLength={160} value={form.operatingLicenseIssuedBy} onChange={(value) => setForm((current) => ({ ...current, operatingLicenseIssuedBy: normalizeTrimmed(value) }))} />
</div>
<FormSubsection title={dict.contactSection} />
<div className="grid gap-4 sm:grid-cols-2">
<LabeledInput label={dict.companyPhone} required maxLength={80} value={form.companyPhone} onChange={(value) => setForm((current) => ({ ...current, companyPhone: value }))} />
<LabeledInput label={dict.companyEmail} required type="email" maxLength={254} value={form.companyEmail} onChange={(value) => setForm((current) => ({ ...current, companyEmail: normalizeEmail(value) }))} />
</div>
<div className="grid gap-4 sm:grid-cols-2">
<LabeledInput label={dict.fax} maxLength={80} value={form.fax} onChange={(value) => setForm((current) => ({ ...current, fax: value }))} />
<LabeledSelect label={dict.yearsActive} required value={form.yearsActive} options={['', ...YEARS_ACTIVE_OPTIONS]} labels={dict.yearsActiveOptions} onChange={(value) => setForm((current) => ({ ...current, yearsActive: value }))} />
</div>
<BilingualInput label={dict.streetAddress} required maxLength={200} value={form.streetAddress} onChange={(value) => setForm((current) => ({ ...current, streetAddress: value }))} />
<div className="grid gap-4 sm:grid-cols-2">
@@ -535,9 +684,28 @@ export default function SignUpPage() {
<BilingualInput label={dict.country} required maxLength={120} value={form.country} onChange={(value) => setForm((current) => ({ ...current, country: value }))} />
</div>
<LabeledInput label={dict.zipCode} required maxLength={40} value={form.zipCode} onChange={(value) => setForm((current) => ({ ...current, zipCode: value.toUpperCase() }))} />
<LabeledSelect label={dict.yearsActive} required value={form.yearsActive} options={['', ...YEARS_ACTIVE_OPTIONS]} labels={dict.yearsActiveOptions} onChange={(value) => setForm((current) => ({ ...current, yearsActive: value }))} />
<FormSubsection title={dict.representativeSection} />
<div className="grid gap-4 sm:grid-cols-2">
<LabeledInput label={dict.representativeName} maxLength={160} value={form.representativeName} onChange={(value) => setForm((current) => ({ ...current, representativeName: normalizeTrimmed(value) }))} />
<LabeledInput label={dict.representativeTitle} maxLength={120} value={form.representativeTitle} onChange={(value) => setForm((current) => ({ ...current, representativeTitle: normalizeTrimmed(value) }))} />
</div>
<FormSubsection title={dict.responsibleSection} />
<div className="grid gap-4 sm:grid-cols-2">
<LabeledInput label={dict.responsibleName} required maxLength={160} value={form.responsibleName} onChange={(value) => setForm((current) => ({ ...current, responsibleName: normalizeTrimmed(value) }))} />
<LabeledInput label={dict.responsibleRole} required maxLength={120} value={form.responsibleRole} onChange={(value) => setForm((current) => ({ ...current, responsibleRole: normalizeTrimmed(value) }))} />
</div>
<div className="grid gap-4 sm:grid-cols-2">
<LabeledInput label={dict.responsibleIdentityNumber} required maxLength={120} value={form.responsibleIdentityNumber} onChange={(value) => setForm((current) => ({ ...current, responsibleIdentityNumber: normalizeUpper(value) }))} />
<LabeledInput label={dict.responsibleQualification} maxLength={200} value={form.responsibleQualification} onChange={(value) => setForm((current) => ({ ...current, responsibleQualification: normalizeTrimmed(value) }))} />
</div>
<div className="grid gap-4 sm:grid-cols-2">
<LabeledInput label={dict.responsiblePhone} required maxLength={80} value={form.responsiblePhone} onChange={(value) => setForm((current) => ({ ...current, responsiblePhone: value }))} />
<LabeledInput label={dict.responsibleEmail} required type="email" maxLength={254} value={form.responsibleEmail} onChange={(value) => setForm((current) => ({ ...current, responsibleEmail: normalizeEmail(value) }))} />
</div>
</div>
<NavActions onBack={() => setStep(1)} onNext={() => setStep(3)} nextLabel={dict.continue} backLabel={dict.back} loadingLabel={dict.working} />
<NavActions onBack={() => setStep(1)} onNext={handleStep2Continue} nextLabel={dict.continue} backLabel={dict.back} loadingLabel={dict.working} />
</div>
) : null}
@@ -570,9 +738,12 @@ export default function SignUpPage() {
<SectionIntro title={dict.reviewTitle} body={dict.reviewBody} />
<div className="rounded-3xl border border-slate-200 bg-slate-50 p-5 text-sm text-slate-600">
<p><span className="font-semibold text-slate-900">{dict.reviewWorkspace}:</span> {form.companyName.fr || form.companyName.ar || dict.companyFallback}{form.companyName.fr && form.companyName.ar ? <span className="ml-2 text-slate-400">/ {form.companyName.ar}</span> : null}</p>
<p className="mt-2"><span className="font-semibold text-slate-900">{dict.reviewLegalName}:</span> {form.legalName || '—'}</p>
<p className="mt-2"><span className="font-semibold text-slate-900">{dict.reviewPlan}:</span> {form.plan} · {dict.billingPeriodOptions[form.billingPeriod]} · MAD</p>
<p className="mt-2"><span className="font-semibold text-slate-900">{dict.reviewOwnerEmail}:</span> {form.email || '—'}</p>
<p className="mt-2"><span className="font-semibold text-slate-900">{dict.reviewCompanyEmail}:</span> {form.companyEmail || '—'}</p>
<p className="mt-2"><span className="font-semibold text-slate-900">{dict.reviewPhone}:</span> {form.companyPhone || '—'}</p>
<p className="mt-2"><span className="font-semibold text-slate-900">{dict.reviewRegistration}:</span> {form.registrationNumber || '—'} / {form.iceNumber || '—'} / {form.taxId || '—'}</p>
</div>
<NavActions
onBack={() => setStep(3)}
@@ -598,7 +769,25 @@ function hasRequiredPartnerFields(form: SignupForm) {
bilingualFilled(form.streetAddress) &&
bilingualFilled(form.city) &&
bilingualFilled(form.country) &&
[form.legalForm, form.registrationNumber, form.zipCode, form.companyPhone, form.yearsActive]
[
form.legalName,
form.legalForm,
form.registrationNumber,
form.iceNumber,
form.taxId,
form.operatingLicenseNumber,
form.operatingLicenseIssuedAt,
form.operatingLicenseIssuedBy,
form.zipCode,
form.companyPhone,
form.companyEmail,
form.yearsActive,
form.responsibleName,
form.responsibleRole,
form.responsibleIdentityNumber,
form.responsiblePhone,
form.responsibleEmail,
]
.every((v) => v.trim().length > 0)
)
}
@@ -612,6 +801,10 @@ function SectionIntro({ title, body }: { title: string; body: string }) {
)
}
function FormSubsection({ title }: { title: string }) {
return <h3 className="pt-2 text-sm font-semibold uppercase tracking-[0.16em] text-slate-500">{title}</h3>
}
function getEmailWarning(emailDelivery: SignupResponse['emailDelivery'], language: 'en' | 'fr' | 'ar') {
if (!emailDelivery) return null
if (emailDelivery.success) return null
@@ -630,17 +823,14 @@ function getErrorMessage(error: unknown, fallback: string) {
return fallback
}
function normalizeTitleCase(value: string) {
if (!value) return value
const trimmedStart = value.replace(/^\s+/, '')
if (!trimmedStart) return ''
return trimmedStart.charAt(0).toUpperCase() + trimmedStart.slice(1).toLowerCase()
}
function normalizeEmail(value: string) {
return value.replace(/\s+/g, '').toLowerCase()
}
function normalizeTrimmed(value: string) {
return value.replace(/^\s+/, '')
}
function normalizeUpper(value: string) {
return value.replace(/^\s+/, '').toUpperCase()
}