kupos-ui-components-lib 9.11.0 → 9.11.2

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.
@@ -130,6 +130,24 @@ function ServiceItemPB({ serviceItem, onBookButtonPress, colors, metaData, child
130
130
  : serviceItem.seat_types || [];
131
131
  const discountedSeats = seats.map((seat) => (Object.assign(Object.assign({}, seat), CommonService.calculateDiscountedPrice(seat.fare, serviceItem))));
132
132
  const hasDiscount = discountedSeats.some((seat) => seat.originalPrice !== seat.discountedPrice);
133
+ // Mirror the same check as SeatSection: hide badge (and its top margin) when
134
+ // both percentage and max_discount exist and the cap is being applied.
135
+ const isMaxDiscountApplied = (() => {
136
+ const { discount_type, discount_value, max_discount } = serviceItem;
137
+ if (discount_type === "percentage" &&
138
+ typeof discount_value === "number" &&
139
+ max_discount != null &&
140
+ max_discount > 0) {
141
+ const lowestFare = discountedSeats
142
+ .map((s) => s.originalPrice)
143
+ .filter((p) => p > 0)
144
+ .sort((a, b) => a - b)[0];
145
+ if (lowestFare != null) {
146
+ return (lowestFare * discount_value) / 100 > max_discount;
147
+ }
148
+ }
149
+ return false;
150
+ })();
133
151
  const dpDiscountEntry = Object.entries((serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.dp_discount_percents) || {})[0];
134
152
  const dpDiscountPercent = dpDiscountEntry === null || dpDiscountEntry === void 0 ? void 0 : dpDiscountEntry[1];
135
153
  const hasDpEnabled = (serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.is_dp_enabled) === true;
@@ -277,7 +295,8 @@ function ServiceItemPB({ serviceItem, onBookButtonPress, colors, metaData, child
277
295
  : coachKey
278
296
  ? "20px 15px 20px 15px"
279
297
  : "20px 15px 10px 15px",
280
- marginTop: hasDiscount || hasOfferText || dpDiscountPercent
298
+ marginTop: (hasDiscount || hasOfferText || dpDiscountPercent) &&
299
+ !isMaxDiscountApplied
281
300
  ? "14px"
282
301
  : "",
283
302
  } },
@@ -74,6 +74,17 @@ function SeatSection({ seatTypes, availableSeats, isSoldOut, priceColor, currenc
74
74
  };
75
75
  const renderSeatPrices = () => {
76
76
  if (isPeru) {
77
+ const isMovilBus = (serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.operator_service_name) === "MovilBus";
78
+ // Multiple unique seat types → show a price row for each (MovilBus only)
79
+ if (isMovilBus && uniqueSeats.length > 1) {
80
+ return uniqueSeats
81
+ .filter((s) => !SEAT_EXCEPTIONS.includes(s.label))
82
+ .map((val, key) => {
83
+ const { discountedPrice } = CommonService.calculateDiscountedPrice(val.price, serviceItem);
84
+ return (React.createElement("span", { key: key, className: "flex items-center text-[13.33px] bold-text" }, formatPrice(discountedPrice)));
85
+ });
86
+ }
87
+ // Single seat type → original behaviour (one lowest-fare price)
77
88
  const allSeats = getAllSeatTypes(seatTypes);
78
89
  const lowestFare = allSeats.length > 0 ? allSeats[0].price : 0;
79
90
  const { discountedPrice } = CommonService.calculateDiscountedPrice(lowestFare, serviceItem);
@@ -115,8 +126,30 @@ function SeatSection({ seatTypes, availableSeats, isSoldOut, priceColor, currenc
115
126
  }
116
127
  return null;
117
128
  })();
129
+ // Hide the % OFF badge when max_discount is capping the percentage discount
130
+ // (i.e. both percentage and max_discount exist, and the raw % amount exceeds the cap)
131
+ const isMaxDiscountApplied = (() => {
132
+ const { discount_type, discount_value, max_discount } = serviceItem !== null && serviceItem !== void 0 ? serviceItem : {};
133
+ if (discount_type === "percentage" &&
134
+ typeof discount_value === "number" &&
135
+ max_discount != null &&
136
+ max_discount > 0 &&
137
+ discountSeat) {
138
+ const rawPercentageDiscount = (discountSeat.originalPrice * discount_value) / 100;
139
+ return rawPercentageDiscount > max_discount;
140
+ }
141
+ return false;
142
+ })();
118
143
  const renderLabels = () => {
119
144
  if (isPeru) {
145
+ const isMovilBus = (serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.operator_service_name) === "MovilBus";
146
+ // Multiple unique seat types → show a label row for each (MovilBus only)
147
+ if (isMovilBus && uniqueSeats.length > 1) {
148
+ return uniqueSeats
149
+ .filter((s) => !SEAT_EXCEPTIONS.includes(s.label))
150
+ .map((val, key) => (React.createElement("span", { key: key, className: `flex items-center justify-between text-[13.33px] ${isSoldOut ? "text-[#c0c0c0]" : ""}` }, CommonService.truncateSeatLabel(val.label))));
151
+ }
152
+ // Single seat type → original behaviour
120
153
  const seats = removeDuplicateSeats ? uniqueSeats : sortedSeatTypes;
121
154
  const filteredSeats = seats.filter((s) => !SEAT_EXCEPTIONS.includes(s.label));
122
155
  const seatLabel = filteredSeats.length > 0
@@ -125,7 +158,6 @@ function SeatSection({ seatTypes, availableSeats, isSoldOut, priceColor, currenc
125
158
  : filteredSeats[0].label
126
159
  : null;
127
160
  const operatorServiceName = (serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.operator_service_name) === "MovilBus";
128
- const bottomLabel = operatorServiceName || "Desde";
129
161
  return (React.createElement(React.Fragment, null,
130
162
  hasDiscount && (React.createElement("span", { className: "text-[13.33px]", style: {
131
163
  color: "#999",
@@ -224,7 +256,7 @@ function SeatSection({ seatTypes, availableSeats, isSoldOut, priceColor, currenc
224
256
  React.createElement("span", { className: "text-[13.33px] font-normal leading-[22px] text-[#ccc]" }, "Antes")),
225
257
  React.createElement("div", { className: "col-start-1 row-start-3 flex h-[20px] items-end" },
226
258
  React.createElement("span", { className: "text-[13.33px] font-normal leading-[20px] text-[#464647]" }, "Desde")),
227
- React.createElement("div", { className: "col-start-2 row-start-1 flex items-center justify-center absolute", style: { top: "-22px", left: "50%", transform: "translateX(-50%)" } }, discountValue != null && (React.createElement("span", { className: "rounded-[100px] bg-[#ff5964] px-[6px] text-[12px] bold-text leading-[20px] text-white", style: {
259
+ React.createElement("div", { className: "col-start-2 row-start-1 flex items-center justify-center absolute", style: { top: "-22px", left: "50%", transform: "translateX(-50%)" } }, discountValue != null && !isMaxDiscountApplied && (React.createElement("span", { className: "rounded-[100px] bg-[#ff5964] px-[6px] text-[12px] bold-text leading-[20px] text-white", style: {
228
260
  animation: "pulse-zoom 2s ease-in-out infinite",
229
261
  whiteSpace: "nowrap",
230
262
  backgroundColor: discountSeatPriceColor,
@@ -64,21 +64,29 @@ function SeatSectionMobile({ seatTypes: seatTypesData, isSoldOut, isPeru, seatPr
64
64
  if (lowestFare === null)
65
65
  return null;
66
66
  const priceColor = isSoldOut ? "#bbb" : seatPriceColor;
67
- const { originalPrice, discountedPrice } = commonService.calculateDiscountedPrice(lowestFare, serviceItem);
68
67
  const isMovilBus = (serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.operator_service_name) === "MovilBus";
69
- const uniqueSeats = getUniqueSeats(seatTypesData !== null && seatTypesData !== void 0 ? seatTypesData : [], 1);
68
+ // Fetch ALL unique seats (no slice limit) for the multi-row MovilBus case
69
+ const uniqueSeats = getUniqueSeats(seatTypesData !== null && seatTypesData !== void 0 ? seatTypesData : [], Infinity);
70
+ // MovilBus + multiple unique seat types → render a row per seat
71
+ if (isMovilBus && uniqueSeats.length > 1) {
72
+ return (React.createElement(React.Fragment, null, uniqueSeats.map((seat, key) => {
73
+ const { discountedPrice } = commonService.calculateDiscountedPrice(Number(seat.fare), serviceItem);
74
+ return (React.createElement("div", { key: key, className: "w-[100%] flex flex-row justify-between items-center" },
75
+ React.createElement("span", { className: "min-[420]:text-[13px] text-[12px] bold-text", style: { color: isSoldOut ? "#bbb" : "#464647" } }, commonService.truncateSeatLabel(seat.label)),
76
+ React.createElement("span", { className: "min-[420]:text-[13px] text-[12px] bold-text", style: { color: priceColor } }, commonService.currency(discountedPrice, currencySign))));
77
+ })));
78
+ }
79
+ // Single seat type (or non-MovilBus) → original behaviour
80
+ const { originalPrice, discountedPrice } = commonService.calculateDiscountedPrice(lowestFare, serviceItem);
70
81
  const seatLabel = uniqueSeats.length > 0
71
82
  ? commonService.truncateSeatLabel(uniqueSeats[0].label)
72
83
  : null;
73
- const bottomLabel = isMovilBus ? seatLabel || "Desde" : "Desde";
74
84
  return (React.createElement(React.Fragment, null,
75
85
  originalPrice !== discountedPrice && (React.createElement("div", { className: "w-[100%] flex flex-row justify-between items-center" },
76
86
  React.createElement("span", { className: "min-[420]:text-[13px] text-[12px]", style: { color: "#bbb" } }, "Antes"),
77
87
  React.createElement("span", { className: "min-[420]:text-[13px] text-[12px] line-through", style: { color: "#bbb" } }, commonService.currency(originalPrice, currencySign)))),
78
88
  React.createElement("div", { className: "w-[100%] flex flex-row justify-between items-center" },
79
- React.createElement("span", { className: "min-[420]:text-[13px] text-[12px] bold-text", style: { color: isSoldOut ? "#bbb" : "#464647" } }, (serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.operator_service_name) === "MovilBus"
80
- ? seatLabel
81
- : "Desde"),
89
+ React.createElement("span", { className: "min-[420]:text-[13px] text-[12px] bold-text", style: { color: isSoldOut ? "#bbb" : "#464647" } }, isMovilBus ? seatLabel || "Desde" : "Desde"),
82
90
  React.createElement("span", { className: "min-[420]:text-[13px] text-[12px] bold-text", style: { color: priceColor } }, commonService.currency(discountedPrice, currencySign)))));
83
91
  };
84
92
  const renderDpSeats = () => {
@@ -146,6 +154,19 @@ function SeatSectionMobile({ seatTypes: seatTypesData, isSoldOut, isPeru, seatPr
146
154
  }
147
155
  return null;
148
156
  })();
157
+ // Hide the % OFF badge when max_discount is capping the percentage discount
158
+ const isMaxDiscountApplied = (() => {
159
+ const { discount_type, discount_value, max_discount } = serviceItem !== null && serviceItem !== void 0 ? serviceItem : {};
160
+ if (discount_type === "percentage" &&
161
+ typeof discount_value === "number" &&
162
+ max_discount != null &&
163
+ max_discount > 0 &&
164
+ discountSeat) {
165
+ const rawPercentageDiscount = (discountSeat.originalPrice * discount_value) / 100;
166
+ return rawPercentageDiscount > max_discount;
167
+ }
168
+ return false;
169
+ })();
149
170
  const getMinValue = (data) => {
150
171
  const vals = (Array.isArray(data) ? data : Object.values(data || {})).map(Number);
151
172
  if (!vals.length)
@@ -198,7 +219,7 @@ function SeatSectionMobile({ seatTypes: seatTypesData, isSoldOut, isPeru, seatPr
198
219
  commonService.discountedCurrency(Number(firstSeatFare), currencySign)),
199
220
  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,
200
221
  React.createElement("div", { className: "relative grid grid-cols-[auto_auto] justify-between gap-x-[8px] " },
201
- discountValue != null && (React.createElement("div", { className: "absolute -top-[18px] right-[0px]", style: {
222
+ discountValue != null && !isMaxDiscountApplied && (React.createElement("div", { className: "absolute -top-[18px] right-[0px]", style: {
202
223
  animation: "pulse-zoom 2s ease-in-out infinite",
203
224
  opacity: isSoldOut ? 0.5 : 1,
204
225
  } },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kupos-ui-components-lib",
3
- "version": "9.11.0",
3
+ "version": "9.11.2",
4
4
  "description": "A reusable UI components package",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -225,6 +225,27 @@ function ServiceItemPB({
225
225
  (seat) => seat.originalPrice !== seat.discountedPrice,
226
226
  );
227
227
 
228
+ // Mirror the same check as SeatSection: hide badge (and its top margin) when
229
+ // both percentage and max_discount exist and the cap is being applied.
230
+ const isMaxDiscountApplied = (() => {
231
+ const { discount_type, discount_value, max_discount } = serviceItem as any;
232
+ if (
233
+ discount_type === "percentage" &&
234
+ typeof discount_value === "number" &&
235
+ max_discount != null &&
236
+ max_discount > 0
237
+ ) {
238
+ const lowestFare = discountedSeats
239
+ .map((s) => s.originalPrice)
240
+ .filter((p) => p > 0)
241
+ .sort((a, b) => a - b)[0];
242
+ if (lowestFare != null) {
243
+ return (lowestFare * discount_value) / 100 > max_discount;
244
+ }
245
+ }
246
+ return false;
247
+ })();
248
+
228
249
  const dpDiscountEntry = Object.entries(
229
250
  serviceItem?.dp_discount_percents || {},
230
251
  )[0];
@@ -532,7 +553,8 @@ function ServiceItemPB({
532
553
  : "20px 15px 10px 15px",
533
554
 
534
555
  marginTop:
535
- hasDiscount || hasOfferText || dpDiscountPercent
556
+ (hasDiscount || hasOfferText || dpDiscountPercent) &&
557
+ !isMaxDiscountApplied
536
558
  ? "14px"
537
559
  : "",
538
560
  }}
@@ -139,6 +139,26 @@ function SeatSection({
139
139
 
140
140
  const renderSeatPrices = () => {
141
141
  if (isPeru) {
142
+ const isMovilBus = serviceItem?.operator_service_name === "MovilBus";
143
+
144
+ // Multiple unique seat types → show a price row for each (MovilBus only)
145
+ if (isMovilBus && uniqueSeats.length > 1) {
146
+ return uniqueSeats
147
+ .filter((s) => !SEAT_EXCEPTIONS.includes(s.label))
148
+ .map((val, key) => {
149
+ const { discountedPrice } = CommonService.calculateDiscountedPrice(
150
+ val.price,
151
+ serviceItem,
152
+ );
153
+ return (
154
+ <span key={key} className="flex items-center text-[13.33px] bold-text">
155
+ {formatPrice(discountedPrice)}
156
+ </span>
157
+ );
158
+ });
159
+ }
160
+
161
+ // Single seat type → original behaviour (one lowest-fare price)
142
162
  const allSeats = getAllSeatTypes(seatTypes);
143
163
  const lowestFare = allSeats.length > 0 ? allSeats[0].price : 0;
144
164
  const { discountedPrice } = CommonService.calculateDiscountedPrice(
@@ -216,8 +236,45 @@ function SeatSection({
216
236
  return null;
217
237
  })();
218
238
 
239
+ // Hide the % OFF badge when max_discount is capping the percentage discount
240
+ // (i.e. both percentage and max_discount exist, and the raw % amount exceeds the cap)
241
+ const isMaxDiscountApplied = (() => {
242
+ const { discount_type, discount_value, max_discount } = serviceItem ?? {};
243
+ if (
244
+ discount_type === "percentage" &&
245
+ typeof discount_value === "number" &&
246
+ max_discount != null &&
247
+ max_discount > 0 &&
248
+ discountSeat
249
+ ) {
250
+ const rawPercentageDiscount =
251
+ (discountSeat.originalPrice * discount_value) / 100;
252
+ return rawPercentageDiscount > max_discount;
253
+ }
254
+ return false;
255
+ })();
256
+
219
257
  const renderLabels = () => {
220
258
  if (isPeru) {
259
+ const isMovilBus = serviceItem?.operator_service_name === "MovilBus";
260
+
261
+ // Multiple unique seat types → show a label row for each (MovilBus only)
262
+ if (isMovilBus && uniqueSeats.length > 1) {
263
+ return uniqueSeats
264
+ .filter((s) => !SEAT_EXCEPTIONS.includes(s.label))
265
+ .map((val, key) => (
266
+ <span
267
+ key={key}
268
+ className={`flex items-center justify-between text-[13.33px] ${
269
+ isSoldOut ? "text-[#c0c0c0]" : ""
270
+ }`}
271
+ >
272
+ {CommonService.truncateSeatLabel(val.label)}
273
+ </span>
274
+ ));
275
+ }
276
+
277
+ // Single seat type → original behaviour
221
278
  const seats = removeDuplicateSeats ? uniqueSeats : sortedSeatTypes;
222
279
  const filteredSeats = seats.filter(
223
280
  (s) => !SEAT_EXCEPTIONS.includes(s.label),
@@ -231,7 +288,6 @@ function SeatSection({
231
288
 
232
289
  const operatorServiceName =
233
290
  serviceItem?.operator_service_name === "MovilBus";
234
- const bottomLabel = operatorServiceName || "Desde";
235
291
 
236
292
  return (
237
293
  <>
@@ -452,7 +508,7 @@ function SeatSection({
452
508
  className="col-start-2 row-start-1 flex items-center justify-center absolute"
453
509
  style={{ top: "-22px", left: "50%", transform: "translateX(-50%)" }}
454
510
  >
455
- {discountValue != null && (
511
+ {discountValue != null && !isMaxDiscountApplied && (
456
512
  <span
457
513
  className="rounded-[100px] bg-[#ff5964] px-[6px] text-[12px] bold-text leading-[20px] text-white"
458
514
  style={{
@@ -149,16 +149,52 @@ function SeatSectionMobile({
149
149
  if (lowestFare === null) return null;
150
150
 
151
151
  const priceColor = isSoldOut ? "#bbb" : seatPriceColor;
152
+ const isMovilBus = serviceItem?.operator_service_name === "MovilBus";
153
+
154
+ // Fetch ALL unique seats (no slice limit) for the multi-row MovilBus case
155
+ const uniqueSeats = getUniqueSeats(seatTypesData ?? [], Infinity as number);
156
+
157
+ // MovilBus + multiple unique seat types → render a row per seat
158
+ if (isMovilBus && uniqueSeats.length > 1) {
159
+ return (
160
+ <>
161
+ {uniqueSeats.map((seat, key) => {
162
+ const { discountedPrice } = commonService.calculateDiscountedPrice(
163
+ Number(seat.fare),
164
+ serviceItem,
165
+ );
166
+ return (
167
+ <div
168
+ key={key}
169
+ className="w-[100%] flex flex-row justify-between items-center"
170
+ >
171
+ <span
172
+ className="min-[420]:text-[13px] text-[12px] bold-text"
173
+ style={{ color: isSoldOut ? "#bbb" : "#464647" }}
174
+ >
175
+ {commonService.truncateSeatLabel(seat.label)}
176
+ </span>
177
+ <span
178
+ className="min-[420]:text-[13px] text-[12px] bold-text"
179
+ style={{ color: priceColor }}
180
+ >
181
+ {commonService.currency(discountedPrice, currencySign)}
182
+ </span>
183
+ </div>
184
+ );
185
+ })}
186
+ </>
187
+ );
188
+ }
189
+
190
+ // Single seat type (or non-MovilBus) → original behaviour
152
191
  const { originalPrice, discountedPrice } =
153
192
  commonService.calculateDiscountedPrice(lowestFare, serviceItem);
154
193
 
155
- const isMovilBus = serviceItem?.operator_service_name === "MovilBus";
156
- const uniqueSeats = getUniqueSeats(seatTypesData ?? [], 1);
157
194
  const seatLabel =
158
195
  uniqueSeats.length > 0
159
196
  ? commonService.truncateSeatLabel(uniqueSeats[0].label)
160
197
  : null;
161
- const bottomLabel = isMovilBus ? seatLabel || "Desde" : "Desde";
162
198
 
163
199
  return (
164
200
  <>
@@ -183,9 +219,7 @@ function SeatSectionMobile({
183
219
  className="min-[420]:text-[13px] text-[12px] bold-text"
184
220
  style={{ color: isSoldOut ? "#bbb" : "#464647" }}
185
221
  >
186
- {serviceItem?.operator_service_name === "MovilBus"
187
- ? seatLabel
188
- : "Desde"}
222
+ {isMovilBus ? seatLabel || "Desde" : "Desde"}
189
223
  </span>
190
224
  <span
191
225
  className="min-[420]:text-[13px] text-[12px] bold-text"
@@ -359,6 +393,23 @@ function SeatSectionMobile({
359
393
  return null;
360
394
  })();
361
395
 
396
+ // Hide the % OFF badge when max_discount is capping the percentage discount
397
+ const isMaxDiscountApplied = (() => {
398
+ const { discount_type, discount_value, max_discount } = serviceItem ?? {};
399
+ if (
400
+ discount_type === "percentage" &&
401
+ typeof discount_value === "number" &&
402
+ max_discount != null &&
403
+ max_discount > 0 &&
404
+ discountSeat
405
+ ) {
406
+ const rawPercentageDiscount =
407
+ (discountSeat.originalPrice * discount_value) / 100;
408
+ return rawPercentageDiscount > max_discount;
409
+ }
410
+ return false;
411
+ })();
412
+
362
413
  const getMinValue = (data: any): number | undefined => {
363
414
  const vals = (Array.isArray(data) ? data : Object.values(data || {})).map(
364
415
  Number,
@@ -476,7 +527,7 @@ function SeatSectionMobile({
476
527
  ) : hasDiscount && discountSeat ? (
477
528
  <div>
478
529
  <div className="relative grid grid-cols-[auto_auto] justify-between gap-x-[8px] ">
479
- {discountValue != null && (
530
+ {discountValue != null && !isMaxDiscountApplied && (
480
531
  <div
481
532
  className="absolute -top-[18px] right-[0px]"
482
533
  style={{