@open-mercato/core 0.4.5-develop-f4858e0ef3 → 0.4.5-develop-4849712ccb

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 (163) 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/generated/entities/catalog_product/index.ts +8 -0
  94. package/generated/entities/catalog_product_unit_conversion/index.ts +12 -0
  95. package/generated/entities/sales_credit_memo_line/index.ts +3 -0
  96. package/generated/entities/sales_invoice_line/index.ts +3 -0
  97. package/generated/entities/sales_order_line/index.ts +3 -0
  98. package/generated/entities/sales_quote_line/index.ts +3 -0
  99. package/generated/entities.ids.generated.ts +1 -0
  100. package/generated/entity-fields-registry.ts +2 -0
  101. package/package.json +2 -2
  102. package/src/modules/auth/i18n/de.json +1 -1
  103. package/src/modules/auth/i18n/en.json +1 -1
  104. package/src/modules/auth/i18n/es.json +1 -1
  105. package/src/modules/auth/i18n/pl.json +1 -1
  106. package/src/modules/catalog/api/prices/route.ts +213 -81
  107. package/src/modules/catalog/api/product-unit-conversions/route.ts +195 -0
  108. package/src/modules/catalog/api/products/route.ts +638 -402
  109. package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +2085 -1072
  110. package/src/modules/catalog/backend/catalog/products/create/page.tsx +1288 -593
  111. package/src/modules/catalog/commands/index.ts +1 -0
  112. package/src/modules/catalog/commands/productUnitConversions.ts +626 -0
  113. package/src/modules/catalog/commands/products.ts +1151 -693
  114. package/src/modules/catalog/commands/shared.ts +19 -5
  115. package/src/modules/catalog/components/products/ProductUomSection.tsx +745 -0
  116. package/src/modules/catalog/components/products/productForm.ts +369 -256
  117. package/src/modules/catalog/components/products/productFormUtils.ts +82 -0
  118. package/src/modules/catalog/data/entities.ts +82 -1
  119. package/src/modules/catalog/data/validators.ts +118 -34
  120. package/src/modules/catalog/events.ts +3 -0
  121. package/src/modules/catalog/i18n/de.json +56 -0
  122. package/src/modules/catalog/i18n/en.json +56 -0
  123. package/src/modules/catalog/i18n/es.json +56 -0
  124. package/src/modules/catalog/i18n/pl.json +56 -0
  125. package/src/modules/catalog/lib/unitCodes.ts +1 -0
  126. package/src/modules/catalog/lib/unitResolution.ts +62 -0
  127. package/src/modules/catalog/migrations/.snapshot-open-mercato.json +245 -0
  128. package/src/modules/catalog/migrations/Migration20260218225422.ts +21 -0
  129. package/src/modules/catalog/migrations/Migration20260219084500.ts +26 -0
  130. package/src/modules/catalog/search.ts +73 -1
  131. package/src/modules/catalog/seed/examples.ts +552 -479
  132. package/src/modules/dashboards/i18n/de.json +1 -1
  133. package/src/modules/dashboards/i18n/en.json +1 -1
  134. package/src/modules/dashboards/i18n/es.json +1 -1
  135. package/src/modules/dashboards/i18n/pl.json +1 -1
  136. package/src/modules/dashboards/seed/analytics.ts +3 -0
  137. package/src/modules/sales/api/order-lines/route.ts +158 -68
  138. package/src/modules/sales/api/quote-lines/route.ts +161 -67
  139. package/src/modules/sales/api/quotes/public/[token]/route.ts +122 -36
  140. package/src/modules/sales/commands/documents.ts +4250 -2424
  141. package/src/modules/sales/commands/shared.ts +7 -2
  142. package/src/modules/sales/components/documents/ItemsSection.tsx +580 -310
  143. package/src/modules/sales/components/documents/LineItemDialog.tsx +1988 -833
  144. package/src/modules/sales/components/documents/ShipmentsSection.tsx +17 -3
  145. package/src/modules/sales/components/documents/lineItemTypes.ts +6 -0
  146. package/src/modules/sales/data/entities.ts +53 -0
  147. package/src/modules/sales/data/validators.ts +36 -0
  148. package/src/modules/sales/frontend/quote/[token]/page.tsx +25 -1
  149. package/src/modules/sales/i18n/de.json +23 -3
  150. package/src/modules/sales/i18n/en.json +23 -3
  151. package/src/modules/sales/i18n/es.json +23 -3
  152. package/src/modules/sales/i18n/pl.json +23 -3
  153. package/src/modules/sales/lib/types.ts +30 -0
  154. package/src/modules/sales/migrations/.snapshot-open-mercato.json +172 -0
  155. package/src/modules/sales/migrations/Migration20260218225423.ts +37 -0
  156. package/src/modules/sales/migrations/Migration20260219084501.ts +73 -0
  157. package/src/modules/sales/search.ts +28 -0
  158. package/src/modules/sales/seed/examples.ts +20 -1
  159. package/src/modules/sales/widgets/injection/document-history/widget.client.tsx +1 -1
  160. package/src/modules/workflows/i18n/de.json +4 -4
  161. package/src/modules/workflows/i18n/en.json +4 -4
  162. package/src/modules/workflows/i18n/es.json +4 -4
  163. package/src/modules/workflows/i18n/pl.json +4 -4
