@open-mercato/core 0.4.5-develop-3ce83a8b24 → 0.4.5-develop-539cff4960

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 (184) hide show
  1. package/dist/generated/entities/catalog_product/index.js +16 -0
  2. package/dist/generated/entities/catalog_product/index.js.map +2 -2
  3. package/dist/generated/entities/catalog_product_unit_conversion/index.js +27 -0
  4. package/dist/generated/entities/catalog_product_unit_conversion/index.js.map +7 -0
  5. package/dist/generated/entities/sales_credit_memo_line/index.js +7 -1
  6. package/dist/generated/entities/sales_credit_memo_line/index.js.map +2 -2
  7. package/dist/generated/entities/sales_invoice_line/index.js +7 -1
  8. package/dist/generated/entities/sales_invoice_line/index.js.map +2 -2
  9. package/dist/generated/entities/sales_order_line/index.js +6 -0
  10. package/dist/generated/entities/sales_order_line/index.js.map +2 -2
  11. package/dist/generated/entities/sales_quote_line/index.js +6 -0
  12. package/dist/generated/entities/sales_quote_line/index.js.map +2 -2
  13. package/dist/generated/entities.ids.generated.js +1 -0
  14. package/dist/generated/entities.ids.generated.js.map +2 -2
  15. package/dist/generated/entity-fields-registry.js +2 -0
  16. package/dist/generated/entity-fields-registry.js.map +2 -2
  17. package/dist/modules/catalog/api/prices/route.js +123 -8
  18. package/dist/modules/catalog/api/prices/route.js.map +2 -2
  19. package/dist/modules/catalog/api/product-unit-conversions/route.js +194 -0
  20. package/dist/modules/catalog/api/product-unit-conversions/route.js.map +7 -0
  21. package/dist/modules/catalog/api/products/route.js +351 -201
  22. package/dist/modules/catalog/api/products/route.js.map +2 -2
  23. package/dist/modules/catalog/backend/catalog/products/[id]/page.js +1267 -497
  24. package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
  25. package/dist/modules/catalog/backend/catalog/products/create/page.js +733 -210
  26. package/dist/modules/catalog/backend/catalog/products/create/page.js.map +2 -2
  27. package/dist/modules/catalog/commands/index.js +1 -0
  28. package/dist/modules/catalog/commands/index.js.map +2 -2
  29. package/dist/modules/catalog/commands/productUnitConversions.js +503 -0
  30. package/dist/modules/catalog/commands/productUnitConversions.js.map +7 -0
  31. package/dist/modules/catalog/commands/products.js +355 -73
  32. package/dist/modules/catalog/commands/products.js.map +2 -2
  33. package/dist/modules/catalog/commands/shared.js +18 -4
  34. package/dist/modules/catalog/commands/shared.js.map +2 -2
  35. package/dist/modules/catalog/components/products/ProductUomSection.js +591 -0
  36. package/dist/modules/catalog/components/products/ProductUomSection.js.map +7 -0
  37. package/dist/modules/catalog/components/products/productForm.js +66 -5
  38. package/dist/modules/catalog/components/products/productForm.js.map +2 -2
  39. package/dist/modules/catalog/components/products/productFormUtils.js +68 -0
  40. package/dist/modules/catalog/components/products/productFormUtils.js.map +7 -0
  41. package/dist/modules/catalog/data/entities.js +86 -0
  42. package/dist/modules/catalog/data/entities.js.map +2 -2
  43. package/dist/modules/catalog/data/validators.js +65 -3
  44. package/dist/modules/catalog/data/validators.js.map +2 -2
  45. package/dist/modules/catalog/events.js +3 -0
  46. package/dist/modules/catalog/events.js.map +2 -2
  47. package/dist/modules/catalog/lib/unitCodes.js +7 -0
  48. package/dist/modules/catalog/lib/unitCodes.js.map +7 -0
  49. package/dist/modules/catalog/lib/unitResolution.js +53 -0
  50. package/dist/modules/catalog/lib/unitResolution.js.map +7 -0
  51. package/dist/modules/catalog/migrations/Migration20260218225422.js +19 -0
  52. package/dist/modules/catalog/migrations/Migration20260218225422.js.map +7 -0
  53. package/dist/modules/catalog/migrations/Migration20260219084500.js +27 -0
  54. package/dist/modules/catalog/migrations/Migration20260219084500.js.map +7 -0
  55. package/dist/modules/catalog/search.js +69 -1
  56. package/dist/modules/catalog/search.js.map +2 -2
  57. package/dist/modules/catalog/seed/examples.js +91 -42
  58. package/dist/modules/catalog/seed/examples.js.map +2 -2
  59. package/dist/modules/dashboards/seed/analytics.js +3 -0
  60. package/dist/modules/dashboards/seed/analytics.js.map +2 -2
  61. package/dist/modules/sales/api/order-lines/route.js +98 -15
  62. package/dist/modules/sales/api/order-lines/route.js.map +2 -2
  63. package/dist/modules/sales/api/quote-lines/route.js +101 -14
  64. package/dist/modules/sales/api/quote-lines/route.js.map +2 -2
  65. package/dist/modules/sales/api/quotes/public/[token]/route.js +87 -12
  66. package/dist/modules/sales/api/quotes/public/[token]/route.js.map +2 -2
  67. package/dist/modules/sales/commands/documents.js +1424 -260
  68. package/dist/modules/sales/commands/documents.js.map +3 -3
  69. package/dist/modules/sales/commands/shared.js +6 -2
  70. package/dist/modules/sales/commands/shared.js.map +2 -2
  71. package/dist/modules/sales/components/documents/ItemsSection.js +216 -86
  72. package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
  73. package/dist/modules/sales/components/documents/LineItemDialog.js +913 -241
  74. package/dist/modules/sales/components/documents/LineItemDialog.js.map +3 -3
  75. package/dist/modules/sales/components/documents/ShipmentsSection.js +15 -3
  76. package/dist/modules/sales/components/documents/ShipmentsSection.js.map +2 -2
  77. package/dist/modules/sales/data/entities.js +59 -3
  78. package/dist/modules/sales/data/entities.js.map +2 -2
  79. package/dist/modules/sales/data/validators.js +35 -0
  80. package/dist/modules/sales/data/validators.js.map +2 -2
  81. package/dist/modules/sales/frontend/quote/[token]/page.js +15 -1
  82. package/dist/modules/sales/frontend/quote/[token]/page.js.map +2 -2
  83. package/dist/modules/sales/migrations/Migration20260218225423.js +31 -0
  84. package/dist/modules/sales/migrations/Migration20260218225423.js.map +7 -0
  85. package/dist/modules/sales/migrations/Migration20260219084501.js +71 -0
  86. package/dist/modules/sales/migrations/Migration20260219084501.js.map +7 -0
  87. package/dist/modules/sales/search.js +28 -0
  88. package/dist/modules/sales/search.js.map +2 -2
  89. package/dist/modules/sales/seed/examples.js +14 -1
  90. package/dist/modules/sales/seed/examples.js.map +2 -2
  91. package/dist/modules/sales/widgets/injection/document-history/widget.client.js +1 -1
  92. package/dist/modules/sales/widgets/injection/document-history/widget.client.js.map +2 -2
  93. package/dist/modules/staff/backend/staff/team-members/[id]/page.js +28 -15
  94. package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
  95. package/dist/modules/staff/translations.js +9 -0
  96. package/dist/modules/staff/translations.js.map +7 -0
  97. package/dist/modules/translations/components/TranslationDrawerAction.js +97 -0
  98. package/dist/modules/translations/components/TranslationDrawerAction.js.map +7 -0
  99. package/dist/modules/translations/lib/extract-record-id.js +31 -2
  100. package/dist/modules/translations/lib/extract-record-id.js.map +2 -2
  101. package/dist/modules/translations/lib/resolve-field-list.js +3 -0
  102. package/dist/modules/translations/lib/resolve-field-list.js.map +2 -2
  103. package/dist/modules/translations/widgets/injection/translation-manager/widget.client.js +105 -36
  104. package/dist/modules/translations/widgets/injection/translation-manager/widget.client.js.map +2 -2
  105. package/dist/modules/translations/widgets/injection-table.js +18 -29
  106. package/dist/modules/translations/widgets/injection-table.js.map +2 -2
  107. package/generated/entities/catalog_product/index.ts +8 -0
  108. package/generated/entities/catalog_product_unit_conversion/index.ts +12 -0
  109. package/generated/entities/sales_credit_memo_line/index.ts +3 -0
  110. package/generated/entities/sales_invoice_line/index.ts +3 -0
  111. package/generated/entities/sales_order_line/index.ts +3 -0
  112. package/generated/entities/sales_quote_line/index.ts +3 -0
  113. package/generated/entities.ids.generated.ts +1 -0
  114. package/generated/entity-fields-registry.ts +2 -0
  115. package/package.json +2 -2
  116. package/src/modules/auth/i18n/de.json +1 -1
  117. package/src/modules/auth/i18n/en.json +1 -1
  118. package/src/modules/auth/i18n/es.json +1 -1
  119. package/src/modules/auth/i18n/pl.json +1 -1
  120. package/src/modules/catalog/api/prices/route.ts +213 -81
  121. package/src/modules/catalog/api/product-unit-conversions/route.ts +195 -0
  122. package/src/modules/catalog/api/products/route.ts +638 -402
  123. package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +2085 -1072
  124. package/src/modules/catalog/backend/catalog/products/create/page.tsx +1288 -593
  125. package/src/modules/catalog/commands/index.ts +1 -0
  126. package/src/modules/catalog/commands/productUnitConversions.ts +626 -0
  127. package/src/modules/catalog/commands/products.ts +1151 -693
  128. package/src/modules/catalog/commands/shared.ts +19 -5
  129. package/src/modules/catalog/components/products/ProductUomSection.tsx +745 -0
  130. package/src/modules/catalog/components/products/productForm.ts +369 -256
  131. package/src/modules/catalog/components/products/productFormUtils.ts +82 -0
  132. package/src/modules/catalog/data/entities.ts +82 -1
  133. package/src/modules/catalog/data/validators.ts +118 -34
  134. package/src/modules/catalog/events.ts +3 -0
  135. package/src/modules/catalog/i18n/de.json +56 -0
  136. package/src/modules/catalog/i18n/en.json +56 -0
  137. package/src/modules/catalog/i18n/es.json +56 -0
  138. package/src/modules/catalog/i18n/pl.json +56 -0
  139. package/src/modules/catalog/lib/unitCodes.ts +1 -0
  140. package/src/modules/catalog/lib/unitResolution.ts +62 -0
  141. package/src/modules/catalog/migrations/.snapshot-open-mercato.json +245 -0
  142. package/src/modules/catalog/migrations/Migration20260218225422.ts +21 -0
  143. package/src/modules/catalog/migrations/Migration20260219084500.ts +26 -0
  144. package/src/modules/catalog/search.ts +73 -1
  145. package/src/modules/catalog/seed/examples.ts +552 -479
  146. package/src/modules/dashboards/i18n/de.json +1 -1
  147. package/src/modules/dashboards/i18n/en.json +1 -1
  148. package/src/modules/dashboards/i18n/es.json +1 -1
  149. package/src/modules/dashboards/i18n/pl.json +1 -1
  150. package/src/modules/dashboards/seed/analytics.ts +3 -0
  151. package/src/modules/sales/api/order-lines/route.ts +158 -68
  152. package/src/modules/sales/api/quote-lines/route.ts +161 -67
  153. package/src/modules/sales/api/quotes/public/[token]/route.ts +122 -36
  154. package/src/modules/sales/commands/documents.ts +4250 -2424
  155. package/src/modules/sales/commands/shared.ts +7 -2
  156. package/src/modules/sales/components/documents/ItemsSection.tsx +580 -310
  157. package/src/modules/sales/components/documents/LineItemDialog.tsx +1988 -833
  158. package/src/modules/sales/components/documents/ShipmentsSection.tsx +17 -3
  159. package/src/modules/sales/components/documents/lineItemTypes.ts +6 -0
  160. package/src/modules/sales/data/entities.ts +53 -0
  161. package/src/modules/sales/data/validators.ts +36 -0
  162. package/src/modules/sales/frontend/quote/[token]/page.tsx +25 -1
  163. package/src/modules/sales/i18n/de.json +23 -3
  164. package/src/modules/sales/i18n/en.json +23 -3
  165. package/src/modules/sales/i18n/es.json +23 -3
  166. package/src/modules/sales/i18n/pl.json +23 -3
  167. package/src/modules/sales/lib/types.ts +30 -0
  168. package/src/modules/sales/migrations/.snapshot-open-mercato.json +172 -0
  169. package/src/modules/sales/migrations/Migration20260218225423.ts +37 -0
  170. package/src/modules/sales/migrations/Migration20260219084501.ts +73 -0
  171. package/src/modules/sales/search.ts +28 -0
  172. package/src/modules/sales/seed/examples.ts +20 -1
  173. package/src/modules/sales/widgets/injection/document-history/widget.client.tsx +1 -1
  174. package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +8 -0
  175. package/src/modules/staff/translations.ts +5 -0
  176. package/src/modules/translations/components/TranslationDrawerAction.tsx +107 -0
  177. package/src/modules/translations/lib/extract-record-id.ts +47 -3
  178. package/src/modules/translations/lib/resolve-field-list.ts +4 -0
  179. package/src/modules/translations/widgets/injection/translation-manager/widget.client.tsx +108 -36
  180. package/src/modules/translations/widgets/injection-table.ts +19 -33
  181. package/src/modules/workflows/i18n/de.json +4 -4
  182. package/src/modules/workflows/i18n/en.json +4 -4
  183. package/src/modules/workflows/i18n/es.json +4 -4
  184. package/src/modules/workflows/i18n/pl.json +4 -4
@@ -5,8 +5,14 @@ import Link from "next/link";
5
5
  import dynamic from "next/dynamic";
6
6
  import { useRouter } from "next/navigation";
7
7
  import { Page, PageBody } from "@open-mercato/ui/backend/Page";
8
- import { CrudForm } from "@open-mercato/ui/backend/CrudForm";
9
- import { updateCrud, createCrud, deleteCrud } from "@open-mercato/ui/backend/utils/crud";
8
+ import {
9
+ CrudForm
10
+ } from "@open-mercato/ui/backend/CrudForm";
11
+ import {
12
+ updateCrud,
13
+ createCrud,
14
+ deleteCrud
15
+ } from "@open-mercato/ui/backend/utils/crud";
10
16
  import { createCrudFormError } from "@open-mercato/ui/backend/utils/serverErrors";
11
17
  import { collectCustomFieldValues } from "@open-mercato/ui/backend/utils/customFieldValues";
12
18
  import { flash } from "@open-mercato/ui/backend/FlashMessages";
@@ -16,11 +22,17 @@ import { Label } from "@open-mercato/ui/primitives/label";
16
22
  import { TagsInput } from "@open-mercato/ui/backend/inputs/TagsInput";
17
23
  import { Textarea } from "@open-mercato/ui/primitives/textarea";
18
24
  import { DataLoader } from "@open-mercato/ui/primitives/DataLoader";
