@graph8/sdk 0.362.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 (615) hide show
  1. package/README.md +33 -10
  2. package/blocks/_foundation/README.md +78 -1
  3. package/blocks/_foundation/block.json +123 -6
  4. package/blocks/_foundation/components/api-error.tsx +9 -7
  5. package/blocks/_foundation/components/brand-provider.tsx +23 -0
  6. package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
  7. package/blocks/_foundation/components/confirmed-action.tsx +90 -0
  8. package/blocks/_foundation/components/connect-graph8.tsx +280 -0
  9. package/blocks/_foundation/components/empty-state.tsx +5 -2
  10. package/blocks/_foundation/components/list-select.tsx +112 -0
  11. package/blocks/_foundation/components/paging-notices.tsx +55 -0
  12. package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
  13. package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
  14. package/blocks/_foundation/components/screen/index.ts +2 -0
  15. package/blocks/_foundation/components/screen/record-link.tsx +76 -0
  16. package/blocks/_foundation/components/screen/split-view.tsx +1 -1
  17. package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
  18. package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
  19. package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
  20. package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
  21. package/blocks/_foundation/hooks/use-brand.ts +52 -0
  22. package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
  23. package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
  24. package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
  25. package/blocks/_foundation/hooks/use-paginated.ts +120 -0
  26. package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
  27. package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
  28. package/blocks/_foundation/lib/g8-cache.ts +454 -0
  29. package/blocks/_foundation/lib/g8-client.ts +1 -1
  30. package/blocks/_foundation/lib/paging.ts +57 -0
  31. package/blocks/_foundation/lib/plural.ts +23 -0
  32. package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
  33. package/blocks/_foundation/server/connect-route.ts +15 -0
  34. package/blocks/_foundation/server/connect.ts +127 -0
  35. package/blocks/_foundation/server/local-key.ts +70 -0
  36. package/blocks/_foundation/server/proxy.ts +103 -3
  37. package/blocks/_foundation/server/route.ts +16 -4
  38. package/blocks/agents/README.md +93 -2
  39. package/blocks/agents/block.json +343 -2
  40. package/blocks/agents/components/agent-activity.tsx +112 -0
  41. package/blocks/agents/components/agent-approvals.tsx +135 -0
  42. package/blocks/agents/components/agent-builder.tsx +29 -10
  43. package/blocks/agents/components/agent-conversations.tsx +143 -0
  44. package/blocks/agents/components/agent-knowledge-panel.tsx +4 -3
  45. package/blocks/agents/components/agent-operators.tsx +164 -0
  46. package/blocks/agents/components/agent-run-detail.tsx +119 -0
  47. package/blocks/agents/components/agent-runs.tsx +19 -10
  48. package/blocks/agents/components/agent-skills-panel.tsx +7 -7
  49. package/blocks/agents/components/agent-test-history.tsx +121 -0
  50. package/blocks/agents/components/agents-screen.tsx +226 -0
  51. package/blocks/agents/components/assistant-sessions.tsx +150 -0
  52. package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
  53. package/blocks/agents/components/from-template-button.tsx +65 -0
  54. package/blocks/agents/components/knowledge-browser.tsx +107 -0
  55. package/blocks/agents/components/knowledge-detail.tsx +156 -0
  56. package/blocks/agents/components/knowledge-form.tsx +69 -0
  57. package/blocks/agents/components/name-dialog.tsx +32 -0
  58. package/blocks/agents/components/receptionist-button.tsx +46 -0
  59. package/blocks/agents/components/save-as-template-button.tsx +32 -0
  60. package/blocks/agents/components/skill-editor.tsx +147 -0
  61. package/blocks/agents/components/skill-library.tsx +111 -0
  62. package/blocks/agents/lib/agent-types.ts +7 -1
  63. package/blocks/agents/lib/facts.tsx +13 -0
  64. package/blocks/agents/lib/knowledge-utils.ts +49 -0
  65. package/blocks/agents/lib/load-more.tsx +141 -0
  66. package/blocks/agents/lib/skill-utils.ts +91 -0
  67. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
  68. package/blocks/analytics/README.md +13 -0
  69. package/blocks/analytics/block.json +45 -1
  70. package/blocks/analytics/components/analytics-screen.tsx +222 -0
  71. package/blocks/analytics/components/kpi-tiles.tsx +1 -1
  72. package/blocks/analytics/components/leaderboard.tsx +14 -7
  73. package/blocks/analytics/components/report-builder.tsx +38 -11
  74. package/blocks/analytics/components/report-detail.tsx +84 -0
  75. package/blocks/analytics/components/trend-chart.tsx +62 -0
  76. package/blocks/appointments/README.md +6 -0
  77. package/blocks/appointments/block.json +33 -2
  78. package/blocks/appointments/components/availability-editor.tsx +17 -5
  79. package/blocks/appointments/components/booking-page.tsx +2 -2
  80. package/blocks/appointments/components/event-type-editor.tsx +2 -1
  81. package/blocks/appointments/components/event-type-related.tsx +4 -2
  82. package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
  83. package/blocks/appointments/components/meeting-detail.tsx +2 -2
  84. package/blocks/appointments/components/meeting-list.tsx +38 -18
  85. package/blocks/appointments/components/meetings-screen.tsx +7 -4
  86. package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
  87. package/blocks/appointments/components/scheduling-screen.tsx +2 -1
  88. package/blocks/block.schema.json +18 -0
  89. package/blocks/block.types.ts +17 -0
  90. package/blocks/campaigns/README.md +41 -11
  91. package/blocks/campaigns/block.json +105 -42
  92. package/blocks/campaigns/components/campaign-assets.tsx +17 -6
  93. package/blocks/campaigns/components/campaign-launch-step.tsx +114 -26
  94. package/blocks/campaigns/components/campaign-list.tsx +7 -3
  95. package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
  96. package/blocks/campaigns/components/campaign-wizard.tsx +52 -14
  97. package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
  98. package/blocks/campaigns/components/icp-detail.tsx +163 -0
  99. package/blocks/campaigns/components/icp-editor.tsx +22 -17
  100. package/blocks/campaigns/components/persona-detail.tsx +181 -0
  101. package/blocks/campaigns/components/persona-editor.tsx +19 -11
  102. package/blocks/campaigns/lib/campaigns-utils.ts +138 -5
  103. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
  104. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
  105. package/blocks/coach/README.md +10 -0
  106. package/blocks/coach/block.json +16 -1
  107. package/blocks/coach/components/call-scorecard.tsx +7 -7
  108. package/blocks/coach/components/coach-screen.tsx +222 -0
  109. package/blocks/company-intelligence/README.md +132 -0
  110. package/blocks/company-intelligence/block.json +318 -0
  111. package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
  112. package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
  113. package/blocks/company-intelligence/components/company-profile.tsx +68 -0
  114. package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
  115. package/blocks/company-intelligence/components/report-detail.tsx +189 -0
  116. package/blocks/company-intelligence/components/research-reports.tsx +136 -0
  117. package/blocks/company-intelligence/components/research-run.tsx +161 -0
  118. package/blocks/company-intelligence/components/source-detail.tsx +117 -0
  119. package/blocks/company-intelligence/components/source-view.tsx +274 -0
  120. package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
  121. package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
  122. package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
  123. package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
  124. package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
  125. package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
  126. package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
  127. package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
  128. package/blocks/copilot/README.md +13 -0
  129. package/blocks/copilot/block.json +14 -1
  130. package/blocks/copilot/components/copilot-memory.tsx +6 -2
  131. package/blocks/copilot/components/copilot-panel.tsx +14 -5
  132. package/blocks/copilot/components/copilot-screen.tsx +148 -0
  133. package/blocks/copilot/components/copilot-team-chat.tsx +13 -6
  134. package/blocks/crm/README.md +34 -0
  135. package/blocks/crm/block.json +107 -2
  136. package/blocks/crm/components/companies-screen.tsx +359 -0
  137. package/blocks/crm/components/company-detail.tsx +10 -8
  138. package/blocks/crm/components/company-table.tsx +11 -4
  139. package/blocks/crm/components/contact-detail.tsx +10 -7
  140. package/blocks/crm/components/contact-lookup-link.tsx +44 -0
  141. package/blocks/crm/components/contact-table.tsx +10 -3
  142. package/blocks/crm/components/contacts-screen.tsx +377 -0
  143. package/blocks/crm/components/dedupe-review.tsx +13 -6
  144. package/blocks/crm/components/import-wizard.tsx +7 -9
  145. package/blocks/crm/components/list-select.tsx +20 -0
  146. package/blocks/crm/components/notes-panel.tsx +4 -4
  147. package/blocks/crm/components/record-engagement-tab.tsx +103 -0
  148. package/blocks/crm/components/record-form.tsx +6 -4
  149. package/blocks/crm/components/task-list.tsx +4 -4
  150. package/blocks/crm/components/tasks-screen.tsx +399 -0
  151. package/blocks/crm/hooks/use-all-lists.ts +18 -0
  152. package/blocks/crm/hooks/use-custom-fields.ts +6 -1
  153. package/blocks/crm/hooks/use-page-query.ts +9 -1
  154. package/blocks/crm/lib/crm-types.ts +26 -2
  155. package/blocks/crm/lib/read-lists.ts +16 -0
  156. package/blocks/crm/lib/screen-parts.tsx +105 -0
  157. package/blocks/deals/README.md +24 -6
  158. package/blocks/deals/block.json +160 -5
  159. package/blocks/deals/components/close-deal-dialog.tsx +169 -0
  160. package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
  161. package/blocks/deals/components/deal-detail.tsx +25 -13
  162. package/blocks/deals/components/deal-filters.tsx +65 -0
  163. package/blocks/deals/components/deal-line-items.tsx +120 -0
  164. package/blocks/deals/components/deal-notes-tab.tsx +97 -0
  165. package/blocks/deals/components/deal-related.tsx +31 -10
  166. package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
  167. package/blocks/deals/components/deals-screen.tsx +66 -26
  168. package/blocks/deals/components/forecast-view.tsx +4 -4
  169. package/blocks/deals/components/pipeline-board.tsx +8 -6
  170. package/blocks/deals/components/pipeline-editor.tsx +132 -0
  171. package/blocks/deals/components/record-deals-tab.tsx +62 -0
  172. package/blocks/deals/components/stage-editor.tsx +5 -5
  173. package/blocks/deals/hooks/use-pipeline-deals.ts +21 -4
  174. package/blocks/deals/hooks/use-pipelines.ts +1 -1
  175. package/blocks/deals/lib/deal-extras.ts +156 -0
  176. package/blocks/dialer/README.md +45 -4
  177. package/blocks/dialer/block.json +84 -3
  178. package/blocks/dialer/components/call-contact-button.tsx +269 -0
  179. package/blocks/dialer/components/call-log-screen.tsx +208 -0
  180. package/blocks/dialer/components/call-log.tsx +23 -7
  181. package/blocks/dialer/components/call-queue.tsx +4 -4
  182. package/blocks/dialer/components/dialer-screen.tsx +361 -0
  183. package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
  184. package/blocks/dialer/components/disposition-panel.tsx +1 -1
  185. package/blocks/dialer/components/live-calls-table.tsx +7 -3
  186. package/blocks/dialer/components/record-calls-tab.tsx +101 -0
  187. package/blocks/dialer/components/session-start-form.tsx +61 -12
  188. package/blocks/dialer/components/softphone.tsx +4 -2
  189. package/blocks/enrichment/README.md +29 -7
  190. package/blocks/enrichment/block.json +49 -9
  191. package/blocks/enrichment/components/bulk-enrich-panel.tsx +83 -29
  192. package/blocks/enrichment/components/email-verify-badge.tsx +31 -11
  193. package/blocks/enrichment/components/enrich-button.tsx +32 -9
  194. package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
  195. package/blocks/enrichment/components/enrichment-screen.tsx +341 -0
  196. package/blocks/enrichment/components/waterfall-config.tsx +66 -17
  197. package/blocks/enrichment/lib/enrichment-types.ts +39 -0
  198. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
  199. package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
  200. package/blocks/forms/README.md +8 -0
  201. package/blocks/forms/block.json +139 -18
  202. package/blocks/forms/components/form-activity.tsx +71 -0
  203. package/blocks/forms/components/form-backfill.tsx +108 -0
  204. package/blocks/forms/components/form-builder.tsx +4 -2
  205. package/blocks/forms/components/form-embed.tsx +4 -2
  206. package/blocks/forms/components/form-groups.tsx +151 -0
  207. package/blocks/forms/components/form-mapping-editor.tsx +6 -9
  208. package/blocks/forms/components/form-route-history.tsx +72 -0
  209. package/blocks/forms/components/form-saved-views.tsx +81 -0
  210. package/blocks/forms/components/forms-screen.tsx +56 -19
  211. package/blocks/forms/components/more-rows.tsx +30 -0
  212. package/blocks/forms/components/submissions-export.tsx +67 -0
  213. package/blocks/forms/components/submissions-table.tsx +17 -6
  214. package/blocks/forms/lib/forms-extra.ts +67 -0
  215. package/blocks/inbox/README.md +32 -3
  216. package/blocks/inbox/block.json +29 -2
  217. package/blocks/inbox/components/inbox-list.tsx +7 -3
  218. package/blocks/inbox/components/inbox-screen.tsx +294 -27
  219. package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
  220. package/blocks/inbox/components/reply-composer.tsx +65 -9
  221. package/blocks/inbox/components/tag-and-assign.tsx +4 -2
  222. package/blocks/inbox/components/thread-badge.tsx +2 -2
  223. package/blocks/inbox/components/thread-view.tsx +1 -1
  224. package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
  225. package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
  226. package/blocks/inbox/lib/inbox-types.ts +47 -1
  227. package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
  228. package/blocks/integrations/README.md +63 -1
  229. package/blocks/integrations/block.json +234 -15
  230. package/blocks/integrations/components/ad-accounts.tsx +82 -0
  231. package/blocks/integrations/components/connection-health.tsx +59 -0
  232. package/blocks/integrations/components/crm-detail.tsx +119 -0
  233. package/blocks/integrations/components/crm-records.tsx +103 -0
  234. package/blocks/integrations/components/field-mappings.tsx +61 -0
  235. package/blocks/integrations/components/integration-list.tsx +13 -8
  236. package/blocks/integrations/components/integrations-screen.tsx +152 -0
  237. package/blocks/integrations/components/migration-panel.tsx +133 -0
  238. package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
  239. package/blocks/integrations/components/sync-status.tsx +11 -9
  240. package/blocks/integrations/components/sync-triggers.tsx +134 -0
  241. package/blocks/integrations/components/webhook-detail.tsx +179 -0
  242. package/blocks/integrations/components/webhook-manager.tsx +43 -25
  243. package/blocks/integrations/components/workspace-tools.tsx +127 -0
  244. package/blocks/integrations/lib/crm-utils.ts +39 -0
  245. package/blocks/integrations/lib/facts.tsx +13 -0
  246. package/blocks/integrations/lib/integrations-utils.ts +11 -1
  247. package/blocks/integrations/lib/load-more.tsx +141 -0
  248. package/blocks/intent/README.md +14 -0
  249. package/blocks/intent/block.json +27 -1
  250. package/blocks/intent/components/account-intent-score.tsx +5 -5
  251. package/blocks/intent/components/intent-feed.tsx +13 -10
  252. package/blocks/intent/components/intent-keyword-manager.tsx +16 -12
  253. package/blocks/intent/components/intent-screen.tsx +265 -0
  254. package/blocks/intent/components/keyword-detail.tsx +124 -0
  255. package/blocks/intent/lib/intent-utils.ts +4 -4
  256. package/blocks/lifecycle/block.json +13 -8
  257. package/blocks/lifecycle/components/account-related.tsx +8 -1
  258. package/blocks/lifecycle/components/churn-list.tsx +5 -2
  259. package/blocks/lifecycle/components/customer-table.tsx +1 -1
  260. package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
  261. package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
  262. package/blocks/lifecycle/lib/lifecycle.ts +1 -1
  263. package/blocks/lists/README.md +20 -1
  264. package/blocks/lists/block.json +69 -3
  265. package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
  266. package/blocks/lists/components/add-to-list-button.tsx +20 -9
  267. package/blocks/lists/components/list-picker.tsx +17 -3
  268. package/blocks/lists/components/list-table.tsx +12 -6
  269. package/blocks/lists/components/lists-screen.tsx +346 -0
  270. package/blocks/lists/components/record-lists-tab.tsx +99 -0
  271. package/blocks/lists/components/suppression-manager.tsx +33 -8
  272. package/blocks/lists/components/suppressions-screen.tsx +264 -0
  273. package/blocks/mailboxes/README.md +17 -0
  274. package/blocks/mailboxes/block.json +69 -17
  275. package/blocks/mailboxes/components/connect-mailbox.tsx +32 -21
  276. package/blocks/mailboxes/components/domain-detail.tsx +148 -0
  277. package/blocks/mailboxes/components/domain-purchase.tsx +20 -14
  278. package/blocks/mailboxes/components/mailbox-detail.tsx +157 -0
  279. package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
  280. package/blocks/mailboxes/components/mailboxes-screen.tsx +228 -0
  281. package/blocks/mailboxes/components/warmup-health.tsx +24 -22
  282. package/blocks/mailboxes/lib/mailboxes-utils.ts +26 -5
  283. package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
  284. package/blocks/newsletter/README.md +12 -0
  285. package/blocks/newsletter/block.json +157 -24
  286. package/blocks/newsletter/components/issue-scheduler.tsx +4 -3
  287. package/blocks/newsletter/components/issue-versions.tsx +93 -0
  288. package/blocks/newsletter/components/more-rows.tsx +30 -0
  289. package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
  290. package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
  291. package/blocks/newsletter/components/newsletter-screen.tsx +39 -14
  292. package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
  293. package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
  294. package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
  295. package/blocks/newsletter/components/subscriber-table.tsx +5 -7
  296. package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
  297. package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
  298. package/blocks/nurture/block.json +28 -14
  299. package/blocks/nurture/components/nurture-builder.tsx +13 -3
  300. package/blocks/nurture/components/nurture-enroll.tsx +12 -10
  301. package/blocks/nurture/components/nurture-form.tsx +27 -10
  302. package/blocks/nurture/components/nurture-screen.tsx +26 -11
  303. package/blocks/nurture/components/nurture-stats.tsx +5 -4
  304. package/blocks/nurture/lib/nurture-types.ts +1 -1
  305. package/blocks/objects/README.md +18 -1
  306. package/blocks/objects/block.json +42 -21
  307. package/blocks/objects/components/attribute-field-input.tsx +14 -1
  308. package/blocks/objects/components/object-not-found.tsx +3 -1
  309. package/blocks/objects/components/object-record-detail.tsx +10 -2
  310. package/blocks/objects/components/object-schema-editor.tsx +4 -2
  311. package/blocks/objects/components/object-table.tsx +13 -3
  312. package/blocks/objects/components/objects-screen.tsx +147 -0
  313. package/blocks/objects/components/record-roles-panel.tsx +15 -5
  314. package/blocks/objects/lib/object-types.ts +1 -1
  315. package/blocks/pages/README.md +14 -0
  316. package/blocks/pages/block.json +140 -23
  317. package/blocks/pages/components/brand-kit-editor.tsx +5 -2
  318. package/blocks/pages/components/content-library.tsx +9 -4
  319. package/blocks/pages/components/more-rows.tsx +30 -0
  320. package/blocks/pages/components/page-ai-tools.tsx +196 -0
  321. package/blocks/pages/components/page-analytics.tsx +176 -0
  322. package/blocks/pages/components/page-clone.tsx +99 -0
  323. package/blocks/pages/components/page-domains.tsx +167 -0
  324. package/blocks/pages/components/page-editor.tsx +1 -1
  325. package/blocks/pages/components/page-generation-status.tsx +48 -0
  326. package/blocks/pages/components/page-list.tsx +2 -1
  327. package/blocks/pages/components/page-tracking.tsx +137 -0
  328. package/blocks/pages/components/pages-screen.tsx +57 -16
  329. package/blocks/pages/components/seo-report.tsx +6 -1
  330. package/blocks/pages/lib/studio.ts +7 -0
  331. package/blocks/phone-numbers/README.md +74 -0
  332. package/blocks/phone-numbers/block.json +262 -0
  333. package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
  334. package/blocks/phone-numbers/components/assign-number.tsx +104 -0
  335. package/blocks/phone-numbers/components/number-search.tsx +239 -0
  336. package/blocks/phone-numbers/components/number-settings.tsx +134 -0
  337. package/blocks/phone-numbers/components/number-usage.tsx +97 -0
  338. package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
  339. package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
  340. package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
  341. package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
  342. package/blocks/phone-numbers/components/release-number.tsx +126 -0
  343. package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
  344. package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
  345. package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
  346. package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
  347. package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
  348. package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
  349. package/blocks/pickers/README.md +23 -0
  350. package/blocks/pickers/block.json +103 -0
  351. package/blocks/pickers/components/company-picker.tsx +90 -0
  352. package/blocks/pickers/components/contact-picker.tsx +131 -0
  353. package/blocks/pickers/components/member-picker.tsx +99 -0
  354. package/blocks/pickers/components/search-picker.tsx +98 -0
  355. package/blocks/pickers/hooks/use-current-member.ts +104 -0
  356. package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
  357. package/blocks/pickers/lib/use-debounced.ts +13 -0
  358. package/blocks/quotes/README.md +13 -0
  359. package/blocks/quotes/block.json +65 -13
  360. package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
  361. package/blocks/quotes/components/product-picker.tsx +3 -1
  362. package/blocks/quotes/components/quote-builder.tsx +1 -1
  363. package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
  364. package/blocks/quotes/components/quote-list.tsx +5 -3
  365. package/blocks/quotes/components/quote-related.tsx +8 -4
  366. package/blocks/quotes/components/quote-viewer.tsx +7 -2
  367. package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
  368. package/blocks/radar/README.md +10 -0
  369. package/blocks/radar/block.json +41 -1
  370. package/blocks/radar/components/competitor-battle-card.tsx +1 -1
  371. package/blocks/radar/components/competitor-detail.tsx +117 -0
  372. package/blocks/radar/components/competitor-tracker.tsx +3 -1
  373. package/blocks/radar/components/radar-overview.tsx +4 -4
  374. package/blocks/radar/components/radar-report.tsx +4 -2
  375. package/blocks/radar/components/radar-screen.tsx +214 -0
  376. package/blocks/radar/components/report-detail.tsx +104 -0
  377. package/blocks/search/README.md +10 -0
  378. package/blocks/search/block.json +16 -1
  379. package/blocks/search/components/filter-builder.tsx +3 -3
  380. package/blocks/search/components/prospect-results.tsx +5 -3
  381. package/blocks/search/components/prospect-search-screen.tsx +235 -0
  382. package/blocks/search/components/saved-searches.tsx +4 -1
  383. package/blocks/search/hooks/use-filter-options.ts +20 -6
  384. package/blocks/search/lib/search-types.ts +2 -0
  385. package/blocks/sequencer/README.md +55 -9
  386. package/blocks/sequencer/block.json +126 -19
  387. package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
  388. package/blocks/sequencer/components/enroll-dialog.tsx +11 -5
  389. package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
  390. package/blocks/sequencer/components/sequence-builder.tsx +224 -60
  391. package/blocks/sequencer/components/sequence-detail.tsx +261 -0
  392. package/blocks/sequencer/components/sequence-enrollments.tsx +116 -0
  393. package/blocks/sequencer/components/sequence-list.tsx +34 -9
  394. package/blocks/sequencer/components/sequence-stats.tsx +11 -10
  395. package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
  396. package/blocks/sequencer/components/sequences-screen.tsx +162 -0
  397. package/blocks/sequencer/components/step-editor.tsx +14 -12
  398. package/blocks/sequencer/lib/sequencer-utils.ts +133 -10
  399. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
  400. package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
  401. package/blocks/social-ads/README.md +19 -0
  402. package/blocks/social-ads/block.json +233 -1
  403. package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
  404. package/blocks/social-ads/components/ads-screen.tsx +152 -0
  405. package/blocks/social-ads/components/audience-sync-detail.tsx +194 -0
  406. package/blocks/social-ads/components/audience-sync.tsx +17 -9
  407. package/blocks/social-ads/components/campaign-ads.tsx +166 -0
  408. package/blocks/social-ads/components/campaign-detail.tsx +138 -0
  409. package/blocks/social-ads/components/campaign-editor.tsx +134 -0
  410. package/blocks/social-ads/components/creative-concepts.tsx +74 -0
  411. package/blocks/social-ads/components/creative-detail.tsx +155 -0
  412. package/blocks/social-ads/components/creative-gallery.tsx +35 -7
  413. package/blocks/social-ads/components/creative-revisions.tsx +57 -0
  414. package/blocks/social-ads/components/creative-tools.tsx +176 -0
  415. package/blocks/social-ads/components/post-composer.tsx +7 -3
  416. package/blocks/social-ads/components/post-detail.tsx +163 -0
  417. package/blocks/social-ads/components/social-ideas.tsx +64 -0
  418. package/blocks/social-ads/components/social-performance.tsx +80 -0
  419. package/blocks/social-ads/components/social-post-list.tsx +11 -3
  420. package/blocks/social-ads/components/social-screen.tsx +187 -0
  421. package/blocks/social-ads/lib/ads-extra.ts +67 -0
  422. package/blocks/surveys/README.md +91 -0
  423. package/blocks/surveys/block.json +299 -0
  424. package/blocks/surveys/components/account-nps.tsx +69 -0
  425. package/blocks/surveys/components/contact-survey-history.tsx +52 -0
  426. package/blocks/surveys/components/org-summary.tsx +82 -0
  427. package/blocks/surveys/components/sending-domains.tsx +113 -0
  428. package/blocks/surveys/components/survey-form.tsx +126 -0
  429. package/blocks/surveys/components/survey-responses.tsx +145 -0
  430. package/blocks/surveys/components/survey-results.tsx +72 -0
  431. package/blocks/surveys/components/survey-send-history.tsx +58 -0
  432. package/blocks/surveys/components/survey-send.tsx +162 -0
  433. package/blocks/surveys/components/survey-triggers.tsx +166 -0
  434. package/blocks/surveys/components/surveys-screen.tsx +234 -0
  435. package/blocks/surveys/lib/surveys-types.ts +228 -0
  436. package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
  437. package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
  438. package/blocks/surveys/recipes/survey-score-report.ts +27 -0
  439. package/blocks/visitors/README.md +10 -0
  440. package/blocks/visitors/block.json +25 -1
  441. package/blocks/visitors/components/snippet-install.tsx +21 -10
  442. package/blocks/visitors/components/traffic-chart.tsx +4 -4
  443. package/blocks/visitors/components/visitor-company-card.tsx +26 -16
  444. package/blocks/visitors/components/visitor-detail.tsx +105 -0
  445. package/blocks/visitors/components/visitor-feed.tsx +16 -7
  446. package/blocks/visitors/components/visitors-screen.tsx +166 -0
  447. package/blocks/webchat/README.md +10 -0
  448. package/blocks/webchat/block.json +21 -7
  449. package/blocks/webchat/components/chat-widget-config.tsx +8 -5
  450. package/blocks/webchat/components/conversation-list.tsx +8 -4
  451. package/blocks/webchat/components/webchat-screen.tsx +245 -0
  452. package/blocks/workbench/README.md +11 -0
  453. package/blocks/workbench/block.json +48 -1
  454. package/blocks/workbench/components/column-recipe-editor.tsx +5 -3
  455. package/blocks/workbench/components/run-progress.tsx +1 -1
  456. package/blocks/workbench/components/workbench-grid.tsx +10 -4
  457. package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
  458. package/blocks/workbench/components/workbench-screen.tsx +202 -0
  459. package/blocks/workbench/components/workbench-sources.tsx +194 -0
  460. package/blocks/workbench/components/workbench-table-list.tsx +14 -2
  461. package/blocks/workflows/README.md +13 -0
  462. package/blocks/workflows/block.json +28 -9
  463. package/blocks/workflows/components/run-history.tsx +31 -9
  464. package/blocks/workflows/components/trigger-config.tsx +3 -1
  465. package/blocks/workflows/components/workflow-editor.tsx +4 -2
  466. package/blocks/workflows/components/workflow-list.tsx +13 -6
  467. package/blocks/workflows/components/workflows-screen.tsx +193 -0
  468. package/dist/cli.js +3109 -1977
  469. package/dist/errors.d.mts +34 -0
  470. package/dist/errors.d.ts +34 -0
  471. package/dist/errors.js +46 -0
  472. package/dist/errors.js.map +1 -0
  473. package/dist/errors.mjs +21 -0
  474. package/dist/errors.mjs.map +1 -0
  475. package/dist/index.browser.js +11 -5
  476. package/dist/index.browser.js.map +1 -1
  477. package/dist/index.browser.mjs +11 -5
  478. package/dist/index.browser.mjs.map +1 -1
  479. package/dist/index.d.mts +164 -25
  480. package/dist/index.d.ts +164 -25
  481. package/dist/index.js +11 -5
  482. package/dist/index.js.map +1 -1
  483. package/dist/index.mjs +11 -5
  484. package/dist/index.mjs.map +1 -1
  485. package/dist/react.browser.js +10 -4
  486. package/dist/react.browser.js.map +1 -1
  487. package/dist/react.browser.mjs +10 -4
  488. package/dist/react.browser.mjs.map +1 -1
  489. package/dist/react.d.mts +142 -5
  490. package/dist/react.d.ts +142 -5
  491. package/dist/react.js +10 -4
  492. package/dist/react.js.map +1 -1
  493. package/dist/react.mjs +10 -4
  494. package/dist/react.mjs.map +1 -1
  495. package/dist/types/blocks/add.d.ts +11 -4
  496. package/dist/types/blocks/brand.d.ts +122 -0
  497. package/dist/types/blocks/compose.d.ts +53 -0
  498. package/dist/types/blocks/create.d.ts +27 -20
  499. package/dist/types/blocks/doctor.d.ts +6 -1
  500. package/dist/types/blocks/flows.d.ts +7 -1
  501. package/dist/types/blocks/navfile.d.ts +35 -0
  502. package/dist/types/blocks/orgBrand.d.ts +34 -0
  503. package/dist/types/blocks/paths.d.ts +4 -0
  504. package/dist/types/blocks/prefixes.d.ts +37 -0
  505. package/dist/types/blocks/registry.d.ts +17 -0
  506. package/dist/types/blocks/screens.d.ts +53 -0
  507. package/dist/types/cli.d.ts +5 -0
  508. package/dist/types/errors.d.ts +33 -0
  509. package/dist/types/http.d.ts +2 -18
  510. package/dist/types/version.d.ts +2 -2
  511. package/docs/agents.md +15 -4
  512. package/docs/api/README.md +2 -2
  513. package/docs/api/by-path.md +1 -0
  514. package/docs/api/desk.d.ts +98 -2
  515. package/docs/api/desk.md +123 -1
  516. package/docs/api/inbox.d.ts +10 -2
  517. package/docs/api/inbox.md +10 -2
  518. package/docs/api/seo.d.ts +19 -1
  519. package/docs/api/seo.md +24 -1
  520. package/package.json +16 -2
  521. package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
  522. package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
  523. package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
  524. package/stacks/6sense.json +55 -17
  525. package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
  526. package/stacks/apollo/pages/companies.tsx.tmpl +33 -0
  527. package/stacks/apollo/pages/contacts.tsx.tmpl +51 -0
  528. package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
  529. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
  530. package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
  531. package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
  532. package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
  533. package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
  534. package/stacks/apollo.json +147 -37
  535. package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
  536. package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
  537. package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
  538. package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
  539. package/stacks/calendly.json +64 -0
  540. package/stacks/clay/pages/companies.tsx.tmpl +31 -0
  541. package/stacks/clay/pages/contacts.tsx.tmpl +31 -0
  542. package/stacks/clay/pages/lists.tsx.tmpl +12 -0
  543. package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
  544. package/stacks/clay.json +73 -27
  545. package/stacks/gong/pages/calls.tsx.tmpl +23 -0
  546. package/stacks/gong/pages/companies.tsx.tmpl +22 -0
  547. package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
  548. package/stacks/gong/pages/lists.tsx.tmpl +12 -0
  549. package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
  550. package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
  551. package/stacks/gong.json +98 -35
  552. package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
  553. package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
  554. package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
  555. package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
  556. package/stacks/hootsuite.json +104 -0
  557. package/stacks/hubspot/pages/companies.tsx.tmpl +26 -0
  558. package/stacks/hubspot/pages/contacts.tsx.tmpl +45 -0
  559. package/stacks/hubspot/pages/deals.tsx.tmpl +20 -0
  560. package/stacks/hubspot/pages/inbox.tsx.tmpl +103 -0
  561. package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
  562. package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
  563. package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
  564. package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
  565. package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
  566. package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
  567. package/stacks/hubspot.json +181 -64
  568. package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
  569. package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
  570. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
  571. package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
  572. package/stacks/intercom.json +52 -20
  573. package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
  574. package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
  575. package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
  576. package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
  577. package/stacks/mailchimp.json +68 -34
  578. package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
  579. package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
  580. package/stacks/outreach/pages/contacts.tsx.tmpl +46 -0
  581. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
  582. package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
  583. package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
  584. package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
  585. package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
  586. package/stacks/outreach.json +106 -25
  587. package/stacks/pipedrive/pages/companies.tsx.tmpl +23 -0
  588. package/stacks/pipedrive/pages/contacts.tsx.tmpl +27 -0
  589. package/stacks/pipedrive/pages/deals.tsx.tmpl +20 -0
  590. package/stacks/pipedrive/pages/inbox.tsx.tmpl +103 -0
  591. package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
  592. package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
  593. package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
  594. package/stacks/pipedrive.json +117 -0
  595. package/stacks/salesforce/pages/companies.tsx.tmpl +26 -0
  596. package/stacks/salesforce/pages/contacts.tsx.tmpl +32 -0
  597. package/stacks/salesforce/pages/deals.tsx.tmpl +20 -0
  598. package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
  599. package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
  600. package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
  601. package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
  602. package/stacks/salesforce.json +85 -31
  603. package/stacks/zoominfo/pages/companies.tsx.tmpl +31 -0
  604. package/stacks/zoominfo/pages/contacts.tsx.tmpl +31 -0
  605. package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
  606. package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
  607. package/stacks/zoominfo.json +115 -0
  608. package/styles/styles.css +1 -1
  609. package/templates/next-app/README.md.tmpl +40 -9
  610. package/templates/next-app/app/globals.css.tmpl +349 -64
  611. package/templates/next-app/app/layout.tsx.tmpl +7 -2
  612. package/templates/next-app/components/app-shell.tsx.tmpl +104 -88
  613. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
  614. package/templates/next-app/dot-gitignore.tmpl +2 -0
  615. package/templates/next-app/package.json.tmpl +1 -1