@@ -0,0 +1,591 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
5
+ import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
6
+ import { Button } from "@open-mercato/ui/primitives/button";
7
+ import { Checkbox } from "@open-mercato/ui/primitives/checkbox";
8
+ import { Input } from "@open-mercato/ui/primitives/input";
9
+ import { Label } from "@open-mercato/ui/primitives/label";
10
+ import { Spinner } from "@open-mercato/ui/primitives/spinner";
11
+ import { ArrowDown, ArrowUp, Plus, Trash2 } from "lucide-react";
12
+ import { createProductUnitConversionDraft } from "./productForm.js";
13
+ import { REFERENCE_UNIT_CODES } from "@open-mercato/shared/lib/units/unitCodes";
14
+ import { toTrimmedOrNull } from "./productFormUtils.js";
15
+ const REFERENCE_UNIT_DISPLAY = {
16
+ kg: "1 kg",
17
+ l: "1 l",
18
+ m2: "1 m\xB2",
19
+ m3: "1 m\xB3",
20
+ pc: "1 pc"
21
+ };
22
+ const REFERENCE_UNIT_OPTIONS = REFERENCE_UNIT_CODES.map((code) => ({
23
+ value: code,
24
+ i18nKey: `catalog.products.unitPrice.options.${code}`,
25
+ fallback: REFERENCE_UNIT_DISPLAY[code] ?? code
26
+ }));
27
+ function normalizeDecimalInput(value) {
28
+ return value.replace(/,/g, ".");
29
+ }
30
+ function toPositiveNumber(value) {
31
+ if (typeof value === "number") {
32
+ return Number.isFinite(value) && value > 0 ? value : null;
33
+ }
34
+ if (typeof value !== "string") return null;
35
+ const normalized = toTrimmedOrNull(value);
36
+ if (!normalized) return null;
37
+ const numeric = Number(normalized.replace(",", "."));
38
+ return Number.isFinite(numeric) && numeric > 0 ? numeric : null;
39
+ }
40
+ function toSortValue(value) {
41
+ const numeric = Number(value);
42
+ return Number.isFinite(numeric) ? numeric : Number.MAX_SAFE_INTEGER;
43
+ }
44
+ function formatPreviewNumber(value) {
45
+ if (!Number.isFinite(value)) return "0";
46
+ const rounded = Math.round(value * 1e6) / 1e6;
47
+ return Number.isInteger(rounded) ? String(rounded) : rounded.toString();
48
+ }
49
+ function normalizeConversions(value) {
50
+ if (!Array.isArray(value)) return [];
51
+ const normalized = value.map((entry) => {
52
+ if (!entry || typeof entry !== "object") return null;
53
+ const row = entry;
54
+ return {
55
+ id: toTrimmedOrNull(row.id) ?? null,
56
+ unitCode: toTrimmedOrNull(row.unitCode) ?? "",
57
+ toBaseFactor: toTrimmedOrNull(row.toBaseFactor) ? normalizeDecimalInput(toTrimmedOrNull(row.toBaseFactor)) : "",
58
+ sortOrder: toTrimmedOrNull(row.sortOrder) ?? "",
59
+ isActive: row.isActive !== false
60
+ };
61
+ }).filter((entry) => Boolean(entry));
62
+ normalized.sort((left, right) => {
63
+ const leftOrder = toSortValue(left.sortOrder);
64
+ const rightOrder = toSortValue(right.sortOrder);
65
+ if (leftOrder === rightOrder) return left.unitCode.localeCompare(right.unitCode);
66
+ return leftOrder - rightOrder;
67
+ });
68
+ return normalized.map((entry, index) => ({
69
+ ...entry,
70
+ sortOrder: String((index + 1) * 10)
71
+ }));
72
+ }
73
+ function buildUnitOptions(entries) {
74
+ const list = Array.isArray(entries) ? entries : [];
75
+ const options = list.map((entry) => {
76
+ const value = toTrimmedOrNull(entry.value);
77
+ if (!value) return null;
78
+ return {
79
+ value,
80
+ label: toTrimmedOrNull(entry.label) ?? value
81
+ };
82
+ }).filter((entry) => Boolean(entry));
83
+ return options.sort((left, right) => left.label.localeCompare(right.label));
84
+ }
85
+ function ProductUomSection({
86
+ values,
87
+ errors,
88
+ setValue,
89
+ embedded = false
90
+ }) {
91
+ const t = useT();
92
+ const [unitOptions, setUnitOptions] = React.useState([]);
93
+ const [loadingUnits, setLoadingUnits] = React.useState(false);
94
+ const [errorLoadingUnits, setErrorLoadingUnits] = React.useState(false);
95
+ const conversions = React.useMemo(
96
+ () => normalizeConversions(values.unitConversions),
97
+ [values.unitConversions]
98
+ );
99
+ React.useEffect(() => {
100
+ let cancelled = false;
101
+ async function loadUnits() {
102
+ setLoadingUnits(true);
103
+ setErrorLoadingUnits(false);
104
+ try {
105
+ const response = await apiCall(
106
+ "/api/catalog/dictionaries/unit",
107
+ void 0,
108
+ { fallback: { entries: [] } }
109
+ );
110
+ if (cancelled) return;
111
+ setUnitOptions(buildUnitOptions(response.result?.entries));
112
+ } catch {
113
+ if (!cancelled) {
114
+ setUnitOptions([]);
115
+ setErrorLoadingUnits(true);
116
+ }
117
+ } finally {
118
+ if (!cancelled) setLoadingUnits(false);
119
+ }
120
+ }
121
+ void loadUnits();
122
+ return () => {
123
+ cancelled = true;
124
+ };
125
+ }, []);
126
+ const findUnitLabel = React.useCallback(
127
+ (value) => {
128
+ const code = toTrimmedOrNull(value);
129
+ if (!code) return null;
130
+ const option = unitOptions.find((entry) => entry.value === code);
131
+ return option?.label ?? code;
132
+ },
133
+ [unitOptions]
134
+ );
135
+ const setConversions = React.useCallback(
136
+ (next) => {
137
+ const normalized = next.map((entry, index) => ({
138
+ ...entry,
139
+ sortOrder: String((index + 1) * 10)
140
+ }));
141
+ setValue("unitConversions", normalized);
142
+ },
143
+ [setValue]
144
+ );
145
+ const addConversion = React.useCallback(() => {
146
+ const next = [
147
+ ...conversions,
148
+ createProductUnitConversionDraft({
149
+ sortOrder: String((conversions.length + 1) * 10)
150
+ })
151
+ ];
152
+ setConversions(next);
153
+ }, [conversions, setConversions]);
154
+ const updateConversion = React.useCallback(
155
+ (index, patch) => {
156
+ const next = conversions.map(
157
+ (entry, rowIndex) => rowIndex === index ? { ...entry, ...patch } : entry
158
+ );
159
+ setConversions(next);
160
+ },
161
+ [conversions, setConversions]
162
+ );
163
+ const removeConversion = React.useCallback(
164
+ (index) => {
165
+ const next = conversions.filter((_entry, rowIndex) => rowIndex !== index);
166
+ setConversions(next);
167
+ },
168
+ [conversions, setConversions]
169
+ );
170
+ const moveConversion = React.useCallback(
171
+ (index, direction) => {
172
+ const targetIndex = direction === "up" ? index - 1 : index + 1;
173
+ if (targetIndex < 0 || targetIndex >= conversions.length) return;
174
+ const next = [...conversions];
175
+ const source = next[index];
176
+ next[index] = next[targetIndex];
177
+ next[targetIndex] = source;
178
+ setConversions(next);
179
+ },
180
+ [conversions, setConversions]
181
+ );
182
+ const defaultUnit = toTrimmedOrNull(values.defaultUnit) ?? "";
183
+ const defaultSalesUnit = toTrimmedOrNull(values.defaultSalesUnit) ?? "";
184
+ const defaultSalesQuantityRaw = toTrimmedOrNull(values.defaultSalesUnitQuantity) ?? "1";
185
+ const defaultSalesQuantity = normalizeDecimalInput(defaultSalesQuantityRaw);
186
+ const unitPriceEnabled = Boolean(values.unitPriceEnabled);
187
+ const unitPriceReferenceUnit = toTrimmedOrNull(values.unitPriceReferenceUnit) ?? "";
188
+ const unitPriceBaseQuantityRaw = toTrimmedOrNull(values.unitPriceBaseQuantity) ?? "";
189
+ const unitPriceBaseQuantity = normalizeDecimalInput(unitPriceBaseQuantityRaw);
190
+ const baseUnitLabel = findUnitLabel(defaultUnit) ?? defaultUnit;
191
+ const salesUnitLabel = findUnitLabel(defaultSalesUnit || defaultUnit) ?? defaultSalesUnit ?? defaultUnit;
192
+ const defaultSalesFactor = React.useMemo(() => {
193
+ const defaultUnitKey = defaultUnit.toLowerCase();
194
+ const defaultSalesKey = (defaultSalesUnit || defaultUnit).toLowerCase();
195
+ if (!defaultUnitKey || !defaultSalesKey) return null;
196
+ if (defaultSalesKey === defaultUnitKey) return 1;
197
+ const row = conversions.find(
198
+ (entry) => entry.isActive && entry.unitCode.toLowerCase() === defaultSalesKey && toPositiveNumber(entry.toBaseFactor) !== null
199
+ );
200
+ return row ? toPositiveNumber(row.toBaseFactor) : null;
201
+ }, [conversions, defaultSalesUnit, defaultUnit]);
202
+ const defaultSalesQuantityNumber = toPositiveNumber(defaultSalesQuantity);
203
+ const defaultSalesQuantityNormalized = defaultSalesQuantityNumber && defaultSalesFactor ? defaultSalesQuantityNumber * defaultSalesFactor : null;
204
+ const unitPriceBaseQuantityNumber = toPositiveNumber(unitPriceBaseQuantity);
205
+ const validConversions = conversions.filter(
206
+ (entry) => toTrimmedOrNull(entry.unitCode) && toTrimmedOrNull(entry.toBaseFactor)
207
+ );
208
+ const conversionPreviewItems = validConversions.slice(0, 3).map((entry) => {
209
+ const label = findUnitLabel(entry.unitCode) ?? entry.unitCode;
210
+ const baseLabel = findUnitLabel(defaultUnit) ?? defaultUnit;
211
+ const factor = toTrimmedOrNull(entry.toBaseFactor) ?? "1";
212
+ return `1 ${label} = ${factor} ${baseLabel || t("catalog.products.uom.baseUnit", "base unit")}`;
213
+ });
214
+ const conversionPreview = validConversions.length > 3 ? `${conversionPreviewItems.join(" \u2022 ")} (+${validConversions.length - 3})` : conversionPreviewItems.join(" \u2022 ");
215
+ return /* @__PURE__ */ jsxs(
216
+ "div",
217
+ {
218
+ className: embedded ? "space-y-5" : "space-y-5 rounded-lg border bg-card p-4",
219
+ children: [
220
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
221
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: t("catalog.products.uom.title", "Units of measure") }),
222
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t(
223
+ "catalog.products.uom.description",
224
+ "Set base unit, sales unit, and packaging conversions."
225
+ ) })
226
+ ] }),
227
+ loadingUnits ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
228
+ /* @__PURE__ */ jsx(Spinner, { className: "h-3 w-3" }),
229
+ t("catalog.products.uom.loadingUnits", "Loading units...")
230
+ ] }) : null,
231
+ errorLoadingUnits ? /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: t(
232
+ "catalog.products.uom.errors.loadUnits",
233
+ "Failed to load units. Please try refreshing the page."
234
+ ) }) : null,
235
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [
236
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
237
+ /* @__PURE__ */ jsx(Label, { htmlFor: "catalog-product-uom-base-unit", children: t("catalog.products.uom.baseUnit", "Base unit") }),
238
+ /* @__PURE__ */ jsxs(
239
+ "select",
240
+ {
241
+ id: "catalog-product-uom-base-unit",
242
+ className: "w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
243
+ value: defaultUnit,
244
+ onChange: (event) => setValue("defaultUnit", event.target.value || null),
245
+ disabled: loadingUnits,
246
+ children: [
247
+ /* @__PURE__ */ jsx("option", { value: "", children: t("catalog.products.uom.selectUnit", "Select unit") }),
248
+ unitOptions.map((option) => /* @__PURE__ */ jsx("option", { value: option.value, children: option.label }, option.value))
249
+ ]
250
+ }
251
+ ),
252
+ errors.defaultUnit ? /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: errors.defaultUnit }) : null
253
+ ] }),
254
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
255
+ /* @__PURE__ */ jsx(Label, { htmlFor: "catalog-product-uom-sales-unit", children: t("catalog.products.uom.defaultSalesUnit", "Default sales unit") }),
256
+ /* @__PURE__ */ jsxs(
257
+ "select",
258
+ {
259
+ id: "catalog-product-uom-sales-unit",
260
+ className: "w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
261
+ value: defaultSalesUnit,
262
+ onChange: (event) => setValue("defaultSalesUnit", event.target.value || null),
263
+ disabled: loadingUnits,
264
+ children: [
265
+ /* @__PURE__ */ jsx("option", { value: "", children: t("catalog.products.uom.selectUnit", "Select unit") }),
266
+ unitOptions.map((option) => /* @__PURE__ */ jsx("option", { value: option.value, children: option.label }, option.value))
267
+ ]
268
+ }
269
+ ),
270
+ errors.defaultSalesUnit ? /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: errors.defaultSalesUnit }) : null
271
+ ] }),
272
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2 md:col-span-2", children: [
273
+ /* @__PURE__ */ jsx(Label, { htmlFor: "catalog-product-uom-default-sales-quantity", children: t(
274
+ "catalog.products.uom.defaultSalesQuantityLabel",
275
+ "Default line quantity (in sales unit)"
276
+ ) }),
277
+ /* @__PURE__ */ jsx(
278
+ Input,
279
+ {
280
+ id: "catalog-product-uom-default-sales-quantity",
281
+ type: "text",
282
+ inputMode: "decimal",
283
+ value: defaultSalesQuantity,
284
+ onChange: (event) => setValue(
285
+ "defaultSalesUnitQuantity",
286
+ normalizeDecimalInput(event.target.value)
287
+ ),
288
+ placeholder: "1"
289
+ }
290
+ ),
291
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t(
292
+ "catalog.products.uom.defaultSalesQuantityHint",
293
+ "Used to prefill quantity in quote/order lines. Value is interpreted in Default sales unit."
294
+ ) }),
295
+ defaultSalesQuantityNumber && salesUnitLabel ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: defaultSalesQuantityNormalized && baseUnitLabel ? t(
296
+ "catalog.products.uom.defaultSalesQuantityPreviewWithNormalization",
297
+ "Default line: {{quantity}} {{salesUnit}} (= {{normalized}} {{baseUnit}}).",
298
+ {
299
+ quantity: formatPreviewNumber(defaultSalesQuantityNumber),
300
+ salesUnit: salesUnitLabel,
301
+ normalized: formatPreviewNumber(
302
+ defaultSalesQuantityNormalized
303
+ ),
304
+ baseUnit: baseUnitLabel
305
+ }
306
+ ) : t(
307
+ "catalog.products.uom.defaultSalesQuantityPreview",
308
+ "Default line: {{quantity}} {{salesUnit}}.",
309
+ {
310
+ quantity: formatPreviewNumber(defaultSalesQuantityNumber),
311
+ salesUnit: salesUnitLabel
312
+ }
313
+ ) }) : null,
314
+ errors.defaultSalesUnitQuantity ? /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: errors.defaultSalesUnitQuantity }) : null
315
+ ] })
316
+ ] }),
317
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [
318
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
319
+ /* @__PURE__ */ jsx(Label, { htmlFor: "catalog-product-uom-rounding-mode", children: t("catalog.products.uom.roundingMode", "Rounding mode") }),
320
+ /* @__PURE__ */ jsxs(
321
+ "select",
322
+ {
323
+ id: "catalog-product-uom-rounding-mode",
324
+ className: "w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
325
+ value: values.uomRoundingMode ?? "half_up",
326
+ onChange: (event) => setValue("uomRoundingMode", event.target.value),
327
+ children: [
328
+ /* @__PURE__ */ jsx("option", { value: "half_up", children: t("catalog.products.uom.roundingModeHalfUp", "Half up (default)") }),
329
+ /* @__PURE__ */ jsx("option", { value: "down", children: t("catalog.products.uom.roundingModeDown", "Round down") }),
330
+ /* @__PURE__ */ jsx("option", { value: "up", children: t("catalog.products.uom.roundingModeUp", "Round up") })
331
+ ]
332
+ }
333
+ )
334
+ ] }),
335
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
336
+ /* @__PURE__ */ jsx(Label, { htmlFor: "catalog-product-uom-rounding-scale", children: t("catalog.products.uom.roundingScale", "Rounding scale (decimal places)") }),
337
+ /* @__PURE__ */ jsx(
338
+ Input,
339
+ {
340
+ id: "catalog-product-uom-rounding-scale",
341
+ type: "number",
342
+ min: 0,
343
+ max: 6,
344
+ value: values.uomRoundingScale ?? 4,
345
+ onChange: (event) => {
346
+ const parsed = Number.parseInt(event.target.value, 10);
347
+ setValue("uomRoundingScale", Number.isFinite(parsed) ? Math.max(0, Math.min(6, parsed)) : 4);
348
+ }
349
+ }
350
+ )
351
+ ] })
352
+ ] }),
353
+ /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
354
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
355
+ /* @__PURE__ */ jsx(
356
+ Checkbox,
357
+ {
358
+ id: "catalog-product-unit-price-enabled",
359
+ checked: unitPriceEnabled,
360
+ onCheckedChange: (checked) => setValue("unitPriceEnabled", checked === true)
361
+ }
362
+ ),
363
+ /* @__PURE__ */ jsx(
364
+ Label,
365
+ {
366
+ htmlFor: "catalog-product-unit-price-enabled",
367
+ className: "text-sm",
368
+ children: t(
369
+ "catalog.products.unitPrice.enable",
370
+ "Enable EU unit price display"
371
+ )
372
+ }
373
+ )
374
+ ] }),
375
+ unitPriceEnabled ? /* @__PURE__ */ jsxs("div", { className: "grid gap-3 md:grid-cols-2", children: [
376
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
377
+ /* @__PURE__ */ jsx(Label, { htmlFor: "catalog-product-uom-reference-unit", children: t(
378
+ "catalog.products.unitPrice.referenceUnit",
379
+ "Reference unit"
380
+ ) }),
381
+ /* @__PURE__ */ jsxs(
382
+ "select",
383
+ {
384
+ id: "catalog-product-uom-reference-unit",
385
+ className: "w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
386
+ value: unitPriceReferenceUnit,
387
+ onChange: (event) => setValue("unitPriceReferenceUnit", event.target.value || null),
388
+ children: [
389
+ /* @__PURE__ */ jsx("option", { value: "", children: t(
390
+ "catalog.products.unitPrice.selectReferenceUnit",
391
+ "Select reference unit"
392
+ ) }),
393
+ REFERENCE_UNIT_OPTIONS.map((option) => /* @__PURE__ */ jsx("option", { value: option.value, children: t(option.i18nKey, option.fallback) }, option.value))
394
+ ]
395
+ }
396
+ )
397
+ ] }),
398
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
399
+ /* @__PURE__ */ jsx(Label, { htmlFor: "catalog-product-uom-unit-price-base-quantity", children: t(
400
+ "catalog.products.unitPrice.baseQuantity",
401
+ "Reference quantity (in base unit)"
402
+ ) }),
403
+ /* @__PURE__ */ jsx(
404
+ Input,
405
+ {
406
+ id: "catalog-product-uom-unit-price-base-quantity",
407
+ type: "text",
408
+ inputMode: "decimal",
409
+ value: unitPriceBaseQuantity,
410
+ onChange: (event) => setValue(
411
+ "unitPriceBaseQuantity",
412
+ normalizeDecimalInput(event.target.value)
413
+ ),
414
+ placeholder: "1"
415
+ }
416
+ )
417
+ ] })
418
+ ] }) : null,
419
+ unitPriceEnabled ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: unitPriceReferenceUnit && unitPriceBaseQuantityNumber ? t(
420
+ "catalog.products.unitPrice.hintWithPreview",
421
+ "Show calculated price per {{quantity}} {{unit}}. For most products use 1 (for example: 1 kg, 1 l, 1 m\xB2).",
422
+ {
423
+ quantity: formatPreviewNumber(unitPriceBaseQuantityNumber),
424
+ unit: unitPriceReferenceUnit
425
+ }
426
+ ) : t(
427
+ "catalog.products.unitPrice.hint",
428
+ "Show calculated price per selected reference unit. In most cases set quantity to 1."
429
+ ) }) : null
430
+ ] }),
431
+ /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
432
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
433
+ /* @__PURE__ */ jsx(Label, { className: "text-sm", children: t("catalog.products.uom.conversions", "Product conversions") }),
434
+ /* @__PURE__ */ jsxs(
435
+ Button,
436
+ {
437
+ type: "button",
438
+ variant: "outline",
439
+ size: "sm",
440
+ onClick: addConversion,
441
+ children: [
442
+ /* @__PURE__ */ jsx(Plus, { className: "mr-1.5 h-4 w-4" }),
443
+ t("catalog.products.uom.addConversion", "Add conversion")
444
+ ]
445
+ }
446
+ )
447
+ ] }),
448
+ conversions.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t(
449
+ "catalog.products.uom.emptyConversions",
450
+ "No conversions configured yet."
451
+ ) }) : /* @__PURE__ */ jsx("div", { className: "space-y-2", children: conversions.map((entry, index) => {
452
+ const conversionFactor = toPositiveNumber(entry.toBaseFactor);
453
+ const conversionPreviewText = entry.unitCode && conversionFactor !== null ? t(
454
+ "catalog.products.uom.conversionPreview",
455
+ "1 {{fromUnit}} = {{factor}} {{baseUnit}}",
456
+ {
457
+ fromUnit: findUnitLabel(entry.unitCode) ?? entry.unitCode,
458
+ factor: formatPreviewNumber(conversionFactor),
459
+ baseUnit: findUnitLabel(defaultUnit) ?? defaultUnit ?? t("catalog.products.uom.baseUnit", "base unit")
460
+ }
461
+ ) : null;
462
+ return /* @__PURE__ */ jsxs(
463
+ "div",
464
+ {
465
+ className: "grid gap-3 rounded-md border p-3 md:grid-cols-12",
466
+ children: [
467
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1 md:col-span-4", children: [
468
+ /* @__PURE__ */ jsx(Label, { htmlFor: `catalog-product-uom-conversion-unit-${index}`, className: "text-xs text-muted-foreground", children: t("catalog.products.uom.conversionUnit", "Sales unit") }),
469
+ /* @__PURE__ */ jsxs(
470
+ "select",
471
+ {
472
+ id: `catalog-product-uom-conversion-unit-${index}`,
473
+ className: "w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
474
+ value: entry.unitCode,
475
+ onChange: (event) => updateConversion(index, { unitCode: event.target.value }),
476
+ disabled: loadingUnits,
477
+ children: [
478
+ /* @__PURE__ */ jsx("option", { value: "", children: t("catalog.products.uom.selectUnit", "Select unit") }),
479
+ unitOptions.map((option) => /* @__PURE__ */ jsx("option", { value: option.value, children: option.label }, option.value))
480
+ ]
481
+ }
482
+ )
483
+ ] }),
484
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1 md:col-span-3", children: [
485
+ /* @__PURE__ */ jsx(Label, { htmlFor: `catalog-product-uom-conversion-factor-${index}`, className: "text-xs text-muted-foreground", children: t(
486
+ "catalog.products.uom.toBaseFactor",
487
+ "Base units per 1 sales unit"
488
+ ) }),
489
+ /* @__PURE__ */ jsx(
490
+ Input,
491
+ {
492
+ id: `catalog-product-uom-conversion-factor-${index}`,
493
+ type: "text",
494
+ inputMode: "decimal",
495
+ value: entry.toBaseFactor,
496
+ onChange: (event) => updateConversion(index, {
497
+ toBaseFactor: normalizeDecimalInput(event.target.value)
498
+ }),
499
+ placeholder: "1"
500
+ }
501
+ )
502
+ ] }),
503
+ /* @__PURE__ */ jsxs("div", { className: "flex items-end gap-2 md:col-span-3", children: [
504
+ /* @__PURE__ */ jsxs("div", { className: "inline-flex h-9 rounded-md border", children: [
505
+ /* @__PURE__ */ jsx(
506
+ Button,
507
+ {
508
+ type: "button",
509
+ variant: "ghost",
510
+ size: "icon",
511
+ className: "h-9 w-9 rounded-none border-r",
512
+ onClick: () => moveConversion(index, "up"),
513
+ disabled: index === 0,
514
+ "aria-label": t(
515
+ "catalog.products.uom.moveUp",
516
+ "Move conversion up"
517
+ ),
518
+ children: /* @__PURE__ */ jsx(ArrowUp, { className: "h-4 w-4" })
519
+ }
520
+ ),
521
+ /* @__PURE__ */ jsx(
522
+ Button,
523
+ {
524
+ type: "button",
525
+ variant: "ghost",
526
+ size: "icon",
527
+ className: "h-9 w-9 rounded-none",
528
+ onClick: () => moveConversion(index, "down"),
529
+ disabled: index === conversions.length - 1,
530
+ "aria-label": t(
531
+ "catalog.products.uom.moveDown",
532
+ "Move conversion down"
533
+ ),
534
+ children: /* @__PURE__ */ jsx(ArrowDown, { className: "h-4 w-4" })
535
+ }
536
+ )
537
+ ] }),
538
+ /* @__PURE__ */ jsxs(
539
+ "label",
540
+ {
541
+ htmlFor: `catalog-product-uom-conversion-active-${index}`,
542
+ className: "flex items-center gap-2 px-1 text-xs text-muted-foreground",
543
+ children: [
544
+ /* @__PURE__ */ jsx(
545
+ Checkbox,
546
+ {
547
+ id: `catalog-product-uom-conversion-active-${index}`,
548
+ checked: entry.isActive,
549
+ onCheckedChange: (checked) => updateConversion(index, { isActive: checked === true })
550
+ }
551
+ ),
552
+ t("catalog.products.uom.active", "Active")
553
+ ]
554
+ }
555
+ )
556
+ ] }),
557
+ /* @__PURE__ */ jsx("div", { className: "flex items-end justify-end md:col-span-2", children: /* @__PURE__ */ jsx(
558
+ Button,
559
+ {
560
+ type: "button",
561
+ variant: "ghost",
562
+ size: "icon",
563
+ className: "text-destructive",
564
+ onClick: () => removeConversion(index),
565
+ "aria-label": t(
566
+ "catalog.products.uom.removeConversion",
567
+ "Remove conversion"
568
+ ),
569
+ children: /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4" })
570
+ }
571
+ ) }),
572
+ conversionPreviewText && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground md:col-span-12", children: conversionPreviewText })
573
+ ]
574
+ },
575
+ entry.id ?? `uom-conversion-${index}`
576
+ );
577
+ }) }),
578
+ conversions.length > 1 ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t(
579
+ "catalog.products.uom.conversionOrderHint",
580
+ "Use arrows to reorder conversion priority."
581
+ ) }) : null,
582
+ conversionPreview ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: conversionPreview }) : null
583
+ ] })
584
+ ]
585
+ }
586
+ );
587
+ }
588
+ export {
589
+ ProductUomSection
590
+ };
591
+ //# sourceMappingURL=ProductUomSection.js.map