19
- import { apiCall, readApiResultOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
25
+ import { Spinner } from "@open-mercato/ui/primitives/spinner";
26
+ import {
27
+ apiCall,
28
+ readApiResultOrThrow
29
+ } from "@open-mercato/ui/backend/utils/apiCall";
20
30
  import { useT } from "@open-mercato/shared/lib/i18n/context";
21
31
  import { useConfirmDialog } from "@open-mercato/ui/backend/confirm-dialog";
22
32
  import { E } from "../../../../../../generated/entities.ids.generated.js";
23
- import { ProductMediaManager } from "@open-mercato/core/modules/catalog/components/products/ProductMediaManager";
33
+ import {
34
+ ProductMediaManager
35
+ } from "@open-mercato/core/modules/catalog/components/products/ProductMediaManager";
24
36
  import {
25
37
  fetchOptionSchemaTemplate
26
38
  } from "../optionSchemaClient.js";
@@ -43,17 +55,105 @@ import {
43
55
  updateWeightValue
44
56
  } from "@open-mercato/core/modules/catalog/components/products/productForm";
45
57
  import { MetadataEditor } from "@open-mercato/core/modules/catalog/components/products/MetadataEditor";
46
- import { buildAttachmentImageUrl, slugifyAttachmentFileName } from "@open-mercato/core/modules/attachments/lib/imageUrls";
58
+ import {
59
+ buildAttachmentImageUrl,
60
+ slugifyAttachmentFileName
61
+ } from "@open-mercato/core/modules/attachments/lib/imageUrls";
47
62
  import {
48
63
  ProductCategorizeSection
49
64
  } from "@open-mercato/core/modules/catalog/components/products/ProductCategorizeSection";
50
- import { AlignLeft, BookMarked, FileText, Layers, Plus, Save, Trash2 } from "lucide-react";
51
- import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@open-mercato/ui/primitives/dialog";
65
+ import { ProductUomSection } from "@open-mercato/core/modules/catalog/components/products/ProductUomSection";
66
+ import { canonicalizeUnitCode } from "@open-mercato/core/modules/catalog/lib/unitCodes";
67
+ import {
68
+ UNIT_PRICE_REFERENCE_UNITS,
69
+ toTrimmedOrNull,
70
+ toPositiveNumberOrNull,
71
+ toIntegerInRangeOrDefault,
72
+ normalizeProductConversionInputs
73
+ } from "@open-mercato/core/modules/catalog/components/products/productFormUtils";
74
+ import {
75
+ AlignLeft,
76
+ BookMarked,
77
+ FileText,
78
+ Layers,
79
+ Plus,
80
+ Save,
81
+ Trash2
82
+ } from "lucide-react";
83
+ import {
84
+ Dialog,
85
+ DialogContent,
86
+ DialogFooter,
87
+ DialogHeader,
88
+ DialogTitle
89
+ } from "@open-mercato/ui/primitives/dialog";
52
90
  const MarkdownEditor = dynamic(() => import("@uiw/react-md-editor"), {
53
91
  ssr: false,
54
- loading: () => /* @__PURE__ */ jsx("div", { className: "flex h-48 items-center justify-center text-sm text-muted-foreground", children: "Loading editor\u2026" })
92
+ loading: () => /* @__PURE__ */ jsx("div", { className: "flex h-48 items-center justify-center text-sm text-muted-foreground", children: /* @__PURE__ */ jsx(Spinner, {}) })
55
93
  });
56
- function EditCatalogProductPage({ params }) {
94
+ function mapVariantPriceSummary(item) {
95
+ if (!item) return null;
96
+ const getString2 = (value) => {
97
+ if (typeof value === "string" && value.trim().length) return value.trim();
98
+ if (typeof value === "number" || typeof value === "bigint")
99
+ return String(value);
100
+ return null;
101
+ };
102
+ const variantId = getString2(item.variant_id ?? item.variantId);
103
+ const id = getString2(item.id);
104
+ if (!variantId || !id) return null;
105
+ const priceKindId = getString2(item.price_kind_id ?? item.priceKindId);
106
+ const currencyCode = getString2(item.currency_code ?? item.currencyCode);
107
+ const unitGross = getString2(item.unit_price_gross ?? item.unitPriceGross);
108
+ const unitNet = getString2(item.unit_price_net ?? item.unitPriceNet);
109
+ const amount = unitGross ?? unitNet ?? null;
110
+ return {
111
+ id,
112
+ variantId,
113
+ priceKindId,
114
+ currencyCode,
115
+ amount,
116
+ displayMode: unitGross ? "including-tax" : "excluding-tax"
117
+ };
118
+ }
119
+ function normalizeVariantOptionValues(input) {
120
+ if (!input || typeof input !== "object") return null;
121
+ const result = {};
122
+ Object.entries(input).forEach(([key, value]) => {
123
+ if (typeof key === "string" && typeof value === "string" && key.trim().length) {
124
+ result[key] = value;
125
+ }
126
+ });
127
+ return Object.keys(result).length ? result : null;
128
+ }
129
+ function readProductConversionRows(items) {
130
+ const rows = Array.isArray(items) ? items : [];
131
+ return rows.map((item) => {
132
+ const id = toTrimmedOrNull(item.id);
133
+ const unitCode = canonicalizeUnitCode(item.unit_code ?? item.unitCode);
134
+ const factor = toPositiveNumberOrNull(
135
+ item.to_base_factor ?? item.toBaseFactor
136
+ );
137
+ if (!unitCode || factor === null) return null;
138
+ const sortOrderRaw = toIntegerInRangeOrDefault(
139
+ item.sort_order ?? item.sortOrder,
140
+ 0,
141
+ 1e5,
142
+ 0
143
+ );
144
+ const isActive = typeof item.is_active === "boolean" ? item.is_active : typeof item.isActive === "boolean" ? item.isActive : true;
145
+ return {
146
+ id: id ?? null,
147
+ unitCode,
148
+ toBaseFactor: String(factor),
149
+ sortOrder: String(sortOrderRaw),
150
+ isActive
151
+ };
152
+ }).filter((entry) => Boolean(entry));
153
+ }
154
+ function EditCatalogProductPage({
155
+ params
156
+ }) {
57
157
  const productId = params?.id ? String(params.id) : null;
58
158
  const t = useT();
59
159
  const router = useRouter();
@@ -64,22 +164,108 @@ function EditCatalogProductPage({ params }) {
64
164
  const [loading, setLoading] = React.useState(true);
65
165
  const [error, setError] = React.useState(null);
66
166
  const offerSnapshotsRef = React.useRef([]);
167
+ const initialConversionsRef = React.useRef([]);
67
168
  const [categorizeOptions, setCategorizeOptions] = React.useState({ categories: [], channels: [], tags: [] });
169
+ const loadVariants = React.useCallback(async (id) => {
170
+ try {
171
+ const [variantsRes, pricesRes] = await Promise.all([
172
+ apiCall(
173
+ `/api/catalog/variants?productId=${encodeURIComponent(id)}&page=1&pageSize=100`
174
+ ),
175
+ apiCall(
176
+ `/api/catalog/prices?productId=${encodeURIComponent(id)}&page=1&pageSize=100`
177
+ )
178
+ ]);
179
+ if (!variantsRes.ok) {
180
+ setVariants([]);
181
+ return;
182
+ }
183
+ const priceMap = {};
184
+ if (pricesRes.ok) {
185
+ const priceItems = Array.isArray(pricesRes.result?.items) ? pricesRes.result?.items : [];
186
+ for (const item of priceItems) {
187
+ const summary = mapVariantPriceSummary(item);
188
+ if (!summary) continue;
189
+ if (!priceMap[summary.variantId]) {
190
+ priceMap[summary.variantId] = [];
191
+ }
192
+ priceMap[summary.variantId].push(summary);
193
+ }
194
+ Object.keys(priceMap).forEach((key) => {
195
+ priceMap[key].sort((left, right) => {
196
+ const leftKey = (left.priceKindId ?? "") + (left.currencyCode ?? "");
197
+ const rightKey = (right.priceKindId ?? "") + (right.currencyCode ?? "");
198
+ return leftKey.localeCompare(rightKey);
199
+ });
200
+ });
201
+ }
202
+ const items = Array.isArray(variantsRes.result?.items) ? variantsRes.result?.items : [];
203
+ setVariants(
204
+ items.map((variant) => {
205
+ const variantId = typeof variant.id === "string" ? variant.id : null;
206
+ if (!variantId) return null;
207
+ const variantRecord = variant;
208
+ const optionValues = normalizeVariantOptionValues(variantRecord?.["option_values"]) ?? normalizeVariantOptionValues(variantRecord?.optionValues);
209
+ return {
210
+ id: variantId,
211
+ name: typeof variant.name === "string" && variant.name.trim().length ? variant.name : variant.sku ?? variantId,
212
+ sku: typeof variant.sku === "string" ? variant.sku : "",
213
+ isDefault: Boolean(variant.is_default ?? variant.isDefault),
214
+ prices: priceMap[variantId] ?? [],
215
+ optionValues
216
+ };
217
+ }).filter((entry) => Boolean(entry))
218
+ );
219
+ } catch (err) {
220
+ console.error("catalog.variants.fetch failed", err);
221
+ setVariants([]);
222
+ }
223
+ }, []);
224
+ const refreshVariants = React.useCallback(async () => {
225
+ if (!productId) return;
226
+ await loadVariants(productId);
227
+ }, [loadVariants, productId]);
228
+ const fetchAttachments = React.useCallback(
229
+ async (id) => {
230
+ try {
231
+ const res = await apiCall(
232
+ `/api/attachments?entityId=${encodeURIComponent(E.catalog.catalog_product)}&recordId=${encodeURIComponent(id)}`
233
+ );
234
+ if (!res.ok) return [];
235
+ return (res.result?.items ?? []).map((item) => ({
236
+ id: item.id,
237
+ url: item.url,
238
+ fileName: item.fileName,
239
+ fileSize: item.fileSize,
240
+ thumbnailUrl: item.thumbnailUrl ?? void 0
241
+ }));
242
+ } catch (err) {
243
+ console.error("attachments.fetch failed", err);
244
+ return [];
245
+ }
246
+ },
247
+ []
248
+ );
68
249
  React.useEffect(() => {
69
250
  const loadTaxRates = async () => {
70
251
  try {
71
- const payload = await readApiResultOrThrow(
72
- "/api/sales/tax-rates?pageSize=200",
73
- void 0,
74
- { errorMessage: t("catalog.products.create.taxRates.error", "Failed to load tax rates."), fallback: { items: [] } }
75
- );
252
+ const payload = await readApiResultOrThrow("/api/sales/tax-rates?pageSize=100", void 0, {
253
+ errorMessage: t(
254
+ "catalog.products.create.taxRates.error",
255
+ "Failed to load tax rates."
256
+ ),
257
+ fallback: { items: [] }
258
+ });
76
259
  const items = Array.isArray(payload.items) ? payload.items : [];
77
260
  setTaxRates(
78
261
  items.map((item) => {
79
262
  const rawRate = typeof item.rate === "number" ? item.rate : Number(item.rate ?? Number.NaN);
80
263
  return {
81
264
  id: String(item.id),
82
- name: typeof item.name === "string" && item.name.trim().length ? item.name : t("catalog.products.create.taxRates.unnamed", "Untitled tax rate"),
265
+ name: typeof item.name === "string" && item.name.trim().length ? item.name : t(
266
+ "catalog.products.create.taxRates.unnamed",
267
+ "Untitled tax rate"
268
+ ),
83
269
  code: typeof item.code === "string" && item.code.trim().length ? item.code : null,
84
270
  rate: Number.isFinite(rawRate) ? rawRate : null,
85
271
  isDefault: Boolean(
@@ -99,7 +285,12 @@ function EditCatalogProductPage({ params }) {
99
285
  React.useEffect(() => {
100
286
  if (!productId) {
101
287
  setLoading(false);
102
- setError(t("catalog.products.edit.errors.idMissing", "Product identifier is missing."));
288
+ setError(
289
+ t(
290
+ "catalog.products.edit.errors.idMissing",
291
+ "Product identifier is missing."
292
+ )
293
+ );
103
294
  return;
104
295
  }
105
296
  let cancelled = false;
@@ -112,25 +303,40 @@ function EditCatalogProductPage({ params }) {
112
303
  );
113
304
  if (!productRes.ok) throw new Error("load_failed");
114
305
  const record = Array.isArray(productRes.result?.items) ? productRes.result?.items?.[0] : void 0;
115
- if (!record) throw new Error(t("catalog.products.edit.errors.notFound", "Product not found."));
116
- const rawMetadata = isRecord(record.metadata) ? record.metadata : isRecord(record.metadata) ? record.metadata : null;
306
+ if (!record)
307
+ throw new Error(
308
+ t("catalog.products.edit.errors.notFound", "Product not found.")
309
+ );
310
+ const rawMetadata = isRecord(record.metadata) ? record.metadata : null;
117
311
  const dimensions = normalizeProductDimensions(
118
312
  record.dimensions ?? rawMetadata?.dimensions ?? null
119
313
  );
314
+ const rawWeightMeta = isRecord(rawMetadata?.weight) ? rawMetadata.weight : null;
120
315
  const weight = normalizeProductWeight({
121
- value: record.weight_value ?? record.weightValue ?? (isRecord(rawMetadata?.weight) ? rawMetadata.weight.value : void 0),
122
- unit: record.weight_unit ?? record.weightUnit ?? (isRecord(rawMetadata?.weight) ? rawMetadata.weight.unit : void 0)
316
+ value: record.weight_value ?? record.weightValue ?? rawWeightMeta?.value,
317
+ unit: record.weight_unit ?? record.weightUnit ?? rawWeightMeta?.unit
123
318
  });
124
319
  const metadata = normalizeMetadata(rawMetadata);
125
320
  const optionSchemaId = typeof record.option_schema_id === "string" ? record.option_schema_id : typeof record.optionSchemaId === "string" ? record.optionSchemaId : null;
126
321
  const taxRateId = typeof record.tax_rate_id === "string" ? record.tax_rate_id : typeof record.taxRateId === "string" ? record.taxRateId : null;
127
322
  const optionSchemaTemplate = optionSchemaId ? await fetchOptionSchemaTemplate(optionSchemaId) : null;
128
- const normalizedSchema = normalizeOptionSchemaRecord(optionSchemaTemplate?.schema);
323
+ const normalizedSchema = normalizeOptionSchemaRecord(
324
+ optionSchemaTemplate?.schema
325
+ );
129
326
  let optionInputs = normalizedSchema ? convertSchemaToProductOptions(normalizedSchema) : [];
130
327
  if (!optionInputs.length) {
131
328
  optionInputs = readOptionSchema(metadata);
132
329
  }
133
- const attachments = await fetchAttachments(productId);
330
+ const [attachments, conversionsRes] = await Promise.all([
331
+ fetchAttachments(productId),
332
+ apiCall(
333
+ `/api/catalog/product-unit-conversions?productId=${encodeURIComponent(productId)}&page=1&pageSize=100`,
334
+ void 0,
335
+ { fallback: { items: [] } }
336
+ )
337
+ ]);
338
+ const conversionRows = conversionsRes.ok ? readProductConversionRows(conversionsRes.result?.items) : [];
339
+ initialConversionsRef.current = conversionRows;
134
340
  const { customValues } = extractCustomFields(record);
135
341
  const offers = readOfferSnapshots(record);
136
342
  offerSnapshotsRef.current = offers;
@@ -138,8 +344,18 @@ function EditCatalogProductPage({ params }) {
138
344
  const channelOptionEntries = buildChannelOptions(offers);
139
345
  const { ids: categoryIds, options: categoryOptions } = readCategorySelections(record);
140
346
  const { values: tagValues, options: tagOptions } = readTagSelections(record);
141
- const defaultMediaId = typeof record.default_media_id === "string" ? record.default_media_id : record.defaultMediaId ?? null;
142
- const defaultMediaUrl = typeof record.default_media_url === "string" ? record.default_media_url : record.defaultMediaUrl ?? "";
347
+ const defaultMediaId = typeof record.default_media_id === "string" ? record.default_media_id : typeof record.defaultMediaId === "string" ? record.defaultMediaId : null;
348
+ const defaultMediaUrl = typeof record.default_media_url === "string" ? record.default_media_url : typeof record.defaultMediaUrl === "string" ? record.defaultMediaUrl : "";
349
+ const {
350
+ defaultUnit,
351
+ defaultSalesUnit,
352
+ defaultSalesUnitQuantity,
353
+ roundingScale,
354
+ roundingMode,
355
+ unitPriceEnabled,
356
+ unitPriceReferenceUnit,
357
+ unitPriceBaseQuantity
358
+ } = extractUomFields(record);
143
359
  const initial = {
144
360
  title: typeof record.title === "string" ? record.title : "",
145
361
  subtitle: typeof record.subtitle === "string" ? record.subtitle : "",
@@ -158,6 +374,17 @@ function EditCatalogProductPage({ params }) {
158
374
  metadata,
159
375
  dimensions: sanitizeProductDimensions(dimensions),
160
376
  weight: sanitizeProductWeight(weight),
377
+ defaultUnit: defaultUnit ?? null,
378
+ defaultSalesUnit: defaultSalesUnit ?? defaultUnit ?? null,
379
+ defaultSalesUnitQuantity: String(defaultSalesUnitQuantity ?? 1),
380
+ uomRoundingScale: String(roundingScale),
381
+ uomRoundingMode: roundingMode,
382
+ unitPriceEnabled,
383
+ unitPriceReferenceUnit: unitPriceReferenceUnit && UNIT_PRICE_REFERENCE_UNITS.has(
384
+ unitPriceReferenceUnit
385
+ ) ? unitPriceReferenceUnit : null,
386
+ unitPriceBaseQuantity: unitPriceBaseQuantity === null ? "" : String(unitPriceBaseQuantity),
387
+ unitConversions: conversionRows,
161
388
  customFieldsetCode: typeof record.custom_fieldset_code === "string" ? record.custom_fieldset_code : typeof record.customFieldsetCode === "string" ? record.customFieldsetCode : null,
162
389
  categoryIds,
163
390
  channelIds,
@@ -175,7 +402,10 @@ function EditCatalogProductPage({ params }) {
175
402
  } catch (err) {
176
403
  console.error("catalog.products.edit.load failed", err);
177
404
  if (!cancelled) {
178
- const message = err instanceof Error && err.message ? err.message : t("catalog.products.edit.errors.load", "Failed to load product details.");
405
+ const message = err instanceof Error && err.message ? err.message : t(
406
+ "catalog.products.edit.errors.load",
407
+ "Failed to load product details."
408
+ );
179
409
  setError(message);
180
410
  }
181
411
  } finally {
@@ -186,16 +416,14 @@ function EditCatalogProductPage({ params }) {
186
416
  return () => {
187
417
  cancelled = true;
188
418
  };
189
- }, [productId, t]);
419
+ }, [fetchAttachments, loadVariants, productId, t]);
190
420
  React.useEffect(() => {
191
421
  let cancelled = false;
192
422
  async function loadPriceKinds() {
193
423
  try {
194
- const payload = await readApiResultOrThrow(
195
- "/api/catalog/price-kinds?pageSize=100",
196
- void 0,
197
- { fallback: { items: [] } }
198
- );
424
+ const payload = await readApiResultOrThrow("/api/catalog/price-kinds?pageSize=100", void 0, {
425
+ fallback: { items: [] }
426
+ });
199
427
  const items = Array.isArray(payload.items) ? payload.items : [];
200
428
  const summaries = items.map((item) => normalizePriceKindSummary(item)).filter((entry) => !!entry);
201
429
  if (!cancelled) {
@@ -214,323 +442,463 @@ function EditCatalogProductPage({ params }) {
214
442
  cancelled = true;
215
443
  };
216
444
  }, []);
217
- const loadVariants = React.useCallback(async (id) => {
218
- try {
219
- const [variantsRes, pricesRes] = await Promise.all([
220
- apiCall(`/api/catalog/variants?productId=${encodeURIComponent(id)}&page=1&pageSize=100`),
221
- apiCall(`/api/catalog/prices?productId=${encodeURIComponent(id)}&page=1&pageSize=100`)
222
- ]);
223
- if (!variantsRes.ok) {
224
- setVariants([]);
225
- return;
226
- }
227
- const priceMap = {};
228
- if (pricesRes.ok) {
229
- const priceItems = Array.isArray(pricesRes.result?.items) ? pricesRes.result?.items : [];
230
- for (const item of priceItems) {
231
- const summary = mapVariantPriceSummary(item);
232
- if (!summary) continue;
233
- if (!priceMap[summary.variantId]) {
234
- priceMap[summary.variantId] = [];
445
+ const handleVariantDeleted = React.useCallback((variantId) => {
446
+ setVariants((prev) => prev.filter((variant) => variant.id !== variantId));
447
+ }, []);
448
+ const groups = React.useMemo(
449
+ () => [
450
+ {
451
+ id: "details",
452
+ column: 1,
453
+ component: ({ values, setValue, errors }) => /* @__PURE__ */ jsx(
454
+ ProductDetailsSection,
455
+ {
456
+ values,
457
+ setValue,
458
+ errors,
459
+ productId: productId ?? ""
235
460
  }
236
- priceMap[summary.variantId].push(summary);
237
- }
238
- Object.keys(priceMap).forEach((key) => {
239
- priceMap[key].sort((a, b) => {
240
- const left = (a.priceKindId ?? "") + (a.currencyCode ?? "");
241
- const right = (b.priceKindId ?? "") + (b.currencyCode ?? "");
242
- return left.localeCompare(right);
243
- });
461
+ )
462
+ },
463
+ {
464
+ id: "dimensions",
465
+ column: 1,
466
+ component: ({ values, setValue, errors }) => /* @__PURE__ */ jsx(
467
+ ProductDimensionsSection,
468
+ {
469
+ values,
470
+ setValue,
471
+ errors
472
+ }
473
+ )
474
+ },
475
+ {
476
+ id: "metadata",
477
+ column: 1,
478
+ component: ({ values, setValue, errors }) => /* @__PURE__ */ jsx(
479
+ ProductMetadataSection,
480
+ {
481
+ values,
482
+ setValue,
483
+ errors
484
+ }
485
+ )
486
+ },
487
+ {
488
+ id: "options",
489
+ column: 1,
490
+ component: ({ values, setValue, errors }) => /* @__PURE__ */ jsx(
491
+ ProductOptionsSection,
492
+ {
493
+ values,
494
+ setValue,
495
+ errors
496
+ }
497
+ )
498
+ },
499
+ {
500
+ id: "product-uom",
501
+ column: 1,
502
+ bare: true,
503
+ component: ({ values, setValue, errors }) => /* @__PURE__ */ jsx(
504
+ ProductUomSection,
505
+ {
506
+ values,
507
+ setValue,
508
+ errors
509
+ }
510
+ )
511
+ },
512
+ {
513
+ id: "variants",
514
+ column: 1,
515
+ component: ({ values, setValue, errors }) => /* @__PURE__ */ jsx(
516
+ ProductVariantsSection,
517
+ {
518
+ values,
519
+ setValue,
520
+ errors,
521
+ productId: productId ?? "",
522
+ variants,
523
+ priceKinds,
524
+ onVariantDeleted: handleVariantDeleted,
525
+ onVariantsReload: refreshVariants
526
+ }
527
+ )
528
+ },
529
+ {
530
+ id: "meta",
531
+ column: 2,
532
+ title: t("catalog.products.create.meta.title", "Product meta"),
533
+ description: t(
534
+ "catalog.products.create.meta.description",
535
+ "Manage subtitle and handle for storefronts."
536
+ ),
537
+ component: ({ values, setValue, errors }) => /* @__PURE__ */ jsx(
538
+ ProductMetaSection,
539
+ {
540
+ values,
541
+ setValue,
542
+ errors,
543
+ taxRates
544
+ }
545
+ )
546
+ },
547
+ {
548
+ id: "categorize",
549
+ column: 2,
550
+ title: t("catalog.products.create.organize.title", "Categorize"),
551
+ description: t(
552
+ "catalog.products.create.organize.description",
553
+ "Assign categories, sales channels, and tags."
554
+ ),
555
+ component: ({ values, setValue, errors }) => /* @__PURE__ */ jsx(
556
+ ProductCategorizeSection,
557
+ {
558
+ values,
559
+ setValue,
560
+ errors,
561
+ initialCategoryOptions: categorizeOptions.categories,
562
+ initialChannelOptions: categorizeOptions.channels,
563
+ initialTagOptions: categorizeOptions.tags
564
+ }
565
+ )
566
+ },
567
+ {
568
+ id: "custom-fields",
569
+ column: 2,
570
+ title: t("catalog.products.edit.custom.title", "Custom attributes"),
571
+ kind: "customFields"
572
+ }
573
+ ],
574
+ [
575
+ categorizeOptions,
576
+ handleVariantDeleted,
577
+ priceKinds,
578
+ productId,
579
+ refreshVariants,
580
+ t,
581
+ taxRates,
582
+ variants
583
+ ]
584
+ );
585
+ const handleSubmit = React.useCallback(
586
+ async (formValues) => {
587
+ if (!productId) {
588
+ throw createCrudFormError(
589
+ t(
590
+ "catalog.products.edit.errors.idMissing",
591
+ "Product identifier is missing."
592
+ )
593
+ );
594
+ }
595
+ const parsed = productFormSchema.safeParse(formValues);
596
+ if (!parsed.success) {
597
+ const issues = parsed.error.issues;
598
+ const fieldErrors = {};
599
+ issues.forEach((issue) => {
600
+ const path = issue.path.join(".") || "form";
601
+ if (!fieldErrors[path]) fieldErrors[path] = issue.message;
244
602
  });
603
+ const message = issues[0]?.message ?? t(
604
+ "catalog.products.edit.errors.validation",
605
+ "Fix highlighted fields."
606
+ );
607
+ throw createCrudFormError(message, fieldErrors);
245
608
  }
246
- const items = Array.isArray(variantsRes.result?.items) ? variantsRes.result?.items : [];
247
- setVariants(
248
- items.map((variant) => {
249
- const variantId = typeof variant.id === "string" ? variant.id : null;
250
- if (!variantId) return null;
251
- const variantRecord = variant;
252
- const optionValues = normalizeVariantOptionValues(variantRecord?.["option_values"]) ?? normalizeVariantOptionValues(variantRecord?.optionValues);
253
- return {
254
- id: variantId,
255
- name: typeof variant.name === "string" && variant.name.trim().length ? variant.name : variant.sku ?? variantId,
256
- sku: typeof variant.sku === "string" ? variant.sku : "",
257
- isDefault: Boolean(variant.is_default ?? variant.isDefault),
258
- prices: priceMap[variantId] ?? [],
259
- optionValues
260
- };
261
- }).filter((entry) => !!entry)
609
+ const values = {
610
+ ...BASE_INITIAL_VALUES,
611
+ ...parsed.data,
612
+ title: parsed.data.title ?? "",
613
+ subtitle: parsed.data.subtitle ?? "",
614
+ handle: parsed.data.handle ?? "",
615
+ description: parsed.data.description ?? "",
616
+ useMarkdown: parsed.data.useMarkdown ?? false,
617
+ taxRateId: parsed.data.taxRateId ?? null,
618
+ mediaDraftId: parsed.data.mediaDraftId ?? productId,
619
+ mediaItems: Array.isArray(parsed.data.mediaItems) ? parsed.data.mediaItems : [],
620
+ defaultMediaId: parsed.data.defaultMediaId ?? null,
621
+ defaultMediaUrl: parsed.data.defaultMediaUrl ?? "",
622
+ hasVariants: parsed.data.hasVariants ?? false,
623
+ options: Array.isArray(parsed.data.options) ? parsed.data.options : [],
624
+ variants: Array.isArray(parsed.data.variants) ? parsed.data.variants : [],
625
+ metadata: parsed.data.metadata ?? {},
626
+ dimensions: parsed.data.dimensions ?? null,
627
+ weight: parsed.data.weight ?? null,
628
+ defaultUnit: parsed.data.defaultUnit ?? null,
629
+ defaultSalesUnit: parsed.data.defaultSalesUnit ?? null,
630
+ defaultSalesUnitQuantity: parsed.data.defaultSalesUnitQuantity?.toString() ?? "1",
631
+ uomRoundingScale: parsed.data.uomRoundingScale?.toString() ?? "4",
632
+ uomRoundingMode: parsed.data.uomRoundingMode ?? "half_up",
633
+ unitPriceEnabled: Boolean(parsed.data.unitPriceEnabled),
634
+ unitPriceReferenceUnit: parsed.data.unitPriceReferenceUnit ?? null,
635
+ unitPriceBaseQuantity: parsed.data.unitPriceBaseQuantity?.toString() ?? "",
636
+ unitConversions: Array.isArray(parsed.data.unitConversions) ? parsed.data.unitConversions.map((entry) => ({
637
+ id: toTrimmedOrNull(entry.id) ?? null,
638
+ unitCode: toTrimmedOrNull(entry.unitCode) ?? "",
639
+ toBaseFactor: toPositiveNumberOrNull(entry.toBaseFactor) === null ? "" : String(toPositiveNumberOrNull(entry.toBaseFactor)),
640
+ sortOrder: typeof entry.sortOrder === "number" && Number.isFinite(entry.sortOrder) ? String(entry.sortOrder) : "",
641
+ isActive: entry.isActive !== false
642
+ })) : [],
643
+ customFieldsetCode: parsed.data.customFieldsetCode ?? null,
644
+ categoryIds: parsed.data.categoryIds ?? [],
645
+ channelIds: parsed.data.channelIds ?? [],
646
+ tags: parsed.data.tags ?? [],
647
+ optionSchemaId: parsed.data.optionSchemaId ?? null
648
+ };
649
+ const title = values.title?.trim();
650
+ if (!title) {
651
+ const message = t(
652
+ "catalog.products.create.errors.title",
653
+ "Provide a product title."
654
+ );
655
+ throw createCrudFormError(message, { title: message });
656
+ }
657
+ const handle = values.handle?.trim() || void 0;
658
+ const description = values.description?.trim() || void 0;
659
+ const metadata = buildMetadataPayload(values);
660
+ const dimensions = sanitizeProductDimensions(values.dimensions ?? null);
661
+ const weight = sanitizeProductWeight(values.weight ?? null);
662
+ const resolveTaxRateValue = (taxRateId) => {
663
+ if (!taxRateId) return null;
664
+ const match = taxRates.find((rate) => rate.id === taxRateId);
665
+ return typeof match?.rate === "number" && Number.isFinite(match.rate) ? match.rate : null;
666
+ };
667
+ const productTaxRateValue = resolveTaxRateValue(values.taxRateId ?? null);
668
+ const defaultMediaId = typeof values.defaultMediaId === "string" && values.defaultMediaId.trim().length ? values.defaultMediaId : null;
669
+ const defaultMediaEntry = defaultMediaId ? values.mediaItems.find((item) => item.id === defaultMediaId) : null;
670
+ const defaultMediaUrl = defaultMediaEntry ? buildAttachmentImageUrl(defaultMediaEntry.id, {
671
+ slug: slugifyAttachmentFileName(defaultMediaEntry.fileName)
672
+ }) : null;
673
+ const defaultUnit = canonicalizeUnitCode(values.defaultUnit);
674
+ const defaultSalesUnit = canonicalizeUnitCode(values.defaultSalesUnit);
675
+ const defaultSalesUnitQuantity = toPositiveNumberOrNull(values.defaultSalesUnitQuantity) ?? 1;
676
+ const uomRoundingScale = toIntegerInRangeOrDefault(
677
+ values.uomRoundingScale,
678
+ 0,
679
+ 6,
680
+ 4
262
681
  );
263
- } catch (err) {
264
- console.error("catalog.variants.fetch failed", err);
265
- setVariants([]);
266
- }
267
- }, []);
268
- const refreshVariants = React.useCallback(async () => {
269
- if (!productId) return;
270
- await loadVariants(productId);
271
- }, [loadVariants, productId]);
272
- const fetchAttachments = React.useCallback(async (id) => {
273
- try {
274
- const res = await apiCall(
275
- `/api/attachments?entityId=${encodeURIComponent(E.catalog.catalog_product)}&recordId=${encodeURIComponent(id)}`
682
+ const uomRoundingMode = values.uomRoundingMode === "down" || values.uomRoundingMode === "up" ? values.uomRoundingMode : "half_up";
683
+ const unitPriceEnabled = Boolean(values.unitPriceEnabled);
684
+ const unitPriceReferenceUnit = canonicalizeUnitCode(
685
+ values.unitPriceReferenceUnit
276
686
  );
277
- if (!res.ok) return [];
278
- return (res.result?.items ?? []).map((item) => ({
279
- id: item.id,
280
- url: item.url,
281
- fileName: item.fileName,
282
- fileSize: item.fileSize,
283
- thumbnailUrl: item.thumbnailUrl ?? void 0
284
- }));
285
- } catch (err) {
286
- console.error("attachments.fetch failed", err);
287
- return [];
288
- }
289
- }, []);
290
- function mapVariantPriceSummary(item) {
291
- if (!item) return null;
292
- const getString2 = (value) => {
293
- if (typeof value === "string" && value.trim().length) return value.trim();
294
- if (typeof value === "number" || typeof value === "bigint") return String(value);
295
- return null;
296
- };
297
- const variantId = getString2(item.variant_id ?? item.variantId);
298
- const id = getString2(item.id);
299
- if (!variantId || !id) return null;
300
- const priceKindId = getString2(item.price_kind_id ?? item.priceKindId);
301
- const currencyCode = getString2(item.currency_code ?? item.currencyCode);
302
- const unitGross = getString2(item.unit_price_gross ?? item.unitPriceGross);
303
- const unitNet = getString2(item.unit_price_net ?? item.unitPriceNet);
304
- const amount = unitGross ?? unitNet ?? null;
305
- return {
306
- id,
307
- variantId,
308
- priceKindId,
309
- currencyCode,
310
- amount,
311
- displayMode: unitGross ? "including-tax" : "excluding-tax"
312
- };
313
- }
314
- function normalizeVariantOptionValues(input) {
315
- if (!input || typeof input !== "object") return null;
316
- const result = {};
317
- Object.entries(input).forEach(([key, value]) => {
318
- if (typeof key === "string" && typeof value === "string" && key.trim().length) {
319
- result[key] = value;
687
+ const unitPriceBaseQuantity = toPositiveNumberOrNull(
688
+ values.unitPriceBaseQuantity
689
+ );
690
+ if (defaultSalesUnit && !defaultUnit) {
691
+ const message = t(
692
+ "catalog.products.uom.errors.baseRequired",
693
+ "Base unit is required when default sales unit is set."
694
+ );
695
+ throw createCrudFormError(message, { defaultSalesUnit: message });
320
696
  }
321
- });
322
- return Object.keys(result).length ? result : null;
323
- }
324
- const handleVariantDeleted = React.useCallback((variantId) => {
325
- setVariants((prev) => prev.filter((variant) => variant.id !== variantId));
326
- }, []);
327
- const groups = React.useMemo(() => [
328
- {
329
- id: "details",
330
- column: 1,
331
- component: ({ values, setValue, errors }) => /* @__PURE__ */ jsx(
332
- ProductDetailsSection,
333
- {
334
- values,
335
- setValue,
336
- errors,
337
- productId: productId ?? ""
697
+ const conversionInputs = normalizeProductConversionInputs(
698
+ values.unitConversions,
699
+ t(
700
+ "catalog.products.uom.errors.duplicateConversion",
701
+ "Duplicate conversion unit is not allowed."
702
+ )
703
+ );
704
+ if (conversionInputs.length && !defaultUnit) {
705
+ const message = t(
706
+ "catalog.products.uom.errors.baseRequiredForConversions",
707
+ "Base unit is required when conversions are configured."
708
+ );
709
+ throw createCrudFormError(message, { defaultUnit: message });
710
+ }
711
+ const defaultUnitKey = defaultUnit?.toLowerCase() ?? null;
712
+ const defaultSalesUnitKey = defaultSalesUnit?.toLowerCase() ?? null;
713
+ if (defaultUnitKey && defaultSalesUnitKey && defaultSalesUnitKey !== defaultUnitKey) {
714
+ const hasDefaultSalesConversion = conversionInputs.some(
715
+ (entry) => entry.isActive && entry.unitCode.toLowerCase() === defaultSalesUnitKey
716
+ );
717
+ if (!hasDefaultSalesConversion) {
718
+ const message = t(
719
+ "catalog.products.uom.errors.defaultSalesConversionRequired",
720
+ "Active conversion for default sales unit is required when it differs from base unit."
721
+ );
722
+ throw createCrudFormError(message, {
723
+ defaultSalesUnit: message,
724
+ unitConversions: message
725
+ });
338
726
  }
339
- )
340
- },
341
- {
342
- id: "dimensions",
343
- column: 1,
344
- component: ({ values, setValue, errors }) => /* @__PURE__ */ jsx(ProductDimensionsSection, { values, setValue, errors })
345
- },
346
- {
347
- id: "metadata",
348
- column: 1,
349
- component: ({ values, setValue, errors }) => /* @__PURE__ */ jsx(ProductMetadataSection, { values, setValue, errors })
350
- },
351
- {
352
- id: "options",
353
- column: 1,
354
- component: ({ values, setValue, errors }) => /* @__PURE__ */ jsx(ProductOptionsSection, { values, setValue, errors })
355
- },
356
- {
357
- id: "variants",
358
- column: 1,
359
- component: ({ values, setValue, errors }) => /* @__PURE__ */ jsx(
360
- ProductVariantsSection,
361
- {
362
- values,
363
- setValue,
364
- errors,
365
- productId: productId ?? "",
366
- variants,
367
- priceKinds,
368
- onVariantDeleted: handleVariantDeleted,
369
- onVariantsReload: refreshVariants
727
+ }
728
+ if (unitPriceEnabled) {
729
+ if (!unitPriceReferenceUnit || !UNIT_PRICE_REFERENCE_UNITS.has(
730
+ unitPriceReferenceUnit
731
+ )) {
732
+ const message = t(
733
+ "catalog.products.unitPrice.errors.referenceUnit",
734
+ "Reference unit is required when unit price display is enabled."
735
+ );
736
+ throw createCrudFormError(message, {
737
+ unitPriceReferenceUnit: message
738
+ });
370
739
  }
371
- )
372
- },
373
- {
374
- id: "meta",
375
- column: 2,
376
- title: t("catalog.products.create.meta.title", "Product meta"),
377
- description: t("catalog.products.create.meta.description", "Manage subtitle and handle for storefronts."),
378
- component: ({ values, setValue, errors }) => /* @__PURE__ */ jsx(ProductMetaSection, { values, setValue, errors, taxRates })
379
- },
380
- {
381
- id: "categorize",
382
- column: 2,
383
- title: t("catalog.products.create.organize.title", "Categorize"),
384
- description: t("catalog.products.create.organize.description", "Assign categories, sales channels, and tags."),
385
- component: ({ values, setValue, errors }) => /* @__PURE__ */ jsx(
386
- ProductCategorizeSection,
387
- {
388
- values,
389
- setValue,
390
- errors,
391
- initialCategoryOptions: categorizeOptions.categories,
392
- initialChannelOptions: categorizeOptions.channels,
393
- initialTagOptions: categorizeOptions.tags
740
+ if (unitPriceBaseQuantity === null) {
741
+ const message = t(
742
+ "catalog.products.unitPrice.errors.baseQuantity",
743
+ "Base quantity is required when unit price display is enabled."
744
+ );
745
+ throw createCrudFormError(message, {
746
+ unitPriceBaseQuantity: message
747
+ });
394
748
  }
395
- )
396
- },
397
- {
398
- id: "custom-fields",
399
- column: 2,
400
- title: t("catalog.products.edit.custom.title", "Custom attributes"),
401
- kind: "customFields"
402
- }
403
- ], [categorizeOptions, handleVariantDeleted, priceKinds, productId, t, taxRates, variants]);
404
- const handleSubmit = React.useCallback(async (formValues) => {
405
- if (!productId) {
406
- throw createCrudFormError(t("catalog.products.edit.errors.idMissing", "Product identifier is missing."));
407
- }
408
- const parsed = productFormSchema.safeParse(formValues);
409
- if (!parsed.success) {
410
- const issues = parsed.error.issues;
411
- const fieldErrors = {};
412
- issues.forEach((issue) => {
413
- const path = issue.path.join(".") || "form";
414
- if (!fieldErrors[path]) fieldErrors[path] = issue.message;
415
- });
416
- const message = issues[0]?.message ?? t("catalog.products.edit.errors.validation", "Fix highlighted fields.");
417
- throw createCrudFormError(message, fieldErrors);
418
- }
419
- const values = {
420
- ...BASE_INITIAL_VALUES,
421
- ...parsed.data,
422
- title: parsed.data.title ?? "",
423
- subtitle: parsed.data.subtitle ?? "",
424
- handle: parsed.data.handle ?? "",
425
- description: parsed.data.description ?? "",
426
- useMarkdown: parsed.data.useMarkdown ?? false,
427
- taxRateId: parsed.data.taxRateId ?? null,
428
- mediaDraftId: parsed.data.mediaDraftId ?? productId,
429
- mediaItems: Array.isArray(parsed.data.mediaItems) ? parsed.data.mediaItems : [],
430
- defaultMediaId: parsed.data.defaultMediaId ?? null,
431
- defaultMediaUrl: parsed.data.defaultMediaUrl ?? "",
432
- hasVariants: parsed.data.hasVariants ?? false,
433
- options: Array.isArray(parsed.data.options) ? parsed.data.options : [],
434
- variants: Array.isArray(parsed.data.variants) ? parsed.data.variants : [],
435
- metadata: parsed.data.metadata ?? {},
436
- dimensions: parsed.data.dimensions ?? null,
437
- weight: parsed.data.weight ?? null,
438
- customFieldsetCode: parsed.data.customFieldsetCode ?? null,
439
- categoryIds: parsed.data.categoryIds ?? [],
440
- channelIds: parsed.data.channelIds ?? [],
441
- tags: parsed.data.tags ?? [],
442
- optionSchemaId: parsed.data.optionSchemaId ?? null
443
- };
444
- const title = values.title?.trim();
445
- if (!title) {
446
- const message = t("catalog.products.create.errors.title", "Provide a product title.");
447
- throw createCrudFormError(message, { title: message });
448
- }
449
- const handle = values.handle?.trim() || void 0;
450
- const description = values.description?.trim() || void 0;
451
- const metadata = buildMetadataPayload(values);
452
- const dimensions = sanitizeProductDimensions(values.dimensions ?? null);
453
- const weight = sanitizeProductWeight(values.weight ?? null);
454
- const resolveTaxRateValue = (taxRateId) => {
455
- if (!taxRateId) return null;
456
- const match = taxRates.find((rate) => rate.id === taxRateId);
457
- return typeof match?.rate === "number" && Number.isFinite(match.rate) ? match.rate : null;
458
- };
459
- const productTaxRateValue = resolveTaxRateValue(values.taxRateId ?? null);
460
- const defaultMediaId = typeof values.defaultMediaId === "string" && values.defaultMediaId.trim().length ? values.defaultMediaId : null;
461
- const defaultMediaEntry = defaultMediaId ? values.mediaItems.find((item) => item.id === defaultMediaId) : null;
462
- const defaultMediaUrl = defaultMediaEntry ? buildAttachmentImageUrl(defaultMediaEntry.id, {
463
- slug: slugifyAttachmentFileName(defaultMediaEntry.fileName)
464
- }) : null;
465
- const payload = {
466
- id: productId,
467
- title,
468
- subtitle: values.subtitle?.trim() || void 0,
469
- description,
470
- handle,
471
- taxRateId: values.taxRateId ?? null,
472
- taxRate: productTaxRateValue ?? null,
473
- isConfigurable: Boolean(values.hasVariants),
474
- metadata,
475
- dimensions,
476
- weightValue: weight?.value ?? null,
477
- weightUnit: weight?.unit ?? null,
478
- defaultMediaId: defaultMediaId ?? void 0,
479
- defaultMediaUrl: defaultMediaUrl ?? void 0,
480
- customFieldsetCode: values.customFieldsetCode?.trim().length ? values.customFieldsetCode : void 0
481
- };
482
- const categoryIds = normalizeIdList(values.categoryIds);
483
- const channelIds = normalizeIdList(values.channelIds);
484
- const tags = normalizeTagValues(values.tags);
485
- payload.categoryIds = categoryIds;
486
- payload.tags = tags;
487
- const optionSchemaDefinition = buildOptionSchemaDefinition(values.options, title);
488
- if (optionSchemaDefinition) {
489
- payload.optionSchema = optionSchemaDefinition;
490
- } else if (values.optionSchemaId) {
491
- payload.optionSchemaId = null;
492
- }
493
- const customFields = collectCustomFieldValues(values);
494
- if (Object.keys(customFields).length) {
495
- payload.customFields = customFields;
496
- }
497
- const previousSnapshots = offerSnapshotsRef.current;
498
- const offersPayload = buildOfferPayloads({
499
- channelIds,
500
- offerSnapshots: previousSnapshots,
501
- fallback: {
749
+ }
750
+ const payload = {
751
+ id: productId,
502
752
  title,
503
- description: description ?? void 0,
504
- defaultMediaId,
505
- defaultMediaUrl: defaultMediaUrl ?? void 0
753
+ subtitle: values.subtitle?.trim() || void 0,
754
+ description,
755
+ handle,
756
+ taxRateId: values.taxRateId ?? null,
757
+ taxRate: productTaxRateValue ?? null,
758
+ isConfigurable: Boolean(values.hasVariants),
759
+ metadata,
760
+ dimensions,
761
+ weightValue: weight?.value ?? null,
762
+ weightUnit: weight?.unit ?? null,
763
+ defaultMediaId: defaultMediaId ?? void 0,
764
+ defaultMediaUrl: defaultMediaUrl ?? void 0,
765
+ defaultUnit: defaultUnit ?? null,
766
+ defaultSalesUnit: defaultSalesUnit ?? defaultUnit ?? null,
767
+ defaultSalesUnitQuantity,
768
+ uomRoundingScale,
769
+ uomRoundingMode,
770
+ unitPriceEnabled,
771
+ unitPriceReferenceUnit: unitPriceEnabled ? unitPriceReferenceUnit : void 0,
772
+ unitPriceBaseQuantity: unitPriceEnabled ? unitPriceBaseQuantity : void 0,
773
+ customFieldsetCode: values.customFieldsetCode?.trim().length ? values.customFieldsetCode : void 0
774
+ };
775
+ const categoryIds = normalizeIdList(values.categoryIds);
776
+ const channelIds = normalizeIdList(values.channelIds);
777
+ const tags = normalizeTagValues(values.tags);
778
+ payload.categoryIds = categoryIds;
779
+ payload.tags = tags;
780
+ const optionSchemaDefinition = buildOptionSchemaDefinition(
781
+ values.options,
782
+ title
783
+ );
784
+ if (optionSchemaDefinition) {
785
+ payload.optionSchema = optionSchemaDefinition;
786
+ } else if (values.optionSchemaId) {
787
+ payload.optionSchemaId = null;
506
788
  }
507
- });
508
- payload.offers = offersPayload;
509
- const removedOffers = previousSnapshots.filter(
510
- (offer) => typeof offer.id === "string" && !channelIds.includes(offer.channelId)
511
- );
512
- if (removedOffers.length) {
513
- try {
514
- for (const offer of removedOffers) {
515
- if (!offer.id) continue;
516
- await deleteCrud("catalog/offers", offer.id, {
517
- errorMessage: t("catalog.products.edit.offers.deleteError", "Failed to remove sales channel offer.")
789
+ const customFields = collectCustomFieldValues(values);
790
+ if (Object.keys(customFields).length) {
791
+ payload.customFields = customFields;
792
+ }
793
+ const previousSnapshots = offerSnapshotsRef.current;
794
+ const offersPayload = buildOfferPayloads({
795
+ channelIds,
796
+ offerSnapshots: previousSnapshots,
797
+ fallback: {
798
+ title,
799
+ description: description ?? void 0,
800
+ defaultMediaId,
801
+ defaultMediaUrl: defaultMediaUrl ?? void 0
802
+ }
803
+ });
804
+ payload.offers = offersPayload;
805
+ const removedOffers = previousSnapshots.filter(
806
+ (offer) => typeof offer.id === "string" && !channelIds.includes(offer.channelId)
807
+ );
808
+ if (removedOffers.length) {
809
+ try {
810
+ for (const offer of removedOffers) {
811
+ if (!offer.id) continue;
812
+ await deleteCrud("catalog/offers", offer.id, {
813
+ errorMessage: t(
814
+ "catalog.products.edit.offers.deleteError",
815
+ "Failed to remove sales channel offer."
816
+ )
817
+ });
818
+ }
819
+ } catch (err) {
820
+ console.error("catalog.products.edit.offers.delete", err);
821
+ throw createCrudFormError(
822
+ t(
823
+ "catalog.products.edit.offers.deleteError",
824
+ "Failed to remove sales channel offer."
825
+ )
826
+ );
827
+ }
828
+ }
829
+ await updateCrud("catalog/products", payload);
830
+ const previousConversionIds = new Set(
831
+ initialConversionsRef.current.map((entry) => toTrimmedOrNull(entry.id)).filter((id) => Boolean(id))
832
+ );
833
+ const nextConversionIds = new Set(
834
+ conversionInputs.map(
835
+ (entry) => entry.id && entry.id.trim().length ? entry.id : null
836
+ ).filter((id) => Boolean(id))
837
+ );
838
+ const removedConversionIds = Array.from(previousConversionIds).filter(
839
+ (id) => !nextConversionIds.has(id)
840
+ );
841
+ for (const conversionId of removedConversionIds) {
842
+ await deleteCrud("catalog/product-unit-conversions", conversionId, {
843
+ errorMessage: t(
844
+ "catalog.products.uom.errors.sync",
845
+ "Failed to synchronize product conversions."
846
+ )
847
+ });
848
+ }
849
+ const persistedConversions = [];
850
+ for (const conversion of conversionInputs) {
851
+ if (conversion.id) {
852
+ await updateCrud("catalog/product-unit-conversions", {
853
+ id: conversion.id,
854
+ unitCode: conversion.unitCode,
855
+ toBaseFactor: conversion.toBaseFactor,
856
+ sortOrder: conversion.sortOrder,
857
+ isActive: conversion.isActive
858
+ });
859
+ persistedConversions.push({
860
+ id: conversion.id,
861
+ unitCode: conversion.unitCode,
862
+ toBaseFactor: String(conversion.toBaseFactor),
863
+ sortOrder: String(conversion.sortOrder),
864
+ isActive: conversion.isActive
518
865
  });
866
+ continue;
519
867
  }
520
- } catch (err) {
521
- console.error("catalog.products.edit.offers.delete", err);
522
- throw createCrudFormError(
523
- t("catalog.products.edit.offers.deleteError", "Failed to remove sales channel offer.")
868
+ const created = await createCrud(
869
+ "catalog/product-unit-conversions",
870
+ {
871
+ productId,
872
+ unitCode: conversion.unitCode,
873
+ toBaseFactor: conversion.toBaseFactor,
874
+ sortOrder: conversion.sortOrder,
875
+ isActive: conversion.isActive
876
+ }
524
877
  );
878
+ const createdId = created.result && typeof created.result === "object" && typeof created.result.id === "string" ? created.result.id : null;
879
+ persistedConversions.push({
880
+ id: createdId,
881
+ unitCode: conversion.unitCode,
882
+ toBaseFactor: String(conversion.toBaseFactor),
883
+ sortOrder: String(conversion.sortOrder),
884
+ isActive: conversion.isActive
885
+ });
525
886
  }
526
- }
527
- await updateCrud("catalog/products", payload);
528
- offerSnapshotsRef.current = mergeOfferSnapshots(previousSnapshots, offersPayload);
529
- flash(t("catalog.products.edit.success", "Product updated."), "success");
530
- router.push("/backend/catalog/products");
531
- }, [productId, t, taxRates, router]);
887
+ initialConversionsRef.current = persistedConversions;
888
+ offerSnapshotsRef.current = mergeOfferSnapshots(
889
+ previousSnapshots,
890
+ offersPayload
891
+ );
892
+ flash(t("catalog.products.edit.success", "Product updated."), "success");
893
+ router.push("/backend/catalog/products");
894
+ },
895
+ [productId, t, taxRates, router]
896
+ );
532
897
  if (!productId) {
533
- return /* @__PURE__ */ jsx(Page, { children: /* @__PURE__ */ jsx(PageBody, { children: /* @__PURE__ */ jsx("div", { className: "rounded border border-destructive/40 bg-destructive/10 px-4 py-3 text-sm text-destructive", children: t("catalog.products.edit.errors.idMissing", "Product identifier is missing.") }) }) });
898
+ return /* @__PURE__ */ jsx(Page, { children: /* @__PURE__ */ jsx(PageBody, { children: /* @__PURE__ */ jsx("div", { className: "rounded border border-destructive/40 bg-destructive/10 px-4 py-3 text-sm text-destructive", children: t(
899
+ "catalog.products.edit.errors.idMissing",
900
+ "Product identifier is missing."
901
+ ) }) }) });
534
902
  }
535
903
  return /* @__PURE__ */ jsx(Page, { children: /* @__PURE__ */ jsxs(PageBody, { children: [
536
904
  error ? /* @__PURE__ */ jsx("div", { className: "mb-4 rounded border border-destructive/40 bg-destructive/10 px-4 py-3 text-sm text-destructive", children: error }) : null,
@@ -539,12 +907,17 @@ function EditCatalogProductPage({ params }) {
539
907
  {
540
908
  title: t("catalog.products.edit.title", "Edit product"),
541
909
  backHref: "/backend/catalog/products",
542
- versionHistory: { resourceKind: "catalog.product", resourceId: productId ? String(productId) : "" },
910
+ versionHistory: {
911
+ resourceKind: "catalog.product",
912
+ resourceId: productId ? String(productId) : ""
913
+ },
543
914
  fields: [],
544
915
  groups,
545
916
  injectionSpotId: "crud-form:catalog.product",
546
917
  entityId: E.catalog.catalog_product,
547
- customFieldsetBindings: { [E.catalog.catalog_product]: { valueKey: "customFieldsetCode" } },
918
+ customFieldsetBindings: {
919
+ [E.catalog.catalog_product]: { valueKey: "customFieldsetCode" }
920
+ },
548
921
  initialValues: initialValues ?? void 0,
549
922
  isLoading: loading,
550
923
  loadingMessage: t("catalog.products.edit.loading", "Loading product"),
@@ -555,13 +928,23 @@ function EditCatalogProductPage({ params }) {
555
928
  )
556
929
  ] }) });
557
930
  }
558
- function ProductDetailsSection({ values, setValue, errors, productId }) {
931
+ function ProductDetailsSection({
932
+ values,
933
+ setValue,
934
+ errors,
935
+ productId
936
+ }) {
559
937
  const t = useT();
560
- const mediaItems = Array.isArray(values.mediaItems) ? values.mediaItems : [];
938
+ const mediaItems = React.useMemo(
939
+ () => Array.isArray(values.mediaItems) ? values.mediaItems : [],
940
+ [values.mediaItems]
941
+ );
561
942
  const handleMediaItemsChange = React.useCallback(
562
943
  (nextItems) => {
563
944
  setValue("mediaItems", nextItems);
564
- const hasCurrent = nextItems.some((item) => item.id === values.defaultMediaId);
945
+ const hasCurrent = nextItems.some(
946
+ (item) => item.id === values.defaultMediaId
947
+ );
565
948
  if (!hasCurrent) {
566
949
  const fallbackId = nextItems[0]?.id ?? null;
567
950
  setValue("defaultMediaId", fallbackId);
@@ -590,7 +973,9 @@ function ProductDetailsSection({ values, setValue, errors, productId }) {
590
973
  if (target) {
591
974
  setValue(
592
975
  "defaultMediaUrl",
593
- buildAttachmentImageUrl(target.id, { slug: slugifyAttachmentFileName(target.fileName) })
976
+ buildAttachmentImageUrl(target.id, {
977
+ slug: slugifyAttachmentFileName(target.fileName)
978
+ })
594
979
  );
595
980
  }
596
981
  },
@@ -607,7 +992,10 @@ function ProductDetailsSection({ values, setValue, errors, productId }) {
607
992
  {
608
993
  value: values.title,
609
994
  onChange: (event) => setValue("title", event.target.value),
610
- placeholder: t("catalog.products.create.placeholders.title", "e.g., Summer sneaker")
995
+ placeholder: t(
996
+ "catalog.products.create.placeholders.title",
997
+ "e.g., Summer sneaker"
998
+ )
611
999
  }
612
1000
  ),
613
1001
  errors.title ? /* @__PURE__ */ jsx("p", { className: "text-xs text-red-600", children: errors.title }) : null
@@ -625,26 +1013,39 @@ function ProductDetailsSection({ values, setValue, errors, productId }) {
625
1013
  className: "gap-2 text-xs",
626
1014
  children: [
627
1015
  values.useMarkdown ? /* @__PURE__ */ jsx(AlignLeft, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx(FileText, { className: "h-4 w-4" }),
628
- values.useMarkdown ? t("catalog.products.create.actions.usePlain", "Use plain text") : t("catalog.products.create.actions.useMarkdown", "Use markdown")
1016
+ values.useMarkdown ? t("catalog.products.create.actions.usePlain", "Use plain text") : t(
1017
+ "catalog.products.create.actions.useMarkdown",
1018
+ "Use markdown"
1019
+ )
629
1020
  ]
630
1021
  }
631
1022
  )
632
1023
  ] }),
633
- values.useMarkdown ? /* @__PURE__ */ jsx("div", { "data-color-mode": "light", className: "overflow-hidden rounded-md border", children: /* @__PURE__ */ jsx(
634
- MarkdownEditor,
1024
+ values.useMarkdown ? /* @__PURE__ */ jsx(
1025
+ "div",
635
1026
  {
636
- value: values.description,
637
- height: 260,
638
- onChange: (val) => setValue("description", val ?? ""),
639
- previewOptions: { remarkPlugins: [] }
1027
+ "data-color-mode": "light",
1028
+ className: "overflow-hidden rounded-md border",
1029
+ children: /* @__PURE__ */ jsx(
1030
+ MarkdownEditor,
1031
+ {
1032
+ value: values.description,
1033
+ height: 260,
1034
+ onChange: (val) => setValue("description", val ?? ""),
1035
+ previewOptions: { remarkPlugins: [] }
1036
+ }
1037
+ )
640
1038
  }
641
- ) }) : /* @__PURE__ */ jsx(
1039
+ ) : /* @__PURE__ */ jsx(
642
1040
  Textarea,
643
1041
  {
644
1042
  className: "min-h-[180px]",
645
1043
  value: values.description,
646
1044
  onChange: (event) => setValue("description", event.target.value),
647
- placeholder: t("catalog.products.create.placeholders.description", "Describe the product...")
1045
+ placeholder: t(
1046
+ "catalog.products.create.placeholders.description",
1047
+ "Describe the product..."
1048
+ )
648
1049
  }
649
1050
  )
650
1051
  ] }),
@@ -661,7 +1062,10 @@ function ProductDetailsSection({ values, setValue, errors, productId }) {
661
1062
  )
662
1063
  ] });
663
1064
  }
664
- function ProductDimensionsSection({ values, setValue }) {
1065
+ function ProductDimensionsSection({
1066
+ values,
1067
+ setValue
1068
+ }) {
665
1069
  const t = useT();
666
1070
  const dimensionValues = normalizeProductDimensions(values.dimensions);
667
1071
  const weightValues = normalizeProductWeight(values.weight);
@@ -675,7 +1079,14 @@ function ProductDimensionsSection({ values, setValue }) {
675
1079
  {
676
1080
  type: "number",
677
1081
  value: dimensionValues?.width ?? "",
678
- onChange: (event) => setValue("dimensions", updateDimensionValue(values.dimensions ?? null, "width", event.target.value)),
1082
+ onChange: (event) => setValue(
1083
+ "dimensions",
1084
+ updateDimensionValue(
1085
+ values.dimensions ?? null,
1086
+ "width",
1087
+ event.target.value
1088
+ )
1089
+ ),
679
1090
  placeholder: "0"
680
1091
  }
681
1092
  )
@@ -687,7 +1098,14 @@ function ProductDimensionsSection({ values, setValue }) {
687
1098
  {
688
1099
  type: "number",
689
1100
  value: dimensionValues?.height ?? "",
690
- onChange: (event) => setValue("dimensions", updateDimensionValue(values.dimensions ?? null, "height", event.target.value)),
1101
+ onChange: (event) => setValue(
1102
+ "dimensions",
1103
+ updateDimensionValue(
1104
+ values.dimensions ?? null,
1105
+ "height",
1106
+ event.target.value
1107
+ )
1108
+ ),
691
1109
  placeholder: "0"
692
1110
  }
693
1111
  )
@@ -699,7 +1117,14 @@ function ProductDimensionsSection({ values, setValue }) {
699
1117
  {
700
1118
  type: "number",
701
1119
  value: dimensionValues?.depth ?? "",
702
- onChange: (event) => setValue("dimensions", updateDimensionValue(values.dimensions ?? null, "depth", event.target.value)),
1120
+ onChange: (event) => setValue(
1121
+ "dimensions",
1122
+ updateDimensionValue(
1123
+ values.dimensions ?? null,
1124
+ "depth",
1125
+ event.target.value
1126
+ )
1127
+ ),
703
1128
  placeholder: "0"
704
1129
  }
705
1130
  )
@@ -710,7 +1135,14 @@ function ProductDimensionsSection({ values, setValue }) {
710
1135
  Input,
711
1136
  {
712
1137
  value: dimensionValues?.unit ?? "",
713
- onChange: (event) => setValue("dimensions", updateDimensionValue(values.dimensions ?? null, "unit", event.target.value)),
1138
+ onChange: (event) => setValue(
1139
+ "dimensions",
1140
+ updateDimensionValue(
1141
+ values.dimensions ?? null,
1142
+ "unit",
1143
+ event.target.value
1144
+ )
1145
+ ),
714
1146
  placeholder: "cm"
715
1147
  }
716
1148
  )
@@ -722,7 +1154,14 @@ function ProductDimensionsSection({ values, setValue }) {
722
1154
  {
723
1155
  type: "number",
724
1156
  value: weightValues?.value ?? "",
725
- onChange: (event) => setValue("weight", updateWeightValue(values.weight ?? null, "value", event.target.value)),
1157
+ onChange: (event) => setValue(
1158
+ "weight",
1159
+ updateWeightValue(
1160
+ values.weight ?? null,
1161
+ "value",
1162
+ event.target.value
1163
+ )
1164
+ ),
726
1165
  placeholder: "0"
727
1166
  }
728
1167
  )
@@ -733,7 +1172,14 @@ function ProductDimensionsSection({ values, setValue }) {
733
1172
  Input,
734
1173
  {
735
1174
  value: weightValues?.unit ?? "",
736
- onChange: (event) => setValue("weight", updateWeightValue(values.weight ?? null, "unit", event.target.value)),
1175
+ onChange: (event) => setValue(
1176
+ "weight",
1177
+ updateWeightValue(
1178
+ values.weight ?? null,
1179
+ "unit",
1180
+ event.target.value
1181
+ )
1182
+ ),
737
1183
  placeholder: "kg"
738
1184
  }
739
1185
  )
@@ -743,9 +1189,12 @@ function ProductDimensionsSection({ values, setValue }) {
743
1189
  }
744
1190
  function ProductMetadataSection({ values, setValue }) {
745
1191
  const metadata = normalizeMetadata(values.metadata);
746
- const handleMetadataChange = React.useCallback((next) => {
747
- setValue("metadata", next);
748
- }, [setValue]);
1192
+ const handleMetadataChange = React.useCallback(
1193
+ (next) => {
1194
+ setValue("metadata", next);
1195
+ },
1196
+ [setValue]
1197
+ );
749
1198
  return /* @__PURE__ */ jsx(MetadataEditor, { value: metadata, onChange: handleMetadataChange, embedded: true });
750
1199
  }
751
1200
  function ProductOptionsSection({ values, setValue }) {
@@ -758,9 +1207,13 @@ function ProductOptionsSection({ values, setValue }) {
758
1207
  const loadSchemas = React.useCallback(async () => {
759
1208
  setSchemaLoading(true);
760
1209
  try {
761
- const res = await apiCall("/api/catalog/option-schemas?page=1&pageSize=100");
1210
+ const res = await apiCall(
1211
+ "/api/catalog/option-schemas?page=1&pageSize=100"
1212
+ );
762
1213
  if (res.ok) {
763
- setSchemaTemplates(Array.isArray(res.result?.items) ? res.result?.items ?? [] : []);
1214
+ setSchemaTemplates(
1215
+ Array.isArray(res.result?.items) ? res.result?.items ?? [] : []
1216
+ );
764
1217
  } else {
765
1218
  setSchemaTemplates([]);
766
1219
  }
@@ -771,60 +1224,99 @@ function ProductOptionsSection({ values, setValue }) {
771
1224
  setSchemaLoading(false);
772
1225
  }
773
1226
  }, []);
774
- const handleDeleteSchema = React.useCallback(async (id) => {
775
- try {
776
- await deleteCrud("catalog/option-schemas", id, {
777
- errorMessage: t("catalog.products.edit.schemas.deleteError", "Failed to delete schema.")
778
- });
779
- flash(t("catalog.products.edit.schemas.deleted", "Schema deleted."), "success");
780
- void loadSchemas();
781
- } catch (err) {
782
- console.error("catalog.option-schemas.delete failed", err);
783
- }
784
- }, [loadSchemas, t]);
785
- const handleSaveSchema = React.useCallback(async (name) => {
786
- if (!name.trim().length) {
787
- const message = t("catalog.products.edit.schemas.nameRequired", "Provide a schema name.");
788
- throw createCrudFormError(message, { name: message });
789
- }
790
- const schemaPayload = buildSchemaFromOptions(Array.isArray(values.options) ? values.options : [], name);
791
- if (!schemaPayload.options?.length) {
792
- throw createCrudFormError(t("catalog.products.edit.schemas.empty", "Add at least one option before saving."), {});
793
- }
794
- const payload = {
795
- name: name.trim(),
796
- code: slugify(name.trim()),
797
- schema: schemaPayload,
798
- isActive: true
799
- };
800
- if (schemaToEdit?.id) payload.id = schemaToEdit.id;
801
- if (schemaToEdit?.id) await updateCrud("catalog/option-schemas", payload);
802
- else await createCrud("catalog/option-schemas", payload);
803
- flash(t("catalog.products.edit.schemas.saved", "Schema saved."), "success");
804
- setSaveSchemaOpen(false);
805
- setSchemaToEdit(null);
806
- void loadSchemas();
807
- }, [schemaToEdit, t, values.options, loadSchemas]);
808
- const handleOptionTitleChange = React.useCallback((optionId, nextTitle) => {
809
- const next = (Array.isArray(values.options) ? values.options : []).map(
810
- (option) => option.id === optionId ? { ...option, title: nextTitle } : option
811
- );
812
- setValue("options", next);
813
- }, [setValue, values.options]);
814
- const setOptionValues = React.useCallback((optionId, labels) => {
815
- const normalized = labels.map((label) => label.trim()).filter((label) => label.length);
816
- const unique = Array.from(new Set(normalized));
817
- const next = (Array.isArray(values.options) ? values.options : []).map((option) => {
818
- if (option.id !== optionId) return option;
819
- const existingByLabel = new Map(option.values.map((value) => [value.label, value]));
820
- const nextValues = unique.map((label) => existingByLabel.get(label) ?? { id: createLocalId(), label });
821
- return {
822
- ...option,
823
- values: nextValues
1227
+ const handleDeleteSchema = React.useCallback(
1228
+ async (id) => {
1229
+ try {
1230
+ await deleteCrud("catalog/option-schemas", id, {
1231
+ errorMessage: t(
1232
+ "catalog.products.edit.schemas.deleteError",
1233
+ "Failed to delete schema."
1234
+ )
1235
+ });
1236
+ flash(
1237
+ t("catalog.products.edit.schemas.deleted", "Schema deleted."),
1238
+ "success"
1239
+ );
1240
+ void loadSchemas();
1241
+ } catch (err) {
1242
+ console.error("catalog.option-schemas.delete failed", err);
1243
+ }
1244
+ },
1245
+ [loadSchemas, t]
1246
+ );
1247
+ const handleSaveSchema = React.useCallback(
1248
+ async (name) => {
1249
+ if (!name.trim().length) {
1250
+ const message = t(
1251
+ "catalog.products.edit.schemas.nameRequired",
1252
+ "Provide a schema name."
1253
+ );
1254
+ throw createCrudFormError(message, { name: message });
1255
+ }
1256
+ const schemaPayload = buildSchemaFromOptions(
1257
+ Array.isArray(values.options) ? values.options : [],
1258
+ name
1259
+ );
1260
+ if (!schemaPayload.options?.length) {
1261
+ throw createCrudFormError(
1262
+ t(
1263
+ "catalog.products.edit.schemas.empty",
1264
+ "Add at least one option before saving."
1265
+ ),
1266
+ {}
1267
+ );
1268
+ }
1269
+ const payload = {
1270
+ name: name.trim(),
1271
+ code: slugify(name.trim()),
1272
+ schema: schemaPayload,
1273
+ isActive: true
824
1274
  };
825
- });
826
- setValue("options", next);
827
- }, [setValue, values.options]);
1275
+ if (schemaToEdit?.id) payload.id = schemaToEdit.id;
1276
+ if (schemaToEdit?.id) await updateCrud("catalog/option-schemas", payload);
1277
+ else await createCrud("catalog/option-schemas", payload);
1278
+ flash(
1279
+ t("catalog.products.edit.schemas.saved", "Schema saved."),
1280
+ "success"
1281
+ );
1282
+ setSaveSchemaOpen(false);
1283
+ setSchemaToEdit(null);
1284
+ void loadSchemas();
1285
+ },
1286
+ [schemaToEdit, t, values.options, loadSchemas]
1287
+ );
1288
+ const handleOptionTitleChange = React.useCallback(
1289
+ (optionId, nextTitle) => {
1290
+ const next = (Array.isArray(values.options) ? values.options : []).map(
1291
+ (option) => option.id === optionId ? { ...option, title: nextTitle } : option
1292
+ );
1293
+ setValue("options", next);
1294
+ },
1295
+ [setValue, values.options]
1296
+ );
1297
+ const setOptionValues = React.useCallback(
1298
+ (optionId, labels) => {
1299
+ const normalized = labels.map((label) => label.trim()).filter((label) => label.length);
1300
+ const unique = Array.from(new Set(normalized));
1301
+ const next = (Array.isArray(values.options) ? values.options : []).map(
1302
+ (option) => {
1303
+ if (option.id !== optionId) return option;
1304
+ const existingByLabel = new Map(
1305
+ option.values.map((value) => [value.label, value])
1306
+ );
1307
+ const nextValues = unique.map(
1308
+ (label) => existingByLabel.get(label) ?? { id: createLocalId(), label }
1309
+ );
1310
+ return {
1311
+ ...option,
1312
+ values: nextValues
1313
+ };
1314
+ }
1315
+ );
1316
+ setValue("options", next);
1317
+ },
1318
+ [setValue, values.options]
1319
+ );
828
1320
  const addOption = React.useCallback(() => {
829
1321
  const next = [
830
1322
  ...Array.isArray(values.options) ? values.options : [],
@@ -832,14 +1324,22 @@ function ProductOptionsSection({ values, setValue }) {
832
1324
  ];
833
1325
  setValue("options", next);
834
1326
  }, [setValue, values.options]);
835
- const removeOption = React.useCallback((optionId) => {
836
- const next = (Array.isArray(values.options) ? values.options : []).filter((option) => option.id !== optionId);
837
- setValue("options", next);
838
- }, [setValue, values.options]);
1327
+ const removeOption = React.useCallback(
1328
+ (optionId) => {
1329
+ const next = (Array.isArray(values.options) ? values.options : []).filter(
1330
+ (option) => option.id !== optionId
1331
+ );
1332
+ setValue("options", next);
1333
+ },
1334
+ [setValue, values.options]
1335
+ );
839
1336
  return /* @__PURE__ */ jsxs(Fragment, { children: [
840
1337
  /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
841
1338
  /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [
842
- /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: t("catalog.products.create.optionsBuilder.title", "Product options") }),
1339
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: t(
1340
+ "catalog.products.create.optionsBuilder.title",
1341
+ "Product options"
1342
+ ) }),
843
1343
  /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
844
1344
  /* @__PURE__ */ jsx(
845
1345
  Button,
@@ -851,7 +1351,10 @@ function ProductOptionsSection({ values, setValue }) {
851
1351
  setSchemaDialogOpen(true);
852
1352
  void loadSchemas();
853
1353
  },
854
- title: t("catalog.products.edit.schemas.manage", "Open schema library"),
1354
+ title: t(
1355
+ "catalog.products.edit.schemas.manage",
1356
+ "Open schema library"
1357
+ ),
855
1358
  children: /* @__PURE__ */ jsx(BookMarked, { className: "h-4 w-4" })
856
1359
  }
857
1360
  ),
@@ -869,10 +1372,19 @@ function ProductOptionsSection({ values, setValue }) {
869
1372
  children: /* @__PURE__ */ jsx(Save, { className: "h-4 w-4" })
870
1373
  }
871
1374
  ),
872
- /* @__PURE__ */ jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: addOption, children: [
873
- /* @__PURE__ */ jsx(Plus, { className: "mr-2 h-4 w-4" }),
874
- t("catalog.products.create.optionsBuilder.add", "Add option")
875
- ] })
1375
+ /* @__PURE__ */ jsxs(
1376
+ Button,
1377
+ {
1378
+ type: "button",
1379
+ variant: "outline",
1380
+ size: "sm",
1381
+ onClick: addOption,
1382
+ children: [
1383
+ /* @__PURE__ */ jsx(Plus, { className: "mr-2 h-4 w-4" }),
1384
+ t("catalog.products.create.optionsBuilder.add", "Add option")
1385
+ ]
1386
+ }
1387
+ )
876
1388
  ] })
877
1389
  ] }),
878
1390
  (Array.isArray(values.options) ? values.options : []).map((option) => /* @__PURE__ */ jsxs("div", { className: "rounded-md bg-muted/40 p-4", children: [
@@ -882,11 +1394,23 @@ function ProductOptionsSection({ values, setValue }) {
882
1394
  {
883
1395
  value: option.title,
884
1396
  onChange: (event) => handleOptionTitleChange(option.id, event.target.value),
885
- placeholder: t("catalog.products.create.optionsBuilder.placeholder", "e.g., Color"),
1397
+ placeholder: t(
1398
+ "catalog.products.create.optionsBuilder.placeholder",
1399
+ "e.g., Color"
1400
+ ),
886
1401
  className: "flex-1"
887
1402
  }
888
1403
  ),
889
- /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", type: "button", onClick: () => removeOption(option.id), children: /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4" }) })
1404
+ /* @__PURE__ */ jsx(
1405
+ Button,
1406
+ {
1407
+ variant: "ghost",
1408
+ size: "icon",
1409
+ type: "button",
1410
+ onClick: () => removeOption(option.id),
1411
+ children: /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4" })
1412
+ }
1413
+ )
890
1414
  ] }),
891
1415
  /* @__PURE__ */ jsxs("div", { className: "mt-3 space-y-2", children: [
892
1416
  /* @__PURE__ */ jsx(Label, { className: "text-xs uppercase text-muted-foreground", children: t("catalog.products.create.optionsBuilder.values", "Values") }),
@@ -895,12 +1419,18 @@ function ProductOptionsSection({ values, setValue }) {
895
1419
  {
896
1420
  value: option.values.map((value) => value.label),
897
1421
  onChange: (labels) => setOptionValues(option.id, labels),
898
- placeholder: t("catalog.products.create.optionsBuilder.valuePlaceholder", "Type a value and press Enter")
1422
+ placeholder: t(
1423
+ "catalog.products.create.optionsBuilder.valuePlaceholder",
1424
+ "Type a value and press Enter"
1425
+ )
899
1426
  }
900
1427
  )
901
1428
  ] })
902
1429
  ] }, option.id)),
903
- !values.options?.length ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("catalog.products.create.optionsBuilder.empty", "No options yet. Add your first option to generate variants.") }) : null
1430
+ !values.options?.length ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t(
1431
+ "catalog.products.create.optionsBuilder.empty",
1432
+ "No options yet. Add your first option to generate variants."
1433
+ ) }) : null
904
1434
  ] }),
905
1435
  /* @__PURE__ */ jsx(
906
1436
  OptionSchemaDialog,
@@ -957,7 +1487,10 @@ function ProductVariantsSection({
957
1487
  () => Array.isArray(values.options) ? values.options : [],
958
1488
  [values.options]
959
1489
  );
960
- const combos = React.useMemo(() => buildVariantCombinations(optionDefinitions), [optionDefinitions]);
1490
+ const combos = React.useMemo(
1491
+ () => buildVariantCombinations(optionDefinitions),
1492
+ [optionDefinitions]
1493
+ );
961
1494
  const existingKeys = React.useMemo(() => {
962
1495
  const set = /* @__PURE__ */ new Set();
963
1496
  variants.forEach((variant) => {
@@ -1023,20 +1556,23 @@ function ProductVariantsSection({
1023
1556
  },
1024
1557
  [priceKindLookup, t]
1025
1558
  );
1026
- const formatPriceAmount = React.useCallback((price) => {
1027
- const amount = typeof price.amount === "string" && price.amount.trim().length ? price.amount.trim() : "";
1028
- if (!amount) return "\u2014";
1029
- if (!price.currencyCode) return amount;
1030
- return `${price.currencyCode.toUpperCase()} ${amount}`;
1031
- }, []);
1559
+ const formatPriceAmount = React.useCallback(
1560
+ (price) => {
1561
+ const amount = typeof price.amount === "string" && price.amount.trim().length ? price.amount.trim() : "";
1562
+ if (!amount) return "\u2014";
1563
+ if (!price.currencyCode) return amount;
1564
+ return `${price.currencyCode.toUpperCase()} ${amount}`;
1565
+ },
1566
+ []
1567
+ );
1032
1568
  const handleDeleteVariant = React.useCallback(
1033
1569
  async (variant) => {
1034
1570
  if (!allowVariantActions) return;
1035
1571
  const label = variant.name || variant.sku || variant.id;
1036
- const confirmMessage = t("catalog.products.edit.variantList.deleteConfirm", 'Delete variant "{{name}}"?').replace(
1037
- "{{name}}",
1038
- label
1039
- );
1572
+ const confirmMessage = t(
1573
+ "catalog.products.edit.variantList.deleteConfirm",
1574
+ 'Delete variant "{{name}}"?'
1575
+ ).replace("{{name}}", label);
1040
1576
  const confirmed = await confirm({
1041
1577
  title: confirmMessage,
1042
1578
  variant: "destructive"
@@ -1045,13 +1581,22 @@ function ProductVariantsSection({
1045
1581
  setDeletingId(variant.id);
1046
1582
  try {
1047
1583
  await deleteCrud("catalog/variants", variant.id, {
1048
- errorMessage: t("catalog.variants.form.deleteError", "Failed to delete variant.")
1584
+ errorMessage: t(
1585
+ "catalog.variants.form.deleteError",
1586
+ "Failed to delete variant."
1587
+ )
1049
1588
  });
1050
- flash(t("catalog.variants.form.deleted", "Variant deleted."), "success");
1589
+ flash(
1590
+ t("catalog.variants.form.deleted", "Variant deleted."),
1591
+ "success"
1592
+ );
1051
1593
  onVariantDeleted(variant.id);
1052
1594
  } catch (err) {
1053
1595
  console.error("catalog.products.edit.variants.delete", err);
1054
- flash(t("catalog.variants.form.deleteError", "Failed to delete variant."), "error");
1596
+ flash(
1597
+ t("catalog.variants.form.deleteError", "Failed to delete variant."),
1598
+ "error"
1599
+ );
1055
1600
  } finally {
1056
1601
  setDeletingId(null);
1057
1602
  }
@@ -1062,7 +1607,10 @@ function ProductVariantsSection({
1062
1607
  if (!productId || !allowVariantActions) return;
1063
1608
  if (!missingCombos.length) {
1064
1609
  flash(
1065
- t("catalog.products.edit.variantList.generateEmpty", "All option combinations already exist."),
1610
+ t(
1611
+ "catalog.products.edit.variantList.generateEmpty",
1612
+ "All option combinations already exist."
1613
+ ),
1066
1614
  "info"
1067
1615
  );
1068
1616
  return;
@@ -1079,11 +1627,23 @@ function ProductVariantsSection({
1079
1627
  isActive: true
1080
1628
  });
1081
1629
  }
1082
- flash(t("catalog.products.edit.variantList.generateSuccess", "Missing variants generated."), "success");
1630
+ flash(
1631
+ t(
1632
+ "catalog.products.edit.variantList.generateSuccess",
1633
+ "Missing variants generated."
1634
+ ),
1635
+ "success"
1636
+ );
1083
1637
  if (onVariantsReload) await onVariantsReload();
1084
1638
  } catch (err) {
1085
1639
  console.error("catalog.products.edit.variantList.generate", err);
1086
- flash(t("catalog.products.edit.variantList.generateError", "Failed to generate variants."), "error");
1640
+ flash(
1641
+ t(
1642
+ "catalog.products.edit.variantList.generateError",
1643
+ "Failed to generate variants."
1644
+ ),
1645
+ "error"
1646
+ );
1087
1647
  } finally {
1088
1648
  setGenerating(false);
1089
1649
  }
@@ -1094,20 +1654,45 @@ function ProductVariantsSection({
1094
1654
  /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [
1095
1655
  /* @__PURE__ */ jsx("h3", { id: "variants", className: "text-sm font-semibold", children: t("catalog.products.edit.variants", "Variants") }),
1096
1656
  /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
1097
- showGenerateButton ? /* @__PURE__ */ jsx(Button, { type: "button", size: "sm", variant: "outline", disabled: generating, onClick: () => {
1098
- void handleGenerateVariants();
1099
- }, children: generating ? t("catalog.products.edit.variantList.generating", "Generating\u2026") : t("catalog.products.edit.variantList.generate", "Generate variants") }) : null,
1100
- allowVariantActions ? /* @__PURE__ */ jsx(Button, { asChild: true, size: "sm", children: /* @__PURE__ */ jsxs(Link, { href: `/backend/catalog/products/${productId}/variants/create`, children: [
1101
- /* @__PURE__ */ jsx(Plus, { className: "mr-2 h-4 w-4" }),
1102
- t("catalog.products.edit.variants.add", "Add variant")
1103
- ] }) }) : null
1657
+ showGenerateButton ? /* @__PURE__ */ jsx(
1658
+ Button,
1659
+ {
1660
+ type: "button",
1661
+ size: "sm",
1662
+ variant: "outline",
1663
+ disabled: generating,
1664
+ onClick: () => {
1665
+ void handleGenerateVariants();
1666
+ },
1667
+ children: generating ? t(
1668
+ "catalog.products.edit.variantList.generating",
1669
+ "Generating\u2026"
1670
+ ) : t(
1671
+ "catalog.products.edit.variantList.generate",
1672
+ "Generate variants"
1673
+ )
1674
+ }
1675
+ ) : null,
1676
+ allowVariantActions ? /* @__PURE__ */ jsx(Button, { asChild: true, size: "sm", children: /* @__PURE__ */ jsxs(
1677
+ Link,
1678
+ {
1679
+ href: `/backend/catalog/products/${productId}/variants/create`,
1680
+ children: [
1681
+ /* @__PURE__ */ jsx(Plus, { className: "mr-2 h-4 w-4" }),
1682
+ t("catalog.products.edit.variants.add", "Add variant")
1683
+ ]
1684
+ }
1685
+ ) }) : null
1104
1686
  ] })
1105
1687
  ] }),
1106
1688
  variants.length ? /* @__PURE__ */ jsx("div", { className: "overflow-x-auto rounded-md border", children: /* @__PURE__ */ jsxs("table", { className: "w-full table-auto text-sm", children: [
1107
1689
  /* @__PURE__ */ jsx("thead", { className: "bg-muted/40 text-left text-xs uppercase text-muted-foreground", children: /* @__PURE__ */ jsxs("tr", { children: [
1108
1690
  /* @__PURE__ */ jsx("th", { className: "px-3 py-2 font-normal", children: t("catalog.products.form.variants", "Variant") }),
1109
1691
  /* @__PURE__ */ jsx("th", { className: "px-3 py-2 font-normal", children: "SKU" }),
1110
- /* @__PURE__ */ jsx("th", { className: "px-3 py-2 font-normal", children: t("catalog.products.edit.variantList.pricesHeading", "Prices") }),
1692
+ /* @__PURE__ */ jsx("th", { className: "px-3 py-2 font-normal", children: t(
1693
+ "catalog.products.edit.variantList.pricesHeading",
1694
+ "Prices"
1695
+ ) }),
1111
1696
  /* @__PURE__ */ jsx("th", { className: "px-3 py-2 font-normal", children: t("catalog.products.edit.variants.default", "Default") }),
1112
1697
  /* @__PURE__ */ jsx("th", { className: "px-3 py-2 font-normal text-right", children: t("catalog.products.edit.variantList.actions", "Actions") })
1113
1698
  ] }) }),
@@ -1121,14 +1706,30 @@ function ProductVariantsSection({
1121
1706
  }
1122
1707
  ) }),
1123
1708
  /* @__PURE__ */ jsx("td", { className: "px-3 py-2 text-muted-foreground", children: variant.sku || "\u2014" }),
1124
- /* @__PURE__ */ jsx("td", { className: "px-3 py-2", children: variant.prices.length ? /* @__PURE__ */ jsx("ul", { className: "space-y-1", children: variant.prices.map((price) => /* @__PURE__ */ jsxs("li", { className: "text-xs text-muted-foreground", children: [
1125
- /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: formatPriceLabel(price) }),
1126
- " ",
1127
- /* @__PURE__ */ jsx("span", { children: formatPriceAmount(price) })
1128
- ] }, price.id)) }) : /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t("catalog.products.edit.variantList.pricesEmpty", "No prices yet.") }) }),
1709
+ /* @__PURE__ */ jsx("td", { className: "px-3 py-2", children: variant.prices.length ? /* @__PURE__ */ jsx("ul", { className: "space-y-1", children: variant.prices.map((price) => /* @__PURE__ */ jsxs(
1710
+ "li",
1711
+ {
1712
+ className: "text-xs text-muted-foreground",
1713
+ children: [
1714
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: formatPriceLabel(price) }),
1715
+ " ",
1716
+ /* @__PURE__ */ jsx("span", { children: formatPriceAmount(price) })
1717
+ ]
1718
+ },
1719
+ price.id
1720
+ )) }) : /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t(
1721
+ "catalog.products.edit.variantList.pricesEmpty",
1722
+ "No prices yet."
1723
+ ) }) }),
1129
1724
  /* @__PURE__ */ jsx("td", { className: "px-3 py-2 text-muted-foreground", children: variant.isDefault ? t("common.yes", "Yes") : "\u2014" }),
1130
1725
  /* @__PURE__ */ jsx("td", { className: "px-3 py-2", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap justify-end gap-2", children: [
1131
- /* @__PURE__ */ jsx(Button, { asChild: true, size: "sm", variant: "outline", children: /* @__PURE__ */ jsx(Link, { href: `/backend/catalog/products/${productId}/variants/${variant.id}`, children: t("catalog.products.list.actions.edit", "Edit") }) }),
1726
+ /* @__PURE__ */ jsx(Button, { asChild: true, size: "sm", variant: "outline", children: /* @__PURE__ */ jsx(
1727
+ Link,
1728
+ {
1729
+ href: `/backend/catalog/products/${productId}/variants/${variant.id}`,
1730
+ children: t("catalog.products.list.actions.edit", "Edit")
1731
+ }
1732
+ ) }),
1132
1733
  allowVariantActions ? /* @__PURE__ */ jsx(
1133
1734
  Button,
1134
1735
  {
@@ -1140,17 +1741,31 @@ function ProductVariantsSection({
1140
1741
  onClick: () => {
1141
1742
  void handleDeleteVariant(variant);
1142
1743
  },
1143
- children: deletingId === variant.id ? t("catalog.products.edit.variantList.deleting", "Deleting\u2026") : t("catalog.products.list.actions.delete", "Delete")
1744
+ children: deletingId === variant.id ? t(
1745
+ "catalog.products.edit.variantList.deleting",
1746
+ "Deleting\u2026"
1747
+ ) : t(
1748
+ "catalog.products.list.actions.delete",
1749
+ "Delete"
1750
+ )
1144
1751
  }
1145
1752
  ) : null
1146
1753
  ] }) })
1147
1754
  ] }, variant.id)) })
1148
- ] }) }) : /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("catalog.products.edit.variants.empty", "No variants defined yet.") })
1755
+ ] }) }) : /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t(
1756
+ "catalog.products.edit.variants.empty",
1757
+ "No variants defined yet."
1758
+ ) })
1149
1759
  ] }),
