@open-mercato/core 0.10.1-develop.7879.1.80d3740285 → 0.10.1-develop.7883.1.44ac2125f5
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/.turbo/turbo-build.log +1 -1
- package/dist/modules/attachments/lib/transferClient.js +72 -0
- package/dist/modules/attachments/lib/transferClient.js.map +7 -0
- package/dist/modules/attachments/lib/transferLimits.js +5 -0
- package/dist/modules/attachments/lib/transferLimits.js.map +7 -0
- package/dist/modules/catalog/backend/catalog/products/[productId]/variants/create/page.js +23 -26
- package/dist/modules/catalog/backend/catalog/products/[productId]/variants/create/page.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/create/page.js +31 -23
- package/dist/modules/catalog/backend/catalog/products/create/page.js.map +2 -2
- package/dist/modules/catalog/components/products/mediaTransfer.js +27 -0
- package/dist/modules/catalog/components/products/mediaTransfer.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/Lane.js +5 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/Lane.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js +2 -1
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/attachments/lib/transferClient.ts +87 -0
- package/src/modules/attachments/lib/transferLimits.ts +1 -0
- package/src/modules/catalog/backend/catalog/products/[productId]/variants/create/page.tsx +23 -32
- package/src/modules/catalog/backend/catalog/products/create/page.tsx +31 -28
- package/src/modules/catalog/components/products/mediaTransfer.ts +40 -0
- package/src/modules/catalog/i18n/de.json +2 -0
- package/src/modules/catalog/i18n/en.json +2 -0
- package/src/modules/catalog/i18n/es.json +2 -0
- package/src/modules/catalog/i18n/ko.json +2 -0
- package/src/modules/catalog/i18n/pl.json +2 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/Lane.tsx +6 -2
- package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +3 -2
package/.turbo/turbo-build.log
CHANGED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { getTelemetryRuntime } from "@open-mercato/shared/lib/telemetry/runtime";
|
|
2
|
+
import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
|
|
3
|
+
import { ATTACHMENT_TRANSFER_MAX_IDS } from "./transferLimits.js";
|
|
4
|
+
function chunkAttachmentIds(attachmentIds, size = ATTACHMENT_TRANSFER_MAX_IDS) {
|
|
5
|
+
const chunks = [];
|
|
6
|
+
for (let index = 0; index < attachmentIds.length; index += size) {
|
|
7
|
+
chunks.push(attachmentIds.slice(index, index + size));
|
|
8
|
+
}
|
|
9
|
+
return chunks;
|
|
10
|
+
}
|
|
11
|
+
function stopsRemainingChunks(error) {
|
|
12
|
+
const name = typeof error === "object" && error !== null ? error.name : void 0;
|
|
13
|
+
return name === "AbortError" || name === "UnauthorizedError";
|
|
14
|
+
}
|
|
15
|
+
function describeError(error) {
|
|
16
|
+
return error instanceof Error && error.message ? error.message : String(error);
|
|
17
|
+
}
|
|
18
|
+
async function transferAttachmentsInChunks({
|
|
19
|
+
entityId,
|
|
20
|
+
attachmentIds,
|
|
21
|
+
fromRecordId,
|
|
22
|
+
toRecordId
|
|
23
|
+
}) {
|
|
24
|
+
const result = { transferredCount: 0, failedIds: [], unconfirmedCount: 0, errors: [] };
|
|
25
|
+
const chunks = chunkAttachmentIds(attachmentIds);
|
|
26
|
+
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex += 1) {
|
|
27
|
+
const chunk = chunks[chunkIndex];
|
|
28
|
+
try {
|
|
29
|
+
const call = await apiCall(
|
|
30
|
+
"/api/attachments/transfer",
|
|
31
|
+
{
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers: { "content-type": "application/json" },
|
|
34
|
+
body: JSON.stringify({
|
|
35
|
+
entityId,
|
|
36
|
+
attachmentIds: chunk,
|
|
37
|
+
...fromRecordId ? { fromRecordId } : {},
|
|
38
|
+
toRecordId
|
|
39
|
+
})
|
|
40
|
+
},
|
|
41
|
+
{ fallback: null }
|
|
42
|
+
);
|
|
43
|
+
if (!call.ok) {
|
|
44
|
+
result.failedIds.push(...chunk);
|
|
45
|
+
if (call.result?.error) result.errors.push(call.result.error);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const updated = call.result?.updated;
|
|
49
|
+
const confirmed = typeof updated === "number" && Number.isFinite(updated) ? Math.min(Math.max(Math.trunc(updated), 0), chunk.length) : 0;
|
|
50
|
+
result.transferredCount += confirmed;
|
|
51
|
+
result.unconfirmedCount += chunk.length - confirmed;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
getTelemetryRuntime()?.reportError(error, {
|
|
54
|
+
module: "attachments",
|
|
55
|
+
code: "attachments.transfer_chunk_failed",
|
|
56
|
+
attributes: { chunkSize: chunk.length }
|
|
57
|
+
});
|
|
58
|
+
result.errors.push(describeError(error));
|
|
59
|
+
if (stopsRemainingChunks(error)) {
|
|
60
|
+
result.failedIds.push(...chunks.slice(chunkIndex).flat());
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
result.failedIds.push(...chunk);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
export {
|
|
69
|
+
chunkAttachmentIds,
|
|
70
|
+
transferAttachmentsInChunks
|
|
71
|
+
};
|
|
72
|
+
//# sourceMappingURL=transferClient.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/attachments/lib/transferClient.ts"],
|
|
4
|
+
"sourcesContent": ["import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { ATTACHMENT_TRANSFER_MAX_IDS } from './transferLimits'\n\nexport type TransferAttachmentsInput = {\n entityId: string\n attachmentIds: string[]\n fromRecordId?: string | null\n toRecordId: string\n}\n\nexport type TransferAttachmentsResult = {\n transferredCount: number\n failedIds: string[]\n unconfirmedCount: number\n errors: string[]\n}\n\nexport function chunkAttachmentIds(attachmentIds: string[], size = ATTACHMENT_TRANSFER_MAX_IDS): string[][] {\n const chunks: string[][] = []\n for (let index = 0; index < attachmentIds.length; index += size) {\n chunks.push(attachmentIds.slice(index, index + size))\n }\n return chunks\n}\n\nfunction stopsRemainingChunks(error: unknown): boolean {\n const name = typeof error === 'object' && error !== null ? (error as { name?: unknown }).name : undefined\n return name === 'AbortError' || name === 'UnauthorizedError'\n}\n\nfunction describeError(error: unknown): string {\n return error instanceof Error && error.message ? error.message : String(error)\n}\n\nexport async function transferAttachmentsInChunks({\n entityId,\n attachmentIds,\n fromRecordId,\n toRecordId,\n}: TransferAttachmentsInput): Promise<TransferAttachmentsResult> {\n const result: TransferAttachmentsResult = { transferredCount: 0, failedIds: [], unconfirmedCount: 0, errors: [] }\n const chunks = chunkAttachmentIds(attachmentIds)\n for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex += 1) {\n const chunk = chunks[chunkIndex]\n try {\n const call = await apiCall<{ ok?: boolean; updated?: number; error?: string }>(\n '/api/attachments/transfer',\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n entityId,\n attachmentIds: chunk,\n ...(fromRecordId ? { fromRecordId } : {}),\n toRecordId,\n }),\n },\n { fallback: null },\n )\n if (!call.ok) {\n result.failedIds.push(...chunk)\n if (call.result?.error) result.errors.push(call.result.error)\n continue\n }\n const updated = call.result?.updated\n const confirmed = typeof updated === 'number' && Number.isFinite(updated)\n ? Math.min(Math.max(Math.trunc(updated), 0), chunk.length)\n : 0\n result.transferredCount += confirmed\n result.unconfirmedCount += chunk.length - confirmed\n } catch (error) {\n getTelemetryRuntime()?.reportError(error, {\n module: 'attachments',\n code: 'attachments.transfer_chunk_failed',\n attributes: { chunkSize: chunk.length },\n })\n result.errors.push(describeError(error))\n if (stopsRemainingChunks(error)) {\n result.failedIds.push(...chunks.slice(chunkIndex).flat())\n break\n }\n result.failedIds.push(...chunk)\n }\n }\n return result\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,2BAA2B;AACpC,SAAS,eAAe;AACxB,SAAS,mCAAmC;AAgBrC,SAAS,mBAAmB,eAAyB,OAAO,6BAAyC;AAC1G,QAAM,SAAqB,CAAC;AAC5B,WAAS,QAAQ,GAAG,QAAQ,cAAc,QAAQ,SAAS,MAAM;AAC/D,WAAO,KAAK,cAAc,MAAM,OAAO,QAAQ,IAAI,CAAC;AAAA,EACtD;AACA,SAAO;AACT;AAEA,SAAS,qBAAqB,OAAyB;AACrD,QAAM,OAAO,OAAO,UAAU,YAAY,UAAU,OAAQ,MAA6B,OAAO;AAChG,SAAO,SAAS,gBAAgB,SAAS;AAC3C;AAEA,SAAS,cAAc,OAAwB;AAC7C,SAAO,iBAAiB,SAAS,MAAM,UAAU,MAAM,UAAU,OAAO,KAAK;AAC/E;AAEA,eAAsB,4BAA4B;AAAA,EAChD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAiE;AAC/D,QAAM,SAAoC,EAAE,kBAAkB,GAAG,WAAW,CAAC,GAAG,kBAAkB,GAAG,QAAQ,CAAC,EAAE;AAChH,QAAM,SAAS,mBAAmB,aAAa;AAC/C,WAAS,aAAa,GAAG,aAAa,OAAO,QAAQ,cAAc,GAAG;AACpE,UAAM,QAAQ,OAAO,UAAU;AAC/B,QAAI;AACF,YAAM,OAAO,MAAM;AAAA,QACjB;AAAA,QACA;AAAA,UACE,QAAQ;AAAA,UACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,UAC9C,MAAM,KAAK,UAAU;AAAA,YACnB;AAAA,YACA,eAAe;AAAA,YACf,GAAI,eAAe,EAAE,aAAa,IAAI,CAAC;AAAA,YACvC;AAAA,UACF,CAAC;AAAA,QACH;AAAA,QACA,EAAE,UAAU,KAAK;AAAA,MACnB;AACA,UAAI,CAAC,KAAK,IAAI;AACZ,eAAO,UAAU,KAAK,GAAG,KAAK;AAC9B,YAAI,KAAK,QAAQ,MAAO,QAAO,OAAO,KAAK,KAAK,OAAO,KAAK;AAC5D;AAAA,MACF;AACA,YAAM,UAAU,KAAK,QAAQ;AAC7B,YAAM,YAAY,OAAO,YAAY,YAAY,OAAO,SAAS,OAAO,IACpE,KAAK,IAAI,KAAK,IAAI,KAAK,MAAM,OAAO,GAAG,CAAC,GAAG,MAAM,MAAM,IACvD;AACJ,aAAO,oBAAoB;AAC3B,aAAO,oBAAoB,MAAM,SAAS;AAAA,IAC5C,SAAS,OAAO;AACd,0BAAoB,GAAG,YAAY,OAAO;AAAA,QACxC,QAAQ;AAAA,QACR,MAAM;AAAA,QACN,YAAY,EAAE,WAAW,MAAM,OAAO;AAAA,MACxC,CAAC;AACD,aAAO,OAAO,KAAK,cAAc,KAAK,CAAC;AACvC,UAAI,qBAAqB,KAAK,GAAG;AAC/B,eAAO,UAAU,KAAK,GAAG,OAAO,MAAM,UAAU,EAAE,KAAK,CAAC;AACxD;AAAA,MACF;AACA,aAAO,UAAU,KAAK,GAAG,KAAK;AAAA,IAChC;AAAA,EACF;AACA,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
import { buildAttachmentImageUrl, slugifyAttachmentFileName } from "@open-mercato/core/modules/attachments/lib/imageUrls";
|
|
33
33
|
import { fetchOptionSchemaTemplate } from "../../../optionSchemaClient.js";
|
|
34
34
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
35
|
+
import { transferDraftMedia } from "@open-mercato/core/modules/catalog/components/products/mediaTransfer";
|
|
35
36
|
const logger = createLogger("catalog");
|
|
36
37
|
function CreateVariantPage({ params }) {
|
|
37
38
|
const productId = params?.productId ? String(params.productId) : null;
|
|
@@ -300,11 +301,19 @@ function CreateVariantPage({ params }) {
|
|
|
300
301
|
if (!variantId) {
|
|
301
302
|
throw createCrudFormError(t("catalog.variants.form.errors.idMissing", "Variant id missing after create."));
|
|
302
303
|
}
|
|
303
|
-
await
|
|
304
|
+
const mediaTransfer = await transferDraftMedia({
|
|
305
|
+
entityId: E.catalog.catalog_product_variant,
|
|
304
306
|
draftId: values.mediaDraftId,
|
|
305
|
-
variantId,
|
|
307
|
+
recordId: variantId,
|
|
306
308
|
mediaItems: Array.isArray(values.mediaItems) ? values.mediaItems : []
|
|
307
309
|
});
|
|
310
|
+
if (mediaTransfer.failedCount > 0) {
|
|
311
|
+
logger.error("attachments.transfer.failed", {
|
|
312
|
+
failed: mediaTransfer.failedCount,
|
|
313
|
+
requested: mediaTransfer.requestedCount,
|
|
314
|
+
errors: mediaTransfer.errors
|
|
315
|
+
});
|
|
316
|
+
}
|
|
308
317
|
await syncVariantPrices({
|
|
309
318
|
priceKinds,
|
|
310
319
|
priceDrafts: values.prices ?? {},
|
|
@@ -315,36 +324,24 @@ function CreateVariantPage({ params }) {
|
|
|
315
324
|
productTaxRateId,
|
|
316
325
|
productTaxRate
|
|
317
326
|
});
|
|
318
|
-
|
|
327
|
+
if (mediaTransfer.failedCount > 0) {
|
|
328
|
+
flash(
|
|
329
|
+
t(
|
|
330
|
+
"catalog.variants.form.mediaTransferIncomplete",
|
|
331
|
+
"Variant created, but {failed} of {total} media files could not be attached.",
|
|
332
|
+
{ failed: mediaTransfer.failedCount, total: mediaTransfer.requestedCount }
|
|
333
|
+
),
|
|
334
|
+
"warning"
|
|
335
|
+
);
|
|
336
|
+
} else {
|
|
337
|
+
flash(t("catalog.variants.form.createSuccess", "Variant created."), "success");
|
|
338
|
+
}
|
|
319
339
|
router.push(`/backend/catalog/products/${productId}#variants`);
|
|
320
340
|
}
|
|
321
341
|
}
|
|
322
342
|
)
|
|
323
343
|
] }) });
|
|
324
344
|
}
|
|
325
|
-
async function transferVariantMedia({
|
|
326
|
-
draftId,
|
|
327
|
-
variantId,
|
|
328
|
-
mediaItems
|
|
329
|
-
}) {
|
|
330
|
-
if (!draftId || !variantId) return;
|
|
331
|
-
const attachmentIds = mediaItems.map((item) => item.id).filter((id) => typeof id === "string" && id.length > 0);
|
|
332
|
-
if (!attachmentIds.length) return;
|
|
333
|
-
await apiCall(
|
|
334
|
-
"/api/attachments/transfer",
|
|
335
|
-
{
|
|
336
|
-
method: "POST",
|
|
337
|
-
headers: { "content-type": "application/json" },
|
|
338
|
-
body: JSON.stringify({
|
|
339
|
-
entityId: E.catalog.catalog_product_variant,
|
|
340
|
-
attachmentIds,
|
|
341
|
-
fromRecordId: draftId,
|
|
342
|
-
toRecordId: variantId
|
|
343
|
-
})
|
|
344
|
-
},
|
|
345
|
-
{ fallback: null }
|
|
346
|
-
);
|
|
347
|
-
}
|
|
348
345
|
async function syncVariantPrices({
|
|
349
346
|
priceKinds,
|
|
350
347
|
priceDrafts,
|
package/dist/modules/catalog/backend/catalog/products/[productId]/variants/create/page.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../../../src/modules/catalog/backend/catalog/products/%5BproductId%5D/variants/create/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { CrudForm, type CrudFormGroup } from '@open-mercato/ui/backend/CrudForm'\nimport { createCrud } from '@open-mercato/ui/backend/utils/crud'\nimport { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { collectCustomFieldValues } from '@open-mercato/ui/backend/utils/customFieldValues'\nimport { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { E } from '#generated/entities.ids.generated'\nimport {\n type VariantFormValues,\n type VariantPriceDraft,\n type OptionDefinition,\n createVariantInitialValues,\n normalizeOptionSchema,\n findInvalidVariantPriceKinds,\n} from '@open-mercato/core/modules/catalog/components/products/variantForm'\nimport {\n type PriceKindSummary,\n type PriceKindApiPayload,\n type TaxRateSummary,\n normalizePriceKindSummary,\n} from '@open-mercato/core/modules/catalog/components/products/productForm'\nimport { parseNumericInput } from '@open-mercato/core/modules/catalog/components/products/productFormUtils'\nimport {\n VariantBasicsSection,\n VariantOptionValuesSection,\n VariantDimensionsSection,\n VariantMetadataSection,\n VariantPricesSection,\n VariantMediaSection,\n} from '@open-mercato/core/modules/catalog/components/products/VariantBuilder'\nimport type { ProductMediaItem } from '@open-mercato/core/modules/catalog/components/products/ProductMediaManager'\nimport { buildAttachmentImageUrl, slugifyAttachmentFileName } from '@open-mercato/core/modules/attachments/lib/imageUrls'\nimport { fetchOptionSchemaTemplate } from '../../../optionSchemaClient'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('catalog')\n\ntype ProductResponse = {\n items?: Array<{\n id?: string\n title?: string | null\n metadata?: Record<string, unknown> | null\n custom_fieldset_code?: string | null\n customFieldsetCode?: string | null\n tax_rate_id?: string | null\n taxRateId?: string | null\n tax_rate?: number | string | null\n }>\n}\n\ntype VariantCreateResult = {\n id?: string\n variantId?: string\n}\n\nexport default function CreateVariantPage({ params }: { params?: { productId?: string } }) {\n const productId = params?.productId ? String(params.productId) : null\n const t = useT()\n const router = useRouter()\n const [optionDefinitions, setOptionDefinitions] = React.useState<OptionDefinition[]>([])\n const [priceKinds, setPriceKinds] = React.useState<PriceKindSummary[]>([])\n const [taxRates, setTaxRates] = React.useState<TaxRateSummary[]>([])\n const [initialValues, setInitialValues] = React.useState<VariantFormValues | null>(null)\n const [productTitle, setProductTitle] = React.useState<string>('')\n const [productTaxRateId, setProductTaxRateId] = React.useState<string | null>(null)\n const [productTaxRate, setProductTaxRate] = React.useState<number | null>(null)\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [isNotFound, setIsNotFound] = React.useState(false)\n\n React.useEffect(() => {\n const loadPriceKinds = async () => {\n try {\n const payload = await readApiResultOrThrow<{ items?: PriceKindApiPayload[] }>(\n '/api/catalog/price-kinds?pageSize=100',\n undefined,\n { errorMessage: t('catalog.priceKinds.errors.load', 'Failed to load price kinds.') },\n )\n const items = Array.isArray(payload.items) ? payload.items : []\n setPriceKinds(items.map((item) => normalizePriceKindSummary(item)).filter((item): item is PriceKindSummary => !!item))\n } catch (err) {\n logger.error('catalog.price-kinds.fetch failed', { err })\n setPriceKinds([])\n }\n }\n loadPriceKinds().catch(() => {})\n }, [t])\n\n React.useEffect(() => {\n const loadTaxRates = async () => {\n try {\n const payload = await readApiResultOrThrow<{ items?: Array<Record<string, unknown>> }>(\n '/api/sales/tax-rates?pageSize=200',\n undefined,\n { errorMessage: t('catalog.products.create.taxRates.error', 'Failed to load tax rates.'), fallback: { items: [] } },\n )\n const items = Array.isArray(payload.items) ? payload.items : []\n setTaxRates(\n items.map((item) => {\n const rawRate = typeof item.rate === 'number' ? item.rate : Number(item.rate ?? Number.NaN)\n return {\n id: String(item.id),\n name:\n typeof item.name === 'string' && item.name.trim().length\n ? item.name\n : t('catalog.products.create.taxRates.unnamed', 'Untitled tax rate'),\n code: typeof item.code === 'string' && item.code.trim().length ? item.code : null,\n rate: Number.isFinite(rawRate) ? rawRate : null,\n isDefault: Boolean(\n typeof item.isDefault === 'boolean'\n ? item.isDefault\n : typeof item.is_default === 'boolean'\n ? item.is_default\n : false,\n ),\n }\n }),\n )\n } catch (err) {\n logger.error('sales.tax-rates.fetch failed', { err })\n setTaxRates([])\n }\n }\n loadTaxRates().catch(() => {})\n }, [t])\n\n React.useEffect(() => {\n if (!productId) return\n let cancelled = false\n async function load() {\n setLoading(true)\n setError(null)\n setIsNotFound(false)\n try {\n const res = await apiCall<ProductResponse>(\n `/api/catalog/products?id=${encodeURIComponent(productId!)}&page=1&pageSize=1`,\n )\n if (!res.ok) throw new Error(t('catalog.variants.form.errors.load', 'Failed to load product context.'))\n const record = Array.isArray(res.result?.items) ? res.result?.items?.[0] : undefined\n if (!record) {\n if (!cancelled) setIsNotFound(true)\n return\n }\n const metadata = (record.metadata ?? {}) as Record<string, unknown>\n const taxRateId =\n typeof (record as any).tax_rate_id === 'string'\n ? (record as any).tax_rate_id\n : typeof (record as any).taxRateId === 'string'\n ? (record as any).taxRateId\n : null\n const taxRateValueRaw =\n typeof (record as any).tax_rate === 'number'\n ? (record as any).tax_rate\n : typeof (record as any).tax_rate === 'string'\n ? Number((record as any).tax_rate)\n : typeof (record as any).taxRate === 'number'\n ? (record as any).taxRate\n : typeof (record as any).taxRate === 'string'\n ? Number((record as any).taxRate)\n : null\n const taxRateValue = Number.isFinite(taxRateValueRaw) ? Number(taxRateValueRaw) : null\n const optionSchemaId =\n typeof (record as any).option_schema_id === 'string'\n ? (record as any).option_schema_id\n : typeof (record as any).optionSchemaId === 'string'\n ? (record as any).optionSchemaId\n : null\n let schemaSource: unknown = metadata.optionSchema ?? (metadata.option_schema as unknown)\n if (optionSchemaId) {\n const template = await fetchOptionSchemaTemplate(optionSchemaId)\n if (template?.schema?.options) {\n schemaSource = template.schema.options.map((option) => ({\n code: option.code,\n label: option.label,\n values: Array.isArray(option.choices)\n ? option.choices.map((choice) => ({ id: choice.code ?? undefined, label: choice.label ?? choice.code ?? '' }))\n : [],\n }))\n }\n }\n if (!cancelled) {\n setOptionDefinitions(normalizeOptionSchema(schemaSource))\n setProductTitle(typeof record.title === 'string' ? record.title : '')\n setProductTaxRateId(taxRateId)\n setProductTaxRate(taxRateValue)\n const base = createVariantInitialValues()\n setInitialValues(base)\n }\n } catch (err) {\n logger.error('catalog.variants.loadProduct failed', { err })\n if (!cancelled) {\n const message = err instanceof Error && err.message ? err.message : t('catalog.variants.form.errors.load', 'Failed to load product context.')\n setError(message)\n }\n } finally {\n if (!cancelled) setLoading(false)\n }\n }\n load()\n return () => { cancelled = true }\n }, [productId, t])\n\n const groups = React.useMemo<CrudFormGroup[]>(() => {\n const list: CrudFormGroup[] = [\n {\n id: 'general',\n column: 1,\n title: t('catalog.variants.form.general', 'General'),\n component: ({ values, setValue, errors }) => (\n <VariantBasicsSection values={values as VariantFormValues} setValue={setValue} errors={errors} />\n ),\n },\n {\n id: 'metadata',\n column: 1,\n title: t('catalog.products.edit.metadata.title', 'Metadata'),\n description: t('catalog.products.edit.metadata.hint', 'Attach structured key/value pairs for integrations.'),\n component: ({ values, setValue }) => (\n <VariantMetadataSection values={values as VariantFormValues} setValue={setValue} showIntro={false} embedded />\n ),\n },\n {\n id: 'prices',\n column: 1,\n title: t('catalog.variants.form.pricesLabel', 'Prices'),\n description: t('catalog.variants.form.pricesHint', 'Populate list prices per price kind.'),\n component: ({ values, setValue }) => (\n <VariantPricesSection\n values={values as VariantFormValues}\n setValue={setValue}\n priceKinds={priceKinds}\n taxRates={taxRates}\n showHeader={false}\n embedded\n />\n ),\n },\n {\n id: 'media',\n column: 1,\n title: t('catalog.variants.form.media', 'Media'),\n component: ({ values, setValue }) => (\n <VariantMediaSection values={values as VariantFormValues} setValue={setValue} showLabel={false} />\n ),\n },\n ]\n\n if (optionDefinitions.length) {\n list.push({\n id: 'options',\n column: 2,\n title: t('catalog.variants.form.options', 'Option values'),\n component: ({ values, setValue }) => (\n <VariantOptionValuesSection\n values={values as VariantFormValues}\n setValue={setValue}\n optionDefinitions={optionDefinitions}\n showHeading={false}\n />\n ),\n })\n }\n\n list.push({\n id: 'dimensions',\n column: 2,\n title: t('catalog.variants.form.dimensions', 'Dimensions & weight'),\n component: ({ values, setValue }) => (\n <VariantDimensionsSection values={values as VariantFormValues} setValue={setValue} showHeading={false} />\n ),\n })\n\n list.push({\n id: 'custom',\n column: 2,\n title: t('catalog.variants.form.customFields', 'Custom attributes'),\n kind: 'customFields',\n })\n\n return list\n }, [optionDefinitions, priceKinds, t, taxRates])\n\n if (!productId) {\n return (\n <Page>\n <PageBody>\n <ErrorMessage label={t('catalog.variants.form.errors.productMissing', 'Product identifier is missing.')} />\n </PageBody>\n </Page>\n )\n }\n\n if (isNotFound) {\n return (\n <Page>\n <PageBody>\n <RecordNotFoundState\n label={t('catalog.products.edit.errors.notFound', 'Product not found.')}\n backHref=\"/backend/catalog/products\"\n backLabel={t('catalog.products.edit.actions.backToList', 'Back to products')}\n />\n </PageBody>\n </Page>\n )\n }\n\n const formTitle = productTitle\n ? t('catalog.variants.form.createTitleFor', 'Create variant for {{title}}').replace('{{title}}', productTitle)\n : t('catalog.variants.form.createTitle', 'Create variant')\n\n return (\n <Page>\n <PageBody>\n {error ? (\n <ErrorMessage label={error} className=\"mb-4\" />\n ) : null}\n <CrudForm<VariantFormValues>\n title={formTitle}\n titleHeadingLevel={1}\n backHref={`/backend/catalog/products/${productId}`}\n fields={[]}\n groups={groups}\n entityId={E.catalog.catalog_product_variant}\n customFieldsetBindings={{ [E.catalog.catalog_product_variant]: { valueKey: 'customFieldsetCode' } }}\n initialValues={initialValues ?? undefined}\n isLoading={loading}\n loadingMessage={t('catalog.variants.form.loading', 'Loading form...')}\n submitLabel={t('catalog.variants.form.createAction', 'Create variant')}\n cancelHref={`/backend/catalog/products/${productId}`}\n onSubmit={async (values) => {\n const name = values.name?.trim()\n if (!name) {\n const message = t('catalog.variants.form.errors.nameRequired', 'Provide the variant name.')\n throw createCrudFormError(message, { name: message })\n }\n const invalidPriceKinds = findInvalidVariantPriceKinds(priceKinds, values.prices)\n if (invalidPriceKinds.length) {\n const message = t('catalog.variants.form.errors.invalidPrice', 'Provide a valid non-negative price.')\n throw createCrudFormError(message, { prices: message })\n }\n const resolveTaxRateValue = (taxRateId?: string | null) => {\n if (!taxRateId) return null\n const match = taxRates.find((rate) => rate.id === taxRateId)\n return typeof match?.rate === 'number' && Number.isFinite(match.rate) ? match.rate : null\n }\n const resolvedTaxRateId = values.taxRateId ?? productTaxRateId ?? null\n const resolvedTaxRateValue =\n values.taxRateId && resolvedTaxRateId\n ? resolveTaxRateValue(resolvedTaxRateId)\n : productTaxRateId\n ? resolveTaxRateValue(productTaxRateId) ?? productTaxRate\n : productTaxRate ?? null\n const metadata = typeof values.metadata === 'object' && values.metadata ? { ...values.metadata } : {}\n const defaultMediaEntry = values.defaultMediaId\n ? (Array.isArray(values.mediaItems) ? values.mediaItems : []).find((item) => item.id === values.defaultMediaId)\n : null\n const defaultMediaUrl = defaultMediaEntry\n ? buildAttachmentImageUrl(defaultMediaEntry.id, {\n slug: slugifyAttachmentFileName(defaultMediaEntry.fileName),\n })\n : null\n const payload: Record<string, unknown> = {\n productId,\n name,\n sku: values.sku?.trim() || undefined,\n barcode: values.barcode?.trim() || undefined,\n gtinType: values.gtinType ?? null,\n hsCode: values.hsCode?.trim() || null,\n isDefault: Boolean(values.isDefault),\n isActive: values.isActive !== false,\n optionValues: Object.keys(values.optionValues ?? {}).length ? values.optionValues : undefined,\n metadata,\n defaultMediaId: values.defaultMediaId ?? undefined,\n defaultMediaUrl: defaultMediaUrl ?? undefined,\n customFieldsetCode: values.customFieldsetCode?.trim().length ? values.customFieldsetCode : undefined,\n taxRateId: resolvedTaxRateId,\n taxRate: resolvedTaxRateValue,\n }\n // CrudForm injects a sentinel `id` (\"create\") while the record is new; never send it to the API.\n Reflect.deleteProperty(payload, 'id')\n const customFields = collectCustomFieldValues(values)\n if (Object.keys(customFields).length) payload.customFields = customFields\n\n const { result } = await createCrud<VariantCreateResult>('catalog/variants', payload)\n const variantId = result?.variantId ?? result?.id\n if (!variantId) {\n throw createCrudFormError(t('catalog.variants.form.errors.idMissing', 'Variant id missing after create.'))\n }\n await transferVariantMedia({\n draftId: values.mediaDraftId,\n variantId,\n mediaItems: Array.isArray(values.mediaItems) ? values.mediaItems : [],\n })\n await syncVariantPrices({\n priceKinds,\n priceDrafts: values.prices ?? {},\n productId,\n variantId,\n taxRates,\n taxRateId: values.taxRateId,\n productTaxRateId,\n productTaxRate,\n })\n flash(t('catalog.variants.form.createSuccess', 'Variant created.'), 'success')\n router.push(`/backend/catalog/products/${productId}#variants`)\n }}\n />\n </PageBody>\n </Page>\n )\n}\n\nasync function transferVariantMedia({\n draftId,\n variantId,\n mediaItems,\n}: {\n draftId?: string | null\n variantId: string\n mediaItems: ProductMediaItem[]\n}): Promise<void> {\n if (!draftId || !variantId) return\n const attachmentIds = mediaItems.map((item) => item.id).filter((id): id is string => typeof id === 'string' && id.length > 0)\n if (!attachmentIds.length) return\n await apiCall(\n '/api/attachments/transfer',\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n entityId: E.catalog.catalog_product_variant,\n attachmentIds,\n fromRecordId: draftId,\n toRecordId: variantId,\n }),\n },\n { fallback: null },\n )\n}\n\nasync function syncVariantPrices({\n priceKinds,\n priceDrafts,\n productId,\n variantId,\n taxRates,\n taxRateId,\n productTaxRateId,\n productTaxRate,\n}: {\n priceKinds: PriceKindSummary[]\n priceDrafts: Record<string, VariantPriceDraft>\n productId: string\n variantId: string\n taxRates: TaxRateSummary[]\n taxRateId: string | null\n productTaxRateId?: string | null\n productTaxRate?: number | null\n}): Promise<void> {\n const selectedTaxRate = taxRates.find((rate) => rate.id === taxRateId) ?? null\n const fallbackProductTaxRate =\n !selectedTaxRate && productTaxRateId\n ? taxRates.find((rate) => rate.id === productTaxRateId) ?? null\n : null\n const resolvedTaxRateValue =\n selectedTaxRate?.rate ??\n fallbackProductTaxRate?.rate ??\n (Number.isFinite(productTaxRate ?? null) ? productTaxRate ?? null : null)\n const resolvedTaxRateId = (selectedTaxRate ?? fallbackProductTaxRate)?.id ?? null\n for (const kind of priceKinds) {\n const draft = priceDrafts?.[kind.id]\n const amount = typeof draft?.amount === 'string' ? draft.amount.trim() : ''\n if (!amount) continue\n const numeric = parseNumericInput(amount)\n if (!Number.isFinite(numeric) || numeric < 0) continue\n const payload: Record<string, unknown> = {\n productId,\n variantId,\n priceKindId: kind.id,\n currencyCode: kind.currencyCode ?? undefined,\n }\n if (resolvedTaxRateId) {\n payload.taxRateId = resolvedTaxRateId\n } else if (typeof resolvedTaxRateValue === 'number' && Number.isFinite(resolvedTaxRateValue)) {\n payload.taxRate = resolvedTaxRateValue\n }\n if (kind.displayMode === 'including-tax') payload.unitPriceGross = numeric\n else payload.unitPriceNet = numeric\n await createCrud('catalog/prices', payload)\n }\n}\n"],
|
|
5
|
-
"mappings": ";AAwNU,cAuGJ,YAvGI;AAtNV,YAAY,WAAW;AACvB,SAAS,iBAAiB;AAC1B,SAAS,MAAM,gBAAgB;AAC/B,SAAS,gBAAoC;AAC7C,SAAS,kBAAkB;AAC3B,SAAS,2BAA2B;AACpC,SAAS,gCAAgC;AACzC,SAAS,SAAS,4BAA4B;AAC9C,SAAS,aAAa;AACtB,SAAS,cAAc,2BAA2B;AAClD,SAAS,YAAY;AACrB,SAAS,SAAS;AAClB;AAAA,EAIE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EAIE;AAAA,OACK;AACP,SAAS,yBAAyB;AAClC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { CrudForm, type CrudFormGroup } from '@open-mercato/ui/backend/CrudForm'\nimport { createCrud } from '@open-mercato/ui/backend/utils/crud'\nimport { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { collectCustomFieldValues } from '@open-mercato/ui/backend/utils/customFieldValues'\nimport { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { E } from '#generated/entities.ids.generated'\nimport {\n type VariantFormValues,\n type VariantPriceDraft,\n type OptionDefinition,\n createVariantInitialValues,\n normalizeOptionSchema,\n findInvalidVariantPriceKinds,\n} from '@open-mercato/core/modules/catalog/components/products/variantForm'\nimport {\n type PriceKindSummary,\n type PriceKindApiPayload,\n type TaxRateSummary,\n normalizePriceKindSummary,\n} from '@open-mercato/core/modules/catalog/components/products/productForm'\nimport { parseNumericInput } from '@open-mercato/core/modules/catalog/components/products/productFormUtils'\nimport {\n VariantBasicsSection,\n VariantOptionValuesSection,\n VariantDimensionsSection,\n VariantMetadataSection,\n VariantPricesSection,\n VariantMediaSection,\n} from '@open-mercato/core/modules/catalog/components/products/VariantBuilder'\nimport { buildAttachmentImageUrl, slugifyAttachmentFileName } from '@open-mercato/core/modules/attachments/lib/imageUrls'\nimport { fetchOptionSchemaTemplate } from '../../../optionSchemaClient'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { transferDraftMedia } from '@open-mercato/core/modules/catalog/components/products/mediaTransfer'\n\nconst logger = createLogger('catalog')\n\ntype ProductResponse = {\n items?: Array<{\n id?: string\n title?: string | null\n metadata?: Record<string, unknown> | null\n custom_fieldset_code?: string | null\n customFieldsetCode?: string | null\n tax_rate_id?: string | null\n taxRateId?: string | null\n tax_rate?: number | string | null\n }>\n}\n\ntype VariantCreateResult = {\n id?: string\n variantId?: string\n}\n\nexport default function CreateVariantPage({ params }: { params?: { productId?: string } }) {\n const productId = params?.productId ? String(params.productId) : null\n const t = useT()\n const router = useRouter()\n const [optionDefinitions, setOptionDefinitions] = React.useState<OptionDefinition[]>([])\n const [priceKinds, setPriceKinds] = React.useState<PriceKindSummary[]>([])\n const [taxRates, setTaxRates] = React.useState<TaxRateSummary[]>([])\n const [initialValues, setInitialValues] = React.useState<VariantFormValues | null>(null)\n const [productTitle, setProductTitle] = React.useState<string>('')\n const [productTaxRateId, setProductTaxRateId] = React.useState<string | null>(null)\n const [productTaxRate, setProductTaxRate] = React.useState<number | null>(null)\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [isNotFound, setIsNotFound] = React.useState(false)\n\n React.useEffect(() => {\n const loadPriceKinds = async () => {\n try {\n const payload = await readApiResultOrThrow<{ items?: PriceKindApiPayload[] }>(\n '/api/catalog/price-kinds?pageSize=100',\n undefined,\n { errorMessage: t('catalog.priceKinds.errors.load', 'Failed to load price kinds.') },\n )\n const items = Array.isArray(payload.items) ? payload.items : []\n setPriceKinds(items.map((item) => normalizePriceKindSummary(item)).filter((item): item is PriceKindSummary => !!item))\n } catch (err) {\n logger.error('catalog.price-kinds.fetch failed', { err })\n setPriceKinds([])\n }\n }\n loadPriceKinds().catch(() => {})\n }, [t])\n\n React.useEffect(() => {\n const loadTaxRates = async () => {\n try {\n const payload = await readApiResultOrThrow<{ items?: Array<Record<string, unknown>> }>(\n '/api/sales/tax-rates?pageSize=200',\n undefined,\n { errorMessage: t('catalog.products.create.taxRates.error', 'Failed to load tax rates.'), fallback: { items: [] } },\n )\n const items = Array.isArray(payload.items) ? payload.items : []\n setTaxRates(\n items.map((item) => {\n const rawRate = typeof item.rate === 'number' ? item.rate : Number(item.rate ?? Number.NaN)\n return {\n id: String(item.id),\n name:\n typeof item.name === 'string' && item.name.trim().length\n ? item.name\n : t('catalog.products.create.taxRates.unnamed', 'Untitled tax rate'),\n code: typeof item.code === 'string' && item.code.trim().length ? item.code : null,\n rate: Number.isFinite(rawRate) ? rawRate : null,\n isDefault: Boolean(\n typeof item.isDefault === 'boolean'\n ? item.isDefault\n : typeof item.is_default === 'boolean'\n ? item.is_default\n : false,\n ),\n }\n }),\n )\n } catch (err) {\n logger.error('sales.tax-rates.fetch failed', { err })\n setTaxRates([])\n }\n }\n loadTaxRates().catch(() => {})\n }, [t])\n\n React.useEffect(() => {\n if (!productId) return\n let cancelled = false\n async function load() {\n setLoading(true)\n setError(null)\n setIsNotFound(false)\n try {\n const res = await apiCall<ProductResponse>(\n `/api/catalog/products?id=${encodeURIComponent(productId!)}&page=1&pageSize=1`,\n )\n if (!res.ok) throw new Error(t('catalog.variants.form.errors.load', 'Failed to load product context.'))\n const record = Array.isArray(res.result?.items) ? res.result?.items?.[0] : undefined\n if (!record) {\n if (!cancelled) setIsNotFound(true)\n return\n }\n const metadata = (record.metadata ?? {}) as Record<string, unknown>\n const taxRateId =\n typeof (record as any).tax_rate_id === 'string'\n ? (record as any).tax_rate_id\n : typeof (record as any).taxRateId === 'string'\n ? (record as any).taxRateId\n : null\n const taxRateValueRaw =\n typeof (record as any).tax_rate === 'number'\n ? (record as any).tax_rate\n : typeof (record as any).tax_rate === 'string'\n ? Number((record as any).tax_rate)\n : typeof (record as any).taxRate === 'number'\n ? (record as any).taxRate\n : typeof (record as any).taxRate === 'string'\n ? Number((record as any).taxRate)\n : null\n const taxRateValue = Number.isFinite(taxRateValueRaw) ? Number(taxRateValueRaw) : null\n const optionSchemaId =\n typeof (record as any).option_schema_id === 'string'\n ? (record as any).option_schema_id\n : typeof (record as any).optionSchemaId === 'string'\n ? (record as any).optionSchemaId\n : null\n let schemaSource: unknown = metadata.optionSchema ?? (metadata.option_schema as unknown)\n if (optionSchemaId) {\n const template = await fetchOptionSchemaTemplate(optionSchemaId)\n if (template?.schema?.options) {\n schemaSource = template.schema.options.map((option) => ({\n code: option.code,\n label: option.label,\n values: Array.isArray(option.choices)\n ? option.choices.map((choice) => ({ id: choice.code ?? undefined, label: choice.label ?? choice.code ?? '' }))\n : [],\n }))\n }\n }\n if (!cancelled) {\n setOptionDefinitions(normalizeOptionSchema(schemaSource))\n setProductTitle(typeof record.title === 'string' ? record.title : '')\n setProductTaxRateId(taxRateId)\n setProductTaxRate(taxRateValue)\n const base = createVariantInitialValues()\n setInitialValues(base)\n }\n } catch (err) {\n logger.error('catalog.variants.loadProduct failed', { err })\n if (!cancelled) {\n const message = err instanceof Error && err.message ? err.message : t('catalog.variants.form.errors.load', 'Failed to load product context.')\n setError(message)\n }\n } finally {\n if (!cancelled) setLoading(false)\n }\n }\n load()\n return () => { cancelled = true }\n }, [productId, t])\n\n const groups = React.useMemo<CrudFormGroup[]>(() => {\n const list: CrudFormGroup[] = [\n {\n id: 'general',\n column: 1,\n title: t('catalog.variants.form.general', 'General'),\n component: ({ values, setValue, errors }) => (\n <VariantBasicsSection values={values as VariantFormValues} setValue={setValue} errors={errors} />\n ),\n },\n {\n id: 'metadata',\n column: 1,\n title: t('catalog.products.edit.metadata.title', 'Metadata'),\n description: t('catalog.products.edit.metadata.hint', 'Attach structured key/value pairs for integrations.'),\n component: ({ values, setValue }) => (\n <VariantMetadataSection values={values as VariantFormValues} setValue={setValue} showIntro={false} embedded />\n ),\n },\n {\n id: 'prices',\n column: 1,\n title: t('catalog.variants.form.pricesLabel', 'Prices'),\n description: t('catalog.variants.form.pricesHint', 'Populate list prices per price kind.'),\n component: ({ values, setValue }) => (\n <VariantPricesSection\n values={values as VariantFormValues}\n setValue={setValue}\n priceKinds={priceKinds}\n taxRates={taxRates}\n showHeader={false}\n embedded\n />\n ),\n },\n {\n id: 'media',\n column: 1,\n title: t('catalog.variants.form.media', 'Media'),\n component: ({ values, setValue }) => (\n <VariantMediaSection values={values as VariantFormValues} setValue={setValue} showLabel={false} />\n ),\n },\n ]\n\n if (optionDefinitions.length) {\n list.push({\n id: 'options',\n column: 2,\n title: t('catalog.variants.form.options', 'Option values'),\n component: ({ values, setValue }) => (\n <VariantOptionValuesSection\n values={values as VariantFormValues}\n setValue={setValue}\n optionDefinitions={optionDefinitions}\n showHeading={false}\n />\n ),\n })\n }\n\n list.push({\n id: 'dimensions',\n column: 2,\n title: t('catalog.variants.form.dimensions', 'Dimensions & weight'),\n component: ({ values, setValue }) => (\n <VariantDimensionsSection values={values as VariantFormValues} setValue={setValue} showHeading={false} />\n ),\n })\n\n list.push({\n id: 'custom',\n column: 2,\n title: t('catalog.variants.form.customFields', 'Custom attributes'),\n kind: 'customFields',\n })\n\n return list\n }, [optionDefinitions, priceKinds, t, taxRates])\n\n if (!productId) {\n return (\n <Page>\n <PageBody>\n <ErrorMessage label={t('catalog.variants.form.errors.productMissing', 'Product identifier is missing.')} />\n </PageBody>\n </Page>\n )\n }\n\n if (isNotFound) {\n return (\n <Page>\n <PageBody>\n <RecordNotFoundState\n label={t('catalog.products.edit.errors.notFound', 'Product not found.')}\n backHref=\"/backend/catalog/products\"\n backLabel={t('catalog.products.edit.actions.backToList', 'Back to products')}\n />\n </PageBody>\n </Page>\n )\n }\n\n const formTitle = productTitle\n ? t('catalog.variants.form.createTitleFor', 'Create variant for {{title}}').replace('{{title}}', productTitle)\n : t('catalog.variants.form.createTitle', 'Create variant')\n\n return (\n <Page>\n <PageBody>\n {error ? (\n <ErrorMessage label={error} className=\"mb-4\" />\n ) : null}\n <CrudForm<VariantFormValues>\n title={formTitle}\n titleHeadingLevel={1}\n backHref={`/backend/catalog/products/${productId}`}\n fields={[]}\n groups={groups}\n entityId={E.catalog.catalog_product_variant}\n customFieldsetBindings={{ [E.catalog.catalog_product_variant]: { valueKey: 'customFieldsetCode' } }}\n initialValues={initialValues ?? undefined}\n isLoading={loading}\n loadingMessage={t('catalog.variants.form.loading', 'Loading form...')}\n submitLabel={t('catalog.variants.form.createAction', 'Create variant')}\n cancelHref={`/backend/catalog/products/${productId}`}\n onSubmit={async (values) => {\n const name = values.name?.trim()\n if (!name) {\n const message = t('catalog.variants.form.errors.nameRequired', 'Provide the variant name.')\n throw createCrudFormError(message, { name: message })\n }\n const invalidPriceKinds = findInvalidVariantPriceKinds(priceKinds, values.prices)\n if (invalidPriceKinds.length) {\n const message = t('catalog.variants.form.errors.invalidPrice', 'Provide a valid non-negative price.')\n throw createCrudFormError(message, { prices: message })\n }\n const resolveTaxRateValue = (taxRateId?: string | null) => {\n if (!taxRateId) return null\n const match = taxRates.find((rate) => rate.id === taxRateId)\n return typeof match?.rate === 'number' && Number.isFinite(match.rate) ? match.rate : null\n }\n const resolvedTaxRateId = values.taxRateId ?? productTaxRateId ?? null\n const resolvedTaxRateValue =\n values.taxRateId && resolvedTaxRateId\n ? resolveTaxRateValue(resolvedTaxRateId)\n : productTaxRateId\n ? resolveTaxRateValue(productTaxRateId) ?? productTaxRate\n : productTaxRate ?? null\n const metadata = typeof values.metadata === 'object' && values.metadata ? { ...values.metadata } : {}\n const defaultMediaEntry = values.defaultMediaId\n ? (Array.isArray(values.mediaItems) ? values.mediaItems : []).find((item) => item.id === values.defaultMediaId)\n : null\n const defaultMediaUrl = defaultMediaEntry\n ? buildAttachmentImageUrl(defaultMediaEntry.id, {\n slug: slugifyAttachmentFileName(defaultMediaEntry.fileName),\n })\n : null\n const payload: Record<string, unknown> = {\n productId,\n name,\n sku: values.sku?.trim() || undefined,\n barcode: values.barcode?.trim() || undefined,\n gtinType: values.gtinType ?? null,\n hsCode: values.hsCode?.trim() || null,\n isDefault: Boolean(values.isDefault),\n isActive: values.isActive !== false,\n optionValues: Object.keys(values.optionValues ?? {}).length ? values.optionValues : undefined,\n metadata,\n defaultMediaId: values.defaultMediaId ?? undefined,\n defaultMediaUrl: defaultMediaUrl ?? undefined,\n customFieldsetCode: values.customFieldsetCode?.trim().length ? values.customFieldsetCode : undefined,\n taxRateId: resolvedTaxRateId,\n taxRate: resolvedTaxRateValue,\n }\n // CrudForm injects a sentinel `id` (\"create\") while the record is new; never send it to the API.\n Reflect.deleteProperty(payload, 'id')\n const customFields = collectCustomFieldValues(values)\n if (Object.keys(customFields).length) payload.customFields = customFields\n\n const { result } = await createCrud<VariantCreateResult>('catalog/variants', payload)\n const variantId = result?.variantId ?? result?.id\n if (!variantId) {\n throw createCrudFormError(t('catalog.variants.form.errors.idMissing', 'Variant id missing after create.'))\n }\n const mediaTransfer = await transferDraftMedia({\n entityId: E.catalog.catalog_product_variant,\n draftId: values.mediaDraftId,\n recordId: variantId,\n mediaItems: Array.isArray(values.mediaItems) ? values.mediaItems : [],\n })\n if (mediaTransfer.failedCount > 0) {\n logger.error('attachments.transfer.failed', {\n failed: mediaTransfer.failedCount,\n requested: mediaTransfer.requestedCount,\n errors: mediaTransfer.errors,\n })\n }\n await syncVariantPrices({\n priceKinds,\n priceDrafts: values.prices ?? {},\n productId,\n variantId,\n taxRates,\n taxRateId: values.taxRateId,\n productTaxRateId,\n productTaxRate,\n })\n if (mediaTransfer.failedCount > 0) {\n flash(\n t(\n 'catalog.variants.form.mediaTransferIncomplete',\n 'Variant created, but {failed} of {total} media files could not be attached.',\n { failed: mediaTransfer.failedCount, total: mediaTransfer.requestedCount },\n ),\n 'warning',\n )\n } else {\n flash(t('catalog.variants.form.createSuccess', 'Variant created.'), 'success')\n }\n router.push(`/backend/catalog/products/${productId}#variants`)\n }}\n />\n </PageBody>\n </Page>\n )\n}\n\nasync function syncVariantPrices({\n priceKinds,\n priceDrafts,\n productId,\n variantId,\n taxRates,\n taxRateId,\n productTaxRateId,\n productTaxRate,\n}: {\n priceKinds: PriceKindSummary[]\n priceDrafts: Record<string, VariantPriceDraft>\n productId: string\n variantId: string\n taxRates: TaxRateSummary[]\n taxRateId: string | null\n productTaxRateId?: string | null\n productTaxRate?: number | null\n}): Promise<void> {\n const selectedTaxRate = taxRates.find((rate) => rate.id === taxRateId) ?? null\n const fallbackProductTaxRate =\n !selectedTaxRate && productTaxRateId\n ? taxRates.find((rate) => rate.id === productTaxRateId) ?? null\n : null\n const resolvedTaxRateValue =\n selectedTaxRate?.rate ??\n fallbackProductTaxRate?.rate ??\n (Number.isFinite(productTaxRate ?? null) ? productTaxRate ?? null : null)\n const resolvedTaxRateId = (selectedTaxRate ?? fallbackProductTaxRate)?.id ?? null\n for (const kind of priceKinds) {\n const draft = priceDrafts?.[kind.id]\n const amount = typeof draft?.amount === 'string' ? draft.amount.trim() : ''\n if (!amount) continue\n const numeric = parseNumericInput(amount)\n if (!Number.isFinite(numeric) || numeric < 0) continue\n const payload: Record<string, unknown> = {\n productId,\n variantId,\n priceKindId: kind.id,\n currencyCode: kind.currencyCode ?? undefined,\n }\n if (resolvedTaxRateId) {\n payload.taxRateId = resolvedTaxRateId\n } else if (typeof resolvedTaxRateValue === 'number' && Number.isFinite(resolvedTaxRateValue)) {\n payload.taxRate = resolvedTaxRateValue\n }\n if (kind.displayMode === 'including-tax') payload.unitPriceGross = numeric\n else payload.unitPriceNet = numeric\n await createCrud('catalog/prices', payload)\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -82,6 +82,7 @@ import {
|
|
|
82
82
|
normalizeProductConversionInputs
|
|
83
83
|
} from "@open-mercato/core/modules/catalog/components/products/productFormUtils";
|
|
84
84
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
85
|
+
import { transferDraftMedia } from "@open-mercato/core/modules/catalog/components/products/mediaTransfer";
|
|
85
86
|
const logger = createLogger("catalog");
|
|
86
87
|
const TRUE_BOOLEAN_VALUES = /* @__PURE__ */ new Set(["true", "1", "yes", "y", "t"]);
|
|
87
88
|
const matchField = (fieldId) => (value) => value === fieldId || value.startsWith(`${fieldId}.`) || value.startsWith(`${fieldId}[`);
|
|
@@ -342,7 +343,6 @@ function CreateCatalogProductPage() {
|
|
|
342
343
|
const description = formValues.description?.trim() || void 0;
|
|
343
344
|
const defaultMediaId = typeof formValues.defaultMediaId === "string" && formValues.defaultMediaId.trim().length ? formValues.defaultMediaId : null;
|
|
344
345
|
const mediaItems = Array.isArray(formValues.mediaItems) ? formValues.mediaItems : [];
|
|
345
|
-
const attachmentIds = mediaItems.map((item) => typeof item.id === "string" ? item.id : null).filter((value) => !!value);
|
|
346
346
|
const mediaDraftId = typeof formValues.mediaDraftId === "string" ? formValues.mediaDraftId : "";
|
|
347
347
|
const defaultMediaEntry = defaultMediaId ? mediaItems.find((item) => item.id === defaultMediaId) : null;
|
|
348
348
|
const defaultMediaUrl = defaultMediaEntry ? buildAttachmentImageUrl(defaultMediaEntry.id, {
|
|
@@ -618,24 +618,18 @@ function CreateCatalogProductPage() {
|
|
|
618
618
|
}
|
|
619
619
|
await createCrud("catalog/prices", pricePayload);
|
|
620
620
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
},
|
|
634
|
-
{ fallback: null }
|
|
635
|
-
);
|
|
636
|
-
if (!transfer.ok) {
|
|
637
|
-
logger.error("attachments.transfer.failed", { err: transfer.result?.error });
|
|
638
|
-
}
|
|
621
|
+
const mediaTransfer = await transferDraftMedia({
|
|
622
|
+
entityId: E.catalog.catalog_product,
|
|
623
|
+
draftId: mediaDraftId,
|
|
624
|
+
recordId: productId,
|
|
625
|
+
mediaItems
|
|
626
|
+
});
|
|
627
|
+
if (mediaTransfer.failedCount > 0) {
|
|
628
|
+
logger.error("attachments.transfer.failed", {
|
|
629
|
+
failed: mediaTransfer.failedCount,
|
|
630
|
+
requested: mediaTransfer.requestedCount,
|
|
631
|
+
errors: mediaTransfer.errors
|
|
632
|
+
});
|
|
639
633
|
}
|
|
640
634
|
if (inboxDraft) {
|
|
641
635
|
try {
|
|
@@ -663,10 +657,24 @@ function CreateCatalogProductPage() {
|
|
|
663
657
|
);
|
|
664
658
|
}
|
|
665
659
|
}
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
660
|
+
if (mediaTransfer.failedCount > 0) {
|
|
661
|
+
flash(
|
|
662
|
+
t(
|
|
663
|
+
"catalog.products.create.mediaTransferIncomplete",
|
|
664
|
+
"Product created, but {failed} of {total} media files could not be attached.",
|
|
665
|
+
{
|
|
666
|
+
failed: mediaTransfer.failedCount,
|
|
667
|
+
total: mediaTransfer.requestedCount
|
|
668
|
+
}
|
|
669
|
+
),
|
|
670
|
+
"warning"
|
|
671
|
+
);
|
|
672
|
+
} else {
|
|
673
|
+
flash(
|
|
674
|
+
t("catalog.products.create.success", "Product created."),
|
|
675
|
+
"success"
|
|
676
|
+
);
|
|
677
|
+
}
|
|
670
678
|
if (inboxDraft) {
|
|
671
679
|
router.push(
|
|
672
680
|
`/backend/inbox-ops/proposals/${encodeURIComponent(inboxDraft.proposalId)}`
|