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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/dist/generated/entities/catalog_product/index.js +16 -0
  2. package/dist/generated/entities/catalog_product/index.js.map +2 -2
  3. package/dist/generated/entities/catalog_product_unit_conversion/index.js +27 -0
  4. package/dist/generated/entities/catalog_product_unit_conversion/index.js.map +7 -0
  5. package/dist/generated/entities/sales_credit_memo_line/index.js +7 -1
  6. package/dist/generated/entities/sales_credit_memo_line/index.js.map +2 -2
  7. package/dist/generated/entities/sales_invoice_line/index.js +7 -1
  8. package/dist/generated/entities/sales_invoice_line/index.js.map +2 -2
  9. package/dist/generated/entities/sales_order_line/index.js +6 -0
  10. package/dist/generated/entities/sales_order_line/index.js.map +2 -2
  11. package/dist/generated/entities/sales_quote_line/index.js +6 -0
  12. package/dist/generated/entities/sales_quote_line/index.js.map +2 -2
  13. package/dist/generated/entities.ids.generated.js +1 -0
  14. package/dist/generated/entities.ids.generated.js.map +2 -2
  15. package/dist/generated/entity-fields-registry.js +2 -0
  16. package/dist/generated/entity-fields-registry.js.map +2 -2
  17. package/dist/modules/catalog/api/prices/route.js +123 -8
  18. package/dist/modules/catalog/api/prices/route.js.map +2 -2
  19. package/dist/modules/catalog/api/product-unit-conversions/route.js +194 -0
  20. package/dist/modules/catalog/api/product-unit-conversions/route.js.map +7 -0
  21. package/dist/modules/catalog/api/products/route.js +351 -201
  22. package/dist/modules/catalog/api/products/route.js.map +2 -2
  23. package/dist/modules/catalog/backend/catalog/products/[id]/page.js +1267 -497
  24. package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
  25. package/dist/modules/catalog/backend/catalog/products/create/page.js +733 -210
  26. package/dist/modules/catalog/backend/catalog/products/create/page.js.map +2 -2
  27. package/dist/modules/catalog/commands/index.js +1 -0
  28. package/dist/modules/catalog/commands/index.js.map +2 -2
  29. package/dist/modules/catalog/commands/productUnitConversions.js +503 -0
  30. package/dist/modules/catalog/commands/productUnitConversions.js.map +7 -0
  31. package/dist/modules/catalog/commands/products.js +355 -73
  32. package/dist/modules/catalog/commands/products.js.map +2 -2
  33. package/dist/modules/catalog/commands/shared.js +18 -4
  34. package/dist/modules/catalog/commands/shared.js.map +2 -2
  35. package/dist/modules/catalog/components/products/ProductUomSection.js +591 -0
  36. package/dist/modules/catalog/components/products/ProductUomSection.js.map +7 -0
  37. package/dist/modules/catalog/components/products/productForm.js +66 -5
  38. package/dist/modules/catalog/components/products/productForm.js.map +2 -2
  39. package/dist/modules/catalog/components/products/productFormUtils.js +68 -0
  40. package/dist/modules/catalog/components/products/productFormUtils.js.map +7 -0
  41. package/dist/modules/catalog/data/entities.js +86 -0
  42. package/dist/modules/catalog/data/entities.js.map +2 -2
  43. package/dist/modules/catalog/data/validators.js +65 -3
  44. package/dist/modules/catalog/data/validators.js.map +2 -2
  45. package/dist/modules/catalog/events.js +3 -0
  46. package/dist/modules/catalog/events.js.map +2 -2
  47. package/dist/modules/catalog/lib/unitCodes.js +7 -0
  48. package/dist/modules/catalog/lib/unitCodes.js.map +7 -0
  49. package/dist/modules/catalog/lib/unitResolution.js +53 -0
  50. package/dist/modules/catalog/lib/unitResolution.js.map +7 -0
  51. package/dist/modules/catalog/migrations/Migration20260218225422.js +19 -0
  52. package/dist/modules/catalog/migrations/Migration20260218225422.js.map +7 -0
  53. package/dist/modules/catalog/migrations/Migration20260219084500.js +27 -0
  54. package/dist/modules/catalog/migrations/Migration20260219084500.js.map +7 -0
  55. package/dist/modules/catalog/search.js +69 -1
  56. package/dist/modules/catalog/search.js.map +2 -2
  57. package/dist/modules/catalog/seed/examples.js +91 -42
  58. package/dist/modules/catalog/seed/examples.js.map +2 -2
  59. package/dist/modules/dashboards/seed/analytics.js +3 -0
  60. package/dist/modules/dashboards/seed/analytics.js.map +2 -2
  61. package/dist/modules/sales/api/order-lines/route.js +98 -15
  62. package/dist/modules/sales/api/order-lines/route.js.map +2 -2
  63. package/dist/modules/sales/api/quote-lines/route.js +101 -14
  64. package/dist/modules/sales/api/quote-lines/route.js.map +2 -2
  65. package/dist/modules/sales/api/quotes/public/[token]/route.js +87 -12
  66. package/dist/modules/sales/api/quotes/public/[token]/route.js.map +2 -2
  67. package/dist/modules/sales/commands/documents.js +1424 -260
  68. package/dist/modules/sales/commands/documents.js.map +3 -3
  69. package/dist/modules/sales/commands/shared.js +6 -2
  70. package/dist/modules/sales/commands/shared.js.map +2 -2
  71. package/dist/modules/sales/components/documents/ItemsSection.js +216 -86
  72. package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
  73. package/dist/modules/sales/components/documents/LineItemDialog.js +913 -241
  74. package/dist/modules/sales/components/documents/LineItemDialog.js.map +3 -3
  75. package/dist/modules/sales/components/documents/ShipmentsSection.js +15 -3
  76. package/dist/modules/sales/components/documents/ShipmentsSection.js.map +2 -2
  77. package/dist/modules/sales/data/entities.js +59 -3
  78. package/dist/modules/sales/data/entities.js.map +2 -2
  79. package/dist/modules/sales/data/validators.js +35 -0
  80. package/dist/modules/sales/data/validators.js.map +2 -2
  81. package/dist/modules/sales/frontend/quote/[token]/page.js +15 -1
  82. package/dist/modules/sales/frontend/quote/[token]/page.js.map +2 -2
  83. package/dist/modules/sales/migrations/Migration20260218225423.js +31 -0
  84. package/dist/modules/sales/migrations/Migration20260218225423.js.map +7 -0
  85. package/dist/modules/sales/migrations/Migration20260219084501.js +71 -0
  86. package/dist/modules/sales/migrations/Migration20260219084501.js.map +7 -0
  87. package/dist/modules/sales/search.js +28 -0
  88. package/dist/modules/sales/search.js.map +2 -2
  89. package/dist/modules/sales/seed/examples.js +14 -1
  90. package/dist/modules/sales/seed/examples.js.map +2 -2
  91. package/dist/modules/sales/widgets/injection/document-history/widget.client.js +1 -1
  92. package/dist/modules/sales/widgets/injection/document-history/widget.client.js.map +2 -2
  93. package/dist/modules/staff/backend/staff/team-members/[id]/page.js +28 -15
  94. package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
  95. package/dist/modules/staff/translations.js +9 -0
  96. package/dist/modules/staff/translations.js.map +7 -0
  97. package/dist/modules/translations/components/TranslationDrawerAction.js +97 -0
  98. package/dist/modules/translations/components/TranslationDrawerAction.js.map +7 -0
  99. package/dist/modules/translations/lib/extract-record-id.js +31 -2
  100. package/dist/modules/translations/lib/extract-record-id.js.map +2 -2
  101. package/dist/modules/translations/lib/resolve-field-list.js +3 -0
  102. package/dist/modules/translations/lib/resolve-field-list.js.map +2 -2
  103. package/dist/modules/translations/widgets/injection/translation-manager/widget.client.js +105 -36
  104. package/dist/modules/translations/widgets/injection/translation-manager/widget.client.js.map +2 -2
  105. package/dist/modules/translations/widgets/injection-table.js +18 -29
  106. package/dist/modules/translations/widgets/injection-table.js.map +2 -2
  107. package/generated/entities/catalog_product/index.ts +8 -0
  108. package/generated/entities/catalog_product_unit_conversion/index.ts +12 -0
  109. package/generated/entities/sales_credit_memo_line/index.ts +3 -0
  110. package/generated/entities/sales_invoice_line/index.ts +3 -0
  111. package/generated/entities/sales_order_line/index.ts +3 -0
  112. package/generated/entities/sales_quote_line/index.ts +3 -0
  113. package/generated/entities.ids.generated.ts +1 -0
  114. package/generated/entity-fields-registry.ts +2 -0
  115. package/package.json +2 -2
  116. package/src/modules/auth/i18n/de.json +1 -1
  117. package/src/modules/auth/i18n/en.json +1 -1
  118. package/src/modules/auth/i18n/es.json +1 -1
  119. package/src/modules/auth/i18n/pl.json +1 -1
  120. package/src/modules/catalog/api/prices/route.ts +213 -81
  121. package/src/modules/catalog/api/product-unit-conversions/route.ts +195 -0
  122. package/src/modules/catalog/api/products/route.ts +638 -402
  123. package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +2085 -1072
  124. package/src/modules/catalog/backend/catalog/products/create/page.tsx +1288 -593
  125. package/src/modules/catalog/commands/index.ts +1 -0
  126. package/src/modules/catalog/commands/productUnitConversions.ts +626 -0
  127. package/src/modules/catalog/commands/products.ts +1151 -693
  128. package/src/modules/catalog/commands/shared.ts +19 -5
  129. package/src/modules/catalog/components/products/ProductUomSection.tsx +745 -0
  130. package/src/modules/catalog/components/products/productForm.ts +369 -256
  131. package/src/modules/catalog/components/products/productFormUtils.ts +82 -0
  132. package/src/modules/catalog/data/entities.ts +82 -1
  133. package/src/modules/catalog/data/validators.ts +118 -34
  134. package/src/modules/catalog/events.ts +3 -0
  135. package/src/modules/catalog/i18n/de.json +56 -0
  136. package/src/modules/catalog/i18n/en.json +56 -0
  137. package/src/modules/catalog/i18n/es.json +56 -0
  138. package/src/modules/catalog/i18n/pl.json +56 -0
  139. package/src/modules/catalog/lib/unitCodes.ts +1 -0
  140. package/src/modules/catalog/lib/unitResolution.ts +62 -0
  141. package/src/modules/catalog/migrations/.snapshot-open-mercato.json +245 -0
  142. package/src/modules/catalog/migrations/Migration20260218225422.ts +21 -0
  143. package/src/modules/catalog/migrations/Migration20260219084500.ts +26 -0
  144. package/src/modules/catalog/search.ts +73 -1
  145. package/src/modules/catalog/seed/examples.ts +552 -479
  146. package/src/modules/dashboards/i18n/de.json +1 -1
  147. package/src/modules/dashboards/i18n/en.json +1 -1
  148. package/src/modules/dashboards/i18n/es.json +1 -1
  149. package/src/modules/dashboards/i18n/pl.json +1 -1
  150. package/src/modules/dashboards/seed/analytics.ts +3 -0
  151. package/src/modules/sales/api/order-lines/route.ts +158 -68
  152. package/src/modules/sales/api/quote-lines/route.ts +161 -67
  153. package/src/modules/sales/api/quotes/public/[token]/route.ts +122 -36
  154. package/src/modules/sales/commands/documents.ts +4250 -2424
  155. package/src/modules/sales/commands/shared.ts +7 -2
  156. package/src/modules/sales/components/documents/ItemsSection.tsx +580 -310
  157. package/src/modules/sales/components/documents/LineItemDialog.tsx +1988 -833
  158. package/src/modules/sales/components/documents/ShipmentsSection.tsx +17 -3
  159. package/src/modules/sales/components/documents/lineItemTypes.ts +6 -0
  160. package/src/modules/sales/data/entities.ts +53 -0
  161. package/src/modules/sales/data/validators.ts +36 -0
  162. package/src/modules/sales/frontend/quote/[token]/page.tsx +25 -1
  163. package/src/modules/sales/i18n/de.json +23 -3
  164. package/src/modules/sales/i18n/en.json +23 -3
  165. package/src/modules/sales/i18n/es.json +23 -3
  166. package/src/modules/sales/i18n/pl.json +23 -3
  167. package/src/modules/sales/lib/types.ts +30 -0
  168. package/src/modules/sales/migrations/.snapshot-open-mercato.json +172 -0
  169. package/src/modules/sales/migrations/Migration20260218225423.ts +37 -0
  170. package/src/modules/sales/migrations/Migration20260219084501.ts +73 -0
  171. package/src/modules/sales/search.ts +28 -0
  172. package/src/modules/sales/seed/examples.ts +20 -1
  173. package/src/modules/sales/widgets/injection/document-history/widget.client.tsx +1 -1
  174. package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +8 -0
  175. package/src/modules/staff/translations.ts +5 -0
  176. package/src/modules/translations/components/TranslationDrawerAction.tsx +107 -0
  177. package/src/modules/translations/lib/extract-record-id.ts +47 -3
  178. package/src/modules/translations/lib/resolve-field-list.ts +4 -0
  179. package/src/modules/translations/widgets/injection/translation-manager/widget.client.tsx +108 -36
  180. package/src/modules/translations/widgets/injection-table.ts +19 -33
  181. package/src/modules/workflows/i18n/de.json +4 -4
  182. package/src/modules/workflows/i18n/en.json +4 -4
  183. package/src/modules/workflows/i18n/es.json +4 -4
  184. package/src/modules/workflows/i18n/pl.json +4 -4
