kupos-ui-components-lib 10.4.34 → 10.4.36

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.
@@ -34,7 +34,6 @@ export const amenities = {
34
34
  "amenity-cortina": require("./amenities/amenity-cortina.png"),
35
35
  };
36
36
 
37
-
38
37
  export const backgroundImages = {
39
38
  "discount-banner-1": require("./discount/green.png"),
40
39
  "discount-banner-2": require("./discount/yellow.png"),
@@ -46,4 +45,4 @@ export const backgroundImages = {
46
45
  "discount-promo-code": require("./discount/promo.png"),
47
46
  "discount-seat-level": require("./discount/seat.png"),
48
47
  "discount-instant": require("./discount/instant.png"),
49
- };
48
+ };
@@ -120,9 +120,6 @@ class DiscountBannersMobile extends React.Component {
120
120
  React.createElement("span", { className: `bold-text flex-shrink-0 rounded-full text-center text-[#464647] overflow-hidden whitespace-nowrap ${sizeClasses.chip}`, style: chipPillInArt
121
121
  ? undefined
122
122
  : { backgroundColor: "rgba(255,255,255,0.55)" } }, label),
123
- showFlamePercent && (React.createElement("span", { className: `bold-text absolute text-[#ffffff] text-center pointer-events-none ${sizeClasses.flameBadge}`, style: Object.assign(Object.assign({}, FLAME_CENTER), { transform: "translate(-50%, -50%)", textShadow: "0 1px 2px rgba(0,0,0,0.28)", fontWeight: 900, whiteSpace: "nowrap" }) },
124
- card.amount,
125
- React.createElement("span", { className: sizeClasses.flamePercent, style: { fontWeight: 700 } }, "%"))),
126
123
  React.createElement("span", { className: `flex flex-col ${sizeClasses.column}` },
127
124
  React.createElement("span", { className: `bold-text text-[#464647] ${sizeClasses.discount}` }, card.discountLabel),
128
125
  React.createElement("span", { className: `bold-text ${sizeClasses.subtitle}`, style: { color: accentColor } }, card.subtitle))));
@@ -305,7 +305,7 @@ function ServiceItemPB({ serviceItem, onBookButtonPress, colors, metaData, child
305
305
  },
306
306
  ];
307
307
  const otherItems = items.filter((i) => i.key !== "pet" && i.key !== "flexible" && !!i.condition);
