@open-mercato/core 0.7.1-develop.7113.1.9d83dca10c → 0.7.1-develop.7121.1.734c263bda
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/helpers/integration/crmFixtures.js.map +1 -1
- package/dist/modules/communication_channels/api/post/channels/[id]/test-send/route.js +9 -2
- package/dist/modules/communication_channels/api/post/channels/[id]/test-send/route.js.map +2 -2
- package/dist/modules/communication_channels/lib/outbound-recipient.js +5 -1
- package/dist/modules/communication_channels/lib/outbound-recipient.js.map +2 -2
- package/dist/modules/communication_channels/lib/test-seed.js +5 -0
- package/dist/modules/communication_channels/lib/test-seed.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.js +1 -1
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.js.map +1 -1
- package/dist/modules/customers/backend/customers/deals/page.js +4 -3
- package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.js +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.js +1 -1
- package/dist/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.js.map +1 -1
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js +1 -1
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
- package/dist/modules/customers/cli.js +5 -5
- package/dist/modules/customers/cli.js.map +2 -2
- package/dist/modules/customers/commands/deals.js.map +2 -2
- package/dist/modules/customers/components/detail/pipelineStageUtils.js +1 -0
- package/dist/modules/customers/components/detail/pipelineStageUtils.js.map +2 -2
- package/dist/modules/customers/components/linking/adapters/dealAdapter.js +2 -2
- package/dist/modules/customers/components/linking/adapters/dealAdapter.js.map +2 -2
- package/dist/modules/customers/lib/closureStage.js.map +2 -2
- package/dist/modules/customers/lib/dealStatus.js +6 -4
- package/dist/modules/customers/lib/dealStatus.js.map +2 -2
- package/dist/modules/customers/lib/dealsSummaryQueries.js +3 -3
- package/dist/modules/customers/lib/dealsSummaryQueries.js.map +1 -1
- package/dist/modules/customers/migrations/Migration20260824180000_deal_status_lost.js +30 -0
- package/dist/modules/customers/migrations/Migration20260824180000_deal_status_lost.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/config.js.map +1 -1
- package/dist/modules/eudr/notifications.js +5 -0
- package/dist/modules/eudr/notifications.js.map +2 -2
- package/dist/modules/query_index/lib/search-tokens.js +59 -28
- package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +146 -99
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/commands/returns.js +1 -30
- package/dist/modules/sales/commands/returns.js.map +2 -2
- package/dist/modules/sales/commands/shared.js +1 -4
- package/dist/modules/sales/commands/shared.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesOrderDraftLines.js +2 -1
- package/dist/modules/sales/components/documents/SalesOrderDraftLines.js.map +2 -2
- package/dist/modules/sales/data/validators.js +3 -0
- package/dist/modules/sales/data/validators.js.map +2 -2
- package/dist/modules/sales/lib/calculations.js +14 -2
- package/dist/modules/sales/lib/calculations.js.map +2 -2
- package/dist/modules/sales/lib/json.js +8 -0
- package/dist/modules/sales/lib/json.js.map +7 -0
- package/dist/modules/sales/lib/lineSnapshots.js +66 -0
- package/dist/modules/sales/lib/lineSnapshots.js.map +7 -0
- package/dist/modules/warranty_claims/notifications.js +5 -0
- package/dist/modules/warranty_claims/notifications.js.map +2 -2
- package/dist/modules/wms/notifications.js +2 -0
- package/dist/modules/wms/notifications.js.map +2 -2
- package/package.json +7 -7
- package/src/helpers/integration/crmFixtures.ts +1 -1
- package/src/modules/communication_channels/api/post/channels/[id]/test-send/route.ts +21 -9
- package/src/modules/communication_channels/lib/adapter.ts +11 -0
- package/src/modules/communication_channels/lib/outbound-recipient.ts +14 -1
- package/src/modules/communication_channels/lib/test-seed.ts +19 -0
- package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.ts +1 -1
- package/src/modules/customers/backend/customers/deals/page.tsx +4 -3
- package/src/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.tsx +5 -5
- package/src/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.tsx +3 -3
- package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +2 -2
- package/src/modules/customers/cli.ts +5 -5
- package/src/modules/customers/commands/deals.ts +4 -3
- package/src/modules/customers/components/detail/pipelineStageUtils.ts +1 -0
- package/src/modules/customers/components/linking/adapters/dealAdapter.tsx +2 -2
- package/src/modules/customers/i18n/de.json +1 -1
- package/src/modules/customers/i18n/en.json +1 -1
- package/src/modules/customers/i18n/es.json +1 -1
- package/src/modules/customers/i18n/ko.json +1 -1
- package/src/modules/customers/i18n/pl.json +1 -1
- package/src/modules/customers/lib/closureStage.ts +2 -1
- package/src/modules/customers/lib/dealStatus.ts +23 -12
- package/src/modules/customers/lib/dealsSummaryQueries.ts +3 -3
- package/src/modules/customers/migrations/Migration20260824180000_deal_status_lost.ts +55 -0
- package/src/modules/dashboards/widgets/dashboard/pipeline-summary/config.ts +1 -1
- package/src/modules/eudr/notifications.ts +5 -0
- package/src/modules/query_index/lib/search-tokens.ts +100 -43
- package/src/modules/sales/commands/documents.ts +185 -105
- package/src/modules/sales/commands/returns.ts +1 -31
- package/src/modules/sales/commands/shared.ts +1 -4
- package/src/modules/sales/components/documents/SalesOrderDraftLines.tsx +12 -1
- package/src/modules/sales/data/validators.ts +3 -0
- package/src/modules/sales/lib/calculations.ts +33 -7
- package/src/modules/sales/lib/json.ts +4 -0
- package/src/modules/sales/lib/lineSnapshots.ts +98 -0
- package/src/modules/sales/lib/types.ts +20 -0
- package/src/modules/warranty_claims/notifications.ts +5 -0
- package/src/modules/wms/notifications.ts +2 -0
|
@@ -2,6 +2,7 @@ const notificationTypes = [
|
|
|
2
2
|
{
|
|
3
3
|
type: "warranty_claims.claim.submitted",
|
|
4
4
|
module: "warranty_claims",
|
|
5
|
+
channels: ["in_app", "email"],
|
|
5
6
|
titleKey: "warranty_claims.notifications.submitted.title",
|
|
6
7
|
bodyKey: "warranty_claims.notifications.submitted.body",
|
|
7
8
|
icon: "clipboard-check",
|
|
@@ -21,6 +22,7 @@ const notificationTypes = [
|
|
|
21
22
|
{
|
|
22
23
|
type: "warranty_claims.claim.assigned",
|
|
23
24
|
module: "warranty_claims",
|
|
25
|
+
channels: ["in_app", "email"],
|
|
24
26
|
titleKey: "warranty_claims.notifications.assigned.title",
|
|
25
27
|
bodyKey: "warranty_claims.notifications.assigned.body",
|
|
26
28
|
icon: "user-check",
|
|
@@ -40,6 +42,7 @@ const notificationTypes = [
|
|
|
40
42
|
{
|
|
41
43
|
type: "warranty_claims.claim.status_changed",
|
|
42
44
|
module: "warranty_claims",
|
|
45
|
+
channels: ["in_app", "email"],
|
|
43
46
|
titleKey: "warranty_claims.notifications.statusChanged.title",
|
|
44
47
|
bodyKey: "warranty_claims.notifications.statusChanged.body",
|
|
45
48
|
icon: "refresh-cw",
|
|
@@ -59,6 +62,7 @@ const notificationTypes = [
|
|
|
59
62
|
{
|
|
60
63
|
type: "warranty_claims.claim.escalated",
|
|
61
64
|
module: "warranty_claims",
|
|
65
|
+
channels: ["in_app", "email"],
|
|
62
66
|
titleKey: "warranty_claims.notifications.escalated.title",
|
|
63
67
|
bodyKey: "warranty_claims.notifications.escalated.body",
|
|
64
68
|
icon: "alarm-clock",
|
|
@@ -78,6 +82,7 @@ const notificationTypes = [
|
|
|
78
82
|
{
|
|
79
83
|
type: "warranty_claims.claim.customer_replied",
|
|
80
84
|
module: "warranty_claims",
|
|
85
|
+
channels: ["in_app", "email"],
|
|
81
86
|
titleKey: "warranty_claims.notifications.customerReplied.title",
|
|
82
87
|
bodyKey: "warranty_claims.notifications.customerReplied.body",
|
|
83
88
|
icon: "message-square-reply",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/warranty_claims/notifications.ts"],
|
|
4
|
-
"sourcesContent": ["import type { NotificationTypeDefinition } from '@open-mercato/shared/modules/notifications/types'\n\nexport const notificationTypes: NotificationTypeDefinition[] = [\n {\n type: 'warranty_claims.claim.submitted',\n module: 'warranty_claims',\n titleKey: 'warranty_claims.notifications.submitted.title',\n bodyKey: 'warranty_claims.notifications.submitted.body',\n icon: 'clipboard-check',\n severity: 'info',\n actions: [\n {\n id: 'view',\n labelKey: 'common.view',\n variant: 'outline',\n href: '/backend/warranty_claims/{sourceEntityId}',\n icon: 'external-link',\n },\n ],\n linkHref: '/backend/warranty_claims/{sourceEntityId}',\n expiresAfterHours: 168,\n },\n {\n type: 'warranty_claims.claim.assigned',\n module: 'warranty_claims',\n titleKey: 'warranty_claims.notifications.assigned.title',\n bodyKey: 'warranty_claims.notifications.assigned.body',\n icon: 'user-check',\n severity: 'info',\n actions: [\n {\n id: 'view',\n labelKey: 'common.view',\n variant: 'outline',\n href: '/backend/warranty_claims/{sourceEntityId}',\n icon: 'external-link',\n },\n ],\n linkHref: '/backend/warranty_claims/{sourceEntityId}',\n expiresAfterHours: 168,\n },\n {\n type: 'warranty_claims.claim.status_changed',\n module: 'warranty_claims',\n titleKey: 'warranty_claims.notifications.statusChanged.title',\n bodyKey: 'warranty_claims.notifications.statusChanged.body',\n icon: 'refresh-cw',\n severity: 'info',\n actions: [\n {\n id: 'view',\n labelKey: 'common.view',\n variant: 'outline',\n href: '/backend/warranty_claims/{sourceEntityId}',\n icon: 'external-link',\n },\n ],\n linkHref: '/backend/warranty_claims/{sourceEntityId}',\n expiresAfterHours: 168,\n },\n {\n type: 'warranty_claims.claim.escalated',\n module: 'warranty_claims',\n titleKey: 'warranty_claims.notifications.escalated.title',\n bodyKey: 'warranty_claims.notifications.escalated.body',\n icon: 'alarm-clock',\n severity: 'warning',\n actions: [\n {\n id: 'view',\n labelKey: 'common.view',\n variant: 'outline',\n href: '/backend/warranty_claims/{sourceEntityId}',\n icon: 'external-link',\n },\n ],\n linkHref: '/backend/warranty_claims/{sourceEntityId}',\n expiresAfterHours: 168,\n },\n {\n type: 'warranty_claims.claim.customer_replied',\n module: 'warranty_claims',\n titleKey: 'warranty_claims.notifications.customerReplied.title',\n bodyKey: 'warranty_claims.notifications.customerReplied.body',\n icon: 'message-square-reply',\n severity: 'info',\n actions: [\n {\n id: 'view',\n labelKey: 'common.view',\n variant: 'outline',\n href: '/backend/warranty_claims/{sourceEntityId}',\n icon: 'external-link',\n },\n ],\n linkHref: '/backend/warranty_claims/{sourceEntityId}',\n expiresAfterHours: 168,\n },\n]\n\nexport default notificationTypes\n"],
|
|
5
|
-
"mappings": "AAEO,MAAM,oBAAkD;AAAA,EAC7D;AAAA,IACE,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,SAAS;AAAA,IACT,MAAM;AAAA,IACN,UAAU;AAAA,IACV,SAAS;AAAA,MACP;AAAA,QACE,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,UAAU;AAAA,IACV,mBAAmB;AAAA,EACrB;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,SAAS;AAAA,IACT,MAAM;AAAA,IACN,UAAU;AAAA,IACV,SAAS;AAAA,MACP;AAAA,QACE,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,UAAU;AAAA,IACV,mBAAmB;AAAA,EACrB;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,SAAS;AAAA,IACT,MAAM;AAAA,IACN,UAAU;AAAA,IACV,SAAS;AAAA,MACP;AAAA,QACE,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,UAAU;AAAA,IACV,mBAAmB;AAAA,EACrB;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,SAAS;AAAA,IACT,MAAM;AAAA,IACN,UAAU;AAAA,IACV,SAAS;AAAA,MACP;AAAA,QACE,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,UAAU;AAAA,IACV,mBAAmB;AAAA,EACrB;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,SAAS;AAAA,IACT,MAAM;AAAA,IACN,UAAU;AAAA,IACV,SAAS;AAAA,MACP;AAAA,QACE,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,UAAU;AAAA,IACV,mBAAmB;AAAA,EACrB;AACF;AAEA,IAAO,wBAAQ;",
|
|
4
|
+
"sourcesContent": ["import type { NotificationTypeDefinition } from '@open-mercato/shared/modules/notifications/types'\n\nexport const notificationTypes: NotificationTypeDefinition[] = [\n {\n type: 'warranty_claims.claim.submitted',\n module: 'warranty_claims',\n channels: ['in_app', 'email'],\n titleKey: 'warranty_claims.notifications.submitted.title',\n bodyKey: 'warranty_claims.notifications.submitted.body',\n icon: 'clipboard-check',\n severity: 'info',\n actions: [\n {\n id: 'view',\n labelKey: 'common.view',\n variant: 'outline',\n href: '/backend/warranty_claims/{sourceEntityId}',\n icon: 'external-link',\n },\n ],\n linkHref: '/backend/warranty_claims/{sourceEntityId}',\n expiresAfterHours: 168,\n },\n {\n type: 'warranty_claims.claim.assigned',\n module: 'warranty_claims',\n channels: ['in_app', 'email'],\n titleKey: 'warranty_claims.notifications.assigned.title',\n bodyKey: 'warranty_claims.notifications.assigned.body',\n icon: 'user-check',\n severity: 'info',\n actions: [\n {\n id: 'view',\n labelKey: 'common.view',\n variant: 'outline',\n href: '/backend/warranty_claims/{sourceEntityId}',\n icon: 'external-link',\n },\n ],\n linkHref: '/backend/warranty_claims/{sourceEntityId}',\n expiresAfterHours: 168,\n },\n {\n type: 'warranty_claims.claim.status_changed',\n module: 'warranty_claims',\n channels: ['in_app', 'email'],\n titleKey: 'warranty_claims.notifications.statusChanged.title',\n bodyKey: 'warranty_claims.notifications.statusChanged.body',\n icon: 'refresh-cw',\n severity: 'info',\n actions: [\n {\n id: 'view',\n labelKey: 'common.view',\n variant: 'outline',\n href: '/backend/warranty_claims/{sourceEntityId}',\n icon: 'external-link',\n },\n ],\n linkHref: '/backend/warranty_claims/{sourceEntityId}',\n expiresAfterHours: 168,\n },\n {\n type: 'warranty_claims.claim.escalated',\n module: 'warranty_claims',\n channels: ['in_app', 'email'],\n titleKey: 'warranty_claims.notifications.escalated.title',\n bodyKey: 'warranty_claims.notifications.escalated.body',\n icon: 'alarm-clock',\n severity: 'warning',\n actions: [\n {\n id: 'view',\n labelKey: 'common.view',\n variant: 'outline',\n href: '/backend/warranty_claims/{sourceEntityId}',\n icon: 'external-link',\n },\n ],\n linkHref: '/backend/warranty_claims/{sourceEntityId}',\n expiresAfterHours: 168,\n },\n {\n type: 'warranty_claims.claim.customer_replied',\n module: 'warranty_claims',\n channels: ['in_app', 'email'],\n titleKey: 'warranty_claims.notifications.customerReplied.title',\n bodyKey: 'warranty_claims.notifications.customerReplied.body',\n icon: 'message-square-reply',\n severity: 'info',\n actions: [\n {\n id: 'view',\n labelKey: 'common.view',\n variant: 'outline',\n href: '/backend/warranty_claims/{sourceEntityId}',\n icon: 'external-link',\n },\n ],\n linkHref: '/backend/warranty_claims/{sourceEntityId}',\n expiresAfterHours: 168,\n },\n]\n\nexport default notificationTypes\n"],
|
|
5
|
+
"mappings": "AAEO,MAAM,oBAAkD;AAAA,EAC7D;AAAA,IACE,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU,CAAC,UAAU,OAAO;AAAA,IAC5B,UAAU;AAAA,IACV,SAAS;AAAA,IACT,MAAM;AAAA,IACN,UAAU;AAAA,IACV,SAAS;AAAA,MACP;AAAA,QACE,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,UAAU;AAAA,IACV,mBAAmB;AAAA,EACrB;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU,CAAC,UAAU,OAAO;AAAA,IAC5B,UAAU;AAAA,IACV,SAAS;AAAA,IACT,MAAM;AAAA,IACN,UAAU;AAAA,IACV,SAAS;AAAA,MACP;AAAA,QACE,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,UAAU;AAAA,IACV,mBAAmB;AAAA,EACrB;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU,CAAC,UAAU,OAAO;AAAA,IAC5B,UAAU;AAAA,IACV,SAAS;AAAA,IACT,MAAM;AAAA,IACN,UAAU;AAAA,IACV,SAAS;AAAA,MACP;AAAA,QACE,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,UAAU;AAAA,IACV,mBAAmB;AAAA,EACrB;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU,CAAC,UAAU,OAAO;AAAA,IAC5B,UAAU;AAAA,IACV,SAAS;AAAA,IACT,MAAM;AAAA,IACN,UAAU;AAAA,IACV,SAAS;AAAA,MACP;AAAA,QACE,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,UAAU;AAAA,IACV,mBAAmB;AAAA,EACrB;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU,CAAC,UAAU,OAAO;AAAA,IAC5B,UAAU;AAAA,IACV,SAAS;AAAA,IACT,MAAM;AAAA,IACN,UAAU;AAAA,IACV,SAAS;AAAA,MACP;AAAA,QACE,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,UAAU;AAAA,IACV,mBAAmB;AAAA,EACrB;AACF;AAEA,IAAO,wBAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,6 +2,7 @@ const notificationTypes = [
|
|
|
2
2
|
{
|
|
3
3
|
type: "wms.inventory.low_stock",
|
|
4
4
|
module: "wms",
|
|
5
|
+
channels: ["in_app", "email"],
|
|
5
6
|
titleKey: "wms.notifications.lowStock.title",
|
|
6
7
|
bodyKey: "wms.notifications.lowStock.body",
|
|
7
8
|
icon: "alert-triangle",
|
|
@@ -21,6 +22,7 @@ const notificationTypes = [
|
|
|
21
22
|
{
|
|
22
23
|
type: "wms.inventory.reservation_shortfall",
|
|
23
24
|
module: "wms",
|
|
25
|
+
channels: ["in_app", "email"],
|
|
24
26
|
titleKey: "wms.notifications.reservationShortfall.title",
|
|
25
27
|
bodyKey: "wms.notifications.reservationShortfall.body",
|
|
26
28
|
icon: "alert-triangle",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/wms/notifications.ts"],
|
|
4
|
-
"sourcesContent": ["import type { NotificationTypeDefinition } from '@open-mercato/shared/modules/notifications/types'\n\nexport const notificationTypes: NotificationTypeDefinition[] = [\n {\n type: 'wms.inventory.low_stock',\n module: 'wms',\n titleKey: 'wms.notifications.lowStock.title',\n bodyKey: 'wms.notifications.lowStock.body',\n icon: 'alert-triangle',\n severity: 'warning',\n actions: [\n {\n id: 'view',\n labelKey: 'wms.notifications.lowStock.renderer.viewInventory',\n variant: 'outline',\n href: '/backend/wms/inventory',\n icon: 'external-link',\n },\n ],\n linkHref: '/backend/wms/inventory',\n expiresAfterHours: 72,\n },\n {\n type: 'wms.inventory.reservation_shortfall',\n module: 'wms',\n titleKey: 'wms.notifications.reservationShortfall.title',\n bodyKey: 'wms.notifications.reservationShortfall.body',\n icon: 'alert-triangle',\n severity: 'warning',\n actions: [\n {\n id: 'view-order',\n labelKey: 'wms.notifications.reservationShortfall.renderer.viewOrder',\n variant: 'outline',\n href: '/backend/sales/orders/{sourceEntityId}',\n icon: 'external-link',\n },\n {\n id: 'view-inventory',\n labelKey: 'wms.notifications.reservationShortfall.renderer.viewInventory',\n variant: 'outline',\n href: '/backend/wms/inventory',\n icon: 'external-link',\n },\n ],\n linkHref: '/backend/sales/orders/{sourceEntityId}',\n expiresAfterHours: 72,\n },\n]\n\nexport default notificationTypes\n"],
|
|
5
|
-
"mappings": "AAEO,MAAM,oBAAkD;AAAA,EAC7D;AAAA,IACE,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,SAAS;AAAA,IACT,MAAM;AAAA,IACN,UAAU;AAAA,IACV,SAAS;AAAA,MACP;AAAA,QACE,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,UAAU;AAAA,IACV,mBAAmB;AAAA,EACrB;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,SAAS;AAAA,IACT,MAAM;AAAA,IACN,UAAU;AAAA,IACV,SAAS;AAAA,MACP;AAAA,QACE,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,MACA;AAAA,QACE,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,UAAU;AAAA,IACV,mBAAmB;AAAA,EACrB;AACF;AAEA,IAAO,wBAAQ;",
|
|
4
|
+
"sourcesContent": ["import type { NotificationTypeDefinition } from '@open-mercato/shared/modules/notifications/types'\n\nexport const notificationTypes: NotificationTypeDefinition[] = [\n {\n type: 'wms.inventory.low_stock',\n module: 'wms',\n channels: ['in_app', 'email'],\n titleKey: 'wms.notifications.lowStock.title',\n bodyKey: 'wms.notifications.lowStock.body',\n icon: 'alert-triangle',\n severity: 'warning',\n actions: [\n {\n id: 'view',\n labelKey: 'wms.notifications.lowStock.renderer.viewInventory',\n variant: 'outline',\n href: '/backend/wms/inventory',\n icon: 'external-link',\n },\n ],\n linkHref: '/backend/wms/inventory',\n expiresAfterHours: 72,\n },\n {\n type: 'wms.inventory.reservation_shortfall',\n module: 'wms',\n channels: ['in_app', 'email'],\n titleKey: 'wms.notifications.reservationShortfall.title',\n bodyKey: 'wms.notifications.reservationShortfall.body',\n icon: 'alert-triangle',\n severity: 'warning',\n actions: [\n {\n id: 'view-order',\n labelKey: 'wms.notifications.reservationShortfall.renderer.viewOrder',\n variant: 'outline',\n href: '/backend/sales/orders/{sourceEntityId}',\n icon: 'external-link',\n },\n {\n id: 'view-inventory',\n labelKey: 'wms.notifications.reservationShortfall.renderer.viewInventory',\n variant: 'outline',\n href: '/backend/wms/inventory',\n icon: 'external-link',\n },\n ],\n linkHref: '/backend/sales/orders/{sourceEntityId}',\n expiresAfterHours: 72,\n },\n]\n\nexport default notificationTypes\n"],
|
|
5
|
+
"mappings": "AAEO,MAAM,oBAAkD;AAAA,EAC7D;AAAA,IACE,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU,CAAC,UAAU,OAAO;AAAA,IAC5B,UAAU;AAAA,IACV,SAAS;AAAA,IACT,MAAM;AAAA,IACN,UAAU;AAAA,IACV,SAAS;AAAA,MACP;AAAA,QACE,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,UAAU;AAAA,IACV,mBAAmB;AAAA,EACrB;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU,CAAC,UAAU,OAAO;AAAA,IAC5B,UAAU;AAAA,IACV,SAAS;AAAA,IACT,MAAM;AAAA,IACN,UAAU;AAAA,IACV,SAAS;AAAA,MACP;AAAA,QACE,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,MACA;AAAA,QACE,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,UAAU;AAAA,IACV,mBAAmB;AAAA,EACrB;AACF;AAEA,IAAO,wBAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.7.1-develop.
|
|
3
|
+
"version": "0.7.1-develop.7121.1.734c263bda",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -252,16 +252,16 @@
|
|
|
252
252
|
"zod": "^4.4.3"
|
|
253
253
|
},
|
|
254
254
|
"peerDependencies": {
|
|
255
|
-
"@open-mercato/ai-assistant": "0.7.1-develop.
|
|
256
|
-
"@open-mercato/shared": "0.7.1-develop.
|
|
257
|
-
"@open-mercato/ui": "0.7.1-develop.
|
|
255
|
+
"@open-mercato/ai-assistant": "0.7.1-develop.7121.1.734c263bda",
|
|
256
|
+
"@open-mercato/shared": "0.7.1-develop.7121.1.734c263bda",
|
|
257
|
+
"@open-mercato/ui": "0.7.1-develop.7121.1.734c263bda",
|
|
258
258
|
"react": "^19.0.0",
|
|
259
259
|
"react-dom": "^19.0.0"
|
|
260
260
|
},
|
|
261
261
|
"devDependencies": {
|
|
262
|
-
"@open-mercato/ai-assistant": "0.7.1-develop.
|
|
263
|
-
"@open-mercato/shared": "0.7.1-develop.
|
|
264
|
-
"@open-mercato/ui": "0.7.1-develop.
|
|
262
|
+
"@open-mercato/ai-assistant": "0.7.1-develop.7121.1.734c263bda",
|
|
263
|
+
"@open-mercato/shared": "0.7.1-develop.7121.1.734c263bda",
|
|
264
|
+
"@open-mercato/ui": "0.7.1-develop.7121.1.734c263bda",
|
|
265
265
|
"@testing-library/dom": "^10.4.1",
|
|
266
266
|
"@testing-library/jest-dom": "^7.0.0",
|
|
267
267
|
"@testing-library/react": "^16.3.1",
|
|
@@ -81,7 +81,7 @@ export async function createDealFixture(
|
|
|
81
81
|
valueCurrency?: string;
|
|
82
82
|
// Optional deal lifecycle fields — all valid on `dealCreateSchema`. Forwarded so KPI/summary
|
|
83
83
|
// tests can seed won/lost/overdue/owned deals across quarters (see TC-CRM-082). `status` is a
|
|
84
|
-
// free-form dictionary value (e.g. 'open', 'in_progress', 'win', '
|
|
84
|
+
// free-form dictionary value (e.g. 'open', 'in_progress', 'win', 'lost'); `expectedCloseAt`
|
|
85
85
|
// accepts an ISO string (the schema coerces it to a Date); `closureOutcome` is 'won' | 'lost'.
|
|
86
86
|
status?: string;
|
|
87
87
|
expectedCloseAt?: string;
|
|
@@ -37,15 +37,20 @@ export const metadata = {
|
|
|
37
37
|
},
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
// `to` is deliberately NOT typed as an email here
|
|
41
|
-
// on the provider (an email address for Gmail/IMAP,
|
|
42
|
-
// Discord), and the adapter is only known once the
|
|
43
|
-
// schema accepts any non-empty single-line string and
|
|
44
|
-
// applies the provider-appropriate rules below,
|
|
45
|
-
//
|
|
46
|
-
//
|
|
40
|
+
// `to` is deliberately NOT typed as an email here, and deliberately optional.
|
|
41
|
+
// The recipient shape depends on the provider (an email address for Gmail/IMAP,
|
|
42
|
+
// a channel snowflake for Discord), and the adapter is only known once the
|
|
43
|
+
// channel is loaded — so the schema accepts any non-empty single-line string and
|
|
44
|
+
// `validateOutboundRecipient` applies the provider-appropriate rules below,
|
|
45
|
+
// defaulting to email. Whether the recipient may be left out is provider-derived
|
|
46
|
+
// too: an adapter that carries its own outbound target (Discord's
|
|
47
|
+
// `defaultChannelId`) accepts a body with no `to` and posts there, which is the
|
|
48
|
+
// smoke test the Discord spec documents; an email provider has no such default
|
|
49
|
+
// and `validateOutboundRecipient` still refuses the request. The CR/LF rejection
|
|
50
|
+
// stays at the schema level so a header-injection attempt is refused before the
|
|
51
|
+
// request reaches the channel lookup or the mutation guard.
|
|
47
52
|
const bodySchema = z.object({
|
|
48
|
-
to: z.string().min(1).max(MAX_OUTBOUND_RECIPIENT_LENGTH).regex(/^[^\r\n]*$/),
|
|
53
|
+
to: z.string().min(1).max(MAX_OUTBOUND_RECIPIENT_LENGTH).regex(/^[^\r\n]*$/).optional(),
|
|
49
54
|
subject: z.string().min(1).max(500).optional(),
|
|
50
55
|
body: z.string().max(50_000).optional(),
|
|
51
56
|
})
|
|
@@ -232,7 +237,14 @@ export async function POST(req: Request, context: RouteContext): Promise<Respons
|
|
|
232
237
|
tenantId: auth.tenantId as string,
|
|
233
238
|
organizationId: channelCredentialsOrg,
|
|
234
239
|
},
|
|
235
|
-
|
|
240
|
+
// `to` is omitted from the metadata rather than passed as `undefined` when
|
|
241
|
+
// the caller left it out, so an adapter reading `metadata.to` sees no key
|
|
242
|
+
// at all and falls through to its own configured target.
|
|
243
|
+
metadata: {
|
|
244
|
+
...(body.to !== undefined ? { to: body.to } : {}),
|
|
245
|
+
subject: body.subject ?? 'Test send',
|
|
246
|
+
testSend: true,
|
|
247
|
+
},
|
|
236
248
|
})
|
|
237
249
|
await guard.afterSuccess()
|
|
238
250
|
return NextResponse.json({
|
|
@@ -60,6 +60,17 @@ export interface ChannelCapabilities {
|
|
|
60
60
|
* applies transport-safety checks only (see `validateOutboundRecipient`) and
|
|
61
61
|
* the adapter owns the provider-specific format — it MUST treat the value as
|
|
62
62
|
* untrusted input.
|
|
63
|
+
*
|
|
64
|
+
* **`'provider-native'` also opts the provider into presence-optionality, not
|
|
65
|
+
* only format.** The hub accepts an outbound request that names no recipient
|
|
66
|
+
* at all and forwards it with no `metadata.to` key, on the understanding that
|
|
67
|
+
* the adapter resolves its own configured target (Discord: `defaultChannelId`).
|
|
68
|
+
* An adapter declaring this therefore MUST resolve such a target, and MUST
|
|
69
|
+
* fail legibly — an operator-readable `SendMessageResult.error`, not a throw —
|
|
70
|
+
* when it has none, because that message is surfaced verbatim to whoever ran
|
|
71
|
+
* the send. Declare `'email'` (or omit the field) if your provider has no
|
|
72
|
+
* default destination: the hub then keeps answering `Recipient is required`,
|
|
73
|
+
* which is the correct outcome for a request with nowhere to go.
|
|
63
74
|
*/
|
|
64
75
|
recipientFormat?: 'email' | 'provider-native'
|
|
65
76
|
}
|
|
@@ -37,11 +37,24 @@ export type OutboundRecipientCheck = { ok: true } | { ok: false; error: string }
|
|
|
37
37
|
* path to send at all (#4976). Validation now follows the adapter's
|
|
38
38
|
* `capabilities.recipientFormat`, defaulting to `'email'` so every existing
|
|
39
39
|
* provider keeps byte-identical behavior.
|
|
40
|
+
*
|
|
41
|
+
* A `'provider-native'` provider may also be addressed by **omitting** the
|
|
42
|
+
* recipient entirely: those adapters are configured with their own outbound
|
|
43
|
+
* target (Discord's `defaultChannelId`) and fall back to it when the message
|
|
44
|
+
* names no channel, which is the smoke test the Discord spec documents in
|
|
45
|
+
* § 6 "Test it". An email provider has no such default, so `'email'` — the
|
|
46
|
+
* format every existing provider resolves to — still requires a recipient.
|
|
47
|
+
* Omission means `undefined` only: `null` or `''` is a caller that meant to
|
|
48
|
+
* supply an address and got it wrong, and stays a 422 on every provider.
|
|
40
49
|
*/
|
|
41
50
|
export function validateOutboundRecipient(
|
|
42
51
|
recipient: unknown,
|
|
43
52
|
capabilities: Pick<ChannelCapabilities, 'recipientFormat'> | null | undefined,
|
|
44
53
|
): OutboundRecipientCheck {
|
|
54
|
+
const isProviderNative = capabilities?.recipientFormat === 'provider-native'
|
|
55
|
+
if (recipient === undefined && isProviderNative) {
|
|
56
|
+
return { ok: true }
|
|
57
|
+
}
|
|
45
58
|
if (typeof recipient !== 'string' || recipient.length === 0) {
|
|
46
59
|
return { ok: false, error: 'Recipient is required' }
|
|
47
60
|
}
|
|
@@ -51,7 +64,7 @@ export function validateOutboundRecipient(
|
|
|
51
64
|
error: `Recipient must be at most ${MAX_OUTBOUND_RECIPIENT_LENGTH} characters`,
|
|
52
65
|
}
|
|
53
66
|
}
|
|
54
|
-
if (
|
|
67
|
+
if (!isProviderNative) {
|
|
55
68
|
return emailRecipientSchema.safeParse(recipient).success
|
|
56
69
|
? { ok: true }
|
|
57
70
|
: { ok: false, error: 'Recipient must be a valid email address' }
|
|
@@ -136,6 +136,24 @@ class TestSeedChannelAdapter implements ChannelAdapter {
|
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Capabilities for the chat stub. Identical to the email stub's except for the
|
|
141
|
+
* recipient shape: a chat channel is addressed by a provider-issued identifier
|
|
142
|
+
* (a Discord snowflake), not an email address.
|
|
143
|
+
*
|
|
144
|
+
* Without this override the chat stub inherited `baseEmailCapabilities`, so it
|
|
145
|
+
* claimed `channelType: 'discord'` while validating recipients as email
|
|
146
|
+
* addresses — a channel that could not be addressed the way the provider it
|
|
147
|
+
* imitates actually is. That left the hub's `'provider-native'` branch
|
|
148
|
+
* (#4976: per-provider recipient validation, and the omitted recipient that
|
|
149
|
+
* falls back to the adapter's own target) reachable only with a live Discord
|
|
150
|
+
* bot, so no integration test could cover it and CI never exercised it.
|
|
151
|
+
*/
|
|
152
|
+
const testSeedChatCapabilities: ChannelCapabilities = {
|
|
153
|
+
...testSeedCapabilities,
|
|
154
|
+
recipientFormat: 'provider-native',
|
|
155
|
+
}
|
|
156
|
+
|
|
139
157
|
/**
|
|
140
158
|
* Chat-flavoured twin of {@link TestSeedChannelAdapter}: same network-free
|
|
141
159
|
* behaviour, but it declares a non-email `channelType`, so a channel connected
|
|
@@ -145,6 +163,7 @@ class TestSeedChannelAdapter implements ChannelAdapter {
|
|
|
145
163
|
class TestSeedChatChannelAdapter extends TestSeedChannelAdapter {
|
|
146
164
|
readonly providerKey: string = TEST_SEED_CHAT_PROVIDER_KEY
|
|
147
165
|
readonly channelType: string = 'discord'
|
|
166
|
+
readonly capabilities = testSeedChatCapabilities
|
|
148
167
|
|
|
149
168
|
async normalizeInbound(raw: InboundMessage): Promise<NormalizedInboundMessage> {
|
|
150
169
|
// Unlike the email stub, this one is reachable: the test-seed ingest action
|
|
@@ -28,6 +28,7 @@ import { Avatar, AvatarStack } from '@open-mercato/ui/primitives/avatar'
|
|
|
28
28
|
import { Tag } from '@open-mercato/ui/primitives/tag'
|
|
29
29
|
import { SimpleTooltip } from '@open-mercato/ui/primitives/tooltip'
|
|
30
30
|
import { Briefcase, AlertTriangle, X } from 'lucide-react'
|
|
31
|
+
import { isLostDealStatus, isWonDealStatus } from '../../../lib/dealStatus'
|
|
31
32
|
import { formatRelativeTime } from '@open-mercato/shared/lib/time'
|
|
32
33
|
import { ViewTabsRow } from './pipeline/components/ViewTabsRow'
|
|
33
34
|
import { DealsKpiStrip } from '../../../components/DealsKpiStrip'
|
|
@@ -86,7 +87,7 @@ function makeDealsPresets(): FilterPreset[] {
|
|
|
86
87
|
},
|
|
87
88
|
},
|
|
88
89
|
// The Deal entity has no dedicated "at risk" or health-score field — `customer_deals`
|
|
89
|
-
// exposes only `status` (open/win/
|
|
90
|
+
// exposes only `status` (open/win/lost/closed/in_progress, dictionary-driven) and
|
|
90
91
|
// `closure_outcome`. Rather than fabricate a mapping, the "At risk" preset is omitted
|
|
91
92
|
// until the data model exposes a first-class signal.
|
|
92
93
|
{
|
|
@@ -855,11 +856,11 @@ export default function CustomersDealsPage() {
|
|
|
855
856
|
subtitle = (
|
|
856
857
|
<span className="text-xs text-status-error-text">{t('customers.deals.list.close.overdue')}</span>
|
|
857
858
|
)
|
|
858
|
-
} else if (row.original.status
|
|
859
|
+
} else if (isWonDealStatus(row.original.status)) {
|
|
859
860
|
subtitle = (
|
|
860
861
|
<span className="text-xs text-muted-foreground">{t('customers.deals.list.close.won')}</span>
|
|
861
862
|
)
|
|
862
|
-
} else if (row.original.status
|
|
863
|
+
} else if (isLostDealStatus(row.original.status)) {
|
|
863
864
|
subtitle = (
|
|
864
865
|
<span className="text-xs text-muted-foreground">{t('customers.deals.list.close.lost')}</span>
|
|
865
866
|
)
|
package/src/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.tsx
CHANGED
|
@@ -21,11 +21,11 @@ import { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'
|
|
|
21
21
|
import { registerComponent } from '@open-mercato/shared/modules/widgets/component-registry'
|
|
22
22
|
|
|
23
23
|
// Deal status values written by the kanban flow. These intentionally do NOT include 'closed'
|
|
24
|
-
// because the rest of the app only persists '
|
|
24
|
+
// because the rest of the app only persists 'lost' for lost deals (see StatusFilterPopover
|
|
25
25
|
// notes). 'in_progress' is kept for backwards compatibility with quick-deals created from
|
|
26
|
-
// the previous version of this dialog.
|
|
27
|
-
//
|
|
28
|
-
const STATUS_OPTIONS = ['open', 'in_progress', 'win', '
|
|
26
|
+
// the previous version of this dialog. Deals written before 0.7.1 carry the misspelled
|
|
27
|
+
// 'loose'; readers still match it through `lib/dealStatus.ts`, but no writer produces it.
|
|
28
|
+
const STATUS_OPTIONS = ['open', 'in_progress', 'win', 'lost'] as const
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Tenant currency option. The page resolves these from the currencies module (see
|
|
@@ -266,7 +266,7 @@ export function QuickDealDialog({
|
|
|
266
266
|
id: 'status',
|
|
267
267
|
label: translateWithFallback(t, 'customers.deals.kanban.quickDeal.status', 'Status'),
|
|
268
268
|
type: 'select',
|
|
269
|
-
// Storage value is preserved (`
|
|
269
|
+
// Storage value is preserved (`lost` etc.), only the displayed label is
|
|
270
270
|
// translated, so we never write a localised string back to the DB.
|
|
271
271
|
options: STATUS_OPTIONS.map((statusValue) => ({
|
|
272
272
|
value: statusValue,
|
package/src/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.tsx
CHANGED
|
@@ -23,13 +23,13 @@ import { FilterPopoverShell } from './FilterPopoverShell'
|
|
|
23
23
|
* Filter options exposed to the operator.
|
|
24
24
|
*
|
|
25
25
|
* The deal `status` column is dictionary-driven (`deal-statuses`); the seeded dictionary
|
|
26
|
-
* carries five values (`open`, `closed`, `win`, `
|
|
26
|
+
* carries five values (`open`, `closed`, `win`, `lost`, `in_progress`) plus any
|
|
27
27
|
* tenant-custom entries. We render every dictionary entry de-duplicated by canonical
|
|
28
28
|
* spelling so the kanban Status pill stays aligned with the list page's advanced filter
|
|
29
29
|
* (which also uses the dictionary). A hard-coded fallback keeps the popover usable while
|
|
30
30
|
* the dictionary is loading or when a tenant has no entries.
|
|
31
31
|
*
|
|
32
|
-
* `won` / `
|
|
32
|
+
* `won` / `loose` are accepted as aliases for `win` / `lost` at the API layer (see
|
|
33
33
|
* `lib/dealStatus.ts:expandDealStatusAliases`, shared by the deals list route and the
|
|
34
34
|
* kanban aggregate route). The UI only exposes the canonical values to avoid duplicate
|
|
35
35
|
* pills, but `canonicalDealStatus` normalizes any alias passed in through `values` so
|
|
@@ -57,7 +57,7 @@ const FALLBACK_STATUS_OPTIONS: Array<{
|
|
|
57
57
|
dotClass: 'bg-status-success-icon',
|
|
58
58
|
},
|
|
59
59
|
{
|
|
60
|
-
value: '
|
|
60
|
+
value: 'lost',
|
|
61
61
|
labelKey: 'customers.deals.kanban.filter.status.lost',
|
|
62
62
|
labelFallback: 'Lost',
|
|
63
63
|
dotClass: 'bg-status-error-icon',
|
|
@@ -1766,7 +1766,7 @@ export default function DealsKanbanPage(): React.ReactElement {
|
|
|
1766
1766
|
}, [invalidateKanbanData, scopeVersion, selectedPipelineId])
|
|
1767
1767
|
|
|
1768
1768
|
const updateDealStatus = React.useCallback(
|
|
1769
|
-
async (dealId: string, status: 'win' | '
|
|
1769
|
+
async (dealId: string, status: 'win' | 'lost') => {
|
|
1770
1770
|
const dealVersion = deals.find((deal) => deal.id === dealId)?.updatedAt ?? null
|
|
1771
1771
|
setPendingDealId(dealId)
|
|
1772
1772
|
try {
|
|
@@ -2348,7 +2348,7 @@ export default function DealsKanbanPage(): React.ReactElement {
|
|
|
2348
2348
|
{
|
|
2349
2349
|
id: 'mark-lost',
|
|
2350
2350
|
label: translateWithFallback(t, 'customers.deals.kanban.menu.markLost', 'Mark as Lost'),
|
|
2351
|
-
onSelect: () => void updateDealStatus(deal.id, '
|
|
2351
|
+
onSelect: () => void updateDealStatus(deal.id, 'lost'),
|
|
2352
2352
|
},
|
|
2353
2353
|
{
|
|
2354
2354
|
id: 'delete',
|
|
@@ -59,7 +59,7 @@ export const DEAL_STATUS_DEFAULTS: DictionaryDefault[] = [
|
|
|
59
59
|
{ value: 'open', label: 'Open', color: '#2563eb', icon: 'lucide:circle' },
|
|
60
60
|
{ value: 'closed', label: 'Closed', color: '#6b7280', icon: 'lucide:check-circle' },
|
|
61
61
|
{ value: 'win', label: 'Win', color: '#22c55e', icon: 'lucide:trophy' },
|
|
62
|
-
{ value: '
|
|
62
|
+
{ value: 'lost', label: 'Lost', color: '#ef4444', icon: 'lucide:flag' },
|
|
63
63
|
{ value: 'in_progress', label: 'In progress', color: '#f59e0b', icon: 'lucide:activity' },
|
|
64
64
|
]
|
|
65
65
|
|
|
@@ -70,7 +70,7 @@ export const PIPELINE_STAGE_DEFAULTS: DictionaryDefault[] = [
|
|
|
70
70
|
{ value: 'offering', label: 'Offering', color: '#22c55e', icon: 'lucide:package' },
|
|
71
71
|
{ value: 'negotiations', label: 'Negotiations', color: '#facc15', icon: 'lucide:handshake' },
|
|
72
72
|
{ value: 'win', label: 'Win', color: '#16a34a', icon: 'lucide:award' },
|
|
73
|
-
{ value: '
|
|
73
|
+
{ value: 'lost', label: 'Lost', color: '#ef4444', icon: 'lucide:flag' },
|
|
74
74
|
{ value: 'stalled', label: 'Stalled', color: '#6b7280', icon: 'lucide:alert-circle' },
|
|
75
75
|
]
|
|
76
76
|
|
|
@@ -827,8 +827,8 @@ export const CUSTOMER_EXAMPLES: ExampleCompany[] = [
|
|
|
827
827
|
slug: 'cedar-creek-retreat',
|
|
828
828
|
title: 'Cedar Creek Retreat Expansion',
|
|
829
829
|
description: 'New wellness center build-out including retail area and treatment rooms.',
|
|
830
|
-
status: '
|
|
831
|
-
pipelineStage: '
|
|
830
|
+
status: 'lost',
|
|
831
|
+
pipelineStage: 'lost',
|
|
832
832
|
valueAmount: 98000,
|
|
833
833
|
valueCurrency: 'USD',
|
|
834
834
|
expectedCloseAt: isoDaysFromNow(-70),
|
|
@@ -2549,7 +2549,7 @@ async function seedCustomerStressTest(
|
|
|
2549
2549
|
const dealId = randomUUID()
|
|
2550
2550
|
const valueAmount = toAmount(monetaryBase + randomInt(0, 7500))
|
|
2551
2551
|
const expectedCloseAt =
|
|
2552
|
-
dealStatus === 'win' || dealStatus === 'closed' || dealStatus === '
|
|
2552
|
+
dealStatus === 'win' || dealStatus === 'closed' || dealStatus === 'lost'
|
|
2553
2553
|
? randomPastDate(120)
|
|
2554
2554
|
: randomFutureDate(120)
|
|
2555
2555
|
const dealRow: CustomerDealRow = {
|
|
@@ -120,9 +120,9 @@ function resolveRequestedClosureOutcome(input: DealUpdateInput): DealClosureOutc
|
|
|
120
120
|
if (input.closureOutcome === 'won' || input.closureOutcome === 'lost') {
|
|
121
121
|
return input.closureOutcome
|
|
122
122
|
}
|
|
123
|
-
// `win` / `
|
|
124
|
-
// spellings the AI stage tool persists. Both must derive the same
|
|
125
|
-
// every closed deal lands in the pipeline's terminal stage (#5107).
|
|
123
|
+
// `win` / `lost` are what the UI closure flows persist; `won` / `lost` are the
|
|
124
|
+
// spellings the AI stage tool persists (`loose` before 0.7.1). Both must derive the same
|
|
125
|
+
// closure outcome so every closed deal lands in the pipeline's terminal stage (#5107).
|
|
126
126
|
return dealClosureOutcomeFromStatus(input.status)
|
|
127
127
|
}
|
|
128
128
|
|
|
@@ -846,6 +846,7 @@ const updateDealCommand: CommandHandler<DealUpdateInput, { dealId: string }> = {
|
|
|
846
846
|
// subscribers (workflow event triggers, business-rules triggers) drop a
|
|
847
847
|
// null-scoped event before trigger matching.
|
|
848
848
|
const newStatus = record.status
|
|
849
|
+
// `loose` stays mapped here for deals written before the 0.7.1 rename.
|
|
849
850
|
const normalizedStatus = newStatus === 'win' ? 'won' : newStatus === 'loose' ? 'lost' : newStatus
|
|
850
851
|
if (previousStatus !== newStatus && (normalizedStatus === 'won' || normalizedStatus === 'lost')) {
|
|
851
852
|
const closureEvent = normalizedStatus === 'won' ? 'customers.deal.won' : 'customers.deal.lost'
|
|
@@ -276,7 +276,7 @@ export function createDealLinkAdapter(options: DealAdapterOptions): LinkEntityAd
|
|
|
276
276
|
{ id: 'all', label: 'All' },
|
|
277
277
|
{ id: 'open', label: 'Open' },
|
|
278
278
|
{ id: 'win', label: 'Won' },
|
|
279
|
-
{ id: '
|
|
279
|
+
{ id: 'lost', label: 'Lost' },
|
|
280
280
|
{ id: 'orphan', label: 'Orphan', dotColor: '#eb9426' /* TODO(ds-review): #eb9426 — requires LinkEntityDialog to support semantic token classNames */ },
|
|
281
281
|
],
|
|
282
282
|
defaultId: 'all',
|
|
@@ -286,7 +286,7 @@ export function createDealLinkAdapter(options: DealAdapterOptions): LinkEntityAd
|
|
|
286
286
|
if (filterId === 'orphan') return meta.isOrphan === true
|
|
287
287
|
// Server-side status filter already applied; this keeps all rows if server honored the filter.
|
|
288
288
|
// Fallback client check on stage in case server returns mixed statuses.
|
|
289
|
-
if (filterId === 'open' || filterId === 'win' || filterId === '
|
|
289
|
+
if (filterId === 'open' || filterId === 'win' || filterId === 'lost') {
|
|
290
290
|
return (meta.stage ?? '').toLowerCase().includes(filterId) || true
|
|
291
291
|
}
|
|
292
292
|
return true
|
|
@@ -1380,7 +1380,7 @@
|
|
|
1380
1380
|
"customers.deals.kanban.quickDeal.probability": "Probability",
|
|
1381
1381
|
"customers.deals.kanban.quickDeal.status": "Status",
|
|
1382
1382
|
"customers.deals.kanban.quickDeal.status.in_progress": "In Bearbeitung",
|
|
1383
|
-
"customers.deals.kanban.quickDeal.status.
|
|
1383
|
+
"customers.deals.kanban.quickDeal.status.lost": "Verloren",
|
|
1384
1384
|
"customers.deals.kanban.quickDeal.status.open": "Offen",
|
|
1385
1385
|
"customers.deals.kanban.quickDeal.status.win": "Gewonnen",
|
|
1386
1386
|
"customers.deals.kanban.quickDeal.submit": "Add deal",
|
|
@@ -1380,7 +1380,7 @@
|
|
|
1380
1380
|
"customers.deals.kanban.quickDeal.probability": "Probability",
|
|
1381
1381
|
"customers.deals.kanban.quickDeal.status": "Status",
|
|
1382
1382
|
"customers.deals.kanban.quickDeal.status.in_progress": "In progress",
|
|
1383
|
-
"customers.deals.kanban.quickDeal.status.
|
|
1383
|
+
"customers.deals.kanban.quickDeal.status.lost": "Lost",
|
|
1384
1384
|
"customers.deals.kanban.quickDeal.status.open": "Open",
|
|
1385
1385
|
"customers.deals.kanban.quickDeal.status.win": "Won",
|
|
1386
1386
|
"customers.deals.kanban.quickDeal.submit": "Add deal",
|
|
@@ -1380,7 +1380,7 @@
|
|
|
1380
1380
|
"customers.deals.kanban.quickDeal.probability": "Probability",
|
|
1381
1381
|
"customers.deals.kanban.quickDeal.status": "Status",
|
|
1382
1382
|
"customers.deals.kanban.quickDeal.status.in_progress": "En curso",
|
|
1383
|
-
"customers.deals.kanban.quickDeal.status.
|
|
1383
|
+
"customers.deals.kanban.quickDeal.status.lost": "Perdido",
|
|
1384
1384
|
"customers.deals.kanban.quickDeal.status.open": "Abierto",
|
|
1385
1385
|
"customers.deals.kanban.quickDeal.status.win": "Ganado",
|
|
1386
1386
|
"customers.deals.kanban.quickDeal.submit": "Add deal",
|
|
@@ -1380,7 +1380,7 @@
|
|
|
1380
1380
|
"customers.deals.kanban.quickDeal.probability": "확률",
|
|
1381
1381
|
"customers.deals.kanban.quickDeal.status": "상태",
|
|
1382
1382
|
"customers.deals.kanban.quickDeal.status.in_progress": "진행 중",
|
|
1383
|
-
"customers.deals.kanban.quickDeal.status.
|
|
1383
|
+
"customers.deals.kanban.quickDeal.status.lost": "실패",
|
|
1384
1384
|
"customers.deals.kanban.quickDeal.status.open": "열림",
|
|
1385
1385
|
"customers.deals.kanban.quickDeal.status.win": "성사",
|
|
1386
1386
|
"customers.deals.kanban.quickDeal.submit": "거래 추가",
|
|
@@ -1380,7 +1380,7 @@
|
|
|
1380
1380
|
"customers.deals.kanban.quickDeal.probability": "Prawdopodobieństwo",
|
|
1381
1381
|
"customers.deals.kanban.quickDeal.status": "Status",
|
|
1382
1382
|
"customers.deals.kanban.quickDeal.status.in_progress": "W toku",
|
|
1383
|
-
"customers.deals.kanban.quickDeal.status.
|
|
1383
|
+
"customers.deals.kanban.quickDeal.status.lost": "Przegrany",
|
|
1384
1384
|
"customers.deals.kanban.quickDeal.status.open": "Otwarty",
|
|
1385
1385
|
"customers.deals.kanban.quickDeal.status.win": "Wygrany",
|
|
1386
1386
|
"customers.deals.kanban.quickDeal.submit": "+ Dodaj szansę",
|
|
@@ -19,7 +19,8 @@ export const TERMINAL_PIPELINE_STAGE_LABELS: Record<DealClosureOutcome, Readonly
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
* Map a deal status spelling (
|
|
22
|
+
* Map a deal status spelling (`win`/`lost`, the AI tool's `won`, or the pre-0.7.1
|
|
23
|
+
* `loose`) to its closure
|
|
23
24
|
* outcome. Case-insensitive because callers such as the AI tool pass free-form model
|
|
24
25
|
* text through, while every persisted status is lower-case.
|
|
25
26
|
*/
|