@contractspec/example.crm-pipeline 3.7.6 → 3.7.10

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 (130) hide show
  1. package/.turbo/turbo-build.log +45 -42
  2. package/AGENTS.md +51 -33
  3. package/CHANGELOG.md +36 -0
  4. package/README.md +67 -148
  5. package/dist/browser/docs/crm-pipeline.docblock.js +1 -1
  6. package/dist/browser/docs/index.js +1 -1
  7. package/dist/browser/events/contact.event.js +1 -1
  8. package/dist/browser/events/deal.event.js +1 -1
  9. package/dist/browser/events/index.js +3 -3
  10. package/dist/browser/events/task.event.js +1 -1
  11. package/dist/browser/handlers/crm.handlers.js +13 -2
  12. package/dist/browser/handlers/index.js +13 -2
  13. package/dist/browser/index.js +680 -447
  14. package/dist/browser/ui/CrmDashboard.js +574 -352
  15. package/dist/browser/ui/CrmDealCard.js +5 -5
  16. package/dist/browser/ui/CrmPipelineBoard.js +13 -13
  17. package/dist/browser/ui/hooks/index.js +21 -10
  18. package/dist/browser/ui/hooks/useDealList.js +20 -9
  19. package/dist/browser/ui/hooks/useDealMutations.js +1 -1
  20. package/dist/browser/ui/index.js +683 -450
  21. package/dist/browser/ui/modals/CreateDealModal.js +12 -12
  22. package/dist/browser/ui/modals/DealActionsModal.js +21 -21
  23. package/dist/browser/ui/modals/index.js +33 -33
  24. package/dist/browser/ui/renderers/index.js +140 -118
  25. package/dist/browser/ui/renderers/pipeline.markdown.js +13 -2
  26. package/dist/browser/ui/renderers/pipeline.renderer.js +108 -97
  27. package/dist/browser/ui/tables/DealListTab.js +390 -0
  28. package/dist/deal/index.d.ts +2 -2
  29. package/dist/docs/crm-pipeline.docblock.js +1 -1
  30. package/dist/docs/index.js +1 -1
  31. package/dist/events/contact.event.js +1 -1
  32. package/dist/events/deal.event.js +1 -1
  33. package/dist/events/index.js +3 -3
  34. package/dist/events/task.event.js +1 -1
  35. package/dist/handlers/crm.handlers.d.ts +2 -0
  36. package/dist/handlers/crm.handlers.js +13 -2
  37. package/dist/handlers/index.d.ts +2 -2
  38. package/dist/handlers/index.js +13 -2
  39. package/dist/index.d.ts +3 -3
  40. package/dist/index.js +680 -447
  41. package/dist/node/docs/crm-pipeline.docblock.js +1 -1
  42. package/dist/node/docs/index.js +1 -1
  43. package/dist/node/events/contact.event.js +1 -1
  44. package/dist/node/events/deal.event.js +1 -1
  45. package/dist/node/events/index.js +3 -3
  46. package/dist/node/events/task.event.js +1 -1
  47. package/dist/node/handlers/crm.handlers.js +13 -2
  48. package/dist/node/handlers/index.js +13 -2
  49. package/dist/node/index.js +680 -447
  50. package/dist/node/ui/CrmDashboard.js +574 -352
  51. package/dist/node/ui/CrmDealCard.js +5 -5
  52. package/dist/node/ui/CrmPipelineBoard.js +13 -13
  53. package/dist/node/ui/hooks/index.js +21 -10
  54. package/dist/node/ui/hooks/useDealList.js +20 -9
  55. package/dist/node/ui/hooks/useDealMutations.js +1 -1
  56. package/dist/node/ui/index.js +683 -450
  57. package/dist/node/ui/modals/CreateDealModal.js +12 -12
  58. package/dist/node/ui/modals/DealActionsModal.js +21 -21
  59. package/dist/node/ui/modals/index.js +33 -33
  60. package/dist/node/ui/renderers/index.js +140 -118
  61. package/dist/node/ui/renderers/pipeline.markdown.js +13 -2
  62. package/dist/node/ui/renderers/pipeline.renderer.js +108 -97
  63. package/dist/node/ui/tables/DealListTab.js +390 -0
  64. package/dist/operations/index.d.ts +1 -1
  65. package/dist/ui/CrmDashboard.js +574 -352
  66. package/dist/ui/CrmDealCard.js +5 -5
  67. package/dist/ui/CrmPipelineBoard.js +13 -13
  68. package/dist/ui/hooks/index.d.ts +2 -2
  69. package/dist/ui/hooks/index.js +21 -10
  70. package/dist/ui/hooks/useDealList.d.ts +8 -2
  71. package/dist/ui/hooks/useDealList.js +20 -9
  72. package/dist/ui/hooks/useDealMutations.d.ts +9 -0
  73. package/dist/ui/hooks/useDealMutations.js +1 -1
  74. package/dist/ui/index.d.ts +3 -3
  75. package/dist/ui/index.js +683 -450
  76. package/dist/ui/modals/CreateDealModal.js +12 -12
  77. package/dist/ui/modals/DealActionsModal.js +21 -21
  78. package/dist/ui/modals/index.js +33 -33
  79. package/dist/ui/renderers/index.d.ts +1 -1
  80. package/dist/ui/renderers/index.js +140 -118
  81. package/dist/ui/renderers/pipeline.markdown.js +13 -2
  82. package/dist/ui/renderers/pipeline.renderer.d.ts +1 -1
  83. package/dist/ui/renderers/pipeline.renderer.js +108 -97
  84. package/dist/ui/tables/DealListTab.d.ts +20 -0
  85. package/dist/ui/tables/DealListTab.js +391 -0
  86. package/dist/ui/tables/DealListTab.smoke.test.d.ts +1 -0
  87. package/package.json +29 -14
  88. package/src/crm-pipeline.feature.ts +86 -86
  89. package/src/deal/deal.enum.ts +8 -8
  90. package/src/deal/deal.operation.ts +255 -255
  91. package/src/deal/deal.schema.ts +92 -92
  92. package/src/deal/deal.test-spec.ts +48 -48
  93. package/src/deal/index.ts +17 -19
  94. package/src/docs/crm-pipeline.docblock.ts +44 -44
  95. package/src/entities/company.entity.ts +52 -52
  96. package/src/entities/contact.entity.ts +67 -67
  97. package/src/entities/deal.entity.ts +134 -134
  98. package/src/entities/index.ts +27 -27
  99. package/src/entities/task.entity.ts +105 -105
  100. package/src/events/contact.event.ts +22 -22
  101. package/src/events/deal.event.ts +77 -77
  102. package/src/events/task.event.ts +19 -19
  103. package/src/example.ts +32 -32
  104. package/src/handlers/crm.handlers.ts +375 -357
  105. package/src/handlers/deal.handlers.ts +179 -179
  106. package/src/handlers/index.ts +18 -19
  107. package/src/handlers/mock-data.ts +167 -167
  108. package/src/index.ts +11 -11
  109. package/src/operations/index.ts +16 -16
  110. package/src/presentations/dashboard.presentation.ts +45 -45
  111. package/src/presentations/pipeline.presentation.ts +90 -90
  112. package/src/seeders/index.ts +26 -26
  113. package/src/shared/overlay-types.ts +23 -23
  114. package/src/ui/CrmDashboard.tsx +210 -279
  115. package/src/ui/CrmDealCard.tsx +64 -64
  116. package/src/ui/CrmPipelineBoard.tsx +105 -105
  117. package/src/ui/hooks/index.ts +3 -3
  118. package/src/ui/hooks/useDealList.ts +113 -85
  119. package/src/ui/hooks/useDealMutations.ts +151 -150
  120. package/src/ui/index.ts +5 -10
  121. package/src/ui/modals/CreateDealModal.tsx +217 -217
  122. package/src/ui/modals/DealActionsModal.tsx +390 -390
  123. package/src/ui/overlays/demo-overlays.ts +43 -43
  124. package/src/ui/renderers/index.ts +4 -3
  125. package/src/ui/renderers/pipeline.markdown.ts +165 -165
  126. package/src/ui/renderers/pipeline.renderer.tsx +17 -16
  127. package/src/ui/tables/DealListTab.smoke.test.tsx +149 -0
  128. package/src/ui/tables/DealListTab.tsx +276 -0
  129. package/tsconfig.json +7 -8
  130. package/tsdown.config.js +7 -3
