@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 @@
1
+ {"version":3,"file":"product-translation-popover.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/product-translation-popover.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,217 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useProductTranslationMutation, useProductTranslations, } from "@voyantjs/products-react";
3
+ import { Button, Input, Label } from "@voyantjs/ui/components";
4
+ import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from "@voyantjs/ui/components/combobox";
5
+ import { Popover, PopoverContent, PopoverTrigger } from "@voyantjs/ui/components/popover";
6
+ import { RichTextEditor } from "@voyantjs/ui/components/rich-text-editor";
7
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@voyantjs/ui/components/tooltip";
8
+ import { cn } from "@voyantjs/ui/lib/utils";
9
+ import { languages } from "@voyantjs/utils/languages";
10
+ import { Globe, Plus, X } from "lucide-react";
11
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
12
+ function recordToDraft(record) {
13
+ return {
14
+ id: record.id,
15
+ languageTag: record.languageTag,
16
+ name: record.name,
17
+ description: record.description ?? "",
18
+ slug: record.slug ?? "",
19
+ shortDescription: record.shortDescription,
20
+ inclusionsHtml: record.inclusionsHtml,
21
+ exclusionsHtml: record.exclusionsHtml,
22
+ termsHtml: record.termsHtml,
23
+ seoTitle: record.seoTitle,
24
+ seoDescription: record.seoDescription,
25
+ };
26
+ }
27
+ function emptyDraft(languageTag) {
28
+ return {
29
+ id: null,
30
+ languageTag,
31
+ name: "",
32
+ description: "",
33
+ slug: "",
34
+ shortDescription: null,
35
+ inclusionsHtml: null,
36
+ exclusionsHtml: null,
37
+ termsHtml: null,
38
+ seoTitle: null,
39
+ seoDescription: null,
40
+ };
41
+ }
42
+ // Rich text is "set" only when it has visible text, not just empty markup like <p></p>.
43
+ export function richTextHasContent(html) {
44
+ return (html
45
+ .replace(/<[^>]*>/g, "")
46
+ .replace(/&nbsp;/g, " ")
47
+ .trim().length > 0);
48
+ }
49
+ function fieldHasContent(draft, field) {
50
+ if (field === "description")
51
+ return richTextHasContent(draft.description);
52
+ return draft[field].trim().length > 0;
53
+ }
54
+ export function languageLabel(tag) {
55
+ const base = tag.split("-")[0]?.toLowerCase() ?? tag;
56
+ return languages[base] ?? tag;
57
+ }
58
+ /**
59
+ * Manages an in-memory draft of a product's translations so Name/Description/
60
+ * Slug can be edited in context from the edit sheet. Seeds from the saved
61
+ * translation records and persists create/update/delete on save.
62
+ *
63
+ * The base product columns hold the default language's Name/Description, so the
64
+ * default-language translation row (if any) just mirrors them and carries the
65
+ * slug (base has no slug column). Fields we don't edit here (short description,
66
+ * inclusions, SEO, …) are preserved untouched.
67
+ */
68
+ export function useProductTranslationDrafts(productId) {
69
+ const query = useProductTranslations(productId ?? undefined, {
70
+ limit: 100,
71
+ enabled: !!productId,
72
+ });
73
+ const mutations = useProductTranslationMutation();
74
+ const [drafts, setDrafts] = useState([]);
75
+ const seededKey = useRef(null);
76
+ const existingRef = useRef([]);
77
+ useEffect(() => {
78
+ const key = productId ?? "__new__";
79
+ if (productId && query.isPending)
80
+ return;
81
+ if (seededKey.current === key)
82
+ return;
83
+ const records = query.data?.data ?? [];
84
+ existingRef.current = records;
85
+ setDrafts(records.map(recordToDraft));
86
+ seededKey.current = key;
87
+ }, [productId, query.isPending, query.data]);
88
+ const setFieldValue = useCallback((languageTag, field, value) => {
89
+ setDrafts((prev) => {
90
+ if (prev.some((draft) => draft.languageTag === languageTag)) {
91
+ return prev.map((draft) => draft.languageTag === languageTag ? { ...draft, [field]: value } : draft);
92
+ }
93
+ return [...prev, { ...emptyDraft(languageTag), [field]: value }];
94
+ });
95
+ }, []);
96
+ const addLanguage = useCallback((languageTag) => {
97
+ setDrafts((prev) => prev.some((draft) => draft.languageTag === languageTag)
98
+ ? prev
99
+ : [...prev, emptyDraft(languageTag)]);
100
+ }, []);
101
+ const removeLanguage = useCallback((languageTag) => {
102
+ setDrafts((prev) => prev.filter((draft) => draft.languageTag !== languageTag));
103
+ }, []);
104
+ const persist = useCallback(async (resolvedProductId, options) => {
105
+ const { defaultLanguageTag, baseName, baseDescription } = options;
106
+ const original = existingRef.current;
107
+ const currentLanguages = new Set(drafts.map((draft) => draft.languageTag));
108
+ const deletes = original
109
+ .filter((record) => !currentLanguages.has(record.languageTag))
110
+ .map((record) => mutations.remove.mutateAsync({
111
+ productId: resolvedProductId,
112
+ translationId: record.id,
113
+ }));
114
+ const upserts = drafts.map((draft) => {
115
+ const isDefault = draft.languageTag === defaultLanguageTag;
116
+ // The default-language row mirrors the base columns so public serving
117
+ // (which prefers translations) stays consistent with what's edited.
118
+ // When base is empty we keep the row's own value rather than wiping it —
119
+ // legacy products often have empty base columns with content only here.
120
+ const name = isDefault ? baseName : draft.name.trim() || baseName;
121
+ const description = isDefault
122
+ ? richTextHasContent(baseDescription)
123
+ ? baseDescription
124
+ : richTextHasContent(draft.description)
125
+ ? draft.description
126
+ : null
127
+ : richTextHasContent(draft.description)
128
+ ? draft.description
129
+ : null;
130
+ const slug = draft.slug.trim() ? draft.slug.trim() : null;
131
+ if (draft.id) {
132
+ return mutations.update.mutateAsync({
133
+ productId: resolvedProductId,
134
+ translationId: draft.id,
135
+ input: { name, description, slug },
136
+ });
137
+ }
138
+ // A brand-new row is only worth creating once it carries content.
139
+ const isEmpty = isDefault
140
+ ? !slug
141
+ : !draft.name.trim() && !richTextHasContent(draft.description) && !slug;
142
+ if (isEmpty)
143
+ return Promise.resolve(null);
144
+ return mutations.create.mutateAsync({
145
+ productId: resolvedProductId,
146
+ input: { languageTag: draft.languageTag, name, description, slug },
147
+ });
148
+ });
149
+ await Promise.all([...deletes, ...upserts]);
150
+ // Force a reseed from the refreshed server state so a second save patches
151
+ // (with real ids) instead of re-creating.
152
+ seededKey.current = null;
153
+ }, [drafts, mutations]);
154
+ return {
155
+ drafts,
156
+ isLoading: !!productId && query.isPending,
157
+ setFieldValue,
158
+ addLanguage,
159
+ removeLanguage,
160
+ persist,
161
+ };
162
+ }
163
+ /** Top-of-sheet switcher: picks which language every translatable field edits. */
164
+ export function ContentLanguageSwitcher({ activeLanguage, defaultLanguageTag, languageTags, messages, onSelect, onAddLanguage, onRemoveLanguage, }) {
165
+ const [addOpen, setAddOpen] = useState(false);
166
+ const otherLanguages = languageTags.filter((tag) => tag !== defaultLanguageTag);
167
+ return (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx("span", { className: "text-xs font-medium text-muted-foreground", children: messages.editingLanguageLabel }), _jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [_jsx(LanguageChip, { active: activeLanguage === defaultLanguageTag, languageTag: defaultLanguageTag, badge: messages.defaultBadge, onSelect: () => onSelect(defaultLanguageTag) }), otherLanguages.map((tag) => (_jsx(LanguageChip, { active: activeLanguage === tag, languageTag: tag, onSelect: () => onSelect(tag), onRemove: () => onRemoveLanguage(tag), removeLabel: messages.translationRemoveLanguage }, tag))), _jsxs(Popover, { open: addOpen, onOpenChange: setAddOpen, children: [_jsx(PopoverTrigger, { render: _jsxs(Button, { type: "button", variant: "outline", size: "sm", className: "h-7 border-dashed", children: [_jsx(Plus, { className: "size-3.5" }), messages.addLanguage] }) }), _jsx(PopoverContent, { align: "start", className: "w-64", children: _jsx(LanguageCombobox, { value: "", exclude: [defaultLanguageTag, ...otherLanguages], placeholder: messages.translationLanguageSearch, emptyLabel: messages.translationLanguageEmpty, onValueChange: (code) => {
168
+ if (code) {
169
+ onAddLanguage(code);
170
+ setAddOpen(false);
171
+ }
172
+ } }) })] })] })] }));
173
+ }
174
+ function LanguageChip({ active, languageTag, badge, onSelect, onRemove, removeLabel, }) {
175
+ return (_jsxs("div", { className: cn("inline-flex items-center gap-1.5 rounded-md border px-2 py-1 text-xs transition-colors", active
176
+ ? "border-primary bg-primary/10 text-foreground"
177
+ : "border-input text-muted-foreground hover:bg-accent"), children: [_jsxs("button", { type: "button", onClick: onSelect, className: "inline-flex items-center gap-1.5", children: [_jsx("span", { className: "font-medium", children: languageLabel(languageTag) }), _jsx("span", { className: "font-mono uppercase opacity-70", children: languageTag }), badge ? (_jsx("span", { className: "rounded bg-muted px-1 text-[10px] font-medium uppercase tracking-wide", children: badge })) : null] }), onRemove ? (_jsx("button", { type: "button", onClick: onRemove, "aria-label": removeLabel, className: "text-muted-foreground hover:text-destructive", children: _jsx(X, { className: "size-3" }) })) : null] }));
178
+ }
179
+ /**
180
+ * A field bound to the sheet's active language. When that's the default
181
+ * language (and the field has a base column), it edits the base value;
182
+ * otherwise it edits the active language's translation draft. The globe is an
183
+ * informational indicator (green when the field has any non-default translation).
184
+ */
185
+ export function TranslatableField({ label, type, field, activeLanguage, defaultLanguageTag, base, translations, messages, placeholder, autoFocus, error, }) {
186
+ const usesBase = !!base && activeLanguage === defaultLanguageTag;
187
+ const activeDraft = translations.drafts.find((draft) => draft.languageTag === activeLanguage);
188
+ const defaultDraft = translations.drafts.find((draft) => draft.languageTag === defaultLanguageTag);
189
+ // When editing the default language, show the base value — but fall back to
190
+ // the default-language translation (legacy products keep content only there).
191
+ // Editing writes to the base columns, promoting that content forward.
192
+ const value = usesBase
193
+ ? (base?.value ?? "") || (defaultDraft?.[field] ?? "")
194
+ : (activeDraft?.[field] ?? "");
195
+ const handleChange = usesBase
196
+ ? (base?.onChange ?? (() => { }))
197
+ : (next) => translations.setFieldValue(activeLanguage, field, next);
198
+ const translatedLanguages = translations.drafts
199
+ .filter((draft) => draft.languageTag !== defaultLanguageTag && fieldHasContent(draft, field))
200
+ .map((draft) => draft.languageTag);
201
+ return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx(Label, { children: label }), _jsx(TranslationIndicator, { languages: translatedLanguages, messages: messages })] }), type === "richtext" ? (_jsx(RichTextEditor, { value: value, onChange: handleChange, placeholder: placeholder, editorClassName: "max-h-[280px] overflow-y-auto" })) : (_jsx(Input, { value: value, onChange: (event) => handleChange(event.target.value), placeholder: placeholder, autoFocus: autoFocus })), error ? _jsx("p", { className: "text-xs text-destructive", children: error }) : null] }));
202
+ }
203
+ export function TranslationIndicator({ languages: translatedLanguages, messages, }) {
204
+ const isTranslated = translatedLanguages.length > 0;
205
+ return (_jsx(TooltipProvider, { delay: 150, children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { render: _jsx("button", { type: "button", className: "inline-flex cursor-help items-center", children: _jsx(Globe, { className: cn("size-3.5", isTranslated ? "text-emerald-500" : "text-muted-foreground/50") }) }) }), _jsx(TooltipContent, { children: isTranslated
206
+ ? `${messages.fieldTranslated}: ${translatedLanguages
207
+ .map((tag) => tag.toUpperCase())
208
+ .join(", ")}`
209
+ : messages.fieldNotTranslated })] }) }));
210
+ }
211
+ export function LanguageCombobox({ value, onValueChange, exclude = [], placeholder, emptyLabel, }) {
212
+ const excludeKey = exclude.join("|");
213
+ const options = useMemo(() => Object.entries(languages)
214
+ .filter(([code]) => !excludeKey.split("|").includes(code))
215
+ .map(([code, name]) => ({ value: code, label: name })), [excludeKey]);
216
+ return (_jsxs(Combobox, { value: value, onValueChange: (next) => onValueChange(next ?? ""), children: [_jsx(ComboboxInput, { placeholder: placeholder, className: "w-full" }), _jsx(ComboboxContent, { children: _jsxs(ComboboxList, { children: [options.map((option) => (_jsxs(ComboboxItem, { value: option.value, children: [_jsx("span", { className: "truncate", children: option.label }), _jsx("span", { className: "font-mono text-xs text-muted-foreground", children: option.value })] }, option.value))), _jsx(ComboboxEmpty, { children: emptyLabel })] }) })] }));
217
+ }
@@ -0,0 +1,12 @@
1
+ import { type OptionUnitData } from "./product-unit-form.js";
2
+ export type { OptionUnitData };
3
+ type UnitDialogProps = {
4
+ open: boolean;
5
+ onOpenChange: (open: boolean) => void;
6
+ optionId: string;
7
+ unit?: OptionUnitData;
8
+ nextSortOrder?: number;
9
+ onSuccess: () => void;
10
+ };
11
+ export declare function UnitDialog({ open, onOpenChange, optionId, unit, nextSortOrder, onSuccess, }: UnitDialogProps): import("react/jsx-runtime").JSX.Element;
12
+ //# sourceMappingURL=product-unit-dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-unit-dialog.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/product-unit-dialog.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,cAAc,EAAY,MAAM,wBAAwB,CAAA;AAEtE,YAAY,EAAE,cAAc,EAAE,CAAA;AAE9B,KAAK,eAAe,GAAG;IACrB,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,CAAC,EAAE,cAAc,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,SAAS,EAAE,MAAM,IAAI,CAAA;CACtB,CAAA;AAED,wBAAgB,UAAU,CAAC,EACzB,IAAI,EACJ,YAAY,EACZ,QAAQ,EACR,IAAI,EACJ,aAAa,EACb,SAAS,GACV,EAAE,eAAe,2CAuBjB"}
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTitle } from "@voyantjs/ui/components";
3
+ import { useProductDetailMessages } from "./host.js";
4
+ import { UnitForm } from "./product-unit-form.js";
5
+ export function UnitDialog({ open, onOpenChange, optionId, unit, nextSortOrder, onSuccess, }) {
6
+ const messages = useProductDetailMessages();
7
+ const unitMessages = messages.products.operations.units;
8
+ const isEditing = !!unit;
9
+ return (_jsx(Sheet, { open: open, onOpenChange: onOpenChange, children: _jsxs(SheetContent, { side: "right", size: "lg", children: [_jsx(SheetHeader, { children: _jsx(SheetTitle, { children: isEditing ? unitMessages.editTitle : unitMessages.newTitle }) }), _jsx(SheetBody, { children: _jsx(UnitForm, { optionId: optionId, unit: unit, nextSortOrder: nextSortOrder, onSuccess: onSuccess, onCancel: () => onOpenChange(false) }) })] }) }));
10
+ }
@@ -0,0 +1,26 @@
1
+ export type OptionUnitData = {
2
+ id: string;
3
+ optionId: string;
4
+ name: string;
5
+ code: string | null;
6
+ description: string | null;
7
+ unitType: "person" | "group" | "room" | "vehicle" | "service" | "other";
8
+ minQuantity: number | null;
9
+ maxQuantity: number | null;
10
+ minAge: number | null;
11
+ maxAge: number | null;
12
+ occupancyMin: number | null;
13
+ occupancyMax: number | null;
14
+ isRequired: boolean;
15
+ isHidden: boolean;
16
+ sortOrder: number;
17
+ };
18
+ export interface UnitFormProps {
19
+ optionId: string;
20
+ unit?: OptionUnitData;
21
+ nextSortOrder?: number;
22
+ onSuccess: () => void;
23
+ onCancel?: () => void;
24
+ }
25
+ export declare function UnitForm({ optionId, unit, nextSortOrder, onSuccess, onCancel }: UnitFormProps): import("react/jsx-runtime").JSX.Element;
26
+ //# sourceMappingURL=product-unit-form.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-unit-form.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/product-unit-form.tsx"],"names":[],"mappings":"AA2CA,MAAM,MAAM,cAAc,GAAG;IAC3B,EAAE,EAAE,MAAM,CAAA;IACV,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,QAAQ,EAAE,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAA;IACvE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,UAAU,EAAE,OAAO,CAAA;IACnB,QAAQ,EAAE,OAAO,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,CAAC,EAAE,cAAc,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AAqCD,wBAAgB,QAAQ,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,aAAa,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,aAAa,2CAuL7F"}
@@ -0,0 +1,109 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useOptionUnitMutation } from "@voyantjs/products-react";
3
+ import { Button, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, Textarea, } from "@voyantjs/ui/components";
4
+ import { Loader2 } from "lucide-react";
5
+ import { useEffect } from "react";
6
+ import { useForm } from "react-hook-form";
7
+ import { z } from "zod/v4";
8
+ import { useProductDetailMessages } from "./host.js";
9
+ import { zodResolver } from "./zod-resolver.js";
10
+ const buildUnitFormSchema = (messages) => z.object({
11
+ name: z.string().min(1, messages.validationNameRequired).max(255),
12
+ code: z.string().max(100).optional().nullable(),
13
+ description: z.string().optional().nullable(),
14
+ unitType: z.enum(["person", "group", "room", "vehicle", "service", "other"]),
15
+ minQuantity: z.coerce.number().int().min(0).optional().or(z.literal("")).nullable(),
16
+ maxQuantity: z.coerce.number().int().min(0).optional().or(z.literal("")).nullable(),
17
+ minAge: z.coerce.number().int().min(0).optional().or(z.literal("")).nullable(),
18
+ maxAge: z.coerce.number().int().min(0).optional().or(z.literal("")).nullable(),
19
+ occupancyMin: z.coerce.number().int().min(0).optional().or(z.literal("")).nullable(),
20
+ occupancyMax: z.coerce.number().int().min(0).optional().or(z.literal("")).nullable(),
21
+ isRequired: z.boolean(),
22
+ isHidden: z.boolean(),
23
+ sortOrder: z.coerce.number().int(),
24
+ });
25
+ function initialValues(unit, nextSortOrder) {
26
+ if (unit) {
27
+ return {
28
+ name: unit.name,
29
+ code: unit.code ?? "",
30
+ description: unit.description ?? "",
31
+ unitType: unit.unitType,
32
+ minQuantity: unit.minQuantity ?? "",
33
+ maxQuantity: unit.maxQuantity ?? "",
34
+ minAge: unit.minAge ?? "",
35
+ maxAge: unit.maxAge ?? "",
36
+ occupancyMin: unit.occupancyMin ?? "",
37
+ occupancyMax: unit.occupancyMax ?? "",
38
+ isRequired: unit.isRequired,
39
+ isHidden: unit.isHidden,
40
+ sortOrder: unit.sortOrder,
41
+ };
42
+ }
43
+ return {
44
+ name: "",
45
+ code: "",
46
+ description: "",
47
+ unitType: "person",
48
+ minQuantity: "",
49
+ maxQuantity: "",
50
+ minAge: "",
51
+ maxAge: "",
52
+ occupancyMin: "",
53
+ occupancyMax: "",
54
+ isRequired: false,
55
+ isHidden: false,
56
+ sortOrder: nextSortOrder ?? 0,
57
+ };
58
+ }
59
+ export function UnitForm({ optionId, unit, nextSortOrder, onSuccess, onCancel }) {
60
+ const messages = useProductDetailMessages();
61
+ const productMessages = messages.products.core;
62
+ const unitMessages = messages.products.operations.units;
63
+ const isEditing = !!unit;
64
+ const { create, update } = useOptionUnitMutation();
65
+ const unitFormSchema = buildUnitFormSchema(unitMessages);
66
+ const unitTypes = [
67
+ { value: "person", label: unitMessages.typePerson },
68
+ { value: "group", label: unitMessages.typeGroup },
69
+ { value: "room", label: unitMessages.typeRoom },
70
+ { value: "vehicle", label: unitMessages.typeVehicle },
71
+ { value: "service", label: unitMessages.typeService },
72
+ { value: "other", label: unitMessages.typeOther },
73
+ ];
74
+ const form = useForm({
75
+ resolver: zodResolver(unitFormSchema),
76
+ defaultValues: initialValues(unit, nextSortOrder),
77
+ });
78
+ useEffect(() => {
79
+ form.reset(initialValues(unit, nextSortOrder));
80
+ }, [unit, nextSortOrder, form]);
81
+ const onSubmit = async (values) => {
82
+ const canHaveAge = values.unitType === "person";
83
+ const canHaveOccupancy = values.unitType === "group" || values.unitType === "room" || values.unitType === "vehicle";
84
+ const payload = {
85
+ name: values.name,
86
+ code: values.code || null,
87
+ description: values.description || null,
88
+ unitType: values.unitType,
89
+ minQuantity: typeof values.minQuantity === "number" ? values.minQuantity : null,
90
+ maxQuantity: typeof values.maxQuantity === "number" ? values.maxQuantity : null,
91
+ minAge: canHaveAge && typeof values.minAge === "number" ? values.minAge : null,
92
+ maxAge: canHaveAge && typeof values.maxAge === "number" ? values.maxAge : null,
93
+ occupancyMin: canHaveOccupancy && typeof values.occupancyMin === "number" ? values.occupancyMin : null,
94
+ occupancyMax: canHaveOccupancy && typeof values.occupancyMax === "number" ? values.occupancyMax : null,
95
+ isRequired: values.isRequired,
96
+ isHidden: values.isHidden,
97
+ sortOrder: values.sortOrder,
98
+ };
99
+ if (isEditing) {
100
+ await update.mutateAsync({ id: unit.id, input: payload });
101
+ }
102
+ else {
103
+ await create.mutateAsync({ optionId, ...payload });
104
+ }
105
+ onSuccess();
106
+ };
107
+ const unitType = form.watch("unitType");
108
+ 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: unitMessages.nameLabel }), _jsx(Input, { ...form.register("name"), placeholder: unitMessages.namePlaceholder }), form.formState.errors.name && (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.name.message }))] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: unitMessages.codeLabel }), _jsx(Input, { ...form.register("code"), placeholder: unitMessages.codePlaceholder })] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: unitMessages.typeLabel }), _jsxs(Select, { value: unitType, onValueChange: (v) => form.setValue("unitType", v), items: unitTypes, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: unitTypes.map((t) => (_jsx(SelectItem, { value: t.value, children: t.label }, t.value))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: unitMessages.sortOrderLabel }), _jsx(Input, { ...form.register("sortOrder"), type: "number" })] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: unitMessages.minQuantityLabel }), _jsx(Input, { ...form.register("minQuantity"), type: "number", min: "0" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: unitMessages.maxQuantityLabel }), _jsx(Input, { ...form.register("maxQuantity"), type: "number", min: "0" })] })] }), unitType === "person" && (_jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: unitMessages.minAgeLabel }), _jsx(Input, { ...form.register("minAge"), type: "number", min: "0" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: unitMessages.maxAgeLabel }), _jsx(Input, { ...form.register("maxAge"), type: "number", min: "0" })] })] })), (unitType === "room" || unitType === "vehicle" || unitType === "group") && (_jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: unitMessages.occupancyMinLabel }), _jsx(Input, { ...form.register("occupancyMin"), type: "number", min: "0" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: unitMessages.occupancyMaxLabel }), _jsx(Input, { ...form.register("occupancyMax"), type: "number", min: "0" })] })] })), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: unitMessages.descriptionLabel }), _jsx(Textarea, { ...form.register("description") })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { checked: form.watch("isRequired"), onCheckedChange: (v) => form.setValue("isRequired", v) }), _jsx(Label, { children: unitMessages.requiredLabel })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { checked: form.watch("isHidden"), onCheckedChange: (v) => form.setValue("isHidden", v) }), _jsx(Label, { children: unitMessages.hiddenLabel })] })] })] }), _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 || create.isPending || update.isPending, children: [(form.formState.isSubmitting || create.isPending || update.isPending) && (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })), isEditing ? productMessages.saveChanges : unitMessages.create] })] })] }));
109
+ }
@@ -0,0 +1,16 @@
1
+ import type { OptionUnitData } from "./product-unit-form.js";
2
+ import { type OptionUnitPriceRuleData } from "./product-unit-price-rule-form.js";
3
+ export type { OptionUnitPriceRuleData };
4
+ type UnitPriceRuleDialogProps = {
5
+ open: boolean;
6
+ onOpenChange: (open: boolean) => void;
7
+ optionPriceRuleId: string;
8
+ optionId: string;
9
+ units: OptionUnitData[];
10
+ preselectedUnitId?: string;
11
+ preselectedCategoryId?: string | null;
12
+ cell?: OptionUnitPriceRuleData;
13
+ onSuccess: () => void;
14
+ };
15
+ export declare function UnitPriceRuleDialog({ open, onOpenChange, optionPriceRuleId, optionId, units, preselectedUnitId, preselectedCategoryId, cell, onSuccess, }: UnitPriceRuleDialogProps): import("react/jsx-runtime").JSX.Element;
16
+ //# sourceMappingURL=product-unit-price-rule-dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-unit-price-rule-dialog.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/product-unit-price-rule-dialog.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAC5D,OAAO,EAAE,KAAK,uBAAuB,EAAqB,MAAM,mCAAmC,CAAA;AAEnG,YAAY,EAAE,uBAAuB,EAAE,CAAA;AAEvC,KAAK,wBAAwB,GAAG;IAC9B,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,iBAAiB,EAAE,MAAM,CAAA;IACzB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,cAAc,EAAE,CAAA;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrC,IAAI,CAAC,EAAE,uBAAuB,CAAA;IAC9B,SAAS,EAAE,MAAM,IAAI,CAAA;CACtB,CAAA;AAED,wBAAgB,mBAAmB,CAAC,EAClC,IAAI,EACJ,YAAY,EACZ,iBAAiB,EACjB,QAAQ,EACR,KAAK,EACL,iBAAiB,EACjB,qBAAqB,EACrB,IAAI,EACJ,SAAS,GACV,EAAE,wBAAwB,2CA4B1B"}
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTitle } from "@voyantjs/ui/components";
3
+ import { useProductDetailMessages } from "./host.js";
4
+ import { UnitPriceRuleForm } from "./product-unit-price-rule-form.js";
5
+ export function UnitPriceRuleDialog({ open, onOpenChange, optionPriceRuleId, optionId, units, preselectedUnitId, preselectedCategoryId, cell, onSuccess, }) {
6
+ const messages = useProductDetailMessages();
7
+ const unitPriceMessages = messages.products.operations.unitPrices;
8
+ const isEditing = !!cell;
9
+ return (_jsx(Sheet, { open: open, onOpenChange: onOpenChange, children: _jsxs(SheetContent, { side: "right", children: [_jsx(SheetHeader, { children: _jsx(SheetTitle, { children: isEditing ? unitPriceMessages.editTitle : unitPriceMessages.newTitle }) }), _jsx(SheetBody, { children: _jsx(UnitPriceRuleForm, { optionPriceRuleId: optionPriceRuleId, optionId: optionId, units: units, preselectedUnitId: preselectedUnitId, preselectedCategoryId: preselectedCategoryId, cell: cell, onSuccess: onSuccess, onCancel: () => onOpenChange(false) }) })] }) }));
10
+ }
@@ -0,0 +1,28 @@
1
+ import type { OptionUnitData } from "./product-unit-form.js";
2
+ export type OptionUnitPriceRuleData = {
3
+ id: string;
4
+ optionPriceRuleId: string;
5
+ optionId: string;
6
+ unitId: string;
7
+ pricingCategoryId: string | null;
8
+ pricingMode: "per_unit" | "per_person" | "per_booking" | "included" | "free" | "on_request";
9
+ sellAmountCents: number | null;
10
+ costAmountCents: number | null;
11
+ minQuantity: number | null;
12
+ maxQuantity: number | null;
13
+ sortOrder: number;
14
+ active: boolean;
15
+ notes: string | null;
16
+ };
17
+ export interface UnitPriceRuleFormProps {
18
+ optionPriceRuleId: string;
19
+ optionId: string;
20
+ units: OptionUnitData[];
21
+ preselectedUnitId?: string;
22
+ preselectedCategoryId?: string | null;
23
+ cell?: OptionUnitPriceRuleData;
24
+ onSuccess: () => void;
25
+ onCancel?: () => void;
26
+ }
27
+ export declare function UnitPriceRuleForm({ optionPriceRuleId, optionId, units, preselectedUnitId, preselectedCategoryId, cell, onSuccess, onCancel, }: UnitPriceRuleFormProps): import("react/jsx-runtime").JSX.Element;
28
+ //# sourceMappingURL=product-unit-price-rule-form.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-unit-price-rule-form.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/product-unit-price-rule-form.tsx"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAsD5D,MAAM,MAAM,uBAAuB,GAAG;IACpC,EAAE,EAAE,MAAM,CAAA;IACV,iBAAiB,EAAE,MAAM,CAAA;IACzB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,WAAW,EAAE,UAAU,GAAG,YAAY,GAAG,aAAa,GAAG,UAAU,GAAG,MAAM,GAAG,YAAY,CAAA;IAC3F,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,OAAO,CAAA;IACf,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CACrB,CAAA;AAED,MAAM,WAAW,sBAAsB;IACrC,iBAAiB,EAAE,MAAM,CAAA;IACzB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,cAAc,EAAE,CAAA;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrC,IAAI,CAAC,EAAE,uBAAuB,CAAA;IAC9B,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AAmCD,wBAAgB,iBAAiB,CAAC,EAChC,iBAAiB,EACjB,QAAQ,EACR,KAAK,EACL,iBAAiB,EACjB,qBAAqB,EACrB,IAAI,EACJ,SAAS,EACT,QAAQ,GACT,EAAE,sBAAsB,2CAyKxB"}
@@ -0,0 +1,126 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useOptionUnitPriceRuleMutation } from "@voyantjs/pricing-react";
3
+ import { PricingCategoryCombobox } from "@voyantjs/pricing-ui/components/pricing-category-combobox";
4
+ import { Button, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, Textarea, } from "@voyantjs/ui/components";
5
+ import { Loader2 } from "lucide-react";
6
+ import { useEffect } from "react";
7
+ import { useForm } from "react-hook-form";
8
+ import { z } from "zod/v4";
9
+ import { useProductDetailMessages } from "./host.js";
10
+ import { zodResolver } from "./zod-resolver.js";
11
+ function getUnitTypeLabel(type, messages) {
12
+ switch (type) {
13
+ case "person":
14
+ return messages.typePerson;
15
+ case "group":
16
+ return messages.typeGroup;
17
+ case "room":
18
+ return messages.typeRoom;
19
+ case "vehicle":
20
+ return messages.typeVehicle;
21
+ case "service":
22
+ return messages.typeService;
23
+ case "other":
24
+ return messages.typeOther;
25
+ default:
26
+ return type;
27
+ }
28
+ }
29
+ const buildCellFormSchema = (messages) => z.object({
30
+ unitId: z.string().min(1, messages.validationUnitRequired),
31
+ pricingCategoryId: z.string().optional().nullable(),
32
+ pricingMode: z.enum([
33
+ "per_unit",
34
+ "per_person",
35
+ "per_booking",
36
+ "included",
37
+ "free",
38
+ "on_request",
39
+ ]),
40
+ sell: z.coerce.number().min(0),
41
+ cost: z.coerce.number().min(0),
42
+ minQuantity: z.coerce.number().int().min(0).optional().or(z.literal("")).nullable(),
43
+ maxQuantity: z.coerce.number().int().min(0).optional().or(z.literal("")).nullable(),
44
+ sortOrder: z.coerce.number().int(),
45
+ active: z.boolean(),
46
+ notes: z.string().optional().nullable(),
47
+ });
48
+ function initialValues(cell, preselectedUnitId, preselectedCategoryId) {
49
+ if (cell) {
50
+ return {
51
+ unitId: cell.unitId,
52
+ pricingCategoryId: cell.pricingCategoryId ?? "",
53
+ pricingMode: cell.pricingMode,
54
+ sell: (cell.sellAmountCents ?? 0) / 100,
55
+ cost: (cell.costAmountCents ?? 0) / 100,
56
+ minQuantity: cell.minQuantity ?? "",
57
+ maxQuantity: cell.maxQuantity ?? "",
58
+ sortOrder: cell.sortOrder,
59
+ active: cell.active,
60
+ notes: cell.notes ?? "",
61
+ };
62
+ }
63
+ return {
64
+ unitId: preselectedUnitId ?? "",
65
+ pricingCategoryId: preselectedCategoryId ?? "",
66
+ pricingMode: "per_person",
67
+ sell: 0,
68
+ cost: 0,
69
+ minQuantity: "",
70
+ maxQuantity: "",
71
+ sortOrder: 0,
72
+ active: true,
73
+ notes: "",
74
+ };
75
+ }
76
+ export function UnitPriceRuleForm({ optionPriceRuleId, optionId, units, preselectedUnitId, preselectedCategoryId, cell, onSuccess, onCancel, }) {
77
+ const messages = useProductDetailMessages();
78
+ const productMessages = messages.products.core;
79
+ const unitPriceMessages = messages.products.operations.unitPrices;
80
+ const unitMessages = messages.products.operations.units;
81
+ const isEditing = !!cell;
82
+ const { create, update } = useOptionUnitPriceRuleMutation();
83
+ const cellFormSchema = buildCellFormSchema(unitPriceMessages);
84
+ const pricingModes = [
85
+ { value: "per_unit", label: unitPriceMessages.pricingModePerUnit },
86
+ { value: "per_person", label: unitPriceMessages.pricingModePerPerson },
87
+ { value: "per_booking", label: unitPriceMessages.pricingModePerBooking },
88
+ { value: "included", label: unitPriceMessages.pricingModeIncluded },
89
+ { value: "free", label: unitPriceMessages.pricingModeFree },
90
+ { value: "on_request", label: unitPriceMessages.pricingModeOnRequest },
91
+ ];
92
+ const form = useForm({
93
+ resolver: zodResolver(cellFormSchema),
94
+ defaultValues: initialValues(cell, preselectedUnitId, preselectedCategoryId),
95
+ });
96
+ useEffect(() => {
97
+ form.reset(initialValues(cell, preselectedUnitId, preselectedCategoryId));
98
+ }, [cell, preselectedUnitId, preselectedCategoryId, form]);
99
+ const onSubmit = async (values) => {
100
+ const payload = {
101
+ optionPriceRuleId,
102
+ optionId,
103
+ unitId: values.unitId,
104
+ pricingCategoryId: values.pricingCategoryId || null,
105
+ pricingMode: values.pricingMode,
106
+ sellAmountCents: Math.round(values.sell * 100),
107
+ costAmountCents: Math.round(values.cost * 100),
108
+ minQuantity: typeof values.minQuantity === "number" ? values.minQuantity : null,
109
+ maxQuantity: typeof values.maxQuantity === "number" ? values.maxQuantity : null,
110
+ sortOrder: values.sortOrder,
111
+ active: values.active,
112
+ notes: values.notes || null,
113
+ };
114
+ if (isEditing) {
115
+ await update.mutateAsync({ id: cell.id, input: payload });
116
+ }
117
+ else {
118
+ await create.mutateAsync(payload);
119
+ }
120
+ onSuccess();
121
+ };
122
+ 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: unitPriceMessages.unitLabel }), _jsxs(Select, { value: form.watch("unitId") || undefined, onValueChange: (v) => form.setValue("unitId", v ?? "", { shouldValidate: true }), items: units.map((u) => ({
123
+ value: u.id,
124
+ label: `${u.name} (${getUnitTypeLabel(u.unitType, unitMessages)})`,
125
+ })), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, { placeholder: unitPriceMessages.unitPlaceholder }) }), _jsx(SelectContent, { children: units.map((u) => (_jsxs(SelectItem, { value: u.id, children: [u.name, " (", getUnitTypeLabel(u.unitType, unitMessages), ")"] }, u.id))) })] }), form.formState.errors.unitId && (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.unitId.message }))] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: unitPriceMessages.categoryLabel }), _jsx(PricingCategoryCombobox, { value: form.watch("pricingCategoryId"), onChange: (value) => form.setValue("pricingCategoryId", value ?? "", { shouldDirty: true }), placeholder: unitPriceMessages.categoryPlaceholder })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: unitPriceMessages.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: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: unitPriceMessages.sellLabel }), _jsx(Input, { ...form.register("sell"), type: "number", step: "0.01", min: "0" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: unitPriceMessages.costLabel }), _jsx(Input, { ...form.register("cost"), 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: unitPriceMessages.minQuantityLabel }), _jsx(Input, { ...form.register("minQuantity"), type: "number", min: "0" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: unitPriceMessages.maxQuantityLabel }), _jsx(Input, { ...form.register("maxQuantity"), type: "number", min: "0" })] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: unitPriceMessages.sortOrderLabel }), _jsx(Input, { ...form.register("sortOrder"), type: "number" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { checked: form.watch("active"), onCheckedChange: (v) => form.setValue("active", v) }), _jsx(Label, { children: unitPriceMessages.activeLabel })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: unitPriceMessages.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 : unitPriceMessages.create] })] })] }));
126
+ }
@@ -0,0 +1,9 @@
1
+ export type TimezoneOption = {
2
+ id: string;
3
+ label: string;
4
+ offset: number;
5
+ };
6
+ export declare const TIMEZONE_OPTIONS: TimezoneOption[];
7
+ export declare const TIMEZONE_IDS: string[];
8
+ export declare function getTimezoneLabel(id: string): string;
9
+ //# sourceMappingURL=timezone-options.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timezone-options.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/timezone-options.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,cAAc,GAAG;IAC3B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf,CAAA;AAsBD,eAAO,MAAM,gBAAgB,kBAAyB,CAAA;AACtD,eAAO,MAAM,YAAY,UAAoC,CAAA;AAI7D,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAGnD"}