diff --git a/apps/edr-passenger-web/portal/src/app/booking/passengers/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/passengers/page.tsx index d5857ac98..e2ca159b5 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/passengers/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/passengers/page.tsx @@ -529,6 +529,10 @@ const passengerSchema = z.object({ dateOfBirth: z.string().min(1, 'Date of birth is required'), gender: z.string().min(1, 'Gender is required'), nationality: z.string().min(1, 'Nationality is required'), + // Adults enter these themselves; children inherit the primary adult's values (see the + // sync effect in the page component) rather than collecting their own. + phone: z.string(), + email: z.string(), nationalId: z.string().optional(), passportNumber: z.string().optional(), passportCountry: z.string().optional(), @@ -557,27 +561,29 @@ function createFormSchema(adultCount: number) { return z.object({ passengers: z.array(passengerSchema), createAccount: z.boolean(), - contactPhone: z.string(), - contactEmail: z.string(), }).superRefine((data, ctx) => { - if (!data.contactEmail || data.contactEmail.trim().length === 0) { - ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Contact email is required', path: ['contactEmail'] }); - } else { - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - if (!emailRegex.test(data.contactEmail)) { - ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Invalid email format', path: ['contactEmail'] }); - } - } - const contactNationality = data.passengers[0]?.nationality || 'ETHIOPIAN'; - const phoneError = validatePhone(data.contactPhone, contactNationality); - if (phoneError) { - ctx.addIssue({ code: z.ZodIssueCode.custom, message: phoneError, path: ['contactPhone'] }); - } - data.passengers.forEach((p, i) => { + const isAdult = i < adultCount; + + // Contact fields are only collected from — and validated against — adults. + // Children's phone/email are inherited from the primary adult, not user-entered. + if (isAdult) { + if (!p.email || p.email.trim().length === 0) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Email is required', path: ['passengers', i, 'email'] }); + } else { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(p.email)) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Invalid email format', path: ['passengers', i, 'email'] }); + } + } + const phoneError = validatePhone(p.phone, p.nationality); + if (phoneError) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: phoneError, path: ['passengers', i, 'phone'] }); + } + } + const age = calculateAge(p.dateOfBirth); if (age === null) return; - const isAdult = i < adultCount; if (isAdult && age <= 5) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Adult passengers must be older than 5 years', path: ['passengers', i, 'dateOfBirth'] }); } else if (!isAdult && age > 5) { @@ -623,6 +629,8 @@ export default function PassengersPage() { dateOfBirth: stored.dateOfBirth || '', gender: (stored.gender as any) || undefined, nationality: stored.nationality || searchCriteria?.nationality || 'ETHIOPIAN', + phone: (i >= adultCount ? storedPassengers[0]?.phone : stored.phone) || '', + email: (i >= adultCount ? storedPassengers[0]?.email : stored.email) || '', nationalId: stored.nationalId || '', passportNumber: stored.passportNumber || '', passportCountry: stored.passportCountry || '', @@ -638,6 +646,10 @@ export default function PassengersPage() { dateOfBirth: '', gender: undefined, nationality: searchCriteria?.nationality || 'ETHIOPIAN', + // Children start out mirroring whatever the primary adult already has on file; + // the sync effect below keeps this current as the primary adult's info changes. + phone: (i >= adultCount ? storedPassengers[0]?.phone : '') || '', + email: (i >= adultCount ? storedPassengers[0]?.email : '') || '', nationalId: '', passportNumber: '', passportCountry: '', @@ -649,14 +661,28 @@ export default function PassengersPage() { }; }), createAccount: false, - contactPhone: storedPassengers[0]?.phone || '', - contactEmail: storedPassengers[0]?.email || '', }, }); const { fields } = useFieldArray({ control, name: 'passengers' }); const passengers = watch('passengers'); + // Children don't collect their own contact info — keep their phone/email mirrored to + // whatever the primary adult (index 0) currently has, so it's always in sync. + const primaryPhone = passengers[0]?.phone; + const primaryEmail = passengers[0]?.email; + useEffect(() => { + for (let i = adultCount; i < passengers.length; i++) { + if (passengers[i]?.phone !== (primaryPhone || '')) { + setValue(`passengers.${i}.phone`, primaryPhone || '', { shouldValidate: true }); + } + if (passengers[i]?.email !== (primaryEmail || '')) { + setValue(`passengers.${i}.email`, primaryEmail || '', { shouldValidate: true }); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [primaryPhone, primaryEmail, adultCount, passengers.length]); + useEffect(() => { const checkFaydaStatus = async () => { try { @@ -719,10 +745,9 @@ export default function PassengersPage() { const normalizedGender = normalizeFaydaGender(d.gender); if (normalizedGender) setValue(`passengers.${targetIndex}.gender`, normalizedGender, { shouldValidate: true }); if (faydaSub) setValue(`passengers.${targetIndex}.faydaSub`, faydaSub); - // Contact info is shared across all passengers — only fill it in if nobody has - // entered it yet, so verifying passenger 2 can't clobber passenger 1's contact. - if (d.email && !watch('contactEmail')) setValue('contactEmail', d.email, { shouldValidate: true }); - if (d.phoneNumber && !watch('contactPhone')) setValue('contactPhone', d.phoneNumber, { shouldValidate: true }); + // Only fill in this passenger's own contact fields if they haven't entered them yet. + if (d.email && !watch(`passengers.${targetIndex}.email`)) setValue(`passengers.${targetIndex}.email`, d.email, { shouldValidate: true }); + if (d.phoneNumber && !watch(`passengers.${targetIndex}.phone`)) setValue(`passengers.${targetIndex}.phone`, d.phoneNumber, { shouldValidate: true }); setValue(`passengers.${targetIndex}.faydaVerified`, true); setValue(`passengers.${targetIndex}.formExpanded`, true); setVerificationStatus((prev) => ({ ...prev, [targetIndex]: 'success' })); @@ -781,8 +806,8 @@ export default function PassengersPage() { setValue('passengers.0.dateOfBirth', passengerData?.dateOfBirth || user.dateOfBirth || ''); if (passengerData?.gender || user.gender) setValue('passengers.0.gender', (passengerData?.gender || user.gender) as any); setValue('passengers.0.nationality', passengerData?.nationality || user.nationality || 'ETHIOPIAN'); - if (passengerData?.phone || user.phone) setValue('contactPhone', passengerData?.phone || user.phone || ''); - if (passengerData?.email || user.email) setValue('contactEmail', passengerData?.email || user.email || ''); + if (passengerData?.phone || user.phone) setValue('passengers.0.phone', passengerData?.phone || user.phone || ''); + if (passengerData?.email || user.email) setValue('passengers.0.email', passengerData?.email || user.email || ''); if (passengerData?.passportNumber) setValue('passengers.0.passportNumber', passengerData.passportNumber); if (passengerData?.passportCountry) setValue('passengers.0.passportCountry', passengerData.passportCountry); if (passengerData?.passportIssueDate) setValue('passengers.0.passportIssueDate', passengerData.passportIssueDate); @@ -935,8 +960,8 @@ export default function PassengersPage() { passportIssueDate: p.passportIssueDate, passportExpiryDate: p.passportExpiryDate, passportIssuingAuthority: p.passportIssuingAuthority, - phone: data.contactPhone, - email: data.contactEmail, + phone: p.phone, + email: p.email, isPrimaryPassenger: i === 0, passengerId: i === 0 && passengerId ? passengerId : undefined, })) @@ -1155,6 +1180,40 @@ export default function PassengersPage() { disabled /> + + {isChildPassenger ? ( +
+ Contact details (phone & email) are shared with the primary passenger and don't need to be entered separately. +
+ ) : ( + <> + {/* Phone */} +
+ + setValue(`passengers.${index}.phone`, v)} + onNormalized={(v) => setValue(`passengers.${index}.phone`, v, { shouldValidate: true })} + error={errors.passengers?.[index]?.phone?.message} + /> +
+ + {/* Email */} +
+ + + {errors.passengers?.[index]?.email && ( +

{errors.passengers[index]?.email?.message}

+ )} +
+ + )} ) : ( @@ -1210,6 +1269,40 @@ export default function PassengersPage() { disabled /> + + {isChildPassenger ? ( +
+ Contact details (phone & email) are shared with the primary passenger and don't need to be entered separately. +
+ ) : ( + <> + {/* Phone */} +
+ + setValue(`passengers.${index}.phone`, v)} + onNormalized={(v) => setValue(`passengers.${index}.phone`, v, { shouldValidate: true })} + error={errors.passengers?.[index]?.phone?.message} + /> +
+ + {/* Email */} +
+ + + {errors.passengers?.[index]?.email && ( +

{errors.passengers[index]?.email?.message}

+ )} +
+ + )} {/* Passport fields */} @@ -1267,40 +1360,6 @@ export default function PassengersPage() { ); })} -
-

Contact Information

-

- This phone number and email will be used for booking and ticketing communication for all passengers. -

-
- {/* Contact Phone */} -
- - setValue('contactPhone', v)} - onNormalized={(v) => setValue('contactPhone', v, { shouldValidate: true })} - error={errors.contactPhone?.message} - /> -
- - {/* Contact Email */} -
- - - {errors.contactEmail && ( -

{errors.contactEmail.message}

- )} -
-
-
- {!isAuthenticated && (