@@ -7,80 +7,83 @@
7
7
  import type { DatabasePort, DbRow } from '@contractspec/lib.runtime-sandbox';
8
8
  /* eslint-disable @typescript-eslint/no-non-null-assertion */
9
9
  import { web } from '@contractspec/lib.runtime-sandbox';
10
+
10
11
  const { generateId } = web;
11
12
 
12
13
  // ============ Types ============
13
14
 
14
15
  export interface Deal {
15
- id: string;
16
- projectId: string;
17
- name: string;
18
- value: number;
19
- currency: string;
20
- pipelineId: string;
21
- stageId: string;
22
- status: 'OPEN' | 'WON' | 'LOST' | 'STALE';
23
- contactId?: string;
24
- companyId?: string;
25
- ownerId: string;
26
- expectedCloseDate?: Date;
27
- wonSource?: string;
28
- lostReason?: string;
29
- notes?: string;
30
- createdAt: Date;
31
- updatedAt: Date;
16
+ id: string;
17
+ projectId: string;
18
+ name: string;
19
+ value: number;
20
+ currency: string;
21
+ pipelineId: string;
22
+ stageId: string;
23
+ status: 'OPEN' | 'WON' | 'LOST' | 'STALE';
24
+ contactId?: string;
25
+ companyId?: string;
26
+ ownerId: string;
27
+ expectedCloseDate?: Date;
28
+ wonSource?: string;
29
+ lostReason?: string;
30
+ notes?: string;
31
+ createdAt: Date;
32
+ updatedAt: Date;
32
33
  }