1150
1760
  ConfirmDialogElement
1151
1761
  ] });
1152
1762
  }
1153
- function ProductMetaSection({ values, setValue, errors, taxRates }) {
1763
+ function ProductMetaSection({
1764
+ values,
1765
+ setValue,
1766
+ errors,
1767
+ taxRates
1768
+ }) {
1154
1769
  const t = useT();
1155
1770
  const handleValue = typeof values.handle === "string" ? values.handle : "";
1156
1771
  const titleSource = typeof values.title === "string" ? values.title : "";
@@ -1190,7 +1805,10 @@ function ProductMetaSection({ values, setValue, errors, taxRates }) {
1190
1805
  {
1191
1806
  value: typeof values.subtitle === "string" ? values.subtitle : "",
1192
1807
  onChange: (event) => setValue("subtitle", event.target.value),
1193
- placeholder: t("catalog.products.create.placeholders.subtitle", "Optional subtitle")
1808
+ placeholder: t(
1809
+ "catalog.products.create.placeholders.subtitle",
1810
+ "Optional subtitle"
1811
+ )
1194
1812
  }
1195
1813
  ),
1196
1814
  errors.subtitle ? /* @__PURE__ */ jsx("p", { className: "text-xs text-red-600", children: errors.subtitle }) : null
@@ -1213,11 +1831,17 @@ function ProductMetaSection({ values, setValue, errors, taxRates }) {
1213
1831
  {
1214
1832
  value: handleValue,
1215
1833
  onChange: handleHandleInputChange,
1216
- placeholder: t("catalog.products.create.placeholders.handle", "e.g., summer-sneaker"),
1834
+ placeholder: t(
1835
+ "catalog.products.create.placeholders.handle",
1836
+ "e.g., summer-sneaker"
1837
+ ),
1217
1838
  className: "font-mono lowercase"
1218
1839
  }
1219
1840
  ),
1220
- /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("catalog.products.create.handleHelp", "Handle is used for URLs and must be unique.") }),
1841
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t(
1842
+ "catalog.products.create.handleHelp",
1843
+ "Handle is used for URLs and must be unique."
1844
+ ) }),
1221
1845
  errors.handle ? /* @__PURE__ */ jsx("p", { className: "text-xs text-red-600", children: errors.handle }) : null
