@qite/tide-booking-component 1.4.110 → 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 (53) hide show
  1. package/build/build-cjs/index.js +2316 -1555
  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 +2213 -1453
  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 +192 -11
  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 +42 -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/shared-confirmation.tsx +105 -0
  36. package/src/shared/booking/summary.tsx +380 -0
  37. package/src/shared/booking/travelers-form.tsx +870 -0
  38. package/src/shared/components/flyin/flyin.tsx +8 -9
  39. package/src/shared/components/flyin/packaging-flights-flyin.tsx +4 -4
  40. package/src/shared/utils/booking-summary.tsx +46 -0
  41. package/src/shared/utils/tide-api-utils.ts +2 -2
  42. package/styles/components/_dropdown.scss +5 -0
  43. package/styles/components/_flyin.scss +43 -0
  44. package/styles/components/_search.scss +5 -0
  45. /package/build/build-cjs/src/shared/booking/{BookingPanel.d.ts → booking-panel.d.ts} +0 -0
  46. /package/build/build-cjs/src/shared/booking/{Sidebar.d.ts → shared-sidebar.d.ts} +0 -0
  47. /package/build/build-cjs/src/shared/booking/{StepIndicators.d.ts → step-indicators.d.ts} +0 -0
  48. /package/build/build-esm/src/shared/booking/{BookingPanel.d.ts → booking-panel.d.ts} +0 -0
  49. /package/build/build-esm/src/shared/booking/{Sidebar.d.ts → shared-sidebar.d.ts} +0 -0
  50. /package/build/build-esm/src/shared/booking/{StepIndicators.d.ts → step-indicators.d.ts} +0 -0
  51. /package/src/shared/booking/{BookingPanel.tsx → booking-panel.tsx} +0 -0
  52. /package/src/shared/booking/{Sidebar.tsx → shared-sidebar.tsx} +0 -0
  53. /package/src/shared/booking/{StepIndicators.tsx → step-indicators.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;