@voyant-travel/trips-react 0.110.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.
Files changed (114) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +30 -0
  3. package/dist/admin/admin-trips-page-controls.d.ts +28 -0
  4. package/dist/admin/admin-trips-page-controls.d.ts.map +1 -0
  5. package/dist/admin/admin-trips-page-controls.js +28 -0
  6. package/dist/admin/admin-trips-page-model.d.ts +87 -0
  7. package/dist/admin/admin-trips-page-model.d.ts.map +1 -0
  8. package/dist/admin/admin-trips-page-model.js +457 -0
  9. package/dist/admin/admin-trips-page.d.ts +6 -0
  10. package/dist/admin/admin-trips-page.d.ts.map +1 -0
  11. package/dist/admin/admin-trips-page.js +322 -0
  12. package/dist/admin/admin-trips-panels.d.ts +11 -0
  13. package/dist/admin/admin-trips-panels.d.ts.map +1 -0
  14. package/dist/admin/admin-trips-panels.js +11 -0
  15. package/dist/admin/index.d.ts +63 -0
  16. package/dist/admin/index.d.ts.map +1 -0
  17. package/dist/admin/index.js +119 -0
  18. package/dist/admin/pages/trip-detail-page.d.ts +10 -0
  19. package/dist/admin/pages/trip-detail-page.d.ts.map +1 -0
  20. package/dist/admin/pages/trip-detail-page.js +12 -0
  21. package/dist/admin/trip-component-display.d.ts +10 -0
  22. package/dist/admin/trip-component-display.d.ts.map +1 -0
  23. package/dist/admin/trip-component-display.js +137 -0
  24. package/dist/admin/trip-detail-host.d.ts +12 -0
  25. package/dist/admin/trip-detail-host.d.ts.map +1 -0
  26. package/dist/admin/trip-detail-host.js +37 -0
  27. package/dist/admin/trip-detail-record-model.d.ts +30 -0
  28. package/dist/admin/trip-detail-record-model.d.ts.map +1 -0
  29. package/dist/admin/trip-detail-record-model.js +56 -0
  30. package/dist/admin/trip-detail-record.d.ts +47 -0
  31. package/dist/admin/trip-detail-record.d.ts.map +1 -0
  32. package/dist/admin/trip-detail-record.js +170 -0
  33. package/dist/admin/trip-list-filters.d.ts +33 -0
  34. package/dist/admin/trip-list-filters.d.ts.map +1 -0
  35. package/dist/admin/trip-list-filters.js +94 -0
  36. package/dist/admin/trips-host.d.ts +15 -0
  37. package/dist/admin/trips-host.d.ts.map +1 -0
  38. package/dist/admin/trips-host.js +233 -0
  39. package/dist/admin/trips-panels/catalog-configurator.d.ts +34 -0
  40. package/dist/admin/trips-panels/catalog-configurator.d.ts.map +1 -0
  41. package/dist/admin/trips-panels/catalog-configurator.js +200 -0
  42. package/dist/admin/trips-panels/catalog-options.d.ts +58 -0
  43. package/dist/admin/trips-panels/catalog-options.d.ts.map +1 -0
  44. package/dist/admin/trips-panels/catalog-options.js +124 -0
  45. package/dist/admin/trips-panels/committed-component-card.d.ts +27 -0
  46. package/dist/admin/trips-panels/committed-component-card.d.ts.map +1 -0
  47. package/dist/admin/trips-panels/committed-component-card.js +44 -0
  48. package/dist/admin/trips-panels/display.d.ts +51 -0
  49. package/dist/admin/trips-panels/display.d.ts.map +1 -0
  50. package/dist/admin/trips-panels/display.js +336 -0
  51. package/dist/admin/trips-panels/flight-configurator.d.ts +34 -0
  52. package/dist/admin/trips-panels/flight-configurator.d.ts.map +1 -0
  53. package/dist/admin/trips-panels/flight-configurator.js +208 -0
  54. package/dist/admin/trips-panels/manual-configurators.d.ts +16 -0
  55. package/dist/admin/trips-panels/manual-configurators.d.ts.map +1 -0
  56. package/dist/admin/trips-panels/manual-configurators.js +41 -0
  57. package/dist/admin/trips-panels/pending-component-card.d.ts +16 -0
  58. package/dist/admin/trips-panels/pending-component-card.d.ts.map +1 -0
  59. package/dist/admin/trips-panels/pending-component-card.js +29 -0
  60. package/dist/admin/trips-panels/shared.d.ts +122 -0
  61. package/dist/admin/trips-panels/shared.d.ts.map +1 -0
  62. package/dist/admin/trips-panels/shared.js +152 -0
  63. package/dist/admin/trips-panels/travelers-section.d.ts +53 -0
  64. package/dist/admin/trips-panels/travelers-section.d.ts.map +1 -0
  65. package/dist/admin/trips-panels/travelers-section.js +183 -0
  66. package/dist/admin/trips-panels/trip-preview-rail.d.ts +52 -0
  67. package/dist/admin/trips-panels/trip-preview-rail.d.ts.map +1 -0
  68. package/dist/admin/trips-panels/trip-preview-rail.js +122 -0
  69. package/dist/cache.d.ts +9 -0
  70. package/dist/cache.d.ts.map +1 -0
  71. package/dist/cache.js +21 -0
  72. package/dist/client.d.ts +15 -0
  73. package/dist/client.d.ts.map +1 -0
  74. package/dist/client.js +51 -0
  75. package/dist/hooks/index.d.ts +7 -0
  76. package/dist/hooks/index.d.ts.map +1 -0
  77. package/dist/hooks/index.js +6 -0
  78. package/dist/hooks/use-price-trip.d.ts +3 -0
  79. package/dist/hooks/use-price-trip.d.ts.map +1 -0
  80. package/dist/hooks/use-price-trip.js +17 -0
  81. package/dist/hooks/use-reserve-trip.d.ts +3 -0
  82. package/dist/hooks/use-reserve-trip.d.ts.map +1 -0
  83. package/dist/hooks/use-reserve-trip.js +17 -0
  84. package/dist/hooks/use-trip-checkout.d.ts +3 -0
  85. package/dist/hooks/use-trip-checkout.d.ts.map +1 -0
  86. package/dist/hooks/use-trip-checkout.js +17 -0
  87. package/dist/hooks/use-trip-components.d.ts +40 -0
  88. package/dist/hooks/use-trip-components.d.ts.map +1 -0
  89. package/dist/hooks/use-trip-components.js +13 -0
  90. package/dist/hooks/use-trip.d.ts +5 -0
  91. package/dist/hooks/use-trip.d.ts.map +1 -0
  92. package/dist/hooks/use-trip.js +13 -0
  93. package/dist/hooks/use-trips.d.ts +6 -0
  94. package/dist/hooks/use-trips.d.ts.map +1 -0
  95. package/dist/hooks/use-trips.js +12 -0
  96. package/dist/index.d.ts +9 -0
  97. package/dist/index.d.ts.map +1 -0
  98. package/dist/index.js +8 -0
  99. package/dist/operations.d.ts +212 -0
  100. package/dist/operations.d.ts.map +1 -0
  101. package/dist/operations.js +92 -0
  102. package/dist/provider.d.ts +2 -0
  103. package/dist/provider.d.ts.map +1 -0
  104. package/dist/provider.js +1 -0
  105. package/dist/query-keys.d.ts +10 -0
  106. package/dist/query-keys.d.ts.map +1 -0
  107. package/dist/query-keys.js +9 -0
  108. package/dist/query-options.d.ts +167 -0
  109. package/dist/query-options.d.ts.map +1 -0
  110. package/dist/query-options.js +22 -0
  111. package/dist/schemas.d.ts +69 -0
  112. package/dist/schemas.d.ts.map +1 -0
  113. package/dist/schemas.js +16 -0
  114. package/package.json +133 -0
