@open-mercato/core 0.4.5-develop-f4858e0ef3 → 0.4.5-develop-4849712ccb
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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/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
|
@@ -0,0 +1,745 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
5
|
+
import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
|
|
6
|
+
import { Button } from "@open-mercato/ui/primitives/button";
|
|
7
|
+
import { Checkbox } from "@open-mercato/ui/primitives/checkbox";
|
|
8
|
+
import { Input } from "@open-mercato/ui/primitives/input";
|
|
9
|
+
import { Label } from "@open-mercato/ui/primitives/label";
|
|
10
|
+
import { Spinner } from "@open-mercato/ui/primitives/spinner";
|
|
11
|
+
import { ArrowDown, ArrowUp, Plus, Trash2 } from "lucide-react";
|
|
12
|
+
import type {
|
|
13
|
+
ProductFormValues,
|
|
14
|
+
ProductUnitConversionDraft,
|
|
15
|
+
} from "./productForm";
|
|
16
|
+
import { createProductUnitConversionDraft } from "./productForm";
|
|
17
|
+
import { REFERENCE_UNIT_CODES } from "@open-mercato/shared/lib/units/unitCodes";
|
|
18
|
+
import { toTrimmedOrNull } from "./productFormUtils";
|
|
19
|
+
|
|
20
|
+
type UnitDictionaryEntry = {
|
|
21
|
+
id?: string;
|
|
22
|
+
value?: string;
|
|
23
|
+
label?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type UnitDictionaryResponse = {
|
|
27
|
+
entries?: UnitDictionaryEntry[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type UnitOption = {
|
|
31
|
+
value: string;
|
|
32
|
+
label: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type ProductUomSectionProps = {
|
|
36
|
+
values: ProductFormValues;
|
|
37
|
+
errors: Record<string, string>;
|
|
38
|
+
setValue: (id: string, value: unknown) => void;
|
|
39
|
+
embedded?: boolean;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const REFERENCE_UNIT_DISPLAY: Record<string, string> = {
|
|
43
|
+
kg: "1 kg", l: "1 l", m2: "1 m\u00B2", m3: "1 m\u00B3", pc: "1 pc",
|
|
44
|
+
};
|
|
45
|
+
const REFERENCE_UNIT_OPTIONS = REFERENCE_UNIT_CODES.map((code) => ({
|
|
46
|
+
value: code,
|
|
47
|
+
i18nKey: `catalog.products.unitPrice.options.${code}` as const,
|
|
48
|
+
fallback: REFERENCE_UNIT_DISPLAY[code] ?? code,
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
function normalizeDecimalInput(value: string): string {
|
|
52
|
+
return value.replace(/,/g, ".");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function toPositiveNumber(value: unknown): number | null {
|
|
56
|
+
if (typeof value === "number") {
|
|
57
|
+
return Number.isFinite(value) && value > 0 ? value : null;
|
|
58
|
+
}
|
|
59
|
+
if (typeof value !== "string") return null;
|
|
60
|
+
const normalized = toTrimmedOrNull(value);
|
|
61
|
+
if (!normalized) return null;
|
|
62
|
+
const numeric = Number(normalized.replace(",", "."));
|
|
63
|
+
return Number.isFinite(numeric) && numeric > 0 ? numeric : null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function toSortValue(value: string): number {
|
|
67
|
+
const numeric = Number(value);
|
|
68
|
+
return Number.isFinite(numeric) ? numeric : Number.MAX_SAFE_INTEGER;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function formatPreviewNumber(value: number): string {
|
|
72
|
+
if (!Number.isFinite(value)) return "0";
|
|
73
|
+
const rounded = Math.round(value * 1_000_000) / 1_000_000;
|
|
74
|
+
return Number.isInteger(rounded) ? String(rounded) : rounded.toString();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function normalizeConversions(value: unknown): ProductUnitConversionDraft[] {
|
|
78
|
+
if (!Array.isArray(value)) return [];
|
|
79
|
+
const normalized = value
|
|
80
|
+
.map((entry) => {
|
|
81
|
+
if (!entry || typeof entry !== "object") return null;
|
|
82
|
+
const row = entry as ProductUnitConversionDraft;
|
|
83
|
+
return {
|
|
84
|
+
id: toTrimmedOrNull(row.id) ?? null,
|
|
85
|
+
unitCode: toTrimmedOrNull(row.unitCode) ?? "",
|
|
86
|
+
toBaseFactor: toTrimmedOrNull(row.toBaseFactor)
|
|
87
|
+
? normalizeDecimalInput(toTrimmedOrNull(row.toBaseFactor) as string)
|
|
88
|
+
: "",
|
|
89
|
+
sortOrder: toTrimmedOrNull(row.sortOrder) ?? "",
|
|
90
|
+
isActive: row.isActive !== false,
|
|
91
|
+
} satisfies ProductUnitConversionDraft;
|
|
92
|
+
})
|
|
93
|
+
.filter((entry): entry is ProductUnitConversionDraft => Boolean(entry));
|
|
94
|
+
normalized.sort((left, right) => {
|
|
95
|
+
const leftOrder = toSortValue(left.sortOrder);
|
|
96
|
+
const rightOrder = toSortValue(right.sortOrder);
|
|
97
|
+
if (leftOrder === rightOrder) return left.unitCode.localeCompare(right.unitCode);
|
|
98
|
+
return leftOrder - rightOrder;
|
|
99
|
+
});
|
|
100
|
+
return normalized.map((entry, index) => ({
|
|
101
|
+
...entry,
|
|
102
|
+
sortOrder: String((index + 1) * 10),
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function buildUnitOptions(
|
|
107
|
+
entries: UnitDictionaryEntry[] | undefined,
|
|
108
|
+
): UnitOption[] {
|
|
109
|
+
const list = Array.isArray(entries) ? entries : [];
|
|
110
|
+
const options = list
|
|
111
|
+
.map((entry) => {
|
|
112
|
+
const value = toTrimmedOrNull(entry.value);
|
|
113
|
+
if (!value) return null;
|
|
114
|
+
return {
|
|
115
|
+
value,
|
|
116
|
+
label: toTrimmedOrNull(entry.label) ?? value,
|
|
117
|
+
} satisfies UnitOption;
|
|
118
|
+
})
|
|
119
|
+
.filter((entry): entry is UnitOption => Boolean(entry));
|
|
120
|
+
return options.sort((left, right) => left.label.localeCompare(right.label));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function ProductUomSection({
|
|
124
|
+
values,
|
|
125
|
+
errors,
|
|
126
|
+
setValue,
|
|
127
|
+
embedded = false,
|
|
128
|
+
}: ProductUomSectionProps) {
|
|
129
|
+
const t = useT();
|
|
130
|
+
const [unitOptions, setUnitOptions] = React.useState<UnitOption[]>([]);
|
|
131
|
+
const [loadingUnits, setLoadingUnits] = React.useState(false);
|
|
132
|
+
const [errorLoadingUnits, setErrorLoadingUnits] = React.useState(false);
|
|
133
|
+
const conversions = React.useMemo(
|
|
134
|
+
() => normalizeConversions(values.unitConversions),
|
|
135
|
+
[values.unitConversions],
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
React.useEffect(() => {
|
|
139
|
+
let cancelled = false;
|
|
140
|
+
async function loadUnits() {
|
|
141
|
+
setLoadingUnits(true);
|
|
142
|
+
setErrorLoadingUnits(false);
|
|
143
|
+
try {
|
|
144
|
+
const response = await apiCall<UnitDictionaryResponse>(
|
|
145
|
+
"/api/catalog/dictionaries/unit",
|
|
146
|
+
undefined,
|
|
147
|
+
{ fallback: { entries: [] } },
|
|
148
|
+
);
|
|
149
|
+
if (cancelled) return;
|
|
150
|
+
setUnitOptions(buildUnitOptions(response.result?.entries));
|
|
151
|
+
} catch {
|
|
152
|
+
if (!cancelled) {
|
|
153
|
+
setUnitOptions([]);
|
|
154
|
+
setErrorLoadingUnits(true);
|
|
155
|
+
}
|
|
156
|
+
} finally {
|
|
157
|
+
if (!cancelled) setLoadingUnits(false);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
void loadUnits();
|
|
161
|
+
return () => {
|
|
162
|
+
cancelled = true;
|
|
163
|
+
};
|
|
164
|
+
}, []);
|
|
165
|
+
|
|
166
|
+
const findUnitLabel = React.useCallback(
|
|
167
|
+
(value: string | null | undefined) => {
|
|
168
|
+
const code = toTrimmedOrNull(value);
|
|
169
|
+
if (!code) return null;
|
|
170
|
+
const option = unitOptions.find((entry) => entry.value === code);
|
|
171
|
+
return option?.label ?? code;
|
|
172
|
+
},
|
|
173
|
+
[unitOptions],
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const setConversions = React.useCallback(
|
|
177
|
+
(next: ProductUnitConversionDraft[]) => {
|
|
178
|
+
const normalized = next.map((entry, index) => ({
|
|
179
|
+
...entry,
|
|
180
|
+
sortOrder: String((index + 1) * 10),
|
|
181
|
+
}));
|
|
182
|
+
setValue("unitConversions", normalized);
|
|
183
|
+
},
|
|
184
|
+
[setValue],
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const addConversion = React.useCallback(() => {
|
|
188
|
+
const next = [
|
|
189
|
+
...conversions,
|
|
190
|
+
createProductUnitConversionDraft({
|
|
191
|
+
sortOrder: String((conversions.length + 1) * 10),
|
|
192
|
+
}),
|
|
193
|
+
];
|
|
194
|
+
setConversions(next);
|
|
195
|
+
}, [conversions, setConversions]);
|
|
196
|
+
|
|
197
|
+
const updateConversion = React.useCallback(
|
|
198
|
+
(index: number, patch: Partial<ProductUnitConversionDraft>) => {
|
|
199
|
+
const next = conversions.map((entry, rowIndex) =>
|
|
200
|
+
rowIndex === index ? { ...entry, ...patch } : entry,
|
|
201
|
+
);
|
|
202
|
+
setConversions(next);
|
|
203
|
+
},
|
|
204
|
+
[conversions, setConversions],
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const removeConversion = React.useCallback(
|
|
208
|
+
(index: number) => {
|
|
209
|
+
const next = conversions.filter((_entry, rowIndex) => rowIndex !== index);
|
|
210
|
+
setConversions(next);
|
|
211
|
+
},
|
|
212
|
+
[conversions, setConversions],
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const moveConversion = React.useCallback(
|
|
216
|
+
(index: number, direction: "up" | "down") => {
|
|
217
|
+
const targetIndex = direction === "up" ? index - 1 : index + 1;
|
|
218
|
+
if (targetIndex < 0 || targetIndex >= conversions.length) return;
|
|
219
|
+
const next = [...conversions];
|
|
220
|
+
const source = next[index];
|
|
221
|
+
next[index] = next[targetIndex];
|
|
222
|
+
next[targetIndex] = source;
|
|
223
|
+
setConversions(next);
|
|
224
|
+
},
|
|
225
|
+
[conversions, setConversions],
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const defaultUnit = toTrimmedOrNull(values.defaultUnit) ?? "";
|
|
229
|
+
const defaultSalesUnit = toTrimmedOrNull(values.defaultSalesUnit) ?? "";
|
|
230
|
+
const defaultSalesQuantityRaw =
|
|
231
|
+
toTrimmedOrNull(values.defaultSalesUnitQuantity) ?? "1";
|
|
232
|
+
const defaultSalesQuantity = normalizeDecimalInput(defaultSalesQuantityRaw);
|
|
233
|
+
const unitPriceEnabled = Boolean(values.unitPriceEnabled);
|
|
234
|
+
const unitPriceReferenceUnit =
|
|
235
|
+
toTrimmedOrNull(values.unitPriceReferenceUnit) ?? "";
|
|
236
|
+
const unitPriceBaseQuantityRaw =
|
|
237
|
+
toTrimmedOrNull(values.unitPriceBaseQuantity) ?? "";
|
|
238
|
+
const unitPriceBaseQuantity = normalizeDecimalInput(unitPriceBaseQuantityRaw);
|
|
239
|
+
|
|
240
|
+
const baseUnitLabel = findUnitLabel(defaultUnit) ?? defaultUnit;
|
|
241
|
+
const salesUnitLabel =
|
|
242
|
+
findUnitLabel(defaultSalesUnit || defaultUnit) ??
|
|
243
|
+
defaultSalesUnit ??
|
|
244
|
+
defaultUnit;
|
|
245
|
+
|
|
246
|
+
const defaultSalesFactor = React.useMemo(() => {
|
|
247
|
+
const defaultUnitKey = defaultUnit.toLowerCase();
|
|
248
|
+
const defaultSalesKey = (defaultSalesUnit || defaultUnit).toLowerCase();
|
|
249
|
+
if (!defaultUnitKey || !defaultSalesKey) return null;
|
|
250
|
+
if (defaultSalesKey === defaultUnitKey) return 1;
|
|
251
|
+
const row = conversions.find(
|
|
252
|
+
(entry) =>
|
|
253
|
+
entry.isActive &&
|
|
254
|
+
entry.unitCode.toLowerCase() === defaultSalesKey &&
|
|
255
|
+
toPositiveNumber(entry.toBaseFactor) !== null,
|
|
256
|
+
);
|
|
257
|
+
return row ? toPositiveNumber(row.toBaseFactor) : null;
|
|
258
|
+
}, [conversions, defaultSalesUnit, defaultUnit]);
|
|
259
|
+
|
|
260
|
+
const defaultSalesQuantityNumber = toPositiveNumber(defaultSalesQuantity);
|
|
261
|
+
const defaultSalesQuantityNormalized =
|
|
262
|
+
defaultSalesQuantityNumber && defaultSalesFactor
|
|
263
|
+
? defaultSalesQuantityNumber * defaultSalesFactor
|
|
264
|
+
: null;
|
|
265
|
+
const unitPriceBaseQuantityNumber = toPositiveNumber(unitPriceBaseQuantity);
|
|
266
|
+
|
|
267
|
+
const validConversions = conversions.filter(
|
|
268
|
+
(entry) =>
|
|
269
|
+
toTrimmedOrNull(entry.unitCode) && toTrimmedOrNull(entry.toBaseFactor),
|
|
270
|
+
);
|
|
271
|
+
const conversionPreviewItems = validConversions
|
|
272
|
+
.slice(0, 3)
|
|
273
|
+
.map((entry) => {
|
|
274
|
+
const label = findUnitLabel(entry.unitCode) ?? entry.unitCode;
|
|
275
|
+
const baseLabel = findUnitLabel(defaultUnit) ?? defaultUnit;
|
|
276
|
+
const factor = toTrimmedOrNull(entry.toBaseFactor) ?? "1";
|
|
277
|
+
return `1 ${label} = ${factor} ${baseLabel || t("catalog.products.uom.baseUnit", "base unit")}`;
|
|
278
|
+
});
|
|
279
|
+
const conversionPreview =
|
|
280
|
+
validConversions.length > 3
|
|
281
|
+
? `${conversionPreviewItems.join(" • ")} (+${validConversions.length - 3})`
|
|
282
|
+
: conversionPreviewItems.join(" • ");
|
|
283
|
+
|
|
284
|
+
return (
|
|
285
|
+
<div
|
|
286
|
+
className={
|
|
287
|
+
embedded ? "space-y-5" : "space-y-5 rounded-lg border bg-card p-4"
|
|
288
|
+
}
|
|
289
|
+
>
|
|
290
|
+
<div className="space-y-1">
|
|
291
|
+
<h3 className="text-sm font-semibold">
|
|
292
|
+
{t("catalog.products.uom.title", "Units of measure")}
|
|
293
|
+
</h3>
|
|
294
|
+
<p className="text-xs text-muted-foreground">
|
|
295
|
+
{t(
|
|
296
|
+
"catalog.products.uom.description",
|
|
297
|
+
"Set base unit, sales unit, and packaging conversions.",
|
|
298
|
+
)}
|
|
299
|
+
</p>
|
|
300
|
+
</div>
|
|
301
|
+
|
|
302
|
+
{loadingUnits ? (
|
|
303
|
+
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
304
|
+
<Spinner className="h-3 w-3" />
|
|
305
|
+
{t("catalog.products.uom.loadingUnits", "Loading units...")}
|
|
306
|
+
</div>
|
|
307
|
+
) : null}
|
|
308
|
+
|
|
309
|
+
{errorLoadingUnits ? (
|
|
310
|
+
<p className="text-xs text-destructive">
|
|
311
|
+
{t(
|
|
312
|
+
"catalog.products.uom.errors.loadUnits",
|
|
313
|
+
"Failed to load units. Please try refreshing the page.",
|
|
314
|
+
)}
|
|
315
|
+
</p>
|
|
316
|
+
) : null}
|
|
317
|
+
|
|
318
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
319
|
+
<div className="space-y-2">
|
|
320
|
+
<Label htmlFor="catalog-product-uom-base-unit">{t("catalog.products.uom.baseUnit", "Base unit")}</Label>
|
|
321
|
+
<select
|
|
322
|
+
id="catalog-product-uom-base-unit"
|
|
323
|
+
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
324
|
+
value={defaultUnit}
|
|
325
|
+
onChange={(event) =>
|
|
326
|
+
setValue("defaultUnit", event.target.value || null)
|
|
327
|
+
}
|
|
328
|
+
disabled={loadingUnits}
|
|
329
|
+
>
|
|
330
|
+
<option value="">
|
|
331
|
+
{t("catalog.products.uom.selectUnit", "Select unit")}
|
|
332
|
+
</option>
|
|
333
|
+
{unitOptions.map((option) => (
|
|
334
|
+
<option key={option.value} value={option.value}>
|
|
335
|
+
{option.label}
|
|
336
|
+
</option>
|
|
337
|
+
))}
|
|
338
|
+
</select>
|
|
339
|
+
{errors.defaultUnit ? (
|
|
340
|
+
<p className="text-xs text-destructive">{errors.defaultUnit}</p>
|
|
341
|
+
) : null}
|
|
342
|
+
</div>
|
|
343
|
+
|
|
344
|
+
<div className="space-y-2">
|
|
345
|
+
<Label htmlFor="catalog-product-uom-sales-unit">
|
|
346
|
+
{t("catalog.products.uom.defaultSalesUnit", "Default sales unit")}
|
|
347
|
+
</Label>
|
|
348
|
+
<select
|
|
349
|
+
id="catalog-product-uom-sales-unit"
|
|
350
|
+
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
351
|
+
value={defaultSalesUnit}
|
|
352
|
+
onChange={(event) =>
|
|
353
|
+
setValue("defaultSalesUnit", event.target.value || null)
|
|
354
|
+
}
|
|
355
|
+
disabled={loadingUnits}
|
|
356
|
+
>
|
|
357
|
+
<option value="">
|
|
358
|
+
{t("catalog.products.uom.selectUnit", "Select unit")}
|
|
359
|
+
</option>
|
|
360
|
+
{unitOptions.map((option) => (
|
|
361
|
+
<option key={option.value} value={option.value}>
|
|
362
|
+
{option.label}
|
|
363
|
+
</option>
|
|
364
|
+
))}
|
|
365
|
+
</select>
|
|
366
|
+
{errors.defaultSalesUnit ? (
|
|
367
|
+
<p className="text-xs text-destructive">{errors.defaultSalesUnit}</p>
|
|
368
|
+
) : null}
|
|
369
|
+
</div>
|
|
370
|
+
|
|
371
|
+
<div className="space-y-2 md:col-span-2">
|
|
372
|
+
<Label htmlFor="catalog-product-uom-default-sales-quantity">
|
|
373
|
+
{t(
|
|
374
|
+
"catalog.products.uom.defaultSalesQuantityLabel",
|
|
375
|
+
"Default line quantity (in sales unit)",
|
|
376
|
+
)}
|
|
377
|
+
</Label>
|
|
378
|
+
<Input
|
|
379
|
+
id="catalog-product-uom-default-sales-quantity"
|
|
380
|
+
type="text"
|
|
381
|
+
inputMode="decimal"
|
|
382
|
+
value={defaultSalesQuantity}
|
|
383
|
+
onChange={(event) =>
|
|
384
|
+
setValue(
|
|
385
|
+
"defaultSalesUnitQuantity",
|
|
386
|
+
normalizeDecimalInput(event.target.value),
|
|
387
|
+
)
|
|
388
|
+
}
|
|
389
|
+
placeholder="1"
|
|
390
|
+
/>
|
|
391
|
+
<p className="text-xs text-muted-foreground">
|
|
392
|
+
{t(
|
|
393
|
+
"catalog.products.uom.defaultSalesQuantityHint",
|
|
394
|
+
"Used to prefill quantity in quote/order lines. Value is interpreted in Default sales unit.",
|
|
395
|
+
)}
|
|
396
|
+
</p>
|
|
397
|
+
{defaultSalesQuantityNumber && salesUnitLabel ? (
|
|
398
|
+
<p className="text-xs text-muted-foreground">
|
|
399
|
+
{defaultSalesQuantityNormalized && baseUnitLabel
|
|
400
|
+
? t(
|
|
401
|
+
"catalog.products.uom.defaultSalesQuantityPreviewWithNormalization",
|
|
402
|
+
"Default line: {{quantity}} {{salesUnit}} (= {{normalized}} {{baseUnit}}).",
|
|
403
|
+
{
|
|
404
|
+
quantity: formatPreviewNumber(defaultSalesQuantityNumber),
|
|
405
|
+
salesUnit: salesUnitLabel,
|
|
406
|
+
normalized: formatPreviewNumber(
|
|
407
|
+
defaultSalesQuantityNormalized,
|
|
408
|
+
),
|
|
409
|
+
baseUnit: baseUnitLabel,
|
|
410
|
+
},
|
|
411
|
+
)
|
|
412
|
+
: t(
|
|
413
|
+
"catalog.products.uom.defaultSalesQuantityPreview",
|
|
414
|
+
"Default line: {{quantity}} {{salesUnit}}.",
|
|
415
|
+
{
|
|
416
|
+
quantity: formatPreviewNumber(defaultSalesQuantityNumber),
|
|
417
|
+
salesUnit: salesUnitLabel,
|
|
418
|
+
},
|
|
419
|
+
)}
|
|
420
|
+
</p>
|
|
421
|
+
) : null}
|
|
422
|
+
{errors.defaultSalesUnitQuantity ? (
|
|
423
|
+
<p className="text-xs text-destructive">
|
|
424
|
+
{errors.defaultSalesUnitQuantity}
|
|
425
|
+
</p>
|
|
426
|
+
) : null}
|
|
427
|
+
</div>
|
|
428
|
+
</div>
|
|
429
|
+
|
|
430
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
431
|
+
<div className="space-y-2">
|
|
432
|
+
<Label htmlFor="catalog-product-uom-rounding-mode">
|
|
433
|
+
{t("catalog.products.uom.roundingMode", "Rounding mode")}
|
|
434
|
+
</Label>
|
|
435
|
+
<select
|
|
436
|
+
id="catalog-product-uom-rounding-mode"
|
|
437
|
+
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
438
|
+
value={values.uomRoundingMode ?? "half_up"}
|
|
439
|
+
onChange={(event) =>
|
|
440
|
+
setValue("uomRoundingMode", event.target.value)
|
|
441
|
+
}
|
|
442
|
+
>
|
|
443
|
+
<option value="half_up">
|
|
444
|
+
{t("catalog.products.uom.roundingModeHalfUp", "Half up (default)")}
|
|
445
|
+
</option>
|
|
446
|
+
<option value="down">
|
|
447
|
+
{t("catalog.products.uom.roundingModeDown", "Round down")}
|
|
448
|
+
</option>
|
|
449
|
+
<option value="up">
|
|
450
|
+
{t("catalog.products.uom.roundingModeUp", "Round up")}
|
|
451
|
+
</option>
|
|
452
|
+
</select>
|
|
453
|
+
</div>
|
|
454
|
+
<div className="space-y-2">
|
|
455
|
+
<Label htmlFor="catalog-product-uom-rounding-scale">
|
|
456
|
+
{t("catalog.products.uom.roundingScale", "Rounding scale (decimal places)")}
|
|
457
|
+
</Label>
|
|
458
|
+
<Input
|
|
459
|
+
id="catalog-product-uom-rounding-scale"
|
|
460
|
+
type="number"
|
|
461
|
+
min={0}
|
|
462
|
+
max={6}
|
|
463
|
+
value={values.uomRoundingScale ?? 4}
|
|
464
|
+
onChange={(event) => {
|
|
465
|
+
const parsed = Number.parseInt(event.target.value, 10);
|
|
466
|
+
setValue("uomRoundingScale", Number.isFinite(parsed) ? Math.max(0, Math.min(6, parsed)) : 4);
|
|
467
|
+
}}
|
|
468
|
+
/>
|
|
469
|
+
</div>
|
|
470
|
+
</div>
|
|
471
|
+
|
|
472
|
+
<div className="space-y-3">
|
|
473
|
+
<div className="flex items-center gap-2">
|
|
474
|
+
<Checkbox
|
|
475
|
+
id="catalog-product-unit-price-enabled"
|
|
476
|
+
checked={unitPriceEnabled}
|
|
477
|
+
onCheckedChange={(checked) =>
|
|
478
|
+
setValue("unitPriceEnabled", checked === true)
|
|
479
|
+
}
|
|
480
|
+
/>
|
|
481
|
+
<Label
|
|
482
|
+
htmlFor="catalog-product-unit-price-enabled"
|
|
483
|
+
className="text-sm"
|
|
484
|
+
>
|
|
485
|
+
{t(
|
|
486
|
+
"catalog.products.unitPrice.enable",
|
|
487
|
+
"Enable EU unit price display",
|
|
488
|
+
)}
|
|
489
|
+
</Label>
|
|
490
|
+
</div>
|
|
491
|
+
|
|
492
|
+
{unitPriceEnabled ? (
|
|
493
|
+
<div className="grid gap-3 md:grid-cols-2">
|
|
494
|
+
<div className="space-y-2">
|
|
495
|
+
<Label htmlFor="catalog-product-uom-reference-unit">
|
|
496
|
+
{t(
|
|
497
|
+
"catalog.products.unitPrice.referenceUnit",
|
|
498
|
+
"Reference unit",
|
|
499
|
+
)}
|
|
500
|
+
</Label>
|
|
501
|
+
<select
|
|
502
|
+
id="catalog-product-uom-reference-unit"
|
|
503
|
+
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
504
|
+
value={unitPriceReferenceUnit}
|
|
505
|
+
onChange={(event) =>
|
|
506
|
+
setValue("unitPriceReferenceUnit", event.target.value || null)
|
|
507
|
+
}
|
|
508
|
+
>
|
|
509
|
+
<option value="">
|
|
510
|
+
{t(
|
|
511
|
+
"catalog.products.unitPrice.selectReferenceUnit",
|
|
512
|
+
"Select reference unit",
|
|
513
|
+
)}
|
|
514
|
+
</option>
|
|
515
|
+
{REFERENCE_UNIT_OPTIONS.map((option) => (
|
|
516
|
+
<option key={option.value} value={option.value}>
|
|
517
|
+
{t(option.i18nKey, option.fallback)}
|
|
518
|
+
</option>
|
|
519
|
+
))}
|
|
520
|
+
</select>
|
|
521
|
+
</div>
|
|
522
|
+
<div className="space-y-2">
|
|
523
|
+
<Label htmlFor="catalog-product-uom-unit-price-base-quantity">
|
|
524
|
+
{t(
|
|
525
|
+
"catalog.products.unitPrice.baseQuantity",
|
|
526
|
+
"Reference quantity (in base unit)",
|
|
527
|
+
)}
|
|
528
|
+
</Label>
|
|
529
|
+
<Input
|
|
530
|
+
id="catalog-product-uom-unit-price-base-quantity"
|
|
531
|
+
type="text"
|
|
532
|
+
inputMode="decimal"
|
|
533
|
+
value={unitPriceBaseQuantity}
|
|
534
|
+
onChange={(event) =>
|
|
535
|
+
setValue(
|
|
536
|
+
"unitPriceBaseQuantity",
|
|
537
|
+
normalizeDecimalInput(event.target.value),
|
|
538
|
+
)
|
|
539
|
+
}
|
|
540
|
+
placeholder="1"
|
|
541
|
+
/>
|
|
542
|
+
</div>
|
|
543
|
+
</div>
|
|
544
|
+
) : null}
|
|
545
|
+
{unitPriceEnabled ? (
|
|
546
|
+
<p className="text-xs text-muted-foreground">
|
|
547
|
+
{unitPriceReferenceUnit && unitPriceBaseQuantityNumber
|
|
548
|
+
? t(
|
|
549
|
+
"catalog.products.unitPrice.hintWithPreview",
|
|
550
|
+
"Show calculated price per {{quantity}} {{unit}}. For most products use 1 (for example: 1 kg, 1 l, 1 m²).",
|
|
551
|
+
{
|
|
552
|
+
quantity: formatPreviewNumber(unitPriceBaseQuantityNumber),
|
|
553
|
+
unit: unitPriceReferenceUnit,
|
|
554
|
+
},
|
|
555
|
+
)
|
|
556
|
+
: t(
|
|
557
|
+
"catalog.products.unitPrice.hint",
|
|
558
|
+
"Show calculated price per selected reference unit. In most cases set quantity to 1.",
|
|
559
|
+
)}
|
|
560
|
+
</p>
|
|
561
|
+
) : null}
|
|
562
|
+
</div>
|
|
563
|
+
|
|
564
|
+
<div className="space-y-3">
|
|
565
|
+
<div className="flex items-center justify-between gap-2">
|
|
566
|
+
<Label className="text-sm">
|
|
567
|
+
{t("catalog.products.uom.conversions", "Product conversions")}
|
|
568
|
+
</Label>
|
|
569
|
+
<Button
|
|
570
|
+
type="button"
|
|
571
|
+
variant="outline"
|
|
572
|
+
size="sm"
|
|
573
|
+
onClick={addConversion}
|
|
574
|
+
>
|
|
575
|
+
<Plus className="mr-1.5 h-4 w-4" />
|
|
576
|
+
{t("catalog.products.uom.addConversion", "Add conversion")}
|
|
577
|
+
</Button>
|
|
578
|
+
</div>
|
|
579
|
+
|
|
580
|
+
{conversions.length === 0 ? (
|
|
581
|
+
<p className="text-xs text-muted-foreground">
|
|
582
|
+
{t(
|
|
583
|
+
"catalog.products.uom.emptyConversions",
|
|
584
|
+
"No conversions configured yet.",
|
|
585
|
+
)}
|
|
586
|
+
</p>
|
|
587
|
+
) : (
|
|
588
|
+
<div className="space-y-2">
|
|
589
|
+
{conversions.map((entry, index) => {
|
|
590
|
+
const conversionFactor = toPositiveNumber(entry.toBaseFactor);
|
|
591
|
+
const conversionPreviewText =
|
|
592
|
+
entry.unitCode && conversionFactor !== null
|
|
593
|
+
? t(
|
|
594
|
+
"catalog.products.uom.conversionPreview",
|
|
595
|
+
"1 {{fromUnit}} = {{factor}} {{baseUnit}}",
|
|
596
|
+
{
|
|
597
|
+
fromUnit: findUnitLabel(entry.unitCode) ?? entry.unitCode,
|
|
598
|
+
factor: formatPreviewNumber(conversionFactor),
|
|
599
|
+
baseUnit:
|
|
600
|
+
findUnitLabel(defaultUnit) ??
|
|
601
|
+
defaultUnit ??
|
|
602
|
+
t("catalog.products.uom.baseUnit", "base unit"),
|
|
603
|
+
},
|
|
604
|
+
)
|
|
605
|
+
: null;
|
|
606
|
+
|
|
607
|
+
return (
|
|
608
|
+
<div
|
|
609
|
+
key={entry.id ?? `uom-conversion-${index}`}
|
|
610
|
+
className="grid gap-3 rounded-md border p-3 md:grid-cols-12"
|
|
611
|
+
>
|
|
612
|
+
<div className="space-y-1 md:col-span-4">
|
|
613
|
+
<Label htmlFor={`catalog-product-uom-conversion-unit-${index}`} className="text-xs text-muted-foreground">
|
|
614
|
+
{t("catalog.products.uom.conversionUnit", "Sales unit")}
|
|
615
|
+
</Label>
|
|
616
|
+
<select
|
|
617
|
+
id={`catalog-product-uom-conversion-unit-${index}`}
|
|
618
|
+
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
619
|
+
value={entry.unitCode}
|
|
620
|
+
onChange={(event) =>
|
|
621
|
+
updateConversion(index, { unitCode: event.target.value })
|
|
622
|
+
}
|
|
623
|
+
disabled={loadingUnits}
|
|
624
|
+
>
|
|
625
|
+
<option value="">
|
|
626
|
+
{t("catalog.products.uom.selectUnit", "Select unit")}
|
|
627
|
+
</option>
|
|
628
|
+
{unitOptions.map((option) => (
|
|
629
|
+
<option key={option.value} value={option.value}>
|
|
630
|
+
{option.label}
|
|
631
|
+
</option>
|
|
632
|
+
))}
|
|
633
|
+
</select>
|
|
634
|
+
</div>
|
|
635
|
+
|
|
636
|
+
<div className="space-y-1 md:col-span-3">
|
|
637
|
+
<Label htmlFor={`catalog-product-uom-conversion-factor-${index}`} className="text-xs text-muted-foreground">
|
|
638
|
+
{t(
|
|
639
|
+
"catalog.products.uom.toBaseFactor",
|
|
640
|
+
"Base units per 1 sales unit",
|
|
641
|
+
)}
|
|
642
|
+
</Label>
|
|
643
|
+
<Input
|
|
644
|
+
id={`catalog-product-uom-conversion-factor-${index}`}
|
|
645
|
+
type="text"
|
|
646
|
+
inputMode="decimal"
|
|
647
|
+
value={entry.toBaseFactor}
|
|
648
|
+
onChange={(event) =>
|
|
649
|
+
updateConversion(index, {
|
|
650
|
+
toBaseFactor: normalizeDecimalInput(event.target.value),
|
|
651
|
+
})
|
|
652
|
+
}
|
|
653
|
+
placeholder="1"
|
|
654
|
+
/>
|
|
655
|
+
</div>
|
|
656
|
+
|
|
657
|
+
<div className="flex items-end gap-2 md:col-span-3">
|
|
658
|
+
<div className="inline-flex h-9 rounded-md border">
|
|
659
|
+
<Button
|
|
660
|
+
type="button"
|
|
661
|
+
variant="ghost"
|
|
662
|
+
size="icon"
|
|
663
|
+
className="h-9 w-9 rounded-none border-r"
|
|
664
|
+
onClick={() => moveConversion(index, "up")}
|
|
665
|
+
disabled={index === 0}
|
|
666
|
+
aria-label={t(
|
|
667
|
+
"catalog.products.uom.moveUp",
|
|
668
|
+
"Move conversion up",
|
|
669
|
+
)}
|
|
670
|
+
>
|
|
671
|
+
<ArrowUp className="h-4 w-4" />
|
|
672
|
+
</Button>
|
|
673
|
+
<Button
|
|
674
|
+
type="button"
|
|
675
|
+
variant="ghost"
|
|
676
|
+
size="icon"
|
|
677
|
+
className="h-9 w-9 rounded-none"
|
|
678
|
+
onClick={() => moveConversion(index, "down")}
|
|
679
|
+
disabled={index === conversions.length - 1}
|
|
680
|
+
aria-label={t(
|
|
681
|
+
"catalog.products.uom.moveDown",
|
|
682
|
+
"Move conversion down",
|
|
683
|
+
)}
|
|
684
|
+
>
|
|
685
|
+
<ArrowDown className="h-4 w-4" />
|
|
686
|
+
</Button>
|
|
687
|
+
</div>
|
|
688
|
+
<label
|
|
689
|
+
htmlFor={`catalog-product-uom-conversion-active-${index}`}
|
|
690
|
+
className="flex items-center gap-2 px-1 text-xs text-muted-foreground"
|
|
691
|
+
>
|
|
692
|
+
<Checkbox
|
|
693
|
+
id={`catalog-product-uom-conversion-active-${index}`}
|
|
694
|
+
checked={entry.isActive}
|
|
695
|
+
onCheckedChange={(checked) =>
|
|
696
|
+
updateConversion(index, { isActive: checked === true })
|
|
697
|
+
}
|
|
698
|
+
/>
|
|
699
|
+
{t("catalog.products.uom.active", "Active")}
|
|
700
|
+
</label>
|
|
701
|
+
</div>
|
|
702
|
+
|
|
703
|
+
<div className="flex items-end justify-end md:col-span-2">
|
|
704
|
+
<Button
|
|
705
|
+
type="button"
|
|
706
|
+
variant="ghost"
|
|
707
|
+
size="icon"
|
|
708
|
+
className="text-destructive"
|
|
709
|
+
onClick={() => removeConversion(index)}
|
|
710
|
+
aria-label={t(
|
|
711
|
+
"catalog.products.uom.removeConversion",
|
|
712
|
+
"Remove conversion",
|
|
713
|
+
)}
|
|
714
|
+
>
|
|
715
|
+
<Trash2 className="h-4 w-4" />
|
|
716
|
+
</Button>
|
|
717
|
+
</div>
|
|
718
|
+
|
|
719
|
+
{conversionPreviewText && (
|
|
720
|
+
<p className="text-xs text-muted-foreground md:col-span-12">
|
|
721
|
+
{conversionPreviewText}
|
|
722
|
+
</p>
|
|
723
|
+
)}
|
|
724
|
+
</div>
|
|
725
|
+
);
|
|
726
|
+
})}
|
|
727
|
+
</div>
|
|
728
|
+
)}
|
|
729
|
+
|
|
730
|
+
{conversions.length > 1 ? (
|
|
731
|
+
<p className="text-xs text-muted-foreground">
|
|
732
|
+
{t(
|
|
733
|
+
"catalog.products.uom.conversionOrderHint",
|
|
734
|
+
"Use arrows to reorder conversion priority.",
|
|
735
|
+
)}
|
|
736
|
+
</p>
|
|
737
|
+
) : null}
|
|
738
|
+
|
|
739
|
+
{conversionPreview ? (
|
|
740
|
+
<p className="text-xs text-muted-foreground">{conversionPreview}</p>
|
|
741
|
+
) : null}
|
|
742
|
+
</div>
|
|
743
|
+
</div>
|
|
744
|
+
);
|
|
745
|
+
}
|