@open-mercato/core 0.10.1-develop.7772.1.9d3d6ce261 → 0.10.1-develop.7774.1.9a13ef7580
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/customers/ai-tools.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +1 -1
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +1 -1
- package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +1 -1
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyDetailTabs.js +3 -2
- package/dist/modules/customers/components/detail/CompanyDetailTabs.js.map +2 -2
- package/dist/modules/customers/components/detail/DealDetailTabs.js +3 -2
- package/dist/modules/customers/components/detail/DealDetailTabs.js.map +2 -2
- package/dist/modules/customers/components/detail/PersonDetailTabs.js +3 -2
- package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
- package/dist/modules/sales/api/documents/factory.js +1 -1
- package/dist/modules/sales/api/documents/factory.js.map +2 -2
- package/dist/modules/sales/api/orders/route.js +5 -4
- package/dist/modules/sales/api/orders/route.js.map +2 -2
- package/dist/modules/sales/api/quotes/route.js +5 -4
- package/dist/modules/sales/api/quotes/route.js.map +2 -2
- package/dist/modules/sales/backend/sales/documents/[id]/page.js +23 -1
- package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesDocumentExtensionSpot.js +26 -0
- package/dist/modules/sales/components/documents/SalesDocumentExtensionSpot.js.map +7 -0
- package/dist/modules/sales/extension-points.js +26 -0
- package/dist/modules/sales/extension-points.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/catalog/AGENTS.md +2 -2
- package/src/modules/customers/AGENTS.md +2 -2
- package/src/modules/customers/ai-tools.ts +6 -9
- package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +1 -1
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +1 -1
- package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +1 -1
- package/src/modules/customers/components/detail/CompanyDetailTabs.tsx +5 -1
- package/src/modules/customers/components/detail/DealDetailTabs.tsx +6 -2
- package/src/modules/customers/components/detail/PersonDetailTabs.tsx +6 -2
- package/src/modules/sales/api/documents/factory.ts +1 -1
- package/src/modules/sales/api/orders/route.ts +4 -3
- package/src/modules/sales/api/quotes/route.ts +4 -3
- package/src/modules/sales/backend/sales/documents/[id]/page.tsx +19 -1
- package/src/modules/sales/components/documents/SalesDocumentExtensionSpot.tsx +44 -0
- package/src/modules/sales/extension-points.ts +26 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { extensionPoints } from "@open-mercato/core/modules/sales/extension-points";
|
|
4
|
+
import { resolveExtensionPointPattern } from "@open-mercato/shared/modules/widgets/extension-points";
|
|
5
|
+
import { InjectionSpot } from "@open-mercato/ui/backend/injection/InjectionSpot";
|
|
6
|
+
function SalesDocumentExtensionSpot({
|
|
7
|
+
surface,
|
|
8
|
+
context,
|
|
9
|
+
data,
|
|
10
|
+
onDataChange
|
|
11
|
+
}) {
|
|
12
|
+
const pattern = surface === "stage-bar" ? extensionPoints.hosts.documentStageBar.pattern : extensionPoints.hosts.documentClosure.pattern;
|
|
13
|
+
return /* @__PURE__ */ jsx(
|
|
14
|
+
InjectionSpot,
|
|
15
|
+
{
|
|
16
|
+
spotId: resolveExtensionPointPattern(pattern, { kind: context.kind }),
|
|
17
|
+
context,
|
|
18
|
+
data,
|
|
19
|
+
onDataChange
|
|
20
|
+
}
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
export {
|
|
24
|
+
SalesDocumentExtensionSpot
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=SalesDocumentExtensionSpot.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/modules/sales/components/documents/SalesDocumentExtensionSpot.tsx"],
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport { extensionPoints } from '@open-mercato/core/modules/sales/extension-points'\nimport { resolveExtensionPointPattern } from '@open-mercato/shared/modules/widgets/extension-points'\nimport { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'\n\nexport type SalesDocumentExtensionContext<TRecord extends { id: string }> = {\n kind: 'order' | 'quote'\n documentId: string | null\n record: TRecord | null\n formId: string\n resourceKind: 'sales.order' | 'sales.quote'\n resourceId?: string\n retryLastMutation: () => Promise<boolean>\n organizationId: string | null\n tenantId: string | null\n}\n\ntype SalesDocumentExtensionSpotProps<TRecord extends { id: string }> = {\n surface: 'stage-bar' | 'closure'\n context: SalesDocumentExtensionContext<TRecord>\n data: TRecord\n onDataChange: (data: TRecord) => void\n}\n\nexport function SalesDocumentExtensionSpot<TRecord extends { id: string }>({\n surface,\n context,\n data,\n onDataChange,\n}: SalesDocumentExtensionSpotProps<TRecord>) {\n const pattern = surface === 'stage-bar'\n ? extensionPoints.hosts.documentStageBar.pattern\n : extensionPoints.hosts.documentClosure.pattern\n\n return (\n <InjectionSpot\n spotId={resolveExtensionPointPattern(pattern, { kind: context.kind })}\n context={context}\n data={data}\n onDataChange={onDataChange}\n />\n )\n}\n"],
|
|
5
|
+
"mappings": ";AAoCI;AAlCJ,SAAS,uBAAuB;AAChC,SAAS,oCAAoC;AAC7C,SAAS,qBAAqB;AAqBvB,SAAS,2BAA2D;AAAA,EACzE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA6C;AAC3C,QAAM,UAAU,YAAY,cACxB,gBAAgB,MAAM,iBAAiB,UACvC,gBAAgB,MAAM,gBAAgB;AAE1C,SACE;AAAA,IAAC;AAAA;AAAA,MACC,QAAQ,6BAA6B,SAAS,EAAE,MAAM,QAAQ,KAAK,CAAC;AAAA,MACpE;AAAA,MACA;AAAA,MACA;AAAA;AAAA,EACF;AAEJ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -17,6 +17,32 @@ const extensionPoints = defineModuleExtensionPoints({
|
|
|
17
17
|
supported: ["render-widget"],
|
|
18
18
|
source: "backend/sales/documents/[id]/page.tsx"
|
|
19
19
|
}),
|
|
20
|
+
documentStageBar: injectionExtensionHost({
|
|
21
|
+
family: "detail",
|
|
22
|
+
pattern: "detail:sales.{kind}:stage-bar",
|
|
23
|
+
parameters: {
|
|
24
|
+
kind: { source: "SalesDocumentExtensionContext.kind", pattern: "^(order|quote)$" }
|
|
25
|
+
},
|
|
26
|
+
supported: ["render-widget"],
|
|
27
|
+
source: ["components/documents/SalesDocumentExtensionSpot.tsx", "backend/sales/documents/[id]/page.tsx"],
|
|
28
|
+
contextContract: "SalesDocumentExtensionContext: kind, documentId, record, formId, resourceKind, resourceId, retryLastMutation, organizationId, tenantId",
|
|
29
|
+
dataContract: "Current sales order or quote record; onDataChange updates the in-page record only",
|
|
30
|
+
scopeContract: "organizationId and tenantId from the loaded, server-scoped document; widget writes must enforce their own authorization",
|
|
31
|
+
runtimeContract: "Rendered below the document header; no native stage transition or mutation lifecycle is added"
|
|
32
|
+
}),
|
|
33
|
+
documentClosure: injectionExtensionHost({
|
|
34
|
+
family: "detail",
|
|
35
|
+
pattern: "detail:sales.{kind}:closure",
|
|
36
|
+
parameters: {
|
|
37
|
+
kind: { source: "SalesDocumentExtensionContext.kind", pattern: "^(order|quote)$" }
|
|
38
|
+
},
|
|
39
|
+
supported: ["render-widget"],
|
|
40
|
+
source: ["components/documents/SalesDocumentExtensionSpot.tsx", "backend/sales/documents/[id]/page.tsx"],
|
|
41
|
+
contextContract: "SalesDocumentExtensionContext: kind, documentId, record, formId, resourceKind, resourceId, retryLastMutation, organizationId, tenantId",
|
|
42
|
+
dataContract: "Current sales order or quote record; onDataChange updates the in-page record only",
|
|
43
|
+
scopeContract: "organizationId and tenantId from the loaded, server-scoped document; widget writes must enforce their own authorization",
|
|
44
|
+
runtimeContract: "Rendered below document details for closure extensions; no native closure dialog or mutation lifecycle is added"
|
|
45
|
+
}),
|
|
20
46
|
documentDetail: injectionExtensionHost({
|
|
21
47
|
family: "detail",
|
|
22
48
|
pattern: "sales.document.detail.{kind}:{surface}",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/sales/extension-points.ts"],
|
|
4
|
-
"sourcesContent": ["import { dataTableExtensionHost, defineModuleExtensionPoints, injectionExtensionHost } from '@open-mercato/shared/modules/widgets/extension-points'\n\nexport const extensionPoints = defineModuleExtensionPoints({\n moduleId: 'sales',\n hosts: {\n ordersTable: dataTableExtensionHost({ tableId: 'sales.orders', source: 'components/documents/SalesDocumentsTable.tsx' }),\n quotesTable: dataTableExtensionHost({ tableId: 'sales.quotes', source: 'components/documents/SalesDocumentsTable.tsx' }),\n paymentsTable: dataTableExtensionHost({ tableId: 'sales.payments', source: 'components/documents/PaymentsSection.tsx' }),\n orderItemColumns: injectionExtensionHost({\n family: 'detail',\n spotId: 'data-table:sales.order.items:columns',\n supported: ['column-widget'],\n source: 'components/documents/ItemsSection.tsx',\n }),\n orderShipping: injectionExtensionHost({\n family: 'detail',\n spotId: 'detail:sales.order:shipping',\n supported: ['render-widget'],\n source: 'backend/sales/documents/[id]/page.tsx',\n }),\n documentDetail: injectionExtensionHost({\n family: 'detail',\n pattern: 'sales.document.detail.{kind}:{surface}',\n parameters: {\n kind: { source: 'SalesDocumentKind', pattern: '^(order|quote)$' },\n surface: { source: 'host declaration', pattern: '^(tabs|details)$' },\n },\n supported: ['render-widget'],\n source: 'backend/sales/documents/[id]/page.tsx',\n }),\n },\n})\n\nexport default extensionPoints\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,wBAAwB,6BAA6B,8BAA8B;AAErF,MAAM,kBAAkB,4BAA4B;AAAA,EACzD,UAAU;AAAA,EACV,OAAO;AAAA,IACL,aAAa,uBAAuB,EAAE,SAAS,gBAAgB,QAAQ,+CAA+C,CAAC;AAAA,IACvH,aAAa,uBAAuB,EAAE,SAAS,gBAAgB,QAAQ,+CAA+C,CAAC;AAAA,IACvH,eAAe,uBAAuB,EAAE,SAAS,kBAAkB,QAAQ,2CAA2C,CAAC;AAAA,IACvH,kBAAkB,uBAAuB;AAAA,MACvC,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,WAAW,CAAC,eAAe;AAAA,MAC3B,QAAQ;AAAA,IACV,CAAC;AAAA,IACD,eAAe,uBAAuB;AAAA,MACpC,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,WAAW,CAAC,eAAe;AAAA,MAC3B,QAAQ;AAAA,IACV,CAAC;AAAA,IACD,gBAAgB,uBAAuB;AAAA,MACrC,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,YAAY;AAAA,QACV,MAAM,EAAE,QAAQ,qBAAqB,SAAS,kBAAkB;AAAA,QAChE,SAAS,EAAE,QAAQ,oBAAoB,SAAS,mBAAmB;AAAA,MACrE;AAAA,MACA,WAAW,CAAC,eAAe;AAAA,MAC3B,QAAQ;AAAA,IACV,CAAC;AAAA,EACH;AACF,CAAC;AAED,IAAO,2BAAQ;",
|
|
4
|
+
"sourcesContent": ["import { dataTableExtensionHost, defineModuleExtensionPoints, injectionExtensionHost } from '@open-mercato/shared/modules/widgets/extension-points'\n\nexport const extensionPoints = defineModuleExtensionPoints({\n moduleId: 'sales',\n hosts: {\n ordersTable: dataTableExtensionHost({ tableId: 'sales.orders', source: 'components/documents/SalesDocumentsTable.tsx' }),\n quotesTable: dataTableExtensionHost({ tableId: 'sales.quotes', source: 'components/documents/SalesDocumentsTable.tsx' }),\n paymentsTable: dataTableExtensionHost({ tableId: 'sales.payments', source: 'components/documents/PaymentsSection.tsx' }),\n orderItemColumns: injectionExtensionHost({\n family: 'detail',\n spotId: 'data-table:sales.order.items:columns',\n supported: ['column-widget'],\n source: 'components/documents/ItemsSection.tsx',\n }),\n orderShipping: injectionExtensionHost({\n family: 'detail',\n spotId: 'detail:sales.order:shipping',\n supported: ['render-widget'],\n source: 'backend/sales/documents/[id]/page.tsx',\n }),\n documentStageBar: injectionExtensionHost({\n family: 'detail',\n pattern: 'detail:sales.{kind}:stage-bar',\n parameters: {\n kind: { source: 'SalesDocumentExtensionContext.kind', pattern: '^(order|quote)$' },\n },\n supported: ['render-widget'],\n source: ['components/documents/SalesDocumentExtensionSpot.tsx', 'backend/sales/documents/[id]/page.tsx'],\n contextContract: 'SalesDocumentExtensionContext: kind, documentId, record, formId, resourceKind, resourceId, retryLastMutation, organizationId, tenantId',\n dataContract: 'Current sales order or quote record; onDataChange updates the in-page record only',\n scopeContract: 'organizationId and tenantId from the loaded, server-scoped document; widget writes must enforce their own authorization',\n runtimeContract: 'Rendered below the document header; no native stage transition or mutation lifecycle is added',\n }),\n documentClosure: injectionExtensionHost({\n family: 'detail',\n pattern: 'detail:sales.{kind}:closure',\n parameters: {\n kind: { source: 'SalesDocumentExtensionContext.kind', pattern: '^(order|quote)$' },\n },\n supported: ['render-widget'],\n source: ['components/documents/SalesDocumentExtensionSpot.tsx', 'backend/sales/documents/[id]/page.tsx'],\n contextContract: 'SalesDocumentExtensionContext: kind, documentId, record, formId, resourceKind, resourceId, retryLastMutation, organizationId, tenantId',\n dataContract: 'Current sales order or quote record; onDataChange updates the in-page record only',\n scopeContract: 'organizationId and tenantId from the loaded, server-scoped document; widget writes must enforce their own authorization',\n runtimeContract: 'Rendered below document details for closure extensions; no native closure dialog or mutation lifecycle is added',\n }),\n documentDetail: injectionExtensionHost({\n family: 'detail',\n pattern: 'sales.document.detail.{kind}:{surface}',\n parameters: {\n kind: { source: 'SalesDocumentKind', pattern: '^(order|quote)$' },\n surface: { source: 'host declaration', pattern: '^(tabs|details)$' },\n },\n supported: ['render-widget'],\n source: 'backend/sales/documents/[id]/page.tsx',\n }),\n },\n})\n\nexport default extensionPoints\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,wBAAwB,6BAA6B,8BAA8B;AAErF,MAAM,kBAAkB,4BAA4B;AAAA,EACzD,UAAU;AAAA,EACV,OAAO;AAAA,IACL,aAAa,uBAAuB,EAAE,SAAS,gBAAgB,QAAQ,+CAA+C,CAAC;AAAA,IACvH,aAAa,uBAAuB,EAAE,SAAS,gBAAgB,QAAQ,+CAA+C,CAAC;AAAA,IACvH,eAAe,uBAAuB,EAAE,SAAS,kBAAkB,QAAQ,2CAA2C,CAAC;AAAA,IACvH,kBAAkB,uBAAuB;AAAA,MACvC,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,WAAW,CAAC,eAAe;AAAA,MAC3B,QAAQ;AAAA,IACV,CAAC;AAAA,IACD,eAAe,uBAAuB;AAAA,MACpC,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,WAAW,CAAC,eAAe;AAAA,MAC3B,QAAQ;AAAA,IACV,CAAC;AAAA,IACD,kBAAkB,uBAAuB;AAAA,MACvC,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,YAAY;AAAA,QACV,MAAM,EAAE,QAAQ,sCAAsC,SAAS,kBAAkB;AAAA,MACnF;AAAA,MACA,WAAW,CAAC,eAAe;AAAA,MAC3B,QAAQ,CAAC,uDAAuD,uCAAuC;AAAA,MACvG,iBAAiB;AAAA,MACjB,cAAc;AAAA,MACd,eAAe;AAAA,MACf,iBAAiB;AAAA,IACnB,CAAC;AAAA,IACD,iBAAiB,uBAAuB;AAAA,MACtC,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,YAAY;AAAA,QACV,MAAM,EAAE,QAAQ,sCAAsC,SAAS,kBAAkB;AAAA,MACnF;AAAA,MACA,WAAW,CAAC,eAAe;AAAA,MAC3B,QAAQ,CAAC,uDAAuD,uCAAuC;AAAA,MACvG,iBAAiB;AAAA,MACjB,cAAc;AAAA,MACd,eAAe;AAAA,MACf,iBAAiB;AAAA,IACnB,CAAC;AAAA,IACD,gBAAgB,uBAAuB;AAAA,MACrC,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,YAAY;AAAA,QACV,MAAM,EAAE,QAAQ,qBAAqB,SAAS,kBAAkB;AAAA,QAChE,SAAS,EAAE,QAAQ,oBAAoB,SAAS,mBAAmB;AAAA,MACrE;AAAA,MACA,WAAW,CAAC,eAAe;AAAA,MAC3B,QAAQ;AAAA,IACV,CAAC;AAAA,EACH;AACF,CAAC;AAED,IAAO,2BAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.10.1-develop.
|
|
3
|
+
"version": "0.10.1-develop.7774.1.9a13ef7580",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -266,16 +266,16 @@
|
|
|
266
266
|
"zod": "^4.4.3"
|
|
267
267
|
},
|
|
268
268
|
"peerDependencies": {
|
|
269
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
270
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
271
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
269
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7774.1.9a13ef7580",
|
|
270
|
+
"@open-mercato/shared": "0.10.1-develop.7774.1.9a13ef7580",
|
|
271
|
+
"@open-mercato/ui": "0.10.1-develop.7774.1.9a13ef7580",
|
|
272
272
|
"react": "^19.0.0",
|
|
273
273
|
"react-dom": "^19.0.0"
|
|
274
274
|
},
|
|
275
275
|
"devDependencies": {
|
|
276
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
277
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
278
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
276
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7774.1.9a13ef7580",
|
|
277
|
+
"@open-mercato/shared": "0.10.1-develop.7774.1.9a13ef7580",
|
|
278
|
+
"@open-mercato/ui": "0.10.1-develop.7774.1.9a13ef7580",
|
|
279
279
|
"@testing-library/dom": "^10.4.1",
|
|
280
280
|
"@testing-library/jest-dom": "^7.0.1",
|
|
281
281
|
"@testing-library/react": "^16.3.3",
|
|
@@ -112,6 +112,6 @@ Two typed AI agents ship from `ai-agents.ts`. See `/framework/ai-assistant/agent
|
|
|
112
112
|
| Agent ID | Mode | Policy | Purpose |
|
|
113
113
|
|----------|------|--------|---------|
|
|
114
114
|
| `catalog.catalog_assistant` | chat | `read-only` | General operator explorer for products, categories, variants, prices, offers, product media, tags, option schemas, and unit conversions via the base catalog tool pack + general-purpose packs. |
|
|
115
|
-
| `catalog.merchandising_assistant` | chat | `
|
|
115
|
+
| `catalog.merchandising_assistant` | chat | `confirm-required`; tenant overrides may restrict to `read-only`, never escalate | D18 agent: proposes descriptions, attribute extractions, title variants, and price adjustments; whitelisted product, batch, attribute, and media writes require approval. See `ai-agents.ts`. |
|
|
116
116
|
|
|
117
|
-
The merchandising assistant is the
|
|
117
|
+
The merchandising assistant is the D18 demo. `<AiChat agent="catalog.merchandising_assistant" />` is injected via `MerchandisingAssistantSheet.tsx` on `/backend/catalog/products` (see `packages/core/src/modules/catalog/backend/catalog/products/page.tsx`). Mutation tools route through `prepareMutation` + the approval-card contract; confirmed batches refresh the DataTable via `catalog.product.updated` events on the DOM event bridge.
|
|
@@ -125,8 +125,8 @@ This module is the reference implementation for the AI framework. Copy `ai-agent
|
|
|
125
125
|
|
|
126
126
|
| Agent ID | Mode | Policy | Purpose |
|
|
127
127
|
|----------|------|--------|---------|
|
|
128
|
-
| `customers.account_assistant` | chat |
|
|
129
|
-
| `customers.update_deal_stage` | tool (mutation) |
|
|
128
|
+
| `customers.account_assistant` | chat | `confirm-required`; tenant overrides may restrict to `read-only`, never escalate | Explores people, companies, deals, activities, tasks, addresses, tags, and settings; whitelisted stage, comment, and activity writes require approval. See `ai-agents.ts`. |
|
|
129
|
+
| `customers.update_deal_stage` | tool (mutation) | Agent mutation policy applies through `prepareMutation` + the approval card | Moves deals between stages or open/won/lost statuses. Declared in `ai-tools/deals-pack.ts`; blocked when the agent is read-only. |
|
|
130
130
|
| `customers.deal_analyzer` | chat, `executionEngine: 'stream-text'` | `confirm-required` | Multi-step agentic demo that exercises loop primitives from PRs #1856..#1869. Calls `customers.analyze_deals`, ranks deals by health score, then calls `customers.update_deal_stage` for the highest-value stalled deal — loop stops immediately after (loop.stopWhen). `loop.prepareStep` scopes the active tool set by step without overriding the model. It intentionally leaves `defaultProvider` and `defaultModel` unset so runtime resolution uses `OM_AI_PROVIDER` / `OM_AI_MODEL` or tenant/runtime overrides. |
|
|
131
131
|
| `customers.deal_analyzer_tool_loop` | chat, `executionEngine: 'tool-loop-agent'` | `confirm-required` | Sibling agent identical to `customers.deal_analyzer` but dispatched via `ToolLoopAgent`. Proves the mutation-approval gate holds for both execution engines (TC-AI-AGENT-LOOP-006). |
|
|
132
132
|
|
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Module-root AI tool contribution for the customers module
|
|
3
|
-
* (Phase 1 WS-C, Step 3.9 — read-only Phase 1 surface).
|
|
2
|
+
* Module-root AI tool contribution for the customers module.
|
|
4
3
|
*
|
|
5
4
|
* The generator walks every module for a top-level `ai-tools.ts` and takes
|
|
6
5
|
* the default/`aiTools` export as the contribution. This file aggregates the
|
|
7
|
-
*
|
|
8
|
-
* addresses+tags, settings) so they all flow through the existing
|
|
6
|
+
* customers packs (people, companies, deals, activities+tasks,
|
|
7
|
+
* addresses+tags, settings, deal analysis) so they all flow through the existing
|
|
9
8
|
* `ai-tools.generated.ts` pipeline without any generator changes.
|
|
10
9
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* `.ai/runs/2026-04-18-ai-framework-unification/step-3.9-checks.md` for the
|
|
15
|
-
* matrix of required features and decisions.
|
|
10
|
+
* Read and mutation tools enforce tenant + organization scoping. Mutations
|
|
11
|
+
* use the pending-action approval contract and command-backed writes;
|
|
12
|
+
* read-only agents cannot expose them. See the framework AI agents guide.
|
|
16
13
|
*/
|
|
17
14
|
import peopleAiTools from './ai-tools/people-pack'
|
|
18
15
|
import companiesAiTools from './ai-tools/companies-pack'
|
|
@@ -530,7 +530,7 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
|
|
|
530
530
|
activeTab={activeTab}
|
|
531
531
|
onTabChange={handleTabChange}
|
|
532
532
|
isLoadingInjectedTabs={injectedTabsLoading}
|
|
533
|
-
injectedTabs={injectedTabs.map((tab) => ({ id: tab.id, label: tab.label }))}
|
|
533
|
+
injectedTabs={injectedTabs.map((tab) => ({ id: tab.id, label: tab.label, priority: tab.priority }))}
|
|
534
534
|
peopleCount={data.counts?.people ?? 0}
|
|
535
535
|
dealsCount={dealCount}
|
|
536
536
|
activitiesCount={data.counts?.activities ?? 0}
|
|
@@ -667,7 +667,7 @@ export default function DealDetailPage({ params }: { params?: { id?: string } })
|
|
|
667
667
|
isLoadingInjectedTabs={injectedTabsLoading}
|
|
668
668
|
activeTab={activeTab}
|
|
669
669
|
onTabChange={handleTabChange}
|
|
670
|
-
injectedTabs={injectedTabs.map((tab) => ({ id: tab.id, label: tab.label }))}
|
|
670
|
+
injectedTabs={injectedTabs.map((tab) => ({ id: tab.id, label: tab.label, priority: tab.priority }))}
|
|
671
671
|
peopleCount={data.counts.people}
|
|
672
672
|
companiesCount={data.counts.companies}
|
|
673
673
|
>
|
|
@@ -568,7 +568,7 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
|
|
|
568
568
|
activeTab={activeTab}
|
|
569
569
|
onTabChange={handleTabChange}
|
|
570
570
|
isLoadingInjectedTabs={injectedTabsLoading}
|
|
571
|
-
injectedTabs={injectedTabs.map((tab) => ({ id: tab.id, label: tab.label }))}
|
|
571
|
+
injectedTabs={injectedTabs.map((tab) => ({ id: tab.id, label: tab.label, priority: tab.priority }))}
|
|
572
572
|
activitiesCount={interactionCount}
|
|
573
573
|
dealsCount={dealCount}
|
|
574
574
|
companiesCount={companyCount}
|
|
@@ -32,6 +32,7 @@ type TabDef = {
|
|
|
32
32
|
label: string
|
|
33
33
|
icon?: React.ReactNode
|
|
34
34
|
count?: React.ReactNode
|
|
35
|
+
priority?: number
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
export const COMPANY_DETAIL_TABS_COMPONENT_ID = 'section:customers.companies.detailTabs'
|
|
@@ -135,8 +136,11 @@ function DefaultCompanyDetailTabs({
|
|
|
135
136
|
...injectedTabs.filter((tab, index) => !builtInTabs.some((native) => native.id === tab.id) && injectedTabs.findIndex((candidate) => candidate.id === tab.id) === index).map((tab) => ({
|
|
136
137
|
id: tab.id as CompanyTabId,
|
|
137
138
|
label: tab.label,
|
|
139
|
+
priority: tab.priority,
|
|
138
140
|
})),
|
|
139
|
-
]
|
|
141
|
+
]
|
|
142
|
+
.sort((left, right) => (right.priority ?? 0) - (left.priority ?? 0))
|
|
143
|
+
.filter((tab) => !hidden.has(tab.id))
|
|
140
144
|
}, [builtInTabs, hiddenTabIds, injectedTabs])
|
|
141
145
|
|
|
142
146
|
React.useEffect(() => {
|
|
@@ -23,6 +23,7 @@ type TabDef = {
|
|
|
23
23
|
label: string
|
|
24
24
|
icon?: React.ReactNode
|
|
25
25
|
count?: React.ReactNode
|
|
26
|
+
priority?: number
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
export const DEAL_DETAIL_TABS_COMPONENT_ID = 'section:customers.deals.detailTabs'
|
|
@@ -30,7 +31,7 @@ export const DEAL_DETAIL_TABS_COMPONENT_ID = 'section:customers.deals.detailTabs
|
|
|
30
31
|
export type DealDetailTabsProps = {
|
|
31
32
|
activeTab: DealTabId
|
|
32
33
|
onTabChange: (tab: DealTabId) => void
|
|
33
|
-
injectedTabs?: Array<{ id: string; label: string }>
|
|
34
|
+
injectedTabs?: Array<{ id: string; label: string; priority?: number }>
|
|
34
35
|
isLoadingInjectedTabs?: boolean
|
|
35
36
|
hiddenTabIds?: string[]
|
|
36
37
|
peopleCount?: number
|
|
@@ -110,8 +111,11 @@ function DefaultDealDetailTabs({
|
|
|
110
111
|
...injectedTabs.filter((tab, index) => !builtInTabs.some((native) => native.id === tab.id) && injectedTabs.findIndex((candidate) => candidate.id === tab.id) === index).map((tab) => ({
|
|
111
112
|
id: tab.id as DealTabId,
|
|
112
113
|
label: tab.label,
|
|
114
|
+
priority: tab.priority,
|
|
113
115
|
})),
|
|
114
|
-
]
|
|
116
|
+
]
|
|
117
|
+
.sort((left, right) => (right.priority ?? 0) - (left.priority ?? 0))
|
|
118
|
+
.filter((tab) => !hidden.has(tab.id))
|
|
115
119
|
}, [builtInTabs, hiddenTabIds, injectedTabs])
|
|
116
120
|
|
|
117
121
|
React.useEffect(() => {
|
|
@@ -37,6 +37,7 @@ type TabDef = {
|
|
|
37
37
|
label: string
|
|
38
38
|
icon?: React.ReactNode
|
|
39
39
|
count?: React.ReactNode
|
|
40
|
+
priority?: number
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
export const PERSON_DETAIL_TABS_COMPONENT_ID = 'section:customers.people.detailTabs'
|
|
@@ -44,7 +45,7 @@ export const PERSON_DETAIL_TABS_COMPONENT_ID = 'section:customers.people.detailT
|
|
|
44
45
|
export type PersonDetailTabsProps = {
|
|
45
46
|
activeTab: PersonTabId
|
|
46
47
|
onTabChange: (tab: PersonTabId) => void
|
|
47
|
-
injectedTabs?: Array<{ id: string; label: string }>
|
|
48
|
+
injectedTabs?: Array<{ id: string; label: string; priority?: number }>
|
|
48
49
|
isLoadingInjectedTabs?: boolean
|
|
49
50
|
hiddenTabIds?: string[]
|
|
50
51
|
activitiesCount?: number
|
|
@@ -148,8 +149,11 @@ function DefaultPersonDetailTabs({
|
|
|
148
149
|
...injectedTabs.filter((tab, index) => !builtInTabs.some((native) => native.id === tab.id) && injectedTabs.findIndex((candidate) => candidate.id === tab.id) === index).map((tab) => ({
|
|
149
150
|
id: tab.id as PersonTabId,
|
|
150
151
|
label: tab.label,
|
|
152
|
+
priority: tab.priority,
|
|
151
153
|
})),
|
|
152
|
-
]
|
|
154
|
+
]
|
|
155
|
+
.sort((left, right) => (right.priority ?? 0) - (left.priority ?? 0))
|
|
156
|
+
.filter((tab) => !hidden.has(tab.id))
|
|
153
157
|
}, [builtInTabs, hiddenTabIds, injectedTabs])
|
|
154
158
|
|
|
155
159
|
React.useEffect(() => {
|
|
@@ -473,7 +473,7 @@ export function buildDocumentCrudOptions(binding: DocumentBinding) {
|
|
|
473
473
|
indexer: {
|
|
474
474
|
entityType: binding.entityId,
|
|
475
475
|
},
|
|
476
|
-
enrichers:
|
|
476
|
+
enrichers: { entityId: binding.entityId },
|
|
477
477
|
list: {
|
|
478
478
|
schema: listSchema,
|
|
479
479
|
entityId: binding.entityId,
|
|
@@ -3,8 +3,8 @@ import { SalesOrder } from '../../data/entities'
|
|
|
3
3
|
import { E } from '#generated/entities.ids.generated'
|
|
4
4
|
import { buildDocumentCrudOptions, buildDocumentOpenApi } from '../documents/factory'
|
|
5
5
|
|
|
6
|
-
const crud = makeCrudRoute(
|
|
7
|
-
buildDocumentCrudOptions({
|
|
6
|
+
const crud = makeCrudRoute({
|
|
7
|
+
...buildDocumentCrudOptions({
|
|
8
8
|
kind: 'order',
|
|
9
9
|
entity: SalesOrder,
|
|
10
10
|
entityId: E.sales.sales_order,
|
|
@@ -15,7 +15,8 @@ const crud = makeCrudRoute(
|
|
|
15
15
|
manageFeature: 'sales.orders.manage',
|
|
16
16
|
viewFeature: 'sales.orders.view',
|
|
17
17
|
}),
|
|
18
|
-
|
|
18
|
+
enrichers: { entityId: E.sales.sales_order },
|
|
19
|
+
})
|
|
19
20
|
|
|
20
21
|
export const metadata = crud.metadata
|
|
21
22
|
export const GET = crud.GET
|
|
@@ -3,8 +3,8 @@ import { SalesQuote } from '../../data/entities'
|
|
|
3
3
|
import { E } from '#generated/entities.ids.generated'
|
|
4
4
|
import { buildDocumentCrudOptions, buildDocumentOpenApi } from '../documents/factory'
|
|
5
5
|
|
|
6
|
-
const crud = makeCrudRoute(
|
|
7
|
-
buildDocumentCrudOptions({
|
|
6
|
+
const crud = makeCrudRoute({
|
|
7
|
+
...buildDocumentCrudOptions({
|
|
8
8
|
kind: 'quote',
|
|
9
9
|
entity: SalesQuote,
|
|
10
10
|
entityId: E.sales.sales_quote,
|
|
@@ -15,7 +15,8 @@ const crud = makeCrudRoute(
|
|
|
15
15
|
manageFeature: 'sales.quotes.manage',
|
|
16
16
|
viewFeature: 'sales.quotes.view',
|
|
17
17
|
}),
|
|
18
|
-
|
|
18
|
+
enrichers: { entityId: E.sales.sales_quote },
|
|
19
|
+
})
|
|
19
20
|
|
|
20
21
|
export const metadata = crud.metadata
|
|
21
22
|
export const GET = crud.GET
|
|
@@ -52,6 +52,7 @@ import type { AdjustmentRowData } from '@open-mercato/core/modules/sales/compone
|
|
|
52
52
|
import { SalesShipmentsSection } from '@open-mercato/core/modules/sales/components/documents/ShipmentsSection'
|
|
53
53
|
import { SalesReturnsSection } from '@open-mercato/core/modules/sales/components/documents/ReturnsSection'
|
|
54
54
|
import { DocumentTotals } from '@open-mercato/core/modules/sales/components/documents/DocumentTotals'
|
|
55
|
+
import { SalesDocumentExtensionSpot, type SalesDocumentExtensionContext } from '@open-mercato/core/modules/sales/components/documents/SalesDocumentExtensionSpot'
|
|
55
56
|
import { E } from '#generated/entities.ids.generated'
|
|
56
57
|
import type { DictionarySelectLabels } from '@open-mercato/core/modules/dictionaries/components/DictionaryEntrySelect'
|
|
57
58
|
import { useCurrencyDictionary } from '@open-mercato/core/modules/customers/components/detail/hooks/useCurrencyDictionary'
|
|
@@ -862,6 +863,8 @@ type StatusOption = {
|
|
|
862
863
|
|
|
863
864
|
type DocumentRecord = {
|
|
864
865
|
id: string
|
|
866
|
+
organizationId?: string | null
|
|
867
|
+
tenantId?: string | null
|
|
865
868
|
orderNumber?: string | null
|
|
866
869
|
quoteNumber?: string | null
|
|
867
870
|
status?: string | null
|
|
@@ -1992,14 +1995,17 @@ export default function SalesDocumentDetailPage({
|
|
|
1992
1995
|
contextId: mutationContextId,
|
|
1993
1996
|
blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
|
|
1994
1997
|
})
|
|
1995
|
-
const detailInjectionContext = React.useMemo(
|
|
1998
|
+
const detailInjectionContext = React.useMemo<SalesDocumentExtensionContext<DocumentRecord>>(
|
|
1996
1999
|
() => ({
|
|
1997
2000
|
kind,
|
|
2001
|
+
documentId: record?.id ?? null,
|
|
1998
2002
|
record,
|
|
1999
2003
|
formId: mutationContextId,
|
|
2000
2004
|
resourceKind: `sales.${kind}`,
|
|
2001
2005
|
resourceId: record?.id ?? undefined,
|
|
2002
2006
|
retryLastMutation,
|
|
2007
|
+
organizationId: record?.organizationId ?? null,
|
|
2008
|
+
tenantId: record?.tenantId ?? null,
|
|
2003
2009
|
}),
|
|
2004
2010
|
[kind, mutationContextId, record, retryLastMutation],
|
|
2005
2011
|
)
|
|
@@ -4779,6 +4785,12 @@ export default function SalesDocumentDetailPage({
|
|
|
4779
4785
|
isDeleting={deleting}
|
|
4780
4786
|
deleteLabel={t('sales.documents.detail.delete', 'Delete')}
|
|
4781
4787
|
/>
|
|
4788
|
+
<SalesDocumentExtensionSpot
|
|
4789
|
+
surface="stage-bar"
|
|
4790
|
+
context={detailInjectionContext}
|
|
4791
|
+
data={record}
|
|
4792
|
+
onDataChange={setRecord}
|
|
4793
|
+
/>
|
|
4782
4794
|
<div className="grid gap-4 md:grid-cols-4">
|
|
4783
4795
|
<div className="md:col-span-3">
|
|
4784
4796
|
<CustomerInlineEditor
|
|
@@ -5023,6 +5035,12 @@ export default function SalesDocumentDetailPage({
|
|
|
5023
5035
|
/>
|
|
5024
5036
|
</div>
|
|
5025
5037
|
|
|
5038
|
+
<SalesDocumentExtensionSpot
|
|
5039
|
+
surface="closure"
|
|
5040
|
+
context={detailInjectionContext}
|
|
5041
|
+
data={record}
|
|
5042
|
+
onDataChange={setRecord}
|
|
5043
|
+
/>
|
|
5026
5044
|
<div className="space-y-4">
|
|
5027
5045
|
<CustomDataSection
|
|
5028
5046
|
title={t('sales.documents.detail.customData', 'Custom data')}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { extensionPoints } from '@open-mercato/core/modules/sales/extension-points'
|
|
4
|
+
import { resolveExtensionPointPattern } from '@open-mercato/shared/modules/widgets/extension-points'
|
|
5
|
+
import { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'
|
|
6
|
+
|
|
7
|
+
export type SalesDocumentExtensionContext<TRecord extends { id: string }> = {
|
|
8
|
+
kind: 'order' | 'quote'
|
|
9
|
+
documentId: string | null
|
|
10
|
+
record: TRecord | null
|
|
11
|
+
formId: string
|
|
12
|
+
resourceKind: 'sales.order' | 'sales.quote'
|
|
13
|
+
resourceId?: string
|
|
14
|
+
retryLastMutation: () => Promise<boolean>
|
|
15
|
+
organizationId: string | null
|
|
16
|
+
tenantId: string | null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type SalesDocumentExtensionSpotProps<TRecord extends { id: string }> = {
|
|
20
|
+
surface: 'stage-bar' | 'closure'
|
|
21
|
+
context: SalesDocumentExtensionContext<TRecord>
|
|
22
|
+
data: TRecord
|
|
23
|
+
onDataChange: (data: TRecord) => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function SalesDocumentExtensionSpot<TRecord extends { id: string }>({
|
|
27
|
+
surface,
|
|
28
|
+
context,
|
|
29
|
+
data,
|
|
30
|
+
onDataChange,
|
|
31
|
+
}: SalesDocumentExtensionSpotProps<TRecord>) {
|
|
32
|
+
const pattern = surface === 'stage-bar'
|
|
33
|
+
? extensionPoints.hosts.documentStageBar.pattern
|
|
34
|
+
: extensionPoints.hosts.documentClosure.pattern
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<InjectionSpot
|
|
38
|
+
spotId={resolveExtensionPointPattern(pattern, { kind: context.kind })}
|
|
39
|
+
context={context}
|
|
40
|
+
data={data}
|
|
41
|
+
onDataChange={onDataChange}
|
|
42
|
+
/>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
@@ -18,6 +18,32 @@ export const extensionPoints = defineModuleExtensionPoints({
|
|
|
18
18
|
supported: ['render-widget'],
|
|
19
19
|
source: 'backend/sales/documents/[id]/page.tsx',
|
|
20
20
|
}),
|
|
21
|
+
documentStageBar: injectionExtensionHost({
|
|
22
|
+
family: 'detail',
|
|
23
|
+
pattern: 'detail:sales.{kind}:stage-bar',
|
|
24
|
+
parameters: {
|
|
25
|
+
kind: { source: 'SalesDocumentExtensionContext.kind', pattern: '^(order|quote)$' },
|
|
26
|
+
},
|
|
27
|
+
supported: ['render-widget'],
|
|
28
|
+
source: ['components/documents/SalesDocumentExtensionSpot.tsx', 'backend/sales/documents/[id]/page.tsx'],
|
|
29
|
+
contextContract: 'SalesDocumentExtensionContext: kind, documentId, record, formId, resourceKind, resourceId, retryLastMutation, organizationId, tenantId',
|
|
30
|
+
dataContract: 'Current sales order or quote record; onDataChange updates the in-page record only',
|
|
31
|
+
scopeContract: 'organizationId and tenantId from the loaded, server-scoped document; widget writes must enforce their own authorization',
|
|
32
|
+
runtimeContract: 'Rendered below the document header; no native stage transition or mutation lifecycle is added',
|
|
33
|
+
}),
|
|
34
|
+
documentClosure: injectionExtensionHost({
|
|
35
|
+
family: 'detail',
|
|
36
|
+
pattern: 'detail:sales.{kind}:closure',
|
|
37
|
+
parameters: {
|
|
38
|
+
kind: { source: 'SalesDocumentExtensionContext.kind', pattern: '^(order|quote)$' },
|
|
39
|
+
},
|
|
40
|
+
supported: ['render-widget'],
|
|
41
|
+
source: ['components/documents/SalesDocumentExtensionSpot.tsx', 'backend/sales/documents/[id]/page.tsx'],
|
|
42
|
+
contextContract: 'SalesDocumentExtensionContext: kind, documentId, record, formId, resourceKind, resourceId, retryLastMutation, organizationId, tenantId',
|
|
43
|
+
dataContract: 'Current sales order or quote record; onDataChange updates the in-page record only',
|
|
44
|
+
scopeContract: 'organizationId and tenantId from the loaded, server-scoped document; widget writes must enforce their own authorization',
|
|
45
|
+
runtimeContract: 'Rendered below document details for closure extensions; no native closure dialog or mutation lifecycle is added',
|
|
46
|
+
}),
|
|
21
47
|
documentDetail: injectionExtensionHost({
|
|
22
48
|
family: 'detail',
|
|
23
49
|
pattern: 'sales.document.detail.{kind}:{surface}',
|