@open-mercato/core 0.6.1-develop.3291.1.6fad645fd0 → 0.6.1-develop.3306.1.9ad9ff2526

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.
Files changed (122) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +14 -0
  3. package/dist/modules/auth/backend/roles/[id]/edit/page.js +2 -1
  4. package/dist/modules/auth/backend/roles/[id]/edit/page.js.map +2 -2
  5. package/dist/modules/auth/backend/users/[id]/edit/page.js +2 -5
  6. package/dist/modules/auth/backend/users/[id]/edit/page.js.map +2 -2
  7. package/dist/modules/catalog/ai-agents.js +47 -5
  8. package/dist/modules/catalog/ai-agents.js.map +2 -2
  9. package/dist/modules/catalog/backend/catalog/categories/[id]/edit/page.js +2 -5
  10. package/dist/modules/catalog/backend/catalog/categories/[id]/edit/page.js.map +2 -2
  11. package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js +4 -3
  12. package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js.map +2 -2
  13. package/dist/modules/catalog/backend/catalog/products/[id]/page.js +2 -9
  14. package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
  15. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js +2 -9
  16. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js.map +2 -2
  17. package/dist/modules/customers/ai-agents.js +174 -1
  18. package/dist/modules/customers/ai-agents.js.map +2 -2
  19. package/dist/modules/customers/ai-tools/deal-analyzer-pack.js +196 -0
  20. package/dist/modules/customers/ai-tools/deal-analyzer-pack.js.map +7 -0
  21. package/dist/modules/customers/ai-tools.js +3 -1
  22. package/dist/modules/customers/ai-tools.js.map +2 -2
  23. package/dist/modules/customers/backend/customers/companies/page.js +11 -1
  24. package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
  25. package/dist/modules/customers/backend/customers/deals/page.js +11 -1
  26. package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
  27. package/dist/modules/customers/backend/customers/people/page.js +11 -1
  28. package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
  29. package/dist/modules/customers/commands/companies.js +37 -15
  30. package/dist/modules/customers/commands/companies.js.map +2 -2
  31. package/dist/modules/customers/components/detail/AiActionChips.js +2 -2
  32. package/dist/modules/customers/components/detail/AiActionChips.js.map +2 -2
  33. package/dist/modules/customers/components/detail/ChangelogTab.js +3 -9
  34. package/dist/modules/customers/components/detail/ChangelogTab.js.map +2 -2
  35. package/dist/modules/customers/components/detail/changelogValues.js +35 -0
  36. package/dist/modules/customers/components/detail/changelogValues.js.map +7 -0
  37. package/dist/modules/customers/components/detail/customFieldUtils.js +1 -1
  38. package/dist/modules/customers/components/detail/customFieldUtils.js.map +1 -1
  39. package/dist/modules/customers/components/formConfig.js +3 -2
  40. package/dist/modules/customers/components/formConfig.js.map +2 -2
  41. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js +4 -9
  42. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js.map +2 -2
  43. package/dist/modules/customers/widgets/injection/ai-deal-analyzer-trigger/widget.client.js +302 -0
  44. package/dist/modules/customers/widgets/injection/ai-deal-analyzer-trigger/widget.client.js.map +7 -0
  45. package/dist/modules/customers/widgets/injection/ai-deal-analyzer-trigger/widget.js +17 -0
  46. package/dist/modules/customers/widgets/injection/ai-deal-analyzer-trigger/widget.js.map +7 -0
  47. package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.js +2 -2
  48. package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.js.map +2 -2
  49. package/dist/modules/customers/widgets/injection-table.js +2 -2
  50. package/dist/modules/customers/widgets/injection-table.js.map +2 -2
  51. package/dist/modules/directory/backend/directory/organizations/[id]/edit/page.js +2 -5
  52. package/dist/modules/directory/backend/directory/organizations/[id]/edit/page.js.map +2 -2
  53. package/dist/modules/directory/backend/directory/tenants/[id]/edit/page.js +2 -5
  54. package/dist/modules/directory/backend/directory/tenants/[id]/edit/page.js.map +2 -2
  55. package/dist/modules/resources/backend/resources/resources/[id]/page.js +2 -5
  56. package/dist/modules/resources/backend/resources/resources/[id]/page.js.map +2 -2
  57. package/dist/modules/sales/backend/sales/channels/[channelId]/edit/page.js +2 -25
  58. package/dist/modules/sales/backend/sales/channels/[channelId]/edit/page.js.map +2 -2
  59. package/dist/modules/sales/components/channels/ChannelOfferForm.js +2 -43
  60. package/dist/modules/sales/components/channels/ChannelOfferForm.js.map +2 -2
  61. package/dist/modules/sales/components/documents/LineItemDialog.js +162 -42
  62. package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
  63. package/dist/modules/sales/components/documents/customFieldHelpers.js +4 -31
  64. package/dist/modules/sales/components/documents/customFieldHelpers.js.map +2 -2
  65. package/package.json +15 -6
  66. package/src/modules/auth/backend/roles/[id]/edit/page.tsx +2 -1
  67. package/src/modules/auth/backend/users/[id]/edit/page.tsx +2 -5
  68. package/src/modules/business_rules/migrations/.snapshot-open-mercato.json +746 -394
  69. package/src/modules/catalog/ai-agents.ts +62 -4
  70. package/src/modules/catalog/backend/catalog/categories/[id]/edit/page.tsx +2 -5
  71. package/src/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.tsx +4 -3
  72. package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +2 -12
  73. package/src/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.tsx +2 -10
  74. package/src/modules/catalog/migrations/.snapshot-open-mercato.json +2519 -1307
  75. package/src/modules/customer_accounts/migrations/.snapshot-open-mercato.json +828 -834
  76. package/src/modules/customers/AGENTS.md +6 -1
  77. package/src/modules/customers/ai-agents.ts +213 -6
  78. package/src/modules/customers/ai-tools/deal-analyzer-pack.ts +286 -0
  79. package/src/modules/customers/ai-tools.ts +2 -0
  80. package/src/modules/customers/backend/customers/companies/page.tsx +10 -0
  81. package/src/modules/customers/backend/customers/deals/page.tsx +10 -0
  82. package/src/modules/customers/backend/customers/people/page.tsx +10 -0
  83. package/src/modules/customers/commands/companies.ts +38 -14
  84. package/src/modules/customers/components/detail/AiActionChips.tsx +2 -2
  85. package/src/modules/customers/components/detail/ChangelogTab.tsx +3 -10
  86. package/src/modules/customers/components/detail/changelogValues.ts +42 -0
  87. package/src/modules/customers/components/detail/customFieldUtils.ts +1 -1
  88. package/src/modules/customers/components/formConfig.tsx +3 -2
  89. package/src/modules/customers/i18n/de.json +7 -0
  90. package/src/modules/customers/i18n/en.json +7 -0
  91. package/src/modules/customers/i18n/es.json +7 -0
  92. package/src/modules/customers/i18n/pl.json +7 -0
  93. package/src/modules/customers/migrations/.snapshot-open-mercato.json +2313 -2336
  94. package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.tsx +4 -6
  95. package/src/modules/customers/widgets/injection/ai-deal-analyzer-trigger/widget.client.tsx +358 -0
  96. package/src/modules/customers/widgets/injection/ai-deal-analyzer-trigger/widget.ts +34 -0
  97. package/src/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.tsx +2 -2
  98. package/src/modules/customers/widgets/injection-table.ts +12 -9
  99. package/src/modules/data_sync/migrations/.snapshot-open-mercato.json +554 -210
  100. package/src/modules/dictionaries/migrations/.snapshot-open-mercato.json +146 -146
  101. package/src/modules/directory/backend/directory/organizations/[id]/edit/page.tsx +2 -5
  102. package/src/modules/directory/backend/directory/tenants/[id]/edit/page.tsx +2 -5
  103. package/src/modules/directory/migrations/.snapshot-open-mercato.json +273 -140
  104. package/src/modules/feature_toggles/migrations/.snapshot-open-mercato.json +242 -137
  105. package/src/modules/integrations/migrations/.snapshot-open-mercato.json +498 -195
  106. package/src/modules/payment_gateways/migrations/.snapshot-open-mercato.json +158 -157
  107. package/src/modules/perspectives/migrations/.snapshot-open-mercato.json +274 -129
  108. package/src/modules/planner/migrations/.snapshot-open-mercato.json +327 -157
  109. package/src/modules/progress/AGENTS.md +72 -0
  110. package/src/modules/resources/backend/resources/resources/[id]/page.tsx +2 -5
  111. package/src/modules/resources/migrations/.snapshot-open-mercato.json +920 -470
  112. package/src/modules/sales/backend/sales/channels/[channelId]/edit/page.tsx +2 -27
  113. package/src/modules/sales/components/channels/ChannelOfferForm.tsx +2 -49
  114. package/src/modules/sales/components/documents/LineItemDialog.tsx +214 -78
  115. package/src/modules/sales/components/documents/customFieldHelpers.ts +4 -43
  116. package/src/modules/sales/i18n/de.json +3 -0
  117. package/src/modules/sales/i18n/en.json +3 -0
  118. package/src/modules/sales/i18n/es.json +3 -0
  119. package/src/modules/sales/i18n/pl.json +3 -0
  120. package/src/modules/sales/migrations/.snapshot-open-mercato.json +4740 -4778
  121. package/src/modules/shipping_carriers/migrations/.snapshot-open-mercato.json +340 -81
  122. package/src/modules/staff/migrations/.snapshot-open-mercato.json +1439 -729
