@open-mercato/core 0.4.6-develop-15c18897fc → 0.4.6-develop-34aa847ce6

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../../../src/modules/catalog/backend/catalog/products/%5BproductId%5D/variants/%5BvariantId%5D/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, updateCrud, deleteCrud } 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 { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { E } from '#generated/entities.ids.generated'\nimport { SendObjectMessageDialog } from '@open-mercato/ui/backend/messages'\nimport {\n type VariantFormValues,\n type VariantPriceDraft,\n type OptionDefinition,\n createVariantInitialValues,\n normalizeOptionSchema,\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 {\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 CreateVariantPage from '../create/page'\n\ntype VariantResponse = {\n items?: Array<Record<string, unknown>>\n}\n\ntype ProductResponse = {\n items?: Array<{\n id?: string\n title?: string | null\n metadata?: Record<string, unknown> | null\n tax_rate_id?: string | null\n taxRateId?: string | null\n tax_rate?: number | string | null\n taxRate?: number | string | null\n }>\n}\n\ntype PriceListResponse = {\n items?: Array<Record<string, unknown>>\n}\n\ntype AttachmentListResponse = {\n items?: ProductMediaItem[]\n}\n\nfunction resolveVariantPriceLabel(prices: Record<string, VariantPriceDraft> | undefined): string | null {\n if (!prices || typeof prices !== 'object') return null\n const entries = Object.values(prices)\n for (const entry of entries) {\n const amount = typeof entry?.amount === 'string' ? entry.amount.trim() : ''\n if (!amount) continue\n const currencyCode =\n typeof entry.currencyCode === 'string' && entry.currencyCode.trim().length\n ? entry.currencyCode.trim().toUpperCase()\n : null\n return currencyCode ? `${currencyCode} ${amount}` : amount\n }\n return null\n}\n\nexport default function EditVariantPage({ params }: { params?: { productId?: string; variantId?: string } }) {\n const router = useRouter()\n const t = useT()\n const productId = params?.productId ? String(params.productId) : null\n const variantId = params?.variantId ? String(params.variantId) : null\n const isCreateSentinel = variantId === 'create'\n const [priceKinds, setPriceKinds] = React.useState<PriceKindSummary[]>([])\n const [taxRates, setTaxRates] = React.useState<TaxRateSummary[]>([])\n const [optionDefinitions, setOptionDefinitions] = React.useState<OptionDefinition[]>([])\n const [initialValues, setInitialValues] = React.useState<VariantFormValues | null>(null)\n const [existingPriceIds, setExistingPriceIds] = React.useState<Record<string, string>>({})\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [currentProductId, setCurrentProductId] = React.useState<string | null>(productId)\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\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 console.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 console.error('sales.tax-rates.fetch failed', err)\n setTaxRates([])\n }\n }\n loadTaxRates().catch(() => {})\n }, [t])\n\n React.useEffect(() => {\n if (!variantId || isCreateSentinel) return\n let cancelled = false\n async function load() {\n setLoading(true)\n setError(null)\n try {\n const variantRes = await apiCall<VariantResponse>(\n `/api/catalog/variants?id=${encodeURIComponent(variantId!)}&page=1&pageSize=1`,\n )\n if (!variantRes.ok) throw new Error('load_variant_failed')\n const record = Array.isArray(variantRes.result?.items) ? variantRes.result?.items?.[0] : undefined\n if (!record) throw new Error(t('catalog.variants.form.errors.notFound', 'Variant not found.'))\n const resolvedProductId =\n typeof record.product_id === 'string'\n ? record.product_id\n : typeof record.productId === 'string'\n ? record.productId\n : currentProductId\n if (resolvedProductId) setCurrentProductId(resolvedProductId)\n const metadata = typeof record.metadata === 'object' && record.metadata ? { ...(record.metadata as Record<string, unknown>) } : {}\n const attachments = await fetchVariantAttachments(variantId!)\n const priceDrafts = await loadVariantPrices(variantId!)\n const priceIdMap: Record<string, string> = {}\n Object.entries(priceDrafts).forEach(([kindId, draft]) => {\n if (draft.priceId) priceIdMap[kindId] = draft.priceId\n })\n setExistingPriceIds(priceIdMap)\n const customDefaults = extractCustomFieldValues(record)\n let loadedOptionDefinitions: OptionDefinition[] = []\n if (resolvedProductId) {\n const productRes = await apiCall<ProductResponse>(\n `/api/catalog/products?id=${encodeURIComponent(resolvedProductId)}&page=1&pageSize=1`,\n )\n if (productRes.ok) {\n const product = Array.isArray(productRes.result?.items) ? productRes.result?.items?.[0] : undefined\n if (product) {\n setProductTitle(typeof product.title === 'string' ? product.title : '')\n const taxRateId =\n typeof (product as any).tax_rate_id === 'string'\n ? (product as any).tax_rate_id\n : typeof (product as any).taxRateId === 'string'\n ? (product as any).taxRateId\n : null\n const taxRateValueRaw =\n typeof (product as any).tax_rate === 'number'\n ? (product as any).tax_rate\n : typeof (product as any).tax_rate === 'string'\n ? Number((product as any).tax_rate)\n : typeof (product as any).taxRate === 'number'\n ? (product as any).taxRate\n : typeof (product as any).taxRate === 'string'\n ? Number((product as any).taxRate)\n : null\n const taxRateValue = Number.isFinite(taxRateValueRaw) ? Number(taxRateValueRaw) : null\n setProductTaxRateId(taxRateId)\n setProductTaxRate(taxRateValue)\n const productMetadata = (product.metadata ?? {}) as Record<string, unknown>\n const optionSchemaId =\n typeof (product as any).option_schema_id === 'string'\n ? (product as any).option_schema_id\n : typeof (product as any).optionSchemaId === 'string'\n ? (product as any).optionSchemaId\n : null\n let schemaSource: unknown =\n productMetadata.optionSchema ?? (productMetadata.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) => ({\n id: choice.code ?? undefined,\n label: choice.label ?? choice.code ?? '',\n }))\n : [],\n }))\n }\n }\n loadedOptionDefinitions = normalizeOptionSchema(schemaSource)\n setOptionDefinitions(loadedOptionDefinitions)\n }\n }\n }\n if (!cancelled) {\n const optionValues =\n typeof record.option_values === 'object' && record.option_values\n ? { ...(record.option_values as Record<string, string>) }\n : typeof record.optionValues === 'object' && record.optionValues\n ? { ...(record.optionValues as Record<string, string>) }\n : {}\n const normalizedOptionValues = reconcileOptionValues(optionValues, loadedOptionDefinitions)\n const defaultMediaId =\n typeof record.default_media_id === 'string'\n ? record.default_media_id\n : typeof record.defaultMediaId === 'string'\n ? record.defaultMediaId\n : attachments[0]?.id ?? null\n const defaultMediaUrl =\n typeof record.default_media_url === 'string'\n ? record.default_media_url\n : typeof record.defaultMediaUrl === 'string'\n ? record.defaultMediaUrl\n : ''\n const base = createVariantInitialValues()\n setInitialValues({\n ...base,\n mediaDraftId: variantId!,\n name: typeof record.name === 'string' ? record.name : '',\n sku: typeof record.sku === 'string' ? record.sku : '',\n barcode: typeof record.barcode === 'string' ? record.barcode : '',\n isDefault: record.is_default === true || record.isDefault === true,\n isActive: record.is_active !== false && record.isActive !== false,\n optionValues: normalizedOptionValues,\n metadata,\n mediaItems: attachments,\n defaultMediaId,\n defaultMediaUrl,\n prices: priceDrafts,\n 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 customFieldsetCode:\n typeof record.custom_fieldset_code === 'string'\n ? record.custom_fieldset_code\n : typeof record.customFieldsetCode === 'string'\n ? record.customFieldsetCode\n : null,\n ...customDefaults,\n })\n }\n } catch (err) {\n console.error('catalog.variants.load.failed', err)\n if (!cancelled) {\n const message = err instanceof Error && err.message ? err.message : t('catalog.variants.form.errors.load', 'Failed to load variant.')\n setError(message)\n }\n } finally {\n if (!cancelled) setLoading(false)\n }\n }\n load()\n return () => { cancelled = true }\n }, [variantId, t, currentProductId])\n\n const groups = React.useMemo<CrudFormGroup[]>(() => {\n const list: CrudFormGroup[] = [\n {\n id: 'general',\n column: 1,\n title: t('catalog.variants.form.nameLabel', 'Name'),\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 (isCreateSentinel) {\n if (!productId) {\n return (\n <Page>\n <PageBody>\n <div className=\"rounded border border-destructive/40 bg-destructive/10 px-4 py-3 text-sm text-destructive\">\n {t('catalog.variants.form.errors.productMissing', 'Product identifier is missing.')}\n </div>\n </PageBody>\n </Page>\n )\n }\n return <CreateVariantPage params={{ productId }} />\n }\n\n if (!variantId || !currentProductId) {\n return (\n <Page>\n <PageBody>\n <div className=\"rounded border border-destructive/40 bg-destructive/10 px-4 py-3 text-sm text-destructive\">\n {t('catalog.variants.form.errors.variantMissing', 'Variant identifier is missing.')}\n </div>\n </PageBody>\n </Page>\n )\n }\n\n const formTitle = productTitle\n ? t('catalog.variants.form.editTitleFor', 'Edit variant \u2022 {{title}}').replace('{{title}}', productTitle)\n : t('catalog.variants.form.editTitle', 'Edit variant')\n const productVariantsHref = `/backend/catalog/products/${currentProductId}#variants`\n\n return (\n <Page>\n <PageBody>\n {error ? (\n <div className=\"mb-4 rounded border border-destructive/40 bg-destructive/10 px-4 py-3 text-sm text-destructive\">{error}</div>\n ) : null}\n <CrudForm<VariantFormValues>\n title={formTitle}\n backHref={productVariantsHref}\n versionHistory={{ resourceKind: 'catalog.variant', resourceId: variantId ? String(variantId) : '' }}\n extraActions={(\n <SendObjectMessageDialog\n object={{\n entityModule: 'catalog',\n entityType: 'variant',\n entityId: variantId,\n previewData: {\n title:\n (typeof initialValues?.name === 'string' && initialValues.name.trim().length\n ? initialValues.name\n : variantId),\n metadata: {\n [t('catalog.variants.form.skuLabel')]:\n (typeof initialValues?.sku === 'string' && initialValues.sku.trim().length\n ? initialValues.sku\n : '-'),\n [t('catalog.variants.form.pricesLabel')]:\n resolveVariantPriceLabel(initialValues?.prices) ?? '-',\n },\n },\n }}\n viewHref={`/backend/catalog/products/${currentProductId}/variants/${variantId}`}\n />\n )}\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 variant...')}\n submitLabel={t('catalog.variants.form.save', 'Save changes')}\n cancelHref={productVariantsHref}\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 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 id: variantId,\n productId: currentProductId,\n name,\n sku: values.sku?.trim() || undefined,\n barcode: values.barcode?.trim() || undefined,\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 const customFields = collectCustomFieldValues(values)\n if (Object.keys(customFields).length) payload.customFields = customFields\n\n await updateCrud('catalog/variants', payload)\n await syncVariantPricesUpdate({\n priceKinds,\n priceDrafts: values.prices ?? {},\n existingPriceIds,\n productId: currentProductId,\n variantId,\n taxRates,\n taxRateId: values.taxRateId,\n productTaxRateId,\n productTaxRate,\n })\n flash(t('catalog.variants.form.updated', 'Variant updated.'), 'success')\n router.push(productVariantsHref)\n }}\n onDelete={async () => {\n await deleteCrud('catalog/variants', variantId!, {\n errorMessage: t('catalog.variants.form.deleteError', 'Failed to delete variant.'),\n })\n flash(t('catalog.variants.form.deleted', 'Variant deleted.'), 'success')\n router.push(productVariantsHref)\n }}\n deleteRedirect={productVariantsHref}\n />\n </PageBody>\n </Page>\n )\n}\n\nfunction reconcileOptionValues(\n optionValues: Record<string, string>,\n optionDefinitions: OptionDefinition[],\n): Record<string, string> {\n if (!optionValues || !optionDefinitions.length) {\n return optionValues ?? {}\n }\n const remaining = new Map(Object.entries(optionValues))\n const normalized: Record<string, string> = {}\n\n for (const option of optionDefinitions) {\n const code = option.code?.trim()\n if (!code) continue\n if (remaining.has(code)) {\n const value = remaining.get(code)\n if (value !== undefined) {\n normalized[code] = value\n }\n remaining.delete(code)\n continue\n }\n const matchKey = findOptionKeyByValue(remaining, option.values)\n if (matchKey) {\n const value = remaining.get(matchKey)\n if (value !== undefined) {\n normalized[code] = value\n }\n remaining.delete(matchKey)\n }\n }\n\n remaining.forEach((value, key) => {\n if (normalized[key] === undefined) {\n normalized[key] = value\n }\n })\n\n return normalized\n}\n\nfunction findOptionKeyByValue(\n candidates: Map<string, string>,\n optionValues: { id: string; label: string }[],\n): string | null {\n if (!optionValues.length) return null\n const matches: string[] = []\n candidates.forEach((value, key) => {\n if (optionValues.some((entry) => entry.label === value)) {\n matches.push(key)\n }\n })\n return matches.length === 1 ? matches[0] : null\n}\n\nasync function fetchVariantAttachments(variantId: string): Promise<ProductMediaItem[]> {\n try {\n const res = await apiCall<AttachmentListResponse>(\n `/api/attachments?entityId=${encodeURIComponent(E.catalog.catalog_product_variant)}&recordId=${encodeURIComponent(variantId)}`,\n )\n if (!res.ok) return []\n return Array.isArray(res.result?.items) ? res.result?.items ?? [] : []\n } catch (err) {\n console.error('catalog.variants.attachments.load', err)\n return []\n }\n}\n\nasync function loadVariantPrices(variantId: string): Promise<Record<string, VariantPriceDraft>> {\n const drafts: Record<string, VariantPriceDraft> = {}\n const pageSize = 100\n let page = 1\n try {\n while (true) {\n const res = await apiCall<PriceListResponse>(\n `/api/catalog/prices?variantId=${encodeURIComponent(variantId)}&page=${page}&pageSize=${pageSize}`,\n )\n if (!res.ok) break\n const items = Array.isArray(res.result?.items) ? res.result?.items : []\n for (const item of items) {\n const kindId =\n typeof item.price_kind_id === 'string'\n ? item.price_kind_id\n : typeof item.priceKindId === 'string'\n ? item.priceKindId\n : null\n if (!kindId) continue\n const unitNet =\n typeof item.unit_price_net === 'string'\n ? item.unit_price_net\n : typeof item.unitPriceNet === 'string'\n ? item.unitPriceNet\n : null\n const unitGross =\n typeof item.unit_price_gross === 'string'\n ? item.unit_price_gross\n : typeof item.unitPriceGross === 'string'\n ? item.unitPriceGross\n : null\n drafts[kindId] = {\n priceKindId: kindId,\n priceId: typeof item.id === 'string' ? item.id : undefined,\n amount: unitNet ?? unitGross ?? '',\n currencyCode:\n typeof item.currency_code === 'string'\n ? item.currency_code\n : typeof item.currencyCode === 'string'\n ? item.currencyCode\n : null,\n displayMode: unitGross ? 'including-tax' : 'excluding-tax',\n }\n }\n if (items.length < pageSize) break\n page += 1\n }\n } catch (err) {\n console.error('catalog.variants.prices.load', err)\n }\n return drafts\n}\n\nfunction extractCustomFieldValues(record: Record<string, unknown>): Record<string, unknown> {\n const customValues: Record<string, unknown> = {}\n Object.entries(record).forEach(([key, value]) => {\n if (key.startsWith('cf_')) customValues[key] = value\n else if (key.startsWith('cf:')) customValues[`cf_${key.slice(3)}`] = value\n })\n return customValues\n}\n\nasync function syncVariantPricesUpdate({\n priceKinds,\n priceDrafts,\n existingPriceIds,\n productId,\n variantId,\n taxRates,\n taxRateId,\n productTaxRateId,\n productTaxRate,\n}: {\n priceKinds: PriceKindSummary[]\n priceDrafts: Record<string, VariantPriceDraft>\n existingPriceIds: Record<string, string>\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 const existingId = draft?.priceId ?? existingPriceIds[kind.id]\n if (!amount) {\n if (existingId) {\n try {\n await deleteCrud('catalog/prices', existingId)\n } catch (err) {\n console.error('catalog.prices.delete', err)\n }\n }\n continue\n }\n const numeric = Number(amount)\n if (Number.isNaN(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) payload.taxRateId = resolvedTaxRateId\n else if (typeof resolvedTaxRateValue === 'number' && Number.isFinite(resolvedTaxRateValue)) payload.taxRate = resolvedTaxRateValue\n if (kind.displayMode === 'including-tax') payload.unitPriceGross = numeric\n else payload.unitPriceNet = numeric\n if (existingId) {\n await updateCrud('catalog/prices', { id: existingId, ...payload })\n } else {\n await createCrud('catalog/prices', payload)\n }\n }\n}\n"],
5
- "mappings": "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",
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, updateCrud, deleteCrud } 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 { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { E } from '#generated/entities.ids.generated'\nimport { SendObjectMessageDialog } from '@open-mercato/ui/backend/messages'\nimport {\n type VariantFormValues,\n type VariantPriceDraft,\n type OptionDefinition,\n createVariantInitialValues,\n normalizeOptionSchema,\n mapPriceItemToDraft,\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 {\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 CreateVariantPage from '../create/page'\n\ntype VariantResponse = {\n items?: Array<Record<string, unknown>>\n}\n\ntype ProductResponse = {\n items?: Array<{\n id?: string\n title?: string | null\n metadata?: Record<string, unknown> | null\n tax_rate_id?: string | null\n taxRateId?: string | null\n tax_rate?: number | string | null\n taxRate?: number | string | null\n }>\n}\n\ntype PriceListResponse = {\n items?: Array<Record<string, unknown>>\n}\n\ntype AttachmentListResponse = {\n items?: ProductMediaItem[]\n}\n\nfunction resolveVariantPriceLabel(prices: Record<string, VariantPriceDraft> | undefined): string | null {\n if (!prices || typeof prices !== 'object') return null\n const entries = Object.values(prices)\n for (const entry of entries) {\n const amount = typeof entry?.amount === 'string' ? entry.amount.trim() : ''\n if (!amount) continue\n const currencyCode =\n typeof entry.currencyCode === 'string' && entry.currencyCode.trim().length\n ? entry.currencyCode.trim().toUpperCase()\n : null\n return currencyCode ? `${currencyCode} ${amount}` : amount\n }\n return null\n}\n\nexport default function EditVariantPage({ params }: { params?: { productId?: string; variantId?: string } }) {\n const router = useRouter()\n const t = useT()\n const productId = params?.productId ? String(params.productId) : null\n const variantId = params?.variantId ? String(params.variantId) : null\n const isCreateSentinel = variantId === 'create'\n const [priceKinds, setPriceKinds] = React.useState<PriceKindSummary[]>([])\n const [taxRates, setTaxRates] = React.useState<TaxRateSummary[]>([])\n const [optionDefinitions, setOptionDefinitions] = React.useState<OptionDefinition[]>([])\n const [initialValues, setInitialValues] = React.useState<VariantFormValues | null>(null)\n const [existingPriceIds, setExistingPriceIds] = React.useState<Record<string, string>>({})\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [currentProductId, setCurrentProductId] = React.useState<string | null>(productId)\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\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 console.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 console.error('sales.tax-rates.fetch failed', err)\n setTaxRates([])\n }\n }\n loadTaxRates().catch(() => {})\n }, [t])\n\n React.useEffect(() => {\n if (!variantId || isCreateSentinel || priceKinds.length === 0) return\n let cancelled = false\n async function load() {\n setLoading(true)\n setError(null)\n try {\n const variantRes = await apiCall<VariantResponse>(\n `/api/catalog/variants?id=${encodeURIComponent(variantId!)}&page=1&pageSize=1`,\n )\n if (!variantRes.ok) throw new Error('load_variant_failed')\n const record = Array.isArray(variantRes.result?.items) ? variantRes.result?.items?.[0] : undefined\n if (!record) throw new Error(t('catalog.variants.form.errors.notFound', 'Variant not found.'))\n const resolvedProductId =\n typeof record.product_id === 'string'\n ? record.product_id\n : typeof record.productId === 'string'\n ? record.productId\n : currentProductId\n if (resolvedProductId) setCurrentProductId(resolvedProductId)\n const metadata = typeof record.metadata === 'object' && record.metadata ? { ...(record.metadata as Record<string, unknown>) } : {}\n const attachments = await fetchVariantAttachments(variantId!)\n const priceDrafts = await loadVariantPrices(variantId!, priceKinds)\n const priceIdMap: Record<string, string> = {}\n Object.entries(priceDrafts).forEach(([kindId, draft]) => {\n if (draft.priceId) priceIdMap[kindId] = draft.priceId\n })\n setExistingPriceIds(priceIdMap)\n const customDefaults = extractCustomFieldValues(record)\n let loadedOptionDefinitions: OptionDefinition[] = []\n if (resolvedProductId) {\n const productRes = await apiCall<ProductResponse>(\n `/api/catalog/products?id=${encodeURIComponent(resolvedProductId)}&page=1&pageSize=1`,\n )\n if (productRes.ok) {\n const product = Array.isArray(productRes.result?.items) ? productRes.result?.items?.[0] : undefined\n if (product) {\n setProductTitle(typeof product.title === 'string' ? product.title : '')\n const taxRateId =\n typeof (product as any).tax_rate_id === 'string'\n ? (product as any).tax_rate_id\n : typeof (product as any).taxRateId === 'string'\n ? (product as any).taxRateId\n : null\n const taxRateValueRaw =\n typeof (product as any).tax_rate === 'number'\n ? (product as any).tax_rate\n : typeof (product as any).tax_rate === 'string'\n ? Number((product as any).tax_rate)\n : typeof (product as any).taxRate === 'number'\n ? (product as any).taxRate\n : typeof (product as any).taxRate === 'string'\n ? Number((product as any).taxRate)\n : null\n const taxRateValue = Number.isFinite(taxRateValueRaw) ? Number(taxRateValueRaw) : null\n setProductTaxRateId(taxRateId)\n setProductTaxRate(taxRateValue)\n const productMetadata = (product.metadata ?? {}) as Record<string, unknown>\n const optionSchemaId =\n typeof (product as any).option_schema_id === 'string'\n ? (product as any).option_schema_id\n : typeof (product as any).optionSchemaId === 'string'\n ? (product as any).optionSchemaId\n : null\n let schemaSource: unknown =\n productMetadata.optionSchema ?? (productMetadata.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) => ({\n id: choice.code ?? undefined,\n label: choice.label ?? choice.code ?? '',\n }))\n : [],\n }))\n }\n }\n loadedOptionDefinitions = normalizeOptionSchema(schemaSource)\n setOptionDefinitions(loadedOptionDefinitions)\n }\n }\n }\n if (!cancelled) {\n const optionValues =\n typeof record.option_values === 'object' && record.option_values\n ? { ...(record.option_values as Record<string, string>) }\n : typeof record.optionValues === 'object' && record.optionValues\n ? { ...(record.optionValues as Record<string, string>) }\n : {}\n const normalizedOptionValues = reconcileOptionValues(optionValues, loadedOptionDefinitions)\n const defaultMediaId =\n typeof record.default_media_id === 'string'\n ? record.default_media_id\n : typeof record.defaultMediaId === 'string'\n ? record.defaultMediaId\n : attachments[0]?.id ?? null\n const defaultMediaUrl =\n typeof record.default_media_url === 'string'\n ? record.default_media_url\n : typeof record.defaultMediaUrl === 'string'\n ? record.defaultMediaUrl\n : ''\n const base = createVariantInitialValues()\n setInitialValues({\n ...base,\n mediaDraftId: variantId!,\n name: typeof record.name === 'string' ? record.name : '',\n sku: typeof record.sku === 'string' ? record.sku : '',\n barcode: typeof record.barcode === 'string' ? record.barcode : '',\n isDefault: record.is_default === true || record.isDefault === true,\n isActive: record.is_active !== false && record.isActive !== false,\n optionValues: normalizedOptionValues,\n metadata,\n mediaItems: attachments,\n defaultMediaId,\n defaultMediaUrl,\n prices: priceDrafts,\n 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 customFieldsetCode:\n typeof record.custom_fieldset_code === 'string'\n ? record.custom_fieldset_code\n : typeof record.customFieldsetCode === 'string'\n ? record.customFieldsetCode\n : null,\n ...customDefaults,\n })\n }\n } catch (err) {\n console.error('catalog.variants.load.failed', err)\n if (!cancelled) {\n const message = err instanceof Error && err.message ? err.message : t('catalog.variants.form.errors.load', 'Failed to load variant.')\n setError(message)\n }\n } finally {\n if (!cancelled) setLoading(false)\n }\n }\n load()\n return () => { cancelled = true }\n }, [variantId, t, currentProductId, priceKinds])\n\n const groups = React.useMemo<CrudFormGroup[]>(() => {\n const list: CrudFormGroup[] = [\n {\n id: 'general',\n column: 1,\n title: t('catalog.variants.form.nameLabel', 'Name'),\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 (isCreateSentinel) {\n if (!productId) {\n return (\n <Page>\n <PageBody>\n <div className=\"rounded border border-destructive/40 bg-destructive/10 px-4 py-3 text-sm text-destructive\">\n {t('catalog.variants.form.errors.productMissing', 'Product identifier is missing.')}\n </div>\n </PageBody>\n </Page>\n )\n }\n return <CreateVariantPage params={{ productId }} />\n }\n\n if (!variantId || !currentProductId) {\n return (\n <Page>\n <PageBody>\n <div className=\"rounded border border-destructive/40 bg-destructive/10 px-4 py-3 text-sm text-destructive\">\n {t('catalog.variants.form.errors.variantMissing', 'Variant identifier is missing.')}\n </div>\n </PageBody>\n </Page>\n )\n }\n\n const formTitle = productTitle\n ? t('catalog.variants.form.editTitleFor', 'Edit variant \u2022 {{title}}').replace('{{title}}', productTitle)\n : t('catalog.variants.form.editTitle', 'Edit variant')\n const productVariantsHref = `/backend/catalog/products/${currentProductId}#variants`\n\n return (\n <Page>\n <PageBody>\n {error ? (\n <div className=\"mb-4 rounded border border-destructive/40 bg-destructive/10 px-4 py-3 text-sm text-destructive\">{error}</div>\n ) : null}\n <CrudForm<VariantFormValues>\n title={formTitle}\n backHref={productVariantsHref}\n versionHistory={{ resourceKind: 'catalog.variant', resourceId: variantId ? String(variantId) : '' }}\n extraActions={(\n <SendObjectMessageDialog\n object={{\n entityModule: 'catalog',\n entityType: 'variant',\n entityId: variantId,\n previewData: {\n title:\n (typeof initialValues?.name === 'string' && initialValues.name.trim().length\n ? initialValues.name\n : variantId),\n metadata: {\n [t('catalog.variants.form.skuLabel')]:\n (typeof initialValues?.sku === 'string' && initialValues.sku.trim().length\n ? initialValues.sku\n : '-'),\n [t('catalog.variants.form.pricesLabel')]:\n resolveVariantPriceLabel(initialValues?.prices) ?? '-',\n },\n },\n }}\n viewHref={`/backend/catalog/products/${currentProductId}/variants/${variantId}`}\n />\n )}\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 variant...')}\n submitLabel={t('catalog.variants.form.save', 'Save changes')}\n cancelHref={productVariantsHref}\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 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 id: variantId,\n productId: currentProductId,\n name,\n sku: values.sku?.trim() || undefined,\n barcode: values.barcode?.trim() || undefined,\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 const customFields = collectCustomFieldValues(values)\n if (Object.keys(customFields).length) payload.customFields = customFields\n\n await updateCrud('catalog/variants', payload)\n await syncVariantPricesUpdate({\n priceKinds,\n priceDrafts: values.prices ?? {},\n existingPriceIds,\n productId: currentProductId,\n variantId,\n taxRates,\n taxRateId: values.taxRateId,\n productTaxRateId,\n productTaxRate,\n })\n flash(t('catalog.variants.form.updated', 'Variant updated.'), 'success')\n router.push(productVariantsHref)\n }}\n onDelete={async () => {\n await deleteCrud('catalog/variants', variantId!, {\n errorMessage: t('catalog.variants.form.deleteError', 'Failed to delete variant.'),\n })\n flash(t('catalog.variants.form.deleted', 'Variant deleted.'), 'success')\n router.push(productVariantsHref)\n }}\n deleteRedirect={productVariantsHref}\n />\n </PageBody>\n </Page>\n )\n}\n\nfunction reconcileOptionValues(\n optionValues: Record<string, string>,\n optionDefinitions: OptionDefinition[],\n): Record<string, string> {\n if (!optionValues || !optionDefinitions.length) {\n return optionValues ?? {}\n }\n const remaining = new Map(Object.entries(optionValues))\n const normalized: Record<string, string> = {}\n\n for (const option of optionDefinitions) {\n const code = option.code?.trim()\n if (!code) continue\n if (remaining.has(code)) {\n const value = remaining.get(code)\n if (value !== undefined) {\n normalized[code] = value\n }\n remaining.delete(code)\n continue\n }\n const matchKey = findOptionKeyByValue(remaining, option.values)\n if (matchKey) {\n const value = remaining.get(matchKey)\n if (value !== undefined) {\n normalized[code] = value\n }\n remaining.delete(matchKey)\n }\n }\n\n remaining.forEach((value, key) => {\n if (normalized[key] === undefined) {\n normalized[key] = value\n }\n })\n\n return normalized\n}\n\nfunction findOptionKeyByValue(\n candidates: Map<string, string>,\n optionValues: { id: string; label: string }[],\n): string | null {\n if (!optionValues.length) return null\n const matches: string[] = []\n candidates.forEach((value, key) => {\n if (optionValues.some((entry) => entry.label === value)) {\n matches.push(key)\n }\n })\n return matches.length === 1 ? matches[0] : null\n}\n\nasync function fetchVariantAttachments(variantId: string): Promise<ProductMediaItem[]> {\n try {\n const res = await apiCall<AttachmentListResponse>(\n `/api/attachments?entityId=${encodeURIComponent(E.catalog.catalog_product_variant)}&recordId=${encodeURIComponent(variantId)}`,\n )\n if (!res.ok) return []\n return Array.isArray(res.result?.items) ? res.result?.items ?? [] : []\n } catch (err) {\n console.error('catalog.variants.attachments.load', err)\n return []\n }\n}\n\nasync function loadVariantPrices(variantId: string, priceKinds: PriceKindSummary[]): Promise<Record<string, VariantPriceDraft>> {\n const kindDisplayModes = new Map(priceKinds.map((k) => [k.id, k.displayMode]))\n const drafts: Record<string, VariantPriceDraft> = {}\n const pageSize = 100\n let page = 1\n try {\n while (true) {\n const res = await apiCall<PriceListResponse>(\n `/api/catalog/prices?variantId=${encodeURIComponent(variantId)}&page=${page}&pageSize=${pageSize}`,\n )\n if (!res.ok) break\n const items = Array.isArray(res.result?.items) ? res.result?.items : []\n for (const item of items) {\n const draft = mapPriceItemToDraft(item as Record<string, unknown>, kindDisplayModes)\n if (draft) drafts[draft.priceKindId] = draft\n }\n if (items.length < pageSize) break\n page += 1\n }\n } catch (err) {\n console.error('catalog.variants.prices.load', err)\n }\n return drafts\n}\n\nfunction extractCustomFieldValues(record: Record<string, unknown>): Record<string, unknown> {\n const customValues: Record<string, unknown> = {}\n Object.entries(record).forEach(([key, value]) => {\n if (key.startsWith('cf_')) customValues[key] = value\n else if (key.startsWith('cf:')) customValues[`cf_${key.slice(3)}`] = value\n })\n return customValues\n}\n\nasync function syncVariantPricesUpdate({\n priceKinds,\n priceDrafts,\n existingPriceIds,\n productId,\n variantId,\n taxRates,\n taxRateId,\n productTaxRateId,\n productTaxRate,\n}: {\n priceKinds: PriceKindSummary[]\n priceDrafts: Record<string, VariantPriceDraft>\n existingPriceIds: Record<string, string>\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 const existingId = draft?.priceId ?? existingPriceIds[kind.id]\n if (!amount) {\n if (existingId) {\n try {\n await deleteCrud('catalog/prices', existingId)\n } catch (err) {\n console.error('catalog.prices.delete', err)\n }\n }\n continue\n }\n const numeric = Number(amount)\n if (Number.isNaN(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) payload.taxRateId = resolvedTaxRateId\n else if (typeof resolvedTaxRateValue === 'number' && Number.isFinite(resolvedTaxRateValue)) payload.taxRate = resolvedTaxRateValue\n if (kind.displayMode === 'including-tax') payload.unitPriceGross = numeric\n else payload.unitPriceNet = numeric\n if (existingId) {\n await updateCrud('catalog/prices', { id: existingId, ...payload })\n } else {\n await createCrud('catalog/prices', payload)\n }\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -49,10 +49,25 @@ function buildVariantMetadata(values) {
49
49
  const metadata = typeof values.metadata === "object" && values.metadata ? { ...values.metadata } : {};
50
50
  return metadata;
51
51
  }
52
+ function mapPriceItemToDraft(item, kindDisplayModes) {
53
+ const kindId = typeof item.price_kind_id === "string" ? item.price_kind_id : typeof item.priceKindId === "string" ? item.priceKindId : null;
54
+ if (!kindId) return null;
55
+ const unitNet = typeof item.unit_price_net === "string" ? item.unit_price_net : typeof item.unitPriceNet === "string" ? item.unitPriceNet : null;
56
+ const unitGross = typeof item.unit_price_gross === "string" ? item.unit_price_gross : typeof item.unitPriceGross === "string" ? item.unitPriceGross : null;
57
+ const kindMode = kindDisplayModes.get(kindId) ?? (unitGross ? "including-tax" : "excluding-tax");
58
+ return {
59
+ priceKindId: kindId,
60
+ priceId: typeof item.id === "string" ? item.id : void 0,
61
+ amount: kindMode === "including-tax" ? unitGross ?? unitNet ?? "" : unitNet ?? unitGross ?? "",
62
+ currencyCode: typeof item.currency_code === "string" ? item.currency_code : typeof item.currencyCode === "string" ? item.currencyCode : null,
63
+ displayMode: kindMode
64
+ };
65
+ }
52
66
  export {
53
67
  VARIANT_BASE_VALUES,
54
68
  buildVariantMetadata,
55
69
  createVariantInitialValues,
70
+ mapPriceItemToDraft,
56
71
  normalizeOptionSchema
57
72
  };
58
73
  //# sourceMappingURL=variantForm.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/catalog/components/products/variantForm.ts"],
4
- "sourcesContent": ["\"use client\"\n\nimport type { ProductMediaItem } from './ProductMediaManager'\nimport { createLocalId } from './productForm'\n\nexport type OptionDefinition = {\n id: string\n code: string\n label: string\n values: Array<{ id: string; label: string }>\n}\n\nexport type VariantPriceDraft = {\n priceKindId: string\n priceId?: string\n amount: string\n currencyCode?: string | null\n displayMode: 'including-tax' | 'excluding-tax'\n}\n\nexport type VariantFormValues = {\n name: string\n sku: string\n barcode: string\n isDefault: boolean\n isActive: boolean\n optionValues: Record<string, string>\n metadata?: Record<string, unknown> | null\n mediaDraftId: string\n mediaItems: ProductMediaItem[]\n defaultMediaId: string | null\n defaultMediaUrl: string\n prices: Record<string, VariantPriceDraft>\n taxRateId: string | null\n customFieldsetCode?: string | null\n}\n\nexport const VARIANT_BASE_VALUES: VariantFormValues = {\n name: '',\n sku: '',\n barcode: '',\n isDefault: false,\n isActive: true,\n optionValues: {},\n metadata: {},\n mediaDraftId: '',\n mediaItems: [],\n defaultMediaId: null,\n defaultMediaUrl: '',\n prices: {},\n taxRateId: null,\n customFieldsetCode: null,\n}\n\nexport const createVariantInitialValues = (): VariantFormValues => ({\n ...VARIANT_BASE_VALUES,\n mediaDraftId: createLocalId(),\n})\n\nexport function normalizeOptionSchema(raw: unknown): OptionDefinition[] {\n if (!Array.isArray(raw)) return []\n return raw\n .map((entry) => normalizeOptionDefinition(entry))\n .filter((entry): entry is OptionDefinition => !!entry)\n}\n\nfunction normalizeOptionDefinition(entry: unknown): OptionDefinition | null {\n if (!entry || typeof entry !== 'object') return null\n const code = extractString((entry as any).code) || createLocalId()\n const label = extractString((entry as any).label) || code\n const values = Array.isArray((entry as any).values)\n ? (entry as any).values\n .map((value: any) => {\n const id = extractString(value?.id) || createLocalId()\n const valueLabel = extractString(value?.label) || id\n return { id, label: valueLabel }\n })\n .filter(\n (value: { id: string; label: string }): value is { id: string; label: string } =>\n value.label.length > 0,\n )\n : []\n return {\n id: extractString((entry as any).id) || createLocalId(),\n code,\n label,\n values,\n }\n}\n\nfunction extractString(value: unknown): string {\n return typeof value === 'string' ? value.trim() : ''\n}\n\nexport function buildVariantMetadata(values: VariantFormValues): Record<string, unknown> {\n const metadata = typeof values.metadata === 'object' && values.metadata ? { ...values.metadata } : {}\n return metadata\n}\n"],
5
- "mappings": ";AAGA,SAAS,qBAAqB;AAkCvB,MAAM,sBAAyC;AAAA,EACpD,MAAM;AAAA,EACN,KAAK;AAAA,EACL,SAAS;AAAA,EACT,WAAW;AAAA,EACX,UAAU;AAAA,EACV,cAAc,CAAC;AAAA,EACf,UAAU,CAAC;AAAA,EACX,cAAc;AAAA,EACd,YAAY,CAAC;AAAA,EACb,gBAAgB;AAAA,EAChB,iBAAiB;AAAA,EACjB,QAAQ,CAAC;AAAA,EACT,WAAW;AAAA,EACX,oBAAoB;AACtB;AAEO,MAAM,6BAA6B,OAA0B;AAAA,EAClE,GAAG;AAAA,EACH,cAAc,cAAc;AAC9B;AAEO,SAAS,sBAAsB,KAAkC;AACtE,MAAI,CAAC,MAAM,QAAQ,GAAG,EAAG,QAAO,CAAC;AACjC,SAAO,IACJ,IAAI,CAAC,UAAU,0BAA0B,KAAK,CAAC,EAC/C,OAAO,CAAC,UAAqC,CAAC,CAAC,KAAK;AACzD;AAEA,SAAS,0BAA0B,OAAyC;AAC1E,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;AAChD,QAAM,OAAO,cAAe,MAAc,IAAI,KAAK,cAAc;AACjE,QAAM,QAAQ,cAAe,MAAc,KAAK,KAAK;AACrD,QAAM,SAAS,MAAM,QAAS,MAAc,MAAM,IAC7C,MAAc,OACZ,IAAI,CAAC,UAAe;AACnB,UAAM,KAAK,cAAc,OAAO,EAAE,KAAK,cAAc;AACrD,UAAM,aAAa,cAAc,OAAO,KAAK,KAAK;AAClD,WAAO,EAAE,IAAI,OAAO,WAAW;AAAA,EACjC,CAAC,EACA;AAAA,IACC,CAAC,UACC,MAAM,MAAM,SAAS;AAAA,EACzB,IACF,CAAC;AACL,SAAO;AAAA,IACL,IAAI,cAAe,MAAc,EAAE,KAAK,cAAc;AAAA,IACtD;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAEA,SAAS,cAAc,OAAwB;AAC7C,SAAO,OAAO,UAAU,WAAW,MAAM,KAAK,IAAI;AACpD;AAEO,SAAS,qBAAqB,QAAoD;AACvF,QAAM,WAAW,OAAO,OAAO,aAAa,YAAY,OAAO,WAAW,EAAE,GAAG,OAAO,SAAS,IAAI,CAAC;AACpG,SAAO;AACT;",
4
+ "sourcesContent": ["\"use client\"\n\nimport type { ProductMediaItem } from './ProductMediaManager'\nimport { createLocalId } from './productForm'\n\nexport type OptionDefinition = {\n id: string\n code: string\n label: string\n values: Array<{ id: string; label: string }>\n}\n\nexport type VariantPriceDraft = {\n priceKindId: string\n priceId?: string\n amount: string\n currencyCode?: string | null\n displayMode: 'including-tax' | 'excluding-tax'\n}\n\nexport type VariantFormValues = {\n name: string\n sku: string\n barcode: string\n isDefault: boolean\n isActive: boolean\n optionValues: Record<string, string>\n metadata?: Record<string, unknown> | null\n mediaDraftId: string\n mediaItems: ProductMediaItem[]\n defaultMediaId: string | null\n defaultMediaUrl: string\n prices: Record<string, VariantPriceDraft>\n taxRateId: string | null\n customFieldsetCode?: string | null\n}\n\nexport const VARIANT_BASE_VALUES: VariantFormValues = {\n name: '',\n sku: '',\n barcode: '',\n isDefault: false,\n isActive: true,\n optionValues: {},\n metadata: {},\n mediaDraftId: '',\n mediaItems: [],\n defaultMediaId: null,\n defaultMediaUrl: '',\n prices: {},\n taxRateId: null,\n customFieldsetCode: null,\n}\n\nexport const createVariantInitialValues = (): VariantFormValues => ({\n ...VARIANT_BASE_VALUES,\n mediaDraftId: createLocalId(),\n})\n\nexport function normalizeOptionSchema(raw: unknown): OptionDefinition[] {\n if (!Array.isArray(raw)) return []\n return raw\n .map((entry) => normalizeOptionDefinition(entry))\n .filter((entry): entry is OptionDefinition => !!entry)\n}\n\nfunction normalizeOptionDefinition(entry: unknown): OptionDefinition | null {\n if (!entry || typeof entry !== 'object') return null\n const code = extractString((entry as any).code) || createLocalId()\n const label = extractString((entry as any).label) || code\n const values = Array.isArray((entry as any).values)\n ? (entry as any).values\n .map((value: any) => {\n const id = extractString(value?.id) || createLocalId()\n const valueLabel = extractString(value?.label) || id\n return { id, label: valueLabel }\n })\n .filter(\n (value: { id: string; label: string }): value is { id: string; label: string } =>\n value.label.length > 0,\n )\n : []\n return {\n id: extractString((entry as any).id) || createLocalId(),\n code,\n label,\n values,\n }\n}\n\nfunction extractString(value: unknown): string {\n return typeof value === 'string' ? value.trim() : ''\n}\n\nexport function buildVariantMetadata(values: VariantFormValues): Record<string, unknown> {\n const metadata = typeof values.metadata === 'object' && values.metadata ? { ...values.metadata } : {}\n return metadata\n}\n\nexport function mapPriceItemToDraft(\n item: Record<string, unknown>,\n kindDisplayModes: Map<string, 'including-tax' | 'excluding-tax'>,\n): VariantPriceDraft | null {\n const kindId =\n typeof item.price_kind_id === 'string'\n ? item.price_kind_id\n : typeof item.priceKindId === 'string'\n ? item.priceKindId\n : null\n if (!kindId) return null\n const unitNet =\n typeof item.unit_price_net === 'string'\n ? item.unit_price_net\n : typeof item.unitPriceNet === 'string'\n ? item.unitPriceNet\n : null\n const unitGross =\n typeof item.unit_price_gross === 'string'\n ? item.unit_price_gross\n : typeof item.unitPriceGross === 'string'\n ? item.unitPriceGross\n : null\n const kindMode = kindDisplayModes.get(kindId) ?? (unitGross ? 'including-tax' : 'excluding-tax')\n return {\n priceKindId: kindId,\n priceId: typeof item.id === 'string' ? item.id : undefined,\n amount: kindMode === 'including-tax' ? (unitGross ?? unitNet ?? '') : (unitNet ?? unitGross ?? ''),\n currencyCode:\n typeof item.currency_code === 'string'\n ? item.currency_code\n : typeof item.currencyCode === 'string'\n ? item.currencyCode\n : null,\n displayMode: kindMode,\n }\n}\n"],
5
+ "mappings": ";AAGA,SAAS,qBAAqB;AAkCvB,MAAM,sBAAyC;AAAA,EACpD,MAAM;AAAA,EACN,KAAK;AAAA,EACL,SAAS;AAAA,EACT,WAAW;AAAA,EACX,UAAU;AAAA,EACV,cAAc,CAAC;AAAA,EACf,UAAU,CAAC;AAAA,EACX,cAAc;AAAA,EACd,YAAY,CAAC;AAAA,EACb,gBAAgB;AAAA,EAChB,iBAAiB;AAAA,EACjB,QAAQ,CAAC;AAAA,EACT,WAAW;AAAA,EACX,oBAAoB;AACtB;AAEO,MAAM,6BAA6B,OAA0B;AAAA,EAClE,GAAG;AAAA,EACH,cAAc,cAAc;AAC9B;AAEO,SAAS,sBAAsB,KAAkC;AACtE,MAAI,CAAC,MAAM,QAAQ,GAAG,EAAG,QAAO,CAAC;AACjC,SAAO,IACJ,IAAI,CAAC,UAAU,0BAA0B,KAAK,CAAC,EAC/C,OAAO,CAAC,UAAqC,CAAC,CAAC,KAAK;AACzD;AAEA,SAAS,0BAA0B,OAAyC;AAC1E,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;AAChD,QAAM,OAAO,cAAe,MAAc,IAAI,KAAK,cAAc;AACjE,QAAM,QAAQ,cAAe,MAAc,KAAK,KAAK;AACrD,QAAM,SAAS,MAAM,QAAS,MAAc,MAAM,IAC7C,MAAc,OACZ,IAAI,CAAC,UAAe;AACnB,UAAM,KAAK,cAAc,OAAO,EAAE,KAAK,cAAc;AACrD,UAAM,aAAa,cAAc,OAAO,KAAK,KAAK;AAClD,WAAO,EAAE,IAAI,OAAO,WAAW;AAAA,EACjC,CAAC,EACA;AAAA,IACC,CAAC,UACC,MAAM,MAAM,SAAS;AAAA,EACzB,IACF,CAAC;AACL,SAAO;AAAA,IACL,IAAI,cAAe,MAAc,EAAE,KAAK,cAAc;AAAA,IACtD;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAEA,SAAS,cAAc,OAAwB;AAC7C,SAAO,OAAO,UAAU,WAAW,MAAM,KAAK,IAAI;AACpD;AAEO,SAAS,qBAAqB,QAAoD;AACvF,QAAM,WAAW,OAAO,OAAO,aAAa,YAAY,OAAO,WAAW,EAAE,GAAG,OAAO,SAAS,IAAI,CAAC;AACpG,SAAO;AACT;AAEO,SAAS,oBACd,MACA,kBAC0B;AAC1B,QAAM,SACJ,OAAO,KAAK,kBAAkB,WAC1B,KAAK,gBACL,OAAO,KAAK,gBAAgB,WAC1B,KAAK,cACL;AACR,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,UACJ,OAAO,KAAK,mBAAmB,WAC3B,KAAK,iBACL,OAAO,KAAK,iBAAiB,WAC3B,KAAK,eACL;AACR,QAAM,YACJ,OAAO,KAAK,qBAAqB,WAC7B,KAAK,mBACL,OAAO,KAAK,mBAAmB,WAC7B,KAAK,iBACL;AACR,QAAM,WAAW,iBAAiB,IAAI,MAAM,MAAM,YAAY,kBAAkB;AAChF,SAAO;AAAA,IACL,aAAa;AAAA,IACb,SAAS,OAAO,KAAK,OAAO,WAAW,KAAK,KAAK;AAAA,IACjD,QAAQ,aAAa,kBAAmB,aAAa,WAAW,KAAO,WAAW,aAAa;AAAA,IAC/F,cACE,OAAO,KAAK,kBAAkB,WAC1B,KAAK,gBACL,OAAO,KAAK,iBAAiB,WAC3B,KAAK,eACL;AAAA,IACR,aAAa;AAAA,EACf;AACF;",
6
6
  "names": []
7
7
  }
@@ -328,6 +328,7 @@ function TranslationManager({
328
328
  size: "sm",
329
329
  onClick: () => mutation.mutate(),
330
330
  disabled: mutation.isPending || !entityType || !recordId,
331
+ "data-testid": "translations-save",
331
332
  children: [
332
333
  /* @__PURE__ */ jsx(Save, { className: "mr-2 h-3 w-3" }),
333
334
  mutation.isPending ? t("translations.manager.actions.saving", "Saving...") : t("translations.manager.actions.save", "Save translations")
@@ -373,6 +374,7 @@ function TranslationManager({
373
374
  type: "button",
374
375
  onClick: () => mutation.mutate(),
375
376
  disabled: mutation.isPending || loadingEntities || !!entitiesError || !entityType || !recordId,
377
+ "data-testid": "translations-save",
376
378
  children: [
377
379
  /* @__PURE__ */ jsx(Save, { className: "mr-2 h-4 w-4" }),
378
380
  mutation.isPending ? t("translations.manager.actions.saving", "Saving...") : t("translations.manager.actions.save", "Save translations")
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/translations/components/TranslationManager.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { ComboboxInput } from '@open-mercato/ui/backend/inputs'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useCustomFieldDefs } from '@open-mercato/ui/backend/utils/customFieldDefs'\nimport { Save, Plus, X } from 'lucide-react'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { locales as defaultLocales } from '@open-mercato/shared/lib/i18n/config'\nimport { ISO_639_1, isValidIso639, getIso639Label } from '@open-mercato/shared/lib/i18n/iso639'\nimport { formatEntityLabel, buildEntityListUrl, getRecordLabel, resolveBaseValue } from '../lib/helpers'\nimport { resolveFieldList } from '../lib/resolve-field-list'\nimport type { ResolvedField } from '../lib/resolve-field-list'\n\ntype TranslationManagerProps = {\n entityType?: string\n recordId?: string\n baseValues?: Record<string, unknown>\n translatableFields?: string[]\n mode?: 'standalone' | 'embedded'\n compact?: boolean\n}\n\ntype EntityOption = { entityId: string; label?: string; source?: string }\n\ntype TranslationsResponse = {\n entityType: string\n entityId: string\n translations: Record<string, Record<string, unknown>>\n createdAt?: string\n updatedAt?: string\n}\n\nfunction useTranslationLocales() {\n return useQuery<string[]>({\n queryKey: ['translation-locales'],\n queryFn: async () => {\n const res = await apiCall<{ locales: string[] }>('/api/translations/locales')\n if (!res.ok) return [...defaultLocales]\n return Array.isArray(res.result?.locales) && res.result.locales.length > 0\n ? res.result.locales\n : [...defaultLocales]\n },\n staleTime: 60_000,\n })\n}\n\nexport function TranslationManager({\n entityType: propEntityType,\n recordId: propRecordId,\n baseValues: propBaseValues,\n translatableFields: propTranslatableFields,\n mode = 'standalone',\n compact = false,\n}: TranslationManagerProps) {\n const t = useT()\n const scopeVersion = useOrganizationScopeVersion()\n const isEmbedded = mode === 'embedded'\n\n const [selectedEntityType, setSelectedEntityType] = React.useState(propEntityType ?? '')\n const [selectedRecordId, setSelectedRecordId] = React.useState(propRecordId ?? '')\n const [activeLocale, setActiveLocale] = React.useState('')\n const [editedTranslations, setEditedTranslations] = React.useState<Record<string, Record<string, string>>>({})\n const [hasUserEdited, setHasUserEdited] = React.useState(false)\n\n const entityType = isEmbedded ? (propEntityType ?? '') : selectedEntityType\n const recordId = isEmbedded ? (propRecordId ?? '') : selectedRecordId\n\n const { data: locales = [...defaultLocales] } = useTranslationLocales()\n\n React.useEffect(() => {\n if (locales.length > 0 && (!activeLocale || !locales.includes(activeLocale))) {\n setActiveLocale(locales[0])\n }\n }, [locales, activeLocale])\n\n React.useEffect(() => {\n if (isEmbedded && propEntityType) setSelectedEntityType(propEntityType)\n }, [isEmbedded, propEntityType])\n\n React.useEffect(() => {\n if (isEmbedded && propRecordId) setSelectedRecordId(propRecordId)\n }, [isEmbedded, propRecordId])\n\n const { data: entities, isLoading: loadingEntities, error: entitiesError } = useQuery<{ items: EntityOption[] }>({\n queryKey: ['entities-list', scopeVersion],\n enabled: !isEmbedded,\n queryFn: async () =>\n readApiResultOrThrow('/api/entities/entities', undefined, {\n errorMessage: t('translations.manager.errors.loadEntities', 'Failed to load entities'),\n }),\n })\n\n const entitySuggestions = React.useMemo(\n () =>\n (entities?.items || []).map((item) => ({\n value: item.entityId,\n label: formatEntityLabel(item.entityId, item.label),\n description: item.entityId,\n })),\n [entities],\n )\n\n const resolveEntityLabel = React.useCallback(\n (value: string) => {\n const match = entities?.items?.find((e) => e.entityId === value)\n return match ? formatEntityLabel(match.entityId, match.label) : formatEntityLabel(value)\n },\n [entities],\n )\n\n const listUrl = React.useMemo(() => entityType ? buildEntityListUrl(entityType) : null, [entityType])\n\n const loadRecordSuggestions = React.useCallback(\n async (query?: string) => {\n if (!entityType || !listUrl) return []\n const url = `${listUrl}?pageSize=20${query ? `&search=${encodeURIComponent(query)}` : ''}`\n const res = await apiCall<{ items: Array<Record<string, unknown>> }>(url)\n if (!res.ok) return []\n const items = res.result?.items ?? []\n return items.map((item) => ({\n value: String(item.id ?? ''),\n label: getRecordLabel(item),\n }))\n },\n [entityType, listUrl],\n )\n\n const { data: recordData } = useQuery<Record<string, unknown> | null>({\n queryKey: ['translation-record-data', entityType, recordId, listUrl, scopeVersion],\n enabled: !isEmbedded && !!entityType && !!recordId && !!listUrl,\n queryFn: async () => {\n const res = await apiCall<{ items: Array<Record<string, unknown>> }>(\n // Some APIs filter by `id` (catalog), others by `ids` (resources) \u2014 send both so the one recognized by the target route's buildFilters is applied\n `${listUrl}?id=${encodeURIComponent(recordId)}&ids=${encodeURIComponent(recordId)}&pageSize=1`,\n )\n if (!res.ok) return null\n const items = res.result?.items\n return Array.isArray(items) && items.length > 0 ? items[0] : null\n },\n })\n\n const baseValues = isEmbedded ? (propBaseValues ?? {}) : (recordData ?? {})\n\n const resolveRecordLabel = React.useCallback(\n (value: string) => {\n if (recordData) return getRecordLabel(recordData)\n return value\n },\n [recordData],\n )\n\n const { data: fieldDefs = [], isLoading: loadingFieldDefs } = useCustomFieldDefs(entityType ? [entityType] : [], {\n enabled: !!entityType,\n })\n\n const fieldList = React.useMemo(\n () => resolveFieldList(entityType, propTranslatableFields, fieldDefs as Array<{ key: string; kind: string; label?: string }>),\n [entityType, propTranslatableFields, fieldDefs],\n )\n\n const {\n data: translationData,\n isLoading: loadingTranslation,\n isError: translationError,\n refetch: refetchTranslation,\n } = useQuery<TranslationsResponse | null>({\n queryKey: ['entity-translation', entityType, recordId, scopeVersion],\n enabled: !!entityType && !!recordId,\n queryFn: async () => {\n const res = await apiCall<TranslationsResponse>(\n `/api/translations/${encodeURIComponent(entityType)}/${encodeURIComponent(recordId)}`,\n )\n if (!res.ok) {\n if (res.response?.status === 404) return null\n return null\n }\n return res.result ?? null\n },\n })\n\n const translationSignature = React.useMemo(() => JSON.stringify(translationData ?? null), [translationData])\n const lastTranslationSignatureRef = React.useRef<string | null>(null)\n\n React.useEffect(() => {\n const sig = translationSignature\n if (sig === lastTranslationSignatureRef.current && hasUserEdited) return\n lastTranslationSignatureRef.current = sig\n\n if (!translationData?.translations) {\n if (!hasUserEdited) setEditedTranslations({})\n return\n }\n\n const parsed: Record<string, Record<string, string>> = {}\n for (const [locale, fields] of Object.entries(translationData.translations)) {\n if (!fields || typeof fields !== 'object') continue\n parsed[locale] = {}\n for (const [key, val] of Object.entries(fields)) {\n parsed[locale][key] = typeof val === 'string' ? val : ''\n }\n }\n if (!hasUserEdited) setEditedTranslations(parsed)\n }, [translationSignature, translationData, hasUserEdited])\n\n const mutation = useMutation({\n mutationFn: async () => {\n if (!entityType || !recordId) {\n throw new Error(t('translations.manager.errors.selectRecord', 'Select an entity and record before saving'))\n }\n const body: Record<string, Record<string, string | null>> = {}\n for (const [locale, fields] of Object.entries(editedTranslations)) {\n const localeFields: Record<string, string | null> = {}\n let hasValues = false\n for (const [key, val] of Object.entries(fields)) {\n if (val && val.trim().length > 0) {\n localeFields[key] = val.trim()\n hasValues = true\n }\n }\n if (hasValues) body[locale] = localeFields\n }\n const res = await apiCall(\n `/api/translations/${encodeURIComponent(entityType)}/${encodeURIComponent(recordId)}`,\n {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n },\n )\n if (!res.ok) {\n throw new Error(t('translations.manager.errors.save', 'Failed to save translations'))\n }\n return true\n },\n onSuccess: () => {\n flash(t('translations.manager.flash.saved', 'Translations saved'), 'success')\n setHasUserEdited(false)\n void refetchTranslation()\n },\n onError: (err: unknown) => {\n const message = err instanceof Error ? err.message : t('translations.manager.errors.save', 'Failed to save translations')\n flash(message, 'error')\n },\n })\n\n const updateFieldValue = (locale: string, fieldKey: string, value: string) => {\n setHasUserEdited(true)\n setEditedTranslations((prev) => ({\n ...prev,\n [locale]: {\n ...prev[locale],\n [fieldKey]: value,\n },\n }))\n }\n\n const getBaseValue = (fieldKey: string): string => resolveBaseValue(baseValues, fieldKey)\n\n const renderRecordPicker = () => {\n if (isEmbedded) return null\n\n return (\n <div className=\"space-y-2\">\n <label className=\"text-xs text-muted-foreground\">\n {t('translations.manager.selectRecord', 'Select record')}\n </label>\n <ComboboxInput\n value={selectedRecordId}\n onChange={(next) => {\n setSelectedRecordId(next)\n setHasUserEdited(false)\n }}\n placeholder={t('translations.manager.searchRecords', 'Search records...')}\n loadSuggestions={loadRecordSuggestions}\n resolveLabel={resolveRecordLabel}\n allowCustomValues\n disabled={!entityType}\n />\n </div>\n )\n }\n\n const renderLocaleTabs = () => (\n <div className=\"flex gap-1 border-b\">\n {locales.map((locale) => (\n <button\n key={locale}\n type=\"button\"\n className={`px-3 py-1.5 text-sm font-medium transition-colors ${\n activeLocale === locale\n ? 'border-b-2 border-primary text-primary'\n : 'text-muted-foreground hover:text-foreground'\n }`}\n onClick={() => setActiveLocale(locale)}\n >\n {locale.toUpperCase()}\n </button>\n ))}\n </div>\n )\n\n const renderFieldTable = () => {\n if (!entityType || !recordId) {\n return (\n <div className=\"rounded border bg-background/70 p-4 text-sm text-muted-foreground\">\n {t('translations.manager.selectFirst', 'Select an entity and record to manage translations.')}\n </div>\n )\n }\n if (loadingTranslation || loadingFieldDefs) {\n return (\n <LoadingMessage\n label={t('translations.manager.loadingTranslations', 'Loading translations...')}\n className=\"border-0 bg-transparent p-4\"\n />\n )\n }\n if (translationError) {\n return (\n <ErrorMessage\n label={t('translations.manager.errors.loadTranslation', 'Failed to load translations')}\n action={(\n <Button variant=\"outline\" size=\"sm\" onClick={() => void refetchTranslation()}>\n {t('translations.manager.actions.retry', 'Retry')}\n </Button>\n )}\n />\n )\n }\n if (!fieldList.length) {\n return (\n <div className=\"rounded border bg-background/70 p-4 text-sm text-muted-foreground\">\n {t('translations.manager.noFields', 'No translatable fields found for this entity type.')}\n </div>\n )\n }\n\n const localeTranslations = editedTranslations[activeLocale] ?? {}\n\n return (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full min-w-[480px] text-sm\">\n <thead>\n <tr className=\"text-xs uppercase tracking-wide text-muted-foreground\">\n <th className=\"px-3 py-2 text-left w-[140px]\">\n {t('translations.manager.fields.field', 'Field')}\n </th>\n {!compact && (\n <th className=\"px-3 py-2 text-left\">\n {t('translations.manager.fields.baseValue', 'Base value')}\n </th>\n )}\n <th className=\"px-3 py-2 text-left\">\n {t('translations.manager.fields.translation', 'Translation')} ({activeLocale.toUpperCase()})\n </th>\n </tr>\n </thead>\n <tbody>\n {fieldList.map((field) => {\n const baseVal = getBaseValue(field.key)\n const translatedVal = localeTranslations[field.key] ?? ''\n\n return (\n <tr key={field.key} className=\"border-t\">\n <td className=\"px-3 py-2 align-top text-xs font-medium text-muted-foreground\">\n {field.label}\n </td>\n {!compact && (\n <td className=\"px-3 py-2 align-top text-xs text-muted-foreground max-w-[200px]\">\n {baseVal ? (\n <span className=\"line-clamp-3\">{baseVal}</span>\n ) : (\n <span className=\"text-muted-foreground/50\">-</span>\n )}\n </td>\n )}\n <td className=\"px-3 py-2 align-top\">\n {field.multiline ? (\n <textarea\n className=\"flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\"\n rows={3}\n value={translatedVal}\n onChange={(e) => updateFieldValue(activeLocale, field.key, e.target.value)}\n placeholder={baseVal || field.label}\n />\n ) : (\n <Input\n value={translatedVal}\n onChange={(e) => updateFieldValue(activeLocale, field.key, e.target.value)}\n placeholder={baseVal || field.label}\n />\n )}\n </td>\n </tr>\n )\n })}\n </tbody>\n </table>\n </div>\n )\n }\n\n React.useEffect(() => {\n const handler = (e: KeyboardEvent) => {\n if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {\n e.preventDefault()\n if (entityType && recordId && !mutation.isPending) mutation.mutate()\n }\n }\n document.addEventListener('keydown', handler)\n return () => document.removeEventListener('keydown', handler)\n }, [entityType, recordId, mutation])\n\n if (compact) {\n return (\n <div className=\"space-y-3\">\n {renderLocaleTabs()}\n {renderFieldTable()}\n <div className=\"flex justify-end\">\n <Button\n type=\"button\"\n size=\"sm\"\n onClick={() => mutation.mutate()}\n disabled={mutation.isPending || !entityType || !recordId}\n >\n <Save className=\"mr-2 h-3 w-3\" />\n {mutation.isPending\n ? t('translations.manager.actions.saving', 'Saving...')\n : t('translations.manager.actions.save', 'Save translations')}\n </Button>\n </div>\n </div>\n )\n }\n\n return (\n <div className=\"space-y-6\">\n <div className=\"flex flex-col gap-3 rounded-lg border bg-card p-4 shadow-sm\">\n <div className=\"space-y-2\">\n <h2 className=\"text-xl font-semibold\">{t('translations.manager.title', 'Translations')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('translations.manager.description', 'Manage translations for entity records across supported locales.')}\n </p>\n </div>\n\n {!isEmbedded && (\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start\">\n <div className=\"flex-1 space-y-3\">\n <div>\n <label className=\"text-xs text-muted-foreground\">\n {t('translations.manager.selectEntity', 'Choose entity')}\n </label>\n <div className=\"mt-1\">\n <ComboboxInput\n value={selectedEntityType}\n onChange={(next) => {\n setSelectedEntityType(next)\n setSelectedRecordId('')\n setHasUserEdited(false)\n }}\n placeholder={t('translations.manager.placeholder', 'Select an entity')}\n suggestions={entitySuggestions}\n resolveLabel={resolveEntityLabel}\n disabled={loadingEntities || !!entitiesError}\n />\n </div>\n {entitiesError && (\n <p className=\"mt-1 text-xs text-red-600\">\n {t('translations.manager.errors.loadEntities', 'Failed to load entities')}\n </p>\n )}\n </div>\n {renderRecordPicker()}\n </div>\n </div>\n )}\n\n <div className=\"rounded-lg border bg-background/70 p-4\">\n {renderLocaleTabs()}\n <div className=\"mt-3\">\n {renderFieldTable()}\n </div>\n </div>\n\n <div className=\"flex justify-end\">\n <Button\n type=\"button\"\n onClick={() => mutation.mutate()}\n disabled={mutation.isPending || loadingEntities || !!entitiesError || !entityType || !recordId}\n >\n <Save className=\"mr-2 h-4 w-4\" />\n {mutation.isPending\n ? t('translations.manager.actions.saving', 'Saving...')\n : t('translations.manager.actions.save', 'Save translations')}\n </Button>\n </div>\n </div>\n </div>\n )\n}\n\nexport function LocaleManager() {\n const t = useT()\n const queryClient = useQueryClient()\n const { data: locales = [], isLoading } = useTranslationLocales()\n const [newLocale, setNewLocale] = React.useState('')\n\n const mutation = useMutation({\n mutationFn: async (updatedLocales: string[]) => {\n const res = await apiCall<{ locales: string[] }>('/api/translations/locales', {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ locales: updatedLocales }),\n })\n if (!res.ok) throw new Error('Failed to save locales')\n return res.result?.locales ?? updatedLocales\n },\n onSuccess: (result) => {\n queryClient.setQueryData(['translation-locales'], result)\n flash(t('translations.locales.flash.saved', 'Locales updated'), 'success')\n },\n onError: () => {\n flash(t('translations.locales.flash.error', 'Failed to update locales'), 'error')\n },\n })\n\n const availableLocales = React.useMemo(\n () => ISO_639_1.filter((entry) => !locales.includes(entry.code)).map((entry) => ({\n value: entry.code,\n label: `${entry.code.toUpperCase()} \u2014 ${entry.label}`,\n })),\n [locales],\n )\n\n const addLocale = () => {\n const code = newLocale.toLowerCase().trim()\n if (!code || !isValidIso639(code) || locales.includes(code)) return\n mutation.mutate([...locales, code])\n setNewLocale('')\n }\n\n const removeLocale = (locale: string) => {\n if (locales.length <= 1) return\n mutation.mutate(locales.filter((l) => l !== locale))\n }\n\n if (isLoading) {\n return <LoadingMessage label={t('translations.locales.loading', 'Loading locales...')} className=\"border-0 bg-transparent p-4\" />\n }\n\n return (\n <div className=\"flex flex-col gap-3 rounded-lg border bg-card p-4 shadow-sm\">\n <div className=\"space-y-1\">\n <h3 className=\"text-lg font-semibold\">{t('translations.locales.title', 'Supported locales')}</h3>\n <p className=\"text-sm text-muted-foreground\">\n {t('translations.locales.description', 'Configure which locales are available for translations. Add ISO language codes (e.g. fr, it, ja, zh).')}\n </p>\n </div>\n\n <div className=\"flex flex-wrap gap-2\">\n {locales.map((locale) => (\n <span\n key={locale}\n className=\"inline-flex items-center gap-1.5 rounded-full border bg-muted/50 px-3 py-1 text-sm font-medium\"\n title={getIso639Label(locale) ?? locale}\n >\n {locale.toUpperCase()}{getIso639Label(locale) ? ` \u2014 ${getIso639Label(locale)}` : ''}\n {locales.length > 1 && (\n <button\n type=\"button\"\n className=\"rounded-full p-0.5 text-muted-foreground hover:text-foreground transition-colors\"\n onClick={() => removeLocale(locale)}\n disabled={mutation.isPending}\n >\n <X className=\"h-3 w-3\" />\n </button>\n )}\n </span>\n ))}\n </div>\n\n <div className=\"flex gap-2 items-center\">\n <div className=\"max-w-[240px] flex-1\">\n <ComboboxInput\n value={newLocale}\n onChange={setNewLocale}\n placeholder={t('translations.locales.addPlaceholder', 'Search language...')}\n suggestions={availableLocales}\n resolveLabel={(value) => {\n const label = getIso639Label(value)\n return label ? `${value.toUpperCase()} \u2014 ${label}` : value.toUpperCase()\n }}\n />\n </div>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={addLocale}\n disabled={mutation.isPending || !newLocale.trim() || !isValidIso639(newLocale) || locales.includes(newLocale.toLowerCase().trim())}\n >\n <Plus className=\"mr-1 h-3 w-3\" />\n {t('translations.locales.add', 'Add')}\n </Button>\n </div>\n </div>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { ComboboxInput } from '@open-mercato/ui/backend/inputs'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useCustomFieldDefs } from '@open-mercato/ui/backend/utils/customFieldDefs'\nimport { Save, Plus, X } from 'lucide-react'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { locales as defaultLocales } from '@open-mercato/shared/lib/i18n/config'\nimport { ISO_639_1, isValidIso639, getIso639Label } from '@open-mercato/shared/lib/i18n/iso639'\nimport { formatEntityLabel, buildEntityListUrl, getRecordLabel, resolveBaseValue } from '../lib/helpers'\nimport { resolveFieldList } from '../lib/resolve-field-list'\nimport type { ResolvedField } from '../lib/resolve-field-list'\n\ntype TranslationManagerProps = {\n entityType?: string\n recordId?: string\n baseValues?: Record<string, unknown>\n translatableFields?: string[]\n mode?: 'standalone' | 'embedded'\n compact?: boolean\n}\n\ntype EntityOption = { entityId: string; label?: string; source?: string }\n\ntype TranslationsResponse = {\n entityType: string\n entityId: string\n translations: Record<string, Record<string, unknown>>\n createdAt?: string\n updatedAt?: string\n}\n\nfunction useTranslationLocales() {\n return useQuery<string[]>({\n queryKey: ['translation-locales'],\n queryFn: async () => {\n const res = await apiCall<{ locales: string[] }>('/api/translations/locales')\n if (!res.ok) return [...defaultLocales]\n return Array.isArray(res.result?.locales) && res.result.locales.length > 0\n ? res.result.locales\n : [...defaultLocales]\n },\n staleTime: 60_000,\n })\n}\n\nexport function TranslationManager({\n entityType: propEntityType,\n recordId: propRecordId,\n baseValues: propBaseValues,\n translatableFields: propTranslatableFields,\n mode = 'standalone',\n compact = false,\n}: TranslationManagerProps) {\n const t = useT()\n const scopeVersion = useOrganizationScopeVersion()\n const isEmbedded = mode === 'embedded'\n\n const [selectedEntityType, setSelectedEntityType] = React.useState(propEntityType ?? '')\n const [selectedRecordId, setSelectedRecordId] = React.useState(propRecordId ?? '')\n const [activeLocale, setActiveLocale] = React.useState('')\n const [editedTranslations, setEditedTranslations] = React.useState<Record<string, Record<string, string>>>({})\n const [hasUserEdited, setHasUserEdited] = React.useState(false)\n\n const entityType = isEmbedded ? (propEntityType ?? '') : selectedEntityType\n const recordId = isEmbedded ? (propRecordId ?? '') : selectedRecordId\n\n const { data: locales = [...defaultLocales] } = useTranslationLocales()\n\n React.useEffect(() => {\n if (locales.length > 0 && (!activeLocale || !locales.includes(activeLocale))) {\n setActiveLocale(locales[0])\n }\n }, [locales, activeLocale])\n\n React.useEffect(() => {\n if (isEmbedded && propEntityType) setSelectedEntityType(propEntityType)\n }, [isEmbedded, propEntityType])\n\n React.useEffect(() => {\n if (isEmbedded && propRecordId) setSelectedRecordId(propRecordId)\n }, [isEmbedded, propRecordId])\n\n const { data: entities, isLoading: loadingEntities, error: entitiesError } = useQuery<{ items: EntityOption[] }>({\n queryKey: ['entities-list', scopeVersion],\n enabled: !isEmbedded,\n queryFn: async () =>\n readApiResultOrThrow('/api/entities/entities', undefined, {\n errorMessage: t('translations.manager.errors.loadEntities', 'Failed to load entities'),\n }),\n })\n\n const entitySuggestions = React.useMemo(\n () =>\n (entities?.items || []).map((item) => ({\n value: item.entityId,\n label: formatEntityLabel(item.entityId, item.label),\n description: item.entityId,\n })),\n [entities],\n )\n\n const resolveEntityLabel = React.useCallback(\n (value: string) => {\n const match = entities?.items?.find((e) => e.entityId === value)\n return match ? formatEntityLabel(match.entityId, match.label) : formatEntityLabel(value)\n },\n [entities],\n )\n\n const listUrl = React.useMemo(() => entityType ? buildEntityListUrl(entityType) : null, [entityType])\n\n const loadRecordSuggestions = React.useCallback(\n async (query?: string) => {\n if (!entityType || !listUrl) return []\n const url = `${listUrl}?pageSize=20${query ? `&search=${encodeURIComponent(query)}` : ''}`\n const res = await apiCall<{ items: Array<Record<string, unknown>> }>(url)\n if (!res.ok) return []\n const items = res.result?.items ?? []\n return items.map((item) => ({\n value: String(item.id ?? ''),\n label: getRecordLabel(item),\n }))\n },\n [entityType, listUrl],\n )\n\n const { data: recordData } = useQuery<Record<string, unknown> | null>({\n queryKey: ['translation-record-data', entityType, recordId, listUrl, scopeVersion],\n enabled: !isEmbedded && !!entityType && !!recordId && !!listUrl,\n queryFn: async () => {\n const res = await apiCall<{ items: Array<Record<string, unknown>> }>(\n // Some APIs filter by `id` (catalog), others by `ids` (resources) \u2014 send both so the one recognized by the target route's buildFilters is applied\n `${listUrl}?id=${encodeURIComponent(recordId)}&ids=${encodeURIComponent(recordId)}&pageSize=1`,\n )\n if (!res.ok) return null\n const items = res.result?.items\n return Array.isArray(items) && items.length > 0 ? items[0] : null\n },\n })\n\n const baseValues = isEmbedded ? (propBaseValues ?? {}) : (recordData ?? {})\n\n const resolveRecordLabel = React.useCallback(\n (value: string) => {\n if (recordData) return getRecordLabel(recordData)\n return value\n },\n [recordData],\n )\n\n const { data: fieldDefs = [], isLoading: loadingFieldDefs } = useCustomFieldDefs(entityType ? [entityType] : [], {\n enabled: !!entityType,\n })\n\n const fieldList = React.useMemo(\n () => resolveFieldList(entityType, propTranslatableFields, fieldDefs as Array<{ key: string; kind: string; label?: string }>),\n [entityType, propTranslatableFields, fieldDefs],\n )\n\n const {\n data: translationData,\n isLoading: loadingTranslation,\n isError: translationError,\n refetch: refetchTranslation,\n } = useQuery<TranslationsResponse | null>({\n queryKey: ['entity-translation', entityType, recordId, scopeVersion],\n enabled: !!entityType && !!recordId,\n queryFn: async () => {\n const res = await apiCall<TranslationsResponse>(\n `/api/translations/${encodeURIComponent(entityType)}/${encodeURIComponent(recordId)}`,\n )\n if (!res.ok) {\n if (res.response?.status === 404) return null\n return null\n }\n return res.result ?? null\n },\n })\n\n const translationSignature = React.useMemo(() => JSON.stringify(translationData ?? null), [translationData])\n const lastTranslationSignatureRef = React.useRef<string | null>(null)\n\n React.useEffect(() => {\n const sig = translationSignature\n if (sig === lastTranslationSignatureRef.current && hasUserEdited) return\n lastTranslationSignatureRef.current = sig\n\n if (!translationData?.translations) {\n if (!hasUserEdited) setEditedTranslations({})\n return\n }\n\n const parsed: Record<string, Record<string, string>> = {}\n for (const [locale, fields] of Object.entries(translationData.translations)) {\n if (!fields || typeof fields !== 'object') continue\n parsed[locale] = {}\n for (const [key, val] of Object.entries(fields)) {\n parsed[locale][key] = typeof val === 'string' ? val : ''\n }\n }\n if (!hasUserEdited) setEditedTranslations(parsed)\n }, [translationSignature, translationData, hasUserEdited])\n\n const mutation = useMutation({\n mutationFn: async () => {\n if (!entityType || !recordId) {\n throw new Error(t('translations.manager.errors.selectRecord', 'Select an entity and record before saving'))\n }\n const body: Record<string, Record<string, string | null>> = {}\n for (const [locale, fields] of Object.entries(editedTranslations)) {\n const localeFields: Record<string, string | null> = {}\n let hasValues = false\n for (const [key, val] of Object.entries(fields)) {\n if (val && val.trim().length > 0) {\n localeFields[key] = val.trim()\n hasValues = true\n }\n }\n if (hasValues) body[locale] = localeFields\n }\n const res = await apiCall(\n `/api/translations/${encodeURIComponent(entityType)}/${encodeURIComponent(recordId)}`,\n {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n },\n )\n if (!res.ok) {\n throw new Error(t('translations.manager.errors.save', 'Failed to save translations'))\n }\n return true\n },\n onSuccess: () => {\n flash(t('translations.manager.flash.saved', 'Translations saved'), 'success')\n setHasUserEdited(false)\n void refetchTranslation()\n },\n onError: (err: unknown) => {\n const message = err instanceof Error ? err.message : t('translations.manager.errors.save', 'Failed to save translations')\n flash(message, 'error')\n },\n })\n\n const updateFieldValue = (locale: string, fieldKey: string, value: string) => {\n setHasUserEdited(true)\n setEditedTranslations((prev) => ({\n ...prev,\n [locale]: {\n ...prev[locale],\n [fieldKey]: value,\n },\n }))\n }\n\n const getBaseValue = (fieldKey: string): string => resolveBaseValue(baseValues, fieldKey)\n\n const renderRecordPicker = () => {\n if (isEmbedded) return null\n\n return (\n <div className=\"space-y-2\">\n <label className=\"text-xs text-muted-foreground\">\n {t('translations.manager.selectRecord', 'Select record')}\n </label>\n <ComboboxInput\n value={selectedRecordId}\n onChange={(next) => {\n setSelectedRecordId(next)\n setHasUserEdited(false)\n }}\n placeholder={t('translations.manager.searchRecords', 'Search records...')}\n loadSuggestions={loadRecordSuggestions}\n resolveLabel={resolveRecordLabel}\n allowCustomValues\n disabled={!entityType}\n />\n </div>\n )\n }\n\n const renderLocaleTabs = () => (\n <div className=\"flex gap-1 border-b\">\n {locales.map((locale) => (\n <button\n key={locale}\n type=\"button\"\n className={`px-3 py-1.5 text-sm font-medium transition-colors ${\n activeLocale === locale\n ? 'border-b-2 border-primary text-primary'\n : 'text-muted-foreground hover:text-foreground'\n }`}\n onClick={() => setActiveLocale(locale)}\n >\n {locale.toUpperCase()}\n </button>\n ))}\n </div>\n )\n\n const renderFieldTable = () => {\n if (!entityType || !recordId) {\n return (\n <div className=\"rounded border bg-background/70 p-4 text-sm text-muted-foreground\">\n {t('translations.manager.selectFirst', 'Select an entity and record to manage translations.')}\n </div>\n )\n }\n if (loadingTranslation || loadingFieldDefs) {\n return (\n <LoadingMessage\n label={t('translations.manager.loadingTranslations', 'Loading translations...')}\n className=\"border-0 bg-transparent p-4\"\n />\n )\n }\n if (translationError) {\n return (\n <ErrorMessage\n label={t('translations.manager.errors.loadTranslation', 'Failed to load translations')}\n action={(\n <Button variant=\"outline\" size=\"sm\" onClick={() => void refetchTranslation()}>\n {t('translations.manager.actions.retry', 'Retry')}\n </Button>\n )}\n />\n )\n }\n if (!fieldList.length) {\n return (\n <div className=\"rounded border bg-background/70 p-4 text-sm text-muted-foreground\">\n {t('translations.manager.noFields', 'No translatable fields found for this entity type.')}\n </div>\n )\n }\n\n const localeTranslations = editedTranslations[activeLocale] ?? {}\n\n return (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full min-w-[480px] text-sm\">\n <thead>\n <tr className=\"text-xs uppercase tracking-wide text-muted-foreground\">\n <th className=\"px-3 py-2 text-left w-[140px]\">\n {t('translations.manager.fields.field', 'Field')}\n </th>\n {!compact && (\n <th className=\"px-3 py-2 text-left\">\n {t('translations.manager.fields.baseValue', 'Base value')}\n </th>\n )}\n <th className=\"px-3 py-2 text-left\">\n {t('translations.manager.fields.translation', 'Translation')} ({activeLocale.toUpperCase()})\n </th>\n </tr>\n </thead>\n <tbody>\n {fieldList.map((field) => {\n const baseVal = getBaseValue(field.key)\n const translatedVal = localeTranslations[field.key] ?? ''\n\n return (\n <tr key={field.key} className=\"border-t\">\n <td className=\"px-3 py-2 align-top text-xs font-medium text-muted-foreground\">\n {field.label}\n </td>\n {!compact && (\n <td className=\"px-3 py-2 align-top text-xs text-muted-foreground max-w-[200px]\">\n {baseVal ? (\n <span className=\"line-clamp-3\">{baseVal}</span>\n ) : (\n <span className=\"text-muted-foreground/50\">-</span>\n )}\n </td>\n )}\n <td className=\"px-3 py-2 align-top\">\n {field.multiline ? (\n <textarea\n className=\"flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\"\n rows={3}\n value={translatedVal}\n onChange={(e) => updateFieldValue(activeLocale, field.key, e.target.value)}\n placeholder={baseVal || field.label}\n />\n ) : (\n <Input\n value={translatedVal}\n onChange={(e) => updateFieldValue(activeLocale, field.key, e.target.value)}\n placeholder={baseVal || field.label}\n />\n )}\n </td>\n </tr>\n )\n })}\n </tbody>\n </table>\n </div>\n )\n }\n\n React.useEffect(() => {\n const handler = (e: KeyboardEvent) => {\n if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {\n e.preventDefault()\n if (entityType && recordId && !mutation.isPending) mutation.mutate()\n }\n }\n document.addEventListener('keydown', handler)\n return () => document.removeEventListener('keydown', handler)\n }, [entityType, recordId, mutation])\n\n if (compact) {\n return (\n <div className=\"space-y-3\">\n {renderLocaleTabs()}\n {renderFieldTable()}\n <div className=\"flex justify-end\">\n <Button\n type=\"button\"\n size=\"sm\"\n onClick={() => mutation.mutate()}\n disabled={mutation.isPending || !entityType || !recordId}\n data-testid=\"translations-save\"\n >\n <Save className=\"mr-2 h-3 w-3\" />\n {mutation.isPending\n ? t('translations.manager.actions.saving', 'Saving...')\n : t('translations.manager.actions.save', 'Save translations')}\n </Button>\n </div>\n </div>\n )\n }\n\n return (\n <div className=\"space-y-6\">\n <div className=\"flex flex-col gap-3 rounded-lg border bg-card p-4 shadow-sm\">\n <div className=\"space-y-2\">\n <h2 className=\"text-xl font-semibold\">{t('translations.manager.title', 'Translations')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('translations.manager.description', 'Manage translations for entity records across supported locales.')}\n </p>\n </div>\n\n {!isEmbedded && (\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start\">\n <div className=\"flex-1 space-y-3\">\n <div>\n <label className=\"text-xs text-muted-foreground\">\n {t('translations.manager.selectEntity', 'Choose entity')}\n </label>\n <div className=\"mt-1\">\n <ComboboxInput\n value={selectedEntityType}\n onChange={(next) => {\n setSelectedEntityType(next)\n setSelectedRecordId('')\n setHasUserEdited(false)\n }}\n placeholder={t('translations.manager.placeholder', 'Select an entity')}\n suggestions={entitySuggestions}\n resolveLabel={resolveEntityLabel}\n disabled={loadingEntities || !!entitiesError}\n />\n </div>\n {entitiesError && (\n <p className=\"mt-1 text-xs text-red-600\">\n {t('translations.manager.errors.loadEntities', 'Failed to load entities')}\n </p>\n )}\n </div>\n {renderRecordPicker()}\n </div>\n </div>\n )}\n\n <div className=\"rounded-lg border bg-background/70 p-4\">\n {renderLocaleTabs()}\n <div className=\"mt-3\">\n {renderFieldTable()}\n </div>\n </div>\n\n <div className=\"flex justify-end\">\n <Button\n type=\"button\"\n onClick={() => mutation.mutate()}\n disabled={mutation.isPending || loadingEntities || !!entitiesError || !entityType || !recordId}\n data-testid=\"translations-save\"\n >\n <Save className=\"mr-2 h-4 w-4\" />\n {mutation.isPending\n ? t('translations.manager.actions.saving', 'Saving...')\n : t('translations.manager.actions.save', 'Save translations')}\n </Button>\n </div>\n </div>\n </div>\n )\n}\n\nexport function LocaleManager() {\n const t = useT()\n const queryClient = useQueryClient()\n const { data: locales = [], isLoading } = useTranslationLocales()\n const [newLocale, setNewLocale] = React.useState('')\n\n const mutation = useMutation({\n mutationFn: async (updatedLocales: string[]) => {\n const res = await apiCall<{ locales: string[] }>('/api/translations/locales', {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ locales: updatedLocales }),\n })\n if (!res.ok) throw new Error('Failed to save locales')\n return res.result?.locales ?? updatedLocales\n },\n onSuccess: (result) => {\n queryClient.setQueryData(['translation-locales'], result)\n flash(t('translations.locales.flash.saved', 'Locales updated'), 'success')\n },\n onError: () => {\n flash(t('translations.locales.flash.error', 'Failed to update locales'), 'error')\n },\n })\n\n const availableLocales = React.useMemo(\n () => ISO_639_1.filter((entry) => !locales.includes(entry.code)).map((entry) => ({\n value: entry.code,\n label: `${entry.code.toUpperCase()} \u2014 ${entry.label}`,\n })),\n [locales],\n )\n\n const addLocale = () => {\n const code = newLocale.toLowerCase().trim()\n if (!code || !isValidIso639(code) || locales.includes(code)) return\n mutation.mutate([...locales, code])\n setNewLocale('')\n }\n\n const removeLocale = (locale: string) => {\n if (locales.length <= 1) return\n mutation.mutate(locales.filter((l) => l !== locale))\n }\n\n if (isLoading) {\n return <LoadingMessage label={t('translations.locales.loading', 'Loading locales...')} className=\"border-0 bg-transparent p-4\" />\n }\n\n return (\n <div className=\"flex flex-col gap-3 rounded-lg border bg-card p-4 shadow-sm\">\n <div className=\"space-y-1\">\n <h3 className=\"text-lg font-semibold\">{t('translations.locales.title', 'Supported locales')}</h3>\n <p className=\"text-sm text-muted-foreground\">\n {t('translations.locales.description', 'Configure which locales are available for translations. Add ISO language codes (e.g. fr, it, ja, zh).')}\n </p>\n </div>\n\n <div className=\"flex flex-wrap gap-2\">\n {locales.map((locale) => (\n <span\n key={locale}\n className=\"inline-flex items-center gap-1.5 rounded-full border bg-muted/50 px-3 py-1 text-sm font-medium\"\n title={getIso639Label(locale) ?? locale}\n >\n {locale.toUpperCase()}{getIso639Label(locale) ? ` \u2014 ${getIso639Label(locale)}` : ''}\n {locales.length > 1 && (\n <button\n type=\"button\"\n className=\"rounded-full p-0.5 text-muted-foreground hover:text-foreground transition-colors\"\n onClick={() => removeLocale(locale)}\n disabled={mutation.isPending}\n >\n <X className=\"h-3 w-3\" />\n </button>\n )}\n </span>\n ))}\n </div>\n\n <div className=\"flex gap-2 items-center\">\n <div className=\"max-w-[240px] flex-1\">\n <ComboboxInput\n value={newLocale}\n onChange={setNewLocale}\n placeholder={t('translations.locales.addPlaceholder', 'Search language...')}\n suggestions={availableLocales}\n resolveLabel={(value) => {\n const label = getIso639Label(value)\n return label ? `${value.toUpperCase()} \u2014 ${label}` : value.toUpperCase()\n }}\n />\n </div>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={addLocale}\n disabled={mutation.isPending || !newLocale.trim() || !isValidIso639(newLocale) || locales.includes(newLocale.toLowerCase().trim())}\n >\n <Plus className=\"mr-1 h-3 w-3\" />\n {t('translations.locales.add', 'Add')}\n </Button>\n </div>\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.4.6-develop-15c18897fc",
3
+ "version": "0.4.6-develop-34aa847ce6",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -207,7 +207,7 @@
207
207
  }
208
208
  },
209
209
  "dependencies": {
210
- "@open-mercato/shared": "0.4.6-develop-15c18897fc",
210
+ "@open-mercato/shared": "0.4.6-develop-34aa847ce6",
211
211
  "@types/html-to-text": "^9.0.4",
212
212
  "@types/semver": "^7.5.8",
213
213
  "@xyflow/react": "^12.6.0",
@@ -8,6 +8,7 @@ import { signJwt } from '@open-mercato/shared/lib/auth/jwt'
8
8
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
9
9
  import type { EventBus } from '@open-mercato/events/types'
10
10
  import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
11
+ import { emitAuthEvent } from '@open-mercato/core/modules/auth/events'
11
12
  import { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'
12
13
  import { readEndpointRateLimitConfig } from '@open-mercato/shared/lib/ratelimit/config'
13
14
  import { checkAuthRateLimit, resetAuthRateLimit } from '@open-mercato/core/modules/auth/lib/rateLimitCheck'
@@ -62,10 +63,12 @@ export async function POST(req: Request) {
62
63
  user = users[0] ?? null
63
64
  }
64
65
  if (!user || !user.passwordHash) {
66
+ void emitAuthEvent('auth.login.failed', { email: parsed.data.email, reason: 'invalid_credentials' }).catch(() => undefined)
65
67
  return NextResponse.json({ ok: false, error: translate('auth.login.errors.invalidCredentials', 'Invalid email or password') }, { status: 401 })
66
68
  }
67
69
  const ok = await auth.verifyPassword(user, parsed.data.password)
68
70
  if (!ok) {
71
+ void emitAuthEvent('auth.login.failed', { email: parsed.data.email, reason: 'invalid_password' }).catch(() => undefined)
69
72
  return NextResponse.json({ ok: false, error: translate('auth.login.errors.invalidCredentials', 'Invalid email or password') }, { status: 401 })
70
73
  }
71
74
  // Optional role requirement
@@ -98,6 +101,7 @@ export async function POST(req: Request) {
98
101
  email: user.email,
99
102
  roles: userRoleNames
100
103
  })
104
+ void emitAuthEvent('auth.login.success', { id: String(user.id), email: user.email, tenantId: resolvedTenantId, organizationId: user.organizationId ? String(user.organizationId) : null }).catch(() => undefined)
101
105
  const responseData: { ok: true; token: string; redirect: string; refreshToken?: string } = {
102
106
  ok: true,
103
107
  token,
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Login form injection contract.
3
+ *
4
+ * Defines types that the login page exposes to widgets injected via
5
+ * the `auth.login:form` InjectionSpot. This allows enterprise modules
6
+ * (or any future auth provider) to extend the login flow without the
7
+ * core auth module knowing about specific providers.
8
+ */
9
+
10
+ /**
11
+ * Describes an alternative authentication method that has claimed the
12
+ * current login flow. When set, the login form hides the password field
13
+ * and delegates submission to the provider.
14
+ */
15
+ export type AuthOverride = {
16
+ /** Stable identifier for the provider (e.g., 'sso', 'social') */
17
+ providerId: string
18
+ /** Label shown on the submit button (e.g., 'Continue with SSO') */
19
+ providerLabel: string
20
+ /** Called instead of the normal password-based submit */
21
+ onSubmit: () => void
22
+ /** Whether to hide the password field */
23
+ hidePassword: boolean
24
+ /** Whether to hide the "Remember me" checkbox */
25
+ hideRememberMe: boolean
26
+ /** Whether to hide the "Forgot password?" link */
27
+ hideForgotPassword: boolean
28
+ }
29
+
30
+ /**
31
+ * Context passed to widgets injected into the login form.
32
+ * Widgets use these values and callbacks to participate in the login flow.
33
+ */
34
+ export type LoginFormWidgetContext = {
35
+ /** Current value of the email input */
36
+ email: string
37
+ /** Current tenant ID (from URL param or localStorage) */
38
+ tenantId: string | null
39
+ /** URL search params — widgets can read provider-specific error codes */
40
+ searchParams: URLSearchParams
41
+ /** Set or clear an alternative auth provider for the current email */
42
+ setAuthOverride: (override: AuthOverride | null) => void
43
+ /** Display an error message in the login form's error area */
44
+ setError: (error: string | null) => void
45
+ }