@@ -1,39 +1,120 @@
1
- "use client"
1
+ "use client";
2
2
 
3
- import * as React from 'react'
4
- import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
5
- import { deleteCrud } from '@open-mercato/ui/backend/utils/crud'
6
- import { normalizeCrudServerError } from '@open-mercato/ui/backend/utils/serverErrors'
7
- import { LoadingMessage, TabEmptyState } from '@open-mercato/ui/backend/detail'
8
- import { Button } from '@open-mercato/ui/primitives/button'
9
- import { flash } from '@open-mercato/ui/backend/FlashMessages'
10
- import { Pencil, Trash2 } from 'lucide-react'
3
+ import * as React from "react";
4
+ import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
5
+ import { deleteCrud } from "@open-mercato/ui/backend/utils/crud";
6
+ import { normalizeCrudServerError } from "@open-mercato/ui/backend/utils/serverErrors";
7
+ import { LoadingMessage, TabEmptyState } from "@open-mercato/ui/backend/detail";
8
+ import { Button } from "@open-mercato/ui/primitives/button";
9
+ import { flash } from "@open-mercato/ui/backend/FlashMessages";
10
+ import { Pencil, Trash2 } from "lucide-react";
11
11
  import {
12
12
  DictionaryValue,
13
13
  type DictionaryMap,
14
14
  createDictionaryMap,
15
15
  normalizeDictionaryEntries,
16
- } from '@open-mercato/core/modules/dictionaries/components/dictionaryAppearance'
17
- import { useT } from '@open-mercato/shared/lib/i18n/context'
18
- import { useOrganizationScopeDetail } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
19
- import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
20
- import { emitSalesDocumentTotalsRefresh } from '@open-mercato/core/modules/sales/lib/frontend/documentTotalsEvents'
21
- import { LineItemDialog } from './LineItemDialog'
22
- import type { SalesLineRecord } from './lineItemTypes'
23
- import { formatMoney, normalizeNumber } from './lineItemUtils'
24
- import type { SectionAction } from '@open-mercato/ui/backend/detail'
25
- import { extractCustomFieldValues } from './customFieldHelpers'
16
+ } from "@open-mercato/core/modules/dictionaries/components/dictionaryAppearance";
17
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
18
+ import { useOrganizationScopeDetail } from "@open-mercato/shared/lib/frontend/useOrganizationScope";
19
+ import { useConfirmDialog } from "@open-mercato/ui/backend/confirm-dialog";
20
+ import { emitSalesDocumentTotalsRefresh } from "@open-mercato/core/modules/sales/lib/frontend/documentTotalsEvents";
21
+ import { LineItemDialog } from "./LineItemDialog";
22
+ import type { SalesLineRecord } from "./lineItemTypes";
23
+ import { formatMoney, normalizeNumber } from "./lineItemUtils";
24
+ import type { SectionAction } from "@open-mercato/ui/backend/detail";
25
+ import { extractCustomFieldValues } from "./customFieldHelpers";
26
+ import { canonicalizeUnitCode } from "@open-mercato/shared/lib/units/unitCodes";
27
+ import type { SalesLineUomSnapshot } from "../../lib/types";
26
28
 
