@qite/tide-booking-component 1.4.110 → 1.4.112

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/build/build-cjs/index.js +2301 -1565
  2. package/build/build-cjs/src/booking-wizard/features/travelers-form/travelers-form.d.ts +1 -2
  3. package/build/build-cjs/src/search-results/components/book-packaging-entry/index.d.ts +1 -0
  4. package/build/build-cjs/src/search-results/store/search-results-slice.d.ts +3 -1
  5. package/build/build-cjs/src/search-results/types.d.ts +3 -0
  6. package/build/build-cjs/src/shared/booking/shared-confirmation.d.ts +25 -0
  7. package/build/build-cjs/src/shared/booking/summary.d.ts +43 -0
  8. package/build/build-cjs/src/shared/booking/travelers-form.d.ts +93 -0
  9. package/build/build-cjs/src/shared/utils/booking-summary.d.ts +1 -0
  10. package/build/build-cjs/src/shared/utils/localization-util.d.ts +6 -0
  11. package/build/build-esm/index.js +2198 -1463
  12. package/build/build-esm/src/booking-wizard/features/travelers-form/travelers-form.d.ts +1 -2
  13. package/build/build-esm/src/search-results/components/book-packaging-entry/index.d.ts +1 -0
  14. package/build/build-esm/src/search-results/store/search-results-slice.d.ts +3 -1
  15. package/build/build-esm/src/search-results/types.d.ts +3 -0
  16. package/build/build-esm/src/shared/booking/shared-confirmation.d.ts +25 -0
  17. package/build/build-esm/src/shared/booking/summary.d.ts +43 -0
  18. package/build/build-esm/src/shared/booking/travelers-form.d.ts +93 -0
  19. package/build/build-esm/src/shared/utils/booking-summary.d.ts +1 -0
  20. package/build/build-esm/src/shared/utils/localization-util.d.ts +6 -0
  21. package/package.json +2 -2
  22. package/src/booking-wizard/components/step-indicator.tsx +1 -1
  23. package/src/booking-wizard/components/step-route.tsx +1 -1
  24. package/src/booking-wizard/features/confirmation/confirmation.tsx +11 -55
  25. package/src/booking-wizard/features/sidebar/index.tsx +1 -1
  26. package/src/booking-wizard/features/summary/summary.tsx +1 -1
  27. package/src/booking-wizard/features/travelers-form/travelers-form.tsx +84 -1010
  28. package/src/search-results/components/book-packaging-entry/index.tsx +201 -21
  29. package/src/search-results/components/book-packaging-entry/wl-sidebar.tsx +1 -4
  30. package/src/search-results/components/group-tour/group-tour-card.tsx +1 -1
  31. package/src/search-results/components/group-tour/group-tour-results.tsx +1 -1
  32. package/src/search-results/components/search-results-container/search-results-container.tsx +40 -14
  33. package/src/search-results/store/search-results-slice.ts +8 -2
  34. package/src/search-results/types.ts +4 -0
  35. package/src/shared/booking/{BookingPanel.tsx → booking-panel.tsx} +1 -1
  36. package/src/shared/booking/shared-confirmation.tsx +105 -0
  37. package/src/shared/booking/summary.tsx +380 -0
  38. package/src/shared/booking/travelers-form.tsx +868 -0
  39. package/src/shared/components/flyin/flyin.tsx +8 -9
  40. package/src/shared/components/flyin/packaging-flights-flyin.tsx +4 -4
  41. package/src/shared/utils/booking-summary.tsx +46 -0
  42. package/src/shared/utils/tide-api-utils.ts +2 -2
  43. package/styles/components/_booking.scss +33 -15
  44. package/styles/components/_cta.scss +2 -2
  45. package/styles/components/_dropdown.scss +5 -0
  46. package/styles/components/_flight-option.scss +1 -1
  47. package/styles/components/_flyin.scss +43 -0
  48. package/styles/components/_search.scss +5 -0
  49. package/styles/components/_step-indicators.scss +41 -15
  50. package/styles/components/_tree.scss +2 -2
  51. /package/build/build-cjs/src/shared/booking/{BookingPanel.d.ts → booking-panel.d.ts} +0 -0
  52. /package/build/build-cjs/src/shared/booking/{Sidebar.d.ts → shared-sidebar.d.ts} +0 -0
  53. /package/build/build-cjs/src/shared/booking/{StepIndicators.d.ts → step-indicators.d.ts} +0 -0
  54. /package/build/build-esm/src/shared/booking/{BookingPanel.d.ts → booking-panel.d.ts} +0 -0
  55. /package/build/build-esm/src/shared/booking/{Sidebar.d.ts → shared-sidebar.d.ts} +0 -0
  56. /package/build/build-esm/src/shared/booking/{StepIndicators.d.ts → step-indicators.d.ts} +0 -0
  57. /package/src/shared/booking/{Sidebar.tsx → shared-sidebar.tsx} +0 -0
  58. /package/src/shared/booking/{StepIndicators.tsx → step-indicators.tsx} +0 -0
