@voyantjs/bookings-ui 0.20.0 → 0.21.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/booking-dialog.d.ts.map +1 -1
- package/dist/components/booking-dialog.js +10 -1
- package/dist/components/booking-group-section.d.ts +11 -1
- package/dist/components/booking-group-section.d.ts.map +1 -1
- package/dist/components/booking-group-section.js +16 -2
- package/dist/components/booking-item-list.d.ts.map +1 -1
- package/dist/components/booking-item-list.js +71 -7
- package/dist/components/booking-list.d.ts.map +1 -1
- package/dist/components/booking-list.js +11 -3
- package/dist/components/booking-payments-summary.d.ts +28 -1
- package/dist/components/booking-payments-summary.d.ts.map +1 -1
- package/dist/components/booking-payments-summary.js +66 -11
- package/dist/components/traveler-list.d.ts +2 -1
- package/dist/components/traveler-list.d.ts.map +1 -1
- package/dist/components/traveler-list.js +126 -12
- package/dist/i18n/en.d.ts +12 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +13 -1
- package/dist/i18n/messages.d.ts +14 -1
- package/dist/i18n/messages.d.ts.map +1 -1
- package/dist/i18n/provider.d.ts +24 -0
- package/dist/i18n/provider.d.ts.map +1 -1
- package/dist/i18n/ro.d.ts +12 -0
- package/dist/i18n/ro.d.ts.map +1 -1
- package/dist/i18n/ro.js +13 -1
- package/dist/journey/components/booking-journey.d.ts +3 -0
- package/dist/journey/components/booking-journey.d.ts.map +1 -0
- package/dist/journey/components/booking-journey.js +376 -0
- package/dist/journey/components/contract-preview-dialog.d.ts +47 -0
- package/dist/journey/components/contract-preview-dialog.d.ts.map +1 -0
- package/dist/journey/components/contract-preview-dialog.js +119 -0
- package/dist/journey/components/journey-steps.d.ts +47 -0
- package/dist/journey/components/journey-steps.d.ts.map +1 -0
- package/dist/journey/components/journey-steps.js +582 -0
- package/dist/journey/components/side-panel.d.ts +12 -0
- package/dist/journey/components/side-panel.d.ts.map +1 -0
- package/dist/journey/components/side-panel.js +172 -0
- package/dist/journey/components/step-header.d.ts +7 -0
- package/dist/journey/components/step-header.d.ts.map +1 -0
- package/dist/journey/components/step-header.js +28 -0
- package/dist/journey/index.d.ts +18 -0
- package/dist/journey/index.d.ts.map +1 -0
- package/dist/journey/index.js +17 -0
- package/dist/journey/lib/draft-state.d.ts +34 -0
- package/dist/journey/lib/draft-state.d.ts.map +1 -0
- package/dist/journey/lib/draft-state.js +54 -0
- package/dist/journey/types.d.ts +248 -0
- package/dist/journey/types.d.ts.map +1 -0
- package/dist/journey/types.js +17 -0
- package/package.json +26 -16
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* `<BookingJourney />` — the unified booking journey shell.
|
|
5
|
+
*
|
|
6
|
+
* Per `docs/architecture/booking-journey-architecture.md` §3 + §8.1.
|
|
7
|
+
*
|
|
8
|
+
* Composes:
|
|
9
|
+
* - draft state (held at this root, mutated by step components)
|
|
10
|
+
* - quote orchestration (debounced re-quote on every meaningful change)
|
|
11
|
+
* - server-side draft persistence (PUT on every step transition)
|
|
12
|
+
* - commit (Review step's Confirm button)
|
|
13
|
+
*
|
|
14
|
+
* Surface differences (CRM picker for operators vs. inline contact for
|
|
15
|
+
* customers, B2B billing default vs. B2C, payment provider hookup) live
|
|
16
|
+
* in injectable slots passed via props — same shell on every surface.
|
|
17
|
+
*/
|
|
18
|
+
import { DEFAULT_PAX_BANDS, defaultBookingFields, defaultDraftShapeFlags, defaultTravelerFields, paxBandsAllowedTotalFrom, } from "@voyantjs/catalog/booking-engine";
|
|
19
|
+
import { useBookingCommit, useBookingDraft, useBookingDraftShape, useBookingHold, useBookingQuote, } from "@voyantjs/catalog-react/booking-engine";
|
|
20
|
+
import { Button } from "@voyantjs/ui/components/button";
|
|
21
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
22
|
+
import { emptyDraft, totalPax } from "../lib/draft-state.js";
|
|
23
|
+
import { JOURNEY_STEP_ORDER, } from "../types.js";
|
|
24
|
+
import { ContractPreviewDialog } from "./contract-preview-dialog.js";
|
|
25
|
+
import { AccommodationStep, AddonsStep, BillingStep, ConfigureStep, PaymentStep, ReviewStep, TravelersStep, } from "./journey-steps.js";
|
|
26
|
+
import { PriceSidePanel } from "./side-panel.js";
|
|
27
|
+
import { StepHeader } from "./step-header.js";
|
|
28
|
+
export function BookingJourney(props) {
|
|
29
|
+
const surface = props.surface ?? "admin";
|
|
30
|
+
const [draft, setDraft] = useState(() => {
|
|
31
|
+
const base = emptyDraft({
|
|
32
|
+
module: props.entityModule,
|
|
33
|
+
id: props.entityId,
|
|
34
|
+
// Empty when storefront — the public engine route resolves
|
|
35
|
+
// it server-side from (entityModule, entityId).
|
|
36
|
+
sourceKind: props.sourceKind ?? "",
|
|
37
|
+
sourceConnectionId: props.sourceConnectionId,
|
|
38
|
+
sourceRef: props.sourceRef,
|
|
39
|
+
}, { buyerType: props.defaultBuyerType ?? (surface === "admin" ? "B2B" : "B2C") });
|
|
40
|
+
// Seed Configure when the caller passed pre-locked state —
|
|
41
|
+
// detail page picks departure + pax, booking flow only handles
|
|
42
|
+
// travelers + addons + payment.
|
|
43
|
+
if (props.initialConfigure) {
|
|
44
|
+
const seed = props.initialConfigure;
|
|
45
|
+
const seedPax = seed.pax;
|
|
46
|
+
base.configure = Object.assign({}, base.configure, seed, {
|
|
47
|
+
pax: { ...base.configure.pax, ...(seedPax ?? {}) },
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
if (props.initialAccommodation) {
|
|
51
|
+
const seed = props.initialAccommodation;
|
|
52
|
+
base.accommodation = Object.assign({ rooms: [], travelerAssignments: {} }, base.accommodation ?? {}, seed);
|
|
53
|
+
}
|
|
54
|
+
return base;
|
|
55
|
+
});
|
|
56
|
+
const fallbackShape = useMemo(() => props.fallbackShape ?? defaultMinimalShape(), [props.fallbackShape]);
|
|
57
|
+
// Server-side draft sync — PUT on each step transition. The shell
|
|
58
|
+
// doesn't read from the server in Phase B (drafts are recovery
|
|
59
|
+
// surface, not source of truth) but the wire is in place.
|
|
60
|
+
const draftSync = useBookingDraft({
|
|
61
|
+
surface,
|
|
62
|
+
draftId: props.draftId,
|
|
63
|
+
enableLoad: false,
|
|
64
|
+
});
|
|
65
|
+
// Live quote — debounced 250ms.
|
|
66
|
+
const quote = useBookingQuote({
|
|
67
|
+
surface,
|
|
68
|
+
draft,
|
|
69
|
+
});
|
|
70
|
+
const shape = useBookingDraftShape({
|
|
71
|
+
surface,
|
|
72
|
+
quote: quote.data,
|
|
73
|
+
fallback: fallbackShape,
|
|
74
|
+
});
|
|
75
|
+
// Step navigation — only show steps the descriptor says are relevant.
|
|
76
|
+
const hideConfigure = props.hideConfigure === true;
|
|
77
|
+
const steps = useMemo(() => JOURNEY_STEP_ORDER.filter((s) => {
|
|
78
|
+
if (hideConfigure && s === "configure")
|
|
79
|
+
return false;
|
|
80
|
+
return isStepVisible(s, shape);
|
|
81
|
+
}), [shape, hideConfigure]);
|
|
82
|
+
const [currentStep, setCurrentStep] = useState(() => steps[0] ?? "configure");
|
|
83
|
+
const [visited, setVisited] = useState(() => new Set([steps[0] ?? "configure"]));
|
|
84
|
+
// If the descriptor changes and removes the current step, reset to
|
|
85
|
+
// the first available step. (Edge case: shape goes from
|
|
86
|
+
// owned→sourced and the relevant step set narrows.)
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (!steps.includes(currentStep)) {
|
|
89
|
+
setCurrentStep(steps[0] ?? "configure");
|
|
90
|
+
}
|
|
91
|
+
}, [steps, currentStep]);
|
|
92
|
+
// PUT the draft to the server every time the user transitions
|
|
93
|
+
// steps. Keeps the recovery surface fresh without saving on every
|
|
94
|
+
// keystroke. The mutation reads the latest draft + quote from the
|
|
95
|
+
// closure on each fire — adding them to the deps array would defeat
|
|
96
|
+
// the "save on step change only" semantics.
|
|
97
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: intentional — fires on step transition only
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
draftSync.save.mutate({
|
|
100
|
+
draft: { ...draft, quoteId: quote.data?.quoteId },
|
|
101
|
+
currentStep,
|
|
102
|
+
currentQuoteId: quote.data?.quoteId,
|
|
103
|
+
});
|
|
104
|
+
}, [currentStep]);
|
|
105
|
+
// Commit
|
|
106
|
+
const commit = useBookingCommit({
|
|
107
|
+
surface,
|
|
108
|
+
draftId: props.draftId,
|
|
109
|
+
onCommitted: props.onCommitted,
|
|
110
|
+
});
|
|
111
|
+
// Inventory hold — fired when the user advances past Configure
|
|
112
|
+
// with a slot + pax picked. Failures are non-blocking (the engine
|
|
113
|
+
// re-validates capacity at commit time anyway); we just don't
|
|
114
|
+
// want to silently let two shoppers race past Configure with one
|
|
115
|
+
// capacity unit between them.
|
|
116
|
+
const holdApi = useBookingHold({ surface });
|
|
117
|
+
const holdState = useRef({});
|
|
118
|
+
const holdSignature = makeHoldSignature(draft, props.entityModule, props.entityId);
|
|
119
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: signature change is the only trigger; refs + closure read latest values
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (currentStep === "configure" || !holdSignature)
|
|
122
|
+
return;
|
|
123
|
+
if (holdState.current.signature === holdSignature)
|
|
124
|
+
return;
|
|
125
|
+
// Inquiry mode is the lead-form path — capture the lead without
|
|
126
|
+
// burning capacity. The operator follows up before any inventory
|
|
127
|
+
// is touched.
|
|
128
|
+
if (draft.payment.intent === "inquiry")
|
|
129
|
+
return;
|
|
130
|
+
const previousToken = holdState.current.holdToken;
|
|
131
|
+
holdState.current = { signature: holdSignature };
|
|
132
|
+
void holdApi
|
|
133
|
+
.place({
|
|
134
|
+
entityModule: props.entityModule,
|
|
135
|
+
entityId: props.entityId,
|
|
136
|
+
draftId: props.draftId,
|
|
137
|
+
parameters: {
|
|
138
|
+
slotId: draft.configure.departureSlotId,
|
|
139
|
+
paxCount: totalPax(draft),
|
|
140
|
+
productId: props.entityId,
|
|
141
|
+
},
|
|
142
|
+
})
|
|
143
|
+
.then((result) => {
|
|
144
|
+
holdState.current = { holdToken: result.holdToken, signature: holdSignature };
|
|
145
|
+
})
|
|
146
|
+
.catch(() => {
|
|
147
|
+
// Non-blocking — see comment above.
|
|
148
|
+
});
|
|
149
|
+
if (previousToken) {
|
|
150
|
+
void holdApi
|
|
151
|
+
.release({ entityModule: props.entityModule, holdToken: previousToken })
|
|
152
|
+
.catch(() => { });
|
|
153
|
+
}
|
|
154
|
+
}, [holdSignature, currentStep]);
|
|
155
|
+
const canAdvance = canAdvanceFromStep(currentStep, draft, shape, quote.data?.available !== false);
|
|
156
|
+
const warnings = warningsForStep(currentStep, draft, shape);
|
|
157
|
+
const idx = steps.indexOf(currentStep);
|
|
158
|
+
const next = steps[idx + 1];
|
|
159
|
+
const prev = steps[idx - 1];
|
|
160
|
+
const advance = () => {
|
|
161
|
+
if (!next || !canAdvance)
|
|
162
|
+
return;
|
|
163
|
+
setCurrentStep(next);
|
|
164
|
+
setVisited((s) => new Set(s).add(next));
|
|
165
|
+
};
|
|
166
|
+
const goBack = () => {
|
|
167
|
+
if (!prev)
|
|
168
|
+
return;
|
|
169
|
+
setCurrentStep(prev);
|
|
170
|
+
};
|
|
171
|
+
const jumpTo = (step) => {
|
|
172
|
+
if (!visited.has(step) && step !== currentStep)
|
|
173
|
+
return;
|
|
174
|
+
setCurrentStep(step);
|
|
175
|
+
};
|
|
176
|
+
const [contractDialogOpen, setContractDialogOpen] = useState(false);
|
|
177
|
+
// Tracks the multi-step storefront checkout flow (book →
|
|
178
|
+
// checkout-start → redirect). The legacy in-process commit has
|
|
179
|
+
// its own `commit.isPending` so the Confirm button merges both
|
|
180
|
+
// when deciding whether to show a spinner.
|
|
181
|
+
const [isHandlingCheckout, setIsHandlingCheckout] = useState(false);
|
|
182
|
+
const contractConfig = props.contract;
|
|
183
|
+
const contractVariables = useMemo(() => {
|
|
184
|
+
if (!contractConfig)
|
|
185
|
+
return {};
|
|
186
|
+
return contractConfig.resolveVariables({ draft, pricing: quote.data?.pricing ?? null });
|
|
187
|
+
}, [contractConfig, draft, quote.data?.pricing]);
|
|
188
|
+
const commitDraft = async () => {
|
|
189
|
+
if (!quote.data?.quoteId)
|
|
190
|
+
return;
|
|
191
|
+
await commit.mutateAsync({
|
|
192
|
+
draft: { ...draft, quoteId: quote.data.quoteId },
|
|
193
|
+
quoteId: quote.data.quoteId,
|
|
194
|
+
paymentIntent: { type: draft.payment.intent === "card" ? "card" : "hold" },
|
|
195
|
+
});
|
|
196
|
+
};
|
|
197
|
+
const handleAccepted = async (acceptance) => {
|
|
198
|
+
if (!props.onContractAccepted) {
|
|
199
|
+
await commitDraft();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
setIsHandlingCheckout(true);
|
|
203
|
+
try {
|
|
204
|
+
await props.onContractAccepted(acceptance, {
|
|
205
|
+
draft,
|
|
206
|
+
pricing: quote.data?.pricing ?? null,
|
|
207
|
+
quoteId: quote.data?.quoteId,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
setIsHandlingCheckout(false);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
const onConfirm = async () => {
|
|
215
|
+
if (!quote.data?.quoteId)
|
|
216
|
+
return;
|
|
217
|
+
// 1. Contract is wired → open the dialog. Acceptance triggers
|
|
218
|
+
// onContractAccepted (the storefront's checkout-start path).
|
|
219
|
+
// 2. No contract but onContractAccepted is wired → call it
|
|
220
|
+
// directly without an acceptance payload. The storefront uses
|
|
221
|
+
// this to drive the /book + /checkout/start handoff so card
|
|
222
|
+
// intents redirect to the PSP. Skipping the dialog when no
|
|
223
|
+
// template is configured is intentional — the dialog is an
|
|
224
|
+
// optional gate, not a required step.
|
|
225
|
+
// 3. Neither wired → in-process commit (the operator dashboard's
|
|
226
|
+
// legacy path).
|
|
227
|
+
if (contractConfig) {
|
|
228
|
+
setContractDialogOpen(true);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
await handleAccepted(null);
|
|
232
|
+
};
|
|
233
|
+
const onContractAccept = async (acceptance) => {
|
|
234
|
+
setContractDialogOpen(false);
|
|
235
|
+
await handleAccepted(acceptance);
|
|
236
|
+
};
|
|
237
|
+
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 === "configure" ? (_jsx(ConfigureStep, { draft: draft, setDraft: setDraft, shape: shape })) : null, currentStep === "billing" ? (_jsx(BillingStep, { draft: draft, setDraft: setDraft, shape: shape, renderLeadContactPicker: props.renderLeadContactPicker, renderExtras: props.renderBillingExtras })) : 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 ?? {
|
|
238
|
+
acceptsCard: false,
|
|
239
|
+
acceptsHold: true,
|
|
240
|
+
acceptsTicketOnCredit: false,
|
|
241
|
+
}, 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", onClick: () => {
|
|
242
|
+
if (prev)
|
|
243
|
+
goBack();
|
|
244
|
+
else
|
|
245
|
+
props.onCancelled?.();
|
|
246
|
+
}, children: "Back" }), next ? (_jsx(Button, { type: "button", onClick: advance, disabled: !canAdvance, className: "ml-auto", children: "Next" })) : 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] }));
|
|
247
|
+
}
|
|
248
|
+
function isStepVisible(step, shape) {
|
|
249
|
+
switch (step) {
|
|
250
|
+
case "configure":
|
|
251
|
+
return shape.showsConfigure;
|
|
252
|
+
case "billing":
|
|
253
|
+
return shape.showsBilling;
|
|
254
|
+
case "travelers":
|
|
255
|
+
return shape.showsTravelers;
|
|
256
|
+
case "accommodation":
|
|
257
|
+
return shape.showsAccommodation;
|
|
258
|
+
case "addons":
|
|
259
|
+
return shape.showsAddons;
|
|
260
|
+
case "payment":
|
|
261
|
+
return shape.showsPayment;
|
|
262
|
+
case "review":
|
|
263
|
+
return shape.showsReview;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function canAdvanceFromStep(step, draft, shape, available) {
|
|
267
|
+
if (!available)
|
|
268
|
+
return false;
|
|
269
|
+
switch (step) {
|
|
270
|
+
case "configure": {
|
|
271
|
+
const total = totalPax(draft);
|
|
272
|
+
return total >= shape.paxBandsAllowedTotal.min && total <= shape.paxBandsAllowedTotal.max;
|
|
273
|
+
}
|
|
274
|
+
case "billing": {
|
|
275
|
+
const c = draft.billing.contact;
|
|
276
|
+
return c.firstName.length > 0 && c.lastName.length > 0 && c.email.length > 0;
|
|
277
|
+
}
|
|
278
|
+
case "travelers": {
|
|
279
|
+
// Hard-reject only on canonical traveler fields (firstName,
|
|
280
|
+
// lastName) — those are always required regardless of
|
|
281
|
+
// descriptor configuration. All other required fields surface
|
|
282
|
+
// as warnings so operators can complete the journey and fill
|
|
283
|
+
// them in later from the booking detail page.
|
|
284
|
+
return draft.travelers.every((t) => t.firstName && t.lastName);
|
|
285
|
+
}
|
|
286
|
+
default:
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Soft warnings for the current step — surfaced inline above the
|
|
292
|
+
* Next button. Don't block advancement; they're hints. Per
|
|
293
|
+
* booking-journey-architecture §12.5.
|
|
294
|
+
*
|
|
295
|
+
* The hard-reject path stays in `canAdvanceFromStep` for fields
|
|
296
|
+
* that are physically required to commit (e.g. traveler names);
|
|
297
|
+
* everything else is a warning here.
|
|
298
|
+
*/
|
|
299
|
+
function warningsForStep(step, draft, shape) {
|
|
300
|
+
const warnings = [];
|
|
301
|
+
switch (step) {
|
|
302
|
+
case "billing": {
|
|
303
|
+
const c = draft.billing.contact;
|
|
304
|
+
if (c.phone == null || c.phone.length === 0) {
|
|
305
|
+
warnings.push("Phone number not set — useful for last-minute supplier contact.");
|
|
306
|
+
}
|
|
307
|
+
if (!draft.billing.address.country) {
|
|
308
|
+
warnings.push("Billing country not set — taxes won't compute until it's filled in.");
|
|
309
|
+
}
|
|
310
|
+
if (draft.billing.buyerType === "B2B" && !draft.billing.company?.vatId) {
|
|
311
|
+
warnings.push("VAT id missing — required for B2B reverse-charge invoicing.");
|
|
312
|
+
}
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
case "travelers": {
|
|
316
|
+
const requiredKeys = shape.travelerFields.filter((f) => f.required).map((f) => f.key);
|
|
317
|
+
const skipBaseline = new Set(["firstName", "lastName"]);
|
|
318
|
+
const optionalRequired = requiredKeys.filter((k) => !skipBaseline.has(k));
|
|
319
|
+
for (const t of draft.travelers) {
|
|
320
|
+
for (const key of optionalRequired) {
|
|
321
|
+
const docs = t.documents ?? {};
|
|
322
|
+
// Email is on the row directly; everything else lives in
|
|
323
|
+
// the document map.
|
|
324
|
+
const value = key === "email" ? t.email : docs[key];
|
|
325
|
+
if (value == null || value === "") {
|
|
326
|
+
const traveler = `${t.firstName || "Traveler"} ${t.lastName || ""}`.trim();
|
|
327
|
+
warnings.push(`${traveler}: ${labelForFieldKey(key, shape)} is required.`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
case "review": {
|
|
334
|
+
if (!draft.payment.intent) {
|
|
335
|
+
warnings.push("Payment intent not set — booking will default to hold.");
|
|
336
|
+
}
|
|
337
|
+
if (draft.travelers.length === 0) {
|
|
338
|
+
warnings.push("No travelers added — at least one is recommended for ops handoff.");
|
|
339
|
+
}
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return warnings;
|
|
344
|
+
}
|
|
345
|
+
function labelForFieldKey(key, shape) {
|
|
346
|
+
return shape.travelerFields.find((f) => f.key === key)?.label ?? key;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Compose a stable signature off the inputs the hold cares about.
|
|
350
|
+
* Includes entity + slot + pax so any change re-issues the hold;
|
|
351
|
+
* excludes billing / traveler details so cosmetic edits don't
|
|
352
|
+
* thrash the inventory layer.
|
|
353
|
+
*/
|
|
354
|
+
function makeHoldSignature(draft, entityModule, entityId) {
|
|
355
|
+
const slot = draft.configure.departureSlotId;
|
|
356
|
+
if (!slot)
|
|
357
|
+
return null;
|
|
358
|
+
const pax = totalPax(draft);
|
|
359
|
+
if (pax <= 0)
|
|
360
|
+
return null;
|
|
361
|
+
return `${entityModule}/${entityId}/${slot}/${pax}`;
|
|
362
|
+
}
|
|
363
|
+
function defaultMinimalShape() {
|
|
364
|
+
return {
|
|
365
|
+
...defaultDraftShapeFlags(),
|
|
366
|
+
paxBands: DEFAULT_PAX_BANDS,
|
|
367
|
+
paxBandsAllowedTotal: paxBandsAllowedTotalFrom(DEFAULT_PAX_BANDS),
|
|
368
|
+
travelerFields: defaultTravelerFields(),
|
|
369
|
+
bookingFields: defaultBookingFields(),
|
|
370
|
+
// Engine-level allow list. Capabilities (per-deployment toggles)
|
|
371
|
+
// narrow further at render time — listing every supported intent
|
|
372
|
+
// here means consumers can opt in via PaymentProviderCapabilities
|
|
373
|
+
// without needing a custom fallbackShape.
|
|
374
|
+
paymentIntents: ["card", "bank_transfer", "hold", "inquiry", "ticket_on_credit"],
|
|
375
|
+
};
|
|
376
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Contract preview dialog. Renders a contract template with the
|
|
4
|
+
* draft variables prefilled and gates the journey on two
|
|
5
|
+
* checkboxes — terms acceptance and an optional marketing opt-in.
|
|
6
|
+
*
|
|
7
|
+
* The dialog is template-driven: the storefront wires a
|
|
8
|
+
* `templateSlug` plus a `resolveVariables(draft)` function that maps
|
|
9
|
+
* the booking draft to the template's variable schema. The dialog
|
|
10
|
+
* fetches the rendered HTML from
|
|
11
|
+
* `POST /v1/public/legal/contracts/templates/by-slug/:slug/preview`
|
|
12
|
+
* and renders it in an iframe (sandboxed — same-origin removed) so
|
|
13
|
+
* inline styles in the contract HTML don't leak into the page.
|
|
14
|
+
*/
|
|
15
|
+
export interface ContractPreviewDialogProps {
|
|
16
|
+
open: boolean;
|
|
17
|
+
onOpenChange: (open: boolean) => void;
|
|
18
|
+
/** Variables to interpolate into the template body. */
|
|
19
|
+
variables: Record<string, unknown>;
|
|
20
|
+
/**
|
|
21
|
+
* URL of the public render endpoint. The storefront wraps the
|
|
22
|
+
* journey and supplies an absolute URL so the dialog stays
|
|
23
|
+
* agnostic about where the API base lives.
|
|
24
|
+
*/
|
|
25
|
+
previewUrl: string;
|
|
26
|
+
/** Optional Accept-Language header for locale resolution. */
|
|
27
|
+
acceptLanguage?: string;
|
|
28
|
+
/** Fired when the user clicks Accept after ticking the gates. */
|
|
29
|
+
onAccept: (acceptance: ContractAcceptance) => void;
|
|
30
|
+
/** Optional marketing-opt-in label. When omitted, marketing
|
|
31
|
+
* consent isn't required to accept. */
|
|
32
|
+
marketingLabel?: string;
|
|
33
|
+
termsLabel?: React.ReactNode;
|
|
34
|
+
}
|
|
35
|
+
export interface ContractAcceptance {
|
|
36
|
+
templateId: string;
|
|
37
|
+
templateSlug: string;
|
|
38
|
+
templateName: string;
|
|
39
|
+
acceptedTerms: true;
|
|
40
|
+
acceptedMarketing: boolean;
|
|
41
|
+
acceptedAt: string;
|
|
42
|
+
/** The exact rendered HTML the user accepted — captured for the
|
|
43
|
+
* audit trail so we can reproduce what they saw. */
|
|
44
|
+
renderedHtml: string;
|
|
45
|
+
}
|
|
46
|
+
export declare function ContractPreviewDialog({ open, onOpenChange, variables, previewUrl, acceptLanguage, onAccept, marketingLabel, termsLabel, }: ContractPreviewDialogProps): React.ReactElement;
|
|
47
|
+
//# sourceMappingURL=contract-preview-dialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract-preview-dialog.d.ts","sourceRoot":"","sources":["../../../src/journey/components/contract-preview-dialog.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,uDAAuD;IACvD,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAClC;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAA;IAClB,6DAA6D;IAC7D,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,iEAAiE;IACjE,QAAQ,EAAE,CAAC,UAAU,EAAE,kBAAkB,KAAK,IAAI,CAAA;IAClD;4CACwC;IACxC,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC7B;AAED,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,EAAE,MAAM,CAAA;IACpB,aAAa,EAAE,IAAI,CAAA;IACnB,iBAAiB,EAAE,OAAO,CAAA;IAC1B,UAAU,EAAE,MAAM,CAAA;IAClB;yDACqD;IACrD,YAAY,EAAE,MAAM,CAAA;CACrB;AAgBD,wBAAgB,qBAAqB,CAAC,EACpC,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,UAAU,EACV,cAAc,EACd,QAAQ,EACR,cAAc,EACd,UAAU,GACX,EAAE,0BAA0B,GAAG,KAAK,CAAC,YAAY,CA6JjD"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { Button } from "@voyantjs/ui/components/button";
|
|
4
|
+
import { Checkbox } from "@voyantjs/ui/components/checkbox";
|
|
5
|
+
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from "@voyantjs/ui/components/dialog";
|
|
6
|
+
import { Skeleton } from "@voyantjs/ui/components/skeleton";
|
|
7
|
+
import * as React from "react";
|
|
8
|
+
export function ContractPreviewDialog({ open, onOpenChange, variables, previewUrl, acceptLanguage, onAccept, marketingLabel, termsLabel, }) {
|
|
9
|
+
const [loadState, setLoadState] = React.useState("idle");
|
|
10
|
+
const [errorMessage, setErrorMessage] = React.useState(null);
|
|
11
|
+
const [renderedHtml, setRenderedHtml] = React.useState("");
|
|
12
|
+
const [template, setTemplate] = React.useState(null);
|
|
13
|
+
const [acceptedTerms, setAcceptedTerms] = React.useState(false);
|
|
14
|
+
// Marketing consent defaults to opted-in; the customer can untick
|
|
15
|
+
// before accepting. Terms still require an explicit tick.
|
|
16
|
+
const [acceptedMarketing, setAcceptedMarketing] = React.useState(true);
|
|
17
|
+
// Stringify the variables once so the effect re-fetches only on
|
|
18
|
+
// meaningful changes — equivalent to a deep-equality check, but
|
|
19
|
+
// cheap enough to run every render.
|
|
20
|
+
const variablesKey = React.useMemo(() => JSON.stringify(variables), [variables]);
|
|
21
|
+
const variablesRef = React.useRef(variables);
|
|
22
|
+
variablesRef.current = variables;
|
|
23
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: variablesKey is the meaningful-change signal; the actual variables are read off variablesRef so the effect doesn't re-fire on every render
|
|
24
|
+
React.useEffect(() => {
|
|
25
|
+
if (!open)
|
|
26
|
+
return;
|
|
27
|
+
let cancelled = false;
|
|
28
|
+
setLoadState("loading");
|
|
29
|
+
setErrorMessage(null);
|
|
30
|
+
setAcceptedTerms(false);
|
|
31
|
+
setAcceptedMarketing(true);
|
|
32
|
+
void fetch(previewUrl, {
|
|
33
|
+
method: "POST",
|
|
34
|
+
credentials: "include",
|
|
35
|
+
headers: {
|
|
36
|
+
"content-type": "application/json",
|
|
37
|
+
...(acceptLanguage ? { "accept-language": acceptLanguage } : {}),
|
|
38
|
+
},
|
|
39
|
+
body: JSON.stringify({ variables: variablesRef.current }),
|
|
40
|
+
})
|
|
41
|
+
.then(async (res) => {
|
|
42
|
+
if (!res.ok) {
|
|
43
|
+
throw new Error(`Preview request failed: ${res.status}`);
|
|
44
|
+
}
|
|
45
|
+
const json = (await res.json());
|
|
46
|
+
if (cancelled)
|
|
47
|
+
return;
|
|
48
|
+
const body = json.data?.rendered ?? "";
|
|
49
|
+
const tmpl = json.data?.template;
|
|
50
|
+
if (!body || !tmpl) {
|
|
51
|
+
throw new Error("Preview response missing rendered body or template metadata");
|
|
52
|
+
}
|
|
53
|
+
setRenderedHtml(body);
|
|
54
|
+
setTemplate({ id: tmpl.id, slug: tmpl.slug, name: tmpl.name });
|
|
55
|
+
setLoadState("ready");
|
|
56
|
+
})
|
|
57
|
+
.catch((err) => {
|
|
58
|
+
if (cancelled)
|
|
59
|
+
return;
|
|
60
|
+
setErrorMessage(err instanceof Error ? err.message : String(err));
|
|
61
|
+
setLoadState("error");
|
|
62
|
+
});
|
|
63
|
+
return () => {
|
|
64
|
+
cancelled = true;
|
|
65
|
+
};
|
|
66
|
+
}, [open, previewUrl, acceptLanguage, variablesKey]);
|
|
67
|
+
const canAccept = loadState === "ready" && acceptedTerms && Boolean(template);
|
|
68
|
+
const handleAccept = () => {
|
|
69
|
+
if (!canAccept || !template)
|
|
70
|
+
return;
|
|
71
|
+
onAccept({
|
|
72
|
+
templateId: template.id,
|
|
73
|
+
templateSlug: template.slug,
|
|
74
|
+
templateName: template.name,
|
|
75
|
+
acceptedTerms: true,
|
|
76
|
+
acceptedMarketing,
|
|
77
|
+
acceptedAt: new Date().toISOString(),
|
|
78
|
+
renderedHtml,
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "flex h-[85vh] w-[95vw] max-w-4xl flex-col gap-0 p-0", children: [_jsxs(DialogHeader, { className: "border-b p-6 pb-4", children: [_jsx(DialogTitle, { children: template?.name ?? "Booking contract" }), _jsx("p", { className: "text-muted-foreground text-sm", children: "Please review the contract below. You can scroll through the document, then tick the boxes and click Accept to continue." })] }), _jsxs("div", { className: "flex-1 overflow-hidden bg-muted/30", children: [loadState === "loading" ? (_jsxs("div", { className: "space-y-3 p-6", children: [_jsx(Skeleton, { className: "h-6 w-1/2" }), _jsx(Skeleton, { className: "h-4 w-full" }), _jsx(Skeleton, { className: "h-4 w-5/6" }), _jsx(Skeleton, { className: "h-4 w-4/5" }), _jsx(Skeleton, { className: "h-4 w-full" })] })) : null, loadState === "error" ? (_jsx("div", { className: "p-6", children: _jsxs("p", { className: "text-destructive text-sm", children: ["Couldn't load the contract preview: ", errorMessage] }) })) : null, loadState === "ready" ? (_jsx("iframe", { title: `${template?.name ?? "Contract"} preview`, srcDoc: wrapPreviewHtml(renderedHtml), sandbox: "", className: "h-full w-full border-0 bg-white" })) : null] }), _jsxs(DialogFooter, { className: "border-t p-6 sm:flex-col sm:items-stretch sm:gap-3", children: [_jsxs("div", { className: "space-y-2 text-sm", children: [_jsxs("label", { className: "flex items-start gap-2", children: [_jsx(Checkbox, { checked: acceptedTerms, onCheckedChange: (v) => setAcceptedTerms(v === true), className: "mt-0.5" }), _jsx("span", { children: termsLabel ?? (_jsx(_Fragment, { children: "I have read and agree to the terms of this contract. I understand that this booking is binding once accepted." })) })] }), _jsxs("label", { className: "flex items-start gap-2", children: [_jsx(Checkbox, { checked: acceptedMarketing, onCheckedChange: (v) => setAcceptedMarketing(v === true), className: "mt-0.5" }), _jsx("span", { children: marketingLabel ??
|
|
82
|
+
"I'd like to receive occasional travel offers and updates by email. (You can unsubscribe at any time.)" })] })] }), _jsxs("div", { className: "flex justify-end gap-2", children: [_jsx(Button, { type: "button", variant: "outline", onClick: () => onOpenChange(false), children: "Cancel" }), _jsx(Button, { type: "button", disabled: !canAccept, onClick: handleAccept, children: "Accept and continue" })] })] })] }) }));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Wrap the rendered template body in a self-contained light-theme HTML
|
|
86
|
+
* document. Templates author their own typography but rarely set a
|
|
87
|
+
* background, so without this the iframe inherits the browser default
|
|
88
|
+
* (transparent) and we'd see whatever shows through — which on the
|
|
89
|
+
* storefront's dark dialog reads as black-on-black.
|
|
90
|
+
*/
|
|
91
|
+
function wrapPreviewHtml(body) {
|
|
92
|
+
return `<!DOCTYPE html>
|
|
93
|
+
<html lang="en">
|
|
94
|
+
<head>
|
|
95
|
+
<meta charset="utf-8" />
|
|
96
|
+
<style>
|
|
97
|
+
:root { color-scheme: light; }
|
|
98
|
+
html, body { margin: 0; background: #ffffff; color: #111827; }
|
|
99
|
+
body {
|
|
100
|
+
padding: 1.5rem 2rem;
|
|
101
|
+
font-family: ui-serif, Georgia, "Times New Roman", serif;
|
|
102
|
+
font-size: 15px;
|
|
103
|
+
line-height: 1.6;
|
|
104
|
+
}
|
|
105
|
+
h1, h2, h3 { font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; color: #0f172a; }
|
|
106
|
+
h1 { font-size: 1.5rem; margin: 0 0 1rem; }
|
|
107
|
+
h2 { font-size: 1.15rem; margin: 1.5rem 0 0.5rem; }
|
|
108
|
+
p { margin: 0.5rem 0; }
|
|
109
|
+
ul, ol { padding-left: 1.5rem; }
|
|
110
|
+
strong { color: #0f172a; }
|
|
111
|
+
a { color: #2563eb; }
|
|
112
|
+
table { border-collapse: collapse; width: 100%; margin: 0.75rem 0; }
|
|
113
|
+
th, td { border: 1px solid #e5e7eb; padding: 0.5rem 0.75rem; text-align: left; }
|
|
114
|
+
th { background: #f9fafb; }
|
|
115
|
+
</style>
|
|
116
|
+
</head>
|
|
117
|
+
<body>${body}</body>
|
|
118
|
+
</html>`;
|
|
119
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step components rendered inside `<BookingJourney />`. Each takes a
|
|
3
|
+
* draft + setDraft pair plus the active descriptor; updates flow up
|
|
4
|
+
* via setDraft and the shell re-quotes on the next debounce tick.
|
|
5
|
+
*
|
|
6
|
+
* Per booking-journey-architecture §3.
|
|
7
|
+
*/
|
|
8
|
+
import type { BookingDraftShape } from "@voyantjs/catalog/booking-engine";
|
|
9
|
+
import { type Draft } from "../lib/draft-state.js";
|
|
10
|
+
import type { LeadContactPickerProps, PaymentProviderCapabilities, PaymentProviderStepRenderProps, TravelerContactPickerProps } from "../types.js";
|
|
11
|
+
interface StepCommonProps {
|
|
12
|
+
draft: Draft;
|
|
13
|
+
setDraft: (next: Draft) => void;
|
|
14
|
+
shape: BookingDraftShape;
|
|
15
|
+
}
|
|
16
|
+
export declare function ConfigureStep({ draft, setDraft, shape, }: StepCommonProps & {
|
|
17
|
+
renderExtras?: () => React.ReactNode;
|
|
18
|
+
}): React.ReactElement;
|
|
19
|
+
export declare function BillingStep({ draft, setDraft, renderLeadContactPicker, renderExtras, }: StepCommonProps & {
|
|
20
|
+
renderLeadContactPicker?: (props: LeadContactPickerProps) => React.ReactNode;
|
|
21
|
+
renderExtras?: () => React.ReactNode;
|
|
22
|
+
}): React.ReactElement;
|
|
23
|
+
export declare function TravelersStep({ draft, setDraft, shape, renderTravelerContactPicker, }: StepCommonProps & {
|
|
24
|
+
renderTravelerContactPicker?: (props: TravelerContactPickerProps) => React.ReactNode;
|
|
25
|
+
}): React.ReactElement;
|
|
26
|
+
export declare function AccommodationStep({ draft, setDraft, shape }: StepCommonProps): React.ReactElement;
|
|
27
|
+
export declare function AddonsStep({ draft, setDraft, shape }: StepCommonProps): React.ReactElement;
|
|
28
|
+
export declare function PaymentStep({ draft, setDraft, shape, capabilities, renderProviderStep, }: StepCommonProps & {
|
|
29
|
+
capabilities: PaymentProviderCapabilities;
|
|
30
|
+
renderProviderStep?: (props: PaymentProviderStepRenderProps) => React.ReactNode;
|
|
31
|
+
}): React.ReactElement;
|
|
32
|
+
export declare function ReviewStep({ draft, setDraft, isCommitting, onConfirm, renderExtras, surface, }: {
|
|
33
|
+
draft: Draft;
|
|
34
|
+
setDraft: (next: Draft) => void;
|
|
35
|
+
isCommitting: boolean;
|
|
36
|
+
onConfirm: () => void;
|
|
37
|
+
renderExtras?: () => React.ReactNode;
|
|
38
|
+
/**
|
|
39
|
+
* Drives the notes field. Public storefronts collect
|
|
40
|
+
* customer-facing "anything we should know?" notes; operator
|
|
41
|
+
* surfaces collect operator-only internal notes. Defaults to
|
|
42
|
+
* `admin` so existing operator usage stays unchanged.
|
|
43
|
+
*/
|
|
44
|
+
surface?: "admin" | "public";
|
|
45
|
+
}): React.ReactElement;
|
|
46
|
+
export {};
|
|
47
|
+
//# sourceMappingURL=journey-steps.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"journey-steps.d.ts","sourceRoot":"","sources":["../../../src/journey/components/journey-steps.tsx"],"names":[],"mappings":"AAEA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAA;AAYzE,OAAO,EACL,KAAK,KAAK,EASX,MAAM,uBAAuB,CAAA;AAC9B,OAAO,KAAK,EACV,sBAAsB,EACtB,2BAA2B,EAC3B,8BAA8B,EAC9B,0BAA0B,EAC3B,MAAM,aAAa,CAAA;AAEpB,UAAU,eAAe;IACvB,KAAK,EAAE,KAAK,CAAA;IACZ,QAAQ,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,IAAI,CAAA;IAC/B,KAAK,EAAE,iBAAiB,CAAA;CACzB;AAMD,wBAAgB,aAAa,CAAC,EAC5B,KAAK,EACL,QAAQ,EACR,KAAK,GACN,EAAE,eAAe,GAAG;IACnB,YAAY,CAAC,EAAE,MAAM,KAAK,CAAC,SAAS,CAAA;CACrC,GAAG,KAAK,CAAC,YAAY,CAYrB;AA+UD,wBAAgB,WAAW,CAAC,EAC1B,KAAK,EACL,QAAQ,EACR,uBAAuB,EACvB,YAAY,GACb,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;CACrC,GAAG,KAAK,CAAC,YAAY,CA8JrB;AAMD,wBAAgB,aAAa,CAAC,EAC5B,KAAK,EACL,QAAQ,EACR,KAAK,EACL,2BAA2B,GAC5B,EAAE,eAAe,GAAG;IACnB,2BAA2B,CAAC,EAAE,CAAC,KAAK,EAAE,0BAA0B,KAAK,KAAK,CAAC,SAAS,CAAA;CACrF,GAAG,KAAK,CAAC,YAAY,CAiFrB;AAmTD,wBAAgB,iBAAiB,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,eAAe,GAAG,KAAK,CAAC,YAAY,CAmGjG;AA6DD,wBAAgB,UAAU,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,eAAe,GAAG,KAAK,CAAC,YAAY,CA8C1F;AAyDD,wBAAgB,WAAW,CAAC,EAC1B,KAAK,EACL,QAAQ,EACR,KAAK,EACL,YAAY,EACZ,kBAAkB,GACnB,EAAE,eAAe,GAAG;IACnB,YAAY,EAAE,2BAA2B,CAAA;IACzC,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,8BAA8B,KAAK,KAAK,CAAC,SAAS,CAAA;CAChF,GAAG,KAAK,CAAC,YAAY,CAuFrB;AA8ED,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,YAAY,EACZ,SAAS,EACT,YAAY,EACZ,OAAO,GACR,EAAE;IACD,KAAK,EAAE,KAAK,CAAA;IACZ,QAAQ,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,IAAI,CAAA;IAC/B,YAAY,EAAE,OAAO,CAAA;IACrB,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,YAAY,CAAC,EAAE,MAAM,KAAK,CAAC,SAAS,CAAA;IACpC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAA;CAC7B,GAAG,KAAK,CAAC,YAAY,CA2DrB"}
|