27
- type SalesDocumentItemsSectionProps = {
28
- documentId: string
29
- kind: 'order' | 'quote'
30
- currencyCode: string | null | undefined
31
- organizationId?: string | null
32
- tenantId?: string | null
33
- onActionChange?: (action: SectionAction | null) => void
34
- onItemsChange?: (items: SalesLineRecord[]) => void
29
+ type ResolvedUnitPriceReference = {
30
+ grossPerReference: number;
31
+ referenceUnitCode: string;
32
+ };
33
+
34
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
35
+ return typeof value === "object" && value !== null && !Array.isArray(value);
36
+ }
37
+
38
+ function isSalesLineUomSnapshot(
39
+ value: unknown,
40
+ ): value is SalesLineUomSnapshot {
41
+ if (!isPlainObject(value)) return false;
42
+ return (
43
+ value.version === 1 &&
44
+ typeof value.enteredQuantity === "string" &&
45
+ typeof value.toBaseFactor === "string" &&
46
+ typeof value.normalizedQuantity === "string"
47
+ );
48
+ }
49
+
50
+ function extractUomSnapshot(
51
+ item: Record<string, unknown>,
52
+ ): SalesLineUomSnapshot | null {
53
+ const raw = item.uom_snapshot ?? item.uomSnapshot;
54
+ if (isSalesLineUomSnapshot(raw)) return raw;
55
+ return null;
56
+ }
57
+
58
+ function resolveUnitPriceReference(
59
+ snapshot: SalesLineUomSnapshot | Record<string, unknown> | null,
60
+ ): ResolvedUnitPriceReference | null {
61
+ if (!snapshot) return null;
62
+
63
+ const ref = isSalesLineUomSnapshot(snapshot)
64
+ ? snapshot.unitPriceReference
65
+ : isPlainObject(snapshot)
66
+ ? isPlainObject(snapshot.unitPriceReference)
67
+ ? snapshot.unitPriceReference
68
+ : isPlainObject(snapshot.unit_price_reference)
69
+ ? snapshot.unit_price_reference
70
+ : null
71
+ : null;
72
+
73
+ if (!ref || !isPlainObject(ref)) return null;
74
+
75
+ const refRecord = ref as Record<string, unknown>;
76
+ const grossPerReference = normalizeNumber(
77
+ refRecord.grossPerReference ?? refRecord.gross_per_reference,
78
+ Number.NaN,
79
+ );
80
+ if (!Number.isFinite(grossPerReference)) return null;
81
+
82
+ const referenceUnitCode =
83
+ typeof refRecord.referenceUnitCode === "string"
84
+ ? refRecord.referenceUnitCode
85
+ : typeof refRecord.reference_unit_code === "string"
86
+ ? refRecord.reference_unit_code
87
+ : typeof refRecord.referenceUnit === "string"
88
+ ? refRecord.referenceUnit
89
+ : null;
90
+ if (!referenceUnitCode) return null;
91
+
92
+ return { grossPerReference, referenceUnitCode };
93
+ }
94
+
95
+ function getUomFields(item: Record<string, unknown>) {
96
+ const uomSnapshot = extractUomSnapshot(item);
97
+ return {
98
+ normalizedQuantity: (item.normalized_quantity ?? item.normalizedQuantity ?? null) as
99
+ | number
100
+ | string
101
+ | null,
102
+ normalizedUnit: (item.normalized_unit ?? item.normalizedUnit ?? null) as string | null,
103
+ quantityUnit: (item.quantity_unit ?? item.quantityUnit ?? null) as string | null,
104
+ uomSnapshot,
105
+ };
35
106
  }
36
107
 
108
+ type SalesDocumentItemsSectionProps = {
109
+ documentId: string;
110
+ kind: "order" | "quote";
111
+ currencyCode: string | null | undefined;
112
+ organizationId?: string | null;
113
+ tenantId?: string | null;
114
+ onActionChange?: (action: SectionAction | null) => void;
115
+ onItemsChange?: (items: SalesLineRecord[]) => void;
116
+ };
117
+
37
118
  export function SalesDocumentItemsSection({
38
119
  documentId,
39
120
  kind,
@@ -43,274 +124,334 @@ export function SalesDocumentItemsSection({
43
124
  onActionChange,
44
125
  onItemsChange,
45
126
  }: SalesDocumentItemsSectionProps) {
46
- const t = useT()
47
- const { organizationId, tenantId } = useOrganizationScopeDetail()
48
- const { confirm, ConfirmDialogElement } = useConfirmDialog()
49
- const resolvedOrganizationId = orgFromProps ?? organizationId ?? null
50
- const resolvedTenantId = tenantFromProps ?? tenantId ?? null
51
- const [items, setItems] = React.useState<SalesLineRecord[]>([])
52
- const [loading, setLoading] = React.useState(false)
53
- const [error, setError] = React.useState<string | null>(null)
54
- const [dialogOpen, setDialogOpen] = React.useState(false)
55
- const [lineForEdit, setLineForEdit] = React.useState<SalesLineRecord | null>(null)
56
- const [lineStatusMap, setLineStatusMap] = React.useState<DictionaryMap>({})
57
- const [shippedTotals, setShippedTotals] = React.useState<Map<string, number>>(new Map())
127
+ const t = useT();
128
+ const { organizationId, tenantId } = useOrganizationScopeDetail();
129
+ const { confirm, ConfirmDialogElement } = useConfirmDialog();
130
+ const resolvedOrganizationId = orgFromProps ?? organizationId ?? null;
131
+ const resolvedTenantId = tenantFromProps ?? tenantId ?? null;
132
+ const [items, setItems] = React.useState<SalesLineRecord[]>([]);
133
+ const [loading, setLoading] = React.useState(false);
134
+ const [error, setError] = React.useState<string | null>(null);
135
+ const [dialogOpen, setDialogOpen] = React.useState(false);
136
+ const [lineForEdit, setLineForEdit] = React.useState<SalesLineRecord | null>(
137
+ null,
138
+ );
139
+ const [lineStatusMap, setLineStatusMap] = React.useState<DictionaryMap>({});
140
+ const [shippedTotals, setShippedTotals] = React.useState<Map<string, number>>(
141
+ new Map(),
142
+ );
58
143
 
59
144
  const resourcePath = React.useMemo(
60
- () => (kind === 'order' ? 'sales/order-lines' : 'sales/quote-lines'),
145
+ () => (kind === "order" ? "sales/order-lines" : "sales/quote-lines"),
61
146
  [kind],
62
- )
63
- const documentKey = kind === 'order' ? 'orderId' : 'quoteId'
64
- const lineStatusesLoaded = React.useRef(false)
65
- const itemsLoadedForDocument = React.useRef<string | null>(null)
66
- const shipmentsLoadedForDocument = React.useRef<string | null>(null)
147
+ );
148
+ const documentKey = kind === "order" ? "orderId" : "quoteId";
149
+ const lineStatusesLoaded = React.useRef(false);
150
+ const itemsLoadedForDocument = React.useRef<string | null>(null);
151
+ const shipmentsLoadedForDocument = React.useRef<string | null>(null);
67
152
  const loadLineStatuses = React.useCallback(async () => {
68
153
  try {
69
- const params = new URLSearchParams({ page: '1', pageSize: '100' })
70
- const response = await apiCall<{ items?: Array<Record<string, unknown>> }>(
71
- `/api/sales/order-line-statuses?${params.toString()}`,
72
- undefined,
73
- { fallback: { items: [] } },
74
- )
75
- const entries = normalizeDictionaryEntries(response.result?.items ?? [])
76
- setLineStatusMap(createDictionaryMap(entries))
154
+ const params = new URLSearchParams({ page: "1", pageSize: "100" });
155
+ const response = await apiCall<{
156
+ items?: Array<Record<string, unknown>>;
157
+ }>(`/api/sales/order-line-statuses?${params.toString()}`, undefined, {
158
+ fallback: { items: [] },
159
+ });
160
+ const entries = normalizeDictionaryEntries(response.result?.items ?? []);
161
+ setLineStatusMap(createDictionaryMap(entries));
77
162
  } catch (err) {
78
- console.error('sales.document.line-statuses.load', err)
79
- setLineStatusMap({})
163
+ console.error("sales.document.line-statuses.load", err);
164
+ setLineStatusMap({});
80
165
  }
81
- }, [])
166
+ }, []);
82
167
 
