@open-mercato/core 0.4.5-develop-3ce83a8b24 → 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
@@ -1,7 +1,9 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
- import { LookupSelect } from "@open-mercato/ui/backend/inputs";
4
+ import {
5
+ LookupSelect
6
+ } from "@open-mercato/ui/backend/inputs";
5
7
  import {
6
8
  CrudForm
7
9
  } from "@open-mercato/ui/backend/CrudForm";
@@ -9,7 +11,12 @@ import { collectCustomFieldValues } from "@open-mercato/ui/backend/utils/customF
9
11
  import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
10
12
  import { createCrud, updateCrud } from "@open-mercato/ui/backend/utils/crud";
11
13
  import { createCrudFormError } from "@open-mercato/ui/backend/utils/serverErrors";
12
- import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@open-mercato/ui/primitives/dialog";
14
+ import {
15
+ Dialog,
16
+ DialogContent,
17
+ DialogHeader,
18
+ DialogTitle
19
+ } from "@open-mercato/ui/primitives/dialog";
13
20
  import { Button } from "@open-mercato/ui/primitives/button";
14
21
  import { Input } from "@open-mercato/ui/primitives/input";
15
22
  import { DollarSign, Settings } from "lucide-react";
@@ -22,12 +29,17 @@ import { E } from "../../../../generated/entities.ids.generated.js";
22
29
  import { useT } from "@open-mercato/shared/lib/i18n/context";
23
30
  import { useOrganizationScopeDetail } from "@open-mercato/shared/lib/frontend/useOrganizationScope";
24
31
  import { formatMoney, normalizeNumber } from "./lineItemUtils.js";
