@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
|
@@ -5,8 +5,14 @@ import Link from "next/link";
|
|
|
5
5
|
import dynamic from "next/dynamic";
|
|
6
6
|
import { useRouter } from "next/navigation";
|
|
7
7
|
import { Page, PageBody } from "@open-mercato/ui/backend/Page";
|
|
8
|
-
import {
|
|
9
|
-
|
|
8
|
+
import {
|
|
9
|
+
CrudForm
|
|
10
|
+
} from "@open-mercato/ui/backend/CrudForm";
|
|
11
|
+
import {
|
|
12
|
+
updateCrud,
|
|
13
|
+
createCrud,
|
|
14
|
+
deleteCrud
|
|
15
|
+
} from "@open-mercato/ui/backend/utils/crud";
|
|
10
16
|
import { createCrudFormError } from "@open-mercato/ui/backend/utils/serverErrors";
|
|
11
17
|
import { collectCustomFieldValues } from "@open-mercato/ui/backend/utils/customFieldValues";
|
|
12
18
|
import { flash } from "@open-mercato/ui/backend/FlashMessages";
|
|
@@ -16,11 +22,17 @@ import { Label } from "@open-mercato/ui/primitives/label";
|
|
|
16
22
|
import { TagsInput } from "@open-mercato/ui/backend/inputs/TagsInput";
|
|
17
23
|
import { Textarea } from "@open-mercato/ui/primitives/textarea";
|
|
18
24
|
import { DataLoader } from "@open-mercato/ui/primitives/DataLoader";
|
|
19
|
-
import {
|
|
25
|
+
import { Spinner } from "@open-mercato/ui/primitives/spinner";
|
|
26
|
+
import {
|
|
27
|
+
apiCall,
|
|
28
|
+
readApiResultOrThrow
|
|
29
|
+
} from "@open-mercato/ui/backend/utils/apiCall";
|
|
20
30
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
21
31
|
import { useConfirmDialog } from "@open-mercato/ui/backend/confirm-dialog";
|
|
22
32
|
import { E } from "../../../../../../generated/entities.ids.generated.js";
|
|
23
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
ProductMediaManager
|
|
35
|
+
} from "@open-mercato/core/modules/catalog/components/products/ProductMediaManager";
|
|
24
36
|
import {
|
|
25
37
|
fetchOptionSchemaTemplate
|
|
26
38
|
} from "../optionSchemaClient.js";
|
|
@@ -43,17 +55,105 @@ import {
|
|
|
43
55
|
updateWeightValue
|
|
44
56
|
} from "@open-mercato/core/modules/catalog/components/products/productForm";
|
|
45
57
|
import { MetadataEditor } from "@open-mercato/core/modules/catalog/components/products/MetadataEditor";
|
|
46
|
-
import {
|
|
58
|
+
import {
|
|
59
|
+
buildAttachmentImageUrl,
|
|
60
|
+
slugifyAttachmentFileName
|
|
61
|
+
} from "@open-mercato/core/modules/attachments/lib/imageUrls";
|
|
47
62
|
import {
|
|
48
63
|
ProductCategorizeSection
|
|
49
64
|
} from "@open-mercato/core/modules/catalog/components/products/ProductCategorizeSection";
|
|
50
|
-
import {
|
|
51
|
-
import {
|
|
65
|
+
import { ProductUomSection } from "@open-mercato/core/modules/catalog/components/products/ProductUomSection";
|
|
66
|
+
import { canonicalizeUnitCode } from "@open-mercato/core/modules/catalog/lib/unitCodes";
|
|
67
|
+
import {
|
|
68
|
+
UNIT_PRICE_REFERENCE_UNITS,
|
|
69
|
+
toTrimmedOrNull,
|
|
70
|
+
toPositiveNumberOrNull,
|
|
71
|
+
toIntegerInRangeOrDefault,
|
|
72
|
+
normalizeProductConversionInputs
|
|
73
|
+
} from "@open-mercato/core/modules/catalog/components/products/productFormUtils";
|
|
74
|
+
import {
|
|
75
|
+
AlignLeft,
|
|
76
|
+
BookMarked,
|
|
77
|
+
FileText,
|
|
78
|
+
Layers,
|
|
79
|
+
Plus,
|
|
80
|
+
Save,
|
|
81
|
+
Trash2
|
|
82
|
+
} from "lucide-react";
|
|
83
|
+
import {
|
|
84
|
+
Dialog,
|
|
85
|
+
DialogContent,
|
|
86
|
+
DialogFooter,
|
|
87
|
+
DialogHeader,
|
|
88
|
+
DialogTitle
|
|
89
|
+
} from "@open-mercato/ui/primitives/dialog";
|
|
52
90
|
const MarkdownEditor = dynamic(() => import("@uiw/react-md-editor"), {
|
|
53
91
|
ssr: false,
|
|
54
|
-
loading: () => /* @__PURE__ */ jsx("div", { className: "flex h-48 items-center justify-center text-sm text-muted-foreground", children:
|
|
92
|
+
loading: () => /* @__PURE__ */ jsx("div", { className: "flex h-48 items-center justify-center text-sm text-muted-foreground", children: /* @__PURE__ */ jsx(Spinner, {}) })
|
|
55
93
|
});
|
|
56
|
-
function
|
|
94
|
+
function mapVariantPriceSummary(item) {
|
|
95
|
+
if (!item) return null;
|
|
96
|
+
const getString2 = (value) => {
|
|
97
|
+
if (typeof value === "string" && value.trim().length) return value.trim();
|
|
98
|
+
if (typeof value === "number" || typeof value === "bigint")
|
|
99
|
+
return String(value);
|
|
100
|
+
return null;
|
|
101
|
+
};
|
|
102
|
+
const variantId = getString2(item.variant_id ?? item.variantId);
|
|
103
|
+
const id = getString2(item.id);
|
|
104
|
+
if (!variantId || !id) return null;
|
|
105
|
+
const priceKindId = getString2(item.price_kind_id ?? item.priceKindId);
|
|
106
|
+
const currencyCode = getString2(item.currency_code ?? item.currencyCode);
|
|
107
|
+
const unitGross = getString2(item.unit_price_gross ?? item.unitPriceGross);
|
|
108
|
+
const unitNet = getString2(item.unit_price_net ?? item.unitPriceNet);
|
|
109
|
+
const amount = unitGross ?? unitNet ?? null;
|
|
110
|
+
return {
|
|
111
|
+
id,
|
|
112
|
+
variantId,
|
|
113
|
+
priceKindId,
|
|
114
|
+
currencyCode,
|
|
115
|
+
amount,
|
|
116
|
+
displayMode: unitGross ? "including-tax" : "excluding-tax"
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function normalizeVariantOptionValues(input) {
|
|
120
|
+
if (!input || typeof input !== "object") return null;
|
|
121
|
+
const result = {};
|
|
122
|
+
Object.entries(input).forEach(([key, value]) => {
|
|
123
|
+
if (typeof key === "string" && typeof value === "string" && key.trim().length) {
|
|
124
|
+
result[key] = value;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
return Object.keys(result).length ? result : null;
|
|
128
|
+
}
|
|
129
|
+
function readProductConversionRows(items) {
|
|
130
|
+
const rows = Array.isArray(items) ? items : [];
|
|
131
|
+
return rows.map((item) => {
|
|
132
|
+
const id = toTrimmedOrNull(item.id);
|
|
133
|
+
const unitCode = canonicalizeUnitCode(item.unit_code ?? item.unitCode);
|
|
134
|
+
const factor = toPositiveNumberOrNull(
|
|
135
|
+
item.to_base_factor ?? item.toBaseFactor
|
|
136
|
+
);
|
|
137
|
+
if (!unitCode || factor === null) return null;
|
|
138
|
+
const sortOrderRaw = toIntegerInRangeOrDefault(
|
|
139
|
+
item.sort_order ?? item.sortOrder,
|
|
140
|
+
0,
|
|
141
|
+
1e5,
|
|
142
|
+
0
|
|
143
|
+
);
|
|
144
|
+
const isActive = typeof item.is_active === "boolean" ? item.is_active : typeof item.isActive === "boolean" ? item.isActive : true;
|
|
145
|
+
return {
|
|
146
|
+
id: id ?? null,
|
|
147
|
+
unitCode,
|
|
148
|
+
toBaseFactor: String(factor),
|
|
149
|
+
sortOrder: String(sortOrderRaw),
|
|
150
|
+
isActive
|
|
151
|
+
};
|
|
152
|
+
}).filter((entry) => Boolean(entry));
|
|
153
|
+
}
|
|
154
|
+
function EditCatalogProductPage({
|
|
155
|
+
params
|
|
156
|
+
}) {
|
|
57
157
|
const productId = params?.id ? String(params.id) : null;
|
|
58
158
|
const t = useT();
|
|
59
159
|
const router = useRouter();
|
|
@@ -64,22 +164,108 @@ function EditCatalogProductPage({ params }) {
|
|
|
64
164
|
const [loading, setLoading] = React.useState(true);
|
|
65
165
|
const [error, setError] = React.useState(null);
|
|
66
166
|
const offerSnapshotsRef = React.useRef([]);
|
|
167
|
+
const initialConversionsRef = React.useRef([]);
|
|
67
168
|
const [categorizeOptions, setCategorizeOptions] = React.useState({ categories: [], channels: [], tags: [] });
|
|
169
|
+
const loadVariants = React.useCallback(async (id) => {
|
|
170
|
+
try {
|
|
171
|
+
const [variantsRes, pricesRes] = await Promise.all([
|
|
172
|
+
apiCall(
|
|
173
|
+
`/api/catalog/variants?productId=${encodeURIComponent(id)}&page=1&pageSize=100`
|
|
174
|
+
),
|
|
175
|
+
apiCall(
|
|
176
|
+
`/api/catalog/prices?productId=${encodeURIComponent(id)}&page=1&pageSize=100`
|
|
177
|
+
)
|
|
178
|
+
]);
|
|
179
|
+
if (!variantsRes.ok) {
|
|
180
|
+
setVariants([]);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const priceMap = {};
|
|
184
|
+
if (pricesRes.ok) {
|
|
185
|
+
const priceItems = Array.isArray(pricesRes.result?.items) ? pricesRes.result?.items : [];
|
|
186
|
+
for (const item of priceItems) {
|
|
187
|
+
const summary = mapVariantPriceSummary(item);
|
|
188
|
+
if (!summary) continue;
|
|
189
|
+
if (!priceMap[summary.variantId]) {
|
|
190
|
+
priceMap[summary.variantId] = [];
|
|
191
|
+
}
|
|
192
|
+
priceMap[summary.variantId].push(summary);
|
|
193
|
+
}
|
|
194
|
+
Object.keys(priceMap).forEach((key) => {
|
|
195
|
+
priceMap[key].sort((left, right) => {
|
|
196
|
+
const leftKey = (left.priceKindId ?? "") + (left.currencyCode ?? "");
|
|
197
|
+
const rightKey = (right.priceKindId ?? "") + (right.currencyCode ?? "");
|
|
198
|
+
return leftKey.localeCompare(rightKey);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
const items = Array.isArray(variantsRes.result?.items) ? variantsRes.result?.items : [];
|
|
203
|
+
setVariants(
|
|
204
|
+
items.map((variant) => {
|
|
205
|
+
const variantId = typeof variant.id === "string" ? variant.id : null;
|
|
206
|
+
if (!variantId) return null;
|
|
207
|
+
const variantRecord = variant;
|
|
208
|
+
const optionValues = normalizeVariantOptionValues(variantRecord?.["option_values"]) ?? normalizeVariantOptionValues(variantRecord?.optionValues);
|
|
209
|
+
return {
|
|
210
|
+
id: variantId,
|
|
211
|
+
name: typeof variant.name === "string" && variant.name.trim().length ? variant.name : variant.sku ?? variantId,
|
|
212
|
+
sku: typeof variant.sku === "string" ? variant.sku : "",
|
|
213
|
+
isDefault: Boolean(variant.is_default ?? variant.isDefault),
|
|
214
|
+
prices: priceMap[variantId] ?? [],
|
|
215
|
+
optionValues
|
|
216
|
+
};
|
|
217
|
+
}).filter((entry) => Boolean(entry))
|
|
218
|
+
);
|
|
219
|
+
} catch (err) {
|
|
220
|
+
console.error("catalog.variants.fetch failed", err);
|
|
221
|
+
setVariants([]);
|
|
222
|
+
}
|
|
223
|
+
}, []);
|
|
224
|
+
const refreshVariants = React.useCallback(async () => {
|
|
225
|
+
if (!productId) return;
|
|
226
|
+
await loadVariants(productId);
|
|
227
|
+
}, [loadVariants, productId]);
|
|
228
|
+
const fetchAttachments = React.useCallback(
|
|
229
|
+
async (id) => {
|
|
230
|
+
try {
|
|
231
|
+
const res = await apiCall(
|
|
232
|
+
`/api/attachments?entityId=${encodeURIComponent(E.catalog.catalog_product)}&recordId=${encodeURIComponent(id)}`
|
|
233
|
+
);
|
|
234
|
+
if (!res.ok) return [];
|
|
235
|
+
return (res.result?.items ?? []).map((item) => ({
|
|
236
|
+
id: item.id,
|
|
237
|
+
url: item.url,
|
|
238
|
+
fileName: item.fileName,
|
|
239
|
+
fileSize: item.fileSize,
|
|
240
|
+
thumbnailUrl: item.thumbnailUrl ?? void 0
|
|
241
|
+
}));
|
|
242
|
+
} catch (err) {
|
|
243
|
+
console.error("attachments.fetch failed", err);
|
|
244
|
+
return [];
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
[]
|
|
248
|
+
);
|
|
68
249
|
React.useEffect(() => {
|
|
69
250
|
const loadTaxRates = async () => {
|
|
70
251
|
try {
|
|
71
|
-
const payload = await readApiResultOrThrow(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
252
|
+
const payload = await readApiResultOrThrow("/api/sales/tax-rates?pageSize=100", void 0, {
|
|
253
|
+
errorMessage: t(
|
|
254
|
+
"catalog.products.create.taxRates.error",
|
|
255
|
+
"Failed to load tax rates."
|
|
256
|
+
),
|
|
257
|
+
fallback: { items: [] }
|
|
258
|
+
});
|
|
76
259
|
const items = Array.isArray(payload.items) ? payload.items : [];
|
|
77
260
|
setTaxRates(
|
|
78
261
|
items.map((item) => {
|
|
79
262
|
const rawRate = typeof item.rate === "number" ? item.rate : Number(item.rate ?? Number.NaN);
|
|
80
263
|
return {
|
|
81
264
|
id: String(item.id),
|
|
82
|
-
name: typeof item.name === "string" && item.name.trim().length ? item.name : t(
|
|
265
|
+
name: typeof item.name === "string" && item.name.trim().length ? item.name : t(
|
|
266
|
+
"catalog.products.create.taxRates.unnamed",
|
|
267
|
+
"Untitled tax rate"
|
|
268
|
+
),
|
|
83
269
|
code: typeof item.code === "string" && item.code.trim().length ? item.code : null,
|
|
84
270
|
rate: Number.isFinite(rawRate) ? rawRate : null,
|
|
85
271
|
isDefault: Boolean(
|
|
@@ -99,7 +285,12 @@ function EditCatalogProductPage({ params }) {
|
|
|
99
285
|
React.useEffect(() => {
|
|
100
286
|
if (!productId) {
|
|
101
287
|
setLoading(false);
|
|
102
|
-
setError(
|
|
288
|
+
setError(
|
|
289
|
+
t(
|
|
290
|
+
"catalog.products.edit.errors.idMissing",
|
|
291
|
+
"Product identifier is missing."
|
|
292
|
+
)
|
|
293
|
+
);
|
|
103
294
|
return;
|
|
104
295
|
}
|
|
105
296
|
let cancelled = false;
|
|
@@ -112,25 +303,40 @@ function EditCatalogProductPage({ params }) {
|
|
|
112
303
|
);
|
|
113
304
|
if (!productRes.ok) throw new Error("load_failed");
|
|
114
305
|
const record = Array.isArray(productRes.result?.items) ? productRes.result?.items?.[0] : void 0;
|
|
115
|
-
if (!record)
|
|
116
|
-
|
|
306
|
+
if (!record)
|
|
307
|
+
throw new Error(
|
|
308
|
+
t("catalog.products.edit.errors.notFound", "Product not found.")
|
|
309
|
+
);
|
|
310
|
+
const rawMetadata = isRecord(record.metadata) ? record.metadata : null;
|
|
117
311
|
const dimensions = normalizeProductDimensions(
|
|
118
312
|
record.dimensions ?? rawMetadata?.dimensions ?? null
|
|
119
313
|
);
|
|
314
|
+
const rawWeightMeta = isRecord(rawMetadata?.weight) ? rawMetadata.weight : null;
|
|
120
315
|
const weight = normalizeProductWeight({
|
|
121
|
-
value: record.weight_value ?? record.weightValue ??
|
|
122
|
-
unit: record.weight_unit ?? record.weightUnit ??
|
|
316
|
+
value: record.weight_value ?? record.weightValue ?? rawWeightMeta?.value,
|
|
317
|
+
unit: record.weight_unit ?? record.weightUnit ?? rawWeightMeta?.unit
|
|
123
318
|
});
|
|
124
319
|
const metadata = normalizeMetadata(rawMetadata);
|
|
125
320
|
const optionSchemaId = typeof record.option_schema_id === "string" ? record.option_schema_id : typeof record.optionSchemaId === "string" ? record.optionSchemaId : null;
|
|
126
321
|
const taxRateId = typeof record.tax_rate_id === "string" ? record.tax_rate_id : typeof record.taxRateId === "string" ? record.taxRateId : null;
|
|
127
322
|
const optionSchemaTemplate = optionSchemaId ? await fetchOptionSchemaTemplate(optionSchemaId) : null;
|
|
128
|
-
const normalizedSchema = normalizeOptionSchemaRecord(
|
|
323
|
+
const normalizedSchema = normalizeOptionSchemaRecord(
|
|
324
|
+
optionSchemaTemplate?.schema
|
|
325
|
+
);
|
|
129
326
|
let optionInputs = normalizedSchema ? convertSchemaToProductOptions(normalizedSchema) : [];
|
|
130
327
|
if (!optionInputs.length) {
|
|
131
328
|
optionInputs = readOptionSchema(metadata);
|
|
132
329
|
}
|
|
133
|
-
const attachments = await
|
|
330
|
+
const [attachments, conversionsRes] = await Promise.all([
|
|
331
|
+
fetchAttachments(productId),
|
|
332
|
+
apiCall(
|
|
333
|
+
`/api/catalog/product-unit-conversions?productId=${encodeURIComponent(productId)}&page=1&pageSize=100`,
|
|
334
|
+
void 0,
|
|
335
|
+
{ fallback: { items: [] } }
|
|
336
|
+
)
|
|
337
|
+
]);
|
|
338
|
+
const conversionRows = conversionsRes.ok ? readProductConversionRows(conversionsRes.result?.items) : [];
|
|
339
|
+
initialConversionsRef.current = conversionRows;
|
|
134
340
|
const { customValues } = extractCustomFields(record);
|
|
135
341
|
const offers = readOfferSnapshots(record);
|
|
136
342
|
offerSnapshotsRef.current = offers;
|
|
@@ -138,8 +344,18 @@ function EditCatalogProductPage({ params }) {
|
|
|
138
344
|
const channelOptionEntries = buildChannelOptions(offers);
|
|
139
345
|
const { ids: categoryIds, options: categoryOptions } = readCategorySelections(record);
|
|
140
346
|
const { values: tagValues, options: tagOptions } = readTagSelections(record);
|
|
141
|
-
const defaultMediaId = typeof record.default_media_id === "string" ? record.default_media_id : record.defaultMediaId
|
|
142
|
-
const defaultMediaUrl = typeof record.default_media_url === "string" ? record.default_media_url : record.defaultMediaUrl
|
|
347
|
+
const defaultMediaId = typeof record.default_media_id === "string" ? record.default_media_id : typeof record.defaultMediaId === "string" ? record.defaultMediaId : null;
|
|
348
|
+
const defaultMediaUrl = typeof record.default_media_url === "string" ? record.default_media_url : typeof record.defaultMediaUrl === "string" ? record.defaultMediaUrl : "";
|
|
349
|
+
const {
|
|
350
|
+
defaultUnit,
|
|
351
|
+
defaultSalesUnit,
|
|
352
|
+
defaultSalesUnitQuantity,
|
|
353
|
+
roundingScale,
|
|
354
|
+
roundingMode,
|
|
355
|
+
unitPriceEnabled,
|
|
356
|
+
unitPriceReferenceUnit,
|
|
357
|
+
unitPriceBaseQuantity
|
|
358
|
+
} = extractUomFields(record);
|
|
143
359
|
const initial = {
|
|
144
360
|
title: typeof record.title === "string" ? record.title : "",
|
|
145
361
|
subtitle: typeof record.subtitle === "string" ? record.subtitle : "",
|
|
@@ -158,6 +374,17 @@ function EditCatalogProductPage({ params }) {
|
|
|
158
374
|
metadata,
|
|
159
375
|
dimensions: sanitizeProductDimensions(dimensions),
|
|
160
376
|
weight: sanitizeProductWeight(weight),
|
|
377
|
+
defaultUnit: defaultUnit ?? null,
|
|
378
|
+
defaultSalesUnit: defaultSalesUnit ?? defaultUnit ?? null,
|
|
379
|
+
defaultSalesUnitQuantity: String(defaultSalesUnitQuantity ?? 1),
|
|
380
|
+
uomRoundingScale: String(roundingScale),
|
|
381
|
+
uomRoundingMode: roundingMode,
|
|
382
|
+
unitPriceEnabled,
|
|
383
|
+
unitPriceReferenceUnit: unitPriceReferenceUnit && UNIT_PRICE_REFERENCE_UNITS.has(
|
|
384
|
+
unitPriceReferenceUnit
|
|
385
|
+
) ? unitPriceReferenceUnit : null,
|
|
386
|
+
unitPriceBaseQuantity: unitPriceBaseQuantity === null ? "" : String(unitPriceBaseQuantity),
|
|
387
|
+
unitConversions: conversionRows,
|
|
161
388
|
customFieldsetCode: typeof record.custom_fieldset_code === "string" ? record.custom_fieldset_code : typeof record.customFieldsetCode === "string" ? record.customFieldsetCode : null,
|
|
162
389
|
categoryIds,
|
|
163
390
|
channelIds,
|
|
@@ -175,7 +402,10 @@ function EditCatalogProductPage({ params }) {
|
|
|
175
402
|
} catch (err) {
|
|
176
403
|
console.error("catalog.products.edit.load failed", err);
|
|
177
404
|
if (!cancelled) {
|
|
178
|
-
const message = err instanceof Error && err.message ? err.message : t(
|
|
405
|
+
const message = err instanceof Error && err.message ? err.message : t(
|
|
406
|
+
"catalog.products.edit.errors.load",
|
|
407
|
+
"Failed to load product details."
|
|
408
|
+
);
|
|
179
409
|
setError(message);
|
|
180
410
|
}
|
|
181
411
|
} finally {
|
|
@@ -186,16 +416,14 @@ function EditCatalogProductPage({ params }) {
|
|
|
186
416
|
return () => {
|
|
187
417
|
cancelled = true;
|
|
188
418
|
};
|
|
189
|
-
}, [productId, t]);
|
|
419
|
+
}, [fetchAttachments, loadVariants, productId, t]);
|
|
190
420
|
React.useEffect(() => {
|
|
191
421
|
let cancelled = false;
|
|
192
422
|
async function loadPriceKinds() {
|
|
193
423
|
try {
|
|
194
|
-
const payload = await readApiResultOrThrow(
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
{ fallback: { items: [] } }
|
|
198
|
-
);
|
|
424
|
+
const payload = await readApiResultOrThrow("/api/catalog/price-kinds?pageSize=100", void 0, {
|
|
425
|
+
fallback: { items: [] }
|
|
426
|
+
});
|
|
199
427
|
const items = Array.isArray(payload.items) ? payload.items : [];
|
|
200
428
|
const summaries = items.map((item) => normalizePriceKindSummary(item)).filter((entry) => !!entry);
|
|
201
429
|
if (!cancelled) {
|
|
@@ -214,323 +442,463 @@ function EditCatalogProductPage({ params }) {
|
|
|
214
442
|
cancelled = true;
|
|
215
443
|
};
|
|
216
444
|
}, []);
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (!summary) continue;
|
|
233
|
-
if (!priceMap[summary.variantId]) {
|
|
234
|
-
priceMap[summary.variantId] = [];
|
|
445
|
+
const handleVariantDeleted = React.useCallback((variantId) => {
|
|
446
|
+
setVariants((prev) => prev.filter((variant) => variant.id !== variantId));
|
|
447
|
+
}, []);
|
|
448
|
+
const groups = React.useMemo(
|
|
449
|
+
() => [
|
|
450
|
+
{
|
|
451
|
+
id: "details",
|
|
452
|
+
column: 1,
|
|
453
|
+
component: ({ values, setValue, errors }) => /* @__PURE__ */ jsx(
|
|
454
|
+
ProductDetailsSection,
|
|
455
|
+
{
|
|
456
|
+
values,
|
|
457
|
+
setValue,
|
|
458
|
+
errors,
|
|
459
|
+
productId: productId ?? ""
|
|
235
460
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
461
|
+
)
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
id: "dimensions",
|
|
465
|
+
column: 1,
|
|
466
|
+
component: ({ values, setValue, errors }) => /* @__PURE__ */ jsx(
|
|
467
|
+
ProductDimensionsSection,
|
|
468
|
+
{
|
|
469
|
+
values,
|
|
470
|
+
setValue,
|
|
471
|
+
errors
|
|
472
|
+
}
|
|
473
|
+
)
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
id: "metadata",
|
|
477
|
+
column: 1,
|
|
478
|
+
component: ({ values, setValue, errors }) => /* @__PURE__ */ jsx(
|
|
479
|
+
ProductMetadataSection,
|
|
480
|
+
{
|
|
481
|
+
values,
|
|
482
|
+
setValue,
|
|
483
|
+
errors
|
|
484
|
+
}
|
|
485
|
+
)
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
id: "options",
|
|
489
|
+
column: 1,
|
|
490
|
+
component: ({ values, setValue, errors }) => /* @__PURE__ */ jsx(
|
|
491
|
+
ProductOptionsSection,
|
|
492
|
+
{
|
|
493
|
+
values,
|
|
494
|
+
setValue,
|
|
495
|
+
errors
|
|
496
|
+
}
|
|
497
|
+
)
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
id: "product-uom",
|
|
501
|
+
column: 1,
|
|
502
|
+
bare: true,
|
|
503
|
+
component: ({ values, setValue, errors }) => /* @__PURE__ */ jsx(
|
|
504
|
+
ProductUomSection,
|
|
505
|
+
{
|
|
506
|
+
values,
|
|
507
|
+
setValue,
|
|
508
|
+
errors
|
|
509
|
+
}
|
|
510
|
+
)
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
id: "variants",
|
|
514
|
+
column: 1,
|
|
515
|
+
component: ({ values, setValue, errors }) => /* @__PURE__ */ jsx(
|
|
516
|
+
ProductVariantsSection,
|
|
517
|
+
{
|
|
518
|
+
values,
|
|
519
|
+
setValue,
|
|
520
|
+
errors,
|
|
521
|
+
productId: productId ?? "",
|
|
522
|
+
variants,
|
|
523
|
+
priceKinds,
|
|
524
|
+
onVariantDeleted: handleVariantDeleted,
|
|
525
|
+
onVariantsReload: refreshVariants
|
|
526
|
+
}
|
|
527
|
+
)
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
id: "meta",
|
|
531
|
+
column: 2,
|
|
532
|
+
title: t("catalog.products.create.meta.title", "Product meta"),
|
|
533
|
+
description: t(
|
|
534
|
+
"catalog.products.create.meta.description",
|
|
535
|
+
"Manage subtitle and handle for storefronts."
|
|
536
|
+
),
|
|
537
|
+
component: ({ values, setValue, errors }) => /* @__PURE__ */ jsx(
|
|
538
|
+
ProductMetaSection,
|
|
539
|
+
{
|
|
540
|
+
values,
|
|
541
|
+
setValue,
|
|
542
|
+
errors,
|
|
543
|
+
taxRates
|
|
544
|
+
}
|
|
545
|
+
)
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
id: "categorize",
|
|
549
|
+
column: 2,
|
|
550
|
+
title: t("catalog.products.create.organize.title", "Categorize"),
|
|
551
|
+
description: t(
|
|
552
|
+
"catalog.products.create.organize.description",
|
|
553
|
+
"Assign categories, sales channels, and tags."
|
|
554
|
+
),
|
|
555
|
+
component: ({ values, setValue, errors }) => /* @__PURE__ */ jsx(
|
|
556
|
+
ProductCategorizeSection,
|
|
557
|
+
{
|
|
558
|
+
values,
|
|
559
|
+
setValue,
|
|
560
|
+
errors,
|
|
561
|
+
initialCategoryOptions: categorizeOptions.categories,
|
|
562
|
+
initialChannelOptions: categorizeOptions.channels,
|
|
563
|
+
initialTagOptions: categorizeOptions.tags
|
|
564
|
+
}
|
|
565
|
+
)
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
id: "custom-fields",
|
|
569
|
+
column: 2,
|
|
570
|
+
title: t("catalog.products.edit.custom.title", "Custom attributes"),
|
|
571
|
+
kind: "customFields"
|
|
572
|
+
}
|
|
573
|
+
],
|
|
574
|
+
[
|
|
575
|
+
categorizeOptions,
|
|
576
|
+
handleVariantDeleted,
|
|
577
|
+
priceKinds,
|
|
578
|
+
productId,
|
|
579
|
+
refreshVariants,
|
|
580
|
+
t,
|
|
581
|
+
taxRates,
|
|
582
|
+
variants
|
|
583
|
+
]
|
|
584
|
+
);
|
|
585
|
+
const handleSubmit = React.useCallback(
|
|
586
|
+
async (formValues) => {
|
|
587
|
+
if (!productId) {
|
|
588
|
+
throw createCrudFormError(
|
|
589
|
+
t(
|
|
590
|
+
"catalog.products.edit.errors.idMissing",
|
|
591
|
+
"Product identifier is missing."
|
|
592
|
+
)
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
const parsed = productFormSchema.safeParse(formValues);
|
|
596
|
+
if (!parsed.success) {
|
|
597
|
+
const issues = parsed.error.issues;
|
|
598
|
+
const fieldErrors = {};
|
|
599
|
+
issues.forEach((issue) => {
|
|
600
|
+
const path = issue.path.join(".") || "form";
|
|
601
|
+
if (!fieldErrors[path]) fieldErrors[path] = issue.message;
|
|
244
602
|
});
|
|
603
|
+
const message = issues[0]?.message ?? t(
|
|
604
|
+
"catalog.products.edit.errors.validation",
|
|
605
|
+
"Fix highlighted fields."
|
|
606
|
+
);
|
|
607
|
+
throw createCrudFormError(message, fieldErrors);
|
|
245
608
|
}
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
609
|
+
const values = {
|
|
610
|
+
...BASE_INITIAL_VALUES,
|
|
611
|
+
...parsed.data,
|
|
612
|
+
title: parsed.data.title ?? "",
|
|
613
|
+
subtitle: parsed.data.subtitle ?? "",
|
|
614
|
+
handle: parsed.data.handle ?? "",
|
|
615
|
+
description: parsed.data.description ?? "",
|
|
616
|
+
useMarkdown: parsed.data.useMarkdown ?? false,
|
|
617
|
+
taxRateId: parsed.data.taxRateId ?? null,
|
|
618
|
+
mediaDraftId: parsed.data.mediaDraftId ?? productId,
|
|
619
|
+
mediaItems: Array.isArray(parsed.data.mediaItems) ? parsed.data.mediaItems : [],
|
|
620
|
+
defaultMediaId: parsed.data.defaultMediaId ?? null,
|
|
621
|
+
defaultMediaUrl: parsed.data.defaultMediaUrl ?? "",
|
|
622
|
+
hasVariants: parsed.data.hasVariants ?? false,
|
|
623
|
+
options: Array.isArray(parsed.data.options) ? parsed.data.options : [],
|
|
624
|
+
variants: Array.isArray(parsed.data.variants) ? parsed.data.variants : [],
|
|
625
|
+
metadata: parsed.data.metadata ?? {},
|
|
626
|
+
dimensions: parsed.data.dimensions ?? null,
|
|
627
|
+
weight: parsed.data.weight ?? null,
|
|
628
|
+
defaultUnit: parsed.data.defaultUnit ?? null,
|
|
629
|
+
defaultSalesUnit: parsed.data.defaultSalesUnit ?? null,
|
|
630
|
+
defaultSalesUnitQuantity: parsed.data.defaultSalesUnitQuantity?.toString() ?? "1",
|
|
631
|
+
uomRoundingScale: parsed.data.uomRoundingScale?.toString() ?? "4",
|
|
632
|
+
uomRoundingMode: parsed.data.uomRoundingMode ?? "half_up",
|
|
633
|
+
unitPriceEnabled: Boolean(parsed.data.unitPriceEnabled),
|
|
634
|
+
unitPriceReferenceUnit: parsed.data.unitPriceReferenceUnit ?? null,
|
|
635
|
+
unitPriceBaseQuantity: parsed.data.unitPriceBaseQuantity?.toString() ?? "",
|
|
636
|
+
unitConversions: Array.isArray(parsed.data.unitConversions) ? parsed.data.unitConversions.map((entry) => ({
|
|
637
|
+
id: toTrimmedOrNull(entry.id) ?? null,
|
|
638
|
+
unitCode: toTrimmedOrNull(entry.unitCode) ?? "",
|
|
639
|
+
toBaseFactor: toPositiveNumberOrNull(entry.toBaseFactor) === null ? "" : String(toPositiveNumberOrNull(entry.toBaseFactor)),
|
|
640
|
+
sortOrder: typeof entry.sortOrder === "number" && Number.isFinite(entry.sortOrder) ? String(entry.sortOrder) : "",
|
|
641
|
+
isActive: entry.isActive !== false
|
|
642
|
+
})) : [],
|
|
643
|
+
customFieldsetCode: parsed.data.customFieldsetCode ?? null,
|
|
644
|
+
categoryIds: parsed.data.categoryIds ?? [],
|
|
645
|
+
channelIds: parsed.data.channelIds ?? [],
|
|
646
|
+
tags: parsed.data.tags ?? [],
|
|
647
|
+
optionSchemaId: parsed.data.optionSchemaId ?? null
|
|
648
|
+
};
|
|
649
|
+
const title = values.title?.trim();
|
|
650
|
+
if (!title) {
|
|
651
|
+
const message = t(
|
|
652
|
+
"catalog.products.create.errors.title",
|
|
653
|
+
"Provide a product title."
|
|
654
|
+
);
|
|
655
|
+
throw createCrudFormError(message, { title: message });
|
|
656
|
+
}
|
|
657
|
+
const handle = values.handle?.trim() || void 0;
|
|
658
|
+
const description = values.description?.trim() || void 0;
|
|
659
|
+
const metadata = buildMetadataPayload(values);
|
|
660
|
+
const dimensions = sanitizeProductDimensions(values.dimensions ?? null);
|
|
661
|
+
const weight = sanitizeProductWeight(values.weight ?? null);
|
|
662
|
+
const resolveTaxRateValue = (taxRateId) => {
|
|
663
|
+
if (!taxRateId) return null;
|
|
664
|
+
const match = taxRates.find((rate) => rate.id === taxRateId);
|
|
665
|
+
return typeof match?.rate === "number" && Number.isFinite(match.rate) ? match.rate : null;
|
|
666
|
+
};
|
|
667
|
+
const productTaxRateValue = resolveTaxRateValue(values.taxRateId ?? null);
|
|
668
|
+
const defaultMediaId = typeof values.defaultMediaId === "string" && values.defaultMediaId.trim().length ? values.defaultMediaId : null;
|
|
669
|
+
const defaultMediaEntry = defaultMediaId ? values.mediaItems.find((item) => item.id === defaultMediaId) : null;
|
|
670
|
+
const defaultMediaUrl = defaultMediaEntry ? buildAttachmentImageUrl(defaultMediaEntry.id, {
|
|
671
|
+
slug: slugifyAttachmentFileName(defaultMediaEntry.fileName)
|
|
672
|
+
}) : null;
|
|
673
|
+
const defaultUnit = canonicalizeUnitCode(values.defaultUnit);
|
|
674
|
+
const defaultSalesUnit = canonicalizeUnitCode(values.defaultSalesUnit);
|
|
675
|
+
const defaultSalesUnitQuantity = toPositiveNumberOrNull(values.defaultSalesUnitQuantity) ?? 1;
|
|
676
|
+
const uomRoundingScale = toIntegerInRangeOrDefault(
|
|
677
|
+
values.uomRoundingScale,
|
|
678
|
+
0,
|
|
679
|
+
6,
|
|
680
|
+
4
|
|
262
681
|
);
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}, []);
|
|
268
|
-
const refreshVariants = React.useCallback(async () => {
|
|
269
|
-
if (!productId) return;
|
|
270
|
-
await loadVariants(productId);
|
|
271
|
-
}, [loadVariants, productId]);
|
|
272
|
-
const fetchAttachments = React.useCallback(async (id) => {
|
|
273
|
-
try {
|
|
274
|
-
const res = await apiCall(
|
|
275
|
-
`/api/attachments?entityId=${encodeURIComponent(E.catalog.catalog_product)}&recordId=${encodeURIComponent(id)}`
|
|
682
|
+
const uomRoundingMode = values.uomRoundingMode === "down" || values.uomRoundingMode === "up" ? values.uomRoundingMode : "half_up";
|
|
683
|
+
const unitPriceEnabled = Boolean(values.unitPriceEnabled);
|
|
684
|
+
const unitPriceReferenceUnit = canonicalizeUnitCode(
|
|
685
|
+
values.unitPriceReferenceUnit
|
|
276
686
|
);
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
console.error("attachments.fetch failed", err);
|
|
287
|
-
return [];
|
|
288
|
-
}
|
|
289
|
-
}, []);
|
|
290
|
-
function mapVariantPriceSummary(item) {
|
|
291
|
-
if (!item) return null;
|
|
292
|
-
const getString2 = (value) => {
|
|
293
|
-
if (typeof value === "string" && value.trim().length) return value.trim();
|
|
294
|
-
if (typeof value === "number" || typeof value === "bigint") return String(value);
|
|
295
|
-
return null;
|
|
296
|
-
};
|
|
297
|
-
const variantId = getString2(item.variant_id ?? item.variantId);
|
|
298
|
-
const id = getString2(item.id);
|
|
299
|
-
if (!variantId || !id) return null;
|
|
300
|
-
const priceKindId = getString2(item.price_kind_id ?? item.priceKindId);
|
|
301
|
-
const currencyCode = getString2(item.currency_code ?? item.currencyCode);
|
|
302
|
-
const unitGross = getString2(item.unit_price_gross ?? item.unitPriceGross);
|
|
303
|
-
const unitNet = getString2(item.unit_price_net ?? item.unitPriceNet);
|
|
304
|
-
const amount = unitGross ?? unitNet ?? null;
|
|
305
|
-
return {
|
|
306
|
-
id,
|
|
307
|
-
variantId,
|
|
308
|
-
priceKindId,
|
|
309
|
-
currencyCode,
|
|
310
|
-
amount,
|
|
311
|
-
displayMode: unitGross ? "including-tax" : "excluding-tax"
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
function normalizeVariantOptionValues(input) {
|
|
315
|
-
if (!input || typeof input !== "object") return null;
|
|
316
|
-
const result = {};
|
|
317
|
-
Object.entries(input).forEach(([key, value]) => {
|
|
318
|
-
if (typeof key === "string" && typeof value === "string" && key.trim().length) {
|
|
319
|
-
result[key] = value;
|
|
687
|
+
const unitPriceBaseQuantity = toPositiveNumberOrNull(
|
|
688
|
+
values.unitPriceBaseQuantity
|
|
689
|
+
);
|
|
690
|
+
if (defaultSalesUnit && !defaultUnit) {
|
|
691
|
+
const message = t(
|
|
692
|
+
"catalog.products.uom.errors.baseRequired",
|
|
693
|
+
"Base unit is required when default sales unit is set."
|
|
694
|
+
);
|
|
695
|
+
throw createCrudFormError(message, { defaultSalesUnit: message });
|
|
320
696
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
{
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
697
|
+
const conversionInputs = normalizeProductConversionInputs(
|
|
698
|
+
values.unitConversions,
|
|
699
|
+
t(
|
|
700
|
+
"catalog.products.uom.errors.duplicateConversion",
|
|
701
|
+
"Duplicate conversion unit is not allowed."
|
|
702
|
+
)
|
|
703
|
+
);
|
|
704
|
+
if (conversionInputs.length && !defaultUnit) {
|
|
705
|
+
const message = t(
|
|
706
|
+
"catalog.products.uom.errors.baseRequiredForConversions",
|
|
707
|
+
"Base unit is required when conversions are configured."
|
|
708
|
+
);
|
|
709
|
+
throw createCrudFormError(message, { defaultUnit: message });
|
|
710
|
+
}
|
|
711
|
+
const defaultUnitKey = defaultUnit?.toLowerCase() ?? null;
|
|
712
|
+
const defaultSalesUnitKey = defaultSalesUnit?.toLowerCase() ?? null;
|
|
713
|
+
if (defaultUnitKey && defaultSalesUnitKey && defaultSalesUnitKey !== defaultUnitKey) {
|
|
714
|
+
const hasDefaultSalesConversion = conversionInputs.some(
|
|
715
|
+
(entry) => entry.isActive && entry.unitCode.toLowerCase() === defaultSalesUnitKey
|
|
716
|
+
);
|
|
717
|
+
if (!hasDefaultSalesConversion) {
|
|
718
|
+
const message = t(
|
|
719
|
+
"catalog.products.uom.errors.defaultSalesConversionRequired",
|
|
720
|
+
"Active conversion for default sales unit is required when it differs from base unit."
|
|
721
|
+
);
|
|
722
|
+
throw createCrudFormError(message, {
|
|
723
|
+
defaultSalesUnit: message,
|
|
724
|
+
unitConversions: message
|
|
725
|
+
});
|
|
338
726
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
{
|
|
352
|
-
id: "options",
|
|
353
|
-
column: 1,
|
|
354
|
-
component: ({ values, setValue, errors }) => /* @__PURE__ */ jsx(ProductOptionsSection, { values, setValue, errors })
|
|
355
|
-
},
|
|
356
|
-
{
|
|
357
|
-
id: "variants",
|
|
358
|
-
column: 1,
|
|
359
|
-
component: ({ values, setValue, errors }) => /* @__PURE__ */ jsx(
|
|
360
|
-
ProductVariantsSection,
|
|
361
|
-
{
|
|
362
|
-
values,
|
|
363
|
-
setValue,
|
|
364
|
-
errors,
|
|
365
|
-
productId: productId ?? "",
|
|
366
|
-
variants,
|
|
367
|
-
priceKinds,
|
|
368
|
-
onVariantDeleted: handleVariantDeleted,
|
|
369
|
-
onVariantsReload: refreshVariants
|
|
727
|
+
}
|
|
728
|
+
if (unitPriceEnabled) {
|
|
729
|
+
if (!unitPriceReferenceUnit || !UNIT_PRICE_REFERENCE_UNITS.has(
|
|
730
|
+
unitPriceReferenceUnit
|
|
731
|
+
)) {
|
|
732
|
+
const message = t(
|
|
733
|
+
"catalog.products.unitPrice.errors.referenceUnit",
|
|
734
|
+
"Reference unit is required when unit price display is enabled."
|
|
735
|
+
);
|
|
736
|
+
throw createCrudFormError(message, {
|
|
737
|
+
unitPriceReferenceUnit: message
|
|
738
|
+
});
|
|
370
739
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
},
|
|
380
|
-
{
|
|
381
|
-
id: "categorize",
|
|
382
|
-
column: 2,
|
|
383
|
-
title: t("catalog.products.create.organize.title", "Categorize"),
|
|
384
|
-
description: t("catalog.products.create.organize.description", "Assign categories, sales channels, and tags."),
|
|
385
|
-
component: ({ values, setValue, errors }) => /* @__PURE__ */ jsx(
|
|
386
|
-
ProductCategorizeSection,
|
|
387
|
-
{
|
|
388
|
-
values,
|
|
389
|
-
setValue,
|
|
390
|
-
errors,
|
|
391
|
-
initialCategoryOptions: categorizeOptions.categories,
|
|
392
|
-
initialChannelOptions: categorizeOptions.channels,
|
|
393
|
-
initialTagOptions: categorizeOptions.tags
|
|
740
|
+
if (unitPriceBaseQuantity === null) {
|
|
741
|
+
const message = t(
|
|
742
|
+
"catalog.products.unitPrice.errors.baseQuantity",
|
|
743
|
+
"Base quantity is required when unit price display is enabled."
|
|
744
|
+
);
|
|
745
|
+
throw createCrudFormError(message, {
|
|
746
|
+
unitPriceBaseQuantity: message
|
|
747
|
+
});
|
|
394
748
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
id: "custom-fields",
|
|
399
|
-
column: 2,
|
|
400
|
-
title: t("catalog.products.edit.custom.title", "Custom attributes"),
|
|
401
|
-
kind: "customFields"
|
|
402
|
-
}
|
|
403
|
-
], [categorizeOptions, handleVariantDeleted, priceKinds, productId, t, taxRates, variants]);
|
|
404
|
-
const handleSubmit = React.useCallback(async (formValues) => {
|
|
405
|
-
if (!productId) {
|
|
406
|
-
throw createCrudFormError(t("catalog.products.edit.errors.idMissing", "Product identifier is missing."));
|
|
407
|
-
}
|
|
408
|
-
const parsed = productFormSchema.safeParse(formValues);
|
|
409
|
-
if (!parsed.success) {
|
|
410
|
-
const issues = parsed.error.issues;
|
|
411
|
-
const fieldErrors = {};
|
|
412
|
-
issues.forEach((issue) => {
|
|
413
|
-
const path = issue.path.join(".") || "form";
|
|
414
|
-
if (!fieldErrors[path]) fieldErrors[path] = issue.message;
|
|
415
|
-
});
|
|
416
|
-
const message = issues[0]?.message ?? t("catalog.products.edit.errors.validation", "Fix highlighted fields.");
|
|
417
|
-
throw createCrudFormError(message, fieldErrors);
|
|
418
|
-
}
|
|
419
|
-
const values = {
|
|
420
|
-
...BASE_INITIAL_VALUES,
|
|
421
|
-
...parsed.data,
|
|
422
|
-
title: parsed.data.title ?? "",
|
|
423
|
-
subtitle: parsed.data.subtitle ?? "",
|
|
424
|
-
handle: parsed.data.handle ?? "",
|
|
425
|
-
description: parsed.data.description ?? "",
|
|
426
|
-
useMarkdown: parsed.data.useMarkdown ?? false,
|
|
427
|
-
taxRateId: parsed.data.taxRateId ?? null,
|
|
428
|
-
mediaDraftId: parsed.data.mediaDraftId ?? productId,
|
|
429
|
-
mediaItems: Array.isArray(parsed.data.mediaItems) ? parsed.data.mediaItems : [],
|
|
430
|
-
defaultMediaId: parsed.data.defaultMediaId ?? null,
|
|
431
|
-
defaultMediaUrl: parsed.data.defaultMediaUrl ?? "",
|
|
432
|
-
hasVariants: parsed.data.hasVariants ?? false,
|
|
433
|
-
options: Array.isArray(parsed.data.options) ? parsed.data.options : [],
|
|
434
|
-
variants: Array.isArray(parsed.data.variants) ? parsed.data.variants : [],
|
|
435
|
-
metadata: parsed.data.metadata ?? {},
|
|
436
|
-
dimensions: parsed.data.dimensions ?? null,
|
|
437
|
-
weight: parsed.data.weight ?? null,
|
|
438
|
-
customFieldsetCode: parsed.data.customFieldsetCode ?? null,
|
|
439
|
-
categoryIds: parsed.data.categoryIds ?? [],
|
|
440
|
-
channelIds: parsed.data.channelIds ?? [],
|
|
441
|
-
tags: parsed.data.tags ?? [],
|
|
442
|
-
optionSchemaId: parsed.data.optionSchemaId ?? null
|
|
443
|
-
};
|
|
444
|
-
const title = values.title?.trim();
|
|
445
|
-
if (!title) {
|
|
446
|
-
const message = t("catalog.products.create.errors.title", "Provide a product title.");
|
|
447
|
-
throw createCrudFormError(message, { title: message });
|
|
448
|
-
}
|
|
449
|
-
const handle = values.handle?.trim() || void 0;
|
|
450
|
-
const description = values.description?.trim() || void 0;
|
|
451
|
-
const metadata = buildMetadataPayload(values);
|
|
452
|
-
const dimensions = sanitizeProductDimensions(values.dimensions ?? null);
|
|
453
|
-
const weight = sanitizeProductWeight(values.weight ?? null);
|
|
454
|
-
const resolveTaxRateValue = (taxRateId) => {
|
|
455
|
-
if (!taxRateId) return null;
|
|
456
|
-
const match = taxRates.find((rate) => rate.id === taxRateId);
|
|
457
|
-
return typeof match?.rate === "number" && Number.isFinite(match.rate) ? match.rate : null;
|
|
458
|
-
};
|
|
459
|
-
const productTaxRateValue = resolveTaxRateValue(values.taxRateId ?? null);
|
|
460
|
-
const defaultMediaId = typeof values.defaultMediaId === "string" && values.defaultMediaId.trim().length ? values.defaultMediaId : null;
|
|
461
|
-
const defaultMediaEntry = defaultMediaId ? values.mediaItems.find((item) => item.id === defaultMediaId) : null;
|
|
462
|
-
const defaultMediaUrl = defaultMediaEntry ? buildAttachmentImageUrl(defaultMediaEntry.id, {
|
|
463
|
-
slug: slugifyAttachmentFileName(defaultMediaEntry.fileName)
|
|
464
|
-
}) : null;
|
|
465
|
-
const payload = {
|
|
466
|
-
id: productId,
|
|
467
|
-
title,
|
|
468
|
-
subtitle: values.subtitle?.trim() || void 0,
|
|
469
|
-
description,
|
|
470
|
-
handle,
|
|
471
|
-
taxRateId: values.taxRateId ?? null,
|
|
472
|
-
taxRate: productTaxRateValue ?? null,
|
|
473
|
-
isConfigurable: Boolean(values.hasVariants),
|
|
474
|
-
metadata,
|
|
475
|
-
dimensions,
|
|
476
|
-
weightValue: weight?.value ?? null,
|
|
477
|
-
weightUnit: weight?.unit ?? null,
|
|
478
|
-
defaultMediaId: defaultMediaId ?? void 0,
|
|
479
|
-
defaultMediaUrl: defaultMediaUrl ?? void 0,
|
|
480
|
-
customFieldsetCode: values.customFieldsetCode?.trim().length ? values.customFieldsetCode : void 0
|
|
481
|
-
};
|
|
482
|
-
const categoryIds = normalizeIdList(values.categoryIds);
|
|
483
|
-
const channelIds = normalizeIdList(values.channelIds);
|
|
484
|
-
const tags = normalizeTagValues(values.tags);
|
|
485
|
-
payload.categoryIds = categoryIds;
|
|
486
|
-
payload.tags = tags;
|
|
487
|
-
const optionSchemaDefinition = buildOptionSchemaDefinition(values.options, title);
|
|
488
|
-
if (optionSchemaDefinition) {
|
|
489
|
-
payload.optionSchema = optionSchemaDefinition;
|
|
490
|
-
} else if (values.optionSchemaId) {
|
|
491
|
-
payload.optionSchemaId = null;
|
|
492
|
-
}
|
|
493
|
-
const customFields = collectCustomFieldValues(values);
|
|
494
|
-
if (Object.keys(customFields).length) {
|
|
495
|
-
payload.customFields = customFields;
|
|
496
|
-
}
|
|
497
|
-
const previousSnapshots = offerSnapshotsRef.current;
|
|
498
|
-
const offersPayload = buildOfferPayloads({
|
|
499
|
-
channelIds,
|
|
500
|
-
offerSnapshots: previousSnapshots,
|
|
501
|
-
fallback: {
|
|
749
|
+
}
|
|
750
|
+
const payload = {
|
|
751
|
+
id: productId,
|
|
502
752
|
title,
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
753
|
+
subtitle: values.subtitle?.trim() || void 0,
|
|
754
|
+
description,
|
|
755
|
+
handle,
|
|
756
|
+
taxRateId: values.taxRateId ?? null,
|
|
757
|
+
taxRate: productTaxRateValue ?? null,
|
|
758
|
+
isConfigurable: Boolean(values.hasVariants),
|
|
759
|
+
metadata,
|
|
760
|
+
dimensions,
|
|
761
|
+
weightValue: weight?.value ?? null,
|
|
762
|
+
weightUnit: weight?.unit ?? null,
|
|
763
|
+
defaultMediaId: defaultMediaId ?? void 0,
|
|
764
|
+
defaultMediaUrl: defaultMediaUrl ?? void 0,
|
|
765
|
+
defaultUnit: defaultUnit ?? null,
|
|
766
|
+
defaultSalesUnit: defaultSalesUnit ?? defaultUnit ?? null,
|
|
767
|
+
defaultSalesUnitQuantity,
|
|
768
|
+
uomRoundingScale,
|
|
769
|
+
uomRoundingMode,
|
|
770
|
+
unitPriceEnabled,
|
|
771
|
+
unitPriceReferenceUnit: unitPriceEnabled ? unitPriceReferenceUnit : void 0,
|
|
772
|
+
unitPriceBaseQuantity: unitPriceEnabled ? unitPriceBaseQuantity : void 0,
|
|
773
|
+
customFieldsetCode: values.customFieldsetCode?.trim().length ? values.customFieldsetCode : void 0
|
|
774
|
+
};
|
|
775
|
+
const categoryIds = normalizeIdList(values.categoryIds);
|
|
776
|
+
const channelIds = normalizeIdList(values.channelIds);
|
|
777
|
+
const tags = normalizeTagValues(values.tags);
|
|
778
|
+
payload.categoryIds = categoryIds;
|
|
779
|
+
payload.tags = tags;
|
|
780
|
+
const optionSchemaDefinition = buildOptionSchemaDefinition(
|
|
781
|
+
values.options,
|
|
782
|
+
title
|
|
783
|
+
);
|
|
784
|
+
if (optionSchemaDefinition) {
|
|
785
|
+
payload.optionSchema = optionSchemaDefinition;
|
|
786
|
+
} else if (values.optionSchemaId) {
|
|
787
|
+
payload.optionSchemaId = null;
|
|
506
788
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
789
|
+
const customFields = collectCustomFieldValues(values);
|
|
790
|
+
if (Object.keys(customFields).length) {
|
|
791
|
+
payload.customFields = customFields;
|
|
792
|
+
}
|
|
793
|
+
const previousSnapshots = offerSnapshotsRef.current;
|
|
794
|
+
const offersPayload = buildOfferPayloads({
|
|
795
|
+
channelIds,
|
|
796
|
+
offerSnapshots: previousSnapshots,
|
|
797
|
+
fallback: {
|
|
798
|
+
title,
|
|
799
|
+
description: description ?? void 0,
|
|
800
|
+
defaultMediaId,
|
|
801
|
+
defaultMediaUrl: defaultMediaUrl ?? void 0
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
payload.offers = offersPayload;
|
|
805
|
+
const removedOffers = previousSnapshots.filter(
|
|
806
|
+
(offer) => typeof offer.id === "string" && !channelIds.includes(offer.channelId)
|
|
807
|
+
);
|
|
808
|
+
if (removedOffers.length) {
|
|
809
|
+
try {
|
|
810
|
+
for (const offer of removedOffers) {
|
|
811
|
+
if (!offer.id) continue;
|
|
812
|
+
await deleteCrud("catalog/offers", offer.id, {
|
|
813
|
+
errorMessage: t(
|
|
814
|
+
"catalog.products.edit.offers.deleteError",
|
|
815
|
+
"Failed to remove sales channel offer."
|
|
816
|
+
)
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
} catch (err) {
|
|
820
|
+
console.error("catalog.products.edit.offers.delete", err);
|
|
821
|
+
throw createCrudFormError(
|
|
822
|
+
t(
|
|
823
|
+
"catalog.products.edit.offers.deleteError",
|
|
824
|
+
"Failed to remove sales channel offer."
|
|
825
|
+
)
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
await updateCrud("catalog/products", payload);
|
|
830
|
+
const previousConversionIds = new Set(
|
|
831
|
+
initialConversionsRef.current.map((entry) => toTrimmedOrNull(entry.id)).filter((id) => Boolean(id))
|
|
832
|
+
);
|
|
833
|
+
const nextConversionIds = new Set(
|
|
834
|
+
conversionInputs.map(
|
|
835
|
+
(entry) => entry.id && entry.id.trim().length ? entry.id : null
|
|
836
|
+
).filter((id) => Boolean(id))
|
|
837
|
+
);
|
|
838
|
+
const removedConversionIds = Array.from(previousConversionIds).filter(
|
|
839
|
+
(id) => !nextConversionIds.has(id)
|
|
840
|
+
);
|
|
841
|
+
for (const conversionId of removedConversionIds) {
|
|
842
|
+
await deleteCrud("catalog/product-unit-conversions", conversionId, {
|
|
843
|
+
errorMessage: t(
|
|
844
|
+
"catalog.products.uom.errors.sync",
|
|
845
|
+
"Failed to synchronize product conversions."
|
|
846
|
+
)
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
const persistedConversions = [];
|
|
850
|
+
for (const conversion of conversionInputs) {
|
|
851
|
+
if (conversion.id) {
|
|
852
|
+
await updateCrud("catalog/product-unit-conversions", {
|
|
853
|
+
id: conversion.id,
|
|
854
|
+
unitCode: conversion.unitCode,
|
|
855
|
+
toBaseFactor: conversion.toBaseFactor,
|
|
856
|
+
sortOrder: conversion.sortOrder,
|
|
857
|
+
isActive: conversion.isActive
|
|
858
|
+
});
|
|
859
|
+
persistedConversions.push({
|
|
860
|
+
id: conversion.id,
|
|
861
|
+
unitCode: conversion.unitCode,
|
|
862
|
+
toBaseFactor: String(conversion.toBaseFactor),
|
|
863
|
+
sortOrder: String(conversion.sortOrder),
|
|
864
|
+
isActive: conversion.isActive
|
|
518
865
|
});
|
|
866
|
+
continue;
|
|
519
867
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
868
|
+
const created = await createCrud(
|
|
869
|
+
"catalog/product-unit-conversions",
|
|
870
|
+
{
|
|
871
|
+
productId,
|
|
872
|
+
unitCode: conversion.unitCode,
|
|
873
|
+
toBaseFactor: conversion.toBaseFactor,
|
|
874
|
+
sortOrder: conversion.sortOrder,
|
|
875
|
+
isActive: conversion.isActive
|
|
876
|
+
}
|
|
524
877
|
);
|
|
878
|
+
const createdId = created.result && typeof created.result === "object" && typeof created.result.id === "string" ? created.result.id : null;
|
|
879
|
+
persistedConversions.push({
|
|
880
|
+
id: createdId,
|
|
881
|
+
unitCode: conversion.unitCode,
|
|
882
|
+
toBaseFactor: String(conversion.toBaseFactor),
|
|
883
|
+
sortOrder: String(conversion.sortOrder),
|
|
884
|
+
isActive: conversion.isActive
|
|
885
|
+
});
|
|
525
886
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
887
|
+
initialConversionsRef.current = persistedConversions;
|
|
888
|
+
offerSnapshotsRef.current = mergeOfferSnapshots(
|
|
889
|
+
previousSnapshots,
|
|
890
|
+
offersPayload
|
|
891
|
+
);
|
|
892
|
+
flash(t("catalog.products.edit.success", "Product updated."), "success");
|
|
893
|
+
router.push("/backend/catalog/products");
|
|
894
|
+
},
|
|
895
|
+
[productId, t, taxRates, router]
|
|
896
|
+
);
|
|
532
897
|
if (!productId) {
|
|
533
|
-
return /* @__PURE__ */ jsx(Page, { children: /* @__PURE__ */ jsx(PageBody, { children: /* @__PURE__ */ jsx("div", { className: "rounded border border-destructive/40 bg-destructive/10 px-4 py-3 text-sm text-destructive", children: t(
|
|
898
|
+
return /* @__PURE__ */ jsx(Page, { children: /* @__PURE__ */ jsx(PageBody, { children: /* @__PURE__ */ jsx("div", { className: "rounded border border-destructive/40 bg-destructive/10 px-4 py-3 text-sm text-destructive", children: t(
|
|
899
|
+
"catalog.products.edit.errors.idMissing",
|
|
900
|
+
"Product identifier is missing."
|
|
901
|
+
) }) }) });
|
|
534
902
|
}
|
|
535
903
|
return /* @__PURE__ */ jsx(Page, { children: /* @__PURE__ */ jsxs(PageBody, { children: [
|
|
536
904
|
error ? /* @__PURE__ */ jsx("div", { className: "mb-4 rounded border border-destructive/40 bg-destructive/10 px-4 py-3 text-sm text-destructive", children: error }) : null,
|
|
@@ -539,12 +907,17 @@ function EditCatalogProductPage({ params }) {
|
|
|
539
907
|
{
|
|
540
908
|
title: t("catalog.products.edit.title", "Edit product"),
|
|
541
909
|
backHref: "/backend/catalog/products",
|
|
542
|
-
versionHistory: {
|
|
910
|
+
versionHistory: {
|
|
911
|
+
resourceKind: "catalog.product",
|
|
912
|
+
resourceId: productId ? String(productId) : ""
|
|
913
|
+
},
|
|
543
914
|
fields: [],
|
|
544
915
|
groups,
|
|
545
916
|
injectionSpotId: "crud-form:catalog.product",
|
|
546
917
|
entityId: E.catalog.catalog_product,
|
|
547
|
-
customFieldsetBindings: {
|
|
918
|
+
customFieldsetBindings: {
|
|
919
|
+
[E.catalog.catalog_product]: { valueKey: "customFieldsetCode" }
|
|
920
|
+
},
|
|
548
921
|
initialValues: initialValues ?? void 0,
|
|
549
922
|
isLoading: loading,
|
|
550
923
|
loadingMessage: t("catalog.products.edit.loading", "Loading product"),
|
|
@@ -555,13 +928,23 @@ function EditCatalogProductPage({ params }) {
|
|
|
555
928
|
)
|
|
556
929
|
] }) });
|
|
557
930
|
}
|
|
558
|
-
function ProductDetailsSection({
|
|
931
|
+
function ProductDetailsSection({
|
|
932
|
+
values,
|
|
933
|
+
setValue,
|
|
934
|
+
errors,
|
|
935
|
+
productId
|
|
936
|
+
}) {
|
|
559
937
|
const t = useT();
|
|
560
|
-
const mediaItems =
|
|
938
|
+
const mediaItems = React.useMemo(
|
|
939
|
+
() => Array.isArray(values.mediaItems) ? values.mediaItems : [],
|
|
940
|
+
[values.mediaItems]
|
|
941
|
+
);
|
|
561
942
|
const handleMediaItemsChange = React.useCallback(
|
|
562
943
|
(nextItems) => {
|
|
563
944
|
setValue("mediaItems", nextItems);
|
|
564
|
-
const hasCurrent = nextItems.some(
|
|
945
|
+
const hasCurrent = nextItems.some(
|
|
946
|
+
(item) => item.id === values.defaultMediaId
|
|
947
|
+
);
|
|
565
948
|
if (!hasCurrent) {
|
|
566
949
|
const fallbackId = nextItems[0]?.id ?? null;
|
|
567
950
|
setValue("defaultMediaId", fallbackId);
|
|
@@ -590,7 +973,9 @@ function ProductDetailsSection({ values, setValue, errors, productId }) {
|
|
|
590
973
|
if (target) {
|
|
591
974
|
setValue(
|
|
592
975
|
"defaultMediaUrl",
|
|
593
|
-
buildAttachmentImageUrl(target.id, {
|
|
976
|
+
buildAttachmentImageUrl(target.id, {
|
|
977
|
+
slug: slugifyAttachmentFileName(target.fileName)
|
|
978
|
+
})
|
|
594
979
|
);
|
|
595
980
|
}
|
|
596
981
|
},
|
|
@@ -607,7 +992,10 @@ function ProductDetailsSection({ values, setValue, errors, productId }) {
|
|
|
607
992
|
{
|
|
608
993
|
value: values.title,
|
|
609
994
|
onChange: (event) => setValue("title", event.target.value),
|
|
610
|
-
placeholder: t(
|
|
995
|
+
placeholder: t(
|
|
996
|
+
"catalog.products.create.placeholders.title",
|
|
997
|
+
"e.g., Summer sneaker"
|
|
998
|
+
)
|
|
611
999
|
}
|
|
612
1000
|
),
|
|
613
1001
|
errors.title ? /* @__PURE__ */ jsx("p", { className: "text-xs text-red-600", children: errors.title }) : null
|
|
@@ -625,26 +1013,39 @@ function ProductDetailsSection({ values, setValue, errors, productId }) {
|
|
|
625
1013
|
className: "gap-2 text-xs",
|
|
626
1014
|
children: [
|
|
627
1015
|
values.useMarkdown ? /* @__PURE__ */ jsx(AlignLeft, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx(FileText, { className: "h-4 w-4" }),
|
|
628
|
-
values.useMarkdown ? t("catalog.products.create.actions.usePlain", "Use plain text") : t(
|
|
1016
|
+
values.useMarkdown ? t("catalog.products.create.actions.usePlain", "Use plain text") : t(
|
|
1017
|
+
"catalog.products.create.actions.useMarkdown",
|
|
1018
|
+
"Use markdown"
|
|
1019
|
+
)
|
|
629
1020
|
]
|
|
630
1021
|
}
|
|
631
1022
|
)
|
|
632
1023
|
] }),
|
|
633
|
-
values.useMarkdown ? /* @__PURE__ */ jsx(
|
|
634
|
-
|
|
1024
|
+
values.useMarkdown ? /* @__PURE__ */ jsx(
|
|
1025
|
+
"div",
|
|
635
1026
|
{
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
1027
|
+
"data-color-mode": "light",
|
|
1028
|
+
className: "overflow-hidden rounded-md border",
|
|
1029
|
+
children: /* @__PURE__ */ jsx(
|
|
1030
|
+
MarkdownEditor,
|
|
1031
|
+
{
|
|
1032
|
+
value: values.description,
|
|
1033
|
+
height: 260,
|
|
1034
|
+
onChange: (val) => setValue("description", val ?? ""),
|
|
1035
|
+
previewOptions: { remarkPlugins: [] }
|
|
1036
|
+
}
|
|
1037
|
+
)
|
|
640
1038
|
}
|
|
641
|
-
)
|
|
1039
|
+
) : /* @__PURE__ */ jsx(
|
|
642
1040
|
Textarea,
|
|
643
1041
|
{
|
|
644
1042
|
className: "min-h-[180px]",
|
|
645
1043
|
value: values.description,
|
|
646
1044
|
onChange: (event) => setValue("description", event.target.value),
|
|
647
|
-
placeholder: t(
|
|
1045
|
+
placeholder: t(
|
|
1046
|
+
"catalog.products.create.placeholders.description",
|
|
1047
|
+
"Describe the product..."
|
|
1048
|
+
)
|
|
648
1049
|
}
|
|
649
1050
|
)
|
|
650
1051
|
] }),
|
|
@@ -661,7 +1062,10 @@ function ProductDetailsSection({ values, setValue, errors, productId }) {
|
|
|
661
1062
|
)
|
|
662
1063
|
] });
|
|
663
1064
|
}
|
|
664
|
-
function ProductDimensionsSection({
|
|
1065
|
+
function ProductDimensionsSection({
|
|
1066
|
+
values,
|
|
1067
|
+
setValue
|
|
1068
|
+
}) {
|
|
665
1069
|
const t = useT();
|
|
666
1070
|
const dimensionValues = normalizeProductDimensions(values.dimensions);
|
|
667
1071
|
const weightValues = normalizeProductWeight(values.weight);
|
|
@@ -675,7 +1079,14 @@ function ProductDimensionsSection({ values, setValue }) {
|
|
|
675
1079
|
{
|
|
676
1080
|
type: "number",
|
|
677
1081
|
value: dimensionValues?.width ?? "",
|
|
678
|
-
onChange: (event) => setValue(
|
|
1082
|
+
onChange: (event) => setValue(
|
|
1083
|
+
"dimensions",
|
|
1084
|
+
updateDimensionValue(
|
|
1085
|
+
values.dimensions ?? null,
|
|
1086
|
+
"width",
|
|
1087
|
+
event.target.value
|
|
1088
|
+
)
|
|
1089
|
+
),
|
|
679
1090
|
placeholder: "0"
|
|
680
1091
|
}
|
|
681
1092
|
)
|
|
@@ -687,7 +1098,14 @@ function ProductDimensionsSection({ values, setValue }) {
|
|
|
687
1098
|
{
|
|
688
1099
|
type: "number",
|
|
689
1100
|
value: dimensionValues?.height ?? "",
|
|
690
|
-
onChange: (event) => setValue(
|
|
1101
|
+
onChange: (event) => setValue(
|
|
1102
|
+
"dimensions",
|
|
1103
|
+
updateDimensionValue(
|
|
1104
|
+
values.dimensions ?? null,
|
|
1105
|
+
"height",
|
|
1106
|
+
event.target.value
|
|
1107
|
+
)
|
|
1108
|
+
),
|
|
691
1109
|
placeholder: "0"
|
|
692
1110
|
}
|
|
693
1111
|
)
|
|
@@ -699,7 +1117,14 @@ function ProductDimensionsSection({ values, setValue }) {
|
|
|
699
1117
|
{
|
|
700
1118
|
type: "number",
|
|
701
1119
|
value: dimensionValues?.depth ?? "",
|
|
702
|
-
onChange: (event) => setValue(
|
|
1120
|
+
onChange: (event) => setValue(
|
|
1121
|
+
"dimensions",
|
|
1122
|
+
updateDimensionValue(
|
|
1123
|
+
values.dimensions ?? null,
|
|
1124
|
+
"depth",
|
|
1125
|
+
event.target.value
|
|
1126
|
+
)
|
|
1127
|
+
),
|
|
703
1128
|
placeholder: "0"
|
|
704
1129
|
}
|
|
705
1130
|
)
|
|
@@ -710,7 +1135,14 @@ function ProductDimensionsSection({ values, setValue }) {
|
|
|
710
1135
|
Input,
|
|
711
1136
|
{
|
|
712
1137
|
value: dimensionValues?.unit ?? "",
|
|
713
|
-
onChange: (event) => setValue(
|
|
1138
|
+
onChange: (event) => setValue(
|
|
1139
|
+
"dimensions",
|
|
1140
|
+
updateDimensionValue(
|
|
1141
|
+
values.dimensions ?? null,
|
|
1142
|
+
"unit",
|
|
1143
|
+
event.target.value
|
|
1144
|
+
)
|
|
1145
|
+
),
|
|
714
1146
|
placeholder: "cm"
|
|
715
1147
|
}
|
|
716
1148
|
)
|
|
@@ -722,7 +1154,14 @@ function ProductDimensionsSection({ values, setValue }) {
|
|
|
722
1154
|
{
|
|
723
1155
|
type: "number",
|
|
724
1156
|
value: weightValues?.value ?? "",
|
|
725
|
-
onChange: (event) => setValue(
|
|
1157
|
+
onChange: (event) => setValue(
|
|
1158
|
+
"weight",
|
|
1159
|
+
updateWeightValue(
|
|
1160
|
+
values.weight ?? null,
|
|
1161
|
+
"value",
|
|
1162
|
+
event.target.value
|
|
1163
|
+
)
|
|
1164
|
+
),
|
|
726
1165
|
placeholder: "0"
|
|
727
1166
|
}
|
|
728
1167
|
)
|
|
@@ -733,7 +1172,14 @@ function ProductDimensionsSection({ values, setValue }) {
|
|
|
733
1172
|
Input,
|
|
734
1173
|
{
|
|
735
1174
|
value: weightValues?.unit ?? "",
|
|
736
|
-
onChange: (event) => setValue(
|
|
1175
|
+
onChange: (event) => setValue(
|
|
1176
|
+
"weight",
|
|
1177
|
+
updateWeightValue(
|
|
1178
|
+
values.weight ?? null,
|
|
1179
|
+
"unit",
|
|
1180
|
+
event.target.value
|
|
1181
|
+
)
|
|
1182
|
+
),
|
|
737
1183
|
placeholder: "kg"
|
|
738
1184
|
}
|
|
739
1185
|
)
|
|
@@ -743,9 +1189,12 @@ function ProductDimensionsSection({ values, setValue }) {
|
|
|
743
1189
|
}
|
|
744
1190
|
function ProductMetadataSection({ values, setValue }) {
|
|
745
1191
|
const metadata = normalizeMetadata(values.metadata);
|
|
746
|
-
const handleMetadataChange = React.useCallback(
|
|
747
|
-
|
|
748
|
-
|
|
1192
|
+
const handleMetadataChange = React.useCallback(
|
|
1193
|
+
(next) => {
|
|
1194
|
+
setValue("metadata", next);
|
|
1195
|
+
},
|
|
1196
|
+
[setValue]
|
|
1197
|
+
);
|
|
749
1198
|
return /* @__PURE__ */ jsx(MetadataEditor, { value: metadata, onChange: handleMetadataChange, embedded: true });
|
|
750
1199
|
}
|
|
751
1200
|
function ProductOptionsSection({ values, setValue }) {
|
|
@@ -758,9 +1207,13 @@ function ProductOptionsSection({ values, setValue }) {
|
|
|
758
1207
|
const loadSchemas = React.useCallback(async () => {
|
|
759
1208
|
setSchemaLoading(true);
|
|
760
1209
|
try {
|
|
761
|
-
const res = await apiCall(
|
|
1210
|
+
const res = await apiCall(
|
|
1211
|
+
"/api/catalog/option-schemas?page=1&pageSize=100"
|
|
1212
|
+
);
|
|
762
1213
|
if (res.ok) {
|
|
763
|
-
setSchemaTemplates(
|
|
1214
|
+
setSchemaTemplates(
|
|
1215
|
+
Array.isArray(res.result?.items) ? res.result?.items ?? [] : []
|
|
1216
|
+
);
|
|
764
1217
|
} else {
|
|
765
1218
|
setSchemaTemplates([]);
|
|
766
1219
|
}
|
|
@@ -771,60 +1224,99 @@ function ProductOptionsSection({ values, setValue }) {
|
|
|
771
1224
|
setSchemaLoading(false);
|
|
772
1225
|
}
|
|
773
1226
|
}, []);
|
|
774
|
-
const handleDeleteSchema = React.useCallback(
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
return {
|
|
822
|
-
...option,
|
|
823
|
-
values: nextValues
|
|
1227
|
+
const handleDeleteSchema = React.useCallback(
|
|
1228
|
+
async (id) => {
|
|
1229
|
+
try {
|
|
1230
|
+
await deleteCrud("catalog/option-schemas", id, {
|
|
1231
|
+
errorMessage: t(
|
|
1232
|
+
"catalog.products.edit.schemas.deleteError",
|
|
1233
|
+
"Failed to delete schema."
|
|
1234
|
+
)
|
|
1235
|
+
});
|
|
1236
|
+
flash(
|
|
1237
|
+
t("catalog.products.edit.schemas.deleted", "Schema deleted."),
|
|
1238
|
+
"success"
|
|
1239
|
+
);
|
|
1240
|
+
void loadSchemas();
|
|
1241
|
+
} catch (err) {
|
|
1242
|
+
console.error("catalog.option-schemas.delete failed", err);
|
|
1243
|
+
}
|
|
1244
|
+
},
|
|
1245
|
+
[loadSchemas, t]
|
|
1246
|
+
);
|
|
1247
|
+
const handleSaveSchema = React.useCallback(
|
|
1248
|
+
async (name) => {
|
|
1249
|
+
if (!name.trim().length) {
|
|
1250
|
+
const message = t(
|
|
1251
|
+
"catalog.products.edit.schemas.nameRequired",
|
|
1252
|
+
"Provide a schema name."
|
|
1253
|
+
);
|
|
1254
|
+
throw createCrudFormError(message, { name: message });
|
|
1255
|
+
}
|
|
1256
|
+
const schemaPayload = buildSchemaFromOptions(
|
|
1257
|
+
Array.isArray(values.options) ? values.options : [],
|
|
1258
|
+
name
|
|
1259
|
+
);
|
|
1260
|
+
if (!schemaPayload.options?.length) {
|
|
1261
|
+
throw createCrudFormError(
|
|
1262
|
+
t(
|
|
1263
|
+
"catalog.products.edit.schemas.empty",
|
|
1264
|
+
"Add at least one option before saving."
|
|
1265
|
+
),
|
|
1266
|
+
{}
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
const payload = {
|
|
1270
|
+
name: name.trim(),
|
|
1271
|
+
code: slugify(name.trim()),
|
|
1272
|
+
schema: schemaPayload,
|
|
1273
|
+
isActive: true
|
|
824
1274
|
};
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1275
|
+
if (schemaToEdit?.id) payload.id = schemaToEdit.id;
|
|
1276
|
+
if (schemaToEdit?.id) await updateCrud("catalog/option-schemas", payload);
|
|
1277
|
+
else await createCrud("catalog/option-schemas", payload);
|
|
1278
|
+
flash(
|
|
1279
|
+
t("catalog.products.edit.schemas.saved", "Schema saved."),
|
|
1280
|
+
"success"
|
|
1281
|
+
);
|
|
1282
|
+
setSaveSchemaOpen(false);
|
|
1283
|
+
setSchemaToEdit(null);
|
|
1284
|
+
void loadSchemas();
|
|
1285
|
+
},
|
|
1286
|
+
[schemaToEdit, t, values.options, loadSchemas]
|
|
1287
|
+
);
|
|
1288
|
+
const handleOptionTitleChange = React.useCallback(
|
|
1289
|
+
(optionId, nextTitle) => {
|
|
1290
|
+
const next = (Array.isArray(values.options) ? values.options : []).map(
|
|
1291
|
+
(option) => option.id === optionId ? { ...option, title: nextTitle } : option
|
|
1292
|
+
);
|
|
1293
|
+
setValue("options", next);
|
|
1294
|
+
},
|
|
1295
|
+
[setValue, values.options]
|
|
1296
|
+
);
|
|
1297
|
+
const setOptionValues = React.useCallback(
|
|
1298
|
+
(optionId, labels) => {
|
|
1299
|
+
const normalized = labels.map((label) => label.trim()).filter((label) => label.length);
|
|
1300
|
+
const unique = Array.from(new Set(normalized));
|
|
1301
|
+
const next = (Array.isArray(values.options) ? values.options : []).map(
|
|
1302
|
+
(option) => {
|
|
1303
|
+
if (option.id !== optionId) return option;
|
|
1304
|
+
const existingByLabel = new Map(
|
|
1305
|
+
option.values.map((value) => [value.label, value])
|
|
1306
|
+
);
|
|
1307
|
+
const nextValues = unique.map(
|
|
1308
|
+
(label) => existingByLabel.get(label) ?? { id: createLocalId(), label }
|
|
1309
|
+
);
|
|
1310
|
+
return {
|
|
1311
|
+
...option,
|
|
1312
|
+
values: nextValues
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
);
|
|
1316
|
+
setValue("options", next);
|
|
1317
|
+
},
|
|
1318
|
+
[setValue, values.options]
|
|
1319
|
+
);
|
|
828
1320
|
const addOption = React.useCallback(() => {
|
|
829
1321
|
const next = [
|
|
830
1322
|
...Array.isArray(values.options) ? values.options : [],
|
|
@@ -832,14 +1324,22 @@ function ProductOptionsSection({ values, setValue }) {
|
|
|
832
1324
|
];
|
|
833
1325
|
setValue("options", next);
|
|
834
1326
|
}, [setValue, values.options]);
|
|
835
|
-
const removeOption = React.useCallback(
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
1327
|
+
const removeOption = React.useCallback(
|
|
1328
|
+
(optionId) => {
|
|
1329
|
+
const next = (Array.isArray(values.options) ? values.options : []).filter(
|
|
1330
|
+
(option) => option.id !== optionId
|
|
1331
|
+
);
|
|
1332
|
+
setValue("options", next);
|
|
1333
|
+
},
|
|
1334
|
+
[setValue, values.options]
|
|
1335
|
+
);
|
|
839
1336
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
840
1337
|
/* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
841
1338
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [
|
|
842
|
-
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: t(
|
|
1339
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: t(
|
|
1340
|
+
"catalog.products.create.optionsBuilder.title",
|
|
1341
|
+
"Product options"
|
|
1342
|
+
) }),
|
|
843
1343
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
844
1344
|
/* @__PURE__ */ jsx(
|
|
845
1345
|
Button,
|
|
@@ -851,7 +1351,10 @@ function ProductOptionsSection({ values, setValue }) {
|
|
|
851
1351
|
setSchemaDialogOpen(true);
|
|
852
1352
|
void loadSchemas();
|
|
853
1353
|
},
|
|
854
|
-
title: t(
|
|
1354
|
+
title: t(
|
|
1355
|
+
"catalog.products.edit.schemas.manage",
|
|
1356
|
+
"Open schema library"
|
|
1357
|
+
),
|
|
855
1358
|
children: /* @__PURE__ */ jsx(BookMarked, { className: "h-4 w-4" })
|
|
856
1359
|
}
|
|
857
1360
|
),
|
|
@@ -869,10 +1372,19 @@ function ProductOptionsSection({ values, setValue }) {
|
|
|
869
1372
|
children: /* @__PURE__ */ jsx(Save, { className: "h-4 w-4" })
|
|
870
1373
|
}
|
|
871
1374
|
),
|
|
872
|
-
/* @__PURE__ */ jsxs(
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
1375
|
+
/* @__PURE__ */ jsxs(
|
|
1376
|
+
Button,
|
|
1377
|
+
{
|
|
1378
|
+
type: "button",
|
|
1379
|
+
variant: "outline",
|
|
1380
|
+
size: "sm",
|
|
1381
|
+
onClick: addOption,
|
|
1382
|
+
children: [
|
|
1383
|
+
/* @__PURE__ */ jsx(Plus, { className: "mr-2 h-4 w-4" }),
|
|
1384
|
+
t("catalog.products.create.optionsBuilder.add", "Add option")
|
|
1385
|
+
]
|
|
1386
|
+
}
|
|
1387
|
+
)
|
|
876
1388
|
] })
|
|
877
1389
|
] }),
|
|
878
1390
|
(Array.isArray(values.options) ? values.options : []).map((option) => /* @__PURE__ */ jsxs("div", { className: "rounded-md bg-muted/40 p-4", children: [
|
|
@@ -882,11 +1394,23 @@ function ProductOptionsSection({ values, setValue }) {
|
|
|
882
1394
|
{
|
|
883
1395
|
value: option.title,
|
|
884
1396
|
onChange: (event) => handleOptionTitleChange(option.id, event.target.value),
|
|
885
|
-
placeholder: t(
|
|
1397
|
+
placeholder: t(
|
|
1398
|
+
"catalog.products.create.optionsBuilder.placeholder",
|
|
1399
|
+
"e.g., Color"
|
|
1400
|
+
),
|
|
886
1401
|
className: "flex-1"
|
|
887
1402
|
}
|
|
888
1403
|
),
|
|
889
|
-
/* @__PURE__ */ jsx(
|
|
1404
|
+
/* @__PURE__ */ jsx(
|
|
1405
|
+
Button,
|
|
1406
|
+
{
|
|
1407
|
+
variant: "ghost",
|
|
1408
|
+
size: "icon",
|
|
1409
|
+
type: "button",
|
|
1410
|
+
onClick: () => removeOption(option.id),
|
|
1411
|
+
children: /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4" })
|
|
1412
|
+
}
|
|
1413
|
+
)
|
|
890
1414
|
] }),
|
|
891
1415
|
/* @__PURE__ */ jsxs("div", { className: "mt-3 space-y-2", children: [
|
|
892
1416
|
/* @__PURE__ */ jsx(Label, { className: "text-xs uppercase text-muted-foreground", children: t("catalog.products.create.optionsBuilder.values", "Values") }),
|
|
@@ -895,12 +1419,18 @@ function ProductOptionsSection({ values, setValue }) {
|
|
|
895
1419
|
{
|
|
896
1420
|
value: option.values.map((value) => value.label),
|
|
897
1421
|
onChange: (labels) => setOptionValues(option.id, labels),
|
|
898
|
-
placeholder: t(
|
|
1422
|
+
placeholder: t(
|
|
1423
|
+
"catalog.products.create.optionsBuilder.valuePlaceholder",
|
|
1424
|
+
"Type a value and press Enter"
|
|
1425
|
+
)
|
|
899
1426
|
}
|
|
900
1427
|
)
|
|
901
1428
|
] })
|
|
902
1429
|
] }, option.id)),
|
|
903
|
-
!values.options?.length ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t(
|
|
1430
|
+
!values.options?.length ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t(
|
|
1431
|
+
"catalog.products.create.optionsBuilder.empty",
|
|
1432
|
+
"No options yet. Add your first option to generate variants."
|
|
1433
|
+
) }) : null
|
|
904
1434
|
] }),
|
|
905
1435
|
/* @__PURE__ */ jsx(
|
|
906
1436
|
OptionSchemaDialog,
|
|
@@ -957,7 +1487,10 @@ function ProductVariantsSection({
|
|
|
957
1487
|
() => Array.isArray(values.options) ? values.options : [],
|
|
958
1488
|
[values.options]
|
|
959
1489
|
);
|
|
960
|
-
const combos = React.useMemo(
|
|
1490
|
+
const combos = React.useMemo(
|
|
1491
|
+
() => buildVariantCombinations(optionDefinitions),
|
|
1492
|
+
[optionDefinitions]
|
|
1493
|
+
);
|
|
961
1494
|
const existingKeys = React.useMemo(() => {
|
|
962
1495
|
const set = /* @__PURE__ */ new Set();
|
|
963
1496
|
variants.forEach((variant) => {
|
|
@@ -1023,20 +1556,23 @@ function ProductVariantsSection({
|
|
|
1023
1556
|
},
|
|
1024
1557
|
[priceKindLookup, t]
|
|
1025
1558
|
);
|
|
1026
|
-
const formatPriceAmount = React.useCallback(
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1559
|
+
const formatPriceAmount = React.useCallback(
|
|
1560
|
+
(price) => {
|
|
1561
|
+
const amount = typeof price.amount === "string" && price.amount.trim().length ? price.amount.trim() : "";
|
|
1562
|
+
if (!amount) return "\u2014";
|
|
1563
|
+
if (!price.currencyCode) return amount;
|
|
1564
|
+
return `${price.currencyCode.toUpperCase()} ${amount}`;
|
|
1565
|
+
},
|
|
1566
|
+
[]
|
|
1567
|
+
);
|
|
1032
1568
|
const handleDeleteVariant = React.useCallback(
|
|
1033
1569
|
async (variant) => {
|
|
1034
1570
|
if (!allowVariantActions) return;
|
|
1035
1571
|
const label = variant.name || variant.sku || variant.id;
|
|
1036
|
-
const confirmMessage = t(
|
|
1037
|
-
"
|
|
1038
|
-
|
|
1039
|
-
);
|
|
1572
|
+
const confirmMessage = t(
|
|
1573
|
+
"catalog.products.edit.variantList.deleteConfirm",
|
|
1574
|
+
'Delete variant "{{name}}"?'
|
|
1575
|
+
).replace("{{name}}", label);
|
|
1040
1576
|
const confirmed = await confirm({
|
|
1041
1577
|
title: confirmMessage,
|
|
1042
1578
|
variant: "destructive"
|
|
@@ -1045,13 +1581,22 @@ function ProductVariantsSection({
|
|
|
1045
1581
|
setDeletingId(variant.id);
|
|
1046
1582
|
try {
|
|
1047
1583
|
await deleteCrud("catalog/variants", variant.id, {
|
|
1048
|
-
errorMessage: t(
|
|
1584
|
+
errorMessage: t(
|
|
1585
|
+
"catalog.variants.form.deleteError",
|
|
1586
|
+
"Failed to delete variant."
|
|
1587
|
+
)
|
|
1049
1588
|
});
|
|
1050
|
-
flash(
|
|
1589
|
+
flash(
|
|
1590
|
+
t("catalog.variants.form.deleted", "Variant deleted."),
|
|
1591
|
+
"success"
|
|
1592
|
+
);
|
|
1051
1593
|
onVariantDeleted(variant.id);
|
|
1052
1594
|
} catch (err) {
|
|
1053
1595
|
console.error("catalog.products.edit.variants.delete", err);
|
|
1054
|
-
flash(
|
|
1596
|
+
flash(
|
|
1597
|
+
t("catalog.variants.form.deleteError", "Failed to delete variant."),
|
|
1598
|
+
"error"
|
|
1599
|
+
);
|
|
1055
1600
|
} finally {
|
|
1056
1601
|
setDeletingId(null);
|
|
1057
1602
|
}
|
|
@@ -1062,7 +1607,10 @@ function ProductVariantsSection({
|
|
|
1062
1607
|
if (!productId || !allowVariantActions) return;
|
|
1063
1608
|
if (!missingCombos.length) {
|
|
1064
1609
|
flash(
|
|
1065
|
-
t(
|
|
1610
|
+
t(
|
|
1611
|
+
"catalog.products.edit.variantList.generateEmpty",
|
|
1612
|
+
"All option combinations already exist."
|
|
1613
|
+
),
|
|
1066
1614
|
"info"
|
|
1067
1615
|
);
|
|
1068
1616
|
return;
|
|
@@ -1079,11 +1627,23 @@ function ProductVariantsSection({
|
|
|
1079
1627
|
isActive: true
|
|
1080
1628
|
});
|
|
1081
1629
|
}
|
|
1082
|
-
flash(
|
|
1630
|
+
flash(
|
|
1631
|
+
t(
|
|
1632
|
+
"catalog.products.edit.variantList.generateSuccess",
|
|
1633
|
+
"Missing variants generated."
|
|
1634
|
+
),
|
|
1635
|
+
"success"
|
|
1636
|
+
);
|
|
1083
1637
|
if (onVariantsReload) await onVariantsReload();
|
|
1084
1638
|
} catch (err) {
|
|
1085
1639
|
console.error("catalog.products.edit.variantList.generate", err);
|
|
1086
|
-
flash(
|
|
1640
|
+
flash(
|
|
1641
|
+
t(
|
|
1642
|
+
"catalog.products.edit.variantList.generateError",
|
|
1643
|
+
"Failed to generate variants."
|
|
1644
|
+
),
|
|
1645
|
+
"error"
|
|
1646
|
+
);
|
|
1087
1647
|
} finally {
|
|
1088
1648
|
setGenerating(false);
|
|
1089
1649
|
}
|
|
@@ -1094,20 +1654,45 @@ function ProductVariantsSection({
|
|
|
1094
1654
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [
|
|
1095
1655
|
/* @__PURE__ */ jsx("h3", { id: "variants", className: "text-sm font-semibold", children: t("catalog.products.edit.variants", "Variants") }),
|
|
1096
1656
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
1097
|
-
showGenerateButton ? /* @__PURE__ */ jsx(
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1657
|
+
showGenerateButton ? /* @__PURE__ */ jsx(
|
|
1658
|
+
Button,
|
|
1659
|
+
{
|
|
1660
|
+
type: "button",
|
|
1661
|
+
size: "sm",
|
|
1662
|
+
variant: "outline",
|
|
1663
|
+
disabled: generating,
|
|
1664
|
+
onClick: () => {
|
|
1665
|
+
void handleGenerateVariants();
|
|
1666
|
+
},
|
|
1667
|
+
children: generating ? t(
|
|
1668
|
+
"catalog.products.edit.variantList.generating",
|
|
1669
|
+
"Generating\u2026"
|
|
1670
|
+
) : t(
|
|
1671
|
+
"catalog.products.edit.variantList.generate",
|
|
1672
|
+
"Generate variants"
|
|
1673
|
+
)
|
|
1674
|
+
}
|
|
1675
|
+
) : null,
|
|
1676
|
+
allowVariantActions ? /* @__PURE__ */ jsx(Button, { asChild: true, size: "sm", children: /* @__PURE__ */ jsxs(
|
|
1677
|
+
Link,
|
|
1678
|
+
{
|
|
1679
|
+
href: `/backend/catalog/products/${productId}/variants/create`,
|
|
1680
|
+
children: [
|
|
1681
|
+
/* @__PURE__ */ jsx(Plus, { className: "mr-2 h-4 w-4" }),
|
|
1682
|
+
t("catalog.products.edit.variants.add", "Add variant")
|
|
1683
|
+
]
|
|
1684
|
+
}
|
|
1685
|
+
) }) : null
|
|
1104
1686
|
] })
|
|
1105
1687
|
] }),
|
|
1106
1688
|
variants.length ? /* @__PURE__ */ jsx("div", { className: "overflow-x-auto rounded-md border", children: /* @__PURE__ */ jsxs("table", { className: "w-full table-auto text-sm", children: [
|
|
1107
1689
|
/* @__PURE__ */ jsx("thead", { className: "bg-muted/40 text-left text-xs uppercase text-muted-foreground", children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
1108
1690
|
/* @__PURE__ */ jsx("th", { className: "px-3 py-2 font-normal", children: t("catalog.products.form.variants", "Variant") }),
|
|
1109
1691
|
/* @__PURE__ */ jsx("th", { className: "px-3 py-2 font-normal", children: "SKU" }),
|
|
1110
|
-
/* @__PURE__ */ jsx("th", { className: "px-3 py-2 font-normal", children: t(
|
|
1692
|
+
/* @__PURE__ */ jsx("th", { className: "px-3 py-2 font-normal", children: t(
|
|
1693
|
+
"catalog.products.edit.variantList.pricesHeading",
|
|
1694
|
+
"Prices"
|
|
1695
|
+
) }),
|
|
1111
1696
|
/* @__PURE__ */ jsx("th", { className: "px-3 py-2 font-normal", children: t("catalog.products.edit.variants.default", "Default") }),
|
|
1112
1697
|
/* @__PURE__ */ jsx("th", { className: "px-3 py-2 font-normal text-right", children: t("catalog.products.edit.variantList.actions", "Actions") })
|
|
1113
1698
|
] }) }),
|
|
@@ -1121,14 +1706,30 @@ function ProductVariantsSection({
|
|
|
1121
1706
|
}
|
|
1122
1707
|
) }),
|
|
1123
1708
|
/* @__PURE__ */ jsx("td", { className: "px-3 py-2 text-muted-foreground", children: variant.sku || "\u2014" }),
|
|
1124
|
-
/* @__PURE__ */ jsx("td", { className: "px-3 py-2", children: variant.prices.length ? /* @__PURE__ */ jsx("ul", { className: "space-y-1", children: variant.prices.map((price) => /* @__PURE__ */ jsxs(
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1709
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-2", children: variant.prices.length ? /* @__PURE__ */ jsx("ul", { className: "space-y-1", children: variant.prices.map((price) => /* @__PURE__ */ jsxs(
|
|
1710
|
+
"li",
|
|
1711
|
+
{
|
|
1712
|
+
className: "text-xs text-muted-foreground",
|
|
1713
|
+
children: [
|
|
1714
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: formatPriceLabel(price) }),
|
|
1715
|
+
" ",
|
|
1716
|
+
/* @__PURE__ */ jsx("span", { children: formatPriceAmount(price) })
|
|
1717
|
+
]
|
|
1718
|
+
},
|
|
1719
|
+
price.id
|
|
1720
|
+
)) }) : /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t(
|
|
1721
|
+
"catalog.products.edit.variantList.pricesEmpty",
|
|
1722
|
+
"No prices yet."
|
|
1723
|
+
) }) }),
|
|
1129
1724
|
/* @__PURE__ */ jsx("td", { className: "px-3 py-2 text-muted-foreground", children: variant.isDefault ? t("common.yes", "Yes") : "\u2014" }),
|
|
1130
1725
|
/* @__PURE__ */ jsx("td", { className: "px-3 py-2", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap justify-end gap-2", children: [
|
|
1131
|
-
/* @__PURE__ */ jsx(Button, { asChild: true, size: "sm", variant: "outline", children: /* @__PURE__ */ jsx(
|
|
1726
|
+
/* @__PURE__ */ jsx(Button, { asChild: true, size: "sm", variant: "outline", children: /* @__PURE__ */ jsx(
|
|
1727
|
+
Link,
|
|
1728
|
+
{
|
|
1729
|
+
href: `/backend/catalog/products/${productId}/variants/${variant.id}`,
|
|
1730
|
+
children: t("catalog.products.list.actions.edit", "Edit")
|
|
1731
|
+
}
|
|
1732
|
+
) }),
|
|
1132
1733
|
allowVariantActions ? /* @__PURE__ */ jsx(
|
|
1133
1734
|
Button,
|
|
1134
1735
|
{
|
|
@@ -1140,17 +1741,31 @@ function ProductVariantsSection({
|
|
|
1140
1741
|
onClick: () => {
|
|
1141
1742
|
void handleDeleteVariant(variant);
|
|
1142
1743
|
},
|
|
1143
|
-
children: deletingId === variant.id ? t(
|
|
1744
|
+
children: deletingId === variant.id ? t(
|
|
1745
|
+
"catalog.products.edit.variantList.deleting",
|
|
1746
|
+
"Deleting\u2026"
|
|
1747
|
+
) : t(
|
|
1748
|
+
"catalog.products.list.actions.delete",
|
|
1749
|
+
"Delete"
|
|
1750
|
+
)
|
|
1144
1751
|
}
|
|
1145
1752
|
) : null
|
|
1146
1753
|
] }) })
|
|
1147
1754
|
] }, variant.id)) })
|
|
1148
|
-
] }) }) : /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t(
|
|
1755
|
+
] }) }) : /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t(
|
|
1756
|
+
"catalog.products.edit.variants.empty",
|
|
1757
|
+
"No variants defined yet."
|
|
1758
|
+
) })
|
|
1149
1759
|
] }),
|
|
1150
1760
|
ConfirmDialogElement
|
|
1151
1761
|
] });
|
|
1152
1762
|
}
|
|
1153
|
-
function ProductMetaSection({
|
|
1763
|
+
function ProductMetaSection({
|
|
1764
|
+
values,
|
|
1765
|
+
setValue,
|
|
1766
|
+
errors,
|
|
1767
|
+
taxRates
|
|
1768
|
+
}) {
|
|
1154
1769
|
const t = useT();
|
|
1155
1770
|
const handleValue = typeof values.handle === "string" ? values.handle : "";
|
|
1156
1771
|
const titleSource = typeof values.title === "string" ? values.title : "";
|
|
@@ -1190,7 +1805,10 @@ function ProductMetaSection({ values, setValue, errors, taxRates }) {
|
|
|
1190
1805
|
{
|
|
1191
1806
|
value: typeof values.subtitle === "string" ? values.subtitle : "",
|
|
1192
1807
|
onChange: (event) => setValue("subtitle", event.target.value),
|
|
1193
|
-
placeholder: t(
|
|
1808
|
+
placeholder: t(
|
|
1809
|
+
"catalog.products.create.placeholders.subtitle",
|
|
1810
|
+
"Optional subtitle"
|
|
1811
|
+
)
|
|
1194
1812
|
}
|
|
1195
1813
|
),
|
|
1196
1814
|
errors.subtitle ? /* @__PURE__ */ jsx("p", { className: "text-xs text-red-600", children: errors.subtitle }) : null
|
|
@@ -1213,11 +1831,17 @@ function ProductMetaSection({ values, setValue, errors, taxRates }) {
|
|
|
1213
1831
|
{
|
|
1214
1832
|
value: handleValue,
|
|
1215
1833
|
onChange: handleHandleInputChange,
|
|
1216
|
-
placeholder: t(
|
|
1834
|
+
placeholder: t(
|
|
1835
|
+
"catalog.products.create.placeholders.handle",
|
|
1836
|
+
"e.g., summer-sneaker"
|
|
1837
|
+
),
|
|
1217
1838
|
className: "font-mono lowercase"
|
|
1218
1839
|
}
|
|
1219
1840
|
),
|
|
1220
|
-
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t(
|
|
1841
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t(
|
|
1842
|
+
"catalog.products.create.handleHelp",
|
|
1843
|
+
"Handle is used for URLs and must be unique."
|
|
1844
|
+
) }),
|
|
1221
1845
|
errors.handle ? /* @__PURE__ */ jsx("p", { className: "text-xs text-red-600", children: errors.handle }) : null
|
|
1222
1846
|
] }),
|
|
1223
1847
|
/* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
@@ -1231,14 +1855,24 @@ function ProductMetaSection({ values, setValue, errors, taxRates }) {
|
|
|
1231
1855
|
size: "icon",
|
|
1232
1856
|
onClick: () => {
|
|
1233
1857
|
if (typeof window !== "undefined") {
|
|
1234
|
-
window.open(
|
|
1858
|
+
window.open(
|
|
1859
|
+
"/backend/config/sales?section=tax-rates",
|
|
1860
|
+
"_blank",
|
|
1861
|
+
"noopener,noreferrer"
|
|
1862
|
+
);
|
|
1235
1863
|
}
|
|
1236
1864
|
},
|
|
1237
|
-
title: t(
|
|
1865
|
+
title: t(
|
|
1866
|
+
"catalog.products.create.taxRates.manage",
|
|
1867
|
+
"Manage tax classes"
|
|
1868
|
+
),
|
|
1238
1869
|
className: "text-muted-foreground hover:text-foreground",
|
|
1239
1870
|
children: [
|
|
1240
1871
|
/* @__PURE__ */ jsx(Layers, { className: "h-4 w-4" }),
|
|
1241
|
-
/* @__PURE__ */ jsx("span", { className: "sr-only", children: t(
|
|
1872
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: t(
|
|
1873
|
+
"catalog.products.create.taxRates.manage",
|
|
1874
|
+
"Manage tax classes"
|
|
1875
|
+
) })
|
|
1242
1876
|
]
|
|
1243
1877
|
}
|
|
1244
1878
|
)
|
|
@@ -1251,12 +1885,24 @@ function ProductMetaSection({ values, setValue, errors, taxRates }) {
|
|
|
1251
1885
|
onChange: (event) => setValue("taxRateId", event.target.value || null),
|
|
1252
1886
|
disabled: !taxRates.length,
|
|
1253
1887
|
children: [
|
|
1254
|
-
/* @__PURE__ */ jsx("option", { value: "", children: taxRates.length ? t(
|
|
1888
|
+
/* @__PURE__ */ jsx("option", { value: "", children: taxRates.length ? t(
|
|
1889
|
+
"catalog.products.create.taxRates.noneSelected",
|
|
1890
|
+
"No tax class selected"
|
|
1891
|
+
) : t(
|
|
1892
|
+
"catalog.products.create.taxRates.emptyOption",
|
|
1893
|
+
"No tax classes available"
|
|
1894
|
+
) }),
|
|
1255
1895
|
taxRates.map((rate) => /* @__PURE__ */ jsx("option", { value: rate.id, children: formatTaxRateLabel(rate) }, rate.id))
|
|
1256
1896
|
]
|
|
1257
1897
|
}
|
|
1258
1898
|
),
|
|
1259
|
-
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: taxRates.length ? t(
|
|
1899
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: taxRates.length ? t(
|
|
1900
|
+
"catalog.products.create.taxRates.help",
|
|
1901
|
+
"Applied to prices unless overridden per variant."
|
|
1902
|
+
) : t(
|
|
1903
|
+
"catalog.products.create.taxRates.empty",
|
|
1904
|
+
"Define tax classes under Sales \u2192 Configuration."
|
|
1905
|
+
) })
|
|
1260
1906
|
] })
|
|
1261
1907
|
] });
|
|
1262
1908
|
}
|
|
@@ -1272,7 +1918,10 @@ function readOptionSchema(metadata) {
|
|
|
1272
1918
|
return raw.map((option) => {
|
|
1273
1919
|
if (!option || typeof option !== "object") return null;
|
|
1274
1920
|
const values = Array.isArray(option.values) ? option.values.map(
|
|
1275
|
-
(value) => value && typeof value === "object" ? {
|
|
1921
|
+
(value) => value && typeof value === "object" ? {
|
|
1922
|
+
id: String(value.id ?? createLocalId()),
|
|
1923
|
+
label: typeof value.label === "string" ? value.label : ""
|
|
1924
|
+
} : null
|
|
1276
1925
|
).filter(
|
|
1277
1926
|
(entry) => !!entry
|
|
1278
1927
|
) : [];
|
|
@@ -1304,7 +1953,9 @@ function formatCategoryLabel(name, fallback, parentName) {
|
|
|
1304
1953
|
return parentName ? `${base} / ${parentName}` : base;
|
|
1305
1954
|
}
|
|
1306
1955
|
function readCategorySelections(record) {
|
|
1307
|
-
const rawCategories = Array.isArray(
|
|
1956
|
+
const rawCategories = Array.isArray(
|
|
1957
|
+
record.categories
|
|
1958
|
+
) ? record.categories : [];
|
|
1308
1959
|
const options = rawCategories.map((entry) => {
|
|
1309
1960
|
const value = getString(entry.id) ?? getString(entry.categoryId) ?? getString(entry.category_id);
|
|
1310
1961
|
if (!value) return null;
|
|
@@ -1316,8 +1967,12 @@ function readCategorySelections(record) {
|
|
|
1316
1967
|
}).filter(
|
|
1317
1968
|
(option) => !!option
|
|
1318
1969
|
);
|
|
1319
|
-
const fallbackIds = normalizeIdList(
|
|
1320
|
-
|
|
1970
|
+
const fallbackIds = normalizeIdList(
|
|
1971
|
+
record.categoryIds
|
|
1972
|
+
);
|
|
1973
|
+
const combinedIds = Array.from(
|
|
1974
|
+
/* @__PURE__ */ new Set([...options.map((option) => option.value), ...fallbackIds])
|
|
1975
|
+
);
|
|
1321
1976
|
return { ids: combinedIds, options };
|
|
1322
1977
|
}
|
|
1323
1978
|
function readTagSelections(record) {
|
|
@@ -1368,7 +2023,9 @@ function buildChannelOptions(offers) {
|
|
|
1368
2023
|
}
|
|
1369
2024
|
function buildOfferPayloads(params) {
|
|
1370
2025
|
const { channelIds, offerSnapshots, fallback } = params;
|
|
1371
|
-
const byChannel = new Map(
|
|
2026
|
+
const byChannel = new Map(
|
|
2027
|
+
offerSnapshots.map((offer) => [offer.channelId, offer])
|
|
2028
|
+
);
|
|
1372
2029
|
const payloads = [];
|
|
1373
2030
|
const fallbackTitle = fallback.title.trim();
|
|
1374
2031
|
for (const channelId of channelIds) {
|
|
@@ -1389,7 +2046,9 @@ function buildOfferPayloads(params) {
|
|
|
1389
2046
|
return payloads;
|
|
1390
2047
|
}
|
|
1391
2048
|
function mergeOfferSnapshots(current, payloads) {
|
|
1392
|
-
const currentByChannel = new Map(
|
|
2049
|
+
const currentByChannel = new Map(
|
|
2050
|
+
current.map((snapshot) => [snapshot.channelId, snapshot])
|
|
2051
|
+
);
|
|
1393
2052
|
return payloads.map((entry) => {
|
|
1394
2053
|
const previous = currentByChannel.get(entry.channelId);
|
|
1395
2054
|
return {
|
|
@@ -1417,6 +2076,47 @@ function ensureString(value) {
|
|
|
1417
2076
|
function isRecord(value) {
|
|
1418
2077
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
1419
2078
|
}
|
|
2079
|
+
function extractUomFields(record) {
|
|
2080
|
+
const unitPriceObject = isRecord(record.unit_price) ? record.unit_price : null;
|
|
2081
|
+
const defaultUnit = canonicalizeUnitCode(
|
|
2082
|
+
record.default_unit ?? record.defaultUnit
|
|
2083
|
+
);
|
|
2084
|
+
const defaultSalesUnit = canonicalizeUnitCode(
|
|
2085
|
+
record.default_sales_unit ?? record.defaultSalesUnit
|
|
2086
|
+
);
|
|
2087
|
+
const defaultSalesUnitQuantity = toPositiveNumberOrNull(
|
|
2088
|
+
record.default_sales_unit_quantity ?? record.defaultSalesUnitQuantity
|
|
2089
|
+
);
|
|
2090
|
+
const roundingScale = toIntegerInRangeOrDefault(
|
|
2091
|
+
record.uom_rounding_scale ?? record.uomRoundingScale,
|
|
2092
|
+
0,
|
|
2093
|
+
6,
|
|
2094
|
+
4
|
|
2095
|
+
);
|
|
2096
|
+
const roundingModeRaw = toTrimmedOrNull(
|
|
2097
|
+
record.uom_rounding_mode ?? record.uomRoundingMode
|
|
2098
|
+
);
|
|
2099
|
+
const roundingMode = roundingModeRaw === "down" || roundingModeRaw === "up" ? roundingModeRaw : "half_up";
|
|
2100
|
+
const unitPriceEnabled = Boolean(
|
|
2101
|
+
unitPriceObject?.enabled ?? record.unit_price_enabled ?? record.unitPriceEnabled
|
|
2102
|
+
);
|
|
2103
|
+
const unitPriceReferenceUnit = canonicalizeUnitCode(
|
|
2104
|
+
unitPriceObject?.reference_unit ?? unitPriceObject?.referenceUnit ?? record.unit_price_reference_unit ?? record.unitPriceReferenceUnit
|
|
2105
|
+
);
|
|
2106
|
+
const unitPriceBaseQuantity = toPositiveNumberOrNull(
|
|
2107
|
+
unitPriceObject?.base_quantity ?? unitPriceObject?.baseQuantity ?? record.unit_price_base_quantity ?? record.unitPriceBaseQuantity
|
|
2108
|
+
);
|
|
2109
|
+
return {
|
|
2110
|
+
defaultUnit,
|
|
2111
|
+
defaultSalesUnit,
|
|
2112
|
+
defaultSalesUnitQuantity,
|
|
2113
|
+
roundingScale,
|
|
2114
|
+
roundingMode,
|
|
2115
|
+
unitPriceEnabled,
|
|
2116
|
+
unitPriceReferenceUnit,
|
|
2117
|
+
unitPriceBaseQuantity
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
1420
2120
|
function buildMetadataPayload(values) {
|
|
1421
2121
|
const metadata = normalizeMetadata(values.metadata);
|
|
1422
2122
|
metadata.__useMarkdown = values.useMarkdown ?? false;
|
|
@@ -1459,7 +2159,10 @@ function buildSchemaFromOptions(options, name) {
|
|
|
1459
2159
|
code: slugify(option.title || createLocalId()),
|
|
1460
2160
|
label: option.title,
|
|
1461
2161
|
inputType: "select",
|
|
1462
|
-
choices: option.values.map((value) => ({
|
|
2162
|
+
choices: option.values.map((value) => ({
|
|
2163
|
+
code: slugify(value.label || value.id),
|
|
2164
|
+
label: value.label
|
|
2165
|
+
}))
|
|
1463
2166
|
}))
|
|
1464
2167
|
};
|
|
1465
2168
|
}
|
|
@@ -1484,35 +2187,83 @@ function OptionSchemaDialog({
|
|
|
1484
2187
|
DataLoader,
|
|
1485
2188
|
{
|
|
1486
2189
|
isLoading: true,
|
|
1487
|
-
loadingMessage: t(
|
|
2190
|
+
loadingMessage: t(
|
|
2191
|
+
"catalog.products.edit.schemas.loading",
|
|
2192
|
+
"Loading\u2026"
|
|
2193
|
+
),
|
|
1488
2194
|
spinnerSize: "md",
|
|
1489
2195
|
children: /* @__PURE__ */ jsx(Fragment, {})
|
|
1490
2196
|
}
|
|
1491
2197
|
) }) : templates.length ? /* @__PURE__ */ jsx("div", { className: "divide-y rounded-md border", children: templates.map((template) => {
|
|
1492
2198
|
const id = typeof template.id === "string" ? template.id : null;
|
|
1493
|
-
return /* @__PURE__ */ jsxs(
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
/* @__PURE__ */ jsxs(Button, { type: "button", size: "icon", variant: "ghost", onClick: () => onEdit(template), children: [
|
|
1502
|
-
/* @__PURE__ */ jsx(Save, { className: "h-4 w-4" }),
|
|
1503
|
-
/* @__PURE__ */ jsx("span", { className: "sr-only", children: t("catalog.products.edit.schemas.edit", "Edit schema") })
|
|
2199
|
+
return /* @__PURE__ */ jsxs(
|
|
2200
|
+
"div",
|
|
2201
|
+
{
|
|
2202
|
+
className: "flex items-center justify-between px-4 py-3",
|
|
2203
|
+
children: [
|
|
2204
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
2205
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: template.name ?? template.id ?? "Schema" }),
|
|
2206
|
+
template.description ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: template.description }) : null
|
|
1504
2207
|
] }),
|
|
1505
|
-
/* @__PURE__ */ jsxs(
|
|
1506
|
-
/* @__PURE__ */ jsx(
|
|
1507
|
-
|
|
2208
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
2209
|
+
/* @__PURE__ */ jsx(
|
|
2210
|
+
Button,
|
|
2211
|
+
{
|
|
2212
|
+
type: "button",
|
|
2213
|
+
size: "sm",
|
|
2214
|
+
variant: "secondary",
|
|
2215
|
+
onClick: () => onSelect(template),
|
|
2216
|
+
children: t("catalog.products.edit.schemas.apply", "Apply")
|
|
2217
|
+
}
|
|
2218
|
+
),
|
|
2219
|
+
id ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2220
|
+
/* @__PURE__ */ jsxs(
|
|
2221
|
+
Button,
|
|
2222
|
+
{
|
|
2223
|
+
type: "button",
|
|
2224
|
+
size: "icon",
|
|
2225
|
+
variant: "ghost",
|
|
2226
|
+
onClick: () => onEdit(template),
|
|
2227
|
+
children: [
|
|
2228
|
+
/* @__PURE__ */ jsx(Save, { className: "h-4 w-4" }),
|
|
2229
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: t(
|
|
2230
|
+
"catalog.products.edit.schemas.edit",
|
|
2231
|
+
"Edit schema"
|
|
2232
|
+
) })
|
|
2233
|
+
]
|
|
2234
|
+
}
|
|
2235
|
+
),
|
|
2236
|
+
/* @__PURE__ */ jsxs(
|
|
2237
|
+
Button,
|
|
2238
|
+
{
|
|
2239
|
+
type: "button",
|
|
2240
|
+
size: "icon",
|
|
2241
|
+
variant: "ghost",
|
|
2242
|
+
onClick: () => void onDelete(id),
|
|
2243
|
+
children: [
|
|
2244
|
+
/* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4 text-destructive" }),
|
|
2245
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: t(
|
|
2246
|
+
"catalog.products.edit.schemas.delete",
|
|
2247
|
+
"Delete schema"
|
|
2248
|
+
) })
|
|
2249
|
+
]
|
|
2250
|
+
}
|
|
2251
|
+
)
|
|
2252
|
+
] }) : null
|
|
1508
2253
|
] })
|
|
1509
|
-
]
|
|
1510
|
-
|
|
1511
|
-
|
|
2254
|
+
]
|
|
2255
|
+
},
|
|
2256
|
+
id ?? template.name ?? createLocalId()
|
|
2257
|
+
);
|
|
1512
2258
|
}) }) : /* @__PURE__ */ jsx("p", { className: "py-4 text-sm text-muted-foreground", children: t("catalog.products.edit.schemas.empty", "No saved schemas yet.") })
|
|
1513
2259
|
] }) });
|
|
1514
2260
|
}
|
|
1515
|
-
function SaveSchemaDialog({
|
|
2261
|
+
function SaveSchemaDialog({
|
|
2262
|
+
open,
|
|
2263
|
+
onOpenChange,
|
|
2264
|
+
defaultName = "",
|
|
2265
|
+
onSubmit
|
|
2266
|
+
}) {
|
|
1516
2267
|
const t = useT();
|
|
1517
2268
|
const [name, setName] = React.useState(defaultName);
|
|
1518
2269
|
const [saving, setSaving] = React.useState(false);
|
|
@@ -1534,10 +2285,29 @@ function SaveSchemaDialog({ open, onOpenChange, defaultName = "", onSubmit }) {
|
|
|
1534
2285
|
/* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: t("catalog.products.edit.schemas.saveTitle", "Save option schema") }) }),
|
|
1535
2286
|
/* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
1536
2287
|
/* @__PURE__ */ jsx(Label, { htmlFor: "schemaName", children: t("catalog.products.edit.schemas.nameLabel", "Schema name") }),
|
|
1537
|
-
/* @__PURE__ */ jsx(
|
|
2288
|
+
/* @__PURE__ */ jsx(
|
|
2289
|
+
Input,
|
|
2290
|
+
{
|
|
2291
|
+
id: "schemaName",
|
|
2292
|
+
value: name,
|
|
2293
|
+
onChange: (event) => setName(event.target.value),
|
|
2294
|
+
placeholder: t(
|
|
2295
|
+
"catalog.products.edit.schemas.namePlaceholder",
|
|
2296
|
+
"e.g., Color + Size set"
|
|
2297
|
+
)
|
|
2298
|
+
}
|
|
2299
|
+
)
|
|
1538
2300
|
] }),
|
|
1539
2301
|
/* @__PURE__ */ jsxs(DialogFooter, { children: [
|
|
1540
|
-
/* @__PURE__ */ jsx(
|
|
2302
|
+
/* @__PURE__ */ jsx(
|
|
2303
|
+
Button,
|
|
2304
|
+
{
|
|
2305
|
+
type: "button",
|
|
2306
|
+
variant: "outline",
|
|
2307
|
+
onClick: () => onOpenChange(false),
|
|
2308
|
+
children: t("common.cancel", "Cancel")
|
|
2309
|
+
}
|
|
2310
|
+
),
|
|
1541
2311
|
/* @__PURE__ */ jsx(Button, { type: "button", onClick: handleSubmit, disabled: saving, children: saving ? t("common.saving", "Saving\u2026") : t("common.save", "Save") })
|
|
1542
2312
|
] })
|
|
1543
2313
|
] }) });
|