@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
@@ -1,4 +1,5 @@
1
1
  import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
2
+ import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
2
3
  import { assertFound } from "@open-mercato/shared/lib/crud/errors";
3
4
  import { ensureOrganizationScope, ensureSameScope, ensureTenantScope } from "@open-mercato/shared/lib/commands/scope";
4
5
  import { extractUndoPayload } from "@open-mercato/shared/lib/commands/undo";
@@ -10,8 +11,11 @@ function toNumericString(value) {
10
11
  if (value === void 0 || value === null) return null;
11
12
  return value.toString();
12
13
  }
13
- async function requireScopedEntity(em, entityClass, id, message) {
14
- const entity = await em.findOne(entityClass, { id, deletedAt: null });
14
+ async function requireScopedEntity(em, entityClass, id, message, scope = { organizationId: null, tenantId: null }) {
15
+ const where = { id, deletedAt: null };
16
+ if (scope.organizationId) where.organizationId = scope.organizationId;
17
+ if (scope.tenantId) where.tenantId = scope.tenantId;
18
+ const entity = await findOneWithDecryption(em, entityClass, where, {}, scope);
15
19
  if (!entity) throw new CrudHttpError(404, { error: message });
16
20
  return entity;
17
21
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/sales/commands/shared.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nexport { assertFound } from '@open-mercato/shared/lib/crud/errors'\nexport { ensureOrganizationScope, ensureSameScope, ensureTenantScope } from '@open-mercato/shared/lib/commands/scope'\nexport { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'\n\nexport function cloneJson<T>(value: T): T {\n if (value === null || value === undefined) return value\n return JSON.parse(JSON.stringify(value)) as T\n}\n\nexport function toNumericString(value: number | null | undefined): string | null {\n if (value === undefined || value === null) return null\n return value.toString()\n}\n\nexport async function requireScopedEntity<T extends { id: string; deletedAt?: Date | null }>(\n em: EntityManager,\n entityClass: { new (): T },\n id: string,\n message: string\n): Promise<T> {\n const entity = await em.findOne(entityClass, { id, deletedAt: null })\n if (!entity) throw new CrudHttpError(404, { error: message })\n return entity\n}\n"],
5
- "mappings": "AACA,SAAS,qBAAqB;AAE9B,SAAS,mBAAmB;AAC5B,SAAS,yBAAyB,iBAAiB,yBAAyB;AAC5E,SAAS,0BAA0B;AAE5B,SAAS,UAAa,OAAa;AACxC,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,SAAO,KAAK,MAAM,KAAK,UAAU,KAAK,CAAC;AACzC;AAEO,SAAS,gBAAgB,OAAiD;AAC/E,MAAI,UAAU,UAAa,UAAU,KAAM,QAAO;AAClD,SAAO,MAAM,SAAS;AACxB;AAEA,eAAsB,oBACpB,IACA,aACA,IACA,SACY;AACZ,QAAM,SAAS,MAAM,GAAG,QAAQ,aAAa,EAAE,IAAI,WAAW,KAAK,CAAC;AACpE,MAAI,CAAC,OAAQ,OAAM,IAAI,cAAc,KAAK,EAAE,OAAO,QAAQ,CAAC;AAC5D,SAAO;AACT;",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nexport { assertFound } from '@open-mercato/shared/lib/crud/errors'\nexport { ensureOrganizationScope, ensureSameScope, ensureTenantScope } from '@open-mercato/shared/lib/commands/scope'\nexport { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'\n\nexport function cloneJson<T>(value: T): T {\n if (value === null || value === undefined) return value\n return JSON.parse(JSON.stringify(value)) as T\n}\n\nexport function toNumericString(value: number | null | undefined): string | null {\n if (value === undefined || value === null) return null\n return value.toString()\n}\n\nexport async function requireScopedEntity<T extends { id: string; deletedAt?: Date | null }>(\n em: EntityManager,\n entityClass: { new (): T },\n id: string,\n message: string,\n scope: { organizationId: string | null; tenantId: string | null } = { organizationId: null, tenantId: null },\n): Promise<T> {\n const where: Record<string, unknown> = { id, deletedAt: null }\n if (scope.organizationId) where.organizationId = scope.organizationId\n if (scope.tenantId) where.tenantId = scope.tenantId\n const entity = await findOneWithDecryption(em, entityClass, where, {}, scope)\n if (!entity) throw new CrudHttpError(404, { error: message })\n return entity\n}\n"],
5
+ "mappings": "AACA,SAAS,qBAAqB;AAC9B,SAAS,6BAA6B;AAEtC,SAAS,mBAAmB;AAC5B,SAAS,yBAAyB,iBAAiB,yBAAyB;AAC5E,SAAS,0BAA0B;AAE5B,SAAS,UAAa,OAAa;AACxC,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,SAAO,KAAK,MAAM,KAAK,UAAU,KAAK,CAAC;AACzC;AAEO,SAAS,gBAAgB,OAAiD;AAC/E,MAAI,UAAU,UAAa,UAAU,KAAM,QAAO;AAClD,SAAO,MAAM,SAAS;AACxB;AAEA,eAAsB,oBACpB,IACA,aACA,IACA,SACA,QAAoE,EAAE,gBAAgB,MAAM,UAAU,KAAK,GAC/F;AACZ,QAAM,QAAiC,EAAE,IAAI,WAAW,KAAK;AAC7D,MAAI,MAAM,eAAgB,OAAM,iBAAiB,MAAM;AACvD,MAAI,MAAM,SAAU,OAAM,WAAW,MAAM;AAC3C,QAAM,SAAS,MAAM,sBAAsB,IAAI,aAAa,OAAO,CAAC,GAAG,KAAK;AAC5E,MAAI,CAAC,OAAQ,OAAM,IAAI,cAAc,KAAK,EAAE,OAAO,QAAQ,CAAC;AAC5D,SAAO;AACT;",
6
6
  "names": []
7
7
  }
@@ -20,6 +20,42 @@ import { emitSalesDocumentTotalsRefresh } from "@open-mercato/core/modules/sales
20
20
  import { LineItemDialog } from "./LineItemDialog.js";
21
21
  import { formatMoney, normalizeNumber } from "./lineItemUtils.js";
22
22
  import { extractCustomFieldValues } from "./customFieldHelpers.js";
23
+ import { canonicalizeUnitCode } from "@open-mercato/shared/lib/units/unitCodes";
24
+ function isPlainObject(value) {
25
+ return typeof value === "object" && value !== null && !Array.isArray(value);
26
+ }
27
+ function isSalesLineUomSnapshot(value) {
28
+ if (!isPlainObject(value)) return false;
29
+ return value.version === 1 && typeof value.enteredQuantity === "string" && typeof value.toBaseFactor === "string" && typeof value.normalizedQuantity === "string";
30
+ }
31
+ function extractUomSnapshot(item) {
32
+ const raw = item.uom_snapshot ?? item.uomSnapshot;
33
+ if (isSalesLineUomSnapshot(raw)) return raw;
34
+ return null;
35
+ }
36
+ function resolveUnitPriceReference(snapshot) {
37
+ if (!snapshot) return null;
38
+ const ref = isSalesLineUomSnapshot(snapshot) ? snapshot.unitPriceReference : isPlainObject(snapshot) ? isPlainObject(snapshot.unitPriceReference) ? snapshot.unitPriceReference : isPlainObject(snapshot.unit_price_reference) ? snapshot.unit_price_reference : null : null;
39
+ if (!ref || !isPlainObject(ref)) return null;
40
+ const refRecord = ref;
41
+ const grossPerReference = normalizeNumber(
42
+ refRecord.grossPerReference ?? refRecord.gross_per_reference,
43
+ Number.NaN
44
+ );
45
+ if (!Number.isFinite(grossPerReference)) return null;
46
+ const referenceUnitCode = typeof refRecord.referenceUnitCode === "string" ? refRecord.referenceUnitCode : typeof refRecord.reference_unit_code === "string" ? refRecord.reference_unit_code : typeof refRecord.referenceUnit === "string" ? refRecord.referenceUnit : null;
47
+ if (!referenceUnitCode) return null;
48
+ return { grossPerReference, referenceUnitCode };
49
+ }
50
+ function getUomFields(item) {
51
+ const uomSnapshot = extractUomSnapshot(item);
52
+ return {
53
+ normalizedQuantity: item.normalized_quantity ?? item.normalizedQuantity ?? null,
54
+ normalizedUnit: item.normalized_unit ?? item.normalizedUnit ?? null,
55
+ quantityUnit: item.quantity_unit ?? item.quantityUnit ?? null,
56
+ uomSnapshot
57
+ };
58
+ }
23
59
  function SalesDocumentItemsSection({
24
60
  documentId,
25
61
  kind,
@@ -38,9 +74,13 @@ function SalesDocumentItemsSection({
38
74
  const [loading, setLoading] = React.useState(false);
39
75
  const [error, setError] = React.useState(null);
40
76
  const [dialogOpen, setDialogOpen] = React.useState(false);
41
- const [lineForEdit, setLineForEdit] = React.useState(null);
77
+ const [lineForEdit, setLineForEdit] = React.useState(
78
+ null
79
+ );
42
80
  const [lineStatusMap, setLineStatusMap] = React.useState({});
43
- const [shippedTotals, setShippedTotals] = React.useState(/* @__PURE__ */ new Map());
81
+ const [shippedTotals, setShippedTotals] = React.useState(
82
+ /* @__PURE__ */ new Map()
83
+ );
44
84
  const resourcePath = React.useMemo(
45
85
  () => kind === "order" ? "sales/order-lines" : "sales/quote-lines",
46
86
  [kind]
@@ -52,11 +92,9 @@ function SalesDocumentItemsSection({
52
92
  const loadLineStatuses = React.useCallback(async () => {
53
93
  try {
54
94
  const params = new URLSearchParams({ page: "1", pageSize: "100" });
55
- const response = await apiCall(
56
- `/api/sales/order-line-statuses?${params.toString()}`,
57
- void 0,
58
- { fallback: { items: [] } }
59
- );
95
+ const response = await apiCall(`/api/sales/order-line-statuses?${params.toString()}`, void 0, {
96
+ fallback: { items: [] }
97
+ });
60
98
  const entries = normalizeDictionaryEntries(response.result?.items ?? []);
61
99
  setLineStatusMap(createDictionaryMap(entries));
62
100
  } catch (err) {
@@ -68,64 +106,88 @@ function SalesDocumentItemsSection({
68
106
  setLoading(true);
69
107
  setError(null);
70
108
  try {
71
- const params = new URLSearchParams({ page: "1", pageSize: "200", [documentKey]: documentId });
72
- const response = await apiCall(
73
- `/api/${resourcePath}?${params.toString()}`,
74
- void 0,
75
- { fallback: { items: [] } }
76
- );
109
+ const params = new URLSearchParams({
110
+ page: "1",
111
+ pageSize: "100",
112
+ [documentKey]: documentId
113
+ });
114
+ const response = await apiCall(`/api/${resourcePath}?${params.toString()}`, void 0, {
115
+ fallback: { items: [] }
116
+ });
77
117
  if (response.ok && Array.isArray(response.result?.items)) {
78
- const mapped = response.result.items.flatMap((item) => {
79
- const id = typeof item.id === "string" ? item.id : null;
80
- if (!id) return [];
81
- const taxRate = normalizeNumber(item.tax_rate ?? item.taxRate, 0);
82
- const customFields = extractCustomFieldValues(item);
83
- const name = typeof item.name === "string" ? item.name : typeof item.catalog_snapshot === "object" && item.catalog_snapshot && typeof item.catalog_snapshot.name === "string" ? item.catalog_snapshot.name : null;
84
- const quantity = normalizeNumber(item.quantity, 0);
85
- const unitPriceNetRaw = normalizeNumber(item.unit_price_net ?? item.unitPriceNet, Number.NaN);
86
- const unitPriceGrossRaw = normalizeNumber(
87
- item.unit_price_gross ?? item.unitPriceGross,
88
- Number.NaN
89
- );
90
- const unitPriceNet = Number.isFinite(unitPriceNetRaw) ? unitPriceNetRaw : Number.isFinite(unitPriceGrossRaw) ? unitPriceGrossRaw / (1 + taxRate / 100) : 0;
91
- const unitPriceGross = Number.isFinite(unitPriceGrossRaw) ? unitPriceGrossRaw : Number.isFinite(unitPriceNetRaw) ? unitPriceNetRaw * (1 + taxRate / 100) : 0;
92
- const totalNetRaw = normalizeNumber(
93
- item.total_net_amount ?? item.totalNetAmount,
94
- Number.NaN
95
- );
96
- const totalGrossRaw = normalizeNumber(
97
- item.total_gross_amount ?? item.totalGrossAmount,
98
- Number.NaN
99
- );
100
- const totalNet = Number.isFinite(totalNetRaw) ? totalNetRaw : unitPriceNet * quantity;
101
- const totalGross = Number.isFinite(totalGrossRaw) ? totalGrossRaw : unitPriceGross * quantity;
102
- const priceModeRaw = item?.metadata && typeof item.metadata === "object" ? item.metadata.priceMode : null;
103
- const priceMode = priceModeRaw === "net" ? "net" : "gross";
104
- const customFieldSetId = typeof item.custom_field_set_id === "string" ? item.custom_field_set_id : typeof item.customFieldSetId === "string" ? item.customFieldSetId : null;
105
- const statusEntryId = typeof item.status_entry_id === "string" ? item.status_entry_id : typeof item.statusEntryId === "string" ? item.statusEntryId : null;
106
- const status = typeof item.status === "string" ? item.status : null;
107
- const record = {
108
- id,
109
- name,
110
- productId: typeof item.product_id === "string" ? item.product_id : null,
111
- productVariantId: typeof item.product_variant_id === "string" ? item.product_variant_id : null,
112
- quantity,
113
- currencyCode: typeof item.currency_code === "string" ? item.currency_code : typeof currencyCode === "string" ? currencyCode : null,
114
- unitPriceNet,
115
- unitPriceGross,
116
- taxRate,
117
- totalNet,
118
- totalGross,
119
- priceMode,
120
- metadata: item.metadata ?? null,
121
- catalogSnapshot: item.catalog_snapshot ?? null,
122
- customFieldSetId,
123
- customFields: Object.keys(customFields).length ? customFields : null,
124
- status,
125
- statusEntryId
126
- };
127
- return [record];
128
- });
118
+ const mapped = response.result.items.flatMap(
119
+ (item) => {
120
+ const id = typeof item.id === "string" ? item.id : null;
121
+ if (!id) return [];
122
+ const taxRate = normalizeNumber(
123
+ item.tax_rate ?? item.taxRate,
124
+ 0
125
+ );
126
+ const customFields = extractCustomFieldValues(
127
+ item
128
+ );
129
+ const name = typeof item.name === "string" ? item.name : typeof item.catalog_snapshot === "object" && item.catalog_snapshot && typeof item.catalog_snapshot.name === "string" ? item.catalog_snapshot.name : null;
130
+ const quantity = normalizeNumber(item.quantity, 0);
131
+ const uomFields = getUomFields(item);
132
+ const quantityUnit = canonicalizeUnitCode(uomFields.quantityUnit);
133
+ const normalizedQuantity = normalizeNumber(
134
+ uomFields.normalizedQuantity,
135
+ quantity
136
+ );
137
+ const normalizedUnit = canonicalizeUnitCode(uomFields.normalizedUnit) ?? quantityUnit;
138
+ const uomSnapshot = uomFields.uomSnapshot;
139
+ const unitPriceNetRaw = normalizeNumber(
140
+ item.unit_price_net ?? item.unitPriceNet,
141
+ Number.NaN
142
+ );
143
+ const unitPriceGrossRaw = normalizeNumber(
144
+ item.unit_price_gross ?? item.unitPriceGross,
145
+ Number.NaN
146
+ );
147
+ const unitPriceNet = Number.isFinite(unitPriceNetRaw) ? unitPriceNetRaw : Number.isFinite(unitPriceGrossRaw) ? unitPriceGrossRaw / (1 + taxRate / 100) : 0;
148
+ const unitPriceGross = Number.isFinite(unitPriceGrossRaw) ? unitPriceGrossRaw : Number.isFinite(unitPriceNetRaw) ? unitPriceNetRaw * (1 + taxRate / 100) : 0;
149
+ const totalNetRaw = normalizeNumber(
150
+ item.total_net_amount ?? item.totalNetAmount,
151
+ Number.NaN
152
+ );
153
+ const totalGrossRaw = normalizeNumber(
154
+ item.total_gross_amount ?? item.totalGrossAmount,
155
+ Number.NaN
156
+ );
157
+ const totalNet = Number.isFinite(totalNetRaw) ? totalNetRaw : unitPriceNet * quantity;
158
+ const totalGross = Number.isFinite(totalGrossRaw) ? totalGrossRaw : unitPriceGross * quantity;
159
+ const priceModeRaw = item.metadata && typeof item.metadata === "object" ? item.metadata.priceMode : null;
160
+ const priceMode = priceModeRaw === "net" ? "net" : "gross";
161
+ const customFieldSetId = typeof item.custom_field_set_id === "string" ? item.custom_field_set_id : typeof item.customFieldSetId === "string" ? item.customFieldSetId : null;
162
+ const statusEntryId = typeof item.status_entry_id === "string" ? item.status_entry_id : typeof item.statusEntryId === "string" ? item.statusEntryId : null;
163
+ const status = typeof item.status === "string" ? item.status : null;
164
+ const record = {
165
+ id,
166
+ name,
167
+ productId: typeof item.product_id === "string" ? item.product_id : null,
168
+ productVariantId: typeof item.product_variant_id === "string" ? item.product_variant_id : null,
169
+ quantity,
170
+ quantityUnit,
171
+ normalizedQuantity,
172
+ normalizedUnit,
173
+ currencyCode: typeof item.currency_code === "string" ? item.currency_code : typeof currencyCode === "string" ? currencyCode : null,
174
+ unitPriceNet,
175
+ unitPriceGross,
176
+ taxRate,
177
+ totalNet,
178
+ totalGross,
179
+ priceMode,
180
+ uomSnapshot,
181
+ metadata: item.metadata ?? null,
182
+ catalogSnapshot: item.catalog_snapshot ?? null,
183
+ customFieldSetId,
184
+ customFields: Object.keys(customFields).length ? customFields : null,
185
+ status,
186
+ statusEntryId
187
+ };
188
+ return [record];
189
+ }
190
+ );
129
191
  setItems(mapped);
130
192
  if (onItemsChange) onItemsChange(mapped);
131
193
  } else {
@@ -146,12 +208,14 @@ function SalesDocumentItemsSection({
146
208
  return;
147
209
  }
148
210
  try {
149
- const params = new URLSearchParams({ page: "1", pageSize: "200", orderId: documentId });
150
- const response = await apiCall(
151
- `/api/sales/shipments?${params.toString()}`,
152
- void 0,
153
- { fallback: { items: [] } }
154
- );
211
+ const params = new URLSearchParams({
212
+ page: "1",
213
+ pageSize: "100",
214
+ orderId: documentId
215
+ });
216
+ const response = await apiCall(`/api/sales/shipments?${params.toString()}`, void 0, {
217
+ fallback: { items: [] }
218
+ });
155
219
  if (response.ok && Array.isArray(response.result?.items)) {
156
220
  const totals = /* @__PURE__ */ new Map();
157
221
  response.result.items.forEach((shipment) => {
@@ -228,7 +292,10 @@ function SalesDocumentItemsSection({
228
292
  const handleDelete = React.useCallback(
229
293
  async (line) => {
230
294
  const confirmed = await confirm({
231
- title: t("sales.documents.items.deleteConfirm", "Delete this line item?"),
295
+ title: t(
296
+ "sales.documents.items.deleteConfirm",
297
+ "Delete this line item?"
298
+ ),
232
299
  variant: "destructive"
233
300
  });
234
301
  if (!confirmed) return;
@@ -240,7 +307,10 @@ function SalesDocumentItemsSection({
240
307
  organizationId: resolvedOrganizationId ?? void 0,
241
308
  tenantId: resolvedTenantId ?? void 0
242
309
  },
243
- errorMessage: t("sales.documents.items.errorDelete", "Failed to delete line.")
310
+ errorMessage: t(
311
+ "sales.documents.items.errorDelete",
312
+ "Failed to delete line."
313
+ )
244
314
  });
245
315
  if (result.ok) {
246
316
  flash(t("sales.documents.items.deleted", "Line removed."), "success");
@@ -250,11 +320,24 @@ function SalesDocumentItemsSection({
250
320
  } catch (err) {
251
321
  console.error("sales.document.items.delete", err);
252
322
  const normalized = normalizeCrudServerError(err);
253
- const fallback = t("sales.documents.items.errorDelete", "Failed to delete line.");
323
+ const fallback = t(
324
+ "sales.documents.items.errorDelete",
325
+ "Failed to delete line."
326
+ );
254
327
  flash(normalized.message || fallback, "error");
255
328
  }
256
329
  },
257
- [confirm, documentId, documentKey, kind, loadItems, resolvedOrganizationId, resourcePath, t, resolvedTenantId]
330
+ [
331
+ confirm,
332
+ documentId,
333
+ documentKey,
334
+ kind,
335
+ loadItems,
336
+ resolvedOrganizationId,
337
+ resourcePath,
338
+ t,
339
+ resolvedTenantId
340
+ ]
258
341
  );
259
342
  const renderStatus = React.useCallback(
260
343
  (line) => {
@@ -286,7 +369,14 @@ function SalesDocumentItemsSection({
286
369
  const variantThumb = meta && typeof meta.variantThumbnail === "string" && meta.variantThumbnail || variantSnapshot && typeof variantSnapshot.thumbnailUrl === "string" && variantSnapshot.thumbnailUrl || null;
287
370
  const thumbnail = variantThumb ?? productThumb;
288
371
  if (thumbnail) {
289
- return /* @__PURE__ */ jsx("img", { src: thumbnail, alt: record.name ?? record.id, className: "h-10 w-10 rounded border object-cover" });
372
+ return /* @__PURE__ */ jsx(
373
+ "img",
374
+ {
375
+ src: thumbnail,
376
+ alt: record.name ?? record.id,
377
+ className: "h-10 w-10 rounded border object-cover"
378
+ }
379
+ );
290
380
  }
291
381
  return /* @__PURE__ */ jsx("div", { className: "flex h-10 w-10 items-center justify-center rounded border bg-muted text-xs text-muted-foreground", children: "N/A" });
292
382
  };
@@ -326,7 +416,15 @@ function SalesDocumentItemsSection({
326
416
  const variantLabel = variantTitle ?? variantSku;
327
417
  const variantSuffix = variantSku && variantLabel && variantSku !== variantLabel ? ` \u2022 ${variantSku}` : "";
328
418
  const showProductSku = productSku && productSku !== variantSku ? productSku : null;
329
- const shippedQuantity = Math.max(0, shippedTotals.get(item.id) ?? 0);
419
+ const shippedQuantity = Math.max(
420
+ 0,
421
+ shippedTotals.get(item.id) ?? 0
422
+ );
423
+ const quantityLabel = item.quantityUnit ? `${item.quantity} ${item.quantityUnit}` : String(item.quantity);
424
+ const showNormalized = Number.isFinite(item.normalizedQuantity) && item.normalizedQuantity > 0 && (item.normalizedUnit ?? null) && (Math.abs(item.normalizedQuantity - item.quantity) > 1e-6 || (item.normalizedUnit ?? null) !== (item.quantityUnit ?? null));
425
+ const unitPriceReference = resolveUnitPriceReference(
426
+ item.uomSnapshot
427
+ );
330
428
  return /* @__PURE__ */ jsxs(
331
429
  "tr",
332
430
  {
@@ -346,32 +444,64 @@ function SalesDocumentItemsSection({
346
444
  ] }) }),
347
445
  /* @__PURE__ */ jsx("td", { className: "px-3 py-3", children: /* @__PURE__ */ jsx("div", { className: "flex items-center", children: renderStatus(item) }) }),
348
446
  /* @__PURE__ */ jsx("td", { className: "px-3 py-3", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-0.5", children: [
349
- /* @__PURE__ */ jsx("span", { className: "font-medium", children: item.quantity }),
350
- shippedQuantity > 0 ? /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t("sales.documents.items.table.shipped", "{{shipped}} / {{total}} shipped", {
351
- shipped: shippedQuantity,
352
- total: item.quantity
353
- }) }) : null
447
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: quantityLabel }),
448
+ showNormalized ? /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
449
+ item.normalizedQuantity,
450
+ " ",
451
+ item.normalizedUnit
452
+ ] }) : null,
453
+ shippedQuantity > 0 ? /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t(
454
+ "sales.documents.items.table.shipped",
455
+ "{{shipped}} / {{total}} shipped",
456
+ {
457
+ shipped: shippedQuantity,
458
+ total: item.quantity
459
+ }
460
+ ) }) : null
354
461
  ] }) }),
355
462
  /* @__PURE__ */ jsx("td", { className: "px-3 py-3", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-0.5", children: [
356
463
  /* @__PURE__ */ jsxs("span", { className: "font-mono text-sm", children: [
357
- formatMoney(item.unitPriceGross, item.currencyCode ?? currencyCode ?? void 0),
464
+ formatMoney(
465
+ item.unitPriceGross,
466
+ item.currencyCode ?? currencyCode ?? void 0
467
+ ),
358
468
  " ",
359
469
  /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t("sales.documents.items.table.gross", "gross") })
360
470
  ] }),
361
471
  /* @__PURE__ */ jsxs("span", { className: "font-mono text-xs text-muted-foreground", children: [
362
- formatMoney(item.unitPriceNet, item.currencyCode ?? currencyCode ?? void 0),
472
+ formatMoney(
473
+ item.unitPriceNet,
474
+ item.currencyCode ?? currencyCode ?? void 0
475
+ ),
363
476
  " ",
364
477
  t("sales.documents.items.table.net", "net")
365
- ] })
478
+ ] }),
479
+ unitPriceReference ? /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t(
480
+ "sales.documents.items.table.unitPriceReference",
481
+ "{{value}} per 1 {{unit}}",
482
+ {
483
+ value: formatMoney(
484
+ unitPriceReference.grossPerReference,
485
+ item.currencyCode ?? currencyCode ?? void 0
486
+ ),
487
+ unit: unitPriceReference.referenceUnitCode
488
+ }
489
+ ) }) : null
366
490
  ] }) }),
367
491
  /* @__PURE__ */ jsx("td", { className: "px-3 py-3 font-semibold", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-0.5", children: [
368
492
  /* @__PURE__ */ jsxs("span", { children: [
369
- formatMoney(item.totalGross, item.currencyCode ?? currencyCode ?? void 0),
493
+ formatMoney(
494
+ item.totalGross,
495
+ item.currencyCode ?? currencyCode ?? void 0
496
+ ),
370
497
  " ",
371
498
  /* @__PURE__ */ jsx("span", { className: "text-xs font-normal text-muted-foreground", children: t("sales.documents.items.table.gross", "gross") })
372
499
  ] }),
373
500
  /* @__PURE__ */ jsxs("span", { className: "text-xs font-medium text-muted-foreground", children: [
374
- formatMoney(item.totalNet, item.currencyCode ?? currencyCode ?? void 0),
501
+ formatMoney(
502
+ item.totalNet,
503
+ item.currencyCode ?? currencyCode ?? void 0
504
+ ),
375
505
  " ",
376
506
  t("sales.documents.items.table.net", "net")
377
507
  ] })