@graph8/sdk 0.303.0 → 0.357.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 (664) hide show
  1. package/README.md +81 -0
  2. package/blocks/README.md +32 -0
  3. package/blocks/_foundation/README.md +78 -0
  4. package/blocks/_foundation/block.json +79 -0
  5. package/blocks/_foundation/components/api-error.tsx +62 -0
  6. package/blocks/_foundation/components/confirm-spend-dialog.tsx +142 -0
  7. package/blocks/_foundation/components/empty-state.tsx +24 -0
  8. package/blocks/_foundation/hooks/use-g8-mutation.ts +88 -0
  9. package/blocks/_foundation/hooks/use-g8-query.ts +87 -0
  10. package/blocks/_foundation/hooks/use-job-poll.ts +130 -0
  11. package/blocks/_foundation/hooks/use-paginated.ts +105 -0
  12. package/blocks/_foundation/lib/g8-client.ts +128 -0
  13. package/blocks/_foundation/recipes/verify-proxy-key.ts +18 -0
  14. package/blocks/_foundation/server/proxy.ts +233 -0
  15. package/blocks/_foundation/server/route.ts +30 -0
  16. package/blocks/agents/README.md +133 -0
  17. package/blocks/agents/block.json +223 -0
  18. package/blocks/agents/components/agent-builder.tsx +199 -0
  19. package/blocks/agents/components/agent-knowledge-panel.tsx +144 -0
  20. package/blocks/agents/components/agent-runs.tsx +175 -0
  21. package/blocks/agents/components/agent-skills-panel.tsx +126 -0
  22. package/blocks/agents/components/agent-test-console.tsx +175 -0
  23. package/blocks/agents/lib/agent-types.ts +114 -0
  24. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +50 -0
  25. package/blocks/agents/recipes/run-and-follow-agent-run.ts +46 -0
  26. package/blocks/agents/recipes/test-agent-before-activating.ts +36 -0
  27. package/blocks/analytics/README.md +73 -0
  28. package/blocks/analytics/block.json +178 -0
  29. package/blocks/analytics/components/funnel-chart.tsx +79 -0
  30. package/blocks/analytics/components/kpi-tiles.tsx +74 -0
  31. package/blocks/analytics/components/leaderboard.tsx +86 -0
  32. package/blocks/analytics/components/report-builder.tsx +200 -0
  33. package/blocks/analytics/hooks/use-report.ts +26 -0
  34. package/blocks/analytics/lib/analytics-types.ts +108 -0
  35. package/blocks/analytics/lib/use-debounced.ts +16 -0
  36. package/blocks/analytics/recipes/01-weekly-outbound-report.ts +32 -0
  37. package/blocks/analytics/recipes/02-revenue-snapshot.ts +22 -0
  38. package/blocks/analytics/recipes/03-saved-report.ts +30 -0
  39. package/blocks/appointments/README.md +80 -0
  40. package/blocks/appointments/block.json +218 -0
  41. package/blocks/appointments/components/availability-editor.tsx +192 -0
  42. package/blocks/appointments/components/booking-page.tsx +154 -0
  43. package/blocks/appointments/components/event-type-editor.tsx +214 -0
  44. package/blocks/appointments/components/meeting-detail.tsx +147 -0
  45. package/blocks/appointments/components/meeting-list.tsx +148 -0
  46. package/blocks/appointments/hooks/use-available-slots.ts +29 -0
  47. package/blocks/appointments/hooks/use-schedules.ts +18 -0
  48. package/blocks/appointments/lib/appointments-types.ts +159 -0
  49. package/blocks/appointments/lib/idempotency.ts +30 -0
  50. package/blocks/appointments/recipes/01-create-event-type-and-hours.ts +27 -0
  51. package/blocks/appointments/recipes/02-find-slots-and-book.ts +34 -0
  52. package/blocks/appointments/recipes/03-bookings-digest.ts +22 -0
  53. package/blocks/appointments/recipes/04-calendar-invite.ts +21 -0
  54. package/blocks/block.schema.json +130 -0
  55. package/blocks/block.types.ts +109 -0
  56. package/blocks/campaigns/README.md +118 -0
  57. package/blocks/campaigns/block.json +361 -0
  58. package/blocks/campaigns/components/campaign-assets.tsx +180 -0
  59. package/blocks/campaigns/components/campaign-audience-step.tsx +91 -0
  60. package/blocks/campaigns/components/campaign-launch-step.tsx +147 -0
  61. package/blocks/campaigns/components/campaign-list.tsx +120 -0
  62. package/blocks/campaigns/components/campaign-metrics.tsx +93 -0
  63. package/blocks/campaigns/components/campaign-select.tsx +52 -0
  64. package/blocks/campaigns/components/campaign-sequence-step.tsx +158 -0
  65. package/blocks/campaigns/components/campaign-wizard.tsx +238 -0
  66. package/blocks/campaigns/components/form-field.tsx +29 -0
  67. package/blocks/campaigns/components/icp-editor.tsx +230 -0
  68. package/blocks/campaigns/components/persona-editor.tsx +210 -0
  69. package/blocks/campaigns/hooks/use-company-profile.ts +34 -0
  70. package/blocks/campaigns/lib/campaigns-utils.ts +155 -0
  71. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +33 -0
  72. package/blocks/campaigns/recipes/campaign-report.ts +22 -0
  73. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +51 -0
  74. package/blocks/campaigns/recipes/set-up-icp-and-personas.ts +37 -0
  75. package/blocks/coach/README.md +93 -0
  76. package/blocks/coach/block.json +125 -0
  77. package/blocks/coach/components/call-scorecard.tsx +126 -0
  78. package/blocks/coach/components/coaching-feed.tsx +140 -0
  79. package/blocks/coach/lib/coach-types.ts +116 -0
  80. package/blocks/coach/recipes/read-a-scorecard.ts +27 -0
  81. package/blocks/coach/recipes/weekly-coaching-digest.ts +53 -0
  82. package/blocks/copilot/README.md +99 -0
  83. package/blocks/copilot/block.json +151 -0
  84. package/blocks/copilot/components/copilot-memory.tsx +87 -0
  85. package/blocks/copilot/components/copilot-panel.tsx +189 -0
  86. package/blocks/copilot/components/copilot-team-chat.tsx +111 -0
  87. package/blocks/copilot/lib/copilot-types.ts +111 -0
  88. package/blocks/copilot/recipes/ask-the-copilot.ts +35 -0
  89. package/blocks/copilot/recipes/post-in-a-work-channel.ts +33 -0
  90. package/blocks/copilot/recipes/remember-and-recall.ts +19 -0
  91. package/blocks/crm/README.md +122 -0
  92. package/blocks/crm/block.json +378 -0
  93. package/blocks/crm/components/activity-timeline.tsx +99 -0
  94. package/blocks/crm/components/company-detail.tsx +118 -0
  95. package/blocks/crm/components/company-table.tsx +173 -0
  96. package/blocks/crm/components/contact-detail.tsx +135 -0
  97. package/blocks/crm/components/contact-table.tsx +217 -0
  98. package/blocks/crm/components/dedupe-review.tsx +164 -0
  99. package/blocks/crm/components/import-wizard.tsx +193 -0
  100. package/blocks/crm/components/notes-panel.tsx +112 -0
  101. package/blocks/crm/components/record-form.tsx +191 -0
  102. package/blocks/crm/components/task-list.tsx +107 -0
  103. package/blocks/crm/hooks/use-custom-fields.ts +18 -0
  104. package/blocks/crm/hooks/use-debounced.ts +13 -0
  105. package/blocks/crm/hooks/use-page-query.ts +52 -0
  106. package/blocks/crm/lib/crm-types.ts +136 -0
  107. package/blocks/crm/recipes/custom-fields-and-values.ts +28 -0
  108. package/blocks/crm/recipes/dedupe-and-merge.ts +30 -0
  109. package/blocks/crm/recipes/notes-tasks-and-activity.ts +23 -0
  110. package/blocks/crm/recipes/upsert-contact-with-company.ts +33 -0
  111. package/blocks/deals/README.md +78 -0
  112. package/blocks/deals/block.json +198 -0
  113. package/blocks/deals/components/deal-detail.tsx +134 -0
  114. package/blocks/deals/components/forecast-view.tsx +112 -0
  115. package/blocks/deals/components/pipeline-board.tsx +142 -0
  116. package/blocks/deals/components/stage-editor.tsx +96 -0
  117. package/blocks/deals/hooks/use-pipeline-deals.ts +67 -0
  118. package/blocks/deals/hooks/use-pipelines.ts +19 -0
  119. package/blocks/deals/lib/deals-types.ts +128 -0
  120. package/blocks/deals/recipes/01-create-deal-and-advance.ts +32 -0
  121. package/blocks/deals/recipes/02-pipeline-report.ts +35 -0
  122. package/blocks/deals/recipes/03-stale-deals-sweep.ts +20 -0
  123. package/blocks/dialer/README.md +114 -0
  124. package/blocks/dialer/block.json +260 -0
  125. package/blocks/dialer/components/call-log.tsx +78 -0
  126. package/blocks/dialer/components/call-queue.tsx +188 -0
  127. package/blocks/dialer/components/dialer-workspace.tsx +47 -0
  128. package/blocks/dialer/components/disposition-panel.tsx +75 -0
  129. package/blocks/dialer/components/live-calls-table.tsx +107 -0
  130. package/blocks/dialer/components/session-start-form.tsx +111 -0
  131. package/blocks/dialer/components/softphone.tsx +122 -0
  132. package/blocks/dialer/components/transcript-viewer.tsx +82 -0
  133. package/blocks/dialer/hooks/use-dialer-numbers.ts +19 -0
  134. package/blocks/dialer/hooks/use-dialer-session.ts +68 -0
  135. package/blocks/dialer/hooks/use-dispositions.ts +29 -0
  136. package/blocks/dialer/lib/dialer-types.ts +158 -0
  137. package/blocks/dialer/lib/softphone-transport.ts +67 -0
  138. package/blocks/dialer/lib/twilio-transport.ts +74 -0
  139. package/blocks/dialer/recipes/01-create-session-and-dial.ts +39 -0
  140. package/blocks/dialer/recipes/02-disposition-calls.ts +26 -0
  141. package/blocks/dialer/recipes/03-review-a-call.ts +23 -0
  142. package/blocks/enrichment/README.md +105 -0
  143. package/blocks/enrichment/block.json +204 -0
  144. package/blocks/enrichment/components/bulk-enrich-panel.tsx +205 -0
  145. package/blocks/enrichment/components/email-verify-badge.tsx +82 -0
  146. package/blocks/enrichment/components/enrich-button.tsx +136 -0
  147. package/blocks/enrichment/components/waterfall-config.tsx +127 -0
  148. package/blocks/enrichment/hooks/use-providers.ts +18 -0
  149. package/blocks/enrichment/lib/enrichment-types.ts +104 -0
  150. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +41 -0
  151. package/blocks/enrichment/recipes/lookup-and-verify.ts +25 -0
  152. package/blocks/enrichment/recipes/waterfall-pipeline.ts +26 -0
  153. package/blocks/forms/README.md +112 -0
  154. package/blocks/forms/block.json +200 -0
  155. package/blocks/forms/components/form-builder.tsx +150 -0
  156. package/blocks/forms/components/form-embed.tsx +103 -0
  157. package/blocks/forms/components/form-mapping-editor.tsx +125 -0
  158. package/blocks/forms/components/site-audit-panel.tsx +73 -0
  159. package/blocks/forms/components/submissions-table.tsx +158 -0
  160. package/blocks/forms/lib/form-types.ts +127 -0
  161. package/blocks/forms/recipes/install-snippet-and-form.ts +21 -0
  162. package/blocks/forms/recipes/retry-failed-deliveries.ts +26 -0
  163. package/blocks/forms/recipes/route-form-to-list.ts +44 -0
  164. package/blocks/inbox/README.md +72 -0
  165. package/blocks/inbox/block.json +194 -0
  166. package/blocks/inbox/components/inbox-list.tsx +156 -0
  167. package/blocks/inbox/components/inbox-screen.tsx +49 -0
  168. package/blocks/inbox/components/reply-composer.tsx +183 -0
  169. package/blocks/inbox/components/tag-and-assign.tsx +189 -0
  170. package/blocks/inbox/components/thread-badge.tsx +17 -0
  171. package/blocks/inbox/components/thread-view.tsx +108 -0
  172. package/blocks/inbox/hooks/use-inbox-thread.ts +24 -0
  173. package/blocks/inbox/hooks/use-inbox-threads.ts +40 -0
  174. package/blocks/inbox/lib/idempotency.ts +30 -0
  175. package/blocks/inbox/lib/inbox-types.ts +100 -0
  176. package/blocks/inbox/recipes/01-triage-and-reply.ts +43 -0
  177. package/blocks/inbox/recipes/02-tag-and-assign.ts +31 -0
  178. package/blocks/inbox/recipes/03-hot-contacts-digest.ts +16 -0
  179. package/blocks/integrations/README.md +75 -0
  180. package/blocks/integrations/block.json +172 -0
  181. package/blocks/integrations/components/integration-list.tsx +165 -0
  182. package/blocks/integrations/components/sync-status.tsx +223 -0
  183. package/blocks/integrations/components/webhook-manager.tsx +300 -0
  184. package/blocks/integrations/lib/integrations-utils.ts +62 -0
  185. package/blocks/integrations/recipes/crm-sync-health.ts +19 -0
  186. package/blocks/integrations/recipes/push-contacts-to-crm.ts +20 -0
  187. package/blocks/integrations/recipes/register-webhook.ts +18 -0
  188. package/blocks/integrations/recipes/verify-webhook-route.ts +33 -0
  189. package/blocks/intent/README.md +57 -0
  190. package/blocks/intent/block.json +138 -0
  191. package/blocks/intent/components/account-intent-score.tsx +91 -0
  192. package/blocks/intent/components/intent-feed.tsx +215 -0
  193. package/blocks/intent/components/intent-keyword-manager.tsx +331 -0
  194. package/blocks/intent/hooks/use-account-intent.ts +46 -0
  195. package/blocks/intent/lib/intent-utils.ts +95 -0
  196. package/blocks/intent/recipes/rank-intent-accounts.ts +36 -0
  197. package/blocks/intent/recipes/track-competitor-domain.ts +23 -0
  198. package/blocks/intent/recipes/who-visited-url.ts +18 -0
  199. package/blocks/lifecycle/README.md +79 -0
  200. package/blocks/lifecycle/block.json +198 -0
  201. package/blocks/lifecycle/components/churn-list.tsx +92 -0
  202. package/blocks/lifecycle/components/customer-table.tsx +119 -0
  203. package/blocks/lifecycle/components/lead-queue.tsx +127 -0
  204. package/blocks/lifecycle/components/lifecycle-board.tsx +102 -0
  205. package/blocks/lifecycle/components/stage-move-dialog.tsx +75 -0
  206. package/blocks/lifecycle/hooks/use-debounced.ts +13 -0
  207. package/blocks/lifecycle/hooks/use-lifecycle-stages.ts +27 -0
  208. package/blocks/lifecycle/lib/lifecycle.ts +99 -0
  209. package/blocks/lifecycle/recipes/customer-brief.ts +23 -0
  210. package/blocks/lifecycle/recipes/move-company-stage.ts +17 -0
  211. package/blocks/lifecycle/recipes/read-lifecycle-board.ts +21 -0
  212. package/blocks/lifecycle/recipes/unlock-stage-contacts.ts +25 -0
  213. package/blocks/lists/README.md +96 -0
  214. package/blocks/lists/block.json +166 -0
  215. package/blocks/lists/components/add-to-list-button.tsx +111 -0
  216. package/blocks/lists/components/list-picker.tsx +94 -0
  217. package/blocks/lists/components/list-table.tsx +160 -0
  218. package/blocks/lists/components/suppression-manager.tsx +153 -0
  219. package/blocks/lists/recipes/create-list-and-add-contacts.ts +35 -0
  220. package/blocks/lists/recipes/refresh-and-check-lists.ts +21 -0
  221. package/blocks/lists/recipes/suppress-and-reinstate.ts +30 -0
  222. package/blocks/mailboxes/README.md +71 -0
  223. package/blocks/mailboxes/block.json +187 -0
  224. package/blocks/mailboxes/components/connect-mailbox.tsx +284 -0
  225. package/blocks/mailboxes/components/domain-purchase.tsx +271 -0
  226. package/blocks/mailboxes/components/mailbox-list.tsx +185 -0
  227. package/blocks/mailboxes/components/warmup-health.tsx +232 -0
  228. package/blocks/mailboxes/lib/mailboxes-utils.ts +69 -0
  229. package/blocks/mailboxes/recipes/buy-domain-and-mailboxes.ts +64 -0
  230. package/blocks/mailboxes/recipes/connect-imap-mailbox.ts +38 -0
  231. package/blocks/mailboxes/recipes/is-it-safe-to-send.ts +17 -0
  232. package/blocks/mailboxes/recipes/warmup-report.ts +31 -0
  233. package/blocks/newsletter/README.md +122 -0
  234. package/blocks/newsletter/block.json +224 -0
  235. package/blocks/newsletter/components/issue-form.tsx +124 -0
  236. package/blocks/newsletter/components/issue-scheduler.tsx +152 -0
  237. package/blocks/newsletter/components/newsletter-editor.tsx +131 -0
  238. package/blocks/newsletter/components/newsletter-stats.tsx +93 -0
  239. package/blocks/newsletter/components/subscriber-table.tsx +173 -0
  240. package/blocks/newsletter/lib/newsletter-types.ts +97 -0
  241. package/blocks/newsletter/recipes/create-newsletter-and-draft-issue.ts +27 -0
  242. package/blocks/newsletter/recipes/import-audience-and-send.ts +43 -0
  243. package/blocks/newsletter/recipes/issue-performance-report.ts +26 -0
  244. package/blocks/nurture/README.md +110 -0
  245. package/blocks/nurture/block.json +191 -0
  246. package/blocks/nurture/components/nurture-builder.tsx +114 -0
  247. package/blocks/nurture/components/nurture-enroll.tsx +107 -0
  248. package/blocks/nurture/components/nurture-form.tsx +120 -0
  249. package/blocks/nurture/components/nurture-stats.tsx +81 -0
  250. package/blocks/nurture/lib/nurture-types.ts +109 -0
  251. package/blocks/nurture/recipes/create-a-nurture.ts +34 -0
  252. package/blocks/nurture/recipes/enroll-contacts-and-launch.ts +24 -0
  253. package/blocks/nurture/recipes/nurture-health-check.ts +28 -0
  254. package/blocks/objects/README.md +120 -0
  255. package/blocks/objects/block.json +243 -0
  256. package/blocks/objects/components/attribute-field-input.tsx +82 -0
  257. package/blocks/objects/components/attribute-form-dialog.tsx +159 -0
  258. package/blocks/objects/components/object-not-found.tsx +34 -0
  259. package/blocks/objects/components/object-record-detail.tsx +143 -0
  260. package/blocks/objects/components/object-schema-editor.tsx +197 -0
  261. package/blocks/objects/components/object-table.tsx +130 -0
  262. package/blocks/objects/components/record-form-dialog.tsx +112 -0
  263. package/blocks/objects/components/record-roles-panel.tsx +106 -0
  264. package/blocks/objects/hooks/use-object-schema.ts +36 -0
  265. package/blocks/objects/lib/object-types.ts +214 -0
  266. package/blocks/objects/recipes/define-object-and-add-records.ts +24 -0
  267. package/blocks/objects/recipes/update-audit-and-archive-records.ts +39 -0
  268. package/blocks/objects/recipes/upsert-records-from-your-system.ts +30 -0
  269. package/blocks/pages/README.md +104 -0
  270. package/blocks/pages/block.json +223 -0
  271. package/blocks/pages/components/brand-kit-editor.tsx +152 -0
  272. package/blocks/pages/components/content-library.tsx +163 -0
  273. package/blocks/pages/components/page-editor.tsx +253 -0
  274. package/blocks/pages/components/page-list.tsx +159 -0
  275. package/blocks/pages/components/seo-report.tsx +159 -0
  276. package/blocks/pages/lib/studio.ts +77 -0
  277. package/blocks/pages/recipes/audit-site-seo.ts +28 -0
  278. package/blocks/pages/recipes/create-and-publish-page.ts +26 -0
  279. package/blocks/pages/recipes/edit-page-section.ts +25 -0
  280. package/blocks/pages/recipes/refresh-brand-kit.ts +22 -0
  281. package/blocks/quotes/README.md +79 -0
  282. package/blocks/quotes/block.json +183 -0
  283. package/blocks/quotes/components/product-picker.tsx +80 -0
  284. package/blocks/quotes/components/quote-builder.tsx +195 -0
  285. package/blocks/quotes/components/quote-list.tsx +79 -0
  286. package/blocks/quotes/components/quote-viewer.tsx +135 -0
  287. package/blocks/quotes/components/signature-status.tsx +77 -0
  288. package/blocks/quotes/hooks/use-debounced.ts +13 -0
  289. package/blocks/quotes/lib/quotes.ts +142 -0
  290. package/blocks/quotes/recipes/create-and-send-quote.ts +44 -0
  291. package/blocks/quotes/recipes/revise-sent-quote.ts +18 -0
  292. package/blocks/quotes/recipes/track-quote-signature.ts +22 -0
  293. package/blocks/radar/README.md +106 -0
  294. package/blocks/radar/block.json +189 -0
  295. package/blocks/radar/components/competitor-battle-card.tsx +53 -0
  296. package/blocks/radar/components/competitor-tracker.tsx +158 -0
  297. package/blocks/radar/components/radar-overview.tsx +67 -0
  298. package/blocks/radar/components/radar-report.tsx +86 -0
  299. package/blocks/radar/hooks/use-competitors.ts +23 -0
  300. package/blocks/radar/lib/radar-types.ts +124 -0
  301. package/blocks/radar/recipes/battle-card.ts +28 -0
  302. package/blocks/radar/recipes/deep-dive-report.ts +32 -0
  303. package/blocks/radar/recipes/marketing-snapshot.ts +18 -0
  304. package/blocks/radar/recipes/track-and-scan.ts +39 -0
  305. package/blocks/search/README.md +129 -0
  306. package/blocks/search/block.json +185 -0
  307. package/blocks/search/components/filter-builder.tsx +149 -0
  308. package/blocks/search/components/prospect-results.tsx +175 -0
  309. package/blocks/search/components/save-to-list-dialog.tsx +135 -0
  310. package/blocks/search/components/saved-searches.tsx +95 -0
  311. package/blocks/search/hooks/use-filter-options.ts +41 -0
  312. package/blocks/search/lib/search-types.ts +149 -0
  313. package/blocks/search/recipes/explore-filter-values.ts +27 -0
  314. package/blocks/search/recipes/saved-searches.ts +34 -0
  315. package/blocks/search/recipes/search-and-save-to-list.ts +37 -0
  316. package/blocks/sequencer/README.md +87 -0
  317. package/blocks/sequencer/block.json +214 -0
  318. package/blocks/sequencer/components/enroll-dialog.tsx +93 -0
  319. package/blocks/sequencer/components/sequence-builder.tsx +306 -0
  320. package/blocks/sequencer/components/sequence-list.tsx +200 -0
  321. package/blocks/sequencer/components/sequence-stats.tsx +256 -0
  322. package/blocks/sequencer/components/step-editor.tsx +213 -0
  323. package/blocks/sequencer/lib/sequencer-utils.ts +168 -0
  324. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +51 -0
  325. package/blocks/sequencer/recipes/launch-and-enroll.ts +44 -0
  326. package/blocks/sequencer/recipes/pause-when-unsafe.ts +30 -0
  327. package/blocks/sequencer/recipes/sequence-report.ts +22 -0
  328. package/blocks/social-ads/README.md +128 -0
  329. package/blocks/social-ads/block.json +256 -0
  330. package/blocks/social-ads/components/ad-campaign-list.tsx +136 -0
  331. package/blocks/social-ads/components/audience-sync.tsx +150 -0
  332. package/blocks/social-ads/components/creative-gallery.tsx +121 -0
  333. package/blocks/social-ads/components/post-composer.tsx +122 -0
  334. package/blocks/social-ads/components/social-post-list.tsx +116 -0
  335. package/blocks/social-ads/hooks/use-social-accounts.ts +16 -0
  336. package/blocks/social-ads/lib/social-ads-types.ts +167 -0
  337. package/blocks/social-ads/recipes/creative-to-gallery.ts +41 -0
  338. package/blocks/social-ads/recipes/schedule-post.ts +51 -0
  339. package/blocks/social-ads/recipes/sync-audience-and-launch.ts +41 -0
  340. package/blocks/visitors/README.md +56 -0
  341. package/blocks/visitors/block.json +139 -0
  342. package/blocks/visitors/components/snippet-install.tsx +169 -0
  343. package/blocks/visitors/components/traffic-chart.tsx +156 -0
  344. package/blocks/visitors/components/visitor-company-card.tsx +92 -0
  345. package/blocks/visitors/components/visitor-feed.tsx +157 -0
  346. package/blocks/visitors/lib/visitors-utils.ts +78 -0
  347. package/blocks/visitors/recipes/check-snippet-installed.ts +24 -0
  348. package/blocks/visitors/recipes/recent-company-visits.ts +27 -0
  349. package/blocks/visitors/recipes/traffic-summary.ts +24 -0
  350. package/blocks/webchat/README.md +87 -0
  351. package/blocks/webchat/block.json +134 -0
  352. package/blocks/webchat/components/chat-widget-config.tsx +160 -0
  353. package/blocks/webchat/components/conversation-list.tsx +74 -0
  354. package/blocks/webchat/lib/webchat-types.ts +83 -0
  355. package/blocks/webchat/recipes/create-bot-and-style-widget.ts +24 -0
  356. package/blocks/webchat/recipes/list-chat-visitors.ts +24 -0
  357. package/blocks/workbench/README.md +135 -0
  358. package/blocks/workbench/block.json +259 -0
  359. package/blocks/workbench/components/column-recipe-editor.tsx +183 -0
  360. package/blocks/workbench/components/run-progress.tsx +132 -0
  361. package/blocks/workbench/components/workbench-add-rows.tsx +100 -0
  362. package/blocks/workbench/components/workbench-grid.tsx +159 -0
  363. package/blocks/workbench/components/workbench-row-actions.tsx +102 -0
  364. package/blocks/workbench/components/workbench-table-list.tsx +105 -0
  365. package/blocks/workbench/hooks/use-workbench-table.ts +30 -0
  366. package/blocks/workbench/lib/waterfall-presets.ts +119 -0
  367. package/blocks/workbench/lib/workbench-types.ts +191 -0
  368. package/blocks/workbench/recipes/build-enriched-table.ts +65 -0
  369. package/blocks/workbench/recipes/import-from-search.ts +42 -0
  370. package/blocks/workbench/recipes/scrape-into-table.ts +36 -0
  371. package/blocks/workbench/recipes/waterfall-column.ts +55 -0
  372. package/blocks/workflows/README.md +100 -0
  373. package/blocks/workflows/block.json +188 -0
  374. package/blocks/workflows/components/run-history.tsx +136 -0
  375. package/blocks/workflows/components/trigger-config.tsx +134 -0
  376. package/blocks/workflows/components/workflow-editor.tsx +142 -0
  377. package/blocks/workflows/components/workflow-list.tsx +153 -0
  378. package/blocks/workflows/hooks/use-workflow.ts +51 -0
  379. package/blocks/workflows/lib/graph.ts +187 -0
  380. package/blocks/workflows/recipes/create-workflow.ts +35 -0
  381. package/blocks/workflows/recipes/find-failed-runs.ts +15 -0
  382. package/blocks/workflows/recipes/run-workflow-and-wait.ts +32 -0
  383. package/blocks/workflows/recipes/set-trigger.ts +22 -0
  384. package/dist/cli.js +5330 -29
  385. package/dist/index.browser.js +7740 -0
  386. package/dist/index.browser.js.map +1 -0
  387. package/dist/index.browser.mjs +7717 -0
  388. package/dist/index.browser.mjs.map +1 -0
  389. package/dist/index.d.mts +2220 -23
  390. package/dist/index.d.ts +2220 -23
  391. package/dist/index.js +64 -3
  392. package/dist/index.js.map +1 -1
  393. package/dist/index.mjs +64 -3
  394. package/dist/index.mjs.map +1 -1
  395. package/dist/react.browser.js +7598 -0
  396. package/dist/react.browser.js.map +1 -0
  397. package/dist/react.browser.mjs +7568 -0
  398. package/dist/react.browser.mjs.map +1 -0
  399. package/dist/react.d.mts +2082 -20
  400. package/dist/react.d.ts +2082 -20
  401. package/dist/react.js +63 -2
  402. package/dist/react.js.map +1 -1
  403. package/dist/react.mjs +63 -2
  404. package/dist/react.mjs.map +1 -1
  405. package/dist/types/agentSetup.d.ts +6 -6
  406. package/dist/types/blocks/add.d.ts +53 -0
  407. package/dist/types/blocks/create.d.ts +46 -0
  408. package/dist/types/blocks/doctor.d.ts +26 -0
  409. package/dist/types/blocks/registry.d.ts +134 -0
  410. package/dist/types/cli.d.ts +16 -0
  411. package/dist/types/core.d.ts +1 -1
  412. package/dist/types/crypto-browser.d.ts +2 -0
  413. package/dist/types/crypto-node.d.ts +6 -0
  414. package/dist/types/react.d.ts +1 -1
  415. package/dist/types/version.d.ts +2 -2
  416. package/dist/types/webhooks.d.ts +2 -2
  417. package/dist/ui/index.d.mts +54 -0
  418. package/dist/ui/index.d.ts +54 -0
  419. package/dist/ui/index.js +105 -0
  420. package/dist/ui/index.js.map +1 -0
  421. package/dist/ui/index.mjs +44 -0
  422. package/dist/ui/index.mjs.map +1 -0
  423. package/dist/ui/lib/compose-refs.d.mts +19 -0
  424. package/dist/ui/lib/compose-refs.d.ts +19 -0
  425. package/dist/ui/lib/compose-refs.js +76 -0
  426. package/dist/ui/lib/compose-refs.js.map +1 -0
  427. package/dist/ui/lib/compose-refs.mjs +41 -0
  428. package/dist/ui/lib/compose-refs.mjs.map +1 -0
  429. package/dist/ui/lib/use-mobile.d.mts +4 -0
  430. package/dist/ui/lib/use-mobile.d.ts +4 -0
  431. package/dist/ui/lib/use-mobile.js +57 -0
  432. package/dist/ui/lib/use-mobile.js.map +1 -0
  433. package/dist/ui/lib/use-mobile.mjs +22 -0
  434. package/dist/ui/lib/use-mobile.mjs.map +1 -0
  435. package/dist/ui/lib/utils.d.mts +10 -0
  436. package/dist/ui/lib/utils.d.ts +10 -0
  437. package/dist/ui/lib/utils.js +33 -0
  438. package/dist/ui/lib/utils.js.map +1 -0
  439. package/dist/ui/lib/utils.mjs +9 -0
  440. package/dist/ui/lib/utils.mjs.map +1 -0
  441. package/dist/ui/primitives/alert-dialog.d.mts +22 -0
  442. package/dist/ui/primitives/alert-dialog.d.ts +22 -0
  443. package/dist/ui/primitives/alert-dialog.js +163 -0
  444. package/dist/ui/primitives/alert-dialog.js.map +1 -0
  445. package/dist/ui/primitives/alert-dialog.mjs +119 -0
  446. package/dist/ui/primitives/alert-dialog.mjs.map +1 -0
  447. package/dist/ui/primitives/avatar.d.mts +8 -0
  448. package/dist/ui/primitives/avatar.d.ts +8 -0
  449. package/dist/ui/primitives/avatar.js +91 -0
  450. package/dist/ui/primitives/avatar.js.map +1 -0
  451. package/dist/ui/primitives/avatar.mjs +55 -0
  452. package/dist/ui/primitives/avatar.mjs.map +1 -0
  453. package/dist/ui/primitives/breadcrumb.d.mts +13 -0
  454. package/dist/ui/primitives/breadcrumb.d.ts +13 -0
  455. package/dist/ui/primitives/breadcrumb.js +127 -0
  456. package/dist/ui/primitives/breadcrumb.js.map +1 -0
  457. package/dist/ui/primitives/breadcrumb.mjs +97 -0
  458. package/dist/ui/primitives/breadcrumb.mjs.map +1 -0
  459. package/dist/ui/primitives/button.d.mts +15 -0
  460. package/dist/ui/primitives/button.d.ts +15 -0
  461. package/dist/ui/primitives/button.js +91 -0
  462. package/dist/ui/primitives/button.js.map +1 -0
  463. package/dist/ui/primitives/button.mjs +56 -0
  464. package/dist/ui/primitives/button.mjs.map +1 -0
  465. package/dist/ui/primitives/card.d.mts +10 -0
  466. package/dist/ui/primitives/card.d.ts +10 -0
  467. package/dist/ui/primitives/card.js +101 -0
  468. package/dist/ui/primitives/card.js.map +1 -0
  469. package/dist/ui/primitives/card.mjs +62 -0
  470. package/dist/ui/primitives/card.mjs.map +1 -0
  471. package/dist/ui/primitives/carousel.d.mts +45 -0
  472. package/dist/ui/primitives/carousel.d.ts +45 -0
  473. package/dist/ui/primitives/carousel.js +300 -0
  474. package/dist/ui/primitives/carousel.js.map +1 -0
  475. package/dist/ui/primitives/carousel.mjs +261 -0
  476. package/dist/ui/primitives/carousel.mjs.map +1 -0
  477. package/dist/ui/primitives/chart.d.mts +68 -0
  478. package/dist/ui/primitives/chart.d.ts +68 -0
  479. package/dist/ui/primitives/chart.js +280 -0
  480. package/dist/ui/primitives/chart.js.map +1 -0
  481. package/dist/ui/primitives/chart.mjs +241 -0
  482. package/dist/ui/primitives/chart.mjs.map +1 -0
  483. package/dist/ui/primitives/collapsible.d.mts +8 -0
  484. package/dist/ui/primitives/collapsible.d.ts +8 -0
  485. package/dist/ui/primitives/collapsible.js +72 -0
  486. package/dist/ui/primitives/collapsible.js.map +1 -0
  487. package/dist/ui/primitives/collapsible.mjs +36 -0
  488. package/dist/ui/primitives/collapsible.mjs.map +1 -0
  489. package/dist/ui/primitives/context-menu.d.mts +29 -0
  490. package/dist/ui/primitives/context-menu.d.ts +29 -0
  491. package/dist/ui/primitives/context-menu.js +203 -0
  492. package/dist/ui/primitives/context-menu.js.map +1 -0
  493. package/dist/ui/primitives/context-menu.mjs +155 -0
  494. package/dist/ui/primitives/context-menu.mjs.map +1 -0
  495. package/dist/ui/primitives/dialog.d.mts +26 -0
  496. package/dist/ui/primitives/dialog.d.ts +26 -0
  497. package/dist/ui/primitives/dialog.js +192 -0
  498. package/dist/ui/primitives/dialog.js.map +1 -0
  499. package/dist/ui/primitives/dialog.mjs +148 -0
  500. package/dist/ui/primitives/dialog.mjs.map +1 -0
  501. package/dist/ui/primitives/drawer.d.mts +26 -0
  502. package/dist/ui/primitives/drawer.d.ts +26 -0
  503. package/dist/ui/primitives/drawer.js +146 -0
  504. package/dist/ui/primitives/drawer.js.map +1 -0
  505. package/dist/ui/primitives/drawer.mjs +103 -0
  506. package/dist/ui/primitives/drawer.mjs.map +1 -0
  507. package/dist/ui/primitives/input.d.mts +5 -0
  508. package/dist/ui/primitives/input.d.ts +5 -0
  509. package/dist/ui/primitives/input.js +46 -0
  510. package/dist/ui/primitives/input.js.map +1 -0
  511. package/dist/ui/primitives/input.mjs +22 -0
  512. package/dist/ui/primitives/input.mjs.map +1 -0
  513. package/dist/ui/primitives/item.d.mts +27 -0
  514. package/dist/ui/primitives/item.d.ts +27 -0
  515. package/dist/ui/primitives/item.js +221 -0
  516. package/dist/ui/primitives/item.js.map +1 -0
  517. package/dist/ui/primitives/item.mjs +188 -0
  518. package/dist/ui/primitives/item.mjs.map +1 -0
  519. package/dist/ui/primitives/kbd.d.mts +15 -0
  520. package/dist/ui/primitives/kbd.d.ts +15 -0
  521. package/dist/ui/primitives/kbd.js +45 -0
  522. package/dist/ui/primitives/kbd.js.map +1 -0
  523. package/dist/ui/primitives/kbd.mjs +21 -0
  524. package/dist/ui/primitives/kbd.mjs.map +1 -0
  525. package/dist/ui/primitives/label.d.mts +6 -0
  526. package/dist/ui/primitives/label.d.ts +6 -0
  527. package/dist/ui/primitives/label.js +55 -0
  528. package/dist/ui/primitives/label.js.map +1 -0
  529. package/dist/ui/primitives/label.mjs +21 -0
  530. package/dist/ui/primitives/label.mjs.map +1 -0
  531. package/dist/ui/primitives/resizable.d.mts +10 -0
  532. package/dist/ui/primitives/resizable.d.ts +10 -0
  533. package/dist/ui/primitives/resizable.js +68 -0
  534. package/dist/ui/primitives/resizable.js.map +1 -0
  535. package/dist/ui/primitives/resizable.mjs +42 -0
  536. package/dist/ui/primitives/resizable.mjs.map +1 -0
  537. package/dist/ui/primitives/save-status.d.mts +31 -0
  538. package/dist/ui/primitives/save-status.d.ts +31 -0
  539. package/dist/ui/primitives/save-status.js +150 -0
  540. package/dist/ui/primitives/save-status.js.map +1 -0
  541. package/dist/ui/primitives/save-status.mjs +126 -0
  542. package/dist/ui/primitives/save-status.mjs.map +1 -0
  543. package/dist/ui/primitives/separator.d.mts +6 -0
  544. package/dist/ui/primitives/separator.d.ts +6 -0
  545. package/dist/ui/primitives/separator.js +62 -0
  546. package/dist/ui/primitives/separator.js.map +1 -0
  547. package/dist/ui/primitives/separator.mjs +28 -0
  548. package/dist/ui/primitives/separator.mjs.map +1 -0
  549. package/dist/ui/primitives/sheet.d.mts +16 -0
  550. package/dist/ui/primitives/sheet.d.ts +16 -0
  551. package/dist/ui/primitives/sheet.js +170 -0
  552. package/dist/ui/primitives/sheet.js.map +1 -0
  553. package/dist/ui/primitives/sheet.mjs +129 -0
  554. package/dist/ui/primitives/sheet.mjs.map +1 -0
  555. package/dist/ui/primitives/sidebar.d.mts +74 -0
  556. package/dist/ui/primitives/sidebar.d.ts +74 -0
  557. package/dist/ui/primitives/sidebar.js +697 -0
  558. package/dist/ui/primitives/sidebar.js.map +1 -0
  559. package/dist/ui/primitives/sidebar.mjs +651 -0
  560. package/dist/ui/primitives/sidebar.mjs.map +1 -0
  561. package/dist/ui/primitives/skeleton.d.mts +5 -0
  562. package/dist/ui/primitives/skeleton.d.ts +5 -0
  563. package/dist/ui/primitives/skeleton.js +40 -0
  564. package/dist/ui/primitives/skeleton.js.map +1 -0
  565. package/dist/ui/primitives/skeleton.mjs +16 -0
  566. package/dist/ui/primitives/skeleton.mjs.map +1 -0
  567. package/dist/ui/primitives/slider.d.mts +20 -0
  568. package/dist/ui/primitives/slider.d.ts +20 -0
  569. package/dist/ui/primitives/slider.js +112 -0
  570. package/dist/ui/primitives/slider.js.map +1 -0
  571. package/dist/ui/primitives/slider.mjs +78 -0
  572. package/dist/ui/primitives/slider.mjs.map +1 -0
  573. package/dist/ui/primitives/sortable.d.mts +50 -0
  574. package/dist/ui/primitives/sortable.d.ts +50 -0
  575. package/dist/ui/primitives/sortable.js +456 -0
  576. package/dist/ui/primitives/sortable.js.map +1 -0
  577. package/dist/ui/primitives/sortable.mjs +435 -0
  578. package/dist/ui/primitives/sortable.mjs.map +1 -0
  579. package/dist/ui/primitives/switch.d.mts +6 -0
  580. package/dist/ui/primitives/switch.d.ts +6 -0
  581. package/dist/ui/primitives/switch.js +67 -0
  582. package/dist/ui/primitives/switch.js.map +1 -0
  583. package/dist/ui/primitives/switch.mjs +33 -0
  584. package/dist/ui/primitives/switch.mjs.map +1 -0
  585. package/dist/ui/primitives/table.d.mts +12 -0
  586. package/dist/ui/primitives/table.d.ts +12 -0
  587. package/dist/ui/primitives/table.js +130 -0
  588. package/dist/ui/primitives/table.js.map +1 -0
  589. package/dist/ui/primitives/table.mjs +89 -0
  590. package/dist/ui/primitives/table.mjs.map +1 -0
  591. package/dist/ui/primitives/tabs.d.mts +9 -0
  592. package/dist/ui/primitives/tabs.d.ts +9 -0
  593. package/dist/ui/primitives/tabs.js +86 -0
  594. package/dist/ui/primitives/tabs.js.map +1 -0
  595. package/dist/ui/primitives/tabs.mjs +49 -0
  596. package/dist/ui/primitives/tabs.mjs.map +1 -0
  597. package/dist/ui/primitives/textarea.d.mts +5 -0
  598. package/dist/ui/primitives/textarea.d.ts +5 -0
  599. package/dist/ui/primitives/textarea.js +43 -0
  600. package/dist/ui/primitives/textarea.js.map +1 -0
  601. package/dist/ui/primitives/textarea.mjs +19 -0
  602. package/dist/ui/primitives/textarea.mjs.map +1 -0
  603. package/dist/ui/primitives/tooltip.d.mts +9 -0
  604. package/dist/ui/primitives/tooltip.d.ts +9 -0
  605. package/dist/ui/primitives/tooltip.js +94 -0
  606. package/dist/ui/primitives/tooltip.js.map +1 -0
  607. package/dist/ui/primitives/tooltip.mjs +57 -0
  608. package/dist/ui/primitives/tooltip.mjs.map +1 -0
  609. package/docs/agents.md +72 -29
  610. package/docs/api/README.md +7 -3
  611. package/docs/api/appObjects.d.ts +3 -1
  612. package/docs/api/appObjects.md +2 -1
  613. package/docs/api/by-path.md +51 -0
  614. package/docs/api/dataPipelines.d.ts +149 -0
  615. package/docs/api/dataPipelines.md +232 -0
  616. package/docs/api/forms.d.ts +16 -0
  617. package/docs/api/forms.md +8 -0
  618. package/docs/api/objects.d.ts +705 -5
  619. package/docs/api/objects.md +1099 -14
  620. package/docs/api/ops.d.ts +1 -1
  621. package/docs/api/ops.md +1 -1
  622. package/docs/api/processLists.d.ts +247 -0
  623. package/docs/api/processLists.md +409 -0
  624. package/docs/api/quotes.d.ts +3 -1
  625. package/docs/api/quotes.md +7 -2
  626. package/docs/api/radar.d.ts +4 -0
  627. package/docs/api/radar.md +1 -0
  628. package/docs/api/reports.d.ts +476 -0
  629. package/docs/api/reports.md +539 -0
  630. package/docs/api/scheduleBuy.d.ts +12 -8
  631. package/docs/api/scheduleBuy.md +13 -10
  632. package/docs/api/seo.d.ts +406 -3
  633. package/docs/api/seo.md +505 -1
  634. package/docs/api/workspaceBackup.d.ts +324 -0
  635. package/docs/api/workspaceBackup.md +427 -0
  636. package/package.json +174 -5
  637. package/stacks/6sense.json +46 -0
  638. package/stacks/README.md +20 -0
  639. package/stacks/apollo.json +128 -0
  640. package/stacks/clay.json +59 -0
  641. package/stacks/gong.json +72 -0
  642. package/stacks/hubspot.json +162 -0
  643. package/stacks/intercom.json +53 -0
  644. package/stacks/mailchimp.json +71 -0
  645. package/stacks/outreach.json +86 -0
  646. package/stacks/salesforce.json +94 -0
  647. package/stacks/stack.schema.json +32 -0
  648. package/stacks/starter.json +22 -0
  649. package/styles/styles.css +2 -0
  650. package/styles/theme.css +181 -0
  651. package/styles/tokens.css +284 -0
  652. package/templates/next-app/README.md.tmpl +35 -0
  653. package/templates/next-app/_pages/redirect-page.tsx.tmpl +5 -0
  654. package/templates/next-app/_pages/route-page.tsx.tmpl +9 -0
  655. package/templates/next-app/app/globals.css.tmpl +8 -0
  656. package/templates/next-app/app/layout.tsx.tmpl +19 -0
  657. package/templates/next-app/components/app-shell.tsx.tmpl +34 -0
  658. package/templates/next-app/dot-env.local.example.tmpl +6 -0
  659. package/templates/next-app/dot-gitignore.tmpl +7 -0
  660. package/templates/next-app/lib/nav.ts.tmpl +2 -0
  661. package/templates/next-app/next.config.mjs.tmpl +4 -0
  662. package/templates/next-app/package.json.tmpl +25 -0
  663. package/templates/next-app/postcss.config.mjs.tmpl +4 -0
  664. package/templates/next-app/tsconfig.json.tmpl +21 -0
