mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 12:00:59 +00:00
Update passenger contact information
This commit is contained in:
@@ -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
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isChildPassenger ? (
|
||||
<div className="md:col-span-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg text-sm text-gray-600 dark:text-gray-400">
|
||||
Contact details (phone & email) are shared with the primary passenger and don't need to be entered separately.
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Phone */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Phone Number *</label>
|
||||
<PhoneInput
|
||||
nationality={passengers[index]?.nationality || 'ETHIOPIAN'}
|
||||
storedValue={passengers[index]?.phone || ''}
|
||||
onInterimChange={(v) => setValue(`passengers.${index}.phone`, v)}
|
||||
onNormalized={(v) => setValue(`passengers.${index}.phone`, v, { shouldValidate: true })}
|
||||
error={errors.passengers?.[index]?.phone?.message}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Email */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Email *</label>
|
||||
<input
|
||||
type="email"
|
||||
{...register(`passengers.${index}.email`)}
|
||||
className={`input-field ${errors.passengers?.[index]?.email ? 'border-red-500' : ''}`}
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
{errors.passengers?.[index]?.email && (
|
||||
<p className="text-red-500 text-xs mt-1">{errors.passengers[index]?.email?.message}</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
@@ -1210,6 +1269,40 @@ export default function PassengersPage() {
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isChildPassenger ? (
|
||||
<div className="md:col-span-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg text-sm text-gray-600 dark:text-gray-400">
|
||||
Contact details (phone & email) are shared with the primary passenger and don't need to be entered separately.
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Phone */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Phone Number *</label>
|
||||
<PhoneInput
|
||||
nationality={passengers[index]?.nationality || 'OTHER'}
|
||||
storedValue={passengers[index]?.phone || ''}
|
||||
onInterimChange={(v) => setValue(`passengers.${index}.phone`, v)}
|
||||
onNormalized={(v) => setValue(`passengers.${index}.phone`, v, { shouldValidate: true })}
|
||||
error={errors.passengers?.[index]?.phone?.message}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Email */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Email *</label>
|
||||
<input
|
||||
type="email"
|
||||
{...register(`passengers.${index}.email`)}
|
||||
className={`input-field ${errors.passengers?.[index]?.email ? 'border-red-500' : ''}`}
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
{errors.passengers?.[index]?.email && (
|
||||
<p className="text-red-500 text-xs mt-1">{errors.passengers[index]?.email?.message}</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Passport fields */}
|
||||
@@ -1267,40 +1360,6 @@ export default function PassengersPage() {
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="card">
|
||||
<h3 className="text-lg font-semibold mb-1 text-gray-900 dark:text-gray-100">Contact Information</h3>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4">
|
||||
This phone number and email will be used for booking and ticketing communication for all passengers.
|
||||
</p>
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
{/* Contact Phone */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Phone Number *</label>
|
||||
<PhoneInput
|
||||
nationality={passengers[0]?.nationality || searchCriteria?.nationality || 'ETHIOPIAN'}
|
||||
storedValue={watch('contactPhone') || ''}
|
||||
onInterimChange={(v) => setValue('contactPhone', v)}
|
||||
onNormalized={(v) => setValue('contactPhone', v, { shouldValidate: true })}
|
||||
error={errors.contactPhone?.message}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Contact Email */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Email *</label>
|
||||
<input
|
||||
type="email"
|
||||
{...register('contactEmail')}
|
||||
className={`input-field ${errors.contactEmail ? 'border-red-500' : ''}`}
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
{errors.contactEmail && (
|
||||
<p className="text-red-500 text-xs mt-1">{errors.contactEmail.message}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isAuthenticated && (
|
||||
<div className="card">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
|
||||
Reference in New Issue
Block a user