@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.
Files changed (126) hide show
  1. package/dist/components/product-detail/date-picker.d.ts +44 -0
  2. package/dist/components/product-detail/date-picker.d.ts.map +1 -0
  3. package/dist/components/product-detail/date-picker.js +125 -0
  4. package/dist/components/product-detail/host.d.ts +53 -0
  5. package/dist/components/product-detail/host.d.ts.map +1 -0
  6. package/dist/components/product-detail/host.js +24 -0
  7. package/dist/components/product-detail/index.d.ts +6 -0
  8. package/dist/components/product-detail/index.d.ts.map +1 -0
  9. package/dist/components/product-detail/index.js +5 -0
  10. package/dist/components/product-detail/product-activity-section.d.ts +4 -0
  11. package/dist/components/product-detail/product-activity-section.d.ts.map +1 -0
  12. package/dist/components/product-detail/product-activity-section.js +37 -0
  13. package/dist/components/product-detail/product-day-sheet.d.ts +14 -0
  14. package/dist/components/product-detail/product-day-sheet.d.ts.map +1 -0
  15. package/dist/components/product-detail/product-day-sheet.js +75 -0
  16. package/dist/components/product-detail/product-day-translation.d.ts +41 -0
  17. package/dist/components/product-detail/product-day-translation.d.ts.map +1 -0
  18. package/dist/components/product-detail/product-day-translation.js +111 -0
  19. package/dist/components/product-detail/product-departure-dialog.d.ts +11 -0
  20. package/dist/components/product-detail/product-departure-dialog.d.ts.map +1 -0
  21. package/dist/components/product-detail/product-departure-dialog.js +10 -0
  22. package/dist/components/product-detail/product-departure-form.d.ts +25 -0
  23. package/dist/components/product-detail/product-departure-form.d.ts.map +1 -0
  24. package/dist/components/product-detail/product-departure-form.js +237 -0
  25. package/dist/components/product-detail/product-departure-pricing-override-dialog.d.ts +8 -0
  26. package/dist/components/product-detail/product-departure-pricing-override-dialog.d.ts.map +1 -0
  27. package/dist/components/product-detail/product-departure-pricing-override-dialog.js +125 -0
  28. package/dist/components/product-detail/product-detail-day-row.d.ts +14 -0
  29. package/dist/components/product-detail/product-detail-day-row.d.ts.map +1 -0
  30. package/dist/components/product-detail/product-detail-day-row.js +43 -0
  31. package/dist/components/product-detail/product-detail-dialog.d.ts +10 -0
  32. package/dist/components/product-detail/product-detail-dialog.d.ts.map +1 -0
  33. package/dist/components/product-detail/product-detail-dialog.js +10 -0
  34. package/dist/components/product-detail/product-detail-form.d.ts +19 -0
  35. package/dist/components/product-detail/product-detail-form.d.ts.map +1 -0
  36. package/dist/components/product-detail/product-detail-form.js +180 -0
  37. package/dist/components/product-detail/product-detail-header.d.ts +12 -0
  38. package/dist/components/product-detail/product-detail-header.d.ts.map +1 -0
  39. package/dist/components/product-detail/product-detail-header.js +19 -0
  40. package/dist/components/product-detail/product-detail-itinerary-section.d.ts +4 -0
  41. package/dist/components/product-detail/product-detail-itinerary-section.d.ts.map +1 -0
  42. package/dist/components/product-detail/product-detail-itinerary-section.js +201 -0
  43. package/dist/components/product-detail/product-detail-page.d.ts +4 -0
  44. package/dist/components/product-detail/product-detail-page.d.ts.map +1 -0
  45. package/dist/components/product-detail/product-detail-page.js +97 -0
  46. package/dist/components/product-detail/product-detail-sections.d.ts +63 -0
  47. package/dist/components/product-detail/product-detail-sections.d.ts.map +1 -0
  48. package/dist/components/product-detail/product-detail-sections.js +143 -0
  49. package/dist/components/product-detail/product-detail-shared.d.ts +264 -0
  50. package/dist/components/product-detail/product-detail-shared.d.ts.map +1 -0
  51. package/dist/components/product-detail/product-detail-shared.js +157 -0
  52. package/dist/components/product-detail/product-detail-skeleton.d.ts +9 -0
  53. package/dist/components/product-detail/product-detail-skeleton.d.ts.map +1 -0
  54. package/dist/components/product-detail/product-detail-skeleton.js +53 -0
  55. package/dist/components/product-detail/product-extras-section.d.ts +4 -0
  56. package/dist/components/product-detail/product-extras-section.d.ts.map +1 -0
  57. package/dist/components/product-detail/product-extras-section.js +141 -0
  58. package/dist/components/product-detail/product-itinerary-form.d.ts +16 -0
  59. package/dist/components/product-detail/product-itinerary-form.d.ts.map +1 -0
  60. package/dist/components/product-detail/product-itinerary-form.js +38 -0
  61. package/dist/components/product-detail/product-market-rules-section.d.ts +6 -0
  62. package/dist/components/product-detail/product-market-rules-section.d.ts.map +1 -0
  63. package/dist/components/product-detail/product-market-rules-section.js +81 -0
  64. package/dist/components/product-detail/product-media-gallery.d.ts +19 -0
  65. package/dist/components/product-detail/product-media-gallery.d.ts.map +1 -0
  66. package/dist/components/product-detail/product-media-gallery.js +114 -0
  67. package/dist/components/product-detail/product-option-price-rule-dialog.d.ts +12 -0
  68. package/dist/components/product-detail/product-option-price-rule-dialog.d.ts.map +1 -0
  69. package/dist/components/product-detail/product-option-price-rule-dialog.js +10 -0
  70. package/dist/components/product-detail/product-option-price-rule-form.d.ts +29 -0
  71. package/dist/components/product-detail/product-option-price-rule-form.d.ts.map +1 -0
  72. package/dist/components/product-detail/product-option-price-rule-form.js +125 -0
  73. package/dist/components/product-detail/product-option-pricing-grid.d.ts +16 -0
  74. package/dist/components/product-detail/product-option-pricing-grid.d.ts.map +1 -0
  75. package/dist/components/product-detail/product-option-pricing-grid.js +193 -0
  76. package/dist/components/product-detail/product-options-pricing.d.ts +34 -0
  77. package/dist/components/product-detail/product-options-pricing.d.ts.map +1 -0
  78. package/dist/components/product-detail/product-options-pricing.js +385 -0
  79. package/dist/components/product-detail/product-options-shared.d.ts +623 -0
  80. package/dist/components/product-detail/product-options-shared.d.ts.map +1 -0
  81. package/dist/components/product-detail/product-options-shared.js +54 -0
  82. package/dist/components/product-detail/product-payment-policy-section.d.ts +17 -0
  83. package/dist/components/product-detail/product-payment-policy-section.d.ts.map +1 -0
  84. package/dist/components/product-detail/product-payment-policy-section.js +58 -0
  85. package/dist/components/product-detail/product-schedule-dialog.d.ts +11 -0
  86. package/dist/components/product-detail/product-schedule-dialog.d.ts.map +1 -0
  87. package/dist/components/product-detail/product-schedule-dialog.js +10 -0
  88. package/dist/components/product-detail/product-schedule-form.d.ts +17 -0
  89. package/dist/components/product-detail/product-schedule-form.d.ts.map +1 -0
  90. package/dist/components/product-detail/product-schedule-form.js +222 -0
  91. package/dist/components/product-detail/product-service-dialog.d.ts +12 -0
  92. package/dist/components/product-detail/product-service-dialog.d.ts.map +1 -0
  93. package/dist/components/product-detail/product-service-dialog.js +10 -0
  94. package/dist/components/product-detail/product-service-form.d.ts +22 -0
  95. package/dist/components/product-detail/product-service-form.d.ts.map +1 -0
  96. package/dist/components/product-detail/product-service-form.js +154 -0
  97. package/dist/components/product-detail/product-translation-popover.d.ts +91 -0
  98. package/dist/components/product-detail/product-translation-popover.d.ts.map +1 -0
  99. package/dist/components/product-detail/product-translation-popover.js +217 -0
  100. package/dist/components/product-detail/product-unit-dialog.d.ts +14 -0
  101. package/dist/components/product-detail/product-unit-dialog.d.ts.map +1 -0
  102. package/dist/components/product-detail/product-unit-dialog.js +10 -0
  103. package/dist/components/product-detail/product-unit-form.d.ts +34 -0
  104. package/dist/components/product-detail/product-unit-form.d.ts.map +1 -0
  105. package/dist/components/product-detail/product-unit-form.js +139 -0
  106. package/dist/components/product-detail/product-unit-price-rule-dialog.d.ts +17 -0
  107. package/dist/components/product-detail/product-unit-price-rule-dialog.d.ts.map +1 -0
  108. package/dist/components/product-detail/product-unit-price-rule-dialog.js +10 -0
  109. package/dist/components/product-detail/product-unit-price-rule-form.d.ts +29 -0
  110. package/dist/components/product-detail/product-unit-price-rule-form.d.ts.map +1 -0
  111. package/dist/components/product-detail/product-unit-price-rule-form.js +145 -0
  112. package/dist/components/product-detail/timezone-options.d.ts +9 -0
  113. package/dist/components/product-detail/timezone-options.d.ts.map +1 -0
  114. package/dist/components/product-detail/timezone-options.js +28 -0
  115. package/dist/components/product-detail/use-product-detail-data.d.ts +41 -0
  116. package/dist/components/product-detail/use-product-detail-data.d.ts.map +1 -0
  117. package/dist/components/product-detail/use-product-detail-data.js +143 -0
  118. package/dist/components/product-detail/use-product-detail-dialogs.d.ts +24 -0
  119. package/dist/components/product-detail/use-product-detail-dialogs.d.ts.map +1 -0
  120. package/dist/components/product-detail/use-product-detail-dialogs.js +40 -0
  121. package/dist/components/product-detail/zod-resolver.d.ts +4 -0
  122. package/dist/components/product-detail/zod-resolver.d.ts.map +1 -0
  123. package/dist/components/product-detail/zod-resolver.js +39 -0
  124. package/dist/components/product-options-section.d.ts.map +1 -1
  125. package/dist/components/product-options-section.js +31 -20
  126. 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"}