@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.
Files changed (57) hide show
  1. package/.turbo/turbo-build.log +45 -42
  2. package/CHANGELOG.md +72 -0
  3. package/README.md +2 -1
  4. package/dist/browser/docs/crm-pipeline.docblock.js +1 -1
  5. package/dist/browser/docs/index.js +1 -1
  6. package/dist/browser/handlers/crm.handlers.js +13 -2
  7. package/dist/browser/handlers/index.js +13 -2
  8. package/dist/browser/index.js +392 -159
  9. package/dist/browser/ui/CrmDashboard.js +366 -144
  10. package/dist/browser/ui/hooks/index.js +19 -8
  11. package/dist/browser/ui/hooks/useDealList.js +19 -8
  12. package/dist/browser/ui/index.js +391 -158
  13. package/dist/browser/ui/renderers/index.js +32 -10
  14. package/dist/browser/ui/renderers/pipeline.markdown.js +13 -2
  15. package/dist/browser/ui/renderers/pipeline.renderer.js +19 -8
  16. package/dist/browser/ui/tables/DealListTab.js +390 -0
  17. package/dist/docs/crm-pipeline.docblock.js +1 -1
  18. package/dist/docs/index.js +1 -1
  19. package/dist/handlers/crm.handlers.d.ts +2 -0
  20. package/dist/handlers/crm.handlers.js +13 -2
  21. package/dist/handlers/index.js +13 -2
  22. package/dist/index.js +392 -159
  23. package/dist/node/docs/crm-pipeline.docblock.js +1 -1
  24. package/dist/node/docs/index.js +1 -1
  25. package/dist/node/handlers/crm.handlers.js +13 -2
  26. package/dist/node/handlers/index.js +13 -2
  27. package/dist/node/index.js +392 -159
  28. package/dist/node/ui/CrmDashboard.js +366 -144
  29. package/dist/node/ui/hooks/index.js +19 -8
  30. package/dist/node/ui/hooks/useDealList.js +19 -8
  31. package/dist/node/ui/index.js +391 -158
  32. package/dist/node/ui/renderers/index.js +32 -10
  33. package/dist/node/ui/renderers/pipeline.markdown.js +13 -2
  34. package/dist/node/ui/renderers/pipeline.renderer.js +19 -8
  35. package/dist/node/ui/tables/DealListTab.js +390 -0
  36. package/dist/ui/CrmDashboard.js +366 -144
  37. package/dist/ui/hooks/index.js +19 -8
  38. package/dist/ui/hooks/useDealList.d.ts +8 -2
  39. package/dist/ui/hooks/useDealList.js +19 -8
  40. package/dist/ui/index.js +391 -158
  41. package/dist/ui/renderers/index.js +32 -10
  42. package/dist/ui/renderers/pipeline.markdown.d.ts +1 -1
  43. package/dist/ui/renderers/pipeline.markdown.js +13 -2
  44. package/dist/ui/renderers/pipeline.renderer.d.ts +1 -1
  45. package/dist/ui/renderers/pipeline.renderer.js +19 -8
  46. package/dist/ui/tables/DealListTab.d.ts +20 -0
  47. package/dist/ui/tables/DealListTab.js +391 -0
  48. package/dist/ui/tables/DealListTab.smoke.test.d.ts +1 -0
  49. package/package.json +29 -13
  50. package/src/docs/crm-pipeline.docblock.ts +1 -1
  51. package/src/handlers/crm.handlers.ts +18 -1
  52. package/src/ui/CrmDashboard.tsx +2 -71
  53. package/src/ui/hooks/useDealList.ts +36 -8
  54. package/src/ui/renderers/pipeline.markdown.ts +1 -1
  55. package/src/ui/renderers/pipeline.renderer.tsx +1 -1
  56. package/src/ui/tables/DealListTab.smoke.test.tsx +149 -0
  57. 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
+ }