@open-mercato/core 0.5.1-develop.3036.f02c281f23 → 0.5.1-develop.3043.1a796c3920

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 (163) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +13 -1
  3. package/dist/helpers/integration/api.js +29 -16
  4. package/dist/helpers/integration/api.js.map +2 -2
  5. package/dist/helpers/integration/auth.js +11 -6
  6. package/dist/helpers/integration/auth.js.map +3 -3
  7. package/dist/modules/auth/commands/roles.js +9 -12
  8. package/dist/modules/auth/commands/roles.js.map +2 -2
  9. package/dist/modules/catalog/ai-agents-context.js +147 -0
  10. package/dist/modules/catalog/ai-agents-context.js.map +7 -0
  11. package/dist/modules/catalog/ai-agents.js +383 -0
  12. package/dist/modules/catalog/ai-agents.js.map +7 -0
  13. package/dist/modules/catalog/ai-tools/_shared.js +318 -0
  14. package/dist/modules/catalog/ai-tools/_shared.js.map +7 -0
  15. package/dist/modules/catalog/ai-tools/authoring-pack.js +391 -0
  16. package/dist/modules/catalog/ai-tools/authoring-pack.js.map +7 -0
  17. package/dist/modules/catalog/ai-tools/categories-pack.js +167 -0
  18. package/dist/modules/catalog/ai-tools/categories-pack.js.map +7 -0
  19. package/dist/modules/catalog/ai-tools/configuration-pack.js +120 -0
  20. package/dist/modules/catalog/ai-tools/configuration-pack.js.map +7 -0
  21. package/dist/modules/catalog/ai-tools/media-tags-pack.js +107 -0
  22. package/dist/modules/catalog/ai-tools/media-tags-pack.js.map +7 -0
  23. package/dist/modules/catalog/ai-tools/merchandising-pack.js +429 -0
  24. package/dist/modules/catalog/ai-tools/merchandising-pack.js.map +7 -0
  25. package/dist/modules/catalog/ai-tools/mutation-pack.js +576 -0
  26. package/dist/modules/catalog/ai-tools/mutation-pack.js.map +7 -0
  27. package/dist/modules/catalog/ai-tools/prices-offers-pack.js +208 -0
  28. package/dist/modules/catalog/ai-tools/prices-offers-pack.js.map +7 -0
  29. package/dist/modules/catalog/ai-tools/products-pack.js +298 -0
  30. package/dist/modules/catalog/ai-tools/products-pack.js.map +7 -0
  31. package/dist/modules/catalog/ai-tools/stats-pack.js +57 -0
  32. package/dist/modules/catalog/ai-tools/stats-pack.js.map +7 -0
  33. package/dist/modules/catalog/ai-tools/types.js +10 -0
  34. package/dist/modules/catalog/ai-tools/types.js.map +7 -0
  35. package/dist/modules/catalog/ai-tools/variants-pack.js +75 -0
  36. package/dist/modules/catalog/ai-tools/variants-pack.js.map +7 -0
  37. package/dist/modules/catalog/ai-tools.js +28 -0
  38. package/dist/modules/catalog/ai-tools.js.map +7 -0
  39. package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js +466 -0
  40. package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js.map +7 -0
  41. package/dist/modules/catalog/backend/catalog/products/page.js +7 -1
  42. package/dist/modules/catalog/backend/catalog/products/page.js.map +2 -2
  43. package/dist/modules/catalog/components/CatalogStatsCard.js +91 -0
  44. package/dist/modules/catalog/components/CatalogStatsCard.js.map +7 -0
  45. package/dist/modules/catalog/components/products/ProductsDataTable.js +23 -3
  46. package/dist/modules/catalog/components/products/ProductsDataTable.js.map +2 -2
  47. package/dist/modules/catalog/events.js +7 -4
  48. package/dist/modules/catalog/events.js.map +2 -2
  49. package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.js +59 -0
  50. package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.js.map +7 -0
  51. package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.js +17 -0
  52. package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.js.map +7 -0
  53. package/dist/modules/catalog/widgets/injection/product-seo/widget.client.js +1 -1
  54. package/dist/modules/catalog/widgets/injection/product-seo/widget.client.js.map +2 -2
  55. package/dist/modules/catalog/widgets/injection-table.js +13 -1
  56. package/dist/modules/catalog/widgets/injection-table.js.map +2 -2
  57. package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.js +94 -0
  58. package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.js.map +7 -0
  59. package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.js +17 -0
  60. package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.js.map +7 -0
  61. package/dist/modules/customer_accounts/widgets/injection-table.js +9 -0
  62. package/dist/modules/customer_accounts/widgets/injection-table.js.map +2 -2
  63. package/dist/modules/customers/ai-agents-context.js +96 -0
  64. package/dist/modules/customers/ai-agents-context.js.map +7 -0
  65. package/dist/modules/customers/ai-agents.js +244 -0
  66. package/dist/modules/customers/ai-agents.js.map +7 -0
  67. package/dist/modules/customers/ai-tools/activities-tasks-pack.js +1015 -0
  68. package/dist/modules/customers/ai-tools/activities-tasks-pack.js.map +7 -0
  69. package/dist/modules/customers/ai-tools/addresses-tags-pack.js +134 -0
  70. package/dist/modules/customers/ai-tools/addresses-tags-pack.js.map +7 -0
  71. package/dist/modules/customers/ai-tools/companies-pack.js +249 -0
  72. package/dist/modules/customers/ai-tools/companies-pack.js.map +7 -0
  73. package/dist/modules/customers/ai-tools/deals-pack.js +348 -0
  74. package/dist/modules/customers/ai-tools/deals-pack.js.map +7 -0
  75. package/dist/modules/customers/ai-tools/people-pack.js +261 -0
  76. package/dist/modules/customers/ai-tools/people-pack.js.map +7 -0
  77. package/dist/modules/customers/ai-tools/settings-pack.js +102 -0
  78. package/dist/modules/customers/ai-tools/settings-pack.js.map +7 -0
  79. package/dist/modules/customers/ai-tools/types.js +10 -0
  80. package/dist/modules/customers/ai-tools/types.js.map +7 -0
  81. package/dist/modules/customers/ai-tools.js +20 -0
  82. package/dist/modules/customers/ai-tools.js.map +7 -0
  83. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js +469 -0
  84. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js.map +7 -0
  85. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js +17 -0
  86. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js.map +7 -0
  87. package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.js +117 -0
  88. package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.js.map +7 -0
  89. package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.js +17 -0
  90. package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.js.map +7 -0
  91. package/dist/modules/customers/widgets/injection-table.js +26 -0
  92. package/dist/modules/customers/widgets/injection-table.js.map +7 -0
  93. package/dist/modules/inbox_ops/ai-tools.js +4 -0
  94. package/dist/modules/inbox_ops/ai-tools.js.map +2 -2
  95. package/dist/modules/inbox_ops/lib/llmProvider.js +52 -7
  96. package/dist/modules/inbox_ops/lib/llmProvider.js.map +2 -2
  97. package/dist/modules/notifications/setup.js +13 -0
  98. package/dist/modules/notifications/setup.js.map +7 -0
  99. package/jest.config.cjs +1 -0
  100. package/jest.setup.ts +18 -0
  101. package/package.json +5 -3
  102. package/src/helpers/integration/api.ts +38 -16
  103. package/src/helpers/integration/auth.ts +13 -6
  104. package/src/modules/auth/commands/roles.ts +10 -12
  105. package/src/modules/catalog/AGENTS.md +11 -0
  106. package/src/modules/catalog/ai-agents-context.ts +239 -0
  107. package/src/modules/catalog/ai-agents.ts +525 -0
  108. package/src/modules/catalog/ai-tools/_shared.ts +487 -0
  109. package/src/modules/catalog/ai-tools/authoring-pack.ts +600 -0
  110. package/src/modules/catalog/ai-tools/categories-pack.ts +192 -0
  111. package/src/modules/catalog/ai-tools/configuration-pack.ts +218 -0
  112. package/src/modules/catalog/ai-tools/media-tags-pack.ts +127 -0
  113. package/src/modules/catalog/ai-tools/merchandising-pack.ts +608 -0
  114. package/src/modules/catalog/ai-tools/mutation-pack.ts +761 -0
  115. package/src/modules/catalog/ai-tools/prices-offers-pack.ts +376 -0
  116. package/src/modules/catalog/ai-tools/products-pack.ts +387 -0
  117. package/src/modules/catalog/ai-tools/stats-pack.ts +84 -0
  118. package/src/modules/catalog/ai-tools/types.ts +81 -0
  119. package/src/modules/catalog/ai-tools/variants-pack.ts +147 -0
  120. package/src/modules/catalog/ai-tools.ts +78 -0
  121. package/src/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.tsx +597 -0
  122. package/src/modules/catalog/backend/catalog/products/page.tsx +23 -2
  123. package/src/modules/catalog/components/CatalogStatsCard.tsx +118 -0
  124. package/src/modules/catalog/components/products/ProductsDataTable.tsx +54 -6
  125. package/src/modules/catalog/events.ts +7 -4
  126. package/src/modules/catalog/i18n/de.json +17 -0
  127. package/src/modules/catalog/i18n/en.json +17 -0
  128. package/src/modules/catalog/i18n/es.json +17 -0
  129. package/src/modules/catalog/i18n/pl.json +17 -0
  130. package/src/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.tsx +109 -0
  131. package/src/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.ts +29 -0
  132. package/src/modules/catalog/widgets/injection/product-seo/widget.client.tsx +1 -1
  133. package/src/modules/catalog/widgets/injection-table.ts +12 -0
  134. package/src/modules/customer_accounts/i18n/de.json +5 -0
  135. package/src/modules/customer_accounts/i18n/en.json +5 -0
  136. package/src/modules/customer_accounts/i18n/es.json +5 -0
  137. package/src/modules/customer_accounts/i18n/pl.json +5 -0
  138. package/src/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.tsx +136 -0
  139. package/src/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.ts +43 -0
  140. package/src/modules/customer_accounts/widgets/injection-table.ts +9 -0
  141. package/src/modules/customers/AGENTS.md +13 -0
  142. package/src/modules/customers/ai-agents-context.ts +150 -0
  143. package/src/modules/customers/ai-agents.ts +355 -0
  144. package/src/modules/customers/ai-tools/activities-tasks-pack.ts +1248 -0
  145. package/src/modules/customers/ai-tools/addresses-tags-pack.ts +145 -0
  146. package/src/modules/customers/ai-tools/companies-pack.ts +362 -0
  147. package/src/modules/customers/ai-tools/deals-pack.ts +505 -0
  148. package/src/modules/customers/ai-tools/people-pack.ts +369 -0
  149. package/src/modules/customers/ai-tools/settings-pack.ts +121 -0
  150. package/src/modules/customers/ai-tools/types.ts +76 -0
  151. package/src/modules/customers/ai-tools.ts +34 -0
  152. package/src/modules/customers/i18n/de.json +25 -0
  153. package/src/modules/customers/i18n/en.json +25 -0
  154. package/src/modules/customers/i18n/es.json +25 -0
  155. package/src/modules/customers/i18n/pl.json +25 -0
  156. package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.tsx +580 -0
  157. package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.ts +36 -0
  158. package/src/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.tsx +191 -0
  159. package/src/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.ts +37 -0
  160. package/src/modules/customers/widgets/injection-table.ts +41 -0
  161. package/src/modules/inbox_ops/ai-tools.ts +4 -0
  162. package/src/modules/inbox_ops/lib/llmProvider.ts +83 -7
  163. package/src/modules/notifications/setup.ts +11 -0