33
34
 
34
35
  export interface Stage {
35
- id: string;
36
- pipelineId: string;
37
- name: string;
38
- position: number;
36
+ id: string;
37
+ pipelineId: string;
38
+ name: string;
39
+ position: number;
39
40
  }
40
41
 
41
42
  export interface CreateDealInput {
42
- name: string;
43
- value: number;
44
- currency?: string;
45
- pipelineId: string;
46
- stageId: string;
47
- contactId?: string;
48
- companyId?: string;
49
- expectedCloseDate?: Date;
43
+ name: string;
44
+ value: number;
45
+ currency?: string;
46
+ pipelineId: string;
47
+ stageId: string;
48
+ contactId?: string;
49
+ companyId?: string;
50
+ expectedCloseDate?: Date;
50
51
  }
51
52
 
52
53
  export interface MoveDealInput {
53
- dealId: string;
54
- stageId: string;
54
+ dealId: string;
55
+ stageId: string;
55
56
  }
56
57
 
57
58
  export interface WinDealInput {
58
- dealId: string;
59
- wonSource?: string;
60
- notes?: string;
59
+ dealId: string;
60
+ wonSource?: string;
61
+ notes?: string;
61
62
  }
62
63
 
63
64
  export interface LoseDealInput {
64
- dealId: string;
65
- lostReason: string;
66
- notes?: string;
65
+ dealId: string;
66
+ lostReason: string;
67
+ notes?: string;
67
68
  }
68
69
 
69
70
  export interface ListDealsInput {
70
- projectId: string;
71
- pipelineId?: string;
72
- stageId?: string;
73
- status?: 'OPEN' | 'WON' | 'LOST' | 'all';
74
- ownerId?: string;
75
- search?: string;
76
- limit?: number;
77
- offset?: number;
71
+ projectId: string;
72
+ pipelineId?: string;
73
+ stageId?: string;
74
+ status?: 'OPEN' | 'WON' | 'LOST' | 'all';
75
+ ownerId?: string;
76
+ search?: string;
77
+ limit?: number;
78
+ offset?: number;
79
+ sortBy?: 'name' | 'value' | 'status' | 'expectedCloseDate' | 'updatedAt';
80
+ sortDirection?: 'asc' | 'desc';
78
81
  }
79
82
 
80
83
  export interface ListDealsOutput {
81
- deals: Deal[];
82
- total: number;
83
- totalValue: number;
84
+ deals: Deal[];
85
+ total: number;
86
+ totalValue: number;
84
87
  }
85
88
 
86
89
  // ============ Row Type ============
