diff --git a/apps/admin/src/app/dashboard/admin-users/page.tsx b/apps/admin/src/app/dashboard/admin-users/page.tsx index c3cad40..862fbb7 100644 --- a/apps/admin/src/app/dashboard/admin-users/page.tsx +++ b/apps/admin/src/app/dashboard/admin-users/page.tsx @@ -15,14 +15,23 @@ interface AdminUser { } const ROLES = ['SUPER_ADMIN', 'ADMIN', 'SUPPORT', 'FINANCE', 'VIEWER'] +const EMPTY_FORM = { + firstName: '', + lastName: '', + email: '', + password: '', + role: 'SUPPORT', + isActive: true, +} export default function AdminUsersPage() { const [admins, setAdmins] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [showModal, setShowModal] = useState(false) - const [form, setForm] = useState({ firstName: '', lastName: '', email: '', password: '', role: 'SUPPORT' }) - const [creating, setCreating] = useState(false) + const [editingAdminId, setEditingAdminId] = useState(null) + const [form, setForm] = useState(EMPTY_FORM) + const [saving, setSaving] = useState(false) function getToken() { return localStorage.getItem('admin_token') ?? '' } @@ -44,25 +53,61 @@ export default function AdminUsersPage() { useEffect(() => { fetchAdmins() }, []) - async function createAdmin(e: React.FormEvent) { + function openCreateModal() { + setEditingAdminId(null) + setForm(EMPTY_FORM) + setError(null) + setShowModal(true) + } + + function openEditModal(admin: AdminUser) { + setEditingAdminId(admin.id) + setForm({ + firstName: admin.firstName, + lastName: admin.lastName, + email: admin.email, + password: '', + role: admin.role, + isActive: admin.isActive, + }) + setError(null) + setShowModal(true) + } + + function closeModal() { + setShowModal(false) + setEditingAdminId(null) + setForm(EMPTY_FORM) + } + + async function saveAdmin(e: React.FormEvent) { e.preventDefault() - setCreating(true) + setSaving(true) setError(null) try { - const res = await fetch(`${ADMIN_API_BASE}/admin/admins`, { - method: 'POST', + const isEditing = Boolean(editingAdminId) + const payload = { + firstName: form.firstName, + lastName: form.lastName, + email: form.email, + role: form.role, + isActive: form.isActive, + ...(form.password ? { password: form.password } : {}), + } + + const res = await fetch(`${ADMIN_API_BASE}/admin/admins${editingAdminId ? `/${editingAdminId}` : ''}`, { + method: isEditing ? 'PATCH' : 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${getToken()}` }, - body: JSON.stringify(form), + body: JSON.stringify(payload), }) const json = await res.json() - if (!res.ok) throw new Error(json?.message ?? 'Failed to create') - setShowModal(false) - setForm({ firstName: '', lastName: '', email: '', password: '', role: 'SUPPORT' }) + if (!res.ok) throw new Error(json?.message ?? `Failed to ${isEditing ? 'update' : 'create'} admin`) + closeModal() await fetchAdmins() } catch (err: any) { setError(err.message) } finally { - setCreating(false) + setSaving(false) } } @@ -82,7 +127,7 @@ export default function AdminUsersPage() {

Admin Users

+ ))} @@ -140,10 +195,10 @@ export default function AdminUsersPage() {
-

New admin user

- +

{editingAdminId ? 'Edit admin user' : 'New admin user'}

+
-
+
@@ -175,11 +230,13 @@ export default function AdminUsersPage() { />
- + setForm({ ...form, password: e.target.value })} @@ -195,10 +252,21 @@ export default function AdminUsersPage() { {ROLES.map((r) => )}
+
+ + +
- - +
diff --git a/apps/api/src/modules/admin/admin.repo.ts b/apps/api/src/modules/admin/admin.repo.ts index 67d06ee..ebcbd14 100644 --- a/apps/api/src/modules/admin/admin.repo.ts +++ b/apps/api/src/modules/admin/admin.repo.ts @@ -322,6 +322,31 @@ export function updateAdminRole(id: string, role: string) { return prisma.adminUser.update({ where: { id }, data: { role: role as any } }) } +export function updateAdmin( + id: string, + data: { + email?: string + firstName?: string + lastName?: string + role?: string + passwordHash?: string + isActive?: boolean + }, +) { + return prisma.adminUser.update({ + where: { id }, + data: { + ...(data.email !== undefined ? { email: data.email } : {}), + ...(data.firstName !== undefined ? { firstName: data.firstName } : {}), + ...(data.lastName !== undefined ? { lastName: data.lastName } : {}), + ...(data.role !== undefined ? { role: data.role as any } : {}), + ...(data.passwordHash !== undefined ? { passwordHash: data.passwordHash } : {}), + ...(data.isActive !== undefined ? { isActive: data.isActive } : {}), + }, + include: { permissions: true }, + }) +} + export async function replaceAdminPermissions(id: string, permissions: any[]) { await prisma.adminUser.findUniqueOrThrow({ where: { id } }) await prisma.$transaction([ diff --git a/apps/api/src/modules/admin/admin.routes.ts b/apps/api/src/modules/admin/admin.routes.ts index 55c9f28..7769cea 100644 --- a/apps/api/src/modules/admin/admin.routes.ts +++ b/apps/api/src/modules/admin/admin.routes.ts @@ -10,6 +10,7 @@ import { companiesQuerySchema, rentersQuerySchema, auditLogQuerySchema, billingQuerySchema, notificationsQuerySchema, invoicesQuerySchema, adminCompanyUpdateSchema, companyStatusSchema, createAdminSchema, adminRoleSchema, adminPermissionsSchema, + updateAdminSchema, homepageUpdateSchema, idParamSchema, companyIdParamSchema, billingAccountIdParamSchema, invoiceIdParamSchema, pricingUpdateSchema, planFeatureCreateSchema, planFeatureUpdateSchema, planFeatureIdParamSchema, promotionCreateSchema, promotionUpdateSchema, promotionIdParamSchema, @@ -188,6 +189,13 @@ router.post('/admins', requireAdminAuth, requireAdminRole('SUPER_ADMIN'), async } catch (err) { next(err) } }) +router.patch('/admins/:id', requireAdminAuth, requireAdminRole('SUPER_ADMIN'), async (req, res, next) => { + try { + const { id } = parseParams(idParamSchema, req) + ok(res, await service.updateAdmin(id, parseBody(updateAdminSchema, req))) + } catch (err) { next(err) } +}) + router.patch('/admins/:id/role', requireAdminAuth, requireAdminRole('SUPER_ADMIN'), async (req, res, next) => { try { const { id } = parseParams(idParamSchema, req) diff --git a/apps/api/src/modules/admin/admin.schemas.ts b/apps/api/src/modules/admin/admin.schemas.ts index e4d316e..0a4c02d 100644 --- a/apps/api/src/modules/admin/admin.schemas.ts +++ b/apps/api/src/modules/admin/admin.schemas.ts @@ -71,14 +71,23 @@ export const permissionSchema = z.object({ }) export const createAdminSchema = z.object({ - email: z.string().email(), - firstName: z.string(), - lastName: z.string(), + email: z.string().email().trim().toLowerCase(), + firstName: z.string().min(1).max(100).trim(), + lastName: z.string().min(1).max(100).trim(), role: z.enum(['SUPER_ADMIN', 'ADMIN', 'SUPPORT', 'FINANCE', 'VIEWER']), password: z.string().min(8), permissions: z.array(permissionSchema).optional(), }) +export const updateAdminSchema = z.object({ + email: z.string().email().trim().toLowerCase().optional(), + firstName: z.string().min(1).max(100).trim().optional(), + lastName: z.string().min(1).max(100).trim().optional(), + role: z.enum(['SUPER_ADMIN', 'ADMIN', 'SUPPORT', 'FINANCE', 'VIEWER']).optional(), + password: z.string().min(8).optional(), + isActive: z.boolean().optional(), +}) + export const adminRoleSchema = z.object({ role: z.enum(['SUPER_ADMIN', 'ADMIN', 'SUPPORT', 'FINANCE', 'VIEWER']), }) diff --git a/apps/api/src/modules/admin/admin.service.ts b/apps/api/src/modules/admin/admin.service.ts index 88fb8a1..65947f6 100644 --- a/apps/api/src/modules/admin/admin.service.ts +++ b/apps/api/src/modules/admin/admin.service.ts @@ -216,6 +216,29 @@ export async function createAdmin(body: { email: string; firstName: string; last return presenter.presentAdminUser(admin) } +export async function updateAdmin( + id: string, + body: { + email?: string + firstName?: string + lastName?: string + role?: string + password?: string + isActive?: boolean + }, +) { + const admin = await repo.updateAdmin(id, { + ...(body.email !== undefined ? { email: body.email } : {}), + ...(body.firstName !== undefined ? { firstName: body.firstName } : {}), + ...(body.lastName !== undefined ? { lastName: body.lastName } : {}), + ...(body.role !== undefined ? { role: body.role } : {}), + ...(body.isActive !== undefined ? { isActive: body.isActive } : {}), + ...(body.password ? { passwordHash: await bcrypt.hash(body.password, 12) } : {}), + }) + + return presenter.presentAdminUser(admin) +} + export function updateAdminRole(id: string, role: string) { return repo.updateAdminRole(id, role) } diff --git a/apps/api/src/modules/auth/auth.company.repo.ts b/apps/api/src/modules/auth/auth.company.repo.ts index 557aab0..4da8026 100644 --- a/apps/api/src/modules/auth/auth.company.repo.ts +++ b/apps/api/src/modules/auth/auth.company.repo.ts @@ -4,8 +4,10 @@ type DbClient = Pick repo.createCompanySignup(tx, { companyName: body.companyName, + legalName: body.legalName, slug, - email: body.email, + ownerEmail: body.email, + companyEmail: body.companyEmail, companyPhone: body.companyPhone, streetAddress: body.streetAddress, city: body.city, @@ -56,8 +58,21 @@ export async function signup(body: CompanySignupInput) { zipCode: body.zipCode, legalForm: body.legalForm, managerName: `${body.firstName} ${body.lastName}`.trim(), + iceNumber: body.iceNumber, + taxId: body.taxId, + operatingLicenseNumber: body.operatingLicenseNumber, + operatingLicenseIssuedAt: body.operatingLicenseIssuedAt, + operatingLicenseIssuedBy: body.operatingLicenseIssuedBy, fax: body.fax, yearsActive: body.yearsActive, + representativeName: body.representativeName, + representativeTitle: body.representativeTitle, + responsibleName: body.responsibleName, + responsibleRole: body.responsibleRole, + responsibleIdentityNumber: body.responsibleIdentityNumber, + responsibleQualification: body.responsibleQualification, + responsiblePhone: body.responsiblePhone, + responsibleEmail: body.responsibleEmail, currency: body.currency, registrationNumber: body.registrationNumber, plan: body.plan, diff --git a/apps/api/src/tests/integration/auth-company-signup.test.ts b/apps/api/src/tests/integration/auth-company-signup.test.ts index 2492009..125f8f6 100644 --- a/apps/api/src/tests/integration/auth-company-signup.test.ts +++ b/apps/api/src/tests/integration/auth-company-signup.test.ts @@ -19,14 +19,29 @@ describe('Company signup API', () => { email: uniqueEmail('trial-owner'), password: 'Password123!', companyName: `Trial Cars ${Date.now()}`, + legalName: `Trial Cars SARL ${Date.now()}`, legalForm: 'LLC', registrationNumber: `REG-${Date.now()}`, + iceNumber: `ICE-${Date.now()}`, + taxId: `IF-${Date.now()}`, + operatingLicenseNumber: `AGR-${Date.now()}`, + operatingLicenseIssuedAt: '2026-01-15', + operatingLicenseIssuedBy: 'Wilaya de Casablanca', streetAddress: '123 Main Street', city: 'Casablanca', country: 'Morocco', zipCode: '20000', companyPhone: '+212600000000', + companyEmail: uniqueEmail('trial-company'), yearsActive: '1', + representativeName: 'Trial Representative', + representativeTitle: 'Managing Director', + responsibleName: 'Operations Lead', + responsibleRole: 'Responsible Manager', + responsibleIdentityNumber: `CIN-${Date.now()}`, + responsibleQualification: '10 years fleet operations experience', + responsiblePhone: '+212611111111', + responsibleEmail: uniqueEmail('trial-responsible'), preferredLanguage: 'en', plan: 'STARTER', billingPeriod: 'MONTHLY', diff --git a/apps/dashboard/src/app/sign-up/[[...sign-up]]/page.tsx b/apps/dashboard/src/app/sign-up/[[...sign-up]]/page.tsx index 6f68c58..86b152e 100644 --- a/apps/dashboard/src/app/sign-up/[[...sign-up]]/page.tsx +++ b/apps/dashboard/src/app/sign-up/[[...sign-up]]/page.tsx @@ -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, l’entreprise 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 l’entreprise', + 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 d’agré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 l’entreprise', fax: 'Fax (optionnel)', yearsActive: 'Années d’activité', + 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 d’identité', + responsibleQualification: 'Qualification / diplôme / expérience', + responsiblePhone: 'Téléphone du responsable', + responsibleEmail: 'E-mail du responsable', + identitySection: 'Identité juridique', + contactSection: 'Coordonnées de l’entreprise', + 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 d’au 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() { setForm((current) => ({ ...current, firstName: value }))} /> setForm((f) => ({ ...f, lastName: current }))} /> setForm((current) => ({ ...current, email: normalizeEmail(value) }))} /> +

{dict.emailReuseHint}

setForm((current) => ({ ...current, password: value }))} /> setForm((current) => ({ ...current, confirmPassword: value }))} /> @@ -522,12 +652,31 @@ export default function SignUpPage() {
+ setForm((current) => ({ ...current, companyName: value }))} /> + setForm((current) => ({ ...current, legalName: normalizeTrimmed(value) }))} /> setForm((current) => ({ ...current, legalForm: value }))} /> - setForm((current) => ({ ...current, registrationNumber: normalizeTitleCase(value) }))} /> +
+ setForm((current) => ({ ...current, registrationNumber: normalizeUpper(value) }))} /> + setForm((current) => ({ ...current, iceNumber: normalizeUpper(value) }))} /> +
+
+ setForm((current) => ({ ...current, taxId: normalizeUpper(value) }))} /> + setForm((current) => ({ ...current, operatingLicenseNumber: normalizeUpper(value) }))} /> +
+
+ setForm((current) => ({ ...current, operatingLicenseIssuedAt: value }))} /> + setForm((current) => ({ ...current, operatingLicenseIssuedBy: normalizeTrimmed(value) }))} /> +
+ +
setForm((current) => ({ ...current, companyPhone: value }))} /> + setForm((current) => ({ ...current, companyEmail: normalizeEmail(value) }))} /> +
+
setForm((current) => ({ ...current, fax: value }))} /> + setForm((current) => ({ ...current, yearsActive: value }))} />
setForm((current) => ({ ...current, streetAddress: value }))} />
@@ -535,9 +684,28 @@ export default function SignUpPage() { setForm((current) => ({ ...current, country: value }))} />
setForm((current) => ({ ...current, zipCode: value.toUpperCase() }))} /> - setForm((current) => ({ ...current, yearsActive: value }))} /> + + +
+ setForm((current) => ({ ...current, representativeName: normalizeTrimmed(value) }))} /> + setForm((current) => ({ ...current, representativeTitle: normalizeTrimmed(value) }))} /> +
+ + +
+ setForm((current) => ({ ...current, responsibleName: normalizeTrimmed(value) }))} /> + setForm((current) => ({ ...current, responsibleRole: normalizeTrimmed(value) }))} /> +
+
+ setForm((current) => ({ ...current, responsibleIdentityNumber: normalizeUpper(value) }))} /> + setForm((current) => ({ ...current, responsibleQualification: normalizeTrimmed(value) }))} /> +
+
+ setForm((current) => ({ ...current, responsiblePhone: value }))} /> + setForm((current) => ({ ...current, responsibleEmail: normalizeEmail(value) }))} /> +
- setStep(1)} onNext={() => setStep(3)} nextLabel={dict.continue} backLabel={dict.back} loadingLabel={dict.working} /> + setStep(1)} onNext={handleStep2Continue} nextLabel={dict.continue} backLabel={dict.back} loadingLabel={dict.working} />
) : null} @@ -570,9 +738,12 @@ export default function SignUpPage() {

{dict.reviewWorkspace}: {form.companyName.fr || form.companyName.ar || dict.companyFallback}{form.companyName.fr && form.companyName.ar ? / {form.companyName.ar} : null}

+

{dict.reviewLegalName}: {form.legalName || '—'}

{dict.reviewPlan}: {form.plan} · {dict.billingPeriodOptions[form.billingPeriod]} · MAD

{dict.reviewOwnerEmail}: {form.email || '—'}

+

{dict.reviewCompanyEmail}: {form.companyEmail || '—'}

{dict.reviewPhone}: {form.companyPhone || '—'}

+

{dict.reviewRegistration}: {form.registrationNumber || '—'} / {form.iceNumber || '—'} / {form.taxId || '—'}

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

{title}

+} + 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() }