25
- import { normalizeCustomFieldSubmitValue, extractCustomFieldValues } from "./customFieldHelpers.js";
32
+ import {
33
+ normalizeCustomFieldSubmitValue,
34
+ extractCustomFieldValues
35
+ } from "./customFieldHelpers.js";
36
+ import { canonicalizeUnitCode } from "@open-mercato/shared/lib/units/unitCodes";
26
37
  const defaultForm = (currencyCode) => ({
27
38
  lineMode: "catalog",
28
39
  productId: null,
29
40
  variantId: null,
30
41
  quantity: "1",
42
+ quantityUnit: null,
31
43
  priceId: null,
32
44
  priceMode: "gross",
33
45
  unitPrice: "",
@@ -39,25 +51,28 @@ const defaultForm = (currencyCode) => ({
39
51
  customFieldSetId: null,
40
52
  statusEntryId: null
41
53
  });
54
+ const UNIT_PRICE_INPUT_SCALE = 4;
42
55
  function buildPriceScopeReason(item, t) {
43
56
  const tags = [];
44
57
  const add = (key) => tags.push(key);
45
- if (item.channel_id || item.channelId) add(t("sales.documents.items.priceScope.channel", "Channel"));
46
- if (item.offer_id || item.offerId) add(t("sales.documents.items.priceScope.offer", "Offer"));
47
- if (item.variant_id || item.variantId) add(t("sales.documents.items.priceScope.variant", "Variant"));
48
- if (item.customer_group_id || item.customerGroupId) add(t("sales.documents.items.priceScope.customerGroup", "Customer group"));
49
- if (item.customer_id || item.customerId) add(t("sales.documents.items.priceScope.customer", "Customer"));
50
- if (item.user_group_id || item.userGroupId) add(t("sales.documents.items.priceScope.userGroup", "User group"));
51
- if (item.user_id || item.userId) add(t("sales.documents.items.priceScope.user", "User"));
58
+ if (item.channel_id || item.channelId)
59
+ add(t("sales.documents.items.priceScope.channel", "Channel"));
60
+ if (item.offer_id || item.offerId)
61
+ add(t("sales.documents.items.priceScope.offer", "Offer"));
62
+ if (item.variant_id || item.variantId)
63
+ add(t("sales.documents.items.priceScope.variant", "Variant"));
64
+ if (item.customer_group_id || item.customerGroupId)
65
+ add(t("sales.documents.items.priceScope.customerGroup", "Customer group"));
66
+ if (item.customer_id || item.customerId)
67
+ add(t("sales.documents.items.priceScope.customer", "Customer"));
68
+ if (item.user_group_id || item.userGroupId)
69
+ add(t("sales.documents.items.priceScope.userGroup", "User group"));
70
+ if (item.user_id || item.userId)
71
+ add(t("sales.documents.items.priceScope.user", "User"));
52
72
  const minQty = normalizeNumber(item.min_quantity, Number.NaN);
53
73
  const maxQty = normalizeNumber(item.max_quantity, Number.NaN);
54
74
  if (Number.isFinite(minQty) || Number.isFinite(maxQty)) {
55
- add(
56
- t(
57
- "sales.documents.items.priceScope.quantity",
58
- "Quantity"
59
- )
60
- );
75
+ add(t("sales.documents.items.priceScope.quantity", "Quantity"));
61
76
  }
62
77
  if (item.starts_at || item.ends_at) {
63
78
  add(t("sales.documents.items.priceScope.schedule", "Scheduled"));
@@ -68,6 +83,49 @@ function buildPriceScopeReason(item, t) {
68
83
  function buildPlaceholder(label) {
69
84
  return /* @__PURE__ */ jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded border bg-muted text-[10px] uppercase text-muted-foreground", children: (label ?? "").slice(0, 2) || "\u2022" });
70
85
  }
86
+ function normalizeUnitCode(value) {
87
+ return canonicalizeUnitCode(value);
88
+ }
89
+ function getRecordBoolean(record, fallback, ...keys) {
90
+ for (const key of keys) {
91
+ const val = record[key];
92
+ if (typeof val === "boolean") return val;
93
+ }
94
+ return fallback;
95
+ }
96
+ function getUomProductFields(item) {
97
+ return {
98
+ defaultUnit: normalizeUnitCode(item.default_unit ?? item.defaultUnit),
99
+ defaultSalesUnit: normalizeUnitCode(
100
+ item.default_sales_unit ?? item.defaultSalesUnit
101
+ ),
102
+ defaultSalesUnitQuantity: normalizeNumber(
103
+ item.default_sales_unit_quantity ?? item.defaultSalesUnitQuantity,
104
+ Number.NaN
105
+ )
106
+ };
107
+ }
108
+ function getUomConversionFields(row) {
109
+ return {
110
+ unitCode: normalizeUnitCode(row.unit_code ?? row.unitCode),
111
+ isActive: getRecordBoolean(row, true, "is_active", "isActive"),
112
+ toBaseFactor: normalizeNumber(
113
+ row.to_base_factor ?? row.toBaseFactor,
114
+ Number.NaN
115
+ )
116
+ };
117
+ }
118
+ function normalizeQuantityPreview(value) {
119
+ if (!Number.isFinite(value)) return 0;
120
+ return Math.round(value * 1e6) / 1e6;
121
+ }
122
+ function normalizeUnitPriceInputValue(value) {
123
+ if (!Number.isFinite(value)) return "";
124
+ const factor = 10 ** UNIT_PRICE_INPUT_SCALE;
125
+ const rounded = Math.round((value + Number.EPSILON) * factor) / factor;
126
+ if (!Number.isFinite(rounded)) return "";
127
+ return rounded.toString();
128
+ }
71
129
  function LineItemDialog({
72
130
  open,
73
131
  kind,
@@ -83,8 +141,12 @@ function LineItemDialog({
83
141
  const scope = useOrganizationScopeDetail();
84
142
  const resolvedOrganizationId = organizationId ?? scope.organizationId ?? null;
85
143
  const resolvedTenantId = tenantId ?? scope.tenantId ?? null;
86
- const [initialValues, setInitialValues] = React.useState(() => defaultForm(currencyCode));
87
- const [lineMode, setLineMode] = React.useState(defaultForm(currencyCode).lineMode);
144
+ const [initialValues, setInitialValues] = React.useState(
145
+ () => defaultForm(currencyCode)
146
+ );
147
+ const [lineMode, setLineMode] = React.useState(
148
+ defaultForm(currencyCode).lineMode
149
+ );
88
150
  const [productOption, setProductOption] = React.useState(null);
89
151
  const [variantOption, setVariantOption] = React.useState(null);
90
152
  const [priceOptions, setPriceOptions] = React.useState([]);
@@ -93,6 +155,7 @@ function LineItemDialog({
93
155
  const [editingId, setEditingId] = React.useState(null);
94
156
  const [taxRates, setTaxRates] = React.useState([]);
95
157
  const [lineStatuses, setLineStatuses] = React.useState([]);
158
+ const [unitOptions, setUnitOptions] = React.useState([]);
96
159
  const [, setLineStatusLoading] = React.useState(false);
97
160
  const productOptionsRef = React.useRef(/* @__PURE__ */ new Map());
98
161
  const variantOptionsRef = React.useRef(/* @__PURE__ */ new Map());
@@ -157,6 +220,7 @@ function LineItemDialog({
157
220
  setProductOption(null);
158
221
  setVariantOption(null);
159
222
  setPriceOptions([]);
223
+ setUnitOptions([]);
160
224
  setEditingId(null);
161
225
  setFormResetKey((prev) => prev + 1);
162
226
  },
@@ -168,20 +232,26 @@ function LineItemDialog({
168
232
  }, [onOpenChange, resetForm]);
169
233
  const loadTaxRates = React.useCallback(async () => {
170
234
  try {
171
- const response = await apiCall(
172
- "/api/sales/tax-rates?pageSize=200",
173
- void 0,
174
- { fallback: { items: [] } }
175
- );
235
+ const response = await apiCall("/api/sales/tax-rates?pageSize=100", void 0, {
236
+ fallback: { items: [] }
237
+ });
176
238
  const items = Array.isArray(response.result?.items) ? response.result.items : [];
177
239
  const parsed = items.map((item) => {
178
240
  const id = typeof item.id === "string" ? item.id : null;
179
241
  const name = typeof item.name === "string" && item.name.trim().length ? item.name.trim() : typeof item.code === "string" ? item.code : null;
180
242
  if (!id || !name) return null;
181
243
  const rate = normalizeNumber(item.rate);
182
- const code = typeof item.code === "string" && item.code.trim().length ? item.code.trim() : null;
183
- const isDefault = Boolean(item.isDefault ?? item.is_default);
184
- return { id, name, code, rate: Number.isFinite(rate) ? rate : null, isDefault };
244
+ const code = typeof item.code === "string" && item.code?.trim().length ? item.code?.trim() ?? null : null;
245
+ const isDefault = Boolean(
246
+ item.isDefault ?? item.is_default
247
+ );
248
+ return {
249
+ id,
250
+ name,
251
+ code,
252
+ rate: Number.isFinite(rate) ? rate : null,
253
+ isDefault
254
+ };
185
255
  }).filter((entry) => Boolean(entry));
186
256
  taxRatesRef.current = parsed;
187
257
  defaultTaxRateRef.current = parsed.find((rate) => rate.isDefault) ?? null;
@@ -199,44 +269,63 @@ function LineItemDialog({
199
269
  async (query) => {
200
270
  const params = new URLSearchParams({ pageSize: "8" });
201
271
  if (query && query.trim().length) params.set("search", query.trim());
202
- const response = await apiCall(
203
- `/api/catalog/products?${params.toString()}`,
204
- void 0,
205
- { fallback: { items: [] } }
206
- );
272
+ const response = await apiCall(`/api/catalog/products?${params.toString()}`, void 0, {
273
+ fallback: { items: [] }
274
+ });
207
275
  const items = Array.isArray(response.result?.items) ? response.result?.items ?? [] : [];
208
276
  const needle = query?.trim().toLowerCase() ?? "";
209
277
  return items.map((item) => {
210
278
  const id = typeof item.id === "string" ? item.id : null;
211
279
  if (!id) return null;
212
- const title = typeof item.title === "string" ? item.title : typeof item.name === "string" ? item.name : id;
213
- const sku = typeof item.sku === "string" ? item.sku : null;
214
- const thumbnail = typeof item.default_media_url === "string" ? item.default_media_url : typeof item.defaultMediaUrl === "string" ? item.defaultMediaUrl : null;
215
- const pricing = typeof item.pricing === "object" && item.pricing ? item.pricing : null;
216
- const metadata = typeof item.metadata === "object" && item.metadata ? item.metadata : null;
217
- const pricingTaxRateId = typeof pricing?.tax_rate_id === "string" && pricing.tax_rate_id.trim().length ? pricing.tax_rate_id.trim() : typeof pricing?.taxRateId === "string" && pricing.taxRateId.trim().length ? pricing.taxRateId.trim() : null;
218
- const metaTaxRateId = typeof metadata?.taxRateId === "string" && metadata.taxRateId.trim().length ? metadata.taxRateId.trim() : typeof metadata?.tax_rate_id === "string" && metadata.tax_rate_id.trim().length ? metadata.tax_rate_id.trim() : null;
280
+ const productItem = item;
281
+ const title = typeof item.title === "string" ? item.title : typeof productItem.name === "string" ? productItem.name : id;
282
+ const sku = typeof productItem.sku === "string" ? productItem.sku : null;
283
+ const thumbnail = typeof productItem.default_media_url === "string" ? productItem.default_media_url : typeof productItem.defaultMediaUrl === "string" ? productItem.defaultMediaUrl : null;
284
+ const pricing = typeof productItem.pricing === "object" && productItem.pricing ? productItem.pricing : null;
285
+ const metadata = typeof productItem.metadata === "object" && productItem.metadata ? productItem.metadata : null;
286
+ const pricingMeta = pricing;
287
+ const metaMeta = metadata;
288
+ const pricingTaxRateId = typeof pricingMeta?.tax_rate_id === "string" && pricingMeta.tax_rate_id.trim().length ? pricingMeta.tax_rate_id.trim() : typeof pricingMeta?.taxRateId === "string" && pricingMeta.taxRateId.trim().length ? pricingMeta.taxRateId.trim() : null;
289
+ const metaTaxRateId = typeof metaMeta?.taxRateId === "string" && metaMeta.taxRateId.trim().length ? metaMeta.taxRateId.trim() : typeof metaMeta?.tax_rate_id === "string" && metaMeta.tax_rate_id.trim().length ? metaMeta.tax_rate_id.trim() : null;
219
290
  const taxRateValue = normalizeNumber(
220
- pricing?.tax_rate ?? pricing?.taxRate ?? item.tax_rate ?? item.taxRate,
291
+ pricingMeta?.tax_rate ?? pricingMeta?.taxRate ?? productItem.tax_rate ?? productItem.taxRate,
221
292
  Number.NaN
222
293
  );
294
+ const uomFields = getUomProductFields(item);
295
+ const defaultUnit = uomFields.defaultUnit;
296
+ const defaultSalesUnit = uomFields.defaultSalesUnit;
297
+ const defaultSalesUnitQuantity = uomFields.defaultSalesUnitQuantity;
223
298
  const matches = !needle || title.toLowerCase().includes(needle) || (sku ? sku.toLowerCase().includes(needle) : false);
224
299
  if (!matches) return null;
225
300
  return {
226
301
  id,
227
302
  title,
228
303
  subtitle: sku ?? void 0,
229
- icon: thumbnail ? /* @__PURE__ */ jsx("img", { src: thumbnail, alt: title, className: "h-8 w-8 rounded object-cover" }) : buildPlaceholder(title),
304
+ icon: thumbnail ? /* @__PURE__ */ jsx(
305
+ "img",
306
+ {
307
+ src: thumbnail,
308
+ alt: title,
309
+ className: "h-8 w-8 rounded object-cover"
310
+ }
311
+ ) : buildPlaceholder(title),
230
312
  option: {
231
313
  id,
232
314
  title,
233
315
  sku,
234
316
  thumbnailUrl: thumbnail,
235
317
  taxRateId: pricingTaxRateId ?? metaTaxRateId ?? null,
236
- taxRate: Number.isFinite(taxRateValue) ? taxRateValue : null
318
+ taxRate: Number.isFinite(taxRateValue) ? taxRateValue : null,
319
+ defaultUnit,
320
+ defaultSalesUnit,
321
+ defaultSalesUnitQuantity: Number.isFinite(
322
+ defaultSalesUnitQuantity
323
+ ) ? defaultSalesUnitQuantity : null
237
324
  }
238
325
  };
239
- }).filter((entry) => Boolean(entry)).map((entry) => {
326
+ }).filter(
327
+ (entry) => Boolean(entry)
328
+ ).map((entry) => {
240
329
  productOptionsRef.current.set(entry.option.id, entry.option);
241
330
  return entry;
242
331
  });
@@ -256,19 +345,28 @@ function LineItemDialog({
256
345
  const id = typeof item.id === "string" ? item.id : null;
257
346
  if (!id) return null;
258
347
  const title = typeof item.name === "string" ? item.name : id;
259
- const sku = typeof item.sku === "string" ? item.sku : null;
260
- const metadata = typeof item.metadata === "object" && item.metadata ? item.metadata : null;
261
- const variantTaxRateId = typeof metadata?.taxRateId === "string" && metadata.taxRateId.trim().length ? metadata.taxRateId.trim() : typeof metadata?.tax_rate_id === "string" && metadata.tax_rate_id.trim().length ? metadata.tax_rate_id.trim() : null;
348
+ const variantItem = item;
349
+ const sku = typeof variantItem.sku === "string" ? variantItem.sku : null;
350
+ const metadata = typeof variantItem.metadata === "object" && variantItem.metadata ? variantItem.metadata : null;
351
+ const variantMeta = metadata;
352
+ const variantTaxRateId = typeof variantMeta?.taxRateId === "string" && variantMeta.taxRateId.trim().length ? variantMeta.taxRateId.trim() : typeof variantMeta?.tax_rate_id === "string" && variantMeta.tax_rate_id.trim().length ? variantMeta.tax_rate_id.trim() : null;
262
353
  const variantTaxRate = normalizeNumber(
263
- item.tax_rate ?? item.taxRate ?? metadata?.tax_rate ?? metadata?.taxRate,
354
+ variantItem.tax_rate ?? variantItem.taxRate ?? variantMeta?.tax_rate ?? variantMeta?.taxRate,
264
355
  Number.NaN
265
356
  );
266
- const thumbnail = typeof item.default_media_url === "string" ? item.default_media_url : typeof item.thumbnailUrl === "string" ? item.thumbnailUrl : fallbackThumbnail ?? null;
357
+ const thumbnail = typeof variantItem.default_media_url === "string" ? variantItem.default_media_url : typeof variantItem.thumbnailUrl === "string" ? variantItem.thumbnailUrl : fallbackThumbnail ?? null;
267
358
  return {
268
359
  id,
269
360
  title,
270
361
  subtitle: sku ?? void 0,
271
- icon: thumbnail ? /* @__PURE__ */ jsx("img", { src: thumbnail, alt: title, className: "h-8 w-8 rounded object-cover" }) : buildPlaceholder(title),
362
+ icon: thumbnail ? /* @__PURE__ */ jsx(
363
+ "img",
364
+ {
365
+ src: thumbnail,
366
+ alt: title,
367
+ className: "h-8 w-8 rounded object-cover"
368
+ }
369
+ ) : buildPlaceholder(title),
272
370
  option: {
273
371
  id,
274
372
  title,
@@ -278,15 +376,82 @@ function LineItemDialog({
278
376
  taxRate: Number.isFinite(variantTaxRate) ? variantTaxRate : null
279
377
  }
280
378
  };
281
- }).filter((entry) => Boolean(entry)).map((entry) => {
379
+ }).filter(
380
+ (entry) => Boolean(entry)
381
+ ).map((entry) => {
282
382
  variantOptionsRef.current.set(entry.option.id, entry.option);
283
383
  return entry;
284
384
  });
285
385
  },
286
386
  []
287
387
  );
388
+ const loadProductUnits = React.useCallback(
389
+ async (productId, option) => {
390
+ if (!productId) {
391
+ setUnitOptions([]);
392
+ return [];
393
+ }
394
+ const map = /* @__PURE__ */ new Map();
395
+ let baseUnit = normalizeUnitCode(option?.defaultUnit);
396
+ let defaultSalesUnit = normalizeUnitCode(option?.defaultSalesUnit);
397
+ if (!baseUnit || !defaultSalesUnit) {
398
+ try {
399
+ const response = await apiCall(
400
+ `/api/catalog/products?id=${encodeURIComponent(productId)}&pageSize=1`,
401
+ void 0,
402
+ { fallback: { items: [] } }
403
+ );
404
+ const records = Array.isArray(response.result?.items) ? response.result.items : [];
405
+ const matched = records.find((entry) => entry.id === productId) ?? records[0] ?? null;
406
+ if (matched) {
407
+ const matchedUom = getUomProductFields(matched);
408
+ baseUnit = baseUnit ?? matchedUom.defaultUnit;
409
+ defaultSalesUnit = defaultSalesUnit ?? matchedUom.defaultSalesUnit;
410
+ }
411
+ } catch (err) {
412
+ console.error("sales.document.items.loadProductUnits.hydration", err);
413
+ }
414
+ }
415
+ if (baseUnit) {
416
+ map.set(baseUnit, { code: baseUnit, toBaseFactor: 1, isBase: true });
417
+ }
418
+ try {
419
+ const response = await apiCall(
420
+ `/api/catalog/product-unit-conversions?productId=${encodeURIComponent(productId)}&pageSize=100`,
421
+ void 0,
422
+ { fallback: { items: [] } }
423
+ );
424
+ const rows = Array.isArray(response.result?.items) ? response.result.items : [];
425
+ for (const row of rows) {
426
+ const conv = getUomConversionFields(row);
427
+ if (!conv.unitCode) continue;
428
+ if (!conv.isActive) continue;
429
+ map.set(conv.unitCode, {
430
+ code: conv.unitCode,
431
+ toBaseFactor: Number.isFinite(conv.toBaseFactor) && conv.toBaseFactor > 0 ? conv.toBaseFactor : null,
432
+ isBase: baseUnit ? conv.unitCode === baseUnit : false
433
+ });
434
+ }
435
+ } catch (err) {
436
+ console.error("sales.document.items.loadUnits", err);
437
+ }
438
+ if (defaultSalesUnit && !map.has(defaultSalesUnit)) {
439
+ map.set(defaultSalesUnit, {
440
+ code: defaultSalesUnit,
441
+ toBaseFactor: baseUnit && defaultSalesUnit === baseUnit ? 1 : null,
442
+ isBase: baseUnit ? defaultSalesUnit === baseUnit : false
443
+ });
444
+ }
445
+ const nextOptions = Array.from(map.values()).sort(
446
+ (left, right) => left.code.localeCompare(right.code)
447
+ );
448
+ setUnitOptions(nextOptions);
449
+ return nextOptions;
450
+ },
451
+ []
452
+ );
288
453
  const loadPrices = React.useCallback(
289
- async (productId, variantId) => {
454
+ async (productId, variantId, quantity, quantityUnit) => {
290
455
  if (!productId) {
291
456
  setPriceOptions([]);
292
457
  return [];
@@ -295,20 +460,38 @@ function LineItemDialog({
295
460
  try {
296
461
  const params = new URLSearchParams({ productId, pageSize: "20" });
297
462
  if (variantId) params.set("variantId", variantId);
298
- const response = await apiCall(
299
- `/api/catalog/prices?${params.toString()}`,
300
- void 0,
301
- { fallback: { items: [] } }
302
- );
463
+ const quantityValue = normalizeNumber(quantity, Number.NaN);
464
+ if (Number.isFinite(quantityValue) && quantityValue > 0) {
465
+ params.set("quantity", String(quantityValue));
466
+ }
467
+ const quantityUnitCode = normalizeUnitCode(quantityUnit);
468
+ if (quantityUnitCode) {
469
+ params.set("quantityUnit", quantityUnitCode);
470
+ }
471
+ const response = await apiCall(`/api/catalog/prices?${params.toString()}`, void 0, {
472
+ fallback: { items: [] }
473
+ });
303
474
  const items = Array.isArray(response.result?.items) ? response.result.items : [];
304
475
  const mapped = items.map((item) => {
305
476
  const id = typeof item.id === "string" ? item.id : null;
306
477
  if (!id) return null;
307
- const amountNet = normalizeNumber(item.unit_price_net, null);
308
- const amountGross = normalizeNumber(item.unit_price_gross, null);
478
+ const amountNetRaw = normalizeNumber(
479
+ item.unit_price_net,
480
+ Number.NaN
481
+ );
482
+ const amountGrossRaw = normalizeNumber(
483
+ item.unit_price_gross,
484
+ Number.NaN
485
+ );
486
+ const amountNet = Number.isFinite(amountNetRaw) ? amountNetRaw : null;
487
+ const amountGross = Number.isFinite(amountGrossRaw) ? amountGrossRaw : null;
309
488
  const currency = typeof item.currency_code === "string" ? item.currency_code : typeof item.currencyCode === "string" ? item.currencyCode : null;
310
489
  const displayMode = item.display_mode === "including-tax" || item.display_mode === "excluding-tax" ? item.display_mode : item.displayMode === "including-tax" || item.displayMode === "excluding-tax" ? item.displayMode : null;
311
- const taxRate = normalizeNumber(item.tax_rate, null);
490
+ const taxRateRaw = normalizeNumber(
491
+ item.tax_rate,
492
+ Number.NaN
493
+ );
494
+ const taxRate = Number.isFinite(taxRateRaw) ? taxRateRaw : null;
312
495
  const priceKindId = typeof item.price_kind_id === "string" ? item.price_kind_id : typeof item.priceKindId === "string" ? item.priceKindId : null;
313
496
  const priceKindTitle = typeof item.price_kind_title === "string" ? item.price_kind_title : typeof item.priceKindTitle === "string" ? item.priceKindTitle : typeof item.price_kind === "object" && item && typeof item.price_kind?.title === "string" ? item.price_kind.title : typeof item.price_kind === "object" && item && typeof item.price_kind?.name === "string" ? item.price_kind.name : null;
314
497
  const priceKindCode = typeof item.price_kind_code === "string" ? item.price_kind_code : typeof item.priceKindCode === "string" ? item.priceKindCode : typeof item.price_kind === "object" && item && typeof item.price_kind?.code === "string" ? item.price_kind.code : null;
@@ -318,7 +501,10 @@ function LineItemDialog({
318
501
  displayMode === "excluding-tax" && amountNet !== null && currency ? formatMoney(amountNet, currency) : null,
319
502
  displayMode ? displayMode === "including-tax" ? t("sales.documents.items.priceGross", "Gross") : t("sales.documents.items.priceNet", "Net") : null
320
503
  ].filter(Boolean);
321
- const { reason, tags } = buildPriceScopeReason(item, (key, fallback) => t(key, fallback));
504
+ const { reason, tags } = buildPriceScopeReason(
505
+ item,
506
+ (key, fallback) => t(key, fallback)
507
+ );
322
508
  const label = labelParts.length > 0 ? labelParts.join(" \u2022 ") : amountGross !== null && currency ? formatMoney(amountGross, currency) : amountNet !== null && currency ? formatMoney(amountNet, currency) : id;
323
509
  return {
324
510
  id,
@@ -346,15 +532,85 @@ function LineItemDialog({
346
532
  },
347
533
  [t]
348
534
  );
535
+ const selectPriceAfterRefresh = React.useCallback(
536
+ (prices, currentPriceId, currentPriceKindId) => {
537
+ if (!prices.length) return null;
538
+ if (currentPriceId) {
539
+ const sameId = prices.find((entry) => entry.id === currentPriceId);
540
+ if (sameId) return sameId;
541
+ }
542
+ if (currentPriceKindId) {
543
+ const sameKind = prices.find(
544
+ (entry) => entry.priceKindId === currentPriceKindId
545
+ );
546
+ if (sameKind) return sameKind;
547
+ }
548
+ return prices[0] ?? null;
549
+ },
550
+ []
551
+ );
552
+ const resolveUnitPriceFactor = React.useCallback(
553
+ (quantityUnit) => {
554
+ const normalized = normalizeUnitCode(quantityUnit);
555
+ if (!normalized) return 1;
556
+ const unit = unitOptions.find((entry) => entry.code === normalized) ?? null;
557
+ const factor = normalizeNumber(unit?.toBaseFactor, Number.NaN);
558
+ if (!Number.isFinite(factor) || factor <= 0) return 1;
559
+ return factor;
560
+ },
561
+ [unitOptions]
562
+ );
563
+ const convertUnitPriceForUnitChange = React.useCallback(
564
+ (rawUnitPrice, fromUnit, toUnit) => {
565
+ const amount = normalizeNumber(rawUnitPrice, Number.NaN);
566
+ if (!Number.isFinite(amount) || amount <= 0) return null;
567
+ const fromCode = normalizeUnitCode(fromUnit);
568
+ const toCode = normalizeUnitCode(toUnit);
569
+ if (!fromCode || !toCode || fromCode === toCode) return null;
570
+ const fromFactor = resolveUnitPriceFactor(fromCode);
571
+ const toFactor = resolveUnitPriceFactor(toCode);
572
+ if (!Number.isFinite(fromFactor) || !Number.isFinite(toFactor) || fromFactor <= 0 || toFactor <= 0) {
573
+ return null;
574
+ }
575
+ const baseAmount = amount / fromFactor;
576
+ const convertedAmount = baseAmount * toFactor;
577
+ if (!Number.isFinite(convertedAmount) || convertedAmount <= 0) return null;
578
+ return normalizeUnitPriceInputValue(convertedAmount);
579
+ },
580
+ [resolveUnitPriceFactor]
581
+ );
582
+ const applyPriceSelection = React.useCallback(
583
+ (selected, setFormValue, options) => {
584
+ if (!setFormValue) return;
585
+ if (selected) {
586
+ const mode = selected.displayMode === "excluding-tax" ? "net" : "gross";
587
+ const amountPerBaseUnit = mode === "net" ? selected.amountNet ?? selected.amountGross ?? 0 : selected.amountGross ?? selected.amountNet ?? 0;
588
+ const factor = resolveUnitPriceFactor(options?.quantityUnit ?? null);
589
+ const amount = Number.isFinite(amountPerBaseUnit * factor) ? amountPerBaseUnit * factor : amountPerBaseUnit;
590
+ setFormValue("priceId", selected.id);
591
+ setFormValue("priceMode", mode);
592
+ setFormValue("unitPrice", normalizeUnitPriceInputValue(amount));
593
+ setFormValue("taxRate", selected.taxRate ?? null);
594
+ setFormValue("taxRateId", findTaxRateIdByValue(selected.taxRate));
595
+ setFormValue(
596
+ "currencyCode",
597
+ selected.currencyCode ?? currencyCode ?? null
598
+ );
599
+ return;
600
+ }
601
+ const fallbackTax = resolveTaxSelection(options?.fallbackTaxSource ?? null);
602
+ setFormValue("taxRate", fallbackTax.taxRate ?? null);
603
+ setFormValue("taxRateId", fallbackTax.taxRateId ?? null);
604
+ },
605
+ [currencyCode, findTaxRateIdByValue, resolveTaxSelection, resolveUnitPriceFactor]
606
+ );
349
607
  const loadLineStatuses = React.useCallback(async () => {
350
608
  setLineStatusLoading(true);
351
609
  try {
352
610
  const params = new URLSearchParams({ page: "1", pageSize: "100" });
353
- const response = await apiCall(
354
- `/api/sales/order-line-statuses?${params.toString()}`,
355
- void 0,
356
- { fallback: { items: [] } }
357
- );
611
+ const response = await apiCall(`/api/sales/order-line-statuses?${params.toString()}`, void 0, {
612
+ fallback: { items: [] }
613
+ });
358
614
  const items = Array.isArray(response.result?.items) ? response.result.items : [];
359
615
  const mapped = items.map((entry) => {
360
616
  const id = typeof entry.id === "string" ? entry.id : null;
@@ -379,18 +635,15 @@ function LineItemDialog({
379
635
  async (query) => {
380
636
  const options = lineStatuses.length && !query ? lineStatuses : await loadLineStatuses();
381
637
  const term = query?.trim().toLowerCase() ?? "";
382
- const currentMap = options.reduce(
383
- (acc, entry) => {
384
- acc[entry.value] = {
385
- value: entry.value,
386
- label: entry.label,
387
- color: entry.color,
388
- icon: entry.icon ?? null
389
- };
390
- return acc;
391
- },
392
- {}
393
- );
638
+ const currentMap = options.reduce((acc, entry) => {
639
+ acc[entry.value] = {
640
+ value: entry.value,
641
+ label: entry.label,
642
+ color: entry.color,
643
+ icon: entry.icon ?? null
644
+ };
645
+ return acc;
646
+ }, {});
394
647
  return options.filter(
395
648
  (option) => !term.length || option.label.toLowerCase().includes(term) || option.value.toLowerCase().includes(term)
396
649
  ).map((option) => ({
@@ -411,44 +664,80 @@ function LineItemDialog({
411
664
  }, [loadLineStatuses, loadTaxRates, open]);
412
665
  const handleFormSubmit = React.useCallback(
413
666
  async (values) => {
414
- console.groupCollapsed("sales.line.submit.start");
415
- console.log("raw values", values);
416
667
  const resolvedDocumentId = typeof documentId === "string" && documentId.trim().length ? documentId : null;
417
668
  const resolvedOrg = resolvedOrganizationId;
418
669
  const resolvedTenant = resolvedTenantId;
419
670
  if (!resolvedOrg || !resolvedTenant || !resolvedDocumentId) {
420
671
  throw createCrudFormError(
421
- t("sales.documents.items.errorScope", "Organization and tenant are required.")
672
+ t(
673
+ "sales.documents.items.errorScope",
674
+ "Organization and tenant are required."
675
+ )
422
676
  );
423
677
  }
424
678
  const lineMode2 = values.lineMode === "custom" ? "custom" : "catalog";
425
679
  const isCustomLine = lineMode2 === "custom";
426
680
  if (!isCustomLine && !values.productId) {
427
681
  throw createCrudFormError(
428
- t("sales.documents.items.errorProductRequired", "Select a product to continue."),
429
- { productId: t("sales.documents.items.errorProductRequired", "Select a product to continue.") }
682
+ t(
683
+ "sales.documents.items.errorProductRequired",
684
+ "Select a product to continue."
685
+ ),
686
+ {
687
+ productId: t(
688
+ "sales.documents.items.errorProductRequired",
689
+ "Select a product to continue."
690
+ )
691
+ }
430
692
  );
431
693
  }
432
694
  if (!isCustomLine && !values.variantId) {
433
695
  throw createCrudFormError(
434
- t("sales.documents.items.errorVariantRequired", "Select a variant to continue."),
435
- { variantId: t("sales.documents.items.errorVariantRequired", "Select a variant to continue.") }
696
+ t(
697
+ "sales.documents.items.errorVariantRequired",
698
+ "Select a variant to continue."
699
+ ),
700
+ {
701
+ variantId: t(
702
+ "sales.documents.items.errorVariantRequired",
703
+ "Select a variant to continue."
704
+ )
705
+ }
436
706
  );
437
707
  }
438
- const qtyNumber = Number(values.quantity ?? values.quantity ?? 0);
439
- console.log("quantity raw -> parsed", { raw: values.quantity, parsed: qtyNumber });
708
+ const qtyNumber = Number(values.quantity ?? 0);
440
709
  if (!Number.isFinite(qtyNumber) || qtyNumber <= 0) {
441
710
  throw createCrudFormError(
442
- t("sales.documents.items.errorQuantity", "Quantity must be greater than 0."),
443
- { quantity: t("sales.documents.items.errorQuantity", "Quantity must be greater than 0.") }
711
+ t(
712
+ "sales.documents.items.errorQuantity",
713
+ "Quantity must be greater than 0."
714
+ ),
715
+ {
716
+ quantity: t(
717
+ "sales.documents.items.errorQuantity",
718
+ "Quantity must be greater than 0."
719
+ )
720
+ }
444
721
  );
445
722
  }
446
- const unitPriceNumber = Number(values.unitPrice ?? values.unitPrice ?? 0);
447
- console.log("unit price raw -> parsed", { raw: values.unitPrice, parsed: unitPriceNumber });
723
+ const resolvedQuantityUnit = (() => {
724
+ const entered = normalizeUnitCode(values.quantityUnit);
725
+ if (isCustomLine) return entered;
726
+ return entered ?? normalizeUnitCode(productOption?.defaultSalesUnit) ?? normalizeUnitCode(productOption?.defaultUnit);
727
+ })();
728
+ const unitPriceNumber = Number(values.unitPrice ?? 0);
448
729
  if (!Number.isFinite(unitPriceNumber) || unitPriceNumber <= 0) {
449
730
  throw createCrudFormError(
450
- t("sales.documents.items.errorUnitPrice", "Unit price must be greater than 0."),
451
- { unitPrice: t("sales.documents.items.errorUnitPrice", "Unit price must be greater than 0.") }
731
+ t(
732
+ "sales.documents.items.errorUnitPrice",
733
+ "Unit price must be greater than 0."
734
+ ),
735
+ {
736
+ unitPrice: t(
737
+ "sales.documents.items.errorUnitPrice",
738
+ "Unit price must be greater than 0."
739
+ )
740
+ }
452
741
  );
453
742
  }
454
743
  const selectedPrice = !isCustomLine && values.priceId ? priceOptions.find((price) => price.id === values.priceId) ?? null : null;
@@ -456,15 +745,28 @@ function LineItemDialog({
456
745
  if (!resolvedCurrency) {
457
746
  throw createCrudFormError(
458
747
  t("sales.documents.items.errorCurrency", "Currency is required."),
459
- { priceId: t("sales.documents.items.errorCurrency", "Currency is required.") }
748
+ {
749
+ priceId: t(
750
+ "sales.documents.items.errorCurrency",
751
+ "Currency is required."
752
+ )
753
+ }
460
754
  );
461
755
  }
462
756
  const resolvedNameRaw = (values.name ?? "").toString().trim();
463
757
  const resolvedName = isCustomLine ? resolvedNameRaw : resolvedNameRaw || variantOption?.title || productOption?.title || void 0;
464
758
  if (isCustomLine && !resolvedName) {
465
759
  throw createCrudFormError(
466
- t("sales.documents.items.errorNameRequired", "Name is required for custom lines."),
467
- { name: t("sales.documents.items.errorNameRequired", "Name is required for custom lines.") }
760
+ t(
761
+ "sales.documents.items.errorNameRequired",
762
+ "Name is required for custom lines."
763
+ ),
764
+ {
765
+ name: t(
766
+ "sales.documents.items.errorNameRequired",
767
+ "Name is required for custom lines."
768
+ )
769
+ }
468
770
  );
469
771
  }
470
772
  const resolvedPriceMode = values.priceMode === "net" ? "net" : "gross";
@@ -494,7 +796,8 @@ function LineItemDialog({
494
796
  variantThumbnail: variantOption.thumbnailUrl ?? productOption?.thumbnailUrl ?? null
495
797
  } : {},
496
798
  ...isCustomLine ? { customLine: true } : {},
497
- lineMode: lineMode2
799
+ lineMode: lineMode2,
800
+ ...resolvedQuantityUnit ? { quantityUnit: resolvedQuantityUnit } : {}
498
801
  };
499
802
  const payload = {
500
803
  [documentKey]: String(resolvedDocumentId),
@@ -503,6 +806,7 @@ function LineItemDialog({
503
806
  productId: isCustomLine ? void 0 : values.productId ? String(values.productId) : void 0,
504
807
  productVariantId: isCustomLine ? void 0 : values.variantId ? String(values.variantId) : void 0,
505
808
  quantity: qtyNumber,
809
+ quantityUnit: resolvedQuantityUnit ?? void 0,
506
810
  currencyCode: String(resolvedCurrency),
507
811
  priceId: !isCustomLine && values.priceId ? String(values.priceId) : void 0,
508
812
  priceMode: resolvedPriceMode,
@@ -523,18 +827,16 @@ function LineItemDialog({
523
827
  payload.customFields = normalizeCustomFieldValues(customFields);
524
828
  }
525
829
  if (resolvedName) payload.name = resolvedName;
526
- console.debug("resolved scope", { resolvedDocumentId, resolvedOrg, resolvedTenant, resolvedCurrency });
527
- console.debug("parsed numbers", { qtyNumber, unitPriceNumber });
528
- console.log("sales.line.submit.payload", payload);
529
- console.log("sales.line.submit.payload.json", JSON.stringify(payload));
530
- console.groupEnd();
531
830
  try {
532
831
  const action = editingId ? updateCrud : createCrud;
533
832
  const result = await action(
534
833
  resourcePath,
535
834
  editingId ? { id: editingId, ...payload } : payload,
536
835
  {
537
- errorMessage: t("sales.documents.items.errorSave", "Failed to save line.")
836
+ errorMessage: t(
837
+ "sales.documents.items.errorSave",
838
+ "Failed to save line."
839
+ )
538
840
  }
539
841
  );
540
842
  if (result.ok) {
@@ -542,7 +844,6 @@ function LineItemDialog({
542
844
  closeDialog();
543
845
  }
544
846
  } catch (err) {
545
- console.error("sales.line.submit.error", err);
546
847
  throw err;
547
848
  }
548
849
  },
@@ -580,10 +881,12 @@ function LineItemDialog({
580
881
  setProductOption(null);
581
882
  setVariantOption(null);
582
883
  setPriceOptions([]);
884
+ setUnitOptions([]);
583
885
  setFormValue?.("productId", null);
584
886
  setFormValue?.("variantId", null);
585
887
  setFormValue?.("priceId", null);
586
888
  setFormValue?.("catalogSnapshot", null);
889
+ setFormValue?.("quantityUnit", null);
587
890
  } else {
588
891
  setFormValue?.("unitPrice", "");
589
892
  setFormValue?.("priceMode", "gross");
@@ -626,7 +929,12 @@ function LineItemDialog({
626
929
  type: "custom",
627
930
  required: true,
628
931
  layout: "half",
629
- component: ({ value, setValue, setFormValue, values }) => /* @__PURE__ */ jsx(
932
+ component: ({
933
+ value,
934
+ setValue,
935
+ setFormValue,
936
+ values
937
+ }) => /* @__PURE__ */ jsx(
630
938
  LookupSelect,
631
939
  {
632
940
  value: typeof value === "string" ? value : null,
@@ -635,11 +943,22 @@ function LineItemDialog({
635
943
  setProductOption(selectedOption);
636
944
  setVariantOption(null);
637
945
  setPriceOptions([]);
946
+ setUnitOptions([]);
638
947
  setValue(next ?? null);
639
948
  setFormValue?.("variantId", null);
640
949
  setFormValue?.("priceId", null);
641
950
  setFormValue?.("unitPrice", "");
642
951
  setFormValue?.("priceMode", "gross");
952
+ const defaultQuantityUnit = selectedOption?.defaultSalesUnit ?? selectedOption?.defaultUnit ?? null;
953
+ setFormValue?.("quantityUnit", defaultQuantityUnit);
954
+ if (typeof selectedOption?.defaultSalesUnitQuantity === "number" && Number.isFinite(
955
+ selectedOption.defaultSalesUnitQuantity
956
+ ) && selectedOption.defaultSalesUnitQuantity > 0) {
957
+ setFormValue?.(
958
+ "quantity",
959
+ String(selectedOption.defaultSalesUnitQuantity)
960
+ );
961
+ }
643
962
  const taxSelection = selectedOption ? resolveTaxSelection(selectedOption) : { taxRate: null, taxRateId: null };
644
963
  setFormValue?.("taxRate", taxSelection.taxRate ?? null);
645
964
  setFormValue?.("taxRateId", taxSelection.taxRateId ?? null);
@@ -654,12 +973,20 @@ function LineItemDialog({
654
973
  id: next,
655
974
  title: selectedOption?.title ?? null,
656
975
  sku: selectedOption?.sku ?? null,
657
- thumbnailUrl: selectedOption?.thumbnailUrl ?? null
976
+ thumbnailUrl: selectedOption?.thumbnailUrl ?? null,
977
+ defaultUnit: selectedOption?.defaultUnit ?? null,
978
+ defaultSalesUnit: selectedOption?.defaultSalesUnit ?? null
658
979
  }
659
980
  } : null
660
981
  );
661
982
  if (next) {
662
- void loadPrices(next, null);
983
+ void loadProductUnits(next, selectedOption);
984
+ void loadPrices(
985
+ next,
986
+ null,
987
+ typeof values?.quantity === "string" ? values.quantity : 1,
988
+ defaultQuantityUnit
989
+ );
663
990
  }
664
991
  },
665
992
  fetchItems: loadProductOptions,
@@ -668,20 +995,42 @@ function LineItemDialog({
668
995
  id: productOption.id,
669
996
  title: productOption.title || productOption.id,
670
997
  subtitle: productOption.sku ?? void 0,
671
- icon: productOption.thumbnailUrl ? /* @__PURE__ */ jsx("img", { src: productOption.thumbnailUrl, alt: productOption.title ?? productOption.id, className: "h-8 w-8 rounded object-cover" }) : buildPlaceholder(productOption.title || productOption.id)
998
+ icon: productOption.thumbnailUrl ? /* @__PURE__ */ jsx(
999
+ "img",
1000
+ {
1001
+ src: productOption.thumbnailUrl,
1002
+ alt: productOption.title ?? productOption.id,
1003
+ className: "h-8 w-8 rounded object-cover"
1004
+ }
1005
+ ) : buildPlaceholder(
1006
+ productOption.title || productOption.id
1007
+ )
672
1008
  }
673
1009
  ] : void 0,
674
1010
  minQuery: 1,
675
- searchPlaceholder: t("sales.documents.items.productSearch", "Search product"),
1011
+ searchPlaceholder: t(
1012
+ "sales.documents.items.productSearch",
1013
+ "Search product"
1014
+ ),
676
1015
  selectLabel: t("ui.lookupSelect.select", "Select"),
677
1016
  selectedLabel: t("ui.lookupSelect.selected", "Selected"),
678
- clearLabel: t("ui.lookupSelect.clearSelection", "Clear selection"),
1017
+ clearLabel: t(
1018
+ "ui.lookupSelect.clearSelection",
1019
+ "Clear selection"
1020
+ ),
679
1021
  emptyLabel: t("ui.lookupSelect.noResults", "No results"),
680
1022
  loadingLabel: t("ui.lookupSelect.searching", "Searching\u2026"),
681
- startTypingLabel: t("ui.lookupSelect.startTyping", "Start typing to search."),
682
- selectedHintLabel: (id) => t("sales.documents.items.selectedProduct", "Selected {{id}}", {
683
- id: productOption?.title ?? id
684
- })
1023
+ startTypingLabel: t(
1024
+ "ui.lookupSelect.startTyping",
1025
+ "Start typing to search."
1026
+ ),
1027
+ selectedHintLabel: (id) => t(
1028
+ "sales.documents.items.selectedProduct",
1029
+ "Selected {{id}}",
1030
+ {
1031
+ id: productOption?.title ?? id
1032
+ }
1033
+ )
685
1034
  }
686
1035
  )
687
1036
  },
@@ -691,7 +1040,12 @@ function LineItemDialog({
691
1040
  type: "custom",
692
1041
  required: true,
693
1042
  layout: "half",
694
- component: ({ value, setValue, setFormValue, values }) => {
1043
+ component: ({
1044
+ value,
1045
+ setValue,
1046
+ setFormValue,
1047
+ values
1048
+ }) => {
695
1049
  const productId = typeof values?.productId === "string" ? values.productId : null;
696
1050
  return /* @__PURE__ */ jsx(
697
1051
  LookupSelect,
@@ -703,13 +1057,19 @@ function LineItemDialog({
703
1057
  setValue(next ?? null);
704
1058
  const existingName = typeof values?.name === "string" ? values.name : "";
705
1059
  if (!existingName.trim()) {
706
- setFormValue?.("name", selectedOption?.title ?? productOption?.title ?? existingName);
1060
+ setFormValue?.(
1061
+ "name",
1062
+ selectedOption?.title ?? productOption?.title ?? existingName
1063
+ );
707
1064
  }
708
1065
  const taxSource = hasTaxMetadata(selectedOption) ? selectedOption : hasTaxMetadata(productOption) ? productOption : null;
709
1066
  if (taxSource) {
710
1067
  const taxSelection = resolveTaxSelection(taxSource);
711
1068
  setFormValue?.("taxRate", taxSelection.taxRate ?? null);
712
- setFormValue?.("taxRateId", taxSelection.taxRateId ?? null);
1069
+ setFormValue?.(
1070
+ "taxRateId",
1071
+ taxSelection.taxRateId ?? null
1072
+ );
713
1073
  }
714
1074
  const prevSnapshot = typeof values?.catalogSnapshot === "object" && values.catalogSnapshot ? values.catalogSnapshot : null;
715
1075
  if (next) {
@@ -724,29 +1084,54 @@ function LineItemDialog({
724
1084
  });
725
1085
  } else if (prevSnapshot) {
726
1086
  const snapshot = { ...prevSnapshot };
727
- if ("variant" in snapshot) delete snapshot.variant;
728
- setFormValue?.("catalogSnapshot", Object.keys(snapshot).length ? snapshot : null);
1087
+ if ("variant" in snapshot)
1088
+ delete snapshot.variant;
1089
+ setFormValue?.(
1090
+ "catalogSnapshot",
1091
+ Object.keys(snapshot).length ? snapshot : null
1092
+ );
729
1093
  } else {
730
1094
  setFormValue?.("catalogSnapshot", null);
731
1095
  }
732
1096
  if (productId) {
733
- void loadPrices(productId, next);
1097
+ const currentQuantity = typeof values?.quantity === "string" ? values.quantity : 1;
1098
+ const currentQuantityUnit = typeof values?.quantityUnit === "string" ? values.quantityUnit : null;
1099
+ void loadPrices(
1100
+ productId,
1101
+ next,
1102
+ currentQuantity,
1103
+ currentQuantityUnit
1104
+ );
734
1105
  }
735
1106
  },
736
1107
  fetchItems: async (query) => {
737
1108
  if (!productId) return [];
738
1109
  const productThumb = productId ? productOptionsRef.current.get(productId)?.thumbnailUrl : null;
739
- const options = await loadVariantOptions(productId, productThumb);
1110
+ const options = await loadVariantOptions(
1111
+ productId,
1112
+ productThumb
1113
+ );
740
1114
  const needle = query?.trim().toLowerCase() ?? "";
741
- return needle.length ? options.filter((option) => option.title.toLowerCase().includes(needle)) : options;
1115
+ return needle.length ? options.filter(
1116
+ (option) => option.title.toLowerCase().includes(needle)
1117
+ ) : options;
742
1118
  },
743
- searchPlaceholder: t("sales.documents.items.variantSearch", "Search variant"),
1119
+ searchPlaceholder: t(
1120
+ "sales.documents.items.variantSearch",
1121
+ "Search variant"
1122
+ ),
744
1123
  selectLabel: t("ui.lookupSelect.select", "Select"),
745
1124
  selectedLabel: t("ui.lookupSelect.selected", "Selected"),
746
- clearLabel: t("ui.lookupSelect.clearSelection", "Clear selection"),
1125
+ clearLabel: t(
1126
+ "ui.lookupSelect.clearSelection",
1127
+ "Clear selection"
1128
+ ),
747
1129
  emptyLabel: t("ui.lookupSelect.noResults", "No results"),
748
1130
  loadingLabel: t("ui.lookupSelect.searching", "Searching\u2026"),
749
- startTypingLabel: t("ui.lookupSelect.startTyping", "Start typing to search."),
1131
+ startTypingLabel: t(
1132
+ "ui.lookupSelect.startTyping",
1133
+ "Start typing to search."
1134
+ ),
750
1135
  minQuery: 0,
751
1136
  options: variantOption ? [
752
1137
  {
@@ -760,12 +1145,18 @@ function LineItemDialog({
760
1145
  alt: variantOption.title ?? variantOption.id,
761
1146
  className: "h-8 w-8 rounded object-cover"
762
1147
  }
763
- ) : buildPlaceholder(variantOption.title || variantOption.id)
1148
+ ) : buildPlaceholder(
1149
+ variantOption.title || variantOption.id
1150
+ )
764
1151
  }
765
1152
  ] : void 0,
766
- selectedHintLabel: (id) => t("sales.documents.items.selectedVariant", "Selected {{id}}", {
767
- id: variantOption?.title ?? id
768
- }),
1153
+ selectedHintLabel: (id) => t(
1154
+ "sales.documents.items.selectedVariant",
1155
+ "Selected {{id}}",
1156
+ {
1157
+ id: variantOption?.title ?? id
1158
+ }
1159
+ ),
769
1160
  disabled: !productId
770
1161
  },
771
1162
  productId ?? "no-product"
@@ -777,7 +1168,12 @@ function LineItemDialog({
777
1168
  label: t("sales.documents.items.price", "Price"),
778
1169
  type: "custom",
779
1170
  layout: "half",
780
- component: ({ value, setValue, setFormValue, values }) => {
1171
+ component: ({
1172
+ value,
1173
+ setValue,
1174
+ setFormValue,
1175
+ values
1176
+ }) => {
781
1177
  const productId = typeof values?.productId === "string" ? values.productId : null;
782
1178
  const variantId = typeof values?.variantId === "string" ? values.variantId : null;
783
1179
  return /* @__PURE__ */ jsx(
@@ -787,26 +1183,22 @@ function LineItemDialog({
787
1183
  onChange: (next) => {
788
1184
  setValue(next ?? null);
789
1185
  const selected = next ? priceOptions.find((entry) => entry.id === next) ?? null : null;
790
- if (selected) {
791
- const mode = selected.displayMode === "excluding-tax" ? "net" : "gross";
792
- const amount = mode === "net" ? selected.amountNet ?? selected.amountGross ?? 0 : selected.amountGross ?? selected.amountNet ?? 0;
793
- setFormValue?.("priceMode", mode);
794
- setFormValue?.("unitPrice", amount.toString());
795
- setFormValue?.("taxRate", selected.taxRate ?? null);
796
- const matchedRateId = findTaxRateIdByValue(selected.taxRate);
797
- setFormValue?.("taxRateId", matchedRateId);
798
- setFormValue?.(
799
- "currencyCode",
800
- selected.currencyCode ?? values?.currencyCode ?? currencyCode ?? null
801
- );
802
- } else {
803
- const fallbackTax = resolveTaxSelection(variantOption ?? productOption ?? null);
804
- setFormValue?.("taxRate", fallbackTax.taxRate ?? null);
805
- setFormValue?.("taxRateId", fallbackTax.taxRateId ?? null);
806
- }
1186
+ applyPriceSelection(
1187
+ selected,
1188
+ setFormValue,
1189
+ {
1190
+ fallbackTaxSource: variantOption ?? productOption ?? null,
1191
+ quantityUnit: typeof values?.quantityUnit === "string" ? values.quantityUnit : null
1192
+ }
1193
+ );
807
1194
  },
808
1195
  fetchItems: async (query) => {
809
- const prices = await loadPrices(productId, variantId);
1196
+ const prices = await loadPrices(
1197
+ productId,
1198
+ variantId,
1199
+ typeof values?.quantity === "string" ? values.quantity : 1,
1200
+ typeof values?.quantityUnit === "string" ? values.quantityUnit : null
1201
+ );
810
1202
  const needle = query?.trim().toLowerCase() ?? "";
811
1203
  return prices.filter((price) => {
812
1204
  if (!needle.length) return true;
@@ -829,13 +1221,22 @@ function LineItemDialog({
829
1221
  },
830
1222
  minQuery: 0,
831
1223
  loading: priceLoading,
832
- searchPlaceholder: t("sales.documents.items.priceSearch", "Select price"),
1224
+ searchPlaceholder: t(
1225
+ "sales.documents.items.priceSearch",
1226
+ "Select price"
1227
+ ),
833
1228
  selectLabel: t("ui.lookupSelect.select", "Select"),
834
1229
  selectedLabel: t("ui.lookupSelect.selected", "Selected"),
835
- clearLabel: t("ui.lookupSelect.clearSelection", "Clear selection"),
1230
+ clearLabel: t(
1231
+ "ui.lookupSelect.clearSelection",
1232
+ "Clear selection"
1233
+ ),
836
1234
  emptyLabel: t("ui.lookupSelect.noResults", "No results"),
837
1235
  loadingLabel: t("ui.lookupSelect.searching", "Searching\u2026"),
838
- startTypingLabel: t("ui.lookupSelect.startTyping", "Start typing to search."),
1236
+ startTypingLabel: t(
1237
+ "ui.lookupSelect.startTyping",
1238
+ "Start typing to search."
1239
+ ),
839
1240
  disabled: !productId
840
1241
  },
841
1242
  productId ? `${productId}-${variantId ?? "no-variant"}` : "price"
@@ -848,32 +1249,72 @@ function LineItemDialog({
848
1249
  label: t("sales.documents.items.unitPrice", "Unit price"),
849
1250
  type: "custom",
850
1251
  layout: "half",
851
- component: ({ value, setValue, setFormValue, values }) => {
1252
+ component: ({
1253
+ value,
1254
+ setValue,
1255
+ setFormValue,
1256
+ values
1257
+ }) => {
852
1258
  const mode = values?.priceMode === "net" ? "net" : "gross";
853
- return /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
854
- /* @__PURE__ */ jsx(
855
- Input,
856
- {
857
- value: typeof value === "string" ? value : value == null ? "" : String(value),
858
- onChange: (event) => setValue(event.target.value),
859
- placeholder: "0.00"
860
- }
861
- ),
862
- /* @__PURE__ */ jsxs(
863
- "select",
1259
+ const selectedPriceId = typeof values?.priceId === "string" ? values.priceId : null;
1260
+ const selectedPrice = selectedPriceId ? priceOptions.find((entry) => entry.id === selectedPriceId) ?? null : null;
1261
+ const selectedCurrency = selectedPrice?.currencyCode ?? (typeof values?.currencyCode === "string" ? values.currencyCode : null);
1262
+ const quantityUnitCode = normalizeUnitCode(values?.quantityUnit);
1263
+ const baseUnitCode = unitOptions.find((option) => option.isBase)?.code ?? null;
1264
+ const selectedUnitOption = quantityUnitCode ? unitOptions.find((option) => option.code === quantityUnitCode) ?? null : null;
1265
+ const unitFactor = (() => {
1266
+ if (!quantityUnitCode) return null;
1267
+ if (baseUnitCode && quantityUnitCode === baseUnitCode) return 1;
1268
+ const value2 = normalizeNumber(selectedUnitOption?.toBaseFactor, Number.NaN);
1269
+ return Number.isFinite(value2) && value2 > 0 ? value2 : null;
1270
+ })();
1271
+ const selectedBaseAmount = selectedPrice ? mode === "net" ? selectedPrice.amountNet ?? selectedPrice.amountGross ?? null : selectedPrice.amountGross ?? selectedPrice.amountNet ?? null : null;
1272
+ const convertedAmount = selectedBaseAmount !== null && unitFactor !== null && Number.isFinite(selectedBaseAmount * unitFactor) ? selectedBaseAmount * unitFactor : null;
1273
+ const isCatalogLine = lineMode !== "custom";
1274
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1275
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
1276
+ /* @__PURE__ */ jsx(
1277
+ Input,
1278
+ {
1279
+ value: typeof value === "string" ? value : value == null ? "" : String(value),
1280
+ onChange: (event) => setValue(event.target.value),
1281
+ placeholder: "0.00"
1282
+ }
1283
+ ),
1284
+ /* @__PURE__ */ jsxs(
1285
+ "select",
1286
+ {
1287
+ className: "w-32 rounded border px-2 text-sm",
1288
+ value: mode,
1289
+ onChange: (event) => {
1290
+ const nextMode = event.target.value === "net" ? "net" : "gross";
1291
+ setFormValue?.("priceMode", nextMode);
1292
+ },
1293
+ children: [
1294
+ /* @__PURE__ */ jsx("option", { value: "gross", children: t("sales.documents.items.priceGross", "Gross") }),
1295
+ /* @__PURE__ */ jsx("option", { value: "net", children: t("sales.documents.items.priceNet", "Net") })
1296
+ ]
1297
+ }
1298
+ )
1299
+ ] }),
1300
+ isCatalogLine && selectedPrice && quantityUnitCode && baseUnitCode ? unitFactor !== null && convertedAmount !== null ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t(
1301
+ "sales.documents.items.priceBasisTemplate",
1302
+ "Catalog price basis: {{baseAmount}} / {{baseUnit}}. Converted for {{unit}}: {{baseAmount}} \xD7 {{factor}} = {{convertedAmount}}.",
864
1303
  {
865
- className: "w-32 rounded border px-2 text-sm",
866
- value: mode,
867
- onChange: (event) => {
868
- const nextMode = event.target.value === "net" ? "net" : "gross";
869
- setFormValue?.("priceMode", nextMode);
870
- },
871
- children: [
872
- /* @__PURE__ */ jsx("option", { value: "gross", children: t("sales.documents.items.priceGross", "Gross") }),
873
- /* @__PURE__ */ jsx("option", { value: "net", children: t("sales.documents.items.priceNet", "Net") })
874
- ]
1304
+ baseAmount: formatMoney(selectedBaseAmount, selectedCurrency),
1305
+ baseUnit: baseUnitCode,
1306
+ unit: quantityUnitCode,
1307
+ factor: unitFactor,
1308
+ convertedAmount: formatMoney(convertedAmount, selectedCurrency)
875
1309
  }
876
- )
1310
+ ) }) : /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t(
1311
+ "sales.documents.items.priceBasisMissingConversion",
1312
+ "Catalog price basis is base unit, but conversion for selected unit is missing."
1313
+ ) }) : null,
1314
+ isCatalogLine && !selectedPrice ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t(
1315
+ "sales.documents.items.priceBasisManual",
1316
+ "Manual unit price mode. Price will not auto-convert until you select a catalog price."
1317
+ ) }) : null
877
1318
  ] });
878
1319
  }
879
1320
  },
@@ -882,7 +1323,12 @@ function LineItemDialog({
882
1323
  label: t("sales.documents.items.taxRate", "Tax class"),
883
1324
  type: "custom",
884
1325
  layout: "half",
885
- component: ({ value, setValue, setFormValue, values }) => {
1326
+ component: ({
1327
+ value,
1328
+ setValue,
1329
+ setFormValue,
1330
+ values
1331
+ }) => {
886
1332
  const resolvedValue = typeof value === "string" && value.trim().length ? value : findTaxRateIdByValue(values?.taxRate);
887
1333
  const handleChange = (event) => {
888
1334
  const nextId = event.target.value || null;
@@ -900,7 +1346,13 @@ function LineItemDialog({
900
1346
  onChange: handleChange,
901
1347
  disabled: !taxRates.length,
902
1348
  children: [
903
- /* @__PURE__ */ jsx("option", { value: "", children: taxRates.length ? t("sales.documents.items.taxRate.none", "No tax class selected") : t("sales.documents.items.taxRate.empty", "No tax classes available") }),
1349
+ /* @__PURE__ */ jsx("option", { value: "", children: taxRates.length ? t(
1350
+ "sales.documents.items.taxRate.none",
1351
+ "No tax class selected"
1352
+ ) : t(
1353
+ "sales.documents.items.taxRate.empty",
1354
+ "No tax classes available"
1355
+ ) }),
904
1356
  taxRates.map((rate) => /* @__PURE__ */ jsxs("option", { value: rate.id, children: [
905
1357
  rate.name,
906
1358
  rate.code ? ` \u2022 ${rate.code.toUpperCase()}` : "",
@@ -917,30 +1369,187 @@ function LineItemDialog({
917
1369
  size: "icon",
918
1370
  onClick: () => {
919
1371
  if (typeof window !== "undefined") {
920
- window.open("/backend/config/sales?section=tax-rates", "_blank", "noopener,noreferrer");
1372
+ window.open(
1373
+ "/backend/config/sales?section=tax-rates",
1374
+ "_blank",
1375
+ "noopener,noreferrer"
1376
+ );
921
1377
  }
922
1378
  },
923
- title: t("catalog.products.create.taxRates.manage", "Manage tax classes"),
1379
+ title: t(
1380
+ "catalog.products.create.taxRates.manage",
1381
+ "Manage tax classes"
1382
+ ),
924
1383
  children: /* @__PURE__ */ jsx(Settings, { className: "h-4 w-4" })
925
1384
  }
926
1385
  )
927
1386
  ] });
928
1387
  }
929
1388
  },
1389
+ {
1390
+ id: "quantityUnit",
1391
+ label: t("sales.documents.items.quantityUnit", "Unit"),
1392
+ type: "custom",
1393
+ layout: "half",
1394
+ component: ({ value, setValue, setFormValue, values }) => {
1395
+ const productId = typeof values?.productId === "string" ? values.productId : null;
1396
+ const variantId = typeof values?.variantId === "string" ? values.variantId : null;
1397
+ if (isCustomLine) {
1398
+ return /* @__PURE__ */ jsx(
1399
+ Input,
1400
+ {
1401
+ value: typeof value === "string" ? value : "",
1402
+ onChange: (event) => setValue(event.target.value || null),
1403
+ placeholder: t(
1404
+ "sales.documents.items.quantityUnitPlaceholder",
1405
+ "e.g. pc"
1406
+ )
1407
+ }
1408
+ );
1409
+ }
1410
+ return /* @__PURE__ */ jsxs(
1411
+ "select",
1412
+ {
1413
+ 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",
1414
+ value: typeof value === "string" ? value : "",
1415
+ onChange: (event) => {
1416
+ const nextValue = event.target.value || null;
1417
+ const nextUnitPrice = convertUnitPriceForUnitChange(
1418
+ values?.unitPrice,
1419
+ typeof value === "string" ? value : null,
1420
+ nextValue
1421
+ );
1422
+ setValue(nextValue);
1423
+ if (nextUnitPrice) {
1424
+ setFormValue?.("unitPrice", nextUnitPrice);
1425
+ }
1426
+ if (productId) {
1427
+ const selectedPriceId = typeof values?.priceId === "string" ? values.priceId : null;
1428
+ const selectedPriceKindId = selectedPriceId ? priceOptions.find(
1429
+ (entry) => entry.id === selectedPriceId
1430
+ )?.priceKindId ?? null : null;
1431
+ void loadPrices(
1432
+ productId,
1433
+ variantId,
1434
+ typeof values?.quantity === "string" ? values.quantity : 1,
1435
+ nextValue
1436
+ ).then((prices) => {
1437
+ if (!selectedPriceId) return;
1438
+ const nextSelected = selectPriceAfterRefresh(
1439
+ prices,
1440
+ selectedPriceId,
1441
+ selectedPriceKindId
1442
+ );
1443
+ if (!nextSelected) {
1444
+ setFormValue?.("priceId", null);
1445
+ return;
1446
+ }
1447
+ applyPriceSelection(
1448
+ nextSelected,
1449
+ setFormValue,
1450
+ {
1451
+ fallbackTaxSource: variantOption ?? productOption ?? null,
1452
+ quantityUnit: nextValue
1453
+ }
1454
+ );
1455
+ });
1456
+ }
1457
+ },
1458
+ disabled: !productId,
1459
+ children: [
1460
+ /* @__PURE__ */ jsx("option", { value: "", children: t("sales.documents.items.quantityUnitSelect", "Select unit") }),
1461
+ unitOptions.map((option) => /* @__PURE__ */ jsx("option", { value: option.code, children: option.isBase ? `${option.code} (${t("sales.documents.items.baseUnitTag", "base")})` : option.code }, option.code))
1462
+ ]
1463
+ }
1464
+ );
1465
+ }
1466
+ },
930
1467
  {
931
1468
  id: "quantity",
932
1469
  label: t("sales.documents.items.quantity", "Quantity"),
933
1470
  type: "custom",
934
1471
  layout: "half",
935
- component: ({ value, setValue }) => /* @__PURE__ */ jsx(
1472
+ component: ({ value, setValue, setFormValue, values }) => /* @__PURE__ */ jsx(
936
1473
  Input,
937
1474
  {
938
1475
  value: typeof value === "string" ? value : value == null ? "" : String(value),
939
- onChange: (event) => setValue(event.target.value),
1476
+ onChange: (event) => {
1477
+ const nextQuantity = event.target.value;
1478
+ setValue(nextQuantity);
1479
+ const productId = typeof values?.productId === "string" ? values.productId : null;
1480
+ const variantId = typeof values?.variantId === "string" ? values.variantId : null;
1481
+ const quantityUnit = typeof values?.quantityUnit === "string" ? values.quantityUnit : null;
1482
+ if (productId) {
1483
+ const selectedPriceId = typeof values?.priceId === "string" ? values.priceId : null;
1484
+ const selectedPriceKindId = selectedPriceId ? priceOptions.find((entry) => entry.id === selectedPriceId)?.priceKindId ?? null : null;
1485
+ void loadPrices(
1486
+ productId,
1487
+ variantId,
1488
+ nextQuantity,
1489
+ quantityUnit
1490
+ ).then((prices) => {
1491
+ if (!selectedPriceId) return;
1492
+ const nextSelected = selectPriceAfterRefresh(
1493
+ prices,
1494
+ selectedPriceId,
1495
+ selectedPriceKindId
1496
+ );
1497
+ if (!nextSelected) {
1498
+ setFormValue?.("priceId", null);
1499
+ return;
1500
+ }
1501
+ applyPriceSelection(
1502
+ nextSelected,
1503
+ setFormValue,
1504
+ {
1505
+ fallbackTaxSource: variantOption ?? productOption ?? null,
1506
+ quantityUnit
1507
+ }
1508
+ );
1509
+ });
1510
+ }
1511
+ },
940
1512
  placeholder: "1"
941
1513
  }
942
1514
  )
943
1515
  },
1516
+ {
1517
+ id: "uomPreview",
1518
+ label: t("sales.documents.items.uomPreview", "Normalized quantity"),
1519
+ type: "custom",
1520
+ layout: "full",
1521
+ component: ({ values }) => {
1522
+ if (isCustomLine) return null;
1523
+ const quantity = normalizeNumber(values?.quantity, Number.NaN);
1524
+ const enteredUnit = normalizeUnitCode(values?.quantityUnit);
1525
+ if (!Number.isFinite(quantity) || quantity <= 0 || !enteredUnit) {
1526
+ return /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t(
1527
+ "sales.documents.items.uomPreviewEmpty",
1528
+ "Select unit and quantity to preview normalization."
1529
+ ) });
1530
+ }
1531
+ const selectedOption = unitOptions.find((option) => option.code === enteredUnit) ?? null;
1532
+ const baseOption = unitOptions.find((option) => option.isBase) ?? null;
1533
+ const factor = selectedOption?.toBaseFactor;
1534
+ if (!Number.isFinite(factor) || !factor || !baseOption) {
1535
+ return /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t(
1536
+ "sales.documents.items.uomPreviewUnavailable",
1537
+ "Missing conversion to base unit."
1538
+ ) });
1539
+ }
1540
+ const normalized = normalizeQuantityPreview(quantity * factor);
1541
+ return /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t(
1542
+ "sales.documents.items.uomPreviewTemplate",
1543
+ "{{quantity}} {{unit}} \u2192 {{normalized}} {{baseUnit}}",
1544
+ {
1545
+ quantity,
1546
+ unit: enteredUnit,
1547
+ normalized,
1548
+ baseUnit: baseOption.code
1549
+ }
1550
+ ) });
1551
+ }
1552
+ },
944
1553
  {
945
1554
  id: "statusEntryId",
946
1555
  label: t("sales.documents.items.status", "Status"),
@@ -951,14 +1560,23 @@ function LineItemDialog({
951
1560
  {
952
1561
  value: typeof value === "string" ? value : null,
953
1562
  onChange: (next) => setValue(next ?? null),
954
- placeholder: t("sales.documents.items.statusPlaceholder", "Select status"),
1563
+ placeholder: t(
1564
+ "sales.documents.items.statusPlaceholder",
1565
+ "Select status"
1566
+ ),
955
1567
  emptyLabel: t("sales.documents.items.statusEmpty", "No status"),
956
1568
  fetchItems: fetchLineStatusItems,
957
- loadingLabel: t("sales.documents.items.statusLoading", "Loading statuses\u2026"),
1569
+ loadingLabel: t(
1570
+ "sales.documents.items.statusLoading",
1571
+ "Loading statuses\u2026"
1572
+ ),
958
1573
  selectLabel: t("ui.lookupSelect.select", "Select"),
959
1574
  selectedLabel: t("ui.lookupSelect.selected", "Selected"),
960
1575
  clearLabel: t("ui.lookupSelect.clearSelection", "Clear selection"),
961
- startTypingLabel: t("ui.lookupSelect.startTyping", "Start typing to search."),
1576
+ startTypingLabel: t(
1577
+ "ui.lookupSelect.startTyping",
1578
+ "Start typing to search."
1579
+ ),
962
1580
  minQuery: 0
963
1581
  }
964
1582
  )
@@ -967,27 +1585,35 @@ function LineItemDialog({
967
1585
  id: "name",
968
1586
  label: t("sales.documents.items.name", "Name"),
969
1587
  type: "text",
970
- placeholder: t("sales.documents.items.namePlaceholder", "Optional line name"),
1588
+ placeholder: t(
1589
+ "sales.documents.items.namePlaceholder",
1590
+ "Optional line name"
1591
+ ),
971
1592
  layout: "full",
972
1593
  required: isCustomLine
973
1594
  }
974
1595
  ];
975
1596
  }, [
1597
+ applyPriceSelection,
1598
+ convertUnitPriceForUnitChange,
976
1599
  currencyCode,
977
1600
  findTaxRateIdByValue,
978
1601
  loadPrices,
1602
+ loadProductUnits,
979
1603
  loadProductOptions,
980
1604
  loadVariantOptions,
981
1605
  fetchLineStatusItems,
982
1606
  priceLoading,
983
1607
  priceOptions,
984
1608
  productOption,
1609
+ unitOptions,
985
1610
  lineMode,
986
1611
  variantOption,
987
1612
  t,
988
1613
  taxRateMap,
989
- taxRates.length,
1614
+ taxRates,
990
1615
  resolveTaxSelection,
1616
+ selectPriceAfterRefresh,
991
1617
  hasTaxMetadata
992
1618
  ]);
993
1619
  const groups = React.useMemo(() => {
@@ -1013,11 +1639,13 @@ function LineItemDialog({
1013
1639
  const snapshot = initialLine.catalogSnapshot ?? null;
1014
1640
  const snapshotProduct = snapshot && typeof snapshot === "object" && typeof snapshot.product === "object" && snapshot.product ? snapshot.product : null;
1015
1641
  const snapshotVariant = snapshot && typeof snapshot === "object" && typeof snapshot.variant === "object" && snapshot.variant ? snapshot.variant : null;
1016
- const metaLineMode = typeof meta?.lineMode === "string" && (meta.lineMode === "custom" || meta.lineMode === "catalog") ? meta.lineMode : meta?.customLine ? "custom" : void 0;
1642
+ const metaRec = typeof meta === "object" && meta ? meta : null;
1643
+ const metaLineMode = typeof metaRec?.lineMode === "string" && (metaRec.lineMode === "custom" || metaRec.lineMode === "catalog") ? metaRec.lineMode : metaRec?.customLine ? "custom" : void 0;
1017
1644
  nextForm.productId = initialLine.productId;
1018
1645
  nextForm.variantId = initialLine.productVariantId;
1019
1646
  nextForm.quantity = initialLine.quantity.toString();
1020
- const metaMode = meta?.priceMode;
1647
+ nextForm.quantityUnit = normalizeUnitCode(initialLine.quantityUnit) ?? null;
1648
+ const metaMode = metaRec?.priceMode;
1021
1649
  const resolvedPriceMode = metaMode === "net" || metaMode === "gross" ? metaMode : initialLine.priceMode ?? "gross";
1022
1650
  nextForm.unitPrice = resolvedPriceMode === "net" ? initialLine.unitPriceNet.toString() : initialLine.unitPriceGross.toString();
1023
1651
  nextForm.priceMode = resolvedPriceMode;
@@ -1027,11 +1655,13 @@ function LineItemDialog({
1027
1655
  nextForm.customFieldSetId = initialLine.customFieldSetId ?? null;
1028
1656
  nextForm.statusEntryId = initialLine.statusEntryId ?? null;
1029
1657
  nextForm.lineMode = metaLineMode ?? (initialLine.productId || initialLine.productVariantId ? "catalog" : "custom");
1030
- const metaTaxRateId = typeof meta.taxRateId === "string" ? meta.taxRateId : null;
1658
+ const metaTaxRateId = typeof metaRec?.taxRateId === "string" ? metaRec.taxRateId : null;
1031
1659
  const fallbackTaxRateId = findTaxRateIdByValue(nextForm.taxRate);
1032
1660
  nextForm.taxRateId = metaTaxRateId ?? fallbackTaxRateId ?? (defaultTaxRateRef.current ? defaultTaxRateRef.current.id : null);
1033
1661
  if (!Number.isFinite(nextForm.taxRate) && nextForm.taxRateId) {
1034
- const matched = taxRatesRef.current.find((rate) => rate.id === nextForm.taxRateId);
1662
+ const matched = taxRatesRef.current.find(
1663
+ (rate) => rate.id === nextForm.taxRateId
1664
+ );
1035
1665
  const numericRate = normalizeNumber(matched?.rate);
1036
1666
  if (Number.isFinite(numericRate)) {
1037
1667
  nextForm.taxRate = numericRate;
@@ -1039,24 +1669,30 @@ function LineItemDialog({
1039
1669
  }
1040
1670
  let resolvedProductOption = null;
1041
1671
  let resolvedVariantOption = null;
1042
- if (typeof meta === "object" && meta) {
1043
- const mode = meta.priceMode;
1672
+ if (metaRec) {
1673
+ const metaRecord = metaRec;
1674
+ const mode = metaRecord.priceMode;
1044
1675
  if (mode === "net" || mode === "gross") {
1045
1676
  nextForm.priceMode = mode;
1046
1677
  nextForm.unitPrice = mode === "net" ? initialLine.unitPriceNet.toString() : initialLine.unitPriceGross.toString();
1047
1678
  }
1048
- nextForm.priceId = typeof meta.priceId === "string" ? meta.priceId : null;
1049
- const productTitle = typeof meta.productTitle === "string" ? meta.productTitle : initialLine.name;
1050
- const productSku = typeof meta.productSku === "string" ? meta.productSku : null;
1051
- const productThumbnail = typeof meta.productThumbnail === "string" ? meta.productThumbnail : null;
1679
+ nextForm.priceId = typeof metaRecord.priceId === "string" ? metaRecord.priceId : null;
1680
+ const productTitle = typeof metaRecord.productTitle === "string" ? metaRecord.productTitle : initialLine.name;
1681
+ const productSku = typeof metaRecord.productSku === "string" ? metaRecord.productSku : null;
1682
+ const productThumbnail = typeof metaRecord.productThumbnail === "string" ? metaRecord.productThumbnail : null;
1052
1683
  if (productTitle && initialLine.productId) {
1053
- const option = { id: initialLine.productId, title: productTitle, sku: productSku, thumbnailUrl: productThumbnail };
1684
+ const option = {
1685
+ id: initialLine.productId,
1686
+ title: productTitle,
1687
+ sku: productSku,
1688
+ thumbnailUrl: productThumbnail
1689
+ };
1054
1690
  productOptionsRef.current.set(initialLine.productId, option);
1055
1691
  resolvedProductOption = option;
1056
1692
  }
1057
- const variantTitle = typeof meta.variantTitle === "string" ? meta.variantTitle : null;
1058
- const variantSku = typeof meta.variantSku === "string" ? meta.variantSku : null;
1059
- const variantThumb = typeof meta.variantThumbnail === "string" ? meta.variantThumbnail : productThumbnail;
1693
+ const variantTitle = typeof metaRecord.variantTitle === "string" ? metaRecord.variantTitle : null;
1694
+ const variantSku = typeof metaRecord.variantSku === "string" ? metaRecord.variantSku : null;
1695
+ const variantThumb = typeof metaRecord.variantThumbnail === "string" ? metaRecord.variantThumbnail : productThumbnail;
1060
1696
  if (variantTitle && initialLine.productVariantId) {
1061
1697
  const option = {
1062
1698
  id: initialLine.productVariantId,
@@ -1069,32 +1705,34 @@ function LineItemDialog({
1069
1705
  }
1070
1706
  }
1071
1707
  if (!resolvedProductOption && initialLine.productId && snapshotProduct) {
1072
- const snapshotTitle = typeof snapshotProduct.title === "string" && snapshotProduct.title.trim().length ? snapshotProduct.title : initialLine.name ?? initialLine.productId;
1073
- const snapshotSku = typeof snapshotProduct.sku === "string" && snapshotProduct.sku.trim().length ? snapshotProduct.sku : null;
1074
- const snapshotThumb = typeof snapshotProduct.thumbnailUrl === "string" ? snapshotProduct.thumbnailUrl : typeof snapshotProduct.thumbnail_url === "string" ? snapshotProduct.thumbnail_url : null;
1075
- const snapshotTaxRate = normalizeNumber(snapshotProduct.taxRate, Number.NaN);
1708
+ const sp = snapshotProduct;
1709
+ const snapshotTitle = typeof sp.title === "string" && sp.title.trim().length ? sp.title : initialLine.name ?? initialLine.productId;
1710
+ const snapshotSku = typeof sp.sku === "string" && sp.sku.trim().length ? sp.sku : null;
1711
+ const snapshotThumb = typeof sp.thumbnailUrl === "string" ? sp.thumbnailUrl : typeof sp.thumbnail_url === "string" ? sp.thumbnail_url : null;
1712
+ const snapshotTaxRate = normalizeNumber(sp.taxRate, Number.NaN);
1076
1713
  const option = {
1077
1714
  id: initialLine.productId,
1078
1715
  title: snapshotTitle,
1079
1716
  sku: snapshotSku,
1080
1717
  thumbnailUrl: snapshotThumb,
1081
- taxRateId: typeof snapshotProduct.taxRateId === "string" ? snapshotProduct.taxRateId : null,
1718
+ taxRateId: typeof sp.taxRateId === "string" ? sp.taxRateId : null,
1082
1719
  taxRate: Number.isFinite(snapshotTaxRate) ? snapshotTaxRate : null
1083
1720
  };
1084
1721
  productOptionsRef.current.set(initialLine.productId, option);
1085
1722
  resolvedProductOption = option;
1086
1723
  }
1087
1724
  if (!resolvedVariantOption && initialLine.productVariantId && snapshotVariant) {
1088
- const snapshotTitle = typeof snapshotVariant.title === "string" && snapshotVariant.title.trim().length ? snapshotVariant.title : initialLine.name ?? initialLine.productVariantId;
1089
- const snapshotSku = typeof snapshotVariant.sku === "string" && snapshotVariant.sku.trim().length ? snapshotVariant.sku : null;
1090
- const snapshotThumb = typeof snapshotVariant.thumbnailUrl === "string" ? snapshotVariant.thumbnailUrl : typeof snapshotVariant.thumbnail_url === "string" ? snapshotVariant.thumbnail_url : resolvedProductOption?.thumbnailUrl ?? productOptionsRef.current.get(initialLine.productId ?? "")?.thumbnailUrl ?? null;
1091
- const snapshotTaxRate = normalizeNumber(snapshotVariant.taxRate, Number.NaN);
1725
+ const sv = snapshotVariant;
1726
+ const snapshotTitle = typeof sv.title === "string" && sv.title.trim().length ? sv.title : initialLine.name ?? initialLine.productVariantId;
1727
+ const snapshotSku = typeof sv.sku === "string" && sv.sku.trim().length ? sv.sku : null;
1728
+ const snapshotThumb = typeof sv.thumbnailUrl === "string" ? sv.thumbnailUrl : typeof sv.thumbnail_url === "string" ? sv.thumbnail_url : resolvedProductOption?.thumbnailUrl ?? productOptionsRef.current.get(initialLine.productId ?? "")?.thumbnailUrl ?? null;
1729
+ const snapshotTaxRate = normalizeNumber(sv.taxRate, Number.NaN);
1092
1730
  const option = {
1093
1731
  id: initialLine.productVariantId,
1094
1732
  title: snapshotTitle,
1095
1733
  sku: snapshotSku,
1096
1734
  thumbnailUrl: snapshotThumb,
1097
- taxRateId: typeof snapshotVariant.taxRateId === "string" ? snapshotVariant.taxRateId : null,
1735
+ taxRateId: typeof sv.taxRateId === "string" ? sv.taxRateId : null,
1098
1736
  taxRate: Number.isFinite(snapshotTaxRate) ? snapshotTaxRate : null
1099
1737
  };
1100
1738
  variantOptionsRef.current.set(initialLine.productVariantId, option);
@@ -1102,51 +1740,85 @@ function LineItemDialog({
1102
1740
  }
1103
1741
  if (resolvedProductOption) setProductOption(resolvedProductOption);
1104
1742
  if (resolvedVariantOption) setVariantOption(resolvedVariantOption);
1105
- const customValues = extractCustomFieldValues(initialLine);
1743
+ const customValues = extractCustomFieldValues(
1744
+ initialLine
1745
+ );
1106
1746
  const merged = { ...nextForm, ...customValues };
1107
1747
  setInitialValues(merged);
1108
1748
  setLineMode(merged.lineMode);
1109
1749
  setFormResetKey((prev) => prev + 1);
1110
1750
  if (initialLine.productId) {
1111
- void loadPrices(initialLine.productId, initialLine.productVariantId);
1751
+ void loadProductUnits(initialLine.productId, resolvedProductOption).then(
1752
+ (options) => {
1753
+ const requestedUnit = normalizeUnitCode(nextForm.quantityUnit);
1754
+ if (requestedUnit && !options.some((entry) => entry.code === requestedUnit)) {
1755
+ setUnitOptions((prev) => [
1756
+ ...prev,
1757
+ { code: requestedUnit, toBaseFactor: null, isBase: false }
1758
+ ]);
1759
+ }
1760
+ }
1761
+ );
1762
+ void loadPrices(
1763
+ initialLine.productId,
1764
+ initialLine.productVariantId,
1765
+ nextForm.quantity,
1766
+ nextForm.quantityUnit
1767
+ );
1112
1768
  } else {
1113
1769
  setPriceOptions([]);
1770
+ setUnitOptions([]);
1114
1771
  }
1115
- }, [currencyCode, findTaxRateIdByValue, initialLine, loadPrices, open, resetForm]);
1116
- return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange: (next) => next ? onOpenChange(true) : closeDialog(), children: /* @__PURE__ */ jsxs(
1117
- DialogContent,
1772
+ }, [
1773
+ currencyCode,
1774
+ findTaxRateIdByValue,
1775
+ initialLine,
1776
+ loadPrices,
1777
+ loadProductUnits,
1778
+ open,
1779
+ resetForm
1780
+ ]);
1781
+ return /* @__PURE__ */ jsx(
1782
+ Dialog,
1118
1783
  {
1119
- className: "sm:max-w-5xl",
1120
- ref: dialogContentRef,
1121
- onKeyDown: (event) => {
1122
- if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
1123
- event.preventDefault();
1124
- dialogContentRef.current?.querySelector("form")?.requestSubmit();
1125
- }
1126
- if (event.key === "Escape") {
1127
- event.preventDefault();
1128
- closeDialog();
1129
- }
1130
- },
1131
- children: [
1132
- /* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: editingId ? t("sales.documents.items.editTitle", "Edit line") : t("sales.documents.items.addTitle", "Add line") }) }),
1133
- /* @__PURE__ */ jsx(
1134
- CrudForm,
1135
- {
1136
- embedded: true,
1137
- fields,
1138
- groups,
1139
- entityId: customFieldEntityId,
1140
- initialValues,
1141
- submitLabel: editingId ? t("sales.documents.items.save", "Save changes") : t("sales.documents.items.addLine", "Add item"),
1142
- onSubmit: handleFormSubmit,
1143
- loadingMessage: t("sales.documents.items.loading", "Loading items\u2026")
1784
+ open,
1785
+ onOpenChange: (next) => next ? onOpenChange(true) : closeDialog(),
1786
+ children: /* @__PURE__ */ jsxs(
1787
+ DialogContent,
1788
+ {
1789
+ className: "sm:max-w-5xl",
1790
+ ref: dialogContentRef,
1791
+ onKeyDown: (event) => {
1792
+ if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
1793
+ event.preventDefault();
1794
+ dialogContentRef.current?.querySelector("form")?.requestSubmit();
1795
+ }
1796
+ if (event.key === "Escape") {
1797
+ event.preventDefault();
1798
+ closeDialog();
1799
+ }
1144
1800
  },
1145
- formResetKey
1146
- )
1147
- ]
1801
+ children: [
1802
+ /* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: editingId ? t("sales.documents.items.editTitle", "Edit line") : t("sales.documents.items.addTitle", "Add line") }) }),
1803
+ /* @__PURE__ */ jsx(
1804
+ CrudForm,
1805
+ {
1806
+ embedded: true,
1807
+ fields,
1808
+ groups,
1809
+ entityId: customFieldEntityId,
1810
+ initialValues,
1811
+ submitLabel: editingId ? t("sales.documents.items.save", "Save changes") : t("sales.documents.items.addLine", "Add item"),
1812
+ onSubmit: handleFormSubmit,
1813
+ loadingMessage: t("sales.documents.items.loading", "Loading items\u2026")
1814
+ },
1815
+ formResetKey
1816
+ )
1817
+ ]
1818
+ }
1819
+ )
1148
1820
  }
1149
- ) });
1821
+ );
1150
1822
  }
1151
1823
  export {
1152
1824
  LineItemDialog