@voyantjs/products-ui 0.101.1 → 0.101.2

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 (121) 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 +217 -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 +177 -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-options-pricing.d.ts +6 -0
  74. package/dist/components/product-detail/product-options-pricing.d.ts.map +1 -0
  75. package/dist/components/product-detail/product-options-pricing.js +363 -0
  76. package/dist/components/product-detail/product-options-shared.d.ts +609 -0
  77. package/dist/components/product-detail/product-options-shared.d.ts.map +1 -0
  78. package/dist/components/product-detail/product-options-shared.js +34 -0
  79. package/dist/components/product-detail/product-payment-policy-section.d.ts +17 -0
  80. package/dist/components/product-detail/product-payment-policy-section.d.ts.map +1 -0
  81. package/dist/components/product-detail/product-payment-policy-section.js +58 -0
  82. package/dist/components/product-detail/product-schedule-dialog.d.ts +11 -0
  83. package/dist/components/product-detail/product-schedule-dialog.d.ts.map +1 -0
  84. package/dist/components/product-detail/product-schedule-dialog.js +10 -0
  85. package/dist/components/product-detail/product-schedule-form.d.ts +17 -0
  86. package/dist/components/product-detail/product-schedule-form.d.ts.map +1 -0
  87. package/dist/components/product-detail/product-schedule-form.js +222 -0
  88. package/dist/components/product-detail/product-service-dialog.d.ts +12 -0
  89. package/dist/components/product-detail/product-service-dialog.d.ts.map +1 -0
  90. package/dist/components/product-detail/product-service-dialog.js +10 -0
  91. package/dist/components/product-detail/product-service-form.d.ts +22 -0
  92. package/dist/components/product-detail/product-service-form.d.ts.map +1 -0
  93. package/dist/components/product-detail/product-service-form.js +154 -0
  94. package/dist/components/product-detail/product-translation-popover.d.ts +91 -0
  95. package/dist/components/product-detail/product-translation-popover.d.ts.map +1 -0
  96. package/dist/components/product-detail/product-translation-popover.js +217 -0
  97. package/dist/components/product-detail/product-unit-dialog.d.ts +12 -0
  98. package/dist/components/product-detail/product-unit-dialog.d.ts.map +1 -0
  99. package/dist/components/product-detail/product-unit-dialog.js +10 -0
  100. package/dist/components/product-detail/product-unit-form.d.ts +26 -0
  101. package/dist/components/product-detail/product-unit-form.d.ts.map +1 -0
  102. package/dist/components/product-detail/product-unit-form.js +109 -0
  103. package/dist/components/product-detail/product-unit-price-rule-dialog.d.ts +16 -0
  104. package/dist/components/product-detail/product-unit-price-rule-dialog.d.ts.map +1 -0
  105. package/dist/components/product-detail/product-unit-price-rule-dialog.js +10 -0
  106. package/dist/components/product-detail/product-unit-price-rule-form.d.ts +28 -0
  107. package/dist/components/product-detail/product-unit-price-rule-form.d.ts.map +1 -0
  108. package/dist/components/product-detail/product-unit-price-rule-form.js +126 -0
  109. package/dist/components/product-detail/timezone-options.d.ts +9 -0
  110. package/dist/components/product-detail/timezone-options.d.ts.map +1 -0
  111. package/dist/components/product-detail/timezone-options.js +28 -0
  112. package/dist/components/product-detail/use-product-detail-data.d.ts +41 -0
  113. package/dist/components/product-detail/use-product-detail-data.d.ts.map +1 -0
  114. package/dist/components/product-detail/use-product-detail-data.js +143 -0
  115. package/dist/components/product-detail/use-product-detail-dialogs.d.ts +24 -0
  116. package/dist/components/product-detail/use-product-detail-dialogs.d.ts.map +1 -0
  117. package/dist/components/product-detail/use-product-detail-dialogs.js +40 -0
  118. package/dist/components/product-detail/zod-resolver.d.ts +4 -0
  119. package/dist/components/product-detail/zod-resolver.d.ts.map +1 -0
  120. package/dist/components/product-detail/zod-resolver.js +39 -0
  121. 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,6 @@
