@voyantjs/bookings-ui 0.50.5 → 0.50.7
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 +1 -1
- package/dist/components/booking-create-dialog.d.ts.map +1 -1
- package/dist/components/booking-create-dialog.js +94 -27
- package/dist/components/booking-create-page.d.ts +2 -1
- package/dist/components/booking-create-page.d.ts.map +1 -1
- package/dist/components/booking-create-page.js +2 -2
- package/dist/components/booking-create-utils.d.ts +33 -0
- package/dist/components/booking-create-utils.d.ts.map +1 -0
- package/dist/components/booking-create-utils.js +67 -0
- package/dist/components/booking-list.d.ts +2 -1
- package/dist/components/booking-list.d.ts.map +1 -1
- package/dist/components/booking-list.js +5 -1
- package/dist/components/bookings-page.d.ts +2 -1
- package/dist/components/bookings-page.d.ts.map +1 -1
- package/dist/components/bookings-page.js +2 -2
- package/dist/components/payment-schedule-section.d.ts +23 -0
- package/dist/components/payment-schedule-section.d.ts.map +1 -1
- package/dist/components/payment-schedule-section.js +48 -3
- package/dist/components/price-breakdown-section.d.ts +1 -0
- package/dist/components/price-breakdown-section.d.ts.map +1 -1
- package/dist/components/price-breakdown-section.js +2 -0
- package/dist/components/product-picker-section.d.ts.map +1 -1
- package/dist/components/product-picker-section.js +8 -4
- package/dist/components/rooms-stepper-section.d.ts +12 -7
- package/dist/components/rooms-stepper-section.d.ts.map +1 -1
- package/dist/components/rooms-stepper-section.js +24 -8
- package/dist/components/shared-room-section.d.ts +2 -0
- package/dist/components/shared-room-section.d.ts.map +1 -1
- package/dist/components/shared-room-section.js +14 -2
- package/dist/components/travelers-section.d.ts +15 -10
- package/dist/components/travelers-section.d.ts.map +1 -1
- package/dist/components/travelers-section.js +96 -14
- package/dist/i18n/en.d.ts +32 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +32 -0
- package/dist/i18n/messages.d.ts +32 -0
- package/dist/i18n/messages.d.ts.map +1 -1
- package/dist/i18n/provider.d.ts +64 -0
- package/dist/i18n/provider.d.ts.map +1 -1
- package/dist/i18n/ro.d.ts +32 -0
- package/dist/i18n/ro.d.ts.map +1 -1
- package/dist/i18n/ro.js +32 -0
- package/package.json +24 -24
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { Button, Label } from "@voyantjs/ui/components";
|
|
3
|
+
import { Button, Checkbox, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyantjs/ui/components";
|
|
4
4
|
import { CurrencyInput } from "@voyantjs/ui/components/currency-input";
|
|
5
5
|
import { DatePicker } from "@voyantjs/ui/components/date-picker";
|
|
6
|
-
import { useBookingsUiMessagesOrDefault } from "../i18n/provider.js";
|
|
6
|
+
import { useBookingsUiI18nOrDefault, useBookingsUiMessagesOrDefault } from "../i18n/provider.js";
|
|
7
7
|
export const emptyPaymentScheduleValue = {
|
|
8
8
|
mode: "unpaid",
|
|
9
9
|
fullDueDate: null,
|
|
@@ -13,6 +13,22 @@ export const emptyPaymentScheduleValue = {
|
|
|
13
13
|
splitFirstDueDate: null,
|
|
14
14
|
splitSecondAmountCents: null,
|
|
15
15
|
splitSecondDueDate: null,
|
|
16
|
+
fullAlreadyPaid: false,
|
|
17
|
+
fullPaymentDate: null,
|
|
18
|
+
fullPaymentMethod: "bank_transfer",
|
|
19
|
+
fullPaymentReference: "",
|
|
20
|
+
advanceAlreadyPaid: false,
|
|
21
|
+
advancePaymentDate: null,
|
|
22
|
+
advancePaymentMethod: "bank_transfer",
|
|
23
|
+
advancePaymentReference: "",
|
|
24
|
+
splitFirstAlreadyPaid: false,
|
|
25
|
+
splitFirstPaymentDate: null,
|
|
26
|
+
splitFirstPaymentMethod: "bank_transfer",
|
|
27
|
+
splitFirstPaymentReference: "",
|
|
28
|
+
splitSecondAlreadyPaid: false,
|
|
29
|
+
splitSecondPaymentDate: null,
|
|
30
|
+
splitSecondPaymentMethod: "bank_transfer",
|
|
31
|
+
splitSecondPaymentReference: "",
|
|
16
32
|
};
|
|
17
33
|
/**
|
|
18
34
|
* Payment schedule picker for booking-create flows. Operators choose one of
|
|
@@ -36,6 +52,7 @@ export const emptyPaymentScheduleValue = {
|
|
|
36
52
|
*/
|
|
37
53
|
export function PaymentScheduleSection({ value, onChange, totalAmountCents, currency, labels, }) {
|
|
38
54
|
const messages = useBookingsUiMessagesOrDefault();
|
|
55
|
+
const { formatCurrency, formatNumber } = useBookingsUiI18nOrDefault();
|
|
39
56
|
const merged = { ...messages.paymentScheduleSection.labels, ...labels };
|
|
40
57
|
const set = (patch) => onChange({ ...value, ...patch });
|
|
41
58
|
const modes = [
|
|
@@ -55,5 +72,33 @@ export function PaymentScheduleSection({ value, onChange, totalAmountCents, curr
|
|
|
55
72
|
splitSecondAmountCents: totalAmountCents - half,
|
|
56
73
|
});
|
|
57
74
|
};
|
|
58
|
-
|
|
75
|
+
const total = typeof totalAmountCents === "number" ? totalAmountCents : null;
|
|
76
|
+
const scheduledTotal = value.mode === "unpaid"
|
|
77
|
+
? 0
|
|
78
|
+
: value.mode === "full"
|
|
79
|
+
? (total ?? 0)
|
|
80
|
+
: value.mode === "advance"
|
|
81
|
+
? (value.advanceAmountCents ?? 0)
|
|
82
|
+
: (value.splitFirstAmountCents ?? 0) + (value.splitSecondAmountCents ?? 0);
|
|
83
|
+
const remaining = total === null ? null : Math.max(0, total - scheduledTotal);
|
|
84
|
+
const formatAmount = (cents) => {
|
|
85
|
+
if (cents === null)
|
|
86
|
+
return "-";
|
|
87
|
+
return currency
|
|
88
|
+
? formatCurrency(cents / 100, currency)
|
|
89
|
+
: formatNumber(cents / 100, {
|
|
90
|
+
minimumFractionDigits: 2,
|
|
91
|
+
maximumFractionDigits: 2,
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
const paymentMethodLabels = messages.bookingPaymentsSummary.paymentMethodLabels;
|
|
95
|
+
const renderPaidFields = (prefix, checked) => {
|
|
96
|
+
const paymentDateKey = `${prefix}PaymentDate`;
|
|
97
|
+
const paymentMethodKey = `${prefix}PaymentMethod`;
|
|
98
|
+
const paymentReferenceKey = `${prefix}PaymentReference`;
|
|
99
|
+
const checkedKey = `${prefix}AlreadyPaid`;
|
|
100
|
+
const checkboxId = `payment-schedule-${prefix}-already-paid`;
|
|
101
|
+
return (_jsxs("div", { className: "flex flex-col gap-2 rounded-md border border-dashed p-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: checkboxId, checked: checked, onCheckedChange: (next) => set({ [checkedKey]: next === true }) }), _jsx(Label, { htmlFor: checkboxId, className: "cursor-pointer text-xs", children: merged.alreadyPaid })] }), checked ? (_jsxs("div", { className: "grid gap-2 sm:grid-cols-3", children: [_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: merged.paymentDate }), _jsx(DatePicker, { value: value[paymentDateKey] ?? "", onChange: (nextValue) => set({ [paymentDateKey]: nextValue }) })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: merged.paymentMethod }), _jsxs(Select, { value: value[paymentMethodKey] ?? "bank_transfer", onValueChange: (nextValue) => set({ [paymentMethodKey]: nextValue ?? "bank_transfer" }), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: ["bank_transfer", "credit_card", "cash", "voucher", "other"].map((method) => (_jsx(SelectItem, { value: method, children: paymentMethodLabels[method === "credit_card" ? "card" : method] }, method))) })] })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: merged.paymentReference }), _jsx(Input, { value: value[paymentReferenceKey] ?? "", onChange: (event) => set({ [paymentReferenceKey]: event.target.value }) })] })] })) : null] }));
|
|
102
|
+
};
|
|
103
|
+
return (_jsxs("div", { className: "flex flex-col gap-3 rounded-md border p-3", children: [_jsx(Label, { children: merged.heading }), _jsxs("div", { className: "grid gap-2 rounded-md bg-muted/40 p-2 text-xs sm:grid-cols-3", children: [_jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx("span", { className: "text-muted-foreground", children: merged.totalDue }), _jsx("span", { className: "font-medium tabular-nums", children: formatAmount(total) })] }), _jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx("span", { className: "text-muted-foreground", children: merged.scheduledTotal }), _jsx("span", { className: "font-medium tabular-nums", children: formatAmount(scheduledTotal) })] }), _jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx("span", { className: "text-muted-foreground", children: merged.remaining }), _jsx("span", { className: "font-medium tabular-nums", children: formatAmount(remaining) })] })] }), _jsx("div", { className: "flex flex-wrap items-center gap-2", children: modes.map((mode) => (_jsx(Button, { type: "button", size: "sm", variant: value.mode === mode.id ? "default" : "ghost", onClick: () => set({ mode: mode.id }), children: mode.label }, mode.id))) }), value.mode === "unpaid" && (_jsx("p", { className: "text-xs text-muted-foreground", children: merged.unpaidHint })), value.mode === "full" && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: merged.dueDate }), _jsx(DatePicker, { value: value.fullDueDate ?? "", onChange: (nextValue) => set({ fullDueDate: nextValue }) })] }), renderPaidFields("full", value.fullAlreadyPaid)] })), value.mode === "advance" && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: merged.amount }), _jsx(CurrencyInput, { value: value.advanceAmountCents, onChange: (next) => set({ advanceAmountCents: next }), currency: currency })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: merged.dueDate }), _jsx(DatePicker, { value: value.advanceDueDate ?? "", onChange: (nextValue) => set({ advanceDueDate: nextValue }) })] })] }), renderPaidFields("advance", value.advanceAlreadyPaid)] })), value.mode === "split" && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-xs font-medium", children: merged.firstInstallment }), totalAmountCents ? (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: handlePreset5050, children: merged.preset5050 })) : null] }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsx(CurrencyInput, { placeholder: merged.amount, value: value.splitFirstAmountCents, onChange: (next) => set({ splitFirstAmountCents: next }), currency: currency }), _jsx(DatePicker, { value: value.splitFirstDueDate ?? "", onChange: (nextValue) => set({ splitFirstDueDate: nextValue }) })] }), renderPaidFields("splitFirst", value.splitFirstAlreadyPaid), _jsx("div", { className: "text-xs font-medium", children: merged.secondInstallment }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsx(CurrencyInput, { placeholder: merged.amount, value: value.splitSecondAmountCents, onChange: (next) => set({ splitSecondAmountCents: next }), currency: currency }), _jsx(DatePicker, { value: value.splitSecondDueDate ?? "", onChange: (nextValue) => set({ splitSecondDueDate: nextValue }) })] }), renderPaidFields("splitSecond", value.splitSecondAlreadyPaid)] }))] }));
|
|
59
104
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"price-breakdown-section.d.ts","sourceRoot":"","sources":["../../src/components/price-breakdown-section.tsx"],"names":[],"mappings":"AAQA,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,4EAA4E;IAC5E,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,4DAA4D;IAC5D,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B;;;OAGG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,WAAW,EAAE,OAAO,CAAA;CACrB;AAED,MAAM,WAAW,mBAAmB;IAClC,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAA;IACnC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,gBAAgB,EAAE,OAAO,CAAA;IACzB,cAAc,EAAE,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"price-breakdown-section.d.ts","sourceRoot":"","sources":["../../src/components/price-breakdown-section.tsx"],"names":[],"mappings":"AAQA,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,4EAA4E;IAC5E,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,4DAA4D;IAC5D,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B;;;OAGG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,WAAW,EAAE,OAAO,CAAA;CACrB;AAED,MAAM,WAAW,mBAAmB;IAClC,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAA;IACnC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,gBAAgB,EAAE,OAAO,CAAA;IACzB,cAAc,EAAE,OAAO,CAAA;IACvB,KAAK,EAAE,kBAAkB,EAAE,CAAA;CAC5B;AAED,MAAM,WAAW,0BAA0B;IACzC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,uEAAuE;IACvE,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACtC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,cAAc,CAAC,EAAE,MAAM,CAAA;QACvB,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,eAAe,CAAC,EAAE,MAAM,CAAA;QACxB,cAAc,CAAC,EAAE,MAAM,CAAA;QACvB,yBAAyB,CAAC,EAAE,MAAM,CAAA;QAClC,sBAAsB,CAAC,EAAE,MAAM,CAAA;KAChC,CAAA;IACD,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,IAAI,CAAA;CAChD;AA0BD;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,EACpC,SAAS,EACT,QAAQ,EACR,cAAc,EACd,SAAS,EACT,MAAM,EACN,QAAQ,GACT,EAAE,0BAA0B,kDAsQ5B"}
|
|
@@ -167,11 +167,13 @@ export function PriceBreakdownSection({ productId, optionId, unitQuantities, cat
|
|
|
167
167
|
priceOverrideReason: overrideReason,
|
|
168
168
|
isManualOverride,
|
|
169
169
|
requiresReason,
|
|
170
|
+
lines,
|
|
170
171
|
});
|
|
171
172
|
}, [
|
|
172
173
|
confirmedAmountCents,
|
|
173
174
|
currency,
|
|
174
175
|
isManualOverride,
|
|
176
|
+
lines,
|
|
175
177
|
onChange,
|
|
176
178
|
overrideReason,
|
|
177
179
|
requiresReason,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"product-picker-section.d.ts","sourceRoot":"","sources":["../../src/components/product-picker-section.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"product-picker-section.d.ts","sourceRoot":"","sources":["../../src/components/product-picker-section.tsx"],"names":[],"mappings":"AA+BA,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAA;IACjB,uFAAuF;IACvF,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB;AAED,MAAM,WAAW,yBAAyB;IACxC,KAAK,EAAE,kBAAkB,CAAA;IACzB,QAAQ,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,CAAA;IAC7C,mEAAmE;IACnE,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,qGAAqG;IACrG,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,wBAAwB,CAAC,EAAE,MAAM,CAAA;QACjC,wBAAwB,CAAC,EAAE,MAAM,CAAA;QACjC,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,UAAU,CAAC,EAAE,MAAM,CAAA;KACpB,CAAA;CACF;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,KAAK,EACL,QAAQ,EACR,OAAc,EACd,WAAmB,EACnB,MAAM,GACP,EAAE,yBAAyB,2CAyI3B"}
|
|
@@ -5,6 +5,7 @@ import { Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, }
|
|
|
5
5
|
import { Combobox, ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from "@voyantjs/ui/components/combobox";
|
|
6
6
|
import * as React from "react";
|
|
7
7
|
import { useBookingsUiMessagesOrDefault } from "../i18n/provider.js";
|
|
8
|
+
import { productMatchesPickerSearch } from "./booking-create-utils.js";
|
|
8
9
|
const OPTION_NONE = "__none__";
|
|
9
10
|
/**
|
|
10
11
|
* Controlled product + option picker. Splits `value` + `onChange` so apps can
|
|
@@ -13,6 +14,7 @@ const OPTION_NONE = "__none__";
|
|
|
13
14
|
*/
|
|
14
15
|
export function ProductPickerSection({ value, onChange, enabled = true, lockProduct = false, labels, }) {
|
|
15
16
|
const [productSearch, setProductSearch] = React.useState("");
|
|
17
|
+
const cachedProductsRef = React.useRef(new Map());
|
|
16
18
|
const messages = useBookingsUiMessagesOrDefault();
|
|
17
19
|
const merged = { ...messages.productPickerSection.labels, ...labels };
|
|
18
20
|
const { data: productsData } = useProducts({
|
|
@@ -24,15 +26,17 @@ export function ProductPickerSection({ value, onChange, enabled = true, lockProd
|
|
|
24
26
|
enabled: enabled && Boolean(value.productId),
|
|
25
27
|
});
|
|
26
28
|
const products = React.useMemo(() => {
|
|
27
|
-
const map = new Map();
|
|
29
|
+
const map = new Map(cachedProductsRef.current);
|
|
28
30
|
for (const product of productsData?.data ?? [])
|
|
29
31
|
map.set(product.id, product);
|
|
30
32
|
if (selectedProductQuery.data)
|
|
31
33
|
map.set(selectedProductQuery.data.id, selectedProductQuery.data);
|
|
34
|
+
cachedProductsRef.current = map;
|
|
32
35
|
return Array.from(map.values());
|
|
33
36
|
}, [productsData?.data, selectedProductQuery.data]);
|
|
34
37
|
const productMap = React.useMemo(() => new Map(products.map((product) => [product.id, product])), [products]);
|
|
35
|
-
const
|
|
38
|
+
const resolveProductLabel = React.useCallback((productId) => productMap.get(productId)?.name ?? cachedProductsRef.current.get(productId)?.name ?? "", [productMap]);
|
|
39
|
+
const selectedProductLabel = value.productId ? resolveProductLabel(value.productId) : "";
|
|
36
40
|
const [productInputValue, setProductInputValue] = React.useState(selectedProductLabel);
|
|
37
41
|
React.useEffect(() => {
|
|
38
42
|
if (selectedProductLabel)
|
|
@@ -44,7 +48,7 @@ export function ProductPickerSection({ value, onChange, enabled = true, lockProd
|
|
|
44
48
|
enabled: enabled && Boolean(value.productId),
|
|
45
49
|
});
|
|
46
50
|
const options = optionsData?.data ?? [];
|
|
47
|
-
return (_jsxs(_Fragment, { children: [!lockProduct && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs(Label, { children: [merged.product, " ", _jsx("span", { className: "text-destructive", children: "*" })] }), _jsxs(Combobox, { items: products.map((product) => product.id), value: value.productId || null, inputValue: productInputValue, autoHighlight: true, disabled: !enabled,
|
|
51
|
+
return (_jsxs(_Fragment, { children: [!lockProduct && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs(Label, { children: [merged.product, " ", _jsx("span", { className: "text-destructive", children: "*" })] }), _jsxs(Combobox, { items: products.map((product) => product.id), value: value.productId || null, inputValue: productInputValue, autoHighlight: true, disabled: !enabled, filter: (id, query) => productMatchesPickerSearch(productMap.get(id), query), itemToStringLabel: (id) => resolveProductLabel(id) || id, itemToStringValue: (id) => id, onInputValueChange: (next) => {
|
|
48
52
|
setProductInputValue(next);
|
|
49
53
|
setProductSearch(next);
|
|
50
54
|
if (!next)
|
|
@@ -52,7 +56,7 @@ export function ProductPickerSection({ value, onChange, enabled = true, lockProd
|
|
|
52
56
|
}, onValueChange: (next) => {
|
|
53
57
|
const productId = next ?? "";
|
|
54
58
|
onChange({ productId, optionId: null });
|
|
55
|
-
setProductInputValue(productId ? (
|
|
59
|
+
setProductInputValue(productId ? resolveProductLabel(productId) : "");
|
|
56
60
|
}, children: [_jsx(ComboboxInput, { placeholder: merged.productSearchPlaceholder, showClear: !!value.productId }), _jsxs(ComboboxContent, { children: [_jsx(ComboboxEmpty, { children: merged.productEmpty }), _jsx(ComboboxList, { children: _jsx(ComboboxCollection, { children: (id) => {
|
|
57
61
|
const product = productMap.get(id);
|
|
58
62
|
if (!product)
|
|
@@ -7,14 +7,19 @@ export interface RoomsStepperSectionProps {
|
|
|
7
7
|
value: RoomsStepperValue;
|
|
8
8
|
onChange: (value: RoomsStepperValue) => void;
|
|
9
9
|
/**
|
|
10
|
-
* Departure the operator picked.
|
|
11
|
-
*
|
|
12
|
-
* product, so there's nothing to show before then.
|
|
10
|
+
* Departure the operator picked. Departure-specific availability wins
|
|
11
|
+
* when present; otherwise the section falls back to option-level units.
|
|
13
12
|
*/
|
|
14
13
|
slotId?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Product option whose units should be shown before a departure is picked.
|
|
16
|
+
* Departure-specific availability wins when `slotId` is present.
|
|
17
|
+
*/
|
|
18
|
+
optionId?: string | null;
|
|
15
19
|
enabled?: boolean;
|
|
16
20
|
labels?: {
|
|
17
21
|
heading?: string;
|
|
22
|
+
noOption?: string;
|
|
18
23
|
noSlot?: string;
|
|
19
24
|
noUnits?: string;
|
|
20
25
|
remaining?: string;
|
|
@@ -23,9 +28,9 @@ export interface RoomsStepperSectionProps {
|
|
|
23
28
|
}
|
|
24
29
|
/**
|
|
25
30
|
* Rooms / per-unit stepper for booking-create flows. Drives
|
|
26
|
-
* `GET /v1/availability/slots/:id/unit-availability` from #235
|
|
27
|
-
*
|
|
28
|
-
*
|
|
31
|
+
* `GET /v1/availability/slots/:id/unit-availability` from #235 when a
|
|
32
|
+
* departure is selected, and option-level units before departure selection,
|
|
33
|
+
* so operators can still build "2 double rooms and 1 single" drafts.
|
|
29
34
|
*
|
|
30
35
|
* The section only tracks **intent** (how many of each unit the operator
|
|
31
36
|
* wants to book). Actual hold/reservation happens when the parent submits
|
|
@@ -41,5 +46,5 @@ export interface RoomsStepperSectionProps {
|
|
|
41
46
|
* disables the "+" button — we don't let the UI submit a request that
|
|
42
47
|
* would 409 at insert time.
|
|
43
48
|
*/
|
|
44
|
-
export declare function RoomsStepperSection({ value, onChange, slotId, enabled, labels, }: RoomsStepperSectionProps): import("react/jsx-runtime").JSX.Element;
|
|
49
|
+
export declare function RoomsStepperSection({ value, onChange, slotId, optionId, enabled, labels, }: RoomsStepperSectionProps): import("react/jsx-runtime").JSX.Element;
|
|
45
50
|
//# sourceMappingURL=rooms-stepper-section.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rooms-stepper-section.d.ts","sourceRoot":"","sources":["../../src/components/rooms-stepper-section.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"rooms-stepper-section.d.ts","sourceRoot":"","sources":["../../src/components/rooms-stepper-section.tsx"],"names":[],"mappings":"AAQA,iEAAiE;AACjE,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACnC;AAED,eAAO,MAAM,sBAAsB,EAAE,iBAAsC,CAAA;AAE3E,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,iBAAiB,CAAA;IACxB,QAAQ,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAA;IAC5C;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,SAAS,CAAC,EAAE,MAAM,CAAA;KACnB,CAAA;CACF;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,KAAK,EACL,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,OAAc,EACd,MAAM,GACP,EAAE,wBAAwB,2CAmG1B"}
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useSlotUnitAvailability } from "@voyantjs/availability-react";
|
|
4
|
+
import { useOptionUnits } from "@voyantjs/products-react";
|
|
4
5
|
import { Button, Label } from "@voyantjs/ui/components";
|
|
5
6
|
import { Minus, Plus } from "lucide-react";
|
|
6
7
|
import { useBookingsUiMessagesOrDefault } from "../i18n/provider.js";
|
|
7
8
|
export const emptyRoomsStepperValue = { quantities: {} };
|
|
8
9
|
/**
|
|
9
10
|
* Rooms / per-unit stepper for booking-create flows. Drives
|
|
10
|
-
* `GET /v1/availability/slots/:id/unit-availability` from #235
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* `GET /v1/availability/slots/:id/unit-availability` from #235 when a
|
|
12
|
+
* departure is selected, and option-level units before departure selection,
|
|
13
|
+
* so operators can still build "2 double rooms and 1 single" drafts.
|
|
13
14
|
*
|
|
14
15
|
* The section only tracks **intent** (how many of each unit the operator
|
|
15
16
|
* wants to book). Actual hold/reservation happens when the parent submits
|
|
@@ -25,15 +26,30 @@ export const emptyRoomsStepperValue = { quantities: {} };
|
|
|
25
26
|
* disables the "+" button — we don't let the UI submit a request that
|
|
26
27
|
* would 409 at insert time.
|
|
27
28
|
*/
|
|
28
|
-
export function RoomsStepperSection({ value, onChange, slotId, enabled = true, labels, }) {
|
|
29
|
+
export function RoomsStepperSection({ value, onChange, slotId, optionId, enabled = true, labels, }) {
|
|
29
30
|
const messages = useBookingsUiMessagesOrDefault();
|
|
30
31
|
const merged = { ...messages.roomsStepperSection.labels, ...labels };
|
|
31
32
|
const availability = useSlotUnitAvailability({ slotId, enabled: enabled && Boolean(slotId) });
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
const optionUnits = useOptionUnits({
|
|
34
|
+
optionId: optionId ?? undefined,
|
|
35
|
+
limit: 100,
|
|
36
|
+
enabled: enabled && !slotId && Boolean(optionId),
|
|
37
|
+
});
|
|
38
|
+
const units = slotId
|
|
39
|
+
? (availability.data?.data ?? [])
|
|
40
|
+
: (optionUnits.data?.data ?? []).map((unit) => ({
|
|
41
|
+
optionUnitId: unit.id,
|
|
42
|
+
unitName: unit.name,
|
|
43
|
+
occupancyMax: unit.occupancyMax,
|
|
44
|
+
initial: null,
|
|
45
|
+
reserved: 0,
|
|
46
|
+
remaining: unit.maxQuantity ?? null,
|
|
47
|
+
}));
|
|
48
|
+
if (!slotId && !optionId) {
|
|
49
|
+
return (_jsxs("div", { className: "flex flex-col gap-2 rounded-md border p-3", children: [_jsx(Label, { children: merged.heading }), _jsx("p", { className: "text-xs text-muted-foreground", children: merged.noOption })] }));
|
|
35
50
|
}
|
|
36
|
-
|
|
51
|
+
const loaded = slotId ? availability.isSuccess : optionUnits.isSuccess;
|
|
52
|
+
if (loaded && units.length === 0) {
|
|
37
53
|
return (_jsxs("div", { className: "flex flex-col gap-2 rounded-md border p-3", children: [_jsx(Label, { children: merged.heading }), _jsx("p", { className: "text-xs text-muted-foreground", children: merged.noUnits })] }));
|
|
38
54
|
}
|
|
39
55
|
const setQuantity = (unitId, qty) => {
|
|
@@ -8,6 +8,7 @@ export interface SharedRoomValue {
|
|
|
8
8
|
groupLabel?: string;
|
|
9
9
|
}
|
|
10
10
|
export declare const emptySharedRoomValue: SharedRoomValue;
|
|
11
|
+
export declare function clearSharedRoomValue(): SharedRoomValue;
|
|
11
12
|
export interface SharedRoomSectionProps {
|
|
12
13
|
value: SharedRoomValue;
|
|
13
14
|
onChange: (value: SharedRoomValue) => void;
|
|
@@ -28,6 +29,7 @@ export interface SharedRoomSectionProps {
|
|
|
28
29
|
groupLabel?: string;
|
|
29
30
|
groupLabelPlaceholder?: string;
|
|
30
31
|
createAction?: string;
|
|
32
|
+
remove?: string;
|
|
31
33
|
};
|
|
32
34
|
}
|
|
33
35
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shared-room-section.d.ts","sourceRoot":"","sources":["../../src/components/shared-room-section.tsx"],"names":[],"mappings":"AA2BA,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,MAAM,CAAA;AAE9C,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAA;IAChB,IAAI,EAAE,cAAc,CAAA;IACpB,sCAAsC;IACtC,OAAO,EAAE,MAAM,CAAA;IACf,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,eAAO,MAAM,oBAAoB,EAAE,eAKlC,CAAA;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,eAAe,CAAA;IACtB,QAAQ,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAA;IAC1C;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,MAAM,CAAC,EAAE;QACP,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,UAAU,CAAC,EAAE,MAAM,CAAA;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,iBAAiB,CAAC,EAAE,MAAM,CAAA;QAC1B,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,UAAU,CAAC,EAAE,MAAM,CAAA;QACnB,gBAAgB,CAAC,EAAE,MAAM,CAAA;QACzB,UAAU,CAAC,EAAE,MAAM,CAAA;QACnB,qBAAqB,CAAC,EAAE,MAAM,CAAA;QAC9B,YAAY,CAAC,EAAE,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"shared-room-section.d.ts","sourceRoot":"","sources":["../../src/components/shared-room-section.tsx"],"names":[],"mappings":"AA2BA,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,MAAM,CAAA;AAE9C,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAA;IAChB,IAAI,EAAE,cAAc,CAAA;IACpB,sCAAsC;IACtC,OAAO,EAAE,MAAM,CAAA;IACf,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,eAAO,MAAM,oBAAoB,EAAE,eAKlC,CAAA;AAED,wBAAgB,oBAAoB,IAAI,eAAe,CAEtD;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,eAAe,CAAA;IACtB,QAAQ,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAA;IAC1C;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,MAAM,CAAC,EAAE;QACP,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,UAAU,CAAC,EAAE,MAAM,CAAA;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,iBAAiB,CAAC,EAAE,MAAM,CAAA;QAC1B,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,UAAU,CAAC,EAAE,MAAM,CAAA;QACnB,gBAAgB,CAAC,EAAE,MAAM,CAAA;QACzB,UAAU,CAAC,EAAE,MAAM,CAAA;QACnB,qBAAqB,CAAC,EAAE,MAAM,CAAA;QAC9B,YAAY,CAAC,EAAE,MAAM,CAAA;QACrB,MAAM,CAAC,EAAE,MAAM,CAAA;KAChB,CAAA;CACF;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,KAAK,EACL,QAAQ,EACR,SAAS,EACT,OAAc,EACd,MAAM,GACP,EAAE,sBAAsB,2CAyKxB"}
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
import { useBookingGroups } from "@voyantjs/bookings-react";
|
|
4
4
|
import { Button, Input, Label, Sheet, SheetBody, SheetContent, SheetFooter, SheetHeader, SheetTitle, } from "@voyantjs/ui/components";
|
|
5
5
|
import { Combobox, ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from "@voyantjs/ui/components/combobox";
|
|
6
|
-
import { Link2, Plus } from "lucide-react";
|
|
6
|
+
import { Link2, Plus, X } from "lucide-react";
|
|
7
7
|
import * as React from "react";
|
|
8
8
|
import { useBookingsUiMessagesOrDefault } from "../i18n/provider.js";
|
|
9
9
|
export const emptySharedRoomValue = {
|
|
@@ -12,6 +12,9 @@ export const emptySharedRoomValue = {
|
|
|
12
12
|
groupId: "",
|
|
13
13
|
groupLabel: "",
|
|
14
14
|
};
|
|
15
|
+
export function clearSharedRoomValue() {
|
|
16
|
+
return { ...emptySharedRoomValue };
|
|
17
|
+
}
|
|
15
18
|
/**
|
|
16
19
|
* Shared-room (partaj) attachment section. Operators can create a new group in
|
|
17
20
|
* a sheet or join an existing product-scoped group with an async combobox.
|
|
@@ -47,6 +50,15 @@ export function SharedRoomSection({ value, onChange, productId, enabled = true,
|
|
|
47
50
|
setDraftGroupLabel(value.groupLabel ?? "");
|
|
48
51
|
}, [createSheetOpen, value.groupLabel]);
|
|
49
52
|
const set = (patch) => onChange({ ...value, ...patch });
|
|
53
|
+
const clear = () => {
|
|
54
|
+
if (!enabled)
|
|
55
|
+
return;
|
|
56
|
+
setCreateSheetOpen(false);
|
|
57
|
+
setDraftGroupLabel("");
|
|
58
|
+
setGroupInputValue("");
|
|
59
|
+
setGroupSearch("");
|
|
60
|
+
onChange(clearSharedRoomValue());
|
|
61
|
+
};
|
|
50
62
|
const selectCreateMode = () => {
|
|
51
63
|
if (!enabled)
|
|
52
64
|
return;
|
|
@@ -66,7 +78,7 @@ export function SharedRoomSection({ value, onChange, productId, enabled = true,
|
|
|
66
78
|
});
|
|
67
79
|
setCreateSheetOpen(false);
|
|
68
80
|
};
|
|
69
|
-
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex flex-col gap-3 rounded-md border p-3", children: [_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { children: merged.toggle }), value.enabled && value.mode === "create" && value.groupLabel ? (_jsx("p", { className: "text-xs text-muted-foreground", children: value.groupLabel })) : value.enabled && value.mode === "create" ? (_jsx("p", { className: "text-xs text-muted-foreground", children: merged.createHint })) : null] }), _jsxs("div", { className: "grid gap-2 sm:grid-cols-2", children: [_jsxs(Button, { type: "button", size: "sm", variant: value.enabled && value.mode === "create" ? "default" : "outline", onClick: selectCreateMode, disabled: !enabled, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), merged.createMode] }), _jsxs(Button, { type: "button", size: "sm", variant: value.enabled && value.mode === "join" ? "default" : "outline", onClick: selectJoinMode, disabled: !enabled || !productId, children: [_jsx(Link2, { className: "mr-2 h-4 w-4" }), merged.joinMode] })] }), value.enabled && value.mode === "join" ? (_jsxs(Combobox, { items: existingGroups.map((group) => group.id), value: value.groupId || null, inputValue: groupInputValue, autoHighlight: true, disabled: !enabled || !productId, itemToStringValue: (id) => groupsMap.get(id)?.label ?? "", onInputValueChange: (next) => {
|
|
81
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex flex-col gap-3 rounded-md border p-3", children: [_jsxs("div", { className: "flex flex-wrap items-start justify-between gap-2", children: [_jsxs("div", { className: "flex min-w-0 flex-col gap-1", children: [_jsx(Label, { children: merged.toggle }), value.enabled && value.mode === "create" && value.groupLabel ? (_jsx("p", { className: "truncate text-xs text-muted-foreground", children: value.groupLabel })) : value.enabled && value.mode === "create" ? (_jsx("p", { className: "text-xs text-muted-foreground", children: merged.createHint })) : null] }), value.enabled ? (_jsxs(Button, { type: "button", size: "sm", variant: "ghost", onClick: clear, disabled: !enabled, children: [_jsx(X, { className: "mr-2 h-4 w-4" }), merged.remove] })) : null] }), _jsxs("div", { className: "grid gap-2 sm:grid-cols-2", children: [_jsxs(Button, { type: "button", size: "sm", variant: value.enabled && value.mode === "create" ? "default" : "outline", onClick: selectCreateMode, disabled: !enabled, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), merged.createMode] }), _jsxs(Button, { type: "button", size: "sm", variant: value.enabled && value.mode === "join" ? "default" : "outline", onClick: selectJoinMode, disabled: !enabled || !productId, children: [_jsx(Link2, { className: "mr-2 h-4 w-4" }), merged.joinMode] })] }), value.enabled && value.mode === "join" ? (_jsxs(Combobox, { items: existingGroups.map((group) => group.id), value: value.groupId || null, inputValue: groupInputValue, autoHighlight: true, disabled: !enabled || !productId, itemToStringValue: (id) => groupsMap.get(id)?.label ?? "", onInputValueChange: (next) => {
|
|
70
82
|
setGroupInputValue(next);
|
|
71
83
|
setGroupSearch(next);
|
|
72
84
|
if (!next)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type TravelerRole = "lead" | "adult" | "child" | "infant";
|
|
2
2
|
export interface TravelerEntry {
|
|
3
|
+
personId: string | null;
|
|
3
4
|
firstName: string;
|
|
4
5
|
lastName: string;
|
|
5
6
|
email: string;
|
|
@@ -31,6 +32,7 @@ export interface TravelersSectionProps {
|
|
|
31
32
|
* When provided, each traveler gets a room-assignment dropdown.
|
|
32
33
|
*/
|
|
33
34
|
roomUnits?: RoomUnitOption[];
|
|
35
|
+
billingPersonId?: string | null;
|
|
34
36
|
labels?: {
|
|
35
37
|
heading?: string;
|
|
36
38
|
addTraveler?: string;
|
|
@@ -46,27 +48,30 @@ export interface TravelersSectionProps {
|
|
|
46
48
|
noRoom?: string;
|
|
47
49
|
remove?: string;
|
|
48
50
|
empty?: string;
|
|
51
|
+
person?: string;
|
|
52
|
+
personSearchPlaceholder?: string;
|
|
53
|
+
personEmpty?: string;
|
|
54
|
+
createNewPerson?: string;
|
|
55
|
+
createPersonSheetTitle?: string;
|
|
56
|
+
addBillingPerson?: string;
|
|
49
57
|
};
|
|
50
58
|
}
|
|
51
59
|
/**
|
|
52
|
-
* Traveler list for booking-create flows. Each row
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* booking detail page afterwards, consistent with the lead-person picker's
|
|
56
|
-
* edit-after-create story.
|
|
60
|
+
* Traveler list for booking-create flows. Each row can point at an existing
|
|
61
|
+
* CRM person, create a new CRM person, or carry manual name/email details,
|
|
62
|
+
* plus role and optional room assignment.
|
|
57
63
|
*
|
|
58
64
|
* ### Parent contract
|
|
59
65
|
*
|
|
60
66
|
* At submit time, the parent:
|
|
61
|
-
* 1.
|
|
62
|
-
* (email match + name, or skip when the operator intentionally left
|
|
63
|
-
* email blank).
|
|
64
|
-
* 2. Inserts a `booking_travelers` row per traveler with `participantType`
|
|
67
|
+
* 1. Inserts a `booking_travelers` row per traveler with `participantType`
|
|
65
68
|
* derived from the role (`lead` / `adult` → traveler; `child` / `infant`
|
|
66
69
|
* → traveler with travelerCategory set).
|
|
70
|
+
* 2. Carries `personId` through when the traveler is tied to CRM, including
|
|
71
|
+
* when the payer is also traveling.
|
|
67
72
|
* 3. Exactly one row should have `role: "lead"` — enforced at submit, not
|
|
68
73
|
* here. The UI lets the operator pick whichever layout they want, then
|
|
69
74
|
* the submit handler errors if the invariant isn't met.
|
|
70
75
|
*/
|
|
71
|
-
export declare function TravelersSection({ value, onChange, roomUnits, labels }: TravelersSectionProps): import("react/jsx-runtime").JSX.Element;
|
|
76
|
+
export declare function TravelersSection({ value, onChange, roomUnits, billingPersonId, labels, }: TravelersSectionProps): import("react/jsx-runtime").JSX.Element;
|
|
72
77
|
//# sourceMappingURL=travelers-section.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"travelers-section.d.ts","sourceRoot":"","sources":["../../src/components/travelers-section.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"travelers-section.d.ts","sourceRoot":"","sources":["../../src/components/travelers-section.tsx"],"names":[],"mappings":"AAgCA,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,CAAA;AAIhE,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,YAAY,CAAA;IAClB,+EAA+E;IAC/E,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1B;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,aAAa,EAAE,CAAA;CAC3B;AAED,eAAO,MAAM,sBAAsB,EAAE,iBAAqC,CAAA;AAE1E,qFAAqF;AACrF,wBAAgB,mBAAmB,CAAC,IAAI,GAAE,YAAsB,GAAG,aAAa,CAE/E;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,iBAAiB,EAAE,MAAM,CAAA;CAC1B;AAED,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,iBAAiB,CAAA;IACxB,QAAQ,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAA;IAC5C;;;OAGG;IACH,SAAS,CAAC,EAAE,cAAc,EAAE,CAAA;IAC5B,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,UAAU,CAAC,EAAE,MAAM,CAAA;QACnB,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,uBAAuB,CAAC,EAAE,MAAM,CAAA;QAChC,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,eAAe,CAAC,EAAE,MAAM,CAAA;QACxB,sBAAsB,CAAC,EAAE,MAAM,CAAA;QAC/B,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAC1B,CAAA;CACF;AAID;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,KAAK,EACL,QAAQ,EACR,SAAS,EACT,eAAe,EACf,MAAM,GACP,EAAE,qBAAqB,2CAuLvB"}
|
|
@@ -1,38 +1,42 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { usePeople, usePerson } from "@voyantjs/crm-react";
|
|
4
|
+
import { PersonForm } from "@voyantjs/crm-ui";
|
|
5
|
+
import { Button, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Sheet, SheetBody, SheetContent, SheetHeader, SheetTitle, } from "@voyantjs/ui/components";
|
|
6
|
+
import { Combobox, ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from "@voyantjs/ui/components/combobox";
|
|
7
|
+
import { Trash2, UserPlus } from "lucide-react";
|
|
8
|
+
import * as React from "react";
|
|
5
9
|
import { useBookingsUiMessagesOrDefault } from "../i18n/provider.js";
|
|
6
10
|
const ALL_ROLES = ["lead", "adult", "child", "infant"];
|
|
7
11
|
export const emptyTravelerListValue = { travelers: [] };
|
|
8
12
|
/** Factory for a blank row — `role` defaults to `adult` unless the list is empty. */
|
|
9
13
|
export function createBlankTraveler(role = "adult") {
|
|
10
|
-
return { firstName: "", lastName: "", email: "", role, roomUnitId: null };
|
|
14
|
+
return { personId: null, firstName: "", lastName: "", email: "", role, roomUnitId: null };
|
|
11
15
|
}
|
|
12
16
|
const NO_ROOM = "__unassigned__";
|
|
13
17
|
/**
|
|
14
|
-
* Traveler list for booking-create flows. Each row
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* booking detail page afterwards, consistent with the lead-person picker's
|
|
18
|
-
* edit-after-create story.
|
|
18
|
+
* Traveler list for booking-create flows. Each row can point at an existing
|
|
19
|
+
* CRM person, create a new CRM person, or carry manual name/email details,
|
|
20
|
+
* plus role and optional room assignment.
|
|
19
21
|
*
|
|
20
22
|
* ### Parent contract
|
|
21
23
|
*
|
|
22
24
|
* At submit time, the parent:
|
|
23
|
-
* 1.
|
|
24
|
-
* (email match + name, or skip when the operator intentionally left
|
|
25
|
-
* email blank).
|
|
26
|
-
* 2. Inserts a `booking_travelers` row per traveler with `participantType`
|
|
25
|
+
* 1. Inserts a `booking_travelers` row per traveler with `participantType`
|
|
27
26
|
* derived from the role (`lead` / `adult` → traveler; `child` / `infant`
|
|
28
27
|
* → traveler with travelerCategory set).
|
|
28
|
+
* 2. Carries `personId` through when the traveler is tied to CRM, including
|
|
29
|
+
* when the payer is also traveling.
|
|
29
30
|
* 3. Exactly one row should have `role: "lead"` — enforced at submit, not
|
|
30
31
|
* here. The UI lets the operator pick whichever layout they want, then
|
|
31
32
|
* the submit handler errors if the invariant isn't met.
|
|
32
33
|
*/
|
|
33
|
-
export function TravelersSection({ value, onChange, roomUnits, labels }) {
|
|
34
|
+
export function TravelersSection({ value, onChange, roomUnits, billingPersonId, labels, }) {
|
|
34
35
|
const messages = useBookingsUiMessagesOrDefault();
|
|
35
36
|
const merged = { ...messages.travelersSection.labels, ...labels };
|
|
37
|
+
const billingPerson = usePerson(billingPersonId ?? undefined, {
|
|
38
|
+
enabled: Boolean(billingPersonId),
|
|
39
|
+
});
|
|
36
40
|
const roleLabels = {
|
|
37
41
|
lead: merged.roleLead,
|
|
38
42
|
adult: merged.roleAdult,
|
|
@@ -52,9 +56,87 @@ export function TravelersSection({ value, onChange, roomUnits, labels }) {
|
|
|
52
56
|
const role = value.travelers.length === 0 ? "lead" : "adult";
|
|
53
57
|
onChange({ travelers: [...value.travelers, createBlankTraveler(role)] });
|
|
54
58
|
};
|
|
55
|
-
|
|
59
|
+
const addBillingPerson = () => {
|
|
60
|
+
if (!billingPerson.data)
|
|
61
|
+
return;
|
|
62
|
+
const role = value.travelers.length === 0 ? "lead" : "adult";
|
|
63
|
+
onChange({
|
|
64
|
+
travelers: [...value.travelers, createTravelerFromPerson(billingPerson.data, role)],
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
const hasBillingPersonTraveler = Boolean(billingPersonId && value.travelers.some((traveler) => traveler.personId === billingPersonId));
|
|
68
|
+
return (_jsxs("div", { className: "flex flex-col gap-2 rounded-md border p-3", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx(Label, { children: merged.heading }), _jsx(Button, { type: "button", size: "sm", variant: "ghost", onClick: addRow, children: merged.addTraveler })] }), billingPersonId && !hasBillingPersonTraveler ? (_jsx("div", { children: _jsxs(Button, { type: "button", size: "sm", variant: "outline", onClick: addBillingPerson, disabled: !billingPerson.data, children: [_jsx(UserPlus, { className: "mr-1 h-3.5 w-3.5" }), merged.addBillingPerson] }) })) : null, value.travelers.length === 0 ? (_jsx("p", { className: "text-xs text-muted-foreground", children: merged.empty })) : (_jsx("div", { className: "flex flex-col gap-2", children: value.travelers.map((traveler, index) => (_jsxs("div", { className: "flex flex-col gap-2 rounded-md border p-2", children: [_jsx(TravelerPersonPicker, { personId: traveler.personId, labels: merged, pinnedPeople: billingPerson.data ? [billingPerson.data] : [], onSelect: (person) => updateAt(index, {
|
|
69
|
+
personId: person.id,
|
|
70
|
+
firstName: person.firstName,
|
|
71
|
+
lastName: person.lastName,
|
|
72
|
+
email: person.email ?? "",
|
|
73
|
+
}), onClear: () => updateAt(index, { personId: null }) }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsx(Input, { placeholder: merged.firstName, value: traveler.firstName, onChange: (e) => updateAt(index, { firstName: e.target.value }) }), _jsx(Input, { placeholder: merged.lastName, value: traveler.lastName, onChange: (e) => updateAt(index, { lastName: e.target.value }) })] }), _jsx(Input, { type: "email", placeholder: merged.email, value: traveler.email, onChange: (e) => updateAt(index, { email: e.target.value }) }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: merged.role }), _jsxs(Select, { value: traveler.role, onValueChange: (v) => updateAt(index, { role: (v ?? "adult") }), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: ALL_ROLES.map((role) => (_jsx(SelectItem, { value: role, children: roleLabels[role] }, role))) })] })] }), roomUnits && roomUnits.length > 0 ? (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: merged.room }), _jsxs(Select, { value: traveler.roomUnitId ?? NO_ROOM, onValueChange: (v) => updateAt(index, { roomUnitId: v === NO_ROOM ? null : (v ?? null) }), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: NO_ROOM, children: merged.noRoom }), roomUnits.map((unit) => (_jsx(SelectItem, { value: unit.unitId,
|
|
56
74
|
// Only disable other rooms at-capacity — the room the
|
|
57
75
|
// traveler is *already* in should stay selectable so
|
|
58
76
|
// re-renders don't strip the selection.
|
|
59
77
|
disabled: unit.remainingCapacity <= 0 && traveler.roomUnitId !== unit.unitId, children: unit.unitName }, unit.unitId)))] })] })] })) : null] }), _jsx("div", { className: "flex justify-end", children: _jsxs(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7 text-destructive", onClick: () => removeAt(index), "aria-label": merged.remove, children: [_jsx(Trash2, { className: "mr-1 h-3.5 w-3.5" }), merged.remove] }) })] }, index))) }))] }));
|
|
60
78
|
}
|
|
79
|
+
function TravelerPersonPicker({ personId, labels, pinnedPeople = [], onSelect, onClear, }) {
|
|
80
|
+
const [search, setSearch] = React.useState("");
|
|
81
|
+
const [inputValue, setInputValue] = React.useState("");
|
|
82
|
+
const [sheetOpen, setSheetOpen] = React.useState(false);
|
|
83
|
+
const peopleQuery = usePeople({ search: search || undefined, limit: 20 });
|
|
84
|
+
const selectedPersonQuery = usePerson(personId ?? undefined, { enabled: Boolean(personId) });
|
|
85
|
+
const people = React.useMemo(() => {
|
|
86
|
+
const map = new Map();
|
|
87
|
+
for (const person of peopleQuery.data?.data ?? [])
|
|
88
|
+
map.set(person.id, person);
|
|
89
|
+
for (const person of pinnedPeople)
|
|
90
|
+
map.set(person.id, person);
|
|
91
|
+
if (selectedPersonQuery.data)
|
|
92
|
+
map.set(selectedPersonQuery.data.id, selectedPersonQuery.data);
|
|
93
|
+
return Array.from(map.values());
|
|
94
|
+
}, [peopleQuery.data?.data, pinnedPeople, selectedPersonQuery.data]);
|
|
95
|
+
const peopleMap = React.useMemo(() => new Map(people.map((person) => [person.id, person])), [people]);
|
|
96
|
+
const selectedLabel = personId ? formatPerson(peopleMap.get(personId)) : "";
|
|
97
|
+
React.useEffect(() => {
|
|
98
|
+
if (selectedLabel)
|
|
99
|
+
setInputValue(selectedLabel);
|
|
100
|
+
}, [selectedLabel]);
|
|
101
|
+
return (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx(Label, { className: "text-xs", children: labels.person }), _jsxs(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7", onClick: () => setSheetOpen(true), children: [_jsx(UserPlus, { className: "mr-1 h-3.5 w-3.5" }), labels.createNewPerson] })] }), _jsxs(Combobox, { items: people.map((person) => person.id), value: personId, inputValue: inputValue, autoHighlight: true, itemToStringValue: (id) => formatPerson(peopleMap.get(id)), onInputValueChange: (next) => {
|
|
102
|
+
setInputValue(next);
|
|
103
|
+
setSearch(next);
|
|
104
|
+
if (!next)
|
|
105
|
+
onClear();
|
|
106
|
+
}, onValueChange: (next) => {
|
|
107
|
+
const nextPerson = peopleMap.get(next ?? "");
|
|
108
|
+
if (nextPerson)
|
|
109
|
+
onSelect(nextPerson);
|
|
110
|
+
setInputValue(nextPerson ? formatPerson(nextPerson) : "");
|
|
111
|
+
}, children: [_jsx(ComboboxInput, { placeholder: labels.personSearchPlaceholder, showClear: !!personId }), _jsxs(ComboboxContent, { children: [_jsx(ComboboxEmpty, { children: labels.personEmpty }), _jsx(ComboboxList, { children: _jsx(ComboboxCollection, { children: (id) => {
|
|
112
|
+
const person = peopleMap.get(id);
|
|
113
|
+
if (!person)
|
|
114
|
+
return null;
|
|
115
|
+
return (_jsx(ComboboxItem, { value: person.id, children: _jsxs("div", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate font-medium", children: formatPersonName(person) }), person.email ? (_jsx("span", { className: "truncate text-xs text-muted-foreground", children: person.email })) : null] }) }, person.id));
|
|
116
|
+
} }) })] })] }), _jsx(Sheet, { open: sheetOpen, onOpenChange: setSheetOpen, children: _jsxs(SheetContent, { side: "right", size: "lg", children: [_jsx(SheetHeader, { children: _jsx(SheetTitle, { children: labels.createPersonSheetTitle }) }), _jsx(SheetBody, { children: _jsx(PersonForm, { mode: { kind: "create" }, onCancel: () => setSheetOpen(false), onSuccess: (saved) => {
|
|
117
|
+
onSelect(saved);
|
|
118
|
+
setInputValue(formatPerson(saved));
|
|
119
|
+
setSheetOpen(false);
|
|
120
|
+
} }) })] }) })] }));
|
|
121
|
+
}
|
|
122
|
+
function createTravelerFromPerson(person, role) {
|
|
123
|
+
return {
|
|
124
|
+
personId: person.id,
|
|
125
|
+
firstName: person.firstName,
|
|
126
|
+
lastName: person.lastName,
|
|
127
|
+
email: person.email ?? "",
|
|
128
|
+
role,
|
|
129
|
+
roomUnitId: null,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function formatPersonName(person) {
|
|
133
|
+
if (!person)
|
|
134
|
+
return "";
|
|
135
|
+
return [person.firstName, person.lastName].filter(Boolean).join(" ").trim();
|
|
136
|
+
}
|
|
137
|
+
function formatPerson(person) {
|
|
138
|
+
if (!person)
|
|
139
|
+
return "";
|
|
140
|
+
const name = formatPersonName(person);
|
|
141
|
+
return person.email ? `${name} · ${person.email}` : name;
|
|
142
|
+
}
|