@graph8/sdk 0.370.0 → 0.373.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 (466) hide show
  1. package/README.md +19 -0
  2. package/blocks/_foundation/README.md +14 -0
  3. package/blocks/_foundation/block.json +44 -0
  4. package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
  5. package/blocks/_foundation/components/empty-state.tsx +5 -2
  6. package/blocks/_foundation/components/list-select.tsx +112 -0
  7. package/blocks/_foundation/components/paging-notices.tsx +55 -0
  8. package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
  9. package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
  10. package/blocks/_foundation/components/screen/index.ts +1 -0
  11. package/blocks/_foundation/components/screen/split-view.tsx +1 -1
  12. package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
  13. package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
  14. package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
  15. package/blocks/_foundation/lib/paging.ts +57 -0
  16. package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
  17. package/blocks/agents/README.md +81 -4
  18. package/blocks/agents/block.json +311 -2
  19. package/blocks/agents/components/agent-activity.tsx +112 -0
  20. package/blocks/agents/components/agent-approvals.tsx +135 -0
  21. package/blocks/agents/components/agent-builder.tsx +17 -3
  22. package/blocks/agents/components/agent-conversations.tsx +143 -0
  23. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
  24. package/blocks/agents/components/agent-operators.tsx +164 -0
  25. package/blocks/agents/components/agent-run-detail.tsx +20 -3
  26. package/blocks/agents/components/agent-runs.tsx +3 -3
  27. package/blocks/agents/components/agent-skills-panel.tsx +7 -7
  28. package/blocks/agents/components/agent-test-history.tsx +121 -0
  29. package/blocks/agents/components/agents-screen.tsx +53 -11
  30. package/blocks/agents/components/assistant-sessions.tsx +150 -0
  31. package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
  32. package/blocks/agents/components/from-template-button.tsx +65 -0
  33. package/blocks/agents/components/knowledge-browser.tsx +107 -0
  34. package/blocks/agents/components/knowledge-detail.tsx +156 -0
  35. package/blocks/agents/components/knowledge-form.tsx +69 -0
  36. package/blocks/agents/components/name-dialog.tsx +32 -0
  37. package/blocks/agents/components/receptionist-button.tsx +46 -0
  38. package/blocks/agents/components/save-as-template-button.tsx +32 -0
  39. package/blocks/agents/components/skill-editor.tsx +147 -0
  40. package/blocks/agents/components/skill-library.tsx +111 -0
  41. package/blocks/agents/lib/agent-types.ts +7 -1
  42. package/blocks/agents/lib/facts.tsx +13 -0
  43. package/blocks/agents/lib/knowledge-utils.ts +49 -0
  44. package/blocks/agents/lib/load-more.tsx +141 -0
  45. package/blocks/agents/lib/skill-utils.ts +91 -0
  46. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
  47. package/blocks/analytics/components/analytics-screen.tsx +16 -3
  48. package/blocks/analytics/components/kpi-tiles.tsx +1 -1
  49. package/blocks/analytics/components/report-builder.tsx +15 -2
  50. package/blocks/appointments/README.md +6 -0
  51. package/blocks/appointments/block.json +27 -0
  52. package/blocks/appointments/components/booking-page.tsx +2 -2
  53. package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
  54. package/blocks/appointments/components/meeting-detail.tsx +2 -2
  55. package/blocks/appointments/components/meeting-list.tsx +29 -13
  56. package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
  57. package/blocks/campaigns/components/campaign-assets.tsx +1 -1
  58. package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
  59. package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
  60. package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
  61. package/blocks/campaigns/components/icp-detail.tsx +11 -2
  62. package/blocks/campaigns/components/icp-editor.tsx +8 -3
  63. package/blocks/campaigns/components/persona-detail.tsx +11 -2
  64. package/blocks/campaigns/components/persona-editor.tsx +6 -1
  65. package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
  66. package/blocks/coach/components/call-scorecard.tsx +7 -7
  67. package/blocks/company-intelligence/README.md +132 -0
  68. package/blocks/company-intelligence/block.json +318 -0
  69. package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
  70. package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
  71. package/blocks/company-intelligence/components/company-profile.tsx +68 -0
  72. package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
  73. package/blocks/company-intelligence/components/report-detail.tsx +189 -0
  74. package/blocks/company-intelligence/components/research-reports.tsx +136 -0
  75. package/blocks/company-intelligence/components/research-run.tsx +161 -0
  76. package/blocks/company-intelligence/components/source-detail.tsx +117 -0
  77. package/blocks/company-intelligence/components/source-view.tsx +274 -0
  78. package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
  79. package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
  80. package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
  81. package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
  82. package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
  83. package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
  84. package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
  85. package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
  86. package/blocks/copilot/components/copilot-memory.tsx +6 -2
  87. package/blocks/copilot/components/copilot-panel.tsx +1 -1
  88. package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
  89. package/blocks/crm/README.md +6 -0
  90. package/blocks/crm/block.json +53 -4
  91. package/blocks/crm/components/companies-screen.tsx +23 -9
  92. package/blocks/crm/components/company-detail.tsx +2 -2
  93. package/blocks/crm/components/contact-detail.tsx +4 -4
  94. package/blocks/crm/components/contact-lookup-link.tsx +44 -0
  95. package/blocks/crm/components/contacts-screen.tsx +31 -13
  96. package/blocks/crm/components/dedupe-review.tsx +6 -1
  97. package/blocks/crm/components/import-wizard.tsx +3 -7
  98. package/blocks/crm/components/list-select.tsx +20 -0
  99. package/blocks/crm/components/record-engagement-tab.tsx +103 -0
  100. package/blocks/crm/components/record-form.tsx +1 -1
  101. package/blocks/crm/components/tasks-screen.tsx +35 -15
  102. package/blocks/crm/hooks/use-all-lists.ts +18 -0
  103. package/blocks/crm/hooks/use-page-query.ts +3 -1
  104. package/blocks/crm/lib/crm-types.ts +14 -0
  105. package/blocks/crm/lib/read-lists.ts +16 -0
  106. package/blocks/deals/README.md +24 -6
  107. package/blocks/deals/block.json +151 -2
  108. package/blocks/deals/components/close-deal-dialog.tsx +169 -0
  109. package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
  110. package/blocks/deals/components/deal-detail.tsx +2 -2
  111. package/blocks/deals/components/deal-filters.tsx +65 -0
  112. package/blocks/deals/components/deal-line-items.tsx +120 -0
  113. package/blocks/deals/components/deal-notes-tab.tsx +97 -0
  114. package/blocks/deals/components/deal-related.tsx +30 -9
  115. package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
  116. package/blocks/deals/components/deals-screen.tsx +57 -22
  117. package/blocks/deals/components/forecast-view.tsx +4 -4
  118. package/blocks/deals/components/pipeline-board.tsx +8 -6
  119. package/blocks/deals/components/pipeline-editor.tsx +132 -0
  120. package/blocks/deals/components/record-deals-tab.tsx +62 -0
  121. package/blocks/deals/components/stage-editor.tsx +1 -1
  122. package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
  123. package/blocks/deals/lib/deal-extras.ts +156 -0
  124. package/blocks/dialer/README.md +14 -5
  125. package/blocks/dialer/block.json +17 -2
  126. package/blocks/dialer/components/call-contact-button.tsx +26 -9
  127. package/blocks/dialer/components/call-log-screen.tsx +4 -1
  128. package/blocks/dialer/components/call-log.tsx +16 -4
  129. package/blocks/dialer/components/call-queue.tsx +3 -3
  130. package/blocks/dialer/components/dialer-screen.tsx +69 -27
  131. package/blocks/dialer/components/disposition-panel.tsx +1 -1
  132. package/blocks/dialer/components/record-calls-tab.tsx +101 -0
  133. package/blocks/dialer/components/session-start-form.tsx +58 -11
  134. package/blocks/dialer/components/softphone.tsx +1 -1
  135. package/blocks/enrichment/README.md +16 -6
  136. package/blocks/enrichment/block.json +19 -7
  137. package/blocks/enrichment/components/bulk-enrich-panel.tsx +76 -27
  138. package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
  139. package/blocks/enrichment/components/enrich-button.tsx +1 -1
  140. package/blocks/enrichment/components/enrichment-screen.tsx +70 -17
  141. package/blocks/enrichment/components/waterfall-config.tsx +27 -7
  142. package/blocks/enrichment/lib/enrichment-types.ts +39 -0
  143. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
  144. package/blocks/forms/README.md +8 -0
  145. package/blocks/forms/block.json +107 -0
  146. package/blocks/forms/components/form-activity.tsx +71 -0
  147. package/blocks/forms/components/form-backfill.tsx +108 -0
  148. package/blocks/forms/components/form-builder.tsx +1 -1
  149. package/blocks/forms/components/form-embed.tsx +1 -1
  150. package/blocks/forms/components/form-groups.tsx +151 -0
  151. package/blocks/forms/components/form-mapping-editor.tsx +6 -9
  152. package/blocks/forms/components/form-route-history.tsx +72 -0
  153. package/blocks/forms/components/form-saved-views.tsx +81 -0
  154. package/blocks/forms/components/forms-screen.tsx +51 -17
  155. package/blocks/forms/components/more-rows.tsx +30 -0
  156. package/blocks/forms/components/submissions-export.tsx +67 -0
  157. package/blocks/forms/components/submissions-table.tsx +15 -6
  158. package/blocks/forms/lib/forms-extra.ts +67 -0
  159. package/blocks/inbox/README.md +5 -0
  160. package/blocks/inbox/block.json +15 -0
  161. package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
  162. package/blocks/inbox/components/thread-badge.tsx +2 -2
  163. package/blocks/inbox/components/thread-view.tsx +1 -1
  164. package/blocks/integrations/README.md +54 -1
  165. package/blocks/integrations/block.json +189 -15
  166. package/blocks/integrations/components/ad-accounts.tsx +82 -0
  167. package/blocks/integrations/components/connection-health.tsx +59 -0
  168. package/blocks/integrations/components/crm-detail.tsx +20 -1
  169. package/blocks/integrations/components/crm-records.tsx +103 -0
  170. package/blocks/integrations/components/field-mappings.tsx +61 -0
  171. package/blocks/integrations/components/integration-list.tsx +4 -4
  172. package/blocks/integrations/components/integrations-screen.tsx +20 -3
  173. package/blocks/integrations/components/migration-panel.tsx +133 -0
  174. package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
  175. package/blocks/integrations/components/sync-status.tsx +6 -6
  176. package/blocks/integrations/components/sync-triggers.tsx +134 -0
  177. package/blocks/integrations/components/webhook-detail.tsx +26 -7
  178. package/blocks/integrations/components/webhook-manager.tsx +30 -17
  179. package/blocks/integrations/components/workspace-tools.tsx +127 -0
  180. package/blocks/integrations/lib/crm-utils.ts +39 -0
  181. package/blocks/integrations/lib/facts.tsx +13 -0
  182. package/blocks/integrations/lib/integrations-utils.ts +5 -0
  183. package/blocks/integrations/lib/load-more.tsx +141 -0
  184. package/blocks/intent/components/account-intent-score.tsx +5 -5
  185. package/blocks/intent/components/intent-feed.tsx +8 -8
  186. package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
  187. package/blocks/intent/lib/intent-utils.ts +4 -4
  188. package/blocks/lifecycle/components/account-related.tsx +8 -1
  189. package/blocks/lifecycle/components/churn-list.tsx +5 -2
  190. package/blocks/lifecycle/components/customer-table.tsx +1 -1
  191. package/blocks/lists/README.md +7 -1
  192. package/blocks/lists/block.json +31 -3
  193. package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
  194. package/blocks/lists/components/add-to-list-button.tsx +17 -7
  195. package/blocks/lists/components/list-picker.tsx +13 -3
  196. package/blocks/lists/components/list-table.tsx +1 -1
  197. package/blocks/lists/components/lists-screen.tsx +4 -2
  198. package/blocks/lists/components/record-lists-tab.tsx +99 -0
  199. package/blocks/lists/components/suppression-manager.tsx +27 -6
  200. package/blocks/lists/components/suppressions-screen.tsx +21 -12
  201. package/blocks/mailboxes/block.json +4 -0
  202. package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
  203. package/blocks/mailboxes/components/domain-detail.tsx +10 -4
  204. package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
  205. package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
  206. package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
  207. package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
  208. package/blocks/mailboxes/components/warmup-health.tsx +20 -20
  209. package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
  210. package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
  211. package/blocks/newsletter/README.md +12 -0
  212. package/blocks/newsletter/block.json +117 -0
  213. package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
  214. package/blocks/newsletter/components/issue-versions.tsx +93 -0
  215. package/blocks/newsletter/components/more-rows.tsx +30 -0
  216. package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
  217. package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
  218. package/blocks/newsletter/components/newsletter-screen.tsx +36 -13
  219. package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
  220. package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
  221. package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
  222. package/blocks/newsletter/components/subscriber-table.tsx +5 -7
  223. package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
  224. package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
  225. package/blocks/nurture/components/nurture-builder.tsx +10 -2
  226. package/blocks/nurture/components/nurture-enroll.tsx +6 -7
  227. package/blocks/nurture/components/nurture-form.tsx +5 -8
  228. package/blocks/nurture/components/nurture-screen.tsx +16 -5
  229. package/blocks/nurture/components/nurture-stats.tsx +5 -4
  230. package/blocks/objects/README.md +1 -1
  231. package/blocks/objects/block.json +13 -5
  232. package/blocks/objects/components/attribute-field-input.tsx +14 -1
  233. package/blocks/objects/components/object-record-detail.tsx +8 -1
  234. package/blocks/objects/components/object-schema-editor.tsx +1 -1
  235. package/blocks/objects/components/record-roles-panel.tsx +15 -5
  236. package/blocks/pages/README.md +14 -0
  237. package/blocks/pages/block.json +113 -1
  238. package/blocks/pages/components/more-rows.tsx +30 -0
  239. package/blocks/pages/components/page-ai-tools.tsx +196 -0
  240. package/blocks/pages/components/page-analytics.tsx +176 -0
  241. package/blocks/pages/components/page-clone.tsx +99 -0
  242. package/blocks/pages/components/page-domains.tsx +167 -0
  243. package/blocks/pages/components/page-editor.tsx +1 -1
  244. package/blocks/pages/components/page-generation-status.tsx +48 -0
  245. package/blocks/pages/components/page-tracking.tsx +137 -0
  246. package/blocks/pages/components/pages-screen.tsx +57 -16
  247. package/blocks/pages/components/seo-report.tsx +6 -1
  248. package/blocks/pages/lib/studio.ts +7 -0
  249. package/blocks/phone-numbers/README.md +74 -0
  250. package/blocks/phone-numbers/block.json +262 -0
  251. package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
  252. package/blocks/phone-numbers/components/assign-number.tsx +104 -0
  253. package/blocks/phone-numbers/components/number-search.tsx +239 -0
  254. package/blocks/phone-numbers/components/number-settings.tsx +134 -0
  255. package/blocks/phone-numbers/components/number-usage.tsx +97 -0
  256. package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
  257. package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
  258. package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
  259. package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
  260. package/blocks/phone-numbers/components/release-number.tsx +126 -0
  261. package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
  262. package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
  263. package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
  264. package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
  265. package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
  266. package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
  267. package/blocks/pickers/README.md +23 -0
  268. package/blocks/pickers/block.json +103 -0
  269. package/blocks/pickers/components/company-picker.tsx +90 -0
  270. package/blocks/pickers/components/contact-picker.tsx +131 -0
  271. package/blocks/pickers/components/member-picker.tsx +99 -0
  272. package/blocks/pickers/components/search-picker.tsx +98 -0
  273. package/blocks/pickers/hooks/use-current-member.ts +104 -0
  274. package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
  275. package/blocks/pickers/lib/use-debounced.ts +13 -0
  276. package/blocks/quotes/README.md +13 -0
  277. package/blocks/quotes/block.json +48 -1
  278. package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
  279. package/blocks/quotes/components/quote-builder.tsx +1 -1
  280. package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
  281. package/blocks/quotes/components/quote-list.tsx +5 -3
  282. package/blocks/quotes/components/quote-related.tsx +3 -2
  283. package/blocks/quotes/components/quote-viewer.tsx +7 -2
  284. package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
  285. package/blocks/radar/components/competitor-battle-card.tsx +1 -1
  286. package/blocks/radar/components/radar-overview.tsx +4 -4
  287. package/blocks/radar/components/radar-screen.tsx +2 -2
  288. package/blocks/sequencer/README.md +6 -0
  289. package/blocks/sequencer/block.json +28 -1
  290. package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
  291. package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
  292. package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
  293. package/blocks/sequencer/components/sequence-builder.tsx +19 -20
  294. package/blocks/sequencer/components/sequence-detail.tsx +4 -2
  295. package/blocks/sequencer/components/sequence-enrollments.tsx +4 -2
  296. package/blocks/sequencer/components/sequence-list.tsx +1 -1
  297. package/blocks/sequencer/components/sequence-stats.tsx +8 -8
  298. package/blocks/sequencer/components/sequences-screen.tsx +3 -0
  299. package/blocks/sequencer/components/step-editor.tsx +11 -11
  300. package/blocks/sequencer/lib/sequencer-utils.ts +5 -5
  301. package/blocks/social-ads/README.md +8 -0
  302. package/blocks/social-ads/block.json +137 -4
  303. package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
  304. package/blocks/social-ads/components/audience-sync.tsx +6 -2
  305. package/blocks/social-ads/components/campaign-ads.tsx +166 -0
  306. package/blocks/social-ads/components/campaign-detail.tsx +6 -1
  307. package/blocks/social-ads/components/campaign-editor.tsx +134 -0
  308. package/blocks/social-ads/components/creative-concepts.tsx +74 -0
  309. package/blocks/social-ads/components/creative-detail.tsx +6 -1
  310. package/blocks/social-ads/components/creative-gallery.tsx +20 -2
  311. package/blocks/social-ads/components/creative-revisions.tsx +57 -0
  312. package/blocks/social-ads/components/creative-tools.tsx +176 -0
  313. package/blocks/social-ads/components/post-composer.tsx +4 -2
  314. package/blocks/social-ads/components/post-detail.tsx +3 -0
  315. package/blocks/social-ads/components/social-ideas.tsx +64 -0
  316. package/blocks/social-ads/components/social-performance.tsx +80 -0
  317. package/blocks/social-ads/components/social-screen.tsx +33 -6
  318. package/blocks/social-ads/lib/ads-extra.ts +67 -0
  319. package/blocks/surveys/README.md +91 -0
  320. package/blocks/surveys/block.json +299 -0
  321. package/blocks/surveys/components/account-nps.tsx +69 -0
  322. package/blocks/surveys/components/contact-survey-history.tsx +52 -0
  323. package/blocks/surveys/components/org-summary.tsx +82 -0
  324. package/blocks/surveys/components/sending-domains.tsx +113 -0
  325. package/blocks/surveys/components/survey-form.tsx +126 -0
  326. package/blocks/surveys/components/survey-responses.tsx +145 -0
  327. package/blocks/surveys/components/survey-results.tsx +72 -0
  328. package/blocks/surveys/components/survey-send-history.tsx +58 -0
  329. package/blocks/surveys/components/survey-send.tsx +162 -0
  330. package/blocks/surveys/components/survey-triggers.tsx +166 -0
  331. package/blocks/surveys/components/surveys-screen.tsx +234 -0
  332. package/blocks/surveys/lib/surveys-types.ts +228 -0
  333. package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
  334. package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
  335. package/blocks/surveys/recipes/survey-score-report.ts +27 -0
  336. package/blocks/visitors/components/snippet-install.tsx +18 -9
  337. package/blocks/visitors/components/traffic-chart.tsx +4 -4
  338. package/blocks/visitors/components/visitor-company-card.tsx +24 -14
  339. package/blocks/visitors/components/visitor-feed.tsx +5 -5
  340. package/blocks/webchat/components/chat-widget-config.tsx +1 -1
  341. package/blocks/webchat/components/conversation-list.tsx +1 -1
  342. package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
  343. package/blocks/workbench/components/run-progress.tsx +1 -1
  344. package/blocks/workbench/components/workbench-grid.tsx +1 -1
  345. package/blocks/workbench/components/workbench-screen.tsx +15 -5
  346. package/blocks/workbench/components/workbench-table-list.tsx +14 -2
  347. package/blocks/workflows/components/workflow-editor.tsx +1 -1
  348. package/dist/cli.js +536 -392
  349. package/dist/index.browser.js +1 -1
  350. package/dist/index.browser.js.map +1 -1
  351. package/dist/index.browser.mjs +1 -1
  352. package/dist/index.browser.mjs.map +1 -1
  353. package/dist/index.d.mts +2 -2
  354. package/dist/index.d.ts +2 -2
  355. package/dist/index.js +1 -1
  356. package/dist/index.js.map +1 -1
  357. package/dist/index.mjs +1 -1
  358. package/dist/index.mjs.map +1 -1
  359. package/dist/react.browser.js +1 -1
  360. package/dist/react.browser.js.map +1 -1
  361. package/dist/react.browser.mjs +1 -1
  362. package/dist/react.browser.mjs.map +1 -1
  363. package/dist/react.d.mts +2 -2
  364. package/dist/react.d.ts +2 -2
  365. package/dist/react.js +1 -1
  366. package/dist/react.js.map +1 -1
  367. package/dist/react.mjs +1 -1
  368. package/dist/react.mjs.map +1 -1
  369. package/dist/types/blocks/brand.d.ts +11 -2
  370. package/dist/types/blocks/create.d.ts +7 -3
  371. package/dist/types/blocks/doctor.d.ts +1 -0
  372. package/dist/types/blocks/flows.d.ts +7 -1
  373. package/dist/types/blocks/orgBrand.d.ts +34 -0
  374. package/dist/types/cli.d.ts +3 -0
  375. package/dist/types/version.d.ts +2 -2
  376. package/docs/api/desk.d.ts +2 -2
  377. package/docs/api/desk.md +3 -3
  378. package/package.json +1 -1
  379. package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
  380. package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
  381. package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
  382. package/stacks/6sense.json +17 -4
  383. package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
  384. package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
  385. package/stacks/apollo/pages/contacts.tsx.tmpl +29 -95
  386. package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
  387. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
  388. package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
  389. package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
  390. package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
  391. package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
  392. package/stacks/apollo.json +36 -8
  393. package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
  394. package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
  395. package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
  396. package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
  397. package/stacks/calendly.json +8 -4
  398. package/stacks/clay/pages/companies.tsx.tmpl +17 -1
  399. package/stacks/clay/pages/contacts.tsx.tmpl +12 -0
  400. package/stacks/clay/pages/lists.tsx.tmpl +12 -0
  401. package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
  402. package/stacks/clay.json +16 -3
  403. package/stacks/gong/pages/calls.tsx.tmpl +23 -0
  404. package/stacks/gong/pages/companies.tsx.tmpl +22 -0
  405. package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
  406. package/stacks/gong/pages/lists.tsx.tmpl +12 -0
  407. package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
  408. package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
  409. package/stacks/gong.json +23 -6
  410. package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
  411. package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
  412. package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
  413. package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
  414. package/stacks/hootsuite.json +9 -4
  415. package/stacks/hubspot/pages/companies.tsx.tmpl +14 -35
  416. package/stacks/hubspot/pages/contacts.tsx.tmpl +29 -95
  417. package/stacks/hubspot/pages/deals.tsx.tmpl +10 -1
  418. package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
  419. package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
  420. package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
  421. package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
  422. package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
  423. package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
  424. package/stacks/hubspot.json +21 -5
  425. package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
  426. package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
  427. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
  428. package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
  429. package/stacks/intercom.json +8 -4
  430. package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
  431. package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
  432. package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
  433. package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
  434. package/stacks/mailchimp.json +20 -5
  435. package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
  436. package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
  437. package/stacks/outreach/pages/contacts.tsx.tmpl +27 -66
  438. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
  439. package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
  440. package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
  441. package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
  442. package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
  443. package/stacks/outreach.json +25 -7
  444. package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
  445. package/stacks/pipedrive/pages/contacts.tsx.tmpl +15 -35
  446. package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
  447. package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
  448. package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
  449. package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
  450. package/stacks/pipedrive.json +4 -2
  451. package/stacks/salesforce/pages/companies.tsx.tmpl +14 -35
  452. package/stacks/salesforce/pages/contacts.tsx.tmpl +20 -35
  453. package/stacks/salesforce/pages/deals.tsx.tmpl +10 -1
  454. package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
  455. package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
  456. package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
  457. package/stacks/salesforce.json +7 -3
  458. package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
  459. package/stacks/zoominfo/pages/contacts.tsx.tmpl +12 -0
  460. package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
  461. package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
  462. package/stacks/zoominfo.json +16 -3
  463. package/styles/styles.css +1 -1
  464. package/templates/next-app/app/globals.css.tmpl +349 -64
  465. package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
  466. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
