@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
package/src/ui/CrmDashboard.tsx
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
ErrorState,
|
|
6
|
+
LoaderBlock,
|
|
7
|
+
StatCard,
|
|
8
|
+
StatCardGroup,
|
|
9
|
+
} from '@contractspec/lib.design-system';
|
|
10
|
+
import {
|
|
11
|
+
Tabs,
|
|
12
|
+
TabsContent,
|
|
13
|
+
TabsList,
|
|
14
|
+
TabsTrigger,
|
|
15
|
+
} from '@contractspec/lib.ui-kit-web/ui/tabs';
|
|
3
16
|
/**
|
|
4
17
|
* CRM Dashboard
|
|
5
18
|
*
|
|
@@ -13,299 +26,286 @@
|
|
|
13
26
|
* - LoseDealContract -> Mark deal as lost
|
|
14
27
|
*/
|
|
15
28
|
import { useCallback, useState } from 'react';
|
|
16
|
-
import {
|
|
17
|
-
Button,
|
|
18
|
-
ErrorState,
|
|
19
|
-
LoaderBlock,
|
|
20
|
-
StatCard,
|
|
21
|
-
StatCardGroup,
|
|
22
|
-
} from '@contractspec/lib.design-system';
|
|
23
|
-
import {
|
|
24
|
-
Tabs,
|
|
25
|
-
TabsContent,
|
|
26
|
-
TabsList,
|
|
27
|
-
TabsTrigger,
|
|
28
|
-
} from '@contractspec/lib.ui-kit-web/ui/tabs';
|
|
29
|
+
import { CrmPipelineBoard } from './CrmPipelineBoard';
|
|
29
30
|
import { type Deal, useDealList } from './hooks/useDealList';
|
|
30
31
|
import { useDealMutations } from './hooks/useDealMutations';
|
|
31
|
-
import { CrmPipelineBoard } from './CrmPipelineBoard';
|
|
32
32
|
import { CreateDealModal } from './modals/CreateDealModal';
|
|
33
33
|
import { DealActionsModal } from './modals/DealActionsModal';
|
|
34
34
|
|
|
35
35
|
// type Tab = 'pipeline' | 'list' | 'metrics';
|
|
36
36
|
|
|
37
37
|
function formatCurrency(value: number, currency = 'USD'): string {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
return new Intl.NumberFormat('en-US', {
|
|
39
|
+
style: 'currency',
|
|
40
|
+
currency,
|
|
41
|
+
minimumFractionDigits: 0,
|
|
42
|
+
maximumFractionDigits: 0,
|
|
43
|
+
}).format(value);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
export function CrmDashboard() {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
|
48
|
+
const [selectedDeal, setSelectedDeal] = useState<Deal | null>(null);
|
|
49
|
+
const [isDealActionsOpen, setIsDealActionsOpen] = useState(false);
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
const { data, dealsByStage, stages, loading, error, stats, refetch } =
|
|
52
|
+
useDealList();
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
const mutations = useDealMutations({
|
|
55
|
+
onSuccess: () => {
|
|
56
|
+
refetch();
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
60
|
+
const handleDealClick = useCallback(
|
|
61
|
+
(dealId: string) => {
|
|
62
|
+
// Find deal in data
|
|
63
|
+
const deal = dealsByStage
|
|
64
|
+
? Object.values(dealsByStage)
|
|
65
|
+
.flat()
|
|
66
|
+
.find((d) => d.id === dealId)
|
|
67
|
+
: null;
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
69
|
+
if (deal) {
|
|
70
|
+
setSelectedDeal(deal);
|
|
71
|
+
setIsDealActionsOpen(true);
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
[dealsByStage]
|
|
75
|
+
);
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
const handleDealMove = useCallback(
|
|
78
|
+
async (dealId: string, toStageId: string) => {
|
|
79
|
+
await mutations.moveDeal({ dealId, stageId: toStageId });
|
|
80
|
+
},
|
|
81
|
+
[mutations]
|
|
82
|
+
);
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
if (loading && !data) {
|
|
85
|
+
return <LoaderBlock label="Loading CRM..." />;
|
|
86
|
+
}
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
88
|
+
if (error) {
|
|
89
|
+
return (
|
|
90
|
+
<ErrorState
|
|
91
|
+
title="Failed to load CRM"
|
|
92
|
+
description={error.message}
|
|
93
|
+
onRetry={refetch}
|
|
94
|
+
retryLabel="Retry"
|
|
95
|
+
/>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
98
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
99
|
+
return (
|
|
100
|
+
<div className="space-y-6">
|
|
101
|
+
{/* Header with Create Button */}
|
|
102
|
+
<div className="flex items-center justify-between">
|
|
103
|
+
<h2 className="font-bold text-2xl">CRM Pipeline</h2>
|
|
104
|
+
<Button onClick={() => setIsCreateModalOpen(true)}>
|
|
105
|
+
<span className="mr-2">+</span> Create Deal
|
|
106
|
+
</Button>
|
|
107
|
+
</div>
|
|
108
108
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
109
|
+
{/* Stats Row */}
|
|
110
|
+
{stats && (
|
|
111
|
+
<StatCardGroup>
|
|
112
|
+
<StatCard
|
|
113
|
+
label="Total Pipeline"
|
|
114
|
+
value={formatCurrency(stats.totalValue)}
|
|
115
|
+
hint={`${stats.total} deals`}
|
|
116
|
+
/>
|
|
117
|
+
<StatCard
|
|
118
|
+
label="Open Deals"
|
|
119
|
+
value={formatCurrency(stats.openValue)}
|
|
120
|
+
hint={`${stats.openCount} active`}
|
|
121
|
+
/>
|
|
122
|
+
<StatCard
|
|
123
|
+
label="Won"
|
|
124
|
+
value={formatCurrency(stats.wonValue)}
|
|
125
|
+
hint={`${stats.wonCount} closed`}
|
|
126
|
+
/>
|
|
127
|
+
<StatCard label="Lost" value={stats.lostCount} hint="deals lost" />
|
|
128
|
+
</StatCardGroup>
|
|
129
|
+
)}
|
|
130
130
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
131
|
+
{/* Tabs */}
|
|
132
|
+
<Tabs defaultValue="pipeline" className="w-full">
|
|
133
|
+
<TabsList>
|
|
134
|
+
<TabsTrigger value="pipeline">
|
|
135
|
+
<span className="mr-2">📊</span>
|
|
136
|
+
Pipeline
|
|
137
|
+
</TabsTrigger>
|
|
138
|
+
<TabsTrigger value="list">
|
|
139
|
+
<span className="mr-2">📋</span>
|
|
140
|
+
All Deals
|
|
141
|
+
</TabsTrigger>
|
|
142
|
+
<TabsTrigger value="metrics">
|
|
143
|
+
<span className="mr-2">📈</span>
|
|
144
|
+
Metrics
|
|
145
|
+
</TabsTrigger>
|
|
146
|
+
</TabsList>
|
|
147
147
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
148
|
+
<TabsContent value="pipeline" className="min-h-[400px]">
|
|
149
|
+
<CrmPipelineBoard
|
|
150
|
+
dealsByStage={dealsByStage}
|
|
151
|
+
stages={stages}
|
|
152
|
+
onDealClick={handleDealClick}
|
|
153
|
+
onDealMove={handleDealMove}
|
|
154
|
+
/>
|
|
155
|
+
</TabsContent>
|
|
156
156
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
157
|
+
<TabsContent value="list" className="min-h-[400px]">
|
|
158
|
+
<DealListTab data={data} onDealClick={handleDealClick} />
|
|
159
|
+
</TabsContent>
|
|
160
160
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
161
|
+
<TabsContent value="metrics" className="min-h-[400px]">
|
|
162
|
+
<MetricsTab stats={stats} />
|
|
163
|
+
</TabsContent>
|
|
164
|
+
</Tabs>
|
|
165
165
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
166
|
+
{/* Create Deal Modal */}
|
|
167
|
+
<CreateDealModal
|
|
168
|
+
isOpen={isCreateModalOpen}
|
|
169
|
+
onClose={() => setIsCreateModalOpen(false)}
|
|
170
|
+
onSubmit={async (input) => {
|
|
171
|
+
await mutations.createDeal(input);
|
|
172
|
+
}}
|
|
173
|
+
stages={stages}
|
|
174
|
+
isLoading={mutations.createState.loading}
|
|
175
|
+
/>
|
|
176
176
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
177
|
+
{/* Deal Actions Modal */}
|
|
178
|
+
<DealActionsModal
|
|
179
|
+
isOpen={isDealActionsOpen}
|
|
180
|
+
deal={selectedDeal}
|
|
181
|
+
stages={stages}
|
|
182
|
+
onClose={() => {
|
|
183
|
+
setIsDealActionsOpen(false);
|
|
184
|
+
setSelectedDeal(null);
|
|
185
|
+
}}
|
|
186
|
+
onWin={async (input) => {
|
|
187
|
+
await mutations.winDeal(input);
|
|
188
|
+
}}
|
|
189
|
+
onLose={async (input) => {
|
|
190
|
+
await mutations.loseDeal(input);
|
|
191
|
+
}}
|
|
192
|
+
onMove={async (input) => {
|
|
193
|
+
await mutations.moveDeal(input);
|
|
194
|
+
refetch();
|
|
195
|
+
}}
|
|
196
|
+
isLoading={mutations.isLoading}
|
|
197
|
+
/>
|
|
198
|
+
</div>
|
|
199
|
+
);
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
interface DealListTabProps {
|
|
203
|
-
|
|
204
|
-
|
|
203
|
+
data: ReturnType<typeof useDealList>['data'];
|
|
204
|
+
onDealClick?: (dealId: string) => void;
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
function DealListTab({ data, onDealClick }: DealListTabProps) {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
208
|
+
if (!data?.deals.length) {
|
|
209
|
+
return (
|
|
210
|
+
<div className="flex h-64 items-center justify-center text-muted-foreground">
|
|
211
|
+
No deals found
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
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
|
-
|
|
216
|
+
return (
|
|
217
|
+
<div className="rounded-lg border border-border">
|
|
218
|
+
<table className="w-full">
|
|
219
|
+
<thead className="border-border border-b bg-muted/30">
|
|
220
|
+
<tr>
|
|
221
|
+
<th className="px-4 py-3 text-left font-medium text-sm">Deal</th>
|
|
222
|
+
<th className="px-4 py-3 text-left font-medium text-sm">Value</th>
|
|
223
|
+
<th className="px-4 py-3 text-left font-medium text-sm">Status</th>
|
|
224
|
+
<th className="px-4 py-3 text-left font-medium text-sm">
|
|
225
|
+
Expected Close
|
|
226
|
+
</th>
|
|
227
|
+
<th className="px-4 py-3 text-left font-medium text-sm">Actions</th>
|
|
228
|
+
</tr>
|
|
229
|
+
</thead>
|
|
230
|
+
<tbody className="divide-y divide-border">
|
|
231
|
+
{data.deals.map((deal: Deal) => (
|
|
232
|
+
<tr key={deal.id} className="hover:bg-muted/50">
|
|
233
|
+
<td className="px-4 py-3">
|
|
234
|
+
<div className="font-medium">{deal.name}</div>
|
|
235
|
+
</td>
|
|
236
|
+
<td className="px-4 py-3 font-mono">
|
|
237
|
+
{formatCurrency(deal.value, deal.currency)}
|
|
238
|
+
</td>
|
|
239
|
+
<td className="px-4 py-3">
|
|
240
|
+
<span
|
|
241
|
+
className={`inline-flex rounded-full px-2 py-0.5 font-medium text-xs ${
|
|
242
|
+
deal.status === 'WON'
|
|
243
|
+
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
|
|
244
|
+
: deal.status === 'LOST'
|
|
245
|
+
? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
|
|
246
|
+
: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
|
|
247
|
+
}`}
|
|
248
|
+
>
|
|
249
|
+
{deal.status}
|
|
250
|
+
</span>
|
|
251
|
+
</td>
|
|
252
|
+
<td className="px-4 py-3 text-muted-foreground">
|
|
253
|
+
{deal.expectedCloseDate?.toLocaleDateString() ?? '-'}
|
|
254
|
+
</td>
|
|
255
|
+
<td className="px-4 py-3">
|
|
256
|
+
<Button
|
|
257
|
+
variant="ghost"
|
|
258
|
+
size="sm"
|
|
259
|
+
onPress={() => onDealClick?.(deal.id)}
|
|
260
|
+
>
|
|
261
|
+
Actions
|
|
262
|
+
</Button>
|
|
263
|
+
</td>
|
|
264
|
+
</tr>
|
|
265
|
+
))}
|
|
266
|
+
</tbody>
|
|
267
|
+
</table>
|
|
268
|
+
</div>
|
|
269
|
+
);
|
|
270
270
|
}
|
|
271
271
|
|
|
272
272
|
function MetricsTab({
|
|
273
|
-
|
|
273
|
+
stats,
|
|
274
274
|
}: {
|
|
275
|
-
|
|
275
|
+
stats: ReturnType<typeof useDealList>['stats'];
|
|
276
276
|
}) {
|
|
277
|
-
|
|
277
|
+
if (!stats) return null;
|
|
278
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
|
-
|
|
279
|
+
return (
|
|
280
|
+
<div className="space-y-6">
|
|
281
|
+
<div className="rounded-xl border border-border bg-card p-6">
|
|
282
|
+
<h3 className="mb-4 font-semibold text-lg">Pipeline Overview</h3>
|
|
283
|
+
<dl className="grid gap-4 sm:grid-cols-3">
|
|
284
|
+
<div>
|
|
285
|
+
<dt className="text-muted-foreground text-sm">Win Rate</dt>
|
|
286
|
+
<dd className="font-semibold text-2xl">
|
|
287
|
+
{stats.total > 0
|
|
288
|
+
? ((stats.wonCount / stats.total) * 100).toFixed(0)
|
|
289
|
+
: 0}
|
|
290
|
+
%
|
|
291
|
+
</dd>
|
|
292
|
+
</div>
|
|
293
|
+
<div>
|
|
294
|
+
<dt className="text-muted-foreground text-sm">Avg Deal Size</dt>
|
|
295
|
+
<dd className="font-semibold text-2xl">
|
|
296
|
+
{formatCurrency(
|
|
297
|
+
stats.total > 0 ? stats.totalValue / stats.total : 0
|
|
298
|
+
)}
|
|
299
|
+
</dd>
|
|
300
|
+
</div>
|
|
301
|
+
<div>
|
|
302
|
+
<dt className="text-muted-foreground text-sm">Conversion</dt>
|
|
303
|
+
<dd className="font-semibold text-2xl">
|
|
304
|
+
{stats.wonCount} / {stats.total}
|
|
305
|
+
</dd>
|
|
306
|
+
</div>
|
|
307
|
+
</dl>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
);
|
|
311
311
|
}
|