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