1222
1846
  ] }),
1223
1847
  /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
@@ -1231,14 +1855,24 @@ function ProductMetaSection({ values, setValue, errors, taxRates }) {
1231
1855
  size: "icon",
1232
1856
  onClick: () => {
1233
1857
  if (typeof window !== "undefined") {
1234
- window.open("/backend/config/sales?section=tax-rates", "_blank", "noopener,noreferrer");
1858
+ window.open(
1859
+ "/backend/config/sales?section=tax-rates",
1860
+ "_blank",
1861
+ "noopener,noreferrer"
1862
+ );
1235
1863
  }
1236
1864
  },
1237
- title: t("catalog.products.create.taxRates.manage", "Manage tax classes"),
1865
+ title: t(
1866
+ "catalog.products.create.taxRates.manage",
1867
+ "Manage tax classes"
1868
+ ),
1238
1869
  className: "text-muted-foreground hover:text-foreground",
1239
1870
  children: [
1240
1871
  /* @__PURE__ */ jsx(Layers, { className: "h-4 w-4" }),
1241
- /* @__PURE__ */ jsx("span", { className: "sr-only", children: t("catalog.products.create.taxRates.manage", "Manage tax classes") })
1872
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: t(
1873
+ "catalog.products.create.taxRates.manage",
1874
+ "Manage tax classes"
1875
+ ) })
1242
1876
  ]
