@graph8/sdk 0.370.0 → 0.374.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 (646) hide show
  1. package/README.md +22 -3
  2. package/blocks/_foundation/README.md +61 -0
  3. package/blocks/_foundation/block.json +57 -0
  4. package/blocks/_foundation/components/auth-gate.tsx +363 -0
  5. package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
  6. package/blocks/_foundation/components/empty-state.tsx +5 -2
  7. package/blocks/_foundation/components/list-select.tsx +112 -0
  8. package/blocks/_foundation/components/paging-notices.tsx +55 -0
  9. package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
  10. package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
  11. package/blocks/_foundation/components/screen/index.ts +1 -0
  12. package/blocks/_foundation/components/screen/split-view.tsx +1 -1
  13. package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
  14. package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
  15. package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
  16. package/blocks/_foundation/lib/paging.ts +57 -0
  17. package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
  18. package/blocks/_foundation/server/auth.ts +815 -0
  19. package/blocks/agents/README.md +81 -4
  20. package/blocks/agents/block.json +311 -2
  21. package/blocks/agents/components/agent-activity.tsx +112 -0
  22. package/blocks/agents/components/agent-approvals.tsx +135 -0
  23. package/blocks/agents/components/agent-builder.tsx +17 -3
  24. package/blocks/agents/components/agent-conversations.tsx +143 -0
  25. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
  26. package/blocks/agents/components/agent-operators.tsx +164 -0
  27. package/blocks/agents/components/agent-run-detail.tsx +20 -3
  28. package/blocks/agents/components/agent-runs.tsx +3 -3
  29. package/blocks/agents/components/agent-skills-panel.tsx +7 -7
  30. package/blocks/agents/components/agent-test-history.tsx +121 -0
  31. package/blocks/agents/components/agents-screen.tsx +53 -11
  32. package/blocks/agents/components/assistant-sessions.tsx +150 -0
  33. package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
  34. package/blocks/agents/components/from-template-button.tsx +65 -0
  35. package/blocks/agents/components/knowledge-browser.tsx +107 -0
  36. package/blocks/agents/components/knowledge-detail.tsx +156 -0
  37. package/blocks/agents/components/knowledge-form.tsx +69 -0
  38. package/blocks/agents/components/name-dialog.tsx +32 -0
  39. package/blocks/agents/components/receptionist-button.tsx +46 -0
  40. package/blocks/agents/components/save-as-template-button.tsx +32 -0
  41. package/blocks/agents/components/skill-editor.tsx +147 -0
  42. package/blocks/agents/components/skill-library.tsx +111 -0
  43. package/blocks/agents/lib/agent-types.ts +7 -1
  44. package/blocks/agents/lib/facts.tsx +13 -0
  45. package/blocks/agents/lib/knowledge-utils.ts +49 -0
  46. package/blocks/agents/lib/load-more.tsx +141 -0
  47. package/blocks/agents/lib/skill-utils.ts +91 -0
  48. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
  49. package/blocks/analytics/components/analytics-screen.tsx +16 -3
  50. package/blocks/analytics/components/kpi-tiles.tsx +1 -1
  51. package/blocks/analytics/components/report-builder.tsx +15 -2
  52. package/blocks/appointments/README.md +6 -0
  53. package/blocks/appointments/block.json +27 -0
  54. package/blocks/appointments/components/booking-page.tsx +2 -2
  55. package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
  56. package/blocks/appointments/components/meeting-detail.tsx +2 -2
  57. package/blocks/appointments/components/meeting-list.tsx +29 -13
  58. package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
  59. package/blocks/campaigns/README.md +1 -1
  60. package/blocks/campaigns/block.json +34 -1
  61. package/blocks/campaigns/components/campaign-assets.tsx +1 -1
  62. package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
  63. package/blocks/campaigns/components/campaign-links.tsx +180 -0
  64. package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
  65. package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
  66. package/blocks/campaigns/components/icp-detail.tsx +11 -2
  67. package/blocks/campaigns/components/icp-editor.tsx +8 -3
  68. package/blocks/campaigns/components/persona-detail.tsx +11 -2
  69. package/blocks/campaigns/components/persona-editor.tsx +6 -1
  70. package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
  71. package/blocks/coach/components/call-scorecard.tsx +7 -7
  72. package/blocks/company-intelligence/README.md +132 -0
  73. package/blocks/company-intelligence/block.json +318 -0
  74. package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
  75. package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
  76. package/blocks/company-intelligence/components/company-profile.tsx +68 -0
  77. package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
  78. package/blocks/company-intelligence/components/report-detail.tsx +189 -0
  79. package/blocks/company-intelligence/components/research-reports.tsx +136 -0
  80. package/blocks/company-intelligence/components/research-run.tsx +165 -0
  81. package/blocks/company-intelligence/components/source-detail.tsx +117 -0
  82. package/blocks/company-intelligence/components/source-view.tsx +274 -0
  83. package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
  84. package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
  85. package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
  86. package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
  87. package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
  88. package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
  89. package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
  90. package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
  91. package/blocks/copilot/components/copilot-memory.tsx +6 -2
  92. package/blocks/copilot/components/copilot-panel.tsx +1 -1
  93. package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
  94. package/blocks/crm/README.md +25 -0
  95. package/blocks/crm/block.json +257 -9
  96. package/blocks/crm/components/bulk-update.tsx +119 -0
  97. package/blocks/crm/components/companies-screen.tsx +23 -9
  98. package/blocks/crm/components/company-detail.tsx +2 -2
  99. package/blocks/crm/components/contact-detail.tsx +4 -4
  100. package/blocks/crm/components/contact-documents-tab.tsx +144 -0
  101. package/blocks/crm/components/contact-insights-tab.tsx +118 -0
  102. package/blocks/crm/components/contact-lookup-link.tsx +85 -0
  103. package/blocks/crm/components/contact-row-actions.tsx +43 -0
  104. package/blocks/crm/components/contact-tag-chips.tsx +113 -0
  105. package/blocks/crm/components/contacts-screen.tsx +77 -18
  106. package/blocks/crm/components/dedupe-review.tsx +6 -1
  107. package/blocks/crm/components/import-wizard.tsx +3 -7
  108. package/blocks/crm/components/list-select.tsx +20 -0
  109. package/blocks/crm/components/owner-assign.tsx +132 -0
  110. package/blocks/crm/components/record-engagement-tab.tsx +103 -0
  111. package/blocks/crm/components/record-form.tsx +1 -1
  112. package/blocks/crm/components/record-open-links.tsx +79 -0
  113. package/blocks/crm/components/tag-assign.tsx +112 -0
  114. package/blocks/crm/components/tag-manager.tsx +178 -0
  115. package/blocks/crm/components/tasks-screen.tsx +35 -15
  116. package/blocks/crm/components/unlock-info-button.tsx +82 -0
  117. package/blocks/crm/components/verify-email-button.tsx +93 -0
  118. package/blocks/crm/hooks/use-all-lists.ts +18 -0
  119. package/blocks/crm/hooks/use-contact-tags.ts +29 -0
  120. package/blocks/crm/hooks/use-page-query.ts +3 -1
  121. package/blocks/crm/lib/crm-types.ts +14 -0
  122. package/blocks/crm/lib/read-lists.ts +16 -0
  123. package/blocks/crm/lib/record-actions.ts +102 -0
  124. package/blocks/deals/README.md +25 -6
  125. package/blocks/deals/block.json +168 -2
  126. package/blocks/deals/components/close-deal-dialog.tsx +169 -0
  127. package/blocks/deals/components/create-deal-button.tsx +109 -0
  128. package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
  129. package/blocks/deals/components/deal-detail.tsx +7 -5
  130. package/blocks/deals/components/deal-filters.tsx +65 -0
  131. package/blocks/deals/components/deal-line-items.tsx +120 -0
  132. package/blocks/deals/components/deal-notes-tab.tsx +97 -0
  133. package/blocks/deals/components/deal-related.tsx +30 -9
  134. package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
  135. package/blocks/deals/components/deals-screen.tsx +57 -22
  136. package/blocks/deals/components/forecast-view.tsx +4 -4
  137. package/blocks/deals/components/pipeline-board.tsx +8 -6
  138. package/blocks/deals/components/pipeline-editor.tsx +132 -0
  139. package/blocks/deals/components/record-deals-tab.tsx +62 -0
  140. package/blocks/deals/components/stage-editor.tsx +1 -1
  141. package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
  142. package/blocks/deals/lib/deal-extras.ts +156 -0
  143. package/blocks/dialer/README.md +14 -5
  144. package/blocks/dialer/block.json +17 -2
  145. package/blocks/dialer/components/call-contact-button.tsx +26 -9
  146. package/blocks/dialer/components/call-log-screen.tsx +10 -2
  147. package/blocks/dialer/components/call-log.tsx +16 -4
  148. package/blocks/dialer/components/call-queue.tsx +3 -3
  149. package/blocks/dialer/components/dialer-screen.tsx +69 -27
  150. package/blocks/dialer/components/disposition-panel.tsx +1 -1
  151. package/blocks/dialer/components/record-calls-tab.tsx +101 -0
  152. package/blocks/dialer/components/session-start-form.tsx +58 -11
  153. package/blocks/dialer/components/softphone.tsx +1 -1
  154. package/blocks/documents/README.md +86 -0
  155. package/blocks/documents/block.json +378 -0
  156. package/blocks/documents/components/account-bar.tsx +48 -0
  157. package/blocks/documents/components/correspondence-form.tsx +95 -0
  158. package/blocks/documents/components/correspondence-import.tsx +106 -0
  159. package/blocks/documents/components/correspondence-panel.tsx +191 -0
  160. package/blocks/documents/components/documents-screen.tsx +76 -0
  161. package/blocks/documents/components/library-document-detail.tsx +175 -0
  162. package/blocks/documents/components/library-document-form.tsx +105 -0
  163. package/blocks/documents/components/library-folder-form.tsx +74 -0
  164. package/blocks/documents/components/library-panel.tsx +235 -0
  165. package/blocks/documents/components/library-upload.tsx +160 -0
  166. package/blocks/documents/components/question-row.tsx +130 -0
  167. package/blocks/documents/components/questionnaire-detail.tsx +197 -0
  168. package/blocks/documents/components/questionnaires-panel.tsx +173 -0
  169. package/blocks/documents/components/record-documents-tab.tsx +109 -0
  170. package/blocks/documents/lib/documents-types.ts +252 -0
  171. package/blocks/documents/lib/download.ts +64 -0
  172. package/blocks/documents/recipes/follow-a-questionnaire.ts +26 -0
  173. package/blocks/documents/recipes/log-account-correspondence.ts +27 -0
  174. package/blocks/documents/recipes/organize-the-library.ts +25 -0
  175. package/blocks/email-signatures/README.md +123 -0
  176. package/blocks/email-signatures/block.json +341 -0
  177. package/blocks/email-signatures/components/email-signatures-screen.tsx +166 -0
  178. package/blocks/email-signatures/components/signature-assign-template.tsx +119 -0
  179. package/blocks/email-signatures/components/signature-block-editor.tsx +87 -0
  180. package/blocks/email-signatures/components/signature-campaign-form.tsx +165 -0
  181. package/blocks/email-signatures/components/signature-campaigns.tsx +75 -0
  182. package/blocks/email-signatures/components/signature-identity-form.tsx +131 -0
  183. package/blocks/email-signatures/components/signature-mailboxes.tsx +195 -0
  184. package/blocks/email-signatures/components/signature-policy.tsx +130 -0
  185. package/blocks/email-signatures/components/signature-preview.tsx +119 -0
  186. package/blocks/email-signatures/components/signature-report.tsx +77 -0
  187. package/blocks/email-signatures/components/signature-template-details.tsx +112 -0
  188. package/blocks/email-signatures/components/signature-template-form.tsx +109 -0
  189. package/blocks/email-signatures/hooks/use-signatures-data.ts +93 -0
  190. package/blocks/email-signatures/lib/signatures-types.ts +392 -0
  191. package/blocks/email-signatures/recipes/assign-a-signature-to-a-new-hire.ts +32 -0
  192. package/blocks/email-signatures/recipes/create-a-template-and-make-it-default.ts +35 -0
  193. package/blocks/email-signatures/recipes/signature-engagement-report.ts +16 -0
  194. package/blocks/enrichment/README.md +38 -10
  195. package/blocks/enrichment/block.json +164 -12
  196. package/blocks/enrichment/components/auto-enrichment-configs.tsx +134 -0
  197. package/blocks/enrichment/components/auto-enrichment-settings.tsx +82 -0
  198. package/blocks/enrichment/components/bulk-enrich-panel.tsx +79 -30
  199. package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
  200. package/blocks/enrichment/components/enrich-button.tsx +1 -1
  201. package/blocks/enrichment/components/enrichment-activity.tsx +93 -0
  202. package/blocks/enrichment/components/enrichment-job-detail.tsx +18 -0
  203. package/blocks/enrichment/components/enrichment-screen.tsx +103 -19
  204. package/blocks/enrichment/components/list-pipelines.tsx +97 -0
  205. package/blocks/enrichment/components/pipeline-detail.tsx +214 -0
  206. package/blocks/enrichment/components/pipeline-editor.tsx +144 -0
  207. package/blocks/enrichment/components/pipeline-templates.tsx +90 -0
  208. package/blocks/enrichment/components/waterfall-config.tsx +27 -7
  209. package/blocks/enrichment/lib/enrichment-types.ts +51 -0
  210. package/blocks/enrichment/lib/pipeline-types.ts +234 -0
  211. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
  212. package/blocks/forms/README.md +10 -0
  213. package/blocks/forms/block.json +107 -0
  214. package/blocks/forms/components/form-activity.tsx +71 -0
  215. package/blocks/forms/components/form-backfill.tsx +108 -0
  216. package/blocks/forms/components/form-builder.tsx +1 -1
  217. package/blocks/forms/components/form-embed.tsx +1 -1
  218. package/blocks/forms/components/form-groups.tsx +151 -0
  219. package/blocks/forms/components/form-mapping-editor.tsx +6 -9
  220. package/blocks/forms/components/form-route-history.tsx +72 -0
  221. package/blocks/forms/components/form-saved-views.tsx +81 -0
  222. package/blocks/forms/components/forms-screen.tsx +57 -20
  223. package/blocks/forms/components/more-rows.tsx +30 -0
  224. package/blocks/forms/components/submissions-export.tsx +67 -0
  225. package/blocks/forms/components/submissions-table.tsx +15 -6
  226. package/blocks/forms/lib/forms-extra.ts +67 -0
  227. package/blocks/inbox/README.md +45 -4
  228. package/blocks/inbox/block.json +145 -8
  229. package/blocks/inbox/components/blocked-senders.tsx +103 -0
  230. package/blocks/inbox/components/compose-message.tsx +193 -0
  231. package/blocks/inbox/components/inbox-screen.tsx +54 -6
  232. package/blocks/inbox/components/inbox-search.tsx +106 -0
  233. package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
  234. package/blocks/inbox/components/tag-manager.tsx +136 -0
  235. package/blocks/inbox/components/thread-actions.tsx +133 -0
  236. package/blocks/inbox/components/thread-badge.tsx +2 -2
  237. package/blocks/inbox/components/thread-view.tsx +3 -1
  238. package/blocks/inbox/hooks/use-inbox-mailboxes.ts +31 -0
  239. package/blocks/inbox/lib/inbox-manage.ts +227 -0
  240. package/blocks/integrations/README.md +54 -1
  241. package/blocks/integrations/block.json +189 -15
  242. package/blocks/integrations/components/ad-accounts.tsx +82 -0
  243. package/blocks/integrations/components/connection-health.tsx +59 -0
  244. package/blocks/integrations/components/crm-detail.tsx +20 -1
  245. package/blocks/integrations/components/crm-records.tsx +103 -0
  246. package/blocks/integrations/components/field-mappings.tsx +61 -0
  247. package/blocks/integrations/components/integration-list.tsx +4 -4
  248. package/blocks/integrations/components/integrations-screen.tsx +20 -3
  249. package/blocks/integrations/components/migration-panel.tsx +133 -0
  250. package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
  251. package/blocks/integrations/components/sync-status.tsx +6 -6
  252. package/blocks/integrations/components/sync-triggers.tsx +134 -0
  253. package/blocks/integrations/components/webhook-detail.tsx +26 -7
  254. package/blocks/integrations/components/webhook-manager.tsx +30 -17
  255. package/blocks/integrations/components/workspace-tools.tsx +127 -0
  256. package/blocks/integrations/lib/crm-utils.ts +39 -0
  257. package/blocks/integrations/lib/facts.tsx +13 -0
  258. package/blocks/integrations/lib/integrations-utils.ts +5 -0
  259. package/blocks/integrations/lib/load-more.tsx +141 -0
  260. package/blocks/intent/components/account-intent-score.tsx +5 -5
  261. package/blocks/intent/components/intent-feed.tsx +8 -8
  262. package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
  263. package/blocks/intent/lib/intent-utils.ts +4 -4
  264. package/blocks/lifecycle/components/account-related.tsx +8 -1
  265. package/blocks/lifecycle/components/churn-list.tsx +5 -2
  266. package/blocks/lifecycle/components/customer-table.tsx +1 -1
  267. package/blocks/lists/README.md +7 -1
  268. package/blocks/lists/block.json +31 -3
  269. package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
  270. package/blocks/lists/components/add-to-list-button.tsx +17 -7
  271. package/blocks/lists/components/list-picker.tsx +13 -3
  272. package/blocks/lists/components/list-table.tsx +1 -1
  273. package/blocks/lists/components/lists-screen.tsx +4 -2
  274. package/blocks/lists/components/record-lists-tab.tsx +99 -0
  275. package/blocks/lists/components/suppression-manager.tsx +27 -6
  276. package/blocks/lists/components/suppressions-screen.tsx +21 -12
  277. package/blocks/mailboxes/block.json +4 -0
  278. package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
  279. package/blocks/mailboxes/components/domain-detail.tsx +10 -4
  280. package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
  281. package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
  282. package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
  283. package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
  284. package/blocks/mailboxes/components/warmup-health.tsx +20 -20
  285. package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
  286. package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
  287. package/blocks/newsletter/README.md +12 -0
  288. package/blocks/newsletter/block.json +117 -0
  289. package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
  290. package/blocks/newsletter/components/issue-versions.tsx +93 -0
  291. package/blocks/newsletter/components/more-rows.tsx +30 -0
  292. package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
  293. package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
  294. package/blocks/newsletter/components/newsletter-screen.tsx +38 -13
  295. package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
  296. package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
  297. package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
  298. package/blocks/newsletter/components/subscriber-table.tsx +5 -7
  299. package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
  300. package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
  301. package/blocks/nurture/components/nurture-builder.tsx +10 -2
  302. package/blocks/nurture/components/nurture-enroll.tsx +6 -7
  303. package/blocks/nurture/components/nurture-form.tsx +5 -8
  304. package/blocks/nurture/components/nurture-screen.tsx +16 -5
  305. package/blocks/nurture/components/nurture-stats.tsx +5 -4
  306. package/blocks/objects/README.md +1 -1
  307. package/blocks/objects/block.json +13 -5
  308. package/blocks/objects/components/attribute-field-input.tsx +14 -1
  309. package/blocks/objects/components/object-record-detail.tsx +8 -1
  310. package/blocks/objects/components/object-schema-editor.tsx +1 -1
  311. package/blocks/objects/components/record-roles-panel.tsx +15 -5
  312. package/blocks/pages/README.md +14 -0
  313. package/blocks/pages/block.json +123 -2
  314. package/blocks/pages/components/more-rows.tsx +30 -0
  315. package/blocks/pages/components/page-ai-tools.tsx +196 -0
  316. package/blocks/pages/components/page-analytics.tsx +176 -0
  317. package/blocks/pages/components/page-clone.tsx +99 -0
  318. package/blocks/pages/components/page-domains.tsx +167 -0
  319. package/blocks/pages/components/page-editor.tsx +1 -1
  320. package/blocks/pages/components/page-generation-status.tsx +48 -0
  321. package/blocks/pages/components/page-tracking.tsx +137 -0
  322. package/blocks/pages/components/pages-screen.tsx +57 -16
  323. package/blocks/pages/components/seo-report.tsx +6 -1
  324. package/blocks/pages/hooks/use-landing-page-paths.ts +60 -0
  325. package/blocks/pages/lib/studio.ts +7 -0
  326. package/blocks/phone-numbers/README.md +74 -0
  327. package/blocks/phone-numbers/block.json +262 -0
  328. package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
  329. package/blocks/phone-numbers/components/assign-number.tsx +104 -0
  330. package/blocks/phone-numbers/components/number-search.tsx +239 -0
  331. package/blocks/phone-numbers/components/number-settings.tsx +134 -0
  332. package/blocks/phone-numbers/components/number-usage.tsx +97 -0
  333. package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
  334. package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
  335. package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
  336. package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
  337. package/blocks/phone-numbers/components/release-number.tsx +126 -0
  338. package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
  339. package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
  340. package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
  341. package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
  342. package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
  343. package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
  344. package/blocks/pickers/README.md +23 -0
  345. package/blocks/pickers/block.json +103 -0
  346. package/blocks/pickers/components/company-picker.tsx +90 -0
  347. package/blocks/pickers/components/contact-picker.tsx +131 -0
  348. package/blocks/pickers/components/member-picker.tsx +99 -0
  349. package/blocks/pickers/components/search-picker.tsx +98 -0
  350. package/blocks/pickers/hooks/use-current-member.ts +104 -0
  351. package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
  352. package/blocks/pickers/lib/use-debounced.ts +13 -0
  353. package/blocks/programmatic-pages/README.md +82 -0
  354. package/blocks/programmatic-pages/block.json +292 -0
  355. package/blocks/programmatic-pages/components/programmatic-campaign-form.tsx +136 -0
  356. package/blocks/programmatic-pages/components/programmatic-generate.tsx +112 -0
  357. package/blocks/programmatic-pages/components/programmatic-generated-pages.tsx +210 -0
  358. package/blocks/programmatic-pages/components/programmatic-overview.tsx +84 -0
  359. package/blocks/programmatic-pages/components/programmatic-pages-screen.tsx +221 -0
  360. package/blocks/programmatic-pages/components/programmatic-preview.tsx +66 -0
  361. package/blocks/programmatic-pages/components/programmatic-publish.tsx +102 -0
  362. package/blocks/programmatic-pages/components/programmatic-settings.tsx +96 -0
  363. package/blocks/programmatic-pages/components/programmatic-source-fields.tsx +75 -0
  364. package/blocks/programmatic-pages/components/programmatic-source.tsx +106 -0
  365. package/blocks/programmatic-pages/components/programmatic-variables.tsx +84 -0
  366. package/blocks/programmatic-pages/hooks/use-programmatic-workspace.ts +19 -0
  367. package/blocks/programmatic-pages/lib/programmatic-types.ts +330 -0
  368. package/blocks/programmatic-pages/recipes/create-a-programmatic-campaign.ts +23 -0
  369. package/blocks/programmatic-pages/recipes/render-and-follow.ts +21 -0
  370. package/blocks/programmatic-pages/recipes/review-then-publish.ts +17 -0
  371. package/blocks/quotes/README.md +38 -1
  372. package/blocks/quotes/block.json +170 -6
  373. package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
  374. package/blocks/quotes/components/manual-products.tsx +144 -0
  375. package/blocks/quotes/components/quote-batch-actions.tsx +86 -0
  376. package/blocks/quotes/components/quote-builder.tsx +1 -1
  377. package/blocks/quotes/components/quote-documents.tsx +94 -0
  378. package/blocks/quotes/components/quote-evidence.tsx +95 -0
  379. package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
  380. package/blocks/quotes/components/quote-list.tsx +28 -8
  381. package/blocks/quotes/components/quote-related.tsx +3 -2
  382. package/blocks/quotes/components/quote-settings.tsx +184 -0
  383. package/blocks/quotes/components/quote-templates.tsx +189 -0
  384. package/blocks/quotes/components/quote-viewer.tsx +23 -5
  385. package/blocks/quotes/components/quotes-screen.tsx +15 -5
  386. package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
  387. package/blocks/quotes/lib/quote-admin.ts +264 -0
  388. package/blocks/radar/components/competitor-battle-card.tsx +1 -1
  389. package/blocks/radar/components/radar-overview.tsx +4 -4
  390. package/blocks/radar/components/radar-screen.tsx +2 -2
  391. package/blocks/sequencer/README.md +61 -0
  392. package/blocks/sequencer/block.json +200 -6
  393. package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
  394. package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
  395. package/blocks/sequencer/components/enrollment-withdraw.tsx +71 -0
  396. package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
  397. package/blocks/sequencer/components/sequence-builder.tsx +32 -23
  398. package/blocks/sequencer/components/sequence-campaign-link.tsx +78 -0
  399. package/blocks/sequencer/components/sequence-detail.tsx +10 -2
  400. package/blocks/sequencer/components/sequence-edit-form.tsx +111 -0
  401. package/blocks/sequencer/components/sequence-enrollments.tsx +5 -2
  402. package/blocks/sequencer/components/sequence-list.tsx +1 -1
  403. package/blocks/sequencer/components/sequence-manage-actions.tsx +119 -0
  404. package/blocks/sequencer/components/sequence-senders-panel.tsx +84 -0
  405. package/blocks/sequencer/components/sequence-stats.tsx +8 -8
  406. package/blocks/sequencer/components/sequence-templates.tsx +160 -0
  407. package/blocks/sequencer/components/sequences-screen.tsx +39 -6
  408. package/blocks/sequencer/components/step-ai-assist.tsx +196 -0
  409. package/blocks/sequencer/components/step-editor.tsx +21 -12
  410. package/blocks/sequencer/components/step-templates.tsx +224 -0
  411. package/blocks/sequencer/lib/sequencer-manage.ts +235 -0
  412. package/blocks/sequencer/lib/sequencer-utils.ts +9 -5
  413. package/blocks/signals/README.md +39 -0
  414. package/blocks/signals/block.json +287 -0
  415. package/blocks/signals/components/auto-seed-panel.tsx +111 -0
  416. package/blocks/signals/components/listener-form.tsx +184 -0
  417. package/blocks/signals/components/matched-posts.tsx +195 -0
  418. package/blocks/signals/components/post-engagements.tsx +114 -0
  419. package/blocks/signals/components/record-social-tab.tsx +49 -0
  420. package/blocks/signals/components/social-listeners-screen.tsx +250 -0
  421. package/blocks/signals/components/social-settings.tsx +128 -0
  422. package/blocks/signals/components/source-lists.tsx +83 -0
  423. package/blocks/signals/components/top-engagers.tsx +68 -0
  424. package/blocks/signals/components/x-integration.tsx +129 -0
  425. package/blocks/signals/lib/signals-types.ts +228 -0
  426. package/blocks/signals/recipes/preview-and-create-a-listener.ts +16 -0
  427. package/blocks/signals/recipes/spend-against-caps.ts +14 -0
  428. package/blocks/signals/recipes/triage-matched-posts.ts +18 -0
  429. package/blocks/social-ads/README.md +8 -0
  430. package/blocks/social-ads/block.json +137 -4
  431. package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
  432. package/blocks/social-ads/components/audience-sync.tsx +6 -2
  433. package/blocks/social-ads/components/campaign-ads.tsx +166 -0
  434. package/blocks/social-ads/components/campaign-detail.tsx +6 -1
  435. package/blocks/social-ads/components/campaign-editor.tsx +134 -0
  436. package/blocks/social-ads/components/creative-concepts.tsx +74 -0
  437. package/blocks/social-ads/components/creative-detail.tsx +6 -1
  438. package/blocks/social-ads/components/creative-gallery.tsx +20 -2
  439. package/blocks/social-ads/components/creative-revisions.tsx +57 -0
  440. package/blocks/social-ads/components/creative-tools.tsx +176 -0
  441. package/blocks/social-ads/components/post-composer.tsx +4 -2
  442. package/blocks/social-ads/components/post-detail.tsx +3 -0
  443. package/blocks/social-ads/components/social-ideas.tsx +64 -0
  444. package/blocks/social-ads/components/social-performance.tsx +80 -0
  445. package/blocks/social-ads/components/social-screen.tsx +33 -6
  446. package/blocks/social-ads/lib/ads-extra.ts +67 -0
  447. package/blocks/surveys/README.md +91 -0
  448. package/blocks/surveys/block.json +299 -0
  449. package/blocks/surveys/components/account-nps.tsx +69 -0
  450. package/blocks/surveys/components/contact-survey-history.tsx +52 -0
  451. package/blocks/surveys/components/org-summary.tsx +82 -0
  452. package/blocks/surveys/components/sending-domains.tsx +113 -0
  453. package/blocks/surveys/components/survey-form.tsx +126 -0
  454. package/blocks/surveys/components/survey-responses.tsx +145 -0
  455. package/blocks/surveys/components/survey-results.tsx +72 -0
  456. package/blocks/surveys/components/survey-send-history.tsx +58 -0
  457. package/blocks/surveys/components/survey-send.tsx +162 -0
  458. package/blocks/surveys/components/survey-triggers.tsx +166 -0
  459. package/blocks/surveys/components/surveys-screen.tsx +234 -0
  460. package/blocks/surveys/lib/surveys-types.ts +228 -0
  461. package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
  462. package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
  463. package/blocks/surveys/recipes/survey-score-report.ts +27 -0
  464. package/blocks/visitors/components/snippet-install.tsx +18 -9
  465. package/blocks/visitors/components/traffic-chart.tsx +7 -6
  466. package/blocks/visitors/components/visitor-company-card.tsx +24 -14
  467. package/blocks/visitors/components/visitor-feed.tsx +5 -5
  468. package/blocks/web-analytics/README.md +102 -0
  469. package/blocks/web-analytics/block.json +292 -0
  470. package/blocks/web-analytics/components/acquisition-tab.tsx +154 -0
  471. package/blocks/web-analytics/components/analytics-filters.tsx +161 -0
  472. package/blocks/web-analytics/components/audience-tab.tsx +135 -0
  473. package/blocks/web-analytics/components/conversions-tab.tsx +128 -0
  474. package/blocks/web-analytics/components/engagement-tab.tsx +123 -0
  475. package/blocks/web-analytics/components/overview-tab.tsx +102 -0
  476. package/blocks/web-analytics/components/pages-tab.tsx +148 -0
  477. package/blocks/web-analytics/components/ranked-table.tsx +103 -0
  478. package/blocks/web-analytics/components/realtime-tab.tsx +103 -0
  479. package/blocks/web-analytics/components/report-card.tsx +71 -0
  480. package/blocks/web-analytics/components/web-analytics-screen.tsx +90 -0
  481. package/blocks/web-analytics/hooks/use-report.ts +48 -0
  482. package/blocks/web-analytics/lib/analytics-scope.ts +155 -0
  483. package/blocks/web-analytics/lib/page-path.ts +14 -0
  484. package/blocks/web-analytics/lib/web-analytics-types.ts +187 -0
  485. package/blocks/web-analytics/recipes/best-landing-pages.ts +26 -0
  486. package/blocks/web-analytics/recipes/form-conversion-check.ts +19 -0
  487. package/blocks/web-analytics/recipes/weekly-traffic-report.ts +37 -0
  488. package/blocks/webchat/components/chat-widget-config.tsx +1 -1
  489. package/blocks/webchat/components/conversation-list.tsx +1 -1
  490. package/blocks/workbench/README.md +54 -2
  491. package/blocks/workbench/block.json +234 -5
  492. package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
  493. package/blocks/workbench/components/run-progress.tsx +1 -1
  494. package/blocks/workbench/components/workbench-auto-enrich.tsx +88 -0
  495. package/blocks/workbench/components/workbench-automation.tsx +36 -0
  496. package/blocks/workbench/components/workbench-connections.tsx +136 -0
  497. package/blocks/workbench/components/workbench-crm-import.tsx +91 -0
  498. package/blocks/workbench/components/workbench-datafile-import.tsx +154 -0
  499. package/blocks/workbench/components/workbench-grid.tsx +71 -8
  500. package/blocks/workbench/components/workbench-pipelines.tsx +173 -0
  501. package/blocks/workbench/components/workbench-row-actions.tsx +8 -1
  502. package/blocks/workbench/components/workbench-schedule.tsx +191 -0
  503. package/blocks/workbench/components/workbench-screen.tsx +65 -22
  504. package/blocks/workbench/components/workbench-send-to-sequence.tsx +116 -0
  505. package/blocks/workbench/components/workbench-table-list.tsx +14 -2
  506. package/blocks/workbench/components/workbench-table-settings.tsx +53 -0
  507. package/blocks/workbench/components/workbench-triggers.tsx +178 -0
  508. package/blocks/workbench/hooks/use-connected-crms.ts +29 -0
  509. package/blocks/workbench/lib/api-status.ts +8 -0
  510. package/blocks/workbench/lib/automation-types.ts +257 -0
  511. package/blocks/workflows/components/workflow-editor.tsx +1 -1
  512. package/dist/cli.js +744 -394
  513. package/dist/index.browser.js +1 -1
  514. package/dist/index.browser.js.map +1 -1
  515. package/dist/index.browser.mjs +1 -1
  516. package/dist/index.browser.mjs.map +1 -1
  517. package/dist/index.d.mts +2 -2
  518. package/dist/index.d.ts +2 -2
  519. package/dist/index.js +1 -1
  520. package/dist/index.js.map +1 -1
  521. package/dist/index.mjs +1 -1
  522. package/dist/index.mjs.map +1 -1
  523. package/dist/react.browser.js +1 -1
  524. package/dist/react.browser.js.map +1 -1
  525. package/dist/react.browser.mjs +1 -1
  526. package/dist/react.browser.mjs.map +1 -1
  527. package/dist/react.d.mts +2 -2
  528. package/dist/react.d.ts +2 -2
  529. package/dist/react.js +1 -1
  530. package/dist/react.js.map +1 -1
  531. package/dist/react.mjs +1 -1
  532. package/dist/react.mjs.map +1 -1
  533. package/dist/types/blocks/auth.d.ts +45 -0
  534. package/dist/types/blocks/brand.d.ts +11 -2
  535. package/dist/types/blocks/create.d.ts +11 -4
  536. package/dist/types/blocks/doctor.d.ts +1 -0
  537. package/dist/types/blocks/flows.d.ts +7 -1
  538. package/dist/types/blocks/orgBrand.d.ts +34 -0
  539. package/dist/types/cli.d.ts +3 -0
  540. package/dist/types/version.d.ts +2 -2
  541. package/docs/api/desk.d.ts +2 -2
  542. package/docs/api/desk.md +3 -3
  543. package/docs/api/forms.md +1 -1
  544. package/package.json +1 -1
  545. package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
  546. package/stacks/6sense/pages/contacts.tsx.tmpl +21 -0
  547. package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
  548. package/stacks/6sense.json +37 -4
  549. package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
  550. package/stacks/apollo/pages/campaigns.tsx.tmpl +27 -0
  551. package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
  552. package/stacks/apollo/pages/contacts.tsx.tmpl +35 -95
  553. package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
  554. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
  555. package/stacks/apollo/pages/lists.tsx.tmpl +24 -0
  556. package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
  557. package/stacks/apollo/pages/sequences.tsx.tmpl +18 -0
  558. package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
  559. package/stacks/apollo.json +68 -9
  560. package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
  561. package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
  562. package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
  563. package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
  564. package/stacks/calendly.json +8 -4
  565. package/stacks/clay/pages/companies.tsx.tmpl +17 -1
  566. package/stacks/clay/pages/contacts.tsx.tmpl +14 -0
  567. package/stacks/clay/pages/lists.tsx.tmpl +24 -0
  568. package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
  569. package/stacks/clay.json +16 -3
  570. package/stacks/gong/pages/calls.tsx.tmpl +23 -0
  571. package/stacks/gong/pages/companies.tsx.tmpl +22 -0
  572. package/stacks/gong/pages/contacts.tsx.tmpl +34 -0
  573. package/stacks/gong/pages/lists.tsx.tmpl +12 -0
  574. package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
  575. package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
  576. package/stacks/gong.json +23 -6
  577. package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
  578. package/stacks/hootsuite/pages/contacts.tsx.tmpl +30 -0
  579. package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
  580. package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
  581. package/stacks/hootsuite.json +19 -4
  582. package/stacks/hubspot/pages/campaigns.tsx.tmpl +30 -0
  583. package/stacks/hubspot/pages/companies.tsx.tmpl +16 -35
  584. package/stacks/hubspot/pages/contacts.tsx.tmpl +35 -95
  585. package/stacks/hubspot/pages/deals.tsx.tmpl +14 -1
  586. package/stacks/hubspot/pages/forms.tsx.tmpl +36 -0
  587. package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
  588. package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
  589. package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
  590. package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
  591. package/stacks/hubspot/pages/sequences.tsx.tmpl +18 -0
  592. package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
  593. package/stacks/hubspot/pages/web-analytics.tsx.tmpl +14 -0
  594. package/stacks/hubspot.json +66 -7
  595. package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
  596. package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
  597. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
  598. package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
  599. package/stacks/intercom.json +18 -4
  600. package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
  601. package/stacks/mailchimp/pages/contacts.tsx.tmpl +30 -0
  602. package/stacks/mailchimp/pages/forms.tsx.tmpl +36 -0
  603. package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
  604. package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
  605. package/stacks/mailchimp.json +22 -6
  606. package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
  607. package/stacks/outreach/pages/campaigns.tsx.tmpl +27 -0
  608. package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
  609. package/stacks/outreach/pages/contacts.tsx.tmpl +29 -66
  610. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
  611. package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
  612. package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
  613. package/stacks/outreach/pages/sequences.tsx.tmpl +18 -0
  614. package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
  615. package/stacks/outreach.json +38 -9
  616. package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
  617. package/stacks/pipedrive/pages/contacts.tsx.tmpl +17 -35
  618. package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
  619. package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
  620. package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
  621. package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
  622. package/stacks/pipedrive.json +14 -2
  623. package/stacks/salesforce/pages/companies.tsx.tmpl +16 -35
  624. package/stacks/salesforce/pages/contacts.tsx.tmpl +26 -35
  625. package/stacks/salesforce/pages/deals.tsx.tmpl +14 -1
  626. package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
  627. package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
  628. package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
  629. package/stacks/salesforce.json +17 -3
  630. package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
  631. package/stacks/zoominfo/pages/contacts.tsx.tmpl +16 -0
  632. package/stacks/zoominfo/pages/lists.tsx.tmpl +24 -0
  633. package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
  634. package/stacks/zoominfo.json +36 -3
  635. package/styles/styles.css +1 -1
  636. package/templates/auth/auth-route.ts.tmpl +16 -0
  637. package/templates/auth/g8.auth.anonymous.ts.tmpl +15 -0
  638. package/templates/auth/g8.auth.basic.ts.tmpl +17 -0
  639. package/templates/auth/g8.auth.jwt.ts.tmpl +23 -0
  640. package/templates/auth/g8.auth.password.ts.tmpl +18 -0
  641. package/templates/auth/g8.auth.session.ts.tmpl +23 -0
  642. package/templates/auth/middleware.ts.tmpl +14 -0
  643. package/templates/next-app/README.md.tmpl +5 -0
  644. package/templates/next-app/app/globals.css.tmpl +349 -64
  645. package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
  646. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
