PageSourceSearch

https://www.projectexpedition.com/assets/responsive/js/TourCard/To…?r=3e7e2defcd76309fab3b2b4f2b5a1e1b

js projectexpedition.com collected 2026-09-24 17:59:42 UTC 14,253 bytes, 429 lines download raw bytes

1/**
2 * TourCardProcessors - Tour Data Processing Logic
3 * 
4 * Handles all tour data transformation and processing
5 * Mirrors architecture of PricingCalculator.js
6 * Part of the Tour Card Architecture refactor
7 * 
8 * METHODS:
9 * - processTourDuration(duration) - Parse and format duration strings
10 * - processTourPrice(tour) - Calculate pricing with commission
11 * - processTourIcons(tour, locations, tags) - Extract icon data
12 * - processTourLocation(tourLocation) - Format location strings
13 * - processActivities(acts) - Parse activity tags
14 * - normalizeRating(peRating) - Normalize ratings to scale
15 * 
16 * MIGRATION FROM:
17 * - widget.js (lines 3304-3608) - Original processor functions
18 * 
19 * USED IN:
20 * - TourCardIntegration.js - Main entry point for tour data processing
21 * - widget.js - Legacy integration (via TourCardIntegration)
22 * 
23 * @class TourCardProcessors
24 * @since 2026-01-12
25 */
26var TourCardProcessors = {
27
28  /**
29   * Process tour duration data
30   * Converts duration string format "days::hours::minutes" into formatted display text
31   * 
32   * @param {string} duration - Duration string in format "days::hours::minutes"
33   * @returns {Object} Object with formatted time string and total duration in minutes
34   * @property {string} raw - Full duration string (e.g., "1 Day 2 Hours 30 Minutes")
35   * @property {string} formatted - Abbreviated duration (e.g., "1 Day 2h 30min")
36   * @property {number} totalMinutes - Total duration in minutes
37   * 
38   * @example
39   * processTourDuration("1::2::30")
40   * // Returns: { raw: "1 Day 2 Hours 30 Minutes", formatted: "1 Day 2h 30min", totalMinutes: 1590 }
41   */
42  processTourDuration: function(duration) {
43    if (!duration) {
44      return { raw: '', formatted: '', totalMinutes: 0 };
45    }
46
47    duration = duration.split(/::/);
48    var days = parseInt(duration[0], 10);
49    var hours = parseInt(duration[1], 10);
50    var minutes = parseInt(duration[2], 10);
51    var sortTime = 0;
52    var time = '';
53
54    if (days > 0) {
55      time += days + ' Day' + (days == 1 ? '' : 's') + ' ';
56      sortTime += days * 1440;
57    }
58    if (hours > 0) {
59      time += hours + ' Hour' + (hours == 1 ? '' : 's') + ' ';
60      sortTime += hours * 60;
61    }
62    if (minutes > 0) {
63      time += minutes + ' Minute' + (minutes == 1 ? '' : 's') + ' ';
64      sortTime += minutes;
65    }
66
67    var formattedTime = time.replace(/Hour(s)?/g, 'h').replace(/Minute(s)?/g, 'min');
68
69    return {
70      raw: time.trim(),
71      formatted: formattedTime.trim(),
72      totalMinutes: sortTime
73    };
74  },
75
76  /**
77   * Process tour price data and return calculated values
78   * Handles commission calculation, discount pricing, and currency formatting
79   * 
80   * @param {Object} tour - Tour object with price information
81   * @returns {Object} Object containing all price-related calculated values
82   * @property {number} priceNumber - Numeric price value
83   * @property {string} price - Formatted price string with currency
84   * @property {string} discountPrice - Formatted discount price (if applicable)
85   * @property {string} priceText - Price descriptor (e.g., "Per Person")
86   * @property {string} currencySpace - Space between currency symbol and amount
87   * @property {string} hasDiscount - CSS class if discount applies
88   * @property {string} bottomText - Additional price note text
89   * @property {string} tooltipTitle - Tooltip text for price
90   * @property {boolean} hasPrices - Whether tour has valid pricing
91   * @property {boolean} isDiscounted - Whether tour has discount pricing
92   * @property {boolean} isPerPerson - Whether price is per person
93   * @property {Array} newPriceArray - Parsed price array
94   * 
95   * @example
96   * processTourPrice({ productPrice: "Per Person::$::100::..." })
97   * // Returns: { priceNumber: 100, price: "$100", isPerPerson: true, ... }
98   */
99  processTourPrice: function(tour) {
100    var priceData = {
101      priceNumber: 0,
102      price: '',
103      discountPrice: '',
104      priceText: '',
105      currencySpace: '',
106      hasDiscount: '',
107      bottomText: '',
108      tooltipTitle: '',
109      isoCode: '',
110      hasPrices: false,
111      isDiscounted: false,
112      isPerPerson: false,
113      newPriceArray: []
114    };
115
116    if (!tour.productPrice || typeof tour.productPrice !== 'string') {
117      return priceData;
118    }
119
120    var newPriceArray = tour.productPrice.split("::");
121    priceData.newPriceArray = newPriceArray;
122
123    // Show just the currency glyph with no surrounding spaces (e.g. "US$ " -> "$"); the ISO code
124    // rendered after the price disambiguates which dollar/etc., so the country prefix is redundant.
125    if (typeof newPriceArray[1] === 'string') {
126      var strippedSymbol = newPriceArray[1].replace(/[A-Za-z]/g, '').replace(/\s+/g, '');
127      if (strippedSymbol !== '') {
128        newPriceArray[1] = strippedSymbol;
129      }
130    }
131
132    var expirationTime = new Date(newPriceArray[6]).getTime();
133    if (typeof earliestExperiation == 'undefined') {
134      var earliestExperiation = expirationTime;
135    } else if (expirationTime < earliestExperiation) {
136      earliestExperiation = expirationTime;
137    }
138
139    if (tour.hasPrices != "1" || newPriceArray.filter(Boolean).length <= 1) {
140      return priceData;
141    }
142
143    priceData.hasPrices = true;
144
145    if (newPriceArray[1] == "$") {
146      priceData.currencySpace = "";
147    } else {
148      priceData.currencySpace = " ";
149    }
150
151    var userType = xUtils.checkUserType();
152    var commissionParams = CommissionParameters.fromLocalStorage();
153
154    // Hide discount prices if markup is being applied
155    if (commissionParams && commissionParams.getServiceFeeMarkup() > 0) {
156      newPriceArray[4] = '';
157    }
158
159    if (newPriceArray[3] != "" && newPriceArray[4] == "") {
160      priceData.hasDiscount = ' low-discounted';
161    }
162
163    if (newPriceArray[4] != "") {
164      priceData.isDiscounted = true;
165      priceData.priceNumber = parseInt(newPriceArray[4]);
166      priceData.discountPrice = newPriceArray[1] + priceData.currencySpace + Number(parseInt(newPriceArray[4])).toLocaleString();
167      priceData.price = newPriceArray[1] + priceData.currencySpace + Number(parseInt(newPriceArray[2])).toLocaleString();
168    } else {
169      priceData.price = newPriceArray[1] + Number(parseInt(newPriceArray[2])).toLocaleString();
170      priceData.priceNumber = parseInt(newPriceArray[2]);
171
172      // Apply markup ONLY when service_fee_markup > 0
173      // Card prices should only show markup, not base commission
174      // Purchase flow (calendar) applies full commission (base + markup)
175      if (commissionParams && commissionParams.getServiceFeeMarkup() > 0) {
176        var original_price = newPriceArray[2];
177
178        // MIGRATED: Use new PricingCalculator for commission markup calculation
179        var priceNumber = PricingCalculator.calculateCommissionPrice(
180          newPriceArray[2],
181          commissionParams
182        );
183        priceData.priceNumber = priceNumber;
184        priceData.price = newPriceArray[1] + Number(Math.round(priceNumber)).toLocaleString();
185      }
186    }
187
188    priceData.priceText = newPriceArray[0];
189    priceData.isPerPerson = (newPriceArray[0] == "Per Person");
190
191    if (newPriceArray[3] != "" && newPriceArray[4] == "") {
192      priceData.bottomText = newPriceArray[3];
193    }
194
195    priceData.tooltipTitle = newPriceArray[7] || '';
196    priceData.isoCode = (newPriceArray[7] || '').toUpperCase();
197
198    return priceData;
199  },
200
201  /**
202   * Process tour icons data (pickup and shore excursion)
203   * Extracts and organizes icon-related information for display
204   * 
205   * @param {Object} tour - Tour object with icon-related information
206   * @param {Object} locations - Locations object to populate (passed by reference)
207   * @param {Array} tags - Tags array to populate (passed by reference)
208   * @returns {Object} Object containing all icon-related data
209   * @property {boolean} hasPickup - Whether tour has pickup service
210   * @property {boolean} hasShoreEx - Whether tour is shore excursion
211   * @property {Array} pickupTowns - Array of pickup locations
212   * @property {string} pickupTooltip - Tooltip text for pickup icon
213   * @property {string} pickupIcon - CSS class for pickup icon
214   * @property {string} shoreExTooltip - Tooltip text for shore ex icon
215   * @property {string} shoreExIcon - CSS class for shore ex icon
216   * @property {string} shoreExCssClass - Additional CSS class for shore ex
217   * @property {boolean} hasRefundPolicy - Has cruise refund policy
218   * @property {boolean} hasShipGuarantee - Has ship guarantee
219   * @property {boolean} shouldShowIcons - Whether to display icons section
220   * 
221   * @example
222   * processTourIcons(tour, {}, [])
223   * // Returns: { hasPickup: true, pickupTooltip: "Pickup available in: ...", ... }
224   */
225  processTourIcons: function(tour, locations, tags) {
226    var iconData = {
227      hasPickup: false,
228      hasShoreEx: false,
229      pickupTowns: [],
230      pickupTooltip: '',
231      pickupIcon: 'tour-card__icon--transportation',
232      shoreExTooltip: '',
233      shoreExIcon: '',
234      shoreExCssClass: '',
235      hasRefundPolicy: false,
236      hasShipGuarantee: false,
237      shouldShowIcons: false
238    };
239
240    if (tour.pickupTowns) {
241      iconData.hasPickup = true;
242      var pickupTownsArray = tour.pickupTowns.split("~~~");
243      var pickupList = "";
244
245      for (var x = 0; x < pickupTownsArray.length; x++) {
246        var this_pickup = pickupTownsArray[x].split("::");
247
248        if (locations[this_pickup[1]] == undefined) {
249          locations[this_pickup[1]] = [];
250        }
251        locations[this_pickup[1]].push(this_pickup[0]);
252
253        pickupList += this_pickup[0] + ", ";
254
255        iconData.pickupTowns.push({
256          name: this_pickup[0],
257          region: this_pickup[1]
258        });
259
260        tags.push(this_pickup[0]);
261        tags.push(this_pickup[1]);
262      }
263
264      tags = tags.concat(pickupTownsArray);
265      iconData.pickupTooltip = "Pickup available in: " + pickupList;
266
267    } else if (tour.hotel_pickup == 1) {
268      iconData.hasPickup = true;
269      iconData.pickupTooltip = '';
270    }
271
272    var hasRefund = (tour.shoreex_refund_policy == "Cruise Friendly Refund Policy");
273    var hasShipGuarantee = (tour.shoreex_ontime_policy == "Back to Ship Guarantee");
274
275    if (hasRefund || hasShipGuarantee) {
276      iconData.hasShoreEx = true;
277      iconData.hasRefundPolicy = hasRefund;
278      iconData.hasShipGuarantee = hasShipGuarantee;
279
280      var shoreex_note = "";
281      var shorex_icon = "";
282
283      if (hasRefund) {
284        shoreex_note = tour.shoreex_refund_policy;
285        shorex_icon = "tour-card__icon--shore-ex-refund";
286      }
287
288      if (hasShipGuarantee) {
289        if (shoreex_note.length == 0) {
290          shoreex_note = "Back to Ship Guarantee";
291          shorex_icon = "tour-card__icon--cruise";
292        } else {
293          shoreex_note += " & Back to Ship Guarantee";
294          shorex_icon = "tour-card__icon--shore-ex-ship";
295        }
296      }
297
298      iconData.shoreExTooltip = shoreex_note;
299      iconData.shoreExIcon = shorex_icon;
300
301      if (iconData.hasPickup) {
302        iconData.shoreExCssClass = "css_shoreex";
303      } else {
304        iconData.shoreExCssClass = "";
305      }
306    }
307
308    iconData.shouldShowIcons = (iconData.hasPickup || iconData.hasShoreEx);
309
310    return iconData;
311  },
312
313  /**
314   * Process tour location data
315   * Formats location string from "city::region" format to display format
316   * 
317   * @param {string} tourLocation - Tour location string in format "city::region"
318   * @returns {string} Formatted location string (e.g., "Paris, France")
319   * 
320   * @example
321   * processTourLocation("Paris::France")
322   * // Returns: "Paris, France"
323   */
324  processTourLocation: function(tourLocation) {
325    if (!tourLocation) {
326      return "";
327    }
328
329    var location = tourLocation.split("::");
330    return location[0] + ", " + location[1];
331  },
332
333  /**
334   * Normalize PE Rating to scale of 10
335   * Ensures rating is within valid range (6-10)
336   * 
337   * @param {number} peRating - Original PE Rating
338   * @returns {number} Normalized rating (6-10)
339   * 
340   * @example
341   * normalizeRating(2.5)
342   * // Returns: 6 (minimum threshold)
343   * 
344   * @example
345   * normalizeRating(4.5)
346   * // Returns: 9
347   */
348  normalizeRating: function(peRating) {
349    var rating = Math.round(peRating * 2);
350
351    if (rating >= 1 && rating <= 6) {
352      rating = 6;
353    }
354
355    return rating;
356  },
357
358  /**
359   * Process tour activities/tags data
360   * Parses activity string and extracts activity information
361   * 
362   * @param {string} acts - Activities string in format "::::~~~id::name~~~id::name"
363   * @returns {Object} Object containing processed activities data
364   * @property {number} totalActivities - Total number of activities
365   * @property {string} mainActivity - Primary activity name
366   * @property {Array} allActivities - Array of all activity names
367   * @property {Array}
367 activityTags - Raw activity tag strings
368   * @property {boolean} hasActivities - Whether tour has activities
369   * 
370   * @example
371   * processActivities("123::Hiking~~~456::Swimming")
372   * // Returns: { 
373   * //   totalActivities: 2, 
374   * //   mainActivity: "Hiking",
375   * //   allActivities: ["Hiking", "Swimming"],
376   * //   hasActivities: true 
377   * // }
378   */
379  processActivities: function(acts) {
380    var activityData = {
381      totalActivities: 0,
382      mainActivity: '',
383      allActivities: [],
384      activityTags: [],
385      hasActivities: false
386    };
387
388    if (acts == null || acts === '') {
389      return activityData;
390    }
391
392    var actTags = acts.split("~~~");
393
394    if (actTags[0] === "::::") {
395      actTags.shift();
396    }
397
398    if (actTags.length > 0 && actTags[0] === "::::") {
399      actTags.shift();
400    }
401
402    activityData.activityTags = actTags;
403    activityData.totalActivities = actTags.length;
404
405    if (activityData.totalActivities === 0) {
406      return activityData;
407    }
408
409    activityData.hasActivities = true;
410
411    for (var i = 0; i < actTags.length; i++) {
412      var parts = actTags[i].split("::");
413      if (parts.length >= 2) {
414        var activityName = parts[1];
415        if (activityName && activityName.trim().length > 0) {
416          activityData.allActivities.push(activityName);
417        }
418      }
419    }
420
421    if (activityData.allActivities.length > 0) {
422      activityData.mainActivity = activityData.allActivities[0];
423    }
424
425    return activityData;
426  }
427
428};
429

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.