@voyantjs/products-ui 0.101.2 → 0.103.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 (40) hide show
  1. package/dist/components/product-detail/product-departure-form.d.ts.map +1 -1
  2. package/dist/components/product-detail/product-departure-form.js +22 -2
  3. package/dist/components/product-detail/product-detail-form.d.ts +3 -0
  4. package/dist/components/product-detail/product-detail-form.d.ts.map +1 -1
  5. package/dist/components/product-detail/product-detail-form.js +31 -4
  6. package/dist/components/product-detail/product-detail-page.d.ts.map +1 -1
  7. package/dist/components/product-detail/product-detail-page.js +2 -3
  8. package/dist/components/product-detail/product-extra-dialog.d.ts +21 -0
  9. package/dist/components/product-detail/product-extra-dialog.d.ts.map +1 -0
  10. package/dist/components/product-detail/product-extra-dialog.js +131 -0
  11. package/dist/components/product-detail/product-option-pricing-grid.d.ts +16 -0
  12. package/dist/components/product-detail/product-option-pricing-grid.d.ts.map +1 -0
  13. package/dist/components/product-detail/product-option-pricing-grid.js +233 -0
  14. package/dist/components/product-detail/product-options-pricing.d.ts +38 -1
  15. package/dist/components/product-detail/product-options-pricing.d.ts.map +1 -1
  16. package/dist/components/product-detail/product-options-pricing.js +136 -46
  17. package/dist/components/product-detail/product-options-shared.d.ts +14 -0
  18. package/dist/components/product-detail/product-options-shared.d.ts.map +1 -1
  19. package/dist/components/product-detail/product-options-shared.js +20 -0
  20. package/dist/components/product-detail/product-translation-popover.d.ts +4 -1
  21. package/dist/components/product-detail/product-translation-popover.d.ts.map +1 -1
  22. package/dist/components/product-detail/product-translation-popover.js +28 -8
  23. package/dist/components/product-detail/product-unit-dialog.d.ts +3 -1
  24. package/dist/components/product-detail/product-unit-dialog.d.ts.map +1 -1
  25. package/dist/components/product-detail/product-unit-dialog.js +2 -2
  26. package/dist/components/product-detail/product-unit-form.d.ts +9 -1
  27. package/dist/components/product-detail/product-unit-form.d.ts.map +1 -1
  28. package/dist/components/product-detail/product-unit-form.js +37 -7
  29. package/dist/components/product-detail/product-unit-price-rule-dialog.d.ts +2 -1
  30. package/dist/components/product-detail/product-unit-price-rule-dialog.d.ts.map +1 -1
  31. package/dist/components/product-detail/product-unit-price-rule-dialog.js +2 -2
  32. package/dist/components/product-detail/product-unit-price-rule-form.d.ts +2 -1
  33. package/dist/components/product-detail/product-unit-price-rule-form.d.ts.map +1 -1
  34. package/dist/components/product-detail/product-unit-price-rule-form.js +28 -9
  35. package/dist/components/product-options-section.d.ts.map +1 -1
  36. package/dist/components/product-options-section.js +31 -20
  37. package/package.json +29 -29
  38. package/dist/components/product-detail/product-extras-section.d.ts +0 -4
  39. package/dist/components/product-detail/product-extras-section.d.ts.map +0 -1
  40. package/dist/components/product-detail/product-extras-section.js +0 -141
@@ -1 +1 @@
1
- {"version":3,"file":"product-options-shared.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/product-options-shared.ts"],"names":[],"mappings":"AAOA,OAAO,EAGL,KAAK,0BAA0B,EAChC,MAAM,0BAA0B,CAAA;AAEjC;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,0BAA0B,CAAA;AAEtD,eAAO,MAAM,mBAAmB,EAAE,MAAM,CACtC,MAAM,EACN,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,aAAa,CAKpD,CAAA;AAED,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAErF;AAED,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAEjF;AAED,wBAAgB,+BAA+B,CAAC,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAEtF;AAED,wBAAgB,gCAAgC,CAAC,MAAM,EAAE,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAErE;AAED,wBAAgB,mCAAmC,CACjD,MAAM,EAAE,aAAa,EACrB,iBAAiB,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAM1B;AAED,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAEjE;AAED,wBAAgB,sCAAsC,CAAC,MAAM,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAKhG"}
1
+ {"version":3,"file":"product-options-shared.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/product-options-shared.ts"],"names":[],"mappings":"AAOA,OAAO,EAGL,KAAK,0BAA0B,EAChC,MAAM,0BAA0B,CAAA;AAEjC;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,0BAA0B,CAAA;AAEtD,eAAO,MAAM,mBAAmB,EAAE,MAAM,CACtC,MAAM,EACN,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,aAAa,CAKpD,CAAA;AAED;;;;;GAKG;AACH,MAAM,MAAM,mBAAmB,GAAG,OAAO,GAAG,OAAO,CAAA;AAEnD;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACtC,QAAQ,CAAC,EAAE,MAAM,GAChB,mBAAmB,CAarB;AAED,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAErF;AAED,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAEjF;AAED,wBAAgB,+BAA+B,CAAC,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAEtF;AAED,wBAAgB,gCAAgC,CAAC,MAAM,EAAE,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAErE;AAED,wBAAgB,mCAAmC,CACjD,MAAM,EAAE,aAAa,EACrB,iBAAiB,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAM1B;AAED,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAEjE;AAED,wBAAgB,sCAAsC,CAAC,MAAM,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAKhG"}
@@ -5,6 +5,26 @@ export const optionStatusVariant = {
5
5
  active: "default",
6
6
  archived: "secondary",
7
7
  };
