@voyantjs/bookings-ui 0.107.0 → 0.108.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/dist/components/option-units-stepper-section.d.ts +9 -1
- package/dist/components/option-units-stepper-section.d.ts.map +1 -1
- package/dist/components/option-units-stepper-section.js +10 -2
- package/dist/components/person-picker-section.d.ts +7 -1
- package/dist/components/person-picker-section.d.ts.map +1 -1
- package/dist/components/person-picker-section.js +2 -2
- package/dist/i18n/en.d.ts +37 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +40 -4
- package/dist/i18n/messages.d.ts +37 -1
- package/dist/i18n/messages.d.ts.map +1 -1
- package/dist/i18n/provider.d.ts +74 -2
- package/dist/i18n/provider.d.ts.map +1 -1
- package/dist/i18n/ro.d.ts +37 -1
- package/dist/i18n/ro.d.ts.map +1 -1
- package/dist/i18n/ro.js +39 -3
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/journey/components/booking-journey.d.ts.map +1 -1
- package/dist/journey/components/booking-journey.js +270 -27
- package/dist/journey/components/journey-steps/accommodation-step.d.ts +3 -0
- package/dist/journey/components/journey-steps/accommodation-step.d.ts.map +1 -0
- package/dist/journey/components/journey-steps/accommodation-step.js +71 -0
- package/dist/journey/components/journey-steps/addons-step.d.ts +3 -0
- package/dist/journey/components/journey-steps/addons-step.d.ts.map +1 -0
- package/dist/journey/components/journey-steps/addons-step.js +40 -0
- package/dist/journey/components/journey-steps/billing-step.d.ts +8 -0
- package/dist/journey/components/journey-steps/billing-step.d.ts.map +1 -0
- package/dist/journey/components/journey-steps/billing-step.js +78 -0
- package/dist/journey/components/journey-steps/configure-steps.d.ts +28 -0
- package/dist/journey/components/journey-steps/configure-steps.d.ts.map +1 -0
- package/dist/journey/components/journey-steps/configure-steps.js +231 -0
- package/dist/journey/components/journey-steps/documents-step.d.ts +11 -0
- package/dist/journey/components/journey-steps/documents-step.d.ts.map +1 -0
- package/dist/journey/components/journey-steps/documents-step.js +36 -0
- package/dist/journey/components/journey-steps/payment-step.d.ts +29 -0
- package/dist/journey/components/journey-steps/payment-step.d.ts.map +1 -0
- package/dist/journey/components/journey-steps/payment-step.js +224 -0
- package/dist/journey/components/journey-steps/review-step.d.ts +27 -0
- package/dist/journey/components/journey-steps/review-step.d.ts.map +1 -0
- package/dist/journey/components/journey-steps/review-step.js +18 -0
- package/dist/journey/components/journey-steps/shared.d.ts +75 -0
- package/dist/journey/components/journey-steps/shared.d.ts.map +1 -0
- package/dist/journey/components/journey-steps/shared.js +108 -0
- package/dist/journey/components/journey-steps/travelers-step.d.ts +7 -0
- package/dist/journey/components/journey-steps/travelers-step.d.ts.map +1 -0
- package/dist/journey/components/journey-steps/travelers-step.js +201 -0
- package/dist/journey/components/journey-steps.d.ts +13 -39
- package/dist/journey/components/journey-steps.d.ts.map +1 -1
- package/dist/journey/components/journey-steps.js +16 -613
- package/dist/journey/components/side-panel.d.ts +7 -2
- package/dist/journey/components/side-panel.d.ts.map +1 -1
- package/dist/journey/components/side-panel.js +73 -24
- package/dist/journey/index.d.ts +2 -2
- package/dist/journey/index.d.ts.map +1 -1
- package/dist/journey/index.js +1 -1
- package/dist/journey/lib/pax-band-dependencies.d.ts +27 -0
- package/dist/journey/lib/pax-band-dependencies.d.ts.map +1 -0
- package/dist/journey/lib/pax-band-dependencies.js +50 -0
- package/dist/journey/lib/payment-schedule.d.ts +19 -0
- package/dist/journey/lib/payment-schedule.d.ts.map +1 -0
- package/dist/journey/lib/payment-schedule.js +90 -0
- package/dist/journey/types.d.ts +141 -8
- package/dist/journey/types.d.ts.map +1 -1
- package/dist/journey/types.js +3 -1
- package/package.json +32 -32
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
/**
|
|
4
4
|
* `<BookingJourney />` — the unified booking journey shell.
|
|
5
5
|
*
|
|
@@ -18,17 +18,26 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
18
18
|
import { DEFAULT_PAX_BANDS, defaultBookingFields, defaultDraftShapeFlags, defaultTravelerFields, paxBandsAllowedTotalFrom, } from "@voyantjs/catalog/booking-engine";
|
|
19
19
|
import { useBookingCommit, useBookingDraft, useBookingDraftShape, useBookingHold, useBookingQuote, } from "@voyantjs/catalog-react/booking-engine";
|
|
20
20
|
import { Button } from "@voyantjs/ui/components/button";
|
|
21
|
+
import { Card, CardContent, CardHeader } from "@voyantjs/ui/components/card";
|
|
22
|
+
import { Skeleton } from "@voyantjs/ui/components/skeleton";
|
|
23
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "@voyantjs/ui/components/tooltip";
|
|
24
|
+
import { Loader2, Lock } from "lucide-react";
|
|
21
25
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
22
26
|
import { formatMessage, useBookingsUiMessagesOrDefault } from "../../i18n/index.js";
|
|
23
27
|
import { emptyDraft, totalPax } from "../lib/draft-state.js";
|
|
28
|
+
import { evaluatePaxBandDependencies } from "../lib/pax-band-dependencies.js";
|
|
24
29
|
import { JOURNEY_STEP_ORDER, } from "../types.js";
|
|
25
30
|
import { ContractPreviewDialog } from "./contract-preview-dialog.js";
|
|
26
|
-
import { AccommodationStep, AddonsStep, BillingStep,
|
|
31
|
+
import { AccommodationStep, AddonsStep, BillingStep, DepartureStep, DocumentsStep, FinalizeControls, OptionsStep, PaymentStep, ReviewStep, TravelersStep, } from "./journey-steps.js";
|
|
27
32
|
import { PriceSidePanel } from "./side-panel.js";
|
|
28
33
|
import { StepHeader } from "./step-header.js";
|
|
29
34
|
export function BookingJourney(props) {
|
|
30
35
|
const messages = useBookingsUiMessagesOrDefault();
|
|
31
36
|
const surface = props.surface ?? "admin";
|
|
37
|
+
// Admin books on a single stacked page (nothing hidden); the storefront
|
|
38
|
+
// keeps the guided one-step-at-a-time wizard. Two deliberately separate
|
|
39
|
+
// flows — see BookingJourneyProps.layout.
|
|
40
|
+
const layout = props.layout ?? (surface === "admin" ? "stacked" : "wizard");
|
|
32
41
|
const [draft, setDraft] = useState(() => {
|
|
33
42
|
const base = emptyDraft({
|
|
34
43
|
module: props.entityModule,
|
|
@@ -77,33 +86,43 @@ export function BookingJourney(props) {
|
|
|
77
86
|
// Step navigation — only show steps the descriptor says are relevant.
|
|
78
87
|
const hideConfigure = props.hideConfigure === true;
|
|
79
88
|
const steps = useMemo(() => JOURNEY_STEP_ORDER.filter((s) => {
|
|
80
|
-
|
|
89
|
+
// `hideConfigure` skips the configure phase — now split across the
|
|
90
|
+
// Departure + Options steps.
|
|
91
|
+
if (hideConfigure && (s === "departure" || s === "options"))
|
|
92
|
+
return false;
|
|
93
|
+
// Internal notes + document generation are operator-only.
|
|
94
|
+
if (s === "documents" && surface !== "admin")
|
|
81
95
|
return false;
|
|
82
96
|
return isStepVisible(s, shape);
|
|
83
|
-
}), [shape, hideConfigure]);
|
|
84
|
-
|
|
85
|
-
|
|
97
|
+
}), [shape, hideConfigure, surface]);
|
|
98
|
+
// The stacked admin layout drops the Review block — the side panel shows the
|
|
99
|
+
// live summary + Confirm at all times, so a separate review section is
|
|
100
|
+
// redundant.
|
|
101
|
+
const stackedSteps = useMemo(() => steps.filter((s) => s !== "review"), [steps]);
|
|
102
|
+
const [currentStep, setCurrentStep] = useState(() => steps[0] ?? "departure");
|
|
103
|
+
const [visited, setVisited] = useState(() => new Set([steps[0] ?? "departure"]));
|
|
86
104
|
// If the descriptor changes and removes the current step, reset to
|
|
87
105
|
// the first available step. (Edge case: shape goes from
|
|
88
106
|
// owned→sourced and the relevant step set narrows.)
|
|
89
107
|
useEffect(() => {
|
|
90
108
|
if (!steps.includes(currentStep)) {
|
|
91
|
-
setCurrentStep(steps[0] ?? "
|
|
109
|
+
setCurrentStep(steps[0] ?? "departure");
|
|
92
110
|
}
|
|
93
111
|
}, [steps, currentStep]);
|
|
94
|
-
// PUT the draft to the server
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
// the
|
|
99
|
-
|
|
112
|
+
// PUT the draft to the server to keep the recovery surface fresh
|
|
113
|
+
// without saving on every keystroke. Wizard saves on each step
|
|
114
|
+
// transition; the stacked admin page has no steps, so it saves on
|
|
115
|
+
// each settled quote (a natural, debounced cadence). The mutation
|
|
116
|
+
// reads the latest draft + quote from the closure on each fire.
|
|
117
|
+
const saveTrigger = layout === "wizard" ? currentStep : (quote.data?.quoteId ?? "");
|
|
118
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: intentional — fires on save trigger only
|
|
100
119
|
useEffect(() => {
|
|
101
120
|
draftSync.save.mutate({
|
|
102
121
|
draft: { ...draft, quoteId: quote.data?.quoteId },
|
|
103
122
|
currentStep,
|
|
104
123
|
currentQuoteId: quote.data?.quoteId,
|
|
105
124
|
});
|
|
106
|
-
}, [
|
|
125
|
+
}, [saveTrigger]);
|
|
107
126
|
// Commit
|
|
108
127
|
const commit = useBookingCommit({
|
|
109
128
|
surface,
|
|
@@ -120,7 +139,12 @@ export function BookingJourney(props) {
|
|
|
120
139
|
const holdSignature = makeHoldSignature(draft, props.entityModule, props.entityId);
|
|
121
140
|
// biome-ignore lint/correctness/useExhaustiveDependencies: signature change is the only trigger; refs + closure read latest values
|
|
122
141
|
useEffect(() => {
|
|
123
|
-
|
|
142
|
+
// Wizard: don't hold while still on the configure steps. Stacked:
|
|
143
|
+
// everything's on one page, so the signature (slot + pax present)
|
|
144
|
+
// is the trigger.
|
|
145
|
+
if (layout === "wizard" && (currentStep === "departure" || currentStep === "options"))
|
|
146
|
+
return;
|
|
147
|
+
if (!holdSignature)
|
|
124
148
|
return;
|
|
125
149
|
if (holdState.current.signature === holdSignature)
|
|
126
150
|
return;
|
|
@@ -154,8 +178,17 @@ export function BookingJourney(props) {
|
|
|
154
178
|
.catch(() => { });
|
|
155
179
|
}
|
|
156
180
|
}, [holdSignature, currentStep]);
|
|
157
|
-
const
|
|
181
|
+
const available = quote.data?.available !== false;
|
|
182
|
+
const canAdvance = canAdvanceFromStep(currentStep, draft, shape, available);
|
|
158
183
|
const warnings = warningsForStep(currentStep, draft, shape, messages);
|
|
184
|
+
// Stacked layout: there's no "current" step, but the section nav still
|
|
185
|
+
// nudges toward the first thing that isn't done yet, and the final
|
|
186
|
+
// Confirm is gated until every section passes its check.
|
|
187
|
+
const firstIncomplete = useMemo(() => stackedSteps.find((s) => !stackedStepComplete(s, draft, shape, available)) ??
|
|
188
|
+
stackedSteps[stackedSteps.length - 1] ??
|
|
189
|
+
stackedSteps[0] ??
|
|
190
|
+
"departure", [stackedSteps, draft, shape, available]);
|
|
191
|
+
const canCommit = useMemo(() => stackedSteps.every((s) => canAdvanceFromStep(s, draft, shape, available)), [stackedSteps, draft, shape, available]);
|
|
159
192
|
const [isAdvanceGuardPending, setIsAdvanceGuardPending] = useState(false);
|
|
160
193
|
const [advanceGuardError, setAdvanceGuardError] = useState(null);
|
|
161
194
|
const idx = steps.indexOf(currentStep);
|
|
@@ -235,6 +268,10 @@ export function BookingJourney(props) {
|
|
|
235
268
|
await commit.mutateAsync({
|
|
236
269
|
draft: { ...draft, quoteId: quote.data.quoteId },
|
|
237
270
|
quoteId: quote.data.quoteId,
|
|
271
|
+
// The owned commit reads the buyer + travelers off `party`, not the
|
|
272
|
+
// draft — without this the create rejects with "no billing person/org".
|
|
273
|
+
party: buildCommitParty(draft),
|
|
274
|
+
initialStatus: resolveInitialStatus(draft),
|
|
238
275
|
paymentIntent: { type: draft.payment.intent === "card" ? "card" : "hold" },
|
|
239
276
|
});
|
|
240
277
|
};
|
|
@@ -278,11 +315,66 @@ export function BookingJourney(props) {
|
|
|
278
315
|
setContractDialogOpen(false);
|
|
279
316
|
await handleAccepted(acceptance);
|
|
280
317
|
};
|
|
281
|
-
|
|
318
|
+
// Bind the picked lead + departure into the billing-extras slot so the
|
|
319
|
+
// template can run lead-aware checks (e.g. duplicate-departure warning).
|
|
320
|
+
const billingExtrasSlot = props.renderBillingExtras
|
|
321
|
+
? () => props.renderBillingExtras?.({
|
|
322
|
+
buyerType: draft.billing.buyerType,
|
|
323
|
+
personId: draft.billing.contact.personId,
|
|
324
|
+
organizationId: draft.billing.organizationId,
|
|
325
|
+
productId: props.entityId,
|
|
326
|
+
departureSlotId: draft.configure.departureSlotId,
|
|
327
|
+
departureDate: draft.configure.departureDate,
|
|
328
|
+
})
|
|
329
|
+
: undefined;
|
|
330
|
+
// Renders one step's content. Shared by both layouts — the wizard shows
|
|
331
|
+
// exactly one at a time; the stacked page renders them all in sections.
|
|
332
|
+
const renderStep = (step) => {
|
|
333
|
+
switch (step) {
|
|
334
|
+
case "departure":
|
|
335
|
+
// First load: the descriptor arrives with the first quote. Show a
|
|
336
|
+
// skeleton rather than the generic fallback, which would flash and
|
|
337
|
+
// then shift into the real layout.
|
|
338
|
+
return !quote.data && quote.isQuoting ? (_jsx(ConfigureStepSkeleton, {})) : (_jsx(DepartureStep, { draft: draft, setDraft: setDraft, shape: shape, productId: props.entityId, renderDeparturePicker: props.renderDeparturePicker }));
|
|
339
|
+
case "options":
|
|
340
|
+
return (_jsx(OptionsStep, { draft: draft, setDraft: setDraft, shape: shape, productId: props.entityId, renderUnitsPicker: props.renderUnitsPicker }));
|
|
341
|
+
case "billing":
|
|
342
|
+
return (_jsx(BillingStep, { draft: draft, setDraft: setDraft, shape: shape, renderLeadContactPicker: props.renderLeadContactPicker, renderExtras: billingExtrasSlot, warnings: warningsForStep("billing", draft, shape, messages) }));
|
|
343
|
+
case "travelers":
|
|
344
|
+
return (_jsx(TravelersStep, { draft: draft, setDraft: setDraft, shape: shape, renderTravelerContactPicker: props.renderTravelerContactPicker, warnings: warningsForStep("travelers", draft, shape, messages) }));
|
|
345
|
+
case "accommodation":
|
|
346
|
+
return _jsx(AccommodationStep, { draft: draft, setDraft: setDraft, shape: shape });
|
|
347
|
+
case "addons":
|
|
348
|
+
return _jsx(AddonsStep, { draft: draft, setDraft: setDraft, shape: shape });
|
|
349
|
+
case "payment":
|
|
350
|
+
return (_jsx(PaymentStep, { draft: draft, setDraft: setDraft, shape: shape, capabilities: props.paymentCapabilities ?? {
|
|
351
|
+
acceptsCard: false,
|
|
352
|
+
acceptsHold: true,
|
|
353
|
+
acceptsTicketOnCredit: false,
|
|
354
|
+
}, renderProviderStep: props.renderPaymentProviderStep, surface: surface, pricing: quote.data?.pricing ?? null }));
|
|
355
|
+
case "documents":
|
|
356
|
+
return _jsx(DocumentsStep, { draft: draft, setDraft: setDraft });
|
|
357
|
+
case "review":
|
|
358
|
+
return (_jsx(ReviewStep, { draft: draft, setDraft: setDraft, isCommitting: commit.isPending || isHandlingCheckout, onConfirm: onConfirm,
|
|
359
|
+
// Stacked has no per-step gates, so the Confirm button enforces
|
|
360
|
+
// the whole-booking validity itself.
|
|
361
|
+
canConfirm: layout === "stacked" ? canCommit : undefined, renderExtras: props.renderReviewExtras, surface: surface, pricing: quote.data?.pricing ?? null, warnings: warningsForStep("review", draft, shape, messages) }));
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
if (layout === "stacked") {
|
|
365
|
+
return (_jsx(StackedJourney, { className: props.className, steps: stackedSteps, renderStep: renderStep, isStepComplete: (s) => stackedStepComplete(s, draft, shape, available), commitError: commit.error, onCancel: props.onCancelled, onConfirm: onConfirm, isCommitting: commit.isPending || isHandlingCheckout, canConfirm: canCommit, sidePanel: _jsx(PriceSidePanel, { pricing: quote.data?.pricing ?? null, isQuoting: quote.isQuoting, invalidReason: quote.data?.invalidReason, entitySummary: props.entitySummary, currentStep: firstIncomplete, steps: stackedSteps, shape: shape, draft: draft, className: props.sidePanelClassName,
|
|
366
|
+
// Price override + voucher live with the pricing, not in Payment.
|
|
367
|
+
pricingExtras: _jsx(FinalizeControls, { draft: draft, setDraft: setDraft, pricing: quote.data?.pricing ?? null, renderVoucherPicker: props.renderVoucherPicker }) }), contractDialog: contractConfig ? (_jsx(ContractPreviewDialog, { open: contractDialogOpen, onOpenChange: setContractDialogOpen, previewUrl: contractConfig.previewUrl, acceptLanguage: contractConfig.acceptLanguage, variables: contractVariables, marketingLabel: contractConfig.marketingLabel, termsLabel: contractConfig.termsLabel, onAccept: onContractAccept })) : null }));
|
|
368
|
+
}
|
|
369
|
+
return (_jsxs("div", { className: props.className, children: [_jsxs("div", { className: "grid grid-cols-1 gap-6 md:grid-cols-8 md:items-start", children: [_jsxs("div", { className: "space-y-6 md:col-span-5", children: [_jsx(StepHeader, { current: currentStep, visited: [...visited], steps: steps, shape: shape, onJumpTo: jumpTo }), currentStep === "departure" ? (
|
|
370
|
+
// First load: the descriptor arrives with the first quote. Show a
|
|
371
|
+
// skeleton rather than the generic fallback, which would flash
|
|
372
|
+
// and then shift into the real layout.
|
|
373
|
+
!quote.data && quote.isQuoting ? (_jsx(ConfigureStepSkeleton, {})) : (_jsx(DepartureStep, { draft: draft, setDraft: setDraft, shape: shape, productId: props.entityId, renderDeparturePicker: props.renderDeparturePicker }))) : null, currentStep === "options" ? (_jsx(OptionsStep, { draft: draft, setDraft: setDraft, shape: shape, productId: props.entityId, renderUnitsPicker: props.renderUnitsPicker })) : null, currentStep === "billing" ? (_jsx(BillingStep, { draft: draft, setDraft: setDraft, shape: shape, renderLeadContactPicker: props.renderLeadContactPicker, renderExtras: billingExtrasSlot })) : null, currentStep === "travelers" ? (_jsx(TravelersStep, { draft: draft, setDraft: setDraft, shape: shape, renderTravelerContactPicker: props.renderTravelerContactPicker })) : null, currentStep === "accommodation" ? (_jsx(AccommodationStep, { draft: draft, setDraft: setDraft, shape: shape })) : null, currentStep === "addons" ? (_jsx(AddonsStep, { draft: draft, setDraft: setDraft, shape: shape })) : null, currentStep === "payment" ? (_jsx(PaymentStep, { draft: draft, setDraft: setDraft, shape: shape, capabilities: props.paymentCapabilities ?? {
|
|
282
374
|
acceptsCard: false,
|
|
283
375
|
acceptsHold: true,
|
|
284
376
|
acceptsTicketOnCredit: false,
|
|
285
|
-
}, renderProviderStep: props.renderPaymentProviderStep })) : null, currentStep === "review" ? (_jsx(ReviewStep, { draft: draft, setDraft: setDraft, isCommitting: commit.isPending || isHandlingCheckout, onConfirm: onConfirm, renderExtras: props.renderReviewExtras, surface: surface })) : null, warnings.length > 0 ? (_jsx("ul", { className: "space-y-1 rounded 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: warnings.map((w) => (_jsxs("li", { children: ["\u26A0 ", w] }, w))) })) : null, _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { type: "button", variant: "outline", disabled: isAdvanceGuardPending, onClick: () => {
|
|
377
|
+
}, renderProviderStep: props.renderPaymentProviderStep, surface: surface, pricing: quote.data?.pricing ?? null })) : null, currentStep === "review" ? (_jsx(ReviewStep, { draft: draft, setDraft: setDraft, isCommitting: commit.isPending || isHandlingCheckout, onConfirm: onConfirm, renderExtras: props.renderReviewExtras, surface: surface, pricing: quote.data?.pricing ?? null })) : null, warnings.length > 0 ? (_jsx("ul", { 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: warnings.map((w) => (_jsxs("li", { children: ["\u26A0 ", w] }, w))) })) : null, _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { type: "button", variant: "outline", disabled: isAdvanceGuardPending, onClick: () => {
|
|
286
378
|
if (prev)
|
|
287
379
|
goBack();
|
|
288
380
|
else
|
|
@@ -291,10 +383,101 @@ export function BookingJourney(props) {
|
|
|
291
383
|
? messages.bookingJourney.navigation.checking
|
|
292
384
|
: messages.bookingJourney.navigation.next })) : null] }), advanceGuardError ? (_jsx("p", { className: "text-destructive text-sm", role: "alert", "aria-live": "polite", children: advanceGuardError })) : null, commit.error ? (_jsx("p", { className: "text-destructive text-sm", children: commit.error instanceof Error ? commit.error.message : String(commit.error) })) : null] }), _jsx("aside", { className: "md:sticky md:top-4 md:col-span-3", children: _jsx(PriceSidePanel, { pricing: quote.data?.pricing ?? null, isQuoting: quote.isQuoting, invalidReason: quote.data?.invalidReason, entitySummary: props.entitySummary, currentStep: currentStep, steps: steps, shape: shape, draft: draft, className: props.sidePanelClassName }) })] }), contractConfig ? (_jsx(ContractPreviewDialog, { open: contractDialogOpen, onOpenChange: setContractDialogOpen, previewUrl: contractConfig.previewUrl, acceptLanguage: contractConfig.acceptLanguage, variables: contractVariables, marketingLabel: contractConfig.marketingLabel, termsLabel: contractConfig.termsLabel, onAccept: onContractAccept })) : null] }));
|
|
293
385
|
}
|
|
386
|
+
/**
|
|
387
|
+
* The buyer + travelers the owned commit reads off `request.party` (the draft
|
|
388
|
+
* carries them but `extractBillingParty` only inspects `party`). B2C supplies
|
|
389
|
+
* `personId`; B2B supplies `organizationId`; traveler person links thread
|
|
390
|
+
* through so the booking attaches to the right CRM records.
|
|
391
|
+
*/
|
|
392
|
+
function buildCommitParty(draft) {
|
|
393
|
+
const c = draft.billing.contact;
|
|
394
|
+
return {
|
|
395
|
+
personId: c.personId,
|
|
396
|
+
organizationId: draft.billing.organizationId,
|
|
397
|
+
billing: {
|
|
398
|
+
personId: c.personId,
|
|
399
|
+
organizationId: draft.billing.organizationId,
|
|
400
|
+
contact: {
|
|
401
|
+
firstName: c.firstName,
|
|
402
|
+
lastName: c.lastName,
|
|
403
|
+
email: c.email,
|
|
404
|
+
phone: c.phone,
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
travelerParty: {
|
|
408
|
+
travelers: draft.travelers.map((t) => ({ personId: t.personId })),
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Initial booking status from the operator's choices: an explicit "save as
|
|
414
|
+
* draft", else live — confirmed when the payment is fully marked paid,
|
|
415
|
+
* otherwise awaiting payment.
|
|
416
|
+
*/
|
|
417
|
+
function resolveInitialStatus(draft) {
|
|
418
|
+
if (draft.saveAsDraft)
|
|
419
|
+
return "draft";
|
|
420
|
+
const schedules = draft.paymentSchedules ?? [];
|
|
421
|
+
const fullyPaid = schedules.length > 0 && schedules.every((s) => s.status === "paid");
|
|
422
|
+
return fullyPaid ? "confirmed" : "awaiting_payment";
|
|
423
|
+
}
|
|
424
|
+
const sectionId = (step) => `bj-section-${step}`;
|
|
425
|
+
/**
|
|
426
|
+
* The sequential gates in the stacked layout: each must be filled before the
|
|
427
|
+
* next sections unlock. Once all three are done, the remaining sections
|
|
428
|
+
* (options, extras, payment) all open together.
|
|
429
|
+
*/
|
|
430
|
+
const GATE_STEPS = new Set(["departure", "billing", "travelers"]);
|
|
431
|
+
/**
|
|
432
|
+
* The admin's guided single-page layout — every section is a block on one
|
|
433
|
+
* page, but content stays collapsed until the previous section is complete,
|
|
434
|
+
* so the operator fills them in sequence and focuses on one at a time.
|
|
435
|
+
*
|
|
436
|
+
* - LOCKED (a prior section isn't done): a muted, disabled summary row.
|
|
437
|
+
* - ACTIVE (the first not-yet-done section): expanded with its full
|
|
438
|
+
* content + a "Continue" button (enabled once the section is valid).
|
|
439
|
+
* - DONE (passed via Continue): collapses to a one-line summary row with a
|
|
440
|
+
* check — click to re-open and edit, so nothing entered is ever lost.
|
|
441
|
+
*
|
|
442
|
+
* Completeness derives from the same per-step gate the wizard uses; the
|
|
443
|
+
* final Review section's Confirm is gated on the whole booking.
|
|
444
|
+
*/
|
|
445
|
+
function StackedJourney({ className, steps, renderStep, isStepComplete, commitError, onCancel, onConfirm, isCommitting, canConfirm, sidePanel, contractDialog, }) {
|
|
446
|
+
const messages = useBookingsUiMessagesOrDefault();
|
|
447
|
+
const nav = messages.bookingJourney.navigation;
|
|
448
|
+
// Progressive unlock gated only on the SEQUENTIAL gates (departure →
|
|
449
|
+
// billing → travelers). Once those are filled, the remaining sections
|
|
450
|
+
// (options, extras, payment) all unlock together — they're independent
|
|
451
|
+
// refinements, not a strict sequence. Unlocked sections stay open so the
|
|
452
|
+
// operator keeps full context of everything they've filled.
|
|
453
|
+
const firstIncompleteGate = steps.find((s) => GATE_STEPS.has(s) && !isStepComplete(s));
|
|
454
|
+
const unlockThroughIndex = firstIncompleteGate
|
|
455
|
+
? steps.indexOf(firstIncompleteGate)
|
|
456
|
+
: steps.length - 1;
|
|
457
|
+
return (_jsxs("div", { className: className, children: [_jsxs("div", { className: "grid grid-cols-1 gap-6 md:grid-cols-8 md:items-start", children: [_jsxs("div", { className: "space-y-3 md:col-span-5", children: [steps.map((step, i) => {
|
|
458
|
+
// Locked: a section beyond the active gate — a muted, disabled row
|
|
459
|
+
// until the operator clears the gates above it.
|
|
460
|
+
if (i > unlockThroughIndex) {
|
|
461
|
+
return (_jsxs("div", { id: sectionId(step), className: "flex w-full scroll-mt-4 items-center gap-3 rounded-md border p-4 opacity-60", children: [_jsx("span", { className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground text-xs", children: i + 1 }), _jsx("div", { className: "min-w-0 flex-1 font-medium text-sm", children: messages.bookingJourney.steps[step] }), _jsx(Lock, { className: "h-4 w-4 text-muted-foreground" })] }, step));
|
|
462
|
+
}
|
|
463
|
+
// Unlocked: full section content, stays open once reached. Its
|
|
464
|
+
// warnings render inside the step's own card (scoped to the block).
|
|
465
|
+
return (_jsx("section", { id: sectionId(step), className: "scroll-mt-4", children: renderStep(step) }, step));
|
|
466
|
+
}), commitError ? (_jsx("p", { className: "text-destructive text-sm", children: commitError instanceof Error ? commitError.message : String(commitError) })) : null, _jsxs("div", { className: "flex items-center gap-2 pt-1", children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => onCancel?.(), children: nav.cancel }), onConfirm ? (canConfirm === false ? (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { render: _jsx("span", { className: "ml-auto inline-flex" }), children: _jsx(Button, { type: "button", disabled: true, children: messages.bookingJourney.review.confirmBooking }) }), _jsx(TooltipContent, { children: messages.bookingJourney.review.completeToConfirm })] })) : (_jsx(Button, { type: "button", className: "ml-auto", onClick: onConfirm, disabled: isCommitting === true, children: isCommitting ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), messages.bookingJourney.review.confirming] })) : (messages.bookingJourney.review.confirmBooking) }))) : null] })] }), _jsx("aside", { className: "md:sticky md:top-4 md:col-span-3", children: sidePanel })] }), contractDialog] }));
|
|
467
|
+
}
|
|
294
468
|
function isStepVisible(step, shape) {
|
|
469
|
+
const subSteps = shape.configureSubSteps ?? [];
|
|
295
470
|
switch (step) {
|
|
296
|
-
case "
|
|
471
|
+
case "departure":
|
|
472
|
+
// The departure step shows whenever the journey has a configure phase
|
|
473
|
+
// (owned products always pick a departure; storefront free-date too).
|
|
297
474
|
return shape.showsConfigure;
|
|
475
|
+
case "options":
|
|
476
|
+
// The options step shows only when there's something to choose —
|
|
477
|
+
// a product option, room/unit selection, or another configure
|
|
478
|
+
// sub-step (cabin, date-range, air). Simple per-person tours skip it.
|
|
479
|
+
return (shape.showsConfigure &&
|
|
480
|
+
subSteps.some((s) => s.kind !== "departure" && s.kind !== "occupancy"));
|
|
298
481
|
case "billing":
|
|
299
482
|
return shape.showsBilling;
|
|
300
483
|
case "travelers":
|
|
@@ -305,34 +488,94 @@ function isStepVisible(step, shape) {
|
|
|
305
488
|
return shape.showsAddons;
|
|
306
489
|
case "payment":
|
|
307
490
|
return shape.showsPayment;
|
|
491
|
+
case "documents":
|
|
492
|
+
// Operator-only block; shown whenever a real booking is being finalized
|
|
493
|
+
// (gated to the admin surface in the step list above).
|
|
494
|
+
return shape.showsReview;
|
|
308
495
|
case "review":
|
|
309
496
|
return shape.showsReview;
|
|
310
497
|
}
|
|
311
498
|
}
|
|
499
|
+
/**
|
|
500
|
+
* First-load placeholder for the Configure step. Mirrors the real layout
|
|
501
|
+
* (departure, travelers, option) closely enough that swapping to the live
|
|
502
|
+
* descriptor causes minimal layout shift.
|
|
503
|
+
*/
|
|
504
|
+
function ConfigureStepSkeleton() {
|
|
505
|
+
return (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(Skeleton, { className: "h-5 w-28" }) }), _jsxs(CardContent, { className: "space-y-6", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Skeleton, { className: "h-4 w-24" }), _jsx(Skeleton, { className: "h-10 w-full" })] }), _jsxs("div", { className: "space-y-3", children: [_jsx(Skeleton, { className: "h-4 w-20" }), _jsxs("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-3", children: [_jsx(Skeleton, { className: "h-16 w-full" }), _jsx(Skeleton, { className: "h-16 w-full" }), _jsx(Skeleton, { className: "h-16 w-full" })] })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Skeleton, { className: "h-4 w-16" }), _jsx(Skeleton, { className: "h-12 w-full" })] })] })] }));
|
|
506
|
+
}
|
|
312
507
|
function canAdvanceFromStep(step, draft, shape, available) {
|
|
313
508
|
if (!available)
|
|
314
509
|
return false;
|
|
315
510
|
switch (step) {
|
|
316
|
-
case "
|
|
317
|
-
|
|
318
|
-
|
|
511
|
+
case "departure": {
|
|
512
|
+
// Require a departure when the descriptor marks it required.
|
|
513
|
+
const requiresDeparture = (shape.configureSubSteps ?? []).some((s) => s.kind === "departure" && s.required);
|
|
514
|
+
if (!requiresDeparture)
|
|
515
|
+
return true;
|
|
516
|
+
return Boolean(draft.configure.departureSlotId || draft.configure.departureDate);
|
|
517
|
+
}
|
|
518
|
+
case "options": {
|
|
519
|
+
// Room products (an `option-units` sub-step) can't be booked — or
|
|
520
|
+
// priced — without at least one room, so block confirm until one is
|
|
521
|
+
// picked. Per-person products have nothing to require here.
|
|
522
|
+
const isRoomProduct = (shape.configureSubSteps ?? []).some((s) => s.kind === "option-units");
|
|
523
|
+
if (!isRoomProduct)
|
|
524
|
+
return true;
|
|
525
|
+
const rooms = (draft.configure.optionSelections ?? []).reduce((sum, s) => sum + (s.quantity ?? 0), 0);
|
|
526
|
+
return rooms > 0;
|
|
319
527
|
}
|
|
320
528
|
case "billing": {
|
|
529
|
+
// B2B: the picked organization is the bill-to. The CRM org picker doesn't
|
|
530
|
+
// collect an individual contact name (and the manual contact inputs are
|
|
531
|
+
// hidden), so requiring one would lock the step with no way to satisfy it.
|
|
532
|
+
if (draft.billing.buyerType === "B2B") {
|
|
533
|
+
return Boolean(draft.billing.organizationId);
|
|
534
|
+
}
|
|
321
535
|
const c = draft.billing.contact;
|
|
322
536
|
return c.firstName.length > 0 && c.lastName.length > 0 && c.email.length > 0;
|
|
323
537
|
}
|
|
324
538
|
case "travelers": {
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
539
|
+
// Pax counts are set on this step now: require the allowed total and
|
|
540
|
+
// that occupancy rules (e.g. "Child under 6 requires an Adult") hold.
|
|
541
|
+
const total = totalPax(draft);
|
|
542
|
+
if (total < shape.paxBandsAllowedTotal.min || total > shape.paxBandsAllowedTotal.max) {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
if (evaluatePaxBandDependencies(draft.configure.pax, shape.paxBandDependencies, shape.paxBands)
|
|
546
|
+
.length > 0) {
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
// Hard-reject only on canonical traveler fields (firstName, lastName);
|
|
550
|
+
// other required fields surface as warnings, fillable later.
|
|
330
551
|
return draft.travelers.every((t) => t.firstName && t.lastName);
|
|
331
552
|
}
|
|
332
553
|
default:
|
|
333
554
|
return true;
|
|
334
555
|
}
|
|
335
556
|
}
|
|
557
|
+
/**
|
|
558
|
+
* Completeness for the stacked admin accordion's AUTO-advance — stricter
|
|
559
|
+
* than `canAdvanceFromStep` so the flow pauses on sections that need a
|
|
560
|
+
* deliberate choice even though they're not hard-required to commit:
|
|
561
|
+
* - options: a product option must be picked (when the product has them);
|
|
562
|
+
* - payment: an intent must be chosen.
|
|
563
|
+
* Everything else defers to the shared gate. Kept separate so the wizard's
|
|
564
|
+
* Next gating (which uses `canAdvanceFromStep`) is unchanged.
|
|
565
|
+
*/
|
|
566
|
+
function stackedStepComplete(step, draft, shape, available) {
|
|
567
|
+
switch (step) {
|
|
568
|
+
case "options": {
|
|
569
|
+
const hasOptions = (shape.configureSubSteps ?? []).some((s) => s.kind === "product-option");
|
|
570
|
+
// No options to choose → nothing to wait for. Otherwise require a pick.
|
|
571
|
+
return hasOptions ? Boolean(draft.configure.variantId) : true;
|
|
572
|
+
}
|
|
573
|
+
case "payment":
|
|
574
|
+
return Boolean(draft.payment.intent);
|
|
575
|
+
default:
|
|
576
|
+
return canAdvanceFromStep(step, draft, shape, available);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
336
579
|
/**
|
|
337
580
|
* Soft warnings for the current step — surfaced inline above the
|
|
338
581
|
* Next button. Don't block advancement; they're hints. Per
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"accommodation-step.d.ts","sourceRoot":"","sources":["../../../../src/journey/components/journey-steps/accommodation-step.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAMlD,wBAAgB,iBAAiB,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,eAAe,GAAG,KAAK,CAAC,YAAY,CAgIjG"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Separator } from "@voyantjs/ui/components";
|
|
4
|
+
import { Button } from "@voyantjs/ui/components/button";
|
|
5
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@voyantjs/ui/components/card";
|
|
6
|
+
import { Label } from "@voyantjs/ui/components/label";
|
|
7
|
+
import { formatMessage, useBookingsUiMessagesOrDefault } from "../../../i18n/index.js";
|
|
8
|
+
import { setAccommodation } from "../../lib/draft-state.js";
|
|
9
|
+
// ─────────────────────────────────────────────────────────────────
|
|
10
|
+
// Accommodation
|
|
11
|
+
// ─────────────────────────────────────────────────────────────────
|
|
12
|
+
export function AccommodationStep({ draft, setDraft, shape }) {
|
|
13
|
+
const messages = useBookingsUiMessagesOrDefault();
|
|
14
|
+
const subSteps = shape.accommodation?.subSteps ?? [];
|
|
15
|
+
const rooms = shape.accommodation?.roomOptions ?? [];
|
|
16
|
+
const accommodation = draft.accommodation ?? {
|
|
17
|
+
rooms: [],
|
|
18
|
+
travelerAssignments: {},
|
|
19
|
+
};
|
|
20
|
+
return (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: messages.bookingJourney.accommodation.title }) }), _jsx(Separator, {}), _jsx(CardContent, { className: "space-y-4", children: rooms.length === 0 && subSteps.length === 0 ? (_jsx("p", { className: "text-muted-foreground text-sm", children: messages.bookingJourney.accommodation.empty })) : (_jsxs("div", { className: "space-y-3", children: [rooms.map((room) => {
|
|
21
|
+
const current = accommodation.rooms.find((r) => r.optionUnitId === room.id);
|
|
22
|
+
const ratePlans = room.ratePlans ?? [];
|
|
23
|
+
return (_jsxs("div", { className: "space-y-3 rounded-md border p-3", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("div", { className: "font-medium", children: room.name }), room.description ? (_jsx("div", { className: "text-muted-foreground text-xs", children: room.description })) : null] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { variant: "outline", size: "sm", type: "button", onClick: () => {
|
|
24
|
+
const list = accommodation.rooms.filter((r) => r.optionUnitId !== room.id);
|
|
25
|
+
const qty = (current?.quantity ?? 0) - 1;
|
|
26
|
+
if (qty > 0) {
|
|
27
|
+
list.push({
|
|
28
|
+
optionUnitId: room.id,
|
|
29
|
+
quantity: qty,
|
|
30
|
+
ratePlanId: current?.ratePlanId,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
setDraft(setAccommodation(draft, {
|
|
34
|
+
...accommodation,
|
|
35
|
+
rooms: list,
|
|
36
|
+
}));
|
|
37
|
+
}, children: "\u2212" }), _jsx("span", { className: "min-w-6 text-center", children: current?.quantity ?? 0 }), _jsx(Button, { variant: "outline", size: "sm", type: "button", onClick: () => {
|
|
38
|
+
const list = accommodation.rooms.filter((r) => r.optionUnitId !== room.id);
|
|
39
|
+
const qty = (current?.quantity ?? 0) + 1;
|
|
40
|
+
// Auto-select the only rate plan when there's
|
|
41
|
+
// exactly one — saves a click on the common case.
|
|
42
|
+
const ratePlanId = current?.ratePlanId ??
|
|
43
|
+
(ratePlans.length === 1 ? ratePlans[0]?.id : undefined);
|
|
44
|
+
list.push({
|
|
45
|
+
optionUnitId: room.id,
|
|
46
|
+
quantity: qty,
|
|
47
|
+
ratePlanId,
|
|
48
|
+
});
|
|
49
|
+
setDraft(setAccommodation(draft, {
|
|
50
|
+
...accommodation,
|
|
51
|
+
rooms: list,
|
|
52
|
+
}));
|
|
53
|
+
}, children: "+" })] })] }), current && current.quantity > 0 && ratePlans.length > 0 ? (_jsx(RatePlanPicker, { roomId: room.id, ratePlans: ratePlans, selected: current.ratePlanId, onSelect: (planId) => {
|
|
54
|
+
const list = accommodation.rooms.map((r) => r.optionUnitId === room.id ? { ...r, ratePlanId: planId } : r);
|
|
55
|
+
setDraft(setAccommodation(draft, {
|
|
56
|
+
...accommodation,
|
|
57
|
+
rooms: list,
|
|
58
|
+
}));
|
|
59
|
+
} })) : null] }, room.id));
|
|
60
|
+
}), subSteps.map((sub) => sub.kind === "extensions" ? (_jsx("div", { className: "rounded-md border p-3 text-muted-foreground text-sm", children: formatMessage(messages.bookingJourney.accommodation.extensionsAvailable, {
|
|
61
|
+
count: sub.options.length,
|
|
62
|
+
plural: sub.options.length === 1 ? "" : "s",
|
|
63
|
+
}) }, "extensions")) : null)] })) })] }));
|
|
64
|
+
}
|
|
65
|
+
function RatePlanPicker({ roomId, ratePlans, selected, onSelect, }) {
|
|
66
|
+
const messages = useBookingsUiMessagesOrDefault();
|
|
67
|
+
return (_jsxs("div", { className: "space-y-2 border-t pt-3", children: [_jsx(Label, { htmlFor: `bj-rate-plan-${roomId}`, children: messages.bookingJourney.accommodation.ratePlan }), _jsx("div", { className: "space-y-2", children: ratePlans.map((plan) => {
|
|
68
|
+
const isSelected = plan.id === selected;
|
|
69
|
+
return (_jsxs("button", { type: "button", onClick: () => onSelect(plan.id), className: `w-full rounded-md border p-2 text-left text-sm ${isSelected ? "border-primary ring-2 ring-primary" : ""}`, children: [_jsx("div", { className: "font-medium", children: plan.name }), plan.description ? (_jsx("div", { className: "text-muted-foreground text-xs", children: plan.description })) : null, plan.cancellationPolicy ? (_jsxs("div", { className: "text-muted-foreground text-xs", children: [messages.bookingJourney.accommodation.cancellationPrefix, " ", plan.cancellationPolicy] })) : null, plan.inclusions && plan.inclusions.length > 0 ? (_jsxs("div", { className: "text-muted-foreground text-xs", children: [messages.bookingJourney.accommodation.includesPrefix, " ", plan.inclusions.join(", ")] })) : null] }, plan.id));
|
|
70
|
+
}) })] }));
|
|
71
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"addons-step.d.ts","sourceRoot":"","sources":["../../../../src/journey/components/journey-steps/addons-step.tsx"],"names":[],"mappings":"AAOA,OAAO,EAAY,KAAK,eAAe,EAAE,MAAM,aAAa,CAAA;AAM5D,wBAAgB,UAAU,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,eAAe,GAAG,KAAK,CAAC,YAAY,CAmD1F"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Separator } from "@voyantjs/ui/components";
|
|
4
|
+
import { Button } from "@voyantjs/ui/components/button";
|
|
5
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@voyantjs/ui/components/card";
|
|
6
|
+
import { useBookingsUiMessagesOrDefault } from "../../../i18n/index.js";
|
|
7
|
+
import { setAddons } from "../../lib/draft-state.js";
|
|
8
|
+
import { bucketBy } from "./shared.js";
|
|
9
|
+
// ─────────────────────────────────────────────────────────────────
|
|
10
|
+
// Add-ons
|
|
11
|
+
// ─────────────────────────────────────────────────────────────────
|
|
12
|
+
export function AddonsStep({ draft, setDraft, shape }) {
|
|
13
|
+
const messages = useBookingsUiMessagesOrDefault();
|
|
14
|
+
const flat = shape.addons?.catalog ?? [];
|
|
15
|
+
const groups = shape.addons?.groups ?? [];
|
|
16
|
+
const all = [...flat, ...groups.flatMap((g) => g.items)];
|
|
17
|
+
return (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: messages.bookingJourney.addons.title }) }), _jsx(Separator, {}), _jsxs(CardContent, { className: "space-y-4", children: [all.length === 0 ? (_jsx("p", { className: "text-muted-foreground text-sm", children: messages.bookingJourney.addons.empty })) : null, groups.map((group) => {
|
|
18
|
+
// Group by port/day when the descriptor asks — cruise
|
|
19
|
+
// excursions arrive grouped by port name.
|
|
20
|
+
const buckets = group.groupBy === "port" || group.groupBy === "day"
|
|
21
|
+
? bucketBy(group.items, (i) => i.groupKey ?? messages.bookingJourney.addons.otherBucket)
|
|
22
|
+
: new Map([["", group.items]]);
|
|
23
|
+
return (_jsxs("div", { className: "space-y-3", children: [_jsx("div", { className: "font-medium text-sm", children: group.label }), [...buckets.entries()].map(([bucket, items]) => (_jsxs("div", { className: "space-y-2", children: [bucket ? (_jsx("div", { className: "text-muted-foreground text-xs uppercase", children: bucket })) : null, items.map((item) => (_jsx(AddonRow, { draft: draft, setDraft: setDraft, item: item }, item.id)))] }, bucket || "all")))] }, group.label));
|
|
24
|
+
}), flat.length > 0 && groups.length === 0 ? (_jsx("div", { className: "space-y-2", children: flat.map((item) => (_jsx(AddonRow, { draft: draft, setDraft: setDraft, item: item }, item.id))) })) : null] })] }));
|
|
25
|
+
}
|
|
26
|
+
function AddonRow({ draft, setDraft, item, }) {
|
|
27
|
+
const current = draft.addons.find((a) => a.extraId === item.id);
|
|
28
|
+
return (_jsxs("div", { className: "flex items-center justify-between rounded-md border p-3", children: [_jsxs("div", { children: [_jsx("div", { className: "font-medium", children: item.name }), item.description ? (_jsx("div", { className: "text-muted-foreground text-xs", children: item.description })) : null] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { variant: "outline", size: "sm", type: "button", onClick: () => {
|
|
29
|
+
const list = draft.addons.filter((a) => a.extraId !== item.id);
|
|
30
|
+
const qty = (current?.quantity ?? 0) - 1;
|
|
31
|
+
if (qty > 0)
|
|
32
|
+
list.push({ extraId: item.id, quantity: qty });
|
|
33
|
+
setDraft(setAddons(draft, list));
|
|
34
|
+
}, children: "\u2212" }), _jsx("span", { className: "min-w-6 text-center", children: current?.quantity ?? 0 }), _jsx(Button, { variant: "outline", size: "sm", type: "button", onClick: () => {
|
|
35
|
+
const list = draft.addons.filter((a) => a.extraId !== item.id);
|
|
36
|
+
const qty = (current?.quantity ?? 0) + 1;
|
|
37
|
+
list.push({ extraId: item.id, quantity: qty });
|
|
38
|
+
setDraft(setAddons(draft, list));
|
|
39
|
+
}, children: "+" })] })] }));
|
|
40
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { LeadContactPickerProps } from "../../types.js";
|
|
2
|
+
import { type StepCommonProps } from "./shared.js";
|
|
3
|
+
export declare function BillingStep({ draft, setDraft, renderLeadContactPicker, renderExtras, warnings, }: StepCommonProps & {
|
|
4
|
+
renderLeadContactPicker?: (props: LeadContactPickerProps) => React.ReactNode;
|
|
5
|
+
renderExtras?: () => React.ReactNode;
|
|
6
|
+
warnings?: ReadonlyArray<string>;
|
|
7
|
+
}): React.ReactElement;
|
|
8
|
+
//# sourceMappingURL=billing-step.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"billing-step.d.ts","sourceRoot":"","sources":["../../../../src/journey/components/journey-steps/billing-step.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAA;AAC5D,OAAO,EAAsC,KAAK,eAAe,EAAE,MAAM,aAAa,CAAA;AAMtF,wBAAgB,WAAW,CAAC,EAC1B,KAAK,EACL,QAAQ,EACR,uBAAuB,EACvB,YAAY,EACZ,QAAQ,GACT,EAAE,eAAe,GAAG;IACnB,uBAAuB,CAAC,EAAE,CAAC,KAAK,EAAE,sBAAsB,KAAK,KAAK,CAAC,SAAS,CAAA;IAC5E,YAAY,CAAC,EAAE,MAAM,KAAK,CAAC,SAAS,CAAA;IACpC,QAAQ,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;CACjC,GAAG,KAAK,CAAC,YAAY,CAyOrB"}
|