83
168
  const loadItems = React.useCallback(async () => {
84
- setLoading(true)
85
- setError(null)
169
+ setLoading(true);
170
+ setError(null);
86
171
  try {
87
- const params = new URLSearchParams({ page: '1', pageSize: '200', [documentKey]: documentId })
88
- const response = await apiCall<{ items?: Array<Record<string, unknown>> }>(
89
- `/api/${resourcePath}?${params.toString()}`,
90
- undefined,
91
- { fallback: { items: [] } },
92
- )
172
+ const params = new URLSearchParams({
173
+ page: "1",
174
+ pageSize: "100",
175
+ [documentKey]: documentId,
176
+ });
177
+ const response = await apiCall<{
178
+ items?: Array<Record<string, unknown>>;
179
+ }>(`/api/${resourcePath}?${params.toString()}`, undefined, {
180
+ fallback: { items: [] },
181
+ });
93
182
  if (response.ok && Array.isArray(response.result?.items)) {
94
- const mapped = response.result.items.flatMap<SalesLineRecord>((item) => {
95
- const id = typeof item.id === 'string' ? item.id : null
96
- if (!id) return []
97
- const taxRate = normalizeNumber((item as any).tax_rate ?? (item as any).taxRate, 0)
98
- const customFields = extractCustomFieldValues(item as Record<string, unknown>)
99
- const name =
100
- typeof item.name === 'string'
101
- ? item.name
102
- : typeof item.catalog_snapshot === 'object' &&
103
- item.catalog_snapshot &&
104
- typeof (item.catalog_snapshot as any).name === 'string'
105
- ? (item.catalog_snapshot as any).name
106
- : null
107
- const quantity = normalizeNumber(item.quantity, 0)
108
- const unitPriceNetRaw = normalizeNumber((item as any).unit_price_net ?? (item as any).unitPriceNet, Number.NaN)
109
- const unitPriceGrossRaw = normalizeNumber(
110
- (item as any).unit_price_gross ?? (item as any).unitPriceGross,
111
- Number.NaN,
112
- )
113
- const unitPriceNet = Number.isFinite(unitPriceNetRaw)
114
- ? unitPriceNetRaw
115
- : Number.isFinite(unitPriceGrossRaw)
116
- ? unitPriceGrossRaw / (1 + taxRate / 100)
117
- : 0
118
- const unitPriceGross = Number.isFinite(unitPriceGrossRaw)
119
- ? unitPriceGrossRaw
120
- : Number.isFinite(unitPriceNetRaw)
121
- ? unitPriceNetRaw * (1 + taxRate / 100)
122
- : 0
123
- const totalNetRaw = normalizeNumber(
124
- (item as any).total_net_amount ?? (item as any).totalNetAmount,
125
- Number.NaN,
126
- )
127
- const totalGrossRaw = normalizeNumber(
128
- (item as any).total_gross_amount ?? (item as any).totalGrossAmount,
129
- Number.NaN,
130
- )
131
- const totalNet = Number.isFinite(totalNetRaw) ? totalNetRaw : unitPriceNet * quantity
132
- const totalGross = Number.isFinite(totalGrossRaw) ? totalGrossRaw : unitPriceGross * quantity
133
- const priceModeRaw =
134
- (item as any)?.metadata && typeof (item as any).metadata === 'object'
135
- ? ((item as any).metadata as Record<string, unknown>).priceMode
136
- : null
137
- const priceMode = priceModeRaw === 'net' ? 'net' : 'gross'
138
- const customFieldSetId =
139
- typeof (item as any).custom_field_set_id === 'string'
140
- ? (item as any).custom_field_set_id
141
- : typeof (item as any).customFieldSetId === 'string'
142
- ? (item as any).customFieldSetId
143
- : null
144
- const statusEntryId =
145
- typeof (item as any).status_entry_id === 'string'
146
- ? (item as any).status_entry_id
147
- : typeof (item as any).statusEntryId === 'string'
148
- ? (item as any).statusEntryId
149
- : null
150
- const status = typeof item.status === 'string' ? item.status : null
151
- const record: SalesLineRecord = {
152
- id,
153
- name,
154
- productId: typeof item.product_id === 'string' ? item.product_id : null,
155
- productVariantId: typeof item.product_variant_id === 'string' ? item.product_variant_id : null,
156
- quantity,
157
- currencyCode:
158
- typeof item.currency_code === 'string'
159
- ? item.currency_code
160
- : typeof currencyCode === 'string'
161
- ? currencyCode
183
+ const mapped = response.result.items.flatMap<SalesLineRecord>(
184
+ (item) => {
185
+ const id = typeof item.id === "string" ? item.id : null;
186
+ if (!id) return [];
187
+ const taxRate = normalizeNumber(
188
+ item.tax_rate ?? item.taxRate,
189
+ 0,
190
+ );
191
+ const customFields = extractCustomFieldValues(
192
+ item as Record<string, unknown>,
193
+ );
194
+ const name =
195
+ typeof item.name === "string"
196
+ ? item.name
197
+ : typeof item.catalog_snapshot === "object" &&
198
+ item.catalog_snapshot &&
199
+ typeof (item.catalog_snapshot as Record<string, unknown>).name === "string"
200
+ ? (item.catalog_snapshot as Record<string, unknown>).name as string
201
+ : null;
202
+ const quantity = normalizeNumber(item.quantity, 0);
203
+ const uomFields = getUomFields(item);
204
+ const quantityUnit = canonicalizeUnitCode(uomFields.quantityUnit);
205
+ const normalizedQuantity = normalizeNumber(
206
+ uomFields.normalizedQuantity,
207
+ quantity,
208
+ );
209
+ const normalizedUnit =
210
+ canonicalizeUnitCode(uomFields.normalizedUnit) ?? quantityUnit;
211
+ const uomSnapshot = uomFields.uomSnapshot;
212
+ const unitPriceNetRaw = normalizeNumber(
213
+ item.unit_price_net ?? item.unitPriceNet,
214
+ Number.NaN,
215
+ );
216
+ const unitPriceGrossRaw = normalizeNumber(
217
+ item.unit_price_gross ?? item.unitPriceGross,
218
+ Number.NaN,
219
+ );
220
+ const unitPriceNet = Number.isFinite(unitPriceNetRaw)
221
+ ? unitPriceNetRaw
222
+ : Number.isFinite(unitPriceGrossRaw)
223
+ ? unitPriceGrossRaw / (1 + taxRate / 100)
224
+ : 0;
225
+ const unitPriceGross = Number.isFinite(unitPriceGrossRaw)
226
+ ? unitPriceGrossRaw
227
+ : Number.isFinite(unitPriceNetRaw)
228
+ ? unitPriceNetRaw * (1 + taxRate / 100)
229
+ : 0;
230
+ const totalNetRaw = normalizeNumber(
231
+ item.total_net_amount ?? item.totalNetAmount,
232
+ Number.NaN,
233
+ );
234
+ const totalGrossRaw = normalizeNumber(
235
+ item.total_gross_amount ?? item.totalGrossAmount,
236
+ Number.NaN,
237
+ );
238
+ const totalNet = Number.isFinite(totalNetRaw)
239
+ ? totalNetRaw
240
+ : unitPriceNet * quantity;
241
+ const totalGross = Number.isFinite(totalGrossRaw)
242
+ ? totalGrossRaw
243
+ : unitPriceGross * quantity;
244
+ const priceModeRaw =
245
+ item.metadata &&
246
+ typeof item.metadata === "object"
247
+ ? (item.metadata as Record<string, unknown>).priceMode
248
+ : null;
249
+ const priceMode = priceModeRaw === "net" ? "net" : "gross";
250
+ const customFieldSetId =
251
+ typeof item.custom_field_set_id === "string"
252
+ ? item.custom_field_set_id
253
+ : typeof item.customFieldSetId === "string"
254
+ ? item.customFieldSetId
255
+ : null;
256
+ const statusEntryId =
257
+ typeof item.status_entry_id === "string"
258
+ ? item.status_entry_id
259
+ : typeof item.statusEntryId === "string"
260
+ ? item.statusEntryId
261
+ : null;
262
+ const status = typeof item.status === "string" ? item.status : null;
263
+ const record: SalesLineRecord = {
264
+ id,
265
+ name,
266
+ productId:
267
+ typeof item.product_id === "string" ? item.product_id : null,
268
+ productVariantId:
269
+ typeof item.product_variant_id === "string"
270
+ ? item.product_variant_id
162
271
  : null,
163
- unitPriceNet,
164
- unitPriceGross,
165
- taxRate,
166
- totalNet,
167
- totalGross,
168
- priceMode,
169
- metadata: (item.metadata as Record<string, unknown> | null | undefined) ?? null,
170
- catalogSnapshot: (item.catalog_snapshot as Record<string, unknown> | null | undefined) ?? null,
171
- customFieldSetId,
172
- customFields: Object.keys(customFields).length ? customFields : null,
173
- status,
174
- statusEntryId,
175
- }
176
- return [record]
177
- })
178
- setItems(mapped)
179
- if (onItemsChange) onItemsChange(mapped)
272
+ quantity,
273
+ quantityUnit,
274
+ normalizedQuantity,
275
+ normalizedUnit,
276
+ currencyCode:
277
+ typeof item.currency_code === "string"
278
+ ? item.currency_code
279
+ : typeof currencyCode === "string"
280
+ ? currencyCode
281
+ : null,
282
+ unitPriceNet,
283
+ unitPriceGross,
284
+ taxRate,
285
+ totalNet,
286
+ totalGross,
287
+ priceMode,
288
+ uomSnapshot,
289
+ metadata:
290
+ (item.metadata as Record<string, unknown> | null | undefined) ??
291
+ null,
292
+ catalogSnapshot:
293
+ (item.catalog_snapshot as
294
+ | Record<string, unknown>
295
+ | null
296
+ | undefined) ?? null,
297
+ customFieldSetId,
298
+ customFields: Object.keys(customFields).length
299
+ ? customFields
300
+ : null,
301
+ status,
302
+ statusEntryId,
303
+ };
304
+ return [record];
305
+ },
306
+ );
307
+ setItems(mapped);
308
+ if (onItemsChange) onItemsChange(mapped);
180
309
  } else {
181
- setItems([])
182
- if (onItemsChange) onItemsChange([])
310
+ setItems([]);
311
+ if (onItemsChange) onItemsChange([]);
183
312
  }
184
313
  } catch (err) {
185
- console.error('sales.document.items.load', err)
186
- setError(t('sales.documents.items.errorLoad', 'Failed to load items.'))
187
- if (onItemsChange) onItemsChange([])
314
+ console.error("sales.document.items.load", err);
315
+ setError(t("sales.documents.items.errorLoad", "Failed to load items."));
316
+ if (onItemsChange) onItemsChange([]);
188
317
  } finally {
189
- setLoading(false)
318
+ setLoading(false);
190
319
  }
191
- }, [currencyCode, documentId, documentKey, onItemsChange, resourcePath, t])
320
+ }, [currencyCode, documentId, documentKey, onItemsChange, resourcePath, t]);
192
321
 
