@contractspec/example.crm-pipeline 1.57.0 → 1.58.0
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 +148 -164
- package/.turbo/turbo-prebuild.log +1 -0
- package/CHANGELOG.md +20 -0
- package/dist/browser/crm-pipeline.feature.js +75 -0
- package/dist/browser/deal/deal.enum.js +18 -0
- package/dist/browser/deal/deal.operation.js +396 -0
- package/dist/browser/deal/deal.schema.js +141 -0
- package/dist/browser/deal/deal.test-spec.js +58 -0
- package/dist/browser/deal/index.js +408 -0
- package/dist/browser/docs/crm-pipeline.docblock.js +113 -0
- package/dist/browser/docs/index.js +113 -0
- package/dist/browser/entities/company.entity.js +52 -0
- package/dist/browser/entities/contact.entity.js +66 -0
- package/dist/browser/entities/deal.entity.js +107 -0
- package/dist/browser/entities/index.js +343 -0
- package/dist/browser/entities/task.entity.js +99 -0
- package/dist/browser/events/contact.event.js +31 -0
- package/dist/browser/events/deal.event.js +101 -0
- package/dist/browser/events/index.js +158 -0
- package/dist/browser/events/task.event.js +28 -0
- package/dist/browser/example.js +39 -0
- package/dist/browser/handlers/crm.handlers.js +160 -0
- package/dist/browser/handlers/deal.handlers.js +293 -0
- package/dist/browser/handlers/index.js +456 -0
- package/dist/browser/handlers/mock-data.js +165 -0
- package/dist/browser/index.js +3279 -0
- package/dist/browser/operations/index.js +407 -0
- package/dist/browser/presentations/dashboard.presentation.js +52 -0
- package/dist/browser/presentations/index.js +284 -0
- package/dist/browser/presentations/pipeline.presentation.js +233 -0
- package/dist/browser/seeders/index.js +22 -0
- package/dist/browser/shared/overlay-types.js +0 -0
- package/dist/browser/ui/CrmDashboard.js +1325 -0
- package/dist/browser/ui/CrmDealCard.js +50 -0
- package/dist/browser/ui/CrmPipelineBoard.js +160 -0
- package/dist/browser/ui/hooks/index.js +186 -0
- package/dist/browser/ui/hooks/useDealList.js +84 -0
- package/dist/browser/ui/hooks/useDealMutations.js +100 -0
- package/dist/browser/ui/index.js +1972 -0
- package/dist/browser/ui/modals/CreateDealModal.js +211 -0
- package/dist/browser/ui/modals/DealActionsModal.js +428 -0
- package/dist/browser/ui/modals/index.js +638 -0
- package/dist/browser/ui/overlays/demo-overlays.js +55 -0
- package/dist/browser/ui/overlays/index.js +55 -0
- package/dist/browser/ui/renderers/index.js +827 -0
- package/dist/browser/ui/renderers/pipeline.markdown.js +564 -0
- package/dist/browser/ui/renderers/pipeline.renderer.js +264 -0
- package/dist/crm-pipeline.feature.d.ts +1 -6
- package/dist/crm-pipeline.feature.d.ts.map +1 -1
- package/dist/crm-pipeline.feature.js +74 -164
- package/dist/deal/deal.enum.d.ts +2 -7
- package/dist/deal/deal.enum.d.ts.map +1 -1
- package/dist/deal/deal.enum.js +16 -22
- package/dist/deal/deal.operation.d.ts +444 -450
- package/dist/deal/deal.operation.d.ts.map +1 -1
- package/dist/deal/deal.operation.js +390 -263
- package/dist/deal/deal.schema.d.ts +251 -256
- package/dist/deal/deal.schema.d.ts.map +1 -1
- package/dist/deal/deal.schema.js +131 -275
- package/dist/deal/deal.test-spec.d.ts +2 -7
- package/dist/deal/deal.test-spec.d.ts.map +1 -1
- package/dist/deal/deal.test-spec.js +56 -62
- package/dist/deal/index.d.ts +7 -4
- package/dist/deal/index.d.ts.map +1 -0
- package/dist/deal/index.js +408 -4
- package/dist/docs/crm-pipeline.docblock.d.ts +2 -1
- package/dist/docs/crm-pipeline.docblock.d.ts.map +1 -0
- package/dist/docs/crm-pipeline.docblock.js +45 -51
- package/dist/docs/index.d.ts +2 -1
- package/dist/docs/index.d.ts.map +1 -0
- package/dist/docs/index.js +114 -1
- package/dist/entities/company.entity.d.ts +27 -32
- package/dist/entities/company.entity.d.ts.map +1 -1
- package/dist/entities/company.entity.js +51 -61
- package/dist/entities/contact.entity.d.ts +31 -36
- package/dist/entities/contact.entity.d.ts.map +1 -1
- package/dist/entities/contact.entity.js +65 -76
- package/dist/entities/deal.entity.d.ts +52 -57
- package/dist/entities/deal.entity.d.ts.map +1 -1
- package/dist/entities/deal.entity.js +104 -116
- package/dist/entities/index.d.ts +6 -10
- package/dist/entities/index.d.ts.map +1 -1
- package/dist/entities/index.js +342 -31
- package/dist/entities/task.entity.d.ts +42 -47
- package/dist/entities/task.entity.d.ts.map +1 -1
- package/dist/entities/task.entity.js +95 -124
- package/dist/events/contact.event.d.ts +21 -27
- package/dist/events/contact.event.d.ts.map +1 -1
- package/dist/events/contact.event.js +29 -42
- package/dist/events/deal.event.d.ts +100 -106
- package/dist/events/deal.event.d.ts.map +1 -1
- package/dist/events/deal.event.js +93 -163
- package/dist/events/index.d.ts +4 -4
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/index.js +158 -4
- package/dist/events/task.event.d.ts +21 -27
- package/dist/events/task.event.d.ts.map +1 -1
- package/dist/events/task.event.js +26 -42
- package/dist/example.d.ts +2 -6
- package/dist/example.d.ts.map +1 -1
- package/dist/example.js +38 -50
- package/dist/handlers/crm.handlers.d.ts +80 -78
- package/dist/handlers/crm.handlers.d.ts.map +1 -1
- package/dist/handlers/crm.handlers.js +155 -166
- package/dist/handlers/deal.handlers.d.ts +58 -63
- package/dist/handlers/deal.handlers.d.ts.map +1 -1
- package/dist/handlers/deal.handlers.js +279 -105
- package/dist/handlers/index.d.ts +10 -4
- package/dist/handlers/index.d.ts.map +1 -0
- package/dist/handlers/index.js +456 -4
- package/dist/handlers/mock-data.d.ts +38 -41
- package/dist/handlers/mock-data.d.ts.map +1 -1
- package/dist/handlers/mock-data.js +162 -184
- package/dist/index.d.ts +13 -42
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3277 -53
- package/dist/node/crm-pipeline.feature.js +75 -0
- package/dist/node/deal/deal.enum.js +18 -0
- package/dist/node/deal/deal.operation.js +396 -0
- package/dist/node/deal/deal.schema.js +141 -0
- package/dist/node/deal/deal.test-spec.js +58 -0
- package/dist/node/deal/index.js +408 -0
- package/dist/node/docs/crm-pipeline.docblock.js +113 -0
- package/dist/node/docs/index.js +113 -0
- package/dist/node/entities/company.entity.js +52 -0
- package/dist/node/entities/contact.entity.js +66 -0
- package/dist/node/entities/deal.entity.js +107 -0
- package/dist/node/entities/index.js +343 -0
- package/dist/node/entities/task.entity.js +99 -0
- package/dist/node/events/contact.event.js +31 -0
- package/dist/node/events/deal.event.js +101 -0
- package/dist/node/events/index.js +158 -0
- package/dist/node/events/task.event.js +28 -0
- package/dist/node/example.js +39 -0
- package/dist/node/handlers/crm.handlers.js +160 -0
- package/dist/node/handlers/deal.handlers.js +293 -0
- package/dist/node/handlers/index.js +456 -0
- package/dist/node/handlers/mock-data.js +165 -0
- package/dist/node/index.js +3279 -0
- package/dist/node/operations/index.js +407 -0
- package/dist/node/presentations/dashboard.presentation.js +52 -0
- package/dist/node/presentations/index.js +284 -0
- package/dist/node/presentations/pipeline.presentation.js +233 -0
- package/dist/node/seeders/index.js +22 -0
- package/dist/node/shared/overlay-types.js +0 -0
- package/dist/node/ui/CrmDashboard.js +1325 -0
- package/dist/node/ui/CrmDealCard.js +50 -0
- package/dist/node/ui/CrmPipelineBoard.js +160 -0
- package/dist/node/ui/hooks/index.js +186 -0
- package/dist/node/ui/hooks/useDealList.js +84 -0
- package/dist/node/ui/hooks/useDealMutations.js +100 -0
- package/dist/node/ui/index.js +1972 -0
- package/dist/node/ui/modals/CreateDealModal.js +211 -0
- package/dist/node/ui/modals/DealActionsModal.js +428 -0
- package/dist/node/ui/modals/index.js +638 -0
- package/dist/node/ui/overlays/demo-overlays.js +55 -0
- package/dist/node/ui/overlays/index.js +55 -0
- package/dist/node/ui/renderers/index.js +827 -0
- package/dist/node/ui/renderers/pipeline.markdown.js +564 -0
- package/dist/node/ui/renderers/pipeline.renderer.js +264 -0
- package/dist/operations/index.d.ts +2 -5
- package/dist/operations/index.d.ts.map +1 -0
- package/dist/operations/index.js +407 -5
- package/dist/presentations/dashboard.presentation.d.ts +2 -7
- package/dist/presentations/dashboard.presentation.d.ts.map +1 -1
- package/dist/presentations/dashboard.presentation.js +51 -60
- package/dist/presentations/index.d.ts +3 -3
- package/dist/presentations/index.d.ts.map +1 -0
- package/dist/presentations/index.js +284 -3
- package/dist/presentations/pipeline.presentation.d.ts +4 -9
- package/dist/presentations/pipeline.presentation.d.ts.map +1 -1
- package/dist/presentations/pipeline.presentation.js +228 -116
- package/dist/seeders/index.d.ts +4 -8
- package/dist/seeders/index.d.ts.map +1 -1
- package/dist/seeders/index.js +21 -45
- package/dist/shared/overlay-types.d.ts +25 -28
- package/dist/shared/overlay-types.d.ts.map +1 -1
- package/dist/shared/overlay-types.js +1 -0
- package/dist/ui/CrmDashboard.d.ts +1 -6
- package/dist/ui/CrmDashboard.d.ts.map +1 -1
- package/dist/ui/CrmDashboard.js +1318 -296
- package/dist/ui/CrmDealCard.d.ts +8 -12
- package/dist/ui/CrmDealCard.d.ts.map +1 -1
- package/dist/ui/CrmDealCard.js +47 -45
- package/dist/ui/CrmPipelineBoard.d.ts +11 -20
- package/dist/ui/CrmPipelineBoard.d.ts.map +1 -1
- package/dist/ui/CrmPipelineBoard.js +157 -94
- package/dist/ui/hooks/index.d.ts +3 -3
- package/dist/ui/hooks/index.d.ts.map +1 -0
- package/dist/ui/hooks/index.js +185 -4
- package/dist/ui/hooks/useDealList.d.ts +28 -32
- package/dist/ui/hooks/useDealList.d.ts.map +1 -1
- package/dist/ui/hooks/useDealList.js +81 -90
- package/dist/ui/hooks/useDealMutations.d.ts +18 -22
- package/dist/ui/hooks/useDealMutations.d.ts.map +1 -1
- package/dist/ui/hooks/useDealMutations.js +97 -155
- package/dist/ui/index.d.ts +8 -14
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +1973 -15
- package/dist/ui/modals/CreateDealModal.d.ts +19 -29
- package/dist/ui/modals/CreateDealModal.d.ts.map +1 -1
- package/dist/ui/modals/CreateDealModal.js +209 -180
- package/dist/ui/modals/DealActionsModal.d.ts +31 -44
- package/dist/ui/modals/DealActionsModal.d.ts.map +1 -1
- package/dist/ui/modals/DealActionsModal.js +424 -367
- package/dist/ui/modals/index.d.ts +3 -3
- package/dist/ui/modals/index.d.ts.map +1 -0
- package/dist/ui/modals/index.js +638 -3
- package/dist/ui/overlays/demo-overlays.d.ts +10 -8
- package/dist/ui/overlays/demo-overlays.d.ts.map +1 -1
- package/dist/ui/overlays/demo-overlays.js +54 -66
- package/dist/ui/overlays/index.d.ts +2 -2
- package/dist/ui/overlays/index.d.ts.map +1 -0
- package/dist/ui/overlays/index.js +56 -3
- package/dist/ui/renderers/index.d.ts +3 -3
- package/dist/ui/renderers/index.d.ts.map +1 -0
- package/dist/ui/renderers/index.js +827 -3
- package/dist/ui/renderers/pipeline.markdown.d.ts +12 -11
- package/dist/ui/renderers/pipeline.markdown.d.ts.map +1 -1
- package/dist/ui/renderers/pipeline.markdown.js +560 -114
- package/dist/ui/renderers/pipeline.renderer.d.ts +9 -7
- package/dist/ui/renderers/pipeline.renderer.d.ts.map +1 -1
- package/dist/ui/renderers/pipeline.renderer.js +261 -24
- package/package.json +476 -90
- package/tsdown.config.js +1 -2
- package/.turbo/turbo-build$colon$bundle.log +0 -164
- package/dist/crm-pipeline.feature.js.map +0 -1
- package/dist/deal/deal.enum.js.map +0 -1
- package/dist/deal/deal.operation.js.map +0 -1
- package/dist/deal/deal.schema.js.map +0 -1
- package/dist/deal/deal.test-spec.js.map +0 -1
- package/dist/docs/crm-pipeline.docblock.js.map +0 -1
- package/dist/entities/company.entity.js.map +0 -1
- package/dist/entities/contact.entity.js.map +0 -1
- package/dist/entities/deal.entity.js.map +0 -1
- package/dist/entities/index.js.map +0 -1
- package/dist/entities/task.entity.js.map +0 -1
- package/dist/events/contact.event.js.map +0 -1
- package/dist/events/deal.event.js.map +0 -1
- package/dist/events/task.event.js.map +0 -1
- package/dist/example.js.map +0 -1
- package/dist/handlers/crm.handlers.js.map +0 -1
- package/dist/handlers/deal.handlers.js.map +0 -1
- package/dist/handlers/mock-data.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/presentations/dashboard.presentation.js.map +0 -1
- package/dist/presentations/pipeline.presentation.js.map +0 -1
- package/dist/seeders/index.js.map +0 -1
- package/dist/ui/CrmDashboard.js.map +0 -1
- package/dist/ui/CrmDealCard.js.map +0 -1
- package/dist/ui/CrmPipelineBoard.js.map +0 -1
- package/dist/ui/hooks/useDealList.js.map +0 -1
- package/dist/ui/hooks/useDealMutations.js.map +0 -1
- package/dist/ui/modals/CreateDealModal.js.map +0 -1
- package/dist/ui/modals/DealActionsModal.js.map +0 -1
- package/dist/ui/overlays/demo-overlays.js.map +0 -1
- package/dist/ui/renderers/pipeline.markdown.js.map +0 -1
- package/dist/ui/renderers/pipeline.renderer.js.map +0 -1
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,3279 @@
|
|
|
1
|
+
// src/crm-pipeline.feature.ts
|
|
2
|
+
import { defineFeature } from "@contractspec/lib.contracts";
|
|
3
|
+
var CrmPipelineFeature = defineFeature({
|
|
4
|
+
meta: {
|
|
5
|
+
key: "crm-pipeline",
|
|
6
|
+
title: "CRM Pipeline",
|
|
7
|
+
description: "CRM and sales pipeline management with deals, contacts, and companies",
|
|
8
|
+
domain: "crm",
|
|
9
|
+
owners: ["@crm-team"],
|
|
10
|
+
tags: ["crm", "sales", "pipeline", "deals"],
|
|
11
|
+
stability: "experimental",
|
|
12
|
+
version: "1.0.0"
|
|
13
|
+
},
|
|
14
|
+
operations: [
|
|
15
|
+
{ key: "crm.deal.create", version: "1.0.0" },
|
|
16
|
+
{ key: "crm.deal.move", version: "1.0.0" },
|
|
17
|
+
{ key: "crm.deal.win", version: "1.0.0" },
|
|
18
|
+
{ key: "crm.deal.lose", version: "1.0.0" },
|
|
19
|
+
{ key: "crm.deal.list", version: "1.0.0" }
|
|
20
|
+
],
|
|
21
|
+
events: [
|
|
22
|
+
{ key: "deal.created", version: "1.0.0" },
|
|
23
|
+
{ key: "deal.moved", version: "1.0.0" },
|
|
24
|
+
{ key: "deal.won", version: "1.0.0" },
|
|
25
|
+
{ key: "deal.lost", version: "1.0.0" },
|
|
26
|
+
{ key: "contact.created", version: "1.0.0" },
|
|
27
|
+
{ key: "task.completed", version: "1.0.0" }
|
|
28
|
+
],
|
|
29
|
+
presentations: [
|
|
30
|
+
{ key: "crm.dashboard", version: "1.0.0" },
|
|
31
|
+
{ key: "crm.pipeline.kanban", version: "1.0.0" },
|
|
32
|
+
{ key: "crm.deal.viewList", version: "1.0.0" },
|
|
33
|
+
{ key: "crm.deal.detail", version: "1.0.0" },
|
|
34
|
+
{ key: "crm.deal.card", version: "1.0.0" },
|
|
35
|
+
{ key: "crm.pipeline.metrics", version: "1.0.0" }
|
|
36
|
+
],
|
|
37
|
+
opToPresentation: [
|
|
38
|
+
{
|
|
39
|
+
op: { key: "crm.deal.list", version: "1.0.0" },
|
|
40
|
+
pres: { key: "crm.pipeline.kanban", version: "1.0.0" }
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
op: { key: "crm.deal.move", version: "1.0.0" },
|
|
44
|
+
pres: { key: "crm.pipeline.kanban", version: "1.0.0" }
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
presentationsTargets: [
|
|
48
|
+
{ key: "crm.dashboard", version: "1.0.0", targets: ["react", "markdown"] },
|
|
49
|
+
{
|
|
50
|
+
key: "crm.pipeline.kanban",
|
|
51
|
+
version: "1.0.0",
|
|
52
|
+
targets: ["react", "markdown"]
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
key: "crm.deal.viewList",
|
|
56
|
+
version: "1.0.0",
|
|
57
|
+
targets: ["react", "markdown", "application/json"]
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
key: "crm.pipeline.metrics",
|
|
61
|
+
version: "1.0.0",
|
|
62
|
+
targets: ["react", "markdown"]
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
capabilities: {
|
|
66
|
+
requires: [
|
|
67
|
+
{ key: "identity", version: "1.0.0" },
|
|
68
|
+
{ key: "audit-trail", version: "1.0.0" },
|
|
69
|
+
{ key: "notifications", version: "1.0.0" }
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// src/deal/deal.enum.ts
|
|
75
|
+
import { defineEnum } from "@contractspec/lib.schema";
|
|
76
|
+
var DealStatusEnum = defineEnum("DealStatus", [
|
|
77
|
+
"OPEN",
|
|
78
|
+
"WON",
|
|
79
|
+
"LOST",
|
|
80
|
+
"STALE"
|
|
81
|
+
]);
|
|
82
|
+
var DealStatusFilterEnum = defineEnum("DealStatusFilter", [
|
|
83
|
+
"OPEN",
|
|
84
|
+
"WON",
|
|
85
|
+
"LOST",
|
|
86
|
+
"all"
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
// src/deal/deal.schema.ts
|
|
90
|
+
import { defineSchemaModel, ScalarTypeEnum } from "@contractspec/lib.schema";
|
|
91
|
+
var DealModel = defineSchemaModel({
|
|
92
|
+
name: "Deal",
|
|
93
|
+
description: "A deal in the CRM pipeline",
|
|
94
|
+
fields: {
|
|
95
|
+
id: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
96
|
+
name: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
97
|
+
value: { type: ScalarTypeEnum.Float_unsecure(), isOptional: false },
|
|
98
|
+
currency: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
99
|
+
pipelineId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
100
|
+
stageId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
101
|
+
status: { type: DealStatusEnum, isOptional: false },
|
|
102
|
+
contactId: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
|
|
103
|
+
companyId: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
|
|
104
|
+
ownerId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
105
|
+
expectedCloseDate: { type: ScalarTypeEnum.DateTime(), isOptional: true },
|
|
106
|
+
createdAt: { type: ScalarTypeEnum.DateTime(), isOptional: false },
|
|
107
|
+
updatedAt: { type: ScalarTypeEnum.DateTime(), isOptional: false }
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
var CreateDealInputModel = defineSchemaModel({
|
|
111
|
+
name: "CreateDealInput",
|
|
112
|
+
description: "Input for creating a deal",
|
|
113
|
+
fields: {
|
|
114
|
+
name: { type: ScalarTypeEnum.NonEmptyString(), isOptional: false },
|
|
115
|
+
value: { type: ScalarTypeEnum.Float_unsecure(), isOptional: false },
|
|
116
|
+
currency: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
|
|
117
|
+
pipelineId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
118
|
+
stageId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
119
|
+
contactId: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
|
|
120
|
+
companyId: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
|
|
121
|
+
expectedCloseDate: { type: ScalarTypeEnum.DateTime(), isOptional: true }
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
var MoveDealInputModel = defineSchemaModel({
|
|
125
|
+
name: "MoveDealInput",
|
|
126
|
+
description: "Input for moving a deal to another stage",
|
|
127
|
+
fields: {
|
|
128
|
+
dealId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
129
|
+
stageId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
130
|
+
position: { type: ScalarTypeEnum.Int_unsecure(), isOptional: true }
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
var DealMovedPayloadModel = defineSchemaModel({
|
|
134
|
+
name: "DealMovedPayload",
|
|
135
|
+
fields: {
|
|
136
|
+
dealId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
137
|
+
fromStage: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
138
|
+
toStage: { type: ScalarTypeEnum.String_unsecure(), isOptional: false }
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
var WinDealInputModel = defineSchemaModel({
|
|
142
|
+
name: "WinDealInput",
|
|
143
|
+
description: "Input for marking a deal as won",
|
|
144
|
+
fields: {
|
|
145
|
+
dealId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
146
|
+
wonSource: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
|
|
147
|
+
notes: { type: ScalarTypeEnum.String_unsecure(), isOptional: true }
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
var DealWonPayloadModel = defineSchemaModel({
|
|
151
|
+
name: "DealWonPayload",
|
|
152
|
+
fields: {
|
|
153
|
+
dealId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
154
|
+
value: { type: ScalarTypeEnum.Float_unsecure(), isOptional: false }
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
var LoseDealInputModel = defineSchemaModel({
|
|
158
|
+
name: "LoseDealInput",
|
|
159
|
+
description: "Input for marking a deal as lost",
|
|
160
|
+
fields: {
|
|
161
|
+
dealId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
162
|
+
lostReason: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
163
|
+
notes: { type: ScalarTypeEnum.String_unsecure(), isOptional: true }
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
var DealLostPayloadModel = defineSchemaModel({
|
|
167
|
+
name: "DealLostPayload",
|
|
168
|
+
fields: {
|
|
169
|
+
dealId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
170
|
+
reason: { type: ScalarTypeEnum.String_unsecure(), isOptional: false }
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
var ListDealsInputModel = defineSchemaModel({
|
|
174
|
+
name: "ListDealsInput",
|
|
175
|
+
description: "Input for listing deals",
|
|
176
|
+
fields: {
|
|
177
|
+
pipelineId: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
|
|
178
|
+
stageId: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
|
|
179
|
+
status: { type: DealStatusFilterEnum, isOptional: true },
|
|
180
|
+
ownerId: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
|
|
181
|
+
search: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
|
|
182
|
+
limit: {
|
|
183
|
+
type: ScalarTypeEnum.Int_unsecure(),
|
|
184
|
+
isOptional: true,
|
|
185
|
+
defaultValue: 20
|
|
186
|
+
},
|
|
187
|
+
offset: {
|
|
188
|
+
type: ScalarTypeEnum.Int_unsecure(),
|
|
189
|
+
isOptional: true,
|
|
190
|
+
defaultValue: 0
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
var ListDealsOutputModel = defineSchemaModel({
|
|
195
|
+
name: "ListDealsOutput",
|
|
196
|
+
description: "Output for listing deals",
|
|
197
|
+
fields: {
|
|
198
|
+
deals: { type: DealModel, isArray: true, isOptional: false },
|
|
199
|
+
total: { type: ScalarTypeEnum.Int_unsecure(), isOptional: false },
|
|
200
|
+
totalValue: { type: ScalarTypeEnum.Float_unsecure(), isOptional: false }
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// src/deal/deal.operation.ts
|
|
205
|
+
import {
|
|
206
|
+
defineCommand,
|
|
207
|
+
defineQuery
|
|
208
|
+
} from "@contractspec/lib.contracts/operations";
|
|
209
|
+
var OWNERS = ["@example.crm-pipeline"];
|
|
210
|
+
var CreateDealContract = defineCommand({
|
|
211
|
+
meta: {
|
|
212
|
+
key: "crm.deal.create",
|
|
213
|
+
version: "1.0.0",
|
|
214
|
+
stability: "stable",
|
|
215
|
+
owners: [...OWNERS],
|
|
216
|
+
tags: ["crm", "deal", "create"],
|
|
217
|
+
description: "Create a new deal in the pipeline.",
|
|
218
|
+
goal: "Allow sales reps to create new opportunities.",
|
|
219
|
+
context: "Deal creation UI, quick add."
|
|
220
|
+
},
|
|
221
|
+
io: {
|
|
222
|
+
input: CreateDealInputModel,
|
|
223
|
+
output: DealModel
|
|
224
|
+
},
|
|
225
|
+
policy: {
|
|
226
|
+
auth: "user"
|
|
227
|
+
},
|
|
228
|
+
sideEffects: {
|
|
229
|
+
emits: [
|
|
230
|
+
{
|
|
231
|
+
key: "deal.created",
|
|
232
|
+
version: "1.0.0",
|
|
233
|
+
when: "Deal is created",
|
|
234
|
+
payload: DealModel
|
|
235
|
+
}
|
|
236
|
+
],
|
|
237
|
+
audit: ["deal.created"]
|
|
238
|
+
},
|
|
239
|
+
acceptance: {
|
|
240
|
+
scenarios: [
|
|
241
|
+
{
|
|
242
|
+
key: "create-deal-happy-path",
|
|
243
|
+
given: ["User is authenticated"],
|
|
244
|
+
when: ["User creates a deal with valid data"],
|
|
245
|
+
then: ["Deal is created", "DealCreated event is emitted"]
|
|
246
|
+
}
|
|
247
|
+
],
|
|
248
|
+
examples: [
|
|
249
|
+
{
|
|
250
|
+
key: "create-basic-deal",
|
|
251
|
+
input: {
|
|
252
|
+
title: "Big Corp Q3 License",
|
|
253
|
+
stageId: "stage-lead",
|
|
254
|
+
value: 50000,
|
|
255
|
+
companyId: "comp-123"
|
|
256
|
+
},
|
|
257
|
+
output: {
|
|
258
|
+
id: "deal-789",
|
|
259
|
+
title: "Big Corp Q3 License",
|
|
260
|
+
status: "open"
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
]
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
var MoveDealContract = defineCommand({
|
|
267
|
+
meta: {
|
|
268
|
+
key: "crm.deal.move",
|
|
269
|
+
version: "1.0.0",
|
|
270
|
+
stability: "stable",
|
|
271
|
+
owners: [...OWNERS],
|
|
272
|
+
tags: ["crm", "deal", "move", "kanban"],
|
|
273
|
+
description: "Move a deal to a different stage.",
|
|
274
|
+
goal: "Allow drag-and-drop stage movement in Kanban.",
|
|
275
|
+
context: "Pipeline Kanban view."
|
|
276
|
+
},
|
|
277
|
+
io: {
|
|
278
|
+
input: MoveDealInputModel,
|
|
279
|
+
output: DealModel
|
|
280
|
+
},
|
|
281
|
+
policy: {
|
|
282
|
+
auth: "user"
|
|
283
|
+
},
|
|
284
|
+
sideEffects: {
|
|
285
|
+
emits: [
|
|
286
|
+
{
|
|
287
|
+
key: "deal.moved",
|
|
288
|
+
version: "1.0.0",
|
|
289
|
+
when: "Deal stage changed",
|
|
290
|
+
payload: DealMovedPayloadModel
|
|
291
|
+
}
|
|
292
|
+
],
|
|
293
|
+
audit: ["deal.moved"]
|
|
294
|
+
},
|
|
295
|
+
acceptance: {
|
|
296
|
+
scenarios: [
|
|
297
|
+
{
|
|
298
|
+
key: "move-deal-happy-path",
|
|
299
|
+
given: ["Deal exists in stage A"],
|
|
300
|
+
when: ["User moves deal to stage B"],
|
|
301
|
+
then: ["Deal stage is updated", "DealMoved event is emitted"]
|
|
302
|
+
}
|
|
303
|
+
],
|
|
304
|
+
examples: [
|
|
305
|
+
{
|
|
306
|
+
key: "move-to-negotiation",
|
|
307
|
+
input: { dealId: "deal-789", targetStageId: "stage-negotiation" },
|
|
308
|
+
output: {
|
|
309
|
+
id: "deal-789",
|
|
310
|
+
stageId: "stage-negotiation",
|
|
311
|
+
movedAt: "2025-01-15T10:00:00Z"
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
]
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
var WinDealContract = defineCommand({
|
|
318
|
+
meta: {
|
|
319
|
+
key: "crm.deal.win",
|
|
320
|
+
version: "1.0.0",
|
|
321
|
+
stability: "stable",
|
|
322
|
+
owners: [...OWNERS],
|
|
323
|
+
tags: ["crm", "deal", "won"],
|
|
324
|
+
description: "Mark a deal as won.",
|
|
325
|
+
goal: "Close a deal as successful.",
|
|
326
|
+
context: "Deal closing flow."
|
|
327
|
+
},
|
|
328
|
+
io: {
|
|
329
|
+
input: WinDealInputModel,
|
|
330
|
+
output: DealModel
|
|
331
|
+
},
|
|
332
|
+
policy: {
|
|
333
|
+
auth: "user"
|
|
334
|
+
},
|
|
335
|
+
sideEffects: {
|
|
336
|
+
emits: [
|
|
337
|
+
{
|
|
338
|
+
key: "deal.won",
|
|
339
|
+
version: "1.0.0",
|
|
340
|
+
when: "Deal is won",
|
|
341
|
+
payload: DealWonPayloadModel
|
|
342
|
+
}
|
|
343
|
+
],
|
|
344
|
+
audit: ["deal.won"]
|
|
345
|
+
},
|
|
346
|
+
acceptance: {
|
|
347
|
+
scenarios: [
|
|
348
|
+
{
|
|
349
|
+
key: "win-deal-happy-path",
|
|
350
|
+
given: ["Deal is open"],
|
|
351
|
+
when: ["User marks deal as won"],
|
|
352
|
+
then: ["Deal status becomes WON", "DealWon event is emitted"]
|
|
353
|
+
}
|
|
354
|
+
],
|
|
355
|
+
examples: [
|
|
356
|
+
{
|
|
357
|
+
key: "mark-won",
|
|
358
|
+
input: {
|
|
359
|
+
dealId: "deal-789",
|
|
360
|
+
actualValue: 52000,
|
|
361
|
+
note: "Signed contract attached"
|
|
362
|
+
},
|
|
363
|
+
output: {
|
|
364
|
+
id: "deal-789",
|
|
365
|
+
status: "won",
|
|
366
|
+
closedAt: "2025-01-20T14:30:00Z"
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
]
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
var LoseDealContract = defineCommand({
|
|
373
|
+
meta: {
|
|
374
|
+
key: "crm.deal.lose",
|
|
375
|
+
version: "1.0.0",
|
|
376
|
+
stability: "stable",
|
|
377
|
+
owners: [...OWNERS],
|
|
378
|
+
tags: ["crm", "deal", "lost"],
|
|
379
|
+
description: "Mark a deal as lost.",
|
|
380
|
+
goal: "Close a deal as unsuccessful.",
|
|
381
|
+
context: "Deal closing flow."
|
|
382
|
+
},
|
|
383
|
+
io: {
|
|
384
|
+
input: LoseDealInputModel,
|
|
385
|
+
output: DealModel
|
|
386
|
+
},
|
|
387
|
+
policy: {
|
|
388
|
+
auth: "user"
|
|
389
|
+
},
|
|
390
|
+
sideEffects: {
|
|
391
|
+
emits: [
|
|
392
|
+
{
|
|
393
|
+
key: "deal.lost",
|
|
394
|
+
version: "1.0.0",
|
|
395
|
+
when: "Deal is lost",
|
|
396
|
+
payload: DealLostPayloadModel
|
|
397
|
+
}
|
|
398
|
+
],
|
|
399
|
+
audit: ["deal.lost"]
|
|
400
|
+
},
|
|
401
|
+
acceptance: {
|
|
402
|
+
scenarios: [
|
|
403
|
+
{
|
|
404
|
+
key: "lose-deal-happy-path",
|
|
405
|
+
given: ["Deal is open"],
|
|
406
|
+
when: ["User marks deal as lost"],
|
|
407
|
+
then: ["Deal status becomes LOST", "DealLost event is emitted"]
|
|
408
|
+
}
|
|
409
|
+
],
|
|
410
|
+
examples: [
|
|
411
|
+
{
|
|
412
|
+
key: "mark-lost",
|
|
413
|
+
input: {
|
|
414
|
+
dealId: "deal-789",
|
|
415
|
+
reason: "competitor",
|
|
416
|
+
note: "Went with cheaper option"
|
|
417
|
+
},
|
|
418
|
+
output: {
|
|
419
|
+
id: "deal-789",
|
|
420
|
+
status: "lost",
|
|
421
|
+
closedAt: "2025-01-21T09:00:00Z"
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
]
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
var ListDealsContract = defineQuery({
|
|
428
|
+
meta: {
|
|
429
|
+
key: "crm.deal.list",
|
|
430
|
+
version: "1.0.0",
|
|
431
|
+
stability: "stable",
|
|
432
|
+
owners: [...OWNERS],
|
|
433
|
+
tags: ["crm", "deal", "list"],
|
|
434
|
+
description: "List deals with filters.",
|
|
435
|
+
goal: "Show pipeline, deal lists, dashboards.",
|
|
436
|
+
context: "Pipeline view, deal list."
|
|
437
|
+
},
|
|
438
|
+
io: {
|
|
439
|
+
input: ListDealsInputModel,
|
|
440
|
+
output: ListDealsOutputModel
|
|
441
|
+
},
|
|
442
|
+
policy: {
|
|
443
|
+
auth: "user"
|
|
444
|
+
},
|
|
445
|
+
acceptance: {
|
|
446
|
+
scenarios: [
|
|
447
|
+
{
|
|
448
|
+
key: "list-deals-happy-path",
|
|
449
|
+
given: ["User has access to deals"],
|
|
450
|
+
when: ["User lists deals"],
|
|
451
|
+
then: ["List of deals is returned"]
|
|
452
|
+
}
|
|
453
|
+
],
|
|
454
|
+
examples: [
|
|
455
|
+
{
|
|
456
|
+
key: "list-filter-stage",
|
|
457
|
+
input: { stageId: "stage-lead", limit: 20 },
|
|
458
|
+
output: { items: [], total: 5, hasMore: false }
|
|
459
|
+
}
|
|
460
|
+
]
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
// src/docs/crm-pipeline.docblock.ts
|
|
464
|
+
import { registerDocBlocks } from "@contractspec/lib.contracts/docs";
|
|
465
|
+
var crmPipelineDocBlocks = [
|
|
466
|
+
{
|
|
467
|
+
id: "docs.examples.crm-pipeline.goal",
|
|
468
|
+
title: "CRM Pipeline — Goal",
|
|
469
|
+
summary: "Deals, stages, contacts, companies, and tasks with auditable stage movement.",
|
|
470
|
+
kind: "goal",
|
|
471
|
+
visibility: "public",
|
|
472
|
+
route: "/docs/examples/crm-pipeline/goal",
|
|
473
|
+
tags: ["crm", "goal"],
|
|
474
|
+
body: `## Why it matters
|
|
475
|
+
- Regenerable CRM flow for deals/stages without code drift.
|
|
476
|
+
- Ensures stage movement, tasks, and contacts stay aligned across surfaces.
|
|
477
|
+
|
|
478
|
+
## Business/Product goal
|
|
479
|
+
- Give sales teams a governed pipeline with auditable moves and notifications.
|
|
480
|
+
- Allow experimentation (feature flags) on stage definitions and task flows.
|
|
481
|
+
|
|
482
|
+
## Success criteria
|
|
483
|
+
- Stage/state changes emit events and remain declarative in spec.
|
|
484
|
+
- PII (contacts) is scoped/redacted in presentations.`
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
id: "docs.examples.crm-pipeline.usage",
|
|
488
|
+
title: "CRM Pipeline — Usage",
|
|
489
|
+
summary: "How to seed, extend, and regenerate the CRM pipeline.",
|
|
490
|
+
kind: "usage",
|
|
491
|
+
visibility: "public",
|
|
492
|
+
route: "/docs/examples/crm-pipeline/usage",
|
|
493
|
+
tags: ["crm", "usage"],
|
|
494
|
+
body: `## Setup
|
|
495
|
+
1) Seed (if available) or create pipeline stages, deals, contacts, companies, tasks.
|
|
496
|
+
2) Configure Notifications for stage changes/tasks; set policy.pii for contact data.
|
|
497
|
+
|
|
498
|
+
## Extend & regenerate
|
|
499
|
+
1) Adjust stage schema/order, deal fields, task fields in the spec.
|
|
500
|
+
2) Regenerate to sync UI/API/events; ensure kanban/action buttons update.
|
|
501
|
+
3) Use Feature Flags to trial new stages or SLA rules.
|
|
502
|
+
|
|
503
|
+
## Guardrails
|
|
504
|
+
- Emit events for stage moves and task completions; log to Audit Trail.
|
|
505
|
+
- Keep required fields enforced in contracts; avoid freeform state.
|
|
506
|
+
- Redact contact PII in markdown/JSON outputs.
|
|
507
|
+
|
|
508
|
+
## Adoption narrative
|
|
509
|
+
|
|
510
|
+
### Before
|
|
511
|
+
- A CRM app with hand-written data models and handler logic.
|
|
512
|
+
- Pipeline stage rules live in code and drift across UI/API/events.
|
|
513
|
+
- Regeneration is risky because specs and implementations are not aligned.
|
|
514
|
+
|
|
515
|
+
### After
|
|
516
|
+
- Contracts define deals, stages, and tasks as the source of truth.
|
|
517
|
+
- Regeneration keeps UI/API/events in sync when stages change.
|
|
518
|
+
- Compliance surfaces (audits, notifications) stay consistent with specs.
|
|
519
|
+
|
|
520
|
+
### Minimal adoption steps
|
|
521
|
+
1) Add ContractSpec CLI and core libraries.
|
|
522
|
+
2) Define one operation (for example, deal/create).
|
|
523
|
+
3) Run contractspec build to generate handlers and types.
|
|
524
|
+
4) Wire the generated handler into your existing router.
|
|
525
|
+
5) Expand to events and presentations as you add surface areas.
|
|
526
|
+
`
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
id: "docs.examples.crm-pipeline.reference",
|
|
530
|
+
title: "CRM Pipeline — Reference",
|
|
531
|
+
summary: "Entities, contracts, events, and presentations for the CRM template.",
|
|
532
|
+
kind: "reference",
|
|
533
|
+
visibility: "public",
|
|
534
|
+
route: "/docs/examples/crm-pipeline",
|
|
535
|
+
tags: ["crm", "reference"],
|
|
536
|
+
body: `## Entities
|
|
537
|
+
- Contact, Company, Deal, Pipeline, Stage, Task.
|
|
538
|
+
|
|
539
|
+
## Contracts
|
|
540
|
+
- deal/create, stage/move, contact/company CRUD, task create/complete.
|
|
541
|
+
|
|
542
|
+
## Events
|
|
543
|
+
- deal.created, stage.moved, task.completed, contact.updated.
|
|
544
|
+
|
|
545
|
+
## Presentations
|
|
546
|
+
- Pipelines/kanban, deal detail, contact/company profiles, task lists.
|
|
547
|
+
|
|
548
|
+
## Notes
|
|
549
|
+
- Stage definitions should be declarative; enforce via spec and regeneration.
|
|
550
|
+
- Use Notifications for deal/task updates; Audit Trail for state changes.`
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
id: "docs.examples.crm-pipeline.constraints",
|
|
554
|
+
title: "CRM Pipeline — Constraints & Safety",
|
|
555
|
+
summary: "Internal guardrails for stages, PII, and regeneration semantics in the CRM template.",
|
|
556
|
+
kind: "reference",
|
|
557
|
+
visibility: "internal",
|
|
558
|
+
route: "/docs/examples/crm-pipeline/constraints",
|
|
559
|
+
tags: ["crm", "constraints", "internal"],
|
|
560
|
+
body: `## Constraints
|
|
561
|
+
- Stage definitions/order must remain declarative; no imperative overrides in code.
|
|
562
|
+
- Events to emit: deal.created, stage.moved, task.completed, contact.updated (minimum).
|
|
563
|
+
- Regeneration should not alter stage semantics without explicit spec change.
|
|
564
|
+
|
|
565
|
+
## PII
|
|
566
|
+
- Mark contact/company PII (emails, phones) for redaction in presentations.
|
|
567
|
+
- Ensure MCP/web outputs avoid raw PII when not needed.
|
|
568
|
+
|
|
569
|
+
## Verification
|
|
570
|
+
- Add fixtures for stage move rules and SLA/task changes.
|
|
571
|
+
- Ensure Audit/Notifications remain wired for stage and task events.
|
|
572
|
+
- Use Feature Flags for experimental stages/SLAs; default safe/off.`
|
|
573
|
+
}
|
|
574
|
+
];
|
|
575
|
+
registerDocBlocks(crmPipelineDocBlocks);
|
|
576
|
+
// src/entities/company.entity.ts
|
|
577
|
+
import {
|
|
578
|
+
defineEntity,
|
|
579
|
+
defineEntityEnum,
|
|
580
|
+
field,
|
|
581
|
+
index
|
|
582
|
+
} from "@contractspec/lib.schema";
|
|
583
|
+
var CompanySizeEnum = defineEntityEnum({
|
|
584
|
+
name: "CompanySize",
|
|
585
|
+
values: ["STARTUP", "SMALL", "MEDIUM", "LARGE", "ENTERPRISE"],
|
|
586
|
+
schema: "crm",
|
|
587
|
+
description: "Size category of a company."
|
|
588
|
+
});
|
|
589
|
+
var CompanyEntity = defineEntity({
|
|
590
|
+
name: "Company",
|
|
591
|
+
description: "A company/organization in the CRM.",
|
|
592
|
+
schema: "crm",
|
|
593
|
+
map: "company",
|
|
594
|
+
fields: {
|
|
595
|
+
id: field.id({ description: "Unique company ID" }),
|
|
596
|
+
name: field.string({ description: "Company name" }),
|
|
597
|
+
domain: field.string({ isOptional: true, description: "Website domain" }),
|
|
598
|
+
website: field.url({ isOptional: true }),
|
|
599
|
+
industry: field.string({ isOptional: true }),
|
|
600
|
+
size: field.enum("CompanySize", { isOptional: true }),
|
|
601
|
+
employeeCount: field.int({ isOptional: true }),
|
|
602
|
+
annualRevenue: field.decimal({ isOptional: true }),
|
|
603
|
+
organizationId: field.foreignKey(),
|
|
604
|
+
ownerId: field.foreignKey({ description: "Account owner" }),
|
|
605
|
+
phone: field.string({ isOptional: true }),
|
|
606
|
+
email: field.email({ isOptional: true }),
|
|
607
|
+
address: field.string({ isOptional: true }),
|
|
608
|
+
city: field.string({ isOptional: true }),
|
|
609
|
+
state: field.string({ isOptional: true }),
|
|
610
|
+
country: field.string({ isOptional: true }),
|
|
611
|
+
postalCode: field.string({ isOptional: true }),
|
|
612
|
+
linkedInUrl: field.url({ isOptional: true }),
|
|
613
|
+
description: field.string({ isOptional: true }),
|
|
614
|
+
tags: field.string({ isArray: true }),
|
|
615
|
+
customFields: field.json({ isOptional: true }),
|
|
616
|
+
createdAt: field.createdAt(),
|
|
617
|
+
updatedAt: field.updatedAt(),
|
|
618
|
+
contacts: field.hasMany("Contact"),
|
|
619
|
+
deals: field.hasMany("Deal")
|
|
620
|
+
},
|
|
621
|
+
indexes: [index.on(["organizationId", "ownerId"]), index.on(["domain"])],
|
|
622
|
+
enums: [CompanySizeEnum]
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
// src/entities/contact.entity.ts
|
|
626
|
+
import {
|
|
627
|
+
defineEntity as defineEntity2,
|
|
628
|
+
defineEntityEnum as defineEntityEnum2,
|
|
629
|
+
field as field2,
|
|
630
|
+
index as index2
|
|
631
|
+
} from "@contractspec/lib.schema";
|
|
632
|
+
var ContactStatusEnum = defineEntityEnum2({
|
|
633
|
+
name: "ContactStatus",
|
|
634
|
+
values: ["LEAD", "PROSPECT", "CUSTOMER", "CHURNED", "ARCHIVED"],
|
|
635
|
+
schema: "crm",
|
|
636
|
+
description: "Status of a contact in the sales funnel."
|
|
637
|
+
});
|
|
638
|
+
var ContactEntity = defineEntity2({
|
|
639
|
+
name: "Contact",
|
|
640
|
+
description: "An individual person in the CRM.",
|
|
641
|
+
schema: "crm",
|
|
642
|
+
map: "contact",
|
|
643
|
+
fields: {
|
|
644
|
+
id: field2.id({ description: "Unique contact ID" }),
|
|
645
|
+
firstName: field2.string({ description: "First name" }),
|
|
646
|
+
lastName: field2.string({ description: "Last name" }),
|
|
647
|
+
email: field2.email({ isOptional: true, isUnique: true }),
|
|
648
|
+
phone: field2.string({ isOptional: true }),
|
|
649
|
+
companyId: field2.string({
|
|
650
|
+
isOptional: true,
|
|
651
|
+
description: "Associated company"
|
|
652
|
+
}),
|
|
653
|
+
jobTitle: field2.string({ isOptional: true }),
|
|
654
|
+
status: field2.enum("ContactStatus", { default: "LEAD" }),
|
|
655
|
+
organizationId: field2.foreignKey(),
|
|
656
|
+
ownerId: field2.foreignKey({
|
|
657
|
+
description: "Sales rep who owns this contact"
|
|
658
|
+
}),
|
|
659
|
+
source: field2.string({ isOptional: true, description: "Lead source" }),
|
|
660
|
+
linkedInUrl: field2.url({ isOptional: true }),
|
|
661
|
+
twitterHandle: field2.string({ isOptional: true }),
|
|
662
|
+
address: field2.string({ isOptional: true }),
|
|
663
|
+
city: field2.string({ isOptional: true }),
|
|
664
|
+
state: field2.string({ isOptional: true }),
|
|
665
|
+
country: field2.string({ isOptional: true }),
|
|
666
|
+
postalCode: field2.string({ isOptional: true }),
|
|
667
|
+
notes: field2.string({ isOptional: true }),
|
|
668
|
+
tags: field2.string({ isArray: true }),
|
|
669
|
+
customFields: field2.json({ isOptional: true }),
|
|
670
|
+
lastContactedAt: field2.dateTime({ isOptional: true }),
|
|
671
|
+
nextFollowUpAt: field2.dateTime({ isOptional: true }),
|
|
672
|
+
createdAt: field2.createdAt(),
|
|
673
|
+
updatedAt: field2.updatedAt(),
|
|
674
|
+
company: field2.belongsTo("Company", ["companyId"], ["id"]),
|
|
675
|
+
deals: field2.hasMany("Deal"),
|
|
676
|
+
tasks: field2.hasMany("Task"),
|
|
677
|
+
activities: field2.hasMany("Activity")
|
|
678
|
+
},
|
|
679
|
+
indexes: [
|
|
680
|
+
index2.on(["organizationId", "status"]),
|
|
681
|
+
index2.on(["organizationId", "ownerId"]),
|
|
682
|
+
index2.on(["organizationId", "companyId"]),
|
|
683
|
+
index2.on(["email"])
|
|
684
|
+
],
|
|
685
|
+
enums: [ContactStatusEnum]
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
// src/entities/deal.entity.ts
|
|
689
|
+
import {
|
|
690
|
+
defineEntity as defineEntity3,
|
|
691
|
+
defineEntityEnum as defineEntityEnum3,
|
|
692
|
+
field as field3,
|
|
693
|
+
index as index3
|
|
694
|
+
} from "@contractspec/lib.schema";
|
|
695
|
+
var DealStatusEnum2 = defineEntityEnum3({
|
|
696
|
+
name: "DealStatus",
|
|
697
|
+
values: ["OPEN", "WON", "LOST", "STALE"],
|
|
698
|
+
schema: "crm",
|
|
699
|
+
description: "Status of a deal."
|
|
700
|
+
});
|
|
701
|
+
var PipelineEntity = defineEntity3({
|
|
702
|
+
name: "Pipeline",
|
|
703
|
+
description: "A sales pipeline with stages.",
|
|
704
|
+
schema: "crm",
|
|
705
|
+
map: "pipeline",
|
|
706
|
+
fields: {
|
|
707
|
+
id: field3.id(),
|
|
708
|
+
name: field3.string({ description: "Pipeline name" }),
|
|
709
|
+
description: field3.string({ isOptional: true }),
|
|
710
|
+
organizationId: field3.foreignKey(),
|
|
711
|
+
isDefault: field3.boolean({ default: false }),
|
|
712
|
+
createdAt: field3.createdAt(),
|
|
713
|
+
updatedAt: field3.updatedAt(),
|
|
714
|
+
stages: field3.hasMany("Stage"),
|
|
715
|
+
deals: field3.hasMany("Deal")
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
var StageEntity = defineEntity3({
|
|
719
|
+
name: "Stage",
|
|
720
|
+
description: "A stage within a sales pipeline.",
|
|
721
|
+
schema: "crm",
|
|
722
|
+
map: "stage",
|
|
723
|
+
fields: {
|
|
724
|
+
id: field3.id(),
|
|
725
|
+
name: field3.string({ description: "Stage name" }),
|
|
726
|
+
pipelineId: field3.foreignKey(),
|
|
727
|
+
position: field3.int({ description: "Order in pipeline" }),
|
|
728
|
+
probability: field3.int({
|
|
729
|
+
default: 0,
|
|
730
|
+
description: "Win probability (0-100)"
|
|
731
|
+
}),
|
|
732
|
+
isWonStage: field3.boolean({ default: false }),
|
|
733
|
+
isLostStage: field3.boolean({ default: false }),
|
|
734
|
+
color: field3.string({
|
|
735
|
+
isOptional: true,
|
|
736
|
+
description: "Stage color for UI"
|
|
737
|
+
}),
|
|
738
|
+
createdAt: field3.createdAt(),
|
|
739
|
+
updatedAt: field3.updatedAt(),
|
|
740
|
+
pipeline: field3.belongsTo("Pipeline", ["pipelineId"], ["id"], {
|
|
741
|
+
onDelete: "Cascade"
|
|
742
|
+
}),
|
|
743
|
+
deals: field3.hasMany("Deal")
|
|
744
|
+
},
|
|
745
|
+
indexes: [index3.on(["pipelineId", "position"])]
|
|
746
|
+
});
|
|
747
|
+
var DealEntity = defineEntity3({
|
|
748
|
+
name: "Deal",
|
|
749
|
+
description: "A sales opportunity/deal.",
|
|
750
|
+
schema: "crm",
|
|
751
|
+
map: "deal",
|
|
752
|
+
fields: {
|
|
753
|
+
id: field3.id({ description: "Unique deal ID" }),
|
|
754
|
+
name: field3.string({ description: "Deal name" }),
|
|
755
|
+
value: field3.decimal({ description: "Deal value" }),
|
|
756
|
+
currency: field3.string({ default: '"USD"' }),
|
|
757
|
+
pipelineId: field3.foreignKey(),
|
|
758
|
+
stageId: field3.foreignKey(),
|
|
759
|
+
status: field3.enum("DealStatus", { default: "OPEN" }),
|
|
760
|
+
contactId: field3.string({ isOptional: true }),
|
|
761
|
+
companyId: field3.string({ isOptional: true }),
|
|
762
|
+
organizationId: field3.foreignKey(),
|
|
763
|
+
ownerId: field3.foreignKey({ description: "Deal owner" }),
|
|
764
|
+
expectedCloseDate: field3.dateTime({ isOptional: true }),
|
|
765
|
+
closedAt: field3.dateTime({ isOptional: true }),
|
|
766
|
+
lostReason: field3.string({ isOptional: true }),
|
|
767
|
+
wonSource: field3.string({ isOptional: true }),
|
|
768
|
+
notes: field3.string({ isOptional: true }),
|
|
769
|
+
tags: field3.string({ isArray: true }),
|
|
770
|
+
customFields: field3.json({ isOptional: true }),
|
|
771
|
+
stagePosition: field3.int({ default: 0 }),
|
|
772
|
+
createdAt: field3.createdAt(),
|
|
773
|
+
updatedAt: field3.updatedAt(),
|
|
774
|
+
pipeline: field3.belongsTo("Pipeline", ["pipelineId"], ["id"]),
|
|
775
|
+
stage: field3.belongsTo("Stage", ["stageId"], ["id"]),
|
|
776
|
+
contact: field3.belongsTo("Contact", ["contactId"], ["id"]),
|
|
777
|
+
company: field3.belongsTo("Company", ["companyId"], ["id"]),
|
|
778
|
+
tasks: field3.hasMany("Task"),
|
|
779
|
+
activities: field3.hasMany("Activity")
|
|
780
|
+
},
|
|
781
|
+
indexes: [
|
|
782
|
+
index3.on(["organizationId", "status"]),
|
|
783
|
+
index3.on(["pipelineId", "stageId", "stagePosition"]),
|
|
784
|
+
index3.on(["ownerId", "status"]),
|
|
785
|
+
index3.on(["expectedCloseDate"])
|
|
786
|
+
],
|
|
787
|
+
enums: [DealStatusEnum2]
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
// src/entities/task.entity.ts
|
|
791
|
+
import {
|
|
792
|
+
defineEntity as defineEntity4,
|
|
793
|
+
defineEntityEnum as defineEntityEnum4,
|
|
794
|
+
field as field4,
|
|
795
|
+
index as index4
|
|
796
|
+
} from "@contractspec/lib.schema";
|
|
797
|
+
var TaskTypeEnum = defineEntityEnum4({
|
|
798
|
+
name: "TaskType",
|
|
799
|
+
values: ["CALL", "EMAIL", "MEETING", "TODO", "FOLLOW_UP", "OTHER"],
|
|
800
|
+
schema: "crm",
|
|
801
|
+
description: "Type of CRM task."
|
|
802
|
+
});
|
|
803
|
+
var TaskPriorityEnum = defineEntityEnum4({
|
|
804
|
+
name: "TaskPriority",
|
|
805
|
+
values: ["LOW", "NORMAL", "HIGH", "URGENT"],
|
|
806
|
+
schema: "crm",
|
|
807
|
+
description: "Priority of a task."
|
|
808
|
+
});
|
|
809
|
+
var TaskStatusEnum = defineEntityEnum4({
|
|
810
|
+
name: "TaskStatus",
|
|
811
|
+
values: ["PENDING", "IN_PROGRESS", "COMPLETED", "CANCELLED"],
|
|
812
|
+
schema: "crm",
|
|
813
|
+
description: "Status of a task."
|
|
814
|
+
});
|
|
815
|
+
var TaskEntity = defineEntity4({
|
|
816
|
+
name: "Task",
|
|
817
|
+
description: "A task or follow-up activity.",
|
|
818
|
+
schema: "crm",
|
|
819
|
+
map: "task",
|
|
820
|
+
fields: {
|
|
821
|
+
id: field4.id(),
|
|
822
|
+
title: field4.string({ description: "Task title" }),
|
|
823
|
+
description: field4.string({ isOptional: true }),
|
|
824
|
+
type: field4.enum("TaskType", { default: "TODO" }),
|
|
825
|
+
priority: field4.enum("TaskPriority", { default: "NORMAL" }),
|
|
826
|
+
status: field4.enum("TaskStatus", { default: "PENDING" }),
|
|
827
|
+
dueDate: field4.dateTime({ isOptional: true }),
|
|
828
|
+
reminderAt: field4.dateTime({ isOptional: true }),
|
|
829
|
+
contactId: field4.string({ isOptional: true }),
|
|
830
|
+
dealId: field4.string({ isOptional: true }),
|
|
831
|
+
companyId: field4.string({ isOptional: true }),
|
|
832
|
+
organizationId: field4.foreignKey(),
|
|
833
|
+
assignedTo: field4.foreignKey({ description: "User assigned to this task" }),
|
|
834
|
+
createdBy: field4.foreignKey(),
|
|
835
|
+
completedAt: field4.dateTime({ isOptional: true }),
|
|
836
|
+
completedBy: field4.string({ isOptional: true }),
|
|
837
|
+
createdAt: field4.createdAt(),
|
|
838
|
+
updatedAt: field4.updatedAt(),
|
|
839
|
+
contact: field4.belongsTo("Contact", ["contactId"], ["id"]),
|
|
840
|
+
deal: field4.belongsTo("Deal", ["dealId"], ["id"]),
|
|
841
|
+
company: field4.belongsTo("Company", ["companyId"], ["id"])
|
|
842
|
+
},
|
|
843
|
+
indexes: [
|
|
844
|
+
index4.on(["organizationId", "assignedTo", "status"]),
|
|
845
|
+
index4.on(["dueDate", "status"]),
|
|
846
|
+
index4.on(["contactId"]),
|
|
847
|
+
index4.on(["dealId"])
|
|
848
|
+
],
|
|
849
|
+
enums: [TaskTypeEnum, TaskPriorityEnum, TaskStatusEnum]
|
|
850
|
+
});
|
|
851
|
+
var ActivityEntity = defineEntity4({
|
|
852
|
+
name: "Activity",
|
|
853
|
+
description: "An activity/interaction logged in the CRM.",
|
|
854
|
+
schema: "crm",
|
|
855
|
+
map: "activity",
|
|
856
|
+
fields: {
|
|
857
|
+
id: field4.id(),
|
|
858
|
+
type: field4.enum("TaskType"),
|
|
859
|
+
subject: field4.string(),
|
|
860
|
+
description: field4.string({ isOptional: true }),
|
|
861
|
+
contactId: field4.string({ isOptional: true }),
|
|
862
|
+
dealId: field4.string({ isOptional: true }),
|
|
863
|
+
companyId: field4.string({ isOptional: true }),
|
|
864
|
+
organizationId: field4.foreignKey(),
|
|
865
|
+
performedBy: field4.foreignKey(),
|
|
866
|
+
outcome: field4.string({ isOptional: true }),
|
|
867
|
+
occurredAt: field4.dateTime(),
|
|
868
|
+
duration: field4.int({
|
|
869
|
+
isOptional: true,
|
|
870
|
+
description: "Duration in minutes"
|
|
871
|
+
}),
|
|
872
|
+
createdAt: field4.createdAt(),
|
|
873
|
+
contact: field4.belongsTo("Contact", ["contactId"], ["id"]),
|
|
874
|
+
deal: field4.belongsTo("Deal", ["dealId"], ["id"]),
|
|
875
|
+
company: field4.belongsTo("Company", ["companyId"], ["id"])
|
|
876
|
+
},
|
|
877
|
+
indexes: [
|
|
878
|
+
index4.on(["contactId", "occurredAt"]),
|
|
879
|
+
index4.on(["dealId", "occurredAt"])
|
|
880
|
+
]
|
|
881
|
+
});
|
|
882
|
+
// src/entities/index.ts
|
|
883
|
+
var crmPipelineSchemaContribution = {
|
|
884
|
+
moduleId: "@contractspec/example.crm-pipeline",
|
|
885
|
+
entities: [
|
|
886
|
+
CompanyEntity,
|
|
887
|
+
ContactEntity,
|
|
888
|
+
DealEntity,
|
|
889
|
+
PipelineEntity,
|
|
890
|
+
StageEntity,
|
|
891
|
+
TaskEntity,
|
|
892
|
+
ActivityEntity
|
|
893
|
+
],
|
|
894
|
+
enums: [
|
|
895
|
+
CompanySizeEnum,
|
|
896
|
+
ContactStatusEnum,
|
|
897
|
+
DealStatusEnum2,
|
|
898
|
+
TaskTypeEnum,
|
|
899
|
+
TaskPriorityEnum,
|
|
900
|
+
TaskStatusEnum
|
|
901
|
+
]
|
|
902
|
+
};
|
|
903
|
+
|
|
904
|
+
// src/events/contact.event.ts
|
|
905
|
+
import { ScalarTypeEnum as ScalarTypeEnum2, defineSchemaModel as defineSchemaModel2 } from "@contractspec/lib.schema";
|
|
906
|
+
import { defineEvent } from "@contractspec/lib.contracts";
|
|
907
|
+
var ContactCreatedPayload = defineSchemaModel2({
|
|
908
|
+
name: "ContactCreatedPayload",
|
|
909
|
+
description: "Payload when a contact is created",
|
|
910
|
+
fields: {
|
|
911
|
+
contactId: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
|
|
912
|
+
email: { type: ScalarTypeEnum2.EmailAddress(), isOptional: true },
|
|
913
|
+
organizationId: {
|
|
914
|
+
type: ScalarTypeEnum2.String_unsecure(),
|
|
915
|
+
isOptional: false
|
|
916
|
+
},
|
|
917
|
+
ownerId: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
|
|
918
|
+
createdAt: { type: ScalarTypeEnum2.DateTime(), isOptional: false }
|
|
919
|
+
}
|
|
920
|
+
});
|
|
921
|
+
var ContactCreatedEvent = defineEvent({
|
|
922
|
+
meta: {
|
|
923
|
+
key: "contact.created",
|
|
924
|
+
version: "1.0.0",
|
|
925
|
+
description: "A new contact has been created.",
|
|
926
|
+
stability: "stable",
|
|
927
|
+
owners: ["@crm-team"],
|
|
928
|
+
tags: ["contact", "created"]
|
|
929
|
+
},
|
|
930
|
+
payload: ContactCreatedPayload
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
// src/events/deal.event.ts
|
|
934
|
+
import { ScalarTypeEnum as ScalarTypeEnum3, defineSchemaModel as defineSchemaModel3 } from "@contractspec/lib.schema";
|
|
935
|
+
import { defineEvent as defineEvent2 } from "@contractspec/lib.contracts";
|
|
936
|
+
var DealCreatedPayload = defineSchemaModel3({
|
|
937
|
+
name: "DealCreatedPayload",
|
|
938
|
+
description: "Payload when a deal is created",
|
|
939
|
+
fields: {
|
|
940
|
+
dealId: { type: ScalarTypeEnum3.String_unsecure(), isOptional: false },
|
|
941
|
+
name: { type: ScalarTypeEnum3.String_unsecure(), isOptional: false },
|
|
942
|
+
value: { type: ScalarTypeEnum3.Float_unsecure(), isOptional: false },
|
|
943
|
+
pipelineId: { type: ScalarTypeEnum3.String_unsecure(), isOptional: false },
|
|
944
|
+
stageId: { type: ScalarTypeEnum3.String_unsecure(), isOptional: false },
|
|
945
|
+
ownerId: { type: ScalarTypeEnum3.String_unsecure(), isOptional: false },
|
|
946
|
+
createdAt: { type: ScalarTypeEnum3.DateTime(), isOptional: false }
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
var DealMovedPayload = defineSchemaModel3({
|
|
950
|
+
name: "DealMovedEventPayload",
|
|
951
|
+
description: "Payload when a deal is moved to another stage",
|
|
952
|
+
fields: {
|
|
953
|
+
dealId: { type: ScalarTypeEnum3.String_unsecure(), isOptional: false },
|
|
954
|
+
fromStageId: { type: ScalarTypeEnum3.String_unsecure(), isOptional: false },
|
|
955
|
+
toStageId: { type: ScalarTypeEnum3.String_unsecure(), isOptional: false },
|
|
956
|
+
movedBy: { type: ScalarTypeEnum3.String_unsecure(), isOptional: false },
|
|
957
|
+
movedAt: { type: ScalarTypeEnum3.DateTime(), isOptional: false }
|
|
958
|
+
}
|
|
959
|
+
});
|
|
960
|
+
var DealWonPayload = defineSchemaModel3({
|
|
961
|
+
name: "DealWonEventPayload",
|
|
962
|
+
description: "Payload when a deal is won",
|
|
963
|
+
fields: {
|
|
964
|
+
dealId: { type: ScalarTypeEnum3.String_unsecure(), isOptional: false },
|
|
965
|
+
value: { type: ScalarTypeEnum3.Float_unsecure(), isOptional: false },
|
|
966
|
+
currency: { type: ScalarTypeEnum3.String_unsecure(), isOptional: false },
|
|
967
|
+
contactId: { type: ScalarTypeEnum3.String_unsecure(), isOptional: true },
|
|
968
|
+
companyId: { type: ScalarTypeEnum3.String_unsecure(), isOptional: true },
|
|
969
|
+
ownerId: { type: ScalarTypeEnum3.String_unsecure(), isOptional: false },
|
|
970
|
+
wonAt: { type: ScalarTypeEnum3.DateTime(), isOptional: false }
|
|
971
|
+
}
|
|
972
|
+
});
|
|
973
|
+
var DealLostPayload = defineSchemaModel3({
|
|
974
|
+
name: "DealLostEventPayload",
|
|
975
|
+
description: "Payload when a deal is lost",
|
|
976
|
+
fields: {
|
|
977
|
+
dealId: { type: ScalarTypeEnum3.String_unsecure(), isOptional: false },
|
|
978
|
+
value: { type: ScalarTypeEnum3.Float_unsecure(), isOptional: false },
|
|
979
|
+
reason: { type: ScalarTypeEnum3.String_unsecure(), isOptional: false },
|
|
980
|
+
ownerId: { type: ScalarTypeEnum3.String_unsecure(), isOptional: false },
|
|
981
|
+
lostAt: { type: ScalarTypeEnum3.DateTime(), isOptional: false }
|
|
982
|
+
}
|
|
983
|
+
});
|
|
984
|
+
var DealCreatedEvent = defineEvent2({
|
|
985
|
+
meta: {
|
|
986
|
+
key: "deal.created",
|
|
987
|
+
version: "1.0.0",
|
|
988
|
+
description: "A new deal has been created.",
|
|
989
|
+
stability: "stable",
|
|
990
|
+
owners: ["@crm-team"],
|
|
991
|
+
tags: ["deal", "created"]
|
|
992
|
+
},
|
|
993
|
+
payload: DealCreatedPayload
|
|
994
|
+
});
|
|
995
|
+
var DealMovedEvent = defineEvent2({
|
|
996
|
+
meta: {
|
|
997
|
+
key: "deal.moved",
|
|
998
|
+
version: "1.0.0",
|
|
999
|
+
description: "A deal has been moved to a different stage.",
|
|
1000
|
+
stability: "stable",
|
|
1001
|
+
owners: ["@crm-team"],
|
|
1002
|
+
tags: ["deal", "moved"]
|
|
1003
|
+
},
|
|
1004
|
+
payload: DealMovedPayload
|
|
1005
|
+
});
|
|
1006
|
+
var DealWonEvent = defineEvent2({
|
|
1007
|
+
meta: {
|
|
1008
|
+
key: "deal.won",
|
|
1009
|
+
version: "1.0.0",
|
|
1010
|
+
description: "A deal has been won.",
|
|
1011
|
+
stability: "stable",
|
|
1012
|
+
owners: ["@crm-team"],
|
|
1013
|
+
tags: ["deal", "won"]
|
|
1014
|
+
},
|
|
1015
|
+
payload: DealWonPayload
|
|
1016
|
+
});
|
|
1017
|
+
var DealLostEvent = defineEvent2({
|
|
1018
|
+
meta: {
|
|
1019
|
+
key: "deal.lost",
|
|
1020
|
+
version: "1.0.0",
|
|
1021
|
+
description: "A deal has been lost.",
|
|
1022
|
+
stability: "stable",
|
|
1023
|
+
owners: ["@crm-team"],
|
|
1024
|
+
tags: ["deal", "lost"]
|
|
1025
|
+
},
|
|
1026
|
+
payload: DealLostPayload
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
// src/events/task.event.ts
|
|
1030
|
+
import { ScalarTypeEnum as ScalarTypeEnum4, defineSchemaModel as defineSchemaModel4 } from "@contractspec/lib.schema";
|
|
1031
|
+
import { defineEvent as defineEvent3 } from "@contractspec/lib.contracts";
|
|
1032
|
+
var TaskCompletedPayload = defineSchemaModel4({
|
|
1033
|
+
name: "TaskCompletedPayload",
|
|
1034
|
+
description: "Payload when a task is completed",
|
|
1035
|
+
fields: {
|
|
1036
|
+
taskId: { type: ScalarTypeEnum4.String_unsecure(), isOptional: false },
|
|
1037
|
+
type: { type: ScalarTypeEnum4.String_unsecure(), isOptional: false },
|
|
1038
|
+
assignedTo: { type: ScalarTypeEnum4.String_unsecure(), isOptional: false },
|
|
1039
|
+
completedBy: { type: ScalarTypeEnum4.String_unsecure(), isOptional: false },
|
|
1040
|
+
completedAt: { type: ScalarTypeEnum4.DateTime(), isOptional: false }
|
|
1041
|
+
}
|
|
1042
|
+
});
|
|
1043
|
+
var TaskCompletedEvent = defineEvent3({
|
|
1044
|
+
meta: {
|
|
1045
|
+
key: "task.completed",
|
|
1046
|
+
version: "1.0.0",
|
|
1047
|
+
description: "A task has been completed.",
|
|
1048
|
+
stability: "stable",
|
|
1049
|
+
owners: ["@crm-team"],
|
|
1050
|
+
tags: ["task", "lifecycle"]
|
|
1051
|
+
},
|
|
1052
|
+
payload: TaskCompletedPayload
|
|
1053
|
+
});
|
|
1054
|
+
// src/example.ts
|
|
1055
|
+
import { defineExample } from "@contractspec/lib.contracts";
|
|
1056
|
+
var example = defineExample({
|
|
1057
|
+
meta: {
|
|
1058
|
+
key: "crm-pipeline",
|
|
1059
|
+
version: "1.0.0",
|
|
1060
|
+
title: "CRM Pipeline",
|
|
1061
|
+
description: "Sales CRM with contacts, companies, deals, pipelines, and tasks.",
|
|
1062
|
+
kind: "template",
|
|
1063
|
+
visibility: "public",
|
|
1064
|
+
stability: "experimental",
|
|
1065
|
+
owners: ["@platform.core"],
|
|
1066
|
+
tags: ["crm", "sales", "pipeline", "deals"]
|
|
1067
|
+
},
|
|
1068
|
+
docs: {
|
|
1069
|
+
rootDocId: "docs.examples.crm-pipeline"
|
|
1070
|
+
},
|
|
1071
|
+
entrypoints: {
|
|
1072
|
+
packageName: "@contractspec/example.crm-pipeline",
|
|
1073
|
+
feature: "./feature",
|
|
1074
|
+
contracts: "./contracts",
|
|
1075
|
+
presentations: "./presentations",
|
|
1076
|
+
handlers: "./handlers",
|
|
1077
|
+
docs: "./docs"
|
|
1078
|
+
},
|
|
1079
|
+
surfaces: {
|
|
1080
|
+
templates: true,
|
|
1081
|
+
sandbox: {
|
|
1082
|
+
enabled: true,
|
|
1083
|
+
modes: ["playground", "specs", "builder", "markdown", "evolution"]
|
|
1084
|
+
},
|
|
1085
|
+
studio: { enabled: true, installable: true },
|
|
1086
|
+
mcp: { enabled: true }
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
var example_default = example;
|
|
1090
|
+
|
|
1091
|
+
// src/handlers/crm.handlers.ts
|
|
1092
|
+
import { web } from "@contractspec/lib.runtime-sandbox";
|
|
1093
|
+
var { generateId } = web;
|
|
1094
|
+
function rowToDeal(row) {
|
|
1095
|
+
return {
|
|
1096
|
+
id: row.id,
|
|
1097
|
+
projectId: row.projectId,
|
|
1098
|
+
name: row.name,
|
|
1099
|
+
value: row.value,
|
|
1100
|
+
currency: row.currency,
|
|
1101
|
+
pipelineId: row.pipelineId,
|
|
1102
|
+
stageId: row.stageId,
|
|
1103
|
+
status: row.status,
|
|
1104
|
+
contactId: row.contactId ?? undefined,
|
|
1105
|
+
companyId: row.companyId ?? undefined,
|
|
1106
|
+
ownerId: row.ownerId,
|
|
1107
|
+
expectedCloseDate: row.expectedCloseDate ? new Date(row.expectedCloseDate) : undefined,
|
|
1108
|
+
wonSource: row.wonSource ?? undefined,
|
|
1109
|
+
lostReason: row.lostReason ?? undefined,
|
|
1110
|
+
notes: row.notes ?? undefined,
|
|
1111
|
+
createdAt: new Date(row.createdAt),
|
|
1112
|
+
updatedAt: new Date(row.updatedAt)
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
function createCrmHandlers(db) {
|
|
1116
|
+
async function listDeals(input) {
|
|
1117
|
+
const {
|
|
1118
|
+
projectId,
|
|
1119
|
+
pipelineId,
|
|
1120
|
+
stageId,
|
|
1121
|
+
status,
|
|
1122
|
+
ownerId,
|
|
1123
|
+
search,
|
|
1124
|
+
limit = 20,
|
|
1125
|
+
offset = 0
|
|
1126
|
+
} = input;
|
|
1127
|
+
let whereClause = "WHERE projectId = ?";
|
|
1128
|
+
const params = [projectId];
|
|
1129
|
+
if (pipelineId) {
|
|
1130
|
+
whereClause += " AND pipelineId = ?";
|
|
1131
|
+
params.push(pipelineId);
|
|
1132
|
+
}
|
|
1133
|
+
if (stageId) {
|
|
1134
|
+
whereClause += " AND stageId = ?";
|
|
1135
|
+
params.push(stageId);
|
|
1136
|
+
}
|
|
1137
|
+
if (status && status !== "all") {
|
|
1138
|
+
whereClause += " AND status = ?";
|
|
1139
|
+
params.push(status);
|
|
1140
|
+
}
|
|
1141
|
+
if (ownerId) {
|
|
1142
|
+
whereClause += " AND ownerId = ?";
|
|
1143
|
+
params.push(ownerId);
|
|
1144
|
+
}
|
|
1145
|
+
if (search) {
|
|
1146
|
+
whereClause += " AND name LIKE ?";
|
|
1147
|
+
params.push(`%${search}%`);
|
|
1148
|
+
}
|
|
1149
|
+
const countResult = (await db.query(`SELECT COUNT(*) as count FROM crm_deal ${whereClause}`, params)).rows;
|
|
1150
|
+
const total = countResult[0]?.count ?? 0;
|
|
1151
|
+
const valueResult = (await db.query(`SELECT COALESCE(SUM(value), 0) as total FROM crm_deal ${whereClause}`, params)).rows;
|
|
1152
|
+
const totalValue = valueResult[0]?.total ?? 0;
|
|
1153
|
+
const dealRows = (await db.query(`SELECT * FROM crm_deal ${whereClause} ORDER BY value DESC LIMIT ? OFFSET ?`, [...params, limit, offset])).rows;
|
|
1154
|
+
return {
|
|
1155
|
+
deals: dealRows.map(rowToDeal),
|
|
1156
|
+
total,
|
|
1157
|
+
totalValue
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
async function createDeal(input, context) {
|
|
1161
|
+
const id = generateId("deal");
|
|
1162
|
+
const now = new Date().toISOString();
|
|
1163
|
+
await db.execute(`INSERT INTO crm_deal (id, projectId, pipelineId, stageId, name, value, currency, status, contactId, companyId, ownerId, expectedCloseDate, createdAt, updatedAt)
|
|
1164
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1165
|
+
id,
|
|
1166
|
+
context.projectId,
|
|
1167
|
+
input.pipelineId,
|
|
1168
|
+
input.stageId,
|
|
1169
|
+
input.name,
|
|
1170
|
+
input.value,
|
|
1171
|
+
input.currency ?? "USD",
|
|
1172
|
+
"OPEN",
|
|
1173
|
+
input.contactId ?? null,
|
|
1174
|
+
input.companyId ?? null,
|
|
1175
|
+
context.ownerId,
|
|
1176
|
+
input.expectedCloseDate?.toISOString() ?? null,
|
|
1177
|
+
now,
|
|
1178
|
+
now
|
|
1179
|
+
]);
|
|
1180
|
+
const rows = (await db.query(`SELECT * FROM crm_deal WHERE id = ?`, [id])).rows;
|
|
1181
|
+
if (!rows[0]) {
|
|
1182
|
+
throw new Error("Failed to create deal");
|
|
1183
|
+
}
|
|
1184
|
+
return rowToDeal(rows[0]);
|
|
1185
|
+
}
|
|
1186
|
+
async function moveDeal(input) {
|
|
1187
|
+
const now = new Date().toISOString();
|
|
1188
|
+
const existing = (await db.query(`SELECT * FROM crm_deal WHERE id = ?`, [input.dealId])).rows;
|
|
1189
|
+
if (!existing[0]) {
|
|
1190
|
+
throw new Error("NOT_FOUND");
|
|
1191
|
+
}
|
|
1192
|
+
const stage = (await db.query(`SELECT * FROM crm_stage WHERE id = ?`, [input.stageId])).rows;
|
|
1193
|
+
if (!stage[0]) {
|
|
1194
|
+
throw new Error("INVALID_STAGE");
|
|
1195
|
+
}
|
|
1196
|
+
await db.execute(`UPDATE crm_deal SET stageId = ?, updatedAt = ? WHERE id = ?`, [input.stageId, now, input.dealId]);
|
|
1197
|
+
const rows = (await db.query(`SELECT * FROM crm_deal WHERE id = ?`, [input.dealId])).rows;
|
|
1198
|
+
return rowToDeal(rows[0]);
|
|
1199
|
+
}
|
|
1200
|
+
async function winDeal(input) {
|
|
1201
|
+
const now = new Date().toISOString();
|
|
1202
|
+
const existing = (await db.query(`SELECT * FROM crm_deal WHERE id = ?`, [input.dealId])).rows;
|
|
1203
|
+
if (!existing[0]) {
|
|
1204
|
+
throw new Error("NOT_FOUND");
|
|
1205
|
+
}
|
|
1206
|
+
await db.execute(`UPDATE crm_deal SET status = 'WON', wonSource = ?, notes = ?, updatedAt = ? WHERE id = ?`, [input.wonSource ?? null, input.notes ?? null, now, input.dealId]);
|
|
1207
|
+
const rows = (await db.query(`SELECT * FROM crm_deal WHERE id = ?`, [input.dealId])).rows;
|
|
1208
|
+
return rowToDeal(rows[0]);
|
|
1209
|
+
}
|
|
1210
|
+
async function loseDeal(input) {
|
|
1211
|
+
const now = new Date().toISOString();
|
|
1212
|
+
const existing = (await db.query(`SELECT * FROM crm_deal WHERE id = ?`, [input.dealId])).rows;
|
|
1213
|
+
if (!existing[0]) {
|
|
1214
|
+
throw new Error("NOT_FOUND");
|
|
1215
|
+
}
|
|
1216
|
+
await db.execute(`UPDATE crm_deal SET status = 'LOST', lostReason = ?, notes = ?, updatedAt = ? WHERE id = ?`, [input.lostReason, input.notes ?? null, now, input.dealId]);
|
|
1217
|
+
const rows = (await db.query(`SELECT * FROM crm_deal WHERE id = ?`, [input.dealId])).rows;
|
|
1218
|
+
return rowToDeal(rows[0]);
|
|
1219
|
+
}
|
|
1220
|
+
async function getDealsByStage(input) {
|
|
1221
|
+
const deals = (await db.query(`SELECT * FROM crm_deal WHERE projectId = ? AND pipelineId = ? AND status = 'OPEN' ORDER BY value DESC`, [input.projectId, input.pipelineId])).rows;
|
|
1222
|
+
const stages = (await db.query(`SELECT * FROM crm_stage WHERE pipelineId = ? ORDER BY position`, [input.pipelineId])).rows;
|
|
1223
|
+
const grouped = {};
|
|
1224
|
+
for (const stage of stages) {
|
|
1225
|
+
grouped[stage.id] = deals.filter((d) => d.stageId === stage.id).map(rowToDeal);
|
|
1226
|
+
}
|
|
1227
|
+
return grouped;
|
|
1228
|
+
}
|
|
1229
|
+
async function getPipelineStages(input) {
|
|
1230
|
+
const rows = (await db.query(`SELECT * FROM crm_stage WHERE pipelineId = ? ORDER BY position`, [input.pipelineId])).rows;
|
|
1231
|
+
return rows.map((row) => ({
|
|
1232
|
+
id: row.id,
|
|
1233
|
+
pipelineId: row.pipelineId,
|
|
1234
|
+
name: row.name,
|
|
1235
|
+
position: row.position
|
|
1236
|
+
}));
|
|
1237
|
+
}
|
|
1238
|
+
return {
|
|
1239
|
+
listDeals,
|
|
1240
|
+
createDeal,
|
|
1241
|
+
moveDeal,
|
|
1242
|
+
winDeal,
|
|
1243
|
+
loseDeal,
|
|
1244
|
+
getDealsByStage,
|
|
1245
|
+
getPipelineStages
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// src/handlers/mock-data.ts
|
|
1250
|
+
var MOCK_STAGES = [
|
|
1251
|
+
{ id: "stage-1", name: "Lead", position: 1, pipelineId: "pipeline-1" },
|
|
1252
|
+
{ id: "stage-2", name: "Qualified", position: 2, pipelineId: "pipeline-1" },
|
|
1253
|
+
{ id: "stage-3", name: "Proposal", position: 3, pipelineId: "pipeline-1" },
|
|
1254
|
+
{ id: "stage-4", name: "Negotiation", position: 4, pipelineId: "pipeline-1" },
|
|
1255
|
+
{ id: "stage-5", name: "Closed", position: 5, pipelineId: "pipeline-1" }
|
|
1256
|
+
];
|
|
1257
|
+
var MOCK_DEALS = [
|
|
1258
|
+
{
|
|
1259
|
+
id: "deal-1",
|
|
1260
|
+
name: "Enterprise License - Acme Corp",
|
|
1261
|
+
value: 75000,
|
|
1262
|
+
currency: "USD",
|
|
1263
|
+
pipelineId: "pipeline-1",
|
|
1264
|
+
stageId: "stage-3",
|
|
1265
|
+
status: "OPEN",
|
|
1266
|
+
contactId: "contact-1",
|
|
1267
|
+
companyId: "company-1",
|
|
1268
|
+
ownerId: "user-1",
|
|
1269
|
+
expectedCloseDate: new Date("2024-05-15T00:00:00Z"),
|
|
1270
|
+
createdAt: new Date("2024-02-01T10:00:00Z"),
|
|
1271
|
+
updatedAt: new Date("2024-04-10T14:30:00Z")
|
|
1272
|
+
},
|
|
1273
|
+
{
|
|
1274
|
+
id: "deal-2",
|
|
1275
|
+
name: "Startup Plan - TechStart Inc",
|
|
1276
|
+
value: 12000,
|
|
1277
|
+
currency: "USD",
|
|
1278
|
+
pipelineId: "pipeline-1",
|
|
1279
|
+
stageId: "stage-2",
|
|
1280
|
+
status: "OPEN",
|
|
1281
|
+
contactId: "contact-2",
|
|
1282
|
+
companyId: "company-2",
|
|
1283
|
+
ownerId: "user-2",
|
|
1284
|
+
expectedCloseDate: new Date("2024-04-30T00:00:00Z"),
|
|
1285
|
+
createdAt: new Date("2024-03-15T09:00:00Z"),
|
|
1286
|
+
updatedAt: new Date("2024-04-08T11:15:00Z")
|
|
1287
|
+
},
|
|
1288
|
+
{
|
|
1289
|
+
id: "deal-3",
|
|
1290
|
+
name: "Professional Services - Global Ltd",
|
|
1291
|
+
value: 45000,
|
|
1292
|
+
currency: "USD",
|
|
1293
|
+
pipelineId: "pipeline-1",
|
|
1294
|
+
stageId: "stage-4",
|
|
1295
|
+
status: "OPEN",
|
|
1296
|
+
contactId: "contact-3",
|
|
1297
|
+
companyId: "company-3",
|
|
1298
|
+
ownerId: "user-1",
|
|
1299
|
+
expectedCloseDate: new Date("2024-04-20T00:00:00Z"),
|
|
1300
|
+
createdAt: new Date("2024-01-20T08:00:00Z"),
|
|
1301
|
+
updatedAt: new Date("2024-04-12T16:45:00Z")
|
|
1302
|
+
},
|
|
1303
|
+
{
|
|
1304
|
+
id: "deal-4",
|
|
1305
|
+
name: "Annual Contract - SmallBiz Co",
|
|
1306
|
+
value: 8500,
|
|
1307
|
+
currency: "USD",
|
|
1308
|
+
pipelineId: "pipeline-1",
|
|
1309
|
+
stageId: "stage-1",
|
|
1310
|
+
status: "OPEN",
|
|
1311
|
+
contactId: "contact-4",
|
|
1312
|
+
companyId: "company-4",
|
|
1313
|
+
ownerId: "user-3",
|
|
1314
|
+
createdAt: new Date("2024-04-05T12:00:00Z"),
|
|
1315
|
+
updatedAt: new Date("2024-04-05T12:00:00Z")
|
|
1316
|
+
},
|
|
1317
|
+
{
|
|
1318
|
+
id: "deal-5",
|
|
1319
|
+
name: "Custom Integration - MegaCorp",
|
|
1320
|
+
value: 125000,
|
|
1321
|
+
currency: "USD",
|
|
1322
|
+
pipelineId: "pipeline-1",
|
|
1323
|
+
stageId: "stage-5",
|
|
1324
|
+
status: "WON",
|
|
1325
|
+
contactId: "contact-5",
|
|
1326
|
+
companyId: "company-5",
|
|
1327
|
+
ownerId: "user-1",
|
|
1328
|
+
expectedCloseDate: new Date("2024-03-31T00:00:00Z"),
|
|
1329
|
+
createdAt: new Date("2023-11-10T10:00:00Z"),
|
|
1330
|
+
updatedAt: new Date("2024-03-28T09:00:00Z")
|
|
1331
|
+
},
|
|
1332
|
+
{
|
|
1333
|
+
id: "deal-6",
|
|
1334
|
+
name: "Pilot Project - NewCo",
|
|
1335
|
+
value: 5000,
|
|
1336
|
+
currency: "USD",
|
|
1337
|
+
pipelineId: "pipeline-1",
|
|
1338
|
+
stageId: "stage-2",
|
|
1339
|
+
status: "LOST",
|
|
1340
|
+
contactId: "contact-6",
|
|
1341
|
+
companyId: "company-6",
|
|
1342
|
+
ownerId: "user-2",
|
|
1343
|
+
createdAt: new Date("2024-01-15T14:00:00Z"),
|
|
1344
|
+
updatedAt: new Date("2024-02-28T10:30:00Z")
|
|
1345
|
+
}
|
|
1346
|
+
];
|
|
1347
|
+
var MOCK_COMPANIES = [
|
|
1348
|
+
{
|
|
1349
|
+
id: "company-1",
|
|
1350
|
+
name: "Acme Corporation",
|
|
1351
|
+
domain: "acme.com",
|
|
1352
|
+
industry: "Technology",
|
|
1353
|
+
size: "1000-5000",
|
|
1354
|
+
website: "https://acme.com",
|
|
1355
|
+
createdAt: new Date("2024-01-01T00:00:00Z")
|
|
1356
|
+
},
|
|
1357
|
+
{
|
|
1358
|
+
id: "company-2",
|
|
1359
|
+
name: "TechStart Inc",
|
|
1360
|
+
domain: "techstart.io",
|
|
1361
|
+
industry: "Software",
|
|
1362
|
+
size: "10-50",
|
|
1363
|
+
website: "https://techstart.io",
|
|
1364
|
+
createdAt: new Date("2024-02-15T00:00:00Z")
|
|
1365
|
+
},
|
|
1366
|
+
{
|
|
1367
|
+
id: "company-3",
|
|
1368
|
+
name: "Global Ltd",
|
|
1369
|
+
domain: "global.com",
|
|
1370
|
+
industry: "Consulting",
|
|
1371
|
+
size: "500-1000",
|
|
1372
|
+
website: "https://global.com",
|
|
1373
|
+
createdAt: new Date("2023-12-01T00:00:00Z")
|
|
1374
|
+
}
|
|
1375
|
+
];
|
|
1376
|
+
var MOCK_CONTACTS = [
|
|
1377
|
+
{
|
|
1378
|
+
id: "contact-1",
|
|
1379
|
+
firstName: "John",
|
|
1380
|
+
lastName: "Smith",
|
|
1381
|
+
email: "john.smith@acme.com",
|
|
1382
|
+
phone: "+1-555-0101",
|
|
1383
|
+
title: "VP of Engineering",
|
|
1384
|
+
companyId: "company-1",
|
|
1385
|
+
createdAt: new Date("2024-01-05T00:00:00Z")
|
|
1386
|
+
},
|
|
1387
|
+
{
|
|
1388
|
+
id: "contact-2",
|
|
1389
|
+
firstName: "Sarah",
|
|
1390
|
+
lastName: "Johnson",
|
|
1391
|
+
email: "sarah@techstart.io",
|
|
1392
|
+
phone: "+1-555-0102",
|
|
1393
|
+
title: "CEO",
|
|
1394
|
+
companyId: "company-2",
|
|
1395
|
+
createdAt: new Date("2024-02-20T00:00:00Z")
|
|
1396
|
+
},
|
|
1397
|
+
{
|
|
1398
|
+
id: "contact-3",
|
|
1399
|
+
firstName: "Michael",
|
|
1400
|
+
lastName: "Brown",
|
|
1401
|
+
email: "michael.brown@global.com",
|
|
1402
|
+
phone: "+1-555-0103",
|
|
1403
|
+
title: "CTO",
|
|
1404
|
+
companyId: "company-3",
|
|
1405
|
+
createdAt: new Date("2023-12-10T00:00:00Z")
|
|
1406
|
+
}
|
|
1407
|
+
];
|
|
1408
|
+
|
|
1409
|
+
// src/handlers/deal.handlers.ts
|
|
1410
|
+
async function mockListDealsHandler(input) {
|
|
1411
|
+
const {
|
|
1412
|
+
pipelineId,
|
|
1413
|
+
stageId,
|
|
1414
|
+
status,
|
|
1415
|
+
ownerId,
|
|
1416
|
+
search,
|
|
1417
|
+
limit = 20,
|
|
1418
|
+
offset = 0
|
|
1419
|
+
} = input;
|
|
1420
|
+
let filtered = [...MOCK_DEALS];
|
|
1421
|
+
if (pipelineId) {
|
|
1422
|
+
filtered = filtered.filter((d) => d.pipelineId === pipelineId);
|
|
1423
|
+
}
|
|
1424
|
+
if (stageId) {
|
|
1425
|
+
filtered = filtered.filter((d) => d.stageId === stageId);
|
|
1426
|
+
}
|
|
1427
|
+
if (status && status !== "all") {
|
|
1428
|
+
filtered = filtered.filter((d) => d.status === status);
|
|
1429
|
+
}
|
|
1430
|
+
if (ownerId) {
|
|
1431
|
+
filtered = filtered.filter((d) => d.ownerId === ownerId);
|
|
1432
|
+
}
|
|
1433
|
+
if (search) {
|
|
1434
|
+
const q = search.toLowerCase();
|
|
1435
|
+
filtered = filtered.filter((d) => d.name.toLowerCase().includes(q));
|
|
1436
|
+
}
|
|
1437
|
+
filtered.sort((a, b) => b.value - a.value);
|
|
1438
|
+
const total = filtered.length;
|
|
1439
|
+
const totalValue = filtered.reduce((sum, d) => sum + d.value, 0);
|
|
1440
|
+
const deals = filtered.slice(offset, offset + limit);
|
|
1441
|
+
return {
|
|
1442
|
+
deals,
|
|
1443
|
+
total,
|
|
1444
|
+
totalValue
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
async function mockCreateDealHandler(input, context) {
|
|
1448
|
+
const now = new Date;
|
|
1449
|
+
const deal3 = {
|
|
1450
|
+
id: `deal-${Date.now()}`,
|
|
1451
|
+
name: input.name,
|
|
1452
|
+
value: input.value,
|
|
1453
|
+
currency: input.currency ?? "USD",
|
|
1454
|
+
pipelineId: input.pipelineId,
|
|
1455
|
+
stageId: input.stageId,
|
|
1456
|
+
status: "OPEN",
|
|
1457
|
+
contactId: input.contactId,
|
|
1458
|
+
companyId: input.companyId,
|
|
1459
|
+
ownerId: context.ownerId,
|
|
1460
|
+
expectedCloseDate: input.expectedCloseDate,
|
|
1461
|
+
createdAt: now,
|
|
1462
|
+
updatedAt: now
|
|
1463
|
+
};
|
|
1464
|
+
MOCK_DEALS.push(deal3);
|
|
1465
|
+
return deal3;
|
|
1466
|
+
}
|
|
1467
|
+
async function mockMoveDealHandler(input) {
|
|
1468
|
+
const dealIndex = MOCK_DEALS.findIndex((d) => d.id === input.dealId);
|
|
1469
|
+
if (dealIndex === -1) {
|
|
1470
|
+
throw new Error("NOT_FOUND");
|
|
1471
|
+
}
|
|
1472
|
+
const deal3 = MOCK_DEALS[dealIndex];
|
|
1473
|
+
if (!deal3) {
|
|
1474
|
+
throw new Error("NOT_FOUND");
|
|
1475
|
+
}
|
|
1476
|
+
const stage = MOCK_STAGES.find((s) => s.id === input.stageId);
|
|
1477
|
+
if (!stage) {
|
|
1478
|
+
throw new Error("INVALID_STAGE");
|
|
1479
|
+
}
|
|
1480
|
+
const updatedDeal = {
|
|
1481
|
+
...deal3,
|
|
1482
|
+
stageId: input.stageId,
|
|
1483
|
+
updatedAt: new Date
|
|
1484
|
+
};
|
|
1485
|
+
MOCK_DEALS[dealIndex] = updatedDeal;
|
|
1486
|
+
return updatedDeal;
|
|
1487
|
+
}
|
|
1488
|
+
async function mockWinDealHandler(input) {
|
|
1489
|
+
const dealIndex = MOCK_DEALS.findIndex((d) => d.id === input.dealId);
|
|
1490
|
+
if (dealIndex === -1) {
|
|
1491
|
+
throw new Error("NOT_FOUND");
|
|
1492
|
+
}
|
|
1493
|
+
const deal3 = MOCK_DEALS[dealIndex];
|
|
1494
|
+
if (!deal3) {
|
|
1495
|
+
throw new Error("NOT_FOUND");
|
|
1496
|
+
}
|
|
1497
|
+
const updatedDeal = {
|
|
1498
|
+
...deal3,
|
|
1499
|
+
status: "WON",
|
|
1500
|
+
updatedAt: new Date
|
|
1501
|
+
};
|
|
1502
|
+
MOCK_DEALS[dealIndex] = updatedDeal;
|
|
1503
|
+
return updatedDeal;
|
|
1504
|
+
}
|
|
1505
|
+
async function mockLoseDealHandler(input) {
|
|
1506
|
+
const dealIndex = MOCK_DEALS.findIndex((d) => d.id === input.dealId);
|
|
1507
|
+
if (dealIndex === -1) {
|
|
1508
|
+
throw new Error("NOT_FOUND");
|
|
1509
|
+
}
|
|
1510
|
+
const deal3 = MOCK_DEALS[dealIndex];
|
|
1511
|
+
if (!deal3) {
|
|
1512
|
+
throw new Error("NOT_FOUND");
|
|
1513
|
+
}
|
|
1514
|
+
const updatedDeal = {
|
|
1515
|
+
...deal3,
|
|
1516
|
+
status: "LOST",
|
|
1517
|
+
updatedAt: new Date
|
|
1518
|
+
};
|
|
1519
|
+
MOCK_DEALS[dealIndex] = updatedDeal;
|
|
1520
|
+
return updatedDeal;
|
|
1521
|
+
}
|
|
1522
|
+
async function mockGetDealsByStageHandler(input) {
|
|
1523
|
+
const deals = MOCK_DEALS.filter((d) => d.pipelineId === input.pipelineId && d.status === "OPEN");
|
|
1524
|
+
const grouped = {};
|
|
1525
|
+
for (const stage of MOCK_STAGES) {
|
|
1526
|
+
grouped[stage.id] = deals.filter((d) => d.stageId === stage.id);
|
|
1527
|
+
}
|
|
1528
|
+
return grouped;
|
|
1529
|
+
}
|
|
1530
|
+
async function mockGetPipelineStagesHandler(input) {
|
|
1531
|
+
return MOCK_STAGES.filter((s) => s.pipelineId === input.pipelineId);
|
|
1532
|
+
}
|
|
1533
|
+
// src/presentations/dashboard.presentation.ts
|
|
1534
|
+
import { definePresentation, StabilityEnum } from "@contractspec/lib.contracts";
|
|
1535
|
+
var CrmDashboardPresentation = definePresentation({
|
|
1536
|
+
meta: {
|
|
1537
|
+
key: "crm.dashboard",
|
|
1538
|
+
version: "1.0.0",
|
|
1539
|
+
title: "CRM Dashboard",
|
|
1540
|
+
description: "Main CRM dashboard with pipeline overview, deal stats, and activities",
|
|
1541
|
+
domain: "crm-pipeline",
|
|
1542
|
+
owners: ["@crm-team"],
|
|
1543
|
+
tags: ["dashboard", "overview"],
|
|
1544
|
+
stability: StabilityEnum.Experimental,
|
|
1545
|
+
goal: "Provide a high-level overview of CRM performance and active deals.",
|
|
1546
|
+
context: "The landing page for CRM users."
|
|
1547
|
+
},
|
|
1548
|
+
source: {
|
|
1549
|
+
type: "component",
|
|
1550
|
+
framework: "react",
|
|
1551
|
+
componentKey: "CrmDashboard"
|
|
1552
|
+
},
|
|
1553
|
+
targets: ["react", "markdown"],
|
|
1554
|
+
policy: {
|
|
1555
|
+
flags: ["crm.enabled"]
|
|
1556
|
+
}
|
|
1557
|
+
});
|
|
1558
|
+
var PipelineMetricsPresentation = definePresentation({
|
|
1559
|
+
meta: {
|
|
1560
|
+
key: "crm.pipeline.metrics",
|
|
1561
|
+
version: "1.0.0",
|
|
1562
|
+
title: "Pipeline Metrics",
|
|
1563
|
+
description: "Pipeline metrics and forecasting view",
|
|
1564
|
+
domain: "crm-pipeline",
|
|
1565
|
+
owners: ["@crm-team"],
|
|
1566
|
+
tags: ["pipeline", "metrics", "forecast"],
|
|
1567
|
+
stability: StabilityEnum.Experimental,
|
|
1568
|
+
goal: "Track pipeline health and sales forecasts.",
|
|
1569
|
+
context: "Data-intensive widget for sales managers."
|
|
1570
|
+
},
|
|
1571
|
+
source: {
|
|
1572
|
+
type: "component",
|
|
1573
|
+
framework: "react",
|
|
1574
|
+
componentKey: "PipelineMetricsView"
|
|
1575
|
+
},
|
|
1576
|
+
targets: ["react", "markdown"],
|
|
1577
|
+
policy: {
|
|
1578
|
+
flags: ["crm.metrics.enabled"]
|
|
1579
|
+
}
|
|
1580
|
+
});
|
|
1581
|
+
|
|
1582
|
+
// src/presentations/pipeline.presentation.ts
|
|
1583
|
+
import { definePresentation as definePresentation2, StabilityEnum as StabilityEnum2 } from "@contractspec/lib.contracts";
|
|
1584
|
+
var PipelineKanbanPresentation = definePresentation2({
|
|
1585
|
+
meta: {
|
|
1586
|
+
key: "crm.pipeline.kanban",
|
|
1587
|
+
version: "1.0.0",
|
|
1588
|
+
title: "Pipeline Kanban",
|
|
1589
|
+
description: "Kanban board view of deals organized by stage",
|
|
1590
|
+
domain: "crm-pipeline",
|
|
1591
|
+
owners: ["@crm-team"],
|
|
1592
|
+
tags: ["pipeline", "kanban", "deals"],
|
|
1593
|
+
stability: StabilityEnum2.Experimental,
|
|
1594
|
+
goal: "Visualize the sales pipeline status and deal distribution across stages.",
|
|
1595
|
+
context: "Used in the sales dashboard and management reports."
|
|
1596
|
+
},
|
|
1597
|
+
source: {
|
|
1598
|
+
type: "component",
|
|
1599
|
+
framework: "react",
|
|
1600
|
+
componentKey: "PipelineKanbanView",
|
|
1601
|
+
props: DealModel
|
|
1602
|
+
},
|
|
1603
|
+
targets: ["react", "markdown"],
|
|
1604
|
+
policy: {
|
|
1605
|
+
flags: ["crm.pipeline.enabled"]
|
|
1606
|
+
}
|
|
1607
|
+
});
|
|
1608
|
+
var DealListPresentation = definePresentation2({
|
|
1609
|
+
meta: {
|
|
1610
|
+
key: "crm.deal.viewList",
|
|
1611
|
+
version: "1.0.0",
|
|
1612
|
+
title: "Deal List",
|
|
1613
|
+
description: "List view of deals with value, status, and owner info",
|
|
1614
|
+
domain: "crm-pipeline",
|
|
1615
|
+
owners: ["@crm-team"],
|
|
1616
|
+
tags: ["deal", "list"],
|
|
1617
|
+
stability: StabilityEnum2.Experimental,
|
|
1618
|
+
goal: "Search, filter, and review deal lists.",
|
|
1619
|
+
context: "Standard view for deal management and bulk actions."
|
|
1620
|
+
},
|
|
1621
|
+
source: {
|
|
1622
|
+
type: "component",
|
|
1623
|
+
framework: "react",
|
|
1624
|
+
componentKey: "DealListView",
|
|
1625
|
+
props: DealModel
|
|
1626
|
+
},
|
|
1627
|
+
targets: ["react", "markdown", "application/json"],
|
|
1628
|
+
policy: {
|
|
1629
|
+
flags: ["crm.deals.enabled"]
|
|
1630
|
+
}
|
|
1631
|
+
});
|
|
1632
|
+
var DealDetailPresentation = definePresentation2({
|
|
1633
|
+
meta: {
|
|
1634
|
+
key: "crm.deal.detail",
|
|
1635
|
+
version: "1.0.0",
|
|
1636
|
+
title: "Deal Details",
|
|
1637
|
+
description: "Detailed view of a deal with activities, contacts, and history",
|
|
1638
|
+
domain: "crm-pipeline",
|
|
1639
|
+
owners: ["@crm-team"],
|
|
1640
|
+
tags: ["deal", "detail"],
|
|
1641
|
+
stability: StabilityEnum2.Experimental,
|
|
1642
|
+
goal: "Deep dive into deal details and historical activities.",
|
|
1643
|
+
context: "The main workspace for managing a single deal execution."
|
|
1644
|
+
},
|
|
1645
|
+
source: {
|
|
1646
|
+
type: "component",
|
|
1647
|
+
framework: "react",
|
|
1648
|
+
componentKey: "DealDetailView"
|
|
1649
|
+
},
|
|
1650
|
+
targets: ["react", "markdown"],
|
|
1651
|
+
policy: {
|
|
1652
|
+
flags: ["crm.deals.enabled"]
|
|
1653
|
+
}
|
|
1654
|
+
});
|
|
1655
|
+
var DealCardPresentation = definePresentation2({
|
|
1656
|
+
meta: {
|
|
1657
|
+
key: "crm.deal.card",
|
|
1658
|
+
version: "1.0.0",
|
|
1659
|
+
title: "Deal Card",
|
|
1660
|
+
description: "Compact deal card for kanban board display",
|
|
1661
|
+
domain: "crm-pipeline",
|
|
1662
|
+
owners: ["@crm-team"],
|
|
1663
|
+
tags: ["deal", "card", "kanban"],
|
|
1664
|
+
stability: StabilityEnum2.Experimental,
|
|
1665
|
+
goal: "Provide a quick overview of deal status in the pipeline view.",
|
|
1666
|
+
context: "Condensed representation used within the Pipeline Kanban board."
|
|
1667
|
+
},
|
|
1668
|
+
source: {
|
|
1669
|
+
type: "component",
|
|
1670
|
+
framework: "react",
|
|
1671
|
+
componentKey: "DealCard",
|
|
1672
|
+
props: DealModel
|
|
1673
|
+
},
|
|
1674
|
+
targets: ["react"],
|
|
1675
|
+
policy: {
|
|
1676
|
+
flags: ["crm.deals.enabled"]
|
|
1677
|
+
}
|
|
1678
|
+
});
|
|
1679
|
+
// src/ui/hooks/useDealList.ts
|
|
1680
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
1681
|
+
import { useTemplateRuntime } from "@contractspec/lib.example-shared-ui";
|
|
1682
|
+
"use client";
|
|
1683
|
+
function useDealList(options = {}) {
|
|
1684
|
+
const { handlers, projectId } = useTemplateRuntime();
|
|
1685
|
+
const { crm: crm2 } = handlers;
|
|
1686
|
+
const [data, setData] = useState(null);
|
|
1687
|
+
const [dealsByStage, setDealsByStage] = useState({});
|
|
1688
|
+
const [stages, setStages] = useState([]);
|
|
1689
|
+
const [loading, setLoading] = useState(true);
|
|
1690
|
+
const [error, setError] = useState(null);
|
|
1691
|
+
const [page, setPage] = useState(1);
|
|
1692
|
+
const pipelineId = options.pipelineId ?? "pipeline-1";
|
|
1693
|
+
const fetchData = useCallback(async () => {
|
|
1694
|
+
setLoading(true);
|
|
1695
|
+
setError(null);
|
|
1696
|
+
try {
|
|
1697
|
+
const [dealsResult, stageDealsResult, stagesResult] = await Promise.all([
|
|
1698
|
+
crm2.listDeals({
|
|
1699
|
+
projectId,
|
|
1700
|
+
pipelineId,
|
|
1701
|
+
stageId: options.stageId,
|
|
1702
|
+
status: options.status === "all" ? undefined : options.status,
|
|
1703
|
+
search: options.search,
|
|
1704
|
+
limit: options.limit ?? 50,
|
|
1705
|
+
offset: (page - 1) * (options.limit ?? 50)
|
|
1706
|
+
}),
|
|
1707
|
+
crm2.getDealsByStage({ projectId, pipelineId }),
|
|
1708
|
+
crm2.getPipelineStages({ pipelineId })
|
|
1709
|
+
]);
|
|
1710
|
+
setData(dealsResult);
|
|
1711
|
+
setDealsByStage(stageDealsResult);
|
|
1712
|
+
setStages(stagesResult);
|
|
1713
|
+
} catch (err) {
|
|
1714
|
+
setError(err instanceof Error ? err : new Error("Unknown error"));
|
|
1715
|
+
} finally {
|
|
1716
|
+
setLoading(false);
|
|
1717
|
+
}
|
|
1718
|
+
}, [
|
|
1719
|
+
crm2,
|
|
1720
|
+
projectId,
|
|
1721
|
+
pipelineId,
|
|
1722
|
+
options.stageId,
|
|
1723
|
+
options.status,
|
|
1724
|
+
options.search,
|
|
1725
|
+
options.limit,
|
|
1726
|
+
page
|
|
1727
|
+
]);
|
|
1728
|
+
useEffect(() => {
|
|
1729
|
+
fetchData();
|
|
1730
|
+
}, [fetchData]);
|
|
1731
|
+
const stats = useMemo(() => {
|
|
1732
|
+
if (!data)
|
|
1733
|
+
return null;
|
|
1734
|
+
const open = data.deals.filter((d) => d.status === "OPEN");
|
|
1735
|
+
const won = data.deals.filter((d) => d.status === "WON");
|
|
1736
|
+
const lost = data.deals.filter((d) => d.status === "LOST");
|
|
1737
|
+
return {
|
|
1738
|
+
total: data.total,
|
|
1739
|
+
totalValue: data.totalValue,
|
|
1740
|
+
openCount: open.length,
|
|
1741
|
+
openValue: open.reduce((sum, d) => sum + d.value, 0),
|
|
1742
|
+
wonCount: won.length,
|
|
1743
|
+
wonValue: won.reduce((sum, d) => sum + d.value, 0),
|
|
1744
|
+
lostCount: lost.length
|
|
1745
|
+
};
|
|
1746
|
+
}, [data]);
|
|
1747
|
+
return {
|
|
1748
|
+
data,
|
|
1749
|
+
dealsByStage,
|
|
1750
|
+
stages,
|
|
1751
|
+
loading,
|
|
1752
|
+
error,
|
|
1753
|
+
stats,
|
|
1754
|
+
page,
|
|
1755
|
+
refetch: fetchData,
|
|
1756
|
+
nextPage: () => setPage((p) => p + 1),
|
|
1757
|
+
prevPage: () => page > 1 && setPage((p) => p - 1)
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
// src/ui/hooks/useDealMutations.ts
|
|
1762
|
+
import { useCallback as useCallback2, useState as useState2 } from "react";
|
|
1763
|
+
import { useTemplateRuntime as useTemplateRuntime2 } from "@contractspec/lib.example-shared-ui";
|
|
1764
|
+
function useDealMutations(options = {}) {
|
|
1765
|
+
const { handlers, projectId } = useTemplateRuntime2();
|
|
1766
|
+
const { crm: crm2 } = handlers;
|
|
1767
|
+
const [createState, setCreateState] = useState2({
|
|
1768
|
+
loading: false,
|
|
1769
|
+
error: null,
|
|
1770
|
+
data: null
|
|
1771
|
+
});
|
|
1772
|
+
const [moveState, setMoveState] = useState2({
|
|
1773
|
+
loading: false,
|
|
1774
|
+
error: null,
|
|
1775
|
+
data: null
|
|
1776
|
+
});
|
|
1777
|
+
const [winState, setWinState] = useState2({
|
|
1778
|
+
loading: false,
|
|
1779
|
+
error: null,
|
|
1780
|
+
data: null
|
|
1781
|
+
});
|
|
1782
|
+
const [loseState, setLoseState] = useState2({
|
|
1783
|
+
loading: false,
|
|
1784
|
+
error: null,
|
|
1785
|
+
data: null
|
|
1786
|
+
});
|
|
1787
|
+
const createDeal = useCallback2(async (input) => {
|
|
1788
|
+
setCreateState({ loading: true, error: null, data: null });
|
|
1789
|
+
try {
|
|
1790
|
+
const result = await crm2.createDeal(input, {
|
|
1791
|
+
projectId,
|
|
1792
|
+
ownerId: "user-1"
|
|
1793
|
+
});
|
|
1794
|
+
setCreateState({ loading: false, error: null, data: result });
|
|
1795
|
+
options.onSuccess?.();
|
|
1796
|
+
return result;
|
|
1797
|
+
} catch (err) {
|
|
1798
|
+
const error = err instanceof Error ? err : new Error("Failed to create deal");
|
|
1799
|
+
setCreateState({ loading: false, error, data: null });
|
|
1800
|
+
options.onError?.(error);
|
|
1801
|
+
return null;
|
|
1802
|
+
}
|
|
1803
|
+
}, [crm2, projectId, options]);
|
|
1804
|
+
const moveDeal = useCallback2(async (input) => {
|
|
1805
|
+
setMoveState({ loading: true, error: null, data: null });
|
|
1806
|
+
try {
|
|
1807
|
+
const result = await crm2.moveDeal(input);
|
|
1808
|
+
setMoveState({ loading: false, error: null, data: result });
|
|
1809
|
+
options.onSuccess?.();
|
|
1810
|
+
return result;
|
|
1811
|
+
} catch (err) {
|
|
1812
|
+
const error = err instanceof Error ? err : new Error("Failed to move deal");
|
|
1813
|
+
setMoveState({ loading: false, error, data: null });
|
|
1814
|
+
options.onError?.(error);
|
|
1815
|
+
return null;
|
|
1816
|
+
}
|
|
1817
|
+
}, [crm2, options]);
|
|
1818
|
+
const winDeal = useCallback2(async (input) => {
|
|
1819
|
+
setWinState({ loading: true, error: null, data: null });
|
|
1820
|
+
try {
|
|
1821
|
+
const result = await crm2.winDeal(input);
|
|
1822
|
+
setWinState({ loading: false, error: null, data: result });
|
|
1823
|
+
options.onSuccess?.();
|
|
1824
|
+
return result;
|
|
1825
|
+
} catch (err) {
|
|
1826
|
+
const error = err instanceof Error ? err : new Error("Failed to mark deal as won");
|
|
1827
|
+
setWinState({ loading: false, error, data: null });
|
|
1828
|
+
options.onError?.(error);
|
|
1829
|
+
return null;
|
|
1830
|
+
}
|
|
1831
|
+
}, [crm2, options]);
|
|
1832
|
+
const loseDeal = useCallback2(async (input) => {
|
|
1833
|
+
setLoseState({ loading: true, error: null, data: null });
|
|
1834
|
+
try {
|
|
1835
|
+
const result = await crm2.loseDeal(input);
|
|
1836
|
+
setLoseState({ loading: false, error: null, data: result });
|
|
1837
|
+
options.onSuccess?.();
|
|
1838
|
+
return result;
|
|
1839
|
+
} catch (err) {
|
|
1840
|
+
const error = err instanceof Error ? err : new Error("Failed to mark deal as lost");
|
|
1841
|
+
setLoseState({ loading: false, error, data: null });
|
|
1842
|
+
options.onError?.(error);
|
|
1843
|
+
return null;
|
|
1844
|
+
}
|
|
1845
|
+
}, [crm2, options]);
|
|
1846
|
+
return {
|
|
1847
|
+
createDeal,
|
|
1848
|
+
moveDeal,
|
|
1849
|
+
winDeal,
|
|
1850
|
+
loseDeal,
|
|
1851
|
+
createState,
|
|
1852
|
+
moveState,
|
|
1853
|
+
winState,
|
|
1854
|
+
loseState,
|
|
1855
|
+
isLoading: createState.loading || moveState.loading || winState.loading || loseState.loading
|
|
1856
|
+
};
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
// src/ui/CrmDealCard.tsx
|
|
1860
|
+
import { jsxDEV } from "react/jsx-dev-runtime";
|
|
1861
|
+
"use client";
|
|
1862
|
+
function formatCurrency(value, currency) {
|
|
1863
|
+
return new Intl.NumberFormat("en-US", {
|
|
1864
|
+
style: "currency",
|
|
1865
|
+
currency,
|
|
1866
|
+
minimumFractionDigits: 0,
|
|
1867
|
+
maximumFractionDigits: 0
|
|
1868
|
+
}).format(value);
|
|
1869
|
+
}
|
|
1870
|
+
function CrmDealCard({ deal: deal3, onClick }) {
|
|
1871
|
+
const daysUntilClose = deal3.expectedCloseDate ? Math.ceil((deal3.expectedCloseDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24)) : null;
|
|
1872
|
+
return /* @__PURE__ */ jsxDEV("div", {
|
|
1873
|
+
onClick,
|
|
1874
|
+
className: "border-border bg-card cursor-pointer rounded-lg border p-3 shadow-sm transition-shadow hover:shadow-md",
|
|
1875
|
+
role: "button",
|
|
1876
|
+
tabIndex: 0,
|
|
1877
|
+
onKeyDown: (e) => {
|
|
1878
|
+
if (e.key === "Enter" || e.key === " ")
|
|
1879
|
+
onClick?.();
|
|
1880
|
+
},
|
|
1881
|
+
children: [
|
|
1882
|
+
/* @__PURE__ */ jsxDEV("h4", {
|
|
1883
|
+
className: "leading-snug font-medium",
|
|
1884
|
+
children: deal3.name
|
|
1885
|
+
}, undefined, false, undefined, this),
|
|
1886
|
+
/* @__PURE__ */ jsxDEV("div", {
|
|
1887
|
+
className: "text-primary mt-2 text-lg font-semibold",
|
|
1888
|
+
children: formatCurrency(deal3.value, deal3.currency)
|
|
1889
|
+
}, undefined, false, undefined, this),
|
|
1890
|
+
/* @__PURE__ */ jsxDEV("div", {
|
|
1891
|
+
className: "text-muted-foreground mt-3 flex items-center justify-between text-xs",
|
|
1892
|
+
children: [
|
|
1893
|
+
daysUntilClose !== null && /* @__PURE__ */ jsxDEV("span", {
|
|
1894
|
+
className: daysUntilClose < 0 ? "text-red-500" : daysUntilClose <= 7 ? "text-yellow-600 dark:text-yellow-500" : "",
|
|
1895
|
+
children: daysUntilClose < 0 ? `${Math.abs(daysUntilClose)}d overdue` : daysUntilClose === 0 ? "Due today" : `${daysUntilClose}d left`
|
|
1896
|
+
}, undefined, false, undefined, this),
|
|
1897
|
+
/* @__PURE__ */ jsxDEV("span", {
|
|
1898
|
+
className: `rounded px-1.5 py-0.5 text-xs font-medium ${deal3.status === "WON" ? "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400" : deal3.status === "LOST" ? "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400" : "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"}`,
|
|
1899
|
+
children: deal3.status
|
|
1900
|
+
}, undefined, false, undefined, this)
|
|
1901
|
+
]
|
|
1902
|
+
}, undefined, true, undefined, this)
|
|
1903
|
+
]
|
|
1904
|
+
}, undefined, true, undefined, this);
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
// src/ui/CrmPipelineBoard.tsx
|
|
1908
|
+
import { useState as useState3 } from "react";
|
|
1909
|
+
import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
|
|
1910
|
+
"use client";
|
|
1911
|
+
function formatCurrency2(value) {
|
|
1912
|
+
if (value >= 1e6)
|
|
1913
|
+
return `$${(value / 1e6).toFixed(1)}M`;
|
|
1914
|
+
if (value >= 1000)
|
|
1915
|
+
return `$${(value / 1000).toFixed(0)}K`;
|
|
1916
|
+
return `$${value}`;
|
|
1917
|
+
}
|
|
1918
|
+
function CrmPipelineBoard({
|
|
1919
|
+
dealsByStage,
|
|
1920
|
+
stages,
|
|
1921
|
+
onDealClick,
|
|
1922
|
+
onDealMove
|
|
1923
|
+
}) {
|
|
1924
|
+
const [quickMoveOpen, setQuickMoveOpen] = useState3(null);
|
|
1925
|
+
const sortedStages = [...stages].sort((a, b) => a.position - b.position);
|
|
1926
|
+
const handleQuickMove = (dealId, toStageId) => {
|
|
1927
|
+
onDealMove?.(dealId, toStageId);
|
|
1928
|
+
setQuickMoveOpen(null);
|
|
1929
|
+
};
|
|
1930
|
+
return /* @__PURE__ */ jsxDEV2("div", {
|
|
1931
|
+
className: "flex gap-4 overflow-x-auto pb-4",
|
|
1932
|
+
children: sortedStages.map((stage) => {
|
|
1933
|
+
const deals = dealsByStage[stage.id] ?? [];
|
|
1934
|
+
const stageValue = deals.reduce((sum, d) => sum + d.value, 0);
|
|
1935
|
+
return /* @__PURE__ */ jsxDEV2("div", {
|
|
1936
|
+
className: "bg-muted/30 flex w-72 flex-shrink-0 flex-col rounded-lg",
|
|
1937
|
+
children: [
|
|
1938
|
+
/* @__PURE__ */ jsxDEV2("div", {
|
|
1939
|
+
className: "border-border flex items-center justify-between border-b px-3 py-2",
|
|
1940
|
+
children: [
|
|
1941
|
+
/* @__PURE__ */ jsxDEV2("div", {
|
|
1942
|
+
children: [
|
|
1943
|
+
/* @__PURE__ */ jsxDEV2("h3", {
|
|
1944
|
+
className: "font-medium",
|
|
1945
|
+
children: stage.name
|
|
1946
|
+
}, undefined, false, undefined, this),
|
|
1947
|
+
/* @__PURE__ */ jsxDEV2("p", {
|
|
1948
|
+
className: "text-muted-foreground text-xs",
|
|
1949
|
+
children: [
|
|
1950
|
+
deals.length,
|
|
1951
|
+
" deals · ",
|
|
1952
|
+
formatCurrency2(stageValue)
|
|
1953
|
+
]
|
|
1954
|
+
}, undefined, true, undefined, this)
|
|
1955
|
+
]
|
|
1956
|
+
}, undefined, true, undefined, this),
|
|
1957
|
+
/* @__PURE__ */ jsxDEV2("span", {
|
|
1958
|
+
className: "bg-muted flex h-6 w-6 items-center justify-center rounded-full text-xs font-medium",
|
|
1959
|
+
children: deals.length
|
|
1960
|
+
}, undefined, false, undefined, this)
|
|
1961
|
+
]
|
|
1962
|
+
}, undefined, true, undefined, this),
|
|
1963
|
+
/* @__PURE__ */ jsxDEV2("div", {
|
|
1964
|
+
className: "flex flex-1 flex-col gap-2 p-2",
|
|
1965
|
+
children: deals.length === 0 ? /* @__PURE__ */ jsxDEV2("div", {
|
|
1966
|
+
className: "border-muted-foreground/20 text-muted-foreground flex h-24 items-center justify-center rounded-md border-2 border-dashed text-xs",
|
|
1967
|
+
children: "No deals"
|
|
1968
|
+
}, undefined, false, undefined, this) : deals.map((deal3) => /* @__PURE__ */ jsxDEV2("div", {
|
|
1969
|
+
className: "group relative",
|
|
1970
|
+
children: [
|
|
1971
|
+
/* @__PURE__ */ jsxDEV2(CrmDealCard, {
|
|
1972
|
+
deal: deal3,
|
|
1973
|
+
onClick: () => onDealClick?.(deal3.id)
|
|
1974
|
+
}, undefined, false, undefined, this),
|
|
1975
|
+
deal3.status === "OPEN" && onDealMove && /* @__PURE__ */ jsxDEV2("div", {
|
|
1976
|
+
className: "absolute top-1 right-1 opacity-0 transition-opacity group-hover:opacity-100",
|
|
1977
|
+
children: [
|
|
1978
|
+
/* @__PURE__ */ jsxDEV2("button", {
|
|
1979
|
+
type: "button",
|
|
1980
|
+
onClick: (e) => {
|
|
1981
|
+
e.stopPropagation();
|
|
1982
|
+
setQuickMoveOpen(quickMoveOpen === deal3.id ? null : deal3.id);
|
|
1983
|
+
},
|
|
1984
|
+
className: "bg-background border-border hover:bg-muted flex h-6 w-6 items-center justify-center rounded border text-xs shadow-sm",
|
|
1985
|
+
title: "Quick move",
|
|
1986
|
+
children: "➡️"
|
|
1987
|
+
}, undefined, false, undefined, this),
|
|
1988
|
+
quickMoveOpen === deal3.id && /* @__PURE__ */ jsxDEV2("div", {
|
|
1989
|
+
className: "bg-card border-border absolute top-7 right-0 z-20 min-w-[140px] rounded-lg border py-1 shadow-lg",
|
|
1990
|
+
children: [
|
|
1991
|
+
/* @__PURE__ */ jsxDEV2("p", {
|
|
1992
|
+
className: "text-muted-foreground px-3 py-1 text-xs font-medium",
|
|
1993
|
+
children: "Move to:"
|
|
1994
|
+
}, undefined, false, undefined, this),
|
|
1995
|
+
sortedStages.filter((s) => s.id !== deal3.stageId).map((s) => /* @__PURE__ */ jsxDEV2("button", {
|
|
1996
|
+
type: "button",
|
|
1997
|
+
onClick: (e) => {
|
|
1998
|
+
e.stopPropagation();
|
|
1999
|
+
handleQuickMove(deal3.id, s.id);
|
|
2000
|
+
},
|
|
2001
|
+
className: "hover:bg-muted w-full px-3 py-1.5 text-left text-sm",
|
|
2002
|
+
children: s.name
|
|
2003
|
+
}, s.id, false, undefined, this))
|
|
2004
|
+
]
|
|
2005
|
+
}, undefined, true, undefined, this)
|
|
2006
|
+
]
|
|
2007
|
+
}, undefined, true, undefined, this)
|
|
2008
|
+
]
|
|
2009
|
+
}, deal3.id, true, undefined, this))
|
|
2010
|
+
}, undefined, false, undefined, this)
|
|
2011
|
+
]
|
|
2012
|
+
}, stage.id, true, undefined, this);
|
|
2013
|
+
})
|
|
2014
|
+
}, undefined, false, undefined, this);
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
// src/ui/modals/CreateDealModal.tsx
|
|
2018
|
+
import { useState as useState4 } from "react";
|
|
2019
|
+
import { Button, Input } from "@contractspec/lib.design-system";
|
|
2020
|
+
import { jsxDEV as jsxDEV3 } from "react/jsx-dev-runtime";
|
|
2021
|
+
"use client";
|
|
2022
|
+
var CURRENCIES = ["USD", "EUR", "GBP", "CAD"];
|
|
2023
|
+
var DEFAULT_PIPELINE_ID = "pipeline-1";
|
|
2024
|
+
function CreateDealModal({
|
|
2025
|
+
isOpen,
|
|
2026
|
+
onClose,
|
|
2027
|
+
onSubmit,
|
|
2028
|
+
stages,
|
|
2029
|
+
isLoading = false
|
|
2030
|
+
}) {
|
|
2031
|
+
const [name, setName] = useState4("");
|
|
2032
|
+
const [value, setValue] = useState4("");
|
|
2033
|
+
const [currency, setCurrency] = useState4("USD");
|
|
2034
|
+
const [stageId, setStageId] = useState4(stages[0]?.id ?? "");
|
|
2035
|
+
const [expectedCloseDate, setExpectedCloseDate] = useState4("");
|
|
2036
|
+
const [error, setError] = useState4(null);
|
|
2037
|
+
const handleSubmit = async (e) => {
|
|
2038
|
+
e.preventDefault();
|
|
2039
|
+
setError(null);
|
|
2040
|
+
if (!name.trim()) {
|
|
2041
|
+
setError("Deal name is required");
|
|
2042
|
+
return;
|
|
2043
|
+
}
|
|
2044
|
+
const numericValue = parseFloat(value);
|
|
2045
|
+
if (isNaN(numericValue) || numericValue <= 0) {
|
|
2046
|
+
setError("Value must be a positive number");
|
|
2047
|
+
return;
|
|
2048
|
+
}
|
|
2049
|
+
if (!stageId) {
|
|
2050
|
+
setError("Please select a pipeline stage");
|
|
2051
|
+
return;
|
|
2052
|
+
}
|
|
2053
|
+
try {
|
|
2054
|
+
await onSubmit({
|
|
2055
|
+
name: name.trim(),
|
|
2056
|
+
value: numericValue,
|
|
2057
|
+
currency,
|
|
2058
|
+
pipelineId: DEFAULT_PIPELINE_ID,
|
|
2059
|
+
stageId,
|
|
2060
|
+
expectedCloseDate: expectedCloseDate ? new Date(expectedCloseDate) : undefined
|
|
2061
|
+
});
|
|
2062
|
+
setName("");
|
|
2063
|
+
setValue("");
|
|
2064
|
+
setCurrency("USD");
|
|
2065
|
+
setStageId(stages[0]?.id ?? "");
|
|
2066
|
+
setExpectedCloseDate("");
|
|
2067
|
+
onClose();
|
|
2068
|
+
} catch (err) {
|
|
2069
|
+
setError(err instanceof Error ? err.message : "Failed to create deal");
|
|
2070
|
+
}
|
|
2071
|
+
};
|
|
2072
|
+
if (!isOpen)
|
|
2073
|
+
return null;
|
|
2074
|
+
return /* @__PURE__ */ jsxDEV3("div", {
|
|
2075
|
+
className: "fixed inset-0 z-50 flex items-center justify-center",
|
|
2076
|
+
children: [
|
|
2077
|
+
/* @__PURE__ */ jsxDEV3("div", {
|
|
2078
|
+
className: "bg-background/80 absolute inset-0 backdrop-blur-sm",
|
|
2079
|
+
onClick: onClose,
|
|
2080
|
+
role: "button",
|
|
2081
|
+
tabIndex: 0,
|
|
2082
|
+
onKeyDown: (e) => {
|
|
2083
|
+
if (e.key === "Enter" || e.key === " ")
|
|
2084
|
+
onClose();
|
|
2085
|
+
},
|
|
2086
|
+
"aria-label": "Close modal"
|
|
2087
|
+
}, undefined, false, undefined, this),
|
|
2088
|
+
/* @__PURE__ */ jsxDEV3("div", {
|
|
2089
|
+
className: "bg-card border-border relative z-10 w-full max-w-md rounded-xl border p-6 shadow-xl",
|
|
2090
|
+
children: [
|
|
2091
|
+
/* @__PURE__ */ jsxDEV3("h2", {
|
|
2092
|
+
className: "mb-4 text-xl font-semibold",
|
|
2093
|
+
children: "Create New Deal"
|
|
2094
|
+
}, undefined, false, undefined, this),
|
|
2095
|
+
/* @__PURE__ */ jsxDEV3("form", {
|
|
2096
|
+
onSubmit: handleSubmit,
|
|
2097
|
+
className: "space-y-4",
|
|
2098
|
+
children: [
|
|
2099
|
+
/* @__PURE__ */ jsxDEV3("div", {
|
|
2100
|
+
children: [
|
|
2101
|
+
/* @__PURE__ */ jsxDEV3("label", {
|
|
2102
|
+
htmlFor: "deal-name",
|
|
2103
|
+
className: "text-muted-foreground mb-1 block text-sm font-medium",
|
|
2104
|
+
children: "Deal Name *"
|
|
2105
|
+
}, undefined, false, undefined, this),
|
|
2106
|
+
/* @__PURE__ */ jsxDEV3(Input, {
|
|
2107
|
+
id: "deal-name",
|
|
2108
|
+
value: name,
|
|
2109
|
+
onChange: (e) => setName(e.target.value),
|
|
2110
|
+
placeholder: "e.g., Enterprise License - Acme Corp",
|
|
2111
|
+
disabled: isLoading
|
|
2112
|
+
}, undefined, false, undefined, this)
|
|
2113
|
+
]
|
|
2114
|
+
}, undefined, true, undefined, this),
|
|
2115
|
+
/* @__PURE__ */ jsxDEV3("div", {
|
|
2116
|
+
className: "flex gap-3",
|
|
2117
|
+
children: [
|
|
2118
|
+
/* @__PURE__ */ jsxDEV3("div", {
|
|
2119
|
+
className: "flex-1",
|
|
2120
|
+
children: [
|
|
2121
|
+
/* @__PURE__ */ jsxDEV3("label", {
|
|
2122
|
+
htmlFor: "deal-value",
|
|
2123
|
+
className: "text-muted-foreground mb-1 block text-sm font-medium",
|
|
2124
|
+
children: "Value *"
|
|
2125
|
+
}, undefined, false, undefined, this),
|
|
2126
|
+
/* @__PURE__ */ jsxDEV3(Input, {
|
|
2127
|
+
id: "deal-value",
|
|
2128
|
+
type: "number",
|
|
2129
|
+
min: "0",
|
|
2130
|
+
step: "0.01",
|
|
2131
|
+
value,
|
|
2132
|
+
onChange: (e) => setValue(e.target.value),
|
|
2133
|
+
placeholder: "50000",
|
|
2134
|
+
disabled: isLoading
|
|
2135
|
+
}, undefined, false, undefined, this)
|
|
2136
|
+
]
|
|
2137
|
+
}, undefined, true, undefined, this),
|
|
2138
|
+
/* @__PURE__ */ jsxDEV3("div", {
|
|
2139
|
+
className: "w-24",
|
|
2140
|
+
children: [
|
|
2141
|
+
/* @__PURE__ */ jsxDEV3("label", {
|
|
2142
|
+
htmlFor: "deal-currency",
|
|
2143
|
+
className: "text-muted-foreground mb-1 block text-sm font-medium",
|
|
2144
|
+
children: "Currency"
|
|
2145
|
+
}, undefined, false, undefined, this),
|
|
2146
|
+
/* @__PURE__ */ jsxDEV3("select", {
|
|
2147
|
+
id: "deal-currency",
|
|
2148
|
+
value: currency,
|
|
2149
|
+
onChange: (e) => setCurrency(e.target.value),
|
|
2150
|
+
disabled: isLoading,
|
|
2151
|
+
className: "border-input bg-background focus:ring-ring h-10 w-full rounded-md border px-3 py-2 text-sm focus:ring-2 focus:outline-none disabled:opacity-50",
|
|
2152
|
+
children: CURRENCIES.map((c) => /* @__PURE__ */ jsxDEV3("option", {
|
|
2153
|
+
value: c,
|
|
2154
|
+
children: c
|
|
2155
|
+
}, c, false, undefined, this))
|
|
2156
|
+
}, undefined, false, undefined, this)
|
|
2157
|
+
]
|
|
2158
|
+
}, undefined, true, undefined, this)
|
|
2159
|
+
]
|
|
2160
|
+
}, undefined, true, undefined, this),
|
|
2161
|
+
/* @__PURE__ */ jsxDEV3("div", {
|
|
2162
|
+
children: [
|
|
2163
|
+
/* @__PURE__ */ jsxDEV3("label", {
|
|
2164
|
+
htmlFor: "deal-stage",
|
|
2165
|
+
className: "text-muted-foreground mb-1 block text-sm font-medium",
|
|
2166
|
+
children: "Pipeline Stage *"
|
|
2167
|
+
}, undefined, false, undefined, this),
|
|
2168
|
+
/* @__PURE__ */ jsxDEV3("select", {
|
|
2169
|
+
id: "deal-stage",
|
|
2170
|
+
value: stageId,
|
|
2171
|
+
onChange: (e) => setStageId(e.target.value),
|
|
2172
|
+
disabled: isLoading,
|
|
2173
|
+
className: "border-input bg-background focus:ring-ring h-10 w-full rounded-md border px-3 py-2 text-sm focus:ring-2 focus:outline-none disabled:opacity-50",
|
|
2174
|
+
children: stages.map((stage) => /* @__PURE__ */ jsxDEV3("option", {
|
|
2175
|
+
value: stage.id,
|
|
2176
|
+
children: stage.name
|
|
2177
|
+
}, stage.id, false, undefined, this))
|
|
2178
|
+
}, undefined, false, undefined, this)
|
|
2179
|
+
]
|
|
2180
|
+
}, undefined, true, undefined, this),
|
|
2181
|
+
/* @__PURE__ */ jsxDEV3("div", {
|
|
2182
|
+
children: [
|
|
2183
|
+
/* @__PURE__ */ jsxDEV3("label", {
|
|
2184
|
+
htmlFor: "deal-close-date",
|
|
2185
|
+
className: "text-muted-foreground mb-1 block text-sm font-medium",
|
|
2186
|
+
children: "Expected Close Date"
|
|
2187
|
+
}, undefined, false, undefined, this),
|
|
2188
|
+
/* @__PURE__ */ jsxDEV3(Input, {
|
|
2189
|
+
id: "deal-close-date",
|
|
2190
|
+
type: "date",
|
|
2191
|
+
value: expectedCloseDate,
|
|
2192
|
+
onChange: (e) => setExpectedCloseDate(e.target.value),
|
|
2193
|
+
disabled: isLoading
|
|
2194
|
+
}, undefined, false, undefined, this)
|
|
2195
|
+
]
|
|
2196
|
+
}, undefined, true, undefined, this),
|
|
2197
|
+
error && /* @__PURE__ */ jsxDEV3("div", {
|
|
2198
|
+
className: "bg-destructive/10 text-destructive rounded-md p-3 text-sm",
|
|
2199
|
+
children: error
|
|
2200
|
+
}, undefined, false, undefined, this),
|
|
2201
|
+
/* @__PURE__ */ jsxDEV3("div", {
|
|
2202
|
+
className: "flex justify-end gap-3 pt-2",
|
|
2203
|
+
children: [
|
|
2204
|
+
/* @__PURE__ */ jsxDEV3(Button, {
|
|
2205
|
+
type: "button",
|
|
2206
|
+
variant: "ghost",
|
|
2207
|
+
onPress: onClose,
|
|
2208
|
+
disabled: isLoading,
|
|
2209
|
+
children: "Cancel"
|
|
2210
|
+
}, undefined, false, undefined, this),
|
|
2211
|
+
/* @__PURE__ */ jsxDEV3(Button, {
|
|
2212
|
+
type: "submit",
|
|
2213
|
+
disabled: isLoading,
|
|
2214
|
+
children: isLoading ? "Creating..." : "Create Deal"
|
|
2215
|
+
}, undefined, false, undefined, this)
|
|
2216
|
+
]
|
|
2217
|
+
}, undefined, true, undefined, this)
|
|
2218
|
+
]
|
|
2219
|
+
}, undefined, true, undefined, this)
|
|
2220
|
+
]
|
|
2221
|
+
}, undefined, true, undefined, this)
|
|
2222
|
+
]
|
|
2223
|
+
}, undefined, true, undefined, this);
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
// src/ui/modals/DealActionsModal.tsx
|
|
2227
|
+
import { useState as useState5 } from "react";
|
|
2228
|
+
import { Button as Button2 } from "@contractspec/lib.design-system";
|
|
2229
|
+
import { jsxDEV as jsxDEV4, Fragment } from "react/jsx-dev-runtime";
|
|
2230
|
+
"use client";
|
|
2231
|
+
function formatCurrency3(value, currency) {
|
|
2232
|
+
return new Intl.NumberFormat("en-US", {
|
|
2233
|
+
style: "currency",
|
|
2234
|
+
currency,
|
|
2235
|
+
minimumFractionDigits: 0,
|
|
2236
|
+
maximumFractionDigits: 0
|
|
2237
|
+
}).format(value);
|
|
2238
|
+
}
|
|
2239
|
+
function DealActionsModal({
|
|
2240
|
+
isOpen,
|
|
2241
|
+
deal: deal3,
|
|
2242
|
+
stages,
|
|
2243
|
+
onClose,
|
|
2244
|
+
onWin,
|
|
2245
|
+
onLose,
|
|
2246
|
+
onMove,
|
|
2247
|
+
isLoading = false
|
|
2248
|
+
}) {
|
|
2249
|
+
const [mode, setMode] = useState5("menu");
|
|
2250
|
+
const [wonSource, setWonSource] = useState5("");
|
|
2251
|
+
const [lostReason, setLostReason] = useState5("");
|
|
2252
|
+
const [notes, setNotes] = useState5("");
|
|
2253
|
+
const [selectedStageId, setSelectedStageId] = useState5("");
|
|
2254
|
+
const [error, setError] = useState5(null);
|
|
2255
|
+
const resetForm = () => {
|
|
2256
|
+
setMode("menu");
|
|
2257
|
+
setWonSource("");
|
|
2258
|
+
setLostReason("");
|
|
2259
|
+
setNotes("");
|
|
2260
|
+
setSelectedStageId("");
|
|
2261
|
+
setError(null);
|
|
2262
|
+
};
|
|
2263
|
+
const handleClose = () => {
|
|
2264
|
+
resetForm();
|
|
2265
|
+
onClose();
|
|
2266
|
+
};
|
|
2267
|
+
const handleWin = async () => {
|
|
2268
|
+
if (!deal3)
|
|
2269
|
+
return;
|
|
2270
|
+
setError(null);
|
|
2271
|
+
try {
|
|
2272
|
+
await onWin({
|
|
2273
|
+
dealId: deal3.id,
|
|
2274
|
+
wonSource: wonSource.trim() || undefined,
|
|
2275
|
+
notes: notes.trim() || undefined
|
|
2276
|
+
});
|
|
2277
|
+
handleClose();
|
|
2278
|
+
} catch (err) {
|
|
2279
|
+
setError(err instanceof Error ? err.message : "Failed to mark deal as won");
|
|
2280
|
+
}
|
|
2281
|
+
};
|
|
2282
|
+
const handleLose = async () => {
|
|
2283
|
+
if (!deal3)
|
|
2284
|
+
return;
|
|
2285
|
+
setError(null);
|
|
2286
|
+
if (!lostReason.trim()) {
|
|
2287
|
+
setError("Please provide a reason for losing the deal");
|
|
2288
|
+
return;
|
|
2289
|
+
}
|
|
2290
|
+
try {
|
|
2291
|
+
await onLose({
|
|
2292
|
+
dealId: deal3.id,
|
|
2293
|
+
lostReason: lostReason.trim(),
|
|
2294
|
+
notes: notes.trim() || undefined
|
|
2295
|
+
});
|
|
2296
|
+
handleClose();
|
|
2297
|
+
} catch (err) {
|
|
2298
|
+
setError(err instanceof Error ? err.message : "Failed to mark deal as lost");
|
|
2299
|
+
}
|
|
2300
|
+
};
|
|
2301
|
+
const handleMove = async () => {
|
|
2302
|
+
if (!deal3)
|
|
2303
|
+
return;
|
|
2304
|
+
setError(null);
|
|
2305
|
+
if (!selectedStageId) {
|
|
2306
|
+
setError("Please select a stage");
|
|
2307
|
+
return;
|
|
2308
|
+
}
|
|
2309
|
+
if (selectedStageId === deal3.stageId) {
|
|
2310
|
+
setError("Deal is already in this stage");
|
|
2311
|
+
return;
|
|
2312
|
+
}
|
|
2313
|
+
try {
|
|
2314
|
+
await onMove({
|
|
2315
|
+
dealId: deal3.id,
|
|
2316
|
+
stageId: selectedStageId
|
|
2317
|
+
});
|
|
2318
|
+
handleClose();
|
|
2319
|
+
} catch (err) {
|
|
2320
|
+
setError(err instanceof Error ? err.message : "Failed to move deal");
|
|
2321
|
+
}
|
|
2322
|
+
};
|
|
2323
|
+
if (!isOpen || !deal3)
|
|
2324
|
+
return null;
|
|
2325
|
+
return /* @__PURE__ */ jsxDEV4("div", {
|
|
2326
|
+
className: "fixed inset-0 z-50 flex items-center justify-center",
|
|
2327
|
+
children: [
|
|
2328
|
+
/* @__PURE__ */ jsxDEV4("div", {
|
|
2329
|
+
className: "bg-background/80 absolute inset-0 backdrop-blur-sm",
|
|
2330
|
+
onClick: handleClose,
|
|
2331
|
+
role: "button",
|
|
2332
|
+
tabIndex: 0,
|
|
2333
|
+
onKeyDown: (e) => {
|
|
2334
|
+
if (e.key === "Enter" || e.key === " ")
|
|
2335
|
+
handleClose();
|
|
2336
|
+
},
|
|
2337
|
+
"aria-label": "Close modal"
|
|
2338
|
+
}, undefined, false, undefined, this),
|
|
2339
|
+
/* @__PURE__ */ jsxDEV4("div", {
|
|
2340
|
+
className: "bg-card border-border relative z-10 w-full max-w-md rounded-xl border p-6 shadow-xl",
|
|
2341
|
+
children: [
|
|
2342
|
+
/* @__PURE__ */ jsxDEV4("div", {
|
|
2343
|
+
className: "border-border mb-4 border-b pb-4",
|
|
2344
|
+
children: [
|
|
2345
|
+
/* @__PURE__ */ jsxDEV4("h2", {
|
|
2346
|
+
className: "text-xl font-semibold",
|
|
2347
|
+
children: deal3.name
|
|
2348
|
+
}, undefined, false, undefined, this),
|
|
2349
|
+
/* @__PURE__ */ jsxDEV4("p", {
|
|
2350
|
+
className: "text-primary text-lg font-medium",
|
|
2351
|
+
children: formatCurrency3(deal3.value, deal3.currency)
|
|
2352
|
+
}, undefined, false, undefined, this),
|
|
2353
|
+
/* @__PURE__ */ jsxDEV4("span", {
|
|
2354
|
+
className: `mt-2 inline-flex rounded-full px-2 py-0.5 text-xs font-medium ${deal3.status === "WON" ? "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400" : deal3.status === "LOST" ? "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400" : "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"}`,
|
|
2355
|
+
children: deal3.status
|
|
2356
|
+
}, undefined, false, undefined, this)
|
|
2357
|
+
]
|
|
2358
|
+
}, undefined, true, undefined, this),
|
|
2359
|
+
mode === "menu" && /* @__PURE__ */ jsxDEV4("div", {
|
|
2360
|
+
className: "space-y-3",
|
|
2361
|
+
children: [
|
|
2362
|
+
deal3.status === "OPEN" && /* @__PURE__ */ jsxDEV4(Fragment, {
|
|
2363
|
+
children: [
|
|
2364
|
+
/* @__PURE__ */ jsxDEV4(Button2, {
|
|
2365
|
+
className: "w-full justify-start",
|
|
2366
|
+
variant: "ghost",
|
|
2367
|
+
onPress: () => setMode("win"),
|
|
2368
|
+
children: [
|
|
2369
|
+
/* @__PURE__ */ jsxDEV4("span", {
|
|
2370
|
+
className: "mr-2",
|
|
2371
|
+
children: "\uD83C\uDFC6"
|
|
2372
|
+
}, undefined, false, undefined, this),
|
|
2373
|
+
" Mark as Won"
|
|
2374
|
+
]
|
|
2375
|
+
}, undefined, true, undefined, this),
|
|
2376
|
+
/* @__PURE__ */ jsxDEV4(Button2, {
|
|
2377
|
+
className: "w-full justify-start",
|
|
2378
|
+
variant: "ghost",
|
|
2379
|
+
onPress: () => setMode("lose"),
|
|
2380
|
+
children: [
|
|
2381
|
+
/* @__PURE__ */ jsxDEV4("span", {
|
|
2382
|
+
className: "mr-2",
|
|
2383
|
+
children: "❌"
|
|
2384
|
+
}, undefined, false, undefined, this),
|
|
2385
|
+
" Mark as Lost"
|
|
2386
|
+
]
|
|
2387
|
+
}, undefined, true, undefined, this),
|
|
2388
|
+
/* @__PURE__ */ jsxDEV4(Button2, {
|
|
2389
|
+
className: "w-full justify-start",
|
|
2390
|
+
variant: "ghost",
|
|
2391
|
+
onPress: () => {
|
|
2392
|
+
setSelectedStageId(deal3.stageId);
|
|
2393
|
+
setMode("move");
|
|
2394
|
+
},
|
|
2395
|
+
children: [
|
|
2396
|
+
/* @__PURE__ */ jsxDEV4("span", {
|
|
2397
|
+
className: "mr-2",
|
|
2398
|
+
children: "➡️"
|
|
2399
|
+
}, undefined, false, undefined, this),
|
|
2400
|
+
" Move to Stage"
|
|
2401
|
+
]
|
|
2402
|
+
}, undefined, true, undefined, this)
|
|
2403
|
+
]
|
|
2404
|
+
}, undefined, true, undefined, this),
|
|
2405
|
+
deal3.status !== "OPEN" && /* @__PURE__ */ jsxDEV4("p", {
|
|
2406
|
+
className: "text-muted-foreground py-4 text-center",
|
|
2407
|
+
children: [
|
|
2408
|
+
"This deal is already ",
|
|
2409
|
+
deal3.status.toLowerCase(),
|
|
2410
|
+
". No actions available."
|
|
2411
|
+
]
|
|
2412
|
+
}, undefined, true, undefined, this),
|
|
2413
|
+
/* @__PURE__ */ jsxDEV4("div", {
|
|
2414
|
+
className: "border-border border-t pt-3",
|
|
2415
|
+
children: /* @__PURE__ */ jsxDEV4(Button2, {
|
|
2416
|
+
className: "w-full",
|
|
2417
|
+
variant: "outline",
|
|
2418
|
+
onPress: handleClose,
|
|
2419
|
+
children: "Close"
|
|
2420
|
+
}, undefined, false, undefined, this)
|
|
2421
|
+
}, undefined, false, undefined, this)
|
|
2422
|
+
]
|
|
2423
|
+
}, undefined, true, undefined, this),
|
|
2424
|
+
mode === "win" && /* @__PURE__ */ jsxDEV4("div", {
|
|
2425
|
+
className: "space-y-4",
|
|
2426
|
+
children: [
|
|
2427
|
+
/* @__PURE__ */ jsxDEV4("div", {
|
|
2428
|
+
children: [
|
|
2429
|
+
/* @__PURE__ */ jsxDEV4("label", {
|
|
2430
|
+
htmlFor: "won-source",
|
|
2431
|
+
className: "text-muted-foreground mb-1 block text-sm font-medium",
|
|
2432
|
+
children: "How did you win this deal?"
|
|
2433
|
+
}, undefined, false, undefined, this),
|
|
2434
|
+
/* @__PURE__ */ jsxDEV4("select", {
|
|
2435
|
+
id: "won-source",
|
|
2436
|
+
value: wonSource,
|
|
2437
|
+
onChange: (e) => setWonSource(e.target.value),
|
|
2438
|
+
className: "border-input bg-background focus:ring-ring h-10 w-full rounded-md border px-3 py-2 text-sm focus:ring-2 focus:outline-none",
|
|
2439
|
+
children: [
|
|
2440
|
+
/* @__PURE__ */ jsxDEV4("option", {
|
|
2441
|
+
value: "",
|
|
2442
|
+
children: "Select a source..."
|
|
2443
|
+
}, undefined, false, undefined, this),
|
|
2444
|
+
/* @__PURE__ */ jsxDEV4("option", {
|
|
2445
|
+
value: "referral",
|
|
2446
|
+
children: "Referral"
|
|
2447
|
+
}, undefined, false, undefined, this),
|
|
2448
|
+
/* @__PURE__ */ jsxDEV4("option", {
|
|
2449
|
+
value: "cold_outreach",
|
|
2450
|
+
children: "Cold Outreach"
|
|
2451
|
+
}, undefined, false, undefined, this),
|
|
2452
|
+
/* @__PURE__ */ jsxDEV4("option", {
|
|
2453
|
+
value: "inbound",
|
|
2454
|
+
children: "Inbound Lead"
|
|
2455
|
+
}, undefined, false, undefined, this),
|
|
2456
|
+
/* @__PURE__ */ jsxDEV4("option", {
|
|
2457
|
+
value: "upsell",
|
|
2458
|
+
children: "Upsell"
|
|
2459
|
+
}, undefined, false, undefined, this),
|
|
2460
|
+
/* @__PURE__ */ jsxDEV4("option", {
|
|
2461
|
+
value: "other",
|
|
2462
|
+
children: "Other"
|
|
2463
|
+
}, undefined, false, undefined, this)
|
|
2464
|
+
]
|
|
2465
|
+
}, undefined, true, undefined, this)
|
|
2466
|
+
]
|
|
2467
|
+
}, undefined, true, undefined, this),
|
|
2468
|
+
/* @__PURE__ */ jsxDEV4("div", {
|
|
2469
|
+
children: [
|
|
2470
|
+
/* @__PURE__ */ jsxDEV4("label", {
|
|
2471
|
+
htmlFor: "win-notes",
|
|
2472
|
+
className: "text-muted-foreground mb-1 block text-sm font-medium",
|
|
2473
|
+
children: "Notes (optional)"
|
|
2474
|
+
}, undefined, false, undefined, this),
|
|
2475
|
+
/* @__PURE__ */ jsxDEV4("textarea", {
|
|
2476
|
+
id: "win-notes",
|
|
2477
|
+
value: notes,
|
|
2478
|
+
onChange: (e) => setNotes(e.target.value),
|
|
2479
|
+
placeholder: "Any additional notes about the win...",
|
|
2480
|
+
rows: 3,
|
|
2481
|
+
className: "border-input bg-background focus:ring-ring w-full rounded-md border px-3 py-2 text-sm focus:ring-2 focus:outline-none"
|
|
2482
|
+
}, undefined, false, undefined, this)
|
|
2483
|
+
]
|
|
2484
|
+
}, undefined, true, undefined, this),
|
|
2485
|
+
error && /* @__PURE__ */ jsxDEV4("div", {
|
|
2486
|
+
className: "bg-destructive/10 text-destructive rounded-md p-3 text-sm",
|
|
2487
|
+
children: error
|
|
2488
|
+
}, undefined, false, undefined, this),
|
|
2489
|
+
/* @__PURE__ */ jsxDEV4("div", {
|
|
2490
|
+
className: "flex justify-end gap-3 pt-2",
|
|
2491
|
+
children: [
|
|
2492
|
+
/* @__PURE__ */ jsxDEV4(Button2, {
|
|
2493
|
+
variant: "ghost",
|
|
2494
|
+
onPress: () => setMode("menu"),
|
|
2495
|
+
disabled: isLoading,
|
|
2496
|
+
children: "Back"
|
|
2497
|
+
}, undefined, false, undefined, this),
|
|
2498
|
+
/* @__PURE__ */ jsxDEV4(Button2, {
|
|
2499
|
+
onPress: handleWin,
|
|
2500
|
+
disabled: isLoading,
|
|
2501
|
+
children: isLoading ? "Processing..." : "\uD83C\uDFC6 Confirm Win"
|
|
2502
|
+
}, undefined, false, undefined, this)
|
|
2503
|
+
]
|
|
2504
|
+
}, undefined, true, undefined, this)
|
|
2505
|
+
]
|
|
2506
|
+
}, undefined, true, undefined, this),
|
|
2507
|
+
mode === "lose" && /* @__PURE__ */ jsxDEV4("div", {
|
|
2508
|
+
className: "space-y-4",
|
|
2509
|
+
children: [
|
|
2510
|
+
/* @__PURE__ */ jsxDEV4("div", {
|
|
2511
|
+
children: [
|
|
2512
|
+
/* @__PURE__ */ jsxDEV4("label", {
|
|
2513
|
+
htmlFor: "lost-reason",
|
|
2514
|
+
className: "text-muted-foreground mb-1 block text-sm font-medium",
|
|
2515
|
+
children: "Why was this deal lost? *"
|
|
2516
|
+
}, undefined, false, undefined, this),
|
|
2517
|
+
/* @__PURE__ */ jsxDEV4("select", {
|
|
2518
|
+
id: "lost-reason",
|
|
2519
|
+
value: lostReason,
|
|
2520
|
+
onChange: (e) => setLostReason(e.target.value),
|
|
2521
|
+
className: "border-input bg-background focus:ring-ring h-10 w-full rounded-md border px-3 py-2 text-sm focus:ring-2 focus:outline-none",
|
|
2522
|
+
children: [
|
|
2523
|
+
/* @__PURE__ */ jsxDEV4("option", {
|
|
2524
|
+
value: "",
|
|
2525
|
+
children: "Select a reason..."
|
|
2526
|
+
}, undefined, false, undefined, this),
|
|
2527
|
+
/* @__PURE__ */ jsxDEV4("option", {
|
|
2528
|
+
value: "price",
|
|
2529
|
+
children: "Price too high"
|
|
2530
|
+
}, undefined, false, undefined, this),
|
|
2531
|
+
/* @__PURE__ */ jsxDEV4("option", {
|
|
2532
|
+
value: "competitor",
|
|
2533
|
+
children: "Lost to competitor"
|
|
2534
|
+
}, undefined, false, undefined, this),
|
|
2535
|
+
/* @__PURE__ */ jsxDEV4("option", {
|
|
2536
|
+
value: "no_budget",
|
|
2537
|
+
children: "No budget"
|
|
2538
|
+
}, undefined, false, undefined, this),
|
|
2539
|
+
/* @__PURE__ */ jsxDEV4("option", {
|
|
2540
|
+
value: "no_decision",
|
|
2541
|
+
children: "No decision made"
|
|
2542
|
+
}, undefined, false, undefined, this),
|
|
2543
|
+
/* @__PURE__ */ jsxDEV4("option", {
|
|
2544
|
+
value: "timing",
|
|
2545
|
+
children: "Bad timing"
|
|
2546
|
+
}, undefined, false, undefined, this),
|
|
2547
|
+
/* @__PURE__ */ jsxDEV4("option", {
|
|
2548
|
+
value: "product_fit",
|
|
2549
|
+
children: "Product not a fit"
|
|
2550
|
+
}, undefined, false, undefined, this),
|
|
2551
|
+
/* @__PURE__ */ jsxDEV4("option", {
|
|
2552
|
+
value: "other",
|
|
2553
|
+
children: "Other"
|
|
2554
|
+
}, undefined, false, undefined, this)
|
|
2555
|
+
]
|
|
2556
|
+
}, undefined, true, undefined, this)
|
|
2557
|
+
]
|
|
2558
|
+
}, undefined, true, undefined, this),
|
|
2559
|
+
/* @__PURE__ */ jsxDEV4("div", {
|
|
2560
|
+
children: [
|
|
2561
|
+
/* @__PURE__ */ jsxDEV4("label", {
|
|
2562
|
+
htmlFor: "lose-notes",
|
|
2563
|
+
className: "text-muted-foreground mb-1 block text-sm font-medium",
|
|
2564
|
+
children: "Notes (optional)"
|
|
2565
|
+
}, undefined, false, undefined, this),
|
|
2566
|
+
/* @__PURE__ */ jsxDEV4("textarea", {
|
|
2567
|
+
id: "lose-notes",
|
|
2568
|
+
value: notes,
|
|
2569
|
+
onChange: (e) => setNotes(e.target.value),
|
|
2570
|
+
placeholder: "Any additional details...",
|
|
2571
|
+
rows: 3,
|
|
2572
|
+
className: "border-input bg-background focus:ring-ring w-full rounded-md border px-3 py-2 text-sm focus:ring-2 focus:outline-none"
|
|
2573
|
+
}, undefined, false, undefined, this)
|
|
2574
|
+
]
|
|
2575
|
+
}, undefined, true, undefined, this),
|
|
2576
|
+
error && /* @__PURE__ */ jsxDEV4("div", {
|
|
2577
|
+
className: "bg-destructive/10 text-destructive rounded-md p-3 text-sm",
|
|
2578
|
+
children: error
|
|
2579
|
+
}, undefined, false, undefined, this),
|
|
2580
|
+
/* @__PURE__ */ jsxDEV4("div", {
|
|
2581
|
+
className: "flex justify-end gap-3 pt-2",
|
|
2582
|
+
children: [
|
|
2583
|
+
/* @__PURE__ */ jsxDEV4(Button2, {
|
|
2584
|
+
variant: "ghost",
|
|
2585
|
+
onPress: () => setMode("menu"),
|
|
2586
|
+
disabled: isLoading,
|
|
2587
|
+
children: "Back"
|
|
2588
|
+
}, undefined, false, undefined, this),
|
|
2589
|
+
/* @__PURE__ */ jsxDEV4(Button2, {
|
|
2590
|
+
variant: "destructive",
|
|
2591
|
+
onPress: handleLose,
|
|
2592
|
+
disabled: isLoading,
|
|
2593
|
+
children: isLoading ? "Processing..." : "❌ Confirm Loss"
|
|
2594
|
+
}, undefined, false, undefined, this)
|
|
2595
|
+
]
|
|
2596
|
+
}, undefined, true, undefined, this)
|
|
2597
|
+
]
|
|
2598
|
+
}, undefined, true, undefined, this),
|
|
2599
|
+
mode === "move" && /* @__PURE__ */ jsxDEV4("div", {
|
|
2600
|
+
className: "space-y-4",
|
|
2601
|
+
children: [
|
|
2602
|
+
/* @__PURE__ */ jsxDEV4("div", {
|
|
2603
|
+
children: [
|
|
2604
|
+
/* @__PURE__ */ jsxDEV4("label", {
|
|
2605
|
+
htmlFor: "move-stage",
|
|
2606
|
+
className: "text-muted-foreground mb-1 block text-sm font-medium",
|
|
2607
|
+
children: "Move to Stage"
|
|
2608
|
+
}, undefined, false, undefined, this),
|
|
2609
|
+
/* @__PURE__ */ jsxDEV4("select", {
|
|
2610
|
+
id: "move-stage",
|
|
2611
|
+
value: selectedStageId,
|
|
2612
|
+
onChange: (e) => setSelectedStageId(e.target.value),
|
|
2613
|
+
className: "border-input bg-background focus:ring-ring h-10 w-full rounded-md border px-3 py-2 text-sm focus:ring-2 focus:outline-none",
|
|
2614
|
+
children: stages.map((stage) => /* @__PURE__ */ jsxDEV4("option", {
|
|
2615
|
+
value: stage.id,
|
|
2616
|
+
children: [
|
|
2617
|
+
stage.name,
|
|
2618
|
+
stage.id === deal3.stageId ? " (current)" : ""
|
|
2619
|
+
]
|
|
2620
|
+
}, stage.id, true, undefined, this))
|
|
2621
|
+
}, undefined, false, undefined, this)
|
|
2622
|
+
]
|
|
2623
|
+
}, undefined, true, undefined, this),
|
|
2624
|
+
error && /* @__PURE__ */ jsxDEV4("div", {
|
|
2625
|
+
className: "bg-destructive/10 text-destructive rounded-md p-3 text-sm",
|
|
2626
|
+
children: error
|
|
2627
|
+
}, undefined, false, undefined, this),
|
|
2628
|
+
/* @__PURE__ */ jsxDEV4("div", {
|
|
2629
|
+
className: "flex justify-end gap-3 pt-2",
|
|
2630
|
+
children: [
|
|
2631
|
+
/* @__PURE__ */ jsxDEV4(Button2, {
|
|
2632
|
+
variant: "ghost",
|
|
2633
|
+
onPress: () => setMode("menu"),
|
|
2634
|
+
disabled: isLoading,
|
|
2635
|
+
children: "Back"
|
|
2636
|
+
}, undefined, false, undefined, this),
|
|
2637
|
+
/* @__PURE__ */ jsxDEV4(Button2, {
|
|
2638
|
+
onPress: handleMove,
|
|
2639
|
+
disabled: isLoading,
|
|
2640
|
+
children: isLoading ? "Moving..." : "➡️ Move Deal"
|
|
2641
|
+
}, undefined, false, undefined, this)
|
|
2642
|
+
]
|
|
2643
|
+
}, undefined, true, undefined, this)
|
|
2644
|
+
]
|
|
2645
|
+
}, undefined, true, undefined, this)
|
|
2646
|
+
]
|
|
2647
|
+
}, undefined, true, undefined, this)
|
|
2648
|
+
]
|
|
2649
|
+
}, undefined, true, undefined, this);
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
// src/ui/CrmDashboard.tsx
|
|
2653
|
+
import { useCallback as useCallback3, useState as useState6 } from "react";
|
|
2654
|
+
import {
|
|
2655
|
+
Button as Button3,
|
|
2656
|
+
ErrorState,
|
|
2657
|
+
LoaderBlock,
|
|
2658
|
+
StatCard,
|
|
2659
|
+
StatCardGroup
|
|
2660
|
+
} from "@contractspec/lib.design-system";
|
|
2661
|
+
import {
|
|
2662
|
+
Tabs,
|
|
2663
|
+
TabsContent,
|
|
2664
|
+
TabsList,
|
|
2665
|
+
TabsTrigger
|
|
2666
|
+
} from "@contractspec/lib.ui-kit-web/ui/tabs";
|
|
2667
|
+
import { jsxDEV as jsxDEV5 } from "react/jsx-dev-runtime";
|
|
2668
|
+
"use client";
|
|
2669
|
+
function formatCurrency4(value, currency = "USD") {
|
|
2670
|
+
return new Intl.NumberFormat("en-US", {
|
|
2671
|
+
style: "currency",
|
|
2672
|
+
currency,
|
|
2673
|
+
minimumFractionDigits: 0,
|
|
2674
|
+
maximumFractionDigits: 0
|
|
2675
|
+
}).format(value);
|
|
2676
|
+
}
|
|
2677
|
+
function CrmDashboard() {
|
|
2678
|
+
const [isCreateModalOpen, setIsCreateModalOpen] = useState6(false);
|
|
2679
|
+
const [selectedDeal, setSelectedDeal] = useState6(null);
|
|
2680
|
+
const [isDealActionsOpen, setIsDealActionsOpen] = useState6(false);
|
|
2681
|
+
const { data, dealsByStage, stages, loading, error, stats, refetch } = useDealList();
|
|
2682
|
+
const mutations = useDealMutations({
|
|
2683
|
+
onSuccess: () => {
|
|
2684
|
+
refetch();
|
|
2685
|
+
}
|
|
2686
|
+
});
|
|
2687
|
+
const handleDealClick = useCallback3((dealId) => {
|
|
2688
|
+
const deal3 = dealsByStage ? Object.values(dealsByStage).flat().find((d) => d.id === dealId) : null;
|
|
2689
|
+
if (deal3) {
|
|
2690
|
+
setSelectedDeal(deal3);
|
|
2691
|
+
setIsDealActionsOpen(true);
|
|
2692
|
+
}
|
|
2693
|
+
}, [dealsByStage]);
|
|
2694
|
+
const handleDealMove = useCallback3(async (dealId, toStageId) => {
|
|
2695
|
+
await mutations.moveDeal({ dealId, stageId: toStageId });
|
|
2696
|
+
}, [mutations]);
|
|
2697
|
+
if (loading && !data) {
|
|
2698
|
+
return /* @__PURE__ */ jsxDEV5(LoaderBlock, {
|
|
2699
|
+
label: "Loading CRM..."
|
|
2700
|
+
}, undefined, false, undefined, this);
|
|
2701
|
+
}
|
|
2702
|
+
if (error) {
|
|
2703
|
+
return /* @__PURE__ */ jsxDEV5(ErrorState, {
|
|
2704
|
+
title: "Failed to load CRM",
|
|
2705
|
+
description: error.message,
|
|
2706
|
+
onRetry: refetch,
|
|
2707
|
+
retryLabel: "Retry"
|
|
2708
|
+
}, undefined, false, undefined, this);
|
|
2709
|
+
}
|
|
2710
|
+
return /* @__PURE__ */ jsxDEV5("div", {
|
|
2711
|
+
className: "space-y-6",
|
|
2712
|
+
children: [
|
|
2713
|
+
/* @__PURE__ */ jsxDEV5("div", {
|
|
2714
|
+
className: "flex items-center justify-between",
|
|
2715
|
+
children: [
|
|
2716
|
+
/* @__PURE__ */ jsxDEV5("h2", {
|
|
2717
|
+
className: "text-2xl font-bold",
|
|
2718
|
+
children: "CRM Pipeline"
|
|
2719
|
+
}, undefined, false, undefined, this),
|
|
2720
|
+
/* @__PURE__ */ jsxDEV5(Button3, {
|
|
2721
|
+
onClick: () => setIsCreateModalOpen(true),
|
|
2722
|
+
children: [
|
|
2723
|
+
/* @__PURE__ */ jsxDEV5("span", {
|
|
2724
|
+
className: "mr-2",
|
|
2725
|
+
children: "+"
|
|
2726
|
+
}, undefined, false, undefined, this),
|
|
2727
|
+
" Create Deal"
|
|
2728
|
+
]
|
|
2729
|
+
}, undefined, true, undefined, this)
|
|
2730
|
+
]
|
|
2731
|
+
}, undefined, true, undefined, this),
|
|
2732
|
+
stats && /* @__PURE__ */ jsxDEV5(StatCardGroup, {
|
|
2733
|
+
children: [
|
|
2734
|
+
/* @__PURE__ */ jsxDEV5(StatCard, {
|
|
2735
|
+
label: "Total Pipeline",
|
|
2736
|
+
value: formatCurrency4(stats.totalValue),
|
|
2737
|
+
hint: `${stats.total} deals`
|
|
2738
|
+
}, undefined, false, undefined, this),
|
|
2739
|
+
/* @__PURE__ */ jsxDEV5(StatCard, {
|
|
2740
|
+
label: "Open Deals",
|
|
2741
|
+
value: formatCurrency4(stats.openValue),
|
|
2742
|
+
hint: `${stats.openCount} active`
|
|
2743
|
+
}, undefined, false, undefined, this),
|
|
2744
|
+
/* @__PURE__ */ jsxDEV5(StatCard, {
|
|
2745
|
+
label: "Won",
|
|
2746
|
+
value: formatCurrency4(stats.wonValue),
|
|
2747
|
+
hint: `${stats.wonCount} closed`
|
|
2748
|
+
}, undefined, false, undefined, this),
|
|
2749
|
+
/* @__PURE__ */ jsxDEV5(StatCard, {
|
|
2750
|
+
label: "Lost",
|
|
2751
|
+
value: stats.lostCount,
|
|
2752
|
+
hint: "deals lost"
|
|
2753
|
+
}, undefined, false, undefined, this)
|
|
2754
|
+
]
|
|
2755
|
+
}, undefined, true, undefined, this),
|
|
2756
|
+
/* @__PURE__ */ jsxDEV5(Tabs, {
|
|
2757
|
+
defaultValue: "pipeline",
|
|
2758
|
+
className: "w-full",
|
|
2759
|
+
children: [
|
|
2760
|
+
/* @__PURE__ */ jsxDEV5(TabsList, {
|
|
2761
|
+
children: [
|
|
2762
|
+
/* @__PURE__ */ jsxDEV5(TabsTrigger, {
|
|
2763
|
+
value: "pipeline",
|
|
2764
|
+
children: [
|
|
2765
|
+
/* @__PURE__ */ jsxDEV5("span", {
|
|
2766
|
+
className: "mr-2",
|
|
2767
|
+
children: "\uD83D\uDCCA"
|
|
2768
|
+
}, undefined, false, undefined, this),
|
|
2769
|
+
"Pipeline"
|
|
2770
|
+
]
|
|
2771
|
+
}, undefined, true, undefined, this),
|
|
2772
|
+
/* @__PURE__ */ jsxDEV5(TabsTrigger, {
|
|
2773
|
+
value: "list",
|
|
2774
|
+
children: [
|
|
2775
|
+
/* @__PURE__ */ jsxDEV5("span", {
|
|
2776
|
+
className: "mr-2",
|
|
2777
|
+
children: "\uD83D\uDCCB"
|
|
2778
|
+
}, undefined, false, undefined, this),
|
|
2779
|
+
"All Deals"
|
|
2780
|
+
]
|
|
2781
|
+
}, undefined, true, undefined, this),
|
|
2782
|
+
/* @__PURE__ */ jsxDEV5(TabsTrigger, {
|
|
2783
|
+
value: "metrics",
|
|
2784
|
+
children: [
|
|
2785
|
+
/* @__PURE__ */ jsxDEV5("span", {
|
|
2786
|
+
className: "mr-2",
|
|
2787
|
+
children: "\uD83D\uDCC8"
|
|
2788
|
+
}, undefined, false, undefined, this),
|
|
2789
|
+
"Metrics"
|
|
2790
|
+
]
|
|
2791
|
+
}, undefined, true, undefined, this)
|
|
2792
|
+
]
|
|
2793
|
+
}, undefined, true, undefined, this),
|
|
2794
|
+
/* @__PURE__ */ jsxDEV5(TabsContent, {
|
|
2795
|
+
value: "pipeline",
|
|
2796
|
+
className: "min-h-[400px]",
|
|
2797
|
+
children: /* @__PURE__ */ jsxDEV5(CrmPipelineBoard, {
|
|
2798
|
+
dealsByStage,
|
|
2799
|
+
stages,
|
|
2800
|
+
onDealClick: handleDealClick,
|
|
2801
|
+
onDealMove: handleDealMove
|
|
2802
|
+
}, undefined, false, undefined, this)
|
|
2803
|
+
}, undefined, false, undefined, this),
|
|
2804
|
+
/* @__PURE__ */ jsxDEV5(TabsContent, {
|
|
2805
|
+
value: "list",
|
|
2806
|
+
className: "min-h-[400px]",
|
|
2807
|
+
children: /* @__PURE__ */ jsxDEV5(DealListTab, {
|
|
2808
|
+
data,
|
|
2809
|
+
onDealClick: handleDealClick
|
|
2810
|
+
}, undefined, false, undefined, this)
|
|
2811
|
+
}, undefined, false, undefined, this),
|
|
2812
|
+
/* @__PURE__ */ jsxDEV5(TabsContent, {
|
|
2813
|
+
value: "metrics",
|
|
2814
|
+
className: "min-h-[400px]",
|
|
2815
|
+
children: /* @__PURE__ */ jsxDEV5(MetricsTab, {
|
|
2816
|
+
stats
|
|
2817
|
+
}, undefined, false, undefined, this)
|
|
2818
|
+
}, undefined, false, undefined, this)
|
|
2819
|
+
]
|
|
2820
|
+
}, undefined, true, undefined, this),
|
|
2821
|
+
/* @__PURE__ */ jsxDEV5(CreateDealModal, {
|
|
2822
|
+
isOpen: isCreateModalOpen,
|
|
2823
|
+
onClose: () => setIsCreateModalOpen(false),
|
|
2824
|
+
onSubmit: async (input) => {
|
|
2825
|
+
await mutations.createDeal(input);
|
|
2826
|
+
},
|
|
2827
|
+
stages,
|
|
2828
|
+
isLoading: mutations.createState.loading
|
|
2829
|
+
}, undefined, false, undefined, this),
|
|
2830
|
+
/* @__PURE__ */ jsxDEV5(DealActionsModal, {
|
|
2831
|
+
isOpen: isDealActionsOpen,
|
|
2832
|
+
deal: selectedDeal,
|
|
2833
|
+
stages,
|
|
2834
|
+
onClose: () => {
|
|
2835
|
+
setIsDealActionsOpen(false);
|
|
2836
|
+
setSelectedDeal(null);
|
|
2837
|
+
},
|
|
2838
|
+
onWin: async (input) => {
|
|
2839
|
+
await mutations.winDeal(input);
|
|
2840
|
+
},
|
|
2841
|
+
onLose: async (input) => {
|
|
2842
|
+
await mutations.loseDeal(input);
|
|
2843
|
+
},
|
|
2844
|
+
onMove: async (input) => {
|
|
2845
|
+
await mutations.moveDeal(input);
|
|
2846
|
+
refetch();
|
|
2847
|
+
},
|
|
2848
|
+
isLoading: mutations.isLoading
|
|
2849
|
+
}, undefined, false, undefined, this)
|
|
2850
|
+
]
|
|
2851
|
+
}, undefined, true, undefined, this);
|
|
2852
|
+
}
|
|
2853
|
+
function DealListTab({ data, onDealClick }) {
|
|
2854
|
+
if (!data?.deals.length) {
|
|
2855
|
+
return /* @__PURE__ */ jsxDEV5("div", {
|
|
2856
|
+
className: "text-muted-foreground flex h-64 items-center justify-center",
|
|
2857
|
+
children: "No deals found"
|
|
2858
|
+
}, undefined, false, undefined, this);
|
|
2859
|
+
}
|
|
2860
|
+
return /* @__PURE__ */ jsxDEV5("div", {
|
|
2861
|
+
className: "border-border rounded-lg border",
|
|
2862
|
+
children: /* @__PURE__ */ jsxDEV5("table", {
|
|
2863
|
+
className: "w-full",
|
|
2864
|
+
children: [
|
|
2865
|
+
/* @__PURE__ */ jsxDEV5("thead", {
|
|
2866
|
+
className: "border-border bg-muted/30 border-b",
|
|
2867
|
+
children: /* @__PURE__ */ jsxDEV5("tr", {
|
|
2868
|
+
children: [
|
|
2869
|
+
/* @__PURE__ */ jsxDEV5("th", {
|
|
2870
|
+
className: "px-4 py-3 text-left text-sm font-medium",
|
|
2871
|
+
children: "Deal"
|
|
2872
|
+
}, undefined, false, undefined, this),
|
|
2873
|
+
/* @__PURE__ */ jsxDEV5("th", {
|
|
2874
|
+
className: "px-4 py-3 text-left text-sm font-medium",
|
|
2875
|
+
children: "Value"
|
|
2876
|
+
}, undefined, false, undefined, this),
|
|
2877
|
+
/* @__PURE__ */ jsxDEV5("th", {
|
|
2878
|
+
className: "px-4 py-3 text-left text-sm font-medium",
|
|
2879
|
+
children: "Status"
|
|
2880
|
+
}, undefined, false, undefined, this),
|
|
2881
|
+
/* @__PURE__ */ jsxDEV5("th", {
|
|
2882
|
+
className: "px-4 py-3 text-left text-sm font-medium",
|
|
2883
|
+
children: "Expected Close"
|
|
2884
|
+
}, undefined, false, undefined, this),
|
|
2885
|
+
/* @__PURE__ */ jsxDEV5("th", {
|
|
2886
|
+
className: "px-4 py-3 text-left text-sm font-medium",
|
|
2887
|
+
children: "Actions"
|
|
2888
|
+
}, undefined, false, undefined, this)
|
|
2889
|
+
]
|
|
2890
|
+
}, undefined, true, undefined, this)
|
|
2891
|
+
}, undefined, false, undefined, this),
|
|
2892
|
+
/* @__PURE__ */ jsxDEV5("tbody", {
|
|
2893
|
+
className: "divide-border divide-y",
|
|
2894
|
+
children: data.deals.map((deal3) => /* @__PURE__ */ jsxDEV5("tr", {
|
|
2895
|
+
className: "hover:bg-muted/50",
|
|
2896
|
+
children: [
|
|
2897
|
+
/* @__PURE__ */ jsxDEV5("td", {
|
|
2898
|
+
className: "px-4 py-3",
|
|
2899
|
+
children: /* @__PURE__ */ jsxDEV5("div", {
|
|
2900
|
+
className: "font-medium",
|
|
2901
|
+
children: deal3.name
|
|
2902
|
+
}, undefined, false, undefined, this)
|
|
2903
|
+
}, undefined, false, undefined, this),
|
|
2904
|
+
/* @__PURE__ */ jsxDEV5("td", {
|
|
2905
|
+
className: "px-4 py-3 font-mono",
|
|
2906
|
+
children: formatCurrency4(deal3.value, deal3.currency)
|
|
2907
|
+
}, undefined, false, undefined, this),
|
|
2908
|
+
/* @__PURE__ */ jsxDEV5("td", {
|
|
2909
|
+
className: "px-4 py-3",
|
|
2910
|
+
children: /* @__PURE__ */ jsxDEV5("span", {
|
|
2911
|
+
className: `inline-flex rounded-full px-2 py-0.5 text-xs font-medium ${deal3.status === "WON" ? "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400" : deal3.status === "LOST" ? "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400" : "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"}`,
|
|
2912
|
+
children: deal3.status
|
|
2913
|
+
}, undefined, false, undefined, this)
|
|
2914
|
+
}, undefined, false, undefined, this),
|
|
2915
|
+
/* @__PURE__ */ jsxDEV5("td", {
|
|
2916
|
+
className: "text-muted-foreground px-4 py-3",
|
|
2917
|
+
children: deal3.expectedCloseDate?.toLocaleDateString() ?? "-"
|
|
2918
|
+
}, undefined, false, undefined, this),
|
|
2919
|
+
/* @__PURE__ */ jsxDEV5("td", {
|
|
2920
|
+
className: "px-4 py-3",
|
|
2921
|
+
children: /* @__PURE__ */ jsxDEV5(Button3, {
|
|
2922
|
+
variant: "ghost",
|
|
2923
|
+
size: "sm",
|
|
2924
|
+
onPress: () => onDealClick?.(deal3.id),
|
|
2925
|
+
children: "Actions"
|
|
2926
|
+
}, undefined, false, undefined, this)
|
|
2927
|
+
}, undefined, false, undefined, this)
|
|
2928
|
+
]
|
|
2929
|
+
}, deal3.id, true, undefined, this))
|
|
2930
|
+
}, undefined, false, undefined, this)
|
|
2931
|
+
]
|
|
2932
|
+
}, undefined, true, undefined, this)
|
|
2933
|
+
}, undefined, false, undefined, this);
|
|
2934
|
+
}
|
|
2935
|
+
function MetricsTab({
|
|
2936
|
+
stats
|
|
2937
|
+
}) {
|
|
2938
|
+
if (!stats)
|
|
2939
|
+
return null;
|
|
2940
|
+
return /* @__PURE__ */ jsxDEV5("div", {
|
|
2941
|
+
className: "space-y-6",
|
|
2942
|
+
children: /* @__PURE__ */ jsxDEV5("div", {
|
|
2943
|
+
className: "border-border bg-card rounded-xl border p-6",
|
|
2944
|
+
children: [
|
|
2945
|
+
/* @__PURE__ */ jsxDEV5("h3", {
|
|
2946
|
+
className: "mb-4 text-lg font-semibold",
|
|
2947
|
+
children: "Pipeline Overview"
|
|
2948
|
+
}, undefined, false, undefined, this),
|
|
2949
|
+
/* @__PURE__ */ jsxDEV5("dl", {
|
|
2950
|
+
className: "grid gap-4 sm:grid-cols-3",
|
|
2951
|
+
children: [
|
|
2952
|
+
/* @__PURE__ */ jsxDEV5("div", {
|
|
2953
|
+
children: [
|
|
2954
|
+
/* @__PURE__ */ jsxDEV5("dt", {
|
|
2955
|
+
className: "text-muted-foreground text-sm",
|
|
2956
|
+
children: "Win Rate"
|
|
2957
|
+
}, undefined, false, undefined, this),
|
|
2958
|
+
/* @__PURE__ */ jsxDEV5("dd", {
|
|
2959
|
+
className: "text-2xl font-semibold",
|
|
2960
|
+
children: [
|
|
2961
|
+
stats.total > 0 ? (stats.wonCount / stats.total * 100).toFixed(0) : 0,
|
|
2962
|
+
"%"
|
|
2963
|
+
]
|
|
2964
|
+
}, undefined, true, undefined, this)
|
|
2965
|
+
]
|
|
2966
|
+
}, undefined, true, undefined, this),
|
|
2967
|
+
/* @__PURE__ */ jsxDEV5("div", {
|
|
2968
|
+
children: [
|
|
2969
|
+
/* @__PURE__ */ jsxDEV5("dt", {
|
|
2970
|
+
className: "text-muted-foreground text-sm",
|
|
2971
|
+
children: "Avg Deal Size"
|
|
2972
|
+
}, undefined, false, undefined, this),
|
|
2973
|
+
/* @__PURE__ */ jsxDEV5("dd", {
|
|
2974
|
+
className: "text-2xl font-semibold",
|
|
2975
|
+
children: formatCurrency4(stats.total > 0 ? stats.totalValue / stats.total : 0)
|
|
2976
|
+
}, undefined, false, undefined, this)
|
|
2977
|
+
]
|
|
2978
|
+
}, undefined, true, undefined, this),
|
|
2979
|
+
/* @__PURE__ */ jsxDEV5("div", {
|
|
2980
|
+
children: [
|
|
2981
|
+
/* @__PURE__ */ jsxDEV5("dt", {
|
|
2982
|
+
className: "text-muted-foreground text-sm",
|
|
2983
|
+
children: "Conversion"
|
|
2984
|
+
}, undefined, false, undefined, this),
|
|
2985
|
+
/* @__PURE__ */ jsxDEV5("dd", {
|
|
2986
|
+
className: "text-2xl font-semibold",
|
|
2987
|
+
children: [
|
|
2988
|
+
stats.wonCount,
|
|
2989
|
+
" / ",
|
|
2990
|
+
stats.total
|
|
2991
|
+
]
|
|
2992
|
+
}, undefined, true, undefined, this)
|
|
2993
|
+
]
|
|
2994
|
+
}, undefined, true, undefined, this)
|
|
2995
|
+
]
|
|
2996
|
+
}, undefined, true, undefined, this)
|
|
2997
|
+
]
|
|
2998
|
+
}, undefined, true, undefined, this)
|
|
2999
|
+
}, undefined, false, undefined, this);
|
|
3000
|
+
}
|
|
3001
|
+
// src/ui/hooks/index.ts
|
|
3002
|
+
"use client";
|
|
3003
|
+
|
|
3004
|
+
// src/ui/renderers/pipeline.renderer.tsx
|
|
3005
|
+
import { jsxDEV as jsxDEV6 } from "react/jsx-dev-runtime";
|
|
3006
|
+
function CrmPipelineBoardWrapper() {
|
|
3007
|
+
const { dealsByStage, stages } = useDealList();
|
|
3008
|
+
return /* @__PURE__ */ jsxDEV6(CrmPipelineBoard, {
|
|
3009
|
+
dealsByStage,
|
|
3010
|
+
stages
|
|
3011
|
+
}, undefined, false, undefined, this);
|
|
3012
|
+
}
|
|
3013
|
+
var crmPipelineReactRenderer = {
|
|
3014
|
+
target: "react",
|
|
3015
|
+
render: async (desc, _ctx) => {
|
|
3016
|
+
if (desc.source.type !== "component") {
|
|
3017
|
+
throw new Error("Invalid source type");
|
|
3018
|
+
}
|
|
3019
|
+
if (desc.source.componentKey !== "CrmPipelineView") {
|
|
3020
|
+
throw new Error(`Unknown component: ${desc.source.componentKey}`);
|
|
3021
|
+
}
|
|
3022
|
+
return /* @__PURE__ */ jsxDEV6(CrmPipelineBoardWrapper, {}, undefined, false, undefined, this);
|
|
3023
|
+
}
|
|
3024
|
+
};
|
|
3025
|
+
|
|
3026
|
+
// src/ui/renderers/pipeline.markdown.ts
|
|
3027
|
+
function formatCurrency5(value, currency = "USD") {
|
|
3028
|
+
return new Intl.NumberFormat("en-US", {
|
|
3029
|
+
style: "currency",
|
|
3030
|
+
currency,
|
|
3031
|
+
minimumFractionDigits: 0
|
|
3032
|
+
}).format(value);
|
|
3033
|
+
}
|
|
3034
|
+
var crmPipelineMarkdownRenderer = {
|
|
3035
|
+
target: "markdown",
|
|
3036
|
+
render: async (desc, _ctx) => {
|
|
3037
|
+
if (desc.source.type !== "component" || desc.source.componentKey !== "PipelineKanbanView") {
|
|
3038
|
+
throw new Error("crmPipelineMarkdownRenderer: not PipelineKanbanView");
|
|
3039
|
+
}
|
|
3040
|
+
const pipelineId = "pipeline-1";
|
|
3041
|
+
const [dealsResult, stages] = await Promise.all([
|
|
3042
|
+
mockListDealsHandler({ pipelineId, limit: 50 }),
|
|
3043
|
+
mockGetPipelineStagesHandler({ pipelineId })
|
|
3044
|
+
]);
|
|
3045
|
+
const deals = dealsResult.deals;
|
|
3046
|
+
const stageList = stages;
|
|
3047
|
+
const dealsByStage = {};
|
|
3048
|
+
for (const stage of stageList) {
|
|
3049
|
+
dealsByStage[stage.id] = deals.filter((d) => d.stageId === stage.id && d.status === "OPEN");
|
|
3050
|
+
}
|
|
3051
|
+
const lines = [
|
|
3052
|
+
"# CRM Pipeline",
|
|
3053
|
+
"",
|
|
3054
|
+
`**Total Value**: ${formatCurrency5(dealsResult.totalValue)}`,
|
|
3055
|
+
`**Total Deals**: ${dealsResult.total}`,
|
|
3056
|
+
""
|
|
3057
|
+
];
|
|
3058
|
+
for (const stage of stageList.sort((a, b) => a.position - b.position)) {
|
|
3059
|
+
const stageDeals = dealsByStage[stage.id] ?? [];
|
|
3060
|
+
const stageValue = stageDeals.reduce((sum, d) => sum + d.value, 0);
|
|
3061
|
+
lines.push(`## ${stage.name}`);
|
|
3062
|
+
lines.push(`_${stageDeals.length} deals · ${formatCurrency5(stageValue)}_`);
|
|
3063
|
+
lines.push("");
|
|
3064
|
+
if (stageDeals.length === 0) {
|
|
3065
|
+
lines.push("_No deals_");
|
|
3066
|
+
} else {
|
|
3067
|
+
for (const deal3 of stageDeals) {
|
|
3068
|
+
lines.push(`- **${deal3.name}** - ${formatCurrency5(deal3.value, deal3.currency)}`);
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
lines.push("");
|
|
3072
|
+
}
|
|
3073
|
+
return {
|
|
3074
|
+
mimeType: "text/markdown",
|
|
3075
|
+
body: lines.join(`
|
|
3076
|
+
`)
|
|
3077
|
+
};
|
|
3078
|
+
}
|
|
3079
|
+
};
|
|
3080
|
+
var crmDashboardMarkdownRenderer = {
|
|
3081
|
+
target: "markdown",
|
|
3082
|
+
render: async (desc, _ctx) => {
|
|
3083
|
+
if (desc.source.type !== "component" || desc.source.componentKey !== "CrmDashboard") {
|
|
3084
|
+
throw new Error("crmDashboardMarkdownRenderer: not CrmDashboard");
|
|
3085
|
+
}
|
|
3086
|
+
const pipelineId = "pipeline-1";
|
|
3087
|
+
const [dealsResult, stages] = await Promise.all([
|
|
3088
|
+
mockListDealsHandler({ pipelineId, limit: 100 }),
|
|
3089
|
+
mockGetPipelineStagesHandler({ pipelineId })
|
|
3090
|
+
]);
|
|
3091
|
+
const deals = dealsResult.deals;
|
|
3092
|
+
const stageList = stages;
|
|
3093
|
+
const openDeals = deals.filter((d) => d.status === "OPEN");
|
|
3094
|
+
const wonDeals = deals.filter((d) => d.status === "WON");
|
|
3095
|
+
const lostDeals = deals.filter((d) => d.status === "LOST");
|
|
3096
|
+
const openValue = openDeals.reduce((sum, d) => sum + d.value, 0);
|
|
3097
|
+
const wonValue = wonDeals.reduce((sum, d) => sum + d.value, 0);
|
|
3098
|
+
const lines = [
|
|
3099
|
+
"# CRM Dashboard",
|
|
3100
|
+
"",
|
|
3101
|
+
"> Sales pipeline overview and key metrics",
|
|
3102
|
+
"",
|
|
3103
|
+
"## Summary",
|
|
3104
|
+
"",
|
|
3105
|
+
"| Metric | Value |",
|
|
3106
|
+
"|--------|-------|",
|
|
3107
|
+
`| Total Deals | ${dealsResult.total} |`,
|
|
3108
|
+
`| Pipeline Value | ${formatCurrency5(dealsResult.totalValue)} |`,
|
|
3109
|
+
`| Open Deals | ${openDeals.length} (${formatCurrency5(openValue)}) |`,
|
|
3110
|
+
`| Won Deals | ${wonDeals.length} (${formatCurrency5(wonValue)}) |`,
|
|
3111
|
+
`| Lost Deals | ${lostDeals.length} |`,
|
|
3112
|
+
"",
|
|
3113
|
+
"## Pipeline Stages",
|
|
3114
|
+
""
|
|
3115
|
+
];
|
|
3116
|
+
lines.push("| Stage | Deals | Value |");
|
|
3117
|
+
lines.push("|-------|-------|-------|");
|
|
3118
|
+
for (const stage of stageList.sort((a, b) => a.position - b.position)) {
|
|
3119
|
+
const stageDeals = openDeals.filter((d) => d.stageId === stage.id);
|
|
3120
|
+
const stageValue = stageDeals.reduce((sum, d) => sum + d.value, 0);
|
|
3121
|
+
lines.push(`| ${stage.name} | ${stageDeals.length} | ${formatCurrency5(stageValue)} |`);
|
|
3122
|
+
}
|
|
3123
|
+
lines.push("");
|
|
3124
|
+
lines.push("## Recent Deals");
|
|
3125
|
+
lines.push("");
|
|
3126
|
+
const recentDeals = deals.slice(0, 10);
|
|
3127
|
+
if (recentDeals.length === 0) {
|
|
3128
|
+
lines.push("_No deals yet._");
|
|
3129
|
+
} else {
|
|
3130
|
+
lines.push("| Deal | Value | Stage | Status |");
|
|
3131
|
+
lines.push("|------|-------|-------|--------|");
|
|
3132
|
+
for (const deal3 of recentDeals) {
|
|
3133
|
+
const stage = stageList.find((s) => s.id === deal3.stageId);
|
|
3134
|
+
lines.push(`| ${deal3.name} | ${formatCurrency5(deal3.value, deal3.currency)} | ${stage?.name ?? "-"} | ${deal3.status} |`);
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
return {
|
|
3138
|
+
mimeType: "text/markdown",
|
|
3139
|
+
body: lines.join(`
|
|
3140
|
+
`)
|
|
3141
|
+
};
|
|
3142
|
+
}
|
|
3143
|
+
};
|
|
3144
|
+
// src/ui/overlays/demo-overlays.ts
|
|
3145
|
+
var crmDemoOverlay = {
|
|
3146
|
+
overlayId: "crm-pipeline.demo-user",
|
|
3147
|
+
version: "1.0.0",
|
|
3148
|
+
description: "Demo mode with sample data",
|
|
3149
|
+
appliesTo: {
|
|
3150
|
+
feature: "crm-pipeline",
|
|
3151
|
+
role: "demo"
|
|
3152
|
+
},
|
|
3153
|
+
modifications: [
|
|
3154
|
+
{
|
|
3155
|
+
type: "hideField",
|
|
3156
|
+
field: "importButton",
|
|
3157
|
+
reason: "Not available in demo"
|
|
3158
|
+
},
|
|
3159
|
+
{
|
|
3160
|
+
type: "hideField",
|
|
3161
|
+
field: "exportButton",
|
|
3162
|
+
reason: "Not available in demo"
|
|
3163
|
+
},
|
|
3164
|
+
{
|
|
3165
|
+
type: "addBadge",
|
|
3166
|
+
position: "header",
|
|
3167
|
+
label: "Demo Mode",
|
|
3168
|
+
variant: "warning"
|
|
3169
|
+
}
|
|
3170
|
+
]
|
|
3171
|
+
};
|
|
3172
|
+
var crmSalesRepOverlay = {
|
|
3173
|
+
overlayId: "crm-pipeline.sales-rep",
|
|
3174
|
+
version: "1.0.0",
|
|
3175
|
+
description: "Sales rep focused view",
|
|
3176
|
+
appliesTo: {
|
|
3177
|
+
feature: "crm-pipeline",
|
|
3178
|
+
role: "sales-rep"
|
|
3179
|
+
},
|
|
3180
|
+
modifications: [
|
|
3181
|
+
{
|
|
3182
|
+
type: "hideField",
|
|
3183
|
+
field: "teamMetrics",
|
|
3184
|
+
reason: "Team metrics for managers only"
|
|
3185
|
+
},
|
|
3186
|
+
{ type: "hideField", field: "pipelineSettings", reason: "Admin only" },
|
|
3187
|
+
{ type: "renameLabel", field: "deals", newLabel: "My Deals" }
|
|
3188
|
+
]
|
|
3189
|
+
};
|
|
3190
|
+
var crmOverlays = [
|
|
3191
|
+
crmDemoOverlay,
|
|
3192
|
+
crmSalesRepOverlay
|
|
3193
|
+
];
|
|
3194
|
+
// src/index.ts
|
|
3195
|
+
import { identityRbacSchemaContribution } from "@contractspec/lib.identity-rbac";
|
|
3196
|
+
import { auditTrailSchemaContribution } from "@contractspec/module.audit-trail";
|
|
3197
|
+
import { notificationsSchemaContribution } from "@contractspec/module.notifications";
|
|
3198
|
+
var schemaComposition = {
|
|
3199
|
+
modules: [
|
|
3200
|
+
identityRbacSchemaContribution,
|
|
3201
|
+
auditTrailSchemaContribution,
|
|
3202
|
+
notificationsSchemaContribution,
|
|
3203
|
+
crmPipelineSchemaContribution
|
|
3204
|
+
],
|
|
3205
|
+
provider: "postgresql",
|
|
3206
|
+
outputPath: "./prisma/schema/generated.prisma"
|
|
3207
|
+
};
|
|
3208
|
+
export {
|
|
3209
|
+
useDealMutations,
|
|
3210
|
+
useDealList,
|
|
3211
|
+
schemaComposition,
|
|
3212
|
+
mockWinDealHandler,
|
|
3213
|
+
mockMoveDealHandler,
|
|
3214
|
+
mockLoseDealHandler,
|
|
3215
|
+
mockListDealsHandler,
|
|
3216
|
+
mockGetPipelineStagesHandler,
|
|
3217
|
+
mockGetDealsByStageHandler,
|
|
3218
|
+
mockCreateDealHandler,
|
|
3219
|
+
example_default as example,
|
|
3220
|
+
crmSalesRepOverlay,
|
|
3221
|
+
crmPipelineSchemaContribution,
|
|
3222
|
+
crmPipelineReactRenderer,
|
|
3223
|
+
crmPipelineMarkdownRenderer,
|
|
3224
|
+
crmOverlays,
|
|
3225
|
+
crmDemoOverlay,
|
|
3226
|
+
crmDashboardMarkdownRenderer,
|
|
3227
|
+
createCrmHandlers,
|
|
3228
|
+
WinDealInputModel,
|
|
3229
|
+
WinDealContract,
|
|
3230
|
+
TaskTypeEnum,
|
|
3231
|
+
TaskStatusEnum,
|
|
3232
|
+
TaskPriorityEnum,
|
|
3233
|
+
TaskEntity,
|
|
3234
|
+
TaskCompletedEvent,
|
|
3235
|
+
StageEntity,
|
|
3236
|
+
PipelineMetricsPresentation,
|
|
3237
|
+
PipelineKanbanPresentation,
|
|
3238
|
+
PipelineEntity,
|
|
3239
|
+
MoveDealInputModel,
|
|
3240
|
+
MoveDealContract,
|
|
3241
|
+
MOCK_STAGES,
|
|
3242
|
+
MOCK_DEALS,
|
|
3243
|
+
MOCK_CONTACTS,
|
|
3244
|
+
MOCK_COMPANIES,
|
|
3245
|
+
LoseDealInputModel,
|
|
3246
|
+
LoseDealContract,
|
|
3247
|
+
ListDealsOutputModel,
|
|
3248
|
+
ListDealsInputModel,
|
|
3249
|
+
ListDealsContract,
|
|
3250
|
+
DealWonPayloadModel,
|
|
3251
|
+
DealWonEvent,
|
|
3252
|
+
DealStatusFilterEnum,
|
|
3253
|
+
DealStatusEnum2 as DealStatusEnum,
|
|
3254
|
+
DealMovedPayloadModel,
|
|
3255
|
+
DealMovedEvent,
|
|
3256
|
+
DealModel,
|
|
3257
|
+
DealLostPayloadModel,
|
|
3258
|
+
DealLostEvent,
|
|
3259
|
+
DealListPresentation,
|
|
3260
|
+
DealEntity,
|
|
3261
|
+
DealDetailPresentation,
|
|
3262
|
+
DealCreatedEvent,
|
|
3263
|
+
DealCardPresentation,
|
|
3264
|
+
DealActionsModal,
|
|
3265
|
+
CrmPipelineFeature,
|
|
3266
|
+
CrmPipelineBoard,
|
|
3267
|
+
CrmDealCard,
|
|
3268
|
+
CrmDashboardPresentation,
|
|
3269
|
+
CrmDashboard,
|
|
3270
|
+
CreateDealModal,
|
|
3271
|
+
CreateDealInputModel,
|
|
3272
|
+
CreateDealContract,
|
|
3273
|
+
ContactStatusEnum,
|
|
3274
|
+
ContactEntity,
|
|
3275
|
+
ContactCreatedEvent,
|
|
3276
|
+
CompanySizeEnum,
|
|
3277
|
+
CompanyEntity,
|
|
3278
|
+
ActivityEntity
|
|
3279
|
+
};
|