@contractspec/example.crm-pipeline 3.7.5 → 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/CHANGELOG.md +16 -0
- 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 +14 -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 +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
package/src/ui/CrmDealCard.tsx
CHANGED
|
@@ -6,78 +6,78 @@
|
|
|
6
6
|
import type { Deal } from './hooks/useDealList';
|
|
7
7
|
|
|
8
8
|
interface CrmDealCardProps {
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
deal: Deal;
|
|
10
|
+
onClick?: () => void;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
function formatCurrency(value: number, currency: string): string {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
return new Intl.NumberFormat('en-US', {
|
|
15
|
+
style: 'currency',
|
|
16
|
+
currency,
|
|
17
|
+
minimumFractionDigits: 0,
|
|
18
|
+
maximumFractionDigits: 0,
|
|
19
|
+
}).format(value);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export function CrmDealCard({ deal, onClick }: CrmDealCardProps) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
const daysUntilClose = deal.expectedCloseDate
|
|
24
|
+
? Math.ceil(
|
|
25
|
+
(deal.expectedCloseDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24)
|
|
26
|
+
)
|
|
27
|
+
: null;
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
29
|
+
return (
|
|
30
|
+
<div
|
|
31
|
+
onClick={onClick}
|
|
32
|
+
className="cursor-pointer rounded-lg border border-border bg-card p-3 shadow-sm transition-shadow hover:shadow-md"
|
|
33
|
+
role="button"
|
|
34
|
+
tabIndex={0}
|
|
35
|
+
onKeyDown={(e) => {
|
|
36
|
+
if (e.key === 'Enter' || e.key === ' ') onClick?.();
|
|
37
|
+
}}
|
|
38
|
+
>
|
|
39
|
+
{/* Deal Name */}
|
|
40
|
+
<h4 className="font-medium leading-snug">{deal.name}</h4>
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
{/* Deal Value */}
|
|
43
|
+
<div className="mt-2 font-semibold text-lg text-primary">
|
|
44
|
+
{formatCurrency(deal.value, deal.currency)}
|
|
45
|
+
</div>
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
47
|
+
{/* Meta Info */}
|
|
48
|
+
<div className="mt-3 flex items-center justify-between text-muted-foreground text-xs">
|
|
49
|
+
{/* Expected Close */}
|
|
50
|
+
{daysUntilClose !== null && (
|
|
51
|
+
<span
|
|
52
|
+
className={
|
|
53
|
+
daysUntilClose < 0
|
|
54
|
+
? 'text-red-500'
|
|
55
|
+
: daysUntilClose <= 7
|
|
56
|
+
? 'text-yellow-600 dark:text-yellow-500'
|
|
57
|
+
: ''
|
|
58
|
+
}
|
|
59
|
+
>
|
|
60
|
+
{daysUntilClose < 0
|
|
61
|
+
? `${Math.abs(daysUntilClose)}d overdue`
|
|
62
|
+
: daysUntilClose === 0
|
|
63
|
+
? 'Due today'
|
|
64
|
+
: `${daysUntilClose}d left`}
|
|
65
|
+
</span>
|
|
66
|
+
)}
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
68
|
+
{/* Status Badge */}
|
|
69
|
+
<span
|
|
70
|
+
className={`rounded px-1.5 py-0.5 font-medium text-xs ${
|
|
71
|
+
deal.status === 'WON'
|
|
72
|
+
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
|
|
73
|
+
: deal.status === 'LOST'
|
|
74
|
+
? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
|
|
75
|
+
: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
|
|
76
|
+
}`}
|
|
77
|
+
>
|
|
78
|
+
{deal.status}
|
|
79
|
+
</span>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
83
|
}
|
|
@@ -10,127 +10,127 @@
|
|
|
10
10
|
* - Drag-and-drop ready (UI only, no lib dependency)
|
|
11
11
|
*/
|
|
12
12
|
import { useState } from 'react';
|
|
13
|
+
import { CrmDealCard } from './CrmDealCard';
|
|
13
14
|
// import { Button } from '@contractspec/lib.design-system';
|
|
14
15
|
import type { Deal } from './hooks/useDealList';
|
|
15
|
-
import { CrmDealCard } from './CrmDealCard';
|
|
16
16
|
|
|
17
17
|
interface CrmPipelineBoardProps {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
dealsByStage: Record<string, Deal[]>;
|
|
19
|
+
stages: { id: string; name: string; position: number }[];
|
|
20
|
+
onDealClick?: (dealId: string) => void;
|
|
21
|
+
onDealMove?: (dealId: string, toStageId: string) => void;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
function formatCurrency(value: number): string {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
if (value >= 1000000) return `$${(value / 1000000).toFixed(1)}M`;
|
|
26
|
+
if (value >= 1000) return `$${(value / 1000).toFixed(0)}K`;
|
|
27
|
+
return `$${value}`;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export function CrmPipelineBoard({
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
dealsByStage,
|
|
32
|
+
stages,
|
|
33
|
+
onDealClick,
|
|
34
|
+
onDealMove,
|
|
35
35
|
}: CrmPipelineBoardProps) {
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
// Track which deal has the quick-move dropdown open
|
|
37
|
+
const [quickMoveOpen, setQuickMoveOpen] = useState<string | null>(null);
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
// Sort stages by position
|
|
40
|
+
const sortedStages = [...stages].sort((a, b) => a.position - b.position);
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
const handleQuickMove = (dealId: string, toStageId: string) => {
|
|
43
|
+
onDealMove?.(dealId, toStageId);
|
|
44
|
+
setQuickMoveOpen(null);
|
|
45
|
+
};
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
return (
|
|
48
|
+
<div className="flex gap-4 overflow-x-auto pb-4">
|
|
49
|
+
{sortedStages.map((stage) => {
|
|
50
|
+
const deals = dealsByStage[stage.id] ?? [];
|
|
51
|
+
const stageValue = deals.reduce((sum, d) => sum + d.value, 0);
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
53
|
+
return (
|
|
54
|
+
<div
|
|
55
|
+
key={stage.id}
|
|
56
|
+
className="flex w-72 flex-shrink-0 flex-col rounded-lg bg-muted/30"
|
|
57
|
+
>
|
|
58
|
+
{/* Stage Header */}
|
|
59
|
+
<div className="flex items-center justify-between border-border border-b px-3 py-2">
|
|
60
|
+
<div>
|
|
61
|
+
<h3 className="font-medium">{stage.name}</h3>
|
|
62
|
+
<p className="text-muted-foreground text-xs">
|
|
63
|
+
{deals.length} deals · {formatCurrency(stageValue)}
|
|
64
|
+
</p>
|
|
65
|
+
</div>
|
|
66
|
+
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-muted font-medium text-xs">
|
|
67
|
+
{deals.length}
|
|
68
|
+
</span>
|
|
69
|
+
</div>
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
71
|
+
{/* Deals Column */}
|
|
72
|
+
<div className="flex flex-1 flex-col gap-2 p-2">
|
|
73
|
+
{deals.length === 0 ? (
|
|
74
|
+
<div className="flex h-24 items-center justify-center rounded-md border-2 border-muted-foreground/20 border-dashed text-muted-foreground text-xs">
|
|
75
|
+
No deals
|
|
76
|
+
</div>
|
|
77
|
+
) : (
|
|
78
|
+
deals.map((deal) => (
|
|
79
|
+
<div key={deal.id} className="group relative">
|
|
80
|
+
<CrmDealCard
|
|
81
|
+
deal={deal}
|
|
82
|
+
onClick={() => onDealClick?.(deal.id)}
|
|
83
|
+
/>
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
85
|
+
{/* Quick Move Button */}
|
|
86
|
+
{deal.status === 'OPEN' && onDealMove && (
|
|
87
|
+
<div className="absolute top-1 right-1 opacity-0 transition-opacity group-hover:opacity-100">
|
|
88
|
+
<button
|
|
89
|
+
type="button"
|
|
90
|
+
onClick={(e) => {
|
|
91
|
+
e.stopPropagation();
|
|
92
|
+
setQuickMoveOpen(
|
|
93
|
+
quickMoveOpen === deal.id ? null : deal.id
|
|
94
|
+
);
|
|
95
|
+
}}
|
|
96
|
+
className="flex h-6 w-6 items-center justify-center rounded border border-border bg-background text-xs shadow-sm hover:bg-muted"
|
|
97
|
+
title="Quick move"
|
|
98
|
+
>
|
|
99
|
+
➡️
|
|
100
|
+
</button>
|
|
101
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
|
-
|
|
102
|
+
{/* Quick Move Dropdown */}
|
|
103
|
+
{quickMoveOpen === deal.id && (
|
|
104
|
+
<div className="absolute top-7 right-0 z-20 min-w-[140px] rounded-lg border border-border bg-card py-1 shadow-lg">
|
|
105
|
+
<p className="px-3 py-1 font-medium text-muted-foreground text-xs">
|
|
106
|
+
Move to:
|
|
107
|
+
</p>
|
|
108
|
+
{sortedStages
|
|
109
|
+
.filter((s) => s.id !== deal.stageId)
|
|
110
|
+
.map((s) => (
|
|
111
|
+
<button
|
|
112
|
+
key={s.id}
|
|
113
|
+
type="button"
|
|
114
|
+
onClick={(e) => {
|
|
115
|
+
e.stopPropagation();
|
|
116
|
+
handleQuickMove(deal.id, s.id);
|
|
117
|
+
}}
|
|
118
|
+
className="w-full px-3 py-1.5 text-left text-sm hover:bg-muted"
|
|
119
|
+
>
|
|
120
|
+
{s.name}
|
|
121
|
+
</button>
|
|
122
|
+
))}
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
))
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
})}
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
136
|
}
|
package/src/ui/hooks/index.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export { type UseDealListOptions, useDealList } from './useDealList';
|
|
4
4
|
export {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
type UseDealMutationsOptions,
|
|
6
|
+
useDealMutations,
|
|
7
7
|
} from './useDealMutations';
|
|
8
8
|
|
|
9
9
|
// Note: For deal types (CreateDealInput, MoveDealInput, etc.), import directly from:
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { useTemplateRuntime } from '@contractspec/lib.example-shared-ui';
|
|
3
4
|
/**
|
|
4
5
|
* Hook for fetching and managing deal list data
|
|
5
6
|
*
|
|
6
7
|
* Uses runtime-local database-backed handlers.
|
|
7
8
|
*/
|
|
8
9
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
9
|
-
import { useTemplateRuntime } from '@contractspec/lib.example-shared-ui';
|
|
10
10
|
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
type CrmHandlers,
|
|
12
|
+
type Deal as RuntimeDeal,
|
|
13
|
+
type ListDealsOutput as RuntimeListDealsOutput,
|
|
14
|
+
type Stage,
|
|
15
15
|
} from '../../handlers/crm.handlers';
|
|
16
16
|
|
|
17
17
|
// Re-export types for convenience
|
|
@@ -19,95 +19,95 @@ export type Deal = RuntimeDeal;
|
|
|
19
19
|
export type ListDealsOutput = RuntimeListDealsOutput;
|
|
20
20
|
|
|
21
21
|
export interface UseDealListOptions {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
pipelineId?: string;
|
|
23
|
+
stageId?: string;
|
|
24
|
+
status?: 'OPEN' | 'WON' | 'LOST' | 'all';
|
|
25
|
+
search?: string;
|
|
26
|
+
limit?: number;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export function useDealList(options: UseDealListOptions = {}) {
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
const { handlers, projectId } = useTemplateRuntime<{ crm: CrmHandlers }>();
|
|
31
|
+
const { crm } = handlers;
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
const [data, setData] = useState<ListDealsOutput | null>(null);
|
|
34
|
+
const [dealsByStage, setDealsByStage] = useState<Record<string, Deal[]>>({});
|
|
35
|
+
const [stages, setStages] = useState<Stage[]>([]);
|
|
36
|
+
const [loading, setLoading] = useState(true);
|
|
37
|
+
const [error, setError] = useState<Error | null>(null);
|
|
38
|
+
const [page, setPage] = useState(1);
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
const pipelineId = options.pipelineId ?? 'pipeline-1';
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
const fetchData = useCallback(async () => {
|
|
43
|
+
setLoading(true);
|
|
44
|
+
setError(null);
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
46
|
+
try {
|
|
47
|
+
const [dealsResult, stageDealsResult, stagesResult] = await Promise.all([
|
|
48
|
+
crm.listDeals({
|
|
49
|
+
projectId,
|
|
50
|
+
pipelineId,
|
|
51
|
+
stageId: options.stageId,
|
|
52
|
+
status: options.status === 'all' ? undefined : options.status,
|
|
53
|
+
search: options.search,
|
|
54
|
+
limit: options.limit ?? 50,
|
|
55
|
+
offset: (page - 1) * (options.limit ?? 50),
|
|
56
|
+
}),
|
|
57
|
+
crm.getDealsByStage({ projectId, pipelineId }),
|
|
58
|
+
crm.getPipelineStages({ pipelineId }),
|
|
59
|
+
]);
|
|
60
|
+
setData(dealsResult);
|
|
61
|
+
setDealsByStage(stageDealsResult);
|
|
62
|
+
setStages(stagesResult);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
setError(err instanceof Error ? err : new Error('Unknown error'));
|
|
65
|
+
} finally {
|
|
66
|
+
setLoading(false);
|
|
67
|
+
}
|
|
68
|
+
}, [
|
|
69
|
+
crm,
|
|
70
|
+
projectId,
|
|
71
|
+
pipelineId,
|
|
72
|
+
options.stageId,
|
|
73
|
+
options.status,
|
|
74
|
+
options.search,
|
|
75
|
+
options.limit,
|
|
76
|
+
page,
|
|
77
|
+
]);
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
fetchData();
|
|
81
|
+
}, [fetchData]);
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
// Calculate stats
|
|
84
|
+
const stats = useMemo(() => {
|
|
85
|
+
if (!data) return null;
|
|
86
|
+
const open = data.deals.filter((d: Deal) => d.status === 'OPEN');
|
|
87
|
+
const won = data.deals.filter((d: Deal) => d.status === 'WON');
|
|
88
|
+
const lost = data.deals.filter((d: Deal) => d.status === 'LOST');
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
90
|
+
return {
|
|
91
|
+
total: data.total,
|
|
92
|
+
totalValue: data.totalValue,
|
|
93
|
+
openCount: open.length,
|
|
94
|
+
openValue: open.reduce((sum: number, d: Deal) => sum + d.value, 0),
|
|
95
|
+
wonCount: won.length,
|
|
96
|
+
wonValue: won.reduce((sum: number, d: Deal) => sum + d.value, 0),
|
|
97
|
+
lostCount: lost.length,
|
|
98
|
+
};
|
|
99
|
+
}, [data]);
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
101
|
+
return {
|
|
102
|
+
data,
|
|
103
|
+
dealsByStage,
|
|
104
|
+
stages,
|
|
105
|
+
loading,
|
|
106
|
+
error,
|
|
107
|
+
stats,
|
|
108
|
+
page,
|
|
109
|
+
refetch: fetchData,
|
|
110
|
+
nextPage: () => setPage((p) => p + 1),
|
|
111
|
+
prevPage: () => page > 1 && setPage((p) => p - 1),
|
|
112
|
+
};
|
|
113
113
|
}
|