193
322
  const loadShippedTotals = React.useCallback(async () => {
194
- if (kind !== 'order') {
195
- setShippedTotals(new Map())
196
- return
323
+ if (kind !== "order") {
324
+ setShippedTotals(new Map());
325
+ return;
197
326
  }
198
327
  try {
199
- const params = new URLSearchParams({ page: '1', pageSize: '200', orderId: documentId })
200
- const response = await apiCall<{ items?: Array<Record<string, unknown>> }>(
201
- `/api/sales/shipments?${params.toString()}`,
202
- undefined,
203
- { fallback: { items: [] } },
204
- )
328
+ const params = new URLSearchParams({
329
+ page: "1",
330
+ pageSize: "100",
331
+ orderId: documentId,
332
+ });
333
+ const response = await apiCall<{
334
+ items?: Array<Record<string, unknown>>;
335
+ }>(`/api/sales/shipments?${params.toString()}`, undefined, {
336
+ fallback: { items: [] },
337
+ });
205
338
  if (response.ok && Array.isArray(response.result?.items)) {
206
- const totals = new Map<string, number>()
339
+ const totals = new Map<string, number>();
207
340
  response.result.items.forEach((shipment) => {
208
- const entries = Array.isArray((shipment as any).items)
209
- ? ((shipment as any).items as Array<Record<string, unknown>>)
210
- : []
341
+ const entries = Array.isArray(shipment.items)
342
+ ? (shipment.items as Array<Record<string, unknown>>)
343
+ : [];
211
344
  entries.forEach((entry) => {
212
345
  const lineId =
213
- typeof (entry as any).orderLineId === 'string'
214
- ? (entry as any).orderLineId
215
- : typeof (entry as any).order_line_id === 'string'
216
- ? (entry as any).order_line_id
217
- : null
218
- if (!lineId) return
219
- const quantity = normalizeNumber((entry as any).quantity, 0)
220
- if (!Number.isFinite(quantity) || quantity <= 0) return
221
- const current = totals.get(lineId) ?? 0
222
- totals.set(lineId, current + quantity)
223
- })
224
- })
225
- setShippedTotals(totals)
346
+ typeof entry.orderLineId === "string"
347
+ ? entry.orderLineId
348
+ : typeof entry.order_line_id === "string"
349
+ ? entry.order_line_id
350
+ : null;
351
+ if (!lineId) return;
352
+ const quantity = normalizeNumber(entry.quantity, 0);
353
+ if (!Number.isFinite(quantity) || quantity <= 0) return;
354
+ const current = totals.get(lineId) ?? 0;
355
+ totals.set(lineId, current + quantity);
356
+ });
357
+ });
358
+ setShippedTotals(totals);
226
359
  } else {
227
- setShippedTotals(new Map())
360
+ setShippedTotals(new Map());
228
361
  }
229
362
  } catch (err) {
230
- console.error('sales.document.shipments.load', err)
231
- setShippedTotals(new Map())
363
+ console.error("sales.document.shipments.load", err);
364
+ setShippedTotals(new Map());
232
365
  }
233
- }, [documentId, kind])
366
+ }, [documentId, kind]);
234
367
 
235
368
  React.useEffect(() => {
236
- if (lineStatusesLoaded.current) return
237
- lineStatusesLoaded.current = true
238
- void loadLineStatuses()
239
- }, [loadLineStatuses])
369
+ if (lineStatusesLoaded.current) return;
370
+ lineStatusesLoaded.current = true;
371
+ void loadLineStatuses();
372
+ }, [loadLineStatuses]);
240
373
 
