@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
|
@@ -10,59 +10,59 @@ import type { OverlayDefinition } from '../../shared/overlay-types';
|
|
|
10
10
|
* Demo user overlay - sample data mode
|
|
11
11
|
*/
|
|
12
12
|
export const crmDemoOverlay: OverlayDefinition = {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
13
|
+
overlayId: 'crm-pipeline.demo-user',
|
|
14
|
+
version: '1.0.0',
|
|
15
|
+
description: 'Demo mode with sample data',
|
|
16
|
+
appliesTo: {
|
|
17
|
+
feature: 'crm-pipeline',
|
|
18
|
+
role: 'demo',
|
|
19
|
+
},
|
|
20
|
+
modifications: [
|
|
21
|
+
{
|
|
22
|
+
type: 'hideField',
|
|
23
|
+
field: 'importButton',
|
|
24
|
+
reason: 'Not available in demo',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
type: 'hideField',
|
|
28
|
+
field: 'exportButton',
|
|
29
|
+
reason: 'Not available in demo',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
type: 'addBadge',
|
|
33
|
+
position: 'header',
|
|
34
|
+
label: 'Demo Mode',
|
|
35
|
+
variant: 'warning',
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
38
|
};
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* Sales rep overlay - focused view for sales
|
|
42
42
|
*/
|
|
43
43
|
export const crmSalesRepOverlay: OverlayDefinition = {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
44
|
+
overlayId: 'crm-pipeline.sales-rep',
|
|
45
|
+
version: '1.0.0',
|
|
46
|
+
description: 'Sales rep focused view',
|
|
47
|
+
appliesTo: {
|
|
48
|
+
feature: 'crm-pipeline',
|
|
49
|
+
role: 'sales-rep',
|
|
50
|
+
},
|
|
51
|
+
modifications: [
|
|
52
|
+
{
|
|
53
|
+
type: 'hideField',
|
|
54
|
+
field: 'teamMetrics',
|
|
55
|
+
reason: 'Team metrics for managers only',
|
|
56
|
+
},
|
|
57
|
+
{ type: 'hideField', field: 'pipelineSettings', reason: 'Admin only' },
|
|
58
|
+
{ type: 'renameLabel', field: 'deals', newLabel: 'My Deals' },
|
|
59
|
+
],
|
|
60
60
|
};
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
63
|
* All overlays for crm-pipeline
|
|
64
64
|
*/
|
|
65
65
|
export const crmOverlays: OverlayDefinition[] = [
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
crmDemoOverlay,
|
|
67
|
+
crmSalesRepOverlay,
|
|
68
68
|
];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// CRM Pipeline renderers
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
export {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
crmDashboardMarkdownRenderer,
|
|
5
|
+
crmPipelineMarkdownRenderer,
|
|
6
6
|
} from './pipeline.markdown';
|
|
7
|
+
export { crmPipelineReactRenderer } from './pipeline.renderer';
|
|
@@ -5,31 +5,31 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { PresentationRenderer } from '@contractspec/lib.contracts-spec/presentations/transform-engine';
|
|
7
7
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
mockGetPipelineStagesHandler,
|
|
9
|
+
mockListDealsHandler,
|
|
10
10
|
} from '../../handlers';
|
|
11
11
|
|
|
12
12
|
interface DealItem {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
value: number;
|
|
16
|
+
currency: string;
|
|
17
|
+
stageId: string;
|
|
18
|
+
status: string;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
interface StageItem {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
position: number;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
function formatCurrency(value: number, currency = 'USD'): string {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
return new Intl.NumberFormat('en-US', {
|
|
29
|
+
style: 'currency',
|
|
30
|
+
currency,
|
|
31
|
+
minimumFractionDigits: 0,
|
|
32
|
+
}).format(value);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/**
|
|
@@ -37,73 +37,73 @@ function formatCurrency(value: number, currency = 'USD'): string {
|
|
|
37
37
|
* Only handles PipelineKanbanView component
|
|
38
38
|
*/
|
|
39
39
|
export const crmPipelineMarkdownRenderer: PresentationRenderer<{
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
mimeType: string;
|
|
41
|
+
body: string;
|
|
42
42
|
}> = {
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43
|
+
target: 'markdown',
|
|
44
|
+
render: async (desc, _ctx) => {
|
|
45
|
+
// Only handle PipelineKanbanView
|
|
46
|
+
if (
|
|
47
|
+
desc.source.type !== 'component' ||
|
|
48
|
+
desc.source.componentKey !== 'PipelineKanbanView'
|
|
49
|
+
) {
|
|
50
|
+
throw new Error('crmPipelineMarkdownRenderer: not PipelineKanbanView');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const pipelineId = 'pipeline-1';
|
|
54
|
+
const [dealsResult, stages] = await Promise.all([
|
|
55
|
+
mockListDealsHandler({ pipelineId, limit: 50 }),
|
|
56
|
+
mockGetPipelineStagesHandler({ pipelineId }),
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
const deals = dealsResult.deals as DealItem[];
|
|
60
|
+
const stageList = stages as StageItem[];
|
|
61
|
+
|
|
62
|
+
// Group deals by stage
|
|
63
|
+
const dealsByStage: Record<string, DealItem[]> = {};
|
|
64
|
+
for (const stage of stageList) {
|
|
65
|
+
dealsByStage[stage.id] = deals.filter(
|
|
66
|
+
(d) => d.stageId === stage.id && d.status === 'OPEN'
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Build Markdown
|
|
71
|
+
const lines: string[] = [
|
|
72
|
+
'# CRM Pipeline',
|
|
73
|
+
'',
|
|
74
|
+
`**Total Value**: ${formatCurrency(dealsResult.totalValue)}`,
|
|
75
|
+
`**Total Deals**: ${dealsResult.total}`,
|
|
76
|
+
'',
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
for (const stage of stageList.sort((a, b) => a.position - b.position)) {
|
|
80
|
+
const stageDeals = dealsByStage[stage.id] ?? [];
|
|
81
|
+
const stageValue = stageDeals.reduce((sum, d) => sum + d.value, 0);
|
|
82
|
+
|
|
83
|
+
lines.push(`## ${stage.name}`);
|
|
84
|
+
lines.push(
|
|
85
|
+
`_${stageDeals.length} deals · ${formatCurrency(stageValue)}_`
|
|
86
|
+
);
|
|
87
|
+
lines.push('');
|
|
88
|
+
|
|
89
|
+
if (stageDeals.length === 0) {
|
|
90
|
+
lines.push('_No deals_');
|
|
91
|
+
} else {
|
|
92
|
+
for (const deal of stageDeals) {
|
|
93
|
+
lines.push(
|
|
94
|
+
`- **${deal.name}** - ${formatCurrency(deal.value, deal.currency)}`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
lines.push('');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
mimeType: 'text/markdown',
|
|
104
|
+
body: lines.join('\n'),
|
|
105
|
+
};
|
|
106
|
+
},
|
|
107
107
|
};
|
|
108
108
|
|
|
109
109
|
/**
|
|
@@ -111,88 +111,88 @@ export const crmPipelineMarkdownRenderer: PresentationRenderer<{
|
|
|
111
111
|
* Only handles CrmDashboard component
|
|
112
112
|
*/
|
|
113
113
|
export const crmDashboardMarkdownRenderer: PresentationRenderer<{
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
mimeType: string;
|
|
115
|
+
body: string;
|
|
116
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
|
-
|
|
117
|
+
target: 'markdown',
|
|
118
|
+
render: async (desc, _ctx) => {
|
|
119
|
+
// Only handle CrmDashboard
|
|
120
|
+
if (
|
|
121
|
+
desc.source.type !== 'component' ||
|
|
122
|
+
desc.source.componentKey !== 'CrmDashboard'
|
|
123
|
+
) {
|
|
124
|
+
throw new Error('crmDashboardMarkdownRenderer: not CrmDashboard');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const pipelineId = 'pipeline-1';
|
|
128
|
+
const [dealsResult, stages] = await Promise.all([
|
|
129
|
+
mockListDealsHandler({ pipelineId, limit: 100 }),
|
|
130
|
+
mockGetPipelineStagesHandler({ pipelineId }),
|
|
131
|
+
]);
|
|
132
|
+
|
|
133
|
+
const deals = dealsResult.deals as DealItem[];
|
|
134
|
+
const stageList = stages as StageItem[];
|
|
135
|
+
|
|
136
|
+
// Calculate stats
|
|
137
|
+
const openDeals = deals.filter((d) => d.status === 'OPEN');
|
|
138
|
+
const wonDeals = deals.filter((d) => d.status === 'WON');
|
|
139
|
+
const lostDeals = deals.filter((d) => d.status === 'LOST');
|
|
140
|
+
const openValue = openDeals.reduce((sum, d) => sum + d.value, 0);
|
|
141
|
+
const wonValue = wonDeals.reduce((sum, d) => sum + d.value, 0);
|
|
142
|
+
|
|
143
|
+
// Build dashboard markdown
|
|
144
|
+
const lines: string[] = [
|
|
145
|
+
'# CRM Dashboard',
|
|
146
|
+
'',
|
|
147
|
+
'> Sales pipeline overview and key metrics',
|
|
148
|
+
'',
|
|
149
|
+
'## Summary',
|
|
150
|
+
'',
|
|
151
|
+
'| Metric | Value |',
|
|
152
|
+
'|--------|-------|',
|
|
153
|
+
`| Total Deals | ${dealsResult.total} |`,
|
|
154
|
+
`| Pipeline Value | ${formatCurrency(dealsResult.totalValue)} |`,
|
|
155
|
+
`| Open Deals | ${openDeals.length} (${formatCurrency(openValue)}) |`,
|
|
156
|
+
`| Won Deals | ${wonDeals.length} (${formatCurrency(wonValue)}) |`,
|
|
157
|
+
`| Lost Deals | ${lostDeals.length} |`,
|
|
158
|
+
'',
|
|
159
|
+
'## Pipeline Stages',
|
|
160
|
+
'',
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
// Stage summary table
|
|
164
|
+
lines.push('| Stage | Deals | Value |');
|
|
165
|
+
lines.push('|-------|-------|-------|');
|
|
166
|
+
for (const stage of stageList.sort((a, b) => a.position - b.position)) {
|
|
167
|
+
const stageDeals = openDeals.filter((d) => d.stageId === stage.id);
|
|
168
|
+
const stageValue = stageDeals.reduce((sum, d) => sum + d.value, 0);
|
|
169
|
+
lines.push(
|
|
170
|
+
`| ${stage.name} | ${stageDeals.length} | ${formatCurrency(stageValue)} |`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
lines.push('');
|
|
175
|
+
lines.push('## Recent Deals');
|
|
176
|
+
lines.push('');
|
|
177
|
+
|
|
178
|
+
// Top 10 recent deals
|
|
179
|
+
const recentDeals = deals.slice(0, 10);
|
|
180
|
+
if (recentDeals.length === 0) {
|
|
181
|
+
lines.push('_No deals yet._');
|
|
182
|
+
} else {
|
|
183
|
+
lines.push('| Deal | Value | Stage | Status |');
|
|
184
|
+
lines.push('|------|-------|-------|--------|');
|
|
185
|
+
for (const deal of recentDeals) {
|
|
186
|
+
const stage = stageList.find((s) => s.id === deal.stageId);
|
|
187
|
+
lines.push(
|
|
188
|
+
`| ${deal.name} | ${formatCurrency(deal.value, deal.currency)} | ${stage?.name ?? '-'} | ${deal.status} |`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
mimeType: 'text/markdown',
|
|
195
|
+
body: lines.join('\n'),
|
|
196
|
+
};
|
|
197
|
+
},
|
|
198
198
|
};
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* Renders the CRM pipeline board component.
|
|
5
5
|
* Data is fetched via the CrmPipelineBoard component's internal hooks.
|
|
6
6
|
*/
|
|
7
|
-
|
|
7
|
+
|
|
8
8
|
import type { PresentationRenderer } from '@contractspec/lib.contracts-spec/presentations/transform-engine';
|
|
9
|
+
import * as React from 'react';
|
|
9
10
|
import { CrmPipelineBoard } from '../CrmPipelineBoard';
|
|
10
11
|
import { useDealList } from '../hooks/useDealList';
|
|
11
12
|
|
|
@@ -13,23 +14,23 @@ import { useDealList } from '../hooks/useDealList';
|
|
|
13
14
|
* Wrapper component that provides data to CrmPipelineBoard
|
|
14
15
|
*/
|
|
15
16
|
function CrmPipelineBoardWrapper() {
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
const { dealsByStage, stages } = useDealList();
|
|
18
|
+
return <CrmPipelineBoard dealsByStage={dealsByStage} stages={stages} />;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export const crmPipelineReactRenderer: PresentationRenderer<React.ReactElement> =
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
{
|
|
23
|
+
target: 'react',
|
|
24
|
+
render: async (desc, _ctx) => {
|
|
25
|
+
if (desc.source.type !== 'component') {
|
|
26
|
+
throw new Error('Invalid source type');
|
|
27
|
+
}
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
if (desc.source.componentKey !== 'CrmPipelineView') {
|
|
30
|
+
throw new Error(`Unknown component: ${desc.source.componentKey}`);
|
|
31
|
+
}
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
// Note: The wrapper component will fetch data internally
|
|
34
|
+
return <CrmPipelineBoardWrapper />;
|
|
35
|
+
},
|
|
36
|
+
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { afterEach, beforeAll, describe, expect, test } from 'bun:test';
|
|
2
|
+
import Window from 'happy-dom/lib/window/Window.js';
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { act } from 'react';
|
|
5
|
+
import { createRoot, type Root } from 'react-dom/client';
|
|
6
|
+
import { MOCK_DEALS } from '../../handlers/mock-data';
|
|
7
|
+
import type { Deal } from '../hooks/useDealList';
|
|
8
|
+
import { DealListDataTable } from './DealListTab';
|
|
9
|
+
|
|
10
|
+
const TEST_DEALS: Deal[] = MOCK_DEALS.map((deal) => ({
|
|
11
|
+
...deal,
|
|
12
|
+
projectId: 'project-1',
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
beforeAll(() => {
|
|
16
|
+
const windowInstance = new Window({
|
|
17
|
+
url: 'https://sandbox.contractspec.local/sandbox?template=crm-pipeline',
|
|
18
|
+
});
|
|
19
|
+
Object.defineProperty(windowInstance, 'SyntaxError', {
|
|
20
|
+
value: SyntaxError,
|
|
21
|
+
configurable: true,
|
|
22
|
+
});
|
|
23
|
+
Object.assign(globalThis, {
|
|
24
|
+
window: windowInstance,
|
|
25
|
+
document: windowInstance.document,
|
|
26
|
+
navigator: windowInstance.navigator,
|
|
27
|
+
HTMLElement: windowInstance.HTMLElement,
|
|
28
|
+
HTMLButtonElement: windowInstance.HTMLButtonElement,
|
|
29
|
+
Node: windowInstance.Node,
|
|
30
|
+
Event: windowInstance.Event,
|
|
31
|
+
MouseEvent: windowInstance.MouseEvent,
|
|
32
|
+
MutationObserver: windowInstance.MutationObserver,
|
|
33
|
+
getComputedStyle: windowInstance.getComputedStyle.bind(windowInstance),
|
|
34
|
+
requestAnimationFrame: (callback: FrameRequestCallback) =>
|
|
35
|
+
setTimeout(() => callback(Date.now()), 0),
|
|
36
|
+
cancelAnimationFrame: (id: number) => clearTimeout(id),
|
|
37
|
+
IS_REACT_ACT_ENVIRONMENT: true,
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
document.body.innerHTML = '';
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
function sortDeals(
|
|
46
|
+
pageIndex: number,
|
|
47
|
+
pageSize: number,
|
|
48
|
+
sorting: { id: string; desc: boolean }[]
|
|
49
|
+
) {
|
|
50
|
+
const [sort] = sorting;
|
|
51
|
+
const sorted = [...TEST_DEALS].sort((left, right) => {
|
|
52
|
+
const leftValue =
|
|
53
|
+
sort?.id === 'deal'
|
|
54
|
+
? left.name
|
|
55
|
+
: sort?.id === 'status'
|
|
56
|
+
? left.status
|
|
57
|
+
: sort?.id === 'expectedCloseDate'
|
|
58
|
+
? (left.expectedCloseDate?.toISOString() ?? '')
|
|
59
|
+
: sort?.id === 'updatedAt'
|
|
60
|
+
? left.updatedAt.toISOString()
|
|
61
|
+
: left.value;
|
|
62
|
+
const rightValue =
|
|
63
|
+
sort?.id === 'deal'
|
|
64
|
+
? right.name
|
|
65
|
+
: sort?.id === 'status'
|
|
66
|
+
? right.status
|
|
67
|
+
: sort?.id === 'expectedCloseDate'
|
|
68
|
+
? (right.expectedCloseDate?.toISOString() ?? '')
|
|
69
|
+
: sort?.id === 'updatedAt'
|
|
70
|
+
? right.updatedAt.toISOString()
|
|
71
|
+
: right.value;
|
|
72
|
+
if (leftValue === rightValue) return 0;
|
|
73
|
+
const comparison = leftValue > rightValue ? 1 : -1;
|
|
74
|
+
return sort?.desc ? comparison * -1 : comparison;
|
|
75
|
+
});
|
|
76
|
+
return sorted.slice(pageIndex * pageSize, pageIndex * pageSize + pageSize);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function Harness() {
|
|
80
|
+
const [sorting, setSorting] = React.useState([{ id: 'value', desc: true }]);
|
|
81
|
+
const [pagination, setPagination] = React.useState({
|
|
82
|
+
pageIndex: 0,
|
|
83
|
+
pageSize: 3,
|
|
84
|
+
});
|
|
85
|
+
return (
|
|
86
|
+
<DealListDataTable
|
|
87
|
+
deals={sortDeals(pagination.pageIndex, pagination.pageSize, sorting)}
|
|
88
|
+
totalItems={TEST_DEALS.length}
|
|
89
|
+
pageIndex={pagination.pageIndex}
|
|
90
|
+
pageSize={pagination.pageSize}
|
|
91
|
+
sorting={sorting}
|
|
92
|
+
onSortingChange={setSorting}
|
|
93
|
+
onPaginationChange={setPagination}
|
|
94
|
+
/>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function renderTable() {
|
|
99
|
+
const container = document.createElement('div');
|
|
100
|
+
document.body.append(container);
|
|
101
|
+
const root: Root = createRoot(container);
|
|
102
|
+
|
|
103
|
+
await act(async () => {
|
|
104
|
+
root.render(<Harness />);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return { container, root };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function click(element: Element | null | undefined) {
|
|
111
|
+
if (!element) {
|
|
112
|
+
throw new Error('Expected clickable element.');
|
|
113
|
+
}
|
|
114
|
+
await act(async () => {
|
|
115
|
+
if ('click' in element && typeof element.click === 'function') {
|
|
116
|
+
element.click();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
element.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
describe('DealListDataTable', () => {
|
|
124
|
+
test('renders the shared table and supports selection plus expansion', async () => {
|
|
125
|
+
const { container, root } = await renderTable();
|
|
126
|
+
|
|
127
|
+
expect(container.textContent).toContain('All Deals');
|
|
128
|
+
expect(container.textContent).toContain('6 total deals');
|
|
129
|
+
|
|
130
|
+
await click(container.querySelector('[aria-label="Select row deal-5"]'));
|
|
131
|
+
expect(container.textContent).toContain('Selected 1');
|
|
132
|
+
|
|
133
|
+
await click(container.querySelector('[aria-label="Expand row deal-5"]'));
|
|
134
|
+
expect(container.textContent).toContain('user-1');
|
|
135
|
+
|
|
136
|
+
await click(
|
|
137
|
+
[...container.getElementsByTagName('button')].find(
|
|
138
|
+
(button) => button.textContent?.trim() === '2'
|
|
139
|
+
)
|
|
140
|
+
);
|
|
141
|
+
expect(container.textContent).toContain(
|
|
142
|
+
'Affichage de 4 à 6 sur 6 résultats'
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
await act(async () => {
|
|
146
|
+
root.unmount();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
});
|