kupos-ui-components-lib 10.4.34 → 10.4.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/images/index.ts +1 -2
- package/dist/components/DiscountBanners/DiscountBannerMobile.js +0 -3
- package/dist/components/ServiceItem/ServiceItemDesktop.js +1 -1
- package/dist/ui/DiscountBanners/defaultBackgrounds.js +2 -2
- package/dist/ui/DiscountBanners/discountUtils.js +3 -3
- package/dist/ui/SeatSection/PeruSeatSection.js +25 -25
- package/dist/ui/ServiceBadges/ServiceBadges.js +1 -1
- package/dist/ui/mobileweb/SeatSectionMobile.js +35 -28
- package/dist/ui/mobileweb/ServiceBadgesMobile.js +1 -1
- package/package.json +1 -1
- package/src/assets/images/index.ts +1 -2
- package/src/components/DiscountBanners/DiscountBannerMobile.tsx +2 -2
- package/src/components/ServiceItem/ServiceItemDesktop.tsx +0 -1
- package/src/ui/DiscountBanners/defaultBackgrounds.ts +2 -2
- package/src/ui/DiscountBanners/discountUtils.ts +3 -3
- package/src/ui/SeatSection/PeruSeatSection.tsx +43 -43
- package/src/ui/ServiceBadges/ServiceBadges.tsx +1 -1
- package/src/ui/mobileweb/SeatSectionMobile.tsx +49 -51
- package/src/ui/mobileweb/ServiceBadgesMobile.tsx +1 -1
|
@@ -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
|
|
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
|
-
|
|
35
|
+
instant_discount: { image: toImageUrl(promoCodeArt), accent: "#E08A00" },
|
|
36
36
|
seat_level: { image: toImageUrl(seatLevelArt), accent: "#B87500" },
|
|
37
|
-
|
|
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: "
|
|
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: "
|
|
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: "
|
|
14
|
+
instant_discount: "Oferta instantánea",
|
|
15
15
|
};
|
|
16
16
|
const ACCENTED_CHARS = {
|
|
17
17
|
á: "a",
|
|
@@ -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
|
|
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 (
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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 (
|
|
173
|
-
return
|
|
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 &&
|
|
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 &&
|
|
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
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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:
|
|
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 = ((
|
|
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 = (
|
|
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
|
-
!((
|
|
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
|
-
((
|
|
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 &&
|
|
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" } },
|
|
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 && ((
|
|
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 &&
|
|
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
|
@@ -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}`}
|
|
@@ -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
|
-
|
|
39
|
+
instant_discount: { image: toImageUrl(promoCodeArt), accent: "#E08A00" },
|
|
40
40
|
seat_level: { image: toImageUrl(seatLevelArt), accent: "#B87500" },
|
|
41
|
-
|
|
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: "
|
|
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: "
|
|
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: "
|
|
21
|
+
instant_discount: "Oferta instantánea",
|
|
22
22
|
};
|
|
23
23
|
const ACCENTED_CHARS: Record<string, string> = {
|
|
24
24
|
á: "a",
|
|
@@ -95,7 +95,9 @@ function getNumberOfSeats(seatTypes: SeatType[]) {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
function hasInclinationSeatTypes(seatTypes: SeatType[]): boolean {
|
|
98
|
-
return seatTypes.some((seat) =>
|
|
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
|
|
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 (
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
//
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
<
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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 (
|
|
295
|
-
return
|
|
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 &&
|
|
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 &&
|
|
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) =>
|
|
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
|
|
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
|
-
//
|
|
210
|
-
|
|
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 } =
|
|
221
|
-
|
|
222
|
-
|
|
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(
|
|
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
|
-
? {
|
|
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 &&
|
|
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
|
-
{
|
|
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 &&
|
|
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={{
|