308
- return (React.createElement(React.Fragment, null, isPeruSites ? (React.createElement(PeruServiceItemDesktop, { serviceItem: serviceItem, onBookButtonPress: onBookButtonPress, colors: colors, metaData: metaData, children: children, busStage: busStage, serviceDetailsLoading: serviceDetailsLoading, cityOrigin: cityOrigin, cityDestination: cityDestination, translation: translation, orignLabel: orignLabel, destinationLabel: destinationLabel, currencySign: currencySign, isCiva: isCiva, showRating: showRating, showLastSeats: showLastSeats, removeArrivalTime: removeArrivalTime, removeDuplicateSeats: removeDuplicateSeats, isPeruSites: isPeruSites, t: (key) => t(key), showAvailableSeats: showAvailableSeats, isSeatIcon: isSeatIcon, isPeru: isPeru, siteType: siteType, isAllinBus: isAllinBus, viewersConfig: viewersConfig, isLoggedIn: isLoggedIn, showLoginModal: showLoginModal, isExpand: isExpand, setIsExpand: setIsExpand, coachKey: coachKey, isFlores: isFlores, isNewUiEnabled: isNewUiEnabledPeru, showLoginOption: showLoginOption, priority_stage_ids: priority_stage_ids })) : isNewUi && !isTrain ? (React.createElement(FeatureServiceUi, { serviceItem: serviceItem, showTopLabel: showTopLabel, getAnimationIcon: getAnimationIcon, cityOrigin: cityOrigin, cityDestination: cityDestination, renderIcon: renderIcon, viewersConfig: viewersConfig, isFeatureDropDownExpand: isFeatureDropDownExpand, ticketQuantity: ticketQuantity, onIncreaseTicketQuantity: onIncreaseTicketQuantity, onDecreaseTicketQuantity: onDecreaseTicketQuantity, onBookButtonPress: onRemateUiButtonClick, onToggleExpand: () => setIsFeatureDropDownExpand &&
308
+ return (React.createElement(React.Fragment, null, isPeruSites ? (React.createElement(PeruServiceItemDesktop, { serviceItem: serviceItem, onBookButtonPress: onBookButtonPress, colors: colors, metaData: metaData, children: children, busStage: busStage, serviceDetailsLoading: serviceDetailsLoading, cityOrigin: cityOrigin, cityDestination: cityDestination, translation: translation, orignLabel: orignLabel, destinationLabel: destinationLabel, currencySign: currencySign, isCiva: isCiva, showRating: showRating, showLastSeats: showLastSeats, removeArrivalTime: removeArrivalTime, removeDuplicateSeats: removeDuplicateSeats, isPeruSites: isPeruSites, t: (key) => t(key), showAvailableSeats: showAvailableSeats, isSeatIcon: isSeatIcon, isPeru: isPeru, siteType: siteType, isAllinBus: isAllinBus, viewersConfig: viewersConfig, isLoggedIn: isLoggedIn, showLoginModal: showLoginModal, isExpand: isExpand, setIsExpand: setIsExpand, coachKey: coachKey, isFlores: isFlores, isNewUiEnabled: isNewUiEnabledPeru, showLoginOption: showLoginOption })) : isNewUi && !isTrain ? (React.createElement(FeatureServiceUi, { serviceItem: serviceItem, showTopLabel: showTopLabel, getAnimationIcon: getAnimationIcon, cityOrigin: cityOrigin, cityDestination: cityDestination, renderIcon: renderIcon, viewersConfig: viewersConfig, isFeatureDropDownExpand: isFeatureDropDownExpand, ticketQuantity: ticketQuantity, onIncreaseTicketQuantity: onIncreaseTicketQuantity, onDecreaseTicketQuantity: onDecreaseTicketQuantity, onBookButtonPress: onRemateUiButtonClick, onToggleExpand: () => setIsFeatureDropDownExpand &&
309
309
  setIsFeatureDropDownExpand(isFeatureDropDownExpand === serviceItem.id ||
310
310
  isFeatureDropDownExpand === true
311
311
  ? null
@@ -32,7 +32,7 @@ export const CAMPAIGN_ART = {
32
32
  first_time: { image: toImageUrl(firstTimeArt), accent: "#D63384" },
33
33
  cashback: { image: toImageUrl(cashbackArt), accent: "#0069D1" },
34
34
  round_trip: { image: toImageUrl(roundTripArt), accent: "#5C8A00" },
35
- promo_code: { image: toImageUrl(promoCodeArt), accent: "#E08A00" },
35
+ instant_discount: { image: toImageUrl(promoCodeArt), accent: "#E08A00" },
36
36
  seat_level: { image: toImageUrl(seatLevelArt), accent: "#B87500" },
37
- instant_discount: { image: toImageUrl(instantArt), accent: "#107E23" },
37
+ promo_code: { image: toImageUrl(instantArt), accent: "#107E23" },
38
38
  };
@@ -3,15 +3,15 @@ const DESCUENTOS_ESPECIALES_KEY = "descuentos_especiales";
3
3
  const DESCUENTOS_ESPECIALES_LABEL = "Descuentos especiales";
4
4
  const REMATE_KEY = "remate";
5
5
  export const CAMPAIGN_TYPE_LABELS = {
6
- last_minute: "Última hora",
6
+ last_minute: "Oferta de último minuto",
7
7
  first_time: "Primera compra",
8
8
  cashback: "Cashback",
9
9
  return_booking: "Pasaje de vuelta",
10
- round_trip: "Ida y vuelta",
10
+ round_trip: "Ofertas de ida y vuelta",
11
11
  early_bird: "Compra anticipada",
12
12
  promo_code: "Código promocional",
13
13
  seat_level: "Descuento por asiento",
14
- instant_discount: "Descuento inmediato",
14
+ instant_discount: "Oferta instantánea",
15
15
  };
16
16
  const ACCENTED_CHARS = {
17
17
  á: "a",
@@ -48,7 +48,8 @@ function getServiceDiscount(service) {
48
48
  service.discount_value > 0) {
49
49
  return { type: "percentage", amount: Number(service.discount_value) };
50
50
  }
51
- if ((service === null || service === void 0 ? void 0 : service.discount_type) === "fixed" &&
51
+ if (((service === null || service === void 0 ? void 0 : service.discount_type) === "fixed" ||
52
+ (service === null || service === void 0 ? void 0 : service.discount_type) === "fixed_price") &&
52
53
  (service === null || service === void 0 ? void 0 : service.discount_value) != null &&
53
54
  service.discount_value > 0) {
54
55
  const rawDiscount = Number(service.discount_value);
@@ -57,7 +57,7 @@ function getNumberOfSeats(seatTypes) {
57
57
  return seatTypes.filter((val) => !SEAT_EXCEPTIONS.includes(val.label)).length;
58
58
  }
59
59
  function hasInclinationSeatTypes(seatTypes) {
60
- return seatTypes.some((seat) => seat.label.includes("°"));
60
+ return (seatTypes !== null && seatTypes !== void 0 ? seatTypes : []).some((seat) => { var _a; return String((_a = seat === null || seat === void 0 ? void 0 : seat.label) !== null && _a !== void 0 ? _a : "").includes("°"); });
61
61
  }
62
62
  function getSeatTypeIcon(label, serviceItem) {
63
63
  var _a, _b, _c, _d;
@@ -75,7 +75,15 @@ function PeruSeatSection({ seatTypes, availableSeats, isSoldOut, priceColor, cur
75
75
  const uniqueSeats = getUniqueSeats(seatTypes);
76
76
  const sortedSeatTypes = getSortedSeatTypes(seatTypes);
77
77
  const numberOfSeats = getNumberOfSeats(seatTypes);
78
- const isCentered = numberOfSeats < 2 || removeDuplicateSeats;
78
+ const isMovilBus = (serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.operator_service_name) === "MovilBus" ||
79
+ (serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.operator_service_name) === "Movil Bus";
80
+ const hasInclination = hasInclinationSeatTypes(seatTypes);
81
+ const seatTypeRows = uniqueSeats.filter((s) => !SEAT_EXCEPTIONS.includes(s.label));
82
+ // A service that exposes more than one seat type (e.g. "Bus Cama" + "160°")
83
+ // lists every type with its own fare, under a shared "Desde" heading.
84
+ const showSeatTypeRows = (isMovilBus || hasInclination) && seatTypeRows.length > 1;
85
+ // Stacked seat-type rows must stay top-aligned with their labels.
86
+ const isCentered = !showSeatTypeRows && (numberOfSeats < 2 || removeDuplicateSeats);
79
87
  const formatPrice = (price) => availableSeats <= 0
80
88
  ? CommonService.currency(0, currencySign)
81
89
  : CommonService.currency(price, currencySign);
@@ -95,23 +103,16 @@ function PeruSeatSection({ seatTypes, availableSeats, isSoldOut, priceColor, cur
95
103
  });
96
104
  };
97
105
  const renderSeatPrices = () => {
98
- const isMovilBus = (serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.operator_service_name) === "MovilBus" ||
99
- (serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.operator_service_name) === "Movil Bus";
100
- const hasInclination = hasInclinationSeatTypes(seatTypes);
101
106
  // Multiple unique seat types → show a price row for each (MovilBus or inclination types)
102
- if ((isMovilBus || hasInclination) && uniqueSeats.length > 1) {
103
- const filteredSeats = uniqueSeats.filter((s) => !SEAT_EXCEPTIONS.includes(s.label));
104
- const prices = filteredSeats.map((val, key) => {
105
- const { discountedPrice } = CommonService.calculateDiscountedPrice(val.price, serviceItem);
106
- return (React.createElement("span", { key: key, className: "flex items-center text-[13.33px] bold-text" }, formatPrice(discountedPrice)));
107
+ if (showSeatTypeRows) {
108
+ const prices = seatTypeRows.map((val, key) => {
109
+ const { originalPrice, discountedPrice } = CommonService.calculateDiscountedPrice(val.price, serviceItem);
110
+ return (React.createElement("span", { key: key, className: "flex items-center text-[13.33px] bold-text" }, formatPrice(showOriginalFare ? originalPrice : discountedPrice)));
107
111
  });
108
- // Only add "Desde" for inclination types, not for MovilBus
109
- if (hasInclination && !isMovilBus) {
110
- return (React.createElement("div", { className: "relative" },
111
- React.createElement("span", { key: "desde", className: "text-[12px] font-normal text-[#6a6a6a] absolute", style: { top: "-15px" } }, "Desde"),
112
- React.createElement("div", { className: "flex flex-col", style: { gap: "12px" } }, prices)));
113
- }
114
- return prices;
112
+ // Stacked fares always sit under a "Desde" heading.
113
+ return (React.createElement("div", { className: "relative" },
114
+ React.createElement("span", { key: "desde", className: "text-[12px] font-normal text-[#6a6a6a] absolute", style: { top: "-15px" } }, "Desde"),
115
+ React.createElement("div", { className: "flex flex-col", style: { gap: "12px" } }, prices)));
115
116
  }
116
117
  // Single seat type → original behaviour (one lowest-fare price)
117
118
  const allSeats = getAllSeatTypes(seatTypes);
@@ -130,6 +131,9 @@ function PeruSeatSection({ seatTypes, availableSeats, isSoldOut, priceColor, cur
130
131
  // itself; only ui_type and the ready-made badge text come from discounts[0].
131
132
  const peruDiscount = (_a = serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.discounts) === null || _a === void 0 ? void 0 : _a[0];
132
133
  const uiType = peruDiscount === null || peruDiscount === void 0 ? void 0 : peruDiscount.ui_type;
134
+ // Tier B (ui_type 1) announces the offer elsewhere, so the fare itself must
135
+ // render exactly as it would with no discount.
136
+ const showOriginalFare = hasDiscount && uiType === 1;
133
137
  const discountType = serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.discount_type;
134
138
  const discountValueRaw = serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.discount_value;
135
139
  const maxDiscount = serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.max_discount;
@@ -165,15 +169,11 @@ function PeruSeatSection({ seatTypes, availableSeats, isSoldOut, priceColor, cur
165
169
  ? `${discountValue}% OFF`
166
170
  : null);
167
171
  const renderLabels = () => {
168
- const isMovilBus = (serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.operator_service_name) === "MovilBus" ||
169
- (serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.operator_service_name) === "Movil Bus";
170
- const hasInclination = hasInclinationSeatTypes(seatTypes);
171
172
  // Multiple unique seat types → show a label row for each (MovilBus or inclination types)
172
- if ((isMovilBus || hasInclination) && uniqueSeats.length > 1) {
173
- return uniqueSeats
174
- .filter((s) => !SEAT_EXCEPTIONS.includes(s.label))
173
+ if (showSeatTypeRows) {
174
+ return seatTypeRows
175
175
  .map((val, key) => (React.createElement("span", { key: key, className: `flex items-center justify-between text-[13.33px] ${isSoldOut ? "text-[#c0c0c0]" : ""}` },
176
- isSeatIcon && !hasDiscount && (React.createElement("img", { src: getSeatTypeIcon(val.label, serviceItem), alt: "seattype", style: {
176
+ isSeatIcon && (React.createElement("img", { src: getSeatTypeIcon(val.label, serviceItem), alt: "seattype", style: {
177
177
  width: "20px",
178
178
  height: "20px",
179
179
  marginRight: "5px",
@@ -202,7 +202,7 @@ function PeruSeatSection({ seatTypes, availableSeats, isSoldOut, priceColor, cur
202
202
  } })),
203
203
  CommonService.capitalize(seatLabel))) : (React.createElement("span", { className: "text-[13.33px]" }, "Desde")))));
204
204
  };
205
- if (hasDiscount && discountSeat) {
205
+ if (hasDiscount && discountSeat && !(uiType === 1 && showSeatTypeRows)) {
206
206
  // Tier B (ui_type 1): offer text is announced elsewhere, but the fare
207
207
  // itself must render exactly as it would with no discount — no
208
208
  // strike-off, no badge, no fire icon.
@@ -40,7 +40,7 @@ const ServiceBadges = ({ showTopLabel, isSoldOut, colors, renderIcon, translatio
40
40
  } },
41
41
  renderIcon("directo", "12px"),
42
42
  React.createElement("div", null, "Tren Express"))),
43
- boardingName && isAllinBus && !showTopLabel && (React.createElement("div", { className: `flex items-center gap-[10px] py-[4px] px-[14px] rounded-[38px] text-[12.5px] z-20`, style: {
43
+ boardingName && isAllinBus && (React.createElement("div", { className: `flex items-center gap-[10px] py-[4px] px-[14px] rounded-[38px] text-[12.5px] z-20`, style: {
44
44
  backgroundColor: colors.topLabelBgColor || "#fff",
45
45
  border: isSoldOut
46
46
  ? "1px solid #ccc"
@@ -29,7 +29,7 @@ const getFilteredSeats = (item) => {
29
29
  return item;
30
30
  };
31
31
  function hasInclinationSeatTypes(seatTypes) {
32
- return seatTypes.some((seat) => seat.label.includes("°"));
32
+ return (seatTypes !== null && seatTypes !== void 0 ? seatTypes : []).some((seat) => { var _a; return String((_a = seat === null || seat === void 0 ? void 0 : seat.label) !== null && _a !== void 0 ? _a : "").includes("°"); });
33
33
  }
34
34
  const getUniqueSeats = (data, limit) => {
35
35
  var _a;
@@ -48,8 +48,19 @@ const getUniqueSeats = (data, limit) => {
48
48
  .slice(0, limit);
49
49
  };
50
50
  function SeatSectionMobile({ seatTypes: seatTypesData, isSoldOut, isPeru, seatPriceColor, currencySign, availableSeats, removeDuplicateSeats, serviceItem, tooltipBgColor, showLastSeats, discountSeatPriceColor, isTrain, discountSeatCrossPriceColor, }) {
51
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
51
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
52
52
  const hasMultipleTypes = ((_a = seatTypesData === null || seatTypesData === void 0 ? void 0 : seatTypesData.length) !== null && _a !== void 0 ? _a : 0) > 2;
53
+ const isMovilBus = (serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.operator_service_name) === "MovilBus" ||
54
+ (serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.operator_service_name) === "Movil Bus";
55
+ const hasInclination = hasInclinationSeatTypes(seatTypesData !== null && seatTypesData !== void 0 ? seatTypesData : []);
56
+ // Fetch ALL unique seats (no slice limit) for the multi-row seat-type case
57
+ const allUniqueSeats = getUniqueSeats(seatTypesData !== null && seatTypesData !== void 0 ? seatTypesData : [], Infinity);
58
+ // A service that exposes more than one seat type (e.g. "Bus Cama" + "160°")
59
+ // lists every type with its own fare, under a shared "Desde" heading.
60
+ const showSeatTypeRows = (isMovilBus || hasInclination) && allUniqueSeats.length > 1;
61
+ // Tier B (ui_type 1) announces the offer elsewhere, so the fare itself must
62
+ // render exactly as it would with no discount.
63
+ const isOfferTextOnlyTier = (serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.ui_type) === 1;
53
64
  const getFare = (fare) => {
54
65
  if (removeDuplicateSeats && availableSeats <= 0 && !isPeru) {
55
66
  return commonService.currency(0, currencySign);
@@ -69,29 +80,17 @@ function SeatSectionMobile({ seatTypes: seatTypesData, isSoldOut, isPeru, seatPr
69
80
  if (lowestFare === null)
70
81
  return null;
71
82
  const priceColor = isSoldOut ? "#bbb" : seatPriceColor;
72
- const isMovilBus = (serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.operator_service_name) === "MovilBus" ||
73
- (serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.operator_service_name) === "Movil Bus";
74
- const hasInclination = hasInclinationSeatTypes(seatTypesData !== null && seatTypesData !== void 0 ? seatTypesData : []);
75
- // Fetch ALL unique seats (no slice limit) for the multi-row MovilBus case
76
- const uniqueSeats = getUniqueSeats(seatTypesData !== null && seatTypesData !== void 0 ? seatTypesData : [], Infinity);
77
- // MovilBus + multiple unique seat types → render a row per seat
78
- if (isMovilBus && uniqueSeats.length > 1) {
79
- return (React.createElement(React.Fragment, null, uniqueSeats.map((seat, key) => {
80
- const { discountedPrice } = commonService.calculateDiscountedPrice(Number(seat.fare), serviceItem);
81
- return (React.createElement("div", { key: key, className: "w-[100%] flex flex-row justify-between items-center" },
82
- React.createElement("span", { className: "min-[420]:text-[13px] text-[12px] bold-text whitespace-nowrap", style: { color: isSoldOut ? "#bbb" : "#464647" } }, commonService.truncateSeatLabel(seat.label)),
83
- React.createElement("span", { className: "min-[420]:text-[13px] text-[12px] bold-text", style: { color: priceColor } }, commonService.currency(discountedPrice, currencySign))));
84
- })));
85
- }
86
- // Inclination seat types + multiple unique seat types → render a row per seat with "Desde" above prices
87
- if (hasInclination && uniqueSeats.length > 1) {
83
+ const uniqueSeats = allUniqueSeats;
84
+ // Multiple unique seat types → render a row per seat, always under a
85
+ // "Desde" heading.
86
+ if (showSeatTypeRows) {
88
87
  return (React.createElement("div", { className: "relative" },
89
88
  React.createElement("span", { className: "min-[420]:text-[13px] text-[12px] absolute", style: { color: "#6a6a6a", top: -20, right: 0 } }, "Desde"),
90
89
  uniqueSeats.map((seat, key) => {
91
- const { discountedPrice } = commonService.calculateDiscountedPrice(Number(seat.fare), serviceItem);
90
+ const { originalPrice, discountedPrice } = commonService.calculateDiscountedPrice(Number(seat.fare), serviceItem);
92
91
  return (React.createElement("div", { key: key, className: "w-[100%] flex flex-row justify-between items-center", style: { marginTop: key > 0 ? "8px" : "0" } },
93
92
  React.createElement("span", { className: "min-[420]:text-[13px] text-[12px] bold-text whitespace-nowrap", style: { color: isSoldOut ? "#bbb" : "#464647" } }, commonService.truncateSeatLabel(seat.label)),
94
- React.createElement("span", { className: "min-[420]:text-[13px] text-[12px] bold-text", style: { color: priceColor } }, commonService.currency(discountedPrice, currencySign))));
93
+ React.createElement("span", { className: "min-[420]:text-[13px] text-[12px] bold-text", style: { color: priceColor } }, commonService.currency(showOriginalFare ? originalPrice : discountedPrice, currencySign))));
95
94
  })));
96
95
  }
97
96
  const { originalPrice, discountedPrice } = commonService.calculateDiscountedPrice(lowestFare, serviceItem);
@@ -165,8 +164,12 @@ function SeatSectionMobile({ seatTypes: seatTypesData, isSoldOut, isPeru, seatPr
165
164
  ? peruDiscountCalc != null &&
166
165
  peruDiscountCalc.originalPrice !== peruDiscountCalc.discountedPrice
167
166
  : discountedSeats === null || discountedSeats === void 0 ? void 0 : discountedSeats.some((s) => s.originalPrice !== s.discountedPrice);
167
+ const showOriginalFare = Boolean(hasDiscount) && isOfferTextOnlyTier;
168
+ // Peru prices off the lowest fare across all seat types, so the label comes
169
+ // from the cheapest seat rather than being left blank.
170
+ const peruLowestFareLabel = (_c = (_b = allUniqueSeats[0]) === null || _b === void 0 ? void 0 : _b.label) !== null && _c !== void 0 ? _c : "";
168
171
  const discountSeat = isPeru && peruDiscountCalc
169
- ? Object.assign({ label: "", fare: peruLowestFare }, peruDiscountCalc) : (_b = discountedSeats === null || discountedSeats === void 0 ? void 0 : discountedSeats.filter((seat) => !EXCEPTIONS.includes(seat.label))) === null || _b === void 0 ? void 0 : _b.sort((a, b) => a.discountedPrice - b.discountedPrice)[0];
172
+ ? Object.assign({ label: peruLowestFareLabel, fare: peruLowestFare }, peruDiscountCalc) : (_d = discountedSeats === null || discountedSeats === void 0 ? void 0 : discountedSeats.filter((seat) => !EXCEPTIONS.includes(seat.label))) === null || _d === void 0 ? void 0 : _d.sort((a, b) => a.discountedPrice - b.discountedPrice)[0];
170
173
  const discountValue = (() => {
171
174
  if ((serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.discount_type) === "percentage" &&
172
175
  typeof (serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.discount_value) === "number") {
@@ -195,7 +198,7 @@ function SeatSectionMobile({ seatTypes: seatTypesData, isSoldOut, isPeru, seatPr
195
198
  })();
196
199
  // Backend-provided ready-made badge text (e.g. "30% OFF" / "$2.000 OFF")
197
200
  // takes precedence over the value computed client-side.
198
- const discountBadgeText = ((_d = (_c = serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.discounts) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d.discount_display_badge) ||
201
+ const discountBadgeText = ((_f = (_e = serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.discounts) === null || _e === void 0 ? void 0 : _e[0]) === null || _f === void 0 ? void 0 : _f.discount_display_badge) ||
199
202
  (isMaxDiscountApplied
200
203
  ? `${commonService.currency(serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.max_discount)} OFF`
201
204
  : discountValue != null
@@ -210,14 +213,14 @@ function SeatSectionMobile({ seatTypes: seatTypesData, isSoldOut, isPeru, seatPr
210
213
  };
211
214
  const originalDpPrice = getMinValue(serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.original_dp_price);
212
215
  const dpDiscountPercent = getMinValue(serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.dp_discount_percents);
213
- const firstSeatFare = (_f = (_e = seatTypesData === null || seatTypesData === void 0 ? void 0 : seatTypesData.filter((item) => getFilteredSeats(item.label) && !EXCEPTIONS.includes(item.label))) === null || _e === void 0 ? void 0 : _e.sort((a, b) => a.fare - b.fare)[0]) === null || _f === void 0 ? void 0 : _f.fare;
216
+ const firstSeatFare = (_h = (_g = seatTypesData === null || seatTypesData === void 0 ? void 0 : seatTypesData.filter((item) => getFilteredSeats(item.label) && !EXCEPTIONS.includes(item.label))) === null || _g === void 0 ? void 0 : _g.sort((a, b) => a.fare - b.fare)[0]) === null || _h === void 0 ? void 0 : _h.fare;
214
217
  const hasDpDiscount = (serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.dp_discounted_seats) &&
215
218
  (serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.dp_discount_percents) &&
216
219
  originalDpPrice != null &&
217
220
  dpDiscountPercent != null &&
218
221
  firstSeatFare != null;
219
222
  return (React.createElement("div", { className: "content-center relative", style: { width: "40%" } }, (serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.is_dp_enabled) &&
220
- !((_g = serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.dp_discounted_seats) === null || _g === void 0 ? void 0 : _g.length) &&
223
+ !((_j = serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.dp_discounted_seats) === null || _j === void 0 ? void 0 : _j.length) &&
221
224
  !dpDiscountPercent ? (React.createElement("div", { className: "flex flex-col justify-between h-[2.5rem]", style: { gap: isSoldOut ? "0px" : "5px" } }, renderDpSeats())) : hasDpDiscount ? (React.createElement("div", { className: "relative grid grid-cols-[auto_auto] justify-between gap-x-[8px] " },
222
225
  !isNaN(Number(dpDiscountPercent)) &&
223
226
  Number(dpDiscountPercent) > 0 && (React.createElement("div", { className: "absolute -top-[18px] right-[0px]", style: {
@@ -249,9 +252,11 @@ function SeatSectionMobile({ seatTypes: seatTypesData, isSoldOut, isPeru, seatPr
249
252
  React.createElement("span", { className: "flex items-center justify-end gap-[4px] text-[14px] bold-text leading-[24px]", style: {
250
253
  color: isSoldOut ? "#bbb" : discountSeatPriceColor || "#ff5964",
251
254
  } },
252
- ((_h = serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.icons) === null || _h === void 0 ? void 0 : _h.fireIcon) ? (React.createElement("img", { src: serviceItem.icons.fireIcon, alt: "discount", className: "h-[16px] w-[16px] object-contain", style: { filter: isSoldOut ? "grayscale" : "" } })) : null,
255
+ ((_k = serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.icons) === null || _k === void 0 ? void 0 : _k.fireIcon) ? (React.createElement("img", { src: serviceItem.icons.fireIcon, alt: "discount", className: "h-[16px] w-[16px] object-contain", style: { filter: isSoldOut ? "grayscale" : "" } })) : null,
253
256
  commonService.discountedCurrency(Number(firstSeatFare), currencySign)),
254
- isSoldOut ? (React.createElement("span", { className: "col-span-2 min-[420]:text-[13px] text-right text-[12px] text-[#ccc]" }, "Agotado")) : null)) : hasDiscount && discountSeat ? (React.createElement("div", null,
257
+ isSoldOut ? (React.createElement("span", { className: "col-span-2 min-[420]:text-[13px] text-right text-[12px] text-[#ccc]" }, "Agotado")) : null)) : hasDiscount &&
258
+ discountSeat &&
259
+ !(isPeru && showSeatTypeRows && isOfferTextOnlyTier) ? (React.createElement("div", null,
255
260
  React.createElement("div", { className: "relative grid grid-cols-[auto_auto] justify-between gap-x-[8px] " },
256
261
  (serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.ui_type) !== 1 && discountBadgeText && (React.createElement(React.Fragment, null,
257
262
  React.createElement("div", { className: "absolute -top-[18px] right-[0px]", style: {
@@ -279,7 +284,9 @@ function SeatSectionMobile({ seatTypes: seatTypesData, isSoldOut, isPeru, seatPr
279
284
  transform: "rotate(-12deg)",
280
285
  transformOrigin: "center",
281
286
  } })))),
282
- React.createElement("span", { className: "min-[420]:text-[13px] text-[12px] leading-[24px]", style: { color: isSoldOut ? "#bbb" : "#464647" } }, (serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.ui_type) === 1 ? discountSeat.label : "Desde"),
287
+ React.createElement("span", { className: "min-[420]:text-[13px] text-[12px] leading-[24px]", style: { color: isSoldOut ? "#bbb" : "#464647" } }, isOfferTextOnlyTier
288
+ ? commonService.capitalize(commonService.truncateSeatLabel(discountSeat.label))
289
+ : "Desde"),
283
290
  React.createElement("span", { className: "flex items-center justify-end gap-[4px] text-[14px] bold-text leading-[24px]", style: {
284
291
  color: isSoldOut
285
292
  ? "#bbb"
@@ -287,7 +294,7 @@ function SeatSectionMobile({ seatTypes: seatTypesData, isSoldOut, isPeru, seatPr
287
294
  ? "#464647"
288
295
  : discountSeatPriceColor || "#ff5964",
289
296
  } },
290
- (serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.ui_type) !== 1 && ((_j = serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.icons) === null || _j === void 0 ? void 0 : _j.fireIcon) ? (React.createElement("img", { src: serviceItem.icons.fireIcon, alt: "discount", className: "h-[16px] w-[16px] object-contain", style: { opacity: isSoldOut ? 0.5 : 1 } })) : null,
297
+ (serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.ui_type) !== 1 && ((_l = serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.icons) === null || _l === void 0 ? void 0 : _l.fireIcon) ? (React.createElement("img", { src: serviceItem.icons.fireIcon, alt: "discount", className: "h-[16px] w-[16px] object-contain", style: { opacity: isSoldOut ? 0.5 : 1 } })) : null,
291
298
  commonService.discountedCurrency((serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.ui_type) === 1
292
299
  ? discountSeat.originalPrice
293
300
  : discountSeat.discountedPrice, currencySign))))) : (React.createElement("div", { className: `flex flex-col justify-between ${isTrain ? "" : "h-[2.5rem]"} `, style: {
@@ -40,7 +40,7 @@ const ServiceBadgesMobile = ({ showTopLabel, isSoldOut, colors, renderIcon, serv
40
40
  } },
41
41
  renderIcon("directo", "12px"),
42
42
  React.createElement("div", null, "Tren Express"))),
43
- boardingName && isAllinBus && !showTopLabel && (React.createElement("div", { className: `flex items-center gap-[2p x] py-[4px] px-[10px] rounded-[38px] min-[420]:text-[12px] text-[10px] h-[24px] z-20`, style: {
43
+ boardingName && isAllinBus && (React.createElement("div", { className: `flex items-center gap-[2p x] py-[4px] px-[10px] rounded-[38px] min-[420]:text-[12px] text-[10px] h-[24px] z-20`, style: {
44
44
  backgroundColor: colors.topLabelBgColor || "#fff",
45
45
  border: isSoldOut
46
46
  ? "1px solid #ccc"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kupos-ui-components-lib",
3
- "version": "10.4.34",
3
+ "version": "10.4.36",
4
4
  "description": "A reusable UI components package",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -34,7 +34,6 @@ export const amenities = {
34
34
  "amenity-cortina": require("./amenities/amenity-cortina.png"),
35
35
  };
36
36
 
37
-
38
37
  export const backgroundImages = {
39
38
  "discount-banner-1": require("./discount/green.png"),
40
39
  "discount-banner-2": require("./discount/yellow.png"),
@@ -46,4 +45,4 @@ export const backgroundImages = {
46
45
  "discount-promo-code": require("./discount/promo.png"),
47
46
  "discount-seat-level": require("./discount/seat.png"),
48
47
  "discount-instant": require("./discount/instant.png"),
49
- };
48
+ };
@@ -190,7 +190,7 @@ class DiscountBannersMobile extends React.Component<
190
190
  >
191
191
  {label}
192
192
  </span>
193
- {showFlamePercent && (
193
+ {/* {showFlamePercent && (
194
194
  <span
195
195
  className={`bold-text absolute text-[#ffffff] text-center pointer-events-none ${sizeClasses.flameBadge}`}
196
196
  style={{
@@ -209,7 +209,7 @@ class DiscountBannersMobile extends React.Component<
209
209
  %
210
210
  </span>
211
211
  </span>
212
- )}
212
+ )} */}
213
213
  <span className={`flex flex-col ${sizeClasses.column}`}>
214
214
  <span
215
215
  className={`bold-text text-[#464647] ${sizeClasses.discount}`}
@@ -520,7 +520,6 @@ function ServiceItemPB({
520
520
  isFlores={isFlores}
521
521
  isNewUiEnabled={isNewUiEnabledPeru}
522
522
  showLoginOption={showLoginOption}
523
- priority_stage_ids={priority_stage_ids}
524
523
  />
525
524
  ) : isNewUi && !isTrain ? (
526
525
  <FeatureServiceUi
@@ -36,7 +36,7 @@ export const CAMPAIGN_ART: Record<string, { image: string; accent: string }> = {
36
36
  first_time: { image: toImageUrl(firstTimeArt), accent: "#D63384" },
37
37
  cashback: { image: toImageUrl(cashbackArt), accent: "#0069D1" },
38
38
  round_trip: { image: toImageUrl(roundTripArt), accent: "#5C8A00" },
39
- promo_code: { image: toImageUrl(promoCodeArt), accent: "#E08A00" },
39
+ instant_discount: { image: toImageUrl(promoCodeArt), accent: "#E08A00" },
40
40
  seat_level: { image: toImageUrl(seatLevelArt), accent: "#B87500" },
41
- instant_discount: { image: toImageUrl(instantArt), accent: "#107E23" },
41
+ promo_code: { image: toImageUrl(instantArt), accent: "#107E23" },
42
42
  };
@@ -10,15 +10,15 @@ const DESCUENTOS_ESPECIALES_LABEL = "Descuentos especiales";
10
10
  const REMATE_KEY = "remate";
11
11
 
12
12
  export const CAMPAIGN_TYPE_LABELS: Record<string, string> = {
13
- last_minute: "Última hora",
13
+ last_minute: "Oferta de último minuto",
14
14
  first_time: "Primera compra",
15
15
  cashback: "Cashback",
16
16
  return_booking: "Pasaje de vuelta",
17
- round_trip: "Ida y vuelta",
17
+ round_trip: "Ofertas de ida y vuelta",
18
18
  early_bird: "Compra anticipada",
19
19
  promo_code: "Código promocional",
20
20
  seat_level: "Descuento por asiento",
21
- instant_discount: "Descuento inmediato",
21
+ instant_discount: "Oferta instantánea",
22
22
  };
23
23
  const ACCENTED_CHARS: Record<string, string> = {
24
24
  á: "a",
@@ -68,7 +68,8 @@ function getServiceDiscount(
68
68
  }
69
69
 
70
70
  if (
71
- service?.discount_type === "fixed" &&
71
+ (service?.discount_type === "fixed" ||
72
+ service?.discount_type === "fixed_price") &&
72
73
  service?.discount_value != null &&
73
74
  service.discount_value > 0
74
75
  ) {
@@ -95,7 +95,9 @@ function getNumberOfSeats(seatTypes: SeatType[]) {
95
95
  }
96
96
 
97
97
  function hasInclinationSeatTypes(seatTypes: SeatType[]): boolean {
98
- return seatTypes.some((seat) => seat.label.includes("°"));
98
+ return (seatTypes ?? []).some((seat) =>
99
+ String(seat?.label ?? "").includes("°"),
100
+ );
99
101
  }
100
102
 
101
103
  function getSeatTypeIcon(label: string, serviceItem: any) {
@@ -121,7 +123,20 @@ function PeruSeatSection({
121
123
  const uniqueSeats = getUniqueSeats(seatTypes);
122
124
  const sortedSeatTypes = getSortedSeatTypes(seatTypes);
123
125
  const numberOfSeats = getNumberOfSeats(seatTypes);
124
- const isCentered = numberOfSeats < 2 || removeDuplicateSeats;
126
+ const isMovilBus =
127
+ serviceItem?.operator_service_name === "MovilBus" ||
128
+ serviceItem?.operator_service_name === "Movil Bus";
129
+ const hasInclination = hasInclinationSeatTypes(seatTypes);
130
+ const seatTypeRows = uniqueSeats.filter(
131
+ (s) => !SEAT_EXCEPTIONS.includes(s.label),
132
+ );
133
+ // A service that exposes more than one seat type (e.g. "Bus Cama" + "160°")
134
+ // lists every type with its own fare, under a shared "Desde" heading.
135
+ const showSeatTypeRows =
136
+ (isMovilBus || hasInclination) && seatTypeRows.length > 1;
137
+ // Stacked seat-type rows must stay top-aligned with their labels.
138
+ const isCentered =
139
+ !showSeatTypeRows && (numberOfSeats < 2 || removeDuplicateSeats);
125
140
 
126
141
  const formatPrice = (price: number) =>
127
142
  availableSeats <= 0
@@ -161,48 +176,36 @@ function PeruSeatSection({
161
176
  };
162
177
 
163
178
  const renderSeatPrices = () => {
164
- const isMovilBus =
165
- serviceItem?.operator_service_name === "MovilBus" ||
166
- serviceItem?.operator_service_name === "Movil Bus";
167
- const hasInclination = hasInclinationSeatTypes(seatTypes);
168
-
169
179
  // Multiple unique seat types → show a price row for each (MovilBus or inclination types)
170
- if ((isMovilBus || hasInclination) && uniqueSeats.length > 1) {
171
- const filteredSeats = uniqueSeats.filter((s) => !SEAT_EXCEPTIONS.includes(s.label));
172
- const prices = filteredSeats.map((val, key) => {
173
- const { discountedPrice } = CommonService.calculateDiscountedPrice(
174
- val.price,
175
- serviceItem,
176
- );
180
+ if (showSeatTypeRows) {
181
+ const prices = seatTypeRows.map((val, key) => {
182
+ const { originalPrice, discountedPrice } =
183
+ CommonService.calculateDiscountedPrice(val.price, serviceItem);
177
184
  return (
178
185
  <span
179
186
  key={key}
180
187
  className="flex items-center text-[13.33px] bold-text"
181
188
  >
182
- {formatPrice(discountedPrice)}
189
+ {formatPrice(showOriginalFare ? originalPrice : discountedPrice)}
183
190
  </span>
184
191
  );
185
192
  });
186
193
 
187
- // Only add "Desde" for inclination types, not for MovilBus
188
- if (hasInclination && !isMovilBus) {
189
- return (
190
- <div className="relative">
191
- <span
192
- key="desde"
193
- className="text-[12px] font-normal text-[#6a6a6a] absolute"
194
- style={{ top: "-15px" }}
195
- >
196
- Desde
197
- </span>
198
- <div className="flex flex-col" style={{ gap: "12px" }}>
199
- {prices}
200
- </div>
194
+ // Stacked fares always sit under a "Desde" heading.
195
+ return (
196
+ <div className="relative">
197
+ <span
198
+ key="desde"
199
+ className="text-[12px] font-normal text-[#6a6a6a] absolute"
200
+ style={{ top: "-15px" }}
201
+ >
202
+ Desde
203
+ </span>
204
+ <div className="flex flex-col" style={{ gap: "12px" }}>
205
+ {prices}
201
206
  </div>
202
- );
203
- }
204
-
205
- return prices;
207
+ </div>
208
+ );
206
209
  }
207
210
 
208
211
  // Single seat type → original behaviour (one lowest-fare price)
@@ -240,6 +243,9 @@ function PeruSeatSection({
240
243
  // itself; only ui_type and the ready-made badge text come from discounts[0].
241
244
  const peruDiscount = serviceItem?.discounts?.[0];
242
245
  const uiType = peruDiscount?.ui_type;
246
+ // Tier B (ui_type 1) announces the offer elsewhere, so the fare itself must
247
+ // render exactly as it would with no discount.
248
+ const showOriginalFare = hasDiscount && uiType === 1;
243
249
  const discountType = serviceItem?.discount_type;
244
250
  const discountValueRaw = serviceItem?.discount_value;
245
251
  const maxDiscount = serviceItem?.max_discount;
@@ -285,15 +291,9 @@ function PeruSeatSection({
285
291
  : null);
286
292
 
287
293
  const renderLabels = () => {
288
- const isMovilBus =
289
- serviceItem?.operator_service_name === "MovilBus" ||
290
- serviceItem?.operator_service_name === "Movil Bus";
291
- const hasInclination = hasInclinationSeatTypes(seatTypes);
292
-
293
294
  // Multiple unique seat types → show a label row for each (MovilBus or inclination types)
294
- if ((isMovilBus || hasInclination) && uniqueSeats.length > 1) {
295
- return uniqueSeats
296
- .filter((s) => !SEAT_EXCEPTIONS.includes(s.label))
295
+ if (showSeatTypeRows) {
296
+ return seatTypeRows
297
297
  .map((val, key) => (
298
298
  <span
299
299
  key={key}
@@ -301,7 +301,7 @@ function PeruSeatSection({
301
301
  isSoldOut ? "text-[#c0c0c0]" : ""
302
302
  }`}
303
303
  >
304
- {isSeatIcon && !hasDiscount && (
304
+ {isSeatIcon && (
305
305
  <img
306
306
  src={getSeatTypeIcon(val.label, serviceItem) as string}
307
307
  alt="seattype"
@@ -376,7 +376,7 @@ function PeruSeatSection({
376
376
  );
377
377
  };
378
378
 
379
- if (hasDiscount && discountSeat) {
379
+ if (hasDiscount && discountSeat && !(uiType === 1 && showSeatTypeRows)) {
380
380
  // Tier B (ui_type 1): offer text is announced elsewhere, but the fare
381
381
  // itself must render exactly as it would with no discount — no
382
382
  // strike-off, no badge, no fire icon.
@@ -97,7 +97,7 @@ const ServiceBadges: React.FC<ServiceBadgesProps> = ({
97
97
  <div>{"Tren Express"}</div>
98
98
  </div>
99
99
  )}
100
- {boardingName && isAllinBus && !showTopLabel && (
100
+ {boardingName && isAllinBus && (
101
101
  <div
102
102
  className={`flex items-center gap-[10px] py-[4px] px-[14px] rounded-[38px] text-[12.5px] z-20`}
103
103
  style={{
@@ -96,7 +96,9 @@ const getFilteredSeats = (item: string) => {
96
96
  };
97
97
 
98
98
  function hasInclinationSeatTypes(seatTypes: SeatType[]): boolean {
99
- return seatTypes.some((seat) => seat.label.includes("°"));
99
+ return (seatTypes ?? []).some((seat) =>
100
+ String(seat?.label ?? "").includes("°"),
101
+ );
100
102
  }
101
103
 
102
104
  const getUniqueSeats = (data: SeatType[], limit: number): SeatType[] => {
@@ -137,6 +139,20 @@ function SeatSectionMobile({
137
139
  }: SeatSectionMobileProps): React.ReactElement {
138
140
  const hasMultipleTypes = (seatTypesData?.length ?? 0) > 2;
139
141
 
142
+ const isMovilBus =
143
+ serviceItem?.operator_service_name === "MovilBus" ||
144
+ serviceItem?.operator_service_name === "Movil Bus";
145
+ const hasInclination = hasInclinationSeatTypes(seatTypesData ?? []);
146
+ // Fetch ALL unique seats (no slice limit) for the multi-row seat-type case
147
+ const allUniqueSeats = getUniqueSeats(seatTypesData ?? [], Infinity as number);
148
+ // A service that exposes more than one seat type (e.g. "Bus Cama" + "160°")
149
+ // lists every type with its own fare, under a shared "Desde" heading.
150
+ const showSeatTypeRows =
151
+ (isMovilBus || hasInclination) && allUniqueSeats.length > 1;
152
+ // Tier B (ui_type 1) announces the offer elsewhere, so the fare itself must
153
+ // render exactly as it would with no discount.
154
+ const isOfferTextOnlyTier = serviceItem?.ui_type === 1;
155
+
140
156
  const getFare = (fare: number) => {
141
157
  if (removeDuplicateSeats && availableSeats <= 0 && !isPeru) {
142
158
  return commonService.currency(0, currencySign);
@@ -165,49 +181,11 @@ function SeatSectionMobile({
165
181
  if (lowestFare === null) return null;
166
182
 
167
183
  const priceColor = isSoldOut ? "#bbb" : seatPriceColor;
168
- const isMovilBus =
169
- serviceItem?.operator_service_name === "MovilBus" ||
170
- serviceItem?.operator_service_name === "Movil Bus";
171
- const hasInclination = hasInclinationSeatTypes(seatTypesData ?? []);
172
-
173
- // Fetch ALL unique seats (no slice limit) for the multi-row MovilBus case
174
- const uniqueSeats = getUniqueSeats(seatTypesData ?? [], Infinity as number);
175
-
176
- // MovilBus + multiple unique seat types → render a row per seat
177
- if (isMovilBus && uniqueSeats.length > 1) {
178
- return (
179
- <>
180
- {uniqueSeats.map((seat, key) => {
181
- const { discountedPrice } = commonService.calculateDiscountedPrice(
182
- Number(seat.fare),
183
- serviceItem,
184
- );
185
- return (
186
- <div
187
- key={key}
188
- className="w-[100%] flex flex-row justify-between items-center"
189
- >
190
- <span
191
- className="min-[420]:text-[13px] text-[12px] bold-text whitespace-nowrap"
192
- style={{ color: isSoldOut ? "#bbb" : "#464647" }}
193
- >
194
- {commonService.truncateSeatLabel(seat.label)}
195
- </span>
196
- <span
197
- className="min-[420]:text-[13px] text-[12px] bold-text"
198
- style={{ color: priceColor }}
199
- >
200
- {commonService.currency(discountedPrice, currencySign)}
201
- </span>
202
- </div>
203
- );
204
- })}
205
- </>
206
- );
207
- }
184
+ const uniqueSeats = allUniqueSeats;
208
185
 
209
- // Inclination seat types + multiple unique seat types → render a row per seat with "Desde" above prices
210
- if (hasInclination && uniqueSeats.length > 1) {
186
+ // Multiple unique seat types → render a row per seat, always under a
187
+ // "Desde" heading.
188
+ if (showSeatTypeRows) {
211
189
  return (
212
190
  <div className="relative">
213
191
  <span
@@ -217,10 +195,11 @@ function SeatSectionMobile({
217
195
  Desde
218
196
  </span>
219
197
  {uniqueSeats.map((seat, key) => {
220
- const { discountedPrice } = commonService.calculateDiscountedPrice(
221
- Number(seat.fare),
222
- serviceItem,
223
- );
198
+ const { originalPrice, discountedPrice } =
199
+ commonService.calculateDiscountedPrice(
200
+ Number(seat.fare),
201
+ serviceItem,
202
+ );
224
203
  return (
225
204
  <div
226
205
  key={key}
@@ -237,7 +216,10 @@ function SeatSectionMobile({
237
216
  className="min-[420]:text-[13px] text-[12px] bold-text"
238
217
  style={{ color: priceColor }}
239
218
  >
240
- {commonService.currency(discountedPrice, currencySign)}
219
+ {commonService.currency(
220
+ showOriginalFare ? originalPrice : discountedPrice,
221
+ currencySign,
222
+ )}
241
223
  </span>
242
224
  </div>
243
225
  );
@@ -436,9 +418,19 @@ function SeatSectionMobile({
436
418
  peruDiscountCalc.originalPrice !== peruDiscountCalc.discountedPrice
437
419
  : discountedSeats?.some((s) => s.originalPrice !== s.discountedPrice);
438
420
 
421
+ const showOriginalFare = Boolean(hasDiscount) && isOfferTextOnlyTier;
422
+
423
+ // Peru prices off the lowest fare across all seat types, so the label comes
424
+ // from the cheapest seat rather than being left blank.
425
+ const peruLowestFareLabel = allUniqueSeats[0]?.label ?? "";
426
+
439
427
  const discountSeat =
440
428
  isPeru && peruDiscountCalc
441
- ? { label: "", fare: peruLowestFare as number, ...peruDiscountCalc }
429
+ ? {
430
+ label: peruLowestFareLabel,
431
+ fare: peruLowestFare as number,
432
+ ...peruDiscountCalc,
433
+ }
442
434
  : discountedSeats
443
435
  ?.filter((seat) => !EXCEPTIONS.includes(seat.label))
444
436
  ?.sort((a, b) => a.discountedPrice - b.discountedPrice)[0];
@@ -602,7 +594,9 @@ function SeatSectionMobile({
602
594
  </span>
603
595
  ) : null}
604
596
  </div>
605
- ) : hasDiscount && discountSeat ? (
597
+ ) : hasDiscount &&
598
+ discountSeat &&
599
+ !(isPeru && showSeatTypeRows && isOfferTextOnlyTier) ? (
606
600
  <div>
607
601
  <div className="relative grid grid-cols-[auto_auto] justify-between gap-x-[8px] ">
608
602
  {serviceItem?.ui_type !== 1 && discountBadgeText && (
@@ -660,7 +654,11 @@ function SeatSectionMobile({
660
654
  className="min-[420]:text-[13px] text-[12px] leading-[24px]"
661
655
  style={{ color: isSoldOut ? "#bbb" : "#464647" }}
662
656
  >
663
- {serviceItem?.ui_type === 1 ? discountSeat.label : "Desde"}
657
+ {isOfferTextOnlyTier
658
+ ? commonService.capitalize(
659
+ commonService.truncateSeatLabel(discountSeat.label),
660
+ )
661
+ : "Desde"}
664
662
  </span>
665
663
  <span
666
664
  className="flex items-center justify-end gap-[4px] text-[14px] bold-text leading-[24px]"
@@ -104,7 +104,7 @@ const ServiceBadgesMobile: React.FC<ServiceBadgesMobileProps> = ({
104
104
  <div>{"Tren Express"}</div>
105
105
  </div>
106
106
  )}
107
- {boardingName && isAllinBus && !showTopLabel && (
107
+ {boardingName && isAllinBus && (
108
108
  <div
109
109
  className={`flex items-center gap-[2p x] py-[4px] px-[10px] rounded-[38px] min-[420]:text-[12px] text-[10px] h-[24px] z-20`}
110
110
  style={{