@voyantjs/bookings-ui 0.35.0 → 0.37.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-combobox.d.ts +13 -0
- package/dist/components/booking-combobox.d.ts.map +1 -0
- package/dist/components/booking-combobox.js +44 -0
- package/dist/components/booking-create-dialog.d.ts +9 -0
- package/dist/components/booking-create-dialog.d.ts.map +1 -1
- package/dist/components/booking-create-dialog.js +110 -90
- package/dist/components/booking-create-page.d.ts +11 -0
- package/dist/components/booking-create-page.d.ts.map +1 -0
- package/dist/components/booking-create-page.js +11 -0
- package/dist/components/booking-detail-page.d.ts.map +1 -1
- package/dist/components/booking-detail-page.js +4 -1
- package/dist/components/booking-item-list.d.ts.map +1 -1
- package/dist/components/booking-item-list.js +5 -3
- package/dist/components/booking-list-filters.d.ts +35 -0
- package/dist/components/booking-list-filters.d.ts.map +1 -0
- package/dist/components/booking-list-filters.js +148 -0
- package/dist/components/booking-list.d.ts.map +1 -1
- package/dist/components/booking-list.js +30 -46
- package/dist/components/person-picker-section.d.ts +15 -7
- package/dist/components/person-picker-section.d.ts.map +1 -1
- package/dist/components/person-picker-section.js +100 -21
- package/dist/components/price-breakdown-section.d.ts +16 -1
- package/dist/components/price-breakdown-section.d.ts.map +1 -1
- package/dist/components/price-breakdown-section.js +36 -5
- package/dist/components/product-picker-section.d.ts.map +1 -1
- package/dist/components/product-picker-section.js +38 -4
- package/dist/components/shared-room-section.d.ts +9 -8
- package/dist/components/shared-room-section.d.ts.map +1 -1
- package/dist/components/shared-room-section.js +67 -14
- package/dist/components/traveler-list.d.ts.map +1 -1
- package/dist/components/traveler-list.js +27 -16
- package/dist/i18n/en.d.ts +284 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +300 -17
- package/dist/i18n/messages.d.ts +255 -1
- package/dist/i18n/messages.d.ts.map +1 -1
- package/dist/i18n/provider.d.ts +568 -2
- package/dist/i18n/provider.d.ts.map +1 -1
- package/dist/i18n/ro.d.ts +284 -1
- package/dist/i18n/ro.d.ts.map +1 -1
- package/dist/i18n/ro.js +301 -18
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/journey/components/booking-journey.d.ts.map +1 -1
- package/dist/journey/components/booking-journey.js +22 -13
- package/dist/journey/components/contract-preview-dialog.d.ts.map +1 -1
- package/dist/journey/components/contract-preview-dialog.js +9 -4
- package/dist/journey/components/journey-steps.d.ts.map +1 -1
- package/dist/journey/components/journey-steps.js +94 -72
- package/dist/journey/components/side-panel.d.ts.map +1 -1
- package/dist/journey/components/side-panel.js +58 -35
- package/dist/journey/components/step-header.d.ts.map +1 -1
- package/dist/journey/components/step-header.js +3 -19
- package/package.json +24 -20
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { usePricingPreview } from "@voyantjs/bookings-react";
|
|
4
|
-
import { Label } from "@voyantjs/ui/components";
|
|
4
|
+
import { Button, Label, Textarea } from "@voyantjs/ui/components";
|
|
5
|
+
import { CurrencyInput } from "@voyantjs/ui/components/currency-input";
|
|
5
6
|
import * as React from "react";
|
|
6
7
|
import { useBookingsUiI18nOrDefault, useBookingsUiMessagesOrDefault } from "../i18n/provider.js";
|
|
7
8
|
/**
|
|
@@ -32,7 +33,7 @@ function matchTier(tiers, qty) {
|
|
|
32
33
|
* - `free` / `included` — render 0.00 without an on-request badge.
|
|
33
34
|
* - `on_request` / anything else — render "On request"; total excludes it.
|
|
34
35
|
*/
|
|
35
|
-
export function PriceBreakdownSection({ productId, optionId, unitQuantities, catalogId, labels, }) {
|
|
36
|
+
export function PriceBreakdownSection({ productId, optionId, unitQuantities, catalogId, labels, onChange, }) {
|
|
36
37
|
const { formatCurrency, formatNumber } = useBookingsUiI18nOrDefault();
|
|
37
38
|
const messages = useBookingsUiMessagesOrDefault();
|
|
38
39
|
const merged = { ...messages.priceBreakdownSection.labels, ...labels };
|
|
@@ -42,6 +43,14 @@ export function PriceBreakdownSection({ productId, optionId, unitQuantities, cat
|
|
|
42
43
|
catalogId: catalogId ?? null,
|
|
43
44
|
enabled: Boolean(productId),
|
|
44
45
|
});
|
|
46
|
+
const quantitiesKey = React.useMemo(() => JSON.stringify(unitQuantities), [unitQuantities]);
|
|
47
|
+
const [manualAmountCents, setManualAmountCents] = React.useState(null);
|
|
48
|
+
const [overrideReason, setOverrideReason] = React.useState("");
|
|
49
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: reset manual confirmation when the priced selection changes
|
|
50
|
+
React.useEffect(() => {
|
|
51
|
+
setManualAmountCents(null);
|
|
52
|
+
setOverrideReason("");
|
|
53
|
+
}, [productId, optionId, catalogId, quantitiesKey]);
|
|
45
54
|
const snapshot = preview.data?.data;
|
|
46
55
|
const currency = snapshot?.catalog.currencyCode ?? null;
|
|
47
56
|
const formatAmount = React.useCallback((cents) => currency
|
|
@@ -147,16 +156,38 @@ export function PriceBreakdownSection({ productId, optionId, unitQuantities, cat
|
|
|
147
156
|
}
|
|
148
157
|
return { lines: out, total: anyOnRequest ? null : runningTotal };
|
|
149
158
|
}, [snapshot, unitQuantities, merged.onRequest, merged.groupRate]);
|
|
159
|
+
const confirmedAmountCents = manualAmountCents ?? total;
|
|
160
|
+
const isManualOverride = manualAmountCents != null && (total === null || manualAmountCents !== total);
|
|
161
|
+
const requiresReason = isManualOverride && overrideReason.trim().length === 0;
|
|
162
|
+
React.useEffect(() => {
|
|
163
|
+
onChange?.({
|
|
164
|
+
catalogAmountCents: total,
|
|
165
|
+
confirmedAmountCents,
|
|
166
|
+
currency,
|
|
167
|
+
priceOverrideReason: overrideReason,
|
|
168
|
+
isManualOverride,
|
|
169
|
+
requiresReason,
|
|
170
|
+
});
|
|
171
|
+
}, [
|
|
172
|
+
confirmedAmountCents,
|
|
173
|
+
currency,
|
|
174
|
+
isManualOverride,
|
|
175
|
+
onChange,
|
|
176
|
+
overrideReason,
|
|
177
|
+
requiresReason,
|
|
178
|
+
total,
|
|
179
|
+
]);
|
|
180
|
+
const manualTotalControls = (_jsxs("div", { className: "flex flex-col gap-2 border-t pt-2", children: [_jsxs("div", { className: "grid gap-2 sm:grid-cols-[minmax(0,1fr)_auto] sm:items-end", children: [_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: merged.manualTotal }), _jsx(CurrencyInput, { value: manualAmountCents, onChange: setManualAmountCents, currency: currency ?? undefined, placeholder: total === null ? merged.onRequest : formatAmount(total) })] }), manualAmountCents != null ? (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setManualAmountCents(null), children: merged.useCatalogTotal })) : null] }), isManualOverride ? (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: merged.overrideReason }), _jsx(Textarea, { value: overrideReason, onChange: (event) => setOverrideReason(event.target.value), placeholder: merged.overrideReasonPlaceholder, rows: 2 }), requiresReason ? (_jsx("p", { className: "text-xs text-destructive", children: merged.overrideReasonRequired })) : null] })) : null] }));
|
|
150
181
|
// Empty states
|
|
151
182
|
if (!productId)
|
|
152
183
|
return null;
|
|
153
184
|
if (preview.isError || (preview.isSuccess && !snapshot)) {
|
|
154
|
-
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.noPricing })] }));
|
|
185
|
+
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.noPricing }), manualTotalControls] }));
|
|
155
186
|
}
|
|
156
187
|
if (lines.length === 0) {
|
|
157
|
-
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.empty })] }));
|
|
188
|
+
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.empty }), manualTotalControls] }));
|
|
158
189
|
}
|
|
159
190
|
return (_jsxs("div", { className: "flex flex-col gap-2 rounded-md border p-3", children: [_jsx(Label, { children: merged.heading }), _jsx("div", { className: "flex flex-col gap-1.5", children: lines.map((line) => (_jsxs("div", { className: "flex items-baseline justify-between text-sm", children: [_jsxs("div", { className: "flex items-baseline gap-2", children: [_jsxs("span", { className: "tabular-nums", children: [formatNumber(line.quantity), "x"] }), _jsx("span", { children: line.label }), line.tierLabel ? (_jsxs("span", { className: "text-xs text-muted-foreground", children: ["\u00B7 ", line.tierLabel] })) : null] }), _jsx("div", { className: "tabular-nums", children: line.totalAmountCents === null
|
|
160
191
|
? merged.onRequest
|
|
161
|
-
: formatAmount(line.totalAmountCents) })] }, line.unitId))) }), _jsxs("div", { className: "mt-1 flex items-baseline justify-between border-t pt-2 text-sm font-medium", children: [_jsx("span", { children: merged.total }), _jsx("span", { className: "tabular-nums", children: total === null ? merged.onRequest : formatAmount(total) })] })] }));
|
|
192
|
+
: formatAmount(line.totalAmountCents) })] }, line.unitId))) }), _jsxs("div", { className: "mt-1 flex items-baseline justify-between border-t pt-2 text-sm font-medium", children: [_jsx("span", { children: merged.total }), _jsx("span", { className: "tabular-nums", children: total === null ? merged.onRequest : formatAmount(total) })] }), _jsxs("div", { className: "flex items-baseline justify-between text-sm font-medium", children: [_jsx("span", { children: merged.confirmedTotal }), _jsx("span", { className: "tabular-nums", children: confirmedAmountCents === null ? merged.onRequest : formatAmount(confirmedAmountCents) })] }), manualTotalControls] }));
|
|
162
193
|
}
|
|
@@ -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":"AA8BA,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,2CAgI3B"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { useProductOptions, useProducts } from "@voyantjs/products-react";
|
|
4
|
-
import {
|
|
3
|
+
import { useProduct, useProductOptions, useProducts, } from "@voyantjs/products-react";
|
|
4
|
+
import { Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyantjs/ui/components";
|
|
5
|
+
import { Combobox, ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from "@voyantjs/ui/components/combobox";
|
|
5
6
|
import * as React from "react";
|
|
6
7
|
import { useBookingsUiMessagesOrDefault } from "../i18n/provider.js";
|
|
7
8
|
const OPTION_NONE = "__none__";
|
|
@@ -19,14 +20,47 @@ export function ProductPickerSection({ value, onChange, enabled = true, lockProd
|
|
|
19
20
|
limit: 20,
|
|
20
21
|
enabled: enabled && !lockProduct,
|
|
21
22
|
});
|
|
22
|
-
const
|
|
23
|
+
const selectedProductQuery = useProduct(value.productId || undefined, {
|
|
24
|
+
enabled: enabled && Boolean(value.productId),
|
|
25
|
+
});
|
|
26
|
+
const products = React.useMemo(() => {
|
|
27
|
+
const map = new Map();
|
|
28
|
+
for (const product of productsData?.data ?? [])
|
|
29
|
+
map.set(product.id, product);
|
|
30
|
+
if (selectedProductQuery.data)
|
|
31
|
+
map.set(selectedProductQuery.data.id, selectedProductQuery.data);
|
|
32
|
+
return Array.from(map.values());
|
|
33
|
+
}, [productsData?.data, selectedProductQuery.data]);
|
|
34
|
+
const productMap = React.useMemo(() => new Map(products.map((product) => [product.id, product])), [products]);
|
|
35
|
+
const selectedProductLabel = value.productId ? (productMap.get(value.productId)?.name ?? "") : "";
|
|
36
|
+
const [productInputValue, setProductInputValue] = React.useState(selectedProductLabel);
|
|
37
|
+
React.useEffect(() => {
|
|
38
|
+
if (selectedProductLabel)
|
|
39
|
+
setProductInputValue(selectedProductLabel);
|
|
40
|
+
}, [selectedProductLabel]);
|
|
23
41
|
const { data: optionsData } = useProductOptions({
|
|
24
42
|
productId: value.productId || undefined,
|
|
25
43
|
limit: 50,
|
|
26
44
|
enabled: enabled && Boolean(value.productId),
|
|
27
45
|
});
|
|
28
46
|
const options = optionsData?.data ?? [];
|
|
29
|
-
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: "*" })] }),
|
|
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, itemToStringValue: (id) => productMap.get(id)?.name ?? "", onInputValueChange: (next) => {
|
|
48
|
+
setProductInputValue(next);
|
|
49
|
+
setProductSearch(next);
|
|
50
|
+
if (!next)
|
|
51
|
+
onChange({ productId: "", optionId: null });
|
|
52
|
+
}, onValueChange: (next) => {
|
|
53
|
+
const productId = next ?? "";
|
|
54
|
+
onChange({ productId, optionId: null });
|
|
55
|
+
setProductInputValue(productId ? (productMap.get(productId)?.name ?? "") : "");
|
|
56
|
+
}, 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
|
+
const product = productMap.get(id);
|
|
58
|
+
if (!product)
|
|
59
|
+
return null;
|
|
60
|
+
return (_jsx(ComboboxItem, { value: product.id, children: _jsxs("div", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate font-medium", children: product.name }), _jsxs("span", { className: "truncate text-xs text-muted-foreground", children: [product.sellCurrency, product.sellAmountCents != null
|
|
61
|
+
? ` · ${product.sellAmountCents / 100}`
|
|
62
|
+
: ""] })] }) }, product.id));
|
|
63
|
+
} }) })] })] })] })), value.productId && options.length > 0 && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: merged.option }), _jsxs(Select, { items: [
|
|
30
64
|
{ label: merged.optionNone, value: OPTION_NONE },
|
|
31
65
|
...options.map((o) => ({ label: o.name, value: o.id })),
|
|
32
66
|
], value: value.optionId ?? OPTION_NONE, onValueChange: (v) => onChange({
|
|
@@ -4,6 +4,8 @@ export interface SharedRoomValue {
|
|
|
4
4
|
mode: SharedRoomMode;
|
|
5
5
|
/** Only meaningful in "join" mode. */
|
|
6
6
|
groupId: string;
|
|
7
|
+
/** Optional label used when creating a new shared-room group. */
|
|
8
|
+
groupLabel?: string;
|
|
7
9
|
}
|
|
8
10
|
export declare const emptySharedRoomValue: SharedRoomValue;
|
|
9
11
|
export interface SharedRoomSectionProps {
|
|
@@ -11,7 +13,7 @@ export interface SharedRoomSectionProps {
|
|
|
11
13
|
onChange: (value: SharedRoomValue) => void;
|
|
12
14
|
/**
|
|
13
15
|
* The product context for fetching joinable groups. When unset, the join
|
|
14
|
-
*
|
|
16
|
+
* combobox is disabled even if the user toggles into join mode.
|
|
15
17
|
*/
|
|
16
18
|
productId?: string;
|
|
17
19
|
enabled?: boolean;
|
|
@@ -22,16 +24,15 @@ export interface SharedRoomSectionProps {
|
|
|
22
24
|
selectPlaceholder?: string;
|
|
23
25
|
noGroups?: string;
|
|
24
26
|
createHint?: string;
|
|
27
|
+
createSheetTitle?: string;
|
|
28
|
+
groupLabel?: string;
|
|
29
|
+
groupLabelPlaceholder?: string;
|
|
30
|
+
createAction?: string;
|
|
25
31
|
};
|
|
26
32
|
}
|
|
27
33
|
/**
|
|
28
|
-
* Shared-room (partaj) attachment section. Operators
|
|
29
|
-
*
|
|
30
|
-
* that already has a primary booking.
|
|
31
|
-
*
|
|
32
|
-
* The section only handles the *selection* — the parent dialog owns the
|
|
33
|
-
* create-group + add-member mutations because they fire *after* the booking
|
|
34
|
-
* insert (we need the new booking id to attach).
|
|
34
|
+
* Shared-room (partaj) attachment section. Operators can create a new group in
|
|
35
|
+
* a sheet or join an existing product-scoped group with an async combobox.
|
|
35
36
|
*/
|
|
36
37
|
export declare function SharedRoomSection({ value, onChange, productId, enabled, labels, }: SharedRoomSectionProps): import("react/jsx-runtime").JSX.Element;
|
|
37
38
|
//# sourceMappingURL=shared-room-section.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shared-room-section.d.ts","sourceRoot":"","sources":["../../src/components/shared-room-section.tsx"],"names":[],"mappings":"
|
|
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;KACtB,CAAA;CACF;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,KAAK,EACL,QAAQ,EACR,SAAS,EACT,OAAc,EACd,MAAM,GACP,EAAE,sBAAsB,2CAyJxB"}
|
|
@@ -1,34 +1,87 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useBookingGroups } from "@voyantjs/bookings-react";
|
|
4
|
-
import { Button, Label,
|
|
4
|
+
import { Button, Input, Label, Sheet, SheetBody, SheetContent, SheetFooter, SheetHeader, SheetTitle, } from "@voyantjs/ui/components";
|
|
5
|
+
import { Combobox, ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from "@voyantjs/ui/components/combobox";
|
|
6
|
+
import { Link2, Plus } from "lucide-react";
|
|
7
|
+
import * as React from "react";
|
|
5
8
|
import { useBookingsUiMessagesOrDefault } from "../i18n/provider.js";
|
|
6
|
-
const GROUP_NONE = "__none__";
|
|
7
9
|
export const emptySharedRoomValue = {
|
|
8
10
|
enabled: false,
|
|
9
11
|
mode: "create",
|
|
10
12
|
groupId: "",
|
|
13
|
+
groupLabel: "",
|
|
11
14
|
};
|
|
12
15
|
/**
|
|
13
|
-
* Shared-room (partaj) attachment section. Operators
|
|
14
|
-
*
|
|
15
|
-
* that already has a primary booking.
|
|
16
|
-
*
|
|
17
|
-
* The section only handles the *selection* — the parent dialog owns the
|
|
18
|
-
* create-group + add-member mutations because they fire *after* the booking
|
|
19
|
-
* insert (we need the new booking id to attach).
|
|
16
|
+
* Shared-room (partaj) attachment section. Operators can create a new group in
|
|
17
|
+
* a sheet or join an existing product-scoped group with an async combobox.
|
|
20
18
|
*/
|
|
21
19
|
export function SharedRoomSection({ value, onChange, productId, enabled = true, labels, }) {
|
|
20
|
+
const [groupSearch, setGroupSearch] = React.useState("");
|
|
21
|
+
const [groupInputValue, setGroupInputValue] = React.useState("");
|
|
22
|
+
const [createSheetOpen, setCreateSheetOpen] = React.useState(false);
|
|
23
|
+
const [draftGroupLabel, setDraftGroupLabel] = React.useState(value.groupLabel ?? "");
|
|
22
24
|
const messages = useBookingsUiMessagesOrDefault();
|
|
23
25
|
const merged = { ...messages.sharedRoomSection.labels, ...labels };
|
|
24
26
|
const { data: groupsData } = useBookingGroups({
|
|
27
|
+
kind: "shared_room",
|
|
25
28
|
productId: productId || undefined,
|
|
26
29
|
limit: 50,
|
|
27
30
|
enabled: enabled && value.enabled && value.mode === "join" && Boolean(productId),
|
|
28
31
|
});
|
|
29
|
-
const existingGroups =
|
|
32
|
+
const existingGroups = React.useMemo(() => {
|
|
33
|
+
const normalizedSearch = groupSearch.trim().toLowerCase();
|
|
34
|
+
const groups = groupsData?.data ?? [];
|
|
35
|
+
if (!normalizedSearch)
|
|
36
|
+
return groups;
|
|
37
|
+
return groups.filter((group) => group.label.toLowerCase().includes(normalizedSearch));
|
|
38
|
+
}, [groupsData?.data, groupSearch]);
|
|
39
|
+
const groupsMap = React.useMemo(() => new Map((groupsData?.data ?? []).map((group) => [group.id, group])), [groupsData?.data]);
|
|
40
|
+
const selectedGroupLabel = value.groupId ? (groupsMap.get(value.groupId)?.label ?? "") : "";
|
|
41
|
+
React.useEffect(() => {
|
|
42
|
+
if (selectedGroupLabel)
|
|
43
|
+
setGroupInputValue(selectedGroupLabel);
|
|
44
|
+
}, [selectedGroupLabel]);
|
|
45
|
+
React.useEffect(() => {
|
|
46
|
+
if (createSheetOpen)
|
|
47
|
+
setDraftGroupLabel(value.groupLabel ?? "");
|
|
48
|
+
}, [createSheetOpen, value.groupLabel]);
|
|
30
49
|
const set = (patch) => onChange({ ...value, ...patch });
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
50
|
+
const selectCreateMode = () => {
|
|
51
|
+
if (!enabled)
|
|
52
|
+
return;
|
|
53
|
+
setCreateSheetOpen(true);
|
|
54
|
+
};
|
|
55
|
+
const selectJoinMode = () => {
|
|
56
|
+
if (!enabled)
|
|
57
|
+
return;
|
|
58
|
+
set({ enabled: true, mode: "join", groupLabel: "" });
|
|
59
|
+
};
|
|
60
|
+
const saveCreateLabel = () => {
|
|
61
|
+
set({
|
|
62
|
+
enabled: true,
|
|
63
|
+
mode: "create",
|
|
64
|
+
groupId: "",
|
|
65
|
+
groupLabel: draftGroupLabel.trim(),
|
|
66
|
+
});
|
|
67
|
+
setCreateSheetOpen(false);
|
|
68
|
+
};
|
|
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) => {
|
|
70
|
+
setGroupInputValue(next);
|
|
71
|
+
setGroupSearch(next);
|
|
72
|
+
if (!next)
|
|
73
|
+
set({ groupId: "" });
|
|
74
|
+
}, onValueChange: (next) => {
|
|
75
|
+
const groupId = next ?? "";
|
|
76
|
+
set({ groupId });
|
|
77
|
+
setGroupInputValue(groupId ? (groupsMap.get(groupId)?.label ?? "") : "");
|
|
78
|
+
}, children: [_jsx(ComboboxInput, { placeholder: merged.selectPlaceholder, showClear: !!value.groupId }), _jsxs(ComboboxContent, { children: [_jsx(ComboboxEmpty, { children: merged.noGroups }), _jsx(ComboboxList, { children: _jsx(ComboboxCollection, { children: (id) => {
|
|
79
|
+
const group = groupsMap.get(id);
|
|
80
|
+
if (!group)
|
|
81
|
+
return null;
|
|
82
|
+
return _jsx(SharedRoomGroupItem, { group: group }, group.id);
|
|
83
|
+
} }) })] })] })) : null] }), _jsx(Sheet, { open: createSheetOpen, onOpenChange: setCreateSheetOpen, children: _jsxs(SheetContent, { side: "right", size: "default", children: [_jsx(SheetHeader, { children: _jsx(SheetTitle, { children: merged.createSheetTitle }) }), _jsx(SheetBody, { children: _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { htmlFor: "shared-room-group-label", children: merged.groupLabel }), _jsx(Input, { id: "shared-room-group-label", value: draftGroupLabel, onChange: (event) => setDraftGroupLabel(event.target.value), placeholder: merged.groupLabelPlaceholder }), _jsx("p", { className: "text-xs text-muted-foreground", children: merged.createHint })] }) }), _jsxs(SheetFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => setCreateSheetOpen(false), children: messages.common.cancel }), _jsx(Button, { type: "button", onClick: saveCreateLabel, children: merged.createAction })] })] }) })] }));
|
|
84
|
+
}
|
|
85
|
+
function SharedRoomGroupItem({ group }) {
|
|
86
|
+
return (_jsx(ComboboxItem, { value: group.id, children: _jsx("span", { className: "truncate font-medium", children: group.label }) }));
|
|
34
87
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"traveler-list.d.ts","sourceRoot":"","sources":["../../src/components/traveler-list.tsx"],"names":[],"mappings":"AAkBA,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,wBAAgB,YAAY,CAAC,EAAE,SAAS,EAAE,UAAkB,EAAE,EAAE,iBAAiB,
|
|
1
|
+
{"version":3,"file":"traveler-list.d.ts","sourceRoot":"","sources":["../../src/components/traveler-list.tsx"],"names":[],"mappings":"AAkBA,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,wBAAgB,YAAY,CAAC,EAAE,SAAS,EAAE,UAAkB,EAAE,EAAE,iBAAiB,2CAqIhF"}
|
|
@@ -4,7 +4,7 @@ import { useBookingTravelerDocuments, useRevealTraveler, useTravelerMutation, us
|
|
|
4
4
|
import { Button, Card, CardContent, CardHeader, CardTitle } from "@voyantjs/ui/components";
|
|
5
5
|
import { Eye, EyeOff, Loader2, Pencil, Plus, Trash2, Users } from "lucide-react";
|
|
6
6
|
import * as React from "react";
|
|
7
|
-
import { useBookingsUiMessagesOrDefault } from "../i18n/provider.js";
|
|
7
|
+
import { formatMessage, useBookingsUiMessagesOrDefault } from "../i18n/provider.js";
|
|
8
8
|
import { TravelerDialog } from "./traveler-dialog.js";
|
|
9
9
|
export function TravelerList({ bookingId, autoReveal = false }) {
|
|
10
10
|
const [dialogOpen, setDialogOpen] = React.useState(false);
|
|
@@ -47,7 +47,7 @@ export function TravelerList({ bookingId, autoReveal = false }) {
|
|
|
47
47
|
return (_jsxs(Card, { "data-slot": "traveler-list", children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between", children: [_jsxs(CardTitle, { className: "flex items-center gap-2", children: [_jsx(Users, { className: "h-4 w-4" }), messages.travelerList.title] }), _jsxs(Button, { size: "sm", onClick: () => {
|
|
48
48
|
setEditing(undefined);
|
|
49
49
|
setDialogOpen(true);
|
|
50
|
-
}, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), messages.travelerList.addTraveler] })] }), _jsx(CardContent, { children: travelers.length === 0 ? (_jsx("p", { className: "py-4 text-center text-sm text-muted-foreground", children: messages.travelerList.empty })) : (_jsx("div", { className: "overflow-x-auto rounded border bg-background", children: _jsxs("table", { className: "w-full min-w-[980px] text-sm", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b text-muted-foreground", children: [_jsx("th", { className: "p-2 text-left font-medium", children: messages.travelerList.columns.name }), _jsx("th", { className: "p-2 text-left font-medium", children: messages.travelerList.columns.email }), _jsx("th", { className: "p-2 text-left font-medium", children: messages.travelerList.columns.phone }), _jsx("th", { className: "p-2 text-left font-medium", children:
|
|
50
|
+
}, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), messages.travelerList.addTraveler] })] }), _jsx(CardContent, { children: travelers.length === 0 ? (_jsx("p", { className: "py-4 text-center text-sm text-muted-foreground", children: messages.travelerList.empty })) : (_jsx("div", { className: "overflow-x-auto rounded border bg-background", children: _jsxs("table", { className: "w-full min-w-[980px] text-sm", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b text-muted-foreground", children: [_jsx("th", { className: "p-2 text-left font-medium", children: messages.travelerList.columns.name }), _jsx("th", { className: "p-2 text-left font-medium", children: messages.travelerList.columns.email }), _jsx("th", { className: "p-2 text-left font-medium", children: messages.travelerList.columns.phone }), _jsx("th", { className: "p-2 text-left font-medium", children: messages.travelerList.columns.role }), _jsx("th", { className: "p-2 text-left font-medium", children: messages.travelerList.columns.dobAge }), _jsx("th", { className: "p-2 text-left font-medium", children: messages.travelerList.columns.documents }), _jsx("th", { className: "w-20 p-2" })] }) }), _jsx("tbody", { children: travelers.map((traveler) => (_jsx(TravelerRow, { bookingId: bookingId, traveler: traveler, documents: documentsByTraveler.get(traveler.id) ?? [], revealed: autoReveal || revealedIds.has(traveler.id), onToggleReveal: autoReveal || allAlreadyRevealed ? undefined : () => toggleReveal(traveler.id), emailUnavailable: messages.travelerList.values.emailUnavailable, phoneUnavailable: messages.travelerList.values.phoneUnavailable, onEdit: () => {
|
|
51
51
|
setEditing(traveler);
|
|
52
52
|
setDialogOpen(true);
|
|
53
53
|
}, onDelete: () => {
|
|
@@ -69,6 +69,7 @@ export function TravelerList({ bookingId, autoReveal = false }) {
|
|
|
69
69
|
* the server, so toggling the eye button creates a permanent record.
|
|
70
70
|
*/
|
|
71
71
|
function TravelerRow({ bookingId, traveler, documents, revealed, onToggleReveal, emailUnavailable, phoneUnavailable, onEdit, onDelete, }) {
|
|
72
|
+
const messages = useBookingsUiMessagesOrDefault();
|
|
72
73
|
const reveal = useRevealTraveler(bookingId, traveler.id, { enabled: revealed });
|
|
73
74
|
// Use the revealed copy when available; otherwise fall back to
|
|
74
75
|
// the masked row from the list endpoint. This keeps the UI snappy
|
|
@@ -79,29 +80,39 @@ function TravelerRow({ bookingId, traveler, documents, revealed, onToggleReveal,
|
|
|
79
80
|
const travelDetails = revealed && revealedTraveler ? revealedTraveler.travelDetails : null;
|
|
80
81
|
const showLoading = revealed && reveal.isLoading;
|
|
81
82
|
const revealError = revealed && reveal.error;
|
|
82
|
-
return (_jsxs(_Fragment, { children: [_jsxs("tr", { className: "border-b", children: [_jsx("td", { className: "p-2", children: showLoading ? (_jsx(RowLoading, {})) : (`${display.firstName ?? ""} ${display.lastName ?? ""}`.trim()) }), _jsx("td", { className: "p-2", children: showLoading ? _jsx(RowLoading, {}) : (display.email ?? emailUnavailable) }), _jsx("td", { className: "p-2", children: showLoading ? _jsx(RowLoading, {}) : (display.phone ?? phoneUnavailable) }), _jsx("td", { className: "p-2", children: _jsxs("div", { className: "flex flex-wrap gap-1.5", children: [display.isPrimary ? _jsx(MiniPill, { children:
|
|
83
|
+
return (_jsxs(_Fragment, { children: [_jsxs("tr", { className: "border-b", children: [_jsx("td", { className: "p-2", children: showLoading ? (_jsx(RowLoading, {})) : (`${display.firstName ?? ""} ${display.lastName ?? ""}`.trim()) }), _jsx("td", { className: "p-2", children: showLoading ? _jsx(RowLoading, {}) : (display.email ?? emailUnavailable) }), _jsx("td", { className: "p-2", children: showLoading ? _jsx(RowLoading, {}) : (display.phone ?? phoneUnavailable) }), _jsx("td", { className: "p-2", children: _jsxs("div", { className: "flex flex-wrap gap-1.5", children: [display.isPrimary ? _jsx(MiniPill, { children: messages.travelerList.roles.primary }) : null, travelDetails?.isLeadTraveler ? (_jsx(MiniPill, { children: messages.travelerList.roles.lead })) : null, display.travelerCategory ? _jsx(MiniPill, { children: display.travelerCategory }) : null] }) }), _jsx("td", { className: "p-2", children: showLoading ? (_jsx(RowLoading, {})) : (formatDobAge(travelDetails?.dateOfBirth, messages.travelerList.values.fieldUnavailable)) }), _jsx("td", { className: "p-2", children: documents.length > 0 ? (_jsxs("div", { className: "flex flex-wrap gap-1.5", children: [documents.slice(0, 2).map((document) => (_jsx(MiniPill, { children: document.type.replaceAll("_", " ") }, document.id))), documents.length > 2 ? _jsxs(MiniPill, { children: ["+", documents.length - 2] }) : null] })) : (_jsx("span", { className: "text-muted-foreground", children: messages.travelerList.values.documentsUnavailable })) }), _jsxs("td", { className: "p-2", children: [_jsxs("div", { className: "flex items-center gap-1", children: [onToggleReveal ? (_jsx("button", { type: "button", onClick: onToggleReveal, className: "text-muted-foreground hover:text-foreground", title: revealed
|
|
84
|
+
? messages.travelerList.actions.hideContactDetails
|
|
85
|
+
: messages.travelerList.actions.revealContactDetails, "aria-label": revealed
|
|
86
|
+
? messages.travelerList.actions.hideTravelerContactDetails
|
|
87
|
+
: messages.travelerList.actions.revealTravelerContactDetails, children: revealed ? _jsx(EyeOff, { className: "h-3.5 w-3.5" }) : _jsx(Eye, { className: "h-3.5 w-3.5" }) })) : null, _jsx("button", { type: "button", onClick: onEdit, className: "text-muted-foreground hover:text-foreground", "aria-label": messages.travelerList.actions.editTraveler, children: _jsx(Pencil, { className: "h-3.5 w-3.5" }) }), _jsx("button", { type: "button", onClick: onDelete, className: "text-muted-foreground hover:text-destructive", "aria-label": messages.travelerList.actions.deleteTraveler, children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) })] }), revealError ? (_jsx("div", { className: "mt-1 text-[10px] text-destructive", children: revealError instanceof Error
|
|
88
|
+
? revealError.message
|
|
89
|
+
: messages.travelerList.validation.revealFailed })) : null] })] }), _jsx("tr", { className: "border-b last:border-b-0", children: _jsx("td", { colSpan: 7, className: "bg-muted/20 px-2 py-3", children: _jsx(TravelerContextGrid, { traveler: display, travelDetails: travelDetails, documents: documents, loading: showLoading }) }) })] }));
|
|
83
90
|
}
|
|
84
91
|
function RowLoading() {
|
|
85
|
-
|
|
92
|
+
const messages = useBookingsUiMessagesOrDefault();
|
|
93
|
+
return (_jsxs("span", { className: "inline-flex items-center gap-1.5 text-muted-foreground", children: [_jsx(Loader2, { className: "h-3 w-3 animate-spin" }), _jsx("span", { className: "text-xs", children: messages.travelerList.loading.decrypting })] }));
|
|
86
94
|
}
|
|
87
95
|
function TravelerContextGrid({ traveler, travelDetails, documents, loading, }) {
|
|
96
|
+
const messages = useBookingsUiMessagesOrDefault();
|
|
88
97
|
if (loading)
|
|
89
98
|
return _jsx(RowLoading, {});
|
|
90
99
|
const fields = [
|
|
91
|
-
[
|
|
92
|
-
[
|
|
93
|
-
[
|
|
94
|
-
[
|
|
95
|
-
[
|
|
96
|
-
[
|
|
97
|
-
[
|
|
98
|
-
[
|
|
100
|
+
[messages.travelerList.context.nationality, travelDetails?.nationality],
|
|
101
|
+
[messages.travelerList.context.passport, travelDetails?.passportNumber],
|
|
102
|
+
[messages.travelerList.context.passportExpiry, formatDateValue(travelDetails?.passportExpiry)],
|
|
103
|
+
[messages.travelerList.context.language, traveler.preferredLanguage],
|
|
104
|
+
[messages.travelerList.context.dietary, travelDetails?.dietaryRequirements],
|
|
105
|
+
[messages.travelerList.context.accessibility, travelDetails?.accessibilityNeeds],
|
|
106
|
+
[messages.travelerList.context.specialRequests, traveler.specialRequests],
|
|
107
|
+
[messages.travelerList.context.notes, traveler.notes],
|
|
99
108
|
];
|
|
100
109
|
const visibleFields = fields.filter(([, value]) => Boolean(value));
|
|
101
110
|
if (visibleFields.length === 0 && documents.length === 0) {
|
|
102
|
-
return _jsx("span", { className: "text-xs text-muted-foreground", children:
|
|
111
|
+
return (_jsx("span", { className: "text-xs text-muted-foreground", children: messages.travelerList.values.noAdditionalContext }));
|
|
103
112
|
}
|
|
104
|
-
return (_jsxs("div", { className: "grid gap-3 md:grid-cols-4", children: [visibleFields.map(([label, value]) => (_jsx(DetailField, { label: label, value: value ??
|
|
113
|
+
return (_jsxs("div", { className: "grid gap-3 md:grid-cols-4", children: [visibleFields.map(([label, value]) => (_jsx(DetailField, { label: label, value: value ?? messages.travelerList.values.fieldUnavailable }, label))), documents.map((document) => (_jsx(DetailField, { label: formatMessage(messages.travelerList.context.documentLabel, {
|
|
114
|
+
type: document.type.replaceAll("_", " "),
|
|
115
|
+
}), value: document.fileName }, document.id)))] }));
|
|
105
116
|
}
|
|
106
117
|
function DetailField({ label, value }) {
|
|
107
118
|
return (_jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "text-[10px] font-medium uppercase text-muted-foreground", children: label }), _jsx("div", { className: "truncate text-xs text-foreground", children: value })] }));
|
|
@@ -109,9 +120,9 @@ function DetailField({ label, value }) {
|
|
|
109
120
|
function MiniPill({ children }) {
|
|
110
121
|
return (_jsx("span", { className: "inline-flex h-5 items-center rounded-full border px-2 text-[11px] capitalize text-muted-foreground", children: children }));
|
|
111
122
|
}
|
|
112
|
-
function formatDobAge(value) {
|
|
123
|
+
function formatDobAge(value, unavailable) {
|
|
113
124
|
if (!value)
|
|
114
|
-
return
|
|
125
|
+
return unavailable;
|
|
115
126
|
const date = new Date(value);
|
|
116
127
|
if (Number.isNaN(date.getTime()))
|
|
117
128
|
return value;
|