@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,85 +1,3 @@
1
- // src/ui/hooks/useDealList.ts
2
- import { useCallback, useEffect, useMemo, useState } from "react";
3
- import { useTemplateRuntime } from "@contractspec/lib.example-shared-ui";
4
- "use client";
5
- function useDealList(options = {}) {
6
- const { handlers, projectId } = useTemplateRuntime();
7
- const { crm } = handlers;
8
- const [data, setData] = useState(null);
9
- const [dealsByStage, setDealsByStage] = useState({});
10
- const [stages, setStages] = useState([]);
11
- const [loading, setLoading] = useState(true);
12
- const [error, setError] = useState(null);
13
- const [page, setPage] = useState(1);
14
- const pipelineId = options.pipelineId ?? "pipeline-1";
15
- const fetchData = useCallback(async () => {
16
- setLoading(true);
17
- setError(null);
18
- try {
19
- const [dealsResult, stageDealsResult, stagesResult] = await Promise.all([
20
- crm.listDeals({
21
- projectId,
22
- pipelineId,
23
- stageId: options.stageId,
24
- status: options.status === "all" ? undefined : options.status,
25
- search: options.search,
26
- limit: options.limit ?? 50,
27
- offset: (page - 1) * (options.limit ?? 50)
28
- }),
29
- crm.getDealsByStage({ projectId, pipelineId }),
30
- crm.getPipelineStages({ pipelineId })
31
- ]);
32
- setData(dealsResult);
33
- setDealsByStage(stageDealsResult);
34
- setStages(stagesResult);
35
- } catch (err) {
36
- setError(err instanceof Error ? err : new Error("Unknown error"));
37
- } finally {
38
- setLoading(false);
39
- }
40
- }, [
41
- crm,
42
- projectId,
43
- pipelineId,
44
- options.stageId,
45
- options.status,
46
- options.search,
47
- options.limit,
48
- page
49
- ]);
50
- useEffect(() => {
51
- fetchData();
52
- }, [fetchData]);
53
- const stats = useMemo(() => {
54
- if (!data)
55
- return null;
56
- const open = data.deals.filter((d) => d.status === "OPEN");
57
- const won = data.deals.filter((d) => d.status === "WON");
58
- const lost = data.deals.filter((d) => d.status === "LOST");
59
- return {
60
- total: data.total,
61
- totalValue: data.totalValue,
62
- openCount: open.length,
63
- openValue: open.reduce((sum, d) => sum + d.value, 0),
64
- wonCount: won.length,
65
- wonValue: won.reduce((sum, d) => sum + d.value, 0),
66
- lostCount: lost.length
67
- };
68
- }, [data]);
69
- return {
70
- data,
71
- dealsByStage,
72
- stages,
73
- loading,
74
- error,
75
- stats,
76
- page,
77
- refetch: fetchData,
78
- nextPage: () => setPage((p) => p + 1),
79
- prevPage: () => page > 1 && setPage((p) => p - 1)
80
- };
81
- }
82
-
83
1
  // src/ui/CrmDealCard.tsx
84
2
  import { jsxDEV } from "react/jsx-dev-runtime";
85
3
  "use client";
