@qite/tide-booking-component 1.4.109 → 1.4.111

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