@voyantjs/bookings-ui 0.80.17 → 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 -79
- 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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useAdminBookingPayments, usePublicBookingPayments } from "@voyantjs/finance-react";
|
|
4
4
|
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Badge, Button, Card, CardContent, CardHeader, CardTitle, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@voyantjs/ui/components";
|
|
5
|
-
import { Banknote, CreditCard, Eye, MoreHorizontal, Pencil, Receipt, Ticket, Trash2, Wallet, } from "lucide-react";
|
|
5
|
+
import { ArrowRightLeft, Banknote, CreditCard, Eye, MoreHorizontal, Pencil, Receipt, Ticket, Trash2, Wallet, } from "lucide-react";
|
|
6
6
|
import * as React from "react";
|
|
7
7
|
import { useBookingsUiI18nOrDefault, useBookingsUiMessagesOrDefault } from "../i18n/provider.js";
|
|
8
8
|
/**
|
|
@@ -48,7 +48,7 @@ const methodIcon = {
|
|
|
48
48
|
* first as the primary identifier — that's the difference between
|
|
49
49
|
* "list of payments" and "list of invoice line-items".
|
|
50
50
|
*/
|
|
51
|
-
export function BookingPaymentsSummary({ bookingId, variant = "public", getInvoiceHref, onViewPayment, onEditPayment, onDeletePayment, }) {
|
|
51
|
+
export function BookingPaymentsSummary({ bookingId, variant = "public", getInvoiceHref, onViewPayment, onConvertProforma, onEditPayment, onDeletePayment, }) {
|
|
52
52
|
const publicQuery = usePublicBookingPayments(bookingId, { enabled: variant === "public" });
|
|
53
53
|
const adminQuery = useAdminBookingPayments(bookingId, { enabled: variant === "admin" });
|
|
54
54
|
const data = variant === "admin" ? adminQuery.data : publicQuery.data;
|
|
@@ -56,9 +56,14 @@ export function BookingPaymentsSummary({ bookingId, variant = "public", getInvoi
|
|
|
56
56
|
const messages = useBookingsUiMessagesOrDefault();
|
|
57
57
|
const card = messages.bookingPaymentsSummary;
|
|
58
58
|
const payments = data?.data?.payments ?? [];
|
|
59
|
-
const
|
|
59
|
+
const hasConvertibleProformas = payments.some((payment) => payment.invoiceType === "proforma");
|
|
60
|
+
const showActionsColumn = Boolean(onViewPayment ||
|
|
61
|
+
(onConvertProforma && hasConvertibleProformas) ||
|
|
62
|
+
onEditPayment ||
|
|
63
|
+
onDeletePayment);
|
|
60
64
|
const [deleteTarget, setDeleteTarget] = React.useState(null);
|
|
61
65
|
const [deletePending, setDeletePending] = React.useState(false);
|
|
66
|
+
const [convertingInvoiceId, setConvertingInvoiceId] = React.useState(null);
|
|
62
67
|
const handleDeleteConfirm = async () => {
|
|
63
68
|
if (!deleteTarget || !onDeletePayment)
|
|
64
69
|
return;
|
|
@@ -71,6 +76,17 @@ export function BookingPaymentsSummary({ bookingId, variant = "public", getInvoi
|
|
|
71
76
|
setDeletePending(false);
|
|
72
77
|
}
|
|
73
78
|
};
|
|
79
|
+
const handleConvertProforma = async (row) => {
|
|
80
|
+
if (!onConvertProforma || row.invoiceType !== "proforma")
|
|
81
|
+
return;
|
|
82
|
+
setConvertingInvoiceId(row.invoiceId);
|
|
83
|
+
try {
|
|
84
|
+
await onConvertProforma(row);
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
setConvertingInvoiceId(null);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
74
90
|
// Empty-state polish: completed totals across all visible rows so
|
|
75
91
|
// the card carries useful summary information even when there are
|
|
76
92
|
// many small partial payments to scan.
|
|
@@ -88,6 +104,7 @@ export function BookingPaymentsSummary({ bookingId, variant = "public", getInvoi
|
|
|
88
104
|
id: payment.id,
|
|
89
105
|
invoiceId: payment.invoiceId,
|
|
90
106
|
invoiceNumber: payment.invoiceNumber,
|
|
107
|
+
invoiceType: payment.invoiceType,
|
|
91
108
|
amountCents: payment.amountCents,
|
|
92
109
|
currency: payment.currency,
|
|
93
110
|
status: payment.status,
|
|
@@ -97,7 +114,7 @@ export function BookingPaymentsSummary({ bookingId, variant = "public", getInvoi
|
|
|
97
114
|
notes: payment.notes,
|
|
98
115
|
};
|
|
99
116
|
const invoiceHref = getInvoiceHref?.(row) ?? null;
|
|
100
|
-
return (_jsxs("tr", { className: "border-b last:border-b-0", children: [_jsx("td", { className: "px-4 py-2.5 text-right font-mono font-medium", children: formatMoney(payment.amountCents, payment.currency) }), _jsx("td", { className: "px-4 py-2.5", children: _jsxs("span", { className: "inline-flex items-center gap-1.5", children: [_jsx(MethodIcon, { className: "h-3.5 w-3.5 text-muted-foreground" }), methodLabel] }) }), _jsx("td", { className: "px-4 py-2.5", children: _jsx(Badge, { variant: statusVariant[payment.status] ?? "secondary", children: messages.bookingPaymentsSummary.paymentStatusLabels[payment.status] ?? payment.status }) }), _jsx("td", { className: "px-4 py-2.5 text-muted-foreground text-xs", children: formatDate(payment.paymentDate) }), _jsx("td", { className: "px-4 py-2.5", children: _jsx("span", { title: payment.referenceNumber ?? undefined, className: "inline-block max-w-[180px] truncate font-mono text-muted-foreground text-xs", children: payment.referenceNumber ?? "—" }) }), _jsx("td", { className: "px-4 py-2.5 font-mono text-xs", children: invoiceHref ? (_jsx("a", { href: invoiceHref, className: "text-foreground underline-offset-2 hover:underline", children: payment.invoiceNumber })) : (payment.invoiceNumber) }), showActionsColumn ? (_jsx("td", { className: "px-2 py-2.5 text-right", children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { render: _jsx(Button, { variant: "ghost", size: "icon", "aria-label": card.actions.open }), children: _jsx(MoreHorizontal, { className: "h-4 w-4" }) }), _jsxs(DropdownMenuContent, { align: "end", children: [onViewPayment ? (_jsxs(DropdownMenuItem, { onClick: () => onViewPayment(row), children: [_jsx(Eye, { className: "mr-2 h-4 w-4" }), card.actions.view] })) : null, onEditPayment ? (_jsxs(DropdownMenuItem, { onClick: () => onEditPayment(row), children: [_jsx(Pencil, { className: "mr-2 h-4 w-4" }), card.actions.edit] })) : null, onDeletePayment ? (_jsxs(_Fragment, { children: [onViewPayment || onEditPayment ? (_jsx(DropdownMenuSeparator, {})) : null, _jsxs(DropdownMenuItem, { variant: "destructive", onClick: () => setDeleteTarget(row), children: [_jsx(Trash2, { className: "mr-2 h-4 w-4" }), card.actions.delete] })] })) : null] })] }) })) : null] }, payment.id));
|
|
117
|
+
return (_jsxs("tr", { className: "border-b last:border-b-0", children: [_jsx("td", { className: "px-4 py-2.5 text-right font-mono font-medium", children: formatMoney(payment.amountCents, payment.currency) }), _jsx("td", { className: "px-4 py-2.5", children: _jsxs("span", { className: "inline-flex items-center gap-1.5", children: [_jsx(MethodIcon, { className: "h-3.5 w-3.5 text-muted-foreground" }), methodLabel] }) }), _jsx("td", { className: "px-4 py-2.5", children: _jsx(Badge, { variant: statusVariant[payment.status] ?? "secondary", children: messages.bookingPaymentsSummary.paymentStatusLabels[payment.status] ?? payment.status }) }), _jsx("td", { className: "px-4 py-2.5 text-muted-foreground text-xs", children: formatDate(payment.paymentDate) }), _jsx("td", { className: "px-4 py-2.5", children: _jsx("span", { title: payment.referenceNumber ?? undefined, className: "inline-block max-w-[180px] truncate font-mono text-muted-foreground text-xs", children: payment.referenceNumber ?? "—" }) }), _jsx("td", { className: "px-4 py-2.5 font-mono text-xs", children: invoiceHref ? (_jsx("a", { href: invoiceHref, className: "text-foreground underline-offset-2 hover:underline", children: payment.invoiceNumber })) : (payment.invoiceNumber) }), showActionsColumn ? (_jsx("td", { className: "px-2 py-2.5 text-right", children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { render: _jsx(Button, { variant: "ghost", size: "icon", "aria-label": card.actions.open }), children: _jsx(MoreHorizontal, { className: "h-4 w-4" }) }), _jsxs(DropdownMenuContent, { align: "end", children: [onViewPayment ? (_jsxs(DropdownMenuItem, { onClick: () => onViewPayment(row), children: [_jsx(Eye, { className: "mr-2 h-4 w-4" }), card.actions.view] })) : null, onConvertProforma && row.invoiceType === "proforma" ? (_jsxs(DropdownMenuItem, { disabled: convertingInvoiceId === row.invoiceId, onClick: () => void handleConvertProforma(row), children: [_jsx(ArrowRightLeft, { className: "mr-2 h-4 w-4" }), card.actions.convertToInvoice] })) : null, onEditPayment ? (_jsxs(DropdownMenuItem, { onClick: () => onEditPayment(row), children: [_jsx(Pencil, { className: "mr-2 h-4 w-4" }), card.actions.edit] })) : null, onDeletePayment ? (_jsxs(_Fragment, { children: [onViewPayment || onEditPayment ? (_jsx(DropdownMenuSeparator, {})) : null, _jsxs(DropdownMenuItem, { variant: "destructive", onClick: () => setDeleteTarget(row), children: [_jsx(Trash2, { className: "mr-2 h-4 w-4" }), card.actions.delete] })] })) : null] })] }) })) : null] }, payment.id));
|
|
101
118
|
}) })] }) })) }), onDeletePayment ? (_jsx(AlertDialog, { open: Boolean(deleteTarget), onOpenChange: (next) => {
|
|
102
119
|
if (!next && !deletePending)
|
|
103
120
|
setDeleteTarget(null);
|
|
@@ -45,8 +45,10 @@ export interface OptionUnitsStepperSectionProps {
|
|
|
45
45
|
remaining?: string;
|
|
46
46
|
unlimited?: string;
|
|
47
47
|
fillsSlotCapacity?: string;
|
|
48
|
+
reviewLine?: string;
|
|
48
49
|
};
|
|
49
50
|
slotHasFiniteCapacity?: boolean;
|
|
51
|
+
invalidOptionUnitIds?: readonly string[];
|
|
50
52
|
}
|
|
51
53
|
/**
|
|
52
54
|
* Rooms / per-unit stepper for booking-create flows. Drives
|
|
@@ -68,7 +70,7 @@ export interface OptionUnitsStepperSectionProps {
|
|
|
68
70
|
* disables the "+" button — we don't let the UI submit a request that
|
|
69
71
|
* would 409 at insert time.
|
|
70
72
|
*/
|
|
71
|
-
export declare function OptionUnitsStepperSection({ value, onChange, productId, slotId, optionId, enabled, onUnitsChange, labels, slotHasFiniteCapacity, }: OptionUnitsStepperSectionProps): import("react/jsx-runtime").JSX.Element;
|
|
73
|
+
export declare function OptionUnitsStepperSection({ value, onChange, productId, slotId, optionId, enabled, onUnitsChange, labels, slotHasFiniteCapacity, invalidOptionUnitIds, }: OptionUnitsStepperSectionProps): import("react/jsx-runtime").JSX.Element;
|
|
72
74
|
export declare function resolveOptionRemainingLabel({ totalRemaining, units, slotHasFiniteCapacity, remaining, unlimited, fillsSlotCapacity, }: {
|
|
73
75
|
totalRemaining: number | null;
|
|
74
76
|
units: ReadonlyArray<Pick<OptionUnitsStepperUnit, "unitType">>;
|
|
@@ -77,6 +79,7 @@ export declare function resolveOptionRemainingLabel({ totalRemaining, units, slo
|
|
|
77
79
|
unlimited: string;
|
|
78
80
|
fillsSlotCapacity?: string;
|
|
79
81
|
}): string;
|
|
82
|
+
export declare function optionRowHasInvalidUnit(units: ReadonlyArray<Pick<OptionUnitsStepperUnit, "optionUnitId">>, invalidOptionUnitIds: ReadonlySet<string>): boolean;
|
|
80
83
|
/**
|
|
81
84
|
* Returns the `optionId` the slot is bound to, derived from the first
|
|
82
85
|
* slot-availability row whose `optionUnitId` we can map to a known
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"option-units-stepper-section.d.ts","sourceRoot":"","sources":["../../src/components/option-units-stepper-section.tsx"],"names":[],"mappings":"AAgBA,iEAAiE;AACjE,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACnC;AAED,eAAO,MAAM,4BAA4B,EAAE,uBAA4C,CAAA;AAEvF,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,yFAAyF;IACzF,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,IAAI,CAAA;IAC/E,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,MAAM,WAAW,8BAA8B;IAC7C,KAAK,EAAE,uBAAuB,CAAA;IAC9B,QAAQ,EAAE,CAAC,KAAK,EAAE,uBAAuB,KAAK,IAAI,CAAA;IAClD,kEAAkE;IAClE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,sBAAsB,EAAE,KAAK,IAAI,CAAA;IACzD,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;QAClB,iBAAiB,CAAC,EAAE,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"option-units-stepper-section.d.ts","sourceRoot":"","sources":["../../src/components/option-units-stepper-section.tsx"],"names":[],"mappings":"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"}
|
|
@@ -28,7 +28,7 @@ export const emptyOptionUnitsStepperValue = { quantities: {} };
|
|
|
28
28
|
* disables the "+" button — we don't let the UI submit a request that
|
|
29
29
|
* would 409 at insert time.
|
|
30
30
|
*/
|
|
31
|
-
export function OptionUnitsStepperSection({ value, onChange, productId, slotId, optionId, enabled = true, onUnitsChange, labels, slotHasFiniteCapacity = false, }) {
|
|
31
|
+
export function OptionUnitsStepperSection({ value, onChange, productId, slotId, optionId, enabled = true, onUnitsChange, labels, slotHasFiniteCapacity = false, invalidOptionUnitIds = [], }) {
|
|
32
32
|
const productsClient = useVoyantProductsContext();
|
|
33
33
|
const messages = useBookingsUiMessagesOrDefault();
|
|
34
34
|
const merged = { ...messages.roomsStepperSection.labels, ...labels };
|
|
@@ -106,6 +106,7 @@ export function OptionUnitsStepperSection({ value, onChange, productId, slotId,
|
|
|
106
106
|
// the no-slot-rows branch and use the product fallback for everything.
|
|
107
107
|
// See issue #960.
|
|
108
108
|
const units = React.useMemo(() => mergeStepperUnits(availabilityUnitRows, optionUnitRows, slotOptionId, Boolean(slotId)), [availabilityUnitRows, optionUnitRows, slotOptionId, slotId]);
|
|
109
|
+
const invalidOptionUnitIdSet = React.useMemo(() => new Set(invalidOptionUnitIds), [invalidOptionUnitIds]);
|
|
109
110
|
React.useEffect(() => {
|
|
110
111
|
onUnitsChange?.(units);
|
|
111
112
|
}, [onUnitsChange, units]);
|
|
@@ -173,6 +174,7 @@ export function OptionUnitsStepperSection({ value, onChange, productId, slotId,
|
|
|
173
174
|
};
|
|
174
175
|
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-2", children: optionRows.map(({ optionKey, optionName, primary, allUnits, totalRemaining }) => {
|
|
175
176
|
const qty = value.quantities[primary.optionUnitId] ?? 0;
|
|
177
|
+
const isInvalid = optionRowHasInvalidUnit(allUnits, invalidOptionUnitIdSet);
|
|
176
178
|
const remainingLabel = resolveOptionRemainingLabel({
|
|
177
179
|
totalRemaining,
|
|
178
180
|
units: allUnits,
|
|
@@ -182,7 +184,7 @@ export function OptionUnitsStepperSection({ value, onChange, productId, slotId,
|
|
|
182
184
|
fillsSlotCapacity: merged.fillsSlotCapacity,
|
|
183
185
|
});
|
|
184
186
|
const atMax = totalRemaining !== null && qty >= totalRemaining;
|
|
185
|
-
return (_jsxs("div", { className:
|
|
187
|
+
return (_jsxs("div", { className: `flex items-center gap-3 rounded-md border px-3 py-2 ${isInvalid ? "border-destructive/70 bg-destructive/5 ring-1 ring-destructive/20" : ""}`, "aria-invalid": isInvalid ? true : undefined, children: [_jsxs("div", { className: "flex-1", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2 text-sm font-medium", children: [_jsx("span", { children: optionName }), isInvalid ? (_jsx("span", { className: "rounded-sm bg-destructive/10 px-1.5 py-0.5 text-[10px] font-medium text-destructive", children: merged.reviewLine })) : null] }), _jsx("div", { className: "text-xs text-muted-foreground", children: remainingLabel })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7 w-7 p-0", onClick: () => setQuantity(primary.optionUnitId, Math.max(0, qty - 1)), disabled: qty <= 0, "aria-label": `${merged.decreaseUnitPrefix} ${optionName}`, children: _jsx(Minus, { className: "h-3.5 w-3.5" }) }), _jsx("span", { className: "min-w-[1.5rem] text-center text-sm tabular-nums", children: qty }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7 w-7 p-0", onClick: () => setQuantity(primary.optionUnitId, qty + 1), disabled: atMax, "aria-label": `${merged.increaseUnitPrefix} ${optionName}`, children: _jsx(Plus, { className: "h-3.5 w-3.5" }) })] })] }, optionKey));
|
|
186
188
|
}) })] }));
|
|
187
189
|
}
|
|
188
190
|
export function resolveOptionRemainingLabel({ totalRemaining, units, slotHasFiniteCapacity, remaining, unlimited, fillsSlotCapacity, }) {
|
|
@@ -193,6 +195,9 @@ export function resolveOptionRemainingLabel({ totalRemaining, units, slotHasFini
|
|
|
193
195
|
}
|
|
194
196
|
return unlimited;
|
|
195
197
|
}
|
|
198
|
+
export function optionRowHasInvalidUnit(units, invalidOptionUnitIds) {
|
|
199
|
+
return units.some((unit) => invalidOptionUnitIds.has(unit.optionUnitId));
|
|
200
|
+
}
|
|
196
201
|
/**
|
|
197
202
|
* Returns the `optionId` the slot is bound to, derived from the first
|
|
198
203
|
* slot-availability row whose `optionUnitId` we can map to a known
|
|
@@ -13,6 +13,19 @@ export interface TravelerEntry {
|
|
|
13
13
|
dateOfBirth: string | null;
|
|
14
14
|
/** option_unit_id the traveler is assigned to (matches OptionUnitsStepper units). */
|
|
15
15
|
roomUnitId: string | null;
|
|
16
|
+
/**
|
|
17
|
+
* Operator-intent enum for `roomUnitId`:
|
|
18
|
+
*
|
|
19
|
+
* - `auto`: assignment was system-derived, eligible to be re-derived
|
|
20
|
+
* when units / DOB / role change.
|
|
21
|
+
* - `manual`: operator clicked a category/room control. Resolver
|
|
22
|
+
* respects the value when still valid.
|
|
23
|
+
* - `none`: operator explicitly picked "No room". Stays null
|
|
24
|
+
* through resolver re-runs.
|
|
25
|
+
*
|
|
26
|
+
* Defaults to `auto` when omitted. See voyantjs/voyant#1267.
|
|
27
|
+
*/
|
|
28
|
+
roomUnitAssignmentSource?: "auto" | "manual" | "none";
|
|
16
29
|
}
|
|
17
30
|
export interface TravelerListValue {
|
|
18
31
|
travelers: TravelerEntry[];
|
|
@@ -20,11 +33,7 @@ export interface TravelerListValue {
|
|
|
20
33
|
export declare const emptyTravelerListValue: TravelerListValue;
|
|
21
34
|
/** Factory for a blank row — `role` defaults to `adult` unless the list is empty. */
|
|
22
35
|
export declare function createBlankTraveler(role?: TravelerRole): TravelerEntry;
|
|
23
|
-
|
|
24
|
-
* Compute integer age in full years from an ISO date-of-birth string.
|
|
25
|
-
* Returns null when the DOB is missing or unparseable.
|
|
26
|
-
*/
|
|
27
|
-
export declare function computeAgeYears(dob: string | null, now?: Date): number | null;
|
|
36
|
+
export { computeAgeYears } from "@voyantjs/bookings/pricing-assignment";
|
|
28
37
|
/**
|
|
29
38
|
* Derive the age-banded traveler role from DOB. Falls back to `adult`
|
|
30
39
|
* when DOB is missing so partial entries still typecheck downstream.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"travelers-section.d.ts","sourceRoot":"","sources":["../../src/components/travelers-section.tsx"],"names":[],"mappings":"AAyCA,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,CAAA;AAEhE,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,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAA;IACb,iEAAiE;IACjE,iBAAiB,EAAE,MAAM,CAAA;IACzB,IAAI,EAAE,YAAY,CAAA;IAClB,0EAA0E;IAC1E,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,qFAAqF;IACrF,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;
|
|
1
|
+
{"version":3,"file":"travelers-section.d.ts","sourceRoot":"","sources":["../../src/components/travelers-section.tsx"],"names":[],"mappings":"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"}
|
|
@@ -21,25 +21,14 @@ export function createBlankTraveler(role = "adult") {
|
|
|
21
21
|
role,
|
|
22
22
|
dateOfBirth: null,
|
|
23
23
|
roomUnitId: null,
|
|
24
|
+
roomUnitAssignmentSource: "auto",
|
|
24
25
|
};
|
|
25
26
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (!dob)
|
|
32
|
-
return null;
|
|
33
|
-
const birth = new Date(dob);
|
|
34
|
-
if (Number.isNaN(birth.getTime()))
|
|
35
|
-
return null;
|
|
36
|
-
let age = now.getFullYear() - birth.getFullYear();
|
|
37
|
-
const beforeBirthday = now.getMonth() < birth.getMonth() ||
|
|
38
|
-
(now.getMonth() === birth.getMonth() && now.getDate() < birth.getDate());
|
|
39
|
-
if (beforeBirthday)
|
|
40
|
-
age -= 1;
|
|
41
|
-
return age >= 0 ? age : null;
|
|
42
|
-
}
|
|
27
|
+
// Re-export `computeAgeYears` from the canonical assignment module so
|
|
28
|
+
// existing consumers of `travelers-section`'s public surface keep
|
|
29
|
+
// working. The implementation lives in `@voyantjs/bookings/pricing-assignment`.
|
|
30
|
+
export { computeAgeYears } from "@voyantjs/bookings/pricing-assignment";
|
|
31
|
+
import { computeAgeYears as _computeAgeYears, matchUnitByDob as matchAssignmentUnitByDob, matchUnitByRoleHint as matchAssignmentUnitByRoleHint, } from "@voyantjs/bookings/pricing-assignment";
|
|
43
32
|
/**
|
|
44
33
|
* Derive the age-banded traveler role from DOB. Falls back to `adult`
|
|
45
34
|
* when DOB is missing so partial entries still typecheck downstream.
|
|
@@ -50,7 +39,7 @@ export function computeAgeYears(dob, now = new Date()) {
|
|
|
50
39
|
* - adult: 18+
|
|
51
40
|
*/
|
|
52
41
|
export function deriveTravelerRoleFromDob(dob) {
|
|
53
|
-
const age =
|
|
42
|
+
const age = _computeAgeYears(dob);
|
|
54
43
|
if (age == null)
|
|
55
44
|
return "adult";
|
|
56
45
|
if (age < 2)
|
|
@@ -60,46 +49,27 @@ export function deriveTravelerRoleFromDob(dob) {
|
|
|
60
49
|
return "adult";
|
|
61
50
|
}
|
|
62
51
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
52
|
+
* Adapter from this file's `RoomGroupUnit` shape (UI-side, uses
|
|
53
|
+
* `unitId`) to the canonical `PricingAssignmentUnit` shape (uses
|
|
54
|
+
* `optionUnitId`). Phase 1 of voyantjs/voyant#1267 will collapse these
|
|
55
|
+
* by renaming the UI shape.
|
|
67
56
|
*/
|
|
57
|
+
function roomGroupUnitsAsAssignmentUnits(units) {
|
|
58
|
+
return units.map((u) => ({
|
|
59
|
+
optionId: null,
|
|
60
|
+
optionUnitId: u.unitId,
|
|
61
|
+
unitName: u.unitName,
|
|
62
|
+
unitCode: u.unitCode,
|
|
63
|
+
minAge: u.minAge,
|
|
64
|
+
maxAge: u.maxAge,
|
|
65
|
+
unitType: u.unitType,
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
68
|
function matchUnitByDob(units, dob) {
|
|
69
|
-
|
|
70
|
-
return null;
|
|
71
|
-
const age = computeAgeYears(dob);
|
|
72
|
-
if (age == null)
|
|
73
|
-
return null;
|
|
74
|
-
const personUnits = units.filter((u) => u.unitType == null || u.unitType === "person");
|
|
75
|
-
const match = personUnits.find((u) => (u.minAge == null || age >= u.minAge) && (u.maxAge == null || age <= u.maxAge));
|
|
76
|
-
return match?.unitId ?? null;
|
|
69
|
+
return matchAssignmentUnitByDob(roomGroupUnitsAsAssignmentUnits(units), dob);
|
|
77
70
|
}
|
|
78
|
-
/**
|
|
79
|
-
* Find the unit matching a role hint when DOB is missing. Maps the
|
|
80
|
-
* role to a representative age and matches against `[minAge, maxAge]`.
|
|
81
|
-
* Returns null when the role doesn't carry an age signal (e.g. `lead`).
|
|
82
|
-
*
|
|
83
|
-
* Routes `infant` to whichever band covers ~1y (e.g. `child_0_5`) and
|
|
84
|
-
* `child` to whichever covers ~8y (e.g. `child_6_12`), regardless of
|
|
85
|
-
* how the product codes the unit names.
|
|
86
|
-
*/
|
|
87
71
|
function matchUnitByRoleHint(units, role) {
|
|
88
|
-
|
|
89
|
-
return null;
|
|
90
|
-
const HINT_AGE = {
|
|
91
|
-
adult: 30,
|
|
92
|
-
child: 8,
|
|
93
|
-
infant: 1,
|
|
94
|
-
};
|
|
95
|
-
const hintAge = HINT_AGE[role];
|
|
96
|
-
if (hintAge == null)
|
|
97
|
-
return null;
|
|
98
|
-
// Only consider units with explicit age bands — units with null
|
|
99
|
-
// min/max would all spuriously match any hint age.
|
|
100
|
-
const banded = units.filter((u) => (u.unitType == null || u.unitType === "person") && (u.minAge != null || u.maxAge != null));
|
|
101
|
-
const match = banded.find((u) => (u.minAge == null || hintAge >= u.minAge) && (u.maxAge == null || hintAge <= u.maxAge));
|
|
102
|
-
return match?.unitId ?? null;
|
|
72
|
+
return matchAssignmentUnitByRoleHint(roomGroupUnitsAsAssignmentUnits(units), role);
|
|
103
73
|
}
|
|
104
74
|
/**
|
|
105
75
|
* The Room dropdown lists one item per option (keyed by the option's
|
|
@@ -190,25 +160,13 @@ export function TravelersSection({ value, onChange, roomUnits, roomGroups, billi
|
|
|
190
160
|
matchUnitByRoleHint(group.units, role) ??
|
|
191
161
|
group.primaryUnitId);
|
|
192
162
|
}, [roomUnits, roomGroups]);
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
return;
|
|
201
|
-
if (!value.travelers.some((t) => !t.roomUnitId))
|
|
202
|
-
return;
|
|
203
|
-
const next = value.travelers.map((t) => t.roomUnitId ? t : { ...t, roomUnitId: pickRoomUnitIdForNewTraveler(t.dateOfBirth, t.role) });
|
|
204
|
-
// Guard against infinite re-runs if hydration can't find a unit
|
|
205
|
-
// (e.g. empty roomGroups): no point dispatching an onChange that
|
|
206
|
-
// doesn't actually change anything.
|
|
207
|
-
const changed = next.some((t, i) => t.roomUnitId !== value.travelers[i]?.roomUnitId);
|
|
208
|
-
if (!changed)
|
|
209
|
-
return;
|
|
210
|
-
onChange({ travelers: next });
|
|
211
|
-
}, [roomUnits, value.travelers, onChange, pickRoomUnitIdForNewTraveler]);
|
|
163
|
+
// Note: there is no hydration effect any more. Travelers attached
|
|
164
|
+
// before the option-units queries resolve get a `null` roomUnitId
|
|
165
|
+
// and `roomUnitAssignmentSource: "auto"`; the resolver in
|
|
166
|
+
// `@voyantjs/bookings/pricing-assignment` re-derives them at every
|
|
167
|
+
// preview/submit pass, and respects `"none"` (explicit No room) /
|
|
168
|
+
// `"manual"` (operator click) when set. Operator intent is now
|
|
169
|
+
// declarative on the row, not implicit in a one-shot effect.
|
|
212
170
|
const addRow = () => {
|
|
213
171
|
// First traveler defaults to `lead` so the operator doesn't have to
|
|
214
172
|
// remember to flip the role on the initial row.
|
|
@@ -217,7 +175,11 @@ export function TravelersSection({ value, onChange, roomUnits, roomGroups, billi
|
|
|
217
175
|
onChange({
|
|
218
176
|
travelers: [
|
|
219
177
|
...value.travelers,
|
|
220
|
-
{
|
|
178
|
+
{
|
|
179
|
+
...blank,
|
|
180
|
+
roomUnitId: pickRoomUnitIdForNewTraveler(null, role),
|
|
181
|
+
roomUnitAssignmentSource: "auto",
|
|
182
|
+
},
|
|
221
183
|
],
|
|
222
184
|
});
|
|
223
185
|
};
|
|
@@ -229,7 +191,11 @@ export function TravelersSection({ value, onChange, roomUnits, roomGroups, billi
|
|
|
229
191
|
onChange({
|
|
230
192
|
travelers: [
|
|
231
193
|
...value.travelers,
|
|
232
|
-
{
|
|
194
|
+
{
|
|
195
|
+
...traveler,
|
|
196
|
+
roomUnitId: pickRoomUnitIdForNewTraveler(traveler.dateOfBirth, role),
|
|
197
|
+
roomUnitAssignmentSource: "auto",
|
|
198
|
+
},
|
|
233
199
|
],
|
|
234
200
|
});
|
|
235
201
|
};
|
|
@@ -239,7 +205,11 @@ export function TravelersSection({ value, onChange, roomUnits, roomGroups, billi
|
|
|
239
205
|
onChange({
|
|
240
206
|
travelers: [
|
|
241
207
|
...value.travelers,
|
|
242
|
-
{
|
|
208
|
+
{
|
|
209
|
+
...traveler,
|
|
210
|
+
roomUnitId: pickRoomUnitIdForNewTraveler(traveler.dateOfBirth, role),
|
|
211
|
+
roomUnitAssignmentSource: "auto",
|
|
212
|
+
},
|
|
243
213
|
],
|
|
244
214
|
});
|
|
245
215
|
};
|
|
@@ -280,6 +250,18 @@ export function TravelersSection({ value, onChange, roomUnits, roomGroups, billi
|
|
|
280
250
|
phone: person.phone ?? "",
|
|
281
251
|
preferredLanguage: person.preferredLanguage ?? "",
|
|
282
252
|
dateOfBirth: person.dateOfBirth ?? null,
|
|
253
|
+
// Re-derive the unit assignment when the operator
|
|
254
|
+
// swaps the linked CRM person — DOB changes, so
|
|
255
|
+
// the resolver may move the row to a different
|
|
256
|
+
// age band. Skip when the operator has already
|
|
257
|
+
// explicitly picked a room/category/"No room".
|
|
258
|
+
...(traveler.roomUnitAssignmentSource === "manual" ||
|
|
259
|
+
traveler.roomUnitAssignmentSource === "none"
|
|
260
|
+
? {}
|
|
261
|
+
: {
|
|
262
|
+
roomUnitId: pickRoomUnitIdForNewTraveler(person.dateOfBirth ?? null, traveler.role),
|
|
263
|
+
roomUnitAssignmentSource: "auto",
|
|
264
|
+
}),
|
|
283
265
|
}), onClear: () => updateAt(index, {
|
|
284
266
|
personId: null,
|
|
285
267
|
firstName: "",
|
|
@@ -288,15 +270,31 @@ export function TravelersSection({ value, onChange, roomUnits, roomGroups, billi
|
|
|
288
270
|
phone: "",
|
|
289
271
|
preferredLanguage: "",
|
|
290
272
|
dateOfBirth: null,
|
|
273
|
+
...(traveler.roomUnitAssignmentSource === "manual" ||
|
|
274
|
+
traveler.roomUnitAssignmentSource === "none"
|
|
275
|
+
? {}
|
|
276
|
+
: {
|
|
277
|
+
roomUnitId: pickRoomUnitIdForNewTraveler(null, traveler.role),
|
|
278
|
+
roomUnitAssignmentSource: "auto",
|
|
279
|
+
}),
|
|
291
280
|
}) }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsx(TravelerCategoryButtons, { traveler: traveler, roomGroups: roomGroups, fallbackLabels: {
|
|
292
281
|
category: merged.category,
|
|
293
282
|
adult: merged.roleAdult,
|
|
294
283
|
child: merged.roleChild,
|
|
295
284
|
infant: merged.roleInfant,
|
|
296
|
-
}, onPickUnit: (unitId, nextRole
|
|
285
|
+
}, onPickUnit: (unitId, nextRole, source) => updateAt(index, {
|
|
286
|
+
roomUnitId: unitId,
|
|
287
|
+
role: nextRole,
|
|
288
|
+
// Only freeze as manual when the dynamic button
|
|
289
|
+
// actually picked a unit. Role-only clicks via
|
|
290
|
+
// the static fallback stay `auto` so the
|
|
291
|
+
// resolver can re-derive once real units load.
|
|
292
|
+
roomUnitAssignmentSource: source,
|
|
293
|
+
}) }), roomUnits && roomUnits.length > 0 ? (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: merged.room }), _jsxs(Select, { items: roomSelectItems, value: mapUnitIdToGroupPrimary(traveler.roomUnitId, roomGroups) ?? NO_ROOM, onValueChange: (v) => updateAt(index, {
|
|
297
294
|
roomUnitId: v === NO_ROOM || !v
|
|
298
295
|
? null
|
|
299
296
|
: pickUnitForRoomChange(traveler.roomUnitId, v, roomGroups),
|
|
297
|
+
roomUnitAssignmentSource: v === NO_ROOM || !v ? "none" : "manual",
|
|
300
298
|
}), 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, 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))) }))] }));
|
|
301
299
|
}
|
|
302
300
|
function TravelerPersonPicker({ personId, labels, pinnedPeople = [], onSelect, onClear, }) {
|
|
@@ -375,6 +373,7 @@ function createTravelerFromPerson(person, role) {
|
|
|
375
373
|
role: effectiveRole,
|
|
376
374
|
dateOfBirth,
|
|
377
375
|
roomUnitId: null,
|
|
376
|
+
roomUnitAssignmentSource: "auto",
|
|
378
377
|
};
|
|
379
378
|
}
|
|
380
379
|
function formatPersonName(person) {
|
|
@@ -431,16 +430,21 @@ function TravelerCategoryButtons({ traveler, roomGroups, fallbackLabels, onPickU
|
|
|
431
430
|
].map(([category, label]) => {
|
|
432
431
|
const { active, nextRole, shouldUpdate } = getStaticTravelerCategoryButtonState(traveler, category);
|
|
433
432
|
return (_jsx(Button, { type: "button", size: "sm", variant: active ? "default" : "outline", className: "h-7 text-xs", onClick: () => {
|
|
433
|
+
// Static fallback: operator chose a role, not a
|
|
434
|
+
// concrete unit. Pass `source: "auto"` so the
|
|
435
|
+
// resolver re-derives the unit instead of freezing
|
|
436
|
+
// a stale auto-assignment as manual.
|
|
434
437
|
if (shouldUpdate)
|
|
435
|
-
onPickUnit(traveler.roomUnitId, nextRole);
|
|
438
|
+
onPickUnit(traveler.roomUnitId, nextRole, "auto");
|
|
436
439
|
}, children: label }, category));
|
|
437
440
|
}) })] }));
|
|
438
441
|
}
|
|
439
442
|
return (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: fallbackLabels.category }), _jsx("div", { className: "grid gap-1", style: { gridTemplateColumns: `repeat(${categoryUnits.length}, minmax(0, 1fr))` }, children: categoryUnits.map((unit) => {
|
|
440
443
|
const { active, nextRole, shouldUpdate } = getDynamicTravelerCategoryButtonState(traveler, unit);
|
|
441
444
|
return (_jsx(Button, { type: "button", size: "sm", variant: active ? "default" : "outline", className: "h-7 text-xs", onClick: () => {
|
|
445
|
+
// Dynamic button: real unit pick, freeze as manual.
|
|
442
446
|
if (shouldUpdate)
|
|
443
|
-
onPickUnit(unit.unitId, nextRole);
|
|
447
|
+
onPickUnit(unit.unitId, nextRole, "manual");
|
|
444
448
|
}, title: unit.minAge != null || unit.maxAge != null
|
|
445
449
|
? `${unit.minAge ?? "0"}–${unit.maxAge ?? "∞"}`
|
|
446
450
|
: undefined, children: unit.unitName }, unit.unitId));
|
package/dist/i18n/en.d.ts
CHANGED
|
@@ -487,6 +487,7 @@ export declare const bookingsUiEn: {
|
|
|
487
487
|
fillsSlotCapacity: string;
|
|
488
488
|
decreaseUnitPrefix: string;
|
|
489
489
|
increaseUnitPrefix: string;
|
|
490
|
+
reviewLine: string;
|
|
490
491
|
};
|
|
491
492
|
};
|
|
492
493
|
sharedRoomSection: {
|
|
@@ -1311,6 +1312,7 @@ export declare const bookingsUiEn: {
|
|
|
1311
1312
|
actions: {
|
|
1312
1313
|
open: string;
|
|
1313
1314
|
view: string;
|
|
1315
|
+
convertToInvoice: string;
|
|
1314
1316
|
edit: string;
|
|
1315
1317
|
delete: string;
|
|
1316
1318
|
};
|
package/dist/i18n/en.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/i18n/en.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,YAAY
|
|
1
|
+
{"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/i18n/en.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA82CK,CAAA"}
|
package/dist/i18n/en.js
CHANGED
|
@@ -487,6 +487,7 @@ export const bookingsUiEn = {
|
|
|
487
487
|
fillsSlotCapacity: "fills slot capacity",
|
|
488
488
|
decreaseUnitPrefix: "Decrease",
|
|
489
489
|
increaseUnitPrefix: "Increase",
|
|
490
|
+
reviewLine: "Review this line",
|
|
490
491
|
},
|
|
491
492
|
},
|
|
492
493
|
sharedRoomSection: {
|
|
@@ -1311,6 +1312,7 @@ export const bookingsUiEn = {
|
|
|
1311
1312
|
actions: {
|
|
1312
1313
|
open: "Open payment menu",
|
|
1313
1314
|
view: "View payment",
|
|
1315
|
+
convertToInvoice: "Convert to invoice",
|
|
1314
1316
|
edit: "Edit payment",
|
|
1315
1317
|
delete: "Delete payment",
|
|
1316
1318
|
},
|
package/dist/i18n/messages.d.ts
CHANGED
|
@@ -401,6 +401,7 @@ export type BookingsUiMessages = {
|
|
|
401
401
|
fillsSlotCapacity: string;
|
|
402
402
|
decreaseUnitPrefix: string;
|
|
403
403
|
increaseUnitPrefix: string;
|
|
404
|
+
reviewLine: string;
|
|
404
405
|
};
|
|
405
406
|
};
|
|
406
407
|
sharedRoomSection: {
|
|
@@ -1164,6 +1165,7 @@ export type BookingsUiMessages = {
|
|
|
1164
1165
|
/** Trigger button screen-reader label. */
|
|
1165
1166
|
open: string;
|
|
1166
1167
|
view: string;
|
|
1168
|
+
convertToInvoice: string;
|
|
1167
1169
|
edit: string;
|
|
1168
1170
|
delete: string;
|
|
1169
1171
|
};
|