@contractspec/example.crm-pipeline 3.7.7 → 3.7.12
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/CHANGELOG.md +72 -0
- package/README.md +2 -1
- package/dist/browser/docs/crm-pipeline.docblock.js +1 -1
- package/dist/browser/docs/index.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 +392 -159
- package/dist/browser/ui/CrmDashboard.js +366 -144
- package/dist/browser/ui/hooks/index.js +19 -8
- package/dist/browser/ui/hooks/useDealList.js +19 -8
- package/dist/browser/ui/index.js +391 -158
- package/dist/browser/ui/renderers/index.js +32 -10
- package/dist/browser/ui/renderers/pipeline.markdown.js +13 -2
- package/dist/browser/ui/renderers/pipeline.renderer.js +19 -8
- package/dist/browser/ui/tables/DealListTab.js +390 -0
- package/dist/docs/crm-pipeline.docblock.js +1 -1
- package/dist/docs/index.js +1 -1
- package/dist/handlers/crm.handlers.d.ts +2 -0
- package/dist/handlers/crm.handlers.js +13 -2
- package/dist/handlers/index.js +13 -2
- package/dist/index.js +392 -159
- package/dist/node/docs/crm-pipeline.docblock.js +1 -1
- package/dist/node/docs/index.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 +392 -159
- package/dist/node/ui/CrmDashboard.js +366 -144
- package/dist/node/ui/hooks/index.js +19 -8
- package/dist/node/ui/hooks/useDealList.js +19 -8
- package/dist/node/ui/index.js +391 -158
- package/dist/node/ui/renderers/index.js +32 -10
- package/dist/node/ui/renderers/pipeline.markdown.js +13 -2
- package/dist/node/ui/renderers/pipeline.renderer.js +19 -8
- package/dist/node/ui/tables/DealListTab.js +390 -0
- package/dist/ui/CrmDashboard.js +366 -144
- package/dist/ui/hooks/index.js +19 -8
- package/dist/ui/hooks/useDealList.d.ts +8 -2
- package/dist/ui/hooks/useDealList.js +19 -8
- package/dist/ui/index.js +391 -158
- package/dist/ui/renderers/index.js +32 -10
- package/dist/ui/renderers/pipeline.markdown.d.ts +1 -1
- 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 +19 -8
- 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 -13
- package/src/docs/crm-pipeline.docblock.ts +1 -1
- package/src/handlers/crm.handlers.ts +18 -1
- package/src/ui/CrmDashboard.tsx +2 -71
- package/src/ui/hooks/useDealList.ts +36 -8
- package/src/ui/renderers/pipeline.markdown.ts +1 -1
- package/src/ui/renderers/pipeline.renderer.tsx +1 -1
- package/src/ui/tables/DealListTab.smoke.test.tsx +149 -0
- package/src/ui/tables/DealListTab.tsx +276 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
DataTable,
|
|
6
|
+
LoaderBlock,
|
|
7
|
+
} from '@contractspec/lib.design-system';
|
|
8
|
+
import type { ContractTableSort } from '@contractspec/lib.presentation-runtime-core';
|
|
9
|
+
import { useContractTable } from '@contractspec/lib.presentation-runtime-react';
|
|
10
|
+
import { Badge } from '@contractspec/lib.ui-kit-web/ui/badge';
|
|
11
|
+
import { HStack, VStack } from '@contractspec/lib.ui-kit-web/ui/stack';
|
|
12
|
+
import { Text } from '@contractspec/lib.ui-kit-web/ui/text';
|
|
13
|
+
import * as React from 'react';
|
|
14
|
+
import { type Deal, useDealList } from '../hooks/useDealList';
|
|
15
|
+
|
|
16
|
+
function formatCurrency(value: number, currency = 'USD') {
|
|
17
|
+
return new Intl.NumberFormat('en-US', {
|
|
18
|
+
style: 'currency',
|
|
19
|
+
currency,
|
|
20
|
+
minimumFractionDigits: 0,
|
|
21
|
+
maximumFractionDigits: 0,
|
|
22
|
+
}).format(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function statusVariant(status: Deal['status']) {
|
|
26
|
+
switch (status) {
|
|
27
|
+
case 'WON':
|
|
28
|
+
return 'default';
|
|
29
|
+
case 'LOST':
|
|
30
|
+
return 'destructive';
|
|
31
|
+
case 'STALE':
|
|
32
|
+
return 'outline';
|
|
33
|
+
default:
|
|
34
|
+
return 'secondary';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface DealListDataTableProps {
|
|
39
|
+
deals: Deal[];
|
|
40
|
+
totalItems: number;
|
|
41
|
+
pageIndex: number;
|
|
42
|
+
pageSize: number;
|
|
43
|
+
sorting: ContractTableSort[];
|
|
44
|
+
loading?: boolean;
|
|
45
|
+
onSortingChange: (sorting: ContractTableSort[]) => void;
|
|
46
|
+
onPaginationChange: (pagination: {
|
|
47
|
+
pageIndex: number;
|
|
48
|
+
pageSize: number;
|
|
49
|
+
}) => void;
|
|
50
|
+
onDealClick?: (dealId: string) => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function DealListDataTable({
|
|
54
|
+
deals,
|
|
55
|
+
totalItems,
|
|
56
|
+
pageIndex,
|
|
57
|
+
pageSize,
|
|
58
|
+
sorting,
|
|
59
|
+
loading,
|
|
60
|
+
onSortingChange,
|
|
61
|
+
onPaginationChange,
|
|
62
|
+
onDealClick,
|
|
63
|
+
}: DealListDataTableProps) {
|
|
64
|
+
const controller = useContractTable<Deal>({
|
|
65
|
+
data: deals,
|
|
66
|
+
columns: [
|
|
67
|
+
{
|
|
68
|
+
id: 'deal',
|
|
69
|
+
header: 'Deal',
|
|
70
|
+
label: 'Deal',
|
|
71
|
+
accessor: (deal) => deal.name,
|
|
72
|
+
cell: ({ item }) => (
|
|
73
|
+
<VStack gap="xs">
|
|
74
|
+
<Text className="font-medium text-sm">{item.name}</Text>
|
|
75
|
+
<Text className="text-muted-foreground text-xs">
|
|
76
|
+
{item.companyId ?? 'Unassigned company'}
|
|
77
|
+
</Text>
|
|
78
|
+
</VStack>
|
|
79
|
+
),
|
|
80
|
+
size: 240,
|
|
81
|
+
minSize: 180,
|
|
82
|
+
canSort: true,
|
|
83
|
+
canPin: true,
|
|
84
|
+
canResize: true,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: 'value',
|
|
88
|
+
header: 'Value',
|
|
89
|
+
label: 'Value',
|
|
90
|
+
accessorKey: 'value',
|
|
91
|
+
cell: ({ item }) => formatCurrency(item.value, item.currency),
|
|
92
|
+
align: 'right',
|
|
93
|
+
size: 140,
|
|
94
|
+
canSort: true,
|
|
95
|
+
canResize: true,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: 'status',
|
|
99
|
+
header: 'Status',
|
|
100
|
+
label: 'Status',
|
|
101
|
+
accessorKey: 'status',
|
|
102
|
+
cell: ({ value }) => (
|
|
103
|
+
<Badge variant={statusVariant(value as Deal['status'])}>
|
|
104
|
+
{String(value)}
|
|
105
|
+
</Badge>
|
|
106
|
+
),
|
|
107
|
+
size: 130,
|
|
108
|
+
canSort: true,
|
|
109
|
+
canHide: true,
|
|
110
|
+
canPin: true,
|
|
111
|
+
canResize: true,
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: 'expectedCloseDate',
|
|
115
|
+
header: 'Expected Close',
|
|
116
|
+
label: 'Expected Close',
|
|
117
|
+
accessor: (deal) => deal.expectedCloseDate?.toISOString() ?? '',
|
|
118
|
+
cell: ({ item }) =>
|
|
119
|
+
item.expectedCloseDate?.toLocaleDateString() ?? 'Not scheduled',
|
|
120
|
+
size: 170,
|
|
121
|
+
canSort: true,
|
|
122
|
+
canHide: true,
|
|
123
|
+
canResize: true,
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: 'updatedAt',
|
|
127
|
+
header: 'Updated',
|
|
128
|
+
label: 'Updated',
|
|
129
|
+
accessor: (deal) => deal.updatedAt.toISOString(),
|
|
130
|
+
cell: ({ item }) => item.updatedAt.toLocaleDateString(),
|
|
131
|
+
size: 140,
|
|
132
|
+
canSort: true,
|
|
133
|
+
canHide: true,
|
|
134
|
+
canResize: true,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: 'actions',
|
|
138
|
+
header: 'Actions',
|
|
139
|
+
label: 'Actions',
|
|
140
|
+
accessor: (deal) => deal.id,
|
|
141
|
+
cell: ({ item }) => (
|
|
142
|
+
<Button
|
|
143
|
+
variant="ghost"
|
|
144
|
+
size="sm"
|
|
145
|
+
onPress={() => onDealClick?.(item.id)}
|
|
146
|
+
>
|
|
147
|
+
Actions
|
|
148
|
+
</Button>
|
|
149
|
+
),
|
|
150
|
+
size: 120,
|
|
151
|
+
canSort: false,
|
|
152
|
+
canHide: false,
|
|
153
|
+
canPin: false,
|
|
154
|
+
canResize: false,
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
executionMode: 'server',
|
|
158
|
+
selectionMode: 'multiple',
|
|
159
|
+
totalItems,
|
|
160
|
+
state: {
|
|
161
|
+
sorting,
|
|
162
|
+
pagination: {
|
|
163
|
+
pageIndex,
|
|
164
|
+
pageSize,
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
onSortingChange: onSortingChange,
|
|
168
|
+
onPaginationChange: onPaginationChange,
|
|
169
|
+
initialState: {
|
|
170
|
+
columnVisibility: { updatedAt: false },
|
|
171
|
+
columnPinning: { left: ['deal', 'status'], right: [] },
|
|
172
|
+
},
|
|
173
|
+
renderExpandedContent: (deal) => (
|
|
174
|
+
<VStack gap="sm" className="py-2">
|
|
175
|
+
<HStack justify="between">
|
|
176
|
+
<Text className="font-medium text-sm">Owner</Text>
|
|
177
|
+
<Text className="text-muted-foreground text-sm">{deal.ownerId}</Text>
|
|
178
|
+
</HStack>
|
|
179
|
+
<HStack justify="between">
|
|
180
|
+
<Text className="font-medium text-sm">Contact</Text>
|
|
181
|
+
<Text className="text-muted-foreground text-sm">
|
|
182
|
+
{deal.contactId ?? 'No linked contact'}
|
|
183
|
+
</Text>
|
|
184
|
+
</HStack>
|
|
185
|
+
{deal.wonSource ? (
|
|
186
|
+
<HStack justify="between">
|
|
187
|
+
<Text className="font-medium text-sm">Won Source</Text>
|
|
188
|
+
<Text className="text-muted-foreground text-sm">
|
|
189
|
+
{deal.wonSource}
|
|
190
|
+
</Text>
|
|
191
|
+
</HStack>
|
|
192
|
+
) : null}
|
|
193
|
+
{deal.lostReason ? (
|
|
194
|
+
<HStack justify="between">
|
|
195
|
+
<Text className="font-medium text-sm">Lost Reason</Text>
|
|
196
|
+
<Text className="text-muted-foreground text-sm">
|
|
197
|
+
{deal.lostReason}
|
|
198
|
+
</Text>
|
|
199
|
+
</HStack>
|
|
200
|
+
) : null}
|
|
201
|
+
{deal.notes ? (
|
|
202
|
+
<VStack gap="xs">
|
|
203
|
+
<Text className="font-medium text-sm">Notes</Text>
|
|
204
|
+
<Text className="text-muted-foreground text-sm">{deal.notes}</Text>
|
|
205
|
+
</VStack>
|
|
206
|
+
) : null}
|
|
207
|
+
</VStack>
|
|
208
|
+
),
|
|
209
|
+
getCanExpand: () => true,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<DataTable
|
|
214
|
+
controller={controller}
|
|
215
|
+
title="All Deals"
|
|
216
|
+
description="Server-mode table using the shared ContractSpec controller."
|
|
217
|
+
loading={loading}
|
|
218
|
+
toolbar={
|
|
219
|
+
<HStack gap="sm" className="flex-wrap">
|
|
220
|
+
<Text className="text-muted-foreground text-sm">
|
|
221
|
+
Selected {controller.selectedRowIds.length}
|
|
222
|
+
</Text>
|
|
223
|
+
<Text className="text-muted-foreground text-sm">
|
|
224
|
+
{totalItems} total deals
|
|
225
|
+
</Text>
|
|
226
|
+
</HStack>
|
|
227
|
+
}
|
|
228
|
+
footer={`Page ${controller.pageIndex + 1} of ${controller.pageCount}`}
|
|
229
|
+
emptyState={
|
|
230
|
+
<div className="rounded-md border border-dashed p-8 text-center text-muted-foreground text-sm">
|
|
231
|
+
No deals found
|
|
232
|
+
</div>
|
|
233
|
+
}
|
|
234
|
+
/>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function DealListTab({
|
|
239
|
+
onDealClick,
|
|
240
|
+
}: {
|
|
241
|
+
onDealClick?: (dealId: string) => void;
|
|
242
|
+
}) {
|
|
243
|
+
const [sorting, setSorting] = React.useState<ContractTableSort[]>([
|
|
244
|
+
{ id: 'value', desc: true },
|
|
245
|
+
]);
|
|
246
|
+
const [pagination, setPagination] = React.useState({
|
|
247
|
+
pageIndex: 0,
|
|
248
|
+
pageSize: 3,
|
|
249
|
+
});
|
|
250
|
+
const { data, loading } = useDealList({
|
|
251
|
+
pageIndex: pagination.pageIndex,
|
|
252
|
+
pageSize: pagination.pageSize,
|
|
253
|
+
sorting,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
if (loading && !data) {
|
|
257
|
+
return <LoaderBlock label="Loading deals..." />;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return (
|
|
261
|
+
<DealListDataTable
|
|
262
|
+
deals={data?.deals ?? []}
|
|
263
|
+
totalItems={data?.total ?? 0}
|
|
264
|
+
pageIndex={pagination.pageIndex}
|
|
265
|
+
pageSize={pagination.pageSize}
|
|
266
|
+
sorting={sorting}
|
|
267
|
+
loading={loading}
|
|
268
|
+
onSortingChange={(nextSorting) => {
|
|
269
|
+
setSorting(nextSorting);
|
|
270
|
+
setPagination((current) => ({ ...current, pageIndex: 0 }));
|
|
271
|
+
}}
|
|
272
|
+
onPaginationChange={setPagination}
|
|
273
|
+
onDealClick={onDealClick}
|
|
274
|
+
/>
|
|
275
|
+
);
|
|
276
|
+
}
|