241
374
  React.useEffect(() => {
242
- if (!documentId) return
243
- if (itemsLoadedForDocument.current === documentId) return
244
- itemsLoadedForDocument.current = documentId
245
- void loadItems()
246
- }, [documentId, loadItems])
375
+ if (!documentId) return;
376
+ if (itemsLoadedForDocument.current === documentId) return;
377
+ itemsLoadedForDocument.current = documentId;
378
+ void loadItems();
379
+ }, [documentId, loadItems]);
247
380
 
248
381
  React.useEffect(() => {
249
- if (kind !== 'order') {
250
- shipmentsLoadedForDocument.current = null
251
- setShippedTotals(new Map())
252
- return
382
+ if (kind !== "order") {
383
+ shipmentsLoadedForDocument.current = null;
384
+ setShippedTotals(new Map());
385
+ return;
253
386
  }
254
- const key = `${kind}:${documentId}`
255
- if (shipmentsLoadedForDocument.current === key) return
256
- shipmentsLoadedForDocument.current = key
257
- void loadShippedTotals()
258
- }, [documentId, kind, loadShippedTotals])
387
+ const key = `${kind}:${documentId}`;
388
+ if (shipmentsLoadedForDocument.current === key) return;
389
+ shipmentsLoadedForDocument.current = key;
390
+ void loadShippedTotals();
391
+ }, [documentId, kind, loadShippedTotals]);
259
392
 
260
393
  const openCreate = React.useCallback(() => {
261
- setLineForEdit(null)
262
- setDialogOpen(true)
263
- }, [])
394
+ setLineForEdit(null);
395
+ setDialogOpen(true);
396
+ }, []);
264
397
 
265
398
  React.useEffect(() => {
266
- if (!onActionChange) return
399
+ if (!onActionChange) return;
267
400
  if (items.length === 0) {
268
- onActionChange(null)
269
- return
401
+ onActionChange(null);
402
+ return;
270
403
  }
271
404
  onActionChange({
272
- label: t('sales.documents.items.add', 'Add item'),
405
+ label: t("sales.documents.items.add", "Add item"),
273
406
  onClick: openCreate,
274
407
  disabled: false,
275
- })
276
- return () => onActionChange(null)
277
- }, [items.length, onActionChange, openCreate, t])
408
+ });
409
+ return () => onActionChange(null);
410
+ }, [items.length, onActionChange, openCreate, t]);
278
411
 
279
412
  const handleEdit = React.useCallback((line: SalesLineRecord) => {
280
- setLineForEdit(line)
281
- setDialogOpen(true)
282
- }, [])
413
+ setLineForEdit(line);
414
+ setDialogOpen(true);
415
+ }, []);
283
416
 
284
417
  const resolveVariantInfo = React.useCallback((record: SalesLineRecord) => {
285
- const meta = (record.metadata as Record<string, unknown> | null | undefined) ?? null
286
- const snapshot = (record.catalogSnapshot as Record<string, unknown> | null | undefined) ?? null
418
+ const meta =
419
+ (record.metadata as Record<string, unknown> | null | undefined) ?? null;
420
+ const snapshot =
421
+ (record.catalogSnapshot as Record<string, unknown> | null | undefined) ??
422
+ null;
287
423
  const variantSnapshot =
288
- snapshot && typeof (snapshot as any).variant === 'object' && (snapshot as any).variant
289
- ? ((snapshot as any).variant as Record<string, unknown>)
290
- : null
424
+ snapshot &&
425
+ typeof snapshot.variant === "object" &&
426
+ snapshot.variant
427
+ ? (snapshot.variant as Record<string, unknown>)
428
+ : null;
291
429
  const variantTitle =
292
- meta && typeof (meta as any).variantTitle === 'string'
293
- ? (meta as any).variantTitle
294
- : variantSnapshot && typeof (variantSnapshot as any).name === 'string'
295
- ? (variantSnapshot as any).name
296
- : null
430
+ meta && typeof meta.variantTitle === "string"
431
+ ? meta.variantTitle
432
+ : variantSnapshot && typeof variantSnapshot.name === "string"
433
+ ? variantSnapshot.name
434
+ : null;
297
435
  const variantSku =
298
- meta && typeof (meta as any).variantSku === 'string'
299
- ? (meta as any).variantSku
300
- : variantSnapshot && typeof (variantSnapshot as any).sku === 'string'
301
- ? (variantSnapshot as any).sku
302
- : null
436
+ meta && typeof meta.variantSku === "string"
437
+ ? meta.variantSku
438
+ : variantSnapshot && typeof variantSnapshot.sku === "string"
439
+ ? variantSnapshot.sku
440
+ : null;
303
441
 
304
- return { variantTitle, variantSku }
305
- }, [])
442
+ return { variantTitle, variantSku };
443
+ }, []);
306
444
 
307
445
  const handleDelete = React.useCallback(
308
446
  async (line: SalesLineRecord) => {
309
447
  const confirmed = await confirm({
310
- title: t('sales.documents.items.deleteConfirm', 'Delete this line item?'),
311
- variant: 'destructive',
312
- })
313
- if (!confirmed) return
448
+ title: t(
449
+ "sales.documents.items.deleteConfirm",
450
+ "Delete this line item?",
451
+ ),
452
+ variant: "destructive",
453
+ });
454
+ if (!confirmed) return;
314
455
  try {
315
456
  const result = await deleteCrud(resourcePath, {
316
457
  body: {
@@ -319,28 +460,48 @@ export function SalesDocumentItemsSection({
319
460
  organizationId: resolvedOrganizationId ?? undefined,
320
461
  tenantId: resolvedTenantId ?? undefined,
321
462
  },
322
- errorMessage: t('sales.documents.items.errorDelete', 'Failed to delete line.'),
323
- })
463
+ errorMessage: t(
464
+ "sales.documents.items.errorDelete",
465
+ "Failed to delete line.",
466
+ ),
467
+ });
324
468
  if (result.ok) {
325
- flash(t('sales.documents.items.deleted', 'Line removed.'), 'success')
326
- await loadItems()
327
- emitSalesDocumentTotalsRefresh({ documentId, kind })
469
+ flash(t("sales.documents.items.deleted", "Line removed."), "success");
470
+ await loadItems();
471
+ emitSalesDocumentTotalsRefresh({ documentId, kind });
328
472
  }
329
473
  } catch (err) {
330
- console.error('sales.document.items.delete', err)
331
- const normalized = normalizeCrudServerError(err)
332
- const fallback = t('sales.documents.items.errorDelete', 'Failed to delete line.')
333
- flash(normalized.message || fallback, 'error')
474
+ console.error("sales.document.items.delete", err);
475
+ const normalized = normalizeCrudServerError(err);
476
+ const fallback = t(
477
+ "sales.documents.items.errorDelete",
478
+ "Failed to delete line.",
479
+ );
480
+ flash(normalized.message || fallback, "error");
334
481
  }
335
482
  },
336
- [confirm, documentId, documentKey, kind, loadItems, resolvedOrganizationId, resourcePath, t, resolvedTenantId],
337
- )
483
+ [
484
+ confirm,
485
+ documentId,
486
+ documentKey,
487
+ kind,
488
+ loadItems,
489
+ resolvedOrganizationId,
490
+ resourcePath,
491
+ t,
492
+ resolvedTenantId,
493
+ ],
494
+ );
338
495
 
339
496
  const renderStatus = React.useCallback(
340
497
  (line: SalesLineRecord) => {
341
- const value = line.status ?? null
498
+ const value = line.status ?? null;
342
499
  if (!value) {
343
- return <span className="text-xs text-muted-foreground">{t('sales.documents.items.table.statusEmpty', 'No status')}</span>
500
+ return (
501
+ <span className="text-xs text-muted-foreground">
502
+ {t("sales.documents.items.table.statusEmpty", "No status")}
503
+ </span>
504
+ );
344
505
  }
345
506
  return (
346
507
  <DictionaryValue
@@ -352,53 +513,76 @@ export function SalesDocumentItemsSection({
352
513
  iconClassName="h-3.5 w-3.5"
353
514
  colorClassName="h-4 w-4 rounded-full border border-border/70"
354
515
  />
355
- )
516
+ );
356
517
  },
357
518
  [lineStatusMap, t],
358
- )
519
+ );
359
520
 
