@voyant-travel/bookings-react 0.202.0 → 0.203.0
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/README.md +8 -8
- package/dist/admin/bookings-host.d.ts +1 -2
- package/dist/admin/bookings-host.js +2 -3
- package/dist/admin/index.d.ts +6 -44
- package/dist/admin/index.js +2 -65
- package/dist/components/booking-create-utils.d.ts +1 -1
- package/dist/components/booking-create-utils.js +5 -17
- package/dist/components/booking-dialog.d.ts +4 -19
- package/dist/components/booking-dialog.js +3 -12
- package/dist/components/booking-list.d.ts +1 -2
- package/dist/components/booking-list.js +5 -13
- package/dist/components/bookings-page.d.ts +1 -2
- package/dist/components/bookings-page.js +2 -2
- package/dist/hooks/booking-create-types.d.ts +34 -0
- package/dist/hooks/booking-create-types.js +1 -0
- package/dist/hooks/index.d.ts +2 -4
- package/dist/hooks/index.js +1 -4
- package/dist/hooks/use-booking-mutation.d.ts +3 -65
- package/dist/hooks/use-booking-mutation.js +1 -10
- package/dist/i18n/en-create-list.d.ts +0 -1
- package/dist/i18n/en-create-list.js +0 -1
- package/dist/i18n/en.d.ts +0 -1
- package/dist/i18n/messages-create-list.d.ts +0 -1
- package/dist/i18n/provider.d.ts +0 -2
- package/dist/i18n/ro-create-list.d.ts +0 -1
- package/dist/i18n/ro-create-list.js +0 -1
- package/dist/i18n/ro.d.ts +0 -1
- package/dist/journey/components/booking-journey-rules.d.ts +0 -22
- package/dist/journey/components/booking-journey-rules.js +0 -54
- package/dist/journey/components/journey-steps/payment-step.js +1 -1
- package/dist/journey/components/journey-steps/shared.d.ts +3 -3
- package/dist/journey/components/journey-steps.d.ts +1 -1
- package/dist/journey/components/journey-steps.js +1 -1
- package/dist/journey/index.d.ts +2 -12
- package/dist/journey/index.js +1 -11
- package/dist/journey/types.d.ts +3 -208
- package/dist/journey/types.js +2 -6
- package/dist/storefront/index.d.ts +0 -2
- package/dist/storefront/index.js +0 -2
- package/dist/ui.d.ts +0 -2
- package/dist/ui.js +0 -2
- package/package.json +33 -33
- package/dist/admin/booking-journey-host.d.ts +0 -23
- package/dist/admin/booking-journey-host.js +0 -280
- package/dist/admin/journey-billing-duplicate-warning.d.ts +0 -2
- package/dist/admin/journey-billing-duplicate-warning.js +0 -26
- package/dist/admin/journey-departure-picker.d.ts +0 -6
- package/dist/admin/journey-departure-picker.js +0 -91
- package/dist/admin/journey-travel-credit-picker.d.ts +0 -2
- package/dist/admin/journey-travel-credit-picker.js +0 -72
- package/dist/admin/journey-units-picker.d.ts +0 -10
- package/dist/admin/journey-units-picker.js +0 -60
- package/dist/admin/pages/booking-journey-page.d.ts +0 -11
- package/dist/admin/pages/booking-journey-page.js +0 -26
- package/dist/admin/pages/booking-new-page.d.ts +0 -15
- package/dist/admin/pages/booking-new-page.js +0 -50
- package/dist/components/booking-create-page.d.ts +0 -13
- package/dist/components/booking-create-page.js +0 -11
- package/dist/components/booking-create-sheet.d.ts +0 -34
- package/dist/components/booking-create-sheet.js +0 -815
- package/dist/hooks/use-booking-convert-mutation.d.ts +0 -80
- package/dist/hooks/use-booking-convert-mutation.js +0 -24
- package/dist/hooks/use-booking-create-mutation.d.ts +0 -336
- package/dist/hooks/use-booking-create-mutation.js +0 -43
- package/dist/hooks/use-booking-dual-create-mutation.d.ts +0 -337
- package/dist/hooks/use-booking-dual-create-mutation.js +0 -45
- package/dist/journey/components/booking-journey.d.ts +0 -2
- package/dist/journey/components/booking-journey.js +0 -485
- package/dist/storefront/storefront-booking-errors.d.ts +0 -12
- package/dist/storefront/storefront-booking-errors.js +0 -50
- package/dist/storefront/storefront-booking-journey.d.ts +0 -71
- package/dist/storefront/storefront-booking-journey.js +0 -327
- package/dist/storefront/storefront-booking-page.d.ts +0 -35
- package/dist/storefront/storefront-booking-page.js +0 -215
- package/dist/storefront/storefront-checkout-bodies.d.ts +0 -36
- package/dist/storefront/storefront-checkout-bodies.js +0 -69
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voyant-travel/bookings-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.203.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -150,24 +150,24 @@
|
|
|
150
150
|
"react-dom": "^19.0.0",
|
|
151
151
|
"react-hook-form": "^7.80.0",
|
|
152
152
|
"zod": "^4.0.0",
|
|
153
|
-
"@voyant-travel/accommodations": "^0.
|
|
153
|
+
"@voyant-travel/accommodations": "^0.163.0",
|
|
154
154
|
"@voyant-travel/admin": "^0.129.1",
|
|
155
|
-
"@voyant-travel/
|
|
156
|
-
"@voyant-travel/
|
|
157
|
-
"@voyant-travel/
|
|
158
|
-
"@voyant-travel/
|
|
159
|
-
"@voyant-travel/
|
|
160
|
-
"@voyant-travel/
|
|
161
|
-
"@voyant-travel/finance-react": "^0.
|
|
162
|
-
"@voyant-travel/
|
|
163
|
-
"@voyant-travel/
|
|
164
|
-
"@voyant-travel/commerce-react": "^0.
|
|
165
|
-
"@voyant-travel/cruises": "^0.
|
|
166
|
-
"@voyant-travel/inventory-react": "^0.
|
|
167
|
-
"@voyant-travel/inventory": "^0.
|
|
168
|
-
"@voyant-travel/storefront-react": "^0.
|
|
155
|
+
"@voyant-travel/catalog-react": "^0.201.0",
|
|
156
|
+
"@voyant-travel/distribution-react": "^0.193.0",
|
|
157
|
+
"@voyant-travel/bookings": "^0.203.0",
|
|
158
|
+
"@voyant-travel/relationships-react": "^0.203.0",
|
|
159
|
+
"@voyant-travel/finance": "^0.203.0",
|
|
160
|
+
"@voyant-travel/catalog": "^0.201.0",
|
|
161
|
+
"@voyant-travel/finance-react": "^0.203.0",
|
|
162
|
+
"@voyant-travel/legal-react": "^0.203.0",
|
|
163
|
+
"@voyant-travel/identity-react": "^0.203.0",
|
|
164
|
+
"@voyant-travel/commerce-react": "^0.85.0",
|
|
165
|
+
"@voyant-travel/cruises": "^0.202.0",
|
|
166
|
+
"@voyant-travel/inventory-react": "^0.85.0",
|
|
167
|
+
"@voyant-travel/inventory": "^0.20.0",
|
|
168
|
+
"@voyant-travel/storefront-react": "^0.205.0",
|
|
169
169
|
"@voyant-travel/ui": "^0.109.6",
|
|
170
|
-
"@voyant-travel/operations-react": "^0.
|
|
170
|
+
"@voyant-travel/operations-react": "^0.84.0"
|
|
171
171
|
},
|
|
172
172
|
"peerDependenciesMeta": {
|
|
173
173
|
"@tanstack/react-table": {
|
|
@@ -251,26 +251,26 @@
|
|
|
251
251
|
"typescript": "^6.0.3",
|
|
252
252
|
"vitest": "^4.1.9",
|
|
253
253
|
"zod": "^4.4.3",
|
|
254
|
-
"@voyant-travel/accommodations": "^0.
|
|
254
|
+
"@voyant-travel/accommodations": "^0.163.0",
|
|
255
255
|
"@voyant-travel/admin": "^0.129.1",
|
|
256
|
-
"@voyant-travel/bookings": "^0.
|
|
257
|
-
"@voyant-travel/catalog": "^0.
|
|
258
|
-
"@voyant-travel/catalog-react": "^0.
|
|
259
|
-
"@voyant-travel/distribution-react": "^0.
|
|
260
|
-
"@voyant-travel/
|
|
261
|
-
"@voyant-travel/finance": "^0.
|
|
262
|
-
"@voyant-travel/
|
|
263
|
-
"@voyant-travel/
|
|
264
|
-
"@voyant-travel/
|
|
265
|
-
"@voyant-travel/commerce-react": "^0.
|
|
266
|
-
"@voyant-travel/cruises": "^0.
|
|
267
|
-
"@voyant-travel/inventory-react": "^0.
|
|
268
|
-
"@voyant-travel/inventory": "^0.
|
|
256
|
+
"@voyant-travel/bookings": "^0.203.0",
|
|
257
|
+
"@voyant-travel/catalog": "^0.201.0",
|
|
258
|
+
"@voyant-travel/catalog-react": "^0.201.0",
|
|
259
|
+
"@voyant-travel/distribution-react": "^0.193.0",
|
|
260
|
+
"@voyant-travel/finance": "^0.203.0",
|
|
261
|
+
"@voyant-travel/finance-react": "^0.203.0",
|
|
262
|
+
"@voyant-travel/identity-react": "^0.203.0",
|
|
263
|
+
"@voyant-travel/legal-react": "^0.203.0",
|
|
264
|
+
"@voyant-travel/relationships-react": "^0.203.0",
|
|
265
|
+
"@voyant-travel/commerce-react": "^0.85.0",
|
|
266
|
+
"@voyant-travel/cruises": "^0.202.0",
|
|
267
|
+
"@voyant-travel/inventory-react": "^0.85.0",
|
|
268
|
+
"@voyant-travel/inventory": "^0.20.0",
|
|
269
269
|
"@voyant-travel/react": "^0.104.2",
|
|
270
|
-
"@voyant-travel/storefront-react": "^0.
|
|
270
|
+
"@voyant-travel/storefront-react": "^0.205.0",
|
|
271
271
|
"@voyant-travel/ui": "^0.109.6",
|
|
272
272
|
"@voyant-travel/voyant-typescript-config": "^0.1.0",
|
|
273
|
-
"@voyant-travel/operations-react": "^0.
|
|
273
|
+
"@voyant-travel/operations-react": "^0.84.0"
|
|
274
274
|
},
|
|
275
275
|
"files": [
|
|
276
276
|
"dist",
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
export interface BookingJourneyHostProps {
|
|
2
|
-
entityModule: string;
|
|
3
|
-
entityId: string;
|
|
4
|
-
/** Usually omitted — the server resolves provenance from `(module, id)`. */
|
|
5
|
-
sourceKind?: string;
|
|
6
|
-
sourceConnectionId?: string;
|
|
7
|
-
sourceRef?: string;
|
|
8
|
-
departureId?: string;
|
|
9
|
-
/** Free-form departure date (ISO) for sourced products with no slot id. */
|
|
10
|
-
departureDate?: string;
|
|
11
|
-
optionId?: string;
|
|
12
|
-
/** Sourced stays/package rate pin — the exact room + rate plan to re-resolve. */
|
|
13
|
-
roomTypeId?: string;
|
|
14
|
-
ratePlanId?: string;
|
|
15
|
-
board?: string;
|
|
16
|
-
/** Preview hints (name + hero image) for sourced entities, which aren't in
|
|
17
|
-
* the owned products table. */
|
|
18
|
-
entityName?: string;
|
|
19
|
-
entityImageUrl?: string;
|
|
20
|
-
draftId: string;
|
|
21
|
-
className?: string;
|
|
22
|
-
}
|
|
23
|
-
export declare function BookingJourneyHost({ entityModule, entityId, sourceKind, sourceConnectionId, sourceRef, departureId, departureDate, optionId, roomTypeId, ratePlanId, board, entityName, entityImageUrl, draftId, className, }: BookingJourneyHostProps): React.ReactElement;
|
|
@@ -1,280 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
/**
|
|
4
|
-
* Packaged admin host for the unified `<BookingJourney />` wizard —
|
|
5
|
-
* supplies CRM-backed contact pickers, the B2B billing default, real
|
|
6
|
-
* departure/units/Travel Credit pickers over operator inventory, and post-commit
|
|
7
|
-
* navigation to the new booking's detail page.
|
|
8
|
-
*
|
|
9
|
-
* Per booking-journey-architecture §8.1 + §10 Phase B, packaged per the
|
|
10
|
-
* packaged-admin RFC Phase 3: cross-route navigation resolves through
|
|
11
|
-
* semantic destinations (`booking.detail` on commit, `catalog.browse` on
|
|
12
|
-
* cancel), and the data client comes from the host-mounted
|
|
13
|
-
* `VoyantReactProvider` — no app route tree, no app fetcher import.
|
|
14
|
-
*/
|
|
15
|
-
import { useQuery } from "@tanstack/react-query";
|
|
16
|
-
import { useAdminNavigate, useOperatorAdminMessages } from "@voyant-travel/admin";
|
|
17
|
-
import { useAddresses } from "@voyant-travel/identity-react";
|
|
18
|
-
import { getProductMediaQueryOptions, getProductQueryOptions } from "@voyant-travel/inventory-react";
|
|
19
|
-
import { useOrganization, usePerson } from "@voyant-travel/relationships-react";
|
|
20
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
21
|
-
import { emptyPersonPickerValue, PersonPickerSection, } from "../components/person-picker-section.js";
|
|
22
|
-
import { BookingJourney, } from "../journey/index.js";
|
|
23
|
-
import { useVoyantBookingsContext } from "../provider.js";
|
|
24
|
-
import { JourneyBillingDuplicateWarning } from "./journey-billing-duplicate-warning.js";
|
|
25
|
-
import { JourneyDeparturePicker } from "./journey-departure-picker.js";
|
|
26
|
-
import { JourneyTravelCreditPicker } from "./journey-travel-credit-picker.js";
|
|
27
|
-
import { JourneyUnitsPicker } from "./journey-units-picker.js";
|
|
28
|
-
/** The catalog browse surface the journey returns to when cancelled. */
|
|
29
|
-
function journeyReturnSurface(vertical) {
|
|
30
|
-
switch (vertical) {
|
|
31
|
-
case "cruises":
|
|
32
|
-
return "cruises";
|
|
33
|
-
case "accommodations":
|
|
34
|
-
return "accommodations";
|
|
35
|
-
default:
|
|
36
|
-
return "products";
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
export function BookingJourneyHost({ entityModule, entityId, sourceKind, sourceConnectionId, sourceRef, departureId, departureDate, optionId, roomTypeId, ratePlanId, board, entityName, entityImageUrl, draftId, className, }) {
|
|
40
|
-
const navigate = useAdminNavigate();
|
|
41
|
-
const entitySummary = useEntitySummary(entityModule, entityId, {
|
|
42
|
-
name: entityName,
|
|
43
|
-
heroImageUrl: entityImageUrl,
|
|
44
|
-
});
|
|
45
|
-
const slots = {
|
|
46
|
-
renderLeadContactPicker({ apply, buyerType }) {
|
|
47
|
-
return _jsx(CrmLeadPicker, { apply: apply, buyerType: buyerType }, buyerType);
|
|
48
|
-
},
|
|
49
|
-
renderBillingExtras(ctx) {
|
|
50
|
-
// Warn if the picked lead already booked this departure.
|
|
51
|
-
return _jsx(JourneyBillingDuplicateWarning, { ...ctx });
|
|
52
|
-
},
|
|
53
|
-
renderDeparturePicker(pickerProps) {
|
|
54
|
-
// Owned: real scheduled departures from availability. Sourced products
|
|
55
|
-
// have none; when one was booked from a specific offer the date came in
|
|
56
|
-
// pre-selected, so we lock it (a different date = a different offer).
|
|
57
|
-
return _jsx(JourneyDeparturePicker, { ...pickerProps, lockDeparture: Boolean(departureDate) });
|
|
58
|
-
},
|
|
59
|
-
renderUnitsPicker(pickerProps) {
|
|
60
|
-
// Rooms/units for the picked option + departure (operator inventory).
|
|
61
|
-
return _jsx(JourneyUnitsPicker, { ...pickerProps });
|
|
62
|
-
},
|
|
63
|
-
renderTravelCreditPicker(pickerProps) {
|
|
64
|
-
// Admin searches and selects a Travel Credit without needing to know the code.
|
|
65
|
-
return _jsx(JourneyTravelCreditPicker, { ...pickerProps });
|
|
66
|
-
},
|
|
67
|
-
renderTravelerContactPicker({ apply, selectedPersonId }) {
|
|
68
|
-
// Travelers reuse the same picker (person-only). Adapt the picker's
|
|
69
|
-
// partial lead-apply to the traveler apply (always a person), and
|
|
70
|
-
// reflect the row's linked person so "Copy from billing" selects it.
|
|
71
|
-
return (_jsx(CrmLeadPicker, { variant: "traveler", linkedPersonId: selectedPersonId, apply: (contact) => apply({
|
|
72
|
-
firstName: contact.firstName ?? "",
|
|
73
|
-
lastName: contact.lastName ?? "",
|
|
74
|
-
email: contact.email,
|
|
75
|
-
phone: contact.phone,
|
|
76
|
-
personId: contact.personId,
|
|
77
|
-
}) }));
|
|
78
|
-
},
|
|
79
|
-
onCommitted(result) {
|
|
80
|
-
// Land on the new booking's detail page.
|
|
81
|
-
navigate("booking.detail", { bookingId: result.bookingId });
|
|
82
|
-
},
|
|
83
|
-
onCancelled() {
|
|
84
|
-
navigate("catalog.browse", { surface: journeyReturnSurface(entityModule) });
|
|
85
|
-
},
|
|
86
|
-
};
|
|
87
|
-
return (_jsx(BookingJourney, { surface: "admin", entityModule: entityModule, entityId: entityId, sourceKind: sourceKind, sourceConnectionId: sourceConnectionId, sourceRef: sourceRef, draftId: draftId, initialConfigure: {
|
|
88
|
-
...(departureId ? { departureSlotId: departureId } : {}),
|
|
89
|
-
...(departureDate ? { departureDate } : {}),
|
|
90
|
-
...(optionId ? { variantId: optionId } : {}),
|
|
91
|
-
...(roomTypeId ? { roomTypeId } : {}),
|
|
92
|
-
...(ratePlanId ? { ratePlanId } : {}),
|
|
93
|
-
...(board ? { board } : {}),
|
|
94
|
-
}, defaultBuyerType: "B2B",
|
|
95
|
-
// The admin in-process commit route can reserve/hold bookings. Tokenized
|
|
96
|
-
// card charges, bank-transfer checkout instructions, and agency-credit
|
|
97
|
-
// account collection are separate flows that this packaged host does not
|
|
98
|
-
// wire yet, so do not advertise them here.
|
|
99
|
-
paymentCapabilities: {
|
|
100
|
-
acceptsCard: false,
|
|
101
|
-
acceptsHold: true,
|
|
102
|
-
acceptsBankTransfer: false,
|
|
103
|
-
acceptsTicketOnCredit: false,
|
|
104
|
-
}, entitySummary: entitySummary, className: className, ...slots }));
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Builds the "what you're booking" preview shown atop the journey side
|
|
108
|
-
* panel — the product's name + first image, for an instant preview before
|
|
109
|
-
* the quote returns. Owned products in the `products` table resolve via the
|
|
110
|
-
* client fetch; a sourced/connect id isn't in that table, so it falls back to
|
|
111
|
-
* the `hints` (name + hero image) carried from the catalog detail page. We
|
|
112
|
-
* can't (and needn't) tell owned from sourced on the client — the URL no
|
|
113
|
-
* longer carries `sourceKind` — so we try the products fetch for the
|
|
114
|
-
* `products` vertical and let the hints cover everything else.
|
|
115
|
-
*/
|
|
116
|
-
function useEntitySummary(entityModule, entityId, hints) {
|
|
117
|
-
const tryProductSummary = entityModule === "products" && Boolean(entityId);
|
|
118
|
-
const client = useVoyantBookingsContext();
|
|
119
|
-
const productQuery = useQuery({
|
|
120
|
-
...getProductQueryOptions(client, entityId),
|
|
121
|
-
enabled: tryProductSummary,
|
|
122
|
-
retry: false,
|
|
123
|
-
});
|
|
124
|
-
const mediaQuery = useQuery({
|
|
125
|
-
...getProductMediaQueryOptions(client, entityId, { mediaType: "image" }),
|
|
126
|
-
enabled: tryProductSummary,
|
|
127
|
-
retry: false,
|
|
128
|
-
});
|
|
129
|
-
return useMemo(() => {
|
|
130
|
-
const product = productQuery.data;
|
|
131
|
-
if (product) {
|
|
132
|
-
// First product-level (not day-scoped) image, if any.
|
|
133
|
-
const heroImageUrl = (mediaQuery.data?.data ?? [])
|
|
134
|
-
.filter((media) => media.mediaType === "image" && media.dayId == null)
|
|
135
|
-
.map((media) => media.url)
|
|
136
|
-
.find(Boolean);
|
|
137
|
-
return {
|
|
138
|
-
name: product.name,
|
|
139
|
-
vertical: "products",
|
|
140
|
-
...(heroImageUrl ? { heroImageUrl } : {}),
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
// Sourced/connect entity (not in the owned table) — use the preview hints.
|
|
144
|
-
if (hints.name) {
|
|
145
|
-
return {
|
|
146
|
-
name: hints.name,
|
|
147
|
-
vertical: entityModule,
|
|
148
|
-
...(hints.heroImageUrl ? { heroImageUrl: hints.heroImageUrl } : {}),
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
return undefined;
|
|
152
|
-
}, [productQuery.data, mediaQuery.data, hints.name, hints.heroImageUrl, entityModule]);
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* CRM-backed picker for the journey's contact slots. Buyer type drives the
|
|
156
|
-
* mode (there's no bill-to toggle): for the LEAD slot it searches PEOPLE on
|
|
157
|
-
* B2C and ORGANIZATIONS on B2B — picking either hydrates the journey from the
|
|
158
|
-
* CRM record (person → name/email/phone; org → company name/tax id). The
|
|
159
|
-
* per-traveler slot is always a person picker.
|
|
160
|
-
*/
|
|
161
|
-
function CrmLeadPicker({ apply, buyerType, variant = "lead", linkedPersonId, }) {
|
|
162
|
-
const t = useOperatorAdminMessages().bookings.detail.bookingJourney;
|
|
163
|
-
// The lead picker bills an organization on B2B; everything else is a person.
|
|
164
|
-
const orgMode = variant === "lead" && buyerType === "B2B";
|
|
165
|
-
const [value, setValue] = useState(emptyPersonPickerValue);
|
|
166
|
-
// Reflect an externally-set person (copy-from-billing, or a re-opened
|
|
167
|
-
// draft) in the combobox. The hydrate effect below then fills the names;
|
|
168
|
-
// the `appliedPersonId` ref keeps that idempotent.
|
|
169
|
-
useEffect(() => {
|
|
170
|
-
if (!linkedPersonId)
|
|
171
|
-
return;
|
|
172
|
-
setValue((cur) => cur.personId === linkedPersonId
|
|
173
|
-
? cur
|
|
174
|
-
: { ...cur, mode: "existing", personId: linkedPersonId });
|
|
175
|
-
}, [linkedPersonId]);
|
|
176
|
-
// Keep the picker's target aligned with the Buyer type radio.
|
|
177
|
-
useEffect(() => {
|
|
178
|
-
setValue((current) => {
|
|
179
|
-
const desired = orgMode ? "organization" : "person";
|
|
180
|
-
return (current.billTo ?? "person") === desired ? current : { ...current, billTo: desired };
|
|
181
|
-
});
|
|
182
|
-
}, [orgMode]);
|
|
183
|
-
// Hydrate from the picked CRM person (B2C). The picker yields just an id;
|
|
184
|
-
// we fetch the record and apply the real name/email/phone.
|
|
185
|
-
const selectedPersonId = !orgMode && value.mode === "existing" && value.personId ? value.personId : undefined;
|
|
186
|
-
const personQuery = usePerson(selectedPersonId, { enabled: Boolean(selectedPersonId) });
|
|
187
|
-
const appliedPersonId = useRef(null);
|
|
188
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: applies once per resolved record; the ref guards re-entry from apply()->setDraft re-renders -- owner: bookings-react; existing suppression is intentional pending typed cleanup.
|
|
189
|
-
useEffect(() => {
|
|
190
|
-
const person = personQuery.data;
|
|
191
|
-
if (!person || appliedPersonId.current === person.id)
|
|
192
|
-
return;
|
|
193
|
-
appliedPersonId.current = person.id;
|
|
194
|
-
apply({
|
|
195
|
-
firstName: person.firstName,
|
|
196
|
-
lastName: person.lastName,
|
|
197
|
-
email: person.email ?? undefined,
|
|
198
|
-
phone: person.phone ?? undefined,
|
|
199
|
-
personId: person.id,
|
|
200
|
-
});
|
|
201
|
-
}, [personQuery.data]);
|
|
202
|
-
// Hydrate the company fields from the picked CRM organization (B2B).
|
|
203
|
-
const selectedOrgId = orgMode && value.organizationId ? value.organizationId : undefined;
|
|
204
|
-
const orgQuery = useOrganization(selectedOrgId, { enabled: Boolean(selectedOrgId) });
|
|
205
|
-
const appliedOrgId = useRef(null);
|
|
206
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: applies once per resolved record; the ref guards re-entry -- owner: bookings-react; existing suppression is intentional pending typed cleanup.
|
|
207
|
-
useEffect(() => {
|
|
208
|
-
const org = orgQuery.data;
|
|
209
|
-
if (!org || appliedOrgId.current === org.id)
|
|
210
|
-
return;
|
|
211
|
-
appliedOrgId.current = org.id;
|
|
212
|
-
const companyName = org.legalName ?? org.name;
|
|
213
|
-
apply({
|
|
214
|
-
organizationId: org.id,
|
|
215
|
-
companyName,
|
|
216
|
-
taxId: org.taxId ?? undefined,
|
|
217
|
-
});
|
|
218
|
-
}, [orgQuery.data]);
|
|
219
|
-
// Hydrate the billing address from the picked person (B2C) or org (B2B)
|
|
220
|
-
// primary CRM address — so the operator never re-types it and the tax
|
|
221
|
-
// country is set. Missing → surfaced as a warning to fix in the picker.
|
|
222
|
-
const addressEntityType = orgMode ? "organization" : "person";
|
|
223
|
-
// Only the billing lead pulls an address; travelers don't carry one.
|
|
224
|
-
const addressEntityId = variant === "lead" ? (orgMode ? selectedOrgId : selectedPersonId) : undefined;
|
|
225
|
-
const addressQuery = useAddresses({
|
|
226
|
-
entityType: addressEntityType,
|
|
227
|
-
entityId: addressEntityId,
|
|
228
|
-
isPrimary: true,
|
|
229
|
-
limit: 1,
|
|
230
|
-
enabled: Boolean(addressEntityId),
|
|
231
|
-
});
|
|
232
|
-
const appliedAddressKey = useRef(null);
|
|
233
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: applies once per resolved address; the ref guards re-entry -- owner: bookings-react; existing suppression is intentional pending typed cleanup.
|
|
234
|
-
useEffect(() => {
|
|
235
|
-
const addr = addressQuery.data?.data?.[0];
|
|
236
|
-
if (!addressEntityId || !addr)
|
|
237
|
-
return;
|
|
238
|
-
const key = `${addressEntityType}:${addressEntityId}`;
|
|
239
|
-
if (appliedAddressKey.current === key)
|
|
240
|
-
return;
|
|
241
|
-
appliedAddressKey.current = key;
|
|
242
|
-
apply({
|
|
243
|
-
address: {
|
|
244
|
-
line1: addr.line1 ?? undefined,
|
|
245
|
-
line2: addr.line2 ?? undefined,
|
|
246
|
-
city: addr.city ?? undefined,
|
|
247
|
-
postal: addr.postalCode ?? undefined,
|
|
248
|
-
country: addr.country ?? undefined,
|
|
249
|
-
},
|
|
250
|
-
});
|
|
251
|
-
}, [addressQuery.data, addressEntityId]);
|
|
252
|
-
function commit(next) {
|
|
253
|
-
setValue(next);
|
|
254
|
-
if ((next.billTo ?? "person") === "organization") {
|
|
255
|
-
if (next.organizationId !== appliedOrgId.current)
|
|
256
|
-
appliedOrgId.current = null;
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
if (next.mode === "existing" && next.personId) {
|
|
260
|
-
if (next.personId !== appliedPersonId.current)
|
|
261
|
-
appliedPersonId.current = null;
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
appliedPersonId.current = null;
|
|
265
|
-
if (next.mode === "new") {
|
|
266
|
-
apply({
|
|
267
|
-
firstName: next.newPerson.firstName,
|
|
268
|
-
lastName: next.newPerson.lastName,
|
|
269
|
-
email: next.newPerson.email || undefined,
|
|
270
|
-
phone: next.newPerson.phone || undefined,
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
return (_jsx(PersonPickerSection, { value: value, onChange: commit,
|
|
275
|
-
// Org is an allowed target for the lead (so its CRM org search runs),
|
|
276
|
-
// but the toggle is hidden — Buyer type is the single control.
|
|
277
|
-
showOrganization: variant === "lead", hideTargetToggle: true, labels: {
|
|
278
|
-
person: variant === "traveler" ? t.travelerPickerLabel : t.leadContactPickerLabel,
|
|
279
|
-
} }));
|
|
280
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
/**
|
|
4
|
-
* Heads-up shown under the billing block when the picked lead (person for B2C,
|
|
5
|
-
* organization for B2B) already has a booking on the selected departure — with
|
|
6
|
-
* a link to each. Helps operators avoid accidental duplicate bookings.
|
|
7
|
-
*/
|
|
8
|
-
import { useAdminNavigate, useOperatorAdminMessages } from "@voyant-travel/admin";
|
|
9
|
-
import { useBookings } from "../hooks/use-bookings.js";
|
|
10
|
-
export function JourneyBillingDuplicateWarning({ buyerType, personId, organizationId, productId, departureSlotId, }) {
|
|
11
|
-
const t = useOperatorAdminMessages().bookings.detail.bookingJourney;
|
|
12
|
-
const navigate = useAdminNavigate();
|
|
13
|
-
const leadId = buyerType === "B2B" ? organizationId : personId;
|
|
14
|
-
// Only a scheduled departure (slot) gives a precise duplicate match.
|
|
15
|
-
const enabled = Boolean(leadId && departureSlotId);
|
|
16
|
-
const query = useBookings({
|
|
17
|
-
...(buyerType === "B2B" ? { organizationId } : { personId }),
|
|
18
|
-
productId,
|
|
19
|
-
availabilitySlotId: departureSlotId,
|
|
20
|
-
enabled,
|
|
21
|
-
});
|
|
22
|
-
const existing = (query.data?.data ?? []).filter((booking) => booking.status !== "cancelled");
|
|
23
|
-
if (!enabled || existing.length === 0)
|
|
24
|
-
return null;
|
|
25
|
-
return (_jsxs("div", { className: "space-y-1 rounded-md border border-amber-300 bg-amber-50 p-3 text-amber-900 text-sm dark:border-amber-700 dark:bg-amber-950 dark:text-amber-100", children: [_jsxs("p", { children: ["\u26A0 ", t.duplicateDepartureWarning] }), _jsx("ul", { className: "space-y-0.5", children: existing.map((booking) => (_jsxs("li", { children: [_jsx("button", { type: "button", className: "font-medium underline underline-offset-2 hover:opacity-80", onClick: () => navigate("booking.detail", { bookingId: booking.id }), children: booking.bookingNumber }), booking.items?.[0]?.title ? (_jsxs("span", { className: "text-amber-900/80 dark:text-amber-100/80", children: [" ", "\u00B7 ", booking.items[0].title] })) : null] }, booking.id))) })] }));
|
|
26
|
-
}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import type { DeparturePickerProps } from "../journey/index.js";
|
|
2
|
-
export declare function JourneyDeparturePicker({ productId, optionId, slotId, departureDate, onChange, lockDeparture, }: DeparturePickerProps & {
|
|
3
|
-
/** The departure is fixed (a sourced offer's date came in pre-selected) —
|
|
4
|
-
* show it read-only rather than a re-editable input. */
|
|
5
|
-
lockDeparture?: boolean;
|
|
6
|
-
}): React.ReactElement;
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useLocale } from "@voyant-travel/admin";
|
|
4
|
-
import { useSlots } from "@voyant-travel/operations-react/availability";
|
|
5
|
-
import { Combobox, ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from "@voyant-travel/ui/components/combobox";
|
|
6
|
-
import { DatePicker } from "@voyant-travel/ui/components/date-picker";
|
|
7
|
-
import { Label } from "@voyant-travel/ui/components/label";
|
|
8
|
-
import { useEffect, useMemo, useState } from "react";
|
|
9
|
-
import { getBookableDepartureSlots } from "../components/booking-create-utils.js";
|
|
10
|
-
import { useBookingsUiMessagesOrDefault } from "../i18n/provider.js";
|
|
11
|
-
export function JourneyDeparturePicker({ productId, optionId, slotId, departureDate, onChange, lockDeparture = false, }) {
|
|
12
|
-
const { resolvedLocale } = useLocale();
|
|
13
|
-
const dateFormatter = useMemo(() => new Intl.DateTimeFormat(resolvedLocale, {
|
|
14
|
-
year: "numeric",
|
|
15
|
-
month: "short",
|
|
16
|
-
day: "numeric",
|
|
17
|
-
}), [resolvedLocale]);
|
|
18
|
-
const messages = useBookingsUiMessagesOrDefault().bookingCreateDialog;
|
|
19
|
-
// Stable "now" so the slot query + future filter don't churn every render.
|
|
20
|
-
const [nowIso] = useState(() => new Date().toISOString());
|
|
21
|
-
const query = useSlots({
|
|
22
|
-
productId: productId || undefined,
|
|
23
|
-
status: "open",
|
|
24
|
-
startsAtFrom: nowIso,
|
|
25
|
-
limit: 100,
|
|
26
|
-
enabled: Boolean(productId) && !lockDeparture,
|
|
27
|
-
});
|
|
28
|
-
const slots = useMemo(() => getBookableDepartureSlots((query.data?.data ?? []), {
|
|
29
|
-
nowIso,
|
|
30
|
-
optionId,
|
|
31
|
-
}), [query.data?.data, nowIso, optionId]);
|
|
32
|
-
const slotMap = useMemo(() => new Map(slots.map((s) => [s.id, s])), [slots]);
|
|
33
|
-
const formatLabel = (slot) => {
|
|
34
|
-
const date = dateFormatter.format(new Date(slot.startsAt));
|
|
35
|
-
const remaining = !slot.unlimited && typeof slot.remainingPax === "number"
|
|
36
|
-
? ` · ${slot.remainingPax} ${messages.labels.remainingCapacity}`
|
|
37
|
-
: "";
|
|
38
|
-
return `${date}${remaining}`;
|
|
39
|
-
};
|
|
40
|
-
const [inputValue, setInputValue] = useState(() => {
|
|
41
|
-
const current = slotId ? slotMap.get(slotId) : undefined;
|
|
42
|
-
return current ? formatLabel(current) : "";
|
|
43
|
-
});
|
|
44
|
-
// The draft keeps the picked slotId across step navigation, but this
|
|
45
|
-
// component's input is local state and re-seeds to "" on remount (before
|
|
46
|
-
// slots load). Reflect the selected slot's label once it resolves so a
|
|
47
|
-
// revisited departure shows as selected rather than empty.
|
|
48
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: formatLabel is render-stable; slotId/slotMap are the real triggers -- owner: bookings-react; existing suppression is intentional pending typed cleanup.
|
|
49
|
-
useEffect(() => {
|
|
50
|
-
if (!slotId)
|
|
51
|
-
return;
|
|
52
|
-
const slot = slotMap.get(slotId);
|
|
53
|
-
if (slot)
|
|
54
|
-
setInputValue((prev) => (prev ? prev : formatLabel(slot)));
|
|
55
|
-
}, [slotId, slotMap]);
|
|
56
|
-
// Sourced offer with a fixed date (came in pre-selected) → show it read-only.
|
|
57
|
-
// A different date would be a different offer, so there's nothing to edit.
|
|
58
|
-
if (lockDeparture && departureDate) {
|
|
59
|
-
return (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { children: messages.fields.departure }), _jsx("p", { className: "font-medium text-sm", children: dateFormatter.format(new Date(`${departureDate.slice(0, 10)}T00:00:00`)) })] }));
|
|
60
|
-
}
|
|
61
|
-
// No scheduled departures for this product → a free date so the operator can
|
|
62
|
-
// still set a departure for non-scheduled products. (No time field — tour /
|
|
63
|
-
// package departures are by date.)
|
|
64
|
-
if (!query.isLoading && slots.length === 0) {
|
|
65
|
-
return (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "bj-departure-date", children: messages.fields.departure }), _jsx(DatePicker, { className: "w-full sm:max-w-xs", value: departureDate ?? "", onChange: (value) => onChange({ departureDate: value || null }) })] }));
|
|
66
|
-
}
|
|
67
|
-
return (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsxs(Label, { children: [messages.fields.departure, " ", _jsx("span", { className: "text-destructive", children: "*" })] }), _jsxs(Combobox, { items: slots.map((s) => s.id), value: slotId || null, inputValue: inputValue, autoHighlight: true, disabled: query.isLoading, filter: () => true, itemToStringLabel: (id) => {
|
|
68
|
-
const slot = slotMap.get(id);
|
|
69
|
-
return slot ? formatLabel(slot) : id;
|
|
70
|
-
}, itemToStringValue: (id) => id, onInputValueChange: (next) => setInputValue(next), onValueChange: (next) => {
|
|
71
|
-
const id = next ?? "";
|
|
72
|
-
const slot = id ? slotMap.get(id) : undefined;
|
|
73
|
-
if (!slot) {
|
|
74
|
-
onChange({ slotId: null, departureDate: null });
|
|
75
|
-
setInputValue("");
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
onChange({
|
|
79
|
-
slotId: slot.id,
|
|
80
|
-
// Anchor the free-date field on the slot's date so downstream
|
|
81
|
-
// (payment schedules, hold) has a concrete departure date.
|
|
82
|
-
departureDate: slot.startsAt.slice(0, 10),
|
|
83
|
-
});
|
|
84
|
-
setInputValue(formatLabel(slot));
|
|
85
|
-
}, children: [_jsx(ComboboxInput, { placeholder: messages.placeholders.departure, showClear: Boolean(slotId) }), _jsxs(ComboboxContent, { children: [_jsx(ComboboxEmpty, { children: messages.placeholders.departureEmpty }), _jsx(ComboboxList, { children: _jsx(ComboboxCollection, { children: (id) => {
|
|
86
|
-
const slot = slotMap.get(id);
|
|
87
|
-
if (!slot)
|
|
88
|
-
return null;
|
|
89
|
-
return (_jsx(ComboboxItem, { value: slot.id, children: formatLabel(slot) }, slot.id));
|
|
90
|
-
} }) })] })] })] }));
|
|
91
|
-
}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
/**
|
|
4
|
-
* Admin Travel Credit picker: an async combobox over the admin Travel Credit list.
|
|
5
|
-
* Staff search by code and pick a Travel Credit (the full remaining balance is
|
|
6
|
-
* redeemed); they never need to know the exact code, unlike storefront
|
|
7
|
-
* customers. Wired into `<BookingJourneyHost />` via `renderTravelCreditPicker`.
|
|
8
|
-
*/
|
|
9
|
-
import { useLocale, useOperatorAdminMessages } from "@voyant-travel/admin";
|
|
10
|
-
import { useTravelCredits } from "@voyant-travel/finance-react";
|
|
11
|
-
import { Combobox, ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from "@voyant-travel/ui/components/combobox";
|
|
12
|
-
import { Label } from "@voyant-travel/ui/components/label";
|
|
13
|
-
import { useEffect, useState } from "react";
|
|
14
|
-
function formatMoney(cents, currency, locale) {
|
|
15
|
-
try {
|
|
16
|
-
return new Intl.NumberFormat(locale, { style: "currency", currency }).format(cents / 100);
|
|
17
|
-
}
|
|
18
|
-
catch {
|
|
19
|
-
return `${(cents / 100).toFixed(2)} ${currency}`;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
function travelCreditLabel(travelCredit, locale) {
|
|
23
|
-
return `${travelCredit.code} · ${formatMoney(travelCredit.remainingAmountCents, travelCredit.currency, locale)}`;
|
|
24
|
-
}
|
|
25
|
-
export function JourneyTravelCreditPicker({ value, onApply, }) {
|
|
26
|
-
const { resolvedLocale } = useLocale();
|
|
27
|
-
const t = useOperatorAdminMessages().bookings.detail.bookingJourney;
|
|
28
|
-
const [inputValue, setInputValue] = useState("");
|
|
29
|
-
const [search, setSearch] = useState("");
|
|
30
|
-
// Active Travel Credits with a remaining balance, filtered by the typed code.
|
|
31
|
-
const query = useTravelCredits({
|
|
32
|
-
status: "active",
|
|
33
|
-
hasBalance: true,
|
|
34
|
-
search: search || undefined,
|
|
35
|
-
limit: 20,
|
|
36
|
-
});
|
|
37
|
-
const travelCredits = query.data?.data ?? [];
|
|
38
|
-
const byId = new Map(travelCredits.map((travelCredit) => [travelCredit.id, travelCredit]));
|
|
39
|
-
const selectedId = value.travelCreditId ?? null;
|
|
40
|
-
// Reflect a pre-selected Travel Credit's label once its record loads.
|
|
41
|
-
useEffect(() => {
|
|
42
|
-
if (selectedId && byId.has(selectedId)) {
|
|
43
|
-
setInputValue(travelCreditLabel(byId.get(selectedId), resolvedLocale));
|
|
44
|
-
}
|
|
45
|
-
}, [selectedId, byId, resolvedLocale]);
|
|
46
|
-
return (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { children: t.travelCreditPickerLabel }), _jsxs(Combobox, { items: travelCredits.map((travelCredit) => travelCredit.id), value: selectedId, inputValue: inputValue, autoHighlight: true, itemToStringLabel: (id) => {
|
|
47
|
-
const v = byId.get(id);
|
|
48
|
-
return v ? travelCreditLabel(v, resolvedLocale) : id;
|
|
49
|
-
}, itemToStringValue: (id) => id, onInputValueChange: (next) => {
|
|
50
|
-
setInputValue(next);
|
|
51
|
-
setSearch(next);
|
|
52
|
-
if (!next)
|
|
53
|
-
onApply(null);
|
|
54
|
-
}, onValueChange: (next) => {
|
|
55
|
-
const id = next ?? null;
|
|
56
|
-
if (!id) {
|
|
57
|
-
onApply(null);
|
|
58
|
-
setInputValue("");
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
const v = byId.get(id);
|
|
62
|
-
if (v) {
|
|
63
|
-
onApply({ travelCreditId: v.id, amountCents: v.remainingAmountCents });
|
|
64
|
-
setInputValue(travelCreditLabel(v, resolvedLocale));
|
|
65
|
-
}
|
|
66
|
-
}, children: [_jsx(ComboboxInput, { placeholder: t.travelCreditSearchPlaceholder, showClear: !!selectedId }), _jsxs(ComboboxContent, { children: [_jsx(ComboboxEmpty, { children: t.travelCreditEmpty }), _jsx(ComboboxList, { children: _jsx(ComboboxCollection, { children: (id) => {
|
|
67
|
-
const v = byId.get(id);
|
|
68
|
-
if (!v)
|
|
69
|
-
return null;
|
|
70
|
-
return (_jsx(ComboboxItem, { value: v.id, children: _jsxs("div", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate font-medium", children: v.code }), _jsx("span", { className: "truncate text-muted-foreground text-xs", children: formatMoney(v.remainingAmountCents, v.currency, resolvedLocale) })] }) }, v.id));
|
|
71
|
-
} }) })] })] })] }));
|
|
72
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { UnitsPickerProps } from "../journey/index.js";
|
|
2
|
-
/**
|
|
3
|
-
* Admin rooms/units picker for the booking journey's `"option-units"`
|
|
4
|
-
* sub-step. Wraps the shared `OptionUnitsStepperSection` (which loads the
|
|
5
|
-
* product's option units + per-slot availability) and mirrors the picked
|
|
6
|
-
* quantities into the journey draft's `configure.optionSelections`.
|
|
7
|
-
*
|
|
8
|
-
* Wired into `<BookingJourneyHost />` via the `renderUnitsPicker` slot.
|
|
9
|
-
*/
|
|
10
|
-
export declare function JourneyUnitsPicker({ productId, optionId, slotId, selections, onChange, }: UnitsPickerProps): React.ReactElement;
|