8
+ /**
9
+ * Derive the pricing layout from the product's booking mode. Multi-day /
10
+ * overnight modes imply rooms; single-day activity modes imply per-person
11
+ * seats. `dayCount` is a fallback for the ambiguous `other` mode (>1 day →
12
+ * rooms), matching the operator rule "more than one day means rooms".
13
+ */
14
+ export function deriveOptionPricingLayout(bookingMode, dayCount) {
15
+ switch (bookingMode) {
16
+ case "stay":
17
+ case "itinerary":
18
+ return "rooms";
19
+ case "date":
20
+ case "date_time":
21
+ case "open":
22
+ case "transfer":
23
+ return "seats";
24
+ default:
25
+ return dayCount != null && dayCount > 1 ? "rooms" : "seats";
26
+ }
27
+ }
8
28
  export function getProductOptionsQueryOptions(client, productId) {
9
29
  return getSharedProductOptionsQueryOptions(client, { productId, limit: 100 });
10
30
  }
@@ -1,6 +1,6 @@
1
1
  import type { useProductDetailMessages } from "./host.js";
2
2
  type ProductCoreMessages = ReturnType<typeof useProductDetailMessages>["products"]["core"];
3
- export type TranslatableField = "name" | "description" | "slug";
3
+ export type TranslatableField = "name" | "description" | "slug" | "inclusionsHtml" | "exclusionsHtml" | "termsHtml";
4
4
  export type TranslationDraft = {
5
5
  id: string | null;
6
6
  languageTag: string;
@@ -20,6 +20,9 @@ export interface PersistTranslationsOptions {
20
20
  defaultLanguageTag: string;
21
21
  baseName: string;
22
22
  baseDescription: string;
23
+ baseInclusionsHtml: string;
24
+ baseExclusionsHtml: string;
25
+ baseTermsHtml: string;
23
26
  }
24
27
  export interface ProductTranslationDrafts {
25
28
  drafts: TranslationDraft[];
@@ -1 +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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"}
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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"}
@@ -9,6 +9,12 @@ import { cn } from "@voyantjs/ui/lib/utils";
9
9
  import { languages } from "@voyantjs/utils/languages";
10
10
  import { Globe, Plus, X } from "lucide-react";
11
11
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
12
+ const RICH_TEXT_FIELDS = new Set([
13
+ "description",
14
+ "inclusionsHtml",
15
+ "exclusionsHtml",
16
+ "termsHtml",
17
+ ]);
12
18
  function recordToDraft(record) {
13
19
  return {
14
20
  id: record.id,
@@ -47,14 +53,23 @@ export function richTextHasContent(html) {
47
53
  .trim().length > 0);
48
54
  }
49
55
  function fieldHasContent(draft, field) {
50
- if (field === "description")
51
- return richTextHasContent(draft.description);
52
- return draft[field].trim().length > 0;
56
+ const value = draft[field] ?? "";
57
+ if (RICH_TEXT_FIELDS.has(field))
58
+ return richTextHasContent(value);
59
+ return value.trim().length > 0;
53
60
  }
54
61
  export function languageLabel(tag) {
55
62
  const base = tag.split("-")[0]?.toLowerCase() ?? tag;
56
63
  return languages[base] ?? tag;
57
64
  }
65
+ // Resolve a rich-text translation column: the default-language row mirrors the
66
+ // base product column (when it has content), every row otherwise uses its own
67
+ // draft, and empty markup collapses to null so the column stays clean.
68
+ function resolveRichText(isDefault, baseValue, draftValue) {
69
+ if (isDefault && richTextHasContent(baseValue))
70
+ return baseValue;
71
+ return richTextHasContent(draftValue ?? "") ? (draftValue ?? "") : null;
72
+ }
58
73
  /**
59
74
  * Manages an in-memory draft of a product's translations so Name/Description/
60
75
  * Slug can be edited in context from the edit sheet. Seeds from the saved
@@ -102,7 +117,7 @@ export function useProductTranslationDrafts(productId) {
102
117
  setDrafts((prev) => prev.filter((draft) => draft.languageTag !== languageTag));
103
118
  }, []);
104
119
  const persist = useCallback(async (resolvedProductId, options) => {
105
- const { defaultLanguageTag, baseName, baseDescription } = options;
120
+ const { defaultLanguageTag, baseName, baseDescription, baseInclusionsHtml, baseExclusionsHtml, baseTermsHtml, } = options;
106
121
  const original = existingRef.current;
107
122
  const currentLanguages = new Set(drafts.map((draft) => draft.languageTag));
108
123
  const deletes = original
@@ -128,22 +143,27 @@ export function useProductTranslationDrafts(productId) {
128
143
  ? draft.description
129
144
  : null;
130
145
  const slug = draft.slug.trim() ? draft.slug.trim() : null;
146
+ const inclusionsHtml = resolveRichText(isDefault, baseInclusionsHtml, draft.inclusionsHtml);
147
+ const exclusionsHtml = resolveRichText(isDefault, baseExclusionsHtml, draft.exclusionsHtml);
148
+ const termsHtml = resolveRichText(isDefault, baseTermsHtml, draft.termsHtml);
149
+ const richInput = { inclusionsHtml, exclusionsHtml, termsHtml };
131
150
  if (draft.id) {
132
151
  return mutations.update.mutateAsync({
133
152
  productId: resolvedProductId,
134
153
  translationId: draft.id,
135
- input: { name, description, slug },
154
+ input: { name, description, slug, ...richInput },
136
155
  });
137
156
  }
138
157
  // A brand-new row is only worth creating once it carries content.
158
+ const hasRichContent = !!inclusionsHtml || !!exclusionsHtml || !!termsHtml;
139
159
  const isEmpty = isDefault
140
- ? !slug
141
- : !draft.name.trim() && !richTextHasContent(draft.description) && !slug;
160
+ ? !slug && !hasRichContent
161
+ : !draft.name.trim() && !richTextHasContent(draft.description) && !slug && !hasRichContent;
142
162
  if (isEmpty)
143
163
  return Promise.resolve(null);
144
164
  return mutations.create.mutateAsync({
145
165
  productId: resolvedProductId,
146
- input: { languageTag: draft.languageTag, name, description, slug },
166
+ input: { languageTag: draft.languageTag, name, description, slug, ...richInput },
147
167
  });
148
168
  });
149
169
  await Promise.all([...deletes, ...upserts]);
@@ -5,8 +5,10 @@ type UnitDialogProps = {
5
5
  onOpenChange: (open: boolean) => void;
6
6
  optionId: string;
7
7
  unit?: OptionUnitData;
8
+ defaultUnitType?: OptionUnitData["unitType"];
9
+ lockUnitType?: boolean;
8
10
  nextSortOrder?: number;
9
11
  onSuccess: () => void;
10
12
  };
11
- export declare function UnitDialog({ open, onOpenChange, optionId, unit, nextSortOrder, onSuccess, }: UnitDialogProps): import("react/jsx-runtime").JSX.Element;
13
+ export declare function UnitDialog({ open, onOpenChange, optionId, unit, defaultUnitType, lockUnitType, nextSortOrder, onSuccess, }: UnitDialogProps): import("react/jsx-runtime").JSX.Element;
12
14
  //# sourceMappingURL=product-unit-dialog.d.ts.map
@@ -1 +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"}
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"}
@@ -2,9 +2,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTitle } from "@voyantjs/ui/components";
3
3
  import { useProductDetailMessages } from "./host.js";
4
4
  import { UnitForm } from "./product-unit-form.js";
5
- export function UnitDialog({ open, onOpenChange, optionId, unit, nextSortOrder, onSuccess, }) {
5
+ export function UnitDialog({ open, onOpenChange, optionId, unit, defaultUnitType, lockUnitType, nextSortOrder, onSuccess, }) {
6
6
  const messages = useProductDetailMessages();
7
7
  const unitMessages = messages.products.operations.units;
8
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) }) })] }) }));
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
10
  }
@@ -18,9 +18,17 @@ export type OptionUnitData = {
18
18
  export interface UnitFormProps {
19
19
  optionId: string;
20
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;
21
29
  nextSortOrder?: number;
22
30
  onSuccess: () => void;
23
31
  onCancel?: () => void;
24
32
  }
25
- export declare function UnitForm({ optionId, unit, nextSortOrder, onSuccess, onCancel }: UnitFormProps): import("react/jsx-runtime").JSX.Element;
33
+ export declare function UnitForm({ optionId, unit, defaultUnitType, lockUnitType, nextSortOrder, onSuccess, onCancel, }: UnitFormProps): import("react/jsx-runtime").JSX.Element;
26
34
  //# sourceMappingURL=product-unit-form.d.ts.map
@@ -1 +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"}
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"}
@@ -7,6 +7,34 @@ import { useForm } from "react-hook-form";
7
7
  import { z } from "zod/v4";
8
8
  import { useProductDetailMessages } from "./host.js";
9
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
+ }
10
38
  const buildUnitFormSchema = (messages) => z.object({
11
39
  name: z.string().min(1, messages.validationNameRequired).max(255),
12
40
  code: z.string().max(100).optional().nullable(),
@@ -22,7 +50,7 @@ const buildUnitFormSchema = (messages) => z.object({
22
50
  isHidden: z.boolean(),
23
51
  sortOrder: z.coerce.number().int(),
24
52
  });
25
- function initialValues(unit, nextSortOrder) {
53
+ function initialValues(unit, nextSortOrder, defaultUnitType) {
26
54
  if (unit) {
27
55
  return {
28
56
  name: unit.name,
@@ -44,7 +72,7 @@ function initialValues(unit, nextSortOrder) {
44
72
  name: "",
45
73
  code: "",
46
74
  description: "",
47
- unitType: "person",
75
+ unitType: defaultUnitType ?? "person",
48
76
  minQuantity: "",
49
77
  maxQuantity: "",
50
78
  minAge: "",
@@ -56,7 +84,7 @@ function initialValues(unit, nextSortOrder) {
56
84
  sortOrder: nextSortOrder ?? 0,
57
85
  };
58
86
  }
59
- export function UnitForm({ optionId, unit, nextSortOrder, onSuccess, onCancel }) {
87
+ export function UnitForm({ optionId, unit, defaultUnitType, lockUnitType, nextSortOrder, onSuccess, onCancel, }) {
60
88
  const messages = useProductDetailMessages();
61
89
  const productMessages = messages.products.core;
62
90
  const unitMessages = messages.products.operations.units;
@@ -73,11 +101,11 @@ export function UnitForm({ optionId, unit, nextSortOrder, onSuccess, onCancel })
73
101
  ];
74
102
  const form = useForm({
75
103
  resolver: zodResolver(unitFormSchema),
76
- defaultValues: initialValues(unit, nextSortOrder),
104
+ defaultValues: initialValues(unit, nextSortOrder, defaultUnitType),
77
105
  });
78
106
  useEffect(() => {
79
- form.reset(initialValues(unit, nextSortOrder));
80
- }, [unit, nextSortOrder, form]);
107
+ form.reset(initialValues(unit, nextSortOrder, defaultUnitType));
108
+ }, [unit, nextSortOrder, defaultUnitType, form]);
81
109
  const onSubmit = async (values) => {
82
110
  const canHaveAge = values.unitType === "person";
83
111
  const canHaveOccupancy = values.unitType === "group" || values.unitType === "room" || values.unitType === "vehicle";
@@ -105,5 +133,7 @@ export function UnitForm({ optionId, unit, nextSortOrder, onSuccess, onCancel })
105
133
  onSuccess();
106
134
  };
107
135
  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] })] })] }));
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] })] })] }));
109
139
  }
@@ -7,10 +7,11 @@ type UnitPriceRuleDialogProps = {
7
7
  optionPriceRuleId: string;
8
8
  optionId: string;
9
9
  units: OptionUnitData[];
10
+ productCurrency?: string;
10
11
  preselectedUnitId?: string;
11
12
  preselectedCategoryId?: string | null;
12
13
  cell?: OptionUnitPriceRuleData;
13
14
  onSuccess: () => void;
14
15
  };
15
- export declare function UnitPriceRuleDialog({ open, onOpenChange, optionPriceRuleId, optionId, units, preselectedUnitId, preselectedCategoryId, cell, onSuccess, }: UnitPriceRuleDialogProps): import("react/jsx-runtime").JSX.Element;
16
+ export declare function UnitPriceRuleDialog({ open, onOpenChange, optionPriceRuleId, optionId, units, productCurrency, preselectedUnitId, preselectedCategoryId, cell, onSuccess, }: UnitPriceRuleDialogProps): import("react/jsx-runtime").JSX.Element;
16
17
  //# sourceMappingURL=product-unit-price-rule-dialog.d.ts.map
@@ -1 +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"}
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"}
@@ -2,9 +2,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTitle } from "@voyantjs/ui/components";
3
3
  import { useProductDetailMessages } from "./host.js";
4
4
  import { UnitPriceRuleForm } from "./product-unit-price-rule-form.js";
5
- export function UnitPriceRuleDialog({ open, onOpenChange, optionPriceRuleId, optionId, units, preselectedUnitId, preselectedCategoryId, cell, onSuccess, }) {
5
+ export function UnitPriceRuleDialog({ open, onOpenChange, optionPriceRuleId, optionId, units, productCurrency, preselectedUnitId, preselectedCategoryId, cell, onSuccess, }) {
6
6
  const messages = useProductDetailMessages();
7
7
  const unitPriceMessages = messages.products.operations.unitPrices;
8
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) }) })] }) }));
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
10
  }
@@ -18,11 +18,12 @@ export interface UnitPriceRuleFormProps {
18
18
  optionPriceRuleId: string;
19
19
  optionId: string;
20
20
  units: OptionUnitData[];
21
+ productCurrency?: string;
21
22
  preselectedUnitId?: string;
22
23
  preselectedCategoryId?: string | null;
23
24
  cell?: OptionUnitPriceRuleData;
24
25
  onSuccess: () => void;
25
26
  onCancel?: () => void;
26
27
  }
27
- export declare function UnitPriceRuleForm({ optionPriceRuleId, optionId, units, preselectedUnitId, preselectedCategoryId, cell, onSuccess, onCancel, }: UnitPriceRuleFormProps): import("react/jsx-runtime").JSX.Element;
28
+ export declare function UnitPriceRuleForm({ optionPriceRuleId, optionId, units, productCurrency, preselectedUnitId, preselectedCategoryId, cell, onSuccess, onCancel, }: UnitPriceRuleFormProps): import("react/jsx-runtime").JSX.Element;
28
29
  //# sourceMappingURL=product-unit-price-rule-form.d.ts.map
@@ -1 +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"}
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"}
@@ -2,11 +2,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useOptionUnitPriceRuleMutation } from "@voyantjs/pricing-react";
3
3
  import { PricingCategoryCombobox } from "@voyantjs/pricing-ui/components/pricing-category-combobox";
4
4
  import { Button, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, Textarea, } from "@voyantjs/ui/components";
5
+ import { CurrencyInput } from "@voyantjs/ui/components/currency-input";
5
6
  import { Loader2 } from "lucide-react";
6
7
  import { useEffect } from "react";
7
8
  import { useForm } from "react-hook-form";
8
9
  import { z } from "zod/v4";
9
- import { useProductDetailMessages } from "./host.js";
10
+ import { useProductDetailMessages, useProductLocale } from "./host.js";
10
11
  import { zodResolver } from "./zod-resolver.js";
11
12
  function getUnitTypeLabel(type, messages) {
12
13
  switch (type) {
@@ -26,6 +27,21 @@ function getUnitTypeLabel(type, messages) {
26
27
  return type;
27
28
  }
28
29
  }
30
+ // "Min/Max quantity" means different things per pricing mode — travelers for
31
+ // per-person, units for per-unit, the whole booking for per-booking. Label it
32
+ // for what's actually being counted.
33
+ function cellQuantityLabels(pricingMode, m) {
34
+ switch (pricingMode) {
35
+ case "per_person":
36
+ return { min: m.minQuantityPerson, max: m.maxQuantityPerson };
37
+ case "per_unit":
38
+ return { min: m.minQuantityUnit, max: m.maxQuantityUnit };
39
+ case "per_booking":
40
+ return { min: m.minQuantityBooking, max: m.maxQuantityBooking };
41
+ default:
42
+ return { min: m.minQuantityLabel, max: m.maxQuantityLabel };
43
+ }
44
+ }
29
45
  const buildCellFormSchema = (messages) => z.object({
30
46
  unitId: z.string().min(1, messages.validationUnitRequired),
31
47
  pricingCategoryId: z.string().optional().nullable(),
@@ -37,8 +53,10 @@ const buildCellFormSchema = (messages) => z.object({
37
53
  "free",
38
54
  "on_request",
39
55
  ]),
40
- sell: z.coerce.number().min(0),
41
- cost: z.coerce.number().min(0),
56
+ // Stored in minor units (cents) so CurrencyInput can render the currency
57
+ // prefix and parse locale-formatted amounts directly.
58
+ sell: z.number().int().min(0),
59
+ cost: z.number().int().min(0),
42
60
  minQuantity: z.coerce.number().int().min(0).optional().or(z.literal("")).nullable(),
43
61
  maxQuantity: z.coerce.number().int().min(0).optional().or(z.literal("")).nullable(),
44
62
  sortOrder: z.coerce.number().int(),
@@ -51,8 +69,8 @@ function initialValues(cell, preselectedUnitId, preselectedCategoryId) {
51
69
  unitId: cell.unitId,
52
70
  pricingCategoryId: cell.pricingCategoryId ?? "",
53
71
  pricingMode: cell.pricingMode,
54
- sell: (cell.sellAmountCents ?? 0) / 100,
55
- cost: (cell.costAmountCents ?? 0) / 100,
72
+ sell: cell.sellAmountCents ?? 0,
73
+ cost: cell.costAmountCents ?? 0,
56
74
  minQuantity: cell.minQuantity ?? "",
57
75
  maxQuantity: cell.maxQuantity ?? "",
58
76
  sortOrder: cell.sortOrder,
@@ -73,11 +91,12 @@ function initialValues(cell, preselectedUnitId, preselectedCategoryId) {
73
91
  notes: "",
74
92
  };
75
93
  }
76
- export function UnitPriceRuleForm({ optionPriceRuleId, optionId, units, preselectedUnitId, preselectedCategoryId, cell, onSuccess, onCancel, }) {
94
+ export function UnitPriceRuleForm({ optionPriceRuleId, optionId, units, productCurrency, preselectedUnitId, preselectedCategoryId, cell, onSuccess, onCancel, }) {
77
95
  const messages = useProductDetailMessages();
78
96
  const productMessages = messages.products.core;
79
97
  const unitPriceMessages = messages.products.operations.unitPrices;
80
98
  const unitMessages = messages.products.operations.units;
99
+ const locale = useProductLocale();
81
100
  const isEditing = !!cell;
82
101
  const { create, update } = useOptionUnitPriceRuleMutation();
83
102
  const cellFormSchema = buildCellFormSchema(unitPriceMessages);
@@ -103,8 +122,8 @@ export function UnitPriceRuleForm({ optionPriceRuleId, optionId, units, preselec
103
122
  unitId: values.unitId,
104
123
  pricingCategoryId: values.pricingCategoryId || null,
105
124
  pricingMode: values.pricingMode,
106
- sellAmountCents: Math.round(values.sell * 100),
107
- costAmountCents: Math.round(values.cost * 100),
125
+ sellAmountCents: Math.round(values.sell),
126
+ costAmountCents: Math.round(values.cost),
108
127
  minQuantity: typeof values.minQuantity === "number" ? values.minQuantity : null,
109
128
  maxQuantity: typeof values.maxQuantity === "number" ? values.maxQuantity : null,
110
129
  sortOrder: values.sortOrder,
@@ -122,5 +141,5 @@ export function UnitPriceRuleForm({ optionPriceRuleId, optionId, units, preselec
122
141
  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
142
  value: u.id,
124
143
  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] })] })] }));
144
+ })), 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(CurrencyInput, { value: form.watch("sell"), onChange: (value) => form.setValue("sell", value ?? 0, { shouldValidate: true }), currency: productCurrency, locale: locale })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: unitPriceMessages.costLabel }), _jsx(CurrencyInput, { value: form.watch("cost"), onChange: (value) => form.setValue("cost", value ?? 0, { shouldValidate: true }), currency: productCurrency, locale: locale })] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: cellQuantityLabels(form.watch("pricingMode"), unitPriceMessages).min }), _jsx(Input, { ...form.register("minQuantity"), type: "number", min: "0" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: cellQuantityLabels(form.watch("pricingMode"), unitPriceMessages).max }), _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
145
  }
@@ -1 +1 @@
1
- {"version":3,"file":"product-options-section.d.ts","sourceRoot":"","sources":["../../src/components/product-options-section.tsx"],"names":[],"mappings":"AAIA,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EAOzB,MAAM,0BAA0B,CAAA;AA6BjC,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAqC9B,wBAAgB,mCAAmC,CACjD,MAAM,EAAE,IAAI,CAAC,mBAAmB,EAAE,MAAM,GAAG,MAAM,CAAC,GACjD,OAAO,CAIT;AAED,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,aAAa,CAAC,IAAI,CAAC,mBAAmB,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,QAAQ,CAAC,CAAC,EACpF,eAAe,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,IAAI,CAAC,gBAAgB,EAAE,UAAU,CAAC,EAAE,CAAC,GAClF,MAAM,EAAE,CASV;AA0CD,MAAM,WAAW,0BAA0B;IACzC,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,mBAAmB,CAAC,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,KAAK,CAAC,SAAS,CAAA;CACvE;AAED,wBAAgB,qBAAqB,CAAC,EACpC,SAAS,EACT,QAAc,EACd,KAAK,EACL,WAAW,EACX,mBAAmB,GACpB,EAAE,0BAA0B,2CAsJ5B"}
1
+ {"version":3,"file":"product-options-section.d.ts","sourceRoot":"","sources":["../../src/components/product-options-section.tsx"],"names":[],"mappings":"AAIA,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EAOzB,MAAM,0BAA0B,CAAA;AA6BjC,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAqC9B,wBAAgB,mCAAmC,CACjD,MAAM,EAAE,IAAI,CAAC,mBAAmB,EAAE,MAAM,GAAG,MAAM,CAAC,GACjD,OAAO,CAIT;AAED,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,aAAa,CAAC,IAAI,CAAC,mBAAmB,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,QAAQ,CAAC,CAAC,EACpF,eAAe,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,IAAI,CAAC,gBAAgB,EAAE,UAAU,CAAC,EAAE,CAAC,GAClF,MAAM,EAAE,CASV;AA0CD,MAAM,WAAW,0BAA0B;IACzC,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,mBAAmB,CAAC,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,KAAK,CAAC,SAAS,CAAA;CACvE;AAED,wBAAgB,qBAAqB,CAAC,EACpC,SAAS,EACT,QAAc,EACd,KAAK,EACL,WAAW,EACX,mBAAmB,GACpB,EAAE,0BAA0B,2CA+J5B"}
@@ -111,34 +111,45 @@ export function ProductOptionsSection({ productId, pageSize = 100, title, descri
111
111
  const nextSortOrder = options.length > 0 ? Math.max(...options.map((option) => option.sortOrder)) + 1 : 0;
112
112
  const resolvedTitle = title ?? messages.productOptionsSection.titles.default;
113
113
  const resolvedDescription = description ?? messages.productOptionsSection.descriptions.default;
114
+ // A product with a single option needs no option chrome — show its pricing
115
+ // table directly. Only flatten when a host injects the details (the grid);
116
+ // bare mounts (apps/dev) keep the expandable units table.
117
+ const flattenedOption = renderOptionDetails && options.length === 1 ? options[0] : undefined;
118
+ const editOption = (option) => {
119
+ setEditingOption(option);
120
+ setDialogOpen(true);
121
+ };
122
+ const duplicateOptionFlow = (option) => {
123
+ duplicateOption.mutate({ sourceOptionId: option.id, productId }, {
124
+ onSuccess: async ({ option: duplicatedOption, unitIdMap }) => {
125
+ await duplicatePricing.mutateAsync({
126
+ sourceOptionId: option.id,
127
+ targetOptionId: duplicatedOption.id,
128
+ productId,
129
+ unitIdMap,
130
+ });
131
+ },
132
+ });
133
+ };
134
+ const deleteOption = (option) => {
135
+ if (confirm(messages.productOptionsSection.deleteConfirm.option.replace("{name}", option.name))) {
136
+ remove.mutate(option.id);
137
+ }
138
+ };
114
139
  return (_jsxs(Card, { "data-slot": "product-options-section", children: [_jsxs(CardHeader, { className: "flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(CardTitle, { children: resolvedTitle }), _jsx(CardDescription, { children: resolvedDescription })] }), _jsxs(Button, { onClick: () => {
115
140
  setEditingOption(undefined);
116
141
  setDialogOpen(true);
117
- }, children: [_jsx(Plus, { className: "mr-2 size-4", "aria-hidden": "true" }), messages.productOptionsSection.actions.addOption] })] }), _jsxs(CardContent, { className: "flex flex-col gap-3", children: [showRoomArrangementWarning ? (_jsxs(Alert, { className: "border-amber-500/40 bg-amber-500/10", children: [_jsx(TriangleAlert, { className: "size-4 text-amber-600", "aria-hidden": "true" }), _jsx(AlertTitle, { children: messages.productOptionsSection.configurationWarnings.roomOptionsTitle }), _jsx(AlertDescription, { children: formatMessage(messages.productOptionsSection.configurationWarnings.roomOptionsDescription, { options: roomArrangementOptionNames.join(", ") }) })] })) : null, isPending ? (_jsx("div", { className: "flex min-h-24 items-center justify-center", children: _jsx(Loader2, { className: "size-4 animate-spin text-muted-foreground" }) })) : isError ? (_jsx("p", { className: "text-sm text-destructive", children: messages.productOptionsSection.loadingError.options })) : options.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: messages.productOptionsSection.empty.options })) : (options.map((option) => (_jsx(OptionRow, { option: option, expanded: expandedOptionId === option.id, onToggle: () => setExpandedOptionId((current) => (current === option.id ? null : option.id)), onEdit: () => {
118
- setEditingOption(option);
119
- setDialogOpen(true);
120
- }, onDuplicate: () => {
121
- duplicateOption.mutate({ sourceOptionId: option.id, productId }, {
122
- onSuccess: async ({ option: duplicatedOption, unitIdMap }) => {
123
- await duplicatePricing.mutateAsync({
124
- sourceOptionId: option.id,
125
- targetOptionId: duplicatedOption.id,
126
- productId,
127
- unitIdMap,
128
- });
129
- },
130
- });
131
- }, onDelete: () => {
132
- if (confirm(messages.productOptionsSection.deleteConfirm.option.replace("{name}", option.name))) {
133
- remove.mutate(option.id);
134
- }
135
- }, messages: messages, children: renderOptionDetails?.(option) }, option.id)))), _jsx(ProductOptionDialog, { open: dialogOpen, onOpenChange: setDialogOpen, productId: productId, option: editingOption, sortOrder: nextSortOrder, onSuccess: () => {
142
+ }, children: [_jsx(Plus, { className: "mr-2 size-4", "aria-hidden": "true" }), messages.productOptionsSection.actions.addOption] })] }), _jsxs(CardContent, { className: "flex flex-col gap-3", children: [showRoomArrangementWarning ? (_jsxs(Alert, { className: "border-amber-500/40 bg-amber-500/10", children: [_jsx(TriangleAlert, { className: "size-4 text-amber-600", "aria-hidden": "true" }), _jsx(AlertTitle, { children: messages.productOptionsSection.configurationWarnings.roomOptionsTitle }), _jsx(AlertDescription, { children: formatMessage(messages.productOptionsSection.configurationWarnings.roomOptionsDescription, { options: roomArrangementOptionNames.join(", ") }) })] })) : null, isPending ? (_jsx("div", { className: "flex min-h-24 items-center justify-center", children: _jsx(Loader2, { className: "size-4 animate-spin text-muted-foreground" }) })) : isError ? (_jsx("p", { className: "text-sm text-destructive", children: messages.productOptionsSection.loadingError.options })) : options.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: messages.productOptionsSection.empty.options })) : flattenedOption ? (
143
+ // A single option needs no chrome at all — show its pricing table
144
+ // directly. Per-option actions (duplicate/edit/delete) only appear
145
+ // once there are 2+ options to disambiguate.
146
+ renderOptionDetails?.(flattenedOption)) : (options.map((option) => (_jsx(OptionRow, { option: option, expanded: expandedOptionId === option.id, onToggle: () => setExpandedOptionId((current) => (current === option.id ? null : option.id)), onEdit: () => editOption(option), onDuplicate: () => duplicateOptionFlow(option), onDelete: () => deleteOption(option), messages: messages, children: renderOptionDetails?.(option) }, option.id)))), _jsx(ProductOptionDialog, { open: dialogOpen, onOpenChange: setDialogOpen, productId: productId, option: editingOption, sortOrder: nextSortOrder, onSuccess: () => {
136
147
  setDialogOpen(false);
137
148
  setEditingOption(undefined);
138
149
  } })] })] }));
139
150
  }
140
151
  function OptionRow({ option, expanded, onToggle, onEdit, onDuplicate, onDelete, messages, children, }) {
141
- return (_jsxs("div", { className: "rounded-md border", children: [_jsxs("div", { className: "flex items-center gap-3 p-3", children: [_jsx("button", { type: "button", onClick: onToggle, className: "text-muted-foreground transition-colors hover:text-foreground", children: expanded ? _jsx(ChevronDown, { className: "size-4" }) : _jsx(ChevronRight, { className: "size-4" }) }), _jsxs("div", { className: "flex flex-1 flex-wrap items-center gap-2", children: [_jsx("span", { className: "text-sm font-medium", children: option.name }), option.code ? (_jsx("span", { className: "font-mono text-xs text-muted-foreground", children: option.code })) : null, _jsx(Badge, { variant: optionStatusVariant[option.status] ?? "outline", children: messages.common.optionStatusLabels[option.status] }), option.isDefault ? (_jsx(Badge, { variant: "secondary", children: messages.productOptionsSection.badges.defaultOption })) : null] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Button, { variant: "ghost", size: "icon-sm", onClick: onDuplicate, "aria-label": messages.productOptionsSection.actions.duplicate, children: _jsx(Copy, { className: "size-4", "aria-hidden": "true" }) }), _jsx(Button, { variant: "ghost", size: "icon-sm", onClick: onEdit, "aria-label": messages.productOptionsSection.actions.edit, children: _jsx(Pencil, { className: "size-4", "aria-hidden": "true" }) }), _jsx(Button, { variant: "ghost", size: "icon-sm", onClick: onDelete, "aria-label": messages.productOptionsSection.actions.delete, children: _jsx(Trash2, { className: "size-4", "aria-hidden": "true" }) })] })] }), expanded ? (_jsxs("div", { className: "flex flex-col gap-4 border-t bg-muted/30 p-3", children: [_jsx(UnitsPanel, { optionId: option.id, messages: messages }), children] })) : null] }));
152
+ return (_jsxs("div", { className: "rounded-md border", children: [_jsxs("div", { className: "flex items-center gap-3 p-3", children: [_jsx("button", { type: "button", onClick: onToggle, className: "text-muted-foreground transition-colors hover:text-foreground", children: expanded ? _jsx(ChevronDown, { className: "size-4" }) : _jsx(ChevronRight, { className: "size-4" }) }), _jsxs("div", { className: "flex flex-1 flex-wrap items-center gap-2", children: [_jsx("span", { className: "text-sm font-medium", children: option.name }), option.code ? (_jsx("span", { className: "font-mono text-xs text-muted-foreground", children: option.code })) : null, _jsx(Badge, { variant: optionStatusVariant[option.status] ?? "outline", children: messages.common.optionStatusLabels[option.status] }), option.isDefault ? (_jsx(Badge, { variant: "secondary", children: messages.productOptionsSection.badges.defaultOption })) : null] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Button, { variant: "ghost", size: "icon-sm", onClick: onDuplicate, "aria-label": messages.productOptionsSection.actions.duplicate, children: _jsx(Copy, { className: "size-4", "aria-hidden": "true" }) }), _jsx(Button, { variant: "ghost", size: "icon-sm", onClick: onEdit, "aria-label": messages.productOptionsSection.actions.edit, children: _jsx(Pencil, { className: "size-4", "aria-hidden": "true" }) }), _jsx(Button, { variant: "ghost", size: "icon-sm", onClick: onDelete, "aria-label": messages.productOptionsSection.actions.delete, children: _jsx(Trash2, { className: "size-4", "aria-hidden": "true" }) })] })] }), expanded ? (_jsx("div", { className: "flex flex-col gap-4 border-t bg-muted/30 p-3", children: children ?? _jsx(UnitsPanel, { optionId: option.id, messages: messages }) })) : null] }));
142
153
  }
143
154
  function UnitsPanel({ optionId, messages, }) {
144
155
  const [dialogOpen, setDialogOpen] = React.useState(false);