@@ -0,0 +1,868 @@
1
+ import { compact, get, sortBy } from 'lodash';
2
+ import React, { ReactNode, useMemo, useState } from 'react';
3
+ import { FormikProps } from 'formik';
4
+ import flat from 'flat';
5
+ import { format, parse } from 'date-fns';
6
+ import { Country, RoomTraveler, Traveler, TravelersFormValues } from '../../booking-wizard/types';
7
+ import LabeledInput from '../../booking-wizard/components/labeled-input';
8
+ import LabeledSelect from '../../booking-wizard/components/labeled-select';
9
+ import PhoneInput from '../../booking-wizard/components/phone-input';
10
+ import GenderControl from '../../booking-wizard/features/travelers-form/controls/gender-control';
11
+ import TypeAheadInput from '../../booking-wizard/features/travelers-form/type-ahead-input';
12
+ import { buildClassName } from '../utils/class-util';
13
+ import { CountryItem, PackagingEntry, PackagingEntryPax } from '@qite/tide-client';
14
+
15
+ export type TravelersFormField = { type: string };
16
+ export type AgentOption = { id: number | string; name: string; postalCode?: string; location?: string };
17
+ export type TypeAheadOption = { key: string; value: string; text: string };
18
+
19
+ export interface SharedTravelersSettings {
20
+ countries?: Country[];
21
+ formFields?: TravelersFormField[];
22
+ mainBookerFormFields?: TravelersFormField[];
23
+ }
24
+
25
+ export interface SharedTravelersFormProps {
26
+ formik: FormikProps<TravelersFormValues>;
27
+ translations: any;
28
+ travellersSettings?: SharedTravelersSettings;
29
+ countries?: CountryItem[];
30
+ agents?: AgentOption[];
31
+ bookingType?: string;
32
+ agentAdressId?: number;
33
+ travelersFirstStep?: boolean;
34
+ isUnavailable?: boolean;
35
+ useCompactForm?: boolean;
36
+ showAllCountries?: boolean;
37
+ showAgentSelection?: boolean;
38
+ initialShowAgents?: boolean;
39
+ renderPreviousButton?: () => ReactNode;
40
+ onBookingTypeChange?: (bookingType: string) => void;
41
+ }
42
+
43
+ export function createTraveler(traveler: RoomTraveler, followNumber: { number: number }, personTranslation?: string, isCompact?: boolean): Traveler {
44
+ if (isCompact) {
45
+ return {
46
+ id: traveler.id,
47
+ firstName: personTranslation,
48
+ lastName: `${followNumber.number++}`,
49
+ birthDate: '',
50
+ gender: '',
51
+ age: traveler.age || 30
52
+ } as Traveler;
53
+ }
54
+
55
+ return {
56
+ id: traveler.id,
57
+ firstName: '',
58
+ lastName: '',
59
+ birthDate: '',
60
+ gender: ''
61
+ } as Traveler;
62
+ }
63
+
64
+ export function createInitialValuesFromRooms(
65
+ formRooms: { adults: RoomTraveler[]; children: RoomTraveler[] }[],
66
+ startDate?: string,
67
+ agentAdressId?: number,
68
+ personTranslation?: string,
69
+ isCompact?: boolean
70
+ ): TravelersFormValues {
71
+ const followNumber = { number: 1 };
72
+
73
+ const initialValues = {
74
+ startDate,
75
+ rooms: formRooms.map((room) => ({
76
+ adults: room.adults.map((traveler) => createTraveler(traveler, followNumber, personTranslation, isCompact)),
77
+ children: room.children.map((traveler) => createTraveler(traveler, followNumber, personTranslation, isCompact))
78
+ })),
79
+ mainBookerId: -1,
80
+ street: '',
81
+ houseNumber: '',
82
+ box: '',
83
+ zipCode: '',
84
+ place: '',
85
+ country: '',
86
+ phone: '',
87
+ email: '',
88
+ emailConfirmation: '',
89
+ travelAgentId: agentAdressId ?? 0,
90
+ travelAgentName: ''
91
+ } as TravelersFormValues;
92
+
93
+ if (initialValues.rooms?.[0]?.adults?.[0]) {
94
+ initialValues.mainBookerId = initialValues.rooms[0].adults[0].id;
95
+ }
96
+
97
+ return initialValues;
98
+ }
99
+
100
+ export function createInitialValuesFromEditablePackagingEntry(editablePackagingEntry: PackagingEntry, agentAdressId?: number): TravelersFormValues {
101
+ console.log('editablePackagingEntry?.pax:', editablePackagingEntry?.pax);
102
+ const pax = editablePackagingEntry?.pax ?? [];
103
+ const rooms = editablePackagingEntry.rooms.map((room) => {
104
+ const roomPax = pax.filter((x: PackagingEntryPax) => room.paxIds.includes(x.id));
105
+ // TODO children/dateofbirth is missing at this point
106
+ return {
107
+ adults: roomPax.map((roomTraveler: PackagingEntryPax) => {
108
+ return {
109
+ id: roomTraveler.id,
110
+ firstName: roomTraveler.firstName ?? '',
111
+ lastName: roomTraveler.lastName ?? '',
112
+ birthDate: roomTraveler.dateOfBirth ? format(new Date(roomTraveler.dateOfBirth), 'yyyy-MM-dd') : '',
113
+ gender: ''
114
+ } as Traveler;
115
+ }),
116
+ children: [] as Traveler[]
117
+ };
118
+ });
119
+
120
+ const values = createInitialValuesFromRooms(
121
+ rooms.map((room) => ({ adults: room.adults as RoomTraveler[], children: room.children as RoomTraveler[] })),
122
+ editablePackagingEntry?.lines?.[0]?.from,
123
+ agentAdressId
124
+ );
125
+
126
+ values.rooms = rooms;
127
+ values.mainBookerId = pax.find((x: any) => x.isMainBooker)?.id ?? rooms[0]?.adults?.[0]?.id ?? -1;
128
+
129
+ const address = editablePackagingEntry?.address;
130
+
131
+ values.street = address?.street ?? '';
132
+ values.houseNumber = address?.houseNumber ?? '';
133
+ values.box = address?.box ?? '';
134
+ values.zipCode = address?.zipCode ?? '';
135
+ values.place = address?.place ?? '';
136
+ values.country = address?.country ?? '';
137
+ values.phone = address?.phone ?? '';
138
+ values.email = address?.email ?? '';
139
+ values.emailConfirmation = address?.email ?? '';
140
+ values.travelAgentId = address?.travelAgentId ?? agentAdressId ?? 0;
141
+
142
+ return values;
143
+ }
144
+
145
+ export function applyTravelersFormValuesToEditablePackagingEntry(editablePackagingEntry: PackagingEntry, values: TravelersFormValues) {
146
+ const travelers = values.rooms.flatMap((room) => [...room.adults, ...room.children]);
147
+ console.log('Applying form values:', values);
148
+ console.log('editablePackagingEntry:', editablePackagingEntry);
149
+
150
+ return {
151
+ ...editablePackagingEntry,
152
+ pax: (editablePackagingEntry.pax ?? []).map((pax) => {
153
+ const traveler = travelers.find((x) => x.id === pax.id);
154
+ if (!traveler) return pax;
155
+
156
+ return {
157
+ ...pax,
158
+ firstName: traveler.firstName ?? '',
159
+ lastName: traveler.lastName ?? '',
160
+ dateOfBirth: traveler.birthDate || null,
161
+ isMainBooker: traveler.id === values.mainBookerId
162
+ };
163
+ }),
164
+ address: {
165
+ ...editablePackagingEntry.address,
166
+ street: values.street,
167
+ houseNumber: values.houseNumber,
168
+ box: values.box,
169
+ zipCode: values.zipCode,
170
+ place: values.place,
171
+ country: values.country,
172
+ travelAgentId: values.travelAgentId,
173
+ phone: values.phone,
174
+ email: values.email
175
+ }
176
+ };
177
+ }
178
+
179
+ const SharedTravelersForm: React.FC<SharedTravelersFormProps> = ({
180
+ formik,
181
+ translations,
182
+ travellersSettings,
183
+ countries,
184
+ agents,
185
+ bookingType,
186
+ agentAdressId,
187
+ travelersFirstStep = false,
188
+ isUnavailable = false,
189
+ useCompactForm = false,
190
+ showAllCountries = false,
191
+ showAgentSelection = false,
192
+ initialShowAgents = false,
193
+ renderPreviousButton,
194
+ onBookingTypeChange
195
+ }) => {
196
+ const [showAgents, setShowAgents] = useState<boolean>(initialShowAgents);
197
+
198
+ const typeaheadAgents = useMemo<TypeAheadOption[]>(
199
+ () =>
200
+ sortBy(
201
+ agents?.map((agent) => ({
202
+ key: `${agent.id}`,
203
+ value: `${agent.name}${agent.postalCode || agent.location ? ` (${compact([agent.postalCode, agent.location]).join(' ')})` : ''}`,
204
+ text: `${agent.name}${agent.postalCode || agent.location ? ` (${compact([agent.postalCode, agent.location]).join(' ')})` : ''}`
205
+ })),
206
+ 'value'
207
+ ) ?? [],
208
+ [agents]
209
+ );
210
+
211
+ const [filteredAgents, setFilteredAgents] = useState<TypeAheadOption[]>(typeaheadAgents);
212
+
213
+ const flatErrors: Record<string, string> = flat(formik.errors);
214
+ const errorKeys = Object.keys(flatErrors).filter((key) => get(formik.touched, key));
215
+ const hasVisibleError = (key: string) => get(formik.errors, key) && get(formik.touched, key);
216
+
217
+ const mainBooker = formik.values.rooms
218
+ .find((room) => room.adults.find((traveler) => traveler.id === formik.values.mainBookerId))
219
+ ?.adults.find((traveler) => traveler.id === formik.values.mainBookerId);
220
+
221
+ const countryOptions = [
222
+ { key: 'empty', value: undefined, label: translations.TRAVELERS_FORM.SELECT_COUNTRY },
223
+ ...(showAllCountries
224
+ ? countries?.map((country) => ({ key: country.iso2, value: country.iso2, label: country.name })) ?? []
225
+ : travellersSettings?.countries?.map((country) => ({ key: country.iso2, value: country.iso2, label: country.name })) ?? [
226
+ { key: 'be', value: 'be', label: translations.TRAVELERS_FORM.COUNTRIES.BELGIUM },
227
+ { key: 'nl', value: 'nl', label: translations.TRAVELERS_FORM.COUNTRIES.NETHERLANDS },
228
+ { key: 'fr', value: 'fr', label: translations.TRAVELERS_FORM.COUNTRIES.FRANCE }
229
+ ])
230
+ ];
231
+
232
+ const handleMainBookerChange: React.FormEventHandler<HTMLInputElement> = (event) => {
233
+ formik.setFieldValue('mainBookerId', parseInt(event.currentTarget.value, 10));
234
+ };
235
+
236
+ const handleAgentChange = (value: string) => {
237
+ setFilteredAgents(typeaheadAgents.filter((x) => x.value.toLocaleLowerCase().indexOf(value.toLocaleLowerCase()) > -1));
238
+ formik.setFieldValue('travelAgentName', value);
239
+ };
240
+
241
+ const handleAgentSelect = (key: string) => {
242
+ const agent = typeaheadAgents.find((x) => x.key === key);
243
+
244
+ formik.setValues({
245
+ ...formik.values,
246
+ travelAgentId: Number(agent?.key),
247
+ travelAgentName: agent?.value ?? ''
248
+ });
249
+
250
+ onBookingTypeChange?.(agentAdressId && agentAdressId !== 0 ? 'b2b' : 'b2b2c');
251
+ };
252
+
253
+ const handleAgentClear = () => {
254
+ formik.setValues({ ...formik.values, travelAgentId: 0, travelAgentName: '' });
255
+ onBookingTypeChange?.('b2c');
256
+ };
257
+
258
+ const toggleAgent = (value: boolean) => {
259
+ setShowAgents(value);
260
+ if (!value) {
261
+ handleAgentClear();
262
+ setFilteredAgents([]);
263
+ }
264
+ };
265
+
266
+ const handleAddTraveler = (roomIndex: number) => {
267
+ const rooms = [...formik.values.rooms];
268
+ const newAdult = { id: Date.now(), firstName: '', lastName: '', birthDate: '', gender: '' } as Traveler;
269
+ rooms[roomIndex] = { ...rooms[roomIndex], adults: [...rooms[roomIndex].adults, newAdult] };
270
+ formik.setFieldValue('rooms', rooms);
271
+ };
272
+
273
+ const handleRemoveTraveler = (roomIndex: number, travelerIndex: number) => {
274
+ const rooms = [...formik.values.rooms];
275
+ const adults = [...rooms[roomIndex].adults];
276
+ if (adults.length <= 1) return;
277
+
278
+ adults.splice(travelerIndex, 1);
279
+ rooms[roomIndex] = { ...rooms[roomIndex], adults };
280
+ formik.setFieldValue('rooms', rooms);
281
+ };
282
+
283
+ const handleAddRoom = () => {
284
+ const rooms = [...formik.values.rooms];
285
+ rooms.push({ adults: [{ id: Date.now(), firstName: '', lastName: '', birthDate: '', gender: '' } as Traveler], children: [] });
286
+ formik.setFieldValue('rooms', rooms);
287
+ };
288
+
289
+ const handleRemoveRoom = (roomIndex: number) => {
290
+ const rooms = [...formik.values.rooms];
291
+ rooms.splice(roomIndex, 1);
292
+ formik.setFieldValue('rooms', rooms);
293
+ };
294
+
295
+ const renderGenderControl = (name: string, value: Traveler) => (
296
+ <div className={buildClassName(['form__group', hasVisibleError(name) && 'form__group--error'])}>
297
+ <label className="form__label">{translations.TRAVELERS_FORM.GENDER_ID} *</label>
298
+ <div className="radiobutton-group">
299
+ {[
300
+ ['m', translations.TRAVELERS_FORM.MALE_GENDER],
301
+ ['f', translations.TRAVELERS_FORM.FEMALE_GENDER]
302
+ ].map(([gender, label]) => (
303
+ <div className="radiobutton" key={gender}>
304
+ <label className="radiobutton__label">
305
+ <input
306
+ type="radio"
307
+ className="radiobutton__input"
308
+ name={name}
309
+ onChange={formik.handleChange}
310
+ onBlur={formik.handleBlur}
311
+ value={gender}
312
+ checked={value.gender === gender}
313
+ />
314
+ {label}
315
+ </label>
316
+ </div>
317
+ ))}
318
+ </div>
319
+ </div>
320
+ );
321
+
322
+ const getControl = (type: string, value: Traveler, name: string) => {
323
+ switch (type) {
324
+ case 'gender':
325
+ return <GenderControl translations={translations} value={value} formik={formik} name={name} />;
326
+ case 'firstName':
327
+ return (
328
+ <LabeledInput
329
+ hasError={hasVisibleError(name)}
330
+ extraClassName="form__group--md-33"
331
+ label={translations.TRAVELERS_FORM.FIRST_NAME}
332
+ required
333
+ name={name}
334
+ onChange={formik.handleChange}
335
+ onBlur={formik.handleBlur}
336
+ value={value.firstName}
337
+ />
338
+ );
339
+ case 'lastName':
340
+ return (
341
+ <LabeledInput
342
+ hasError={hasVisibleError(name)}
343
+ extraClassName="form__group--md-33"
344
+ label={translations.TRAVELERS_FORM.LAST_NAME}
345
+ required
346
+ name={name}
347
+ onChange={formik.handleChange}
348
+ onBlur={formik.handleBlur}
349
+ value={value.lastName}
350
+ />
351
+ );
352
+ case 'birthDate':
353
+ return (
354
+ <LabeledInput
355
+ type="date"
356
+ hasError={hasVisibleError(name)}
357
+ extraClassName="form__group--md-33"
358
+ label={translations.TRAVELERS_FORM.BIRTHDATE}
359
+ required
360
+ name={name}
361
+ onChange={formik.handleChange}
362
+ onBlur={formik.handleBlur}
363
+ value={value.birthDate}
364
+ />
365
+ );
366
+ case 'country':
367
+ return (
368
+ <LabeledSelect
369
+ hasError={hasVisibleError('country')}
370
+ label={translations.TRAVELERS_FORM.COUNTRY}
371
+ required
372
+ name="country"
373
+ onChange={formik.handleChange}
374
+ onBlur={formik.handleBlur}
375
+ value={formik.values.country}
376
+ options={countryOptions}
377
+ />
378
+ );
379
+ case 'phone':
380
+ return (
381
+ <PhoneInput
382
+ countries={travellersSettings?.countries ?? []}
383
+ countryIso2={formik.values.country}
384
+ hasError={hasVisibleError('phone')}
385
+ label={translations.TRAVELERS_FORM.PHONE}
386
+ required
387
+ name="phone"
388
+ onChange={formik.handleChange}
389
+ onBlur={formik.handleBlur}
390
+ value={formik.values.phone}
391
+ />
392
+ );
393
+ case 'email':
394
+ return (
395
+ <>
396
+ <LabeledInput
397
+ type="email"
398
+ hasError={hasVisibleError('email')}
399
+ extraClassName="form__group--md-33"
400
+ label={translations.TRAVELERS_FORM.EMAIL}
401
+ required
402
+ name="email"
403
+ onChange={formik.handleChange}
404
+ onBlur={formik.handleBlur}
405
+ value={formik.values.email}
406
+ />
407
+ <LabeledInput
408
+ type="email"
409
+ hasError={hasVisibleError('emailConfirmation')}
410
+ extraClassName="form__group--md-33"
411
+ label={translations.TRAVELERS_FORM.REPEAT_EMAIL}
412
+ required
413
+ name="emailConfirmation"
414
+ onChange={formik.handleChange}
415
+ onBlur={formik.handleBlur}
416
+ value={formik.values.emailConfirmation}
417
+ />
418
+ </>
419
+ );
420
+ case 'street':
421
+ return (
422
+ <LabeledInput
423
+ hasError={hasVisibleError('street')}
424
+ extraClassName="form__group--50 form__group--sm-60"
425
+ label={translations.TRAVELERS_FORM.STREET}
426
+ required
427
+ name="street"
428
+ onChange={formik.handleChange}
429
+ onBlur={formik.handleBlur}
430
+ value={formik.values.street}
431
+ />
432
+ );
433
+
434
+ case 'houseNumber':
435
+ return (
436
+ <LabeledInput
437
+ hasError={hasVisibleError('houseNumber')}
438
+ extraClassName="form__group--30 form__group--sm-20"
439
+ label={translations.TRAVELERS_FORM.HOUSE_NUMBER}
440
+ required
441
+ name="houseNumber"
442
+ onChange={formik.handleChange}
443
+ onBlur={formik.handleBlur}
444
+ value={formik.values.houseNumber}
445
+ />
446
+ );
447
+
448
+ case 'box':
449
+ return (
450
+ <LabeledInput
451
+ hasError={hasVisibleError('box')}
452
+ extraClassName="form__group--20"
453
+ label={translations.TRAVELERS_FORM.POST_BOX}
454
+ name="box"
455
+ onChange={formik.handleChange}
456
+ onBlur={formik.handleBlur}
457
+ value={formik.values.box}
458
+ />
459
+ );
460
+
461
+ case 'zipCode':
462
+ return (
463
+ <LabeledInput
464
+ hasError={hasVisibleError('zipCode')}
465
+ extraClassName="form__group--40 form__group--sm-20"
466
+ label={translations.TRAVELERS_FORM.ZIPCODE}
467
+ required
468
+ name="zipCode"
469
+ onChange={formik.handleChange}
470
+ onBlur={formik.handleBlur}
471
+ value={formik.values.zipCode}
472
+ />
473
+ );
474
+
475
+ case 'place':
476
+ return (
477
+ <LabeledInput
478
+ hasError={hasVisibleError('place')}
479
+ extraClassName="form__group--60 form__group--sm-40"
480
+ label={translations.TRAVELERS_FORM.CITY}
481
+ required
482
+ name="place"
483
+ onChange={formik.handleChange}
484
+ onBlur={formik.handleBlur}
485
+ value={formik.values.place}
486
+ />
487
+ );
488
+ default:
489
+ return null;
490
+ }
491
+ };
492
+
493
+ const renderRoomLabel = (room: TravelersFormValues['rooms'][number]) =>
494
+ compact([
495
+ room.adults.length,
496
+ room.adults.length === 1 && ` ${translations.TRAVELERS_FORM.ADULT}`,
497
+ room.adults.length > 1 && ` ${translations.TRAVELERS_FORM.ADULTS}`,
498
+ room.adults?.length && room.children?.length && ', ',
499
+ room.children.length,
500
+ room.children.length === 1 && ` ${translations.TRAVELERS_FORM.CHILD}`,
501
+ room.children.length > 1 && ` ${translations.TRAVELERS_FORM.CHILDREN}`
502
+ ]).join('');
503
+
504
+ const renderTravelerFields = (travelerValues: Traveler, namePrefix: string, isAdult: boolean, roomIndex: number, travelerIndex: number) => {
505
+ if (useCompactForm) {
506
+ return (
507
+ <div className="form__row">
508
+ <LabeledInput
509
+ hasError={hasVisibleError(`${namePrefix}.age`)}
510
+ extraClassName="form__group--md-33"
511
+ label={translations.TRAVELERS_FORM.AGE}
512
+ required
513
+ name={`${namePrefix}.age`}
514
+ onChange={formik.handleChange}
515
+ onBlur={formik.handleBlur}
516
+ value={travelerValues.age}
517
+ />
518
+ </div>
519
+ );
520
+ }
521
+
522
+ if (travellersSettings?.formFields?.length) {
523
+ return (
524
+ <div className="travelers-form__grid">
525
+ {travellersSettings.formFields.map((field, index) => (
526
+ <div key={index} className={`control control--${field.type}`}>
527
+ {getControl(field.type, travelerValues, `${namePrefix}.${field.type}`)}
528
+ </div>
529
+ ))}
530
+ </div>
531
+ );
532
+ }
533
+
534
+ return (
535
+ <>
536
+ <div className="form__row">{renderGenderControl(`${namePrefix}.gender`, travelerValues)}</div>
537
+ <div className="form__row">
538
+ <LabeledInput
539
+ hasError={hasVisibleError(`${namePrefix}.firstName`)}
540
+ extraClassName="form__group--md-33"
541
+ label={translations.TRAVELERS_FORM.FIRST_NAME}
542
+ required
543
+ name={`${namePrefix}.firstName`}
544
+ onChange={formik.handleChange}
545
+ onBlur={formik.handleBlur}
546
+ value={travelerValues.firstName}
547
+ />
548
+ <LabeledInput
549
+ hasError={hasVisibleError(`${namePrefix}.lastName`)}
550
+ extraClassName="form__group--md-33"
551
+ label={translations.TRAVELERS_FORM.LAST_NAME}
552
+ required
553
+ name={`${namePrefix}.lastName`}
554
+ onChange={formik.handleChange}
555
+ onBlur={formik.handleBlur}
556
+ value={travelerValues.lastName}
557
+ />
558
+ <LabeledInput
559
+ type="date"
560
+ hasError={hasVisibleError(`${namePrefix}.birthDate`)}
561
+ extraClassName="form__group--md-33"
562
+ label={translations.TRAVELERS_FORM.BIRTHDATE}
563
+ required
564
+ name={`${namePrefix}.birthDate`}
565
+ onChange={formik.handleChange}
566
+ onBlur={formik.handleBlur}
567
+ value={travelerValues.birthDate}
568
+ />
569
+ </div>
570
+ {travelersFirstStep && isAdult && formik.values.rooms[roomIndex].adults.length > 1 && (
571
+ <button type="button" className="cta cta--secondary" onClick={() => handleRemoveTraveler(roomIndex, travelerIndex)}>
572
+ {translations.TRAVELERS_FORM.REMOVE_TRAVELER}
573
+ </button>
574
+ )}
575
+ </>
576
+ );
577
+ };
578
+
579
+ return (
580
+ <form
581
+ className="form form__travelers"
582
+ name="booking--travellers"
583
+ id="booking--travellers"
584
+ noValidate
585
+ onSubmit={formik.handleSubmit}
586
+ onReset={formik.handleReset}>
587
+ <div className="form__travelers__wrapper">
588
+ {formik.values.rooms.map((room, roomIndex) => (
589
+ <div key={roomIndex}>
590
+ {formik.values.rooms.length > 1 && (
591
+ <div className="form__region">
592
+ <div className="form__region-header">
593
+ <h5 className="form__region-heading">
594
+ {translations.SHARED.ROOM} {roomIndex + 1}
595
+ </h5>
596
+ <p className="form__region-label">{renderRoomLabel(room)}</p>
597
+ </div>
598
+ {!useCompactForm && travelersFirstStep && formik.values.rooms.length > 1 && (
599
+ <button type="button" className="cta cta--secondary" onClick={() => handleRemoveRoom(roomIndex)}>
600
+ Verwijder reisgezelschap
601
+ </button>
602
+ )}
603
+ </div>
604
+ )}
605
+
606
+ {room.adults.map((travelerValues, index) => (
607
+ <div className="form__region" key={travelerValues.id}>
608
+ <div className="form__region-header">
609
+ <h5 className="form__region-heading">
610
+ {translations.TRAVELERS_FORM.TRAVELER} {index + 1}
611
+ </h5>
612
+ <p className="form__region-label">{translations.TRAVELERS_FORM.ADULT}</p>
613
+ <div className="radiobutton">
614
+ <label className="radiobutton__label">
615
+ <input
616
+ type="radio"
617
+ name="mainBookerId"
618
+ onChange={handleMainBookerChange}
619
+ onBlur={formik.handleBlur}
620
+ value={travelerValues.id}
621
+ checked={formik.values.mainBookerId === travelerValues.id}
622
+ className="radiobutton__input"
623
+ />
624
+ {translations.TRAVELERS_FORM.MAIN_BOOKER}
625
+ </label>
626
+ </div>
627
+ </div>
628
+ {renderTravelerFields(travelerValues, `rooms[${roomIndex}].adults[${index}]`, true, roomIndex, index)}
629
+ </div>
630
+ ))}
631
+
632
+ {room.children.map((travelerValues, index) => (
633
+ <div className="form__region" key={travelerValues.id}>
634
+ <div className="form__region-header">
635
+ <h5 className="form__region-heading">
636
+ {translations.TRAVELERS_FORM.TRAVELER} {room.adults.length + index + 1}
637
+ </h5>
638
+ <p className="form__region-label">{translations.TRAVELERS_FORM.CHILD}</p>
639
+ </div>
640
+ {renderTravelerFields(travelerValues, `rooms[${roomIndex}].children[${index}]`, false, roomIndex, index)}
641
+ </div>
642
+ ))}
643
+
644
+ {!useCompactForm && travelersFirstStep && (
645
+ <div className="form__region">
646
+ <button type="button" className="cta cta--select" onClick={() => handleAddTraveler(roomIndex)}>
647
+ {translations.TRAVELERS_FORM.ADD_TRAVELER}
648
+ </button>
649
+ </div>
650
+ )}
651
+ </div>
652
+ ))}
653
+
654
+ {!useCompactForm && (bookingType !== 'b2b' || travellersSettings?.mainBookerFormFields?.length) ? (
655
+ <div className="form__region">
656
+ <div className="form__region-header">
657
+ <h5 className="form__region-heading">{translations.TRAVELERS_FORM.MAIN_BOOKER}</h5>
658
+ <p className="form__region-label">
659
+ {compact([
660
+ compact([mainBooker?.firstName, mainBooker?.lastName]).join(' '),
661
+ mainBooker?.birthDate && format(parse(mainBooker.birthDate, 'yyyy-MM-dd', new Date()), 'dd-MM-yyyy')
662
+ ]).join(', ')}
663
+ </p>
664
+ </div>
665
+ {travellersSettings?.mainBookerFormFields?.length ? (
666
+ <div className="main-booker-form__grid">
667
+ {travellersSettings.mainBookerFormFields.map((field, index) => (
668
+ <div key={index} className={`control control--${field.type}`}>
669
+ {getControl(field.type, {} as Traveler, field.type)}
670
+ </div>
671
+ ))}
672
+ </div>
673
+ ) : (
674
+ <>
675
+ <div className="form__twocolumn">
676
+ <div className="form__twocolumn-column">
677
+ <div className="form__row">
678
+ <LabeledInput
679
+ hasError={hasVisibleError('street')}
680
+ extraClassName="form__group--50 form__group--sm-60"
681
+ label={translations.TRAVELERS_FORM.STREET}
682
+ required
683
+ name="street"
684
+ onChange={formik.handleChange}
685
+ onBlur={formik.handleBlur}
686
+ value={formik.values.street}
687
+ />
688
+ <LabeledInput
689
+ hasError={hasVisibleError('houseNumber')}
690
+ extraClassName="form__group--30 form__group--sm-20"
691
+ label={translations.TRAVELERS_FORM.HOUSE_NUMBER}
692
+ required
693
+ name="houseNumber"
694
+ onChange={formik.handleChange}
695
+ onBlur={formik.handleBlur}
696
+ value={formik.values.houseNumber}
697
+ />
698
+ <LabeledInput
699
+ hasError={hasVisibleError('box')}
700
+ extraClassName="form__group--20"
701
+ label={translations.TRAVELERS_FORM.POST_BOX}
702
+ name="box"
703
+ onChange={formik.handleChange}
704
+ onBlur={formik.handleBlur}
705
+ value={formik.values.box}
706
+ />
707
+ </div>
708
+ </div>
709
+ <div className="form__twocolumn-column">
710
+ <div className="form__row">
711
+ <LabeledInput
712
+ hasError={hasVisibleError('zipCode')}
713
+ extraClassName="form__group--40 form__group--sm-20"
714
+ label={translations.TRAVELERS_FORM.ZIPCODE}
715
+ required
716
+ name="zipCode"
717
+ onChange={formik.handleChange}
718
+ onBlur={formik.handleBlur}
719
+ value={formik.values.zipCode}
720
+ />
721
+ <LabeledInput
722
+ hasError={hasVisibleError('place')}
723
+ extraClassName="form__group--60 form__group--sm-40"
724
+ label={translations.TRAVELERS_FORM.CITY}
725
+ required
726
+ name="place"
727
+ onChange={formik.handleChange}
728
+ onBlur={formik.handleBlur}
729
+ value={formik.values.place}
730
+ />
731
+ <LabeledSelect
732
+ hasError={hasVisibleError('country')}
733
+ extraClassName="form__group--sm-40"
734
+ label={translations.TRAVELERS_FORM.COUNTRY}
735
+ required
736
+ name="country"
737
+ onChange={formik.handleChange}
738
+ onBlur={formik.handleBlur}
739
+ value={formik.values.country}
740
+ options={countryOptions}
741
+ />
742
+ </div>
743
+ </div>
744
+ </div>
745
+ <div className="form__row">
746
+ <LabeledInput
747
+ hasError={hasVisibleError('phone')}
748
+ extraClassName="form__group--md-33"
749
+ label={translations.TRAVELERS_FORM.PHONE}
750
+ required
751
+ name="phone"
752
+ onChange={formik.handleChange}
753
+ onBlur={formik.handleBlur}
754
+ value={formik.values.phone}
755
+ />
756
+ <LabeledInput
757
+ type="email"
758
+ hasError={hasVisibleError('email')}
759
+ extraClassName="form__group--md-33"
760
+ label={translations.TRAVELERS_FORM.EMAIL}
761
+ required
762
+ name="email"
763
+ onChange={formik.handleChange}
764
+ onBlur={formik.handleBlur}
765
+ value={formik.values.email}
766
+ />
767
+ <LabeledInput
768
+ type="email"
769
+ hasError={hasVisibleError('emailConfirmation')}
770
+ extraClassName="form__group--md-33"
771
+ label={translations.TRAVELERS_FORM.REPEAT_EMAIL}
772
+ required
773
+ name="emailConfirmation"
774
+ onChange={formik.handleChange}
775
+ onBlur={formik.handleBlur}
776
+ value={formik.values.emailConfirmation}
777
+ />
778
+ </div>
779
+ </>
780
+ )}
781
+ </div>
782
+ ) : !useCompactForm ? (
783
+ <div className="form__region">
784
+ <div className="form__row">
785
+ <LabeledInput
786
+ hasError={hasVisibleError('phone')}
787
+ extraClassName="form__group--md-33"
788
+ label={translations.TRAVELERS_FORM.PHONE}
789
+ required
790
+ name="phone"
791
+ onChange={formik.handleChange}
792
+ onBlur={formik.handleBlur}
793
+ value={formik.values.phone}
794
+ />
795
+ </div>
796
+ </div>
797
+ ) : null}
798
+
799
+ {!useCompactForm && showAgentSelection && (
800
+ <div className="form__region">
801
+ <div className="form__region-header">
802
+ <h5 className="form__region-heading">{translations.TRAVELERS_FORM.BOOK_WITH_AGENT}</h5>
803
+ <div className="checkbox" id="cbxChooseOffice">
804
+ <label className="checkbox__label">
805
+ <input
806
+ type="checkbox"
807
+ name="booking--mainbooker"
808
+ checked={showAgents}
809
+ onChange={() => toggleAgent(!showAgents)}
810
+ className="checkbox__input"
811
+ />
812
+ {translations.TRAVELERS_FORM.CHOOSE_OFFICE}
813
+ </label>
814
+ </div>
815
+ </div>
816
+ {showAgents && (
817
+ <div className="form__row form__row--choose-office">
818
+ <div className={buildClassName(['form__group', 'form__group--icon', hasVisibleError('travelAgentId') && 'form__group--error'])}>
819
+ <TypeAheadInput
820
+ value={formik.values.travelAgentName}
821
+ options={filteredAgents}
822
+ onChange={handleAgentChange}
823
+ onSelect={handleAgentSelect}
824
+ onClear={handleAgentClear}
825
+ name="travelAgentName"
826
+ placeholder={translations.TRAVELERS_FORM.CHOOSE_AGENT_PLACEHOLDER}
827
+ />
828
+ </div>
829
+ </div>
830
+ )}
831
+ </div>
832
+ )}
833
+ </div>
834
+
835
+ {!useCompactForm && errorKeys.length > 0 && (
836
+ <div className="form__region form__region--errors">
837
+ <div className="form__row">
838
+ <div className="form__group">
839
+ <p className="form__error-heading">{translations.TRAVELERS_FORM.VALIDATION_MESSAGE}:</p>
840
+ <ul className="list">
841
+ {errorKeys.map((key) => (
842
+ <li key={key}>{get(flatErrors, key)}</li>
843
+ ))}
844
+ </ul>
845
+ </div>
846
+ </div>
847
+ </div>
848
+ )}
849
+
850
+ {travelersFirstStep && (
851
+ <div className="booking__navigator">
852
+ <button type="button" className="cta cta--select" onClick={handleAddRoom}>
853
+ {translations.TRAVELERS_FORM.ADD_ROOM}
854
+ </button>
855
+ </div>
856
+ )}
857
+
858
+ <div className="booking__navigator">
859
+ {renderPreviousButton?.()}
860
+ <button type="submit" title={translations.STEPS.NEXT} className={'cta' + (isUnavailable ? ' cta--disabled' : '')}>
861
+ {translations.STEPS.NEXT}
862
+ </button>
863
+ </div>
864
+ </form>
865
+ );
866
+ };
867
+
868
+ export default SharedTravelersForm;