@@ -3,6 +3,7 @@
3
3
  import { useState } from "react";
4
4
  import { useG8Query } from "@/g8/hooks/use-g8-query";
5
5
  import type { G8Envelope } from "@/g8/lib/g8-client";
6
+ import type { G8QueryKey } from "@/g8/lib/g8-cache";
6
7
  import type { PageInfo } from "../lib/crm-types";
7
8
 
8
9
  export interface UsePageQueryResult<T> {
@@ -24,16 +25,23 @@ export interface UsePageQueryResult<T> {
24
25
  * have one, so the table keeps a page number instead.
25
26
  *
26
27
  * Changing `deps` (a filter, a search box) goes back to page 1 without a second request.
28
+ *
29
+ * Optional `cacheKey` joins the shared query cache (each page cached as `[...cacheKey, { page, deps }]`),
30
+ * so the list shows instantly when you come back and refreshes when a write invalidates its prefix.
27
31
  */
28
32
  export function usePageQuery<T>(
29
33
  fetchPage: (page: number, signal: AbortSignal) => Promise<G8Envelope<T[]>>,
30
34
  deps: readonly unknown[],
31
35
  pageSize = 25,
36
+ cacheKey?: readonly unknown[],
37
+ /** Hold the read (for example until URL filters are known). Default true. */
38
+ enabled = true,
32
39
  ): UsePageQueryResult<T> {
33
40
  const key = JSON.stringify(deps);
34
41
  const [state, setState] = useState({ key, page: 1 });
35
42
  const page = state.key === key ? state.page : 1;
36
- const query = useG8Query((signal) => fetchPage(page, signal), [key, page]);
43
+ const queryKey: G8QueryKey | undefined = cacheKey ? [...cacheKey, { page, pageSize, deps: key }] : undefined;
44
+ const query = useG8Query((signal) => fetchPage(page, signal), [key, page], { key: queryKey, enabled });
37
45
 
38
46
  const info = query.data?.pagination as Partial<PageInfo> | undefined;
39
47
  const rows = (query.data?.data ?? []) as T[];
@@ -31,6 +31,8 @@ export interface ContactRecord {
31
31
  last_name?: string | null;
32
32
  full_name?: string | null;
33
33
  work_email?: string | null;
34
+ /** Personal addresses, comma separated. */
35
+ personal_emails?: string | null;
34
36
  direct_phone?: string | null;
35
37
  mobile_phone?: string | null;
36
38
  job_title?: string | null;
@@ -102,9 +104,26 @@ export function isMasked(value: unknown): boolean {
102
104
  return typeof value === "string" && value.trim() === "***";
103
105
  }
104
106
 
105
- export function contactName(c: Pick<ContactRecord, "first_name" | "last_name" | "full_name" | "work_email">): string {
107
+ export function contactName(c: Pick<ContactRecord, "first_name" | "last_name" | "full_name" | "work_email"> & { id?: number | string }): string {
106
108
  const joined = [c.first_name, c.last_name].filter(Boolean).join(" ").trim();
107
- return joined || c.full_name || (c.work_email && !isMasked(c.work_email) ? c.work_email : "") || "Unnamed contact";
109
+ return joined || c.full_name || (c.work_email && !isMasked(c.work_email) ? c.work_email : "") || (c.id != null ? `#${c.id}` : "-");
110
+ }
111
+
112
+ /**
113
+ * The line under a contact's name: "VP of Sales at Acme". The company is the company's own name (`company.name`, else the
114
+ * list row's `company_name`); it never falls back to anything about the person. "at ..." is left out when there is no
115
+ * title or no company. A company that happens to share the person's name (a freelancer) is shown as the API sent it.
116
+ */
117
+ export function contactSubtitle(c: Pick<ContactRecord, "job_title" | "company_name" | "company">): string {
118
+ const company = [c.company?.name, c.company_name].map((n) => (n ?? "").trim()).find((n) => n !== "");
119
+ const title = (c.job_title ?? "").trim();
120
+ if (title && company) return `${title} at ${company}`;
121
+ return title || company || "No title or company";
122
+ }
123
+
124
+ /** A company's display name: name, then domain, then `#id`, then a dash. Never "Unnamed". */
125
+ export function companyName(c: { name?: string | null; domain?: string | null; id?: number | string }): string {
126
+ return c.name || c.domain || (c.id != null ? `#${c.id}` : "-");
108
127
  }
109
128
 
110
129
  export function initials(name: string): string {
@@ -134,3 +153,8 @@ export function errorDetail(error: unknown): Record<string, unknown> | undefined
134
153
  const detail = (error as { detail?: unknown } | null)?.detail;
135
154
  return detail && typeof detail === "object" && !Array.isArray(detail) ? (detail as Record<string, unknown>) : undefined;
136
155
  }
156
+
157
+ /** Passed as the second argument to a screen's `renderRowActions` / `renderDetailActions`: reload what the screen shows (filters, page, sort and selection are kept). */
158
+ export interface ScreenActionContext {
159
+ reload: () => void;
160
+ }
@@ -0,0 +1,16 @@
1
+ "use client";
2
+
3
+ /**
4
+ * readLists: one page of audience lists, for ListSelect and useAllLists.
5
+ *
6
+ * SDK operations: GET /lists?search&page&limit (lists.listLists) read
7
+ */
8
+ import { g8Get } from "@/g8/lib/g8-client";
9
+ import { hasNextOf } from "@/g8/lib/paging";
10
+ import type { ListSelectOption, ListsReader } from "@/g8/components/list-select";
11
+
12
+ export const readLists: ListsReader = async (q, page, pageSize, signal) => {
13
+ const env = await g8Get<ListSelectOption[]>("/lists", { search: q || undefined, page, limit: pageSize }, { signal });
14
+ const rows = (env.data ?? []) as ListSelectOption[];
15
+ return { rows, hasNext: hasNextOf(env, pageSize, rows.length) };
16
+ };
@@ -0,0 +1,105 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Small pieces shared by ContactsScreen, CompaniesScreen and TasksScreen: client-side sorting of the loaded
5
+ * page, a sortable table header, row selection that resets with the data source, the retry-safe bulk delete
6
+ * (Promise.allSettled, 404 = already gone, always refetch) and a pager. No operations of its own except the
7
+ * DELETE the caller names.
8
+ */
9
+
10
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
11
+ import { Button, TableHead } from "@graph8/sdk/ui";
12
+ import { ArrowDown, ArrowUp, ChevronsUpDown } from "lucide-react";
13
+ import { g8Delete } from "@/g8/lib/g8-client";
14
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
15
+ import { errorStatus, isMasked } from "./crm-types";
16
+
17
+ export type SortDir = "asc" | "desc";
18
+ export interface SortState { key: string; dir: SortDir }
19
+
20
+ /** Sort rows by a text accessor. Empty and locked values always go last. Sorts only what is loaded (the API has no sort). */
21
+ export function sortRows<T>(rows: T[], sort: SortState | null, get: (row: T, key: string) => string | null | undefined): T[] {
22
+ if (!sort) return rows;
23
+ const sign = sort.dir === "asc" ? 1 : -1;
24
+ const norm = (r: T) => { const v = get(r, sort.key); return v == null || v === "" || isMasked(v) ? null : v.toLowerCase(); };
25
+ return [...rows].sort((a, b) => {
26
+ const x = norm(a), y = norm(b);
27
+ if (x === y) return 0;
28
+ if (x == null) return 1;
29
+ if (y == null) return -1;
30
+ return x < y ? -sign : sign;
31
+ });
32
+ }
33
+
34
+ export function useSort(initial: SortState | null = null) {
35
+ const [sort, setSort] = useState<SortState | null>(initial);
36
+ const toggle = useCallback((key: string) => setSort((s) => (s?.key !== key ? { key, dir: "asc" } : s.dir === "asc" ? { key, dir: "desc" } : null)), []);
37
+ return { sort, toggle };
38
+ }
39
+
40
+ export function SortHead({ label, sortKey, sort, onSort, className }: { label: string; sortKey: string; sort: SortState | null; onSort: (key: string) => void; className?: string }) {
41
+ const active = sort?.key === sortKey;
42
+ const Icon = !active ? ChevronsUpDown : sort.dir === "asc" ? ArrowUp : ArrowDown;
43
+ return (
44
+ <TableHead className={className} aria-sort={active ? (sort.dir === "asc" ? "ascending" : "descending") : "none"}>
45
+ <button type="button" className="inline-flex items-center gap-1 font-medium hover:text-foreground" onClick={() => onSort(sortKey)}>
46
+ {label}<Icon className={`size-3.5 ${active ? "text-foreground" : "text-muted-foreground"}`} aria-hidden />
47
+ </button>
48
+ </TableHead>
49
+ );
50
+ }
51
+
52
+ /** Selected rows keyed by id. Clears itself when `scope` changes (another filter, list or reload). */
53
+ export function useRowSelection<T extends { id: number | string }>(scope: string) {
54
+ const [selected, setSelected] = useState<Map<T["id"], T>>(new Map());
55
+ const last = useRef(scope);
56
+ useEffect(() => {
57
+ if (last.current === scope) return;
58
+ last.current = scope;
59
+ setSelected(new Map());
60
+ }, [scope]);
61
+ const toggle = (row: T) => setSelected((m) => { const n = new Map(m); if (!n.delete(row.id)) n.set(row.id, row); return n; });
62
+ const togglePage = (rows: T[]) => setSelected((m) => {
63
+ const n = new Map(m);
64
+ const all = rows.length > 0 && rows.every((r) => n.has(r.id));
65
+ for (const r of rows) all ? n.delete(r.id) : n.set(r.id, r);
66
+ return n;
67
+ });
68
+ const drop = (ids: Array<T["id"]>) => setSelected((m) => { const n = new Map(m); ids.forEach((id) => n.delete(id)); return n; });
69
+ const clear = () => setSelected(new Map());
70
+ const ids = useMemo(() => [...selected.keys()], [selected]);
71
+ return { selected, ids, rows: [...selected.values()], toggle, togglePage, drop, clear };
72
+ }
73
+
74
+ /** Deletes every id under `basePath` (`/contacts`). A 404 counts as done; never throws; `onSettled` gets who is gone. */
75
+ export function useBulkDelete<Id extends number | string = number>(basePath: string, onSettled: (gone: Id[], total: number) => void) {
76
+ return useG8Mutation(async (ids: Id[]) => {
77
+ const results = await Promise.allSettled(ids.map((id) => g8Delete(`${basePath}/${id}`)));
78
+ const gone = ids.filter((_, i) => results[i].status === "fulfilled" || errorStatus((results[i] as PromiseRejectedResult).reason) === 404);
79
+ return { gone, total: ids.length };
80
+ }, {
81
+ // `/contacts` -> every cached contacts list and count refreshes (a deleted record's detail is left alone).
82
+ invalidates: [[basePath.split("/")[1], "list"], [basePath.split("/")[1], "count"], [basePath.split("/")[1], "record"]],
83
+ onSuccess: ({ gone, total }) => onSettled(gone, total),
84
+ });
85
+ }
86
+
87
+ export function Pager({ page, pageCount, total, noun, hasNext, loading, onPage }: { page: number; pageCount: number; total: number; noun: string; hasNext: boolean; loading: boolean; onPage: (p: number) => void }) {
88
+ return (
89
+ <div className="flex items-center justify-between border-t px-3 py-2 text-sm text-muted-foreground">
90
+ <span className="tabular-nums">{total.toLocaleString()} {noun}</span>
91
+ <div className="flex items-center gap-2">
92
+ <Button variant="outline" size="sm" disabled={page <= 1 || loading} onClick={() => onPage(page - 1)}>Previous</Button>
93
+ <span className="tabular-nums">Page {page} of {pageCount}</span>
94
+ <Button variant="outline" size="sm" disabled={!hasNext || loading} onClick={() => onPage(page + 1)}>Next</Button>
95
+ </div>
96
+ </div>
97
+ );
98
+ }
99
+
100
+ /** Date-only or timestamp to a short UTC date, so a `2026-10-15` due date never shifts a day. */
101
+ export function formatDay(value?: string | null): string {
102
+ if (!value) return "";
103
+ const d = new Date(value.length === 10 ? `${value}T00:00:00Z` : value);
104
+ return Number.isNaN(d.getTime()) ? "" : d.toLocaleDateString(undefined, { timeZone: "UTC", month: "short", day: "numeric", year: "numeric" });
105
+ }
@@ -11,19 +11,33 @@ compose under `DealDetail`'s `children`). Drag and drop needs `npm i @dnd-kit/co
11
11
 
12
12
  - Board and List views, search by name, a pipeline switcher when there is more than one pipeline.
13
13
  - Headline numbers: open deals, open value, estimated weighted value, won value.
14
- - Open a deal (board card or list row, deep link `?id=<deal id>`): Details (edit, stage history, confirmed delete) and
15
- Activity tabs. Moving a card into a won or lost stage asks for the close date and amount first.
14
+ - Open a deal (board card or list row, deep link `?id=<deal id>`): Details (edit, stage history, confirmed delete),
15
+ Contacts (add with a role, change a role, make primary, remove), Line items (add, edit, remove, totals), Notes (add,
16
+ edit, delete), Tasks (add, complete), Quotes and Activity tabs. Moving a card into a won or lost stage asks for the
17
+ close date and amount first.
18
+ - **Close deal** (detail header): won or lost, with a close date, an amount for won, and a reason picked from the org's
19
+ close reasons (add one on the spot). **API gap:** no deal operation accepts a close reason (`closed_lost_reason` is
20
+ read only), so the reason is saved as a deal note, "Closed lost. Reason: Price". **Move to pipeline** is next to it.
21
+ - Filters on Board and List: owner, stage, outcome (open, won, lost), stale (no activity for 7 to 90 days, or never),
22
+ activity, close-date range and "no contact". `filterQuery(state)` turns them into the `GET /deals` parameters.
23
+ - Pipeline settings: create, rename, make default and delete (confirmed) pipelines, add or archive close reasons,
24
+ then the stage summary and StageEditor.
25
+ - Quotes tab: the deal's quotes. To offer "Create quote from this deal", pass the quotes block's button:
26
+ `<DealsScreen renderQuoteActions={(deal) => <CreateQuoteFromDeal dealId={deal.id} />} />` (quotes depends on deals,
27
+ so the deals block cannot import it).
16
28
  - **New deal**: name, amount, close date, stage, an **owner** and **at least one contact**. The API requires both
17
29
  (`owner_id`, and `contact_ids` with one or more mashup contact ids that all work at one company; the older note that
18
30
  the list may be empty was wrong). A company that already has a deal is refused with 409 unless "Allow another deal"
19
31
  is ticked.
20
- - Forecast tab (totals, win rate, stuck deals, estimated weighted pipeline) and Pipeline settings tab (stage names,
21
- probabilities, won/lost types, order, delete).
32
+ - Forecast tab (totals, win rate, stuck deals, estimated weighted pipeline) and Pipeline settings tab (pipelines, close
33
+ reasons, stage names, probabilities, won/lost types, order, delete).
22
34
  - Slots for cross-block glue, all optional: `renderRowActions(deal)`, `renderDetailActions(deal)`,
23
35
  `extraDetailTabs`, `toolbarActions`. Example: add a Quotes tab with `<QuoteList dealId={deal.id} />`.
24
36
 
25
- Scopes the screen needs on top of the lists below: `team:read` (owner default and picker) and `contacts:read`
26
- (contact search). Proxy `allowedPrefixes` must include `deals`, `pipelines`, `team-members`, `contacts` (and `activity`
37
+ Scopes the screen needs on top of the lists below: `team:read` (owner default and picker), `contacts:read`
38
+ (contact search), `notes:read` and `notes:write` (Notes tab), `tasks:read` and `tasks:write` (Tasks tab) and
39
+ `deals:delete` (remove a contact, delete a note, archive a close reason).
40
+ Proxy `allowedPrefixes` must include `deals`, `pipelines`, `team-members`, `contacts`, `tasks` (and `activity`
27
41
  for the timeline).
28
42
 
29
43
 
@@ -107,3 +121,7 @@ create), `deal.id` + `deal.revision` (card -> detail -> update/delete).
107
121
  - `01-create-deal-and-advance.ts`: create, move, close as won.
108
122
  - `02-pipeline-report.ts`: totals, win rate, stall points, estimated weighted forecast.
109
123
  - `03-stale-deals-sweep.ts`: find neglected open deals, optionally flag them with one bulk write.
124
+
125
+ ## Cross-block glue
126
+
127
+ - `RecordDealsTab` (`components/record-deals-tab.tsx`): the deals of one contact or company (`GET /contacts/{id}/deals`, `GET /companies/{id}/deals`), each opening in the deals screen. Use it as an `extraDetailTabs` entry of `ContactsScreen` / `CompaniesScreen`. `DealsScreen` also takes optional `onOpenContact` / `onOpenCompany`: the Contacts tab gains "Open in contacts" and the contact sheet gains "Open company".
@@ -8,30 +8,56 @@
8
8
  "optimistic stage moves with rollback and closed-stage rules handled",
9
9
  "deal create, edit, stage history and confirmed delete",
10
10
  "stage editor: names, probabilities, won/lost types, order",
11
- "forecast: totals, win rate, stuck deals and an estimated weighted pipeline"
11
+ "forecast: totals, win rate, stuck deals and an estimated weighted pipeline",
12
+ "deal notes, tasks, contacts with roles and primary, and line items with totals",
13
+ "close a deal as won or lost with a reason",
14
+ "create, rename, default and delete pipelines, and manage close reasons",
15
+ "list filters: owner, stage, outcome, stale, activity, close-date range, no contact"
12
16
  ],
13
17
  "dependsOn": [
14
18
  "crm"
15
19
  ],
16
20
  "operations": [
21
+ "add_deal_contact_deals__deal_id__contacts_post",
22
+ "archive_deal_close_reason_deals_close_reasons__reason_id__delete",
23
+ "create_deal_close_reason_deals_close_reasons_post",
17
24
  "create_deal_deals_post",
25
+ "create_deal_note_deals__deal_id__notes_post",
26
+ "create_global_task_tasks_post",
27
+ "create_pipeline_pipelines_post",
18
28
  "create_stage_pipelines__pipeline_id__stages_post",
19
29
  "delete_deal_deals__deal_id__delete",
30
+ "delete_deal_note_deals__deal_id__notes__note_id__delete",
31
+ "delete_pipeline_pipelines__pipeline_id__delete",
20
32
  "delete_stage_pipelines__pipeline_id__stages__stage_id__delete",
33
+ "get_company_deals_companies__company_id__deals_get",
34
+ "get_contact_deals_contacts__contact_id__deals_get",
21
35
  "get_deal_analytics_deals_analytics_get",
22
36
  "get_deal_deals__deal_id__get",
23
37
  "get_deal_stage_velocity_deals_stage_velocity_get",
24
38
  "get_my_team_member_team_members_me_get",
25
39
  "get_pipeline_pipelines__pipeline_id__get",
40
+ "list_all_tasks_tasks_get",
26
41
  "list_contacts_contacts_get",
42
+ "list_deal_close_reasons_deals_close_reasons_get",
27
43
  "list_deal_history_deals__deal_id__history_get",
44
+ "list_deal_line_items_deals__deal_id__line_items_get",
45
+ "list_deal_notes_deals__deal_id__notes_get",
28
46
  "list_deals_deals_get",
29
47
  "list_pipelines_deals_pipelines_get",
30
48
  "list_quotes_quotes_get",
31
49
  "list_team_members_team_members_get",
50
+ "move_deal_to_pipeline_deals__deal_id__pipeline_put",
51
+ "remove_deal_contact_deals__deal_id__contacts__person_id__delete",
32
52
  "reorder_stages_pipelines__pipeline_id__stages_reorder_put",
53
+ "replace_deal_line_items_deals__deal_id__line_items_put",
54
+ "set_deal_contact_role_deals__deal_id__contacts__contact_id__put",
55
+ "set_primary_deal_contact_deals__deal_id__contacts__person_id__primary_put",
33
56
  "update_deal_deals__deal_id__patch",
34
- "update_stage_pipelines__pipeline_id__stages__stage_id__put"
57
+ "update_deal_note_deals__deal_id__notes__note_id__put",
58
+ "update_pipeline_pipelines__pipeline_id__put",
59
+ "update_stage_pipelines__pipeline_id__stages__stage_id__put",
60
+ "update_task_tasks__task_id__patch"
35
61
  ],
36
62
  "components": [
37
63
  {
@@ -101,15 +127,136 @@
101
127
  "name": "DealsScreen",
102
128
  "file": "components/deals-screen.tsx",
103
129
  "purpose": "Finished deals product: board and list, new deal with the owner defaulted to the API key user, deal detail with activity, forecast and pipeline settings.",
104
- "operations": [],
105
- "tiers": []
130
+ "operations": [
131
+ "get_deal_deals__deal_id__get",
132
+ "list_contacts_contacts_get"
133
+ ],
134
+ "tiers": [
135
+ "read"
136
+ ]
106
137
  },
107
138
  {
108
139
  "name": "DealContactsTab",
109
140
  "file": "components/deal-related.tsx",
110
141
  "purpose": "A deal's contacts with roles; open one for the full CRM record. Also exports DealQuotesTab, a read-only list of the deal's quotes.",
111
142
  "operations": [
112
- "get_deal_deals__deal_id__get"
143
+ "get_deal_deals__deal_id__get",
144
+ "list_quotes_quotes_get"
145
+ ],
146
+ "tiers": [
147
+ "read"
148
+ ]
149
+ },
150
+ {
151
+ "name": "DealNotesTab",
152
+ "file": "components/deal-notes-tab.tsx",
153
+ "purpose": "Notes of one deal: list, add, edit (only when the text changed) and confirmed delete; synced notes are read only.",
154
+ "operations": [
155
+ "create_deal_note_deals__deal_id__notes_post",
156
+ "delete_deal_note_deals__deal_id__notes__note_id__delete",
157
+ "list_deal_notes_deals__deal_id__notes_get",
158
+ "update_deal_note_deals__deal_id__notes__note_id__put"
159
+ ],
160
+ "tiers": [
161
+ "read",
162
+ "write",
163
+ "destructive"
164
+ ]
165
+ },
166
+ {
167
+ "name": "DealTasksTab",
168
+ "file": "components/deal-tasks-tab.tsx",
169
+ "purpose": "Follow-up tasks linked to one deal: list (open or all), add with due date and priority, complete and reopen.",
170
+ "operations": [
171
+ "create_global_task_tasks_post",
172
+ "list_all_tasks_tasks_get",
173
+ "update_task_tasks__task_id__patch"
174
+ ],
175
+ "tiers": [
176
+ "read",
177
+ "write"
178
+ ]
179
+ },
180
+ {
181
+ "name": "DealContactsEditor",
182
+ "file": "components/deal-contacts-editor.tsx",
183
+ "purpose": "Buying committee of one deal: add a contact with a role, change a role, make one primary, remove one (confirmed).",
184
+ "operations": [
185
+ "add_deal_contact_deals__deal_id__contacts_post",
186
+ "get_deal_deals__deal_id__get",
187
+ "list_contacts_contacts_get",
188
+ "remove_deal_contact_deals__deal_id__contacts__person_id__delete",
189
+ "set_deal_contact_role_deals__deal_id__contacts__contact_id__put",
190
+ "set_primary_deal_contact_deals__deal_id__contacts__person_id__primary_put"
191
+ ],
192
+ "tiers": [
193
+ "read",
194
+ "write",
195
+ "destructive"
196
+ ]
197
+ },
198
+ {
199
+ "name": "DealLineItems",
200
+ "file": "components/deal-line-items.tsx",
201
+ "purpose": "Products on a deal: add, edit and remove lines (dollars in, cents sent), per-line totals, the deal total and the quote-mismatch warning.",
202
+ "operations": [
203
+ "list_deal_line_items_deals__deal_id__line_items_get",
204
+ "replace_deal_line_items_deals__deal_id__line_items_put"
205
+ ],
206
+ "tiers": [
207
+ "read",
208
+ "write"
209
+ ]
210
+ },
211
+ {
212
+ "name": "DealActions",
213
+ "file": "components/close-deal-dialog.tsx",
214
+ "purpose": "Deal header actions: Close deal as won or lost with a reason (saved as a note, the API has no reason field), add close reasons, and Move to pipeline. CloseDealDialog is the dialog alone.",
215
+ "operations": [
216
+ "create_deal_close_reason_deals_close_reasons_post",
217
+ "create_deal_note_deals__deal_id__notes_post",
218
+ "list_deal_close_reasons_deals_close_reasons_get",
219
+ "move_deal_to_pipeline_deals__deal_id__pipeline_put",
220
+ "update_deal_deals__deal_id__patch"
221
+ ],
222
+ "tiers": [
223
+ "read",
224
+ "write"
225
+ ]
226
+ },
227
+ {
228
+ "name": "PipelineEditor",
229
+ "file": "components/pipeline-editor.tsx",
230
+ "purpose": "Create, rename, make default and delete (confirmed) pipelines, and add or archive (confirmed) close reasons. Stages are edited by StageEditor.",
231
+ "operations": [
232
+ "archive_deal_close_reason_deals_close_reasons__reason_id__delete",
233
+ "create_deal_close_reason_deals_close_reasons_post",
234
+ "create_pipeline_pipelines_post",
235
+ "delete_pipeline_pipelines__pipeline_id__delete",
236
+ "list_deal_close_reasons_deals_close_reasons_get",
237
+ "list_pipelines_deals_pipelines_get",
238
+ "update_pipeline_pipelines__pipeline_id__put"
239
+ ],
240
+ "tiers": [
241
+ "read",
242
+ "write",
243
+ "destructive"
244
+ ]
245
+ },
246
+ {
247
+ "name": "DealFilterBar",
248
+ "file": "components/deal-filters.tsx",
249
+ "purpose": "List filters: owner, stage, outcome, stale (7 to 90 days), activity, close-date range and no contact. Pass filterQuery(state) to usePipelineDeals.",
250
+ "operations": [],
251
+ "tiers": []
252
+ },
253
+ {
254
+ "name": "RecordDealsTab",
255
+ "file": "components/record-deals-tab.tsx",
256
+ "purpose": "The deals linked to one contact or company, each opening in the deals screen; an extraDetailTabs entry for ContactsScreen and CompaniesScreen.",
257
+ "operations": [
258
+ "get_contact_deals_contacts__contact_id__deals_get",
259
+ "get_company_deals_companies__company_id__deals_get"
113
260
  ],
114
261
  "tiers": [
115
262
  "read"
@@ -148,6 +295,10 @@
148
295
  {
149
296
  "file": "lib/deals-types.ts",
150
297
  "purpose": "Deal, pipeline and stage types, stage kind, money formatting, weighted-pipeline estimate and closed-stage error markers."
298
+ },
299
+ {
300
+ "file": "lib/deal-extras.ts",
301
+ "purpose": "Note, task, line-item and close-reason shapes, contact roles, money in cents, and the deal filter state with its GET /deals query."
151
302
  }
152
303
  ],
153
304
  "recipes": [
@@ -195,7 +346,11 @@
195
346
  "deals:delete",
196
347
  "deals:read",
197
348
  "deals:write",
349
+ "notes:read",
350
+ "notes:write",
198
351
  "quotes:read",
352
+ "tasks:read",
353
+ "tasks:write",
199
354
  "team:read"
200
355
  ],
201
356
  "defaults": [
@@ -0,0 +1,169 @@
1
+ "use client";
2
+
3
+ /**
4
+ * DealActions: the header actions of one deal: "Close deal" (won or lost, with a reason) and "Move to pipeline".
5
+ * CloseDealDialog is the dialog behind the first one and can be mounted on its own.
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. GET /deals/close-reasons?kind=won|lost (deals.listDealCloseReasons) read - the reasons the org keeps
9
+ * POST /deals/close-reasons { kind, label } (deals.createDealCloseReason) write - add a reason while closing
10
+ * 2. PATCH /deals/{id} { stage_id, close_date, amount?, expected_revision } (deals.updateDeal) write - the close itself
11
+ * 3. POST /deals/{id}/notes { content } (notes.createDealNote) write - records the reason on the deal
12
+ * 4. PUT /deals/{id}/pipeline { target_pipeline_id } (deals.moveDealToPipeline) write
13
+ * API GAP: no deal operation accepts a close reason (`closed_lost_reason` is read-only on the deal), so the chosen
14
+ * reason is saved as a note, "Closed lost. Reason: Price", right after the stage moves. If the move succeeded but the
15
+ * note failed, the dialog says so and retries only the note (the deal is never closed twice).
16
+ * A won deal needs a close date and an amount; the API also needs an owner. Date-only values are sent as YYYY-MM-DD.
17
+ */
18
+ import { useState } from "react";
19
+ import { Button, Dialog, DialogContent, DialogDescription, DialogTitle, Input, Label } from "@graph8/sdk/ui";
20
+ import { ApiError } from "@/g8/components/api-error";
21
+ import { g8Get, g8Patch, g8Post, g8Put } from "@/g8/lib/g8-client";
22
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
23
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
24
+ import { closedStageMarker, stageKind, type Deal, type Pipeline, type Stage } from "../lib/deals-types";
25
+ import { reasonsOf } from "../lib/deal-extras";
26
+ import { localToday } from "./pipeline-board";
27
+
28
+ export interface CloseDealDialogProps {
29
+ deal: Deal;
30
+ /** The stages of the deal's pipeline. */
31
+ stages: Stage[];
32
+ open: boolean;
33
+ onClose: () => void;
34
+ /** Called once the deal is closed (and the reason recorded). */
35
+ onClosed?: () => void;
36
+ }
37
+
38
+ type Outcome = "won" | "lost";
39
+
40
+ export function CloseDealDialog({ deal, stages, open, onClose, onClosed }: CloseDealDialogProps) {
41
+ const [outcome, setOutcome] = useState<Outcome>("won");
42
+ const [stageId, setStageId] = useState("");
43
+ const [date, setDate] = useState(deal.close_date?.slice(0, 10) ?? localToday());
44
+ const [amount, setAmount] = useState(deal.amount != null ? String(deal.amount) : "");
45
+ const [reasonId, setReasonId] = useState("");
46
+ const [newReason, setNewReason] = useState("");
47
+ // Retry-safe: once the stage move succeeded, a retry only records the reason.
48
+ const [moved, setMoved] = useState(false);
49
+ const targets = stages.filter((s) => stageKind(s) === outcome);
50
+ const stage = targets.find((s) => s.id === stageId) ?? targets[0];
51
+ const reasons = useG8Query(async (signal) => reasonsOf(await g8Get("/deals/close-reasons", { kind: outcome }, { signal })), [outcome], { enabled: open, key: ["deals", "close-reasons", outcome] });
52
+ const options = (reasons.data ?? []).filter((r) => !r.kind || r.kind === outcome);
53
+ const reason = options.find((r) => r.id === reasonId);
54
+
55
+ const addReason = useG8Mutation(async () => {
56
+ await g8Post("/deals/close-reasons", { kind: outcome, label: newReason.trim() });
57
+ const label = newReason.trim();
58
+ setNewReason("");
59
+ return label;
60
+ }, { invalidates: [["deals", "close-reasons"]], onSuccess: (label) => { void reasons.refetch().then((r) => { const found = r?.find((x) => x.label === label); if (found) setReasonId(found.id); }); } });
61
+
62
+ const close = useG8Mutation(async () => {
63
+ if (!moved) {
64
+ const body: Record<string, unknown> = { stage_id: stage?.id, close_date: date, expected_revision: deal.revision ?? undefined };
65
+ // Won always carries the amount (the API requires it); lost sends it only when it was changed.
66
+ if (amount !== "" && (outcome === "won" || Number(amount) !== deal.amount)) body.amount = Number(amount);
67
+ await g8Patch(`/deals/${encodeURIComponent(deal.id)}`, body);
68
+ setMoved(true);
69
+ }
70
+ if (reason) await g8Post(`/deals/${encodeURIComponent(deal.id)}/notes`, { content: `Closed ${outcome}. Reason: ${reason.label}` });
71
+ }, { invalidates: [["deals"]], onSuccess: () => { onClosed?.(); onClose(); } });
72
+
73
+ const valid = Boolean(stage) && Boolean(date) && (outcome !== "won" || amount !== "");
74
+ const marker = closedStageMarker(close.error);
75
+ return (
76
+ <Dialog open={open} onOpenChange={(o) => { if (!o && !close.loading) onClose(); }}>
77
+ <DialogContent>
78
+ <DialogTitle>Close "{deal.name ?? "this deal"}"</DialogTitle>
79
+ <DialogDescription>Moves the deal to a closed stage. The close date is required, and the amount for a won deal.</DialogDescription>
80
+ <form className="space-y-3" onSubmit={(e) => { e.preventDefault(); if (valid) void close.mutate(undefined); }}>
81
+ <div className="space-y-1">
82
+ <Label htmlFor="close-outcome">Outcome</Label>
83
+ <select id="close-outcome" className="h-9 w-full rounded-md border bg-background px-2 text-sm" disabled={moved} value={outcome} onChange={(e) => { setOutcome(e.target.value as Outcome); setStageId(""); setReasonId(""); }}>
84
+ <option value="won">Won</option>
85
+ <option value="lost">Lost</option>
86
+ </select>
87
+ </div>
88
+ {targets.length === 0 ? <p className="text-sm text-muted-foreground">This pipeline has no {outcome} stage. Add one under Pipeline settings (stage type "Closed {outcome}").</p> : null}
89
+ {targets.length > 1 ? (
90
+ <div className="space-y-1">
91
+ <Label htmlFor="close-stage">Stage</Label>
92
+ <select id="close-stage" className="h-9 w-full rounded-md border bg-background px-2 text-sm" disabled={moved} value={stage?.id ?? ""} onChange={(e) => setStageId(e.target.value)}>
93
+ {targets.map((s) => <option key={s.id} value={s.id}>{s.name}</option>)}
94
+ </select>
95
+ </div>
96
+ ) : null}
97
+ <div className="grid gap-3 sm:grid-cols-2">
98
+ <div className="space-y-1"><Label htmlFor="close-date">Close date</Label><Input id="close-date" type="date" disabled={moved} value={date} onChange={(e) => setDate(e.target.value)} /></div>
99
+ <div className="space-y-1"><Label htmlFor="close-amount">Amount{outcome === "won" ? "" : " (optional)"}</Label><Input id="close-amount" type="number" min={0} disabled={moved} value={amount} onChange={(e) => setAmount(e.target.value)} /></div>
100
+ </div>
101
+ <div className="space-y-1">
102
+ <Label htmlFor="close-reason">{outcome === "won" ? "Why did you win?" : "Why was it lost?"}</Label>
103
+ <select id="close-reason" className="h-9 w-full rounded-md border bg-background px-2 text-sm" value={reasonId} onChange={(e) => setReasonId(e.target.value)}>
104
+ <option value="">{reasons.loading ? "Loading reasons" : "No reason"}</option>
105
+ {options.map((r) => <option key={r.id} value={r.id}>{r.label}</option>)}
106
+ </select>
107
+ {reasons.error ? <ApiError error={reasons.error} compact title="Could not load reasons" onRetry={() => void reasons.refetch()} /> : null}
108
+ <div className="flex gap-2">
109
+ <Input aria-label="New reason" placeholder="Add a reason" value={newReason} onChange={(e) => setNewReason(e.target.value)} />
110
+ <Button type="button" variant="outline" loading={addReason.loading} disabled={!newReason.trim()} onClick={() => void addReason.mutate(undefined)}>Add reason</Button>
111
+ </div>
112
+ {addReason.error ? <ApiError error={addReason.error} title="Could not add the reason" /> : null}
113
+ </div>
114
+ {moved && close.error ? <p role="status" className="text-sm text-muted-foreground">The deal is closed, but the reason was not saved. Try again to save only the reason.</p> : null}
115
+ {close.error ? <ApiError error={marker ? new Error(`The API needs more before this deal can close (${marker.replace(/_/g, " ")}).`) : close.error} title={moved ? "Could not save the reason" : "Could not close the deal"} /> : null}
116
+ <div className="flex justify-end gap-2">
117
+ <Button type="button" variant="outline" disabled={close.loading} onClick={onClose}>Cancel</Button>
118
+ <Button type="submit" loading={close.loading} disabled={!valid}>{moved ? "Save reason" : `Close as ${outcome}`}</Button>
119
+ </div>
120
+ </form>
121
+ </DialogContent>
122
+ </Dialog>
123
+ );
124
+ }
125
+
126
+ export interface DealActionsProps {
127
+ deal: Deal;
128
+ pipelines: Pipeline[];
129
+ onChanged?: () => void;
130
+ }
131
+
132
+ export function DealActions({ deal, pipelines, onChanged }: DealActionsProps) {
133
+ const [closing, setClosing] = useState(false);
134
+ const [moving, setMoving] = useState(false);
135
+ const [target, setTarget] = useState("");
136
+ const current = pipelines.find((p) => p.id === deal.pipeline_id);
137
+ const stages = current?.stages ?? [];
138
+ const isOpen = stageKind(stages.find((s) => s.id === deal.stage_id)) === "open";
139
+ const others = pipelines.filter((p) => p.id !== deal.pipeline_id);
140
+ const move = useG8Mutation(() => g8Put(`/deals/${encodeURIComponent(deal.id)}/pipeline`, { target_pipeline_id: target }), {
141
+ invalidates: [["deals"]],
142
+ onSuccess: () => { setMoving(false); setTarget(""); onChanged?.(); },
143
+ });
144
+ return (
145
+ <>
146
+ {isOpen && stages.length > 0 ? <Button size="sm" onClick={() => setClosing(true)}>Close deal</Button> : null}
147
+ {others.length > 0 ? <Button size="sm" variant="outline" onClick={() => setMoving(true)}>Move to pipeline</Button> : null}
148
+ {closing ? <CloseDealDialog key={deal.id} deal={deal} stages={stages} open onClose={() => setClosing(false)} onClosed={onChanged} /> : null}
149
+ <Dialog open={moving} onOpenChange={(o) => { if (!o && !move.loading) setMoving(false); }}>
150
+ <DialogContent>
151
+ <DialogTitle>Move "{deal.name ?? "this deal"}" to another pipeline</DialogTitle>
152
+ <DialogDescription>The API places it in a stage of the pipeline you pick; check the board afterwards.</DialogDescription>
153
+ <div className="space-y-1">
154
+ <Label htmlFor="move-pipeline">Pipeline</Label>
155
+ <select id="move-pipeline" className="h-9 w-full rounded-md border bg-background px-2 text-sm" value={target} onChange={(e) => setTarget(e.target.value)}>
156
+ <option value="">Choose a pipeline</option>
157
+ {others.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
158
+ </select>
159
+ </div>
160
+ {move.error ? <ApiError error={move.error} title="Could not move the deal" /> : null}
161
+ <div className="flex justify-end gap-2">
162
+ <Button type="button" variant="outline" disabled={move.loading} onClick={() => setMoving(false)}>Cancel</Button>
163
+ <Button type="button" loading={move.loading} disabled={!target} onClick={() => void move.mutate(undefined)}>Move deal</Button>
164
+ </div>
165
+ </DialogContent>
166
+ </Dialog>
167
+ </>
168
+ );
169
+ }