@@ -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
+ }
@@ -0,0 +1,109 @@
1
+ "use client";
2
+
3
+ /**
4
+ * CreateDealButton: "Create deal" for one contact. Opens a sheet with the deal form already filled in from the
5
+ * contact and their company: the deal is linked to the contact (the API derives the company from its contacts),
6
+ * named "<company> - <contact>", and owned by the API key's person (or a member you pick). Use it in
7
+ * `ContactsScreen.renderDetailActions`:
8
+ * <CreateDealButton contactId={c.id} onCreated={(dealId) => router.push(`/deals?id=${dealId}`)} />
9
+ *
10
+ * SDK operations, in order:
11
+ * 1. getContact GET /contacts/{contact_id} (read) the name and company shown and used as the default deal name
12
+ * 2. getMyTeamMember GET /team-members/me (read) the owner; listTeamMembers (read) when the key has no person
13
+ * 3. listPipelines GET /deals/pipelines (read) the stage picker
14
+ * 4. createDeal POST /deals (write) once, retry-safe (a second save edits the same deal)
15
+ * A company that already has a deal answers 409; the sheet then offers "Create another deal for this company".
16
+ * Nothing here is billable, external or destructive.
17
+ */
18
+
19
+ import { useState } from "react";
20
+ import { Button, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, Skeleton } from "@graph8/sdk/ui";
21
+ import { Handshake } from "lucide-react";
22
+ import { ApiError } from "@/g8/components/api-error";
23
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
24
+ import { g8Get } from "@/g8/lib/g8-client";
25
+ import { useDefaultOwner } from "../hooks/use-default-owner";
26
+ import { dataOf } from "../lib/deals-types";
27
+ import { DealDetail } from "./deal-detail";
28
+
29
+ export interface CreateDealButtonProps {
30
+ /** The CRM contact the deal is for. */
31
+ contactId: number;
32
+ /** Called with the new deal's id after it was created (route to your deals screen). */
33
+ onCreated?: (dealId: string) => void;
34
+ label?: string;
35
+ variant?: "default" | "outline" | "secondary" | "ghost";
36
+ size?: "default" | "sm";
37
+ }
38
+
39
+ interface ContactBits {
40
+ first_name?: string | null;
41
+ last_name?: string | null;
42
+ full_name?: string | null;
43
+ work_email?: string | null;
44
+ company_name?: string | null;
45
+ company?: { name?: string | null; domain?: string | null } | null;
46
+ }
47
+
48
+ export const contactLabel = (c: ContactBits | undefined | null, id: number): string =>
49
+ c?.full_name?.trim() || [c?.first_name, c?.last_name].filter(Boolean).join(" ").trim() || c?.work_email || `Contact ${id}`;
50
+ export const companyLabelOf = (c: ContactBits | undefined | null): string => (c?.company?.name || c?.company_name || "").trim();
51
+
52
+ function Form({ contactId, onCreated }: { contactId: number; onCreated?: (dealId: string) => void }) {
53
+ const owner = useDefaultOwner();
54
+ const contact = useG8Query(async (signal) => dataOf<ContactBits>(await g8Get(`/contacts/${contactId}`, undefined, { signal })), [contactId], { key: ["contacts", "deal-prefill", contactId] });
55
+ const [ownerId, setOwnerId] = useState<string | null>(null);
56
+ const [another, setAnother] = useState(false);
57
+ if (owner.loading || (contact.loading && !contact.data)) return <Skeleton className="h-24 w-full" aria-label="Loading the deal form" />;
58
+ if (contact.error) return <ApiError error={contact.error} title="Could not load the contact" onRetry={() => void contact.refetch()} />;
59
+ const effective = ownerId ?? owner.defaultOwner?.id ?? "";
60
+ if (!effective) return <ApiError error={owner.error ?? new Error("No team member found to own this deal.")} title="A deal needs an owner" onRetry={() => void owner.reload()} />;
61
+ const company = companyLabelOf(contact.data);
62
+ const person = contactLabel(contact.data, contactId);
63
+ return (
64
+ <div className="space-y-3">
65
+ <dl className="grid grid-cols-[auto,1fr] gap-x-3 gap-y-1 rounded-md border bg-muted/40 p-3 text-sm">
66
+ <dt className="text-muted-foreground">Contact</dt>
67
+ <dd className="font-medium">{person}</dd>
68
+ <dt className="text-muted-foreground">Company</dt>
69
+ <dd className="font-medium">{company || "None on this contact"}</dd>
70
+ </dl>
71
+ {owner.source === "first-member" ? (
72
+ <div className="space-y-1">
73
+ <label htmlFor="create-deal-owner" className="text-sm font-medium">Owner</label>
74
+ <select id="create-deal-owner" className="h-9 w-full rounded-md border bg-background px-2 text-sm" value={effective} onChange={(e) => setOwnerId(e.target.value)}>
75
+ {owner.owners.map((o) => <option key={o.id} value={o.id}>{o.name}{o.email && o.email !== o.name ? ` (${o.email})` : ""}</option>)}
76
+ </select>
77
+ <p className="text-xs text-muted-foreground">This key has no person of its own, so choose who owns the deal.</p>
78
+ </div>
79
+ ) : null}
80
+ <label className="flex items-center gap-2 text-sm">
81
+ <input type="checkbox" checked={another} onChange={(e) => setAnother(e.target.checked)} />
82
+ Create another deal for this company
83
+ </label>
84
+ <p className="text-xs text-muted-foreground">Leave this off and a company that already has a deal is refused, so you never create a duplicate by accident.</p>
85
+ <DealDetail ownerId={effective} contactIds={[contactId]} allowDuplicate={another} defaultName={[company, person].filter(Boolean).join(" - ")} onSaved={(deal) => { if (deal.id) onCreated?.(deal.id); }} />
86
+ </div>
87
+ );
88
+ }
89
+
90
+ export function CreateDealButton({ contactId, onCreated, label = "Create deal", variant = "outline", size = "sm" }: CreateDealButtonProps) {
91
+ const [open, setOpen] = useState(false);
92
+ return (
93
+ <>
94
+ <Button size={size} variant={variant} onClick={() => setOpen(true)}>
95
+ <Handshake className="mr-1 size-4" aria-hidden />
96
+ {label}
97
+ </Button>
98
+ <Sheet open={open} onOpenChange={setOpen}>
99
+ <SheetContent className="w-full overflow-y-auto sm:max-w-xl">
100
+ <SheetHeader>
101
+ <SheetTitle>Create deal</SheetTitle>
102
+ <SheetDescription>The deal is linked to this contact and their company.</SheetDescription>
103
+ </SheetHeader>
104
+ <div className="p-4">{open ? <Form key={contactId} contactId={contactId} onCreated={onCreated} /> : null}</div>
105
+ </SheetContent>
106
+ </Sheet>
107
+ </>
108
+ );
109
+ }
@@ -0,0 +1,134 @@
1
+ "use client";
2
+
3
+ /**
4
+ * DealContactsEditor: the buying committee of one deal. Add a contact (with an optional role), change a role, make
5
+ * a contact the primary one, and remove a contact. The list itself is DealContactsTab; open a row for the full CRM record.
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. GET /deals/{id} (deals.getDeal) read - the contacts, roles and primary (via DealContactsTab)
9
+ * 2. GET /contacts?name= (crm contacts.listContacts) read - find a contact to add
10
+ * 3. POST /deals/{id}/contacts { person_id, role } (deals.addDealContact) write - the contact must work at the deal's company
11
+ * 4. PUT /deals/{id}/contacts/{contact_id} { role } (deals.setDealContactRole) write - null clears the role
12
+ * 5. PUT /deals/{id}/contacts/{person_id}/primary (deals.setPrimaryDealContact) write
13
+ * 6. DELETE /deals/{id}/contacts/{person_id} (deals.removeDealContact) DESTRUCTIVE -> ConfirmSpendDialog first
14
+ * Roles are champion, decision_maker, influencer, blocker, coach and end_user. A deal keeps at least one contact:
15
+ * the API refuses to remove the last one and the message is shown.
16
+ */
17
+ import { useState } from "react";
18
+ import { Search } from "lucide-react";
19
+ import { Button, Input } from "@graph8/sdk/ui";
20
+ import { ApiError } from "@/g8/components/api-error";
21
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
22
+ import { EntityAvatar } from "@/g8/components/screen/entity-avatar";
23
+ import { g8Delete, g8Get, g8Post, g8Put } from "@/g8/lib/g8-client";
24
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
25
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
26
+ import { useDebouncedValue } from "@/g8/crm/hooks/use-debounced";
27
+ import { contactName, type ContactRecord } from "@/g8/crm/lib/crm-types";
28
+ import { dataOf, type Deal, type DealContact } from "../lib/deals-types";
29
+ import { CONTACT_ROLES, roleLabel } from "../lib/deal-extras";
30
+ import { DealContactsTab } from "./deal-related";
31
+
32
+ export interface DealContactsEditorProps {
33
+ dealId: string;
34
+ /** Called after any change, so a screen can refresh its numbers. */
35
+ onChanged?: () => void;
36
+ /** Open a contact in your contacts screen (see DealContactsTab). */
37
+ onOpenContact?: (contactId: number) => void;
38
+ /** Open a contact's company in your companies screen. */
39
+ onOpenCompany?: (companyId: number) => void;
40
+ className?: string;
41
+ }
42
+
43
+ function AddContact({ dealId, existing, onAdded }: { dealId: string; existing: ReadonlySet<number>; onAdded: () => void }) {
44
+ const [text, setText] = useState("");
45
+ const [role, setRole] = useState("");
46
+ const term = useDebouncedValue(text.trim());
47
+ const found = useG8Query(async (signal) => (await g8Get<ContactRecord[]>("/contacts", { name: term, limit: 8, page: 1 }, { signal })).data ?? [], [term], { enabled: term.length > 1 });
48
+ const add = useG8Mutation(
49
+ (c: ContactRecord) => g8Post(`/deals/${encodeURIComponent(dealId)}/contacts`, { person_id: String(c.id), ...(role ? { role } : {}) }),
50
+ { invalidates: [["deals", "detail", dealId], ["deals", "list"]], onSuccess: () => { setText(""); setRole(""); onAdded(); } },
51
+ );
52
+ const choices = (found.data ?? []).filter((c) => !existing.has(c.id));
53
+ return (
54
+ <div className="space-y-2">
55
+ <div className="flex flex-wrap items-end gap-2">
56
+ <div className="relative min-w-48 flex-1">
57
+ <Search className="pointer-events-none absolute left-2 top-2.5 size-4 text-muted-foreground" aria-hidden />
58
+ <Input aria-label="Add a contact to this deal" className="pl-8" placeholder="Search contacts to add" value={text} onChange={(e) => setText(e.target.value)} />
59
+ </div>
60
+ <select aria-label="Role for the new contact" className="h-9 rounded-md border bg-background px-2 text-sm" value={role} onChange={(e) => setRole(e.target.value)}>
61
+ <option value="">No role</option>
62
+ {CONTACT_ROLES.map((r) => <option key={r} value={r}>{roleLabel(r)}</option>)}
63
+ </select>
64
+ </div>
65
+ {add.error ? <ApiError error={add.error} title="Could not add the contact" /> : null}
66
+ {found.error ? <ApiError error={found.error} title="Could not search contacts" /> : null}
67
+ {term.length > 1 && !found.loading && !found.error && choices.length === 0 ? <p className="text-xs text-muted-foreground">No other contacts match.</p> : null}
68
+ <ul className="divide-y rounded-md border empty:hidden" aria-label="Contacts to add">
69
+ {choices.map((c) => (
70
+ <li key={c.id}>
71
+ <button type="button" className="flex w-full items-center gap-2 p-2 text-left text-sm hover:bg-muted disabled:opacity-60" disabled={add.loading} aria-label={`Add ${contactName(c)}`} onClick={() => void add.mutate(c)}>
72
+ <EntityAvatar name={contactName(c)} size="sm" />
73
+ <span className="font-medium">{contactName(c)}</span>
74
+ <span className="text-xs text-muted-foreground">{[c.job_title, c.company_name].filter(Boolean).join(" at ")}</span>
75
+ </button>
76
+ </li>
77
+ ))}
78
+ </ul>
79
+ </div>
80
+ );
81
+ }
82
+
83
+ export function DealContactsEditor({ dealId, onChanged, onOpenContact, onOpenCompany, className }: DealContactsEditorProps) {
84
+ const base = `/deals/${encodeURIComponent(dealId)}/contacts`;
85
+ const [busyId, setBusyId] = useState<number | null>(null);
86
+ const [removing, setRemoving] = useState<DealContact | null>(null);
87
+ // Same query key as DealContactsTab: one request, and the ids already on the deal are hidden from the search results.
88
+ const deal = useG8Query(async (signal) => dataOf<Deal>(await g8Get(`/deals/${encodeURIComponent(dealId)}`, undefined, { signal })), [dealId], { key: ["deals", "detail", dealId] });
89
+ const existing = new Set((deal.data?.contacts ?? []).map((c) => c.id));
90
+ const done = () => onChanged?.();
91
+ const refresh = [["deals", "detail", dealId], ["deals", "list"]] as const;
92
+
93
+ const role = useG8Mutation(async (v: { contact: DealContact; role: string }) => {
94
+ setBusyId(v.contact.id);
95
+ try { await g8Put(`${base}/${v.contact.id}`, { role: v.role || null }); } finally { setBusyId(null); }
96
+ }, { invalidates: refresh, onSuccess: done });
97
+ const primary = useG8Mutation(async (c: DealContact) => {
98
+ setBusyId(c.id);
99
+ try { await g8Put(`${base}/${encodeURIComponent(String(c.id))}/primary`); } finally { setBusyId(null); }
100
+ }, { invalidates: refresh, onSuccess: done });
101
+ const remove = useG8Mutation(async (c: DealContact) => {
102
+ setBusyId(c.id);
103
+ try { await g8Delete(`${base}/${encodeURIComponent(String(c.id))}`); } finally { setBusyId(null); }
104
+ }, { invalidates: refresh, onSuccess: () => { setRemoving(null); done(); }, onError: () => setRemoving(null) });
105
+ const failure = role.error ?? primary.error ?? remove.error;
106
+
107
+ return (
108
+ <div className={`space-y-4 ${className ?? ""}`}>
109
+ <AddContact dealId={dealId} existing={existing} onAdded={done} />
110
+ {failure ? <ApiError error={failure} title="That contact change failed" /> : null}
111
+ <DealContactsTab
112
+ dealId={dealId}
113
+ onOpenContact={onOpenContact}
114
+ onOpenCompany={onOpenCompany}
115
+ renderContactActions={(c, deal) => {
116
+ const isPrimary = deal.primary_contact?.id === c.id;
117
+ const name = c.name || c.email || `contact ${c.id}`;
118
+ const options = c.role && !CONTACT_ROLES.includes(c.role as (typeof CONTACT_ROLES)[number]) ? [c.role, ...CONTACT_ROLES] : CONTACT_ROLES;
119
+ return (
120
+ <span className="flex shrink-0 items-center gap-1">
121
+ <select aria-label={`Role of ${name}`} className="h-8 rounded-md border bg-background px-1 text-xs" disabled={busyId === c.id} value={c.role ?? ""} onChange={(e) => void role.mutate({ contact: c, role: e.target.value })}>
122
+ <option value="">No role</option>
123
+ {options.map((r) => <option key={r} value={r}>{roleLabel(r)}</option>)}
124
+ </select>
125
+ <Button size="sm" variant="ghost" disabled={isPrimary || busyId === c.id} aria-label={`Make ${name} primary`} onClick={() => void primary.mutate(c)}>{isPrimary ? "Primary" : "Make primary"}</Button>
126
+ <Button size="sm" variant="ghost" disabled={busyId === c.id} aria-label={`Remove ${name}`} onClick={() => setRemoving(c)}>Remove</Button>
127
+ </span>
128
+ );
129
+ }}
130
+ />
131
+ <ConfirmSpendDialog open={removing != null} tier="destructive" title={`Remove ${removing?.name || removing?.email || "this contact"} from the deal?`} description="The contact stays in your CRM; only the link to this deal is removed." confirmLabel="Remove contact" affected={1} confirming={remove.loading} onConfirm={() => removing && void remove.mutate(removing)} onCancel={() => setRemoving(null)} />
132
+ </div>
133
+ );
134
+ }
@@ -33,6 +33,8 @@ export interface DealDetailProps {
33
33
  pipelineId?: string;
34
34
  /** Create mode: allow a second deal for a company that already has one (the API refuses it with 409 otherwise). */
35
35
  allowDuplicate?: boolean;
36
+ /** Create mode: the name the form starts with (for example "Acme - Maya Chen"). Typing replaces it. */
37
+ defaultName?: string;
36
38
  onSaved?: (deal: Deal) => void;
37
39
  onDeleted?: (dealId: string) => void;
38
40
  children?: ReactNode;
@@ -53,10 +55,10 @@ function fromDeal(d: Deal): Form {
53
55
  return { name: d.name ?? "", amount: d.amount != null ? String(d.amount) : "", closeDate: d.close_date?.slice(0, 10) ?? "", stageId: d.stage_id ?? "", description: d.description ?? "" };
54
56
  }
55
57
 
56
- export function DealDetail({ dealId, ownerId, contactIds = [], pipelineId, allowDuplicate, onSaved, onDeleted, children, className }: DealDetailProps) {
58
+ export function DealDetail({ dealId, ownerId, contactIds = [], pipelineId, allowDuplicate, defaultName, onSaved, onDeleted, children, className }: DealDetailProps) {
57
59
  const brand = useBrand();
58
60
  const { pipelines, defaultPipeline } = usePipelines();
59
- const [form, setForm] = useState<Form>(EMPTY);
61
+ const [form, setForm] = useState<Form>({ ...EMPTY, name: defaultName ?? "" });
60
62
  const [confirmDelete, setConfirmDelete] = useState(false);
61
63
  // Retry-safe create: once POST /deals succeeded, further saves edit that deal instead of creating another.
62
64
  const [createdId, setCreatedId] = useState<string | null>(null);
@@ -74,7 +76,7 @@ export function DealDetail({ dealId, ownerId, contactIds = [], pipelineId, allow
74
76
  const dirty = useRef(false);
75
77
  // A different deal (or create mode again) must not show the previous one's fields. Declared BEFORE the
76
78
  // seeding effect so that, when the record is already cached, the reset runs first and the seed wins.
77
- useEffect(() => { dirty.current = false; setForm(EMPTY); setCreatedId(null); setCreatedRevision(null); setConfirmDelete(false); }, [dealId]);
79
+ useEffect(() => { dirty.current = false; setForm({ ...EMPTY, name: dealId ? "" : defaultName ?? "" }); setCreatedId(null); setCreatedRevision(null); setConfirmDelete(false); }, [dealId]);
78
80
  useEffect(() => { if (deal.data && !dirty.current) setForm(fromDeal(deal.data)); }, [deal.data]);
79
81
 
80
82
  const pipeline = pipelines.find((p) => p.id === (deal.data?.pipeline_id ?? pipelineId)) ?? defaultPipeline;
@@ -128,14 +130,14 @@ export function DealDetail({ dealId, ownerId, contactIds = [], pipelineId, allow
128
130
 
129
131
  {d ? (
130
132
  <section aria-label="Contacts" className="space-y-1 text-sm">
131
- <h3 className="font-semibold">Contacts ({d.contact_count ?? d.contacts?.length ?? 0})</h3>
133
+ <h3 className="font-medium">Contacts ({d.contact_count ?? d.contacts?.length ?? 0})</h3>
132
134
  {(d.contacts ?? []).map((c) => <p key={c.id}>{c.name ?? c.email}{c.role ? ` · ${c.role}` : ""}{d.primary_contact?.id === c.id ? " (primary)" : ""}</p>)}
133
135
  <p className="text-muted-foreground">Owner: {d.owner_name ?? d.owner_email ?? "none"} · {formatMoney(d.amount, d.currency)}</p>
134
136
  </section>
135
137
  ) : null}
136
138
  {dealId ? (
137
139
  <section aria-label="Stage history" className="space-y-1 text-sm">
138
- <h3 className="font-semibold">Stage history</h3>
140
+ <h3 className="font-medium">Stage history</h3>
139
141
  {history.error ? <ApiError error={history.error} title="Could not load history" onRetry={() => void history.refetch()} /> : null}
140
142
  {(history.data ?? []).length === 0 && !history.loading ? <p className="text-muted-foreground">No stage changes yet.</p> : null}
141
143
  {(history.data ?? []).map((h, i) => <p key={i}>{h.from_stage_name ?? "New"} to {h.to_stage_name ?? "unknown"} <span className="text-muted-foreground">{h.changed_at ?? h.created_at ?? ""}</span></p>)}
@@ -0,0 +1,65 @@
1
+ "use client";
2
+
3
+ /**
4
+ * DealFilterBar: the deal list filters beside the search box: owner, stage, outcome, stale, activity, close-date
5
+ * range and "no contact". It only edits a DealFilterState; `filterQuery(state)` turns that into the `usePipelineDeals`
6
+ * filters (GET /deals owner_id, stage_id, outcome, stale_before, has_activity, close_after, close_before, has_contact).
7
+ *
8
+ * SDK operations: none directly (the owners come from the deals block's useDefaultOwner: GET /team-members).
9
+ * Dates are date-only (YYYY-MM-DD); the "to" date is inclusive, the API's close_before is exclusive, so
10
+ * `filterQuery` adds one day in UTC.
11
+ */
12
+ import { Button } from "@graph8/sdk/ui";
13
+ import { NO_FILTERS, filtersActive, type DealFilterState } from "../lib/deal-extras";
14
+ import type { Stage } from "../lib/deals-types";
15
+ import type { OwnerOption } from "../hooks/use-default-owner";
16
+
17
+ export interface DealFilterBarProps {
18
+ value: DealFilterState;
19
+ onChange: (next: DealFilterState) => void;
20
+ stages: Stage[];
21
+ owners: OwnerOption[];
22
+ className?: string;
23
+ }
24
+
25
+ const SELECT = "h-9 rounded-md border bg-background px-2 text-sm";
26
+ const DATE = "h-9 rounded-md border bg-background px-2 text-sm";
27
+
28
+ export function DealFilterBar({ value, onChange, stages, owners, className }: DealFilterBarProps) {
29
+ const set = <K extends keyof DealFilterState>(key: K, v: DealFilterState[K]) => onChange({ ...value, [key]: v });
30
+ return (
31
+ <div className={`flex flex-wrap items-center gap-2 ${className ?? ""}`} role="group" aria-label="Deal filters">
32
+ <select aria-label="Filter by owner" className={SELECT} value={value.ownerId} onChange={(e) => set("ownerId", e.target.value)}>
33
+ <option value="">Any owner</option>
34
+ {owners.map((o) => <option key={o.id} value={o.id}>{o.name}</option>)}
35
+ </select>
36
+ <select aria-label="Filter by stage" className={SELECT} value={value.stageId} onChange={(e) => set("stageId", e.target.value)}>
37
+ <option value="">Any stage</option>
38
+ {stages.map((s) => <option key={s.id} value={s.id}>{s.name}</option>)}
39
+ </select>
40
+ <select aria-label="Filter by outcome" className={SELECT} value={value.outcome} onChange={(e) => set("outcome", e.target.value as DealFilterState["outcome"])}>
41
+ <option value="">Open, won and lost</option>
42
+ <option value="open">Open</option>
43
+ <option value="won">Won</option>
44
+ <option value="lost">Lost</option>
45
+ </select>
46
+ <select aria-label="Filter by staleness" className={SELECT} value={value.staleDays} onChange={(e) => set("staleDays", e.target.value as DealFilterState["staleDays"])}>
47
+ <option value="">Any activity age</option>
48
+ {(["7", "14", "30", "60", "90"] as const).map((d) => <option key={d} value={d}>Stale {d}+ days</option>)}
49
+ </select>
50
+ <select aria-label="Filter by activity" className={SELECT} value={value.activity} onChange={(e) => set("activity", e.target.value as DealFilterState["activity"])}>
51
+ <option value="">With or without activity</option>
52
+ <option value="yes">Has activity</option>
53
+ <option value="no">No activity</option>
54
+ </select>
55
+ <label className="flex items-center gap-1 text-xs text-muted-foreground">Closes from
56
+ <input aria-label="Close date from" type="date" className={DATE} value={value.closeFrom} max={value.closeTo || undefined} onChange={(e) => set("closeFrom", e.target.value)} />
57
+ </label>
58
+ <label className="flex items-center gap-1 text-xs text-muted-foreground">to
59
+ <input aria-label="Close date to" type="date" className={DATE} value={value.closeTo} min={value.closeFrom || undefined} onChange={(e) => set("closeTo", e.target.value)} />
60
+ </label>
61
+ <label className="flex items-center gap-1 text-sm"><input type="checkbox" checked={value.orphaned} onChange={(e) => set("orphaned", e.target.checked)} />No contact</label>
62
+ {filtersActive(value) ? <Button size="sm" variant="ghost" onClick={() => onChange(NO_FILTERS)}>Clear filters</Button> : null}
63
+ </div>
64
+ );
65
+ }