@@ -95,7 +13,7 @@ function CrmDealCard({ deal, onClick }) {
95
13
  const daysUntilClose = deal.expectedCloseDate ? Math.ceil((deal.expectedCloseDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24)) : null;
96
14
  return /* @__PURE__ */ jsxDEV("div", {
97
15
  onClick,
98
- className: "border-border bg-card cursor-pointer rounded-lg border p-3 shadow-sm transition-shadow hover:shadow-md",
16
+ className: "cursor-pointer rounded-lg border border-border bg-card p-3 shadow-sm transition-shadow hover:shadow-md",
99
17
  role: "button",
100
18
  tabIndex: 0,
101
19
  onKeyDown: (e) => {
@@ -104,22 +22,22 @@ function CrmDealCard({ deal, onClick }) {
104
22
  },
105
23
  children: [
106
24
  /* @__PURE__ */ jsxDEV("h4", {
107
- className: "leading-snug font-medium",
25
+ className: "font-medium leading-snug",
108
26
  children: deal.name
109
27
  }, undefined, false, undefined, this),
110
28
  /* @__PURE__ */ jsxDEV("div", {
111
- className: "text-primary mt-2 text-lg font-semibold",
29
+ className: "mt-2 font-semibold text-lg text-primary",
112
30
  children: formatCurrency(deal.value, deal.currency)
113
31
  }, undefined, false, undefined, this),
114
32
  /* @__PURE__ */ jsxDEV("div", {
115
- className: "text-muted-foreground mt-3 flex items-center justify-between text-xs",
33
+ className: "mt-3 flex items-center justify-between text-muted-foreground text-xs",
116
34
  children: [
117
35
  daysUntilClose !== null && /* @__PURE__ */ jsxDEV("span", {
118
36
  className: daysUntilClose < 0 ? "text-red-500" : daysUntilClose <= 7 ? "text-yellow-600 dark:text-yellow-500" : "",
119
37
  children: daysUntilClose < 0 ? `${Math.abs(daysUntilClose)}d overdue` : daysUntilClose === 0 ? "Due today" : `${daysUntilClose}d left`
120
38
  }, undefined, false, undefined, this),
121
39
  /* @__PURE__ */ jsxDEV("span", {
122
- className: `rounded px-1.5 py-0.5 text-xs font-medium ${deal.status === "WON" ? "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400" : deal.status === "LOST" ? "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400" : "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"}`,
40
+ className: `rounded px-1.5 py-0.5 font-medium text-xs ${deal.status === "WON" ? "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400" : deal.status === "LOST" ? "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400" : "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"}`,
123
41
  children: deal.status
124
42
  }, undefined, false, undefined, this)
125
43
  ]
@@ -129,7 +47,7 @@ function CrmDealCard({ deal, onClick }) {
129
47
  }
130
48
 
131
49
  // src/ui/CrmPipelineBoard.tsx
132
- import { useState as useState2 } from "react";
50
+ import { useState } from "react";
133
51
  import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
134
52
  "use client";
135
53
  function formatCurrency2(value) {
@@ -145,7 +63,7 @@ function CrmPipelineBoard({
145
63
  onDealClick,
146
64
  onDealMove
147
65
  }) {
148
- const [quickMoveOpen, setQuickMoveOpen] = useState2(null);
66
+ const [quickMoveOpen, setQuickMoveOpen] = useState(null);
149
67
  const sortedStages = [...stages].sort((a, b) => a.position - b.position);
150
68
  const handleQuickMove = (dealId, toStageId) => {
151
69
  onDealMove?.(dealId, toStageId);
@@ -157,10 +75,10 @@ function CrmPipelineBoard({
157
75
  const deals = dealsByStage[stage.id] ?? [];
158
76
  const stageValue = deals.reduce((sum, d) => sum + d.value, 0);
159
77
  return /* @__PURE__ */ jsxDEV2("div", {
160
- className: "bg-muted/30 flex w-72 flex-shrink-0 flex-col rounded-lg",
78
+ className: "flex w-72 flex-shrink-0 flex-col rounded-lg bg-muted/30",
161
79
  children: [
162
80
  /* @__PURE__ */ jsxDEV2("div", {
163
- className: "border-border flex items-center justify-between border-b px-3 py-2",
81
+ className: "flex items-center justify-between border-border border-b px-3 py-2",
164
82
  children: [
165
83
  /* @__PURE__ */ jsxDEV2("div", {
166
84
  children: [
@@ -179,7 +97,7 @@ function CrmPipelineBoard({
179
97
  ]
180
98
  }, undefined, true, undefined, this),
181
99
  /* @__PURE__ */ jsxDEV2("span", {
182
- className: "bg-muted flex h-6 w-6 items-center justify-center rounded-full text-xs font-medium",
100
+ className: "flex h-6 w-6 items-center justify-center rounded-full bg-muted font-medium text-xs",
183
101
  children: deals.length
184
102
  }, undefined, false, undefined, this)
185
103
  ]
@@ -187,7 +105,7 @@ function CrmPipelineBoard({
187
105
  /* @__PURE__ */ jsxDEV2("div", {
188
106
  className: "flex flex-1 flex-col gap-2 p-2",
189
107
  children: deals.length === 0 ? /* @__PURE__ */ jsxDEV2("div", {
190
- className: "border-muted-foreground/20 text-muted-foreground flex h-24 items-center justify-center rounded-md border-2 border-dashed text-xs",
108
+ className: "flex h-24 items-center justify-center rounded-md border-2 border-muted-foreground/20 border-dashed text-muted-foreground text-xs",
191
109
  children: "No deals"
192
110
  }, undefined, false, undefined, this) : deals.map((deal) => /* @__PURE__ */ jsxDEV2("div", {
193
111
  className: "group relative",
@@ -205,15 +123,15 @@ function CrmPipelineBoard({
205
123
  e.stopPropagation();
206
124
  setQuickMoveOpen(quickMoveOpen === deal.id ? null : deal.id);
207
125
  },
208
- className: "bg-background border-border hover:bg-muted flex h-6 w-6 items-center justify-center rounded border text-xs shadow-sm",
126
+ className: "flex h-6 w-6 items-center justify-center rounded border border-border bg-background text-xs shadow-sm hover:bg-muted",
209
127
  title: "Quick move",
210
128
  children: "➡️"
211
129
  }, undefined, false, undefined, this),
212
130
  quickMoveOpen === deal.id && /* @__PURE__ */ jsxDEV2("div", {
213
- className: "bg-card border-border absolute top-7 right-0 z-20 min-w-[140px] rounded-lg border py-1 shadow-lg",
131
+ className: "absolute top-7 right-0 z-20 min-w-[140px] rounded-lg border border-border bg-card py-1 shadow-lg",
214
132
  children: [
215
133
  /* @__PURE__ */ jsxDEV2("p", {
216
- className: "text-muted-foreground px-3 py-1 text-xs font-medium",
134
+ className: "px-3 py-1 font-medium text-muted-foreground text-xs",
217
135
  children: "Move to:"
218
136
  }, undefined, false, undefined, this),
219
137
  sortedStages.filter((s) => s.id !== deal.stageId).map((s) => /* @__PURE__ */ jsxDEV2("button", {
@@ -222,7 +140,7 @@ function CrmPipelineBoard({
222
140
  e.stopPropagation();
223
141
  handleQuickMove(deal.id, s.id);
224
142
  },
225
- className: "hover:bg-muted w-full px-3 py-1.5 text-left text-sm",
143
+ className: "w-full px-3 py-1.5 text-left text-sm hover:bg-muted",
226
144
  children: s.name
227
145
  }, s.id, false, undefined, this))
228
146
  ]
@@ -238,6 +156,99 @@ function CrmPipelineBoard({
238
156
  }, undefined, false, undefined, this);
239
157
  }
240
158
 
159
+ // src/ui/hooks/useDealList.ts
160
+ import { useTemplateRuntime } from "@contractspec/lib.example-shared-ui";
161
+ import { useCallback, useEffect, useMemo, useState as useState2 } from "react";
162
+ "use client";
163
+ function useDealList(options = {}) {
164
+ const { handlers, projectId } = useTemplateRuntime();
165
+ const { crm } = handlers;
166
+ const [data, setData] = useState2(null);
167
+ const [dealsByStage, setDealsByStage] = useState2({});
168
+ const [stages, setStages] = useState2([]);
169
+ const [loading, setLoading] = useState2(true);
170
+ const [error, setError] = useState2(null);
171
+ const [internalPage, setInternalPage] = useState2(0);
172
+ const pipelineId = options.pipelineId ?? "pipeline-1";
173
+ const pageIndex = options.pageIndex ?? internalPage;
174
+ const pageSize = options.pageSize ?? options.limit ?? 50;
175
+ const [sort] = options.sorting ?? [];
176
+ const sortBy = sort?.id;
177
+ const sortDirection = sort ? sort.desc ? "desc" : "asc" : undefined;
178
+ const fetchData = useCallback(async () => {
179
+ setLoading(true);
180
+ setError(null);
181
+ try {
182
+ const [dealsResult, stageDealsResult, stagesResult] = await Promise.all([
183
+ crm.listDeals({
184
+ projectId,
185
+ pipelineId,
186
+ stageId: options.stageId,
187
+ status: options.status === "all" ? undefined : options.status,
188
+ search: options.search,
189
+ limit: pageSize,
190
+ offset: pageIndex * pageSize,
191
+ sortBy: sortBy === "name" || sortBy === "value" || sortBy === "status" || sortBy === "expectedCloseDate" || sortBy === "updatedAt" ? sortBy : undefined,
192
+ sortDirection
193
+ }),
194
+ crm.getDealsByStage({ projectId, pipelineId }),
195
+ crm.getPipelineStages({ pipelineId })
196
+ ]);
197
+ setData(dealsResult);
198
+ setDealsByStage(stageDealsResult);
199
+ setStages(stagesResult);
200
+ } catch (err) {
201
+ setError(err instanceof Error ? err : new Error("Unknown error"));
202
+ } finally {
203
+ setLoading(false);
204
+ }
205
+ }, [
206
+ crm,
207
+ projectId,
208
+ pipelineId,
209
+ options.stageId,
210
+ options.status,
211
+ options.search,
212
+ pageIndex,
213
+ pageSize,
214
+ sortBy,
215
+ sortDirection
216
+ ]);
217
+ useEffect(() => {
218
+ fetchData();
219
+ }, [fetchData]);
220
+ const stats = useMemo(() => {
221
+ if (!data)
222
+ return null;
223
+ const open = data.deals.filter((d) => d.status === "OPEN");
224
+ const won = data.deals.filter((d) => d.status === "WON");
225
+ const lost = data.deals.filter((d) => d.status === "LOST");
226
+ return {
227
+ total: data.total,
228
+ totalValue: data.totalValue,
229
+ openCount: open.length,
230
+ openValue: open.reduce((sum, d) => sum + d.value, 0),
231
+ wonCount: won.length,
232
+ wonValue: won.reduce((sum, d) => sum + d.value, 0),
233
+ lostCount: lost.length
234
+ };
235
+ }, [data]);
236
+ return {
237
+ data,
238
+ dealsByStage,
239
+ stages,
240
+ loading,
241
+ error,
242
+ stats,
243
+ page: pageIndex + 1,
244
+ pageIndex,
245
+ pageSize,
246
+ refetch: fetchData,
247
+ nextPage: options.pageIndex === undefined ? () => setInternalPage((page) => page + 1) : undefined,
248
+ prevPage: options.pageIndex === undefined ? () => pageIndex > 0 && setInternalPage((page) => page - 1) : undefined
249
+ };
250
+ }
251
+
241
252
  // src/ui/renderers/pipeline.renderer.tsx
242
253
  import { jsxDEV as jsxDEV3 } from "react/jsx-dev-runtime";
243
254
  function CrmPipelineBoardWrapper() {
@@ -0,0 +1,390 @@
1
+ // src/ui/hooks/useDealList.ts
2
+ import { useTemplateRuntime } from "@contractspec/lib.example-shared-ui";
3
+ import { useCallback, useEffect, useMemo, useState } from "react";
4
+ "use client";
5
+ function useDealList(options = {}) {
6
+ const { handlers, projectId } = useTemplateRuntime();
7
+ const { crm } = handlers;
8
+ const [data, setData] = useState(null);
9
+ const [dealsByStage, setDealsByStage] = useState({});
10
+ const [stages, setStages] = useState([]);
11
+ const [loading, setLoading] = useState(true);
12
+ const [error, setError] = useState(null);
13
+ const [internalPage, setInternalPage] = useState(0);
14
+ const pipelineId = options.pipelineId ?? "pipeline-1";
15
+ const pageIndex = options.pageIndex ?? internalPage;
16
+ const pageSize = options.pageSize ?? options.limit ?? 50;
17
+ const [sort] = options.sorting ?? [];
18
+ const sortBy = sort?.id;
19
+ const sortDirection = sort ? sort.desc ? "desc" : "asc" : undefined;
20
+ const fetchData = useCallback(async () => {
21
+ setLoading(true);
22
+ setError(null);
23
+ try {
24
+ const [dealsResult, stageDealsResult, stagesResult] = await Promise.all([
25
+ crm.listDeals({
26
+ projectId,
27
+ pipelineId,
28
+ stageId: options.stageId,
29
+ status: options.status === "all" ? undefined : options.status,
30
+ search: options.search,
31
+ limit: pageSize,
32
+ offset: pageIndex * pageSize,
33
+ sortBy: sortBy === "name" || sortBy === "value" || sortBy === "status" || sortBy === "expectedCloseDate" || sortBy === "updatedAt" ? sortBy : undefined,
34
+ sortDirection
35
+ }),
36
+ crm.getDealsByStage({ projectId, pipelineId }),
37
+ crm.getPipelineStages({ pipelineId })
38
+ ]);
39
+ setData(dealsResult);
40
+ setDealsByStage(stageDealsResult);
41
+ setStages(stagesResult);
42
+ } catch (err) {
43
+ setError(err instanceof Error ? err : new Error("Unknown error"));
44
+ } finally {
45
+ setLoading(false);
46
+ }
47
+ }, [
48
+ crm,
49
+ projectId,
50
+ pipelineId,
51
+ options.stageId,
52
+ options.status,
53
+ options.search,
54
+ pageIndex,
55
+ pageSize,
56
+ sortBy,
57
+ sortDirection
58
+ ]);
59
+ useEffect(() => {
60
+ fetchData();
61
+ }, [fetchData]);
62
+ const stats = useMemo(() => {
63
+ if (!data)
64
+ return null;
65
+ const open = data.deals.filter((d) => d.status === "OPEN");
66
+ const won = data.deals.filter((d) => d.status === "WON");
67
+ const lost = data.deals.filter((d) => d.status === "LOST");
68
+ return {
69
+ total: data.total,
70
+ totalValue: data.totalValue,
71
+ openCount: open.length,
72
+ openValue: open.reduce((sum, d) => sum + d.value, 0),
73
+ wonCount: won.length,
74
+ wonValue: won.reduce((sum, d) => sum + d.value, 0),
75
+ lostCount: lost.length
76
+ };
77
+ }, [data]);
78
+ return {
79
+ data,
80
+ dealsByStage,
81
+ stages,
82
+ loading,
83
+ error,
84
+ stats,
85
+ page: pageIndex + 1,
86
+ pageIndex,
87
+ pageSize,
88
+ refetch: fetchData,
89
+ nextPage: options.pageIndex === undefined ? () => setInternalPage((page) => page + 1) : undefined,
90
+ prevPage: options.pageIndex === undefined ? () => pageIndex > 0 && setInternalPage((page) => page - 1) : undefined
91
+ };
92
+ }
93
+
94
+ // src/ui/tables/DealListTab.tsx
95
+ import {
96
+ Button,
97
+ DataTable,
98
+ LoaderBlock
99
+ } from "@contractspec/lib.design-system";
100
+ import { useContractTable } from "@contractspec/lib.presentation-runtime-react";
101
+ import { Badge } from "@contractspec/lib.ui-kit-web/ui/badge";
102
+ import { HStack, VStack } from "@contractspec/lib.ui-kit-web/ui/stack";
103
+ import { Text } from "@contractspec/lib.ui-kit-web/ui/text";
104
+ import * as React from "react";
105
+ import { jsxDEV } from "react/jsx-dev-runtime";
106
+ "use client";
107
+ function formatCurrency(value, currency = "USD") {
108
+ return new Intl.NumberFormat("en-US", {
109
+ style: "currency",
110
+ currency,
111
+ minimumFractionDigits: 0,
112
+ maximumFractionDigits: 0
113
+ }).format(value);
114
+ }
115
+ function statusVariant(status) {
116
+ switch (status) {
117
+ case "WON":
118
+ return "default";
119
+ case "LOST":
120
+ return "destructive";
121
+ case "STALE":
122
+ return "outline";
123
+ default:
124
+ return "secondary";
125
+ }
126
+ }
127
+ function DealListDataTable({
128
+ deals,
129
+ totalItems,
130
+ pageIndex,
131
+ pageSize,
132
+ sorting,
133
+ loading,
134
+ onSortingChange,
135
+ onPaginationChange,
136
+ onDealClick
137
+ }) {
138
+ const controller = useContractTable({
139
+ data: deals,
140
+ columns: [
141
+ {
142
+ id: "deal",
143
+ header: "Deal",
144
+ label: "Deal",
145
+ accessor: (deal) => deal.name,
146
+ cell: ({ item }) => /* @__PURE__ */ jsxDEV(VStack, {
147
+ gap: "xs",
148
+ children: [
149
+ /* @__PURE__ */ jsxDEV(Text, {
150
+ className: "font-medium text-sm",
151
+ children: item.name
152
+ }, undefined, false, undefined, this),
153
+ /* @__PURE__ */ jsxDEV(Text, {
154
+ className: "text-muted-foreground text-xs",
155
+ children: item.companyId ?? "Unassigned company"
156
+ }, undefined, false, undefined, this)
157
+ ]
158
+ }, undefined, true, undefined, this),
159
+ size: 240,
160
+ minSize: 180,
161
+ canSort: true,
162
+ canPin: true,
163
+ canResize: true
164
+ },
165
+ {
166
+ id: "value",
167
+ header: "Value",
168
+ label: "Value",
169
+ accessorKey: "value",
170
+ cell: ({ item }) => formatCurrency(item.value, item.currency),
171
+ align: "right",
172
+ size: 140,
173
+ canSort: true,
174
+ canResize: true
175
+ },
176
+ {
177
+ id: "status",
178
+ header: "Status",
179
+ label: "Status",
180
+ accessorKey: "status",
181
+ cell: ({ value }) => /* @__PURE__ */ jsxDEV(Badge, {
182
+ variant: statusVariant(value),
183
+ children: String(value)
184
+ }, undefined, false, undefined, this),
185
+ size: 130,
186
+ canSort: true,
187
+ canHide: true,
188
+ canPin: true,
189
+ canResize: true
190
+ },
191
+ {
192
+ id: "expectedCloseDate",
193
+ header: "Expected Close",
194
+ label: "Expected Close",
195
+ accessor: (deal) => deal.expectedCloseDate?.toISOString() ?? "",
196
+ cell: ({ item }) => item.expectedCloseDate?.toLocaleDateString() ?? "Not scheduled",
197
+ size: 170,
198
+ canSort: true,
199
+ canHide: true,
200
+ canResize: true
201
+ },
202
+ {
203
+ id: "updatedAt",
204
+ header: "Updated",
205
+ label: "Updated",
206
+ accessor: (deal) => deal.updatedAt.toISOString(),
207
+ cell: ({ item }) => item.updatedAt.toLocaleDateString(),
208
+ size: 140,
209
+ canSort: true,
210
+ canHide: true,
211
+ canResize: true
212
+ },
213
+ {
214
+ id: "actions",
215
+ header: "Actions",
216
+ label: "Actions",
217
+ accessor: (deal) => deal.id,
218
+ cell: ({ item }) => /* @__PURE__ */ jsxDEV(Button, {
219
+ variant: "ghost",
220
+ size: "sm",
221
+ onPress: () => onDealClick?.(item.id),
222
+ children: "Actions"
223
+ }, undefined, false, undefined, this),
224
+ size: 120,
225
+ canSort: false,
226
+ canHide: false,
227
+ canPin: false,
228
+ canResize: false
229
+ }
230
+ ],
231
+ executionMode: "server",
232
+ selectionMode: "multiple",
233
+ totalItems,
234
+ state: {
235
+ sorting,
236
+ pagination: {
237
+ pageIndex,
238
+ pageSize
239
+ }
240
+ },
241
+ onSortingChange,
242
+ onPaginationChange,
243
+ initialState: {
244
+ columnVisibility: { updatedAt: false },
245
+ columnPinning: { left: ["deal", "status"], right: [] }
246
+ },
247
+ renderExpandedContent: (deal) => /* @__PURE__ */ jsxDEV(VStack, {
248
+ gap: "sm",
249
+ className: "py-2",
250
+ children: [
251
+ /* @__PURE__ */ jsxDEV(HStack, {
252
+ justify: "between",
253
+ children: [
254
+ /* @__PURE__ */ jsxDEV(Text, {
255
+ className: "font-medium text-sm",
256
+ children: "Owner"
257
+ }, undefined, false, undefined, this),
258
+ /* @__PURE__ */ jsxDEV(Text, {
259
+ className: "text-muted-foreground text-sm",
260
+ children: deal.ownerId
261
+ }, undefined, false, undefined, this)
262
+ ]
263
+ }, undefined, true, undefined, this),
264
+ /* @__PURE__ */ jsxDEV(HStack, {
265
+ justify: "between",
266
+ children: [
267
+ /* @__PURE__ */ jsxDEV(Text, {
268
+ className: "font-medium text-sm",
269
+ children: "Contact"
270
+ }, undefined, false, undefined, this),
271
+ /* @__PURE__ */ jsxDEV(Text, {
272
+ className: "text-muted-foreground text-sm",
273
+ children: deal.contactId ?? "No linked contact"
274
+ }, undefined, false, undefined, this)
275
+ ]
276
+ }, undefined, true, undefined, this),
277
+ deal.wonSource ? /* @__PURE__ */ jsxDEV(HStack, {
278
+ justify: "between",
279
+ children: [
280
+ /* @__PURE__ */ jsxDEV(Text, {
281
+ className: "font-medium text-sm",
282
+ children: "Won Source"
283
+ }, undefined, false, undefined, this),
284
+ /* @__PURE__ */ jsxDEV(Text, {
285
+ className: "text-muted-foreground text-sm",
286
+ children: deal.wonSource
287
+ }, undefined, false, undefined, this)
288
+ ]
289
+ }, undefined, true, undefined, this) : null,
290
+ deal.lostReason ? /* @__PURE__ */ jsxDEV(HStack, {
291
+ justify: "between",
292
+ children: [
293
+ /* @__PURE__ */ jsxDEV(Text, {
294
+ className: "font-medium text-sm",
295
+ children: "Lost Reason"
296
+ }, undefined, false, undefined, this),
297
+ /* @__PURE__ */ jsxDEV(Text, {
298
+ className: "text-muted-foreground text-sm",
299
+ children: deal.lostReason
300
+ }, undefined, false, undefined, this)
301
+ ]
302
+ }, undefined, true, undefined, this) : null,
303
+ deal.notes ? /* @__PURE__ */ jsxDEV(VStack, {
304
+ gap: "xs",
305
+ children: [
306
+ /* @__PURE__ */ jsxDEV(Text, {
307
+ className: "font-medium text-sm",
308
+ children: "Notes"
309
+ }, undefined, false, undefined, this),
310
+ /* @__PURE__ */ jsxDEV(Text, {
311
+ className: "text-muted-foreground text-sm",
312
+ children: deal.notes
313
+ }, undefined, false, undefined, this)
314
+ ]
315
+ }, undefined, true, undefined, this) : null
316
+ ]
317
+ }, undefined, true, undefined, this),
318
+ getCanExpand: () => true
319
+ });
320
+ return /* @__PURE__ */ jsxDEV(DataTable, {
321
+ controller,
322
+ title: "All Deals",
323
+ description: "Server-mode table using the shared ContractSpec controller.",
324
+ loading,
325
+ toolbar: /* @__PURE__ */ jsxDEV(HStack, {
326
+ gap: "sm",
327
+ className: "flex-wrap",
328
+ children: [
329
+ /* @__PURE__ */ jsxDEV(Text, {
330
+ className: "text-muted-foreground text-sm",
331
+ children: [
332
+ "Selected ",
333
+ controller.selectedRowIds.length
334
+ ]
335
+ }, undefined, true, undefined, this),
336
+ /* @__PURE__ */ jsxDEV(Text, {
337
+ className: "text-muted-foreground text-sm",
338
+ children: [
339
+ totalItems,
340
+ " total deals"
341
+ ]
342
+ }, undefined, true, undefined, this)
343
+ ]
344
+ }, undefined, true, undefined, this),
345
+ footer: `Page ${controller.pageIndex + 1} of ${controller.pageCount}`,
346
+ emptyState: /* @__PURE__ */ jsxDEV("div", {
347
+ className: "rounded-md border border-dashed p-8 text-center text-muted-foreground text-sm",
348
+ children: "No deals found"
349
+ }, undefined, false, undefined, this)
350
+ }, undefined, false, undefined, this);
351
+ }
352
+ function DealListTab({
353
+ onDealClick
354
+ }) {
355
+ const [sorting, setSorting] = React.useState([
356
+ { id: "value", desc: true }
357
+ ]);
358
+ const [pagination, setPagination] = React.useState({
359
+ pageIndex: 0,
360
+ pageSize: 3
361
+ });
362
+ const { data, loading } = useDealList({
363
+ pageIndex: pagination.pageIndex,
364
+ pageSize: pagination.pageSize,
365
+ sorting
366
+ });
367
+ if (loading && !data) {
368
+ return /* @__PURE__ */ jsxDEV(LoaderBlock, {
369
+ label: "Loading deals..."
370
+ }, undefined, false, undefined, this);
371
+ }
372
+ return /* @__PURE__ */ jsxDEV(DealListDataTable, {
373
+ deals: data?.deals ?? [],
374
+ totalItems: data?.total ?? 0,
375
+ pageIndex: pagination.pageIndex,
376
+ pageSize: pagination.pageSize,
377
+ sorting,
378
+ loading,
379
+ onSortingChange: (nextSorting) => {
380
+ setSorting(nextSorting);
381
+ setPagination((current) => ({ ...current, pageIndex: 0 }));
382
+ },
383
+ onPaginationChange: setPagination,
384
+ onDealClick
385
+ }, undefined, false, undefined, this);
386
+ }
387
+ export {
388
+ DealListTab,
389
+ DealListDataTable
390
+ };
@@ -2,5 +2,5 @@
2
2
  * Deal domain - Deal management in CRM pipeline.
3
3
  */
4
4
  export { DealStatusEnum, DealStatusFilterEnum } from './deal.enum';
5
- export { DealModel, CreateDealInputModel, MoveDealInputModel, DealMovedPayloadModel, WinDealInputModel, DealWonPayloadModel, LoseDealInputModel, DealLostPayloadModel, ListDealsInputModel, ListDealsOutputModel, } from './deal.schema';
6
- export { CreateDealContract, MoveDealContract, WinDealContract, LoseDealContract, ListDealsContract, } from './deal.operation';
5
+ export { CreateDealContract, ListDealsContract, LoseDealContract, MoveDealContract, WinDealContract, } from './deal.operation';
6
+ export { CreateDealInputModel, DealLostPayloadModel, DealModel, DealMovedPayloadModel, DealWonPayloadModel, ListDealsInputModel, ListDealsOutputModel, LoseDealInputModel, MoveDealInputModel, WinDealInputModel, } from './deal.schema';