@@ -9,6 +9,7 @@ import { updateCrud, deleteCrud } from '@open-mercato/ui/backend/utils/crud'
9
9
  import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
10
10
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
11
11
  import { useT } from '@open-mercato/shared/lib/i18n/context'
12
+ import { extractCustomFieldEntries } from '@open-mercato/shared/lib/crud/custom-fields-client'
12
13
  import { useChannelFields, buildChannelPayload, type ChannelFormValues } from '@open-mercato/core/modules/sales/components/channels/channelFormFields'
13
14
  import { E } from '#generated/entities.ids.generated'
14
15
  import { SalesChannelOffersPanel } from '@open-mercato/core/modules/sales/components/channels/SalesChannelOffersPanel'
@@ -196,31 +197,5 @@ function mapChannelToFormValues(item: Record<string, unknown>): ChannelFormValue
196
197
  : null,
197
198
  isActive: item.isActive === true || item.is_active === true,
198
199
  }
199
- const mergeCustomObject = (source: unknown) => {
200
- if (!source || typeof source !== 'object' || Array.isArray(source)) return
201
- for (const [key, value] of Object.entries(source as Record<string, unknown>)) {
202
- if (!key) continue
203
- values[`cf_${key}`] = value
204
- }
205
- }
206
- const mergeCustomArray = (source: unknown) => {
207
- if (!Array.isArray(source)) return
208
- source.forEach((entry) => {
209
- if (!entry || typeof entry !== 'object') return
210
- const key = typeof (entry as Record<string, unknown>).key === 'string'
211
- ? (entry as Record<string, unknown>).key
212
- : null
213
- if (!key) return
214
- values[`cf_${key}`] = (entry as Record<string, unknown>).value
215
- })
216
- }
217
- mergeCustomObject(item.customValues)
218
- mergeCustomObject((item as Record<string, unknown>).custom_values)
219
- mergeCustomObject(item.customFields)
220
- mergeCustomObject((item as Record<string, unknown>).custom_fields)
221
- mergeCustomArray(item.customFields)
222
- mergeCustomArray((item as Record<string, unknown>).custom_fields)
223
- mergeCustomArray((item as Record<string, unknown>).customFieldEntries)
224
- mergeCustomArray((item as Record<string, unknown>).custom_field_entries)
225
- return values
200
+ return { ...values, ...extractCustomFieldEntries(item) }
226
201
  }
