@voyantjs/bookings-ui 0.80.18 → 0.81.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/booking-create-dialog.d.ts +0 -18
- package/dist/components/booking-create-dialog.d.ts.map +1 -1
- package/dist/components/booking-create-dialog.js +89 -202
- package/dist/components/booking-create-utils.d.ts +1 -1
- package/dist/components/booking-create-utils.d.ts.map +1 -1
- package/dist/components/booking-create-utils.js +20 -8
- package/dist/components/booking-detail-page.d.ts.map +1 -1
- package/dist/components/booking-detail-page.js +3 -1
- package/dist/components/booking-payments-summary.d.ts +4 -1
- package/dist/components/booking-payments-summary.d.ts.map +1 -1
- package/dist/components/booking-payments-summary.js +21 -4
- package/dist/components/option-units-stepper-section.d.ts +4 -1
- package/dist/components/option-units-stepper-section.d.ts.map +1 -1
- package/dist/components/option-units-stepper-section.js +7 -2
- package/dist/components/travelers-section.d.ts +14 -5
- package/dist/components/travelers-section.d.ts.map +1 -1
- package/dist/components/travelers-section.js +83 -88
- package/dist/i18n/en.d.ts +2 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +2 -0
- package/dist/i18n/messages.d.ts +2 -0
- package/dist/i18n/messages.d.ts.map +1 -1
- package/dist/i18n/provider.d.ts +4 -0
- package/dist/i18n/provider.d.ts.map +1 -1
- package/dist/i18n/ro.d.ts +2 -0
- package/dist/i18n/ro.d.ts.map +1 -1
- package/dist/i18n/ro.js +2 -0
- package/package.json +32 -30
|
@@ -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,34 +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
|
-
// empty `roomUnits` so the next load (e.g. product change) rehydrates.
|
|
201
|
-
const hasHydratedNullsRef = React.useRef(false);
|
|
202
|
-
React.useEffect(() => {
|
|
203
|
-
if (!roomUnits || roomUnits.length === 0) {
|
|
204
|
-
hasHydratedNullsRef.current = false;
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
if (hasHydratedNullsRef.current)
|
|
208
|
-
return;
|
|
209
|
-
hasHydratedNullsRef.current = true;
|
|
210
|
-
if (!value.travelers.some((t) => !t.roomUnitId))
|
|
211
|
-
return;
|
|
212
|
-
const next = value.travelers.map((t) => t.roomUnitId ? t : { ...t, roomUnitId: pickRoomUnitIdForNewTraveler(t.dateOfBirth, t.role) });
|
|
213
|
-
// Guard against a stray onChange if hydration can't find a unit
|
|
214
|
-
// (e.g. empty roomGroups): no point dispatching an update that
|
|
215
|
-
// doesn't actually change anything.
|
|
216
|
-
const changed = next.some((t, i) => t.roomUnitId !== value.travelers[i]?.roomUnitId);
|
|
217
|
-
if (!changed)
|
|
218
|
-
return;
|
|
219
|
-
onChange({ travelers: next });
|
|
220
|
-
}, [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.
|
|
221
170
|
const addRow = () => {
|
|
222
171
|
// First traveler defaults to `lead` so the operator doesn't have to
|
|
223
172
|
// remember to flip the role on the initial row.
|
|
@@ -226,7 +175,11 @@ export function TravelersSection({ value, onChange, roomUnits, roomGroups, billi
|
|
|
226
175
|
onChange({
|
|
227
176
|
travelers: [
|
|
228
177
|
...value.travelers,
|
|
229
|
-
{
|
|
178
|
+
{
|
|
179
|
+
...blank,
|
|
180
|
+
roomUnitId: pickRoomUnitIdForNewTraveler(null, role),
|
|
181
|
+
roomUnitAssignmentSource: "auto",
|
|
182
|
+
},
|
|
230
183
|
],
|
|
231
184
|
});
|
|
232
185
|
};
|
|
@@ -238,7 +191,11 @@ export function TravelersSection({ value, onChange, roomUnits, roomGroups, billi
|
|
|
238
191
|
onChange({
|
|
239
192
|
travelers: [
|
|
240
193
|
...value.travelers,
|
|
241
|
-
{
|
|
194
|
+
{
|
|
195
|
+
...traveler,
|
|
196
|
+
roomUnitId: pickRoomUnitIdForNewTraveler(traveler.dateOfBirth, role),
|
|
197
|
+
roomUnitAssignmentSource: "auto",
|
|
198
|
+
},
|
|
242
199
|
],
|
|
243
200
|
});
|
|
244
201
|
};
|
|
@@ -248,7 +205,11 @@ export function TravelersSection({ value, onChange, roomUnits, roomGroups, billi
|
|
|
248
205
|
onChange({
|
|
249
206
|
travelers: [
|
|
250
207
|
...value.travelers,
|
|
251
|
-
{
|
|
208
|
+
{
|
|
209
|
+
...traveler,
|
|
210
|
+
roomUnitId: pickRoomUnitIdForNewTraveler(traveler.dateOfBirth, role),
|
|
211
|
+
roomUnitAssignmentSource: "auto",
|
|
212
|
+
},
|
|
252
213
|
],
|
|
253
214
|
});
|
|
254
215
|
};
|
|
@@ -289,6 +250,18 @@ export function TravelersSection({ value, onChange, roomUnits, roomGroups, billi
|
|
|
289
250
|
phone: person.phone ?? "",
|
|
290
251
|
preferredLanguage: person.preferredLanguage ?? "",
|
|
291
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
|
+
}),
|
|
292
265
|
}), onClear: () => updateAt(index, {
|
|
293
266
|
personId: null,
|
|
294
267
|
firstName: "",
|
|
@@ -297,15 +270,31 @@ export function TravelersSection({ value, onChange, roomUnits, roomGroups, billi
|
|
|
297
270
|
phone: "",
|
|
298
271
|
preferredLanguage: "",
|
|
299
272
|
dateOfBirth: null,
|
|
273
|
+
...(traveler.roomUnitAssignmentSource === "manual" ||
|
|
274
|
+
traveler.roomUnitAssignmentSource === "none"
|
|
275
|
+
? {}
|
|
276
|
+
: {
|
|
277
|
+
roomUnitId: pickRoomUnitIdForNewTraveler(null, traveler.role),
|
|
278
|
+
roomUnitAssignmentSource: "auto",
|
|
279
|
+
}),
|
|
300
280
|
}) }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsx(TravelerCategoryButtons, { traveler: traveler, roomGroups: roomGroups, fallbackLabels: {
|
|
301
281
|
category: merged.category,
|
|
302
282
|
adult: merged.roleAdult,
|
|
303
283
|
child: merged.roleChild,
|
|
304
284
|
infant: merged.roleInfant,
|
|
305
|
-
}, 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, {
|
|
306
294
|
roomUnitId: v === NO_ROOM || !v
|
|
307
295
|
? null
|
|
308
296
|
: pickUnitForRoomChange(traveler.roomUnitId, v, roomGroups),
|
|
297
|
+
roomUnitAssignmentSource: v === NO_ROOM || !v ? "none" : "manual",
|
|
309
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))) }))] }));
|
|
310
299
|
}
|
|
311
300
|
function TravelerPersonPicker({ personId, labels, pinnedPeople = [], onSelect, onClear, }) {
|
|
@@ -384,6 +373,7 @@ function createTravelerFromPerson(person, role) {
|
|
|
384
373
|
role: effectiveRole,
|
|
385
374
|
dateOfBirth,
|
|
386
375
|
roomUnitId: null,
|
|
376
|
+
roomUnitAssignmentSource: "auto",
|
|
387
377
|
};
|
|
388
378
|
}
|
|
389
379
|
function formatPersonName(person) {
|
|
@@ -440,16 +430,21 @@ function TravelerCategoryButtons({ traveler, roomGroups, fallbackLabels, onPickU
|
|
|
440
430
|
].map(([category, label]) => {
|
|
441
431
|
const { active, nextRole, shouldUpdate } = getStaticTravelerCategoryButtonState(traveler, category);
|
|
442
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.
|
|
443
437
|
if (shouldUpdate)
|
|
444
|
-
onPickUnit(traveler.roomUnitId, nextRole);
|
|
438
|
+
onPickUnit(traveler.roomUnitId, nextRole, "auto");
|
|
445
439
|
}, children: label }, category));
|
|
446
440
|
}) })] }));
|
|
447
441
|
}
|
|
448
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) => {
|
|
449
443
|
const { active, nextRole, shouldUpdate } = getDynamicTravelerCategoryButtonState(traveler, unit);
|
|
450
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.
|
|
451
446
|
if (shouldUpdate)
|
|
452
|
-
onPickUnit(unit.unitId, nextRole);
|
|
447
|
+
onPickUnit(unit.unitId, nextRole, "manual");
|
|
453
448
|
}, title: unit.minAge != null || unit.maxAge != null
|
|
454
449
|
? `${unit.minAge ?? "0"}–${unit.maxAge ?? "∞"}`
|
|
455
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
|
};
|