@voyantjs/bookings-ui 0.80.18 → 0.81.1
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-create-dialog.d.ts +0 -18
- package/dist/components/booking-create-dialog.d.ts.map +1 -1
- package/dist/components/booking-create-dialog.js +89 -202
- package/dist/components/booking-create-utils.d.ts +1 -1
- package/dist/components/booking-create-utils.d.ts.map +1 -1
- package/dist/components/booking-create-utils.js +20 -8
- package/dist/components/booking-detail-page.d.ts.map +1 -1
- package/dist/components/booking-detail-page.js +3 -1
- package/dist/components/booking-payments-summary.d.ts +4 -1
- package/dist/components/booking-payments-summary.d.ts.map +1 -1
- package/dist/components/booking-payments-summary.js +21 -4
- package/dist/components/option-units-stepper-section.d.ts +4 -1
- package/dist/components/option-units-stepper-section.d.ts.map +1 -1
- package/dist/components/option-units-stepper-section.js +7 -2
- package/dist/components/travelers-section.d.ts +14 -5
- package/dist/components/travelers-section.d.ts.map +1 -1
- package/dist/components/travelers-section.js +83 -88
- package/dist/i18n/en.d.ts +2 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +2 -0
- package/dist/i18n/messages.d.ts +2 -0
- package/dist/i18n/messages.d.ts.map +1 -1
- package/dist/i18n/provider.d.ts +4 -0
- package/dist/i18n/provider.d.ts.map +1 -1
- package/dist/i18n/ro.d.ts +2 -0
- package/dist/i18n/ro.d.ts.map +1 -1
- package/dist/i18n/ro.js +2 -0
- package/package.json +32 -30
|
@@ -1,22 +1,4 @@
|
|
|
1
1
|
import { type BookingRecord } from "@voyantjs/bookings-react";
|
|
2
|
-
import { type OptionUnitsStepperUnit } from "./option-units-stepper-section.js";
|
|
3
|
-
/**
|
|
4
|
-
* Pick the unit for a traveler. Priorities:
|
|
5
|
-
* 1. If we have an age (from DOB) and it falls into a unit's
|
|
6
|
-
* `[minAge, maxAge]` window, use that unit.
|
|
7
|
-
* 2. Otherwise honor an explicit role hint (Child / Infant / Adult
|
|
8
|
-
* buttons) by mapping the hint to a representative age and
|
|
9
|
-
* matching the age band. This works for products whose units
|
|
10
|
-
* encode the band in the code (`child_0_5`, `child_6_12`) instead
|
|
11
|
-
* of bare `CHILD`/`INFANT`.
|
|
12
|
-
* 3. Fall back to code/name matching for legacy products that don't
|
|
13
|
-
* configure min/max ages.
|
|
14
|
-
*
|
|
15
|
-
* `roleHint` covers the common case where the operator knows the
|
|
16
|
-
* traveler is a child but doesn't have the exact DOB. Without it, a
|
|
17
|
-
* roleless traveler would silently default to Adult pricing.
|
|
18
|
-
*/
|
|
19
|
-
export declare function pickUnitForAge(units: OptionUnitsStepperUnit[], age: number | null, roleHint?: "adult" | "child" | "infant" | null): OptionUnitsStepperUnit | undefined;
|
|
20
2
|
export interface BookingCreateDialogProps {
|
|
21
3
|
open: boolean;
|
|
22
4
|
onOpenChange: (open: boolean) => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"booking-create-dialog.d.ts","sourceRoot":"","sources":["../../src/components/booking-create-dialog.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"booking-create-dialog.d.ts","sourceRoot":"","sources":["../../src/components/booking-create-dialog.tsx"],"names":[],"mappings":"AAeA,OAAO,EAKL,KAAK,aAAa,EAInB,MAAM,0BAA0B,CAAA;AA4PjC,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;IAC5C,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,sBAAsB;IACrC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;IAC5C,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,sEAAsE;IACtE,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,gBAAgB,EAChB,aAAa,GACd,EAAE,wBAAwB,2CAsB1B;AAED,wBAAgB,iBAAiB,CAAC,EAChC,SAAS,EACT,gBAAgB,EAChB,aAAa,EACb,OAAc,EACd,QAAQ,GACT,EAAE,sBAAsB,2CAm8BxB"}
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useQueries, useQuery } from "@tanstack/react-query";
|
|
4
4
|
import { getSlotQueryOptions, useSlots, useSlotUnitAvailability, useVoyantAvailabilityContext, } from "@voyantjs/availability-react";
|
|
5
|
-
import {
|
|
5
|
+
import { resolveBookingDraft, resolveBookingExtraLines, travelersToRows, } from "@voyantjs/bookings/pricing-assignment";
|
|
6
|
+
import { useBookingCreateMutation, useBookingTaxPreview, VoyantApiError, } from "@voyantjs/bookings-react";
|
|
6
7
|
import { useOrganization, usePerson } from "@voyantjs/crm-react";
|
|
7
8
|
import { useProductExtras } from "@voyantjs/extras-react";
|
|
8
9
|
import { useAddresses } from "@voyantjs/identity-react";
|
|
@@ -20,7 +21,7 @@ import { emptyPersonPickerValue, PersonPickerSection, } from "./person-picker-se
|
|
|
20
21
|
import { PriceBreakdownSection } from "./price-breakdown-section.js";
|
|
21
22
|
import { ProductPickerSection } from "./product-picker-section.js";
|
|
22
23
|
import { emptySharedRoomValue, SharedRoomSection, } from "./shared-room-section.js";
|
|
23
|
-
import {
|
|
24
|
+
import { emptyTravelerListValue, TravelersSection, } from "./travelers-section.js";
|
|
24
25
|
import { emptyVoucherPickerValue, VoucherPickerSection, } from "./voucher-picker-section.js";
|
|
25
26
|
function generateBookingNumber() {
|
|
26
27
|
const now = new Date();
|
|
@@ -117,186 +118,6 @@ function stripOptionPrefix(name) {
|
|
|
117
118
|
const idx = name.indexOf(" - ");
|
|
118
119
|
return idx > 0 ? name.slice(idx + 3) : name;
|
|
119
120
|
}
|
|
120
|
-
/**
|
|
121
|
-
* Pick the unit for a traveler. Priorities:
|
|
122
|
-
* 1. If we have an age (from DOB) and it falls into a unit's
|
|
123
|
-
* `[minAge, maxAge]` window, use that unit.
|
|
124
|
-
* 2. Otherwise honor an explicit role hint (Child / Infant / Adult
|
|
125
|
-
* buttons) by mapping the hint to a representative age and
|
|
126
|
-
* matching the age band. This works for products whose units
|
|
127
|
-
* encode the band in the code (`child_0_5`, `child_6_12`) instead
|
|
128
|
-
* of bare `CHILD`/`INFANT`.
|
|
129
|
-
* 3. Fall back to code/name matching for legacy products that don't
|
|
130
|
-
* configure min/max ages.
|
|
131
|
-
*
|
|
132
|
-
* `roleHint` covers the common case where the operator knows the
|
|
133
|
-
* traveler is a child but doesn't have the exact DOB. Without it, a
|
|
134
|
-
* roleless traveler would silently default to Adult pricing.
|
|
135
|
-
*/
|
|
136
|
-
export function pickUnitForAge(units, age, roleHint = null) {
|
|
137
|
-
if (units.length === 0)
|
|
138
|
-
return undefined;
|
|
139
|
-
const personUnits = units.filter((u) => u.unitType == null || u.unitType === "person");
|
|
140
|
-
const pool = personUnits.length > 0 ? personUnits : units;
|
|
141
|
-
const sorted = [...pool].sort((a, b) => (a.minAge ?? 0) - (b.minAge ?? 0));
|
|
142
|
-
const matchByAge = (target) => sorted.find((u) => (u.minAge == null || target >= u.minAge) && (u.maxAge == null || target <= u.maxAge));
|
|
143
|
-
if (age != null) {
|
|
144
|
-
const match = matchByAge(age);
|
|
145
|
-
if (match)
|
|
146
|
-
return match;
|
|
147
|
-
}
|
|
148
|
-
if (roleHint) {
|
|
149
|
-
const HINT_AGE = { adult: 30, child: 8, infant: 1 };
|
|
150
|
-
const hintAge = HINT_AGE[roleHint];
|
|
151
|
-
// Only consider units with at least one explicit age bound. Without
|
|
152
|
-
// this, legacy units with null min/max (just bare ADULT/CHILD codes)
|
|
153
|
-
// would all match every hint age and collapse onto the first sorted
|
|
154
|
-
// entry (almost always Adult). Code-matching below handles those.
|
|
155
|
-
const banded = sorted.filter((u) => u.minAge != null || u.maxAge != null);
|
|
156
|
-
const match = banded.find((u) => (u.minAge == null || hintAge >= u.minAge) && (u.maxAge == null || hintAge <= u.maxAge));
|
|
157
|
-
if (match)
|
|
158
|
-
return match;
|
|
159
|
-
}
|
|
160
|
-
const findByCode = (code) => sorted.find((u) => (u.unitCode ?? "").toUpperCase() === code) ??
|
|
161
|
-
sorted.find((u) => new RegExp(`\\b${code}\\b`, "i").test(u.unitName));
|
|
162
|
-
if (roleHint === "child")
|
|
163
|
-
return findByCode("CHILD") ?? sorted[0];
|
|
164
|
-
if (roleHint === "infant")
|
|
165
|
-
return findByCode("INFANT") ?? sorted[0];
|
|
166
|
-
return findByCode("ADULT") ?? sorted[sorted.length - 1] ?? sorted[0];
|
|
167
|
-
}
|
|
168
|
-
/**
|
|
169
|
-
* Take the operator-picked per-option quantities (which are tracked
|
|
170
|
-
* against each option's primary "Adult" unit by the stepper) plus the
|
|
171
|
-
* travelers list, and redistribute both so that:
|
|
172
|
-
* - each traveler's `roomUnitId` points at the age-banded unit
|
|
173
|
-
* matching their DOB (Adult / Child / Infant / etc.)
|
|
174
|
-
* - `quantities` reflects the per-unit counts after redistribution —
|
|
175
|
-
* a 3-pax "Standard double" with 2 adults + 1 child becomes
|
|
176
|
-
* `{ adultUnit: 2, childUnit: 1 }` instead of `{ adultUnit: 3 }`.
|
|
177
|
-
*
|
|
178
|
-
* Slots without a configured `dateOfBirth` keep the option's adult
|
|
179
|
-
* default so partially-filled bookings still typecheck.
|
|
180
|
-
*/
|
|
181
|
-
/**
|
|
182
|
-
* Rebuild stepper quantities from per-traveler unit assignments.
|
|
183
|
-
*
|
|
184
|
-
* Each traveler's `roomUnitId` is now the operator's explicit choice
|
|
185
|
-
* (DOB-pre-picked at attach, overridable via the dynamic category
|
|
186
|
-
* buttons), so we count assignments directly and add any per-option
|
|
187
|
-
* residual on the adult/primary unit when the stepper qty exceeds the
|
|
188
|
-
* number of travelers actually assigned. Unlike the older
|
|
189
|
-
* DOB-driven rewrite, this never moves a traveler off their chosen
|
|
190
|
-
* unit — operator selection always wins.
|
|
191
|
-
*/
|
|
192
|
-
function redistributeByAge(quantities, travelers, units) {
|
|
193
|
-
if (units.length === 0)
|
|
194
|
-
return { quantities, travelers };
|
|
195
|
-
const unitsByOption = new Map();
|
|
196
|
-
for (const unit of units) {
|
|
197
|
-
if (!unit.optionId)
|
|
198
|
-
continue;
|
|
199
|
-
const list = unitsByOption.get(unit.optionId);
|
|
200
|
-
if (list)
|
|
201
|
-
list.push(unit);
|
|
202
|
-
else
|
|
203
|
-
unitsByOption.set(unit.optionId, [unit]);
|
|
204
|
-
}
|
|
205
|
-
const unitToOption = new Map(units.map((u) => [u.optionUnitId, u.optionId]));
|
|
206
|
-
const unitById = new Map(units.map((u) => [u.optionUnitId, u]));
|
|
207
|
-
// Per-option total from the stepper. This is the count the operator
|
|
208
|
-
// committed to when picking rooms.
|
|
209
|
-
const totalByOption = new Map();
|
|
210
|
-
for (const [unitId, qty] of Object.entries(quantities)) {
|
|
211
|
-
if (qty <= 0)
|
|
212
|
-
continue;
|
|
213
|
-
const optionId = unitToOption.get(unitId);
|
|
214
|
-
if (!optionId)
|
|
215
|
-
continue;
|
|
216
|
-
totalByOption.set(optionId, (totalByOption.get(optionId) ?? 0) + qty);
|
|
217
|
-
}
|
|
218
|
-
const assignedForDefaulting = new Map();
|
|
219
|
-
for (const traveler of travelers) {
|
|
220
|
-
if (!traveler.roomUnitId)
|
|
221
|
-
continue;
|
|
222
|
-
const optionId = unitToOption.get(traveler.roomUnitId);
|
|
223
|
-
if (!optionId)
|
|
224
|
-
continue;
|
|
225
|
-
assignedForDefaulting.set(optionId, (assignedForDefaulting.get(optionId) ?? 0) + 1);
|
|
226
|
-
}
|
|
227
|
-
const optionDemand = Array.from(totalByOption.entries());
|
|
228
|
-
const nextTravelers = travelers.map((traveler) => {
|
|
229
|
-
if (traveler.roomUnitId && unitById.has(traveler.roomUnitId))
|
|
230
|
-
return traveler;
|
|
231
|
-
const optionId = optionDemand.find(([candidate, total]) => (assignedForDefaulting.get(candidate) ?? 0) < total)?.[0] ?? optionDemand[0]?.[0];
|
|
232
|
-
if (!optionId)
|
|
233
|
-
return traveler;
|
|
234
|
-
const age = computeAgeYears(traveler.dateOfBirth);
|
|
235
|
-
const roleHint = traveler.role === "adult" || traveler.role === "child" || traveler.role === "infant"
|
|
236
|
-
? traveler.role
|
|
237
|
-
: null;
|
|
238
|
-
const unit = pickUnitForAge(unitsByOption.get(optionId) ?? [], age, roleHint);
|
|
239
|
-
if (!unit)
|
|
240
|
-
return traveler;
|
|
241
|
-
assignedForDefaulting.set(optionId, (assignedForDefaulting.get(optionId) ?? 0) + 1);
|
|
242
|
-
return { ...traveler, roomUnitId: unit.optionUnitId };
|
|
243
|
-
});
|
|
244
|
-
// Count actual traveler assignments per unit + per option.
|
|
245
|
-
const next = {};
|
|
246
|
-
const assignedByOption = new Map();
|
|
247
|
-
for (const t of nextTravelers) {
|
|
248
|
-
if (!t.roomUnitId)
|
|
249
|
-
continue;
|
|
250
|
-
const optionId = unitToOption.get(t.roomUnitId);
|
|
251
|
-
if (!optionId)
|
|
252
|
-
continue;
|
|
253
|
-
next[t.roomUnitId] = (next[t.roomUnitId] ?? 0) + 1;
|
|
254
|
-
assignedByOption.set(optionId, (assignedByOption.get(optionId) ?? 0) + 1);
|
|
255
|
-
}
|
|
256
|
-
// Residual = operator picked N rooms but only added M travelers; put
|
|
257
|
-
// the leftover on the option's adult/primary unit so the price total
|
|
258
|
-
// matches the stepper.
|
|
259
|
-
for (const [optionId, total] of totalByOption) {
|
|
260
|
-
const assigned = assignedByOption.get(optionId) ?? 0;
|
|
261
|
-
const residual = Math.max(0, total - assigned);
|
|
262
|
-
if (residual === 0)
|
|
263
|
-
continue;
|
|
264
|
-
const adult = pickUnitForAge(unitsByOption.get(optionId) ?? [], null);
|
|
265
|
-
if (!adult)
|
|
266
|
-
continue;
|
|
267
|
-
next[adult.optionUnitId] = (next[adult.optionUnitId] ?? 0) + residual;
|
|
268
|
-
}
|
|
269
|
-
return { quantities: next, travelers: nextTravelers };
|
|
270
|
-
}
|
|
271
|
-
function travelersToRows(value) {
|
|
272
|
-
return value.travelers.map((traveler) => {
|
|
273
|
-
// Age-derived category (DOB-driven). The `role` field still
|
|
274
|
-
// carries the `lead` flag separately for the booking primary; the
|
|
275
|
-
// demographic category comes from age, not from a manual select.
|
|
276
|
-
const age = computeAgeYears(traveler.dateOfBirth);
|
|
277
|
-
const ageCategory = age == null
|
|
278
|
-
? traveler.role === "child" || traveler.role === "infant" || traveler.role === "adult"
|
|
279
|
-
? traveler.role
|
|
280
|
-
: null
|
|
281
|
-
: age < 2
|
|
282
|
-
? "infant"
|
|
283
|
-
: age < 18
|
|
284
|
-
? "child"
|
|
285
|
-
: "adult";
|
|
286
|
-
return {
|
|
287
|
-
personId: traveler.personId,
|
|
288
|
-
firstName: traveler.firstName.trim(),
|
|
289
|
-
lastName: traveler.lastName.trim(),
|
|
290
|
-
email: traveler.email.trim() || null,
|
|
291
|
-
phone: traveler.phone.trim() || null,
|
|
292
|
-
preferredLanguage: traveler.preferredLanguage.trim() || null,
|
|
293
|
-
participantType: "traveler",
|
|
294
|
-
travelerCategory: ageCategory,
|
|
295
|
-
isPrimary: traveler.role === "lead",
|
|
296
|
-
roomUnitId: traveler.roomUnitId,
|
|
297
|
-
};
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
121
|
function sameRoomUnits(left, right) {
|
|
301
122
|
if (left.length !== right.length)
|
|
302
123
|
return false;
|
|
@@ -310,6 +131,33 @@ function sameRoomUnits(left, right) {
|
|
|
310
131
|
unit.remaining === other.remaining);
|
|
311
132
|
});
|
|
312
133
|
}
|
|
134
|
+
function isPayloadResolverMismatchBody(body) {
|
|
135
|
+
if (typeof body !== "object" || body === null)
|
|
136
|
+
return false;
|
|
137
|
+
const candidate = body;
|
|
138
|
+
return (candidate.code === "payload_resolver_mismatch" &&
|
|
139
|
+
Array.isArray(candidate.mismatches) &&
|
|
140
|
+
candidate.mismatches.every((mismatch) => {
|
|
141
|
+
if (typeof mismatch !== "object" || mismatch === null)
|
|
142
|
+
return false;
|
|
143
|
+
const item = mismatch;
|
|
144
|
+
return ((item.kind === "qty" || item.kind === "missing" || item.kind === "extra") &&
|
|
145
|
+
typeof item.optionUnitId === "string" &&
|
|
146
|
+
typeof item.submittedQuantity === "number" &&
|
|
147
|
+
typeof item.resolvedQuantity === "number");
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
function formatPayloadResolverMismatchError(body, unitLabels) {
|
|
151
|
+
const details = body.mismatches
|
|
152
|
+
.map((mismatch) => {
|
|
153
|
+
const label = unitLabels[mismatch.optionUnitId] ?? mismatch.optionUnitId;
|
|
154
|
+
return `${label}: sent ${mismatch.submittedQuantity}, expected ${mismatch.resolvedQuantity}`;
|
|
155
|
+
})
|
|
156
|
+
.join("; ");
|
|
157
|
+
return details
|
|
158
|
+
? `Booking options are out of sync. Review these lines: ${details}.`
|
|
159
|
+
: "Booking options are out of sync. Review the selected traveler and option lines.";
|
|
160
|
+
}
|
|
313
161
|
/**
|
|
314
162
|
* Operator booking-create dialog. Composes the booking-create picker
|
|
315
163
|
* sections — product, departure, rooms, person, shared-room, travelers,
|
|
@@ -361,6 +209,7 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
|
|
|
361
209
|
// bundle. Defaults on so the operator opts out, not in.
|
|
362
210
|
const [notifyTraveler, setNotifyTraveler] = React.useState(true);
|
|
363
211
|
const [error, setError] = React.useState(null);
|
|
212
|
+
const [payloadMismatchUnitIds, setPayloadMismatchUnitIds] = React.useState([]);
|
|
364
213
|
const { formatDate } = useBookingsUiI18nOrDefault();
|
|
365
214
|
const messages = useBookingsUiMessagesOrDefault();
|
|
366
215
|
const availabilityClient = useVoyantAvailabilityContext();
|
|
@@ -392,6 +241,7 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
|
|
|
392
241
|
setCreateAsDraft(false);
|
|
393
242
|
setNotifyTraveler(true);
|
|
394
243
|
setError(null);
|
|
244
|
+
setPayloadMismatchUnitIds([]);
|
|
395
245
|
}
|
|
396
246
|
else if (resolvedDefaultProductId) {
|
|
397
247
|
setProduct((prev) => {
|
|
@@ -415,6 +265,7 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
|
|
|
415
265
|
setRoomUnits([]);
|
|
416
266
|
setSharedRoom(emptySharedRoomValue);
|
|
417
267
|
setExtraLines([]);
|
|
268
|
+
setPayloadMismatchUnitIds([]);
|
|
418
269
|
}, [product.productId, defaultSlotId]);
|
|
419
270
|
const [slotsFromIso, setSlotsFromIso] = React.useState(() => new Date().toISOString());
|
|
420
271
|
React.useEffect(() => {
|
|
@@ -443,6 +294,7 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
|
|
|
443
294
|
}, [slotsData?.data, slotsFromIso, product.optionId, allOpenSlots]);
|
|
444
295
|
const selectedSlot = React.useMemo(() => slots.find((slot) => slot.id === slotId) ?? (defaultSlot?.id === slotId ? defaultSlot : null), [slots, slotId, defaultSlot]);
|
|
445
296
|
const setSelectedSlot = React.useCallback((nextSlotId) => {
|
|
297
|
+
setPayloadMismatchUnitIds([]);
|
|
446
298
|
const selectedSlot = nextSlotId ? allOpenSlots.find((slot) => slot.id === nextSlotId) : null;
|
|
447
299
|
if (selectedSlot?.optionId && selectedSlot.optionId !== product.optionId) {
|
|
448
300
|
setProduct((prev) => ({ ...prev, optionId: selectedSlot.optionId }));
|
|
@@ -452,6 +304,7 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
|
|
|
452
304
|
React.useEffect(() => {
|
|
453
305
|
setRooms(emptyOptionUnitsStepperValue);
|
|
454
306
|
setRoomUnits([]);
|
|
307
|
+
setPayloadMismatchUnitIds([]);
|
|
455
308
|
if (!slotId || !product.optionId)
|
|
456
309
|
return;
|
|
457
310
|
const selectedDeparture = allOpenSlots.find((slot) => slot.id === slotId) ??
|
|
@@ -487,9 +340,9 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
|
|
|
487
340
|
// (Adult / Child / Senior). The age-band lives separately on the
|
|
488
341
|
// traveler and only affects pricing; both an adult and a child sit
|
|
489
342
|
// in the same Standard double room. Each entry's `unitId` is set to
|
|
490
|
-
// the option's primary unit so existing `roomUnitId`-keyed
|
|
491
|
-
//
|
|
492
|
-
//
|
|
343
|
+
// the option's primary unit so the existing `roomUnitId`-keyed
|
|
344
|
+
// plumbing keeps working — `resolveBookingDraft` moves the traveler
|
|
345
|
+
// to the matching age-banded unit at preview + submit.
|
|
493
346
|
const roomUnitOptions = React.useMemo(() => {
|
|
494
347
|
const units = getTravelerAssignableStepperUnits(roomUnits.length > 0 ? roomUnits : (slotUnitAvailability.data?.data ?? []));
|
|
495
348
|
if (units.length === 0)
|
|
@@ -577,12 +430,20 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
|
|
|
577
430
|
}
|
|
578
431
|
return Array.from(groups.values());
|
|
579
432
|
}, [roomUnits]);
|
|
580
|
-
// Apply the same
|
|
581
|
-
//
|
|
582
|
-
//
|
|
583
|
-
//
|
|
584
|
-
|
|
585
|
-
|
|
433
|
+
// Apply the same draft resolver we use at submit so live pricing
|
|
434
|
+
// and persisted item lines cannot drift. Person-priced options
|
|
435
|
+
// (excursions) derive line quantities from the traveler list;
|
|
436
|
+
// accommodation options preserve operator-picked stepper quantities.
|
|
437
|
+
const displayDraft = React.useMemo(() => resolveBookingDraft({
|
|
438
|
+
quantities: rooms.quantities,
|
|
439
|
+
travelers: travelers.travelers,
|
|
440
|
+
units: getTravelerAssignableStepperUnits(roomUnits),
|
|
441
|
+
}), [rooms.quantities, travelers.travelers, roomUnits]);
|
|
442
|
+
const displayQuantities = displayDraft.quantities;
|
|
443
|
+
const displayExtraLines = React.useMemo(() => resolveBookingExtraLines({
|
|
444
|
+
extraLines,
|
|
445
|
+
travelerCount: travelers.travelers.length,
|
|
446
|
+
}), [extraLines, travelers.travelers.length]);
|
|
586
447
|
// Currency placeholder — used for voucher + payment schedule display.
|
|
587
448
|
// Consumers hooking in real product data should override this by wrapping
|
|
588
449
|
// the component or swapping in their own currency-aware hook.
|
|
@@ -622,6 +483,7 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
|
|
|
622
483
|
const hasSelectedUnits = React.useMemo(() => Object.values(rooms.quantities).some((qty) => qty > 0), [rooms.quantities]);
|
|
623
484
|
const handleSubmit = async () => {
|
|
624
485
|
setError(null);
|
|
486
|
+
setPayloadMismatchUnitIds([]);
|
|
625
487
|
if (!product.productId) {
|
|
626
488
|
setError(messages.bookingCreateDialog.validation.selectProduct);
|
|
627
489
|
return;
|
|
@@ -683,17 +545,28 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
|
|
|
683
545
|
return;
|
|
684
546
|
}
|
|
685
547
|
const paymentSchedules = paymentScheduleToRows(paymentSchedule, pricingCurrency, confirmedSellAmountCents);
|
|
686
|
-
//
|
|
687
|
-
//
|
|
688
|
-
//
|
|
548
|
+
// Resolve the draft once, then derive every shape the wire
|
|
549
|
+
// format needs from the result. Person-priced options get
|
|
550
|
+
// per-band quantities (1 adult + 1 child + 1 infant, not
|
|
551
|
+
// "3 x Adult"); accommodation options keep operator-picked
|
|
552
|
+
// room quantities. Server gets `clientLineKey` + `travelerIndexes`
|
|
553
|
+
// on each line so it can write `booking_item_travelers` rows.
|
|
689
554
|
const submitUnits = roomUnits.length > 0
|
|
690
555
|
? getTravelerAssignableStepperUnits(roomUnits)
|
|
691
556
|
: getTravelerAssignableStepperUnits((slotUnitAvailability.data?.data ?? []).map((unit) => ({
|
|
692
557
|
...unit,
|
|
693
558
|
optionId: product.optionId,
|
|
694
559
|
})));
|
|
695
|
-
const redistributed =
|
|
696
|
-
|
|
560
|
+
const redistributed = resolveBookingDraft({
|
|
561
|
+
quantities: rooms.quantities,
|
|
562
|
+
travelers: travelers.travelers,
|
|
563
|
+
units: submitUnits,
|
|
564
|
+
});
|
|
565
|
+
const itemLines = itemLinesToRows(redistributed.quantities, submitUnits, pricing, redistributed.travelerIndexesByUnitId);
|
|
566
|
+
const resolvedExtraLines = resolveBookingExtraLines({
|
|
567
|
+
extraLines,
|
|
568
|
+
travelerCount: travelers.travelers.length,
|
|
569
|
+
});
|
|
697
570
|
const travelerRows = travelersToRows({ travelers: redistributed.travelers });
|
|
698
571
|
const voucherRedemption = voucher.picked && voucher.picked.remainingAmountCents != null
|
|
699
572
|
? {
|
|
@@ -773,7 +646,7 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
|
|
|
773
646
|
confirmedSellAmountCents,
|
|
774
647
|
priceOverrideReason: priceOverrideReason || null,
|
|
775
648
|
itemLines: itemLines.length > 0 ? itemLines : undefined,
|
|
776
|
-
extraLines:
|
|
649
|
+
extraLines: resolvedExtraLines.length > 0 ? resolvedExtraLines : undefined,
|
|
777
650
|
travelers: travelerRows.length > 0 ? travelerRows : undefined,
|
|
778
651
|
paymentSchedules: paymentSchedules.length > 0 ? paymentSchedules : undefined,
|
|
779
652
|
voucherRedemption,
|
|
@@ -792,15 +665,26 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
|
|
|
792
665
|
onCreated?.(booking);
|
|
793
666
|
}
|
|
794
667
|
catch (err) {
|
|
668
|
+
if (err instanceof VoyantApiError && isPayloadResolverMismatchBody(err.body)) {
|
|
669
|
+
setPayloadMismatchUnitIds(Array.from(new Set(err.body.mismatches.map((mismatch) => mismatch.optionUnitId))));
|
|
670
|
+
setError(formatPayloadResolverMismatchError(err.body, roomUnitLabels));
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
795
673
|
setError(err instanceof Error ? err.message : messages.bookingCreateDialog.validation.createFailed);
|
|
796
674
|
}
|
|
797
675
|
};
|
|
798
676
|
const isSubmitting = createBookingMutation.isPending;
|
|
799
|
-
return (_jsxs("div", { className: "grid min-h-0 flex-1 gap-6 lg:grid-cols-12", children: [_jsxs("div", { className: "flex min-h-0 min-w-0 flex-col lg:col-span-8", children: [_jsxs("div", { className: "flex flex-1 flex-col gap-4 overflow-y-auto px-1 pb-2", children: [_jsx(ProductPickerSection, { value: product, onChange:
|
|
677
|
+
return (_jsxs("div", { className: "grid min-h-0 flex-1 gap-6 lg:grid-cols-12", children: [_jsxs("div", { className: "flex min-h-0 min-w-0 flex-col lg:col-span-8", children: [_jsxs("div", { className: "flex flex-1 flex-col gap-4 overflow-y-auto px-1 pb-2", children: [_jsx(ProductPickerSection, { value: product, onChange: (next) => {
|
|
678
|
+
setPayloadMismatchUnitIds([]);
|
|
679
|
+
setProduct(next);
|
|
680
|
+
}, enabled: enabled, lockProduct: Boolean(defaultProductId || defaultSlotId), labels: {
|
|
800
681
|
optionNone: messages.bookingCreateDialog.labels.noSpecificOption,
|
|
801
|
-
}, showOptionPicker: false }), product.productId ? (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { children: messages.bookingCreateDialog.fields.departure }), _jsx(AsyncCombobox, { value: slotId, onChange: (v) => setSelectedSlot(v), items: slots, selectedItem: selectedSlot, getKey: (slot) => slot.id, getLabel: (slot) => formatSlotLabel(slot), placeholder: messages.bookingCreateDialog.placeholders.departure, emptyText: messages.bookingCreateDialog.placeholders.departureEmpty, triggerClassName: "w-full", disabled: Boolean(defaultSlotId), clearable: !defaultSlotId })] })) : null, product.productId && slotId ? (_jsx(OptionUnitsStepperSection, { value: rooms, onChange:
|
|
682
|
+
}, showOptionPicker: false }), product.productId ? (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { children: messages.bookingCreateDialog.fields.departure }), _jsx(AsyncCombobox, { value: slotId, onChange: (v) => setSelectedSlot(v), items: slots, selectedItem: selectedSlot, getKey: (slot) => slot.id, getLabel: (slot) => formatSlotLabel(slot), placeholder: messages.bookingCreateDialog.placeholders.departure, emptyText: messages.bookingCreateDialog.placeholders.departureEmpty, triggerClassName: "w-full", disabled: Boolean(defaultSlotId), clearable: !defaultSlotId })] })) : null, product.productId && slotId ? (_jsx(OptionUnitsStepperSection, { value: rooms, onChange: (next) => {
|
|
683
|
+
setPayloadMismatchUnitIds([]);
|
|
684
|
+
setRooms(next);
|
|
685
|
+
}, productId: product.productId, slotId: slotId, optionId: product.optionId, enabled: enabled, onUnitsChange: handleRoomUnitsChange, slotHasFiniteCapacity: Boolean(selectedSlot) &&
|
|
802
686
|
!selectedSlot?.unlimited &&
|
|
803
|
-
typeof selectedSlot?.remainingPax === "number", labels: {
|
|
687
|
+
typeof selectedSlot?.remainingPax === "number", invalidOptionUnitIds: payloadMismatchUnitIds, labels: {
|
|
804
688
|
heading: messages.bookingCreateDialog.labels.roomsHeading,
|
|
805
689
|
noOption: messages.bookingCreateDialog.labels.roomsNoOption,
|
|
806
690
|
noSlot: messages.bookingCreateDialog.labels.roomsNoSlot,
|
|
@@ -825,7 +709,10 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
|
|
|
825
709
|
noGroups: messages.bookingCreateDialog.labels.sharedRoomNoGroups,
|
|
826
710
|
createHint: messages.bookingCreateDialog.labels.sharedRoomCreateHint,
|
|
827
711
|
remove: messages.bookingCreateDialog.labels.sharedRoomRemove,
|
|
828
|
-
} })) : null, product.productId && slotId ? (_jsx(TravelersSection, { value: travelers, onChange:
|
|
712
|
+
} })) : null, product.productId && slotId ? (_jsx(TravelersSection, { value: travelers, onChange: (next) => {
|
|
713
|
+
setPayloadMismatchUnitIds([]);
|
|
714
|
+
setTravelers(next);
|
|
715
|
+
}, roomUnits: roomUnitOptions.length > 0 ? roomUnitOptions : undefined, roomGroups: roomGroups.length > 0 ? roomGroups : undefined, billingPersonId: (person.billTo ?? "person") === "person" ? person.personId : null, labels: {
|
|
829
716
|
heading: messages.bookingCreateDialog.labels.travelerHeading,
|
|
830
717
|
addTraveler: messages.bookingCreateDialog.labels.addTraveler,
|
|
831
718
|
person: messages.bookingCreateDialog.labels.travelerPerson,
|
|
@@ -858,7 +745,7 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
|
|
|
858
745
|
: messages.bookingCreateDialog.actions.createAwaitingPaymentBooking] })] })] }), _jsxs("div", { className: "flex flex-col gap-4 lg:col-span-4", children: [_jsx(BookingPreviewCard, { productId: product.productId, optionId: product.optionId, slotId: slotId, slotLabel: (() => {
|
|
859
746
|
const slot = slots.find((s) => s.id === slotId);
|
|
860
747
|
return slot ? formatSlotLabel(slot) : null;
|
|
861
|
-
})(), unitQuantities: displayQuantities, unitLabels: roomUnitLabels, extraLines:
|
|
748
|
+
})(), unitQuantities: displayQuantities, unitLabels: roomUnitLabels, extraLines: displayExtraLines, travelers: travelers.travelers, messages: messages, onPricingChange: setPricing }), product.productId && slotId ? (_jsx(VoucherPickerSection, { value: voucher, onChange: setVoucher, currency: currency, labels: {
|
|
862
749
|
heading: messages.bookingCreateDialog.labels.voucherHeading,
|
|
863
750
|
codePlaceholder: messages.bookingCreateDialog.labels.voucherCodePlaceholder,
|
|
864
751
|
apply: messages.bookingCreateDialog.labels.voucherApply,
|
|
@@ -42,7 +42,7 @@ export declare function getBookableDepartureSlots<TSlot extends DepartureSlotSea
|
|
|
42
42
|
nowIso: string;
|
|
43
43
|
optionId: string | null;
|
|
44
44
|
}): TSlot[];
|
|
45
|
-
export declare function itemLinesToRows(quantities: Record<string, number>, units: BookingCreateUnitLineRecord[], pricing: BookingCreatePricingRecord | null): BookingCreateItemLineInput[];
|
|
45
|
+
export declare function itemLinesToRows(quantities: Record<string, number>, units: BookingCreateUnitLineRecord[], pricing: BookingCreatePricingRecord | null, travelerIndexesByUnitId?: Record<string, number[]>): BookingCreateItemLineInput[];
|
|
46
46
|
export declare function getSelectedSharedRoomUnitId(quantities: Record<string, number>): string | null;
|
|
47
47
|
export {};
|
|
48
48
|
//# sourceMappingURL=booking-create-utils.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"booking-create-utils.d.ts","sourceRoot":"","sources":["../../src/components/booking-create-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,0BAA0B,CAAA;AAC1E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAE7D,MAAM,MAAM,yBAAyB,GAAG,IAAI,CAAC,aAAa,EAAE,aAAa,GAAG,MAAM,GAAG,cAAc,CAAC,CAAA;AAEpG,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,MAAM,CAAA;IACV,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,8BAA8B;IAC7C,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;CAChC;AAED,MAAM,WAAW,0BAA0B;IACzC,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAA;IACnC,KAAK,EAAE,8BAA8B,EAAE,CAAA;CACxC;AAED,KAAK,4BAA4B,GAC7B,QAAQ,GACR,OAAO,GACP,MAAM,GACN,SAAS,GACT,SAAS,GACT,OAAO,GACP,IAAI,CAAA;AAER,MAAM,WAAW,yCAAyC;IACxD,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,4BAA4B,CAAA;CACxC;AAED,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMhE;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAIjG;AAED,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,yBAAyB,GAAG,IAAI,GAAG,SAAS,EACrD,KAAK,EAAE,MAAM,GACZ,OAAO,CAKT;AAED,MAAM,MAAM,oCAAoC,GAAG,OAAO,GAAG,iBAAiB,GAAG,eAAe,CAAA;AAEhG,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAI5E;AAED,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,GAC3E,oCAAoC,CAOtC;AAED,wBAAgB,iCAAiC,CAC/C,KAAK,SAAS,yCAAyC,EACvD,KAAK,EAAE,SAAS,KAAK,EAAE,GAAG,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"booking-create-utils.d.ts","sourceRoot":"","sources":["../../src/components/booking-create-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,0BAA0B,CAAA;AAC1E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAE7D,MAAM,MAAM,yBAAyB,GAAG,IAAI,CAAC,aAAa,EAAE,aAAa,GAAG,MAAM,GAAG,cAAc,CAAC,CAAA;AAEpG,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,MAAM,CAAA;IACV,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,8BAA8B;IAC7C,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;CAChC;AAED,MAAM,WAAW,0BAA0B;IACzC,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAA;IACnC,KAAK,EAAE,8BAA8B,EAAE,CAAA;CACxC;AAED,KAAK,4BAA4B,GAC7B,QAAQ,GACR,OAAO,GACP,MAAM,GACN,SAAS,GACT,SAAS,GACT,OAAO,GACP,IAAI,CAAA;AAER,MAAM,WAAW,yCAAyC;IACxD,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,4BAA4B,CAAA;CACxC;AAED,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMhE;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAIjG;AAED,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,yBAAyB,GAAG,IAAI,GAAG,SAAS,EACrD,KAAK,EAAE,MAAM,GACZ,OAAO,CAKT;AAED,MAAM,MAAM,oCAAoC,GAAG,OAAO,GAAG,iBAAiB,GAAG,eAAe,CAAA;AAEhG,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAI5E;AAED,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,GAC3E,oCAAoC,CAOtC;AAED,wBAAgB,iCAAiC,CAC/C,KAAK,SAAS,yCAAyC,EACvD,KAAK,EAAE,SAAS,KAAK,EAAE,GAAG,KAAK,EAAE,CAoBlC;AAED,wBAAgB,yBAAyB,CAAC,KAAK,SAAS,yBAAyB,EAC/E,KAAK,EAAE,SAAS,KAAK,EAAE,EACvB,OAAO,EAAE;IACP,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB,GACA,KAAK,EAAE,CAST;AAED,wBAAgB,eAAe,CAC7B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAClC,KAAK,EAAE,2BAA2B,EAAE,EACpC,OAAO,EAAE,0BAA0B,GAAG,IAAI,EAC1C,uBAAuB,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAM,GACrD,0BAA0B,EAAE,CA+C9B;AAED,wBAAgB,2BAA2B,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,GAAG,IAAI,CAE7F"}
|
|
@@ -32,20 +32,26 @@ export function validateBillingPersonContact(contact) {
|
|
|
32
32
|
return "valid";
|
|
33
33
|
}
|
|
34
34
|
export function getTravelerAssignableStepperUnits(units) {
|
|
35
|
-
|
|
35
|
+
// "Inventory" units (rooms, vehicles) are containers a traveler is
|
|
36
|
+
// placed into. When an option configures one, person-typed
|
|
37
|
+
// pricing-tier units on the same option are hidden from the
|
|
38
|
+
// stepper since their pricing folds into the container's per-pax
|
|
39
|
+
// accounting at submit time.
|
|
40
|
+
const isInventoryType = (unit) => unit.unitType === "room" || unit.unitType === "vehicle";
|
|
41
|
+
const hasInventoryByOption = new Map();
|
|
36
42
|
for (const unit of units) {
|
|
37
43
|
const optionKey = unit.optionId ?? unit.optionUnitId;
|
|
38
|
-
if (unit
|
|
39
|
-
|
|
40
|
-
else if (!
|
|
41
|
-
|
|
44
|
+
if (isInventoryType(unit))
|
|
45
|
+
hasInventoryByOption.set(optionKey, true);
|
|
46
|
+
else if (!hasInventoryByOption.has(optionKey))
|
|
47
|
+
hasInventoryByOption.set(optionKey, false);
|
|
42
48
|
}
|
|
43
49
|
return units.filter((unit) => {
|
|
44
|
-
if (unit
|
|
50
|
+
if (isInventoryType(unit))
|
|
45
51
|
return true;
|
|
46
52
|
if (unit.unitType !== "person")
|
|
47
53
|
return false;
|
|
48
|
-
return !
|
|
54
|
+
return !hasInventoryByOption.get(unit.optionId ?? unit.optionUnitId);
|
|
49
55
|
});
|
|
50
56
|
}
|
|
51
57
|
export function getBookableDepartureSlots(slots, options) {
|
|
@@ -59,7 +65,7 @@ export function getBookableDepartureSlots(slots, options) {
|
|
|
59
65
|
})
|
|
60
66
|
.sort((left, right) => left.startsAt.localeCompare(right.startsAt));
|
|
61
67
|
}
|
|
62
|
-
export function itemLinesToRows(quantities, units, pricing) {
|
|
68
|
+
export function itemLinesToRows(quantities, units, pricing, travelerIndexesByUnitId = {}) {
|
|
63
69
|
const unitsById = new Map(units.map((unit) => [unit.optionUnitId, unit]));
|
|
64
70
|
const unitNames = new Map(units.map((unit) => [unit.optionUnitId, unit.unitName]));
|
|
65
71
|
const pricedLines = new Map((pricing?.lines ?? []).map((line) => [line.unitId, line]));
|
|
@@ -86,13 +92,19 @@ export function itemLinesToRows(quantities, units, pricing) {
|
|
|
86
92
|
}
|
|
87
93
|
const unitSellAmountCents = pricedLine?.unitAmountCents ??
|
|
88
94
|
(totalSellAmountCents != null ? Math.floor(totalSellAmountCents / quantity) : null);
|
|
95
|
+
const travelerIndexes = travelerIndexesByUnitId[optionUnitId];
|
|
89
96
|
return {
|
|
97
|
+
// Server uses `clientLineKey` to look up this item after insert
|
|
98
|
+
// and link it to travelers via `booking_item_travelers`. Only
|
|
99
|
+
// stamp when there's an actual traveler mapping to write.
|
|
100
|
+
clientLineKey: travelerIndexes?.length ? `unit:${optionUnitId}` : undefined,
|
|
90
101
|
optionId: unitsById.get(optionUnitId)?.optionId ?? null,
|
|
91
102
|
optionUnitId,
|
|
92
103
|
quantity,
|
|
93
104
|
title: pricedLine?.label ?? unitNames.get(optionUnitId) ?? null,
|
|
94
105
|
unitSellAmountCents,
|
|
95
106
|
totalSellAmountCents,
|
|
107
|
+
travelerIndexes: travelerIndexes?.length ? travelerIndexes : undefined,
|
|
96
108
|
};
|
|
97
109
|
});
|
|
98
110
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"booking-detail-page.d.ts","sourceRoot":"","sources":["../../src/components/booking-detail-page.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,aAAa,EAInB,MAAM,0BAA0B,CAAA;
|
|
1
|
+
{"version":3,"file":"booking-detail-page.d.ts","sourceRoot":"","sources":["../../src/components/booking-detail-page.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,aAAa,EAInB,MAAM,0BAA0B,CAAA;AAgCjC,OAAO,EAAE,KAAK,SAAS,EAAY,MAAM,OAAO,CAAA;AAkBhD;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,6EAA6E;IAC7E,OAAO,EAAE,SAAS,GAAG,CAAC,CAAC,OAAO,EAAE,aAAa,KAAK,SAAS,CAAC,CAAA;CAC7D;AAED,MAAM,WAAW,sBAAsB;IACrC,2DAA2D;IAC3D,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,SAAS,CAAA;IAC9C,sDAAsD;IACtD,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,SAAS,CAAA;IACpD,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,SAAS,CAAA;IACrD,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,SAAS,CAAA;IACnD,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,SAAS,CAAA;IACtD,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,SAAS,CAAA;IACpD,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,SAAS,CAAA;IAClD,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,SAAS,CAAA;IACjD,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,SAAS,CAAA;IACnD,wEAAwE;IACxE,WAAW,CAAC,EAAE,oBAAoB,CAAA;IAClC,wDAAwD;IACxD,SAAS,CAAC,EAAE,oBAAoB,CAAA;CACjC;AAED,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;gEAC4D;IAC5D,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;IACzC,kBAAkB,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,IAAI,CAAA;IACrD,6EAA6E;IAC7E,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;IACnD,wEAAwE;IACxE,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;IAClD,KAAK,CAAC,EAAE,sBAAsB,CAAA;CAC/B;AAED,wBAAgB,iBAAiB,CAAC,EAChC,EAAE,EACF,SAAS,EACT,MAAM,EACN,cAAc,EACd,MAAM,EACN,YAAY,EACZ,kBAAkB,EAClB,gBAAgB,EAChB,eAAe,EACf,KAAK,GACN,EAAE,sBAAsB,2CAiSxB;AASD;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,aAAa,CAAA;CAAE,2CA8DhF"}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { bookingStatusBadgeVariant, useBooking, useBookingMutation, } from "@voyantjs/bookings-react";
|
|
4
4
|
import { useOrganization, usePerson } from "@voyantjs/crm-react";
|
|
5
|
+
import { useInvoiceMutation } from "@voyantjs/finance-react";
|
|
5
6
|
import { Badge, Button, Card, CardContent, CardHeader, CardTitle, cn, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@voyantjs/ui/components";
|
|
6
7
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@voyantjs/ui/components/tabs";
|
|
7
8
|
import { Ban, Calendar, ChevronRight, CreditCard, Mail, MapPin, MoreHorizontal, Pencil, Phone, RefreshCw, Trash2, Users, } from "lucide-react";
|
|
@@ -31,6 +32,7 @@ export function BookingDetailPage({ id, className, locale, hideBreadcrumb, onBac
|
|
|
31
32
|
const [cancelDialogOpen, setCancelDialogOpen] = useState(false);
|
|
32
33
|
const { data: bookingData, isPending } = useBooking(id);
|
|
33
34
|
const { remove } = useBookingMutation();
|
|
35
|
+
const { convertToInvoice } = useInvoiceMutation();
|
|
34
36
|
if (isPending) {
|
|
35
37
|
return (_jsx("div", { className: cn("flex items-center justify-center py-12", className), children: _jsx("p", { className: "text-sm text-muted-foreground", children: messages.common.loading }) }));
|
|
36
38
|
}
|
|
@@ -49,7 +51,7 @@ export function BookingDetailPage({ id, className, locale, hideBreadcrumb, onBac
|
|
|
49
51
|
}
|
|
50
52
|
}, children: [_jsx(Trash2, { className: "h-4 w-4", "aria-hidden": "true" }), detailMessages.deleteAction] })] })] }), slots?.header?.(booking), _jsx(Card, { children: _jsxs(CardContent, { className: "grid grid-cols-2 gap-6 py-6 sm:grid-cols-4", children: [_jsx(SummaryStat, { label: detailMessages.summarySell, value: formatAmount(booking.sellAmountCents, booking.sellCurrency, resolvedLocale, detailMessages.noValue), hint: sellHint }), _jsx(SummaryStat, { label: detailMessages.summaryCostMargin, value: formatAmount(booking.costAmountCents, booking.sellCurrency, resolvedLocale, detailMessages.noValue), hint: formatMargin(booking.marginPercent, detailMessages.noValue) }), _jsx(SummaryStat, { label: detailMessages.summaryDates, value: booking.startDate
|
|
51
53
|
? `${formatDate(booking.startDate, resolvedLocale, detailMessages.noValue)} - ${formatDate(booking.endDate, resolvedLocale, detailMessages.noValue)}`
|
|
52
|
-
: detailMessages.tbd, icon: _jsx(Calendar, { className: "h-3.5 w-3.5", "aria-hidden": "true" }) }), _jsx(SummaryStat, { label: detailMessages.summaryTravelers, value: booking.pax != null ? String(booking.pax) : detailMessages.noValue, icon: _jsx(Users, { className: "h-3.5 w-3.5", "aria-hidden": "true" }) }), booking.personId ? (_jsx(SummaryPersonLink, { label: detailMessages.summaryPerson, personId: booking.personId, onOpen: onPersonOpen })) : null, booking.organizationId ? (_jsx(SummaryOrganizationLink, { label: detailMessages.summaryOrganization, organizationId: booking.organizationId, onOpen: onOrganizationOpen })) : null, _jsx(SummaryStat, { label: detailMessages.summaryCreated, value: formatDate(booking.createdAt, resolvedLocale, detailMessages.noValue) }), _jsx(SummaryStat, { label: detailMessages.summaryUpdated, value: formatDate(booking.updatedAt, resolvedLocale, detailMessages.noValue) })] }) }), slots?.afterSummary?.(booking), _jsxs(Tabs, { defaultValue: "overview", children: [_jsxs(TabsList, { className: "w-full justify-start", children: [_jsx(TabsTrigger, { value: "overview", children: detailMessages.tabOverview }), _jsx(TabsTrigger, { value: "travelers", children: detailMessages.tabTravelers }), _jsx(TabsTrigger, { value: "finance", children: detailMessages.tabFinance }), slots?.invoicesTab ? (_jsx(TabsTrigger, { value: "invoices", children: slots.invoicesTab.label ?? detailMessages.tabInvoices })) : null, _jsx(TabsTrigger, { value: "suppliers", children: detailMessages.tabSuppliers }), _jsx(TabsTrigger, { value: "documents", children: detailMessages.tabDocuments }), _jsx(TabsTrigger, { value: "activity", children: detailMessages.tabActivity }), slots?.ledgerTab ? (_jsx(TabsTrigger, { value: "ledger", children: slots.ledgerTab.label ?? detailMessages.tabLedger })) : null] }), _jsxs(TabsContent, { value: "overview", className: "mt-4 flex flex-col gap-6", children: [slots?.overviewStart?.(booking), _jsx(BookingItemList, { bookingId: id }), _jsx(BookingGroupSection, { bookingId: id }), visibleInternalNotes(booking.internalNotes) ? (_jsx(Card, { children: _jsxs(CardContent, { className: "py-5", children: [_jsx("p", { className: "mb-1 text-xs font-medium text-muted-foreground", children: detailMessages.internalNotesLabel }), _jsx("p", { className: "whitespace-pre-wrap text-sm", children: visibleInternalNotes(booking.internalNotes) })] }) })) : null, slots?.overviewEnd?.(booking)] }), _jsxs(TabsContent, { value: "travelers", className: "mt-4 flex flex-col gap-6", children: [slots?.travelersStart?.(booking), _jsx(BookingBillingContextCard, { booking: booking }), _jsx(TravelerList, { bookingId: id, autoReveal: true })] }), _jsxs(TabsContent, { value: "finance", className: "mt-4 flex flex-col gap-6", children: [onCollectPayment || onRecordPayment ? (_jsxs("div", { className: "flex items-center justify-end gap-2", children: [onRecordPayment ? (_jsx(Button, { variant: "outline", onClick: () => onRecordPayment(booking), children: detailMessages.recordPaymentAction })) : null, onCollectPayment ? (_jsx(Button, { onClick: () => onCollectPayment(booking), children: detailMessages.collectPaymentAction })) : null] })) : null, slots?.financeStart?.(booking), _jsx(BookingPaymentReconciliationBanner, { bookingId: id }), _jsx(BookingPaymentsSummary, { bookingId: id, variant: "admin" }), _jsx(BookingPaymentScheduleList, { bookingId: id }), _jsx(BookingGuaranteeList, { bookingId: id }), slots?.financeEnd?.(booking)] }), slots?.invoicesTab ? (_jsx(TabsContent, { value: "invoices", className: "mt-4 flex flex-col gap-6", children: renderTabSlot(slots.invoicesTab.content, booking) })) : null, _jsx(TabsContent, { value: "suppliers", className: "mt-4", children: _jsx(SupplierStatusList, { bookingId: id }) }), _jsx(TabsContent, { value: "documents", className: "mt-4 flex flex-col gap-4", children: slots?.documents ? (slots.documents(booking)) : (_jsx("p", { className: "rounded-md border border-dashed p-4 text-sm text-muted-foreground", children: detailMessages.documentsSlotEmpty })) }), _jsxs(TabsContent, { value: "activity", className: "mt-4 flex flex-col gap-6", children: [_jsx(BookingActivityTimeline, { bookingId: id }), _jsx(BookingNotes, { bookingId: id }), slots?.activityEnd?.(booking)] }), slots?.ledgerTab ? (_jsx(TabsContent, { value: "ledger", className: "mt-4 flex flex-col gap-6", children: renderTabSlot(slots.ledgerTab.content, booking) })) : null] }), _jsx(BookingDialog, { open: editOpen, onOpenChange: setEditOpen, booking: booking }), _jsx(StatusChangeDialog, { open: statusDialogOpen, onOpenChange: setStatusDialogOpen, bookingId: id, currentStatus: booking.status }), _jsx(BookingCancellationDialog, { open: cancelDialogOpen, onOpenChange: setCancelDialogOpen, booking: booking })] }));
|
|
54
|
+
: detailMessages.tbd, icon: _jsx(Calendar, { className: "h-3.5 w-3.5", "aria-hidden": "true" }) }), _jsx(SummaryStat, { label: detailMessages.summaryTravelers, value: booking.pax != null ? String(booking.pax) : detailMessages.noValue, icon: _jsx(Users, { className: "h-3.5 w-3.5", "aria-hidden": "true" }) }), booking.personId ? (_jsx(SummaryPersonLink, { label: detailMessages.summaryPerson, personId: booking.personId, onOpen: onPersonOpen })) : null, booking.organizationId ? (_jsx(SummaryOrganizationLink, { label: detailMessages.summaryOrganization, organizationId: booking.organizationId, onOpen: onOrganizationOpen })) : null, _jsx(SummaryStat, { label: detailMessages.summaryCreated, value: formatDate(booking.createdAt, resolvedLocale, detailMessages.noValue) }), _jsx(SummaryStat, { label: detailMessages.summaryUpdated, value: formatDate(booking.updatedAt, resolvedLocale, detailMessages.noValue) })] }) }), slots?.afterSummary?.(booking), _jsxs(Tabs, { defaultValue: "overview", children: [_jsxs(TabsList, { className: "w-full justify-start", children: [_jsx(TabsTrigger, { value: "overview", children: detailMessages.tabOverview }), _jsx(TabsTrigger, { value: "travelers", children: detailMessages.tabTravelers }), _jsx(TabsTrigger, { value: "finance", children: detailMessages.tabFinance }), slots?.invoicesTab ? (_jsx(TabsTrigger, { value: "invoices", children: slots.invoicesTab.label ?? detailMessages.tabInvoices })) : null, _jsx(TabsTrigger, { value: "suppliers", children: detailMessages.tabSuppliers }), _jsx(TabsTrigger, { value: "documents", children: detailMessages.tabDocuments }), _jsx(TabsTrigger, { value: "activity", children: detailMessages.tabActivity }), slots?.ledgerTab ? (_jsx(TabsTrigger, { value: "ledger", children: slots.ledgerTab.label ?? detailMessages.tabLedger })) : null] }), _jsxs(TabsContent, { value: "overview", className: "mt-4 flex flex-col gap-6", children: [slots?.overviewStart?.(booking), _jsx(BookingItemList, { bookingId: id }), _jsx(BookingGroupSection, { bookingId: id }), visibleInternalNotes(booking.internalNotes) ? (_jsx(Card, { children: _jsxs(CardContent, { className: "py-5", children: [_jsx("p", { className: "mb-1 text-xs font-medium text-muted-foreground", children: detailMessages.internalNotesLabel }), _jsx("p", { className: "whitespace-pre-wrap text-sm", children: visibleInternalNotes(booking.internalNotes) })] }) })) : null, slots?.overviewEnd?.(booking)] }), _jsxs(TabsContent, { value: "travelers", className: "mt-4 flex flex-col gap-6", children: [slots?.travelersStart?.(booking), _jsx(BookingBillingContextCard, { booking: booking }), _jsx(TravelerList, { bookingId: id, autoReveal: true })] }), _jsxs(TabsContent, { value: "finance", className: "mt-4 flex flex-col gap-6", children: [onCollectPayment || onRecordPayment ? (_jsxs("div", { className: "flex items-center justify-end gap-2", children: [onRecordPayment ? (_jsx(Button, { variant: "outline", onClick: () => onRecordPayment(booking), children: detailMessages.recordPaymentAction })) : null, onCollectPayment ? (_jsx(Button, { onClick: () => onCollectPayment(booking), children: detailMessages.collectPaymentAction })) : null] })) : null, slots?.financeStart?.(booking), _jsx(BookingPaymentReconciliationBanner, { bookingId: id }), _jsx(BookingPaymentsSummary, { bookingId: id, variant: "admin", onConvertProforma: (row) => convertToInvoice.mutateAsync({ id: row.invoiceId }) }), _jsx(BookingPaymentScheduleList, { bookingId: id }), _jsx(BookingGuaranteeList, { bookingId: id }), slots?.financeEnd?.(booking)] }), slots?.invoicesTab ? (_jsx(TabsContent, { value: "invoices", className: "mt-4 flex flex-col gap-6", children: renderTabSlot(slots.invoicesTab.content, booking) })) : null, _jsx(TabsContent, { value: "suppliers", className: "mt-4", children: _jsx(SupplierStatusList, { bookingId: id }) }), _jsx(TabsContent, { value: "documents", className: "mt-4 flex flex-col gap-4", children: slots?.documents ? (slots.documents(booking)) : (_jsx("p", { className: "rounded-md border border-dashed p-4 text-sm text-muted-foreground", children: detailMessages.documentsSlotEmpty })) }), _jsxs(TabsContent, { value: "activity", className: "mt-4 flex flex-col gap-6", children: [_jsx(BookingActivityTimeline, { bookingId: id }), _jsx(BookingNotes, { bookingId: id }), slots?.activityEnd?.(booking)] }), slots?.ledgerTab ? (_jsx(TabsContent, { value: "ledger", className: "mt-4 flex flex-col gap-6", children: renderTabSlot(slots.ledgerTab.content, booking) })) : null] }), _jsx(BookingDialog, { open: editOpen, onOpenChange: setEditOpen, booking: booking }), _jsx(StatusChangeDialog, { open: statusDialogOpen, onOpenChange: setStatusDialogOpen, bookingId: id, currentStatus: booking.status }), _jsx(BookingCancellationDialog, { open: cancelDialogOpen, onOpenChange: setCancelDialogOpen, booking: booking })] }));
|
|
53
55
|
}
|
|
54
56
|
function renderTabSlot(content, booking) {
|
|
55
57
|
return typeof content === "function" ? content(booking) : content;
|
|
@@ -2,6 +2,7 @@ export interface BookingPaymentsSummaryRow {
|
|
|
2
2
|
id: string;
|
|
3
3
|
invoiceId: string;
|
|
4
4
|
invoiceNumber: string;
|
|
5
|
+
invoiceType?: "invoice" | "proforma" | "credit_note";
|
|
5
6
|
amountCents: number;
|
|
6
7
|
currency: string;
|
|
7
8
|
status: string;
|
|
@@ -32,6 +33,8 @@ export interface BookingPaymentsSummaryProps {
|
|
|
32
33
|
* on menu items, so this is a click handler rather than an href.
|
|
33
34
|
*/
|
|
34
35
|
onViewPayment?: (row: BookingPaymentsSummaryRow) => void;
|
|
36
|
+
/** Convert the row's proforma invoice into a final invoice. */
|
|
37
|
+
onConvertProforma?: (row: BookingPaymentsSummaryRow) => Promise<unknown> | unknown;
|
|
35
38
|
/** Edit handler — typically opens a dialog pre-filled with the row. */
|
|
36
39
|
onEditPayment?: (row: BookingPaymentsSummaryRow) => void;
|
|
37
40
|
/**
|
|
@@ -60,5 +63,5 @@ export interface BookingPaymentsSummaryProps {
|
|
|
60
63
|
* first as the primary identifier — that's the difference between
|
|
61
64
|
* "list of payments" and "list of invoice line-items".
|
|
62
65
|
*/
|
|
63
|
-
export declare function BookingPaymentsSummary({ bookingId, variant, getInvoiceHref, onViewPayment, onEditPayment, onDeletePayment, }: BookingPaymentsSummaryProps): import("react/jsx-runtime").JSX.Element;
|
|
66
|
+
export declare function BookingPaymentsSummary({ bookingId, variant, getInvoiceHref, onViewPayment, onConvertProforma, onEditPayment, onDeletePayment, }: BookingPaymentsSummaryProps): import("react/jsx-runtime").JSX.Element;
|
|
64
67
|
//# sourceMappingURL=booking-payments-summary.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"booking-payments-summary.d.ts","sourceRoot":"","sources":["../../src/components/booking-payments-summary.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"booking-payments-summary.d.ts","sourceRoot":"","sources":["../../src/components/booking-payments-summary.tsx"],"names":[],"mappings":"AAkEA,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,MAAM,CAAA;IACrB,WAAW,CAAC,EAAE,SAAS,GAAG,UAAU,GAAG,aAAa,CAAA;IACpD,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,aAAa,EAAE,MAAM,CAAA;IACrB,WAAW,EAAE,MAAM,CAAA;IACnB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CACrB;AAED,MAAM,WAAW,2BAA2B;IAC1C,SAAS,EAAE,MAAM,CAAA;IACjB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAA;IAC5B;;;;OAIG;IACH,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,yBAAyB,KAAK,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;IAC9E;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,yBAAyB,KAAK,IAAI,CAAA;IACxD,+DAA+D;IAC/D,iBAAiB,CAAC,EAAE,CAAC,GAAG,EAAE,yBAAyB,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAA;IAClF,uEAAuE;IACvE,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,yBAAyB,KAAK,IAAI,CAAA;IACxD;;;;OAIG;IACH,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,yBAAyB,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;CAC3E;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,SAAS,EACT,OAAkB,EAClB,cAAc,EACd,aAAa,EACb,iBAAiB,EACjB,aAAa,EACb,eAAe,GAChB,EAAE,2BAA2B,2CAiR7B"}
|