@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.
- package/dist/generated/entities/catalog_product/index.js +16 -0
- package/dist/generated/entities/catalog_product/index.js.map +2 -2
- package/dist/generated/entities/catalog_product_unit_conversion/index.js +27 -0
- package/dist/generated/entities/catalog_product_unit_conversion/index.js.map +7 -0
- package/dist/generated/entities/sales_credit_memo_line/index.js +7 -1
- package/dist/generated/entities/sales_credit_memo_line/index.js.map +2 -2
- package/dist/generated/entities/sales_invoice_line/index.js +7 -1
- package/dist/generated/entities/sales_invoice_line/index.js.map +2 -2
- package/dist/generated/entities/sales_order_line/index.js +6 -0
- package/dist/generated/entities/sales_order_line/index.js.map +2 -2
- package/dist/generated/entities/sales_quote_line/index.js +6 -0
- package/dist/generated/entities/sales_quote_line/index.js.map +2 -2
- package/dist/generated/entities.ids.generated.js +1 -0
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +2 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/catalog/api/prices/route.js +123 -8
- package/dist/modules/catalog/api/prices/route.js.map +2 -2
- package/dist/modules/catalog/api/product-unit-conversions/route.js +194 -0
- package/dist/modules/catalog/api/product-unit-conversions/route.js.map +7 -0
- package/dist/modules/catalog/api/products/route.js +351 -201
- package/dist/modules/catalog/api/products/route.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js +1267 -497
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/create/page.js +733 -210
- package/dist/modules/catalog/backend/catalog/products/create/page.js.map +2 -2
- package/dist/modules/catalog/commands/index.js +1 -0
- package/dist/modules/catalog/commands/index.js.map +2 -2
- package/dist/modules/catalog/commands/productUnitConversions.js +503 -0
- package/dist/modules/catalog/commands/productUnitConversions.js.map +7 -0
- package/dist/modules/catalog/commands/products.js +355 -73
- package/dist/modules/catalog/commands/products.js.map +2 -2
- package/dist/modules/catalog/commands/shared.js +18 -4
- package/dist/modules/catalog/commands/shared.js.map +2 -2
- package/dist/modules/catalog/components/products/ProductUomSection.js +591 -0
- package/dist/modules/catalog/components/products/ProductUomSection.js.map +7 -0
- package/dist/modules/catalog/components/products/productForm.js +66 -5
- package/dist/modules/catalog/components/products/productForm.js.map +2 -2
- package/dist/modules/catalog/components/products/productFormUtils.js +68 -0
- package/dist/modules/catalog/components/products/productFormUtils.js.map +7 -0
- package/dist/modules/catalog/data/entities.js +86 -0
- package/dist/modules/catalog/data/entities.js.map +2 -2
- package/dist/modules/catalog/data/validators.js +65 -3
- package/dist/modules/catalog/data/validators.js.map +2 -2
- package/dist/modules/catalog/events.js +3 -0
- package/dist/modules/catalog/events.js.map +2 -2
- package/dist/modules/catalog/lib/unitCodes.js +7 -0
- package/dist/modules/catalog/lib/unitCodes.js.map +7 -0
- package/dist/modules/catalog/lib/unitResolution.js +53 -0
- package/dist/modules/catalog/lib/unitResolution.js.map +7 -0
- package/dist/modules/catalog/migrations/Migration20260218225422.js +19 -0
- package/dist/modules/catalog/migrations/Migration20260218225422.js.map +7 -0
- package/dist/modules/catalog/migrations/Migration20260219084500.js +27 -0
- package/dist/modules/catalog/migrations/Migration20260219084500.js.map +7 -0
- package/dist/modules/catalog/search.js +69 -1
- package/dist/modules/catalog/search.js.map +2 -2
- package/dist/modules/catalog/seed/examples.js +91 -42
- package/dist/modules/catalog/seed/examples.js.map +2 -2
- package/dist/modules/dashboards/seed/analytics.js +3 -0
- package/dist/modules/dashboards/seed/analytics.js.map +2 -2
- package/dist/modules/sales/api/order-lines/route.js +98 -15
- package/dist/modules/sales/api/order-lines/route.js.map +2 -2
- package/dist/modules/sales/api/quote-lines/route.js +101 -14
- package/dist/modules/sales/api/quote-lines/route.js.map +2 -2
- package/dist/modules/sales/api/quotes/public/[token]/route.js +87 -12
- package/dist/modules/sales/api/quotes/public/[token]/route.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +1424 -260
- package/dist/modules/sales/commands/documents.js.map +3 -3
- package/dist/modules/sales/commands/shared.js +6 -2
- package/dist/modules/sales/commands/shared.js.map +2 -2
- package/dist/modules/sales/components/documents/ItemsSection.js +216 -86
- package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/LineItemDialog.js +913 -241
- package/dist/modules/sales/components/documents/LineItemDialog.js.map +3 -3
- package/dist/modules/sales/components/documents/ShipmentsSection.js +15 -3
- package/dist/modules/sales/components/documents/ShipmentsSection.js.map +2 -2
- package/dist/modules/sales/data/entities.js +59 -3
- package/dist/modules/sales/data/entities.js.map +2 -2
- package/dist/modules/sales/data/validators.js +35 -0
- package/dist/modules/sales/data/validators.js.map +2 -2
- package/dist/modules/sales/frontend/quote/[token]/page.js +15 -1
- package/dist/modules/sales/frontend/quote/[token]/page.js.map +2 -2
- package/dist/modules/sales/migrations/Migration20260218225423.js +31 -0
- package/dist/modules/sales/migrations/Migration20260218225423.js.map +7 -0
- package/dist/modules/sales/migrations/Migration20260219084501.js +71 -0
- package/dist/modules/sales/migrations/Migration20260219084501.js.map +7 -0
- package/dist/modules/sales/search.js +28 -0
- package/dist/modules/sales/search.js.map +2 -2
- package/dist/modules/sales/seed/examples.js +14 -1
- package/dist/modules/sales/seed/examples.js.map +2 -2
- package/dist/modules/sales/widgets/injection/document-history/widget.client.js +1 -1
- package/dist/modules/sales/widgets/injection/document-history/widget.client.js.map +2 -2
- package/dist/modules/staff/backend/staff/team-members/[id]/page.js +28 -15
- package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
- package/dist/modules/staff/translations.js +9 -0
- package/dist/modules/staff/translations.js.map +7 -0
- package/dist/modules/translations/components/TranslationDrawerAction.js +97 -0
- package/dist/modules/translations/components/TranslationDrawerAction.js.map +7 -0
- package/dist/modules/translations/lib/extract-record-id.js +31 -2
- package/dist/modules/translations/lib/extract-record-id.js.map +2 -2
- package/dist/modules/translations/lib/resolve-field-list.js +3 -0
- package/dist/modules/translations/lib/resolve-field-list.js.map +2 -2
- package/dist/modules/translations/widgets/injection/translation-manager/widget.client.js +105 -36
- package/dist/modules/translations/widgets/injection/translation-manager/widget.client.js.map +2 -2
- package/dist/modules/translations/widgets/injection-table.js +18 -29
- package/dist/modules/translations/widgets/injection-table.js.map +2 -2
- package/generated/entities/catalog_product/index.ts +8 -0
- package/generated/entities/catalog_product_unit_conversion/index.ts +12 -0
- package/generated/entities/sales_credit_memo_line/index.ts +3 -0
- package/generated/entities/sales_invoice_line/index.ts +3 -0
- package/generated/entities/sales_order_line/index.ts +3 -0
- package/generated/entities/sales_quote_line/index.ts +3 -0
- package/generated/entities.ids.generated.ts +1 -0
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +2 -2
- package/src/modules/auth/i18n/de.json +1 -1
- package/src/modules/auth/i18n/en.json +1 -1
- package/src/modules/auth/i18n/es.json +1 -1
- package/src/modules/auth/i18n/pl.json +1 -1
- package/src/modules/catalog/api/prices/route.ts +213 -81
- package/src/modules/catalog/api/product-unit-conversions/route.ts +195 -0
- package/src/modules/catalog/api/products/route.ts +638 -402
- package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +2085 -1072
- package/src/modules/catalog/backend/catalog/products/create/page.tsx +1288 -593
- package/src/modules/catalog/commands/index.ts +1 -0
- package/src/modules/catalog/commands/productUnitConversions.ts +626 -0
- package/src/modules/catalog/commands/products.ts +1151 -693
- package/src/modules/catalog/commands/shared.ts +19 -5
- package/src/modules/catalog/components/products/ProductUomSection.tsx +745 -0
- package/src/modules/catalog/components/products/productForm.ts +369 -256
- package/src/modules/catalog/components/products/productFormUtils.ts +82 -0
- package/src/modules/catalog/data/entities.ts +82 -1
- package/src/modules/catalog/data/validators.ts +118 -34
- package/src/modules/catalog/events.ts +3 -0
- package/src/modules/catalog/i18n/de.json +56 -0
- package/src/modules/catalog/i18n/en.json +56 -0
- package/src/modules/catalog/i18n/es.json +56 -0
- package/src/modules/catalog/i18n/pl.json +56 -0
- package/src/modules/catalog/lib/unitCodes.ts +1 -0
- package/src/modules/catalog/lib/unitResolution.ts +62 -0
- package/src/modules/catalog/migrations/.snapshot-open-mercato.json +245 -0
- package/src/modules/catalog/migrations/Migration20260218225422.ts +21 -0
- package/src/modules/catalog/migrations/Migration20260219084500.ts +26 -0
- package/src/modules/catalog/search.ts +73 -1
- package/src/modules/catalog/seed/examples.ts +552 -479
- package/src/modules/dashboards/i18n/de.json +1 -1
- package/src/modules/dashboards/i18n/en.json +1 -1
- package/src/modules/dashboards/i18n/es.json +1 -1
- package/src/modules/dashboards/i18n/pl.json +1 -1
- package/src/modules/dashboards/seed/analytics.ts +3 -0
- package/src/modules/sales/api/order-lines/route.ts +158 -68
- package/src/modules/sales/api/quote-lines/route.ts +161 -67
- package/src/modules/sales/api/quotes/public/[token]/route.ts +122 -36
- package/src/modules/sales/commands/documents.ts +4250 -2424
- package/src/modules/sales/commands/shared.ts +7 -2
- package/src/modules/sales/components/documents/ItemsSection.tsx +580 -310
- package/src/modules/sales/components/documents/LineItemDialog.tsx +1988 -833
- package/src/modules/sales/components/documents/ShipmentsSection.tsx +17 -3
- package/src/modules/sales/components/documents/lineItemTypes.ts +6 -0
- package/src/modules/sales/data/entities.ts +53 -0
- package/src/modules/sales/data/validators.ts +36 -0
- package/src/modules/sales/frontend/quote/[token]/page.tsx +25 -1
- package/src/modules/sales/i18n/de.json +23 -3
- package/src/modules/sales/i18n/en.json +23 -3
- package/src/modules/sales/i18n/es.json +23 -3
- package/src/modules/sales/i18n/pl.json +23 -3
- package/src/modules/sales/lib/types.ts +30 -0
- package/src/modules/sales/migrations/.snapshot-open-mercato.json +172 -0
- package/src/modules/sales/migrations/Migration20260218225423.ts +37 -0
- package/src/modules/sales/migrations/Migration20260219084501.ts +73 -0
- package/src/modules/sales/search.ts +28 -0
- package/src/modules/sales/seed/examples.ts +20 -1
- package/src/modules/sales/widgets/injection/document-history/widget.client.tsx +1 -1
- package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +8 -0
- package/src/modules/staff/translations.ts +5 -0
- package/src/modules/translations/components/TranslationDrawerAction.tsx +107 -0
- package/src/modules/translations/lib/extract-record-id.ts +47 -3
- package/src/modules/translations/lib/resolve-field-list.ts +4 -0
- package/src/modules/translations/widgets/injection/translation-manager/widget.client.tsx +108 -36
- package/src/modules/translations/widgets/injection-table.ts +19 -33
- package/src/modules/workflows/i18n/de.json +4 -4
- package/src/modules/workflows/i18n/en.json +4 -4
- package/src/modules/workflows/i18n/es.json +4 -4
- 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
|
|
4
|
-
import { apiCall } from
|
|
5
|
-
import { deleteCrud } from
|
|
6
|
-
import { normalizeCrudServerError } from
|
|
7
|
-
import { LoadingMessage, TabEmptyState } from
|
|
8
|
-
import { Button } from
|
|
9
|
-
import { flash } from
|
|
10
|
-
import { Pencil, Trash2 } from
|
|
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
|
|
17
|
-
import { useT } from
|
|
18
|
-
import { useOrganizationScopeDetail } from
|
|
19
|
-
import { useConfirmDialog } from
|
|
20
|
-
import { emitSalesDocumentTotalsRefresh } from
|
|
21
|
-
import { LineItemDialog } from
|
|
22
|
-
import type { SalesLineRecord } from
|
|
23
|
-
import { formatMoney, normalizeNumber } from
|
|
24
|
-
import type { SectionAction } from
|
|
25
|
-
import { extractCustomFieldValues } from
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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>(
|
|
56
|
-
|
|
57
|
-
|
|
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 ===
|
|
145
|
+
() => (kind === "order" ? "sales/order-lines" : "sales/quote-lines"),
|
|
61
146
|
[kind],
|
|
62
|
-
)
|
|
63
|
-
const documentKey = kind ===
|
|
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:
|
|
70
|
-
const response = await apiCall<{
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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(
|
|
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({
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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>(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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(
|
|
186
|
-
setError(t(
|
|
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 !==
|
|
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({
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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(
|
|
209
|
-
? (
|
|
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
|
|
214
|
-
?
|
|
215
|
-
: typeof
|
|
216
|
-
?
|
|
217
|
-
: null
|
|
218
|
-
if (!lineId) return
|
|
219
|
-
const quantity = normalizeNumber(
|
|
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(
|
|
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 !==
|
|
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(
|
|
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 =
|
|
286
|
-
|
|
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 &&
|
|
289
|
-
|
|
290
|
-
|
|
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
|
|
293
|
-
?
|
|
294
|
-
: variantSnapshot && typeof
|
|
295
|
-
?
|
|
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
|
|
299
|
-
?
|
|
300
|
-
: variantSnapshot && typeof
|
|
301
|
-
?
|
|
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(
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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(
|
|
323
|
-
|
|
463
|
+
errorMessage: t(
|
|
464
|
+
"sales.documents.items.errorDelete",
|
|
465
|
+
"Failed to delete line.",
|
|
466
|
+
),
|
|
467
|
+
});
|
|
324
468
|
if (result.ok) {
|
|
325
|
-
flash(t(
|
|
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(
|
|
331
|
-
const normalized = normalizeCrudServerError(err)
|
|
332
|
-
const fallback = t(
|
|
333
|
-
|
|
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
|
-
[
|
|
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
|
|
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 =
|
|
362
|
-
|
|
363
|
-
const
|
|
364
|
-
|
|
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 &&
|
|
367
|
-
|
|
368
|
-
|
|
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 &&
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
|
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(
|
|
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(
|
|
579
|
+
title={t("sales.documents.items.empty", "No items yet.")}
|
|
396
580
|
description={t(
|
|
397
|
-
|
|
398
|
-
|
|
581
|
+
"sales.documents.items.subtitle",
|
|
582
|
+
"Add products and configure pricing for this document.",
|
|
399
583
|
)}
|
|
400
584
|
action={{
|
|
401
|
-
label: t(
|
|
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">
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
<th className="px-3 py-2 font-medium">
|
|
414
|
-
|
|
415
|
-
|
|
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 =
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
const
|
|
426
|
-
const
|
|
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">
|
|
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">
|
|
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">
|
|
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">{
|
|
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(
|
|
460
|
-
shipped
|
|
461
|
-
total
|
|
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(
|
|
710
|
+
{formatMoney(
|
|
711
|
+
item.unitPriceGross,
|
|
712
|
+
item.currencyCode ?? currencyCode ?? undefined,
|
|
713
|
+
)}{" "}
|
|
471
714
|
<span className="text-xs text-muted-foreground">
|
|
472
|
-
{t(
|
|
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(
|
|
477
|
-
|
|
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(
|
|
747
|
+
{formatMoney(
|
|
748
|
+
item.totalGross,
|
|
749
|
+
item.currencyCode ?? currencyCode ?? undefined,
|
|
750
|
+
)}{" "}
|
|
485
751
|
<span className="text-xs font-normal text-muted-foreground">
|
|
486
|
-
{t(
|
|
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(
|
|
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
|
}
|