@@ -0,0 +1,322 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useMutation } from "@tanstack/react-query";
4
+ import { useOperatorAdminMessages as useAdminMessages, useAdminNavigate, } from "@voyant-travel/admin";
5
+ import { emptyPersonPickerValue } from "@voyant-travel/bookings-react/components/person-picker-section";
6
+ import { emptyVoucherPickerValue } from "@voyant-travel/bookings-react/components/voucher-picker-section";
7
+ import { PersonPickerSection, } from "@voyant-travel/bookings-react/ui";
8
+ import { usePerson } from "@voyant-travel/relationships-react";
9
+ import { CurrencyCombobox } from "@voyant-travel/ui/components/currency-combobox";
10
+ import { Textarea } from "@voyant-travel/ui/components/textarea";
11
+ import { useEffect, useMemo, useState } from "react";
12
+ import { addTripComponent, createTrip, getTrip, previewTripCancellation, priceTrip, removeTripComponent, reserveTrip, startTripCheckout, updateTripComponent, } from "../operations.js";
13
+ import { useVoyantTripsContext } from "../provider.js";
14
+ import { CancellationPreviewSection, CheckboxRow, } from "./admin-trips-page-controls.js";
15
+ import { apiError, assertTripCreationRequirements, booleanFromRecord, defaultPaymentCurrency, derivePayerEmail, derivePayerName, failuresToString, hydrateBilling, hydrateTravelers, hydrateVoucher, metadataWithComponentBookingSetup, pendingToAddInput, serializeBilling, stringFromRecord, } from "./admin-trips-page-model.js";
16
+ import { AddComponentMenu, CommittedComponentCard, ComponentsEmpty, componentTitleFor, Field, findOverlappingComponent, newPendingComponent, PendingComponentCard, PrimaryAction, Section, StatusAlert, sortComponentsBySchedule, TripPreviewRail, TripTravelersSection, } from "./admin-trips-panels.js";
17
+ export function AdminTripsPage({ initialTrip = null }) {
18
+ const navigateTo = useAdminNavigate();
19
+ const t = useAdminMessages().trips.adminComposer;
20
+ const [state, setState] = useState({
21
+ trip: initialTrip,
22
+ message: null,
23
+ error: null,
24
+ });
25
+ const [billing, setBilling] = useState(emptyPersonPickerValue);
26
+ const [travelers, setTravelers] = useState([]);
27
+ const [notes, setNotes] = useState("");
28
+ const [pending, setPending] = useState([]);
29
+ const [committingLocalId, setCommittingLocalId] = useState(null);
30
+ const [voucher, setVoucher] = useState(emptyVoucherPickerValue);
31
+ const [createAsDraft, setCreateAsDraft] = useState(false);
32
+ const [paymentCurrency, setPaymentCurrency] = useState(defaultPaymentCurrency);
33
+ const [selectedCancellationIds, setSelectedCancellationIds] = useState([]);
34
+ const [cancellationReason, setCancellationReason] = useState(t.cancellation.defaultReason);
35
+ const [cancellationPreview, setCancellationPreview] = useState(null);
36
+ const { baseUrl, fetcher } = useVoyantTripsContext();
37
+ const client = useMemo(() => ({ baseUrl, fetcher }), [baseUrl, fetcher]);
38
+ const trip = state.trip;
39
+ const envelopeId = trip?.envelope.id;
40
+ const components = useMemo(() => sortComponentsBySchedule((trip?.components ?? []).filter((component) => component.status !== "removed")), [trip?.components]);
41
+ const selectedCount = selectedCancellationIds.length;
42
+ const envelopeStatus = trip?.envelope.status;
43
+ // Once the trip is reserved or further along, removal touches real holds /
44
+ // bookings — operators must go through cancellation preview. Before that
45
+ // (`draft` / priced) a component is a no-op blueprint and can be deleted.
46
+ const trapReserved = envelopeStatus === "reserved" ||
47
+ envelopeStatus === "reserve_in_progress" ||
48
+ envelopeStatus === "checkout_started" ||
49
+ envelopeStatus === "booked";
50
+ const billingPersonQuery = usePerson(billing.personId || undefined, {
51
+ enabled: billing.mode === "existing" && Boolean(billing.personId),
52
+ });
53
+ const payerName = derivePayerName(billing, billingPersonQuery.data, t);
54
+ const payerEmail = derivePayerEmail(billing, billingPersonQuery.data);
55
+ useEffect(() => {
56
+ setState((current) => ({ ...current, trip: initialTrip }));
57
+ const travelerParty = initialTrip?.envelope.travelerParty;
58
+ if (!travelerParty) {
59
+ setBilling(emptyPersonPickerValue);
60
+ setTravelers([]);
61
+ setNotes("");
62
+ setVoucher(emptyVoucherPickerValue);
63
+ setCreateAsDraft(false);
64
+ setPaymentCurrency(defaultPaymentCurrency);
65
+ return;
66
+ }
67
+ setBilling(hydrateBilling(travelerParty));
68
+ setTravelers(hydrateTravelers(travelerParty));
69
+ setNotes(initialTrip.envelope.description ?? "");
70
+ setVoucher(hydrateVoucher(travelerParty));
71
+ const constraints = initialTrip.envelope.constraints;
72
+ setCreateAsDraft(booleanFromRecord(constraints, "createAsDraft"));
73
+ setPaymentCurrency(stringFromRecord(constraints, "paymentCurrency") ||
74
+ initialTrip.envelope.aggregateCurrency ||
75
+ defaultPaymentCurrency);
76
+ }, [initialTrip]);
77
+ function showError(error) {
78
+ setState((current) => ({ ...current, error: apiError(error, t), message: null }));
79
+ }
80
+ async function ensureTrip() {
81
+ if (state.trip)
82
+ return state.trip;
83
+ assertTripCreationRequirements({ billing, travelers, payerName, payerEmail }, t);
84
+ const created = await createTrip(client, {
85
+ description: notes || undefined,
86
+ travelerParty: {
87
+ billing: serializeBilling(billing, payerName, payerEmail),
88
+ travelers,
89
+ voucher: voucher.picked
90
+ ? {
91
+ id: voucher.picked.id,
92
+ code: voucher.picked.code,
93
+ currency: voucher.picked.currency,
94
+ remainingAmountCents: voucher.picked.remainingAmountCents,
95
+ }
96
+ : null,
97
+ },
98
+ constraints: {
99
+ channel: "admin-composer",
100
+ compositionMode: "cross-vertical",
101
+ createAsDraft,
102
+ paymentCurrency,
103
+ },
104
+ });
105
+ setState((current) => ({ ...current, trip: created }));
106
+ return created;
107
+ }
108
+ const commitMutation = useMutation({
109
+ mutationFn: async (component) => {
110
+ const currentTrip = await ensureTrip();
111
+ const input = pendingToAddInput(component, {
112
+ billing,
113
+ travelers,
114
+ payerName,
115
+ payerEmail,
116
+ paymentCurrency,
117
+ }, t);
118
+ if (!input)
119
+ throw new Error(t.errors.componentNotReady);
120
+ await addTripComponent(client, currentTrip.envelope.id, input);
121
+ return priceTrip(client, currentTrip.envelope.id, {
122
+ scope: { locale: "en-US", audience: "staff", market: "default", currency: paymentCurrency },
123
+ });
124
+ },
125
+ onSuccess: (result, component) => {
126
+ setPending((current) => current.filter((p) => p.localId !== component.localId));
127
+ setState({
128
+ trip: { envelope: result.envelope, components: result.components },
129
+ message: t.statusMessages.componentAddedAndPriced,
130
+ error: failuresToString(result.failures, t),
131
+ });
132
+ setCancellationPreview(null);
133
+ setCommittingLocalId(null);
134
+ },
135
+ onError: (error, component) => {
136
+ const message = apiError(error, t);
137
+ setPending((current) => current.map((p) => (p.localId === component.localId ? { ...p, commitError: message } : p)));
138
+ setCommittingLocalId(null);
139
+ },
140
+ });
141
+ const reserveMutation = useMutation({
142
+ mutationFn: async () => {
143
+ if (!envelopeId)
144
+ throw new Error(t.errors.priceTripFirst);
145
+ assertTripCreationRequirements({ billing, travelers, payerName, payerEmail }, t);
146
+ const reserved = await reserveTrip(client, envelopeId, {
147
+ idempotencyKey: `admin-reserve-${envelopeId}`,
148
+ refreshScope: {
149
+ locale: "en-US",
150
+ audience: "staff",
151
+ market: "default",
152
+ currency: paymentCurrency,
153
+ },
154
+ });
155
+ if (reserved.failures.length > 0)
156
+ return { reserved, checkout: null };
157
+ const checkout = await startTripCheckout(client, envelopeId, {
158
+ idempotencyKey: `admin-checkout-${envelopeId}`,
159
+ intent: "card",
160
+ request: { initiatedBy: "admin-trips", collectionCurrency: paymentCurrency },
161
+ });
162
+ return { reserved, checkout };
163
+ },
164
+ onSuccess: ({ reserved, checkout }) => {
165
+ const trip = checkout
166
+ ? { envelope: checkout.envelope, components: checkout.components }
167
+ : { envelope: reserved.envelope, components: reserved.components };
168
+ setState({
169
+ trip,
170
+ message: checkout?.target.paymentSessionId
171
+ ? t.statusMessages.tripReservedWithLink
172
+ : t.statusMessages.tripReserved,
173
+ error: failuresToString(reserved.failures, t),
174
+ });
175
+ // Keep operators in the trip aggregate after reserve; individual booking
176
+ // links remain available from each component card.
177
+ if (reserved.failures.length === 0) {
178
+ navigateTo("trip.detail", { tripId: reserved.envelope.id });
179
+ }
180
+ },
181
+ onError: (error) => showError(error),
182
+ });
183
+ const removeComponentMutation = useMutation({
184
+ mutationFn: async (componentId) => {
185
+ if (!envelopeId)
186
+ throw new Error(t.errors.noTrip);
187
+ await removeTripComponent(client, componentId);
188
+ return getTrip(client, envelopeId);
189
+ },
190
+ onMutate: (componentId) => {
191
+ const previousTrip = state.trip;
192
+ // Optimistically mark the component as removed so the card disappears
193
+ // immediately. Aggregate totals re-derive from the visible components.
194
+ if (previousTrip) {
195
+ setState((current) => ({
196
+ ...current,
197
+ trip: {
198
+ ...previousTrip,
199
+ components: previousTrip.components.map((component) => component.id === componentId
200
+ ? { ...component, status: "removed" }
201
+ : component),
202
+ },
203
+ }));
204
+ }
205
+ setSelectedCancellationIds((current) => current.filter((id) => id !== componentId));
206
+ setCancellationPreview(null);
207
+ return { previousTrip };
208
+ },
209
+ onSuccess: (updatedTrip) => {
210
+ setState({ trip: updatedTrip, message: t.statusMessages.componentRemoved, error: null });
211
+ },
212
+ onError: (error, _componentId, context) => {
213
+ if (context?.previousTrip) {
214
+ setState((current) => ({ ...current, trip: context.previousTrip }));
215
+ }
216
+ showError(error);
217
+ },
218
+ });
219
+ const updateComponentSetupMutation = useMutation({
220
+ mutationFn: async ({ componentId, metadata, }) => updateTripComponent(client, componentId, { metadata }),
221
+ onMutate: ({ componentId, metadata }) => {
222
+ const previousTrip = state.trip;
223
+ if (previousTrip) {
224
+ setState((current) => ({
225
+ ...current,
226
+ trip: {
227
+ ...previousTrip,
228
+ components: previousTrip.components.map((component) => component.id === componentId ? { ...component, metadata } : component),
229
+ },
230
+ }));
231
+ }
232
+ return { previousTrip };
233
+ },
234
+ onSuccess: (updatedComponent) => {
235
+ setState((current) => current.trip
236
+ ? {
237
+ ...current,
238
+ trip: {
239
+ ...current.trip,
240
+ components: current.trip.components.map((component) => component.id === updatedComponent.id ? updatedComponent : component),
241
+ },
242
+ }
243
+ : current);
244
+ },
245
+ onError: (error, _input, context) => {
246
+ if (context?.previousTrip) {
247
+ setState((current) => ({ ...current, trip: context.previousTrip }));
248
+ }
249
+ showError(error);
250
+ },
251
+ });
252
+ const cancellationMutation = useMutation({
253
+ mutationFn: async () => {
254
+ if (!envelopeId)
255
+ throw new Error(t.errors.noTripToCancel);
256
+ return previewTripCancellation(client, envelopeId, {
257
+ componentIds: selectedCancellationIds,
258
+ reason: cancellationReason,
259
+ request: { initiatedBy: "admin" },
260
+ });
261
+ },
262
+ onSuccess: (result) => {
263
+ setCancellationPreview({
264
+ refund: result.preview.estimatedRefundAmountCents,
265
+ penalty: result.preview.estimatedPenaltyAmountCents,
266
+ staffActionRequired: result.preview.staffActionRequired,
267
+ warnings: result.preview.warnings,
268
+ });
269
+ setState({
270
+ trip: { envelope: result.envelope, components: result.components },
271
+ message: t.statusMessages.cancellationPreviewReady,
272
+ error: null,
273
+ });
274
+ },
275
+ onError: (error) => showError(error),
276
+ });
277
+ const isBusy = commitMutation.isPending ||
278
+ reserveMutation.isPending ||
279
+ cancellationMutation.isPending ||
280
+ removeComponentMutation.isPending ||
281
+ updateComponentSetupMutation.isPending;
282
+ function handleAddVertical(kind) {
283
+ setPending((current) => [...current, newPendingComponent(kind)]);
284
+ }
285
+ function updatePending(next) {
286
+ setPending((current) => current.map((p) => (p.localId === next.localId ? next : p)));
287
+ }
288
+ function removePending(localId) {
289
+ setPending((current) => current.filter((p) => p.localId !== localId));
290
+ }
291
+ function toggleCancellationSelection(componentId, checked) {
292
+ setCancellationPreview(null);
293
+ setSelectedCancellationIds((current) => checked
294
+ ? [...new Set([...current, componentId])]
295
+ : current.filter((id) => id !== componentId));
296
+ }
297
+ function updateComponentBookingSetup(component, setup) {
298
+ const metadata = metadataWithComponentBookingSetup(component, setup);
299
+ updateComponentSetupMutation.mutate({ componentId: component.id, metadata });
300
+ }
301
+ function commitPending(component) {
302
+ const overlap = findOverlappingComponent(component, components);
303
+ if (overlap) {
304
+ setPending((current) => current.map((p) => p.localId === component.localId
305
+ ? {
306
+ ...p,
307
+ commitError: `These dates overlap with "${componentTitleFor(overlap)}". Adjust the schedule before adding to the trip.`,
308
+ }
309
+ : p));
310
+ return;
311
+ }
312
+ setCommittingLocalId(component.localId);
313
+ setPending((current) => current.map((p) => (p.localId === component.localId ? { ...p, commitError: null } : p)));
314
+ commitMutation.mutate(component);
315
+ }
316
+ return (_jsxs("main", { className: "mx-auto flex w-full max-w-screen-2xl flex-col gap-6 px-4 py-6 sm:px-6 lg:px-8", children: [_jsxs("header", { className: "flex flex-col gap-1", children: [_jsx("h1", { className: "font-semibold text-2xl tracking-tight", children: t.heading }), _jsx("p", { className: "text-muted-foreground text-sm", children: t.subheading })] }), state.error ? (_jsx(StatusAlert, { title: t.requestFailed, message: state.error, tone: "error" })) : null, _jsxs("div", { className: "grid min-h-0 flex-1 gap-6 lg:grid-cols-12", children: [_jsxs("div", { className: "flex min-w-0 flex-col gap-4 lg:col-span-8", children: [_jsx(Section, { title: t.billingSectionTitle, children: _jsx(PersonPickerSection, { value: billing, onChange: setBilling }) }), _jsx(TripTravelersSection, { value: travelers, onChange: setTravelers, billingPersonId: billing.mode === "existing" ? billing.personId || null : null }), _jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("h2", { className: "font-medium text-base", children: t.itinerarySectionTitle }), components.length === 0 && pending.length === 0 ? _jsx(ComponentsEmpty, {}) : null, components.map((component, index) => (_jsx(CommittedComponentCard, { component: component, index: index, selectable: trapReserved, selected: selectedCancellationIds.includes(component.id), onSelectedChange: (checked) => toggleCancellationSelection(component.id, checked), onRemove: trapReserved ? undefined : () => removeComponentMutation.mutate(component.id), removePending: removeComponentMutation.isPending &&
317
+ removeComponentMutation.variables === component.id, bookingSetupEditable: !trapReserved, bookingSetupSaving: updateComponentSetupMutation.isPending &&
318
+ updateComponentSetupMutation.variables?.componentId === component.id, onBookingSetupChange: updateComponentBookingSetup }, component.id))), pending.map((entry) => (_jsx(PendingComponentCard, { pending: entry, onChange: updatePending, onRemove: () => removePending(entry.localId), onCommit: () => commitPending(entry), committing: committingLocalId === entry.localId && commitMutation.isPending, travelers: travelers }, entry.localId))), _jsx(AddComponentMenu, { onAdd: handleAddVertical, disabled: isBusy })] }), trapReserved && selectedCount > 0 ? (_jsx(CancellationPreviewSection, { messages: t, selectedCount: selectedCount, cancellationReason: cancellationReason, onCancellationReasonChange: setCancellationReason, cancellationPreview: cancellationPreview, paymentCurrency: paymentCurrency, isBusy: isBusy, hasEnvelope: Boolean(envelopeId), isPending: cancellationMutation.isPending, onPreview: () => cancellationMutation.mutate(), onClearSelection: () => {
319
+ setSelectedCancellationIds([]);
320
+ setCancellationPreview(null);
321
+ } })) : null, _jsx(Section, { title: t.internalNotesSectionTitle, description: t.internalNotesSectionDescription, children: _jsx(Field, { label: t.internalNotesLabel, children: _jsx(Textarea, { rows: 3, value: notes, onChange: (event) => setNotes(event.target.value), placeholder: t.internalNotesPlaceholder }) }) }), _jsx(Section, { title: t.paymentSectionTitle, children: _jsx(Field, { label: t.paymentCurrencyLabel, children: _jsx(CurrencyCombobox, { value: paymentCurrency, onChange: (value) => setPaymentCurrency(value ?? defaultPaymentCurrency) }) }) }), _jsx(Section, { title: t.onReserveSectionTitle, description: t.onReserveSectionDescription, children: _jsx(CheckboxRow, { id: "composer-create-as-draft", checked: createAsDraft, onCheckedChange: setCreateAsDraft, label: t.startInDraftLabel, hint: t.startInDraftHint }) }), _jsx(PrimaryAction, { status: trip?.envelope.status, componentCount: components.length, isBusy: isBusy, pricePending: commitMutation.isPending, reservePending: reserveMutation.isPending, onReserve: () => reserveMutation.mutate() })] }), _jsx("aside", { className: "flex flex-col gap-4 lg:col-span-4", children: _jsx(TripPreviewRail, { trip: trip, pendingCount: pending.length, travelers: travelers, billing: billing, billingPersonId: billing.mode === "existing" ? billing.personId || null : null, voucher: voucher, onVoucherChange: setVoucher, paymentCurrency: paymentCurrency }) })] })] }));
322
+ }
@@ -0,0 +1,11 @@
1
+ export * from "./trips-panels/catalog-configurator.js";
2
+ export * from "./trips-panels/catalog-options.js";
3
+ export * from "./trips-panels/committed-component-card.js";
4
+ export * from "./trips-panels/display.js";
5
+ export * from "./trips-panels/flight-configurator.js";
6
+ export * from "./trips-panels/manual-configurators.js";
7
+ export * from "./trips-panels/pending-component-card.js";
8
+ export * from "./trips-panels/shared.js";
9
+ export * from "./trips-panels/travelers-section.js";
10
+ export * from "./trips-panels/trip-preview-rail.js";
11
+ //# sourceMappingURL=admin-trips-panels.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin-trips-panels.d.ts","sourceRoot":"","sources":["../../src/admin/admin-trips-panels.tsx"],"names":[],"mappings":"AAEA,cAAc,wCAAwC,CAAA;AACtD,cAAc,mCAAmC,CAAA;AACjD,cAAc,4CAA4C,CAAA;AAC1D,cAAc,2BAA2B,CAAA;AACzC,cAAc,uCAAuC,CAAA;AACrD,cAAc,wCAAwC,CAAA;AACtD,cAAc,0CAA0C,CAAA;AACxD,cAAc,0BAA0B,CAAA;AACxC,cAAc,qCAAqC,CAAA;AACnD,cAAc,qCAAqC,CAAA"}
@@ -0,0 +1,11 @@
1
+ "use client";
2
+ export * from "./trips-panels/catalog-configurator.js";
3
+ export * from "./trips-panels/catalog-options.js";
4
+ export * from "./trips-panels/committed-component-card.js";
5
+ export * from "./trips-panels/display.js";
6
+ export * from "./trips-panels/flight-configurator.js";
7
+ export * from "./trips-panels/manual-configurators.js";
8
+ export * from "./trips-panels/pending-component-card.js";
9
+ export * from "./trips-panels/shared.js";
10
+ export * from "./trips-panels/travelers-section.js";
11
+ export * from "./trips-panels/trip-preview-rail.js";
@@ -0,0 +1,63 @@
1
+ import { type AdminExtension, type NavItem } from "@voyant-travel/admin";
2
+ /**
3
+ * Semantic destinations the trips admin surfaces navigate to
4
+ * (packaged-admin RFC §4.7). The trips list opens the trip detail page, the
5
+ * detail page links back to the list and across to the booking/person
6
+ * pages — instead of importing a host route tree they resolve these keys
7
+ * through `useAdminHref`/`useAdminNavigate` from `@voyant-travel/admin`. Hosts
8
+ * register one resolver per key (`satisfies AdminDestinationResolvers`).
9
+ * Keys shared with other domains (`booking.detail`, `person.detail`) come
10
+ * from the bookings-react augmentation bound above.
11
+ */
12
+ declare module "@voyant-travel/admin" {
13
+ interface AdminDestinations {
14
+ /** The trips list page. */
15
+ "trip.list": Record<string, never>;
16
+ /** A trip's detail page; the `"new"` pseudo-id opens the composer. */
17
+ "trip.detail": {
18
+ tripId: string;
19
+ };
20
+ }
21
+ }
22
+ export type { AdminTripsPageProps } from "./admin-trips-page.js";
23
+ export type { TripListFiltersPopoverProps, TripStatusFilter } from "./trip-list-filters.js";
24
+ export interface CreateTripsAdminExtensionOptions {
25
+ /** Mount path of the trips pages inside the admin workspace. Default `/trips`. */
26
+ basePath?: string;
27
+ /** Localized nav/page labels. Defaults are the English operator nav labels. */
28
+ labels?: {
29
+ trips?: string;
30
+ allTrips?: string;
31
+ newTrip?: string;
32
+ };
33
+ /** Nav icon — icon choice stays with the host (e.g. lucide `Route`). */
34
+ icon?: NavItem["icon"];
35
+ }
36
+ /**
37
+ * The trips admin contribution (packaged-admin RFC Phase 3,
38
+ * `@voyant-travel/<domain>-react/admin` convention).
39
+ *
40
+ * NAVIGATION: package-delivered. Trips is NOT part of the BASE operator
41
+ * navigation (`createOperatorAdminNavigation` in `@voyant-travel/admin`), so the
42
+ * extension contributes the Trips group itself — spliced in right after
43
+ * Bookings (both belong to the booking lifecycle) via `insertAfter`, with
44
+ * All trips / New trip sub-items. The icon stays a host choice.
45
+ *
46
+ * ROUTES: contributions carry the FULL route implementation (packaged-admin
47
+ * RFC §4.2/§4.8) — lazy `page` module loaders, data loaders fed by the
48
+ * host-supplied {@link AdminRouteLoaderContext} (QueryClient + runtime +
49
+ * params), per-route SSR mode. Hosts bind them into their code-assembled
50
+ * admin route tree; no per-route host files needed. The pages stay
51
+ * code-split because each contribution's `page` dynamically imports the
52
+ * specific host/page module — never this barrel — and the trips
53
+ * itself mounts through a nested `React.lazy`, so its heavy panel stack
54
+ * (flights search, catalog browse, booking journey) only fetches when an
55
+ * operator actually composes/edits a trip. The list keeps its filter/sort/
56
+ * paging state local (no URL search contracts), and every cross-route link
57
+ * resolves through the semantic destinations declared above — no app RPC
58
+ * client, no host route tree.
59
+ *
60
+ * WIDGETS: none contributed and no slots exposed yet.
61
+ */
62
+ export declare function createTripsAdminExtension(options?: CreateTripsAdminExtensionOptions): AdminExtension;
63
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/admin/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,cAAc,EAKnB,KAAK,OAAO,EACb,MAAM,sBAAsB,CAAA;AAc7B;;;;;;;;;GASG;AACH,OAAO,QAAQ,sBAAsB,CAAC;IACpC,UAAU,iBAAiB;QACzB,2BAA2B;QAC3B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;QAClC,sEAAsE;QACtE,aAAa,EAAE;YAAE,MAAM,EAAE,MAAM,CAAA;SAAE,CAAA;KAClC;CACF;AAWD,YAAY,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAA;AAChE,YAAY,EAAE,2BAA2B,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAE3F,MAAM,WAAW,gCAAgC;IAC/C,kFAAkF;IAClF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,+EAA+E;IAC/E,MAAM,CAAC,EAAE;QACP,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,OAAO,CAAC,EAAE,MAAM,CAAA;KACjB,CAAA;IACD,wEAAwE;IACxE,IAAI,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,CAAA;CACvB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,GAAE,gCAAqC,GAC7C,cAAc,CAiFhB"}
@@ -0,0 +1,119 @@
1
+ import { adminRoutePageModule, defineAdminExtension, } from "@voyant-travel/admin";
2
+ // Lean static only: the client module (fetcher). Query options resolve via
3
+ // dynamic import inside the loaders so the trips data layer
4
+ // (client + response schemas) stays out of the workspace-chrome chunk that
5
+ // evaluates this factory.
6
+ import { defaultFetcher } from "../client.js";
7
+ /**
8
+ * The trips admin contribution (packaged-admin RFC Phase 3,
9
+ * `@voyant-travel/<domain>-react/admin` convention).
10
+ *
11
+ * NAVIGATION: package-delivered. Trips is NOT part of the BASE operator
12
+ * navigation (`createOperatorAdminNavigation` in `@voyant-travel/admin`), so the
13
+ * extension contributes the Trips group itself — spliced in right after
14
+ * Bookings (both belong to the booking lifecycle) via `insertAfter`, with
15
+ * All trips / New trip sub-items. The icon stays a host choice.
16
+ *
17
+ * ROUTES: contributions carry the FULL route implementation (packaged-admin
18
+ * RFC §4.2/§4.8) — lazy `page` module loaders, data loaders fed by the
19
+ * host-supplied {@link AdminRouteLoaderContext} (QueryClient + runtime +
20
+ * params), per-route SSR mode. Hosts bind them into their code-assembled
21
+ * admin route tree; no per-route host files needed. The pages stay
22
+ * code-split because each contribution's `page` dynamically imports the
23
+ * specific host/page module — never this barrel — and the trips
24
+ * itself mounts through a nested `React.lazy`, so its heavy panel stack
25
+ * (flights search, catalog browse, booking journey) only fetches when an
26
+ * operator actually composes/edits a trip. The list keeps its filter/sort/
27
+ * paging state local (no URL search contracts), and every cross-route link
28
+ * resolves through the semantic destinations declared above — no app RPC
29
+ * client, no host route tree.
30
+ *
31
+ * WIDGETS: none contributed and no slots exposed yet.
32
+ */
33
+ export function createTripsAdminExtension(options = {}) {
34
+ const { basePath = "/trips", labels = {}, icon } = options;
35
+ const { trips = "Trips", allTrips = "All trips", newTrip = "New trip" } = labels;
36
+ return defineAdminExtension({
37
+ id: "trips",
38
+ navigation: [
39
+ {
40
+ // Splice Trips in right after Bookings — both belong to the booking
41
+ // lifecycle. `insertAfter` keeps the contribution shape; the resolver
42
+ // splices in place rather than appending at the end.
43
+ insertAfter: "bookings",
44
+ items: [
45
+ {
46
+ id: "trips",
47
+ title: trips,
48
+ url: basePath,
49
+ icon,
50
+ items: [
51
+ {
52
+ id: "trips-list",
53
+ title: allTrips,
54
+ url: basePath,
55
+ },
56
+ {
57
+ id: "trips-new",
58
+ title: newTrip,
59
+ url: `${basePath}/new`,
60
+ },
61
+ ],
62
+ },
63
+ ],
64
+ },
65
+ ],
66
+ routes: [
67
+ {
68
+ id: "trips-index",
69
+ path: basePath,
70
+ title: trips,
71
+ // Route-backed destination (RFC §4.7 endgame): the key resolves by
72
+ // pure path interpolation of this route, so the host's resolver is
73
+ // generated (`voyant admin generate --destinations`).
74
+ destination: "trip.list",
75
+ ssr: "data-only",
76
+ page: () => import("./trips-host.js").then((module) => adminRoutePageModule(module.TripsHost)),
77
+ // Dynamic import on purpose: the query options pull the
78
+ // trips data layer (client + response schemas), and a
79
+ // static import here would pin it into the workspace-chrome chunk
80
+ // that evaluates this factory. The host module carries the matching
81
+ // initial params so the seeded cache entry lines up with the page's
82
+ // first query.
83
+ loader: async ({ queryClient, runtime }) => {
84
+ const [{ listTripsQueryOptions }, { initialTripsListParams }] = await Promise.all([
85
+ import("../query-options.js"),
86
+ import("./trips-host.js"),
87
+ ]);
88
+ return queryClient.ensureQueryData(listTripsQueryOptions(loaderClient(runtime), initialTripsListParams));
89
+ },
90
+ },
91
+ {
92
+ id: "trips-detail",
93
+ path: `${basePath}/$id`,
94
+ title: trips,
95
+ destination: "trip.detail",
96
+ destinationParams: { id: "tripId" },
97
+ ssr: "data-only",
98
+ page: () => import("./pages/trip-detail-page.js"),
99
+ // The `"new"` pseudo-id mounts the composer with no trip to fetch.
100
+ // Dynamic import on purpose — see the trips index loader above.
101
+ loader: async ({ queryClient, runtime, params }) => {
102
+ const id = params.id;
103
+ if (!id || id === "new")
104
+ return;
105
+ const { getTripQueryOptions } = await import("../query-options.js");
106
+ return queryClient.ensureQueryData(getTripQueryOptions(loaderClient(runtime), id));
107
+ },
108
+ },
109
+ ],
110
+ });
111
+ }
112
+ /**
113
+ * Bridge the host-supplied {@link AdminRouteRuntime} (optional fetcher) to
114
+ * the required-fetcher client contract the trips query options
115
+ * take — SSR loaders run with the host runtime's cookie-forwarding fetcher.
116
+ */
117
+ function loaderClient(runtime) {
118
+ return { baseUrl: runtime.baseUrl, fetcher: runtime.fetcher ?? defaultFetcher };
119
+ }
@@ -0,0 +1,10 @@
1
+ import type { AdminRoutePageProps } from "@voyant-travel/admin";
2
+ /**
3
+ * Param-taking page for the `trips-detail` contribution: reads the
4
+ * trip envelope id off {@link AdminRoutePageProps} and binds it onto the
5
+ * packaged host. Resolved lazily through the contribution's `page` loader so
6
+ * the detail page (and the composer behind its Edit mode) lands in its own
7
+ * chunk.
8
+ */
9
+ export default function TripDetailPage({ params }: AdminRoutePageProps): import("react/jsx-runtime").JSX.Element;
10
+ //# sourceMappingURL=trip-detail-page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trip-detail-page.d.ts","sourceRoot":"","sources":["../../../src/admin/pages/trip-detail-page.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAI/D;;;;;;GAMG;AACH,MAAM,CAAC,OAAO,UAAU,cAAc,CAAC,EAAE,MAAM,EAAE,EAAE,mBAAmB,2CAErE"}
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { TripDetailHost } from "../trip-detail-host.js";
3
+ /**
4
+ * Param-taking page for the `trips-detail` contribution: reads the
5
+ * trip envelope id off {@link AdminRoutePageProps} and binds it onto the
6
+ * packaged host. Resolved lazily through the contribution's `page` loader so
7
+ * the detail page (and the composer behind its Edit mode) lands in its own
8
+ * chunk.
9
+ */
10
+ export default function TripDetailPage({ params }) {
11
+ return _jsx(TripDetailHost, { id: params.id ?? "" });
12
+ }
@@ -0,0 +1,10 @@
1
+ import type { TripComponent } from "@voyant-travel/trips";
2
+ export declare function readComponentSchedule(component: TripComponent): {
3
+ start: string | null;
4
+ end: string | null;
5
+ };
6
+ export declare function formatScheduleLabel(component: TripComponent): string | null;
7
+ export declare function sortComponentsBySchedule(components: TripComponent[]): TripComponent[];
8
+ export declare function componentTitleFor(component: TripComponent, resolvedEntityName?: string | null): string;
9
+ export declare function componentReferenceLabelFor(component: TripComponent): string;
10
+ //# sourceMappingURL=trip-component-display.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trip-component-display.d.ts","sourceRoot":"","sources":["../../src/admin/trip-component-display.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAEzD,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,aAAa,GAAG;IAC/D,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CACnB,CA8BA;AAED,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,aAAa,GAAG,MAAM,GAAG,IAAI,CAM3E;AAED,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,aAAa,EAAE,GAAG,aAAa,EAAE,CAcrF;AAED,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,aAAa,EACxB,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,GACjC,MAAM,CAgER;AAED,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,aAAa,GAAG,MAAM,CAW3E"}