@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.
- package/dist/components/product-detail/product-departure-form.d.ts.map +1 -1
- package/dist/components/product-detail/product-departure-form.js +22 -2
- package/dist/components/product-detail/product-detail-form.d.ts +3 -0
- package/dist/components/product-detail/product-detail-form.d.ts.map +1 -1
- package/dist/components/product-detail/product-detail-form.js +31 -4
- package/dist/components/product-detail/product-detail-page.d.ts.map +1 -1
- package/dist/components/product-detail/product-detail-page.js +2 -3
- package/dist/components/product-detail/product-extra-dialog.d.ts +21 -0
- package/dist/components/product-detail/product-extra-dialog.d.ts.map +1 -0
- package/dist/components/product-detail/product-extra-dialog.js +131 -0
- package/dist/components/product-detail/product-option-pricing-grid.d.ts +16 -0
- package/dist/components/product-detail/product-option-pricing-grid.d.ts.map +1 -0
- package/dist/components/product-detail/product-option-pricing-grid.js +233 -0
- package/dist/components/product-detail/product-options-pricing.d.ts +38 -1
- package/dist/components/product-detail/product-options-pricing.d.ts.map +1 -1
- package/dist/components/product-detail/product-options-pricing.js +136 -46
- package/dist/components/product-detail/product-options-shared.d.ts +14 -0
- package/dist/components/product-detail/product-options-shared.d.ts.map +1 -1
- package/dist/components/product-detail/product-options-shared.js +20 -0
- package/dist/components/product-detail/product-translation-popover.d.ts +4 -1
- package/dist/components/product-detail/product-translation-popover.d.ts.map +1 -1
- package/dist/components/product-detail/product-translation-popover.js +28 -8
- package/dist/components/product-detail/product-unit-dialog.d.ts +3 -1
- package/dist/components/product-detail/product-unit-dialog.d.ts.map +1 -1
- package/dist/components/product-detail/product-unit-dialog.js +2 -2
- package/dist/components/product-detail/product-unit-form.d.ts +9 -1
- package/dist/components/product-detail/product-unit-form.d.ts.map +1 -1
- package/dist/components/product-detail/product-unit-form.js +37 -7
- package/dist/components/product-detail/product-unit-price-rule-dialog.d.ts +2 -1
- package/dist/components/product-detail/product-unit-price-rule-dialog.d.ts.map +1 -1
- package/dist/components/product-detail/product-unit-price-rule-dialog.js +2 -2
- package/dist/components/product-detail/product-unit-price-rule-form.d.ts +2 -1
- package/dist/components/product-detail/product-unit-price-rule-form.d.ts.map +1 -1
- package/dist/components/product-detail/product-unit-price-rule-form.js +28 -9
- package/dist/components/product-options-section.d.ts.map +1 -1
- package/dist/components/product-options-section.js +31 -20
- package/package.json +29 -29
- package/dist/components/product-detail/product-extras-section.d.ts +0 -4
- package/dist/components/product-detail/product-extras-section.d.ts.map +0 -1
- 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":"AA0BA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,WAAW,CAAA;AAEzD,KAAK,mBAAmB,GAAG,UAAU,CAAC,OAAO,wBAAwB,CAAC,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CAAA;AAE1F,MAAM,MAAM,iBAAiB,
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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,
|
|
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":"
|
|
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
|
-
|
|
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,
|
|
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":"
|
|
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
|
-
|
|
41
|
-
|
|
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:
|
|
55
|
-
cost:
|
|
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
|
|
107
|
-
costAmountCents: Math.round(values.cost
|
|
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(
|
|
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,
|
|
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 })) :
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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 ? (
|
|
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);
|