@@ -1,13 +1,34 @@
1
1
  "use client"
2
2
 
3
+ import * as React from 'react'
3
4
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
4
- import ProductsDataTable from '../../../components/products/ProductsDataTable'
5
+ import ProductsDataTable, {
6
+ type ProductsDataTableSnapshot,
7
+ } from '../../../components/products/ProductsDataTable'
5
8
 
9
+ /**
10
+ * Step 5.15 — Phase 3 WS-D.
11
+ *
12
+ * The catalog merchandising AI trigger moved behind the widget-injection
13
+ * system and now mounts in `data-table:catalog.products:header`. The
14
+ * products list page no longer imports `MerchandisingAssistantSheet`,
15
+ * `hasAllFeatures`, or the `/api/auth/feature-check` polling helper —
16
+ * feature gating is handled by the injection widget's `features`
17
+ * metadata (`catalog.products.view` + `ai_assistant.view`). The snapshot
18
+ * subscription is kept so host-side observability hooks the DataTable's
19
+ * current filter/total count for future extensions.
20
+ */
6
21
  export default function CatalogProductsPage() {
22
+ const [, setSnapshot] = React.useState<ProductsDataTableSnapshot>({
23
+ search: '',
24
+ filterValues: {},
25
+ total: 0,
26
+ })
27
+
7
28
  return (
8
29
  <Page>
9
30
  <PageBody>
10
- <ProductsDataTable />
31
+ <ProductsDataTable onSnapshotChange={setSnapshot} />
11
32
  </PageBody>
12
33
  </Page>
13
34
  )
@@ -0,0 +1,118 @@
1
+ "use client"
2
+
3
+ /**
4
+ * Demo dynamic AI UI part.
5
+ *
6
+ * Registered as `catalog.stats-card`. Tools (`catalog.show_stats`) emit a
7
+ * `{ uiPart: { componentId: 'catalog.stats-card', payload: { ... } } }`
8
+ * envelope and the chat client renders this card inline. Serves as the
9
+ * canonical example for how third-party modules contribute custom AI UI
10
+ * parts: define a presentational React component, register it on the
11
+ * shared `defaultAiUiPartRegistry` once at module load, and the dispatcher
12
+ * needs zero special handling.
13
+ */
14
+
15
+ import * as React from 'react'
16
+ import { Boxes, FolderTree, PackageCheck, Tags } from 'lucide-react'
17
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
18
+ import {
19
+ defaultAiUiPartRegistry,
20
+ type AiUiPartProps,
21
+ } from '@open-mercato/ui/ai'
22
+
23
+ export interface CatalogStatsCardPayload {
24
+ products?: number
25
+ activeProducts?: number
26
+ categories?: number
27
+ tags?: number
28
+ generatedAt?: string
29
+ note?: string
30
+ }
31
+
32
+ function formatCount(value: unknown): string {
33
+ if (typeof value === 'number' && Number.isFinite(value)) {
34
+ return value.toLocaleString()
35
+ }
36
+ return '—'
37
+ }
38
+
39
+ function StatTile({
40
+ icon,
41
+ label,
42
+ value,
43
+ }: {
44
+ icon: React.ReactNode
45
+ label: string
46
+ value: string
47
+ }) {
48
+ return (
49
+ <div className="flex flex-col gap-1 rounded-md border border-border bg-card p-3">
50
+ <div className="flex items-center gap-1.5 text-xs uppercase tracking-wide text-muted-foreground">
51
+ {icon}
52
+ <span>{label}</span>
53
+ </div>
54
+ <div className="text-2xl font-semibold leading-none">{value}</div>
55
+ </div>
56
+ )
57
+ }
58
+
59
+ export function CatalogStatsCard({ payload }: AiUiPartProps) {
60
+ const t = useT()
61
+ const data = (payload ?? {}) as CatalogStatsCardPayload
62
+ return (
63
+ <div
64
+ className="rounded-lg border border-border bg-muted/30 p-3"
65
+ data-ai-ui-part="catalog.stats-card"
66
+ >
67
+ <div className="mb-2 flex items-center gap-2 text-sm font-medium">
68
+ <Boxes className="size-4 text-primary" aria-hidden />
69
+ <span>{t('catalog.stats.title', 'Catalog overview')}</span>
70
+ </div>
71
+ <div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
72
+ <StatTile
73
+ icon={<Boxes className="size-3" aria-hidden />}
74
+ label={t('catalog.stats.products', 'Products')}
75
+ value={formatCount(data.products)}
76
+ />
77
+ <StatTile
78
+ icon={<PackageCheck className="size-3" aria-hidden />}
79
+ label={t('catalog.stats.active', 'Active')}
80
+ value={formatCount(data.activeProducts)}
81
+ />
82
+ <StatTile
83
+ icon={<FolderTree className="size-3" aria-hidden />}
84
+ label={t('catalog.stats.categories', 'Categories')}
85
+ value={formatCount(data.categories)}
86
+ />
87
+ <StatTile
88
+ icon={<Tags className="size-3" aria-hidden />}
89
+ label={t('catalog.stats.tags', 'Tags')}
90
+ value={formatCount(data.tags)}
91
+ />
92
+ </div>
93
+ {data.note ? (
94
+ <p className="mt-2 text-xs text-muted-foreground">{data.note}</p>
95
+ ) : null}
96
+ {data.generatedAt ? (
97
+ <p className="mt-2 text-[10px] text-muted-foreground">
98
+ {t('catalog.stats.snapshotAt', 'Snapshot at {time}').replace('{time}', new Date(data.generatedAt).toLocaleString())}
99
+ </p>
100
+ ) : null}
101
+ </div>
102
+ )
103
+ }
104
+
105
+ let registered = false
106
+ /**
107
+ * Idempotent self-registration on the module-global UI-part registry.
108
+ * Mirrors the pattern in `@open-mercato/ui/ai/records/registry` —
109
+ * importing this file from a client module is enough for the registry
110
+ * to know about `catalog.stats-card`.
111
+ */
112
+ export function registerCatalogStatsCard(): void {
113
+ if (registered) return
114
+ registered = true
115
+ defaultAiUiPartRegistry.register('catalog.stats-card', CatalogStatsCard)
116
+ }
117
+
118
+ registerCatalogStatsCard()
@@ -17,6 +17,7 @@ import { BooleanIcon } from '@open-mercato/ui/backend/ValueIcons'
17
17
  import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
18
18
  import { useT } from '@open-mercato/shared/lib/i18n/context'
19
19
  import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
20
+ import { useAppEvent } from '@open-mercato/ui/backend/injection/useAppEvent'
20
21
  import { E } from '#generated/entities.ids.generated'
21
22
  import { ProductImageCell } from './ProductImageCell'
22
23
 
@@ -139,7 +140,31 @@ function renderPrice(pricing: PricingInfo | undefined, currency?: string | null,
139
140
  )
140
141
  }
