@voyantjs/products-ui 0.101.1 → 0.102.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/dist/components/product-detail/date-picker.d.ts +44 -0
  2. package/dist/components/product-detail/date-picker.d.ts.map +1 -0
  3. package/dist/components/product-detail/date-picker.js +125 -0
  4. package/dist/components/product-detail/host.d.ts +53 -0
  5. package/dist/components/product-detail/host.d.ts.map +1 -0
  6. package/dist/components/product-detail/host.js +24 -0
  7. package/dist/components/product-detail/index.d.ts +6 -0
  8. package/dist/components/product-detail/index.d.ts.map +1 -0
  9. package/dist/components/product-detail/index.js +5 -0
  10. package/dist/components/product-detail/product-activity-section.d.ts +4 -0
  11. package/dist/components/product-detail/product-activity-section.d.ts.map +1 -0
  12. package/dist/components/product-detail/product-activity-section.js +37 -0
  13. package/dist/components/product-detail/product-day-sheet.d.ts +14 -0
  14. package/dist/components/product-detail/product-day-sheet.d.ts.map +1 -0
  15. package/dist/components/product-detail/product-day-sheet.js +75 -0
  16. package/dist/components/product-detail/product-day-translation.d.ts +41 -0
  17. package/dist/components/product-detail/product-day-translation.d.ts.map +1 -0
  18. package/dist/components/product-detail/product-day-translation.js +111 -0
  19. package/dist/components/product-detail/product-departure-dialog.d.ts +11 -0
  20. package/dist/components/product-detail/product-departure-dialog.d.ts.map +1 -0
  21. package/dist/components/product-detail/product-departure-dialog.js +10 -0
  22. package/dist/components/product-detail/product-departure-form.d.ts +25 -0
  23. package/dist/components/product-detail/product-departure-form.d.ts.map +1 -0
  24. package/dist/components/product-detail/product-departure-form.js +237 -0
  25. package/dist/components/product-detail/product-departure-pricing-override-dialog.d.ts +8 -0
  26. package/dist/components/product-detail/product-departure-pricing-override-dialog.d.ts.map +1 -0
  27. package/dist/components/product-detail/product-departure-pricing-override-dialog.js +125 -0
  28. package/dist/components/product-detail/product-detail-day-row.d.ts +14 -0
  29. package/dist/components/product-detail/product-detail-day-row.d.ts.map +1 -0
  30. package/dist/components/product-detail/product-detail-day-row.js +43 -0
  31. package/dist/components/product-detail/product-detail-dialog.d.ts +10 -0
  32. package/dist/components/product-detail/product-detail-dialog.d.ts.map +1 -0
  33. package/dist/components/product-detail/product-detail-dialog.js +10 -0
  34. package/dist/components/product-detail/product-detail-form.d.ts +19 -0
  35. package/dist/components/product-detail/product-detail-form.d.ts.map +1 -0
  36. package/dist/components/product-detail/product-detail-form.js +180 -0
  37. package/dist/components/product-detail/product-detail-header.d.ts +12 -0
  38. package/dist/components/product-detail/product-detail-header.d.ts.map +1 -0
  39. package/dist/components/product-detail/product-detail-header.js +19 -0
  40. package/dist/components/product-detail/product-detail-itinerary-section.d.ts +4 -0
  41. package/dist/components/product-detail/product-detail-itinerary-section.d.ts.map +1 -0
  42. package/dist/components/product-detail/product-detail-itinerary-section.js +201 -0
  43. package/dist/components/product-detail/product-detail-page.d.ts +4 -0
  44. package/dist/components/product-detail/product-detail-page.d.ts.map +1 -0
  45. package/dist/components/product-detail/product-detail-page.js +97 -0
  46. package/dist/components/product-detail/product-detail-sections.d.ts +63 -0
  47. package/dist/components/product-detail/product-detail-sections.d.ts.map +1 -0
  48. package/dist/components/product-detail/product-detail-sections.js +143 -0
  49. package/dist/components/product-detail/product-detail-shared.d.ts +264 -0
  50. package/dist/components/product-detail/product-detail-shared.d.ts.map +1 -0
  51. package/dist/components/product-detail/product-detail-shared.js +157 -0
  52. package/dist/components/product-detail/product-detail-skeleton.d.ts +9 -0
  53. package/dist/components/product-detail/product-detail-skeleton.d.ts.map +1 -0
  54. package/dist/components/product-detail/product-detail-skeleton.js +53 -0
  55. package/dist/components/product-detail/product-extras-section.d.ts +4 -0
  56. package/dist/components/product-detail/product-extras-section.d.ts.map +1 -0
  57. package/dist/components/product-detail/product-extras-section.js +141 -0
  58. package/dist/components/product-detail/product-itinerary-form.d.ts +16 -0
  59. package/dist/components/product-detail/product-itinerary-form.d.ts.map +1 -0
  60. package/dist/components/product-detail/product-itinerary-form.js +38 -0
  61. package/dist/components/product-detail/product-market-rules-section.d.ts +6 -0
  62. package/dist/components/product-detail/product-market-rules-section.d.ts.map +1 -0
  63. package/dist/components/product-detail/product-market-rules-section.js +81 -0
  64. package/dist/components/product-detail/product-media-gallery.d.ts +19 -0
  65. package/dist/components/product-detail/product-media-gallery.d.ts.map +1 -0
  66. package/dist/components/product-detail/product-media-gallery.js +114 -0
  67. package/dist/components/product-detail/product-option-price-rule-dialog.d.ts +12 -0
  68. package/dist/components/product-detail/product-option-price-rule-dialog.d.ts.map +1 -0
  69. package/dist/components/product-detail/product-option-price-rule-dialog.js +10 -0
  70. package/dist/components/product-detail/product-option-price-rule-form.d.ts +29 -0
  71. package/dist/components/product-detail/product-option-price-rule-form.d.ts.map +1 -0
  72. package/dist/components/product-detail/product-option-price-rule-form.js +125 -0
  73. package/dist/components/product-detail/product-option-pricing-grid.d.ts +16 -0
  74. package/dist/components/product-detail/product-option-pricing-grid.d.ts.map +1 -0
  75. package/dist/components/product-detail/product-option-pricing-grid.js +193 -0
  76. package/dist/components/product-detail/product-options-pricing.d.ts +34 -0
  77. package/dist/components/product-detail/product-options-pricing.d.ts.map +1 -0
  78. package/dist/components/product-detail/product-options-pricing.js +385 -0
  79. package/dist/components/product-detail/product-options-shared.d.ts +623 -0
  80. package/dist/components/product-detail/product-options-shared.d.ts.map +1 -0
  81. package/dist/components/product-detail/product-options-shared.js +54 -0
  82. package/dist/components/product-detail/product-payment-policy-section.d.ts +17 -0
  83. package/dist/components/product-detail/product-payment-policy-section.d.ts.map +1 -0
  84. package/dist/components/product-detail/product-payment-policy-section.js +58 -0
  85. package/dist/components/product-detail/product-schedule-dialog.d.ts +11 -0
  86. package/dist/components/product-detail/product-schedule-dialog.d.ts.map +1 -0
  87. package/dist/components/product-detail/product-schedule-dialog.js +10 -0
  88. package/dist/components/product-detail/product-schedule-form.d.ts +17 -0
  89. package/dist/components/product-detail/product-schedule-form.d.ts.map +1 -0
  90. package/dist/components/product-detail/product-schedule-form.js +222 -0
  91. package/dist/components/product-detail/product-service-dialog.d.ts +12 -0
  92. package/dist/components/product-detail/product-service-dialog.d.ts.map +1 -0
  93. package/dist/components/product-detail/product-service-dialog.js +10 -0
  94. package/dist/components/product-detail/product-service-form.d.ts +22 -0
  95. package/dist/components/product-detail/product-service-form.d.ts.map +1 -0
  96. package/dist/components/product-detail/product-service-form.js +154 -0
  97. package/dist/components/product-detail/product-translation-popover.d.ts +91 -0
  98. package/dist/components/product-detail/product-translation-popover.d.ts.map +1 -0
  99. package/dist/components/product-detail/product-translation-popover.js +217 -0
  100. package/dist/components/product-detail/product-unit-dialog.d.ts +14 -0
  101. package/dist/components/product-detail/product-unit-dialog.d.ts.map +1 -0
  102. package/dist/components/product-detail/product-unit-dialog.js +10 -0
  103. package/dist/components/product-detail/product-unit-form.d.ts +34 -0
  104. package/dist/components/product-detail/product-unit-form.d.ts.map +1 -0
  105. package/dist/components/product-detail/product-unit-form.js +139 -0
  106. package/dist/components/product-detail/product-unit-price-rule-dialog.d.ts +17 -0
  107. package/dist/components/product-detail/product-unit-price-rule-dialog.d.ts.map +1 -0
  108. package/dist/components/product-detail/product-unit-price-rule-dialog.js +10 -0
  109. package/dist/components/product-detail/product-unit-price-rule-form.d.ts +29 -0
  110. package/dist/components/product-detail/product-unit-price-rule-form.d.ts.map +1 -0
  111. package/dist/components/product-detail/product-unit-price-rule-form.js +145 -0
  112. package/dist/components/product-detail/timezone-options.d.ts +9 -0
  113. package/dist/components/product-detail/timezone-options.d.ts.map +1 -0
  114. package/dist/components/product-detail/timezone-options.js +28 -0
  115. package/dist/components/product-detail/use-product-detail-data.d.ts +41 -0
  116. package/dist/components/product-detail/use-product-detail-data.d.ts.map +1 -0
  117. package/dist/components/product-detail/use-product-detail-data.js +143 -0
  118. package/dist/components/product-detail/use-product-detail-dialogs.d.ts +24 -0
  119. package/dist/components/product-detail/use-product-detail-dialogs.d.ts.map +1 -0
  120. package/dist/components/product-detail/use-product-detail-dialogs.js +40 -0
  121. package/dist/components/product-detail/zod-resolver.d.ts +4 -0
  122. package/dist/components/product-detail/zod-resolver.d.ts.map +1 -0
  123. package/dist/components/product-detail/zod-resolver.js +39 -0
  124. package/dist/components/product-options-section.d.ts.map +1 -1
  125. package/dist/components/product-options-section.js +31 -20
  126. package/package.json +38 -19
