@qite/tide-booking-component 1.4.104 → 1.4.106
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.
- package/build/build-cjs/index.js +1312 -187
- package/build/build-cjs/src/search-results/components/excursions/day-by-day-excursions.d.ts +4 -0
- package/build/build-cjs/src/search-results/components/excursions/excursion-details.d.ts +3 -0
- package/build/build-cjs/src/search-results/components/excursions/excursion-results.d.ts +8 -0
- package/build/build-cjs/src/search-results/store/search-results-selectors.d.ts +122 -0
- package/build/build-cjs/src/search-results/store/search-results-slice.d.ts +30 -2
- package/build/build-cjs/src/search-results/types.d.ts +27 -1
- package/build/build-cjs/src/search-results/utils/query-utils.d.ts +1 -0
- package/build/build-cjs/src/shared/components/flyin/flyin.d.ts +2 -1
- package/build/build-cjs/src/shared/utils/localization-util.d.ts +3 -0
- package/build/build-cjs/src/shared/utils/tide-api-utils.d.ts +6 -0
- package/build/build-esm/index.js +1304 -187
- package/build/build-esm/src/search-results/components/excursions/day-by-day-excursions.d.ts +4 -0
- package/build/build-esm/src/search-results/components/excursions/excursion-details.d.ts +3 -0
- package/build/build-esm/src/search-results/components/excursions/excursion-results.d.ts +8 -0
- package/build/build-esm/src/search-results/store/search-results-selectors.d.ts +122 -0
- package/build/build-esm/src/search-results/store/search-results-slice.d.ts +30 -2
- package/build/build-esm/src/search-results/types.d.ts +27 -1
- package/build/build-esm/src/search-results/utils/query-utils.d.ts +1 -0
- package/build/build-esm/src/shared/components/flyin/flyin.d.ts +2 -1
- package/build/build-esm/src/shared/utils/localization-util.d.ts +3 -0
- package/build/build-esm/src/shared/utils/tide-api-utils.d.ts +6 -0
- package/package.json +2 -2
- package/src/search-results/components/excursions/day-by-day-excursions.tsx +169 -0
- package/src/search-results/components/excursions/excursion-details.tsx +340 -0
- package/src/search-results/components/excursions/excursion-results.tsx +186 -0
- package/src/search-results/components/hotel/hotel-card.tsx +0 -3
- package/src/search-results/components/icon.tsx +1 -4
- package/src/search-results/components/itinerary/full-itinerary.tsx +161 -53
- package/src/search-results/components/itinerary/index.tsx +31 -7
- package/src/search-results/components/search-results-container/search-results-container.tsx +90 -28
- package/src/search-results/store/search-results-selectors.ts +11 -0
- package/src/search-results/store/search-results-slice.ts +46 -3
- package/src/search-results/types.ts +42 -1
- package/src/search-results/utils/query-utils.ts +1 -0
- package/src/shared/components/flyin/accommodation-flyin.tsx +4 -2
- package/src/shared/components/flyin/flights-flyin.tsx +3 -1
- package/src/shared/components/flyin/flyin.tsx +18 -6
- package/src/shared/components/flyin/group-tour-flyin.tsx +3 -1
- package/src/shared/translations/ar-SA.json +8 -2
- package/src/shared/translations/da-DK.json +8 -2
- package/src/shared/translations/de-DE.json +8 -2
- package/src/shared/translations/en-GB.json +8 -2
- package/src/shared/translations/es-ES.json +8 -2
- package/src/shared/translations/fr-BE.json +8 -2
- package/src/shared/translations/fr-FR.json +8 -2
- package/src/shared/translations/is-IS.json +8 -2
- package/src/shared/translations/it-IT.json +8 -2
- package/src/shared/translations/ja-JP.json +8 -2
- package/src/shared/translations/nl-BE.json +8 -2
- package/src/shared/translations/nl-NL.json +8 -2
- package/src/shared/translations/no-NO.json +8 -2
- package/src/shared/translations/pl-PL.json +8 -2
- package/src/shared/translations/pt-PT.json +8 -2
- package/src/shared/translations/sv-SE.json +8 -2
- package/src/shared/utils/localization-util.ts +14 -0
- package/src/shared/utils/tide-api-utils.ts +8 -0
- package/styles/components/_search.scss +11 -1
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import React, { useEffect, useMemo, useRef } from 'react';
|
|
1
|
+
import React, { useContext, useEffect, useMemo, useRef } from 'react';
|
|
2
2
|
import { useSelector } from 'react-redux';
|
|
3
3
|
import { SearchResultsRootState } from '../../store/search-results-store';
|
|
4
|
-
import { ClientPortalItinerary } from '@qite/tide-client';
|
|
4
|
+
import { ClientPortalItinerary, ClientPortalItineraryItem, ClientPortalItineraryNode } from '@qite/tide-client';
|
|
5
5
|
import Spinner from '../spinner/spinner';
|
|
6
|
+
import SearchResultsConfigurationContext from '../../search-results-configuration-context';
|
|
7
|
+
import { getTranslations } from '../../../shared/utils/localization-util';
|
|
8
|
+
import { format } from 'date-fns';
|
|
6
9
|
|
|
7
|
-
const formatNodeDate = (date?: Date | null) => {
|
|
10
|
+
const formatNodeDate = (date?: Date | string | null) => {
|
|
8
11
|
if (!date) return '';
|
|
9
12
|
|
|
10
13
|
try {
|
|
@@ -13,7 +16,7 @@ const formatNodeDate = (date?: Date | null) => {
|
|
|
13
16
|
day: '2-digit',
|
|
14
17
|
month: '2-digit',
|
|
15
18
|
year: 'numeric'
|
|
16
|
-
}).format(date);
|
|
19
|
+
}).format(new Date(date));
|
|
17
20
|
} catch {
|
|
18
21
|
return '';
|
|
19
22
|
}
|
|
@@ -25,7 +28,79 @@ const escapeHtml = (value?: string | null) => {
|
|
|
25
28
|
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
26
29
|
};
|
|
27
30
|
|
|
28
|
-
const
|
|
31
|
+
const getNodeDayRange = (node: ClientPortalItineraryNode) => {
|
|
32
|
+
const startDay = Number(node.startDay ?? 0);
|
|
33
|
+
const endDay = Number(node.endDay ?? startDay);
|
|
34
|
+
|
|
35
|
+
return { startDay, endDay };
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const getItemDuration = (item: ClientPortalItineraryItem) => {
|
|
39
|
+
const duration = Number(item.productDuration ?? 0);
|
|
40
|
+
|
|
41
|
+
return Number.isFinite(duration) && duration > 0 ? duration : 0;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const isAccommodationItem = (item: ClientPortalItineraryItem) => {
|
|
45
|
+
return getItemDuration(item) > 1 && item.templateName?.toLowerCase().includes('hotel');
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const getAccommodationName = (item: ClientPortalItineraryItem) => {
|
|
49
|
+
return item.title || 'Onbekende accommodatie';
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const getItemDeduplicationKey = (item: ClientPortalItineraryItem) => {
|
|
53
|
+
const templateName = item.templateName?.toLowerCase();
|
|
54
|
+
|
|
55
|
+
// if (templateName === 'hotel') {
|
|
56
|
+
// return [item.templateName, item.productCode, item.accommodationCode, item.regimeCode].filter(Boolean).join('|');
|
|
57
|
+
// }
|
|
58
|
+
|
|
59
|
+
// if (templateName === 'excursion') {
|
|
60
|
+
// return [item.templateName, item.productCode].filter(Boolean).join('|');
|
|
61
|
+
// }
|
|
62
|
+
|
|
63
|
+
return [item.templateName, item.productCode, item.title].filter(Boolean).join('|');
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const getUniqueItems = (items: ClientPortalItineraryItem[] = []) => {
|
|
67
|
+
const seen = new Set<string>();
|
|
68
|
+
|
|
69
|
+
return items.filter((item) => {
|
|
70
|
+
const key = getItemDeduplicationKey(item) || item.itemGuid;
|
|
71
|
+
|
|
72
|
+
if (seen.has(key)) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
seen.add(key);
|
|
77
|
+
return true;
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const findAccommodationForDay = (nodes: ClientPortalItineraryNode[], currentDay: number) => {
|
|
82
|
+
for (const node of nodes ?? []) {
|
|
83
|
+
const { startDay } = getNodeDayRange(node);
|
|
84
|
+
const uniqueItems = getUniqueItems(node.items ?? []);
|
|
85
|
+
|
|
86
|
+
for (const item of uniqueItems) {
|
|
87
|
+
if (!isAccommodationItem(item)) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const duration = getItemDuration(item);
|
|
92
|
+
const accommodationEndDay = startDay + duration - 1;
|
|
93
|
+
|
|
94
|
+
if (currentDay >= startDay && currentDay <= accommodationEndDay) {
|
|
95
|
+
return item;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return null;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const buildItineraryHtml = (itinerary: ClientPortalItinerary | null, translations: any) => {
|
|
29
104
|
if (!itinerary) {
|
|
30
105
|
return `
|
|
31
106
|
<div class="itinerary-shell">
|
|
@@ -34,12 +109,30 @@ const buildItineraryHtml = (itinerary?: ClientPortalItinerary | null) => {
|
|
|
34
109
|
`;
|
|
35
110
|
}
|
|
36
111
|
|
|
37
|
-
const
|
|
112
|
+
const allNodes = itinerary.nodes ?? [];
|
|
113
|
+
|
|
114
|
+
const nodesHtml = allNodes
|
|
38
115
|
.map((node) => {
|
|
39
|
-
const
|
|
116
|
+
const { startDay } = getNodeDayRange(node);
|
|
117
|
+
const day = node.startDate ? format(node.startDate, 'd') : null;
|
|
118
|
+
const month = node.startDate ? format(node.startDate, 'MMM') : null;
|
|
119
|
+
const uniqueItems = getUniqueItems(node.items ?? []);
|
|
120
|
+
|
|
121
|
+
const hasItems = uniqueItems.length > 0;
|
|
122
|
+
const hasAccommodationInCurrentNode = uniqueItems.some(isAccommodationItem);
|
|
123
|
+
const activeAccommodation = findAccommodationForDay(allNodes, startDay);
|
|
124
|
+
|
|
125
|
+
const accommodationBanner =
|
|
126
|
+
!hasAccommodationInCurrentNode && activeAccommodation
|
|
127
|
+
? `
|
|
128
|
+
<div class="itinerary-node__accommodation-banner">
|
|
129
|
+
Accommodatie voor deze dag: <strong>${escapeHtml(getAccommodationName(activeAccommodation))}</strong>
|
|
130
|
+
</div>
|
|
131
|
+
`
|
|
132
|
+
: '';
|
|
40
133
|
|
|
41
134
|
const itemsHtml = hasItems
|
|
42
|
-
?
|
|
135
|
+
? uniqueItems
|
|
43
136
|
.map(
|
|
44
137
|
(item) => `
|
|
45
138
|
<article class="itinerary-item" data-template="${escapeHtml(item.templateName)}">
|
|
@@ -48,19 +141,24 @@ const buildItineraryHtml = (itinerary?: ClientPortalItinerary | null) => {
|
|
|
48
141
|
`
|
|
49
142
|
)
|
|
50
143
|
.join('')
|
|
51
|
-
: `<div class="itinerary-node__empty"
|
|
144
|
+
: `<div class="itinerary-node__empty">${translations.ITINERARY.NO_ITEMS}</div>`;
|
|
52
145
|
|
|
53
146
|
return `
|
|
54
147
|
<section class="itinerary-node">
|
|
55
148
|
<header class="itinerary-node__header">
|
|
56
|
-
|
|
57
|
-
|
|
149
|
+
|
|
150
|
+
<div class="itinerary-node__day">
|
|
151
|
+
<p class="itinerary-node__day-day">${day}</p>
|
|
152
|
+
<p class="itinerary-node__day-month">${month}</p>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<div>
|
|
58
156
|
<h2 class="itinerary-node__title">${escapeHtml(node.title)}</h2>
|
|
59
|
-
<div class="itinerary-node__date">${escapeHtml(formatNodeDate(node.startDate))}</div>
|
|
60
157
|
</div>
|
|
61
158
|
</header>
|
|
62
159
|
|
|
63
160
|
<div class="itinerary-node__content">
|
|
161
|
+
${accommodationBanner}
|
|
64
162
|
${itemsHtml}
|
|
65
163
|
</div>
|
|
66
164
|
</section>
|
|
@@ -73,7 +171,7 @@ const buildItineraryHtml = (itinerary?: ClientPortalItinerary | null) => {
|
|
|
73
171
|
? `
|
|
74
172
|
<section class="itinerary-default-items">
|
|
75
173
|
<h2 class="itinerary-default-items__title">Algemene info</h2>
|
|
76
|
-
${itinerary.defaultItems
|
|
174
|
+
${getUniqueItems(itinerary.defaultItems)
|
|
77
175
|
.map(
|
|
78
176
|
(item) => `
|
|
79
177
|
<article class="itinerary-item" data-template="${escapeHtml(item.templateName)}">
|
|
@@ -103,31 +201,36 @@ interface FullItineraryProps {
|
|
|
103
201
|
}
|
|
104
202
|
|
|
105
203
|
const FullItinerary: React.FC<FullItineraryProps> = ({ isLoading }) => {
|
|
106
|
-
if (isLoading) {
|
|
107
|
-
return <Spinner />;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
204
|
const { itinerary } = useSelector((state: SearchResultsRootState) => state.searchResults);
|
|
205
|
+
const context = useContext(SearchResultsConfigurationContext);
|
|
206
|
+
const translations = getTranslations(context?.languageCode ?? 'en-GB');
|
|
207
|
+
|
|
111
208
|
const hostRef = useRef<HTMLDivElement | null>(null);
|
|
112
209
|
const shadowRootRef = useRef<ShadowRoot | null>(null);
|
|
113
210
|
|
|
114
|
-
const html = useMemo(() => buildItineraryHtml(itinerary as ClientPortalItinerary | null), [itinerary]);
|
|
211
|
+
const html = useMemo(() => buildItineraryHtml(itinerary as ClientPortalItinerary | null, translations), [itinerary, translations]);
|
|
115
212
|
|
|
116
213
|
useEffect(() => {
|
|
117
|
-
if (
|
|
214
|
+
if (isLoading) {
|
|
118
215
|
return;
|
|
119
216
|
}
|
|
120
217
|
|
|
121
|
-
|
|
122
|
-
|
|
218
|
+
const host = hostRef.current;
|
|
219
|
+
|
|
220
|
+
if (!host) {
|
|
221
|
+
return;
|
|
123
222
|
}
|
|
124
223
|
|
|
125
|
-
const shadowRoot =
|
|
224
|
+
const shadowRoot = host.shadowRoot ?? host.attachShadow({ mode: 'open' });
|
|
225
|
+
|
|
226
|
+
shadowRootRef.current = shadowRoot;
|
|
126
227
|
|
|
127
228
|
shadowRoot.innerHTML = `
|
|
128
229
|
<style>
|
|
129
230
|
:host {
|
|
130
231
|
all: initial;
|
|
232
|
+
display: block;
|
|
233
|
+
width: 100%;
|
|
131
234
|
}
|
|
132
235
|
|
|
133
236
|
*,
|
|
@@ -169,27 +272,31 @@ const FullItinerary: React.FC<FullItineraryProps> = ({ isLoading }) => {
|
|
|
169
272
|
.itinerary-node__header {
|
|
170
273
|
display: flex;
|
|
171
274
|
gap: 16px;
|
|
172
|
-
align-items:
|
|
173
|
-
padding: 20px 20px 16px;
|
|
275
|
+
align-items: center;
|
|
174
276
|
border-bottom: 1px solid #eef0f2;
|
|
175
277
|
background: #fafafa;
|
|
176
278
|
}
|
|
177
279
|
|
|
178
280
|
.itinerary-node__day {
|
|
179
|
-
flex: 0 0
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
color: #fff;
|
|
188
|
-
text-align: center;
|
|
281
|
+
flex: 50px 0 0;
|
|
282
|
+
display: flex;
|
|
283
|
+
flex-flow: column;
|
|
284
|
+
align-items: center;
|
|
285
|
+
justify-content: center;
|
|
286
|
+
background: #1f9470;
|
|
287
|
+
color: white;
|
|
288
|
+
border: 1.5px solid var(--tide-booking-color-secondary);
|
|
189
289
|
}
|
|
190
290
|
|
|
191
|
-
.itinerary-
|
|
192
|
-
|
|
291
|
+
.itinerary-node__day-month {
|
|
292
|
+
margin: 0px;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.itinerary-node__day-day {
|
|
296
|
+
margin: 0px;
|
|
297
|
+
font-weight: var(--tide-booking-search-results-label-date-month-font-weight);
|
|
298
|
+
color: var(--tide-booking-search-results-label-date-month-color);
|
|
299
|
+
font-size: 24px;
|
|
193
300
|
}
|
|
194
301
|
|
|
195
302
|
.itinerary-node__title {
|
|
@@ -198,18 +305,22 @@ const FullItinerary: React.FC<FullItineraryProps> = ({ isLoading }) => {
|
|
|
198
305
|
line-height: 1.25;
|
|
199
306
|
}
|
|
200
307
|
|
|
201
|
-
.itinerary-node__date {
|
|
202
|
-
margin-top: 6px;
|
|
203
|
-
font-size: 14px;
|
|
204
|
-
color: #6b7280;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
308
|
.itinerary-node__content {
|
|
208
309
|
display: grid;
|
|
209
310
|
gap: 20px;
|
|
210
311
|
padding: 20px;
|
|
211
312
|
}
|
|
212
313
|
|
|
314
|
+
.itinerary-node__accommodation-banner {
|
|
315
|
+
padding: 12px 16px;
|
|
316
|
+
border: 1px solid #dbeafe;
|
|
317
|
+
border-radius: 12px;
|
|
318
|
+
background: #eff6ff;
|
|
319
|
+
color: #1e3a8a;
|
|
320
|
+
font-size: 14px;
|
|
321
|
+
line-height: 1.4;
|
|
322
|
+
}
|
|
323
|
+
|
|
213
324
|
.itinerary-item {
|
|
214
325
|
display: block;
|
|
215
326
|
border-radius: 12px;
|
|
@@ -243,14 +354,6 @@ const FullItinerary: React.FC<FullItineraryProps> = ({ isLoading }) => {
|
|
|
243
354
|
.itinerary-shell {
|
|
244
355
|
padding: 16px;
|
|
245
356
|
}
|
|
246
|
-
|
|
247
|
-
.itinerary-node__header {
|
|
248
|
-
flex-direction: column;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
.itinerary-node__day {
|
|
252
|
-
min-width: auto;
|
|
253
|
-
}
|
|
254
357
|
}
|
|
255
358
|
|
|
256
359
|
${itinerary?.styleSheetBody ?? ''}
|
|
@@ -258,9 +361,14 @@ const FullItinerary: React.FC<FullItineraryProps> = ({ isLoading }) => {
|
|
|
258
361
|
|
|
259
362
|
${html}
|
|
260
363
|
`;
|
|
261
|
-
}, [html, itinerary?.styleSheetBody]);
|
|
262
|
-
|
|
263
|
-
return
|
|
364
|
+
}, [html, itinerary?.styleSheetBody, isLoading]);
|
|
365
|
+
|
|
366
|
+
return (
|
|
367
|
+
<>
|
|
368
|
+
{isLoading && <Spinner />}
|
|
369
|
+
<div ref={hostRef} style={{ display: isLoading ? 'none' : 'block' }} />
|
|
370
|
+
</>
|
|
371
|
+
);
|
|
264
372
|
};
|
|
265
373
|
|
|
266
374
|
export default FullItinerary;
|
|
@@ -102,6 +102,25 @@ const getSegmentTitle = (segment: PackagingEntryLine) => {
|
|
|
102
102
|
return segment.productName ?? segment.accommodationName;
|
|
103
103
|
};
|
|
104
104
|
|
|
105
|
+
const SERVICE_TYPE_PRIORITY: Record<number, number> = {
|
|
106
|
+
7: 0, // Flight
|
|
107
|
+
13: 1, // Transfer
|
|
108
|
+
3: 2, // Hotel
|
|
109
|
+
4: 3 // Excursion
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const getServiceTypePriority = (serviceType?: number) => {
|
|
113
|
+
return SERVICE_TYPE_PRIORITY[serviceType ?? -1] ?? 2;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const getDateOnlyTime = (date?: string | Date | null) => {
|
|
117
|
+
if (!date) return 0;
|
|
118
|
+
|
|
119
|
+
const parsedDate = new Date(date);
|
|
120
|
+
|
|
121
|
+
return new Date(parsedDate.getFullYear(), parsedDate.getMonth(), parsedDate.getDate()).getTime();
|
|
122
|
+
};
|
|
123
|
+
|
|
105
124
|
const Itinerary: React.FC<ItineraryProps> = ({ isOpen, handleSetIsOpen, isLoading, onEditAccommodation }) => {
|
|
106
125
|
const context = useContext(SearchResultsConfigurationContext);
|
|
107
126
|
const translations = getTranslations(context?.languageCode ?? 'en-GB');
|
|
@@ -111,16 +130,21 @@ const Itinerary: React.FC<ItineraryProps> = ({ isOpen, handleSetIsOpen, isLoadin
|
|
|
111
130
|
|
|
112
131
|
const sortedLines = useMemo(() => {
|
|
113
132
|
return [...(packagingEntry?.lines ?? [])].sort((a, b) => {
|
|
114
|
-
const
|
|
115
|
-
const
|
|
133
|
+
const dateA = getDateOnlyTime(a.from);
|
|
134
|
+
const dateB = getDateOnlyTime(b.from);
|
|
135
|
+
|
|
136
|
+
if (dateA !== dateB) {
|
|
137
|
+
return dateA - dateB;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const priorityA = getServiceTypePriority(a.serviceType);
|
|
141
|
+
const priorityB = getServiceTypePriority(b.serviceType);
|
|
116
142
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
return orderA - orderB;
|
|
143
|
+
if (priorityA !== priorityB) {
|
|
144
|
+
return priorityA - priorityB;
|
|
120
145
|
}
|
|
121
146
|
|
|
122
|
-
|
|
123
|
-
return new Date(a.from).getTime() - new Date(b.from).getTime();
|
|
147
|
+
return (a.order ?? Infinity) - (b.order ?? Infinity);
|
|
124
148
|
});
|
|
125
149
|
}, [packagingEntry]);
|
|
126
150
|
|
|
@@ -85,6 +85,7 @@ import {
|
|
|
85
85
|
} from '../../utils/search-results-utils';
|
|
86
86
|
import {
|
|
87
87
|
ACCOMMODATION_SERVICE_TYPE,
|
|
88
|
+
EXCURSION_SERVICE_TYPE,
|
|
88
89
|
FLIGHT_SERVICE_TYPE,
|
|
89
90
|
getDepartureAirportFromEntry,
|
|
90
91
|
getDestinationAirportFromEntry,
|
|
@@ -109,12 +110,14 @@ import {
|
|
|
109
110
|
selectUniqueOutwardFlights,
|
|
110
111
|
selectUniqueReturnFlights
|
|
111
112
|
} from '../../store/search-results-selectors';
|
|
113
|
+
import DayByDayExcursions from '../excursions/day-by-day-excursions';
|
|
112
114
|
|
|
113
115
|
type BuildPackagingEntryPartialArgs = {
|
|
114
116
|
sourceEntry: PackagingEntry | null | undefined;
|
|
115
117
|
selectedHotelCode: string | null | undefined;
|
|
116
118
|
accommodationResults: PackagingAccommodationResponse[];
|
|
117
119
|
selectedFlight: PackagingFlightResponse | null;
|
|
120
|
+
confirmedExcursionsByDay: Record<string, PackagingAccommodationResponse[]>;
|
|
118
121
|
seed: SearchSeed;
|
|
119
122
|
transactionId: string;
|
|
120
123
|
language: string;
|
|
@@ -148,7 +151,8 @@ const SearchResultsContainer: React.FC = () => {
|
|
|
148
151
|
transactionId,
|
|
149
152
|
flyInType,
|
|
150
153
|
itinerary,
|
|
151
|
-
packagingFlightResults
|
|
154
|
+
packagingFlightResults,
|
|
155
|
+
confirmedExcursionsByDay
|
|
152
156
|
} = useSelector((state: SearchResultsRootState) => state.searchResults);
|
|
153
157
|
|
|
154
158
|
const isMobile = useMediaQuery('(max-width: 1200px)');
|
|
@@ -1099,6 +1103,7 @@ const SearchResultsContainer: React.FC = () => {
|
|
|
1099
1103
|
selectedHotelCode: selectedPackagingAccoResultCode,
|
|
1100
1104
|
accommodationResults: packagingAccoResults,
|
|
1101
1105
|
selectedFlight: selectedCombinationFlight ?? null,
|
|
1106
|
+
confirmedExcursionsByDay,
|
|
1102
1107
|
seed,
|
|
1103
1108
|
transactionId: transactionId ?? context.packagingEntry?.transactionId ?? '',
|
|
1104
1109
|
language: context.languageCode ?? 'en-GB'
|
|
@@ -1118,6 +1123,7 @@ const SearchResultsContainer: React.FC = () => {
|
|
|
1118
1123
|
packagingAccoResults,
|
|
1119
1124
|
packagingAccoSearchDetails,
|
|
1120
1125
|
selectedCombinationFlight,
|
|
1126
|
+
confirmedExcursionsByDay,
|
|
1121
1127
|
transactionId,
|
|
1122
1128
|
dispatch
|
|
1123
1129
|
]);
|
|
@@ -1126,23 +1132,70 @@ const SearchResultsContainer: React.FC = () => {
|
|
|
1126
1132
|
|
|
1127
1133
|
const removeFlightLines = (lines: PackagingEntryLine[]) => lines.filter((line) => line.serviceType !== FLIGHT_SERVICE_TYPE);
|
|
1128
1134
|
|
|
1135
|
+
const removeExcursionLines = (lines: PackagingEntryLine[]) => lines.filter((line) => line.serviceType !== EXCURSION_SERVICE_TYPE);
|
|
1136
|
+
|
|
1129
1137
|
const buildAccommodationLinesFromSelection = (selectedHotel: PackagingAccommodationResponse, seed: SearchSeed): PackagingEntryLine[] => {
|
|
1130
|
-
|
|
1138
|
+
return buildPackagingAccommodationLines(selectedHotel, seed, ACCOMMODATION_SERVICE_TYPE);
|
|
1139
|
+
};
|
|
1140
|
+
|
|
1141
|
+
const buildExcursionLinesFromConfirmedDays = (confirmedExcursionsByDay: Record<string, PackagingAccommodationResponse[]>): PackagingEntryLine[] => {
|
|
1142
|
+
return Object.values(confirmedExcursionsByDay)
|
|
1143
|
+
.flat()
|
|
1144
|
+
.flatMap((excursion) => {
|
|
1145
|
+
const selectedOptions = excursion.rooms.flatMap((room) => room.options.filter((option) => option.isSelected));
|
|
1146
|
+
|
|
1147
|
+
const parentGuid = crypto.randomUUID();
|
|
1148
|
+
|
|
1149
|
+
return selectedOptions.map(
|
|
1150
|
+
(option, index) =>
|
|
1151
|
+
({
|
|
1152
|
+
guid: option.guid ?? crypto.randomUUID(),
|
|
1153
|
+
moment: '',
|
|
1154
|
+
parentGuid: index === 0 ? null : parentGuid,
|
|
1155
|
+
order: index,
|
|
1156
|
+
isChanged: true,
|
|
1157
|
+
from: excursion.fromDate,
|
|
1158
|
+
to: excursion.toDate,
|
|
1159
|
+
serviceType: EXCURSION_SERVICE_TYPE,
|
|
1160
|
+
productName: excursion.name,
|
|
1161
|
+
productCode: excursion.code,
|
|
1162
|
+
accommodationName: option.accommodationName,
|
|
1163
|
+
accommodationCode: option.accommodationCode,
|
|
1164
|
+
regimeName: option.regimeName,
|
|
1165
|
+
regimeCode: option.regimeCode,
|
|
1166
|
+
country: excursion.countryId ? { id: excursion.countryId, name: excursion.countryName, localizations: [] } : null,
|
|
1167
|
+
region: excursion.regionId ? { id: excursion.regionId, name: excursion.regionName, localizations: [] } : null,
|
|
1168
|
+
oord: excursion.oordId ? { id: excursion.oordId, name: excursion.oordName, localizations: [] } : null,
|
|
1169
|
+
location: excursion.locationId ? { id: excursion.locationId, name: excursion.locationName, localizations: [] } : null,
|
|
1170
|
+
longitude: excursion.longitude ?? null,
|
|
1171
|
+
latitude: excursion.latitude ?? null,
|
|
1172
|
+
pax: Array.isArray(option.paxIds)
|
|
1173
|
+
? option.paxIds.map((paxId, paxIndex) => ({
|
|
1174
|
+
paxId: paxId,
|
|
1175
|
+
room: 0,
|
|
1176
|
+
order: paxIndex
|
|
1177
|
+
}))
|
|
1178
|
+
: [],
|
|
1179
|
+
flightInformation: null
|
|
1180
|
+
} satisfies PackagingEntryLine)
|
|
1181
|
+
);
|
|
1182
|
+
});
|
|
1183
|
+
};
|
|
1184
|
+
|
|
1185
|
+
const buildPackagingAccommodationLines = (selectedItem: PackagingAccommodationResponse, seed: SearchSeed, serviceType: number): PackagingEntryLine[] => {
|
|
1186
|
+
if (!selectedItem) return [];
|
|
1131
1187
|
const parentGuid = crypto.randomUUID();
|
|
1132
1188
|
|
|
1133
|
-
return
|
|
1189
|
+
return selectedItem.rooms
|
|
1134
1190
|
.filter((room) => room.options.some((o) => o.isSelected))
|
|
1135
1191
|
.map((room, index) => {
|
|
1136
1192
|
const option = room.options.find((o) => o.isSelected)!;
|
|
1137
1193
|
|
|
1138
|
-
const pax =
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
order: paxIndex
|
|
1144
|
-
}))
|
|
1145
|
-
) ?? [];
|
|
1194
|
+
const pax = option.paxIds.map((p, paxIndex) => ({
|
|
1195
|
+
paxId: p,
|
|
1196
|
+
room: index,
|
|
1197
|
+
order: paxIndex
|
|
1198
|
+
}));
|
|
1146
1199
|
|
|
1147
1200
|
return {
|
|
1148
1201
|
guid: option.guid ?? crypto.randomUUID(),
|
|
@@ -1150,21 +1203,21 @@ const SearchResultsContainer: React.FC = () => {
|
|
|
1150
1203
|
parentGuid: index === 0 ? null : parentGuid,
|
|
1151
1204
|
order: index,
|
|
1152
1205
|
isChanged: true,
|
|
1153
|
-
from:
|
|
1154
|
-
to:
|
|
1155
|
-
serviceType:
|
|
1156
|
-
productName:
|
|
1157
|
-
productCode:
|
|
1206
|
+
from: selectedItem.fromDate,
|
|
1207
|
+
to: selectedItem.toDate,
|
|
1208
|
+
serviceType: serviceType,
|
|
1209
|
+
productName: selectedItem.name,
|
|
1210
|
+
productCode: selectedItem.code,
|
|
1158
1211
|
accommodationName: option.accommodationName,
|
|
1159
1212
|
accommodationCode: option.accommodationCode,
|
|
1160
1213
|
regimeName: option.regimeName,
|
|
1161
1214
|
regimeCode: option.regimeCode,
|
|
1162
|
-
country:
|
|
1163
|
-
region:
|
|
1164
|
-
oord:
|
|
1165
|
-
location:
|
|
1166
|
-
longitude:
|
|
1167
|
-
latitude:
|
|
1215
|
+
country: selectedItem.countryId ? { id: selectedItem.countryId, name: selectedItem.countryName, localizations: [] } : null,
|
|
1216
|
+
region: selectedItem.regionId ? { id: selectedItem.regionId, name: selectedItem.regionName, localizations: [] } : null,
|
|
1217
|
+
oord: selectedItem.oordId ? { id: selectedItem.oordId, name: selectedItem.oordName, localizations: [] } : null,
|
|
1218
|
+
location: selectedItem.locationId ? { id: selectedItem.locationId, name: selectedItem.locationName, localizations: [] } : null,
|
|
1219
|
+
longitude: selectedItem.longitude ?? null,
|
|
1220
|
+
latitude: selectedItem.latitude ?? null,
|
|
1168
1221
|
pax,
|
|
1169
1222
|
flightInformation: null
|
|
1170
1223
|
} satisfies PackagingEntryLine;
|
|
@@ -1298,6 +1351,7 @@ const SearchResultsContainer: React.FC = () => {
|
|
|
1298
1351
|
selectedHotelCode,
|
|
1299
1352
|
accommodationResults,
|
|
1300
1353
|
selectedFlight,
|
|
1354
|
+
confirmedExcursionsByDay,
|
|
1301
1355
|
seed,
|
|
1302
1356
|
transactionId,
|
|
1303
1357
|
language
|
|
@@ -1329,6 +1383,15 @@ const SearchResultsContainer: React.FC = () => {
|
|
|
1329
1383
|
}
|
|
1330
1384
|
}
|
|
1331
1385
|
|
|
1386
|
+
// excursions
|
|
1387
|
+
const excursionLines = buildExcursionLinesFromConfirmedDays(confirmedExcursionsByDay);
|
|
1388
|
+
|
|
1389
|
+
nextLines = removeExcursionLines(nextLines);
|
|
1390
|
+
|
|
1391
|
+
if (excursionLines.length) {
|
|
1392
|
+
nextLines = [...nextLines, ...excursionLines];
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1332
1395
|
nextLines = nextLines.map((line, index) => ({
|
|
1333
1396
|
...line,
|
|
1334
1397
|
order: index
|
|
@@ -1353,7 +1416,7 @@ const SearchResultsContainer: React.FC = () => {
|
|
|
1353
1416
|
return structuredClone(sourceEntry);
|
|
1354
1417
|
}
|
|
1355
1418
|
|
|
1356
|
-
let paxId =
|
|
1419
|
+
let paxId = 0;
|
|
1357
1420
|
|
|
1358
1421
|
const pax =
|
|
1359
1422
|
seed.rooms?.flatMap((room, roomIndex) =>
|
|
@@ -1498,8 +1561,6 @@ const SearchResultsContainer: React.FC = () => {
|
|
|
1498
1561
|
{context.searchConfiguration.qsmType === PortalQsmType.GroupTour && <GroupTourResults isLoading={isLoading} />}
|
|
1499
1562
|
|
|
1500
1563
|
{context.searchConfiguration.qsmType === PortalQsmType.AccommodationAndFlight && !context.packagingEntry && context.showFlightResults && (
|
|
1501
|
-
// bookingPackageDetails?.outwardFlights &&
|
|
1502
|
-
// <FlightResults flights={bookingPackageDetails?.outwardFlights} isDeparture={true} />
|
|
1503
1564
|
<>
|
|
1504
1565
|
<div className="search__results__label search__results__label--secondary">
|
|
1505
1566
|
<div className="search__results__label__date">
|
|
@@ -1555,9 +1616,9 @@ const SearchResultsContainer: React.FC = () => {
|
|
|
1555
1616
|
|
|
1556
1617
|
{context.showHotelAccommodationResults && !context.packagingEntry && <HotelAccommodationResults isLoading={isLoading} />}
|
|
1557
1618
|
|
|
1619
|
+
{context.searchConfiguration.qsmType === PortalQsmType.AccommodationAndFlight && !context.packagingEntry && <DayByDayExcursions />}
|
|
1620
|
+
|
|
1558
1621
|
{context.searchConfiguration.qsmType === PortalQsmType.AccommodationAndFlight && !context.packagingEntry && context.showFlightResults && (
|
|
1559
|
-
// bookingPackageDetails?.returnFlights &&
|
|
1560
|
-
// <FlightResults flights={bookingPackageDetails?.returnFlights} isDeparture={false} />
|
|
1561
1622
|
<>
|
|
1562
1623
|
<div className="search__results__label search__results__label--secondary">
|
|
1563
1624
|
<div className="search__results__label__date">
|
|
@@ -1597,7 +1658,7 @@ const SearchResultsContainer: React.FC = () => {
|
|
|
1597
1658
|
</>
|
|
1598
1659
|
)}
|
|
1599
1660
|
|
|
1600
|
-
{context.searchConfiguration.qsmType === PortalQsmType.AccommodationAndFlight && context.packagingEntry &&
|
|
1661
|
+
{context.searchConfiguration.qsmType === PortalQsmType.AccommodationAndFlight && context.packagingEntry && (
|
|
1601
1662
|
<FullItinerary isLoading={itineraryIsLoading} />
|
|
1602
1663
|
)}
|
|
1603
1664
|
</div>
|
|
@@ -1613,6 +1674,7 @@ const SearchResultsContainer: React.FC = () => {
|
|
|
1613
1674
|
flyInType={flyInType}
|
|
1614
1675
|
isPackageEditFlow={!!context.packagingEntry}
|
|
1615
1676
|
sortByTypes={sortByTypes}
|
|
1677
|
+
activeSearchSeed={activeSearchSeed}
|
|
1616
1678
|
/>
|
|
1617
1679
|
</>
|
|
1618
1680
|
)}
|
|
@@ -5,6 +5,17 @@ import { PackagingFlightResponse } from '@qite/tide-client';
|
|
|
5
5
|
|
|
6
6
|
const selectSearchResultsState = (state: SearchResultsRootState) => state.searchResults;
|
|
7
7
|
|
|
8
|
+
export const selectPackagingAccoResults = createSelector([selectSearchResultsState], (state) => state.packagingAccoResults);
|
|
9
|
+
export const selectSelectedPackagingAccoResultCode = createSelector([selectSearchResultsState], (state) => state.selectedPackagingAccoResultCode);
|
|
10
|
+
export const selectSelectedPackagingAccoResult = createSelector(
|
|
11
|
+
[selectPackagingAccoResults, selectSelectedPackagingAccoResultCode],
|
|
12
|
+
(packagingAccoResults, selectedPackagingAccoResultCode) => {
|
|
13
|
+
if (!selectedPackagingAccoResultCode) return null;
|
|
14
|
+
|
|
15
|
+
return packagingAccoResults.find((accoResult) => accoResult.code === selectedPackagingAccoResultCode) ?? null;
|
|
16
|
+
}
|
|
17
|
+
);
|
|
18
|
+
|
|
8
19
|
export const selectPackagingFlightResults = createSelector([selectSearchResultsState], (state) => state.packagingFlightResults);
|
|
9
20
|
export const selectFilteredPackagingFlightResults = createSelector([selectSearchResultsState], (state) => state.filteredPackagingFlightResults);
|
|
10
21
|
|