141
142
 
142
- export default function ProductsDataTable() {
143
+ export type ProductsDataTableSnapshot = {
144
+ search: string
145
+ filterValues: FilterValues
146
+ total: number
147
+ }
148
+
149
+ export type ProductsDataTableProps = {
150
+ /**
151
+ * Extra actions rendered alongside the built-in Create button in the
152
+ * DataTable header. Used by the Step 4.9 AI merchandising sheet
153
+ * trigger without coupling DataTable to the AI module.
154
+ */
155
+ extraActions?: React.ReactNode
156
+ /**
157
+ * Optional callback invoked whenever the table's search / filter /
158
+ * total-matching snapshot changes. Used by the Step 4.9 AI merchandising
159
+ * sheet to form a selection-aware pageContext per spec §10.1.
160
+ */
161
+ onSnapshotChange?: (snapshot: ProductsDataTableSnapshot) => void
162
+ }
163
+
164
+ export default function ProductsDataTable({
165
+ extraActions,
166
+ onSnapshotChange,
167
+ }: ProductsDataTableProps = {}) {
143
168
  const t = useT()
144
169
  const { confirm, ConfirmDialogElement } = useConfirmDialog()
145
170
  const scopeVersion = useOrganizationScopeVersion()
@@ -153,6 +178,15 @@ export default function ProductsDataTable() {
153
178
  const [filterValues, setFilterValues] = React.useState<FilterValues>({})
154
179
  const [isLoading, setIsLoading] = React.useState(false)
155
180
  const [reloadToken, setReloadToken] = React.useState(0)
181
+ // Step 5.18 (spec §10 line 836, D18 demo): refresh the list when a
182
+ // catalog.product.* event arrives via the DOM event bridge. Confirmed
183
+ // AI bulk mutations (one `ai.action.confirmed` + one
184
+ // `catalog.product.updated` per record) and direct API writes both
185
+ // surface here so the table reflects the new state without a manual
186
+ // reload.
187
+ useAppEvent('catalog.product.*', () => {
188
+ setReloadToken((token) => token + 1)
189
+ })
156
190
  const [customFieldsetFilter, setCustomFieldsetFilter] = React.useState<string | null>(null)
157
191
  const { data: customFieldDefs = [] } = useCustomFieldDefs(ENTITY_ID, {
158
192
  keyExtras: [scopeVersion, reloadToken],
@@ -599,6 +633,11 @@ export default function ProductsDataTable() {
599
633
  }
600
634
  }, [confirm, t])
601
635
 
636
+ React.useEffect(() => {
637
+ if (!onSnapshotChange) return
638
+ onSnapshotChange({ search, filterValues, total })
639
+ }, [onSnapshotChange, search, filterValues, total])
640
+
602
641
  const currentParams = React.useMemo(() => Object.fromEntries(new URLSearchParams(queryParams)), [queryParams])
603
642
 
604
643
  const exportConfig = React.useMemo(() => ({
@@ -624,11 +663,14 @@ export default function ProductsDataTable() {
624
663
  isRefreshing: isLoading,
625
664
  }}
626
665
  actions={(
627
- <Button asChild>
628
- <Link href="/backend/catalog/products/create">
629
- {t('catalog.products.actions.create', 'Create')}
630
- </Link>
631
- </Button>
666
+ <div className="flex items-center gap-2">
667
+ {extraActions}
668
+ <Button asChild>
669
+ <Link href="/backend/catalog/products/create">
670
+ {t('catalog.products.actions.create', 'Create')}
671
+ </Link>
672
+ </Button>
673
+ </div>
632
674
  )}
633
675
  columns={columns}
634
676
  data={rows}
@@ -649,6 +691,12 @@ export default function ProductsDataTable() {
649
691
  page,
650
692
  sorting,
651
693
  scopeVersion,
694
+ // Step 5.15: surface `total` so the merchandising AI widget
695
+ // (rendered in `data-table:catalog.products:header`) can build
696
+ // a selection-aware pageContext per spec §10.1 without taking a
697
+ // dependency on the host page.
698
+ total,
699
+ totalMatching: total,
652
700
  }}
653
701
  pagination={{
654
702
  page,
@@ -6,10 +6,13 @@ import { createModuleEvents } from '@open-mercato/shared/modules/events'
6
6
  * Declares all events that can be emitted by the catalog module.
7
7
  */
8
8
  const events = [
9
- // Products
10
- { id: 'catalog.product.created', label: 'Product Created', entity: 'product', category: 'crud' },
11
- { id: 'catalog.product.updated', label: 'Product Updated', entity: 'product', category: 'crud' },
12
- { id: 'catalog.product.deleted', label: 'Product Deleted', entity: 'product', category: 'crud' },
9
+ // Products — Step 5.18 (spec §10 line 836, §9.8): catalog CRUD events
10
+ // bridge to the DataTable on /backend/catalog/catalog/products via the
11
+ // DOM event bridge so confirmed mutations (AI or otherwise) auto-refresh
12
+ // the list without a round-trip.
13
+ { id: 'catalog.product.created', label: 'Product Created', entity: 'product', category: 'crud', clientBroadcast: true },
14
+ { id: 'catalog.product.updated', label: 'Product Updated', entity: 'product', category: 'crud', clientBroadcast: true },
15
+ { id: 'catalog.product.deleted', label: 'Product Deleted', entity: 'product', category: 'crud', clientBroadcast: true },
13
16
  { id: 'catalog.product_unit_conversion.created', label: 'Product Unit Conversion Created', entity: 'product_unit_conversion', category: 'crud' },
14
17
  { id: 'catalog.product_unit_conversion.updated', label: 'Product Unit Conversion Updated', entity: 'product_unit_conversion', category: 'crud' },
15
18
  { id: 'catalog.product_unit_conversion.deleted', label: 'Product Unit Conversion Deleted', entity: 'product_unit_conversion', category: 'crud' },
@@ -85,6 +85,17 @@
85
85
  "catalog.errors.id_required": "Datensatzkennung ist erforderlich.",
86
86
  "catalog.errors.organization_required": "Organisationskontext ist erforderlich.",
87
87
  "catalog.errors.tenant_required": "Mandantenkontext ist erforderlich.",
88
+ "catalog.merchandising_assistant.context.filteredByCategory": "Filtered by category",
89
+ "catalog.merchandising_assistant.context.tags": "{count} tags",
90
+ "catalog.merchandising_assistant.dock.subtitle": "Catalog",
91
+ "catalog.merchandising_assistant.popover.heading": "AI assistants",
92
+ "catalog.merchandising_assistant.sheet.composerPlaceholder": "Frage nach Beschreibungen, Attributen, Titeln oder Preisideen…",
93
+ "catalog.merchandising_assistant.sheet.description": "Nur-Lese-Demo. Schlägt Beschreibungen, Attribute, Titel und Preisanpassungen für die aktuelle Auswahl vor. In dieser Phase werden keine Änderungen angewendet.",
94
+ "catalog.merchandising_assistant.sheet.dock": "Dock to side",
95
+ "catalog.merchandising_assistant.sheet.selectionPill": "Bearbeitet {count} Produkte",
96
+ "catalog.merchandising_assistant.sheet.title": "Katalog-Merchandising-Assistent",
97
+ "catalog.merchandising_assistant.trigger.ariaLabel": "KI-Merchandising-Assistent öffnen",
98
+ "catalog.merchandising_assistant.trigger.label": "AI",
88
99
  "catalog.module.description": "Verwalte wiederverwendbare Produkte, Varianten und Preise für Vertriebsdokumente.",
89
100
  "catalog.module.title": "Produktkatalog",
90
101
  "catalog.nav.group": "Katalog",
@@ -540,6 +551,12 @@
540
551
  "catalog.search.priceKind.promotion": "Aktion",
541
552
  "catalog.search.status.inactive": "Inaktiv",
542
553
  "catalog.search.variant.default": "Standard",
554
+ "catalog.stats.active": "Aktiv",
555
+ "catalog.stats.categories": "Kategorien",
556
+ "catalog.stats.products": "Produkte",
557
+ "catalog.stats.snapshotAt": "Stand: {time}",
558
+ "catalog.stats.tags": "Tags",
559
+ "catalog.stats.title": "Katalogübersicht",
543
560
  "catalog.variants.errors.skuExists": "SKU wird bereits verwendet.",
544
561
  "catalog.variants.form.barcodeLabel": "Barcode",
545
562
  "catalog.variants.form.barcodePlaceholder": "EAN, UPC usw.",
@@ -85,6 +85,17 @@
85
85
  "catalog.errors.id_required": "Record identifier is required.",
86
86
  "catalog.errors.organization_required": "Organization context is required.",
87
87
  "catalog.errors.tenant_required": "Tenant context is required.",
88
+ "catalog.merchandising_assistant.context.filteredByCategory": "Filtered by category",
89
+ "catalog.merchandising_assistant.context.tags": "{count} tags",
90
+ "catalog.merchandising_assistant.dock.subtitle": "Catalog",
91
+ "catalog.merchandising_assistant.popover.heading": "AI assistants",
92
+ "catalog.merchandising_assistant.sheet.composerPlaceholder": "Ask for descriptions, attributes, titles, or price ideas...",
93
+ "catalog.merchandising_assistant.sheet.description": "Read-only demo. Proposes descriptions, attributes, titles, and price adjustments for the current selection. No writes are applied in this phase.",
94
+ "catalog.merchandising_assistant.sheet.dock": "Dock to side",
95
+ "catalog.merchandising_assistant.sheet.selectionPill": "Acting on {count} products",
96
+ "catalog.merchandising_assistant.sheet.title": "Catalog merchandising assistant",
97
+ "catalog.merchandising_assistant.trigger.ariaLabel": "Open AI merchandising assistant",
98
+ "catalog.merchandising_assistant.trigger.label": "AI",
88
99
  "catalog.module.description": "Manage reusable products, variants, and pricing for sales documents.",
89
100
  "catalog.module.title": "Product Catalog",
90
101
  "catalog.nav.group": "Catalog",
@@ -540,6 +551,12 @@
540
551
  "catalog.search.priceKind.promotion": "Promotion",
541
552
  "catalog.search.status.inactive": "Inactive",
542
553
  "catalog.search.variant.default": "Default",
554
+ "catalog.stats.active": "Active",
555
+ "catalog.stats.categories": "Categories",
556
+ "catalog.stats.products": "Products",
557
+ "catalog.stats.snapshotAt": "Snapshot at {time}",
558
+ "catalog.stats.tags": "Tags",
559
+ "catalog.stats.title": "Catalog overview",
543
560
  "catalog.variants.errors.skuExists": "SKU already in use.",
544
561
  "catalog.variants.form.barcodeLabel": "Barcode",
545
562
  "catalog.variants.form.barcodePlaceholder": "EAN, UPC, etc.",
@@ -85,6 +85,17 @@
85
85
  "catalog.errors.id_required": "Se requiere el identificador del registro.",
86
86
  "catalog.errors.organization_required": "Se requiere el contexto de la organización.",
87
87
  "catalog.errors.tenant_required": "Se requiere el contexto del inquilino.",
88
+ "catalog.merchandising_assistant.context.filteredByCategory": "Filtered by category",
89
+ "catalog.merchandising_assistant.context.tags": "{count} tags",
90
+ "catalog.merchandising_assistant.dock.subtitle": "Catalog",
91
+ "catalog.merchandising_assistant.popover.heading": "AI assistants",
92
+ "catalog.merchandising_assistant.sheet.composerPlaceholder": "Pide descripciones, atributos, títulos o ideas de precios…",
93
+ "catalog.merchandising_assistant.sheet.description": "Demo de solo lectura. Propone descripciones, atributos, títulos y ajustes de precios para la selección actual. En esta fase no se aplican cambios.",
94
+ "catalog.merchandising_assistant.sheet.dock": "Dock to side",
95
+ "catalog.merchandising_assistant.sheet.selectionPill": "Actuando sobre {count} productos",
96
+ "catalog.merchandising_assistant.sheet.title": "Asistente de merchandising del catálogo",
97
+ "catalog.merchandising_assistant.trigger.ariaLabel": "Abrir el asistente de merchandising IA",
98
+ "catalog.merchandising_assistant.trigger.label": "AI",
88
99
  "catalog.module.description": "Administra productos reutilizables, variantes y precios para los documentos de ventas.",
89
100
  "catalog.module.title": "Catálogo de productos",
90
101
  "catalog.nav.group": "Catálogo",
@@ -540,6 +551,12 @@
540
551
  "catalog.search.priceKind.promotion": "Promoción",
541
552
  "catalog.search.status.inactive": "Inactivo",
542
553
  "catalog.search.variant.default": "Predeterminado",
554
+ "catalog.stats.active": "Activos",
555
+ "catalog.stats.categories": "Categorías",
556
+ "catalog.stats.products": "Productos",
557
+ "catalog.stats.snapshotAt": "Captura en {time}",
558
+ "catalog.stats.tags": "Etiquetas",
559
+ "catalog.stats.title": "Resumen del catálogo",
543
560
  "catalog.variants.errors.skuExists": "El SKU ya está en uso.",
544
561
  "catalog.variants.form.barcodeLabel": "Código de barras",
545
562
  "catalog.variants.form.barcodePlaceholder": "EAN, UPC, etc.",
@@ -85,6 +85,17 @@
85
85
  "catalog.errors.id_required": "Wymagany jest identyfikator rekordu.",
86
86
  "catalog.errors.organization_required": "Wymagany jest kontekst organizacji.",
87
87
  "catalog.errors.tenant_required": "Wymagany jest kontekst dzierżawy.",
88
+ "catalog.merchandising_assistant.context.filteredByCategory": "Filtered by category",
89
+ "catalog.merchandising_assistant.context.tags": "{count} tags",
90
+ "catalog.merchandising_assistant.dock.subtitle": "Catalog",
91
+ "catalog.merchandising_assistant.popover.heading": "AI assistants",
92
+ "catalog.merchandising_assistant.sheet.composerPlaceholder": "Poproś o opisy, atrybuty, tytuły lub propozycje cen…",
93
+ "catalog.merchandising_assistant.sheet.description": "Demo tylko do odczytu. Proponuje opisy, atrybuty, tytuły i korekty cen dla bieżącego wyboru. Na tym etapie żadne zmiany nie są zapisywane.",
94
+ "catalog.merchandising_assistant.sheet.dock": "Dock to side",
95
+ "catalog.merchandising_assistant.sheet.selectionPill": "Działa na {count} produktach",
96
+ "catalog.merchandising_assistant.sheet.title": "Asystent merchandisingu katalogu",
97
+ "catalog.merchandising_assistant.trigger.ariaLabel": "Otwórz asystenta merchandisingu AI",
98
+ "catalog.merchandising_assistant.trigger.label": "AI",
88
99
  "catalog.module.description": "Zarządzaj produktami, wariantami i cennikami wykorzystywanymi w sprzedaży.",
89
100
  "catalog.module.title": "Katalog produktów",
90
101
  "catalog.nav.group": "Katalog",
@@ -540,6 +551,12 @@
540
551
  "catalog.search.priceKind.promotion": "Promocja",
541
552
  "catalog.search.status.inactive": "Nieaktywny",
542
553
  "catalog.search.variant.default": "Domyślny",
554
+ "catalog.stats.active": "Aktywne",
555
+ "catalog.stats.categories": "Kategorie",
556
+ "catalog.stats.products": "Produkty",
557
+ "catalog.stats.snapshotAt": "Migawka o {time}",
558
+ "catalog.stats.tags": "Tagi",
559
+ "catalog.stats.title": "Przegląd katalogu",
543
560
  "catalog.variants.errors.skuExists": "SKU jest już używany.",
544
561
  "catalog.variants.form.barcodeLabel": "Kod kreskowy",
545
562
  "catalog.variants.form.barcodePlaceholder": "EAN, UPC itp.",
@@ -0,0 +1,109 @@
1
+ "use client"
2
+
3
+ /**
4
+ * Step 5.15 — Catalog merchandising AiChat injection widget.
5
+ *
6
+ * Reuses the Step 4.9 `MerchandisingAssistantSheet` component but loads
7
+ * it through the widget-injection system instead of being imported
8
+ * directly from the products list page. `pageContext` follows spec §10.1
9
+ * exactly and is derived from the DataTable's injection context (filter
10
+ * snapshot + total-matching count). Selection data is not exposed by the
11
+ * shared DataTable today (Phase 2 contract); `selectedCount` ships as 0
12
+ * until the host lifts selection into injection context.
13
+ */
14
+
15
+ import * as React from 'react'
16
+ import MerchandisingAssistantSheet, {
17
+ type MerchandisingPageContext,
18
+ type MerchandisingPageContextFilter,
19
+ } from '../../../backend/catalog/products/MerchandisingAssistantSheet'
20
+
21
+ interface HostInjectionContext {
22
+ search?: string
23
+ filters?: {
24
+ categoryIds?: unknown
25
+ tagIds?: unknown
26
+ status?: unknown
27
+ }
28
+ customFieldset?: string | null
29
+ page?: number
30
+ sorting?: unknown
31
+ scopeVersion?: unknown
32
+ total?: number | string
33
+ totalMatching?: number | string
34
+ /** Selected row IDs from DataTable (auto-enriched when bulk actions are present). */
35
+ _selectedRowIds?: string[]
36
+ _selectedCount?: number
37
+ }
38
+
39
+ interface MerchandisingAssistantTriggerProps {
40
+ context?: HostInjectionContext
41
+ }
42
+
43
+ function readString(value: unknown): string | null {
44
+ return typeof value === 'string' && value.length > 0 ? value : null
45
+ }
46
+
47
+ function readNumber(value: unknown): number {
48
+ if (typeof value === 'number' && Number.isFinite(value)) return value
49
+ if (typeof value === 'string') {
50
+ const parsed = Number.parseInt(value, 10)
51
+ if (Number.isFinite(parsed)) return parsed
52
+ }
53
+ return 0
54
+ }
55
+
56
+ function normalizeFilters(context: HostInjectionContext | undefined): MerchandisingPageContextFilter {
57
+ const rawCategories = context?.filters?.categoryIds
58
+ const categoryIds = Array.isArray(rawCategories) ? rawCategories : []
59
+ const firstCategoryId = categoryIds
60
+ .map(readString)
61
+ .find((value): value is string => value !== null && value.length > 0) ?? null
62
+
63
+ const rawTags = context?.filters?.tagIds
64
+ const tags = Array.isArray(rawTags)
65
+ ? rawTags.map(readString).filter((value): value is string => value !== null)
66
+ : []
67
+
68
+ const status = readString(context?.filters?.status)
69
+
70
+ return {
71
+ categoryId: firstCategoryId,
72
+ priceRange: null,
73
+ tags,
74
+ status,
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Exposed for unit tests so the page-context derivation is exercisable
80
+ * without mounting the widget.
81
+ */
82
+ export function computeCatalogMerchandisingPageContext(
83
+ context: HostInjectionContext | undefined,
84
+ ): MerchandisingPageContext {
85
+ const totalMatching = readNumber(context?.totalMatching ?? context?.total)
86
+ const selectedRowIds = Array.isArray(context?._selectedRowIds) ? context._selectedRowIds : []
87
+ const selectedCount = selectedRowIds.length > 0 ? selectedRowIds.length : readNumber(context?._selectedCount)
88
+ return {
89
+ view: 'catalog.products.list',
90
+ entityType: 'catalog.products.list',
91
+ recordType: null,
92
+ recordId: selectedRowIds.join(','),
93
+ extra: {
94
+ filter: normalizeFilters(context),
95
+ totalMatching,
96
+ selectedCount,
97
+ },
98
+ }
99
+ }
100
+
101
+ export default function MerchandisingAssistantTriggerWidget({
102
+ context,
103
+ }: MerchandisingAssistantTriggerProps) {
104
+ const pageContext = React.useMemo(
105
+ () => computeCatalogMerchandisingPageContext(context),
106
+ [context],
107
+ )
108
+ return <MerchandisingAssistantSheet pageContext={pageContext} />
109
+ }
@@ -0,0 +1,29 @@
1
+ import type { InjectionWidgetModule } from '@open-mercato/shared/modules/widgets/injection'
2
+ import MerchandisingAssistantTriggerWidget from './widget.client'
3
+
4
+ /**
5
+ * Step 5.15 (Phase 3 WS-D) — Catalog merchandising AiChat injection.
6
+ *
7
+ * Moves the Step 4.9 demo embed behind the widget-injection system so the
8
+ * products-list page no longer hosts the trigger directly. The widget
9
+ * targets `data-table:catalog.products:header` (owned by the shared
10
+ * `DataTable` primitive). The existing `MerchandisingAssistantSheet`
11
+ * component is reused verbatim so the Phase 2 read-only contract is
12
+ * preserved.
13
+ *
14
+ * Feature-gated behind `catalog.products.view` + `ai_assistant.view`.
15
+ */
16
+ const widget: InjectionWidgetModule<Record<string, unknown>, Record<string, unknown>> = {
17
+ metadata: {
18
+ id: 'catalog.injection.merchandising-assistant-trigger',
19
+ title: 'Catalog Merchandising Assistant Trigger',
20
+ description:
21
+ 'Renders an "AI Merchandising" button in the products list header that opens a sheet embedding the catalog merchandising assistant.',
22
+ features: ['catalog.products.view', 'ai_assistant.view'],
23
+ priority: 100,
24
+ enabled: true,
25
+ },
26
+ Widget: MerchandisingAssistantTriggerWidget,
27
+ }
28
+
29
+ export default widget
@@ -79,7 +79,7 @@ export default function ProductSeoWidget({ data }: InjectionWidgetComponentProps
79
79
  }
80
80
 
81
81
  return (
82
- <div className="space-y-3 rounded-lg border bg-card p-4 shadow-sm">
82
+ <div className="mt-4 w-full space-y-3 rounded-lg border bg-card p-4 shadow-sm">
83
83
  <div className="flex items-start justify-between gap-3">
84
84
  <div>
85
85
  <div className="text-sm font-semibold text-foreground">{t('catalog.products.create.seoWidget.title', 'SEO Optimization')}</div>
@@ -26,6 +26,18 @@ export const injectionTable: ModuleInjectionTable = {
26
26
  widgetId: 'catalog.injection.product-bulk-delete',
27
27
  priority: 40,
28
28
  },
29
+ // Step 5.15 — Phase 3 WS-D.
30
+ // Merchandising assistant trigger moved behind the injection system so the
31
+ // products list page no longer imports `MerchandisingAssistantSheet`
32
+ // directly. The DataTable's `injectionSpotId="data-table:catalog.products"`
33
+ // exposes the `:search-trailing` variant that this widget targets — the
34
+ // round icon-only trigger lives next to the products list search input.
35
+ 'data-table:catalog.products:search-trailing': [
36
+ {
37
+ widgetId: 'catalog.injection.merchandising-assistant-trigger',
38
+ priority: 100,
39
+ },
40
+ ],
29
41
  }
30
42
 
31
43
  export default injectionTable
@@ -202,6 +202,11 @@
202
202
  "customer_accounts.notifications.user.locked.title": "Customer account locked",
203
203
  "customer_accounts.notifications.user.signup.body": "A new customer account was created.",
204
204
  "customer_accounts.notifications.user.signup.title": "New customer signup",
205
+ "customer_accounts.portal_ai_assistant.sheet.composerPlaceholder": "Frage zu deinem Konto stellen...",
206
+ "customer_accounts.portal_ai_assistant.sheet.description": "Read-only-Assistent für Portalkunden. Fragen zu deinem Konto und zur aktuellen Aktivität.",
207
+ "customer_accounts.portal_ai_assistant.sheet.title": "Portal-AI-Assistent",
208
+ "customer_accounts.portal_ai_assistant.trigger.ariaLabel": "Portal-KI-Assistent öffnen",
209
+ "customer_accounts.portal_ai_assistant.trigger.label": "KI fragen",
205
210
  "customer_accounts.search.badge.customerRole": "Kundenrolle",
206
211
  "customer_accounts.search.badge.customerUser": "Kundenbenutzer",
207
212
  "customer_accounts.settings.back": "Kundenbenutzer",
@@ -202,6 +202,11 @@
202
202
  "customer_accounts.notifications.user.locked.title": "Customer account locked",
203
203
  "customer_accounts.notifications.user.signup.body": "A new customer account was created.",
204
204
  "customer_accounts.notifications.user.signup.title": "New customer signup",
205
+ "customer_accounts.portal_ai_assistant.sheet.composerPlaceholder": "Ask about your account...",
206
+ "customer_accounts.portal_ai_assistant.sheet.description": "Read-only assistant for portal customers. Ask about your account and recent activity.",
207
+ "customer_accounts.portal_ai_assistant.sheet.title": "Portal AI assistant",
208
+ "customer_accounts.portal_ai_assistant.trigger.ariaLabel": "Open portal AI assistant",
209
+ "customer_accounts.portal_ai_assistant.trigger.label": "Ask AI",
205
210
  "customer_accounts.search.badge.customerRole": "Customer role",
206
211
  "customer_accounts.search.badge.customerUser": "Customer user",
207
212
  "customer_accounts.settings.back": "Customer Users",
@@ -202,6 +202,11 @@
202
202
  "customer_accounts.notifications.user.locked.title": "Customer account locked",
203
203
  "customer_accounts.notifications.user.signup.body": "A new customer account was created.",
204
204
  "customer_accounts.notifications.user.signup.title": "New customer signup",
205
+ "customer_accounts.portal_ai_assistant.sheet.composerPlaceholder": "Pregunta sobre tu cuenta...",
206
+ "customer_accounts.portal_ai_assistant.sheet.description": "Asistente de solo lectura para clientes del portal. Pregunta sobre tu cuenta y actividad reciente.",
207
+ "customer_accounts.portal_ai_assistant.sheet.title": "Asistente AI del portal",
208
+ "customer_accounts.portal_ai_assistant.trigger.ariaLabel": "Abrir asistente AI del portal",
209
+ "customer_accounts.portal_ai_assistant.trigger.label": "Preguntar a la IA",
205
210
  "customer_accounts.search.badge.customerRole": "Rol de cliente",
206
211
  "customer_accounts.search.badge.customerUser": "Usuario de cliente",
207
212
  "customer_accounts.settings.back": "Usuarios de clientes",
@@ -202,6 +202,11 @@
202
202
  "customer_accounts.notifications.user.locked.title": "Customer account locked",
203
203
  "customer_accounts.notifications.user.signup.body": "A new customer account was created.",
204
204
  "customer_accounts.notifications.user.signup.title": "New customer signup",
205
+ "customer_accounts.portal_ai_assistant.sheet.composerPlaceholder": "Zapytaj o swoje konto...",
206
+ "customer_accounts.portal_ai_assistant.sheet.description": "Asystent tylko do odczytu dla klientów portalu. Zapytaj o swoje konto i ostatnią aktywność.",
207
+ "customer_accounts.portal_ai_assistant.sheet.title": "Asystent AI portalu",
208
+ "customer_accounts.portal_ai_assistant.trigger.ariaLabel": "Otwórz asystenta AI portalu",
209
+ "customer_accounts.portal_ai_assistant.trigger.label": "Zapytaj AI",
205
210
  "customer_accounts.search.badge.customerRole": "Rola klienta",
206
211
  "customer_accounts.search.badge.customerUser": "Użytkownik klienta",
207
212
  "customer_accounts.settings.back": "Użytkownicy klientów",