@@ -19,7 +19,7 @@ import { DndContext, KeyboardSensor, PointerSensor, useDraggable, useDroppable,
19
19
  import { Button, Input, Label, Skeleton } from "@graph8/sdk/ui";
20
20
  import { ApiError } from "@/g8/components/api-error";
21
21
  import { EmptyState } from "@/g8/components/empty-state";
22
- import { usePipelineDeals } from "../hooks/use-pipeline-deals";
22
+ import { usePipelineDeals, type PipelineDealFilters } from "../hooks/use-pipeline-deals";
23
23
  import { usePipelines } from "../hooks/use-pipelines";
24
24
  import { closedStageMarker, formatDate, formatMoney, stageKind, sumAmounts, type Deal, type Stage } from "../lib/deals-types";
25
25
 
@@ -28,6 +28,8 @@ export interface PipelineBoardProps {
28
28
  pipelineId?: string;
29
29
  search?: string;
30
30
  ownerId?: string;
31
+ /** More list filters (stage, outcome, stale, close-date range ...); the same object the screen passes to usePipelineDeals. */
32
+ filters?: Omit<PipelineDealFilters, "search" | "ownerId">;
31
33
  onSelectDeal?: (deal: Deal) => void;
32
34
  /** Called after a stage move was saved, so a screen can refresh its totals. */
33
35
  onChanged?: () => void;
@@ -61,7 +63,7 @@ function Column({ stage, stages, deals, moving, onMove, onSelect }: { stage: Sta
61
63
  return (
62
64
  <section ref={setNodeRef} aria-label={stage.name} className={`flex w-72 shrink-0 flex-col gap-2 rounded-lg border bg-muted/40 p-2 ${isOver ? "ring-2 ring-primary" : ""}`}>
63
65
  <header className="flex items-center justify-between px-1">
64
- <h3 className="text-sm font-semibold">{stage.name}{kind !== "open" ? ` (${kind})` : ""}</h3>
66
+ <h3 className="text-sm font-medium">{stage.name}{kind !== "open" ? ` (${kind})` : ""}</h3>
65
67
  <span className="text-xs text-muted-foreground">{deals.length} · {formatMoney(sumAmounts(deals), deals[0]?.currency)}</span>
66
68
  </header>
67
69
  <div className="flex min-h-16 flex-col gap-2">
@@ -85,7 +87,7 @@ function CloseDialog({ stage, deal, onConfirm, onCancel }: { stage: Stage; deal:
85
87
  return (
86
88
  <div role="dialog" aria-modal="true" aria-label={`Close deal as ${stageKind(stage)}`} className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4">
87
89
  <form className="w-full max-w-sm space-y-3 rounded-lg bg-background p-5 shadow-xl" onSubmit={(e) => { e.preventDefault(); if (valid) onConfirm({ close_date: date, amount: amount === "" ? undefined : Number(amount) }); }}>
88
- <h2 className="text-base font-semibold">Move "{deal.name}" to {stage.name}</h2>
90
+ <h2 className="text-base font-medium">Move "{deal.name}" to {stage.name}</h2>
89
91
  <div className="space-y-1"><Label htmlFor="cd-date">Close date</Label><Input id="cd-date" type="date" value={date} onChange={(e) => setDate(e.target.value)} /></div>
90
92
  {won ? <div className="space-y-1"><Label htmlFor="cd-amount">Amount</Label><Input id="cd-amount" type="number" min={0} value={amount} onChange={(e) => setAmount(e.target.value)} /></div> : null}
91
93
  <div className="flex justify-end gap-2"><Button type="button" variant="outline" onClick={onCancel}>Cancel</Button><Button type="submit" disabled={!valid}>Move deal</Button></div>
@@ -94,10 +96,10 @@ function CloseDialog({ stage, deal, onConfirm, onCancel }: { stage: Stage; deal:
94
96
  );
95
97
  }
96
98
 
97
- export function PipelineBoard({ pipelineId, search, ownerId, onSelectDeal, onChanged, className }: PipelineBoardProps) {
99
+ export function PipelineBoard({ pipelineId, search, ownerId, filters, onSelectDeal, onChanged, className }: PipelineBoardProps) {
98
100
  const pipelines = usePipelines();
99
101
  const pipeline = pipelines.pipelines.find((p) => p.id === pipelineId) ?? pipelines.defaultPipeline;
100
- const { deals, loading, error, reload, moveDeal, movingIds } = usePipelineDeals(pipeline?.id, { search, ownerId });
102
+ const { deals, loading, error, reload, moveDeal, movingIds } = usePipelineDeals(pipeline?.id, { search, ownerId, ...filters });
101
103
  const [closing, setClosing] = useState<{ deal: Deal; stage: Stage } | null>(null);
102
104
  const [moveError, setMoveError] = useState<unknown>(null);
103
105
  const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } }), useSensor(KeyboardSensor));
@@ -127,7 +129,7 @@ export function PipelineBoard({ pipelineId, search, ownerId, onSelectDeal, onCha
127
129
  if (pipelines.loading || !pipeline) return pipelines.loading ? <Skeleton className="h-64 w-full" aria-label="Loading pipeline" /> : <EmptyState title="No pipeline yet" description="Create a pipeline with stages, then your deals appear here." />;
128
130
  return (
129
131
  <div className={`space-y-3 ${className ?? ""}`}>
130
- <h2 className="text-base font-semibold">{pipeline.name}</h2>
132
+ <h2 className="text-base font-medium">{pipeline.name}</h2>
131
133
  {error ? <ApiError error={error} title="Could not load deals" onRetry={() => void reload()} /> : null}
132
134
  {moveError ? <ApiError error={moveError} title="That move was not saved" /> : null}
133
135
  {loading && deals.length === 0 ? <Skeleton className="h-48 w-full" aria-label="Loading deals" /> : (
@@ -0,0 +1,132 @@
1
+ "use client";
2
+
3
+ /**
4
+ * PipelineEditor: create, rename, set the default and delete whole pipelines, and keep the close reasons.
5
+ * Stages of one pipeline are edited by StageEditor, which the deals screen shows right below.
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. GET /deals/pipelines (deals.listPipelines) read - via usePipelines
9
+ * 2. POST /pipelines { name, blank: true } (cliPipelines.createPipeline) write - a blank pipeline (add stages next)
10
+ * PUT /pipelines/{id} { name } (cliPipelines.updatePipeline) write - only the changed field is sent
11
+ * PUT /pipelines/{id} { set_default } (cliPipelines.updatePipeline) write
12
+ * 3. DELETE /pipelines/{id} (cliPipelines.deletePipeline) DESTRUCTIVE -> ConfirmSpendDialog first
13
+ * 4. GET /deals/close-reasons (deals.listDealCloseReasons) read
14
+ * POST /deals/close-reasons { kind, label } (deals.createDealCloseReason) write
15
+ * DELETE /deals/close-reasons/{id} (deals.archiveDealCloseReason) DESTRUCTIVE -> ConfirmSpendDialog (hidden unless the API says you may)
16
+ * Pipelines live under the campaigns:* scopes; close reasons under deals:*. The API refuses to delete a pipeline that
17
+ * still has deals; its message is shown. Not covered by this API: suggesting a pipeline with AI is a billable call this
18
+ * editor leaves out.
19
+ */
20
+ import { useState } from "react";
21
+ import { Plus } from "lucide-react";
22
+ import { Button, Input, Skeleton } from "@graph8/sdk/ui";
23
+ import { ApiError } from "@/g8/components/api-error";
24
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
25
+ import { g8Delete, g8Get, g8Post, g8Put } from "@/g8/lib/g8-client";
26
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
27
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
28
+ import { usePipelines } from "../hooks/use-pipelines";
29
+ import type { Pipeline } from "../lib/deals-types";
30
+ import { reasonsOf, type CloseReason } from "../lib/deal-extras";
31
+
32
+ export interface PipelineEditorProps {
33
+ /** Called after a pipeline was created, renamed, made default or deleted. */
34
+ onChanged?: (event: { kind: "created" | "renamed" | "default" | "deleted"; pipelineId?: string }) => void;
35
+ className?: string;
36
+ }
37
+
38
+ function PipelineRow({ pipeline, busy, onRename, onDefault, onDelete }: { pipeline: Pipeline; busy: boolean; onRename: (name: string) => void; onDefault: () => void; onDelete: () => void }) {
39
+ const [name, setName] = useState(pipeline.name);
40
+ const changed = name.trim() !== pipeline.name && name.trim() !== "";
41
+ return (
42
+ <li className="flex flex-wrap items-center gap-2 rounded-md border p-2">
43
+ <Input aria-label={`Name of pipeline ${pipeline.name}`} className="w-56" value={name} onChange={(e) => setName(e.target.value)} />
44
+ <Button size="sm" variant="outline" loading={busy} disabled={!changed || busy} aria-label={`Rename ${pipeline.name}`} onClick={() => onRename(name.trim())}>Rename</Button>
45
+ {pipeline.is_default ? <span className="text-xs text-muted-foreground">Default</span> : <Button size="sm" variant="ghost" disabled={busy} aria-label={`Make ${pipeline.name} the default`} onClick={onDefault}>Make default</Button>}
46
+ <Button size="sm" variant="ghost" disabled={busy} aria-label={`Delete pipeline ${pipeline.name}`} onClick={onDelete}>Delete</Button>
47
+ </li>
48
+ );
49
+ }
50
+
51
+ function CloseReasons() {
52
+ const [kind, setKind] = useState<"won" | "lost">("lost");
53
+ const [label, setLabel] = useState("");
54
+ const [archiving, setArchiving] = useState<CloseReason | null>(null);
55
+ const list = useG8Query(async (signal) => reasonsOf(await g8Get("/deals/close-reasons", { kind }, { signal })), [kind], { key: ["deals", "close-reasons", kind] });
56
+ const add = useG8Mutation(() => g8Post("/deals/close-reasons", { kind, label: label.trim() }), { invalidates: [["deals", "close-reasons"]], onSuccess: () => setLabel("") });
57
+ const archive = useG8Mutation((r: CloseReason) => g8Delete(`/deals/close-reasons/${encodeURIComponent(r.id)}`), { invalidates: [["deals", "close-reasons"]], onSuccess: () => setArchiving(null), onError: () => setArchiving(null) });
58
+ const reasons = (list.data ?? []).filter((r) => !r.kind || r.kind === kind);
59
+ return (
60
+ <section aria-label="Close reasons" className="space-y-2">
61
+ <h3 className="text-base font-semibold">Close reasons</h3>
62
+ <p className="text-sm text-muted-foreground">The reasons offered when a deal is closed. Archiving one keeps it on past deals.</p>
63
+ <div className="flex gap-2">
64
+ <select aria-label="Reason type" className="h-9 rounded-md border bg-background px-2 text-sm" value={kind} onChange={(e) => setKind(e.target.value as "won" | "lost")}>
65
+ <option value="won">Won</option>
66
+ <option value="lost">Lost</option>
67
+ </select>
68
+ <Input aria-label="New close reason" placeholder={`Add a ${kind} reason`} value={label} onChange={(e) => setLabel(e.target.value)} />
69
+ <Button loading={add.loading} disabled={!label.trim()} onClick={() => void add.mutate(undefined)}>Add reason</Button>
70
+ </div>
71
+ {add.error ? <ApiError error={add.error} title="Could not add the reason" /> : null}
72
+ {archive.error ? <ApiError error={archive.error} title="Could not archive the reason" /> : null}
73
+ {list.error ? <ApiError error={list.error} title="Could not load close reasons" onRetry={() => void list.refetch()} /> : null}
74
+ {list.loading && !list.data ? <Skeleton className="h-10 w-full" aria-label="Loading close reasons" /> : null}
75
+ {!list.loading && !list.error && reasons.length === 0 ? <p className="text-sm text-muted-foreground">No {kind} reasons yet.</p> : null}
76
+ <ul className="divide-y rounded-md border empty:hidden" aria-label={`${kind} reasons`}>
77
+ {reasons.map((r) => (
78
+ <li key={r.id} className="flex items-center justify-between gap-2 p-2 text-sm">
79
+ <span>{r.label}</span>
80
+ {r.canArchive ? <Button size="sm" variant="ghost" aria-label={`Archive reason ${r.label}`} onClick={() => setArchiving(r)}>Archive</Button> : null}
81
+ </li>
82
+ ))}
83
+ </ul>
84
+ <ConfirmSpendDialog open={archiving != null} tier="destructive" title={`Archive the reason "${archiving?.label}"?`} description="It is no longer offered when closing deals." confirmLabel="Archive reason" affected={1} confirming={archive.loading} onConfirm={() => archiving && void archive.mutate(archiving)} onCancel={() => setArchiving(null)} />
85
+ </section>
86
+ );
87
+ }
88
+
89
+ export function PipelineEditor({ onChanged, className }: PipelineEditorProps) {
90
+ const { pipelines, loading, error, refetch } = usePipelines();
91
+ const [newName, setNewName] = useState("");
92
+ const [removing, setRemoving] = useState<Pipeline | null>(null);
93
+ const [busyId, setBusyId] = useState<string | null>(null);
94
+ const inv = [["deals", "pipelines"]] as const;
95
+
96
+ const create = useG8Mutation(async () => g8Post("/pipelines", { name: newName.trim(), blank: true }), { invalidates: inv, onSuccess: () => { setNewName(""); onChanged?.({ kind: "created" }); } });
97
+ const rename = useG8Mutation(async (v: { id: string; name: string }) => {
98
+ setBusyId(v.id);
99
+ try { await g8Put(`/pipelines/${encodeURIComponent(v.id)}`, { name: v.name }); } finally { setBusyId(null); }
100
+ }, { invalidates: inv, onSuccess: (_r, v) => onChanged?.({ kind: "renamed", pipelineId: v.id }) });
101
+ const makeDefault = useG8Mutation(async (id: string) => {
102
+ setBusyId(id);
103
+ try { await g8Put(`/pipelines/${encodeURIComponent(id)}`, { set_default: true }); } finally { setBusyId(null); }
104
+ }, { invalidates: inv, onSuccess: (_r, id) => onChanged?.({ kind: "default", pipelineId: id }) });
105
+ const remove = useG8Mutation(async (p: Pipeline) => {
106
+ setBusyId(p.id);
107
+ try { await g8Delete(`/pipelines/${encodeURIComponent(p.id)}`); } finally { setBusyId(null); }
108
+ }, { invalidates: inv, onSuccess: (_r, p) => { setRemoving(null); onChanged?.({ kind: "deleted", pipelineId: p.id }); }, onError: () => setRemoving(null) });
109
+ const failure = create.error ?? rename.error ?? makeDefault.error ?? remove.error;
110
+
111
+ return (
112
+ <div className={`space-y-6 ${className ?? ""}`}>
113
+ <section aria-label="Pipelines" className="space-y-2">
114
+ <h3 className="text-base font-semibold">Pipelines</h3>
115
+ {error ? <ApiError error={error} title="Could not load pipelines" onRetry={() => void refetch()} /> : null}
116
+ {loading && pipelines.length === 0 ? <Skeleton className="h-16 w-full" aria-label="Loading pipelines" /> : null}
117
+ {failure ? <ApiError error={failure} title="That pipeline change failed" /> : null}
118
+ <ul className="space-y-2">
119
+ {pipelines.map((p) => (
120
+ <PipelineRow key={`${p.id}:${p.name}`} pipeline={p} busy={busyId === p.id} onRename={(name) => void rename.mutate({ id: p.id, name })} onDefault={() => void makeDefault.mutate(p.id)} onDelete={() => setRemoving(p)} />
121
+ ))}
122
+ </ul>
123
+ <form className="flex gap-2" onSubmit={(e) => { e.preventDefault(); if (newName.trim()) void create.mutate(undefined); }}>
124
+ <Input aria-label="New pipeline name" placeholder="New pipeline name" value={newName} onChange={(e) => setNewName(e.target.value)} />
125
+ <Button type="submit" loading={create.loading} disabled={!newName.trim()}><Plus className="size-4" aria-hidden />Create pipeline</Button>
126
+ </form>
127
+ </section>
128
+ <CloseReasons />
129
+ <ConfirmSpendDialog open={removing != null} tier="destructive" title={`Delete the "${removing?.name}" pipeline?`} description="Its stages are removed too. The API refuses while deals are still in it: move them to another pipeline first." confirmLabel="Delete pipeline" affected={1} confirming={remove.loading} onConfirm={() => removing && void remove.mutate(removing)} onCancel={() => setRemoving(null)} />
130
+ </div>
131
+ );
132
+ }
@@ -0,0 +1,62 @@
1
+ "use client";
2
+
3
+ /**
4
+ * RecordDealsTab: the deals linked to one contact or company, each opening in the deals screen. Use it as an
5
+ * `extraDetailTabs` entry of ContactsScreen / CompaniesScreen:
6
+ * { id: "deals", label: "Deals", render: (c) => <RecordDealsTab kind="contacts" id={c.id} onOpenDeal={(id) => router.push(`/deals?id=${id}`)} /> }
7
+ *
8
+ * SDK operations, in order:
9
+ * 1. getContactDeals GET /contacts/{id}/deals (read) when kind="contacts"
10
+ * getCompanyDeals GET /companies/{id}/deals (read) when kind="companies"
11
+ * Nothing here is billable, external or destructive.
12
+ */
13
+
14
+ import { Skeleton } from "@graph8/sdk/ui";
15
+ import { Handshake } from "lucide-react";
16
+ import { ApiError } from "@/g8/components/api-error";
17
+ import { EmptyState } from "@/g8/components/empty-state";
18
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
19
+ import { g8Get } from "@/g8/lib/g8-client";
20
+ import { dataOf, formatMoney } from "../lib/deals-types";
21
+
22
+ /** One row of GET /contacts/{id}/deals and GET /companies/{id}/deals (the API's ContactDealResponse). */
23
+ interface LinkedDeal { deal_id?: string | null; name?: string | null; stage?: string | null; value?: number | null; currency?: string | null; role?: string | null }
24
+
25
+ export interface RecordDealsTabProps {
26
+ kind: "contacts" | "companies";
27
+ id: number;
28
+ /** Route to the deal in your deals screen. Without it the rows link to `${dealsPath}?id=<deal id>`. */
29
+ onOpenDeal?: (dealId: string) => void;
30
+ /** Path of your deals screen for the plain links. Default "/deals". */
31
+ dealsPath?: string;
32
+ }
33
+
34
+ export function RecordDealsTab({ kind, id, onOpenDeal, dealsPath = "/deals" }: RecordDealsTabProps) {
35
+ const q = useG8Query(async (signal) => {
36
+ const inner = dataOf<{ items?: LinkedDeal[] } | LinkedDeal[]>(await g8Get(`/${kind}/${id}/deals`, undefined, { signal }));
37
+ return (Array.isArray(inner) ? inner : (inner.items ?? [])).filter((d) => d.deal_id);
38
+ }, [kind, id]);
39
+ if (q.error) return <ApiError error={q.error} title="Could not load deals" onRetry={() => void q.refetch()} />;
40
+ if (!q.data) return <Skeleton className="h-24 w-full" aria-label="Loading deals" />;
41
+ if (q.data.length === 0) return <EmptyState compact icon={<Handshake className="size-8" />} title="No deals yet" description="Deals linked to this record show up here." />;
42
+ return (
43
+ <ul className="divide-y rounded-md border" aria-label="Deals">
44
+ {q.data.map((d) => (
45
+ <li key={d.deal_id}>
46
+ <a
47
+ href={`${dealsPath}?id=${encodeURIComponent(d.deal_id as string)}`}
48
+ className="flex items-center justify-between gap-3 p-3 text-sm hover:bg-muted"
49
+ onClick={(e) => {
50
+ if (!onOpenDeal || e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
51
+ e.preventDefault();
52
+ onOpenDeal(d.deal_id as string);
53
+ }}
54
+ >
55
+ <span className="min-w-0 truncate font-medium">{d.name || "Untitled deal"}</span>
56
+ <span className="shrink-0 text-xs text-muted-foreground">{[d.stage, d.value != null ? formatMoney(d.value, d.currency) : null].filter(Boolean).join(" · ")}</span>
57
+ </a>
58
+ </li>
59
+ ))}
60
+ </ul>
61
+ );
62
+ }
@@ -69,7 +69,7 @@ export function StageEditor({ pipelineId, onChanged, className }: StageEditorPro
69
69
  if (pipeline.loading && !pipeline.data) return <Skeleton className="h-48 w-full" aria-label="Loading stages" />;
70
70
  return (
71
71
  <div className={`space-y-3 ${className ?? ""}`}>
72
- <h2 className="text-base font-semibold">{pipeline.data?.name} stages</h2>
72
+ <h2 className="text-base font-medium">{pipeline.data?.name} stages</h2>
73
73
  {failure ? <ApiError error={failure} title="That stage change failed" /> : null}
74
74
  <ol className="space-y-2">
75
75
  {stages.map((s, i) => (
@@ -4,7 +4,7 @@
4
4
  * usePipelineDeals: the deals in one pipeline, all pages up to `maxDeals`, with an optimistic stage move.
5
5
  *
6
6
  * SDK operations:
7
- * GET /deals?pipeline_id=&search=&owner_id=&page=&limit= (deals.listDeals) read
7
+ * GET /deals?pipeline_id=&search=&owner_id=&stage_id=&outcome=&stale_before=&has_activity=&close_after=&close_before=&has_contact=&page=&limit= (deals.listDeals) read
8
8
  * PATCH /deals/{deal_id} (deals.updateDeal) write - the stage move
9
9
  *
10
10
  * `moveDeal` updates the board immediately and, if the API refuses, refetches the list (the server is the truth)
@@ -22,6 +22,19 @@ import { dataOf } from "../lib/deals-types";
22
22
  export interface PipelineDealFilters {
23
23
  search?: string;
24
24
  ownerId?: string;
25
+ /** Only this stage (GET /deals stage_id). */
26
+ stageId?: string;
27
+ /** Exactly one of open, won or lost. */
28
+ outcome?: string;
29
+ /** Deals last touched before this timestamp OR never touched (GET /deals stale_before). */
30
+ staleBefore?: string;
31
+ hasActivity?: boolean;
32
+ /** Close date on or after this timestamp or YYYY-MM-DD (inclusive). */
33
+ closeAfter?: string;
34
+ /** Close date strictly before this timestamp or YYYY-MM-DD (exclusive). */
35
+ closeBefore?: string;
36
+ /** false returns the deals with no contact. */
37
+ hasContact?: boolean;
25
38
  }
26
39
 
27
40
  export interface MoveExtra {
@@ -36,14 +49,14 @@ export function usePipelineDeals(pipelineId: string | undefined, filters: Pipeli
36
49
  async (signal) => {
37
50
  const all: Deal[] = [];
38
51
  for (let page = 1; all.length < maxDeals; page++) {
39
- const env = await g8Get<Deal[]>("/deals", { pipeline_id: pipelineId, search: filters.search || undefined, owner_id: filters.ownerId || undefined, page, limit: 100 }, { signal });
52
+ const env = await g8Get<Deal[]>("/deals", { pipeline_id: pipelineId, search: filters.search || undefined, owner_id: filters.ownerId || undefined, stage_id: filters.stageId, outcome: filters.outcome, stale_before: filters.staleBefore, has_activity: filters.hasActivity, close_after: filters.closeAfter, close_before: filters.closeBefore, has_contact: filters.hasContact, page, limit: 100 }, { signal });
40
53
  all.push(...(env.data ?? []));
41
54
  if (!env.pagination?.has_next) break;
42
55
  }
43
56
  return all.slice(0, maxDeals);
44
57
  },
45
- [pipelineId, filters.search, filters.ownerId, maxDeals],
46
- { enabled: Boolean(pipelineId), key: ["deals", "list", "board", { pipelineId, search: filters.search || undefined, ownerId: filters.ownerId || undefined, maxDeals }] },
58
+ [pipelineId, filters.search, filters.ownerId, filters.stageId, filters.outcome, filters.staleBefore, filters.hasActivity, filters.closeAfter, filters.closeBefore, filters.hasContact, maxDeals],
59
+ { enabled: Boolean(pipelineId), key: ["deals", "list", "board", { pipelineId, search: filters.search || undefined, ownerId: filters.ownerId || undefined, stageId: filters.stageId, outcome: filters.outcome, staleBefore: filters.staleBefore, hasActivity: filters.hasActivity, closeAfter: filters.closeAfter, closeBefore: filters.closeBefore, hasContact: filters.hasContact, maxDeals }] },
47
60
  );
48
61
  useEffect(() => { if (query.data) setDeals(query.data); }, [query.data]);
49
62
 
@@ -0,0 +1,156 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Shapes and pure helpers for the deal depth components: notes, tasks, contact roles, line items, close reasons
5
+ * and the list filters. Types only describe the fields the components read; the API may return more.
6
+ *
7
+ * Money: a deal's `amount` is in DOLLARS (major units); a LINE ITEM's `unit_amount` and `line_total` are in CENTS.
8
+ * Dates: date-only values are `YYYY-MM-DD` and are shifted with UTC arithmetic, never through a local Date.
9
+ */
10
+ import { dataOf } from "./deals-types";
11
+
12
+ /** Buying-committee roles the API accepts on a deal contact (any other value is a 422). */
13
+ export const CONTACT_ROLES = ["champion", "decision_maker", "influencer", "blocker", "coach", "end_user"] as const;
14
+
15
+ export function roleLabel(role: string | null | undefined): string {
16
+ if (!role) return "No role";
17
+ const text = role.replace(/_/g, " ");
18
+ return text.charAt(0).toUpperCase() + text.slice(1);
19
+ }
20
+
21
+ export interface DealNote {
22
+ id: string;
23
+ content: string;
24
+ created_by_name?: string | null;
25
+ created_at?: string | null;
26
+ updated_at?: string | null;
27
+ read_only?: boolean;
28
+ }
29
+
30
+ export interface DealTask {
31
+ id: string;
32
+ title: string;
33
+ description?: string | null;
34
+ due_date?: string | null;
35
+ status?: string | null;
36
+ priority?: number | null;
37
+ assignee_name?: string | null;
38
+ }
39
+
40
+ export interface DealLineItem {
41
+ id?: string;
42
+ product_name: string;
43
+ description?: string | null;
44
+ quantity: number;
45
+ /** Cents. */
46
+ unit_amount: number;
47
+ discount_pct: number;
48
+ billing_frequency?: string | null;
49
+ sort_order?: number;
50
+ stripe_product_id?: string | null;
51
+ stripe_price_id?: string | null;
52
+ manual_product_id?: string | null;
53
+ }
54
+
55
+ export interface LineItemsPayload {
56
+ deal_id: string;
57
+ /** Dollars. */
58
+ amount?: number | null;
59
+ currency?: string | null;
60
+ line_items?: DealLineItem[];
61
+ amount_mismatch?: boolean;
62
+ /** Dollars. */
63
+ linked_quote_total?: number | null;
64
+ }
65
+
66
+ export interface CloseReason {
67
+ id: string;
68
+ label: string;
69
+ kind: string;
70
+ /** Whether this person may archive it (created it themselves, or may manage all). */
71
+ canArchive: boolean;
72
+ }
73
+
74
+ /** Rows of a list response that is either a bare array or `{ items | tasks | notes | reasons | contacts: [] }`. */
75
+ export function rowsOf<T>(env: unknown): T[] {
76
+ const inner = dataOf<unknown>(env);
77
+ if (Array.isArray(inner)) return inner as T[];
78
+ const o = inner as Record<string, unknown> | null;
79
+ for (const key of ["items", "tasks", "notes", "reasons", "contacts"]) if (o && Array.isArray(o[key])) return o[key] as T[];
80
+ return [];
81
+ }
82
+
83
+ /** The close reasons of `GET /deals/close-reasons`: `{ can_manage, reasons: [{ id, label, kind, archived? }] }`. */
84
+ export function reasonsOf(env: unknown): CloseReason[] {
85
+ const inner = dataOf<{ can_manage?: boolean | null; reasons?: Array<Record<string, unknown>> } | null>(env);
86
+ const manage = Boolean(inner?.can_manage);
87
+ return (inner?.reasons ?? [])
88
+ .filter((r) => !r.archived && !r.archived_at)
89
+ .map((r) => ({ id: String(r.id ?? ""), label: String(r.label ?? r.name ?? ""), kind: String(r.kind ?? ""), canArchive: manage || r.can_archive === true || r.can_delete === true }))
90
+ .filter((r) => r.id && r.label);
91
+ }
92
+
93
+ /** Whole cents from a dollars text input; null when it is not a non-negative number. */
94
+ export function dollarsToCents(text: string): number | null {
95
+ const n = Number(text.trim());
96
+ return text.trim() !== "" && Number.isFinite(n) && n >= 0 ? Math.round(n * 100) : null;
97
+ }
98
+
99
+ export const centsToDollarsText = (cents: number): string => (cents / 100).toFixed(2);
100
+
101
+ export function lineTotalCents(l: Pick<DealLineItem, "unit_amount" | "quantity" | "discount_pct">): number {
102
+ return Math.round(l.unit_amount * l.quantity * (1 - Math.min(100, Math.max(0, l.discount_pct)) / 100));
103
+ }
104
+
105
+ export function formatCents(cents: number | null | undefined, currency?: string | null): string {
106
+ if (cents == null) return "";
107
+ try {
108
+ return new Intl.NumberFormat(undefined, { style: "currency", currency: currency || "USD" }).format(cents / 100);
109
+ } catch {
110
+ return String(cents / 100);
111
+ }
112
+ }
113
+
114
+ /** `date` (YYYY-MM-DD) shifted by whole days, in UTC so the result is the same in every time zone. */
115
+ export function addDaysUtc(date: string, days: number): string {
116
+ const d = new Date(`${date}T00:00:00Z`);
117
+ d.setUTCDate(d.getUTCDate() + days);
118
+ return d.toISOString().slice(0, 10);
119
+ }
120
+
121
+ /** Midnight UTC `days` before today, as an ISO timestamp. Stable all day, so it can sit in a query key. */
122
+ export function staleCutoff(days: number, now = new Date()): string {
123
+ return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() - days)).toISOString();
124
+ }
125
+
126
+ export interface DealFilterState {
127
+ ownerId: string;
128
+ stageId: string;
129
+ outcome: "" | "open" | "won" | "lost";
130
+ /** Only deals untouched for this many days (or never touched). "" = off. */
131
+ staleDays: "" | "7" | "14" | "30" | "60" | "90";
132
+ activity: "" | "yes" | "no";
133
+ /** Inclusive date range of the close date. */
134
+ closeFrom: string;
135
+ closeTo: string;
136
+ /** Only deals with no contact at all. */
137
+ orphaned: boolean;
138
+ }
139
+
140
+ export const NO_FILTERS: DealFilterState = { ownerId: "", stageId: "", outcome: "", staleDays: "", activity: "", closeFrom: "", closeTo: "", orphaned: false };
141
+
142
+ export const filtersActive = (f: DealFilterState): boolean => (Object.keys(NO_FILTERS) as Array<keyof DealFilterState>).some((k) => f[k] !== NO_FILTERS[k]);
143
+
144
+ /** The `GET /deals` query for a filter state. `close_before` is exclusive, so the inclusive "to" date becomes the next day. */
145
+ export function filterQuery(f: DealFilterState, now = new Date()) {
146
+ return {
147
+ ownerId: f.ownerId || undefined,
148
+ stageId: f.stageId || undefined,
149
+ outcome: f.outcome || undefined,
150
+ staleBefore: f.staleDays ? staleCutoff(Number(f.staleDays), now) : undefined,
151
+ hasActivity: f.activity === "" ? undefined : f.activity === "yes",
152
+ closeAfter: f.closeFrom || undefined,
153
+ closeBefore: f.closeTo ? addDaysUtc(f.closeTo, 1) : undefined,
154
+ hasContact: f.orphaned ? false : undefined,
155
+ };
156
+ }
@@ -13,8 +13,10 @@ browser, record outcomes, and review transcripts. Copy `DialerWorkspace` for the
13
13
 
14
14
  - `DialerScreen` (`components/dialer-screen.tsx`): **Session** tab picks a list (ListPicker), creates a session (dials
15
15
  nobody), then runs the queue: every batch, voicemail drop, softphone credential and stop is confirmed; finished calls
16
- open the outcome panel and transcript. **Sessions** tab lists every session; a row opens `DialerSessionDetail` (stats, details, calls, queue; pause, confirmed resume, confirmed stop; `?tab=sessions&session=<id>`). **History** tab lists the rep's calls. Props are optional: `userId` (needed to
17
- enable the browser softphone), `userEmail`, `listId`, `sessionId`, `transport` (default: the Twilio adapter, loaded
16
+ open the outcome panel and transcript. **Sessions** tab lists every session; a row opens `DialerSessionDetail` (stats, details, calls, queue; pause, confirmed resume, confirmed stop; `?tab=sessions&session=<id>`). **History** tab lists the rep's calls. Props are optional: `userId` (overrides the rep; without it the
17
+ rep is the signed-in team member from `GET /team-members/me`, so the softphone works out of the box with a personal key,
18
+ and a `MemberPicker` switches rep where the API allows it; a personal key naming another rep gets a 403, which the form
19
+ explains; an organisation key is not a person, so the rep is chosen with the picker), `userEmail`, `listId`, `sessionId`, `transport` (default: the Twilio adapter, loaded
18
20
  on demand, needs `@twilio/voice-sdk`). Resume a session with `?session=<id>`. Add `&view=full` to any detail (session, call, and the same in the other screens) to open it as a full page.
19
21
  - `CallLogScreen` (`components/call-log-screen.tsx`): history of one rep, contact or session with outcome filter and search;
20
22
  open a call for recording, transcript and AI grade (`?id=<room_name>`), and **Call back** the number (`CallContactButton`). Needs a rep email (it asks) or a prop.
@@ -90,8 +92,10 @@ Softphone), `call_id` = `room_name` (live call -> disposition -> transcript -> r
90
92
 
91
93
  ## Stack nav pages
92
94
 
93
- `CallQueue` and `CallLog` render with no props (a stack uses them as pages): the start form asks for the rep's user id
94
- and the call log for a rep email. Pass `userId` / `userEmail` from your session to skip those fields.
95
+ `CallQueue` and `CallLog` render with no props (a stack uses them as pages): the start form defaults the rep to the
96
+ signed-in team member (a `MemberPicker` switches it; the raw user id is only a fallback under "Use a user id instead"),
97
+ the list is a `ListPicker`, and the call log shows the signed-in rep's calls (a rep email can still be typed). Pass
98
+ `userId` / `userEmail` from your session to override.
95
99
 
96
100
  ## Gotchas
97
101
 
@@ -135,7 +139,7 @@ and the call log for a rep email. Pass `userId` / `userEmail` from your session
135
139
  | `voice.getCallTranscript` | GET /voice/dialer/calls/{room}/transcript | read | TranscriptViewer |
136
140
  | `voice.getCallGrading` | GET /voice/dialer/calls/{room}/grading | read | TranscriptViewer |
137
141
  | `voice.downloadCallRecording` | GET /voice/calls/{id}/recording | read | TranscriptViewer |
138
- | `teamMembers.getMyTeamMember` | GET /team-members/me | read | CallContactButton (only without `userId`) |
142
+ | `teamMembers.getMyTeamMember` | GET /team-members/me | read | CallContactButton, DialerScreen, SessionStartForm, CallLogScreen (only without `userId` / `userEmail`; via the pickers block `useCurrentMember`) |
139
143
  | `voice.listDialerSessions` | GET /voice/dialer/sessions | read | CallContactButton |
140
144
  | `dialer.manualDial` | POST /dialer/parallel-calls/manual-dial | external | CallContactButton |
141
145
 
@@ -144,3 +148,8 @@ and the call log for a rep email. Pass `userId` / `userEmail` from your session
144
148
  - `01-create-session-and-dial.ts`: draft, one confirmed batch, poll queue-stats.
145
149
  - `02-disposition-calls.ts`: record outcomes from the org's accepted list.
146
150
  - `03-review-a-call.ts`: transcript, AI grade, rep performance (`dialerAnalytics`).
151
+
152
+ ## Cross-block glue
153
+
154
+
155
+ - `RecordCallsTab` (`components/record-calls-tab.tsx`): a contact's call history (`listCallsForContact`, 25 then "Show more" up to the API maximum of 100, then a notice) with a confirmed `CallContactButton`. `extraDetailTabs` entry for ContactsScreen.
@@ -11,7 +11,8 @@
11
11
  "call log, transcript, AI grading and recording per call"
12
12
  ],
13
13
  "dependsOn": [
14
- "lists"
14
+ "lists",
15
+ "pickers"
15
16
  ],
16
17
  "operations": [
17
18
  "create_session_draft_dialer_sessions_draft_post",
@@ -26,6 +27,7 @@
26
27
  "list_calls_for_contact_voice_dialer_calls_get",
27
28
  "list_dialer_numbers_voice_dialer_numbers_get",
28
29
  "list_dialer_sessions_voice_dialer_sessions_get",
30
+ "list_team_members_team_members_get",
29
31
  "list_voice_dispositions_voice_dispositions_get",
30
32
  "manual_dial_dialer_parallel_calls_manual_dial_post",
31
33
  "mint_softphone_token_dialer_sessions_token_get",
@@ -64,7 +66,9 @@
64
66
  "purpose": "Create a draft session (dials nobody) for a list, caller ID and mode.",
65
67
  "operations": [
66
68
  "create_session_draft_dialer_sessions_draft_post",
67
- "list_dialer_numbers_voice_dialer_numbers_get"
69
+ "get_my_team_member_team_members_me_get",
70
+ "list_dialer_numbers_voice_dialer_numbers_get",
71
+ "list_team_members_team_members_get"
68
72
  ],
69
73
  "tiers": [
70
74
  "read",
@@ -194,6 +198,17 @@
194
198
  "read",
195
199
  "external"
196
200
  ]
201
+ },
202
+ {
203
+ "name": "RecordCallsTab",
204
+ "file": "components/record-calls-tab.tsx",
205
+ "purpose": "One contact's call history with a confirmed Call button; an extraDetailTabs entry for ContactsScreen.",
206
+ "operations": [
207
+ "list_calls_for_contact_voice_dialer_calls_get"
208
+ ],
209
+ "tiers": [
210
+ "read"
211
+ ]
197
212
  }
198
213
  ],
199
214
  "hooks": [
@@ -24,14 +24,16 @@
24
24
  * The call is only offered once the softphone is ready, so the rep is never late to their own call (the response
25
25
  * is the first moment the conference name is known; a softphone that registers after it would join late).
26
26
  */
27
- import { useEffect, useState } from "react";
27
+ import { useEffect, useRef, useState } from "react";
28
28
  import { Phone } from "lucide-react";
29
29
  import { Button } from "@graph8/sdk/ui";
30
30
  import { ApiError } from "@/g8/components/api-error";
31
+ import { FirstNotice } from "@/g8/components/paging-notices";
31
32
  import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
32
33
  import { g8Get, g8Post } from "@/g8/lib/g8-client";
33
34
  import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
34
35
  import { useG8Query } from "@/g8/hooks/use-g8-query";
36
+ import { useCurrentMember } from "@/g8/pickers/hooks/use-current-member";
35
37
  import { useDialerNumbers } from "../hooks/use-dialer-numbers";
36
38
  import { dataOf } from "../lib/dialer-types";
37
39
  import type { SoftphoneTransport } from "../lib/softphone-transport";
@@ -100,6 +102,10 @@ function displayName(c: CallableContact): string {
100
102
  return c.full_name || c.name || [c.first_name, c.last_name].filter(Boolean).join(" ") || c.email || "this contact";
101
103
  }
102
104
 
105
+ /** Dialer sessions per read and the most pages read per status; beyond that the picker says so. */
106
+ const SESSION_PAGE = 25;
107
+ const SESSION_PAGES = 8;
108
+
103
109
  export function CallContactButton(props: CallContactButtonProps) {
104
110
  const { contact, label = "Call", className } = props;
105
111
  const [open, setOpen] = useState(false);
@@ -116,15 +122,12 @@ export function CallContactButton(props: CallContactButtonProps) {
116
122
 
117
123
  function CallContactPanel(props: CallContactButtonProps & { onClose: () => void }) {
118
124
  const { contact, sessionId, fromPhone, transport, onCalled, onClose, className } = props;
119
- const me = useG8Query(
120
- async (signal) => dataOf<{ propelauth_user_id?: string | null; email?: string | null }>(await g8Get("/team-members/me", undefined, { signal })),
121
- [],
122
- { enabled: !props.userId },
123
- );
124
- const userId = props.userId ?? me.data?.propelauth_user_id ?? undefined;
125
+ // The shared resolver (foundation): the same one DialerScreen uses, so the two cannot drift apart.
126
+ const me = useCurrentMember({ enabled: !props.userId });
127
+ const userId = props.userId ?? me.userId;
125
128
  // user_id and user_email must describe the same person: a userEmail without a userId (e.g. a history filtered to
126
129
  // another rep) is ignored, and both come from /team-members/me.
127
- const userEmail = props.userId ? props.userEmail : (me.data?.email ?? undefined);
130
+ const userEmail = props.userId ? props.userEmail : me.email;
128
131
  const phones = contactPhones(contact);
129
132
  const [phone, setPhone] = useState(phones[0]?.phone ?? "");
130
133
  const [session, setSession] = useState(sessionId ?? "");
@@ -134,11 +137,24 @@ function CallContactPanel(props: CallContactButtonProps & { onClose: () => void
134
137
  const [conference, setConference] = useState<string | null>(null);
135
138
  const audio = useLazyTransport(transport, true);
136
139
 
140
+ // Every page of each status is read (25 per page) up to SESSION_PAGES pages; `capped` says the cap cut a status short.
141
+ const capped = useRef(false);
137
142
  const sessions = useG8Query(
138
143
  async (signal) => {
144
+ let cut = false;
139
145
  const lists = await Promise.all(
140
- CALLABLE.map(async (status) => dataOf<{ sessions?: SessionOption[] }>(await g8Get("/voice/dialer/sessions", { status, page: 1, page_size: 25, user_email: userEmail }, { signal })).sessions ?? []),
146
+ CALLABLE.map(async (status) => {
147
+ const rows: SessionOption[] = [];
148
+ for (let page = 1; page <= SESSION_PAGES; page++) {
149
+ const got = dataOf<{ sessions?: SessionOption[] }>(await g8Get("/voice/dialer/sessions", { status, page, page_size: SESSION_PAGE, user_email: userEmail }, { signal })).sessions ?? [];
150
+ rows.push(...got);
151
+ if (got.length < SESSION_PAGE) return rows;
152
+ }
153
+ cut = true;
154
+ return rows;
155
+ }),
141
156
  );
157
+ capped.current = cut;
142
158
  return lists.flat();
143
159
  },
144
160
  [userEmail],
@@ -207,6 +223,7 @@ function CallContactPanel(props: CallContactButtonProps & { onClose: () => void
207
223
  {(sessions.data ?? []).map((s) => <option key={s.session_id} value={s.session_id}>{s.name || s.session_id} ({(s.status ?? "").toLowerCase()})</option>)}
208
224
  </select>
209
225
  ) : null}
226
+ {!sessionId ? <FirstNotice force={capped.current} shown={(sessions.data ?? []).length} limit={0} noun="sessions" /> : null}
210
227
  {!sessionId && sessions.data?.length === 0 ? (
211
228
  <p className="text-muted-foreground">No paused or completed session yet. Start one on the Dialer screen and pause it; the call is logged under it.</p>
212
229
  ) : null}