@@ -20,6 +20,7 @@ import {
20
20
  } from '@open-mercato/ui/primitives/select'
21
21
  import { Loader2, Search, Image as ImageIcon, Trash2 } from 'lucide-react'
22
22
  import { useT } from '@open-mercato/shared/lib/i18n/context'
23
+ import { extractCustomFieldEntries } from '@open-mercato/shared/lib/crud/custom-fields-client'
23
24
  import { E } from '#generated/entities.ids.generated'
24
25
  import { buildAttachmentImageUrl, slugifyAttachmentFileName } from '@open-mercato/core/modules/attachments/lib/imageUrls'
25
26
  import { cn } from '@open-mercato/shared/lib/utils'
@@ -753,55 +754,7 @@ function collectPriceIds(source: PriceOverrideDraft[] | null | undefined): Set<s
753
754
  }
754
755
 
755
756
  function mergeCustomFieldValues(target: Record<string, unknown>, source: Record<string, unknown> | null | undefined) {
756
- if (!source || typeof source !== 'object') return
757
- const assign = (key: string | null | undefined, value: unknown) => {
758
- if (!key) return
759
- target[`cf_${key}`] = value
760
- }
761
- for (const [rawKey, rawValue] of Object.entries(source)) {
762
- if (rawKey.startsWith('cf_')) {
763
- if (rawKey.endsWith('__is_multi')) continue
764
- target[rawKey] = rawValue
765
- } else if (rawKey.startsWith('cf:')) {
766
- assign(rawKey.slice(3), rawValue)
767
- }
768
- }
769
- const customValues =
770
- (source as Record<string, unknown>).customValues ??
771
- (source as Record<string, unknown>).custom_values
772
- if (customValues && typeof customValues === 'object' && !Array.isArray(customValues)) {
773
- for (const [key, value] of Object.entries(customValues as Record<string, unknown>)) {
774
- assign(key, value)
775
- }
776
- }
777
- const customFields =
778
- (source as Record<string, unknown>).customFields ??
779
- (source as Record<string, unknown>).custom_fields
780
- if (Array.isArray(customFields)) {
781
- customFields.forEach((entry) => {
782
- if (!entry || typeof entry !== 'object') return
783
- const entryRecord = entry as Record<string, unknown>
784
- const key = typeof entryRecord.key === 'string' ? entryRecord.key : null
785
- if (!key) return
786
- assign(key, entryRecord.value)
787
- })
788
- } else if (customFields && typeof customFields === 'object') {
789
- for (const [key, value] of Object.entries(customFields as Record<string, unknown>)) {
790
- assign(key, value)
791
- }
792
- }
793
- const customEntries =
794
- (source as Record<string, unknown>).customFieldEntries ??
795
- (source as Record<string, unknown>).custom_field_entries
796
- if (Array.isArray(customEntries)) {
797
- customEntries.forEach((entry) => {
798
- if (!entry || typeof entry !== 'object') return
799
- const entryRecord = entry as Record<string, unknown>
800
- const key = typeof entryRecord.key === 'string' ? entryRecord.key : null
801
- if (!key) return
802
- assign(key, entryRecord.value)
803
- })
804
- }
757
+ Object.assign(target, extractCustomFieldEntries(source ?? {}))
805
758
  }