1243
1877
  }
1244
1878
  )
@@ -1251,12 +1885,24 @@ function ProductMetaSection({ values, setValue, errors, taxRates }) {
1251
1885
  onChange: (event) => setValue("taxRateId", event.target.value || null),
1252
1886
  disabled: !taxRates.length,
1253
1887
  children: [
1254
- /* @__PURE__ */ jsx("option", { value: "", children: taxRates.length ? t("catalog.products.create.taxRates.noneSelected", "No tax class selected") : t("catalog.products.create.taxRates.emptyOption", "No tax classes available") }),
1888
+ /* @__PURE__ */ jsx("option", { value: "", children: taxRates.length ? t(
1889
+ "catalog.products.create.taxRates.noneSelected",
1890
+ "No tax class selected"
1891
+ ) : t(
1892
+ "catalog.products.create.taxRates.emptyOption",
1893
+ "No tax classes available"
1894
+ ) }),
1255
1895
  taxRates.map((rate) => /* @__PURE__ */ jsx("option", { value: rate.id, children: formatTaxRateLabel(rate) }, rate.id))
1256
1896
  ]
1257
1897
  }
1258
1898
  ),
1259
- /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: taxRates.length ? t("catalog.products.create.taxRates.help", "Applied to prices unless overridden per variant.") : t("catalog.products.create.taxRates.empty", "Define tax classes under Sales \u2192 Configuration.") })
1899
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: taxRates.length ? t(
1900
+ "catalog.products.create.taxRates.help",
1901
+ "Applied to prices unless overridden per variant."
1902
+ ) : t(
1903
+ "catalog.products.create.taxRates.empty",
1904
+ "Define tax classes under Sales \u2192 Configuration."
1905
+ ) })
1260
1906
  ] })
