@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.
Files changed (58) hide show
  1. package/build/build-cjs/index.js +1312 -187
  2. package/build/build-cjs/src/search-results/components/excursions/day-by-day-excursions.d.ts +4 -0
  3. package/build/build-cjs/src/search-results/components/excursions/excursion-details.d.ts +3 -0
  4. package/build/build-cjs/src/search-results/components/excursions/excursion-results.d.ts +8 -0
  5. package/build/build-cjs/src/search-results/store/search-results-selectors.d.ts +122 -0
  6. package/build/build-cjs/src/search-results/store/search-results-slice.d.ts +30 -2
  7. package/build/build-cjs/src/search-results/types.d.ts +27 -1
  8. package/build/build-cjs/src/search-results/utils/query-utils.d.ts +1 -0
  9. package/build/build-cjs/src/shared/components/flyin/flyin.d.ts +2 -1
  10. package/build/build-cjs/src/shared/utils/localization-util.d.ts +3 -0
  11. package/build/build-cjs/src/shared/utils/tide-api-utils.d.ts +6 -0
  12. package/build/build-esm/index.js +1304 -187
  13. package/build/build-esm/src/search-results/components/excursions/day-by-day-excursions.d.ts +4 -0
  14. package/build/build-esm/src/search-results/components/excursions/excursion-details.d.ts +3 -0
  15. package/build/build-esm/src/search-results/components/excursions/excursion-results.d.ts +8 -0
  16. package/build/build-esm/src/search-results/store/search-results-selectors.d.ts +122 -0
  17. package/build/build-esm/src/search-results/store/search-results-slice.d.ts +30 -2
  18. package/build/build-esm/src/search-results/types.d.ts +27 -1
  19. package/build/build-esm/src/search-results/utils/query-utils.d.ts +1 -0
  20. package/build/build-esm/src/shared/components/flyin/flyin.d.ts +2 -1
  21. package/build/build-esm/src/shared/utils/localization-util.d.ts +3 -0
  22. package/build/build-esm/src/shared/utils/tide-api-utils.d.ts +6 -0
  23. package/package.json +2 -2
  24. package/src/search-results/components/excursions/day-by-day-excursions.tsx +169 -0
  25. package/src/search-results/components/excursions/excursion-details.tsx +340 -0
  26. package/src/search-results/components/excursions/excursion-results.tsx +186 -0
  27. package/src/search-results/components/hotel/hotel-card.tsx +0 -3
  28. package/src/search-results/components/icon.tsx +1 -4
  29. package/src/search-results/components/itinerary/full-itinerary.tsx +161 -53
  30. package/src/search-results/components/itinerary/index.tsx +31 -7
  31. package/src/search-results/components/search-results-container/search-results-container.tsx +90 -28
  32. package/src/search-results/store/search-results-selectors.ts +11 -0
  33. package/src/search-results/store/search-results-slice.ts +46 -3
  34. package/src/search-results/types.ts +42 -1
  35. package/src/search-results/utils/query-utils.ts +1 -0
  36. package/src/shared/components/flyin/accommodation-flyin.tsx +4 -2
  37. package/src/shared/components/flyin/flights-flyin.tsx +3 -1
  38. package/src/shared/components/flyin/flyin.tsx +18 -6
  39. package/src/shared/components/flyin/group-tour-flyin.tsx +3 -1
  40. package/src/shared/translations/ar-SA.json +8 -2
  41. package/src/shared/translations/da-DK.json +8 -2
  42. package/src/shared/translations/de-DE.json +8 -2
  43. package/src/shared/translations/en-GB.json +8 -2
  44. package/src/shared/translations/es-ES.json +8 -2
  45. package/src/shared/translations/fr-BE.json +8 -2
  46. package/src/shared/translations/fr-FR.json +8 -2
  47. package/src/shared/translations/is-IS.json +8 -2
  48. package/src/shared/translations/it-IT.json +8 -2
  49. package/src/shared/translations/ja-JP.json +8 -2
  50. package/src/shared/translations/nl-BE.json +8 -2
  51. package/src/shared/translations/nl-NL.json +8 -2
  52. package/src/shared/translations/no-NO.json +8 -2
  53. package/src/shared/translations/pl-PL.json +8 -2
  54. package/src/shared/translations/pt-PT.json +8 -2
  55. package/src/shared/translations/sv-SE.json +8 -2
  56. package/src/shared/utils/localization-util.ts +14 -0
  57. package/src/shared/utils/tide-api-utils.ts +8 -0
  58. 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
26
29
  };
27
30
 
28
- const buildItineraryHtml = (itinerary?: ClientPortalItinerary | null) => {
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 nodesHtml = (itinerary.nodes ?? [])
112
+ const allNodes = itinerary.nodes ?? [];
113
+
114
+ const nodesHtml = allNodes
38
115
  .map((node) => {
39
- const hasItems = Array.isArray(node.items) && node.items.length > 0;
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
- ? node.items
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">Geen items voor deze dag.</div>`;
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
- <div class="itinerary-node__day">Dag ${node.startDay}${node.endDay > node.startDay ? ` - ${node.endDay}` : ''}</div>
57
- <div class="itinerary-node__meta">
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 (!hostRef.current) {
214
+ if (isLoading) {
118
215
  return;
119
216
  }
120
217
 
121
- if (!shadowRootRef.current) {
122
- shadowRootRef.current = hostRef.current.attachShadow({ mode: 'open' });
218
+ const host = hostRef.current;
219
+
220
+ if (!host) {
221
+ return;
123
222
  }
124
223
 
125
- const shadowRoot = shadowRootRef.current;
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: flex-start;
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 auto;
180
- min-width: 72px;
181
- padding: 8px 12px;
182
- border-radius: 999px;
183
- font-size: 14px;
184
- font-weight: 700;
185
- line-height: 1;
186
- background: #111827;
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-node__meta {
192
- min-width: 0;
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 <div ref={hostRef} />;
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 orderA = a.order ?? Infinity;
115
- const orderB = b.order ?? Infinity;
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
- // First sort by order
118
- if (orderA !== orderB) {
119
- return orderA - orderB;
143
+ if (priorityA !== priorityB) {
144
+ return priorityA - priorityB;
120
145
  }
121
146
 
122
- // Fallback to date
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
- if (!selectedHotel) return [];
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 selectedHotel.rooms
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
- seed.rooms?.flatMap((room, roomIndex) =>
1140
- room.pax.map((p, paxIndex) => ({
1141
- paxId: p.id,
1142
- room: roomIndex,
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: selectedHotel.fromDate,
1154
- to: selectedHotel.toDate,
1155
- serviceType: ACCOMMODATION_SERVICE_TYPE,
1156
- productName: selectedHotel.name,
1157
- productCode: selectedHotel.code,
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: selectedHotel.countryId ? { id: selectedHotel.countryId, name: selectedHotel.countryName, localizations: [] } : null,
1163
- region: selectedHotel.regionId ? { id: selectedHotel.regionId, name: selectedHotel.regionName, localizations: [] } : null,
1164
- oord: selectedHotel.oordId ? { id: selectedHotel.oordId, name: selectedHotel.oordName, localizations: [] } : null,
1165
- location: selectedHotel.locationId ? { id: selectedHotel.locationId, name: selectedHotel.locationName, localizations: [] } : null,
1166
- longitude: selectedHotel.longitude ?? null,
1167
- latitude: selectedHotel.latitude ?? null,
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 = 1;
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 && itinerary && (
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