@@ -0,0 +1,91 @@
1
+ import type { useProductDetailMessages } from "./host.js";
2
+ type ProductCoreMessages = ReturnType<typeof useProductDetailMessages>["products"]["core"];
3
+ export type TranslatableField = "name" | "description" | "slug";
4
+ export type TranslationDraft = {
5
+ id: string | null;
6
+ languageTag: string;
7
+ name: string;
8
+ description: string;
9
+ slug: string;
10
+ shortDescription: string | null;
11
+ inclusionsHtml: string | null;
12
+ exclusionsHtml: string | null;
13
+ termsHtml: string | null;
14
+ seoTitle: string | null;
15
+ seoDescription: string | null;
16
+ };
17
+ export declare function richTextHasContent(html: string): boolean;
18
+ export declare function languageLabel(tag: string): string;
19
+ export interface PersistTranslationsOptions {
20
+ defaultLanguageTag: string;
21
+ baseName: string;
22
+ baseDescription: string;
23
+ }
24
+ export interface ProductTranslationDrafts {
25
+ drafts: TranslationDraft[];
26
+ isLoading: boolean;
27
+ setFieldValue: (languageTag: string, field: TranslatableField, value: string) => void;
28
+ addLanguage: (languageTag: string) => void;
29
+ removeLanguage: (languageTag: string) => void;
30
+ persist: (productId: string, options: PersistTranslationsOptions) => Promise<void>;
31
+ }
32
+ /**
33
+ * Manages an in-memory draft of a product's translations so Name/Description/
34
+ * Slug can be edited in context from the edit sheet. Seeds from the saved
35
+ * translation records and persists create/update/delete on save.
36
+ *
37
+ * The base product columns hold the default language's Name/Description, so the
38
+ * default-language translation row (if any) just mirrors them and carries the
39
+ * slug (base has no slug column). Fields we don't edit here (short description,
40
+ * inclusions, SEO, …) are preserved untouched.
41
+ */
42
+ export declare function useProductTranslationDrafts(productId: string | null): ProductTranslationDrafts;
43
+ export interface ContentLanguageSwitcherProps {
44
+ activeLanguage: string;
45
+ defaultLanguageTag: string;
46
+ /** The language tags that currently have a translation draft (excluding the default). */
47
+ languageTags: string[];
48
+ messages: ProductCoreMessages;
49
+ onSelect: (languageTag: string) => void;
50
+ onAddLanguage: (languageTag: string) => void;
51
+ onRemoveLanguage: (languageTag: string) => void;
52
+ }
53
+ /** Top-of-sheet switcher: picks which language every translatable field edits. */
54
+ export declare function ContentLanguageSwitcher({ activeLanguage, defaultLanguageTag, languageTags, messages, onSelect, onAddLanguage, onRemoveLanguage, }: ContentLanguageSwitcherProps): import("react/jsx-runtime").JSX.Element;
55
+ export interface TranslatableFieldProps {
56
+ label: string;
57
+ type: "text" | "richtext";
58
+ field: TranslatableField;
59
+ activeLanguage: string;
60
+ defaultLanguageTag: string;
61
+ /** The base product value (used when the active language is the default). Omit for slug. */
62
+ base?: {
63
+ value: string;
64
+ onChange: (value: string) => void;
65
+ };
66
+ translations: ProductTranslationDrafts;
67
+ messages: ProductCoreMessages;
68
+ placeholder?: string;
69
+ autoFocus?: boolean;
70
+ error?: string;
71
+ }
72
+ /**
73
+ * A field bound to the sheet's active language. When that's the default
74
+ * language (and the field has a base column), it edits the base value;
75
+ * otherwise it edits the active language's translation draft. The globe is an
76
+ * informational indicator (green when the field has any non-default translation).
77
+ */
78
+ export declare function TranslatableField({ label, type, field, activeLanguage, defaultLanguageTag, base, translations, messages, placeholder, autoFocus, error, }: TranslatableFieldProps): import("react/jsx-runtime").JSX.Element;
79
+ export declare function TranslationIndicator({ languages: translatedLanguages, messages, }: {
80
+ languages: string[];
81
+ messages: ProductCoreMessages;
82
+ }): import("react/jsx-runtime").JSX.Element;
83
+ export declare function LanguageCombobox({ value, onValueChange, exclude, placeholder, emptyLabel, }: {
84
+ value: string;
85
+ onValueChange: (languageTag: string) => void;
86
+ exclude?: string[];
87
+ placeholder?: string;
88
+ emptyLabel?: string;
89
+ }): import("react/jsx-runtime").JSX.Element;
90
+ export {};
91
+ //# sourceMappingURL=product-translation-popover.d.ts.map
@@ -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,14 @@
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
+ defaultUnitType?: OptionUnitData["unitType"];
9
+ lockUnitType?: boolean;
10
+ nextSortOrder?: number;
11
+ onSuccess: () => void;
12
+ };
13
+ export declare function UnitDialog({ open, onOpenChange, optionId, unit, defaultUnitType, lockUnitType, nextSortOrder, onSuccess, }: UnitDialogProps): import("react/jsx-runtime").JSX.Element;
14
+ //# 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,eAAe,CAAC,EAAE,cAAc,CAAC,UAAU,CAAC,CAAA;IAC5C,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,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,eAAe,EACf,YAAY,EACZ,aAAa,EACb,SAAS,GACV,EAAE,eAAe,2CAyBjB"}
@@ -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, defaultUnitType, lockUnitType, 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, defaultUnitType: defaultUnitType, lockUnitType: lockUnitType, nextSortOrder: nextSortOrder, onSuccess: onSuccess, onCancel: () => onOpenChange(false) }) })] }) }));
10
+ }
@@ -0,0 +1,34 @@
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
+ /** Pre-selected unit type for the "add" path (e.g. Room vs Traveler type). */
22
+ defaultUnitType?: OptionUnitData["unitType"];
23
+ /**
24
+ * Hide the unit-type picker entirely. Used when the form is opened from a
25
+ * type-specific context (e.g. "Add room"), so the agent can't turn a room
26
+ * into a vehicle and create a nonsensical mix in the pricing grid.
27
+ */
28
+ lockUnitType?: boolean;
29
+ nextSortOrder?: number;
30
+ onSuccess: () => void;
31
+ onCancel?: () => void;
32
+ }
33
+ export declare function UnitForm({ optionId, unit, defaultUnitType, lockUnitType, nextSortOrder, onSuccess, onCancel, }: UnitFormProps): import("react/jsx-runtime").JSX.Element;
34
+ //# 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":"AA2EA,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,8EAA8E;IAC9E,eAAe,CAAC,EAAE,cAAc,CAAC,UAAU,CAAC,CAAA;IAC5C;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AAyCD,wBAAgB,QAAQ,CAAC,EACvB,QAAQ,EACR,IAAI,EACJ,eAAe,EACf,YAAY,EACZ,aAAa,EACb,SAAS,EACT,QAAQ,GACT,EAAE,aAAa,2CA2Lf"}
@@ -0,0 +1,139 @@
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
+ // "Min/Max quantity" is meaningless to an agent — phrase it in terms of the
11
+ // thing being counted (rooms / vehicles / travelers) for the selected type.
12
+ function quantityLabels(unitType, m) {
13
+ switch (unitType) {
14
+ case "room":
15
+ return { min: m.quantityRoomMin, max: m.quantityRoomMax };
16
+ case "vehicle":
17
+ return { min: m.quantityVehicleMin, max: m.quantityVehicleMax };
18
+ case "person":
19
+ return { min: m.quantityPersonMin, max: m.quantityPersonMax };
20
+ default:
21
+ return { min: m.minQuantityLabel, max: m.maxQuantityLabel };
22
+ }
23
+ }
24
+ // Occupancy = how many people fit in one unit (guests per room, seats per
25
+ // vehicle, group size). Label it for the selected type.
26
+ function occupancyLabels(unitType, m) {
27
+ switch (unitType) {
28
+ case "room":
29
+ return { min: m.occupancyRoomMin, max: m.occupancyRoomMax };
30
+ case "vehicle":
31
+ return { min: m.occupancyVehicleMin, max: m.occupancyVehicleMax };
32
+ case "group":
33
+ return { min: m.occupancyGroupMin, max: m.occupancyGroupMax };
34
+ default:
35
+ return { min: m.occupancyMinLabel, max: m.occupancyMaxLabel };
36
+ }
37
+ }
38
+ const buildUnitFormSchema = (messages) => z.object({
39
+ name: z.string().min(1, messages.validationNameRequired).max(255),
40
+ code: z.string().max(100).optional().nullable(),
41
+ description: z.string().optional().nullable(),
42
+ unitType: z.enum(["person", "group", "room", "vehicle", "service", "other"]),
43
+ minQuantity: z.coerce.number().int().min(0).optional().or(z.literal("")).nullable(),
44
+ maxQuantity: z.coerce.number().int().min(0).optional().or(z.literal("")).nullable(),
45
+ minAge: z.coerce.number().int().min(0).optional().or(z.literal("")).nullable(),
46
+ maxAge: z.coerce.number().int().min(0).optional().or(z.literal("")).nullable(),
47
+ occupancyMin: z.coerce.number().int().min(0).optional().or(z.literal("")).nullable(),
48
+ occupancyMax: z.coerce.number().int().min(0).optional().or(z.literal("")).nullable(),
49
+ isRequired: z.boolean(),
50
+ isHidden: z.boolean(),
51
+ sortOrder: z.coerce.number().int(),
52
+ });
53
+ function initialValues(unit, nextSortOrder, defaultUnitType) {
54
+ if (unit) {
55
+ return {
56
+ name: unit.name,
57
+ code: unit.code ?? "",
58
+ description: unit.description ?? "",
59
+ unitType: unit.unitType,
60
+ minQuantity: unit.minQuantity ?? "",
61
+ maxQuantity: unit.maxQuantity ?? "",
62
+ minAge: unit.minAge ?? "",
63
+ maxAge: unit.maxAge ?? "",
64
+ occupancyMin: unit.occupancyMin ?? "",
65
+ occupancyMax: unit.occupancyMax ?? "",
66
+ isRequired: unit.isRequired,
67
+ isHidden: unit.isHidden,
68
+ sortOrder: unit.sortOrder,
69
+ };
70
+ }
71
+ return {
72
+ name: "",
73
+ code: "",
74
+ description: "",
75
+ unitType: defaultUnitType ?? "person",
76
+ minQuantity: "",
77
+ maxQuantity: "",
78
+ minAge: "",
79
+ maxAge: "",
80
+ occupancyMin: "",
81
+ occupancyMax: "",
82
+ isRequired: false,
83
+ isHidden: false,
84
+ sortOrder: nextSortOrder ?? 0,
85
+ };
86
+ }
87
+ export function UnitForm({ optionId, unit, defaultUnitType, lockUnitType, nextSortOrder, onSuccess, onCancel, }) {
88
+ const messages = useProductDetailMessages();
89
+ const productMessages = messages.products.core;
90
+ const unitMessages = messages.products.operations.units;
91
+ const isEditing = !!unit;
92
+ const { create, update } = useOptionUnitMutation();
93
+ const unitFormSchema = buildUnitFormSchema(unitMessages);
94
+ const unitTypes = [
95
+ { value: "person", label: unitMessages.typePerson },
96
+ { value: "group", label: unitMessages.typeGroup },
97
+ { value: "room", label: unitMessages.typeRoom },
98
+ { value: "vehicle", label: unitMessages.typeVehicle },
99
+ { value: "service", label: unitMessages.typeService },
100
+ { value: "other", label: unitMessages.typeOther },
101
+ ];
102
+ const form = useForm({
103
+ resolver: zodResolver(unitFormSchema),
104
+ defaultValues: initialValues(unit, nextSortOrder, defaultUnitType),
105
+ });
106
+ useEffect(() => {
107
+ form.reset(initialValues(unit, nextSortOrder, defaultUnitType));
108
+ }, [unit, nextSortOrder, defaultUnitType, form]);
109
+ const onSubmit = async (values) => {
110
+ const canHaveAge = values.unitType === "person";
111
+ const canHaveOccupancy = values.unitType === "group" || values.unitType === "room" || values.unitType === "vehicle";
112
+ const payload = {
113
+ name: values.name,
114
+ code: values.code || null,
115
+ description: values.description || null,
116
+ unitType: values.unitType,
117
+ minQuantity: typeof values.minQuantity === "number" ? values.minQuantity : null,
118
+ maxQuantity: typeof values.maxQuantity === "number" ? values.maxQuantity : null,
119
+ minAge: canHaveAge && typeof values.minAge === "number" ? values.minAge : null,
120
+ maxAge: canHaveAge && typeof values.maxAge === "number" ? values.maxAge : null,
121
+ occupancyMin: canHaveOccupancy && typeof values.occupancyMin === "number" ? values.occupancyMin : null,
122
+ occupancyMax: canHaveOccupancy && typeof values.occupancyMax === "number" ? values.occupancyMax : null,
123
+ isRequired: values.isRequired,
124
+ isHidden: values.isHidden,
125
+ sortOrder: values.sortOrder,
126
+ };
127
+ if (isEditing) {
128
+ await update.mutateAsync({ id: unit.id, input: payload });
129
+ }
130
+ else {
131
+ await create.mutateAsync({ optionId, ...payload });
132
+ }
133
+ onSuccess();
134
+ };
135
+ const unitType = form.watch("unitType");
136
+ const qtyLabels = quantityLabels(unitType, unitMessages);
137
+ const occLabels = occupancyLabels(unitType, unitMessages);
138
+ 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: [lockUnitType ? null : (_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: qtyLabels.min }), _jsx(Input, { ...form.register("minQuantity"), type: "number", min: "0" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: qtyLabels.max }), _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: occLabels.min }), _jsx(Input, { ...form.register("occupancyMin"), type: "number", min: "0" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: occLabels.max }), _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] })] })] }));
139
+ }
@@ -0,0 +1,17 @@
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
+ productCurrency?: string;
11
+ preselectedUnitId?: string;
12
+ preselectedCategoryId?: string | null;
13
+ cell?: OptionUnitPriceRuleData;
14
+ onSuccess: () => void;
15
+ };
16
+ export declare function UnitPriceRuleDialog({ open, onOpenChange, optionPriceRuleId, optionId, units, productCurrency, preselectedUnitId, preselectedCategoryId, cell, onSuccess, }: UnitPriceRuleDialogProps): import("react/jsx-runtime").JSX.Element;
17
+ //# 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,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,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,eAAe,EACf,iBAAiB,EACjB,qBAAqB,EACrB,IAAI,EACJ,SAAS,GACV,EAAE,wBAAwB,2CA6B1B"}
@@ -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, productCurrency, 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, productCurrency: productCurrency, preselectedUnitId: preselectedUnitId, preselectedCategoryId: preselectedCategoryId, cell: cell, onSuccess: onSuccess, onCancel: () => onOpenChange(false) }) })] }) }));
10
+ }
@@ -0,0 +1,29 @@
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
+ productCurrency?: string;
22
+ preselectedUnitId?: string;
23
+ preselectedCategoryId?: string | null;
24
+ cell?: OptionUnitPriceRuleData;
25
+ onSuccess: () => void;
26
+ onCancel?: () => void;
27
+ }
28
+ export declare function UnitPriceRuleForm({ optionPriceRuleId, optionId, units, productCurrency, preselectedUnitId, preselectedCategoryId, cell, onSuccess, onCancel, }: UnitPriceRuleFormProps): import("react/jsx-runtime").JSX.Element;
29
+ //# 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":"AAoBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAA;AA2E5D,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,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,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,eAAe,EACf,iBAAiB,EACjB,qBAAqB,EACrB,IAAI,EACJ,SAAS,EACT,QAAQ,GACT,EAAE,sBAAsB,2CAoLxB"}