1261
1907
  ] });
1262
1908
  }
@@ -1272,7 +1918,10 @@ function readOptionSchema(metadata) {
1272
1918
  return raw.map((option) => {
1273
1919
  if (!option || typeof option !== "object") return null;
1274
1920
  const values = Array.isArray(option.values) ? option.values.map(
1275
- (value) => value && typeof value === "object" ? { id: String(value.id ?? createLocalId()), label: typeof value.label === "string" ? value.label : "" } : null
1921
+ (value) => value && typeof value === "object" ? {
1922
+ id: String(value.id ?? createLocalId()),
1923
+ label: typeof value.label === "string" ? value.label : ""
1924
+ } : null
1276
1925
  ).filter(
1277
1926
  (entry) => !!entry
1278
1927
  ) : [];
@@ -1304,7 +1953,9 @@ function formatCategoryLabel(name, fallback, parentName) {
1304
1953
  return parentName ? `${base} / ${parentName}` : base;
1305
1954
  }
1306
1955
  function readCategorySelections(record) {
1307
- const rawCategories = Array.isArray(record.categories) ? record.categories : [];
1956
+ const rawCategories = Array.isArray(
1957
+ record.categories
1958
+ ) ? record.categories : [];
1308
1959
  const options = rawCategories.map((entry) => {
1309
1960
  const value = getString(entry.id) ?? getString(entry.categoryId) ?? getString(entry.category_id);
1310
1961
  if (!value) return null;
@@ -1316,8 +1967,12 @@ function readCategorySelections(record) {
1316
1967
  }).filter(
1317
1968
  (option) => !!option
1318
1969
  );
1319
- const fallbackIds = normalizeIdList(record.categoryIds);
1320
- const combinedIds = Array.from(/* @__PURE__ */ new Set([...options.map((option) => option.value), ...fallbackIds]));
1970
+ const fallbackIds = normalizeIdList(
1971
+ record.categoryIds
1972
+ );
1973
+ const combinedIds = Array.from(
1974
+ /* @__PURE__ */ new Set([...options.map((option) => option.value), ...fallbackIds])
1975
+ );
1321
1976
  return { ids: combinedIds, options };
1322
1977
  }
1323
1978
  function readTagSelections(record) {
@@ -1368,7 +2023,9 @@ function buildChannelOptions(offers) {
1368
2023
  }
1369
2024
  function buildOfferPayloads(params) {
1370
2025
  const { channelIds, offerSnapshots, fallback } = params;
1371
- const byChannel = new Map(offerSnapshots.map((offer) => [offer.channelId, offer]));
2026
+ const byChannel = new Map(
2027
+ offerSnapshots.map((offer) => [offer.channelId, offer])
2028
+ );
1372
2029
  const payloads = [];
1373
2030
  const fallbackTitle = fallback.title.trim();
1374
2031
  for (const channelId of channelIds) {
@@ -1389,7 +2046,9 @@ function buildOfferPayloads(params) {
1389
2046
  return payloads;
1390
2047
  }
1391
2048
  function mergeOfferSnapshots(current, payloads) {
1392
- const currentByChannel = new Map(current.map((snapshot) => [snapshot.channelId, snapshot]));
2049
+ const currentByChannel = new Map(
2050
+ current.map((snapshot) => [snapshot.channelId, snapshot])
2051
+ );
1393
2052
  return payloads.map((entry) => {
1394
2053
  const previous = currentByChannel.get(entry.channelId);
1395
2054
  return {
@@ -1417,6 +2076,47 @@ function ensureString(value) {
1417
2076
  function isRecord(value) {
1418
2077
  return !!value && typeof value === "object" && !Array.isArray(value);
1419
2078
  }
2079
+ function extractUomFields(record) {
2080
+ const unitPriceObject = isRecord(record.unit_price) ? record.unit_price : null;
2081
+ const defaultUnit = canonicalizeUnitCode(
2082
+ record.default_unit ?? record.defaultUnit
2083
+ );
2084
+ const defaultSalesUnit = canonicalizeUnitCode(
2085
+ record.default_sales_unit ?? record.defaultSalesUnit
2086
+ );
2087
+ const defaultSalesUnitQuantity = toPositiveNumberOrNull(
2088
+ record.default_sales_unit_quantity ?? record.defaultSalesUnitQuantity
2089
+ );
2090
+ const roundingScale = toIntegerInRangeOrDefault(
2091
+ record.uom_rounding_scale ?? record.uomRoundingScale,
2092
+ 0,
2093
+ 6,
2094
+ 4
2095
+ );
2096
+ const roundingModeRaw = toTrimmedOrNull(
2097
+ record.uom_rounding_mode ?? record.uomRoundingMode
2098
+ );
2099
+ const roundingMode = roundingModeRaw === "down" || roundingModeRaw === "up" ? roundingModeRaw : "half_up";
2100
+ const unitPriceEnabled = Boolean(
2101
+ unitPriceObject?.enabled ?? record.unit_price_enabled ?? record.unitPriceEnabled
2102
+ );
2103
+ const unitPriceReferenceUnit = canonicalizeUnitCode(
2104
+ unitPriceObject?.reference_unit ?? unitPriceObject?.referenceUnit ?? record.unit_price_reference_unit ?? record.unitPriceReferenceUnit
2105
+ );
2106
+ const unitPriceBaseQuantity = toPositiveNumberOrNull(
2107
+ unitPriceObject?.base_quantity ?? unitPriceObject?.baseQuantity ?? record.unit_price_base_quantity ?? record.unitPriceBaseQuantity
2108
+ );
2109
+ return {
2110
+ defaultUnit,
2111
+ defaultSalesUnit,
2112
+ defaultSalesUnitQuantity,
2113
+ roundingScale,
2114
+ roundingMode,
2115
+ unitPriceEnabled,
2116
+ unitPriceReferenceUnit,
2117
+ unitPriceBaseQuantity
2118
+ };
2119
+ }
1420
2120
  function buildMetadataPayload(values) {
1421
2121
  const metadata = normalizeMetadata(values.metadata);
1422
2122
  metadata.__useMarkdown = values.useMarkdown ?? false;
@@ -1459,7 +2159,10 @@ function buildSchemaFromOptions(options, name) {
1459
2159
  code: slugify(option.title || createLocalId()),
1460
2160
  label: option.title,
1461
2161
  inputType: "select",
1462
- choices: option.values.map((value) => ({ code: slugify(value.label || value.id), label: value.label }))
2162
+ choices: option.values.map((value) => ({
2163
+ code: slugify(value.label || value.id),
2164
+ label: value.label
2165
+ }))
1463
2166
  }))
1464
2167
  };
1465
2168
  }
@@ -1484,35 +2187,83 @@ function OptionSchemaDialog({
1484
2187
  DataLoader,
1485
2188
  {
1486
2189
  isLoading: true,
1487
- loadingMessage: t("catalog.products.edit.schemas.loading", "Loading\u2026"),
2190
+ loadingMessage: t(
2191
+ "catalog.products.edit.schemas.loading",
2192
+ "Loading\u2026"
2193
+ ),
1488
2194
  spinnerSize: "md",
1489
2195
  children: /* @__PURE__ */ jsx(Fragment, {})
1490
2196
  }
1491
2197
  ) }) : templates.length ? /* @__PURE__ */ jsx("div", { className: "divide-y rounded-md border", children: templates.map((template) => {
1492
2198
  const id = typeof template.id === "string" ? template.id : null;
1493
- return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-4 py-3", children: [
1494
- /* @__PURE__ */ jsxs("div", { children: [
1495
- /* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: template.name ?? template.id ?? "Schema" }),
1496
- template.description ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: template.description }) : null
1497
- ] }),
1498
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1499
- /* @__PURE__ */ jsx(Button, { type: "button", size: "sm", variant: "secondary", onClick: () => onSelect(template), children: t("catalog.products.edit.schemas.apply", "Apply") }),
1500
- id ? /* @__PURE__ */ jsxs(Fragment, { children: [
1501
- /* @__PURE__ */ jsxs(Button, { type: "button", size: "icon", variant: "ghost", onClick: () => onEdit(template), children: [
1502
- /* @__PURE__ */ jsx(Save, { className: "h-4 w-4" }),
1503
- /* @__PURE__ */ jsx("span", { className: "sr-only", children: t("catalog.products.edit.schemas.edit", "Edit schema") })
2199
+ return /* @__PURE__ */ jsxs(
2200
+ "div",
2201
+ {
2202
+ className: "flex items-center justify-between px-4 py-3",
2203
+ children: [
2204
+ /* @__PURE__ */ jsxs("div", { children: [
2205
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: template.name ?? template.id ?? "Schema" }),
2206
+ template.description ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: template.description }) : null
1504
2207
  ] }),
1505
- /* @__PURE__ */ jsxs(Button, { type: "button", size: "icon", variant: "ghost", onClick: () => void onDelete(id), children: [
1506
- /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4 text-destructive" }),
1507
- /* @__PURE__ */ jsx("span", { className: "sr-only", children: t("catalog.products.edit.schemas.delete", "Delete schema") })
2208
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2209
+ /* @__PURE__ */ jsx(
2210
+ Button,
2211
+ {
2212
+ type: "button",
2213
+ size: "sm",
2214
+ variant: "secondary",
2215
+ onClick: () => onSelect(template),
2216
+ children: t("catalog.products.edit.schemas.apply", "Apply")
2217
+ }
2218
+ ),
2219
+ id ? /* @__PURE__ */ jsxs(Fragment, { children: [
2220
+ /* @__PURE__ */ jsxs(
2221
+ Button,
2222
+ {
2223
+ type: "button",
2224
+ size: "icon",
2225
+ variant: "ghost",
2226
+ onClick: () => onEdit(template),
2227
+ children: [
2228
+ /* @__PURE__ */ jsx(Save, { className: "h-4 w-4" }),
2229
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: t(
2230
+ "catalog.products.edit.schemas.edit",
2231
+ "Edit schema"
2232
+ ) })
2233
+ ]
2234
+ }
2235
+ ),
2236
+ /* @__PURE__ */ jsxs(
2237
+ Button,
2238
+ {
2239
+ type: "button",
2240
+ size: "icon",
2241
+ variant: "ghost",
2242
+ onClick: () => void onDelete(id),
2243
+ children: [
2244
+ /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4 text-destructive" }),
2245
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: t(
2246
+ "catalog.products.edit.schemas.delete",
2247
+ "Delete schema"
2248
+ ) })
2249
+ ]
2250
+ }
2251
+ )
2252
+ ] }) : null
1508
2253
  ] })
1509
- ] }) : null
1510
- ] })
1511
- ] }, id ?? template.name ?? createLocalId());
2254
+ ]
2255
+ },
2256
+ id ?? template.name ?? createLocalId()
2257
+ );
1512
2258
  }) }) : /* @__PURE__ */ jsx("p", { className: "py-4 text-sm text-muted-foreground", children: t("catalog.products.edit.schemas.empty", "No saved schemas yet.") })