@@ -89,327 +92,342 @@ export interface ListDealsOutput {
89
92
  // to the expected types since we know the schema
90
93
 
91
94
  interface DealRow {
92
- id: string;
93
- projectId: string;
94
- name: string;
95
- value: number;
96
- currency: string;
97
- pipelineId: string;
98
- stageId: string;
99
- status: string;
100
- contactId: string | null;
101
- companyId: string | null;
102
- ownerId: string;
103
- expectedCloseDate: string | null;
104
- wonSource: string | null;
105
- lostReason: string | null;
106
- notes: string | null;
107
- createdAt: string;
108
- updatedAt: string;
95
+ id: string;
96
+ projectId: string;
97
+ name: string;
98
+ value: number;
99
+ currency: string;
100
+ pipelineId: string;
101
+ stageId: string;
102
+ status: string;
103
+ contactId: string | null;
104
+ companyId: string | null;
105
+ ownerId: string;
106
+ expectedCloseDate: string | null;
107
+ wonSource: string | null;
108
+ lostReason: string | null;
109
+ notes: string | null;
110
+ createdAt: string;
111
+ updatedAt: string;
109
112
  }
110
113
 
111
114
  interface StageRow {
112
- id: string;
113
- pipelineId: string;
114
- name: string;
115
- position: number;
115
+ id: string;
116
+ pipelineId: string;
117
+ name: string;
118
+ position: number;
116
119
  }
117
120
 
118
121
  function rowToDeal(row: DealRow): Deal {
119
- return {
120
- id: row.id,
121
- projectId: row.projectId,
122
- name: row.name,
123
- value: row.value,
124
- currency: row.currency,
125
- pipelineId: row.pipelineId,
126
- stageId: row.stageId,
127
- status: row.status as Deal['status'],
128
- contactId: row.contactId ?? undefined,
129
- companyId: row.companyId ?? undefined,
130
- ownerId: row.ownerId,
131
- expectedCloseDate: row.expectedCloseDate
132
- ? new Date(row.expectedCloseDate)
133
- : undefined,
134
- wonSource: row.wonSource ?? undefined,
135
- lostReason: row.lostReason ?? undefined,
136
- notes: row.notes ?? undefined,
137
- createdAt: new Date(row.createdAt),
138
- updatedAt: new Date(row.updatedAt),
139
- };
122
+ return {
123
+ id: row.id,
124
+ projectId: row.projectId,
125
+ name: row.name,
126
+ value: row.value,
127
+ currency: row.currency,
128
+ pipelineId: row.pipelineId,
129
+ stageId: row.stageId,
130
+ status: row.status as Deal['status'],
131
+ contactId: row.contactId ?? undefined,
132
+ companyId: row.companyId ?? undefined,
133
+ ownerId: row.ownerId,
134
+ expectedCloseDate: row.expectedCloseDate
135
+ ? new Date(row.expectedCloseDate)
136
+ : undefined,
137
+ wonSource: row.wonSource ?? undefined,
138
+ lostReason: row.lostReason ?? undefined,
139
+ notes: row.notes ?? undefined,
140
+ createdAt: new Date(row.createdAt),
141
+ updatedAt: new Date(row.updatedAt),
142
+ };
140
143
  }
141
144
 
142
145
  // ============ Handler Factory ============
143
146
 
147
+ const DEAL_SORT_COLUMNS: Record<
148
+ NonNullable<ListDealsInput['sortBy']>,
149
+ string
150
+ > = {
151
+ name: 'name',
152
+ value: 'value',
153
+ status: 'status',
154
+ expectedCloseDate: 'expectedCloseDate',
155
+ updatedAt: 'updatedAt',
156
+ };
157
+
144
158
  export function createCrmHandlers(db: DatabasePort) {
145
- /**
146
- * List deals with filtering
147
- */
148
- async function listDeals(input: ListDealsInput): Promise<ListDealsOutput> {
149
- const {
150
- projectId,
151
- pipelineId,
152
- stageId,
153
- status,
154
- ownerId,
155
- search,
156
- limit = 20,
157
- offset = 0,
158
- } = input;
159
-
160
- let whereClause = 'WHERE projectId = ?';
161
- const params: (string | number)[] = [projectId];
162
-
163
- if (pipelineId) {
164
- whereClause += ' AND pipelineId = ?';
165
- params.push(pipelineId);
166
- }
167
-
168
- if (stageId) {
169
- whereClause += ' AND stageId = ?';
170
- params.push(stageId);
171
- }
172
-
173
- if (status && status !== 'all') {
174
- whereClause += ' AND status = ?';
175
- params.push(status);
176
- }
177
-
178
- if (ownerId) {
179
- whereClause += ' AND ownerId = ?';
180
- params.push(ownerId);
181
- }
182
-
183
- if (search) {
184
- whereClause += ' AND name LIKE ?';
185
- params.push(`%${search}%`);
186
- }
187
-
188
- // Get total count
189
- const countResult = (
190
- await db.query(
191
- `SELECT COUNT(*) as count FROM crm_deal ${whereClause}`,
192
- params
193
- )
194
- ).rows as DbRow[];
195
- const total = (countResult[0]?.count as number) ?? 0;
196
-
197
- // Get total value
198
- const valueResult = (
199
- await db.query(
200
- `SELECT COALESCE(SUM(value), 0) as total FROM crm_deal ${whereClause}`,
201
- params
202
- )
203
- ).rows as DbRow[];
204
- const totalValue = (valueResult[0]?.total as number) ?? 0;
205
-
206
- // Get paginated deals
207
- const dealRows = (
208
- await db.query(
209
- `SELECT * FROM crm_deal ${whereClause} ORDER BY value DESC LIMIT ? OFFSET ?`,
210
- [...params, limit, offset]
211
- )
212
- ).rows as unknown as DealRow[];
213
-
214
- return {
215
- deals: dealRows.map(rowToDeal),
216
- total,
217
- totalValue,
218
- };
219
- }
220
-
221
- /**
222
- * Create a new deal
223
- */
224
- async function createDeal(
225
- input: CreateDealInput,
226
- context: { projectId: string; ownerId: string }
227
- ): Promise<Deal> {
228
- const id = generateId('deal');
229
- const now = new Date().toISOString();
230
-
231
- await db.execute(
232
- `INSERT INTO crm_deal (id, projectId, pipelineId, stageId, name, value, currency, status, contactId, companyId, ownerId, expectedCloseDate, createdAt, updatedAt)
159
+ /**
160
+ * List deals with filtering
161
+ */
162
+ async function listDeals(input: ListDealsInput): Promise<ListDealsOutput> {
163
+ const {
164
+ projectId,
165
+ pipelineId,
166
+ stageId,
167
+ status,
168
+ ownerId,
169
+ search,
170
+ limit = 20,
171
+ offset = 0,
172
+ sortBy = 'value',
173
+ sortDirection = 'desc',
174
+ } = input;
175
+
176
+ let whereClause = 'WHERE projectId = ?';
177
+ const params: (string | number)[] = [projectId];
178
+
179
+ if (pipelineId) {
180
+ whereClause += ' AND pipelineId = ?';
181
+ params.push(pipelineId);
182
+ }
183
+
184
+ if (stageId) {
185
+ whereClause += ' AND stageId = ?';
186
+ params.push(stageId);
187
+ }
188
+
189
+ if (status && status !== 'all') {
190
+ whereClause += ' AND status = ?';
191
+ params.push(status);
192
+ }
193
+
194
+ if (ownerId) {
195
+ whereClause += ' AND ownerId = ?';
196
+ params.push(ownerId);
197
+ }
198
+
199
+ if (search) {
200
+ whereClause += ' AND name LIKE ?';
201
+ params.push(`%${search}%`);
202
+ }
203
+
204
+ // Get total count
205
+ const countResult = (
206
+ await db.query(
207
+ `SELECT COUNT(*) as count FROM crm_deal ${whereClause}`,
208
+ params
209
+ )
210
+ ).rows as DbRow[];
211
+ const total = (countResult[0]?.count as number) ?? 0;
212
+
213
+ // Get total value
214
+ const valueResult = (
215
+ await db.query(
216
+ `SELECT COALESCE(SUM(value), 0) as total FROM crm_deal ${whereClause}`,
217
+ params
218
+ )
219
+ ).rows as DbRow[];
220
+ const totalValue = (valueResult[0]?.total as number) ?? 0;
221
+
222
+ // Get paginated deals
223
+ const orderByColumn = DEAL_SORT_COLUMNS[sortBy] ?? DEAL_SORT_COLUMNS.value;
224
+ const orderByDirection = sortDirection === 'asc' ? 'ASC' : 'DESC';
225
+ const dealRows = (
226
+ await db.query(
227
+ `SELECT * FROM crm_deal ${whereClause} ORDER BY ${orderByColumn} ${orderByDirection} LIMIT ? OFFSET ?`,
228
+ [...params, limit, offset]
229
+ )
230
+ ).rows as unknown as DealRow[];
231
+
232
+ return {
233
+ deals: dealRows.map(rowToDeal),
234
+ total,
235
+ totalValue,
236
+ };
237
+ }
238
+
239
+ /**
240
+ * Create a new deal
241
+ */
242
+ async function createDeal(
243
+ input: CreateDealInput,
244
+ context: { projectId: string; ownerId: string }
245
+ ): Promise<Deal> {
246
+ const id = generateId('deal');
247
+ const now = new Date().toISOString();
248
+
249
+ await db.execute(
250
+ `INSERT INTO crm_deal (id, projectId, pipelineId, stageId, name, value, currency, status, contactId, companyId, ownerId, expectedCloseDate, createdAt, updatedAt)
233
251
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
234
- [
235
- id,
236
- context.projectId,
237
- input.pipelineId,
238
- input.stageId,
239
- input.name,
240
- input.value,
241
- input.currency ?? 'USD',
242
- 'OPEN',
243
- input.contactId ?? null,
244
- input.companyId ?? null,
245
- context.ownerId,
246
- input.expectedCloseDate?.toISOString() ?? null,
247
- now,
248
- now,
249
- ]
250
- );
251
-
252
- const rows = (await db.query(`SELECT * FROM crm_deal WHERE id = ?`, [id]))
253
- .rows as unknown as DealRow[];
254
-
255
- if (!rows[0]) {
256
- throw new Error('Failed to create deal');
257
- }
258
-
259
- return rowToDeal(rows[0]);
260
- }
261
-
262
- /**
263
- * Move a deal to a different stage
264
- */
265
- async function moveDeal(input: MoveDealInput): Promise<Deal> {
266
- const now = new Date().toISOString();
267
-
268
- // Verify deal exists
269
- const existing = (
270
- await db.query(`SELECT * FROM crm_deal WHERE id = ?`, [input.dealId])
271
- ).rows as unknown as DealRow[];
272
-
273
- if (!existing[0]) {
274
- throw new Error('NOT_FOUND');
275
- }
276
-
277
- // Verify stage exists
278
- const stage = (
279
- await db.query(`SELECT * FROM crm_stage WHERE id = ?`, [input.stageId])
280
- ).rows as unknown as StageRow[];
281
-
282
- if (!stage[0]) {
283
- throw new Error('INVALID_STAGE');
284
- }
285
-
286
- await db.execute(
287
- `UPDATE crm_deal SET stageId = ?, updatedAt = ? WHERE id = ?`,
288
- [input.stageId, now, input.dealId]
289
- );
290
-
291
- const rows = (
292
- await db.query(`SELECT * FROM crm_deal WHERE id = ?`, [input.dealId])
293
- ).rows as unknown as DealRow[];
294
-
295
- return rowToDeal(rows[0]!);
296
- }
297
-
298
- /**
299
- * Mark a deal as won
300
- */
301
- async function winDeal(input: WinDealInput): Promise<Deal> {
302
- const now = new Date().toISOString();
303
-
304
- // Verify deal exists
305
- const existing = (
306
- await db.query(`SELECT * FROM crm_deal WHERE id = ?`, [input.dealId])
307
- ).rows as unknown as DealRow[];
308
-
309
- if (!existing[0]) {
310
- throw new Error('NOT_FOUND');
311
- }
312
-
313
- await db.execute(
314
- `UPDATE crm_deal SET status = 'WON', wonSource = ?, notes = ?, updatedAt = ? WHERE id = ?`,
315
- [input.wonSource ?? null, input.notes ?? null, now, input.dealId]
316
- );
317
-
318
- const rows = (
319
- await db.query(`SELECT * FROM crm_deal WHERE id = ?`, [input.dealId])
320
- ).rows as unknown as DealRow[];
321
-
322
- return rowToDeal(rows[0]!);
323
- }
324
-
325
- /**
326
- * Mark a deal as lost
327
- */
328
- async function loseDeal(input: LoseDealInput): Promise<Deal> {
329
- const now = new Date().toISOString();
330
-
331
- // Verify deal exists
332
- const existing = (
333
- await db.query(`SELECT * FROM crm_deal WHERE id = ?`, [input.dealId])
334
- ).rows as unknown as DealRow[];
335
-
336
- if (!existing[0]) {
337
- throw new Error('NOT_FOUND');
338
- }
339
-
340
- await db.execute(
341
- `UPDATE crm_deal SET status = 'LOST', lostReason = ?, notes = ?, updatedAt = ? WHERE id = ?`,
342
- [input.lostReason, input.notes ?? null, now, input.dealId]
343
- );
344
-
345
- const rows = (
346
- await db.query(`SELECT * FROM crm_deal WHERE id = ?`, [input.dealId])
347
- ).rows as unknown as DealRow[];
348
-
349
- return rowToDeal(rows[0]!);
350
- }
351
-
352
- /**
353
- * Get deals grouped by stage
354
- */
355
- async function getDealsByStage(input: {
356
- projectId: string;
357
- pipelineId: string;
358
- }): Promise<Record<string, Deal[]>> {
359
- const deals = (
360
- await db.query(
361
- `SELECT * FROM crm_deal WHERE projectId = ? AND pipelineId = ? AND status = 'OPEN' ORDER BY value DESC`,
362
- [input.projectId, input.pipelineId]
363
- )
364
- ).rows as unknown as DealRow[];
365
-
366
- const stages = (
367
- await db.query(
368
- `SELECT * FROM crm_stage WHERE pipelineId = ? ORDER BY position`,
369
- [input.pipelineId]
370
- )
371
- ).rows as unknown as StageRow[];
372
-
373
- const grouped: Record<string, Deal[]> = {};
374
- for (const stage of stages) {
375
- grouped[stage.id] = deals
376
- .filter((d) => d.stageId === stage.id)
377
- .map(rowToDeal);
378
- }
379
-
380
- return grouped;
381
- }
382
-
383
- /**
384
- * Get pipeline stages
385
- */
386
- async function getPipelineStages(input: {
387
- pipelineId: string;
388
- }): Promise<Stage[]> {
389
- const rows = (
390
- await db.query(
391
- `SELECT * FROM crm_stage WHERE pipelineId = ? ORDER BY position`,
392
- [input.pipelineId]
393
- )
394
- ).rows as unknown as StageRow[];
395
-
396
- return rows.map((row) => ({
397
- id: row.id,
398
- pipelineId: row.pipelineId,
399
- name: row.name,
400
- position: row.position,
401
- }));
402
- }
403
-
404
- return {
405
- listDeals,
406
- createDeal,
407
- moveDeal,
408
- winDeal,
409
- loseDeal,
410
- getDealsByStage,
411
- getPipelineStages,
412
- };
252
+ [
253
+ id,
254
+ context.projectId,
255
+ input.pipelineId,
256
+ input.stageId,
257
+ input.name,
258
+ input.value,
259
+ input.currency ?? 'USD',
260
+ 'OPEN',
261
+ input.contactId ?? null,
262
+ input.companyId ?? null,
263
+ context.ownerId,
264
+ input.expectedCloseDate?.toISOString() ?? null,
265
+ now,
266
+ now,
267
+ ]
268
+ );
269
+
270
+ const rows = (await db.query(`SELECT * FROM crm_deal WHERE id = ?`, [id]))
271
+ .rows as unknown as DealRow[];
272
+
273
+ if (!rows[0]) {
274
+ throw new Error('Failed to create deal');
275
+ }
276
+
277
+ return rowToDeal(rows[0]);
278
+ }
279
+
280
+ /**
281
+ * Move a deal to a different stage
282
+ */
283
+ async function moveDeal(input: MoveDealInput): Promise<Deal> {
284
+ const now = new Date().toISOString();
285
+
286
+ // Verify deal exists
287
+ const existing = (
288
+ await db.query(`SELECT * FROM crm_deal WHERE id = ?`, [input.dealId])
289
+ ).rows as unknown as DealRow[];
290
+
291
+ if (!existing[0]) {
292
+ throw new Error('NOT_FOUND');
293
+ }
294
+
295
+ // Verify stage exists
296
+ const stage = (
297
+ await db.query(`SELECT * FROM crm_stage WHERE id = ?`, [input.stageId])
298
+ ).rows as unknown as StageRow[];
299
+
300
+ if (!stage[0]) {
301
+ throw new Error('INVALID_STAGE');
302
+ }
303
+
304
+ await db.execute(
305
+ `UPDATE crm_deal SET stageId = ?, updatedAt = ? WHERE id = ?`,
306
+ [input.stageId, now, input.dealId]
307
+ );
308
+
309
+ const rows = (
310
+ await db.query(`SELECT * FROM crm_deal WHERE id = ?`, [input.dealId])
311
+ ).rows as unknown as DealRow[];
312
+
313
+ return rowToDeal(rows[0]!);
314
+ }
315
+
316
+ /**
317
+ * Mark a deal as won
318
+ */
319
+ async function winDeal(input: WinDealInput): Promise<Deal> {
320
+ const now = new Date().toISOString();
321
+
322
+ // Verify deal exists
323
+ const existing = (
324
+ await db.query(`SELECT * FROM crm_deal WHERE id = ?`, [input.dealId])
325
+ ).rows as unknown as DealRow[];
326
+
327
+ if (!existing[0]) {
328
+ throw new Error('NOT_FOUND');
329
+ }
330
+
331
+ await db.execute(
332
+ `UPDATE crm_deal SET status = 'WON', wonSource = ?, notes = ?, updatedAt = ? WHERE id = ?`,
333
+ [input.wonSource ?? null, input.notes ?? null, now, input.dealId]
334
+ );
335
+
336
+ const rows = (
337
+ await db.query(`SELECT * FROM crm_deal WHERE id = ?`, [input.dealId])
338
+ ).rows as unknown as DealRow[];
339
+
340
+ return rowToDeal(rows[0]!);
341
+ }
342
+
343
+ /**
344
+ * Mark a deal as lost
345
+ */
346
+ async function loseDeal(input: LoseDealInput): Promise<Deal> {
347
+ const now = new Date().toISOString();
348
+
349
+ // Verify deal exists
350
+ const existing = (
351
+ await db.query(`SELECT * FROM crm_deal WHERE id = ?`, [input.dealId])
352
+ ).rows as unknown as DealRow[];
353
+
354
+ if (!existing[0]) {
355
+ throw new Error('NOT_FOUND');
356
+ }
357
+
358
+ await db.execute(
359
+ `UPDATE crm_deal SET status = 'LOST', lostReason = ?, notes = ?, updatedAt = ? WHERE id = ?`,
360
+ [input.lostReason, input.notes ?? null, now, input.dealId]
361
+ );
362
+
363
+ const rows = (
364
+ await db.query(`SELECT * FROM crm_deal WHERE id = ?`, [input.dealId])
365
+ ).rows as unknown as DealRow[];
366
+
367
+ return rowToDeal(rows[0]!);
368
+ }
369
+
370
+ /**
371
+ * Get deals grouped by stage
372
+ */
373
+ async function getDealsByStage(input: {
374
+ projectId: string;
375
+ pipelineId: string;
376
+ }): Promise<Record<string, Deal[]>> {
377
+ const deals = (
378
+ await db.query(
379
+ `SELECT * FROM crm_deal WHERE projectId = ? AND pipelineId = ? AND status = 'OPEN' ORDER BY value DESC`,
380
+ [input.projectId, input.pipelineId]
381
+ )
382
+ ).rows as unknown as DealRow[];
383
+
384
+ const stages = (
385
+ await db.query(
386
+ `SELECT * FROM crm_stage WHERE pipelineId = ? ORDER BY position`,
387
+ [input.pipelineId]
388
+ )
389
+ ).rows as unknown as StageRow[];
390
+
391
+ const grouped: Record<string, Deal[]> = {};
392
+ for (const stage of stages) {
393
+ grouped[stage.id] = deals
394
+ .filter((d) => d.stageId === stage.id)
395
+ .map(rowToDeal);
396
+ }
397
+
398
+ return grouped;
399
+ }
400
+
401
+ /**
402
+ * Get pipeline stages
403
+ */
404
+ async function getPipelineStages(input: {
405
+ pipelineId: string;
406
+ }): Promise<Stage[]> {
407
+ const rows = (
408
+ await db.query(
409
+ `SELECT * FROM crm_stage WHERE pipelineId = ? ORDER BY position`,
410
+ [input.pipelineId]
411
+ )
412
+ ).rows as unknown as StageRow[];
413
+
414
+ return rows.map((row) => ({
415
+ id: row.id,
416
+ pipelineId: row.pipelineId,
417
+ name: row.name,
418
+ position: row.position,
419
+ }));
420
+ }
421
+
422
+ return {
423
+ listDeals,
424
+ createDeal,
425
+ moveDeal,
426
+ winDeal,
427
+ loseDeal,
428
+ getDealsByStage,
429
+ getPipelineStages,
430
+ };
413
431
  }
414
432
 
415
433
  export type CrmHandlers = ReturnType<typeof createCrmHandlers>;