@ticketboothapp/booking 1.2.56 → 1.2.59

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.
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Compact compare cards: Figtree only (no Poppins). Small type + tight spacing for dialog embeds.
3
+ */
4
+
5
+ .column {
6
+ overflow: hidden;
7
+ border-radius: 0.5rem;
8
+ padding: 0.625rem 0.75rem;
9
+ background: var(--light-orange-background);
10
+ /* Cascade to itinerary `<span>`s that only use Tailwind weight/color classes */
11
+ font-family: 'Figtree', var(--booking-font-sans, ui-sans-serif), sans-serif;
12
+ }
13
+
14
+ .kicker {
15
+ margin: 0 0 0.375rem 0;
16
+ font-family: 'Figtree', var(--booking-font-sans, ui-sans-serif), sans-serif;
17
+ font-size: 0.625rem;
18
+ font-weight: 600;
19
+ letter-spacing: 0.07em;
20
+ text-transform: uppercase;
21
+ line-height: 1.2;
22
+ color: var(--grey-text, var(--booking-text-muted, #78716c));
23
+ }
24
+
25
+ .row {
26
+ display: flex;
27
+ flex-wrap: wrap;
28
+ align-items: baseline;
29
+ gap: 0;
30
+ margin: 0 0 0.2rem 0;
31
+ }
32
+
33
+ .label {
34
+ font-family: 'Figtree', var(--booking-font-sans, ui-sans-serif), sans-serif;
35
+ font-size: 0.8125rem;
36
+ font-weight: 600;
37
+ line-height: 1.3;
38
+ color: var(--primary-text, var(--booking-text, #1c1917));
39
+ margin-right: 0.3em;
40
+ }
41
+
42
+ .value {
43
+ font-family: 'Figtree', var(--booking-font-sans, ui-sans-serif), sans-serif;
44
+ font-size: 0.8125rem;
45
+ font-weight: 400;
46
+ line-height: 1.3;
47
+ color: var(--primary-text, var(--booking-text, #1c1917));
48
+ flex: 1;
49
+ min-width: 0;
50
+ overflow-wrap: break-word;
51
+ }
52
+
53
+ .itineraryBlock {
54
+ margin: 0.15rem 0 0.2rem 0;
55
+ }
56
+
57
+ .itineraryList {
58
+ margin: 0.2rem 0 0 0;
59
+ padding: 0;
60
+ list-style: none;
61
+ font-family: 'Figtree', var(--booking-font-sans, ui-sans-serif), sans-serif;
62
+ font-size: 0.8125rem;
63
+ font-weight: 400;
64
+ line-height: 1.3;
65
+ color: var(--primary-text, var(--booking-text, #1c1917));
66
+ }
67
+
68
+ .itineraryList li + li {
69
+ margin-top: 0.125rem;
70
+ }
71
+
72
+ .totalRow {
73
+ display: flex;
74
+ flex-wrap: wrap;
75
+ align-items: baseline;
76
+ justify-content: space-between;
77
+ gap: 0.5rem 0.75rem;
78
+ margin-top: 0.5rem;
79
+ padding-top: 0.5rem;
80
+ border-top: 1px solid rgba(231, 229, 228, 0.8);
81
+ }
82
+
83
+ .totalLabel {
84
+ font-family: 'Figtree', var(--booking-font-sans, ui-sans-serif), sans-serif;
85
+ font-size: 0.8125rem;
86
+ font-weight: 600;
87
+ color: var(--primary-text, var(--booking-text, #1c1917));
88
+ }
89
+
90
+ .totalAmount {
91
+ font-family: 'Figtree', var(--booking-font-sans, ui-sans-serif), sans-serif;
92
+ font-size: 0.875rem;
93
+ font-weight: 600;
94
+ font-variant-numeric: tabular-nums;
95
+ line-height: 1.2;
96
+ color: var(--primary-text, var(--booking-text, #1c1917));
97
+ }
@@ -0,0 +1,228 @@
1
+ 'use client';
2
+
3
+ import styles from './change-booking-compare.module.css';
4
+
5
+ export type ItineraryStepLine = { time: string | null; label: string };
6
+
7
+ export type ChangeHighlightVariant = 'current' | 'new';
8
+
9
+ /** Same date formatting as manage-booking / ChangeBookingDialog (Mountain Time label). */
10
+ export function formatBookingDateForChangeCompare(dateTime: string | null | undefined): string {
11
+ if (!dateTime) return '—';
12
+ try {
13
+ const date = new Date(dateTime);
14
+ return date.toLocaleDateString('en-US', {
15
+ weekday: 'short',
16
+ year: 'numeric',
17
+ month: 'short',
18
+ day: 'numeric',
19
+ timeZone: 'America/Denver',
20
+ });
21
+ } catch {
22
+ return dateTime;
23
+ }
24
+ }
25
+
26
+ export function formatBookingItemsForCompare(
27
+ items: Array<{ category: string; count: number }> | null | undefined,
28
+ ): string {
29
+ if (!items?.length) return '—';
30
+ const labels: Record<string, string> = {
31
+ ADULT: 'adult',
32
+ CHILD: 'child',
33
+ INFANT: 'infant',
34
+ SENIOR: 'senior',
35
+ STUDENT: 'student',
36
+ };
37
+ const parts = items
38
+ .filter((item) => item.count > 0)
39
+ .map((item) => {
40
+ const label = labels[item.category] || item.category.toLowerCase();
41
+ return `${item.count} ${label}${item.count !== 1 ? 's' : ''}`;
42
+ });
43
+ return parts.length > 0 ? parts.join(', ') : '—';
44
+ }
45
+
46
+ export function computeItineraryStepChanged(
47
+ stepsA: ItineraryStepLine[] | null,
48
+ stepsB: ItineraryStepLine[] | null,
49
+ ): boolean[] | undefined {
50
+ if (!stepsA) return undefined;
51
+ return stepsA.map((a, i) => {
52
+ const b = stepsB?.[i];
53
+ return !b || a.time !== b.time || a.label !== b.label;
54
+ });
55
+ }
56
+
57
+ /** True when the two rendered itinerary lines are the same. */
58
+ export function itinerariesEqualForChangeCompare(
59
+ a: ItineraryStepLine[] | null,
60
+ b: ItineraryStepLine[] | null,
61
+ ): boolean {
62
+ const lenA = a?.length ?? 0;
63
+ const lenB = b?.length ?? 0;
64
+ if (lenA === 0 && lenB === 0) return true;
65
+ if (lenA !== lenB || !a || !b) return false;
66
+ return a.every((step, i) => {
67
+ const o = b[i];
68
+ return step.time === o.time && step.label === o.label;
69
+ });
70
+ }
71
+
72
+ /**
73
+ * When the flow reports “changes” but the compare cards show the same date, tickets line,
74
+ * itinerary, and total, hide the second column and skip strike-through/bold.
75
+ */
76
+ export function changeBookingCompareColumnsVisuallyMatch(params: {
77
+ dateCurrent: string;
78
+ dateNew: string;
79
+ ticketsCurrent: string;
80
+ ticketsNew: string;
81
+ itineraryCurrent: ItineraryStepLine[] | null;
82
+ itineraryNew: ItineraryStepLine[] | null;
83
+ totalCurrent: string;
84
+ totalNew: string;
85
+ }): boolean {
86
+ return (
87
+ params.dateCurrent === params.dateNew &&
88
+ params.ticketsCurrent === params.ticketsNew &&
89
+ params.totalCurrent === params.totalNew &&
90
+ itinerariesEqualForChangeCompare(params.itineraryCurrent, params.itineraryNew)
91
+ );
92
+ }
93
+
94
+ export function BookingChangeSummaryColumn({
95
+ kicker,
96
+ tourName,
97
+ dateStr,
98
+ ticketsStr,
99
+ itinerarySteps,
100
+ highlightVariant,
101
+ totalFormatted,
102
+ dateChanged = false,
103
+ ticketsChanged = false,
104
+ itineraryStepChanged,
105
+ }: {
106
+ kicker: string;
107
+ tourName: string;
108
+ dateStr: string;
109
+ ticketsStr: string;
110
+ itinerarySteps: ItineraryStepLine[] | null;
111
+ highlightVariant: ChangeHighlightVariant;
112
+ /** e.g. "C$123.45" */
113
+ totalFormatted: string;
114
+ dateChanged?: boolean;
115
+ ticketsChanged?: boolean;
116
+ itineraryStepChanged?: boolean[];
117
+ }) {
118
+ const dateClass =
119
+ highlightVariant === 'current' && dateChanged
120
+ ? 'line-through text-stone-500'
121
+ : highlightVariant === 'new' && dateChanged
122
+ ? 'font-semibold text-stone-900'
123
+ : '';
124
+ const ticketsClass =
125
+ highlightVariant === 'current' && ticketsChanged
126
+ ? 'line-through text-stone-500'
127
+ : highlightVariant === 'new' && ticketsChanged
128
+ ? 'font-semibold text-stone-900'
129
+ : '';
130
+
131
+ return (
132
+ <div className={`booking-change-summary-column ${styles.column}`}>
133
+ <p className={styles.kicker}>{kicker}</p>
134
+
135
+ {/* Same pattern as manage-booking `BookingDetails`: label + value on one baseline row */}
136
+ <div className={styles.row}>
137
+ <span className={styles.label}>Tour:</span>
138
+ <span className={styles.value}>{tourName.trim()}</span>
139
+ </div>
140
+ <div className={styles.row}>
141
+ <span className={styles.label}>Date:</span>
142
+ <span className={styles.value}>
143
+ {dateClass ? <span className={dateClass}>{dateStr}</span> : dateStr}
144
+ </span>
145
+ </div>
146
+ <div className={styles.row}>
147
+ <span className={styles.label}>Tickets:</span>
148
+ <span className={styles.value}>
149
+ {ticketsClass ? <span className={ticketsClass}>{ticketsStr}</span> : ticketsStr}
150
+ </span>
151
+ </div>
152
+
153
+ <div className={styles.itineraryBlock}>
154
+ <div className={styles.row}>
155
+ <span className={styles.label}>Itinerary:</span>
156
+ </div>
157
+ {itinerarySteps && itinerarySteps.length > 0 ? (
158
+ <ul className={styles.itineraryList}>
159
+ {itinerarySteps.map((step, i) => (
160
+ <li
161
+ key={i}
162
+ className={
163
+ itineraryStepChanged?.[i]
164
+ ? highlightVariant === 'current'
165
+ ? 'line-through text-stone-500'
166
+ : 'font-semibold text-stone-900'
167
+ : undefined
168
+ }
169
+ >
170
+ {step.time ? (
171
+ <>
172
+ <span
173
+ className={
174
+ itineraryStepChanged?.[i]
175
+ ? highlightVariant === 'current'
176
+ ? 'font-medium text-stone-500'
177
+ : 'font-semibold text-stone-900'
178
+ : 'font-medium text-stone-800'
179
+ }
180
+ >
181
+ {step.time}
182
+ </span>
183
+ <span
184
+ className={
185
+ itineraryStepChanged?.[i] && highlightVariant === 'new'
186
+ ? 'text-stone-400'
187
+ : 'text-stone-500'
188
+ }
189
+ >
190
+ {' '}
191
+ ·{' '}
192
+ </span>
193
+ <span
194
+ className={
195
+ itineraryStepChanged?.[i] && highlightVariant === 'new'
196
+ ? 'font-semibold text-stone-900'
197
+ : undefined
198
+ }
199
+ >
200
+ {step.label}
201
+ </span>
202
+ </>
203
+ ) : (
204
+ <span
205
+ className={
206
+ itineraryStepChanged?.[i] && highlightVariant === 'new'
207
+ ? 'font-semibold text-stone-900'
208
+ : undefined
209
+ }
210
+ >
211
+ {step.label}
212
+ </span>
213
+ )}
214
+ </li>
215
+ ))}
216
+ </ul>
217
+ ) : (
218
+ <p className={`${styles.value} mt-1 text-stone-500`}>—</p>
219
+ )}
220
+ </div>
221
+
222
+ <div className={styles.totalRow}>
223
+ <span className={styles.totalLabel}>Total:</span>
224
+ <span className={styles.totalAmount}>{totalFormatted}</span>
225
+ </div>
226
+ </div>
227
+ );
228
+ }
@@ -0,0 +1,53 @@
1
+ import type { Currency } from './CurrencySwitcher';
2
+
3
+ /** Payload passed to `onChangeBooking` when applying a dashboard-managed booking change. */
4
+ export type ProviderDashboardChangeBookingPayload = {
5
+ productId: string;
6
+ dateTime: string;
7
+ bookingItems: Array<{ category: string; count: number }>;
8
+ returnAvailabilityId?: string | null;
9
+ pickupLocationId?: string | null;
10
+ travelerHotel?: string | null;
11
+ startTime?: string | null;
12
+ passengerCount?: number | null;
13
+ childSafetySeatsCount?: number | null;
14
+ foodRestrictions?: string | null;
15
+ addOnSelections?: Array<{ addOnId: string; variantId?: string; quantity?: number }> | null;
16
+ cancellationPolicyId?: string | null;
17
+ promoCode?: string | null;
18
+ newTotalAmount?: number;
19
+ additionalHoursCount?: number | null;
20
+ pricingAdjustment?: {
21
+ mode: 'AUTO' | 'MANUAL_LINES';
22
+ lineOverrides?: Array<{ lineKey: string; amount: number; reason?: string }>;
23
+ additionalAdjustments?: Array<{ label: string; amount: number }>;
24
+ } | null;
25
+ capacitySeatCredit?: {
26
+ enabled: boolean;
27
+ previousPassengerCount?: number | null;
28
+ previousAvailabilityId?: string | null;
29
+ previousReturnAvailabilityId?: string | null;
30
+ } | null;
31
+ };
32
+
33
+ /** Seeds the flow when opening provider change-booking (matches TicketBooth `BookingWidget` `initialBooking`). */
34
+ export type ProviderDashboardInitialBooking = {
35
+ bookingReference: string;
36
+ productId: string;
37
+ availabilityId?: string;
38
+ dateTime: string;
39
+ originalTotalAmount?: number;
40
+ originalCurrency?: Currency | string;
41
+ originalPromoAmount?: number;
42
+ originalPromoLabel?: string | null;
43
+ bookingItems: Array<{ category: string; count: number }>;
44
+ returnAvailabilityId?: string | null;
45
+ pickupLocationId?: string | null;
46
+ travelerHotel?: string | null;
47
+ startTime?: string | null;
48
+ privateShuttleDetails?: { passengerCount?: number };
49
+ cancellationPolicyId?: string | null;
50
+ promoCode?: string | null;
51
+ additionalHoursCount?: number | null;
52
+ addOnSelections?: Array<{ addOnId: string; variantId?: string; quantity?: number }> | null;
53
+ };
package/src/index.ts CHANGED
@@ -13,11 +13,30 @@ export {
13
13
 
14
14
  /** Canonical Via Via booking UI — same modules as `@/components/booking/*` on the site. */
15
15
  export { BookingFlow } from './components/booking/BookingFlow';
16
+ export type { ChangeFlowSelectionPreview } from './components/booking/BookingFlow';
16
17
  export { PrivateShuttleBookingFlow } from './components/booking/PrivateShuttleBookingFlow';
17
18
  export type { BookingFlowUiOptions } from './components/booking/booking-flow-ui';
19
+ export type {
20
+ ProviderDashboardChangeBookingPayload,
21
+ ProviderDashboardInitialBooking,
22
+ } from './components/booking/provider-dashboard-change-booking';
23
+ export {
24
+ BookingChangeSummaryColumn,
25
+ changeBookingCompareColumnsVisuallyMatch,
26
+ computeItineraryStepChanged,
27
+ formatBookingDateForChangeCompare,
28
+ formatBookingItemsForCompare,
29
+ itinerariesEqualForChangeCompare,
30
+ } from './components/booking/change-booking-compare';
31
+ export type {
32
+ ItineraryStepLine,
33
+ ChangeHighlightVariant,
34
+ } from './components/booking/change-booking-compare';
35
+ export { getItineraryStepLabel } from './lib/booking/itinerary-display';
18
36
  export { PARTNER_EMBEDDED_BOOKING_FLOW_UI_BASE } from './components/booking/booking-flow-ui';
19
37
  export { default as BookingDialog } from './components/booking/BookingDialog';
20
38
  export { default as BookingProductGrid } from './components/booking/BookingProductGrid';
39
+ export { DefaultTermsContent as TermsContent } from './components/booking/DefaultTermsContent';
21
40
 
22
41
  export type {
23
42
  BookingAppMode,
@@ -413,18 +413,35 @@ export function computeOrderSummary(
413
413
 
414
414
  const isTaxIncludedInPrice = pricingConfig.currenciesWithTaxIncluded.includes(currency);
415
415
 
416
- const basePrice = pricing.reduce((sum, rate) => {
417
- const qty = quantities[rate.category] ?? 0;
418
- return sum + qty * (rate.price ?? 0);
419
- }, 0);
416
+ const round2 = (n: number) => Math.round(n * 100) / 100;
417
+
418
+ const ticketLineItems: OrderSummaryTicketLine[] = pricing
419
+ .map((rate) => {
420
+ const qty = quantities[rate.category] ?? 0;
421
+ if (qty === 0) return null;
422
+ const pricePerUnit = rate.price ?? 0;
423
+ return {
424
+ category: rate.category,
425
+ qty,
426
+ pricePerUnit,
427
+ itemTotal: round2(qty * pricePerUnit),
428
+ };
429
+ })
430
+ .filter((line): line is OrderSummaryTicketLine => line != null);
431
+
432
+ const basePrice = ticketLineItems.reduce((sum, line) => sum + line.itemTotal, 0);
420
433
 
421
434
  const perPersonInDisplay = selectedReturnOption?.priceAdjustmentByCurrency?.[currency] ?? 0;
422
435
  const returnPriceAdjustment =
423
- totalQuantity > 0 && perPersonInDisplay !== 0 ? totalQuantity * perPersonInDisplay : 0;
436
+ totalQuantity > 0 && perPersonInDisplay !== 0
437
+ ? round2(totalQuantity * perPersonInDisplay)
438
+ : 0;
424
439
 
425
440
  const cancellationPolicyFee =
426
441
  cancellationPolicyId && pricingConfig?.cancellationPolicies?.length
427
- ? (pricingConfig.cancellationPolicies.find((p) => p.id === cancellationPolicyId)?.feeByCurrency?.[currency] ?? 0)
442
+ ? round2(
443
+ pricingConfig.cancellationPolicies.find((p) => p.id === cancellationPolicyId)?.feeByCurrency?.[currency] ?? 0
444
+ )
428
445
  : 0;
429
446
 
430
447
  const fees = pricingConfig.fees ?? {};
@@ -434,23 +451,14 @@ export function computeOrderSummary(
434
451
  ? []
435
452
  : Object.entries(byCurrency).map(([name, amountPerPerson]) => ({
436
453
  name,
437
- totalAmount: totalQuantity * amountPerPerson,
454
+ totalAmount: round2(totalQuantity * amountPerPerson),
438
455
  description: fees[name]?.description,
439
456
  }));
440
457
 
441
458
  const feesTotal = feeLineItems.reduce((s, f) => s + f.totalAmount, 0);
442
- const subtotal = basePrice + returnPriceAdjustment + cancellationPolicyFee + feesTotal;
443
- const tax = isTaxIncludedInPrice ? 0 : subtotal * pricingConfig.taxRate;
444
- const total = subtotal + tax;
445
-
446
- const ticketLineItems: OrderSummaryTicketLine[] = pricing
447
- .map((rate) => {
448
- const qty = quantities[rate.category] ?? 0;
449
- if (qty === 0) return null;
450
- const pricePerUnit = rate.price ?? 0;
451
- return { category: rate.category, qty, pricePerUnit, itemTotal: qty * pricePerUnit };
452
- })
453
- .filter((line): line is OrderSummaryTicketLine => line != null);
459
+ const subtotal = round2(basePrice + returnPriceAdjustment + cancellationPolicyFee + feesTotal);
460
+ const tax = isTaxIncludedInPrice ? 0 : round2(subtotal * pricingConfig.taxRate);
461
+ const total = round2(subtotal + tax);
454
462
 
455
463
  return {
456
464
  subtotal,
@@ -838,6 +838,13 @@ export interface ChangeBookingQuoteRequest {
838
838
  newAddOnSelections?: Array<{ addOnId: string; variantId?: string; quantity?: number }>;
839
839
  /** Full new-booking total shown in the UI; server verifies within tolerance then uses this for the session so charge matches screen. */
840
840
  clientProposedTotal?: number;
841
+ /** Optional change-flow capacity hint so API can exclude current booking seats from sold counts. */
842
+ capacitySeatCredit?: {
843
+ enabled: boolean;
844
+ previousPassengerCount?: number | null;
845
+ previousAvailabilityId?: string | null;
846
+ previousReturnAvailabilityId?: string | null;
847
+ } | null;
841
848
  }
842
849
 
843
850
  export interface ChangeBookingQuoteReceipt {
@@ -51,7 +51,8 @@ export interface BookingRuntimeSlots {
51
51
  ProductTag: BookingSlotComponent;
52
52
  PillVariant: unknown;
53
53
  ImageModal: BookingSlotComponent;
54
- TermsContent: BookingSlotComponent;
54
+ /** Optional override; package supplies a full default terms component when omitted. */
55
+ TermsContent?: BookingSlotComponent;
55
56
  PlusIcon: ComponentType<SVGProps<SVGSVGElement>>;
56
57
  MinusIcon: ComponentType<SVGProps<SVGSVGElement>>;
57
58
  /** Via Via `ButtonHoverColor` enum from `@/components/button`. */