@voyantjs/products-ui 0.101.1 → 0.102.0
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/product-detail/date-picker.d.ts +44 -0
- package/dist/components/product-detail/date-picker.d.ts.map +1 -0
- package/dist/components/product-detail/date-picker.js +125 -0
- package/dist/components/product-detail/host.d.ts +53 -0
- package/dist/components/product-detail/host.d.ts.map +1 -0
- package/dist/components/product-detail/host.js +24 -0
- package/dist/components/product-detail/index.d.ts +6 -0
- package/dist/components/product-detail/index.d.ts.map +1 -0
- package/dist/components/product-detail/index.js +5 -0
- package/dist/components/product-detail/product-activity-section.d.ts +4 -0
- package/dist/components/product-detail/product-activity-section.d.ts.map +1 -0
- package/dist/components/product-detail/product-activity-section.js +37 -0
- package/dist/components/product-detail/product-day-sheet.d.ts +14 -0
- package/dist/components/product-detail/product-day-sheet.d.ts.map +1 -0
- package/dist/components/product-detail/product-day-sheet.js +75 -0
- package/dist/components/product-detail/product-day-translation.d.ts +41 -0
- package/dist/components/product-detail/product-day-translation.d.ts.map +1 -0
- package/dist/components/product-detail/product-day-translation.js +111 -0
- package/dist/components/product-detail/product-departure-dialog.d.ts +11 -0
- package/dist/components/product-detail/product-departure-dialog.d.ts.map +1 -0
- package/dist/components/product-detail/product-departure-dialog.js +10 -0
- package/dist/components/product-detail/product-departure-form.d.ts +25 -0
- package/dist/components/product-detail/product-departure-form.d.ts.map +1 -0
- package/dist/components/product-detail/product-departure-form.js +237 -0
- package/dist/components/product-detail/product-departure-pricing-override-dialog.d.ts +8 -0
- package/dist/components/product-detail/product-departure-pricing-override-dialog.d.ts.map +1 -0
- package/dist/components/product-detail/product-departure-pricing-override-dialog.js +125 -0
- package/dist/components/product-detail/product-detail-day-row.d.ts +14 -0
- package/dist/components/product-detail/product-detail-day-row.d.ts.map +1 -0
- package/dist/components/product-detail/product-detail-day-row.js +43 -0
- package/dist/components/product-detail/product-detail-dialog.d.ts +10 -0
- package/dist/components/product-detail/product-detail-dialog.d.ts.map +1 -0
- package/dist/components/product-detail/product-detail-dialog.js +10 -0
- package/dist/components/product-detail/product-detail-form.d.ts +19 -0
- package/dist/components/product-detail/product-detail-form.d.ts.map +1 -0
- package/dist/components/product-detail/product-detail-form.js +180 -0
- package/dist/components/product-detail/product-detail-header.d.ts +12 -0
- package/dist/components/product-detail/product-detail-header.d.ts.map +1 -0
- package/dist/components/product-detail/product-detail-header.js +19 -0
- package/dist/components/product-detail/product-detail-itinerary-section.d.ts +4 -0
- package/dist/components/product-detail/product-detail-itinerary-section.d.ts.map +1 -0
- package/dist/components/product-detail/product-detail-itinerary-section.js +201 -0
- package/dist/components/product-detail/product-detail-page.d.ts +4 -0
- package/dist/components/product-detail/product-detail-page.d.ts.map +1 -0
- package/dist/components/product-detail/product-detail-page.js +97 -0
- package/dist/components/product-detail/product-detail-sections.d.ts +63 -0
- package/dist/components/product-detail/product-detail-sections.d.ts.map +1 -0
- package/dist/components/product-detail/product-detail-sections.js +143 -0
- package/dist/components/product-detail/product-detail-shared.d.ts +264 -0
- package/dist/components/product-detail/product-detail-shared.d.ts.map +1 -0
- package/dist/components/product-detail/product-detail-shared.js +157 -0
- package/dist/components/product-detail/product-detail-skeleton.d.ts +9 -0
- package/dist/components/product-detail/product-detail-skeleton.d.ts.map +1 -0
- package/dist/components/product-detail/product-detail-skeleton.js +53 -0
- package/dist/components/product-detail/product-extras-section.d.ts +4 -0
- package/dist/components/product-detail/product-extras-section.d.ts.map +1 -0
- package/dist/components/product-detail/product-extras-section.js +141 -0
- package/dist/components/product-detail/product-itinerary-form.d.ts +16 -0
- package/dist/components/product-detail/product-itinerary-form.d.ts.map +1 -0
- package/dist/components/product-detail/product-itinerary-form.js +38 -0
- package/dist/components/product-detail/product-market-rules-section.d.ts +6 -0
- package/dist/components/product-detail/product-market-rules-section.d.ts.map +1 -0
- package/dist/components/product-detail/product-market-rules-section.js +81 -0
- package/dist/components/product-detail/product-media-gallery.d.ts +19 -0
- package/dist/components/product-detail/product-media-gallery.d.ts.map +1 -0
- package/dist/components/product-detail/product-media-gallery.js +114 -0
- package/dist/components/product-detail/product-option-price-rule-dialog.d.ts +12 -0
- package/dist/components/product-detail/product-option-price-rule-dialog.d.ts.map +1 -0
- package/dist/components/product-detail/product-option-price-rule-dialog.js +10 -0
- package/dist/components/product-detail/product-option-price-rule-form.d.ts +29 -0
- package/dist/components/product-detail/product-option-price-rule-form.d.ts.map +1 -0
- package/dist/components/product-detail/product-option-price-rule-form.js +125 -0
- package/dist/components/product-detail/product-option-pricing-grid.d.ts +16 -0
- package/dist/components/product-detail/product-option-pricing-grid.d.ts.map +1 -0
- package/dist/components/product-detail/product-option-pricing-grid.js +193 -0
- package/dist/components/product-detail/product-options-pricing.d.ts +34 -0
- package/dist/components/product-detail/product-options-pricing.d.ts.map +1 -0
- package/dist/components/product-detail/product-options-pricing.js +385 -0
- package/dist/components/product-detail/product-options-shared.d.ts +623 -0
- package/dist/components/product-detail/product-options-shared.d.ts.map +1 -0
- package/dist/components/product-detail/product-options-shared.js +54 -0
- package/dist/components/product-detail/product-payment-policy-section.d.ts +17 -0
- package/dist/components/product-detail/product-payment-policy-section.d.ts.map +1 -0
- package/dist/components/product-detail/product-payment-policy-section.js +58 -0
- package/dist/components/product-detail/product-schedule-dialog.d.ts +11 -0
- package/dist/components/product-detail/product-schedule-dialog.d.ts.map +1 -0
- package/dist/components/product-detail/product-schedule-dialog.js +10 -0
- package/dist/components/product-detail/product-schedule-form.d.ts +17 -0
- package/dist/components/product-detail/product-schedule-form.d.ts.map +1 -0
- package/dist/components/product-detail/product-schedule-form.js +222 -0
- package/dist/components/product-detail/product-service-dialog.d.ts +12 -0
- package/dist/components/product-detail/product-service-dialog.d.ts.map +1 -0
- package/dist/components/product-detail/product-service-dialog.js +10 -0
- package/dist/components/product-detail/product-service-form.d.ts +22 -0
- package/dist/components/product-detail/product-service-form.d.ts.map +1 -0
- package/dist/components/product-detail/product-service-form.js +154 -0
- package/dist/components/product-detail/product-translation-popover.d.ts +91 -0
- package/dist/components/product-detail/product-translation-popover.d.ts.map +1 -0
- package/dist/components/product-detail/product-translation-popover.js +217 -0
- package/dist/components/product-detail/product-unit-dialog.d.ts +14 -0
- package/dist/components/product-detail/product-unit-dialog.d.ts.map +1 -0
- package/dist/components/product-detail/product-unit-dialog.js +10 -0
- package/dist/components/product-detail/product-unit-form.d.ts +34 -0
- package/dist/components/product-detail/product-unit-form.d.ts.map +1 -0
- package/dist/components/product-detail/product-unit-form.js +139 -0
- package/dist/components/product-detail/product-unit-price-rule-dialog.d.ts +17 -0
- package/dist/components/product-detail/product-unit-price-rule-dialog.d.ts.map +1 -0
- package/dist/components/product-detail/product-unit-price-rule-dialog.js +10 -0
- package/dist/components/product-detail/product-unit-price-rule-form.d.ts +29 -0
- package/dist/components/product-detail/product-unit-price-rule-form.d.ts.map +1 -0
- package/dist/components/product-detail/product-unit-price-rule-form.js +145 -0
- package/dist/components/product-detail/timezone-options.d.ts +9 -0
- package/dist/components/product-detail/timezone-options.d.ts.map +1 -0
- package/dist/components/product-detail/timezone-options.js +28 -0
- package/dist/components/product-detail/use-product-detail-data.d.ts +41 -0
- package/dist/components/product-detail/use-product-detail-data.d.ts.map +1 -0
- package/dist/components/product-detail/use-product-detail-data.js +143 -0
- package/dist/components/product-detail/use-product-detail-dialogs.d.ts +24 -0
- package/dist/components/product-detail/use-product-detail-dialogs.d.ts.map +1 -0
- package/dist/components/product-detail/use-product-detail-dialogs.js +40 -0
- package/dist/components/product-detail/zod-resolver.d.ts +4 -0
- package/dist/components/product-detail/zod-resolver.d.ts.map +1 -0
- package/dist/components/product-detail/zod-resolver.js +39 -0
- package/dist/components/product-options-section.d.ts.map +1 -1
- package/dist/components/product-options-section.js +31 -20
- package/package.json +38 -19
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useOptionPriceRuleMutation } from "@voyantjs/pricing-react";
|
|
3
|
+
import { CancellationPolicyCombobox } from "@voyantjs/pricing-ui/components/cancellation-policy-combobox";
|
|
4
|
+
import { PriceCatalogCombobox } from "@voyantjs/pricing-ui/components/price-catalog-combobox";
|
|
5
|
+
import { PriceScheduleCombobox } from "@voyantjs/pricing-ui/components/price-schedule-combobox";
|
|
6
|
+
import { Button, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, Textarea, } from "@voyantjs/ui/components";
|
|
7
|
+
import { Loader2 } from "lucide-react";
|
|
8
|
+
import { useEffect } from "react";
|
|
9
|
+
import { useForm } from "react-hook-form";
|
|
10
|
+
import { z } from "zod/v4";
|
|
11
|
+
import { useProductDetailMessages } from "./host.js";
|
|
12
|
+
import { zodResolver } from "./zod-resolver.js";
|
|
13
|
+
const buildRuleFormSchema = (messages) => z.object({
|
|
14
|
+
priceCatalogId: z.string().min(1, messages.validationCatalogRequired),
|
|
15
|
+
priceScheduleId: z.string().optional().nullable(),
|
|
16
|
+
cancellationPolicyId: z.string().optional().nullable(),
|
|
17
|
+
name: z.string().min(1, messages.validationNameRequired).max(255),
|
|
18
|
+
code: z.string().max(100).optional().nullable(),
|
|
19
|
+
description: z.string().optional().nullable(),
|
|
20
|
+
pricingMode: z.enum(["per_person", "per_booking", "starting_from", "free", "on_request"]),
|
|
21
|
+
baseSell: z.coerce.number().min(0),
|
|
22
|
+
baseCost: z.coerce.number().min(0),
|
|
23
|
+
minPerBooking: z.coerce.number().int().min(0).optional().or(z.literal("")).nullable(),
|
|
24
|
+
maxPerBooking: z.coerce.number().int().min(0).optional().or(z.literal("")).nullable(),
|
|
25
|
+
allPricingCategories: z.boolean(),
|
|
26
|
+
isDefault: z.boolean(),
|
|
27
|
+
active: z.boolean(),
|
|
28
|
+
notes: z.string().optional().nullable(),
|
|
29
|
+
});
|
|
30
|
+
function initialValues(rule) {
|
|
31
|
+
if (rule) {
|
|
32
|
+
return {
|
|
33
|
+
priceCatalogId: rule.priceCatalogId,
|
|
34
|
+
priceScheduleId: rule.priceScheduleId ?? "",
|
|
35
|
+
cancellationPolicyId: rule.cancellationPolicyId ?? "",
|
|
36
|
+
name: rule.name,
|
|
37
|
+
code: rule.code ?? "",
|
|
38
|
+
description: rule.description ?? "",
|
|
39
|
+
pricingMode: rule.pricingMode,
|
|
40
|
+
baseSell: (rule.baseSellAmountCents ?? 0) / 100,
|
|
41
|
+
baseCost: (rule.baseCostAmountCents ?? 0) / 100,
|
|
42
|
+
minPerBooking: rule.minPerBooking ?? "",
|
|
43
|
+
maxPerBooking: rule.maxPerBooking ?? "",
|
|
44
|
+
allPricingCategories: rule.allPricingCategories,
|
|
45
|
+
isDefault: rule.isDefault,
|
|
46
|
+
active: rule.active,
|
|
47
|
+
notes: rule.notes ?? "",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
priceCatalogId: "",
|
|
52
|
+
priceScheduleId: "",
|
|
53
|
+
cancellationPolicyId: "",
|
|
54
|
+
name: "",
|
|
55
|
+
code: "",
|
|
56
|
+
description: "",
|
|
57
|
+
pricingMode: "per_person",
|
|
58
|
+
baseSell: 0,
|
|
59
|
+
baseCost: 0,
|
|
60
|
+
minPerBooking: "",
|
|
61
|
+
maxPerBooking: "",
|
|
62
|
+
allPricingCategories: true,
|
|
63
|
+
isDefault: false,
|
|
64
|
+
active: true,
|
|
65
|
+
notes: "",
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export function OptionPriceRuleForm({ productId, optionId, rule, onSuccess, onCancel, }) {
|
|
69
|
+
const messages = useProductDetailMessages();
|
|
70
|
+
const productMessages = messages.products.core;
|
|
71
|
+
const priceRuleMessages = messages.products.operations.priceRules;
|
|
72
|
+
const isEditing = !!rule;
|
|
73
|
+
const { create, update } = useOptionPriceRuleMutation();
|
|
74
|
+
const ruleFormSchema = buildRuleFormSchema(priceRuleMessages);
|
|
75
|
+
const pricingModes = [
|
|
76
|
+
{ value: "per_person", label: priceRuleMessages.pricingModePerPerson },
|
|
77
|
+
{ value: "per_booking", label: priceRuleMessages.pricingModePerBooking },
|
|
78
|
+
{ value: "starting_from", label: priceRuleMessages.pricingModeStartingFrom },
|
|
79
|
+
{ value: "free", label: priceRuleMessages.pricingModeFree },
|
|
80
|
+
{ value: "on_request", label: priceRuleMessages.pricingModeOnRequest },
|
|
81
|
+
];
|
|
82
|
+
const form = useForm({
|
|
83
|
+
resolver: zodResolver(ruleFormSchema),
|
|
84
|
+
defaultValues: initialValues(rule),
|
|
85
|
+
});
|
|
86
|
+
const watchedCatalogId = form.watch("priceCatalogId");
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
form.reset(initialValues(rule));
|
|
89
|
+
}, [rule, form]);
|
|
90
|
+
const onSubmit = async (values) => {
|
|
91
|
+
const payload = {
|
|
92
|
+
productId,
|
|
93
|
+
optionId,
|
|
94
|
+
priceCatalogId: values.priceCatalogId,
|
|
95
|
+
priceScheduleId: values.priceScheduleId || null,
|
|
96
|
+
cancellationPolicyId: values.cancellationPolicyId || null,
|
|
97
|
+
name: values.name,
|
|
98
|
+
code: values.code || null,
|
|
99
|
+
description: values.description || null,
|
|
100
|
+
pricingMode: values.pricingMode,
|
|
101
|
+
baseSellAmountCents: Math.round(values.baseSell * 100),
|
|
102
|
+
baseCostAmountCents: Math.round(values.baseCost * 100),
|
|
103
|
+
minPerBooking: typeof values.minPerBooking === "number" ? values.minPerBooking : null,
|
|
104
|
+
maxPerBooking: typeof values.maxPerBooking === "number" ? values.maxPerBooking : null,
|
|
105
|
+
allPricingCategories: values.allPricingCategories,
|
|
106
|
+
isDefault: values.isDefault,
|
|
107
|
+
active: values.active,
|
|
108
|
+
notes: values.notes || null,
|
|
109
|
+
};
|
|
110
|
+
if (isEditing) {
|
|
111
|
+
await update.mutateAsync({ id: rule.id, input: payload });
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
await create.mutateAsync(payload);
|
|
115
|
+
}
|
|
116
|
+
onSuccess();
|
|
117
|
+
};
|
|
118
|
+
return (_jsxs("form", { onSubmit: form.handleSubmit(onSubmit), className: "flex flex-1 flex-col gap-4 overflow-hidden", children: [_jsxs("div", { className: "grid gap-4", children: [_jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: priceRuleMessages.catalogLabel }), _jsx(PriceCatalogCombobox, { value: form.watch("priceCatalogId"), onChange: (value) => {
|
|
119
|
+
form.setValue("priceCatalogId", value ?? "", {
|
|
120
|
+
shouldDirty: true,
|
|
121
|
+
shouldValidate: true,
|
|
122
|
+
});
|
|
123
|
+
form.setValue("priceScheduleId", "", { shouldDirty: true });
|
|
124
|
+
} }), form.formState.errors.priceCatalogId && (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.priceCatalogId.message }))] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: priceRuleMessages.nameLabel }), _jsx(Input, { ...form.register("name"), placeholder: priceRuleMessages.namePlaceholder }), form.formState.errors.name && (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.name.message }))] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: priceRuleMessages.scheduleLabel }), _jsx(PriceScheduleCombobox, { priceCatalogId: watchedCatalogId, value: form.watch("priceScheduleId"), onChange: (value) => form.setValue("priceScheduleId", value ?? "", { shouldDirty: true }) })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: priceRuleMessages.cancellationPolicyLabel }), _jsx(CancellationPolicyCombobox, { value: form.watch("cancellationPolicyId"), onChange: (value) => form.setValue("cancellationPolicyId", value ?? "", { shouldDirty: true }) })] })] }), _jsxs("div", { className: "grid grid-cols-3 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: priceRuleMessages.pricingModeLabel }), _jsxs(Select, { value: form.watch("pricingMode"), onValueChange: (v) => form.setValue("pricingMode", v), items: pricingModes, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: pricingModes.map((m) => (_jsx(SelectItem, { value: m.value, children: m.label }, m.value))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: priceRuleMessages.baseSellInputLabel }), _jsx(Input, { ...form.register("baseSell"), type: "number", step: "0.01", min: "0" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: priceRuleMessages.baseCostInputLabel }), _jsx(Input, { ...form.register("baseCost"), type: "number", step: "0.01", min: "0" })] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: priceRuleMessages.codeLabel }), _jsx(Input, { ...form.register("code"), placeholder: priceRuleMessages.codePlaceholder })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: priceRuleMessages.descriptionLabel }), _jsx(Input, { ...form.register("description") })] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: priceRuleMessages.minPerBookingLabel }), _jsx(Input, { ...form.register("minPerBooking"), type: "number", min: "0" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: priceRuleMessages.maxPerBookingLabel }), _jsx(Input, { ...form.register("maxPerBooking"), type: "number", min: "0" })] })] }), _jsxs("div", { className: "grid grid-cols-3 gap-4", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { checked: form.watch("allPricingCategories"), onCheckedChange: (v) => form.setValue("allPricingCategories", v) }), _jsx(Label, { children: priceRuleMessages.allCategoriesSwitchLabel })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { checked: form.watch("isDefault"), onCheckedChange: (v) => form.setValue("isDefault", v) }), _jsx(Label, { children: priceRuleMessages.defaultSwitchLabel })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { checked: form.watch("active"), onCheckedChange: (v) => form.setValue("active", v) }), _jsx(Label, { children: priceRuleMessages.activeSwitchLabel })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: priceRuleMessages.notesLabel }), _jsx(Textarea, { ...form.register("notes") })] })] }), _jsxs("div", { className: "flex items-center justify-end gap-2", children: [onCancel ? (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: onCancel, children: productMessages.cancel })) : null, _jsxs(Button, { type: "submit", size: "sm", disabled: form.formState.isSubmitting, children: [form.formState.isSubmitting && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), isEditing ? productMessages.saveChanges : priceRuleMessages.create] })] })] }));
|
|
125
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type OptionPricingLayout } from "./product-options-shared.js";
|
|
2
|
+
export interface OptionPricingGridProps {
|
|
3
|
+
productId: string;
|
|
4
|
+
optionId: string;
|
|
5
|
+
optionName: string;
|
|
6
|
+
productCurrency: string;
|
|
7
|
+
layout: OptionPricingLayout;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The everyday pricing surface for a booking option: one table that merges
|
|
11
|
+
* inventory (rooms / traveler types) with what each traveler pays. The single
|
|
12
|
+
* default rate plan is auto-managed and hidden — agents never see catalogs or
|
|
13
|
+
* rate-plan chrome here (that lives under Advanced).
|
|
14
|
+
*/
|
|
15
|
+
export declare function OptionPricingGrid({ productId, optionId, optionName, productCurrency, layout, }: OptionPricingGridProps): import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
//# sourceMappingURL=product-option-pricing-grid.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"product-option-pricing-grid.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/product-option-pricing-grid.tsx"],"names":[],"mappings":"AAqBA,OAAO,EAML,KAAK,mBAAmB,EACzB,MAAM,6BAA6B,CAAA;AA8BpC,MAAM,WAAW,sBAAsB;IACrC,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,eAAe,EAAE,MAAM,CAAA;IACvB,MAAM,EAAE,mBAAmB,CAAA;CAC5B;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,SAAS,EACT,QAAQ,EACR,UAAU,EACV,eAAe,EACf,MAAM,GACP,EAAE,sBAAsB,2CAoWxB"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
4
|
+
import { formatMessage } from "@voyantjs/i18n";
|
|
5
|
+
import { useOptionPriceRuleMutation, useOptionUnitPriceRuleMutation, usePriceCatalogMutation, } from "@voyantjs/pricing-react";
|
|
6
|
+
import { useOptionUnitMutation, useVoyantProductsContext } from "@voyantjs/products-react";
|
|
7
|
+
import { Button } from "@voyantjs/ui/components/button";
|
|
8
|
+
import { Pencil, Plus, Trash2 } from "lucide-react";
|
|
9
|
+
import { useState } from "react";
|
|
10
|
+
import { useProductDetailMessages } from "./host.js";
|
|
11
|
+
import { categoryAppliesToUnit, formatProductMoney, getCategoryCondition, TravelerCategoryDialog, } from "./product-options-pricing.js";
|
|
12
|
+
import { getOptionPriceRulesQueryOptions, getOptionUnitPriceRulesQueryOptions, getOptionUnitsQueryOptions, getPriceCatalogsQueryOptions, getPricingCategoriesQueryOptions, } from "./product-options-shared.js";
|
|
13
|
+
import { UnitDialog } from "./product-unit-dialog.js";
|
|
14
|
+
import { UnitPriceRuleDialog, } from "./product-unit-price-rule-dialog.js";
|
|
15
|
+
function formatAvailability(unit, messages) {
|
|
16
|
+
if (unit.maxQuantity != null && unit.maxQuantity > 0) {
|
|
17
|
+
return formatMessage(messages.perDeparture, { count: unit.maxQuantity });
|
|
18
|
+
}
|
|
19
|
+
return "—";
|
|
20
|
+
}
|
|
21
|
+
function unitSubtitle(unit, layout, messages) {
|
|
22
|
+
if (layout === "rooms") {
|
|
23
|
+
const sleeps = unit.occupancyMax ?? unit.occupancyMin;
|
|
24
|
+
return sleeps != null ? formatMessage(messages.sleeps, { count: sleeps }) : null;
|
|
25
|
+
}
|
|
26
|
+
if (unit.minAge != null || unit.maxAge != null) {
|
|
27
|
+
return `${unit.minAge ?? 0}–${unit.maxAge ?? "∞"}`;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The everyday pricing surface for a booking option: one table that merges
|
|
33
|
+
* inventory (rooms / traveler types) with what each traveler pays. The single
|
|
34
|
+
* default rate plan is auto-managed and hidden — agents never see catalogs or
|
|
35
|
+
* rate-plan chrome here (that lives under Advanced).
|
|
36
|
+
*/
|
|
37
|
+
export function OptionPricingGrid({ productId, optionId, optionName, productCurrency, layout, }) {
|
|
38
|
+
const client = useVoyantProductsContext();
|
|
39
|
+
const messages = useProductDetailMessages();
|
|
40
|
+
const t = messages.products.operations.pricingGrid;
|
|
41
|
+
const { data: unitsData, refetch: refetchUnits } = useQuery(getOptionUnitsQueryOptions(client, optionId));
|
|
42
|
+
const { data: rulesData, refetch: refetchRules } = useQuery(getOptionPriceRulesQueryOptions(client, optionId));
|
|
43
|
+
const { data: categoriesData, refetch: refetchCategories } = useQuery(getPricingCategoriesQueryOptions(client));
|
|
44
|
+
const { data: catalogsData } = useQuery(getPriceCatalogsQueryOptions(client));
|
|
45
|
+
const rules = rulesData?.data ?? [];
|
|
46
|
+
const defaultRule = rules.find((rule) => rule.isDefault) ?? rules[0];
|
|
47
|
+
const { data: cellsData, refetch: refetchCells } = useQuery({
|
|
48
|
+
...getOptionUnitPriceRulesQueryOptions(client, defaultRule?.id ?? "__none__"),
|
|
49
|
+
enabled: Boolean(defaultRule?.id),
|
|
50
|
+
});
|
|
51
|
+
const { remove: removeUnit } = useOptionUnitMutation();
|
|
52
|
+
const { remove: removeCell } = useOptionUnitPriceRuleMutation();
|
|
53
|
+
const { create: createRule } = useOptionPriceRuleMutation();
|
|
54
|
+
const { create: createCatalog } = usePriceCatalogMutation();
|
|
55
|
+
const deleteUnitMutation = useMutation({
|
|
56
|
+
mutationFn: (id) => removeUnit.mutateAsync(id),
|
|
57
|
+
onSuccess: () => {
|
|
58
|
+
void refetchUnits();
|
|
59
|
+
void refetchCells();
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
const deleteCellMutation = useMutation({
|
|
63
|
+
mutationFn: (id) => removeCell.mutateAsync(id),
|
|
64
|
+
onSuccess: () => void refetchCells(),
|
|
65
|
+
});
|
|
66
|
+
const [unitDialogOpen, setUnitDialogOpen] = useState(false);
|
|
67
|
+
const [editingUnit, setEditingUnit] = useState();
|
|
68
|
+
const [defaultUnitType, setDefaultUnitType] = useState("room");
|
|
69
|
+
const [categoryDialogOpen, setCategoryDialogOpen] = useState(false);
|
|
70
|
+
const [cellDialogOpen, setCellDialogOpen] = useState(false);
|
|
71
|
+
const [cellRuleId, setCellRuleId] = useState();
|
|
72
|
+
const [editingCell, setEditingCell] = useState();
|
|
73
|
+
const [preselectedUnitId, setPreselectedUnitId] = useState();
|
|
74
|
+
const [preselectedCategoryId, setPreselectedCategoryId] = useState();
|
|
75
|
+
const units = (unitsData?.data ?? [])
|
|
76
|
+
.filter((unit) => !unit.isHidden)
|
|
77
|
+
.slice()
|
|
78
|
+
.sort((a, b) => a.sortOrder - b.sortOrder);
|
|
79
|
+
// Inventory wins over the booking-mode hint: an option that actually holds
|
|
80
|
+
// rooms (or vehicles/groups) is always priced as a rooms grid, even if the
|
|
81
|
+
// product's booking mode was set to a per-person type. The `layout` prop only
|
|
82
|
+
// decides the shape for a brand-new option that has no inventory yet.
|
|
83
|
+
const hasRoomLikeUnits = units.some((unit) => unit.unitType === "room" || unit.unitType === "vehicle" || unit.unitType === "group");
|
|
84
|
+
const hasPersonUnits = units.some((unit) => unit.unitType === "person");
|
|
85
|
+
const effectiveLayout = hasRoomLikeUnits
|
|
86
|
+
? "rooms"
|
|
87
|
+
: hasPersonUnits
|
|
88
|
+
? "seats"
|
|
89
|
+
: layout;
|
|
90
|
+
const cells = cellsData?.data ?? [];
|
|
91
|
+
const referencedCategoryIds = new Set(cells.flatMap((cell) => (cell.pricingCategoryId ? [cell.pricingCategoryId] : [])));
|
|
92
|
+
const categories = (categoriesData?.data ?? [])
|
|
93
|
+
.filter((category) => category.active &&
|
|
94
|
+
(((category.productId == null || category.productId === productId) &&
|
|
95
|
+
(category.optionId == null || category.optionId === optionId)) ||
|
|
96
|
+
referencedCategoryIds.has(category.id)))
|
|
97
|
+
.slice()
|
|
98
|
+
.sort((a, b) => a.sortOrder - b.sortOrder);
|
|
99
|
+
// Traveler-type columns. Seats layout prices each traveler-type row once
|
|
100
|
+
// (single price column). Rooms layout splits price by traveler category once
|
|
101
|
+
// any exist, else shows a single base-price column per room.
|
|
102
|
+
const columns = effectiveLayout === "rooms" && categories.length > 0
|
|
103
|
+
? categories.map((category) => ({
|
|
104
|
+
id: category.id,
|
|
105
|
+
name: category.name,
|
|
106
|
+
metadata: category.metadata,
|
|
107
|
+
}))
|
|
108
|
+
: [{ id: null, name: t.priceColumn }];
|
|
109
|
+
const nextUnitSortOrder = units.length > 0 ? Math.max(...units.map((u) => u.sortOrder)) + 1 : 0;
|
|
110
|
+
const findCell = (unitId, categoryId) => cells.find((cell) => cell.unitId === unitId && (cell.pricingCategoryId ?? null) === categoryId) ?? null;
|
|
111
|
+
// Lazily materialize the hidden default rate plan (and a default catalog if
|
|
112
|
+
// the tenant has none) the first time the agent enters a price. Keeps the
|
|
113
|
+
// common path free of any rate-plan/catalog ceremony.
|
|
114
|
+
async function ensureRatePlanId() {
|
|
115
|
+
if (defaultRule?.id)
|
|
116
|
+
return defaultRule.id;
|
|
117
|
+
const catalogs = catalogsData?.data ?? [];
|
|
118
|
+
const existingCatalog = catalogs.find((catalog) => catalog.isDefault) ?? catalogs[0];
|
|
119
|
+
const catalogId = existingCatalog?.id ??
|
|
120
|
+
(await createCatalog.mutateAsync({
|
|
121
|
+
code: "default",
|
|
122
|
+
name: t.priceColumn,
|
|
123
|
+
catalogType: "public",
|
|
124
|
+
isDefault: true,
|
|
125
|
+
})).id;
|
|
126
|
+
const created = await createRule.mutateAsync({
|
|
127
|
+
productId,
|
|
128
|
+
optionId,
|
|
129
|
+
priceCatalogId: catalogId,
|
|
130
|
+
name: optionName,
|
|
131
|
+
pricingMode: "per_person",
|
|
132
|
+
baseSellAmountCents: 0,
|
|
133
|
+
baseCostAmountCents: 0,
|
|
134
|
+
allPricingCategories: effectiveLayout === "seats",
|
|
135
|
+
isDefault: true,
|
|
136
|
+
active: true,
|
|
137
|
+
});
|
|
138
|
+
await refetchRules();
|
|
139
|
+
return created.id;
|
|
140
|
+
}
|
|
141
|
+
async function openCellDialog(unit, categoryId) {
|
|
142
|
+
const ruleId = await ensureRatePlanId();
|
|
143
|
+
setCellRuleId(ruleId);
|
|
144
|
+
setEditingCell(undefined);
|
|
145
|
+
setPreselectedUnitId(unit.id);
|
|
146
|
+
setPreselectedCategoryId(categoryId);
|
|
147
|
+
setCellDialogOpen(true);
|
|
148
|
+
}
|
|
149
|
+
const addRoomOrTraveler = () => {
|
|
150
|
+
setEditingUnit(undefined);
|
|
151
|
+
setDefaultUnitType(effectiveLayout === "rooms" ? "room" : "person");
|
|
152
|
+
setUnitDialogOpen(true);
|
|
153
|
+
};
|
|
154
|
+
const unitColumnLabel = effectiveLayout === "rooms" ? t.roomColumn : t.travelerColumn;
|
|
155
|
+
return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { children: [_jsx("p", { className: "text-sm font-medium", children: effectiveLayout === "rooms" ? t.roomsTitle : t.seatsTitle }), _jsx("p", { className: "text-xs text-muted-foreground", children: effectiveLayout === "rooms" ? t.roomsDescription : t.seatsDescription })] }), _jsxs("div", { className: "flex items-center gap-2", children: [effectiveLayout === "rooms" ? (_jsxs(Button, { variant: "outline", size: "sm", onClick: () => setCategoryDialogOpen(true), children: [_jsx(Plus, { className: "mr-1 h-3.5 w-3.5" }), t.addTravelerType] })) : null, _jsxs(Button, { variant: "outline", size: "sm", onClick: addRoomOrTraveler, children: [_jsx(Plus, { className: "mr-1 h-3.5 w-3.5" }), effectiveLayout === "rooms" ? t.addRoom : t.addTravelerType] })] })] }), units.length === 0 ? (_jsx("p", { className: "rounded-md border bg-background px-3 py-6 text-center text-sm text-muted-foreground", children: effectiveLayout === "rooms" ? t.emptyRooms : t.emptySeats })) : (_jsx("div", { className: "overflow-x-auto rounded-md border bg-background", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b bg-muted/40 text-muted-foreground", children: [_jsx("th", { className: "p-2.5 text-left font-medium", children: unitColumnLabel }), _jsx("th", { className: "p-2.5 text-left font-medium", children: t.availableColumn }), columns.map((column) => {
|
|
156
|
+
const condition = getCategoryCondition(column.metadata);
|
|
157
|
+
return (_jsxs("th", { className: "p-2.5 text-left font-medium", children: [_jsx("div", { children: column.name }), condition ? (_jsx("div", { className: "mt-0.5 max-w-[220px] text-[10px] font-normal normal-case leading-snug text-muted-foreground", children: condition })) : null] }, column.id ?? "__base__"));
|
|
158
|
+
}), _jsx("th", { className: "w-[72px] p-2.5 text-right font-medium" })] }) }), _jsx("tbody", { children: units.map((unit) => {
|
|
159
|
+
const subtitle = unitSubtitle(unit, effectiveLayout, t);
|
|
160
|
+
return (_jsxs("tr", { className: "border-b last:border-b-0", children: [_jsxs("td", { className: "p-2.5", children: [_jsx("div", { className: "font-medium", children: unit.name }), subtitle ? (_jsx("div", { className: "text-[11px] text-muted-foreground", children: subtitle })) : null] }), _jsx("td", { className: "p-2.5 text-muted-foreground", children: formatAvailability(unit, t) }), columns.map((column) => {
|
|
161
|
+
const cell = findCell(unit.id, column.id);
|
|
162
|
+
const canPrice = categoryAppliesToUnit(column, unit);
|
|
163
|
+
return (_jsx("td", { className: "p-2.5", children: cell ? (_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("button", { type: "button", onClick: () => {
|
|
164
|
+
setCellRuleId(defaultRule?.id);
|
|
165
|
+
setEditingCell(cell);
|
|
166
|
+
setPreselectedUnitId(undefined);
|
|
167
|
+
setPreselectedCategoryId(undefined);
|
|
168
|
+
setCellDialogOpen(true);
|
|
169
|
+
}, className: "font-mono text-foreground hover:underline", children: formatProductMoney(cell.sellAmountCents, productCurrency) }), _jsx("button", { type: "button", "aria-label": t.deleteRoom, onClick: () => deleteCellMutation.mutate(cell.id), className: "text-muted-foreground hover:text-destructive", children: _jsx(Trash2, { className: "h-3 w-3" }) })] })) : canPrice ? (_jsxs("button", { type: "button", onClick: () => void openCellDialog(unit, column.id), className: "inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground", children: [_jsx(Plus, { className: "h-3 w-3" }), t.setPrice] })) : (_jsx("span", { className: "text-muted-foreground", children: "\u2014" })) }, column.id ?? "__base__"));
|
|
170
|
+
}), _jsx("td", { className: "p-2.5", children: _jsxs("div", { className: "flex items-center justify-end gap-1", children: [_jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": t.editRoom, onClick: () => {
|
|
171
|
+
setEditingUnit(unit);
|
|
172
|
+
setDefaultUnitType(unit.unitType);
|
|
173
|
+
setUnitDialogOpen(true);
|
|
174
|
+
}, children: _jsx(Pencil, { className: "h-4 w-4" }) }), _jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": t.deleteRoom, onClick: () => {
|
|
175
|
+
if (confirm(formatMessage(t.deleteRoomConfirm, { name: unit.name }))) {
|
|
176
|
+
deleteUnitMutation.mutate(unit.id);
|
|
177
|
+
}
|
|
178
|
+
}, children: _jsx(Trash2, { className: "h-4 w-4" }) })] }) })] }, unit.id));
|
|
179
|
+
}) })] }) })), _jsx(UnitDialog, { open: unitDialogOpen, onOpenChange: setUnitDialogOpen, optionId: optionId, unit: editingUnit, defaultUnitType: editingUnit ? undefined : defaultUnitType, lockUnitType: true, nextSortOrder: nextUnitSortOrder, onSuccess: () => {
|
|
180
|
+
setUnitDialogOpen(false);
|
|
181
|
+
setEditingUnit(undefined);
|
|
182
|
+
void refetchUnits();
|
|
183
|
+
} }), _jsx(TravelerCategoryDialog, { open: categoryDialogOpen, onOpenChange: setCategoryDialogOpen, productId: productId, units: units, nextSortOrder: categories.length > 0 ? Math.max(...categories.map((c) => c.sortOrder)) + 1 : 0, onSuccess: () => {
|
|
184
|
+
setCategoryDialogOpen(false);
|
|
185
|
+
void refetchCategories();
|
|
186
|
+
} }), _jsx(UnitPriceRuleDialog, { open: cellDialogOpen, onOpenChange: setCellDialogOpen, optionPriceRuleId: cellRuleId ?? defaultRule?.id ?? "", optionId: optionId, units: units, productCurrency: productCurrency, preselectedUnitId: preselectedUnitId, preselectedCategoryId: preselectedCategoryId, cell: editingCell, onSuccess: () => {
|
|
187
|
+
setCellDialogOpen(false);
|
|
188
|
+
setEditingCell(undefined);
|
|
189
|
+
setPreselectedUnitId(undefined);
|
|
190
|
+
setPreselectedCategoryId(undefined);
|
|
191
|
+
void refetchCells();
|
|
192
|
+
} })] }));
|
|
193
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
import { useProductDetailMessages } from "./host.js";
|
|
3
|
+
import { type OptionPricingLayout } from "./product-options-shared.js";
|
|
4
|
+
import type { OptionUnitData } from "./product-unit-dialog.js";
|
|
5
|
+
export declare function getUnitTypeLabel(type: OptionUnitData["unitType"], messages: ReturnType<typeof useProductDetailMessages>["products"]["operations"]["units"]): string;
|
|
6
|
+
export declare function getCategoryCondition(metadata: Record<string, unknown> | null | undefined): string | null;
|
|
7
|
+
export declare function categoryAppliesToUnit(category: {
|
|
8
|
+
id: string | null;
|
|
9
|
+
metadata?: Record<string, unknown> | null;
|
|
10
|
+
}, unit: OptionUnitData): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Per-option pricing surface. The everyday view is the merged rooms/seats
|
|
13
|
+
* grid; the full rate-plan machinery (multiple plans, catalogs, cost prices,
|
|
14
|
+
* cancellation) plus any injected per-departure inventory live behind an
|
|
15
|
+
* Advanced disclosure so low-tech agents never have to see them.
|
|
16
|
+
*/
|
|
17
|
+
export declare function PricingPanel({ productId, optionId, optionName, productCurrency, layout, extras, }: {
|
|
18
|
+
productId: string;
|
|
19
|
+
optionId: string;
|
|
20
|
+
optionName: string;
|
|
21
|
+
productCurrency: string;
|
|
22
|
+
layout: OptionPricingLayout;
|
|
23
|
+
extras?: React.ReactNode;
|
|
24
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
25
|
+
export declare function TravelerCategoryDialog({ open, onOpenChange, productId, units, nextSortOrder, onSuccess, }: {
|
|
26
|
+
open: boolean;
|
|
27
|
+
onOpenChange: (open: boolean) => void;
|
|
28
|
+
productId: string;
|
|
29
|
+
units: OptionUnitData[];
|
|
30
|
+
nextSortOrder: number;
|
|
31
|
+
onSuccess: () => void;
|
|
32
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
33
|
+
export declare function formatProductMoney(amountCents: number | null | undefined, currency: string): string;
|
|
34
|
+
//# sourceMappingURL=product-options-pricing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"product-options-pricing.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/product-options-pricing.tsx"],"names":[],"mappings":"AAuCA,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAA;AAEnC,OAAO,EAAE,wBAAwB,EAAE,MAAM,WAAW,CAAA;AAMpD,OAAO,EAKL,KAAK,mBAAmB,EACzB,MAAM,6BAA6B,CAAA;AACpC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAA;AA0B9D,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,EAChC,QAAQ,EAAE,UAAU,CAAC,OAAO,wBAAwB,CAAC,CAAC,UAAU,CAAC,CAAC,YAAY,CAAC,CAAC,OAAO,CAAC,UAkBzF;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,iBAGxF;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE;IAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;CAAE,EAC1E,IAAI,EAAE,cAAc,WAMrB;AAeD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,EAC3B,SAAS,EACT,QAAQ,EACR,UAAU,EACV,eAAe,EACf,MAAM,EACN,MAAM,GACP,EAAE;IACD,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,eAAe,EAAE,MAAM,CAAA;IACvB,MAAM,EAAE,mBAAmB,CAAA;IAC3B,MAAM,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CACzB,2CA+CA;AAwcD,wBAAgB,sBAAsB,CAAC,EACrC,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,KAAK,EACL,aAAa,EACb,SAAS,GACV,EAAE;IACD,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,cAAc,EAAE,CAAA;IACvB,aAAa,EAAE,MAAM,CAAA;IACrB,SAAS,EAAE,MAAM,IAAI,CAAA;CACtB,2CAgNA;AAgOD,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,UAG1F"}
|