@contractspec/example.crm-pipeline 3.7.16 → 3.7.18

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