@@ -0,0 +1,78 @@
1
+ # deals: pipelines and revenue
2
+
3
+ A kanban of deals by stage with drag and drop, a deal detail with history, a stage editor, and a forecast.
4
+ Install with `npx @graph8/sdk add deals` (depends on `crm`, whose notes, tasks and activity timeline you can
5
+ compose under `DealDetail`'s `children`). Drag and drop needs `npm i @dnd-kit/core`.
6
+
7
+ ## The flow, in order
8
+
9
+ 1. **Pipelines** `GET /deals/pipelines` (`usePipelines`): `[{ id, name, is_default, stages: [{ id, name,
10
+ position, probability, stage_type, color }] }]`. **Stage ids only ever come from here.** `stage_type` is
11
+ free text: `"won"` and `"lost"` close a deal, anything else is open.
12
+ 2. **Deals** `GET /deals?pipeline_id=&page=&limit=` (`usePipelineDeals`): each deal has `id` (a UUID string),
13
+ `stage_id`, `amount`, `close_date`, `owner_*`, and `revision`.
14
+ 3. **Move** `PATCH /deals/{id}` with `{ stage_id, expected_revision }` (`PipelineBoard`). It is a plain write:
15
+ no confirm dialog. The board updates optimistically and rolls back if the API refuses.
16
+ 4. **Closing**: a move into a won/lost stage needs a close date (past dates are allowed), an amount for
17
+ won, and an owner. The board prompts for the date and amount first; if the API still answers 422 with
18
+ `close_date_required_for_won_stage`, `amount_required_for_won_stage` or `owner_required_for_closed_stage`,
19
+ the board shows what is missing and rolls back.
20
+ 5. **Create / edit** (`DealDetail`): `POST /deals` needs `name`, `owner_id` and `contact_ids` (a list, may be
21
+ empty); `stage_id` defaults to the first stage of the default pipeline. Edits send `expected_revision`;
22
+ a stale revision is refused, so reload and retry. `DELETE /deals/{id}` is destructive and confirmed.
23
+ 6. **Configure** (`StageEditor`): `GET /pipelines/{id}`, `POST .../stages`, `PUT .../stages/{stage_id}`,
24
+ `PUT .../stages/reorder` (send every stage id in the new order), `DELETE .../stages/{stage_id}` (destructive,
25
+ confirmed; refused while deals remain in it). These live under the `campaigns:*` scopes.
26
+ 7. **Forecast** (`ForecastView`): `GET /deals/analytics`, `GET /deals/stage-velocity`, and open deals for the
27
+ weighted estimate.
28
+
29
+ IDs passed between steps: `pipeline.id` (list -> deals filter -> stage editor), `stage.id` (columns -> move,
30
+ create), `deal.id` + `deal.revision` (card -> detail -> update/delete).
31
+
32
+ ## Prerequisites a human must do
33
+
34
+ - None in the graph8 app for the basic board: a default pipeline exists.
35
+ - Give the key `deals:read` / `deals:write` (and `deals:delete` to delete). StageEditor needs `campaigns:read`,
36
+ `campaigns:write` and `campaigns:delete`.
37
+ - `npm i @dnd-kit/core`.
38
+
39
+ ## Defaults (the agent does not need to ask)
40
+
41
+ - The board shows the default pipeline. Moves save immediately; no confirm dialog (nobody is contacted).
42
+ - Every card has a "Move to" select, the keyboard and screen-reader path, doing the same PATCH as dragging.
43
+ - Ask the human only who should **own deals created in the UI** (the API requires an owner).
44
+
45
+ ## Gotchas
46
+
47
+ - The analytics endpoint returns **no weighted pipeline** on purpose. `ForecastView` computes one from open deals
48
+ and stage probabilities, labels it an estimate and counts deals whose stage has no probability (an unset
49
+ probability is 0). Set probabilities in `StageEditor`.
50
+ - Mixed currencies: when `currency.is_mixed` is true the analytics withhold sums; the view shows counts only.
51
+ - `GET /deals/pipelines` (stage ids used by deals) and `GET /pipelines` (pipeline management) are different
52
+ routes returning the same pipeline objects; the board uses the first, the editor the second.
53
+ - `POST /deals/bulk` has PUT semantics: `""` clears a field, `null` leaves it alone (see recipe 03).
54
+
55
+ ## Operations
56
+
57
+ | Operation | Method and path | Tier | Used by |
58
+ |---|---|---|---|
59
+ | `deals.listPipelines` | GET /deals/pipelines | read | usePipelines |
60
+ | `deals.listDeals` | GET /deals | read | usePipelineDeals, ForecastView |
61
+ | `deals.updateDeal` | PATCH /deals/{id} | write | PipelineBoard, DealDetail |
62
+ | `deals.getDeal` | GET /deals/{id} | read | DealDetail |
63
+ | `deals.listDealHistory` | GET /deals/{id}/history | read | DealDetail |
64
+ | `deals.createDeal` | POST /deals | write | DealDetail |
65
+ | `deals.deleteDeal` | DELETE /deals/{id} | destructive | DealDetail |
66
+ | `cliPipelines.getPipeline` | GET /pipelines/{id} | read | StageEditor |
67
+ | `cliPipelines.createStage` | POST /pipelines/{id}/stages | write | StageEditor |
68
+ | `cliPipelines.updateStage` | PUT /pipelines/{id}/stages/{stage_id} | write | StageEditor |
69
+ | `cliPipelines.reorderStages` | PUT /pipelines/{id}/stages/reorder | write | StageEditor |
70
+ | `cliPipelines.deleteStage` | DELETE /pipelines/{id}/stages/{stage_id} | destructive | StageEditor |
71
+ | `deals.getDealAnalytics` | GET /deals/analytics | read | ForecastView |
72
+ | `deals.getDealStageVelocity` | GET /deals/stage-velocity | read | ForecastView |
73
+
74
+ ## Recipes (server side)
75
+
76
+ - `01-create-deal-and-advance.ts`: create, move, close as won.
77
+ - `02-pipeline-report.ts`: totals, win rate, stall points, estimated weighted forecast.
78
+ - `03-stale-deals-sweep.ts`: find neglected open deals, optionally flag them with one bulk write.
@@ -0,0 +1,198 @@
1
+ {
2
+ "$schema": "../block.schema.json",
3
+ "id": "deals",
4
+ "title": "Deals & pipelines",
5
+ "summary": "Revenue pipeline: drag-and-drop kanban, deal detail and history, stage configuration and a forecast with stage velocity.",
6
+ "features": [
7
+ "kanban board with drag and drop plus a keyboard Move to control",
8
+ "optimistic stage moves with rollback and closed-stage rules handled",
9
+ "deal create, edit, stage history and confirmed delete",
10
+ "stage editor: names, probabilities, won/lost types, order",
11
+ "forecast: totals, win rate, stuck deals and an estimated weighted pipeline"
12
+ ],
13
+ "dependsOn": [
14
+ "crm"
15
+ ],
16
+ "operations": [
17
+ "create_deal_deals_post",
18
+ "create_stage_pipelines__pipeline_id__stages_post",
19
+ "delete_deal_deals__deal_id__delete",
20
+ "delete_stage_pipelines__pipeline_id__stages__stage_id__delete",
21
+ "get_deal_analytics_deals_analytics_get",
22
+ "get_deal_deals__deal_id__get",
23
+ "get_deal_stage_velocity_deals_stage_velocity_get",
24
+ "get_pipeline_pipelines__pipeline_id__get",
25
+ "list_deal_history_deals__deal_id__history_get",
26
+ "list_deals_deals_get",
27
+ "list_pipelines_deals_pipelines_get",
28
+ "reorder_stages_pipelines__pipeline_id__stages_reorder_put",
29
+ "update_deal_deals__deal_id__patch",
30
+ "update_stage_pipelines__pipeline_id__stages__stage_id__put"
31
+ ],
32
+ "components": [
33
+ {
34
+ "name": "PipelineBoard",
35
+ "file": "components/pipeline-board.tsx",
36
+ "purpose": "Kanban of deals by stage; drag a card (or use Move to) to change its stage, with a close-date/amount prompt for won and lost.",
37
+ "operations": [
38
+ "list_deals_deals_get",
39
+ "list_pipelines_deals_pipelines_get",
40
+ "update_deal_deals__deal_id__patch"
41
+ ],
42
+ "tiers": [
43
+ "read",
44
+ "write"
45
+ ]
46
+ },
47
+ {
48
+ "name": "DealDetail",
49
+ "file": "components/deal-detail.tsx",
50
+ "purpose": "Create or edit a deal with stage, amount and close date; contacts, stage history and a confirmed delete.",
51
+ "operations": [
52
+ "create_deal_deals_post",
53
+ "delete_deal_deals__deal_id__delete",
54
+ "get_deal_deals__deal_id__get",
55
+ "list_deal_history_deals__deal_id__history_get",
56
+ "list_pipelines_deals_pipelines_get",
57
+ "update_deal_deals__deal_id__patch"
58
+ ],
59
+ "tiers": [
60
+ "read",
61
+ "write",
62
+ "destructive"
63
+ ]
64
+ },
65
+ {
66
+ "name": "StageEditor",
67
+ "file": "components/stage-editor.tsx",
68
+ "purpose": "Rename, reorder, add and delete stages and set win probability and won/lost type.",
69
+ "operations": [
70
+ "create_stage_pipelines__pipeline_id__stages_post",
71
+ "delete_stage_pipelines__pipeline_id__stages__stage_id__delete",
72
+ "get_pipeline_pipelines__pipeline_id__get",
73
+ "reorder_stages_pipelines__pipeline_id__stages_reorder_put",
74
+ "update_stage_pipelines__pipeline_id__stages__stage_id__put"
75
+ ],
76
+ "tiers": [
77
+ "read",
78
+ "write",
79
+ "destructive"
80
+ ]
81
+ },
82
+ {
83
+ "name": "ForecastView",
84
+ "file": "components/forecast-view.tsx",
85
+ "purpose": "Pipeline totals, win rate, deals by stage, stage velocity, stuck deals and a labelled weighted-pipeline estimate.",
86
+ "operations": [
87
+ "get_deal_analytics_deals_analytics_get",
88
+ "get_deal_stage_velocity_deals_stage_velocity_get",
89
+ "list_deals_deals_get",
90
+ "list_pipelines_deals_pipelines_get"
91
+ ],
92
+ "tiers": [
93
+ "read"
94
+ ]
95
+ }
96
+ ],
97
+ "hooks": [
98
+ {
99
+ "name": "usePipelines",
100
+ "file": "hooks/use-pipelines.ts",
101
+ "purpose": "Every pipeline with ordered stages; the only source of stage ids.",
102
+ "operations": [
103
+ "list_pipelines_deals_pipelines_get"
104
+ ]
105
+ },
106
+ {
107
+ "name": "usePipelineDeals",
108
+ "file": "hooks/use-pipeline-deals.ts",
109
+ "purpose": "Deals of one pipeline (all pages, capped) with an optimistic stage move that rolls back on refusal.",
110
+ "operations": [
111
+ "list_deals_deals_get",
112
+ "update_deal_deals__deal_id__patch"
113
+ ]
114
+ }
115
+ ],
116
+ "lib": [
117
+ {
118
+ "file": "lib/deals-types.ts",
119
+ "purpose": "Deal, pipeline and stage types, stage kind, money formatting, weighted-pipeline estimate and closed-stage error markers."
120
+ }
121
+ ],
122
+ "recipes": [
123
+ {
124
+ "file": "recipes/01-create-deal-and-advance.ts",
125
+ "title": "Create a deal and move it through the pipeline",
126
+ "when": "Tracking revenue from an agent or webhook; includes closing as won with the required fields."
127
+ },
128
+ {
129
+ "file": "recipes/02-pipeline-report.ts",
130
+ "title": "Pipeline report with an estimated weighted forecast",
131
+ "when": "A weekly summary; read-only."
132
+ },
133
+ {
134
+ "file": "recipes/03-stale-deals-sweep.ts",
135
+ "title": "Find open deals with no recent activity and flag them",
136
+ "when": "A nightly job; list is read-only, the flag is one bulk write."
137
+ }
138
+ ],
139
+ "prerequisites": [
140
+ {
141
+ "id": "pipeline-exists",
142
+ "description": "At least one pipeline with stages. New orgs have a default pipeline; custom stages are set up with StageEditor.",
143
+ "where": "api",
144
+ "verify": "GET /deals/pipelines returns a pipeline with stages and one with is_default true."
145
+ },
146
+ {
147
+ "id": "deals-scopes",
148
+ "description": "Key scopes deals:read and deals:write for the board, detail and forecast (deals:delete to delete a deal); campaigns:read, campaigns:write and campaigns:delete for StageEditor (pipelines are managed under the campaigns scopes).",
149
+ "where": "api",
150
+ "verify": "`g8.api.me.describeCurrentKey()` lists deals:read and deals:write (and campaigns:* if you use StageEditor)."
151
+ },
152
+ {
153
+ "id": "dnd-kit",
154
+ "description": "The drag-and-drop packages: `npm i @dnd-kit/core`. The Move to select works without dragging.",
155
+ "where": "app",
156
+ "verify": "`import \"@dnd-kit/core\"` resolves."
157
+ }
158
+ ],
159
+ "scopes": [
160
+ "campaigns:delete",
161
+ "campaigns:read",
162
+ "campaigns:write",
163
+ "deals:delete",
164
+ "deals:read",
165
+ "deals:write"
166
+ ],
167
+ "defaults": [
168
+ {
169
+ "decision": "Which pipeline the board shows",
170
+ "default": "The org's default pipeline (is_default), or the pipelineId prop",
171
+ "askHuman": false
172
+ },
173
+ {
174
+ "decision": "How a stage move is saved",
175
+ "default": "Immediately and optimistically; rolled back with the API's message if refused. No confirm dialog: it contacts nobody and is easy to undo",
176
+ "askHuman": false
177
+ },
178
+ {
179
+ "decision": "What a card shows",
180
+ "default": "Name, amount, owner, close date and a Move to control",
181
+ "askHuman": false
182
+ },
183
+ {
184
+ "decision": "Who owns deals created in the UI",
185
+ "default": "The signed-in user of your app: pass their graph8 user id as ownerId (the API requires an owner)",
186
+ "askHuman": true
187
+ },
188
+ {
189
+ "decision": "Weighted forecast",
190
+ "default": "Computed client-side from stage probabilities and labelled an estimate; the API gives no weighted total",
191
+ "askHuman": false
192
+ }
193
+ ],
194
+ "peerDependencies": {
195
+ "react": ">=18",
196
+ "@dnd-kit/core": "^6.3.1"
197
+ }
198
+ }
@@ -0,0 +1,134 @@
1
+ "use client";
2
+
3
+ /**
4
+ * DealDetail: view, edit, create and delete one deal, with its contacts and stage history.
5
+ *
6
+ * SDK operations, in order:
7
+ * 1. GET /deals/pipelines (deals.listPipelines) read - the stage picker
8
+ * 2. GET /deals/{id} (deals.getDeal) read - edit mode
9
+ * 3. GET /deals/{id}/history (deals.listDealHistory) read
10
+ * 4. POST /deals (deals.createDeal) write - create mode (needs owner_id and contact_ids)
11
+ * PATCH /deals/{id} { expected_revision } (deals.updateDeal) write - a stale revision is refused (409), reload and retry
12
+ * 5. DELETE /deals/{id} (deals.deleteDeal) DESTRUCTIVE -> ConfirmSpendDialog first
13
+ * Compose notes, tasks and the activity timeline from the crm block under `children`.
14
+ */
15
+ import { useEffect, useState, type ReactNode } from "react";
16
+ import { Button, Input, Label, Skeleton, Textarea } from "@graph8/sdk/ui";
17
+ import { ApiError } from "@/g8/components/api-error";
18
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
19
+ import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
20
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
21
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
22
+ import { usePipelines } from "../hooks/use-pipelines";
23
+ import { dataOf, formatMoney, type Deal } from "../lib/deals-types";
24
+
25
+ export interface DealDetailProps {
26
+ /** Edit this deal; omit to create one. */
27
+ dealId?: string;
28
+ /** Create mode: the graph8 user id who owns the new deal (required by the API). */
29
+ ownerId?: string;
30
+ /** Create mode: contact ids to link (the API requires the list; it may be empty). */
31
+ contactIds?: number[];
32
+ pipelineId?: string;
33
+ onSaved?: (deal: Deal) => void;
34
+ onDeleted?: (dealId: string) => void;
35
+ children?: ReactNode;
36
+ className?: string;
37
+ }
38
+
39
+ interface HistoryItem {
40
+ from_stage_name?: string | null;
41
+ to_stage_name?: string | null;
42
+ changed_at?: string | null;
43
+ created_at?: string | null;
44
+ }
45
+
46
+ interface Form { name: string; amount: string; closeDate: string; stageId: string; description: string }
47
+ const EMPTY: Form = { name: "", amount: "", closeDate: "", stageId: "", description: "" };
48
+
49
+ function fromDeal(d: Deal): Form {
50
+ 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 ?? "" };
51
+ }
52
+
53
+ export function DealDetail({ dealId, ownerId, contactIds = [], pipelineId, onSaved, onDeleted, children, className }: DealDetailProps) {
54
+ const { pipelines, defaultPipeline } = usePipelines();
55
+ const [form, setForm] = useState<Form>(EMPTY);
56
+ const [confirmDelete, setConfirmDelete] = useState(false);
57
+ // Retry-safe create: once POST /deals succeeded, further saves edit that deal instead of creating another.
58
+ const [createdId, setCreatedId] = useState<string | null>(null);
59
+ const [createdRevision, setCreatedRevision] = useState<number | null>(null);
60
+ const effectiveId = dealId ?? createdId ?? undefined;
61
+ const deal = useG8Query(async (signal) => dataOf<Deal>(await g8Get(`/deals/${encodeURIComponent(dealId as string)}`, undefined, { signal })), [dealId], { enabled: Boolean(dealId) });
62
+ const history = useG8Query(async (signal) => dataOf<{ items?: HistoryItem[] }>(await g8Get(`/deals/${encodeURIComponent(dealId as string)}/history`, { limit: 20 }, { signal })).items ?? [], [dealId], { enabled: Boolean(dealId) });
63
+ useEffect(() => { if (deal.data) setForm(fromDeal(deal.data)); }, [deal.data]);
64
+ // A different deal (or create mode again) must not show the previous one's fields.
65
+ useEffect(() => { setForm(EMPTY); setCreatedId(null); setCreatedRevision(null); setConfirmDelete(false); }, [dealId]);
66
+
67
+ const pipeline = pipelines.find((p) => p.id === (deal.data?.pipeline_id ?? pipelineId)) ?? defaultPipeline;
68
+ const set = <K extends keyof Form>(k: K, v: Form[K]) => setForm((f) => ({ ...f, [k]: v }));
69
+
70
+ const save = useG8Mutation(async () => {
71
+ const body = {
72
+ name: form.name.trim(),
73
+ amount: form.amount === "" ? null : Number(form.amount),
74
+ close_date: form.closeDate || null, // date-only: YYYY-MM-DD, never toISOString()
75
+ description: form.description || null,
76
+ stage_id: form.stageId || undefined,
77
+ };
78
+ const env = effectiveId
79
+ ? await g8Patch(`/deals/${encodeURIComponent(effectiveId)}`, { ...body, expected_revision: (dealId ? deal.data?.revision : createdRevision) ?? undefined })
80
+ : await g8Post("/deals", { ...body, owner_id: ownerId, contact_ids: contactIds, pipeline_id: pipeline?.id, amount: body.amount ?? undefined, close_date: body.close_date ?? undefined });
81
+ return dataOf<Deal>(env);
82
+ }, { onSuccess: (saved) => { if (!dealId && saved.id) { setCreatedId(saved.id); setCreatedRevision(saved.revision ?? null); } if (dealId) { void deal.refetch(); void history.refetch(); } onSaved?.(saved); } });
83
+ const remove = useG8Mutation(async () => { await g8Delete(`/deals/${encodeURIComponent(dealId as string)}`); }, { onSuccess: () => { setConfirmDelete(false); onDeleted?.(dealId as string); }, onError: () => setConfirmDelete(false) });
84
+
85
+ if (deal.error) return <ApiError error={deal.error} title="Could not load the deal" onRetry={() => void deal.refetch()} />;
86
+ if (dealId && deal.loading && !deal.data) return <Skeleton className="h-56 w-full" aria-label="Loading deal" />;
87
+ const d = deal.data;
88
+ const canCreate = Boolean(ownerId);
89
+
90
+ return (
91
+ <div className={`space-y-4 ${className ?? ""}`}>
92
+ <form className="space-y-3" onSubmit={(e) => { e.preventDefault(); if (form.name.trim() && (dealId || canCreate)) void save.mutate(undefined); }}>
93
+ <div className="grid gap-3 sm:grid-cols-2">
94
+ <div className="space-y-1"><Label htmlFor="deal-name">Name</Label><Input id="deal-name" value={form.name} onChange={(e) => set("name", e.target.value)} /></div>
95
+ <div className="space-y-1"><Label htmlFor="deal-amount">Amount{d?.currency ? ` (${d.currency})` : ""}</Label><Input id="deal-amount" type="number" min={0} value={form.amount} onChange={(e) => set("amount", e.target.value)} /></div>
96
+ <div className="space-y-1"><Label htmlFor="deal-close">Close date</Label><Input id="deal-close" type="date" value={form.closeDate} onChange={(e) => set("closeDate", e.target.value)} /></div>
97
+ <div className="space-y-1">
98
+ <Label htmlFor="deal-stage">Stage</Label>
99
+ <select id="deal-stage" className="h-9 w-full rounded-md border bg-background px-2 text-sm" value={form.stageId} onChange={(e) => set("stageId", e.target.value)}>
100
+ <option value="">{dealId ? "Keep current stage" : "First stage"}</option>
101
+ {(pipeline?.stages ?? []).map((s) => <option key={s.id} value={s.id}>{s.name}</option>)}
102
+ </select>
103
+ </div>
104
+ </div>
105
+ <div className="space-y-1"><Label htmlFor="deal-desc">Description</Label><Textarea id="deal-desc" rows={3} value={form.description} onChange={(e) => set("description", e.target.value)} /></div>
106
+ {!dealId && !canCreate ? <p className="text-sm text-muted-foreground">Pass ownerId (a graph8 user id) to create deals: the API requires an owner.</p> : null}
107
+ {save.error ? <ApiError error={save.error} title="Could not save the deal" /> : null}
108
+ {remove.error ? <ApiError error={remove.error} title="Could not delete the deal" /> : null}
109
+ <div className="flex justify-between">
110
+ {dealId ? <Button type="button" variant="ghost" onClick={() => setConfirmDelete(true)}>Delete deal</Button> : <span />}
111
+ <Button type="submit" loading={save.loading} disabled={!form.name.trim() || (!effectiveId && !canCreate)}>{effectiveId ? "Save changes" : "Create deal"}</Button>
112
+ </div>
113
+ </form>
114
+
115
+ {d ? (
116
+ <section aria-label="Contacts" className="space-y-1 text-sm">
117
+ <h3 className="font-semibold">Contacts ({d.contact_count ?? d.contacts?.length ?? 0})</h3>
118
+ {(d.contacts ?? []).map((c) => <p key={c.id}>{c.name ?? c.email}{c.role ? ` · ${c.role}` : ""}{d.primary_contact?.id === c.id ? " (primary)" : ""}</p>)}
119
+ <p className="text-muted-foreground">Owner: {d.owner_name ?? d.owner_email ?? "none"} · {formatMoney(d.amount, d.currency)}</p>
120
+ </section>
121
+ ) : null}
122
+ {dealId ? (
123
+ <section aria-label="Stage history" className="space-y-1 text-sm">
124
+ <h3 className="font-semibold">Stage history</h3>
125
+ {history.error ? <ApiError error={history.error} title="Could not load history" onRetry={() => void history.refetch()} /> : null}
126
+ {(history.data ?? []).length === 0 && !history.loading ? <p className="text-muted-foreground">No stage changes yet.</p> : null}
127
+ {(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>)}
128
+ </section>
129
+ ) : null}
130
+ {children}
131
+ <ConfirmSpendDialog open={confirmDelete} tier="destructive" title={`Delete "${d?.name ?? "this deal"}"?`} description="The deal and its stage history are removed. Linked contacts and companies are kept." confirmLabel="Delete deal" affected={1} confirming={remove.loading} onConfirm={() => void remove.mutate(undefined)} onCancel={() => setConfirmDelete(false)} />
132
+ </div>
133
+ );
134
+ }
@@ -0,0 +1,112 @@
1
+ "use client";
2
+
3
+ /**
4
+ * ForecastView: pipeline totals, win rate, deals per stage, monthly won/lost trend, stuck deals and an ESTIMATED
5
+ * weighted forecast.
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. GET /deals/pipelines (deals.listPipelines) read - stage probabilities
9
+ * 2. GET /deals/analytics?pipeline_id= (deals.getDealAnalytics) read - totals, by_stage, trend
10
+ * 3. GET /deals/stage-velocity?pipeline_id= (deals.getDealStageVelocity) read - time in stage, stuck deals
11
+ * 4. GET /deals?pipeline_id=&outcome=open (deals.listDeals) read - to weight open deals
12
+ * The API deliberately returns NO weighted-pipeline number (a stage's probability defaults to 0, which would drop
13
+ * deals silently). The weighted figure here is computed from the open deals and each stage's probability and is
14
+ * labelled an estimate, with a count of deals whose stage has no probability set (see StageEditor).
15
+ */
16
+ import { useMemo } from "react";
17
+ import { Skeleton } from "@graph8/sdk/ui";
18
+ import { ApiError } from "@/g8/components/api-error";
19
+ import { EmptyState } from "@/g8/components/empty-state";
20
+ import { g8Get } from "@/g8/lib/g8-client";
21
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
22
+ import { usePipelines } from "../hooks/use-pipelines";
23
+ import { dataOf, formatMoney, weightedPipeline, type Deal } from "../lib/deals-types";
24
+
25
+ export interface ForecastViewProps {
26
+ pipelineId?: string;
27
+ /** Days without movement before a deal counts as stuck. */
28
+ stuckAfterDays?: number;
29
+ className?: string;
30
+ }
31
+
32
+ interface Analytics {
33
+ totals?: { deal_count: number; won_count: number; lost_count: number; open_count: number; won_value?: number | null; open_value?: number | null; win_rate?: number | null; avg_deal_size?: number | null };
34
+ currency?: { code?: string | null; is_mixed?: boolean };
35
+ by_stage?: Array<{ stage_id?: string | null; stage_name: string; deal_count: number; value?: number | null; outcome: string }>;
36
+ trend?: Array<{ period: string; won_count: number; won_value?: number | null; lost_count: number }>;
37
+ disclosures?: string[];
38
+ }
39
+ interface Velocity {
40
+ by_stage?: Array<{ stage_id: string; stage_name: string; avg_days_in_stage?: number | null; conversion_rate?: number | null; still_in_stage_count: number }>;
41
+ stuck_deals?: { total_count: number; threshold_days: number; deals?: Array<{ deal_id?: string; id?: string; name?: string; stage_name?: string; days_in_stage?: number }> };
42
+ }
43
+
44
+ function Tile({ label, value, hint }: { label: string; value: string; hint?: string }) {
45
+ return <div className="rounded-lg border p-3"><div className="text-xs uppercase text-muted-foreground">{label}</div><div className="text-lg font-semibold">{value}</div>{hint ? <div className="text-xs text-muted-foreground">{hint}</div> : null}</div>;
46
+ }
47
+
48
+ const MAX_OPEN_DEALS = 2000;
49
+
50
+ export function ForecastView({ pipelineId, stuckAfterDays = 14, className }: ForecastViewProps) {
51
+ const { pipelines, defaultPipeline, loading: loadingPipelines, error: pipelinesError } = usePipelines();
52
+ const pipeline = pipelines.find((p) => p.id === pipelineId) ?? defaultPipeline;
53
+ const id = pipeline?.id;
54
+ const analytics = useG8Query(async (s) => dataOf<Analytics>(await g8Get("/deals/analytics", { pipeline_id: id }, { signal: s })), [id], { enabled: Boolean(id) });
55
+ const velocity = useG8Query(async (s) => dataOf<Velocity>(await g8Get("/deals/stage-velocity", { pipeline_id: id, stuck_threshold_days: stuckAfterDays }, { signal: s })), [id, stuckAfterDays], { enabled: Boolean(id) });
56
+ const open = useG8Query(async (s) => {
57
+ const all: Deal[] = [];
58
+ // Every page, so the weighted figure is not silently low for pipelines with more than 100 open deals.
59
+ for (let page = 1; all.length < MAX_OPEN_DEALS; page++) {
60
+ const env = await g8Get<Deal[]>("/deals", { pipeline_id: id, outcome: "open", page, limit: 100 }, { signal: s });
61
+ all.push(...(env.data ?? []));
62
+ if (!env.pagination?.has_next) break;
63
+ }
64
+ return all;
65
+ }, [id], { enabled: Boolean(id) });
66
+ const weighted = useMemo(() => weightedPipeline(open.data ?? [], pipeline?.stages ?? []), [open.data, pipeline]);
67
+
68
+ const error = pipelinesError ?? analytics.error ?? velocity.error ?? open.error;
69
+ if (error) return <ApiError error={error} title="Could not load the forecast" onRetry={() => { void analytics.refetch(); void velocity.refetch(); void open.refetch(); }} />;
70
+ if (loadingPipelines || analytics.loading || open.loading) return <Skeleton className="h-64 w-full" aria-label="Loading forecast" />;
71
+ const t = analytics.data?.totals;
72
+ if (!pipeline || !t || t.deal_count === 0) return <EmptyState className={className} title="No deals to forecast" description="Create deals in this pipeline and the forecast fills in." />;
73
+ const code = analytics.data?.currency?.code ?? undefined;
74
+ const mixed = analytics.data?.currency?.is_mixed;
75
+ const maxCount = Math.max(1, ...(analytics.data?.by_stage ?? []).map((s) => s.deal_count));
76
+
77
+ return (
78
+ <div className={`space-y-5 ${className ?? ""}`}>
79
+ <div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
80
+ <Tile label="Open pipeline" value={mixed ? "Mixed currencies" : formatMoney(t.open_value, code)} hint={`${t.open_count} open deals`} />
81
+ <Tile label="Weighted (estimate)" value={mixed ? "n/a" : formatMoney(Math.round(weighted.value), code)} hint={weighted.unweighted > 0 ? `${weighted.unweighted} open deals sit in stages with no probability` : "From stage probabilities"} />
82
+ <Tile label="Won" value={mixed ? `${t.won_count} deals` : formatMoney(t.won_value, code)} hint={`${t.won_count} won, ${t.lost_count} lost`} />
83
+ <Tile label="Win rate" value={t.win_rate != null ? `${Math.round(t.win_rate * 100)}%` : "n/a"} hint={t.avg_deal_size != null && !mixed ? `Avg deal ${formatMoney(t.avg_deal_size, code)}` : undefined} />
84
+ </div>
85
+
86
+ <section aria-label="Deals by stage" className="space-y-1">
87
+ <h3 className="text-sm font-semibold">Deals by stage</h3>
88
+ {(analytics.data?.by_stage ?? []).map((s) => (
89
+ <div key={`${s.stage_id}-${s.stage_name}`} className="flex items-center gap-2 text-sm">
90
+ <span className="w-40 truncate">{s.stage_name}</span>
91
+ <div className="h-3 flex-1 rounded bg-muted"><div className="h-3 rounded bg-primary" style={{ width: `${(s.deal_count / maxCount) * 100}%` }} /></div>
92
+ <span className="w-24 text-right">{s.deal_count} · {mixed ? "" : formatMoney(s.value, code)}</span>
93
+ </div>
94
+ ))}
95
+ </section>
96
+
97
+ <section aria-label="Stage velocity" className="space-y-1 text-sm">
98
+ <h3 className="font-semibold">Where deals stall</h3>
99
+ {(velocity.data?.by_stage ?? []).map((s) => <p key={s.stage_id}>{s.stage_name}: {s.avg_days_in_stage != null ? `${s.avg_days_in_stage.toFixed(1)} days average` : "no data"}{s.conversion_rate != null ? `, ${Math.round(s.conversion_rate * 100)}% advance` : ""}, {s.still_in_stage_count} waiting</p>)}
100
+ <p className="text-muted-foreground">{velocity.data?.stuck_deals?.total_count ?? 0} deals have not moved in {velocity.data?.stuck_deals?.threshold_days ?? stuckAfterDays}+ days.</p>
101
+ </section>
102
+
103
+ {(analytics.data?.trend ?? []).length > 0 ? (
104
+ <section aria-label="Won and lost by month" className="space-y-1 text-sm">
105
+ <h3 className="font-semibold">Won and lost by month</h3>
106
+ {(analytics.data?.trend ?? []).slice(-6).map((p) => <p key={p.period}>{p.period}: {p.won_count} won{p.won_value != null && !mixed ? ` (${formatMoney(p.won_value, code)})` : ""}, {p.lost_count} lost</p>)}
107
+ </section>
108
+ ) : null}
109
+ {(analytics.data?.disclosures ?? []).map((d) => <p key={d} className="text-xs text-muted-foreground">{d}</p>)}
110
+ </div>
111
+ );
112
+ }