360
521
  const renderImage = (record: SalesLineRecord) => {
361
- const meta = (record.metadata as Record<string, unknown> | null | undefined) ?? {}
362
- const snapshot = (record.catalogSnapshot as Record<string, unknown> | null | undefined) ?? {}
363
- const productSnapshot = typeof snapshot === 'object' && snapshot ? (snapshot as any).product ?? {} : {}
364
- const variantSnapshot = typeof snapshot === 'object' && snapshot ? (snapshot as any).variant ?? {} : {}
522
+ const meta =
523
+ (record.metadata as Record<string, unknown> | null | undefined) ?? {};
524
+ const snapshot =
525
+ (record.catalogSnapshot as Record<string, unknown> | null | undefined) ??
526
+ {};
527
+ const productSnapshot: Record<string, unknown> =
528
+ typeof snapshot === "object" && snapshot
529
+ ? ((snapshot.product as Record<string, unknown>) ?? {})
530
+ : {};
531
+ const variantSnapshot: Record<string, unknown> =
532
+ typeof snapshot === "object" && snapshot
533
+ ? ((snapshot.variant as Record<string, unknown>) ?? {})
534
+ : {};
365
535
  const productThumb =
366
- (meta && typeof (meta as any).productThumbnail === 'string' && (meta as any).productThumbnail) ||
367
- (productSnapshot && typeof productSnapshot.thumbnailUrl === 'string' && productSnapshot.thumbnailUrl) ||
368
- null
536
+ (meta &&
537
+ typeof meta.productThumbnail === "string" &&
538
+ meta.productThumbnail) ||
539
+ (productSnapshot &&
540
+ typeof productSnapshot.thumbnailUrl === "string" &&
541
+ productSnapshot.thumbnailUrl) ||
542
+ null;
369
543
  const variantThumb =
370
- (meta && typeof (meta as any).variantThumbnail === 'string' && (meta as any).variantThumbnail) ||
371
- (variantSnapshot && typeof variantSnapshot.thumbnailUrl === 'string' && variantSnapshot.thumbnailUrl) ||
372
- null
373
- const thumbnail = variantThumb ?? productThumb
544
+ (meta &&
545
+ typeof meta.variantThumbnail === "string" &&
546
+ meta.variantThumbnail) ||
547
+ (variantSnapshot &&
548
+ typeof variantSnapshot.thumbnailUrl === "string" &&
549
+ variantSnapshot.thumbnailUrl) ||
550
+ null;
551
+ const thumbnail = variantThumb ?? productThumb;
374
552
  if (thumbnail) {
375
- return <img src={thumbnail} alt={record.name ?? record.id} className="h-10 w-10 rounded border object-cover" />
553
+ return (
554
+ <img
555
+ src={thumbnail}
556
+ alt={record.name ?? record.id}
557
+ className="h-10 w-10 rounded border object-cover"
558
+ />
559
+ );
376
560
  }
377
561
  return (
378
562
  <div className="flex h-10 w-10 items-center justify-center rounded border bg-muted text-xs text-muted-foreground">
379
563
  N/A
380
564
  </div>
381
- )
382
- }
565
+ );
566
+ };
383
567
 