1513
2259
  ] }) });
1514
2260
  }
1515
- function SaveSchemaDialog({ open, onOpenChange, defaultName = "", onSubmit }) {
2261
+ function SaveSchemaDialog({
2262
+ open,
2263
+ onOpenChange,
2264
+ defaultName = "",
2265
+ onSubmit
2266
+ }) {
1516
2267
  const t = useT();
1517
2268
  const [name, setName] = React.useState(defaultName);
1518
2269
  const [saving, setSaving] = React.useState(false);
@@ -1534,10 +2285,29 @@ function SaveSchemaDialog({ open, onOpenChange, defaultName = "", onSubmit }) {
1534
2285
  /* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: t("catalog.products.edit.schemas.saveTitle", "Save option schema") }) }),
1535
2286
  /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1536
2287
  /* @__PURE__ */ jsx(Label, { htmlFor: "schemaName", children: t("catalog.products.edit.schemas.nameLabel", "Schema name") }),
1537
- /* @__PURE__ */ jsx(Input, { id: "schemaName", value: name, onChange: (event) => setName(event.target.value), placeholder: t("catalog.products.edit.schemas.namePlaceholder", "e.g., Color + Size set") })
2288
+ /* @__PURE__ */ jsx(
2289
+ Input,
2290
+ {
2291
+ id: "schemaName",
2292
+ value: name,
2293
+ onChange: (event) => setName(event.target.value),
2294
+ placeholder: t(
2295
+ "catalog.products.edit.schemas.namePlaceholder",
2296
+ "e.g., Color + Size set"
2297
+ )
2298
+ }
2299
+ )
1538
2300
  ] }),
1539
2301
  /* @__PURE__ */ jsxs(DialogFooter, { children: [
1540
- /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", onClick: () => onOpenChange(false), children: t("common.cancel", "Cancel") }),
2302
+ /* @__PURE__ */ jsx(
2303
+ Button,
2304
+ {
2305
+ type: "button",
2306
+ variant: "outline",
2307
+ onClick: () => onOpenChange(false),
2308
+ children: t("common.cancel", "Cancel")
2309
+ }
2310
+ ),
1541
2311
  /* @__PURE__ */ jsx(Button, { type: "button", onClick: handleSubmit, disabled: saving, children: saving ? t("common.saving", "Saving\u2026") : t("common.save", "Save") })
1542
2312
  ] })
1543
2313
  ] }) });