@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.
- package/.turbo/turbo-build.log +45 -42
- package/AGENTS.md +51 -33
- package/CHANGELOG.md +36 -0
- package/README.md +67 -148
- package/dist/browser/docs/crm-pipeline.docblock.js +1 -1
- package/dist/browser/docs/index.js +1 -1
- package/dist/browser/events/contact.event.js +1 -1
- package/dist/browser/events/deal.event.js +1 -1
- package/dist/browser/events/index.js +3 -3
- package/dist/browser/events/task.event.js +1 -1
- package/dist/browser/handlers/crm.handlers.js +13 -2
- package/dist/browser/handlers/index.js +13 -2
- package/dist/browser/index.js +680 -447
- package/dist/browser/ui/CrmDashboard.js +574 -352
- package/dist/browser/ui/CrmDealCard.js +5 -5
- package/dist/browser/ui/CrmPipelineBoard.js +13 -13
- package/dist/browser/ui/hooks/index.js +21 -10
- package/dist/browser/ui/hooks/useDealList.js +20 -9
- package/dist/browser/ui/hooks/useDealMutations.js +1 -1
- package/dist/browser/ui/index.js +683 -450
- package/dist/browser/ui/modals/CreateDealModal.js +12 -12
- package/dist/browser/ui/modals/DealActionsModal.js +21 -21
- package/dist/browser/ui/modals/index.js +33 -33
- package/dist/browser/ui/renderers/index.js +140 -118
- package/dist/browser/ui/renderers/pipeline.markdown.js +13 -2
- package/dist/browser/ui/renderers/pipeline.renderer.js +108 -97
- package/dist/browser/ui/tables/DealListTab.js +390 -0
- package/dist/deal/index.d.ts +2 -2
- package/dist/docs/crm-pipeline.docblock.js +1 -1
- package/dist/docs/index.js +1 -1
- package/dist/events/contact.event.js +1 -1
- package/dist/events/deal.event.js +1 -1
- package/dist/events/index.js +3 -3
- package/dist/events/task.event.js +1 -1
- package/dist/handlers/crm.handlers.d.ts +2 -0
- package/dist/handlers/crm.handlers.js +13 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +13 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +680 -447
- package/dist/node/docs/crm-pipeline.docblock.js +1 -1
- package/dist/node/docs/index.js +1 -1
- package/dist/node/events/contact.event.js +1 -1
- package/dist/node/events/deal.event.js +1 -1
- package/dist/node/events/index.js +3 -3
- package/dist/node/events/task.event.js +1 -1
- package/dist/node/handlers/crm.handlers.js +13 -2
- package/dist/node/handlers/index.js +13 -2
- package/dist/node/index.js +680 -447
- package/dist/node/ui/CrmDashboard.js +574 -352
- package/dist/node/ui/CrmDealCard.js +5 -5
- package/dist/node/ui/CrmPipelineBoard.js +13 -13
- package/dist/node/ui/hooks/index.js +21 -10
- package/dist/node/ui/hooks/useDealList.js +20 -9
- package/dist/node/ui/hooks/useDealMutations.js +1 -1
- package/dist/node/ui/index.js +683 -450
- package/dist/node/ui/modals/CreateDealModal.js +12 -12
- package/dist/node/ui/modals/DealActionsModal.js +21 -21
- package/dist/node/ui/modals/index.js +33 -33
- package/dist/node/ui/renderers/index.js +140 -118
- package/dist/node/ui/renderers/pipeline.markdown.js +13 -2
- package/dist/node/ui/renderers/pipeline.renderer.js +108 -97
- package/dist/node/ui/tables/DealListTab.js +390 -0
- package/dist/operations/index.d.ts +1 -1
- package/dist/ui/CrmDashboard.js +574 -352
- package/dist/ui/CrmDealCard.js +5 -5
- package/dist/ui/CrmPipelineBoard.js +13 -13
- package/dist/ui/hooks/index.d.ts +2 -2
- package/dist/ui/hooks/index.js +21 -10
- package/dist/ui/hooks/useDealList.d.ts +8 -2
- package/dist/ui/hooks/useDealList.js +20 -9
- package/dist/ui/hooks/useDealMutations.d.ts +9 -0
- package/dist/ui/hooks/useDealMutations.js +1 -1
- package/dist/ui/index.d.ts +3 -3
- package/dist/ui/index.js +683 -450
- package/dist/ui/modals/CreateDealModal.js +12 -12
- package/dist/ui/modals/DealActionsModal.js +21 -21
- package/dist/ui/modals/index.js +33 -33
- package/dist/ui/renderers/index.d.ts +1 -1
- package/dist/ui/renderers/index.js +140 -118
- package/dist/ui/renderers/pipeline.markdown.js +13 -2
- package/dist/ui/renderers/pipeline.renderer.d.ts +1 -1
- package/dist/ui/renderers/pipeline.renderer.js +108 -97
- package/dist/ui/tables/DealListTab.d.ts +20 -0
- package/dist/ui/tables/DealListTab.js +391 -0
- package/dist/ui/tables/DealListTab.smoke.test.d.ts +1 -0
- package/package.json +29 -14
- package/src/crm-pipeline.feature.ts +86 -86
- package/src/deal/deal.enum.ts +8 -8
- package/src/deal/deal.operation.ts +255 -255
- package/src/deal/deal.schema.ts +92 -92
- package/src/deal/deal.test-spec.ts +48 -48
- package/src/deal/index.ts +17 -19
- package/src/docs/crm-pipeline.docblock.ts +44 -44
- package/src/entities/company.entity.ts +52 -52
- package/src/entities/contact.entity.ts +67 -67
- package/src/entities/deal.entity.ts +134 -134
- package/src/entities/index.ts +27 -27
- package/src/entities/task.entity.ts +105 -105
- package/src/events/contact.event.ts +22 -22
- package/src/events/deal.event.ts +77 -77
- package/src/events/task.event.ts +19 -19
- package/src/example.ts +32 -32
- package/src/handlers/crm.handlers.ts +375 -357
- package/src/handlers/deal.handlers.ts +179 -179
- package/src/handlers/index.ts +18 -19
- package/src/handlers/mock-data.ts +167 -167
- package/src/index.ts +11 -11
- package/src/operations/index.ts +16 -16
- package/src/presentations/dashboard.presentation.ts +45 -45
- package/src/presentations/pipeline.presentation.ts +90 -90
- package/src/seeders/index.ts +26 -26
- package/src/shared/overlay-types.ts +23 -23
- package/src/ui/CrmDashboard.tsx +210 -279
- package/src/ui/CrmDealCard.tsx +64 -64
- package/src/ui/CrmPipelineBoard.tsx +105 -105
- package/src/ui/hooks/index.ts +3 -3
- package/src/ui/hooks/useDealList.ts +113 -85
- package/src/ui/hooks/useDealMutations.ts +151 -150
- package/src/ui/index.ts +5 -10
- package/src/ui/modals/CreateDealModal.tsx +217 -217
- package/src/ui/modals/DealActionsModal.tsx +390 -390
- package/src/ui/overlays/demo-overlays.ts +43 -43
- package/src/ui/renderers/index.ts +4 -3
- package/src/ui/renderers/pipeline.markdown.ts +165 -165
- package/src/ui/renderers/pipeline.renderer.tsx +17 -16
- package/src/ui/tables/DealListTab.smoke.test.tsx +149 -0
- package/src/ui/tables/DealListTab.tsx +276 -0
- package/tsconfig.json +7 -8
- 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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
id: string;
|
|
37
|
+
pipelineId: string;
|
|
38
|
+
name: string;
|
|
39
|
+
position: number;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
export interface CreateDealInput {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
54
|
-
|
|
54
|
+
dealId: string;
|
|
55
|
+
stageId: string;
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
export interface WinDealInput {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
dealId: string;
|
|
60
|
+
wonSource?: string;
|
|
61
|
+
notes?: string;
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
export interface LoseDealInput {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
dealId: string;
|
|
66
|
+
lostReason: string;
|
|
67
|
+
notes?: string;
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
export interface ListDealsInput {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
id: string;
|
|
116
|
+
pipelineId: string;
|
|
117
|
+
name: string;
|
|
118
|
+
position: number;
|
|
116
119
|
}
|
|
117
120
|
|
|
118
121
|
function rowToDeal(row: DealRow): Deal {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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>;
|