@contractspec/example.crm-pipeline 3.7.6 → 3.7.7
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 +8 -8
- package/AGENTS.md +51 -33
- package/README.md +66 -148
- 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/index.js +293 -293
- package/dist/browser/ui/CrmDashboard.js +221 -221
- package/dist/browser/ui/CrmDealCard.js +5 -5
- package/dist/browser/ui/CrmPipelineBoard.js +13 -13
- package/dist/browser/ui/hooks/index.js +2 -2
- package/dist/browser/ui/hooks/useDealList.js +1 -1
- package/dist/browser/ui/hooks/useDealMutations.js +1 -1
- package/dist/browser/ui/index.js +290 -290
- 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 +116 -116
- package/dist/browser/ui/renderers/pipeline.renderer.js +97 -97
- package/dist/deal/index.d.ts +2 -2
- 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/index.d.ts +2 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +293 -293
- 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/index.js +293 -293
- package/dist/node/ui/CrmDashboard.js +221 -221
- package/dist/node/ui/CrmDealCard.js +5 -5
- package/dist/node/ui/CrmPipelineBoard.js +13 -13
- package/dist/node/ui/hooks/index.js +2 -2
- package/dist/node/ui/hooks/useDealList.js +1 -1
- package/dist/node/ui/hooks/useDealMutations.js +1 -1
- package/dist/node/ui/index.js +290 -290
- 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 +116 -116
- package/dist/node/ui/renderers/pipeline.renderer.js +97 -97
- package/dist/operations/index.d.ts +1 -1
- package/dist/ui/CrmDashboard.js +221 -221
- 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 +2 -2
- package/dist/ui/hooks/useDealList.js +1 -1
- 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 +290 -290
- 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 +116 -116
- package/dist/ui/renderers/pipeline.renderer.d.ts +1 -1
- package/dist/ui/renderers/pipeline.renderer.js +97 -97
- package/package.json +10 -10
- 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 +43 -43
- 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 +358 -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 +256 -256
- 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 +85 -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/tsconfig.json +7 -8
- package/tsdown.config.js +7 -3
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { Button } from '@contractspec/lib.design-system';
|
|
3
4
|
/**
|
|
4
5
|
* DealActionsModal - Actions for a specific deal (Win, Lose, Move)
|
|
5
6
|
*
|
|
@@ -7,418 +8,417 @@
|
|
|
7
8
|
* via useDealMutations hook.
|
|
8
9
|
*/
|
|
9
10
|
import { useState } from 'react';
|
|
10
|
-
import { Button } from '@contractspec/lib.design-system';
|
|
11
11
|
|
|
12
12
|
// Local type definitions for modal props
|
|
13
13
|
export interface Deal {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
value: number;
|
|
17
|
+
currency: string;
|
|
18
|
+
stageId: string;
|
|
19
|
+
status: 'OPEN' | 'WON' | 'LOST' | 'STALE';
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export interface WinDealInput {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
dealId: string;
|
|
24
|
+
wonSource?: string;
|
|
25
|
+
notes?: string;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export interface LoseDealInput {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
dealId: string;
|
|
30
|
+
lostReason: string;
|
|
31
|
+
notes?: string;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export interface MoveDealInput {
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
dealId: string;
|
|
36
|
+
stageId: string;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
type ActionMode = 'menu' | 'win' | 'lose' | 'move';
|
|
40
40
|
|
|
41
41
|
interface DealActionsModalProps {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
42
|
+
isOpen: boolean;
|
|
43
|
+
deal: Deal | null;
|
|
44
|
+
stages: { id: string; name: string }[];
|
|
45
|
+
onClose: () => void;
|
|
46
|
+
onWin: (input: WinDealInput) => Promise<void>;
|
|
47
|
+
onLose: (input: LoseDealInput) => Promise<void>;
|
|
48
|
+
onMove: (input: MoveDealInput) => Promise<void>;
|
|
49
|
+
isLoading?: boolean;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
function formatCurrency(value: number, currency: string): string {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
return new Intl.NumberFormat('en-US', {
|
|
54
|
+
style: 'currency',
|
|
55
|
+
currency,
|
|
56
|
+
minimumFractionDigits: 0,
|
|
57
|
+
maximumFractionDigits: 0,
|
|
58
|
+
}).format(value);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
export function DealActionsModal({
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
62
|
+
isOpen,
|
|
63
|
+
deal,
|
|
64
|
+
stages,
|
|
65
|
+
onClose,
|
|
66
|
+
onWin,
|
|
67
|
+
onLose,
|
|
68
|
+
onMove,
|
|
69
|
+
isLoading = false,
|
|
70
70
|
}: DealActionsModalProps) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
233
|
-
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
71
|
+
const [mode, setMode] = useState<ActionMode>('menu');
|
|
72
|
+
const [wonSource, setWonSource] = useState('');
|
|
73
|
+
const [lostReason, setLostReason] = useState('');
|
|
74
|
+
const [notes, setNotes] = useState('');
|
|
75
|
+
const [selectedStageId, setSelectedStageId] = useState('');
|
|
76
|
+
const [error, setError] = useState<string | null>(null);
|
|
77
|
+
|
|
78
|
+
const resetForm = () => {
|
|
79
|
+
setMode('menu');
|
|
80
|
+
setWonSource('');
|
|
81
|
+
setLostReason('');
|
|
82
|
+
setNotes('');
|
|
83
|
+
setSelectedStageId('');
|
|
84
|
+
setError(null);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const handleClose = () => {
|
|
88
|
+
resetForm();
|
|
89
|
+
onClose();
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const handleWin = async () => {
|
|
93
|
+
if (!deal) return;
|
|
94
|
+
setError(null);
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
await onWin({
|
|
98
|
+
dealId: deal.id,
|
|
99
|
+
wonSource: wonSource.trim() || undefined,
|
|
100
|
+
notes: notes.trim() || undefined,
|
|
101
|
+
});
|
|
102
|
+
handleClose();
|
|
103
|
+
} catch (err) {
|
|
104
|
+
setError(
|
|
105
|
+
err instanceof Error ? err.message : 'Failed to mark deal as won'
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const handleLose = async () => {
|
|
111
|
+
if (!deal) return;
|
|
112
|
+
setError(null);
|
|
113
|
+
|
|
114
|
+
if (!lostReason.trim()) {
|
|
115
|
+
setError('Please provide a reason for losing the deal');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
await onLose({
|
|
121
|
+
dealId: deal.id,
|
|
122
|
+
lostReason: lostReason.trim(),
|
|
123
|
+
notes: notes.trim() || undefined,
|
|
124
|
+
});
|
|
125
|
+
handleClose();
|
|
126
|
+
} catch (err) {
|
|
127
|
+
setError(
|
|
128
|
+
err instanceof Error ? err.message : 'Failed to mark deal as lost'
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const handleMove = async () => {
|
|
134
|
+
if (!deal) return;
|
|
135
|
+
setError(null);
|
|
136
|
+
|
|
137
|
+
if (!selectedStageId) {
|
|
138
|
+
setError('Please select a stage');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (selectedStageId === deal.stageId) {
|
|
143
|
+
setError('Deal is already in this stage');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
await onMove({
|
|
149
|
+
dealId: deal.id,
|
|
150
|
+
stageId: selectedStageId,
|
|
151
|
+
});
|
|
152
|
+
handleClose();
|
|
153
|
+
} catch (err) {
|
|
154
|
+
setError(err instanceof Error ? err.message : 'Failed to move deal');
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
if (!isOpen || !deal) return null;
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
162
|
+
{/* Backdrop */}
|
|
163
|
+
<div
|
|
164
|
+
className="absolute inset-0 bg-background/80 backdrop-blur-sm"
|
|
165
|
+
onClick={handleClose}
|
|
166
|
+
role="button"
|
|
167
|
+
tabIndex={0}
|
|
168
|
+
onKeyDown={(e) => {
|
|
169
|
+
if (e.key === 'Enter' || e.key === ' ') handleClose();
|
|
170
|
+
}}
|
|
171
|
+
aria-label="Close modal"
|
|
172
|
+
/>
|
|
173
|
+
|
|
174
|
+
{/* Modal */}
|
|
175
|
+
<div className="relative z-10 w-full max-w-md rounded-xl border border-border bg-card p-6 shadow-xl">
|
|
176
|
+
{/* Deal Header */}
|
|
177
|
+
<div className="mb-4 border-border border-b pb-4">
|
|
178
|
+
<h2 className="font-semibold text-xl">{deal.name}</h2>
|
|
179
|
+
<p className="font-medium text-lg text-primary">
|
|
180
|
+
{formatCurrency(deal.value, deal.currency)}
|
|
181
|
+
</p>
|
|
182
|
+
<span
|
|
183
|
+
className={`mt-2 inline-flex rounded-full px-2 py-0.5 font-medium text-xs ${
|
|
184
|
+
deal.status === 'WON'
|
|
185
|
+
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
|
|
186
|
+
: deal.status === 'LOST'
|
|
187
|
+
? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
|
|
188
|
+
: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
|
|
189
|
+
}`}
|
|
190
|
+
>
|
|
191
|
+
{deal.status}
|
|
192
|
+
</span>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
{/* Main Menu */}
|
|
196
|
+
{mode === 'menu' && (
|
|
197
|
+
<div className="space-y-3">
|
|
198
|
+
{deal.status === 'OPEN' && (
|
|
199
|
+
<>
|
|
200
|
+
<Button
|
|
201
|
+
className="w-full justify-start"
|
|
202
|
+
variant="ghost"
|
|
203
|
+
onPress={() => setMode('win')}
|
|
204
|
+
>
|
|
205
|
+
<span className="mr-2">🏆</span> Mark as Won
|
|
206
|
+
</Button>
|
|
207
|
+
<Button
|
|
208
|
+
className="w-full justify-start"
|
|
209
|
+
variant="ghost"
|
|
210
|
+
onPress={() => setMode('lose')}
|
|
211
|
+
>
|
|
212
|
+
<span className="mr-2">❌</span> Mark as Lost
|
|
213
|
+
</Button>
|
|
214
|
+
<Button
|
|
215
|
+
className="w-full justify-start"
|
|
216
|
+
variant="ghost"
|
|
217
|
+
onPress={() => {
|
|
218
|
+
setSelectedStageId(deal.stageId);
|
|
219
|
+
setMode('move');
|
|
220
|
+
}}
|
|
221
|
+
>
|
|
222
|
+
<span className="mr-2">➡️</span> Move to Stage
|
|
223
|
+
</Button>
|
|
224
|
+
</>
|
|
225
|
+
)}
|
|
226
|
+
{deal.status !== 'OPEN' && (
|
|
227
|
+
<p className="py-4 text-center text-muted-foreground">
|
|
228
|
+
This deal is already {deal.status.toLowerCase()}. No actions
|
|
229
|
+
available.
|
|
230
|
+
</p>
|
|
231
|
+
)}
|
|
232
|
+
<div className="border-border border-t pt-3">
|
|
233
|
+
<Button
|
|
234
|
+
className="w-full"
|
|
235
|
+
variant="outline"
|
|
236
|
+
onPress={handleClose}
|
|
237
|
+
>
|
|
238
|
+
Close
|
|
239
|
+
</Button>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
)}
|
|
243
|
+
|
|
244
|
+
{/* Win Form */}
|
|
245
|
+
{mode === 'win' && (
|
|
246
|
+
<div className="space-y-4">
|
|
247
|
+
<div>
|
|
248
|
+
<label
|
|
249
|
+
htmlFor="won-source"
|
|
250
|
+
className="mb-1 block font-medium text-muted-foreground text-sm"
|
|
251
|
+
>
|
|
252
|
+
How did you win this deal?
|
|
253
|
+
</label>
|
|
254
|
+
<select
|
|
255
|
+
id="won-source"
|
|
256
|
+
value={wonSource}
|
|
257
|
+
onChange={(e) => setWonSource(e.target.value)}
|
|
258
|
+
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"
|
|
259
|
+
>
|
|
260
|
+
<option value="">Select a source...</option>
|
|
261
|
+
<option value="referral">Referral</option>
|
|
262
|
+
<option value="cold_outreach">Cold Outreach</option>
|
|
263
|
+
<option value="inbound">Inbound Lead</option>
|
|
264
|
+
<option value="upsell">Upsell</option>
|
|
265
|
+
<option value="other">Other</option>
|
|
266
|
+
</select>
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
<div>
|
|
270
|
+
<label
|
|
271
|
+
htmlFor="win-notes"
|
|
272
|
+
className="mb-1 block font-medium text-muted-foreground text-sm"
|
|
273
|
+
>
|
|
274
|
+
Notes (optional)
|
|
275
|
+
</label>
|
|
276
|
+
<textarea
|
|
277
|
+
id="win-notes"
|
|
278
|
+
value={notes}
|
|
279
|
+
onChange={(e) => setNotes(e.target.value)}
|
|
280
|
+
placeholder="Any additional notes about the win..."
|
|
281
|
+
rows={3}
|
|
282
|
+
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"
|
|
283
|
+
/>
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
{error && (
|
|
287
|
+
<div className="rounded-md bg-destructive/10 p-3 text-destructive text-sm">
|
|
288
|
+
{error}
|
|
289
|
+
</div>
|
|
290
|
+
)}
|
|
291
|
+
|
|
292
|
+
<div className="flex justify-end gap-3 pt-2">
|
|
293
|
+
<Button
|
|
294
|
+
variant="ghost"
|
|
295
|
+
onPress={() => setMode('menu')}
|
|
296
|
+
disabled={isLoading}
|
|
297
|
+
>
|
|
298
|
+
Back
|
|
299
|
+
</Button>
|
|
300
|
+
<Button onPress={handleWin} disabled={isLoading}>
|
|
301
|
+
{isLoading ? 'Processing...' : '🏆 Confirm Win'}
|
|
302
|
+
</Button>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
)}
|
|
306
|
+
|
|
307
|
+
{/* Lose Form */}
|
|
308
|
+
{mode === 'lose' && (
|
|
309
|
+
<div className="space-y-4">
|
|
310
|
+
<div>
|
|
311
|
+
<label
|
|
312
|
+
htmlFor="lost-reason"
|
|
313
|
+
className="mb-1 block font-medium text-muted-foreground text-sm"
|
|
314
|
+
>
|
|
315
|
+
Why was this deal lost? *
|
|
316
|
+
</label>
|
|
317
|
+
<select
|
|
318
|
+
id="lost-reason"
|
|
319
|
+
value={lostReason}
|
|
320
|
+
onChange={(e) => setLostReason(e.target.value)}
|
|
321
|
+
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"
|
|
322
|
+
>
|
|
323
|
+
<option value="">Select a reason...</option>
|
|
324
|
+
<option value="price">Price too high</option>
|
|
325
|
+
<option value="competitor">Lost to competitor</option>
|
|
326
|
+
<option value="no_budget">No budget</option>
|
|
327
|
+
<option value="no_decision">No decision made</option>
|
|
328
|
+
<option value="timing">Bad timing</option>
|
|
329
|
+
<option value="product_fit">Product not a fit</option>
|
|
330
|
+
<option value="other">Other</option>
|
|
331
|
+
</select>
|
|
332
|
+
</div>
|
|
333
|
+
|
|
334
|
+
<div>
|
|
335
|
+
<label
|
|
336
|
+
htmlFor="lose-notes"
|
|
337
|
+
className="mb-1 block font-medium text-muted-foreground text-sm"
|
|
338
|
+
>
|
|
339
|
+
Notes (optional)
|
|
340
|
+
</label>
|
|
341
|
+
<textarea
|
|
342
|
+
id="lose-notes"
|
|
343
|
+
value={notes}
|
|
344
|
+
onChange={(e) => setNotes(e.target.value)}
|
|
345
|
+
placeholder="Any additional details..."
|
|
346
|
+
rows={3}
|
|
347
|
+
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"
|
|
348
|
+
/>
|
|
349
|
+
</div>
|
|
350
|
+
|
|
351
|
+
{error && (
|
|
352
|
+
<div className="rounded-md bg-destructive/10 p-3 text-destructive text-sm">
|
|
353
|
+
{error}
|
|
354
|
+
</div>
|
|
355
|
+
)}
|
|
356
|
+
|
|
357
|
+
<div className="flex justify-end gap-3 pt-2">
|
|
358
|
+
<Button
|
|
359
|
+
variant="ghost"
|
|
360
|
+
onPress={() => setMode('menu')}
|
|
361
|
+
disabled={isLoading}
|
|
362
|
+
>
|
|
363
|
+
Back
|
|
364
|
+
</Button>
|
|
365
|
+
<Button
|
|
366
|
+
variant="destructive"
|
|
367
|
+
onPress={handleLose}
|
|
368
|
+
disabled={isLoading}
|
|
369
|
+
>
|
|
370
|
+
{isLoading ? 'Processing...' : '❌ Confirm Loss'}
|
|
371
|
+
</Button>
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
374
|
+
)}
|
|
375
|
+
|
|
376
|
+
{/* Move Form */}
|
|
377
|
+
{mode === 'move' && (
|
|
378
|
+
<div className="space-y-4">
|
|
379
|
+
<div>
|
|
380
|
+
<label
|
|
381
|
+
htmlFor="move-stage"
|
|
382
|
+
className="mb-1 block font-medium text-muted-foreground text-sm"
|
|
383
|
+
>
|
|
384
|
+
Move to Stage
|
|
385
|
+
</label>
|
|
386
|
+
<select
|
|
387
|
+
id="move-stage"
|
|
388
|
+
value={selectedStageId}
|
|
389
|
+
onChange={(e) => setSelectedStageId(e.target.value)}
|
|
390
|
+
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"
|
|
391
|
+
>
|
|
392
|
+
{stages.map((stage) => (
|
|
393
|
+
<option key={stage.id} value={stage.id}>
|
|
394
|
+
{stage.name}
|
|
395
|
+
{stage.id === deal.stageId ? ' (current)' : ''}
|
|
396
|
+
</option>
|
|
397
|
+
))}
|
|
398
|
+
</select>
|
|
399
|
+
</div>
|
|
400
|
+
|
|
401
|
+
{error && (
|
|
402
|
+
<div className="rounded-md bg-destructive/10 p-3 text-destructive text-sm">
|
|
403
|
+
{error}
|
|
404
|
+
</div>
|
|
405
|
+
)}
|
|
406
|
+
|
|
407
|
+
<div className="flex justify-end gap-3 pt-2">
|
|
408
|
+
<Button
|
|
409
|
+
variant="ghost"
|
|
410
|
+
onPress={() => setMode('menu')}
|
|
411
|
+
disabled={isLoading}
|
|
412
|
+
>
|
|
413
|
+
Back
|
|
414
|
+
</Button>
|
|
415
|
+
<Button onPress={handleMove} disabled={isLoading}>
|
|
416
|
+
{isLoading ? 'Moving...' : '➡️ Move Deal'}
|
|
417
|
+
</Button>
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
)}
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
);
|
|
424
424
|
}
|