@handled-ai/design-system 0.8.0 → 0.9.0

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 (336) hide show
  1. package/README.md +14 -4
  2. package/dist/charts/bar-chart-component.d.ts +24 -0
  3. package/dist/charts/bar-chart-component.js +123 -0
  4. package/dist/charts/bar-chart-component.js.map +1 -0
  5. package/dist/charts/chart-tooltip.d.ts +26 -0
  6. package/dist/charts/chart-tooltip.js +69 -0
  7. package/dist/charts/chart-tooltip.js.map +1 -0
  8. package/dist/charts/chart.d.ts +64 -0
  9. package/dist/charts/chart.js +285 -0
  10. package/dist/charts/chart.js.map +1 -0
  11. package/dist/charts/donut-chart.d.ts +21 -0
  12. package/dist/charts/donut-chart.js +96 -0
  13. package/dist/charts/donut-chart.js.map +1 -0
  14. package/dist/charts/index.d.ts +11 -0
  15. package/dist/charts/index.js +10 -0
  16. package/dist/charts/index.js.map +1 -0
  17. package/dist/charts/pipeline-overview.d.ts +76 -0
  18. package/dist/charts/pipeline-overview.js +372 -0
  19. package/dist/charts/pipeline-overview.js.map +1 -0
  20. package/dist/charts/sankey-chart.d.ts +52 -0
  21. package/dist/charts/sankey-chart.js +219 -0
  22. package/dist/charts/sankey-chart.js.map +1 -0
  23. package/dist/charts/top-line-metrics.d.ts +26 -0
  24. package/dist/charts/top-line-metrics.js +224 -0
  25. package/dist/charts/top-line-metrics.js.map +1 -0
  26. package/dist/charts/trend-area-chart.d.ts +21 -0
  27. package/dist/charts/trend-area-chart.js +150 -0
  28. package/dist/charts/trend-area-chart.js.map +1 -0
  29. package/dist/charts/volume-analysis-chart.d.ts +19 -0
  30. package/dist/charts/volume-analysis-chart.js +121 -0
  31. package/dist/charts/volume-analysis-chart.js.map +1 -0
  32. package/dist/components/activity-detail.d.ts +38 -0
  33. package/dist/components/activity-detail.js +163 -0
  34. package/dist/components/activity-detail.js.map +1 -0
  35. package/dist/components/activity-log.d.ts +21 -0
  36. package/dist/components/activity-log.js +61 -0
  37. package/dist/components/activity-log.js.map +1 -0
  38. package/dist/components/agent-popover.d.ts +71 -0
  39. package/dist/components/agent-popover.js +282 -0
  40. package/dist/components/agent-popover.js.map +1 -0
  41. package/dist/components/agent-widget.d.ts +24 -0
  42. package/dist/components/agent-widget.js +117 -0
  43. package/dist/components/agent-widget.js.map +1 -0
  44. package/dist/components/avatar.d.ts +13 -0
  45. package/dist/components/avatar.js +140 -0
  46. package/dist/components/avatar.js.map +1 -0
  47. package/dist/components/badge.d.ts +12 -0
  48. package/dist/components/badge.js +75 -0
  49. package/dist/components/badge.js.map +1 -0
  50. package/dist/components/button.d.ts +13 -0
  51. package/dist/components/button.js +83 -0
  52. package/dist/components/button.js.map +1 -0
  53. package/dist/components/card.d.ts +11 -0
  54. package/dist/components/card.js +119 -0
  55. package/dist/components/card.js.map +1 -0
  56. package/dist/components/contact-list.d.ts +34 -0
  57. package/dist/components/contact-list.js +84 -0
  58. package/dist/components/contact-list.js.map +1 -0
  59. package/dist/components/dashboard-cards.d.ts +10 -0
  60. package/dist/components/dashboard-cards.js +164 -0
  61. package/dist/components/dashboard-cards.js.map +1 -0
  62. package/dist/components/data-table-display.d.ts +19 -0
  63. package/dist/components/data-table-display.js +109 -0
  64. package/dist/components/data-table-display.js.map +1 -0
  65. package/dist/components/data-table-filter.d.ts +18 -0
  66. package/dist/components/data-table-filter.js +107 -0
  67. package/dist/components/data-table-filter.js.map +1 -0
  68. package/dist/components/data-table-quick-views.d.ts +13 -0
  69. package/dist/components/data-table-quick-views.js +90 -0
  70. package/dist/components/data-table-quick-views.js.map +1 -0
  71. package/dist/components/data-table-toolbar.d.ts +18 -0
  72. package/dist/components/data-table-toolbar.js +45 -0
  73. package/dist/components/data-table-toolbar.js.map +1 -0
  74. package/dist/components/data-table.d.ts +39 -0
  75. package/dist/components/data-table.js +821 -0
  76. package/dist/components/data-table.js.map +1 -0
  77. package/dist/components/detail-view.d.ts +44 -0
  78. package/dist/components/detail-view.js +165 -0
  79. package/dist/components/detail-view.js.map +1 -0
  80. package/dist/components/dialog.d.ts +19 -0
  81. package/dist/components/dialog.js +188 -0
  82. package/dist/components/dialog.js.map +1 -0
  83. package/dist/components/dropdown-menu.d.ts +27 -0
  84. package/dist/components/dropdown-menu.js +279 -0
  85. package/dist/components/dropdown-menu.js.map +1 -0
  86. package/dist/components/entity-panel.d.ts +69 -0
  87. package/dist/components/entity-panel.js +584 -0
  88. package/dist/components/entity-panel.js.map +1 -0
  89. package/dist/components/inbox-row.d.ts +27 -0
  90. package/dist/components/inbox-row.js +139 -0
  91. package/dist/components/inbox-row.js.map +1 -0
  92. package/dist/components/inbox-toolbar.d.ts +21 -0
  93. package/dist/components/inbox-toolbar.js +203 -0
  94. package/dist/components/inbox-toolbar.js.map +1 -0
  95. package/dist/components/input.d.ts +5 -0
  96. package/dist/components/input.js +50 -0
  97. package/dist/components/input.js.map +1 -0
  98. package/dist/components/insights-filter-bar.d.ts +21 -0
  99. package/dist/components/insights-filter-bar.js +99 -0
  100. package/dist/components/insights-filter-bar.js.map +1 -0
  101. package/dist/components/item-list-display.d.ts +22 -0
  102. package/dist/components/item-list-display.js +240 -0
  103. package/dist/components/item-list-display.js.map +1 -0
  104. package/dist/components/item-list-filter.d.ts +16 -0
  105. package/dist/components/item-list-filter.js +87 -0
  106. package/dist/components/item-list-filter.js.map +1 -0
  107. package/dist/components/item-list-toolbar.d.ts +25 -0
  108. package/dist/components/item-list-toolbar.js +79 -0
  109. package/dist/components/item-list-toolbar.js.map +1 -0
  110. package/dist/components/item-list.d.ts +20 -0
  111. package/dist/components/item-list.js +702 -0
  112. package/dist/components/item-list.js.map +1 -0
  113. package/dist/components/label.d.ts +6 -0
  114. package/dist/components/label.js +55 -0
  115. package/dist/components/label.js.map +1 -0
  116. package/dist/components/message.d.ts +23 -0
  117. package/dist/components/message.js +117 -0
  118. package/dist/components/message.js.map +1 -0
  119. package/dist/components/metric-card.d.ts +25 -0
  120. package/dist/components/metric-card.js +107 -0
  121. package/dist/components/metric-card.js.map +1 -0
  122. package/dist/components/performance-metrics-table.d.ts +38 -0
  123. package/dist/components/performance-metrics-table.js +342 -0
  124. package/dist/components/performance-metrics-table.js.map +1 -0
  125. package/dist/components/preview-list.d.ts +14 -0
  126. package/dist/components/preview-list.js +83 -0
  127. package/dist/components/preview-list.js.map +1 -0
  128. package/dist/components/progress.d.ts +6 -0
  129. package/dist/components/progress.js +69 -0
  130. package/dist/components/progress.js.map +1 -0
  131. package/dist/components/quick-action-chat-area.d.ts +24 -0
  132. package/dist/components/quick-action-chat-area.js +178 -0
  133. package/dist/components/quick-action-chat-area.js.map +1 -0
  134. package/dist/components/quick-action-modal.d.ts +30 -0
  135. package/dist/components/quick-action-modal.js +288 -0
  136. package/dist/components/quick-action-modal.js.map +1 -0
  137. package/dist/components/quick-action-sidebar-nav.d.ts +51 -0
  138. package/dist/components/quick-action-sidebar-nav.js +528 -0
  139. package/dist/components/quick-action-sidebar-nav.js.map +1 -0
  140. package/dist/components/recommended-actions-section.d.ts +23 -0
  141. package/dist/components/recommended-actions-section.js +215 -0
  142. package/dist/components/recommended-actions-section.js.map +1 -0
  143. package/dist/components/report-card.d.ts +26 -0
  144. package/dist/components/report-card.js +69 -0
  145. package/dist/components/report-card.js.map +1 -0
  146. package/dist/components/score-analysis-modal.d.ts +26 -0
  147. package/dist/components/score-analysis-modal.js +141 -0
  148. package/dist/components/score-analysis-modal.js.map +1 -0
  149. package/dist/components/score-breakdown.d.ts +17 -0
  150. package/dist/components/score-breakdown.js +162 -0
  151. package/dist/components/score-breakdown.js.map +1 -0
  152. package/dist/components/score-feedback.d.ts +40 -0
  153. package/dist/components/score-feedback.js +209 -0
  154. package/dist/components/score-feedback.js.map +1 -0
  155. package/dist/components/score-ring.d.ts +14 -0
  156. package/dist/components/score-ring.js +79 -0
  157. package/dist/components/score-ring.js.map +1 -0
  158. package/dist/components/scroll-area.d.ts +7 -0
  159. package/dist/components/scroll-area.js +101 -0
  160. package/dist/components/scroll-area.js.map +1 -0
  161. package/dist/components/select.d.ts +17 -0
  162. package/dist/components/select.js +228 -0
  163. package/dist/components/select.js.map +1 -0
  164. package/dist/components/separator.d.ts +6 -0
  165. package/dist/components/separator.js +61 -0
  166. package/dist/components/separator.js.map +1 -0
  167. package/dist/components/sheet.d.ts +16 -0
  168. package/dist/components/sheet.js +168 -0
  169. package/dist/components/sheet.js.map +1 -0
  170. package/dist/components/sidebar.d.ts +73 -0
  171. package/dist/components/sidebar.js +723 -0
  172. package/dist/components/sidebar.js.map +1 -0
  173. package/dist/components/signal-feedback-inline.d.ts +51 -0
  174. package/dist/components/signal-feedback-inline.js +548 -0
  175. package/dist/components/signal-feedback-inline.js.map +1 -0
  176. package/dist/components/simple-data-table.d.ts +15 -0
  177. package/dist/components/simple-data-table.js +91 -0
  178. package/dist/components/simple-data-table.js.map +1 -0
  179. package/dist/components/skeleton.d.ts +5 -0
  180. package/dist/components/skeleton.js +44 -0
  181. package/dist/components/skeleton.js.map +1 -0
  182. package/dist/components/status-badge.d.ts +10 -0
  183. package/dist/components/status-badge.js +82 -0
  184. package/dist/components/status-badge.js.map +1 -0
  185. package/dist/components/styled-bar-list.d.ts +20 -0
  186. package/dist/components/styled-bar-list.js +59 -0
  187. package/dist/components/styled-bar-list.js.map +1 -0
  188. package/dist/components/suggested-actions.d.ts +110 -0
  189. package/dist/components/suggested-actions.js +1538 -0
  190. package/dist/components/suggested-actions.js.map +1 -0
  191. package/dist/components/table.d.ts +12 -0
  192. package/dist/components/table.js +147 -0
  193. package/dist/components/table.js.map +1 -0
  194. package/dist/components/tabs.d.ts +14 -0
  195. package/dist/components/tabs.js +129 -0
  196. package/dist/components/tabs.js.map +1 -0
  197. package/dist/components/textarea.d.ts +5 -0
  198. package/dist/components/textarea.js +47 -0
  199. package/dist/components/textarea.js.map +1 -0
  200. package/dist/components/timeline-activity.d.ts +34 -0
  201. package/dist/components/timeline-activity.js +181 -0
  202. package/dist/components/timeline-activity.js.map +1 -0
  203. package/dist/components/tooltip.d.ts +9 -0
  204. package/dist/components/tooltip.js +93 -0
  205. package/dist/components/tooltip.js.map +1 -0
  206. package/dist/components/view-mode-toggle.d.ts +16 -0
  207. package/dist/components/view-mode-toggle.js +24 -0
  208. package/dist/components/view-mode-toggle.js.map +1 -0
  209. package/dist/hooks/use-mobile.d.ts +3 -0
  210. package/dist/hooks/use-mobile.js +21 -0
  211. package/dist/hooks/use-mobile.js.map +1 -0
  212. package/dist/index.d.ts +68 -1878
  213. package/dist/index.js +69 -10918
  214. package/dist/index.js.map +1 -1
  215. package/dist/lib/icons.d.ts +18 -0
  216. package/dist/lib/icons.js +21 -0
  217. package/dist/lib/icons.js.map +1 -0
  218. package/dist/lib/utils.d.ts +5 -0
  219. package/dist/lib/utils.js +9 -0
  220. package/dist/lib/utils.js.map +1 -0
  221. package/dist/prototype/index.d.ts +20 -0
  222. package/dist/prototype/index.js +8 -0
  223. package/dist/prototype/index.js.map +1 -0
  224. package/dist/prototype/prototype-accounts-view.d.ts +22 -0
  225. package/dist/prototype/prototype-accounts-view.js +70 -0
  226. package/dist/prototype/prototype-accounts-view.js.map +1 -0
  227. package/dist/prototype/prototype-admin-view.d.ts +21 -0
  228. package/dist/prototype/prototype-admin-view.js +53 -0
  229. package/dist/prototype/prototype-admin-view.js.map +1 -0
  230. package/dist/prototype/prototype-config.d.ts +226 -0
  231. package/dist/prototype/prototype-config.js +1 -0
  232. package/dist/prototype/prototype-config.js.map +1 -0
  233. package/dist/prototype/prototype-inbox-view.d.ts +48 -0
  234. package/dist/prototype/prototype-inbox-view.js +701 -0
  235. package/dist/prototype/prototype-inbox-view.js.map +1 -0
  236. package/dist/prototype/prototype-insights-view.d.ts +23 -0
  237. package/dist/prototype/prototype-insights-view.js +335 -0
  238. package/dist/prototype/prototype-insights-view.js.map +1 -0
  239. package/dist/prototype/prototype-shell.d.ts +40 -0
  240. package/dist/prototype/prototype-shell.js +190 -0
  241. package/dist/prototype/prototype-shell.js.map +1 -0
  242. package/dist/prototype/prototype-work-queue-view.d.ts +8 -0
  243. package/dist/prototype/prototype-work-queue-view.js +17 -0
  244. package/dist/prototype/prototype-work-queue-view.js.map +1 -0
  245. package/dist/three/agent-orb.d.ts +39 -0
  246. package/dist/three/agent-orb.js +500 -0
  247. package/dist/three/agent-orb.js.map +1 -0
  248. package/dist/three/index.d.ts +2 -0
  249. package/dist/three/index.js +2 -0
  250. package/dist/three/index.js.map +1 -0
  251. package/package.json +98 -17
  252. package/src/charts/bar-chart-component.tsx +150 -0
  253. package/src/charts/chart-tooltip.tsx +86 -0
  254. package/src/charts/chart.tsx +371 -0
  255. package/src/charts/donut-chart.tsx +112 -0
  256. package/src/charts/index.ts +13 -0
  257. package/src/charts/pipeline-overview.tsx +476 -0
  258. package/src/charts/sankey-chart.tsx +290 -0
  259. package/src/charts/top-line-metrics.tsx +261 -0
  260. package/src/charts/trend-area-chart.tsx +150 -0
  261. package/src/charts/volume-analysis-chart.tsx +124 -0
  262. package/src/components/activity-detail.tsx +233 -0
  263. package/src/components/activity-log.tsx +89 -0
  264. package/src/components/agent-popover.tsx +373 -0
  265. package/src/components/agent-widget.tsx +163 -0
  266. package/src/components/avatar.tsx +109 -0
  267. package/src/components/badge.tsx +48 -0
  268. package/src/components/button.tsx +59 -0
  269. package/src/components/card.tsx +92 -0
  270. package/src/components/contact-list.tsx +121 -0
  271. package/src/components/dashboard-cards.tsx +170 -0
  272. package/src/components/data-table-display.tsx +139 -0
  273. package/src/components/data-table-filter.tsx +138 -0
  274. package/src/components/data-table-quick-views.tsx +103 -0
  275. package/src/components/data-table-toolbar.tsx +56 -0
  276. package/src/components/data-table.tsx +915 -0
  277. package/src/components/detail-view.tsx +237 -0
  278. package/src/components/dialog.tsx +158 -0
  279. package/src/components/dropdown-menu.tsx +257 -0
  280. package/src/components/entity-panel.tsx +767 -0
  281. package/src/components/inbox-row.tsx +132 -0
  282. package/src/components/inbox-toolbar.tsx +213 -0
  283. package/src/components/input.tsx +21 -0
  284. package/src/components/insights-filter-bar.tsx +132 -0
  285. package/src/components/item-list-display.tsx +278 -0
  286. package/src/components/item-list-filter.tsx +118 -0
  287. package/src/components/item-list-toolbar.tsx +97 -0
  288. package/src/components/item-list.tsx +843 -0
  289. package/src/components/label.tsx +24 -0
  290. package/src/components/message.tsx +83 -0
  291. package/src/components/metric-card.tsx +178 -0
  292. package/src/components/performance-metrics-table.tsx +442 -0
  293. package/src/components/preview-list.tsx +62 -0
  294. package/src/components/progress.tsx +31 -0
  295. package/src/components/quick-action-chat-area.tsx +156 -0
  296. package/src/components/quick-action-modal.tsx +331 -0
  297. package/src/components/quick-action-sidebar-nav.tsx +592 -0
  298. package/src/components/recommended-actions-section.tsx +258 -0
  299. package/src/components/report-card.tsx +106 -0
  300. package/src/components/score-analysis-modal.tsx +172 -0
  301. package/src/components/score-breakdown.tsx +179 -0
  302. package/src/components/score-feedback.tsx +288 -0
  303. package/src/components/score-ring.tsx +87 -0
  304. package/src/components/scroll-area.tsx +58 -0
  305. package/src/components/select.tsx +190 -0
  306. package/src/components/separator.tsx +28 -0
  307. package/src/components/sheet.tsx +143 -0
  308. package/src/components/sidebar.tsx +726 -0
  309. package/src/components/signal-feedback-inline.tsx +591 -0
  310. package/src/components/simple-data-table.tsx +124 -0
  311. package/src/components/skeleton.tsx +15 -0
  312. package/src/components/status-badge.tsx +63 -0
  313. package/src/components/styled-bar-list.tsx +70 -0
  314. package/src/components/suggested-actions.tsx +1985 -0
  315. package/src/components/table.tsx +116 -0
  316. package/src/components/tabs.tsx +91 -0
  317. package/src/components/textarea.tsx +18 -0
  318. package/src/components/timeline-activity.tsx +234 -0
  319. package/src/components/tooltip.tsx +57 -0
  320. package/src/components/view-mode-toggle.tsx +39 -0
  321. package/src/hooks/use-mobile.ts +21 -0
  322. package/src/index.ts +77 -0
  323. package/src/lib/icons.ts +18 -0
  324. package/src/lib/utils.ts +6 -0
  325. package/src/prototype/index.ts +11 -0
  326. package/src/prototype/prototype-accounts-view.tsx +112 -0
  327. package/src/prototype/prototype-admin-view.tsx +67 -0
  328. package/src/prototype/prototype-config.ts +243 -0
  329. package/src/prototype/prototype-inbox-view.tsx +810 -0
  330. package/src/prototype/prototype-insights-view.tsx +379 -0
  331. package/src/prototype/prototype-shell.tsx +219 -0
  332. package/src/prototype/prototype-work-queue-view.tsx +30 -0
  333. package/src/styles/globals.css +299 -0
  334. package/src/three/agent-orb.tsx +557 -0
  335. package/src/three/index.ts +5 -0
  336. package/src/types/r3f.d.ts +8 -0