806
759
 
807
760
  function buildChannelOffersHref(channelId?: string | null): string {
@@ -23,6 +23,11 @@ import {
23
23
  } from "@open-mercato/ui/primitives/dialog";
24
24
  import { Button } from "@open-mercato/ui/primitives/button";
25
25
  import { Input } from "@open-mercato/ui/primitives/input";
26
+ import {
27
+ Alert,
28
+ AlertDescription,
29
+ AlertTitle,
30
+ } from "@open-mercato/ui/primitives/alert";
26
31
  import {
27
32
  Select,
28
33
  SelectContent,
@@ -350,6 +355,71 @@ function normalizeUnitPriceInputValue(value: number): string {
350
355
  return rounded.toString();
351
356
  }
352
357
 
358
+ function mapProductOption(item: Record<string, unknown>): ProductOption | null {
359
+ const id = typeof item.id === "string" ? item.id : null;
360
+ if (!id) return null;
361
+ const productItem = item as ApiProductItem;
362
+ const title =
363
+ typeof item.title === "string"
364
+ ? item.title
365
+ : typeof productItem.name === "string"
366
+ ? productItem.name
367
+ : id;
368
+ const sku = typeof productItem.sku === "string" ? productItem.sku : null;
369
+ const thumbnail =
370
+ typeof productItem.default_media_url === "string"
371
+ ? productItem.default_media_url
372
+ : typeof productItem.defaultMediaUrl === "string"
373
+ ? productItem.defaultMediaUrl
374
+ : null;
375
+ const pricing =
376
+ typeof productItem.pricing === "object" && productItem.pricing
377
+ ? productItem.pricing
378
+ : null;
379
+ const metadata =
380
+ typeof productItem.metadata === "object" && productItem.metadata
381
+ ? productItem.metadata
382
+ : null;
383
+ const pricingMeta = pricing as ApiPricingMetadata | null;
384
+ const metaMeta = metadata as ApiPricingMetadata | null;
385
+ const pricingTaxRateId =
386
+ typeof pricingMeta?.tax_rate_id === "string" &&
387
+ pricingMeta.tax_rate_id.trim().length
388
+ ? pricingMeta.tax_rate_id.trim()
389
+ : typeof pricingMeta?.taxRateId === "string" &&
390
+ pricingMeta.taxRateId.trim().length
391
+ ? pricingMeta.taxRateId.trim()
392
+ : null;
393
+ const metaTaxRateId =
394
+ typeof metaMeta?.taxRateId === "string" && metaMeta.taxRateId.trim().length
395
+ ? metaMeta.taxRateId.trim()
396
+ : typeof metaMeta?.tax_rate_id === "string" &&
397
+ metaMeta.tax_rate_id.trim().length
398
+ ? metaMeta.tax_rate_id.trim()
399
+ : null;
400
+ const taxRateValue = normalizeNumber(
401
+ pricingMeta?.tax_rate ??
402
+ pricingMeta?.taxRate ??
403
+ productItem.tax_rate ??
404
+ productItem.taxRate,
405
+ Number.NaN,
406
+ );
407
+ const uomFields = getUomProductFields(item);
408
+ return {
409
+ id,
410
+ title,
411
+ sku,
412
+ thumbnailUrl: thumbnail,
413
+ taxRateId: pricingTaxRateId ?? metaTaxRateId ?? null,
414
+ taxRate: Number.isFinite(taxRateValue) ? taxRateValue : null,
415
+ defaultUnit: uomFields.defaultUnit,
416
+ defaultSalesUnit: uomFields.defaultSalesUnit,
417
+ defaultSalesUnitQuantity: Number.isFinite(uomFields.defaultSalesUnitQuantity)
418
+ ? uomFields.defaultSalesUnitQuantity
419
+ : null,
420
+ };
421
+ }
422
+
353
423
  export function LineItemDialog({
354
424
  open,
355
425
  kind,
@@ -382,6 +452,8 @@ export function LineItemDialog({
382
452
  const [taxRates, setTaxRates] = React.useState<TaxRateOption[]>([]);
383
453
  const [lineStatuses, setLineStatuses] = React.useState<StatusOption[]>([]);
384
454
  const [unitOptions, setUnitOptions] = React.useState<UnitOption[]>([]);
455
+ const [deletedCatalogReference, setDeletedCatalogReference] =
456
+ React.useState(false);
385
457
  const [, setLineStatusLoading] = React.useState(false);
386
458
  const productOptionsRef = React.useRef<Map<string, ProductOption>>(new Map());
387
459
  const variantOptionsRef = React.useRef<Map<string, VariantOption>>(new Map());
@@ -477,6 +549,7 @@ export function LineItemDialog({
477
549
  setVariantOption(null);
478
550
  setPriceOptions([]);
479
551
  setUnitOptions([]);
552
+ setDeletedCatalogReference(false);
480
553
  setEditingId(null);
481
554
  setFormResetKey((prev) => prev + 1);
482
555
  },
@@ -539,6 +612,29 @@ export function LineItemDialog({
539
612
  }
540
613
  }, []);
541
614
 
615
+ const loadProductOptionById = React.useCallback(
616
+ async (productId: string): Promise<ProductOption | null> => {
617
+ if (!productId) return null;
618
+ const response = await apiCall<{
619
+ items?: Array<Record<string, unknown>>;
620
+ }>(
621
+ `/api/catalog/products?id=${encodeURIComponent(productId)}&pageSize=1`,
622
+ undefined,
623
+ { fallback: { items: [] } },
624
+ );
625
+ const items = Array.isArray(response.result?.items)
626
+ ? response.result.items
627
+ : [];
628
+ const matched =
629
+ items.find((entry) => entry.id === productId) ?? items[0] ?? null;
630
+ if (!matched) return null;
631
+ const option = mapProductOption(matched);
632
+ if (option) productOptionsRef.current.set(option.id, option);
633
+ return option;
634
+ },
635
+ [],
636
+ );
637
+
542
638
  const loadProductOptions = React.useCallback(
543
639
  async (query?: string): Promise<LookupSelectItem[]> => {
544
640
  const params = new URLSearchParams({ pageSize: "8" });
@@ -557,88 +653,22 @@ export function LineItemDialog({
557
653
  : [];
558
654
  const mapped = items
559
655
  .map((item) => {
560
- const id = typeof item.id === "string" ? item.id : null;
561
- if (!id) return null;
562
- const productItem = item as ApiProductItem;
563
- const title =
564
- typeof item.title === "string"
565
- ? item.title
566
- : typeof productItem.name === "string"
567
- ? productItem.name
568
- : id;
569
- const sku =
570
- typeof productItem.sku === "string" ? productItem.sku : null;
571
- const thumbnail =
572
- typeof productItem.default_media_url === "string"
573
- ? productItem.default_media_url
574
- : typeof productItem.defaultMediaUrl === "string"
575
- ? productItem.defaultMediaUrl
576
- : null;
577
- const pricing =
578
- typeof productItem.pricing === "object" && productItem.pricing
579
- ? productItem.pricing
580
- : null;
581
- const metadata =
582
- typeof productItem.metadata === "object" && productItem.metadata
583
- ? productItem.metadata
584
- : null;
585
- const pricingMeta = pricing as ApiPricingMetadata | null;
586
- const metaMeta = metadata as ApiPricingMetadata | null;
587
- const pricingTaxRateId =
588
- typeof pricingMeta?.tax_rate_id === "string" &&
589
- pricingMeta.tax_rate_id.trim().length
590
- ? pricingMeta.tax_rate_id.trim()
591
- : typeof pricingMeta?.taxRateId === "string" &&
592
- pricingMeta.taxRateId.trim().length
593
- ? pricingMeta.taxRateId.trim()
594
- : null;
595
- const metaTaxRateId =
596
- typeof metaMeta?.taxRateId === "string" &&
597
- metaMeta.taxRateId.trim().length
598
- ? metaMeta.taxRateId.trim()
599
- : typeof metaMeta?.tax_rate_id === "string" &&
600
- metaMeta.tax_rate_id.trim().length
601
- ? metaMeta.tax_rate_id.trim()
602
- : null;
603
- const taxRateValue = normalizeNumber(
604
- pricingMeta?.tax_rate ??
605
- pricingMeta?.taxRate ??
606
- productItem.tax_rate ??
607
- productItem.taxRate,
608
- Number.NaN,
609
- );
610
- const uomFields = getUomProductFields(item);
611
- const defaultUnit = uomFields.defaultUnit;
612
- const defaultSalesUnit = uomFields.defaultSalesUnit;
613
- const defaultSalesUnitQuantity = uomFields.defaultSalesUnitQuantity;
656
+ const option = mapProductOption(item);
657
+ if (!option) return null;
614
658
  return {
615
- id,
616
- title,
617
- subtitle: sku ?? undefined,
618
- icon: thumbnail ? (
659
+ id: option.id,
660
+ title: option.title,
661
+ subtitle: option.sku ?? undefined,
662
+ icon: option.thumbnailUrl ? (
619
663
  <img
620
- src={thumbnail}
621
- alt={title}
664
+ src={option.thumbnailUrl}
665
+ alt={option.title}
622
666
  className="h-8 w-8 rounded object-cover"
623
667
  />
624
668
  ) : (
625
- buildPlaceholder(title)
669
+ buildPlaceholder(option.title)
626
670
  ),
627
- option: {
628
- id,
629
- title,
630
- sku,
631
- thumbnailUrl: thumbnail,
632
- taxRateId: pricingTaxRateId ?? metaTaxRateId ?? null,
633
- taxRate: Number.isFinite(taxRateValue) ? taxRateValue : null,
634
- defaultUnit,
635
- defaultSalesUnit,
636
- defaultSalesUnitQuantity: Number.isFinite(
637
- defaultSalesUnitQuantity,
638
- )
639
- ? defaultSalesUnitQuantity
640
- : null,
641
- } satisfies ProductOption,
671
+ option,
642
672
  } as LookupSelectItem & { option: ProductOption };
643
673
  })
644
674
  .filter(
@@ -1474,6 +1504,18 @@ export function LineItemDialog({
1474
1504
  setFormValue?.("catalogSnapshot", null);
1475
1505
  setFormValue?.("quantityUnit", null);
1476
1506
  } else {
1507
+ if (deletedCatalogReference) {
1508
+ setDeletedCatalogReference(false);
1509
+ setProductOption(null);
1510
+ setVariantOption(null);
1511
+ setPriceOptions([]);
1512
+ setUnitOptions([]);
1513
+ setFormValue?.("productId", null);
1514
+ setFormValue?.("variantId", null);
1515
+ setFormValue?.("priceId", null);
1516
+ setFormValue?.("catalogSnapshot", null);
1517
+ setFormValue?.("quantityUnit", null);
1518
+ }
1477
1519
  setFormValue?.("unitPrice", "");
1478
1520
  setFormValue?.("priceMode", "gross");
1479
1521
  }
@@ -1508,6 +1550,35 @@ export function LineItemDialog({
1508
1550
  );
1509
1551
  },
1510
1552
  } satisfies CrudField,
1553
+ ...(deletedCatalogReference
1554
+ ? [
1555
+ {
1556
+ id: "deletedCatalogReference",
1557
+ label: t(
1558
+ "sales.documents.items.deletedCatalogReference.label",
1559
+ "Deleted catalog item",
1560
+ ),
1561
+ type: "custom",
1562
+ layout: "full",
1563
+ component: () => (
1564
+ <Alert status="information" style="lighter">
1565
+ <AlertTitle>
1566
+ {t(
1567
+ "sales.documents.items.deletedCatalogReference.title",
1568
+ "This catalog item was deleted",
1569
+ )}
1570
+ </AlertTitle>
1571
+ <AlertDescription>
1572
+ {t(
1573
+ "sales.documents.items.deletedCatalogReference.description",
1574
+ "The saved line data is kept and the line is shown as a custom line. Switch to Catalog item to replace it with an existing product.",
1575
+ )}
1576
+ </AlertDescription>
1577
+ </Alert>
1578
+ ),
1579
+ } satisfies CrudField,
1580
+ ]
1581
+ : []),
1511
1582
  ...(!isCustomLine
1512
1583
  ? [
1513
1584
  {
@@ -1528,6 +1599,7 @@ export function LineItemDialog({
1528
1599
  const selectedOption = next
1529
1600
  ? (productOptionsRef.current.get(next) ?? null)
1530
1601
  : null;
1602
+ if (next) setDeletedCatalogReference(false);
1531
1603
  setProductOption(selectedOption);
1532
1604
  setVariantOption(null);
1533
1605
  setPriceOptions([]);
@@ -2371,6 +2443,7 @@ export function LineItemDialog({
2371
2443
  loadProductOptions,
2372
2444
  loadVariantOptions,
2373
2445
  fetchLineStatusItems,
2446
+ deletedCatalogReference,
2374
2447
  priceLoading,
2375
2448
  priceOptions,
2376
2449
  productOption,
@@ -2403,6 +2476,8 @@ export function LineItemDialog({
2403
2476
  resetForm();
2404
2477
  return;
2405
2478
  }
2479
+ let cancelled = false;
2480
+ setDeletedCatalogReference(false);
2406
2481
  setEditingId(initialLine.id);
2407
2482
  const nextForm = defaultForm(initialLine.currencyCode ?? currencyCode);
2408
2483
  const meta = initialLine.metadata ?? {};
@@ -2615,18 +2690,74 @@ export function LineItemDialog({
2615
2690
  variantOptionsRef.current.set(initialLine.productVariantId, option);
2616
2691
  resolvedVariantOption = option;
2617
2692
  }
2618
- if (resolvedProductOption) setProductOption(resolvedProductOption);
2619
- if (resolvedVariantOption) setVariantOption(resolvedVariantOption);
2693
+ if (resolvedProductOption) {
2694
+ setProductOption(resolvedProductOption);
2695
+ } else {
2696
+ setProductOption(null);
2697
+ }
2698
+ if (resolvedVariantOption) {
2699
+ setVariantOption(resolvedVariantOption);
2700
+ } else {
2701
+ setVariantOption(null);
2702
+ }
2620
2703
  const customValues = extractCustomFieldValues(
2621
2704
  initialLine as Record<string, unknown>,
2622
2705
  );
2623
2706
  const merged = { ...nextForm, ...customValues };
2707
+ const markDeletedCatalogReference = () => {
2708
+ if (cancelled) return;
2709
+ const nextMerged = { ...merged, lineMode: "custom" as const };
2710
+ setDeletedCatalogReference(true);
2711
+ setProductOption(null);
2712
+ setVariantOption(null);
2713
+ setPriceOptions([]);
2714
+ setUnitOptions([]);
2715
+ setInitialValues(nextMerged);
2716
+ setLineMode("custom");
2717
+ setFormResetKey((prev) => prev + 1);
2718
+ };
2624
2719
  setInitialValues(merged);
2625
2720
  setLineMode(merged.lineMode);
2626
2721
  setFormResetKey((prev) => prev + 1);
2627
2722
  if (initialLine.productId) {
2723
+ void (async () => {
2724
+ try {
2725
+ const currentProduct = await loadProductOptionById(
2726
+ initialLine.productId as string,
2727
+ );
2728
+ if (cancelled) return;
2729
+ if (!currentProduct) {
2730
+ markDeletedCatalogReference();
2731
+ return;
2732
+ }
2733
+ if (merged.lineMode !== "custom") {
2734
+ setProductOption(currentProduct);
2735
+ }
2736
+ if (initialLine.productVariantId) {
2737
+ await loadVariantOptions(
2738
+ initialLine.productId as string,
2739
+ currentProduct.thumbnailUrl,
2740
+ );
2741
+ if (cancelled) return;
2742
+ const currentVariant =
2743
+ variantOptionsRef.current.get(initialLine.productVariantId) ??
2744
+ null;
2745
+ if (!currentVariant) {
2746
+ markDeletedCatalogReference();
2747
+ return;
2748
+ }
2749
+ if (merged.lineMode !== "custom") {
2750
+ setVariantOption(currentVariant);
2751
+ }
2752
+ }
2753
+ setDeletedCatalogReference(false);
2754
+ } catch (err) {
2755
+ console.error("sales.document.items.verifyCatalogReference", err);
2756
+ }
2757
+ })();
2628
2758
  void loadProductUnits(initialLine.productId, resolvedProductOption).then(
2629
2759
  (options) => {
2760
+ if (cancelled) return;
2630
2761
  const requestedUnit = normalizeUnitCode(nextForm.quantityUnit);
2631
2762
  if (
2632
2763
  requestedUnit &&
@@ -2649,12 +2780,17 @@ export function LineItemDialog({
2649
2780
  setPriceOptions([]);
2650
2781
  setUnitOptions([]);
2651
2782
  }
2783
+ return () => {
2784
+ cancelled = true;
2785
+ };
2652
2786
  }, [
2653
2787
  currencyCode,
2654
2788
  findTaxRateIdByValue,
2655
2789
  initialLine,
2790
+ loadProductOptionById,
2656
2791
  loadPrices,
2657
2792
  loadProductUnits,
2793
+ loadVariantOptions,
2658
2794
  open,
2659
2795
  resetForm,
2660
2796
  ]);
@@ -1,4 +1,5 @@
1
1
  import { normalizeCustomFieldResponse } from '@open-mercato/shared/lib/custom-fields/normalize'
2
+ import { extractCustomFieldEntries } from '@open-mercato/shared/lib/crud/custom-fields-client'
2
3
 
3
4
  export const normalizeCustomFieldSubmitValue = (value: unknown): unknown => {
4
5
  if (Array.isArray(value)) return value.filter((entry) => entry !== undefined)
@@ -24,47 +25,7 @@ export const prefixCustomFieldValues = (input?: Record<string, unknown> | null):
24
25
 
25
26
  export const extractCustomFieldValues = (source?: Record<string, unknown> | null): Record<string, unknown> => {
26
27
  if (!source || typeof source !== 'object') return {}
27
- const merged: Record<string, unknown> = {}
28
- const assign = (key: unknown, value: unknown) => {
29
- if (typeof key !== 'string') return
30
- const trimmed = key.trim()
31
- if (!trimmed.length) return
32
- const normalizedKey = trimmed.startsWith('cf_')
33
- ? trimmed
34
- : trimmed.startsWith('cf:')
35
- ? `cf_${trimmed.slice(3)}`
36
- : `cf_${trimmed}`
37
- if (normalizedKey.endsWith('__is_multi')) return
38
- merged[normalizedKey] = value
39
- }
40
-
41
- Object.entries(source).forEach(([key, value]) => {
42
- if (key.startsWith('cf_') || key.startsWith('cf:')) {
43
- assign(key, value)
44
- }
45
- })
46
-
47
- const customValues = (source as any).customValues ?? (source as any).custom_values
48
- if (customValues && typeof customValues === 'object' && !Array.isArray(customValues)) {
49
- Object.entries(customValues as Record<string, unknown>).forEach(([key, value]) => assign(key, value))
50
- }
51
-
52
- const customFields = (source as any).customFields ?? (source as any).custom_fields
53
- if (Array.isArray(customFields)) {
54
- ;(customFields as Array<Record<string, unknown>>).forEach((entry) => {
55
- const key =
56
- typeof entry?.key === 'string'
57
- ? entry.key
58
- : typeof (entry as any)?.id === 'string'
59
- ? (entry as any).id
60
- : null
61
- if (!key) return
62
- assign(key, (entry as any)?.value)
63
- })
64
- } else if (customFields && typeof customFields === 'object') {
65
- Object.entries(customFields as Record<string, unknown>).forEach(([key, value]) => assign(key, value))
66
- }
67
-
68
- const normalized = normalizeCustomFieldResponse(merged)
69
- return normalized ? prefixCustomFieldValues(normalized) : prefixCustomFieldValues(merged)
28
+ const extracted = extractCustomFieldEntries(source)
29
+ const normalized = normalizeCustomFieldResponse(extracted)
30
+ return normalized ? prefixCustomFieldValues(normalized) : prefixCustomFieldValues(extracted)
70
31
  }
@@ -925,6 +925,9 @@
925
925
  "sales.documents.items.created": "Position hinzugefügt.",
926
926
  "sales.documents.items.deleteConfirm": "Diese Position löschen?",
927
927
  "sales.documents.items.deleted": "Position entfernt.",
928
+ "sales.documents.items.deletedCatalogReference.description": "Die gespeicherten Positionsdaten bleiben erhalten und die Position wird als benutzerdefinierte Position angezeigt. Wechsle zu Katalogprodukt, um sie durch ein vorhandenes Produkt zu ersetzen.",
929
+ "sales.documents.items.deletedCatalogReference.label": "Gelöschtes Katalogprodukt",
930
+ "sales.documents.items.deletedCatalogReference.title": "Dieses Katalogprodukt wurde gelöscht",
928
931
  "sales.documents.items.editTitle": "Position bearbeiten",
929
932
  "sales.documents.items.empty": "Noch keine Positionen.",
930
933
  "sales.documents.items.errorCurrency": "Währung ist erforderlich.",
@@ -925,6 +925,9 @@
925
925
  "sales.documents.items.created": "Line added.",
926
926
  "sales.documents.items.deleteConfirm": "Delete this line item?",
927
927
  "sales.documents.items.deleted": "Line removed.",
928
+ "sales.documents.items.deletedCatalogReference.description": "The saved line data is kept and the line is shown as a custom line. Switch to Catalog item to replace it with an existing product.",
929
+ "sales.documents.items.deletedCatalogReference.label": "Deleted catalog item",
930
+ "sales.documents.items.deletedCatalogReference.title": "This catalog item was deleted",
928
931
  "sales.documents.items.editTitle": "Edit line",
929
932
  "sales.documents.items.empty": "No items yet.",
930
933
  "sales.documents.items.errorCurrency": "Currency is required.",
@@ -925,6 +925,9 @@
925
925
  "sales.documents.items.created": "Línea agregada.",
926
926
  "sales.documents.items.deleteConfirm": "¿Eliminar esta línea de artículo?",
927
927
  "sales.documents.items.deleted": "Línea eliminada.",
928
+ "sales.documents.items.deletedCatalogReference.description": "Los datos guardados de la línea se conservan y la línea se muestra como personalizada. Cambia a Artículo del catálogo para reemplazarla por un producto existente.",
929
+ "sales.documents.items.deletedCatalogReference.label": "Artículo de catálogo eliminado",
930
+ "sales.documents.items.deletedCatalogReference.title": "Este artículo del catálogo fue eliminado",
928
931
  "sales.documents.items.editTitle": "Editar línea",
929
932
  "sales.documents.items.empty": "Aún no hay artículos.",
930
933
  "sales.documents.items.errorCurrency": "La moneda es obligatoria.",
@@ -925,6 +925,9 @@
925
925
  "sales.documents.items.created": "Pozycja dodana.",
926
926
  "sales.documents.items.deleteConfirm": "Usunąć tę pozycję?",
927
927
  "sales.documents.items.deleted": "Pozycja usunięta.",
928
+ "sales.documents.items.deletedCatalogReference.description": "Zapisane dane pozycji pozostają bez zmian, a pozycja jest pokazana jako niestandardowa. Przełącz na Produkt z katalogu, aby zastąpić ją istniejącym produktem.",
929
+ "sales.documents.items.deletedCatalogReference.label": "Usunięty produkt katalogowy",
930
+ "sales.documents.items.deletedCatalogReference.title": "Ten produkt katalogowy został usunięty",
928
931
  "sales.documents.items.editTitle": "Edytuj pozycję",
929
932
  "sales.documents.items.empty": "Brak pozycji.",
930
933
  "sales.documents.items.errorCurrency": "Waluta jest wymagana.",