1
+ export declare function PricingPanel({ productId, optionId, productCurrency, }: {
2
+ productId: string;
3
+ optionId: string;
4
+ productCurrency: string;
5
+ }): import("react/jsx-runtime").JSX.Element;
6
+ //# 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":"AA+HA,wBAAgB,YAAY,CAAC,EAC3B,SAAS,EACT,QAAQ,EACR,eAAe,GAChB,EAAE;IACD,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,eAAe,EAAE,MAAM,CAAA;CACxB,2CA6EA"}
@@ -0,0 +1,363 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMutation, useQuery } from "@tanstack/react-query";
3
+ import { useProductExtras } from "@voyantjs/extras-react";
4
+ import { formatMessage } from "@voyantjs/i18n";
5
+ import { useExtraPriceRuleMutation, useExtraPriceRules, useOptionPriceRuleMutation, useOptionUnitPriceRuleMutation, usePricingCategoryMutation, } from "@voyantjs/pricing-react";
6
+ import { useVoyantProductsContext } from "@voyantjs/products-react";
7
+ import { Badge, Button, Dialog, DialogBody, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Textarea, } from "@voyantjs/ui/components";
8
+ import { Checkbox } from "@voyantjs/ui/components/checkbox";
9
+ import { MoreHorizontal, Pencil, Plus, Trash2 } from "lucide-react";
10
+ import { useEffect, useState } from "react";
11
+ import { useProductDetailMessages } from "./host.js";
12
+ import { OptionPriceRuleDialog, } from "./product-option-price-rule-dialog.js";
13
+ import { getOptionPriceRulesQueryOptions, getOptionUnitPriceRulesQueryOptions, getOptionUnitsQueryOptions, getPricingCategoriesQueryOptions, } from "./product-options-shared.js";
14
+ import { UnitPriceRuleDialog, } from "./product-unit-price-rule-dialog.js";
15
+ function getRulePricingModeLabel(value, messages) {
16
+ switch (value) {
17
+ case "per_person":
18
+ return messages.pricingModePerPerson;
19
+ case "per_booking":
20
+ return messages.pricingModePerBooking;
21
+ case "starting_from":
22
+ return messages.pricingModeStartingFrom;
23
+ case "free":
24
+ return messages.pricingModeFree;
25
+ case "on_request":
26
+ return messages.pricingModeOnRequest;
27
+ default:
28
+ return value;
29
+ }
30
+ }
31
+ function getUnitTypeLabel(type, messages) {
32
+ switch (type) {
33
+ case "person":
34
+ return messages.typePerson;
35
+ case "group":
36
+ return messages.typeGroup;
37
+ case "room":
38
+ return messages.typeRoom;
39
+ case "vehicle":
40
+ return messages.typeVehicle;
41
+ case "service":
42
+ return messages.typeService;
43
+ case "other":
44
+ return messages.typeOther;
45
+ default:
46
+ return type;
47
+ }
48
+ }
49
+ function getCategoryCondition(metadata) {
50
+ const condition = metadata?.condition;
51
+ return typeof condition === "string" && condition.trim().length > 0 ? condition : null;
52
+ }
53
+ function categoryAppliesToUnit(category, unit) {
54
+ if (!category.id)
55
+ return true;
56
+ const allowedUnitIds = category.metadata?.allowedUnitIds;
57
+ if (!Array.isArray(allowedUnitIds) || allowedUnitIds.length === 0)
58
+ return true;
59
+ return allowedUnitIds.includes(unit.id);
60
+ }
61
+ function ActionMenu({ children }) {
62
+ return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 text-muted-foreground", children: _jsx(MoreHorizontal, { className: "h-4 w-4" }) }) }), _jsx(DropdownMenuContent, { align: "end", children: children })] }));
63
+ }
64
+ export function PricingPanel({ productId, optionId, productCurrency, }) {
65
+ const messages = useProductDetailMessages();
66
+ const client = useVoyantProductsContext();
67
+ const priceRuleMessages = messages.products.operations.priceRules;
68
+ const [ruleDialogOpen, setRuleDialogOpen] = useState(false);
69
+ const [editingRule, setEditingRule] = useState();
70
+ const { data, refetch } = useQuery(getOptionPriceRulesQueryOptions(client, optionId));
71
+ const { remove: removeRule } = useOptionPriceRuleMutation();
72
+ const deleteMutation = useMutation({
73
+ mutationFn: (id) => removeRule.mutateAsync(id),
74
+ onSuccess: () => void refetch(),
75
+ });
76
+ const rules = data?.data ?? [];
77
+ return (_jsxs("div", { children: [_jsxs("div", { className: "mb-2 flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("p", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: priceRuleMessages.sectionTitle }), _jsx("p", { className: "text-xs text-muted-foreground", children: priceRuleMessages.sectionDescription })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => {
78
+ setEditingRule(undefined);
79
+ setRuleDialogOpen(true);
80
+ }, children: [_jsx(Plus, { className: "mr-1 h-3 w-3" }), priceRuleMessages.addAction] })] }), rules.length === 0 ? (_jsx("p", { className: "py-2 text-center text-xs text-muted-foreground", children: priceRuleMessages.empty })) : (_jsx("div", { className: "flex flex-col gap-3", children: rules.map((rule) => (_jsx(PriceRuleCard, { rule: rule, productId: productId, optionId: optionId, productCurrency: productCurrency, onEdit: () => {
81
+ setEditingRule(rule);
82
+ setRuleDialogOpen(true);
83
+ }, onDelete: () => {
84
+ if (confirm(formatMessage(priceRuleMessages.deleteRuleConfirm, { name: rule.name }))) {
85
+ deleteMutation.mutate(rule.id);
86
+ }
87
+ } }, rule.id))) })), _jsx(OptionPriceRuleDialog, { open: ruleDialogOpen, onOpenChange: setRuleDialogOpen, productId: productId, optionId: optionId, rule: editingRule, onSuccess: () => {
88
+ setRuleDialogOpen(false);
89
+ setEditingRule(undefined);
90
+ void refetch();
91
+ } })] }));
92
+ }
93
+ function PriceRuleCard({ rule, productId, optionId, productCurrency, onEdit, onDelete, }) {
94
+ const messages = useProductDetailMessages();
95
+ const priceRuleMessages = messages.products.operations.priceRules;
96
+ return (_jsxs("div", { className: "rounded-lg border bg-background p-4", children: [_jsxs("div", { className: "flex items-start justify-between", children: [_jsxs("div", { className: "flex flex-col gap-1", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-sm font-medium", children: rule.name }), _jsx(Badge, { variant: "outline", className: "text-xs capitalize", children: getRulePricingModeLabel(rule.pricingMode, priceRuleMessages) }), rule.isDefault && _jsx(Badge, { variant: "secondary", children: priceRuleMessages.defaultBadge }), _jsx(Badge, { variant: rule.active ? "default" : "outline", children: rule.active ? priceRuleMessages.activeBadge : priceRuleMessages.inactiveBadge })] }), _jsxs("div", { className: "flex items-center gap-3 text-xs text-muted-foreground", children: [_jsxs("span", { children: [priceRuleMessages.baseSellLabel, ":", " ", _jsx("span", { className: "font-mono text-foreground", children: formatProductMoney(rule.baseSellAmountCents, productCurrency) })] }), _jsxs("span", { children: [priceRuleMessages.baseCostLabel, ":", " ", _jsx("span", { className: "font-mono text-foreground", children: formatProductMoney(rule.baseCostAmountCents, productCurrency) })] }), rule.allPricingCategories && _jsx("span", { children: priceRuleMessages.allCategoriesLabel })] })] }), _jsxs(ActionMenu, { children: [_jsxs(DropdownMenuItem, { onClick: onEdit, children: [_jsx(Pencil, { className: "h-4 w-4" }), priceRuleMessages.editAction] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { variant: "destructive", onClick: onDelete, children: [_jsx(Trash2, { className: "h-4 w-4" }), priceRuleMessages.deleteAction] })] })] }), _jsxs("div", { className: "mt-3", children: [_jsx(UnitPriceMatrix, { productId: productId, optionPriceRuleId: rule.id, optionId: optionId, pricingMode: rule.pricingMode, allPricingCategories: rule.allPricingCategories, productCurrency: productCurrency }), _jsx(ExtraPriceRulesPanel, { productId: productId, optionId: optionId, optionPriceRuleId: rule.id, productCurrency: productCurrency })] })] }));
97
+ }
98
+ function UnitPriceMatrix({ productId, optionPriceRuleId, optionId, pricingMode, allPricingCategories, productCurrency, }) {
99
+ const messages = useProductDetailMessages();
100
+ const client = useVoyantProductsContext();
101
+ const priceRuleMessages = messages.products.operations.priceRules;
102
+ const unitMessages = messages.products.operations.units;
103
+ const [dialogOpen, setDialogOpen] = useState(false);
104
+ const [editingCell, setEditingCell] = useState();
105
+ const [preselectedUnitId, setPreselectedUnitId] = useState();
106
+ const [preselectedCategoryId, setPreselectedCategoryId] = useState();
107
+ const [categoryDialogOpen, setCategoryDialogOpen] = useState(false);
108
+ const { data: unitsData } = useQuery(getOptionUnitsQueryOptions(client, optionId));
109
+ const { data: categoriesData, refetch: refetchCategories } = useQuery(getPricingCategoriesQueryOptions(client));
110
+ const { data: cellsData, refetch: refetchCells } = useQuery(getOptionUnitPriceRulesQueryOptions(client, optionPriceRuleId));
111
+ const { remove } = useOptionUnitPriceRuleMutation();
112
+ const deleteMutation = useMutation({
113
+ mutationFn: (id) => remove.mutateAsync(id),
114
+ onSuccess: () => void refetchCells(),
115
+ });
116
+ const units = (unitsData?.data ?? []).slice().sort((a, b) => a.sortOrder - b.sortOrder);
117
+ const cells = cellsData?.data ?? [];
118
+ const referencedCategoryIds = new Set(cells.flatMap((cell) => (cell.pricingCategoryId ? [cell.pricingCategoryId] : [])));
119
+ const categories = (categoriesData?.data ?? []).filter((category) => category.active &&
120
+ (((category.productId == null || category.productId === productId) &&
121
+ (category.optionId == null || category.optionId === optionId)) ||
122
+ referencedCategoryIds.has(category.id)));
123
+ const isPersonOnly = units.length > 0 && units.every((unit) => unit.unitType === "person");
124
+ const findCell = (unitId, categoryId) => cells.find((cell) => cell.unitId === unitId && (cell.pricingCategoryId ?? null) === categoryId) ?? null;
125
+ if (units.length === 0) {
126
+ return _jsx("p", { className: "text-xs italic text-muted-foreground", children: priceRuleMessages.addUnitsHint });
127
+ }
128
+ if (pricingMode === "per_booking") {
129
+ return (_jsx("p", { className: "text-xs italic text-muted-foreground", children: priceRuleMessages.perBookingFlatHint }));
130
+ }
131
+ // Per-pax tour with no category cross-cut: render a simple unit-only table
132
+ // (Sell / Cost) instead of the unit×category matrix. Operators on
133
+ // accommodation products (or rules with allPricingCategories=false) still
134
+ // get the full matrix.
135
+ const useSimpleTable = pricingMode === "per_person" && allPricingCategories;
136
+ const tableTitle = useSimpleTable
137
+ ? isPersonOnly
138
+ ? priceRuleMessages.personUnitPricingTitle
139
+ : priceRuleMessages.unitPricingTitle
140
+ : isPersonOnly
141
+ ? priceRuleMessages.personUnitCategoryTitle
142
+ : priceRuleMessages.unitCategoryTitle;
143
+ const unitColumnLabel = isPersonOnly
144
+ ? priceRuleMessages.tableTravelerUnit
145
+ : priceRuleMessages.tableUnit;
146
+ const columns = useSimpleTable
147
+ ? [{ id: null, name: priceRuleMessages.tableSell }]
148
+ : categories.length > 0
149
+ ? categories.map((category) => ({
150
+ id: category.id,
151
+ name: category.name,
152
+ metadata: category.metadata,
153
+ }))
154
+ : [{ id: null, name: priceRuleMessages.defaultBadge }];
155
+ return (_jsxs("div", { children: [_jsxs("div", { className: "mb-2 flex items-center justify-between", children: [_jsx("p", { className: "text-[10px] font-medium uppercase tracking-wide text-muted-foreground", children: tableTitle }), !useSimpleTable ? (_jsxs(Button, { variant: "outline", size: "sm", onClick: () => setCategoryDialogOpen(true), children: [_jsx(Plus, { className: "mr-1 h-3 w-3" }), priceRuleMessages.addTravelerCategory] })) : null] }), _jsx("div", { className: "overflow-x-auto rounded border", children: _jsxs("table", { className: "w-full text-xs", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b bg-muted/50 text-muted-foreground", children: [_jsx("th", { className: "p-2 text-left font-medium", children: unitColumnLabel }), columns.map((category) => {
156
+ const condition = getCategoryCondition(category.metadata);
157
+ return (_jsxs("th", { className: "p-2 text-left font-medium", children: [_jsx("div", { children: category.name }), condition ? (_jsx("div", { className: "mt-0.5 max-w-[220px] text-[10px] font-normal leading-snug text-muted-foreground normal-case", children: condition })) : null] }, category.id ?? "__default__"));
158
+ })] }) }), _jsx("tbody", { children: units.map((unit) => (_jsxs("tr", { className: "border-b last:border-b-0", children: [_jsxs("td", { className: "p-2 font-medium", children: [unit.name, _jsxs("span", { className: "ml-1 text-[10px] text-muted-foreground", children: ["(", getUnitTypeLabel(unit.unitType, unitMessages), ")"] })] }), columns.map((category) => {
159
+ const cell = findCell(unit.id, category.id);
160
+ const canPriceCategory = categoryAppliesToUnit(category, unit);
161
+ return (_jsx("td", { className: "p-2", children: cell ? (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx("button", { type: "button", onClick: () => {
162
+ setEditingCell(cell);
163
+ setPreselectedUnitId(undefined);
164
+ setPreselectedCategoryId(undefined);
165
+ setDialogOpen(true);
166
+ }, className: "font-mono text-foreground hover:underline", children: formatProductMoney(cell.sellAmountCents, productCurrency) }), _jsx("button", { type: "button", onClick: () => {
167
+ if (confirm(priceRuleMessages.deleteCellConfirm)) {
168
+ deleteMutation.mutate(cell.id);
169
+ }
170
+ }, className: "text-muted-foreground hover:text-destructive", children: _jsx(Trash2, { className: "h-3 w-3" }) })] })) : canPriceCategory ? (_jsx("button", { type: "button", onClick: () => {
171
+ setEditingCell(undefined);
172
+ setPreselectedUnitId(unit.id);
173
+ setPreselectedCategoryId(category.id);
174
+ setDialogOpen(true);
175
+ }, className: "text-muted-foreground hover:text-foreground", children: _jsx(Plus, { className: "h-3 w-3" }) })) : (_jsx("span", { className: "text-muted-foreground", children: "-" })) }, category.id ?? "__default__"));
176
+ })] }, unit.id))) })] }) }), _jsx(TravelerCategoryDialog, { open: categoryDialogOpen, onOpenChange: setCategoryDialogOpen, productId: productId, units: units, nextSortOrder: categories.length > 0 ? Math.max(...categories.map((c) => c.sortOrder)) + 1 : 0, onSuccess: () => {
177
+ setCategoryDialogOpen(false);
178
+ void refetchCategories();
179
+ } }), _jsx(UnitPriceRuleDialog, { open: dialogOpen, onOpenChange: setDialogOpen, optionPriceRuleId: optionPriceRuleId, optionId: optionId, units: units, preselectedUnitId: preselectedUnitId, preselectedCategoryId: preselectedCategoryId, cell: editingCell, onSuccess: () => {
180
+ setDialogOpen(false);
181
+ setEditingCell(undefined);
182
+ setPreselectedUnitId(undefined);
183
+ setPreselectedCategoryId(undefined);
184
+ void refetchCells();
185
+ } })] }));
186
+ }
187
+ function initialTravelerCategoryState() {
188
+ return {
189
+ name: "",
190
+ code: "",
191
+ categoryType: "child",
192
+ minAge: "",
193
+ maxAge: "",
194
+ condition: "",
195
+ allowedUnitIds: [],
196
+ };
197
+ }
198
+ function parseOptionalInteger(value) {
199
+ const trimmed = value.trim();
200
+ if (!trimmed)
201
+ return null;
202
+ const parsed = Number(trimmed);
203
+ return Number.isFinite(parsed) ? Math.trunc(parsed) : null;
204
+ }
205
+ function TravelerCategoryDialog({ open, onOpenChange, productId, units, nextSortOrder, onSuccess, }) {
206
+ const messages = useProductDetailMessages();
207
+ const priceRuleMessages = messages.products.operations.priceRules;
208
+ const pricingCategoryMessages = messages.pricing.categories;
209
+ const { create } = usePricingCategoryMutation();
210
+ const [state, setState] = useState(() => initialTravelerCategoryState());
211
+ const [error, setError] = useState(null);
212
+ const travelerCategoryTypes = [
213
+ { value: "adult", label: pricingCategoryMessages.typeAdult },
214
+ { value: "child", label: pricingCategoryMessages.typeChild },
215
+ { value: "infant", label: pricingCategoryMessages.typeInfant },
216
+ { value: "senior", label: pricingCategoryMessages.typeSenior },
217
+ { value: "group", label: pricingCategoryMessages.typeGroup },
218
+ { value: "other", label: pricingCategoryMessages.typeOther },
219
+ ];
220
+ useEffect(() => {
221
+ if (open) {
222
+ setState(initialTravelerCategoryState());
223
+ setError(null);
224
+ }
225
+ }, [open]);
226
+ const toggleUnit = (unitId, checked) => {
227
+ setState((prev) => ({
228
+ ...prev,
229
+ allowedUnitIds: checked
230
+ ? [...prev.allowedUnitIds, unitId]
231
+ : prev.allowedUnitIds.filter((id) => id !== unitId),
232
+ }));
233
+ };
234
+ const save = async () => {
235
+ const name = state.name.trim();
236
+ if (!name) {
237
+ setError(priceRuleMessages.travelerCategoryNameRequired);
238
+ return;
239
+ }
240
+ const selectedUnits = units.filter((unit) => state.allowedUnitIds.includes(unit.id));
241
+ const minAge = parseOptionalInteger(state.minAge);
242
+ const maxAge = parseOptionalInteger(state.maxAge);
243
+ const condition = state.condition.trim();
244
+ const metadata = {};
245
+ if (condition)
246
+ metadata.condition = condition;
247
+ if (selectedUnits.length > 0) {
248
+ metadata.allowedUnitIds = selectedUnits.map((unit) => unit.id);
249
+ metadata.allowedUnitCodes = selectedUnits.map((unit) => unit.code).filter(Boolean);
250
+ metadata.allowedUnitNames = selectedUnits.map((unit) => unit.name);
251
+ }
252
+ try {
253
+ await create.mutateAsync({
254
+ productId,
255
+ optionId: null,
256
+ unitId: null,
257
+ name,
258
+ code: state.code.trim() || null,
259
+ categoryType: state.categoryType,
260
+ seatOccupancy: 1,
261
+ isAgeQualified: minAge != null || maxAge != null,
262
+ minAge,
263
+ maxAge,
264
+ internalUseOnly: false,
265
+ active: true,
266
+ sortOrder: nextSortOrder,
267
+ metadata: Object.keys(metadata).length > 0 ? metadata : null,
268
+ });
269
+ onSuccess();
270
+ }
271
+ catch (err) {
272
+ setError(err instanceof Error ? err.message : priceRuleMessages.travelerCategorySaveFailed);
273
+ }
274
+ };
275
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: priceRuleMessages.travelerCategoryDialogTitle }), _jsx(DialogDescription, { children: priceRuleMessages.travelerCategoryDialogDescription })] }), _jsxs(DialogBody, { className: "grid gap-4", children: [_jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "traveler-category-name", children: pricingCategoryMessages.nameLabel }), _jsx(Input, { id: "traveler-category-name", autoFocus: true, value: state.name, placeholder: priceRuleMessages.travelerCategoryNamePlaceholder, onChange: (event) => setState((prev) => ({ ...prev, name: event.target.value })) })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "traveler-category-code", children: pricingCategoryMessages.codeLabel }), _jsx(Input, { id: "traveler-category-code", value: state.code, placeholder: priceRuleMessages.travelerCategoryCodePlaceholder, onChange: (event) => setState((prev) => ({ ...prev, code: event.target.value })) })] })] }), _jsxs("div", { className: "grid gap-4 sm:grid-cols-3", children: [_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: pricingCategoryMessages.typeLabel }), _jsxs(Select, { value: state.categoryType, onValueChange: (value) => setState((prev) => ({
276
+ ...prev,
277
+ categoryType: (value ?? "child"),
278
+ })), items: travelerCategoryTypes, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: travelerCategoryTypes.map((type) => (_jsx(SelectItem, { value: type.value, children: type.label }, type.value))) })] })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "traveler-category-min-age", children: pricingCategoryMessages.minAgeLabel }), _jsx(Input, { id: "traveler-category-min-age", type: "number", min: "0", value: state.minAge, onChange: (event) => setState((prev) => ({ ...prev, minAge: event.target.value })) })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "traveler-category-max-age", children: pricingCategoryMessages.maxAgeLabel }), _jsx(Input, { id: "traveler-category-max-age", type: "number", min: "0", value: state.maxAge, onChange: (event) => setState((prev) => ({ ...prev, maxAge: event.target.value })) })] })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: priceRuleMessages.travelerCategoryAppliesToLabel }), _jsx("div", { className: "grid gap-2 rounded border p-3 sm:grid-cols-3", children: units.map((unit) => {
279
+ const checkboxId = `traveler-category-unit-${unit.id}`;
280
+ return (_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx(Checkbox, { id: checkboxId, checked: state.allowedUnitIds.includes(unit.id), onCheckedChange: (checked) => toggleUnit(unit.id, checked === true) }), _jsx(Label, { htmlFor: checkboxId, className: "font-normal", children: unit.name })] }, unit.id));
281
+ }) }), _jsx("p", { className: "text-muted-foreground text-xs", children: priceRuleMessages.travelerCategoryAppliesToHint })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "traveler-category-condition", children: priceRuleMessages.travelerCategoryConditionLabel }), _jsx(Textarea, { id: "traveler-category-condition", value: state.condition, placeholder: priceRuleMessages.travelerCategoryConditionPlaceholder, onChange: (event) => setState((prev) => ({ ...prev, condition: event.target.value })) })] }), error ? _jsx("p", { className: "text-sm text-destructive", children: error }) : null] }), _jsxs(DialogFooter, { className: "-mx-6 -mb-6", children: [_jsx(Button, { variant: "ghost", onClick: () => onOpenChange(false), children: pricingCategoryMessages.cancel }), _jsx(Button, { onClick: () => void save(), disabled: create.isPending, children: priceRuleMessages.createTravelerCategory })] })] }) }));
282
+ }
283
+ function ExtraPriceRulesPanel({ productId, optionId, optionPriceRuleId, productCurrency, }) {
284
+ const messages = useProductDetailMessages();
285
+ const extraPriceMessages = messages.products.operations.extraPrices;
286
+ const extrasQuery = useProductExtras({ productId, active: true, limit: 100 });
287
+ const rulesQuery = useExtraPriceRules({ optionPriceRuleId, optionId, active: true, limit: 100 });
288
+ const { remove } = useExtraPriceRuleMutation();
289
+ const [pricingExtraId, setPricingExtraId] = useState(null);
290
+ const extras = extrasQuery.data?.data ?? [];
291
+ const rules = rulesQuery.data?.data ?? [];
292
+ const ruleByExtraId = new Map(rules.flatMap((rule) => (rule.productExtraId ? [[rule.productExtraId, rule]] : [])));
293
+ if (extras.length === 0)
294
+ return null;
295
+ const pricingExtra = extras.find((extra) => extra.id === pricingExtraId) ?? null;
296
+ return (_jsxs("div", { className: "mt-4 border-t pt-3", children: [_jsx("div", { className: "mb-2 text-[10px] font-medium uppercase tracking-wide text-muted-foreground", children: extraPriceMessages.sectionTitle }), _jsx("div", { className: "flex flex-col gap-2", children: extras.map((extra) => {
297
+ const rule = ruleByExtraId.get(extra.id);
298
+ return (_jsxs("div", { className: "flex items-center justify-between gap-3 rounded border px-2 py-1.5 text-xs", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("span", { className: "font-medium", children: extra.name }), extra.pricedPerPerson ? (_jsx("span", { className: "ml-2 text-muted-foreground", children: extraPriceMessages.perTraveler })) : null] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "font-mono", children: rule?.sellAmountCents != null
299
+ ? formatProductMoney(rule.sellAmountCents, productCurrency)
300
+ : extraPriceMessages.noAmount }), _jsx(Button, { variant: "outline", size: "sm", onClick: () => setPricingExtraId(extra.id), children: extraPriceMessages.setPrice }), rule ? (_jsx(Button, { variant: "ghost", size: "sm", onClick: () => remove.mutate(rule.id, { onSuccess: () => void rulesQuery.refetch() }), children: extraPriceMessages.remove })) : null] })] }, extra.id));
301
+ }) }), pricingExtra ? (_jsx(ExtraPriceRuleDialog, { open: !!pricingExtra, onOpenChange: (open) => {
302
+ if (!open)
303
+ setPricingExtraId(null);
304
+ }, optionPriceRuleId: optionPriceRuleId, optionId: optionId, extra: pricingExtra, existingRule: ruleByExtraId.get(pricingExtra.id), nextSortOrder: rules.length, productCurrency: productCurrency, onSuccess: () => {
305
+ setPricingExtraId(null);
306
+ void rulesQuery.refetch();
307
+ } })) : null] }));
308
+ }
309
+ function ExtraPriceRuleDialog({ open, onOpenChange, optionPriceRuleId, optionId, extra, existingRule, nextSortOrder, productCurrency, onSuccess, }) {
310
+ const messages = useProductDetailMessages();
311
+ const extraPriceMessages = messages.products.operations.extraPrices;
312
+ const { create, update } = useExtraPriceRuleMutation();
313
+ const [amount, setAmount] = useState("");
314
+ const [pricingMode, setPricingMode] = useState("per_booking");
315
+ const isEditing = !!existingRule;
316
+ const pricingModes = [
317
+ { value: "per_booking", label: extraPriceMessages.pricingPerBooking },
318
+ { value: "per_person", label: extraPriceMessages.pricingPerPerson },
319
+ { value: "included", label: extraPriceMessages.pricingIncluded },
320
+ { value: "on_request", label: extraPriceMessages.pricingOnRequest },
321
+ { value: "unavailable", label: extraPriceMessages.pricingUnavailable },
322
+ ];
323
+ useEffect(() => {
324
+ setAmount(existingRule?.sellAmountCents != null ? String(existingRule.sellAmountCents / 100) : "");
325
+ setPricingMode(existingRule?.pricingMode ?? defaultExtraPriceRuleMode(extra));
326
+ }, [existingRule, extra]);
327
+ const save = async () => {
328
+ const parsedAmount = amount.trim() === "" ? null : Math.round(Number(amount) * 100);
329
+ if (parsedAmount != null && (!Number.isFinite(parsedAmount) || parsedAmount < 0))
330
+ return;
331
+ const payload = {
332
+ optionPriceRuleId,
333
+ optionId,
334
+ productExtraId: extra.id,
335
+ optionExtraConfigId: null,
336
+ pricingMode,
337
+ sellAmountCents: parsedAmount,
338
+ costAmountCents: null,
339
+ active: true,
340
+ sortOrder: existingRule?.sortOrder ?? nextSortOrder,
341
+ };
342
+ if (existingRule)
343
+ await update.mutateAsync({ id: existingRule.id, input: payload });
344
+ else
345
+ await create.mutateAsync(payload);
346
+ onSuccess();
347
+ };
348
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: isEditing ? extraPriceMessages.editTitle : extraPriceMessages.newTitle }), _jsx(DialogDescription, { children: extra.name })] }), _jsxs(DialogBody, { className: "grid gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: extraPriceMessages.pricingModeLabel }), _jsxs(Select, { value: pricingMode, onValueChange: (value) => setPricingMode((value ?? "per_booking")), items: pricingModes, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: pricingModes.map((mode) => (_jsx(SelectItem, { value: mode.value, children: mode.label }, mode.value))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: extraPriceMessages.sellAmountLabel }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { value: amount, type: "number", min: "0", step: "0.01", onChange: (event) => setAmount(event.target.value) }), _jsx("span", { className: "min-w-12 text-muted-foreground text-sm", children: productCurrency })] })] })] }), _jsxs(DialogFooter, { className: "-mx-6 -mb-6", children: [_jsx(Button, { variant: "ghost", onClick: () => onOpenChange(false), children: extraPriceMessages.cancel }), _jsx(Button, { onClick: () => void save(), children: extraPriceMessages.save })] })] }) }));
349
+ }
350
+ function defaultExtraPriceRuleMode(extra) {
351
+ if (extra.pricedPerPerson || extra.pricingMode === "per_person")
352
+ return "per_person";
353
+ if (extra.pricingMode === "included" || extra.pricingMode === "free")
354
+ return "included";
355
+ if (extra.pricingMode === "on_request")
356
+ return "on_request";
357
+ return "per_booking";
358
+ }
359
+ function formatProductMoney(amountCents, currency) {
360
+ if (amountCents == null)
361
+ return "-";
362
+ return `${(amountCents / 100).toFixed(2)} ${currency}`;
363
+ }