@@ -0,0 +1,915 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import {
5
+ Briefcase,
6
+ Calendar,
7
+ DollarSign,
8
+ History,
9
+ Link as LinkIcon,
10
+ SearchX,
11
+ TrendingUp,
12
+ User,
13
+ Users,
14
+ } from "lucide-react"
15
+ import {
16
+ createColumnHelper,
17
+ flexRender,
18
+ getCoreRowModel,
19
+ getSortedRowModel,
20
+ useReactTable,
21
+ type SortingState,
22
+ type VisibilityState,
23
+ } from "@tanstack/react-table"
24
+
25
+ import { cn } from "../lib/utils"
26
+ import { Badge } from "./badge"
27
+ import {
28
+ DataTableQuickViews,
29
+ type DataTableQuickViewValue,
30
+ } from "./data-table-quick-views"
31
+ import { DataTableToolbar } from "./data-table-toolbar"
32
+ import { type DataTableFilterCategory } from "./data-table-filter"
33
+ import { ScoreAnalysisModal } from "./score-analysis-modal"
34
+ import type { ScoreFactor } from "./score-breakdown"
35
+ import { Citation, type SourceDef } from "./detail-view"
36
+
37
+ export type DataRow = {
38
+ id: string
39
+ name: string
40
+ industry: string[]
41
+ accountRisks: string[]
42
+ riskScore: number
43
+ expansionScore: number
44
+ growthIndicators: string[]
45
+ lastInteraction: string
46
+ lastInteractionDays: number
47
+ createdAt: string
48
+ revenue: string
49
+ headcount: string
50
+ lastFunding: string
51
+ owner: string
52
+ opportunityCount: number
53
+ productAdoptionScore: number
54
+ }
55
+
56
+ const QUICK_VIEWS = [
57
+ "Balance Flight Detected",
58
+ "Not Touched in 30+ Days",
59
+ "Open Opportunity, Stalled",
60
+ "Growth Signal Detected",
61
+ "Low Product Adoption",
62
+ ]
63
+
64
+ const MORE_QUICK_VIEWS = [
65
+ "Missed meeting this week",
66
+ "High churn risk score",
67
+ "Key contact departed",
68
+ "Recent large inflow",
69
+ "Dormant (no payments)",
70
+ "Support tickets elevated",
71
+ ]
72
+
73
+ const FILTER_CATEGORIES: DataTableFilterCategory[] = [
74
+ {
75
+ id: "industry",
76
+ label: "Industry",
77
+ icon: Briefcase,
78
+ options: [
79
+ "Software",
80
+ "E-commerce",
81
+ "Financial Technology",
82
+ "Workforce Management",
83
+ "Artificial Intelligence",
84
+ "Health Technology",
85
+ "Design",
86
+ ],
87
+ },
88
+ {
89
+ id: "lastInteraction",
90
+ label: "Last interaction",
91
+ icon: History,
92
+ options: ["1 day ago", "3 days ago", "1 week ago", "1 month ago", "2 months ago"],
93
+ },
94
+ {
95
+ id: "createdAt",
96
+ label: "Created at",
97
+ icon: Calendar,
98
+ options: ["Last 30 days", "Last 90 days", "This year", "Last year"],
99
+ },
100
+ {
101
+ id: "revenue",
102
+ label: "Revenue",
103
+ icon: DollarSign,
104
+ options: ["$0 - $1M", "$1M - $10M", "$10M - $50M", "$50M+"],
105
+ },
106
+ {
107
+ id: "headcount",
108
+ label: "Headcount",
109
+ icon: Users,
110
+ options: ["11-50", "51-200", "201-500", "500-1000", "1000+"],
111
+ },
112
+ {
113
+ id: "lastFunding",
114
+ label: "Last funding",
115
+ icon: TrendingUp,
116
+ options: ["Seed", "Series A", "Series B", "Series C+", "Undisclosed"],
117
+ },
118
+ {
119
+ id: "owner",
120
+ label: "Owner",
121
+ icon: User,
122
+ options: ["Sam Lee", "Alex Morgan", "Jordan Case", "Taylor Reed"],
123
+ },
124
+ {
125
+ id: "opportunityCount",
126
+ label: "Opportunity count",
127
+ icon: LinkIcon,
128
+ options: ["0", "1", "2", "3+"],
129
+ },
130
+ ]
131
+
132
+ const ROWS: DataRow[] = [
133
+ {
134
+ id: "rappi",
135
+ name: "Rappi",
136
+ industry: ["E-commerce", "Food Delivery", "Financial Technology"],
137
+ accountRisks: ["Flight Risk", "Low Engagement"],
138
+ riskScore: 65,
139
+ expansionScore: 45,
140
+ growthIndicators: ["Job Openings", "Recent Funding"],
141
+ lastInteraction: "1 week ago",
142
+ lastInteractionDays: 7,
143
+ createdAt: "This year",
144
+ revenue: "$10M - $50M",
145
+ headcount: "1000+",
146
+ lastFunding: "Series C+",
147
+ owner: "Sam Lee",
148
+ opportunityCount: 2,
149
+ productAdoptionScore: 62,
150
+ },
151
+ {
152
+ id: "codeshot",
153
+ name: "Codeshot",
154
+ industry: ["Software"],
155
+ accountRisks: ["Flight Risk", "Low Engagement"],
156
+ riskScore: 85,
157
+ expansionScore: 20,
158
+ growthIndicators: [],
159
+ lastInteraction: "2 months ago",
160
+ lastInteractionDays: 64,
161
+ createdAt: "Last year",
162
+ revenue: "$1M - $10M",
163
+ headcount: "201-500",
164
+ lastFunding: "Series A",
165
+ owner: "Alex Morgan",
166
+ opportunityCount: 1,
167
+ productAdoptionScore: 31,
168
+ },
169
+ {
170
+ id: "lovi",
171
+ name: "Lovi",
172
+ industry: ["Artificial Intelligence", "Health Technology"],
173
+ accountRisks: ["Low Engagement", "Key Contact Departure"],
174
+ riskScore: 55,
175
+ expansionScore: 75,
176
+ growthIndicators: ["Recent Funding", "Headcount Expansion"],
177
+ lastInteraction: "1 month ago",
178
+ lastInteractionDays: 36,
179
+ createdAt: "This year",
180
+ revenue: "$10M - $50M",
181
+ headcount: "500-1000",
182
+ lastFunding: "Series B",
183
+ owner: "Jordan Case",
184
+ opportunityCount: 3,
185
+ productAdoptionScore: 38,
186
+ },
187
+ {
188
+ id: "anthropic",
189
+ name: "Anthropic",
190
+ industry: ["Software"],
191
+ accountRisks: [],
192
+ riskScore: 25,
193
+ expansionScore: 68,
194
+ growthIndicators: ["Recent Funding", "Headcount Expansion"],
195
+ lastInteraction: "3 days ago",
196
+ lastInteractionDays: 3,
197
+ createdAt: "Last 90 days",
198
+ revenue: "$50M+",
199
+ headcount: "1000+",
200
+ lastFunding: "Series C+",
201
+ owner: "Sam Lee",
202
+ opportunityCount: 2,
203
+ productAdoptionScore: 86,
204
+ },
205
+ {
206
+ id: "buildbear",
207
+ name: "BuildBear",
208
+ industry: ["Software"],
209
+ accountRisks: [],
210
+ riskScore: 35,
211
+ expansionScore: 92,
212
+ growthIndicators: ["Recent Funding", "Revenue Growth"],
213
+ lastInteraction: "1 day ago",
214
+ lastInteractionDays: 1,
215
+ createdAt: "Last 30 days",
216
+ revenue: "$1M - $10M",
217
+ headcount: "51-200",
218
+ lastFunding: "Seed",
219
+ owner: "Taylor Reed",
220
+ opportunityCount: 2,
221
+ productAdoptionScore: 91,
222
+ },
223
+ {
224
+ id: "content-mobbin",
225
+ name: "Content-mobbin",
226
+ industry: ["Workforce Management"],
227
+ accountRisks: [],
228
+ riskScore: 28,
229
+ expansionScore: 85,
230
+ growthIndicators: ["Recent Funding", "Headcount Expansion"],
231
+ lastInteraction: "1 week ago",
232
+ lastInteractionDays: 9,
233
+ createdAt: "Last 30 days",
234
+ revenue: "$0 - $1M",
235
+ headcount: "11-50",
236
+ lastFunding: "Seed",
237
+ owner: "Taylor Reed",
238
+ opportunityCount: 2,
239
+ productAdoptionScore: 77,
240
+ },
241
+ {
242
+ id: "figma",
243
+ name: "Figma",
244
+ industry: ["Design", "Software"],
245
+ accountRisks: [],
246
+ riskScore: 15,
247
+ expansionScore: 88,
248
+ growthIndicators: ["Headcount Expansion", "Job Openings"],
249
+ lastInteraction: "3 days ago",
250
+ lastInteractionDays: 3,
251
+ createdAt: "This year",
252
+ revenue: "$50M+",
253
+ headcount: "1000+",
254
+ lastFunding: "Series C+",
255
+ owner: "Alex Morgan",
256
+ opportunityCount: 1,
257
+ productAdoptionScore: 94,
258
+ },
259
+ {
260
+ id: "loom",
261
+ name: "Loom",
262
+ industry: ["Software"],
263
+ accountRisks: ["Key Contact Departure"],
264
+ riskScore: 35,
265
+ expansionScore: 68,
266
+ growthIndicators: ["Headcount Expansion", "Job Openings"],
267
+ lastInteraction: "1 month ago",
268
+ lastInteractionDays: 33,
269
+ createdAt: "Last year",
270
+ revenue: "$10M - $50M",
271
+ headcount: "500-1000",
272
+ lastFunding: "Series B",
273
+ owner: "Jordan Case",
274
+ opportunityCount: 1,
275
+ productAdoptionScore: 58,
276
+ },
277
+ {
278
+ id: "miro",
279
+ name: "Miro",
280
+ industry: ["Software"],
281
+ accountRisks: [],
282
+ riskScore: 32,
283
+ expansionScore: 55,
284
+ growthIndicators: [],
285
+ lastInteraction: "1 week ago",
286
+ lastInteractionDays: 8,
287
+ createdAt: "This year",
288
+ revenue: "$50M+",
289
+ headcount: "1000+",
290
+ lastFunding: "Series C+",
291
+ owner: "Sam Lee",
292
+ opportunityCount: 0,
293
+ productAdoptionScore: 64,
294
+ },
295
+ {
296
+ id: "webflow",
297
+ name: "Webflow",
298
+ industry: ["Software"],
299
+ accountRisks: [],
300
+ riskScore: 25,
301
+ expansionScore: 72,
302
+ growthIndicators: ["Recent Funding", "Headcount Expansion"],
303
+ lastInteraction: "1 week ago",
304
+ lastInteractionDays: 10,
305
+ createdAt: "Last 90 days",
306
+ revenue: "$10M - $50M",
307
+ headcount: "500-1000",
308
+ lastFunding: "Series B",
309
+ owner: "Alex Morgan",
310
+ opportunityCount: 2,
311
+ productAdoptionScore: 71,
312
+ },
313
+ ]
314
+
315
+ type ScoreAnalysisData = {
316
+ title: string
317
+ description: string
318
+ whyNow: string
319
+ evidence: React.ReactNode[]
320
+ factors: ScoreFactor[]
321
+ }
322
+
323
+ const RISK_SOURCES: SourceDef[] = [
324
+ { id: 1, summary: "Weekly active users declined 12% over the past 30 days with no recovery trend.", meta: "Product telemetry \u00b7 2h ago" },
325
+ { id: 2, summary: "Critical support ticket #4821 has been unresolved for over 48 hours.", meta: "Zendesk \u00b7 6h ago" },
326
+ { id: 3, summary: "Competitor mentions detected in recent Slack conversations from the finance team.", meta: "Slack signal \u00b7 1d ago" },
327
+ ]
328
+
329
+ const EXPANSION_SOURCES: SourceDef[] = [
330
+ { id: 1, summary: "Treasury feature utilization is above the 85th percentile compared to peer accounts.", meta: "Product telemetry \u00b7 3h ago" },
331
+ { id: 2, summary: "Multiple feature requests submitted for advanced reporting and API access.", meta: "Zendesk \u00b7 1d ago" },
332
+ { id: 3, summary: "Finance department headcount grew from 8 to 14 in the last quarter.", meta: "LinkedIn signal \u00b7 2d ago" },
333
+ ]
334
+
335
+ const SCORE_ANALYSIS: Record<string, (row: DataRow) => ScoreAnalysisData> = {
336
+ Risk: (row) => ({
337
+ title: "Risk Score Analysis",
338
+ description:
339
+ "Estimated probability of churn within the next 90 days based on activity and support signals",
340
+ whyNow:
341
+ row.riskScore >= 60
342
+ ? "Critical risk factors detected requiring immediate intervention to prevent churn."
343
+ : "Account health is stable, but monitoring recent support interactions is recommended.",
344
+ evidence: [
345
+ <>Recent decline in <span className="font-medium text-foreground">weekly active users (-12%)</span> with no recovery trend<Citation number={1} source={RISK_SOURCES[0]} /></>,
346
+ <>Unresolved <span className="font-medium text-foreground">critical support ticket</span> open for over 48 hours<Citation number={2} source={RISK_SOURCES[1]} /></>,
347
+ <>Competitor presence detected in recent conversations from finance team<Citation number={3} source={RISK_SOURCES[2]} /></>,
348
+ ],
349
+ factors: [
350
+ { key: "engagement", label: "Engagement drop", score: Math.min(row.riskScore + 10, 100), why: "Weekly active users declined 12% over past 30 days" },
351
+ { key: "support", label: "Support load", score: Math.min(row.riskScore + 5, 100), why: "Unresolved critical ticket open for >48h" },
352
+ { key: "competitive", label: "Competitive risk", score: null, risk: row.riskScore >= 60 ? "High" as const : "Low" as const, why: "Competitor mentions detected in recent conversations" },
353
+ { key: "usage", label: "Product usage", score: Math.max(100 - row.riskScore, 10), why: "Core feature adoption remains consistent" },
354
+ ],
355
+ }),
356
+ Expansion: (row) => ({
357
+ title: "Expansion Score Analysis",
358
+ description:
359
+ "Likelihood of successful upsell and cross-sell opportunities based on usage and engagement",
360
+ whyNow:
361
+ row.expansionScore >= 70
362
+ ? "Usage patterns and growth signals indicate readiness for additional product adoption."
363
+ : "Moderate expansion potential; consider targeted engagement to increase adoption.",
364
+ evidence: [
365
+ <>Treasury utilization above <span className="font-medium text-foreground">85th percentile</span> vs peer accounts<Citation number={1} source={EXPANSION_SOURCES[0]} /></>,
366
+ <>Frequent <span className="font-medium text-foreground">feature requests</span> for advanced reporting and API access<Citation number={2} source={EXPANSION_SOURCES[1]} /></>,
367
+ <>Recent <span className="font-medium text-foreground">team expansion</span> in finance department (8 &rarr; 14)<Citation number={3} source={EXPANSION_SOURCES[2]} /></>,
368
+ ],
369
+ factors: [
370
+ { key: "usage-depth", label: "Usage depth", score: Math.min(row.expansionScore + 8, 100), why: "Active usage across 4+ core product features" },
371
+ { key: "growth", label: "Growth signals", score: row.expansionScore, why: row.growthIndicators.length > 0 ? row.growthIndicators.join(", ") + " detected" : "No active growth signals" },
372
+ { key: "fit", label: "Product fit", score: Math.min(row.expansionScore + 12, 100), why: "Company profile matches high-expansion ICP" },
373
+ { key: "timing", label: "Timing", score: null, risk: row.expansionScore >= 70 ? "Low" as const : "Medium" as const, why: "Within typical evaluation window for upsell" },
374
+ ],
375
+ }),
376
+ }
377
+
378
+ const QUICK_VIEW_FILTERS: Record<string, (row: DataRow) => boolean> = {
379
+ "Balance Flight Detected": (row) => row.riskScore >= 60,
380
+ "Not Touched in 30+ Days": (row) => row.lastInteractionDays >= 30,
381
+ "Open Opportunity, Stalled": (row) =>
382
+ row.opportunityCount > 0 && row.lastInteractionDays >= 21,
383
+ "Growth Signal Detected": (row) => row.expansionScore >= 70,
384
+ "Low Product Adoption": (row) => row.productAdoptionScore <= 40,
385
+ "Missed meeting this week": (row) => row.lastInteractionDays >= 6,
386
+ "High churn risk score": (row) => row.riskScore >= 75,
387
+ "Key contact departed": (row) =>
388
+ row.accountRisks.some((risk) => risk.toLowerCase().includes("contact")),
389
+ "Recent large inflow": (row) => row.expansionScore >= 85,
390
+ "Dormant (no payments)": (row) => row.opportunityCount === 0,
391
+ "Support tickets elevated": (row) =>
392
+ row.accountRisks.some((risk) => risk.toLowerCase().includes("engagement")),
393
+ }
394
+
395
+ const columnHelper = createColumnHelper<DataRow>()
396
+
397
+ const DEFAULT_COLUMN_VISIBILITY: VisibilityState = {
398
+ industry: false,
399
+ lastInteraction: false,
400
+ revenue: false,
401
+ headcount: false,
402
+ lastFunding: false,
403
+ owner: false,
404
+ opportunityCount: false,
405
+ }
406
+
407
+ function getEntityColor(name: string) {
408
+ const colors = [
409
+ "bg-muted text-muted-foreground",
410
+ "bg-gray-100 text-gray-600",
411
+ "bg-zinc-100 text-zinc-600",
412
+ "bg-blue-50 text-blue-600",
413
+ "bg-indigo-50 text-indigo-600",
414
+ "bg-violet-50 text-violet-600",
415
+ ]
416
+
417
+ let hash = 0
418
+ for (let i = 0; i < name.length; i += 1) {
419
+ hash = name.charCodeAt(i) + ((hash << 5) - hash)
420
+ }
421
+ return colors[Math.abs(hash) % colors.length]
422
+ }
423
+
424
+ function getIndustryColor(industry: string) {
425
+ const colors: Record<string, string> = {
426
+ "E-commerce": "bg-emerald-50 text-emerald-700 border-emerald-100",
427
+ "Food Delivery": "bg-blue-50 text-blue-700 border-blue-100",
428
+ "Financial Technology": "bg-amber-50 text-amber-700 border-amber-100",
429
+ "Workforce Management": "bg-violet-50 text-violet-700 border-violet-100",
430
+ "Artificial Intelligence": "bg-rose-50 text-rose-700 border-rose-100",
431
+ "Health Technology": "bg-orange-50 text-orange-700 border-orange-100",
432
+ Software: "bg-muted text-muted-foreground border-border",
433
+ }
434
+
435
+ return colors[industry] ?? "bg-muted text-muted-foreground border-border"
436
+ }
437
+
438
+ function toggleFilterValue(
439
+ current: Record<string, string[]>,
440
+ categoryId: string,
441
+ option: string
442
+ ) {
443
+ const currentValues = current[categoryId] ?? []
444
+ const isActive = currentValues.includes(option)
445
+ const nextValues = isActive
446
+ ? currentValues.filter((value) => value !== option)
447
+ : [...currentValues, option]
448
+
449
+ return {
450
+ ...current,
451
+ [categoryId]: nextValues,
452
+ }
453
+ }
454
+
455
+ function isRowMatchingCategoryFilter(
456
+ row: DataRow,
457
+ categoryId: string,
458
+ options: string[]
459
+ ) {
460
+ if (options.length === 0) {
461
+ return true
462
+ }
463
+
464
+ switch (categoryId) {
465
+ case "industry":
466
+ return options.some((option) => row.industry.includes(option))
467
+ case "lastInteraction":
468
+ return options.includes(row.lastInteraction)
469
+ case "createdAt":
470
+ return options.includes(row.createdAt)
471
+ case "revenue":
472
+ return options.includes(row.revenue)
473
+ case "headcount":
474
+ return options.includes(row.headcount)
475
+ case "lastFunding":
476
+ return options.includes(row.lastFunding)
477
+ case "owner":
478
+ return options.includes(row.owner)
479
+ case "opportunityCount":
480
+ return options.some((option) => {
481
+ if (option === "3+") {
482
+ return row.opportunityCount >= 3
483
+ }
484
+ return row.opportunityCount === Number(option)
485
+ })
486
+ default:
487
+ return true
488
+ }
489
+ }
490
+
491
+ export interface DataTableProps {
492
+ onRowClick?: (row: DataRow) => void
493
+ rows?: DataRow[]
494
+ filterCategories?: DataTableFilterCategory[]
495
+ quickViews?: string[]
496
+ moreQuickViews?: string[]
497
+ quickViewFilters?: Record<string, (row: DataRow) => boolean>
498
+ iconMap?: { salesforce?: string }
499
+ entityUrlBuilder?: (row: DataRow) => string
500
+ onScoreFactorFeedback?: (account: string, scoreType: string, factorKey: string, type: "up" | "down" | null, detail?: string) => void
501
+ onScoreApproveFeedback?: (account: string, scoreType: string, reasons: string[], detail: string) => void
502
+ onScoreDismissFeedback?: (account: string, scoreType: string, reasons: string[], detail: string) => void
503
+ }
504
+
505
+ export function DataTable({
506
+ onRowClick,
507
+ rows: rowsProp,
508
+ filterCategories: filterCategoriesProp,
509
+ quickViews: quickViewsProp,
510
+ moreQuickViews: moreQuickViewsProp,
511
+ quickViewFilters: quickViewFiltersProp,
512
+ iconMap,
513
+ entityUrlBuilder,
514
+ onScoreFactorFeedback,
515
+ onScoreApproveFeedback,
516
+ onScoreDismissFeedback,
517
+ }: DataTableProps = {}) {
518
+ const resolvedRows = rowsProp ?? ROWS
519
+ const resolvedFilterCategories = filterCategoriesProp ?? FILTER_CATEGORIES
520
+ const resolvedQuickViews = quickViewsProp ?? QUICK_VIEWS
521
+ const resolvedMoreQuickViews = moreQuickViewsProp ?? MORE_QUICK_VIEWS
522
+ const resolvedQuickViewFilters = quickViewFiltersProp ?? QUICK_VIEW_FILTERS
523
+
524
+ const [sorting, setSorting] = React.useState<SortingState>([])
525
+ const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>(
526
+ DEFAULT_COLUMN_VISIBILITY
527
+ )
528
+ const [selectedFilters, setSelectedFilters] = React.useState<Record<string, string[]>>(
529
+ {}
530
+ )
531
+ const [activeQuickView, setActiveQuickView] =
532
+ React.useState<DataTableQuickViewValue>(null)
533
+ const [scoreModal, setScoreModal] = React.useState<{
534
+ row: DataRow
535
+ type: "Risk" | "Expansion"
536
+ } | null>(null)
537
+
538
+ React.useEffect(() => {
539
+ if (!activeQuickView) {
540
+ return
541
+ }
542
+
543
+ setColumnVisibility((previous) => {
544
+ const next = { ...previous }
545
+ if (activeQuickView.includes("Touched")) {
546
+ next.lastInteraction = true
547
+ }
548
+ if (activeQuickView.includes("Growth")) {
549
+ next.expansionScore = true
550
+ }
551
+ if (activeQuickView.includes("risk") || activeQuickView.includes("Risk")) {
552
+ next.riskScore = true
553
+ }
554
+ return next
555
+ })
556
+ }, [activeQuickView])
557
+
558
+ const filteredRows = React.useMemo(() => {
559
+ return resolvedRows.filter((row) => {
560
+ const quickViewMatches = activeQuickView
561
+ ? (resolvedQuickViewFilters[activeQuickView]?.(row) ?? true)
562
+ : true
563
+
564
+ if (!quickViewMatches) {
565
+ return false
566
+ }
567
+
568
+ return Object.entries(selectedFilters).every(([categoryId, options]) =>
569
+ isRowMatchingCategoryFilter(row, categoryId, options)
570
+ )
571
+ })
572
+ }, [activeQuickView, selectedFilters, resolvedRows, resolvedQuickViewFilters])
573
+
574
+ const columns = React.useMemo(
575
+ () => [
576
+ columnHelper.accessor("name", {
577
+ header: "Entity",
578
+ cell: (info) => {
579
+ const row = info.row.original
580
+ const sfUrl = entityUrlBuilder?.(row)
581
+ return (
582
+ <div className="flex min-w-max items-center gap-3">
583
+ <div
584
+ className={cn(
585
+ "flex h-6 w-6 shrink-0 items-center justify-center rounded text-[10px] font-bold",
586
+ getEntityColor(row.name)
587
+ )}
588
+ >
589
+ {row.name.slice(0, 1)}
590
+ </div>
591
+ <span className="text-sm font-medium text-foreground">
592
+ {row.name}
593
+ </span>
594
+ {iconMap?.salesforce && (
595
+ <a
596
+ href={sfUrl ?? "#"}
597
+ target="_blank"
598
+ rel="noopener noreferrer"
599
+ onClick={(e) => e.stopPropagation()}
600
+ className="shrink-0 text-muted-foreground hover:text-foreground transition-colors"
601
+ >
602
+ <img src={iconMap.salesforce} alt="Salesforce" className="w-4 h-4 object-contain" />
603
+ </a>
604
+ )}
605
+ </div>
606
+ )
607
+ },
608
+ }),
609
+ columnHelper.accessor("accountRisks", {
610
+ header: "Risk Signals",
611
+ cell: (info) => {
612
+ const risks = info.getValue()
613
+ if (!risks.length) {
614
+ return <span className="text-xs text-muted-foreground">None</span>
615
+ }
616
+
617
+ return (
618
+ <div className="flex min-w-max items-center gap-1.5">
619
+ {risks.slice(0, 2).map((risk) => (
620
+ <Badge
621
+ key={risk}
622
+ variant="outline"
623
+ className="rounded-md border-red-200 bg-red-50 px-2 py-0.5 text-xs font-normal text-red-700"
624
+ >
625
+ {risk}
626
+ </Badge>
627
+ ))}
628
+ </div>
629
+ )
630
+ },
631
+ }),
632
+ columnHelper.accessor("riskScore", {
633
+ id: "riskScore",
634
+ header: "Risk Score",
635
+ cell: (info) => (
636
+ <div
637
+ className="inline-flex cursor-pointer"
638
+ onClick={(e) => {
639
+ e.stopPropagation()
640
+ setScoreModal({ row: info.row.original, type: "Risk" })
641
+ }}
642
+ >
643
+ <Badge
644
+ variant="outline"
645
+ className={cn(
646
+ "px-2 py-0.5 text-xs font-medium hover:underline decoration-dotted underline-offset-2",
647
+ info.getValue() >= 60
648
+ ? "border-red-200 bg-red-50 text-red-700"
649
+ : "border-border bg-muted/50 text-foreground"
650
+ )}
651
+ >
652
+ {info.getValue()}%
653
+ </Badge>
654
+ </div>
655
+ ),
656
+ }),
657
+ columnHelper.accessor("expansionScore", {
658
+ id: "expansionScore",
659
+ header: "Expansion Score",
660
+ cell: (info) => (
661
+ <div
662
+ className="inline-flex cursor-pointer"
663
+ onClick={(e) => {
664
+ e.stopPropagation()
665
+ setScoreModal({ row: info.row.original, type: "Expansion" })
666
+ }}
667
+ >
668
+ <Badge
669
+ variant="outline"
670
+ className={cn(
671
+ "px-2 py-0.5 text-xs font-medium hover:underline decoration-dotted underline-offset-2",
672
+ info.getValue() >= 70
673
+ ? "border-blue-200 bg-blue-50 text-blue-700"
674
+ : "border-border bg-muted/50 text-foreground"
675
+ )}
676
+ >
677
+ {info.getValue()}%
678
+ </Badge>
679
+ </div>
680
+ ),
681
+ }),
682
+ columnHelper.accessor("growthIndicators", {
683
+ header: "Growth Signals",
684
+ cell: (info) => {
685
+ const indicators = info.getValue()
686
+ if (!indicators.length) {
687
+ return <span className="text-xs text-muted-foreground">None</span>
688
+ }
689
+ return (
690
+ <div className="flex min-w-max items-center gap-1.5">
691
+ {indicators.slice(0, 2).map((indicator) => (
692
+ <Badge
693
+ key={indicator}
694
+ variant="outline"
695
+ className="rounded-md border-blue-200 bg-blue-50 px-2 py-0.5 text-xs font-normal text-blue-700"
696
+ >
697
+ {indicator}
698
+ </Badge>
699
+ ))}
700
+ </div>
701
+ )
702
+ },
703
+ }),
704
+ columnHelper.accessor("industry", {
705
+ header: "Industry",
706
+ cell: (info) => (
707
+ <div className="flex min-w-max items-center gap-1.5">
708
+ {info.getValue().slice(0, 2).map((industry) => (
709
+ <Badge
710
+ key={industry}
711
+ variant="outline"
712
+ className={cn(
713
+ "rounded-md px-2 py-0.5 text-xs font-normal",
714
+ getIndustryColor(industry)
715
+ )}
716
+ >
717
+ {industry}
718
+ </Badge>
719
+ ))}
720
+ </div>
721
+ ),
722
+ }),
723
+ columnHelper.accessor("lastInteraction", {
724
+ header: "Last interaction",
725
+ cell: (info) => <span className="text-sm">{info.getValue()}</span>,
726
+ }),
727
+ columnHelper.accessor("revenue", {
728
+ header: "Revenue",
729
+ cell: (info) => (
730
+ <span className="rounded-md bg-muted/50 px-2 py-0.5 text-xs font-medium">
731
+ {info.getValue()}
732
+ </span>
733
+ ),
734
+ }),
735
+ columnHelper.accessor("headcount", {
736
+ header: "Headcount",
737
+ cell: (info) => (
738
+ <span className="rounded-md bg-muted/50 px-2 py-0.5 text-xs font-medium">
739
+ {info.getValue()}
740
+ </span>
741
+ ),
742
+ }),
743
+ columnHelper.accessor("lastFunding", {
744
+ header: "Last funding",
745
+ cell: (info) => (
746
+ <span className="rounded-md bg-muted/50 px-2 py-0.5 text-xs font-medium">
747
+ {info.getValue()}
748
+ </span>
749
+ ),
750
+ }),
751
+ columnHelper.accessor("owner", {
752
+ header: "Owner",
753
+ cell: (info) => <span className="text-sm">{info.getValue()}</span>,
754
+ }),
755
+ columnHelper.accessor("opportunityCount", {
756
+ header: "Opportunity count",
757
+ cell: (info) => (
758
+ <span className="rounded-md bg-muted/50 px-2 py-0.5 text-xs font-medium">
759
+ {info.getValue()}
760
+ </span>
761
+ ),
762
+ }),
763
+ ],
764
+ [iconMap, entityUrlBuilder]
765
+ )
766
+
767
+ const table = useReactTable({
768
+ data: filteredRows,
769
+ columns,
770
+ state: {
771
+ sorting,
772
+ columnVisibility,
773
+ },
774
+ onSortingChange: setSorting,
775
+ onColumnVisibilityChange: setColumnVisibility,
776
+ getCoreRowModel: getCoreRowModel(),
777
+ getSortedRowModel: getSortedRowModel(),
778
+ })
779
+
780
+ const displayColumns = table.getAllLeafColumns().map((column) => {
781
+ const header = column.columnDef.header
782
+ const label = typeof header === "string" ? header : column.id
783
+
784
+ return {
785
+ id: column.id,
786
+ label,
787
+ visible: column.getIsVisible(),
788
+ canHide: column.getCanHide(),
789
+ }
790
+ })
791
+
792
+ const toggleCategoryFilter = (categoryId: string, option: string) => {
793
+ setSelectedFilters((previous) => toggleFilterValue(previous, categoryId, option))
794
+ }
795
+
796
+ return (
797
+ <div className="relative flex h-full min-h-[560px] flex-col bg-background">
798
+ <DataTableToolbar
799
+ categories={resolvedFilterCategories}
800
+ selectedFilters={selectedFilters}
801
+ onToggleFilter={toggleCategoryFilter}
802
+ sorting={sorting}
803
+ onSortingChange={setSorting}
804
+ displayColumns={displayColumns}
805
+ onToggleColumn={(columnId) => table.getColumn(columnId)?.toggleVisibility()}
806
+ onResetDisplay={() => setColumnVisibility(DEFAULT_COLUMN_VISIBILITY)}
807
+ />
808
+
809
+ <DataTableQuickViews
810
+ quickViews={resolvedQuickViews}
811
+ moreViews={resolvedMoreQuickViews}
812
+ activeView={activeQuickView}
813
+ onViewChange={setActiveQuickView}
814
+ />
815
+
816
+ <div className="relative min-h-0 flex-1 overflow-auto border-t border-border">
817
+ <table className="w-max min-w-full border-collapse text-sm">
818
+ <thead className="sticky top-0 z-10 bg-background">
819
+ {table.getHeaderGroups().map((headerGroup) => (
820
+ <tr key={headerGroup.id} className="border-b border-border">
821
+ {headerGroup.headers.map((header) => (
822
+ <th
823
+ key={header.id}
824
+ className="h-10 border-r border-border px-4 text-left text-xs font-medium text-muted-foreground/80 last:border-r-0"
825
+ >
826
+ {header.isPlaceholder
827
+ ? null
828
+ : flexRender(
829
+ header.column.columnDef.header,
830
+ header.getContext()
831
+ )}
832
+ </th>
833
+ ))}
834
+ </tr>
835
+ ))}
836
+ </thead>
837
+ <tbody>
838
+ {table.getRowModel().rows.length > 0 ? (
839
+ <>
840
+ {table.getRowModel().rows.map((row) => (
841
+ <tr
842
+ key={row.id}
843
+ onClick={() => onRowClick?.(row.original)}
844
+ className={cn(
845
+ "group border-b border-border/50 transition-colors hover:bg-muted/30",
846
+ onRowClick && "cursor-pointer",
847
+ )}
848
+ >
849
+ {row.getVisibleCells().map((cell) => (
850
+ <td
851
+ key={cell.id}
852
+ className="border-r border-border/40 px-4 py-2.5 align-middle last:border-r-0"
853
+ >
854
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
855
+ </td>
856
+ ))}
857
+ </tr>
858
+ ))}
859
+ <tr>
860
+ <td
861
+ className="px-4 py-2 text-xs text-muted-foreground"
862
+ colSpan={columns.length}
863
+ >
864
+ {table.getRowModel().rows.length} rows
865
+ </td>
866
+ </tr>
867
+ </>
868
+ ) : (
869
+ <tr>
870
+ <td colSpan={columns.length} className="h-52 px-4 text-center">
871
+ <div className="flex flex-col items-center gap-1 text-muted-foreground">
872
+ <SearchX className="h-7 w-7 opacity-40" />
873
+ <p className="text-sm font-medium">No rows found</p>
874
+ <p className="text-xs">Try adjusting your filters or quick views</p>
875
+ </div>
876
+ </td>
877
+ </tr>
878
+ )}
879
+ </tbody>
880
+ </table>
881
+ </div>
882
+
883
+ {scoreModal && (() => {
884
+ const data = SCORE_ANALYSIS[scoreModal.type](scoreModal.row)
885
+ return (
886
+ <ScoreAnalysisModal
887
+ open
888
+ onOpenChange={(open) => { if (!open) setScoreModal(null) }}
889
+ title={data.title}
890
+ description={data.description}
891
+ score={scoreModal.type === "Risk" ? scoreModal.row.riskScore : scoreModal.row.expansionScore}
892
+ whyNow={data.whyNow}
893
+ evidence={data.evidence}
894
+ factors={data.factors}
895
+ onFactorFeedback={onScoreFactorFeedback
896
+ ? (key, type, detail) => onScoreFactorFeedback(scoreModal.row.name, scoreModal.type, key, type, detail)
897
+ : (key, type, detail) => console.log("Factor feedback:", { account: scoreModal.row.name, factor: key, type, detail })
898
+ }
899
+ companyName={scoreModal.row.name}
900
+ opportunityUrl={`https://acme.lightning.force.com/lightning/r/Opportunity/006${scoreModal.row.id}/view`}
901
+ onApprove={() => console.log("Approved signal — creating opportunity:", { account: scoreModal.row.name, type: scoreModal.type })}
902
+ onApproveFeedback={onScoreApproveFeedback
903
+ ? (reasons, detail) => onScoreApproveFeedback(scoreModal.row.name, scoreModal.type, reasons, detail)
904
+ : (reasons, detail) => console.log("Approval feedback:", { account: scoreModal.row.name, reasons, detail })
905
+ }
906
+ onDismiss={onScoreDismissFeedback
907
+ ? (reasons, detail) => onScoreDismissFeedback(scoreModal.row.name, scoreModal.type, reasons, detail)
908
+ : (reasons, detail) => console.log("Dismissed signal:", { account: scoreModal.row.name, reasons, detail })
909
+ }
910
+ />
911
+ )
912
+ })()}
913
+ </div>
914
+ )
915
+ }