384
568
  return (
385
569
  <div className="space-y-4">
386
570
  {loading ? (
387
571
  <LoadingMessage
388
- label={t('sales.documents.items.loading', 'Loading items…')}
572
+ label={t("sales.documents.items.loading", "Loading items…")}
389
573
  className="border-0 bg-transparent p-0 py-8 justify-center"
390
574
  />
391
575
  ) : error ? (
392
576
  <p className="text-sm text-destructive">{error}</p>
393
577
  ) : items.length === 0 ? (
394
578
  <TabEmptyState
395
- title={t('sales.documents.items.empty', 'No items yet.')}
579
+ title={t("sales.documents.items.empty", "No items yet.")}
396
580
  description={t(
397
- 'sales.documents.items.subtitle',
398
- 'Add products and configure pricing for this document.'
581
+ "sales.documents.items.subtitle",
582
+ "Add products and configure pricing for this document.",
399
583
  )}
400
584
  action={{
401
- label: t('sales.documents.items.add', 'Add item'),
585
+ label: t("sales.documents.items.add", "Add item"),
402
586
  onClick: openCreate,
403
587
  }}
404
588
  />
@@ -407,23 +591,63 @@ export function SalesDocumentItemsSection({
407
591
  <table className="w-full text-sm">
408
592
  <thead className="bg-muted">
409
593
  <tr className="text-left">
410
- <th className="px-3 py-2 font-medium">{t('sales.documents.items.table.product', 'Product')}</th>
411
- <th className="px-3 py-2 font-medium">{t('sales.documents.items.table.status', 'Status')}</th>
412
- <th className="px-3 py-2 font-medium">{t('sales.documents.items.table.quantity', 'Qty')}</th>
413
- <th className="px-3 py-2 font-medium">{t('sales.documents.items.table.unit', 'Unit price')}</th>
414
- <th className="px-3 py-2 font-medium">{t('sales.documents.items.table.total', 'Total')}</th>
415
- <th className="px-3 py-2 font-medium sr-only">{t('sales.documents.items.table.actions', 'Actions')}</th>
594
+ <th className="px-3 py-2 font-medium">
595
+ {t("sales.documents.items.table.product", "Product")}
596
+ </th>
597
+ <th className="px-3 py-2 font-medium">
598
+ {t("sales.documents.items.table.status", "Status")}
599
+ </th>
600
+ <th className="px-3 py-2 font-medium">
601
+ {t("sales.documents.items.table.quantity", "Qty")}
602
+ </th>
603
+ <th className="px-3 py-2 font-medium">
604
+ {t("sales.documents.items.table.unit", "Unit price")}
605
+ </th>
606
+ <th className="px-3 py-2 font-medium">
607
+ {t("sales.documents.items.table.total", "Total")}
608
+ </th>
609
+ <th className="px-3 py-2 font-medium sr-only">
610
+ {t("sales.documents.items.table.actions", "Actions")}
611
+ </th>
416
612
  </tr>
417
613
  </thead>
418
614
  <tbody>
419
615
  {items.map((item) => {
420
- const meta = (item.metadata as Record<string, unknown> | null | undefined) ?? null
421
- const { variantTitle, variantSku } = resolveVariantInfo(item)
422
- const productSku = meta && typeof (meta as any).productSku === 'string' ? (meta as any).productSku : null
423
- const variantLabel = variantTitle ?? variantSku
424
- const variantSuffix = variantSku && variantLabel && variantSku !== variantLabel ? ` • ${variantSku}` : ''
425
- const showProductSku = productSku && productSku !== variantSku ? productSku : null
426
- const shippedQuantity = Math.max(0, shippedTotals.get(item.id) ?? 0)
616
+ const meta =
617
+ (item.metadata as
618
+ | Record<string, unknown>
619
+ | null
620
+ | undefined) ?? null;
621
+ const { variantTitle, variantSku } = resolveVariantInfo(item);
622
+ const productSku =
623
+ meta && typeof meta.productSku === "string"
624
+ ? meta.productSku
625
+ : null;
626
+ const variantLabel = variantTitle ?? variantSku;
627
+ const variantSuffix =
628
+ variantSku && variantLabel && variantSku !== variantLabel
629
+ ? ` • ${variantSku}`
630
+ : "";
631
+ const showProductSku =
632
+ productSku && productSku !== variantSku ? productSku : null;
633
+ const shippedQuantity = Math.max(
634
+ 0,
635
+ shippedTotals.get(item.id) ?? 0,
636
+ );
637
+ const quantityLabel = item.quantityUnit
638
+ ? `${item.quantity} ${item.quantityUnit}`
639
+ : String(item.quantity);
640
+ const showNormalized =
641
+ Number.isFinite(item.normalizedQuantity) &&
642
+ item.normalizedQuantity > 0 &&
643
+ (item.normalizedUnit ?? null) &&
644
+ (Math.abs(item.normalizedQuantity - item.quantity) >
645
+ 0.000001 ||
646
+ (item.normalizedUnit ?? null) !==
647
+ (item.quantityUnit ?? null));
648
+ const unitPriceReference = resolveUnitPriceReference(
649
+ item.uomSnapshot,
650
+ );
427
651
 
428
652
  return (
429
653
  <tr
@@ -435,7 +659,10 @@ export function SalesDocumentItemsSection({
435
659
  <div className="flex items-center gap-3">
436
660
  {renderImage(item)}
437
661
  <div className="min-w-0">
438
- <div className="truncate font-medium">{item.name ?? t('sales.documents.items.untitled', 'Untitled')}</div>
662
+ <div className="truncate font-medium">
663
+ {item.name ??
664
+ t("sales.documents.items.untitled", "Untitled")}
665
+ </div>
439
666
  {variantLabel ? (
440
667
  <div className="text-xs text-muted-foreground truncate">
441
668
  {variantLabel}
@@ -443,23 +670,36 @@ export function SalesDocumentItemsSection({
443
670
  </div>
444
671
  ) : null}
445
672
  {showProductSku ? (
446
- <div className="text-xs text-muted-foreground truncate">{showProductSku}</div>
673
+ <div className="text-xs text-muted-foreground truncate">
674
+ {showProductSku}
675
+ </div>
447
676
  ) : null}
448
677
  </div>
449
678
  </div>
450
679
  </td>
451
680
  <td className="px-3 py-3">
452
- <div className="flex items-center">{renderStatus(item)}</div>
681
+ <div className="flex items-center">
682
+ {renderStatus(item)}
683
+ </div>
453
684
  </td>
454
685
  <td className="px-3 py-3">
455
686
  <div className="flex flex-col gap-0.5">
456
- <span className="font-medium">{item.quantity}</span>
687
+ <span className="font-medium">{quantityLabel}</span>
688
+ {showNormalized ? (
689
+ <span className="text-xs text-muted-foreground">
690
+ {item.normalizedQuantity} {item.normalizedUnit}
691
+ </span>
692
+ ) : null}
457
693
  {shippedQuantity > 0 ? (
458
694
  <span className="text-xs text-muted-foreground">
459
- {t('sales.documents.items.table.shipped', '{{shipped}} / {{total}} shipped', {
460
- shipped: shippedQuantity,
461
- total: item.quantity,
462
- })}
695
+ {t(
696
+ "sales.documents.items.table.shipped",
697
+ "{{shipped}} / {{total}} shipped",
698
+ {
699
+ shipped: shippedQuantity,
700
+ total: item.quantity,
701
+ },
702
+ )}
463
703
  </span>
464
704
  ) : null}
465
705
  </div>
@@ -467,27 +707,57 @@ export function SalesDocumentItemsSection({
467
707
  <td className="px-3 py-3">
468
708
  <div className="flex flex-col gap-0.5">
469
709
  <span className="font-mono text-sm">
470
- {formatMoney(item.unitPriceGross, item.currencyCode ?? currencyCode ?? undefined)}{' '}
710
+ {formatMoney(
711
+ item.unitPriceGross,
712
+ item.currencyCode ?? currencyCode ?? undefined,
713
+ )}{" "}
471
714
  <span className="text-xs text-muted-foreground">
472
- {t('sales.documents.items.table.gross', 'gross')}
715
+ {t("sales.documents.items.table.gross", "gross")}
473
716
  </span>
474
717
  </span>
475
718
  <span className="font-mono text-xs text-muted-foreground">
476
- {formatMoney(item.unitPriceNet, item.currencyCode ?? currencyCode ?? undefined)}{' '}
477
- {t('sales.documents.items.table.net', 'net')}
719
+ {formatMoney(
720
+ item.unitPriceNet,
721
+ item.currencyCode ?? currencyCode ?? undefined,
722
+ )}{" "}
723
+ {t("sales.documents.items.table.net", "net")}
478
724
  </span>
725
+ {unitPriceReference ? (
726
+ <span className="text-xs text-muted-foreground">
727
+ {t(
728
+ "sales.documents.items.table.unitPriceReference",
729
+ "{{value}} per 1 {{unit}}",
730
+ {
731
+ value: formatMoney(
732
+ unitPriceReference.grossPerReference,
733
+ item.currencyCode ??
734
+ currencyCode ??
735
+ undefined,
736
+ ),
737
+ unit: unitPriceReference.referenceUnitCode,
738
+ },
739
+ )}
740
+ </span>
741
+ ) : null}
479
742
  </div>
480
743
  </td>
481
744
  <td className="px-3 py-3 font-semibold">
482
745
  <div className="flex flex-col gap-0.5">
483
746
  <span>
484
- {formatMoney(item.totalGross, item.currencyCode ?? currencyCode ?? undefined)}{' '}
747
+ {formatMoney(
748
+ item.totalGross,
749
+ item.currencyCode ?? currencyCode ?? undefined,
750
+ )}{" "}
485
751
  <span className="text-xs font-normal text-muted-foreground">
486
- {t('sales.documents.items.table.gross', 'gross')}
752
+ {t("sales.documents.items.table.gross", "gross")}
487
753
  </span>
488
754
  </span>
489
755
  <span className="text-xs font-medium text-muted-foreground">
490
- {formatMoney(item.totalNet, item.currencyCode ?? currencyCode ?? undefined)} {t('sales.documents.items.table.net', 'net')}
756
+ {formatMoney(
757
+ item.totalNet,
758
+ item.currencyCode ?? currencyCode ?? undefined,
759
+ )}{" "}
760
+ {t("sales.documents.items.table.net", "net")}
491
761
  </span>
492
762
  </div>
493
763
  </td>
@@ -498,8 +768,8 @@ export function SalesDocumentItemsSection({
498
768
  variant="ghost"
499
769
  className="h-8 w-8"
500
770
  onClick={(event) => {
501
- event.stopPropagation()
502
- handleEdit(item)
771
+ event.stopPropagation();
772
+ handleEdit(item);
503
773
  }}
504
774
  >
505
775
  <Pencil className="h-4 w-4" />
@@ -509,8 +779,8 @@ export function SalesDocumentItemsSection({
509
779
  variant="ghost"
510
780
  className="h-8 w-8 text-destructive"
511
781
  onClick={(event) => {
512
- event.stopPropagation()
513
- void handleDelete(item)
782
+ event.stopPropagation();
783
+ void handleDelete(item);
514
784
  }}
515
785
  >
516
786
  <Trash2 className="h-4 w-4" />
@@ -518,7 +788,7 @@ export function SalesDocumentItemsSection({
518
788
  </div>
519
789
  </td>
520
790
  </tr>
521
- )
791
+ );
522
792
  })}
523
793
  </tbody>
524
794
  </table>
@@ -528,8 +798,8 @@ export function SalesDocumentItemsSection({
528
798
  <LineItemDialog
529
799
  open={dialogOpen}
530
800
  onOpenChange={(next) => {
531
- setDialogOpen(next)
532
- if (!next) setLineForEdit(null)
801
+ setDialogOpen(next);
802
+ if (!next) setLineForEdit(null);
533
803
  }}
534
804
  kind={kind}
535
805
  documentId={documentId}
@@ -538,11 +808,11 @@ export function SalesDocumentItemsSection({
538
808
  tenantId={resolvedTenantId}
539
809
  initialLine={lineForEdit}
540
810
  onSaved={async () => {
541
- await loadItems()
542
- emitSalesDocumentTotalsRefresh({ documentId, kind })
811
+ await loadItems();
812
+ emitSalesDocumentTotalsRefresh({ documentId, kind });
543
813
  }}
544
814
  />
545
815
  {ConfirmDialogElement}
546
816
  </div>
547
- )
817
+ );
548
818
  }