@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,80 @@
1
+ "use client";
2
+
3
+ /**
4
+ * ProductPicker: choose a quotable product (and its price) to add to a quote.
5
+ *
6
+ * Only products marked quotable in graph8 appear: Stripe products the workspace opted in, and manual products.
7
+ * Creating or updating a quote with a non-quotable product is refused by the API, which is why the picker reads
8
+ * this list instead of the raw Stripe catalog. Tiered Stripe prices are not offered (the API leaves them out).
9
+ *
10
+ * SDK operations, in the order the picker uses them:
11
+ * 1. listQuotableProducts GET /quotable-products?currency&page&limit (read; `{ items, total, page, limit }`)
12
+ *
13
+ * A pick returns a ready LineItem (`onPick`), carrying `stripe_product_id` + `stripe_price_id` or
14
+ * `manual_product_id`, so the builder never has to guess ids.
15
+ */
16
+
17
+ import { useState } from "react";
18
+ import { Button, Input, Skeleton } from "@graph8/sdk/ui";
19
+ import { g8Get } from "@/g8/lib/g8-client";
20
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
21
+ import { ApiError } from "@/g8/components/api-error";
22
+ import { EmptyState } from "@/g8/components/empty-state";
23
+ import { body, frequencyOf, lineFromProduct, money, text } from "../lib/quotes";
24
+ import type { LineItem, ProductPrice, QuotableProduct } from "../lib/quotes";
25
+
26
+ export interface ProductPickerProps {
27
+ /** Only offer prices in this currency (the quote's currency). */
28
+ currency?: string;
29
+ onPick: (line: LineItem) => void;
30
+ pageSize?: number;
31
+ className?: string;
32
+ }
33
+
34
+ const intervalLabel = (p: ProductPrice) => (frequencyOf(p.recurring_interval) === "one_time" ? "one time" : `per ${p.recurring_interval}`);
35
+
36
+ export function ProductPicker({ currency, onPick, pageSize = 25, className }: ProductPickerProps) {
37
+ const [page, setPage] = useState(1);
38
+ const [search, setSearch] = useState("");
39
+ const q = useG8Query((signal) => g8Get("/quotable-products", { currency: currency?.toUpperCase(), page, limit: pageSize }, { signal }), [currency, page, pageSize]);
40
+ const data = body(q.data);
41
+ const items = (Array.isArray(data.items) ? data.items : []) as QuotableProduct[];
42
+ const total = typeof data.total === "number" ? data.total : items.length;
43
+ const needle = search.trim().toLowerCase();
44
+ const shown = needle ? items.filter((p) => `${p.name} ${text(p.category)}`.toLowerCase().includes(needle)) : items;
45
+
46
+ return (
47
+ <div className={className}>
48
+ <Input aria-label="Search products" placeholder="Filter this page by name or category" value={search} onChange={(e) => setSearch(e.target.value)} className="mb-3" />
49
+ {q.error ? <ApiError error={q.error} onRetry={() => void q.refetch()} /> : null}
50
+ {q.loading && !q.data ? <div className="space-y-2" aria-busy="true">{[0, 1, 2].map((i) => <Skeleton key={i} className="h-12 w-full" />)}</div> : null}
51
+ {!q.loading && !q.error && shown.length === 0 ? (
52
+ <EmptyState title="No quotable products" description="Mark a Stripe product as quotable, or add a manual product, in graph8's quote settings." />
53
+ ) : null}
54
+ <ul className="divide-y rounded-md border">
55
+ {shown.map((p) => (
56
+ <li key={p.stripe_product_id ?? p.manual_product_id} className="p-3 text-sm">
57
+ <div className="font-medium">{p.name} <span className="text-muted-foreground text-xs font-normal">{p.source}{p.category ? ` · ${p.category}` : ""}</span></div>
58
+ {p.description ? <div className="text-muted-foreground text-xs">{p.description}</div> : null}
59
+ <div className="mt-2 flex flex-wrap gap-2">
60
+ {p.prices.length === 0 ? <span className="text-muted-foreground text-xs">No flat price for this currency.</span> : null}
61
+ {p.prices.map((price, i) => (
62
+ <Button key={price.stripe_price_id ?? i} size="sm" variant="outline" aria-label={`Add ${p.name} ${money(price.unit_amount, price.currency ?? currency)} ${intervalLabel(price)}`}
63
+ onClick={() => onPick(lineFromProduct(p, price))}>
64
+ {money(price.unit_amount, price.currency ?? currency)} {intervalLabel(price)}
65
+ </Button>
66
+ ))}
67
+ </div>
68
+ </li>
69
+ ))}
70
+ </ul>
71
+ <div className="mt-3 flex items-center justify-between text-sm">
72
+ <span className="text-muted-foreground">{total} products</span>
73
+ <div className="flex gap-2">
74
+ <Button size="sm" variant="outline" disabled={page === 1} onClick={() => setPage(page - 1)}>Previous</Button>
75
+ <Button size="sm" variant="outline" disabled={page * pageSize >= total} onClick={() => setPage(page + 1)}>Next</Button>
76
+ </div>
77
+ </div>
78
+ </div>
79
+ );
80
+ }
@@ -0,0 +1,195 @@
1
+ "use client";
2
+
3
+ /**
4
+ * QuoteBuilder: create or edit a DRAFT quote: title, signer, contract dates, payment terms and line items picked
5
+ * from quotable products (ProductPicker), with live totals.
6
+ *
7
+ * SDK operations, in the order the builder uses them:
8
+ * 1. getQuote GET /quotes/{id} (read; only when editing, to fill the form)
9
+ * 2. listQuotableProducts GET /quotable-products (read; inside ProductPicker)
10
+ * 3. searchQuoteContacts GET /quote-contacts?q&company_id&deal_id (read; the signer picker)
11
+ * 4. createQuote POST /quotes (external tier)
12
+ * 5. updateQuotePatch PATCH /quotes/{id} (external tier; drafts only, the server refuses others)
13
+ *
14
+ * Saving a draft emails nobody: the contract tiers quote writes `external` because the same route family sends
15
+ * quotes, but a draft only stores data. So this screen saves in one click and keeps the confirmation for the
16
+ * step that really reaches the customer, which is SEND in QuoteViewer (ConfirmSpendDialog, tier external).
17
+ *
18
+ * Required by the API: `title`, `deal_id`, `contract_start_date`, at least one line item, and a signer
19
+ * (`signer_contact_id` or `signer_email`). The form lists what is still missing instead of failing with a 422.
20
+ * `deal_id` comes from the deals block (PipelineBoard / DealDetail); this screen never invents one.
21
+ */
22
+
23
+ import { useEffect, useMemo, useState } from "react";
24
+ import { Button, Card, CardContent, CardHeader, CardTitle, Input, Label, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Textarea } from "@graph8/sdk/ui";
25
+ import { g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
26
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
27
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
28
+ import { ApiError } from "@/g8/components/api-error";
29
+ import { PAYMENT_TERMS, asRec, body, linesFromQuote, lineTotal, money, subtotal, text, toRequestLines } from "../lib/quotes";
30
+ import type { LineItem, PaymentTerms, Rec } from "../lib/quotes";
31
+ import { useDebounced } from "../hooks/use-debounced";
32
+ import { ProductPicker } from "./product-picker";
33
+
34
+ export interface QuoteBuilderProps {
35
+ /** Edit this draft. Omit to create a new quote (then `dealId` is required). */
36
+ quoteId?: string;
37
+ /** The deal the quote belongs to (UUID from the deals block). */
38
+ dealId?: string;
39
+ /** Company (mashup company id) used to find signer contacts. */
40
+ companyId?: number;
41
+ currency?: string;
42
+ /** Called with the saved quote id. Route to QuoteViewer from here. */
43
+ onSaved?: (quoteId: string) => void;
44
+ className?: string;
45
+ }
46
+
47
+ interface Form {
48
+ title: string;
49
+ currency: string;
50
+ payment_terms: PaymentTerms;
51
+ contract_start_date: string;
52
+ valid_until: string;
53
+ tax: string;
54
+ notes: string;
55
+ signer_email: string;
56
+ signer_name: string;
57
+ signer_contact_id: number | null;
58
+ }
59
+
60
+ const EMPTY: Form = { title: "", currency: "USD", payment_terms: "net_30", contract_start_date: "", valid_until: "", tax: "0", notes: "", signer_email: "", signer_name: "", signer_contact_id: null };
61
+
62
+ function SignerPicker({ form, setForm, companyId, dealId }: { form: Form; setForm: (f: Form) => void; companyId?: number; dealId?: string }) {
63
+ const [q, setQ] = useState("");
64
+ const debounced = useDebounced(q);
65
+ const found = useG8Query((signal) => g8Get("/quote-contacts", { q: debounced || undefined, company_id: companyId, deal_id: dealId, limit: 20 }, { signal }), [debounced, companyId, dealId]);
66
+ const contacts = (Array.isArray(body(found.data).items) ? body(found.data).items : []) as Rec[];
67
+ return (
68
+ <div className="space-y-2">
69
+ <Label htmlFor="qb-signer-search">Signer</Label>
70
+ <Input id="qb-signer-search" placeholder="Search contacts by name or email" value={q} onChange={(e) => setQ(e.target.value)} />
71
+ {found.error ? <ApiError error={found.error} onRetry={() => void found.refetch()} /> : null}
72
+ <div className="flex flex-wrap gap-2">
73
+ {contacts.map((c) => (
74
+ <Button key={text(c.id)} size="sm" variant={form.signer_contact_id === Number(c.id) ? "default" : "outline"}
75
+ onClick={() => setForm({ ...form, signer_contact_id: Number(c.id), signer_email: text(c.email), signer_name: text(c.person_name) })}>
76
+ {text(c.name)}
77
+ </Button>
78
+ ))}
79
+ </div>
80
+ <div className="grid gap-2 sm:grid-cols-2">
81
+ <Input aria-label="Signer email" placeholder="signer@company.com" value={form.signer_email} onChange={(e) => setForm({ ...form, signer_email: e.target.value, signer_contact_id: null })} />
82
+ <Input aria-label="Signer name" placeholder="Signer name" value={form.signer_name} onChange={(e) => setForm({ ...form, signer_name: e.target.value })} />
83
+ </div>
84
+ </div>
85
+ );
86
+ }
87
+
88
+ function LinesTable({ lines, setLines, currency }: { lines: LineItem[]; setLines: (l: LineItem[]) => void; currency: string }) {
89
+ const patch = (key: string, p: Partial<LineItem>) => setLines(lines.map((l) => (l.key === key ? { ...l, ...p } : l)));
90
+ if (lines.length === 0) return <p className="text-muted-foreground text-sm">No line items yet. Pick a product below.</p>;
91
+ return (
92
+ <Table>
93
+ <TableHeader><TableRow><TableHead>Product</TableHead><TableHead>Qty</TableHead><TableHead>Unit ({currency})</TableHead><TableHead>Discount %</TableHead><TableHead className="text-right">Total</TableHead><TableHead /></TableRow></TableHeader>
94
+ <TableBody>
95
+ {lines.map((l) => (
96
+ <TableRow key={l.key}>
97
+ <TableCell>{l.product_name}<div className="text-muted-foreground text-xs">{l.billing_frequency === "one_time" ? "one time" : `per ${l.billing_frequency}`}</div></TableCell>
98
+ <TableCell><Input aria-label={`Quantity of ${l.product_name}`} type="number" min={1} value={l.quantity} className="w-20" onChange={(e) => patch(l.key, { quantity: Math.max(1, Number(e.target.value) || 1) })} /></TableCell>
99
+ <TableCell><Input aria-label={`Unit price of ${l.product_name}`} type="number" min={0} step="0.01" value={l.unit_amount / 100} className="w-28" onChange={(e) => patch(l.key, { unit_amount: Math.round((Number(e.target.value) || 0) * 100) })} /></TableCell>
100
+ <TableCell><Input aria-label={`Discount of ${l.product_name}`} type="number" min={0} max={100} value={l.discount_pct} className="w-20" onChange={(e) => patch(l.key, { discount_pct: Math.min(100, Math.max(0, Number(e.target.value) || 0)) })} /></TableCell>
101
+ <TableCell className="text-right">{money(lineTotal(l), currency)}</TableCell>
102
+ <TableCell className="text-right"><Button size="sm" variant="ghost" aria-label={`Remove ${l.product_name}`} onClick={() => setLines(lines.filter((x) => x.key !== l.key))}>Remove</Button></TableCell>
103
+ </TableRow>
104
+ ))}
105
+ </TableBody>
106
+ </Table>
107
+ );
108
+ }
109
+
110
+ export function QuoteBuilder({ quoteId, dealId, companyId, currency = "USD", onSaved, className }: QuoteBuilderProps) {
111
+ const existing = useG8Query((signal) => g8Get(`/quotes/${encodeURIComponent(quoteId ?? "")}`, undefined, { signal }), [quoteId], { enabled: Boolean(quoteId) });
112
+ const [form, setForm] = useState<Form>({ ...EMPTY, currency });
113
+ const [lines, setLines] = useState<LineItem[]>([]);
114
+ const detail = body(existing.data);
115
+ const status = text(detail.status, "draft");
116
+
117
+ useEffect(() => {
118
+ if (!quoteId || !existing.data) return;
119
+ setForm({
120
+ title: text(detail.title), currency: text(detail.currency, currency), payment_terms: (PAYMENT_TERMS as readonly string[]).includes(text(detail.payment_terms)) ? (text(detail.payment_terms) as PaymentTerms) : "net_30",
121
+ contract_start_date: text(detail.contract_start_date).slice(0, 10), valid_until: text(detail.valid_until).slice(0, 10), tax: String(Number(detail.tax_amount ?? 0) / 100),
122
+ notes: text(detail.public_notes), signer_email: text(detail.signer_contact_email ?? detail.signer_email), signer_name: text(detail.signer_contact_name ?? detail.signer_name),
123
+ signer_contact_id: typeof detail.signer_contact_id === "number" ? detail.signer_contact_id : null,
124
+ });
125
+ setLines(linesFromQuote(detail.line_items));
126
+ }, [existing.data]);
127
+
128
+ const taxCents = Math.round((Number(form.tax) || 0) * 100);
129
+ const sub = useMemo(() => subtotal(lines), [lines]);
130
+ const effectiveDeal = dealId ?? text(detail.deal_id);
131
+ const missing = [
132
+ !form.title.trim() && "a title",
133
+ !quoteId && !effectiveDeal && "a deal",
134
+ !form.contract_start_date && "a contract start date",
135
+ lines.length === 0 && "at least one line item",
136
+ !form.signer_email.trim() && form.signer_contact_id == null && "a signer",
137
+ ].filter(Boolean) as string[];
138
+
139
+ const save = useG8Mutation(async () => {
140
+ const payload: Rec = {
141
+ title: form.title.trim(), currency: form.currency, payment_terms: form.payment_terms, contract_start_date: form.contract_start_date,
142
+ line_items: toRequestLines(lines), tax_amount: taxCents, public_notes: form.notes || null, valid_until: form.valid_until || null,
143
+ ...(form.signer_contact_id != null ? { signer_contact_id: form.signer_contact_id } : { signer_email: form.signer_email.trim(), signer_name: form.signer_name.trim() || null }),
144
+ };
145
+ if (quoteId) return body(await g8Patch(`/quotes/${encodeURIComponent(quoteId)}`, payload));
146
+ return body(await g8Post("/quotes", { ...payload, deal_id: effectiveDeal, ...(companyId != null ? { mashup_company_id: companyId } : {}) }));
147
+ }, { onSuccess: (q) => { const id = text(q.id) || quoteId; if (id) onSaved?.(id); } });
148
+
149
+ if (quoteId && existing.loading && !existing.data) return <div className={className} aria-busy="true"><Skeleton className="h-48 w-full" /></div>;
150
+ if (quoteId && existing.error) return <ApiError error={existing.error} onRetry={() => void existing.refetch()} />;
151
+ if (quoteId && status !== "draft") return <p className={className}>This quote is {status}. Only drafts can be edited; use "Edit as draft" in the quote view to make a new draft from it.</p>;
152
+
153
+ const set = (p: Partial<Form>) => setForm({ ...form, ...p });
154
+ return (
155
+ <div className={className}>
156
+ <Card className="mb-4">
157
+ <CardHeader><CardTitle className="text-sm">Quote details</CardTitle></CardHeader>
158
+ <CardContent className="grid gap-3 sm:grid-cols-2">
159
+ <div className="space-y-1 sm:col-span-2"><Label htmlFor="qb-title">Title</Label><Input id="qb-title" value={form.title} onChange={(e) => set({ title: e.target.value })} /></div>
160
+ <div className="space-y-1"><Label htmlFor="qb-start">Contract start</Label><Input id="qb-start" type="date" value={form.contract_start_date} onChange={(e) => set({ contract_start_date: e.target.value })} /></div>
161
+ <div className="space-y-1"><Label htmlFor="qb-valid">Valid until</Label><Input id="qb-valid" type="date" value={form.valid_until} onChange={(e) => set({ valid_until: e.target.value })} /></div>
162
+ <div className="space-y-1">
163
+ <Label htmlFor="qb-terms">Payment terms</Label>
164
+ <select id="qb-terms" value={form.payment_terms} onChange={(e) => set({ payment_terms: e.target.value as PaymentTerms })} className="border-input bg-background h-9 w-full rounded-md border px-2 text-sm">
165
+ {PAYMENT_TERMS.map((t) => <option key={t} value={t}>{t.replace(/_/g, " ")}</option>)}
166
+ </select>
167
+ </div>
168
+ <div className="space-y-1"><Label htmlFor="qb-tax">Tax ({form.currency})</Label><Input id="qb-tax" type="number" min={0} step="0.01" value={form.tax} onChange={(e) => set({ tax: e.target.value })} /></div>
169
+ <div className="sm:col-span-2"><SignerPicker form={form} setForm={setForm} companyId={companyId} dealId={effectiveDeal || undefined} /></div>
170
+ <div className="space-y-1 sm:col-span-2"><Label htmlFor="qb-notes">Notes for the customer</Label><Textarea id="qb-notes" rows={2} value={form.notes} onChange={(e) => set({ notes: e.target.value })} /></div>
171
+ </CardContent>
172
+ </Card>
173
+
174
+ <Card className="mb-4">
175
+ <CardHeader><CardTitle className="text-sm">Line items</CardTitle></CardHeader>
176
+ <CardContent className="space-y-4">
177
+ <LinesTable lines={lines} setLines={setLines} currency={form.currency} />
178
+ <div className="text-right text-sm" data-testid="totals">
179
+ <div>Subtotal {money(sub, form.currency)}</div>
180
+ <div>Tax {money(taxCents, form.currency)}</div>
181
+ <div className="font-semibold">Total {money(sub + taxCents, form.currency)}</div>
182
+ </div>
183
+ <details>
184
+ <summary className="cursor-pointer text-sm font-medium">Add a product</summary>
185
+ <ProductPicker className="mt-3" currency={form.currency} onPick={(line) => setLines([...lines, line])} />
186
+ </details>
187
+ </CardContent>
188
+ </Card>
189
+
190
+ {save.error ? <ApiError error={save.error} title="Could not save the quote" /> : null}
191
+ {missing.length ? <p className="text-muted-foreground mb-2 text-sm" data-testid="missing">Still needed: {missing.join(", ")}.</p> : null}
192
+ <Button disabled={missing.length > 0} loading={save.loading} onClick={() => void save.mutate(undefined)}>{quoteId ? "Save draft" : "Create draft"}</Button>
193
+ </div>
194
+ );
195
+ }
@@ -0,0 +1,79 @@
1
+ "use client";
2
+
3
+ /**
4
+ * QuoteList: quotes with a status filter, totals in the quote's own currency, and paging.
5
+ *
6
+ * SDK operations, in the order the screen uses them:
7
+ * 1. listQuotes GET /quotes?status&mashup_company_id&deal_id&page&limit (read; `{ items, total, page, limit }`)
8
+ *
9
+ * Read-only: no billable, external or destructive call, so no confirmation. Pass `dealId` or `companyId` to show the
10
+ * quotes of one deal or company (for a tab on a deal page). `onOpen` receives the quote id for QuoteViewer.
11
+ */
12
+
13
+ import { useState } from "react";
14
+ import { Button, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
15
+ import { g8Get } from "@/g8/lib/g8-client";
16
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
17
+ import { ApiError } from "@/g8/components/api-error";
18
+ import { EmptyState } from "@/g8/components/empty-state";
19
+ import { body, money, statusLabel, text } from "../lib/quotes";
20
+ import type { Rec } from "../lib/quotes";
21
+
22
+ export interface QuoteListProps {
23
+ dealId?: string;
24
+ companyId?: number;
25
+ onOpen?: (quoteId: string) => void;
26
+ onNew?: () => void;
27
+ pageSize?: number;
28
+ className?: string;
29
+ }
30
+
31
+ const STATUSES = ["", "draft", "sent", "viewed", "accepted", "declined", "voided", "expired", "archived"];
32
+
33
+ export function QuoteList({ dealId, companyId, onOpen, onNew, pageSize = 25, className }: QuoteListProps) {
34
+ const [status, setStatus] = useState("");
35
+ const [page, setPage] = useState(1);
36
+ const q = useG8Query(
37
+ (signal) => g8Get("/quotes", { status: status || undefined, deal_id: dealId, mashup_company_id: companyId, page, limit: pageSize }, { signal }),
38
+ [status, dealId, companyId, page, pageSize],
39
+ );
40
+ const data = body(q.data);
41
+ const rows = (Array.isArray(data.items) ? data.items : []) as Rec[];
42
+ const total = typeof data.total === "number" ? data.total : rows.length;
43
+
44
+ return (
45
+ <div className={className}>
46
+ <div className="mb-3 flex items-center gap-2">
47
+ <select aria-label="Status" value={status} onChange={(e) => { setStatus(e.target.value); setPage(1); }} className="border-input bg-background h-9 rounded-md border px-2 text-sm">
48
+ {STATUSES.map((s) => <option key={s} value={s}>{s ? statusLabel(s) : "All statuses"}</option>)}
49
+ </select>
50
+ {onNew ? <Button className="ml-auto" onClick={onNew}>New quote</Button> : null}
51
+ </div>
52
+ {q.error ? <ApiError error={q.error} onRetry={() => void q.refetch()} /> : null}
53
+ {q.loading && !q.data ? <div className="space-y-2" aria-busy="true">{[0, 1, 2].map((i) => <Skeleton key={i} className="h-10 w-full" />)}</div> : !q.error && rows.length === 0 ? (
54
+ <EmptyState title="No quotes" description="Create a quote from a deal, pick products, and send it for signature." action={onNew ? <Button onClick={onNew}>New quote</Button> : undefined} />
55
+ ) : (
56
+ <Table>
57
+ <TableHeader><TableRow><TableHead>Quote</TableHead><TableHead>Status</TableHead><TableHead className="text-right">Total</TableHead><TableHead>Sent</TableHead></TableRow></TableHeader>
58
+ <TableBody>
59
+ {rows.map((r) => (
60
+ <TableRow key={text(r.id)}>
61
+ <TableCell><button type="button" className="font-medium hover:underline" onClick={() => onOpen?.(text(r.id))}>{text(r.title, "Untitled quote")}</button><div className="text-muted-foreground text-xs">{text(r.quote_number)}</div></TableCell>
62
+ <TableCell>{statusLabel(r.status)}</TableCell>
63
+ <TableCell className="text-right">{money(r.total, text(r.currency, "USD"))}</TableCell>
64
+ <TableCell>{r.sent_at ? new Date(text(r.sent_at)).toLocaleDateString() : ""}</TableCell>
65
+ </TableRow>
66
+ ))}
67
+ </TableBody>
68
+ </Table>
69
+ )}
70
+ <div className="mt-3 flex items-center justify-between text-sm">
71
+ <span className="text-muted-foreground">{total} quotes</span>
72
+ <div className="flex gap-2">
73
+ <Button size="sm" variant="outline" disabled={page === 1} onClick={() => setPage(page - 1)}>Previous</Button>
74
+ <Button size="sm" variant="outline" disabled={page * pageSize >= total} onClick={() => setPage(page + 1)}>Next</Button>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ );
79
+ }
@@ -0,0 +1,135 @@
1
+ "use client";
2
+
3
+ /**
4
+ * QuoteViewer: one quote read-only (line items, totals, terms, signer) with the actions its status allows and the
5
+ * e-signature status (SignatureStatus).
6
+ *
7
+ * SDK operations, in the order the screen uses them:
8
+ * 1. getQuote GET /quotes/{id} (read; line items, totals in CENTS, envelope)
9
+ * 2. sendQuote POST /quotes/{id}/send (external: emails the signer; also the resend for a sent quote)
10
+ * 3. editQuoteAsDraft POST /quotes/{id}/edit-as-draft (external; `sent` quotes only, returns `new_quote_id`: voids the sent copy, signing link dies, new draft)
11
+ * 4. duplicateQuote POST /quotes/{id}/duplicate (external tier, but only copies into a new draft)
12
+ * 5. deleteQuote DELETE /quotes/{id} (destructive; drafts only)
13
+ *
14
+ * Send, edit-as-draft and delete go through ConfirmSpendDialog. Duplicate creates an unsent draft and emails
15
+ * nobody, so it runs on one click (same reasoning as saving a draft in QuoteBuilder).
16
+ * Send needs a signer on the quote; without one the button explains what is missing instead of failing.
17
+ */
18
+
19
+ import { useState } from "react";
20
+ import { Button, Card, CardContent, CardHeader, CardTitle, Input, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Textarea } from "@graph8/sdk/ui";
21
+ import { g8Delete, g8Get, g8Post } from "@/g8/lib/g8-client";
22
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
23
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
24
+ import { ApiError } from "@/g8/components/api-error";
25
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
26
+ import { asRec, body, lineTotal, linesFromQuote, money, statusLabel, text } from "../lib/quotes";
27
+ import { SignatureStatus } from "./signature-status";
28
+
29
+ export interface QuoteViewerProps {
30
+ quoteId: string;
31
+ /** Open the builder for this draft. */
32
+ onEdit?: (quoteId: string) => void;
33
+ /** Called with the id of a new draft (duplicate, edit-as-draft). */
34
+ onOpenQuote?: (quoteId: string) => void;
35
+ /** Called after the quote was deleted. */
36
+ onDeleted?: () => void;
37
+ className?: string;
38
+ }
39
+
40
+ type Pending = "send" | "edit" | "delete";
41
+
42
+ export function QuoteViewer({ quoteId, onEdit, onOpenQuote, onDeleted, className }: QuoteViewerProps) {
43
+ const base = `/quotes/${encodeURIComponent(quoteId)}`;
44
+ const q = useG8Query((signal) => g8Get(base, undefined, { signal }), [quoteId]);
45
+ const [pending, setPending] = useState<Pending | null>(null);
46
+ const [subject, setSubject] = useState("");
47
+ const [message, setMessage] = useState("");
48
+
49
+ const run = useG8Mutation(async (kind: Pending) => {
50
+ if (kind === "send") return body(await g8Post(`${base}/send`, { subject: subject || undefined, message: message || undefined }));
51
+ if (kind === "edit") return body(await g8Post(`${base}/edit-as-draft`));
52
+ return body(await g8Delete(base));
53
+ }, {
54
+ onSuccess: (res, kind) => {
55
+ setPending(null);
56
+ if (kind === "delete") return onDeleted?.();
57
+ if (kind === "edit" && text(res.new_quote_id)) return onOpenQuote?.(text(res.new_quote_id));
58
+ void q.refetch();
59
+ },
60
+ });
61
+ const copy = useG8Mutation(async () => body(await g8Post(`${base}/duplicate`)), { onSuccess: (res) => { if (text(res.id)) onOpenQuote?.(text(res.id)); } });
62
+
63
+ if (q.loading && !q.data) return <div className={className} aria-busy="true"><Skeleton className="h-48 w-full" /></div>;
64
+ if (q.error) return <ApiError error={q.error} onRetry={() => void q.refetch()} />;
65
+
66
+ const d = body(q.data);
67
+ const status = text(d.status, "draft");
68
+ const currency = text(d.currency, "USD");
69
+ const lines = linesFromQuote(d.line_items);
70
+ const hasSigner = Boolean(text(d.signer_contact_email ?? d.signer_email) || d.signer_contact_id);
71
+ const draft = status === "draft";
72
+ const sent = status === "sent" || status === "viewed";
73
+ const name = text(d.title, "this quote");
74
+
75
+ return (
76
+ <div className={className}>
77
+ <div className="mb-4 flex flex-wrap items-center gap-2">
78
+ <h2 className="text-lg font-semibold">{text(d.title, "Untitled quote")}</h2>
79
+ <span className="text-muted-foreground text-sm" data-testid="quote-status">{statusLabel(status)} · {text(d.quote_number)}</span>
80
+ <div className="ml-auto flex flex-wrap gap-2">
81
+ {draft ? <Button variant="outline" onClick={() => onEdit?.(quoteId)}>Edit</Button> : null}
82
+ {status === "sent" ? <Button variant="outline" onClick={() => setPending("edit")}>Edit as draft</Button> : null}
83
+ <Button variant="outline" loading={copy.loading} onClick={() => void copy.mutate(undefined)}>Duplicate</Button>
84
+ {draft ? <Button variant="ghost" onClick={() => setPending("delete")}>Delete</Button> : null}
85
+ {draft || sent ? <Button disabled={!hasSigner} title={hasSigner ? undefined : "Add a signer in the builder first"} onClick={() => setPending("send")}>{sent ? "Resend" : "Send for signature"}</Button> : null}
86
+ </div>
87
+ </div>
88
+ {run.error ? <ApiError error={run.error} title="That action failed" /> : null}
89
+ {copy.error ? <ApiError error={copy.error} title="Could not duplicate" /> : null}
90
+ {draft && !hasSigner ? <p className="text-muted-foreground mb-3 text-sm">Add a signer before sending.</p> : null}
91
+
92
+ <Card className="mb-4">
93
+ <CardHeader><CardTitle className="text-sm">Line items</CardTitle></CardHeader>
94
+ <CardContent>
95
+ <Table>
96
+ <TableHeader><TableRow><TableHead>Product</TableHead><TableHead>Qty</TableHead><TableHead>Unit</TableHead><TableHead>Discount</TableHead><TableHead className="text-right">Total</TableHead></TableRow></TableHeader>
97
+ <TableBody>
98
+ {lines.map((l) => (
99
+ <TableRow key={l.key}><TableCell>{l.product_name}</TableCell><TableCell>{l.quantity}</TableCell><TableCell>{money(l.unit_amount, currency)}</TableCell><TableCell>{l.discount_pct ? `${l.discount_pct}%` : ""}</TableCell><TableCell className="text-right">{money(lineTotal(l), currency)}</TableCell></TableRow>
100
+ ))}
101
+ </TableBody>
102
+ </Table>
103
+ <div className="mt-3 text-right text-sm" data-testid="saved-totals">
104
+ <div>Subtotal {money(d.subtotal, currency)}</div>
105
+ {Number(d.discount_amount) ? <div>Discount -{money(d.discount_amount, currency)}</div> : null}
106
+ <div>Tax {money(d.tax_amount, currency)}</div>
107
+ <div className="font-semibold">Total {money(d.total, currency)}</div>
108
+ </div>
109
+ </CardContent>
110
+ </Card>
111
+
112
+ <p className="text-muted-foreground mb-4 text-sm">
113
+ Signer: {text(d.signer_contact_name ?? d.signer_name) || "none"} {text(d.signer_contact_email ?? d.signer_email)} · Payment terms: {text(d.payment_terms).replace(/_/g, " ")}
114
+ {d.valid_until ? ` · Valid until ${text(d.valid_until).slice(0, 10)}` : ""}
115
+ </p>
116
+ <SignatureStatus quoteId={quoteId} quote={asRec(d)} onChanged={() => void q.refetch()} />
117
+
118
+ {pending === "send" ? (
119
+ <ConfirmSpendDialog open tier="external" title={`${sent ? "Resend" : "Send"} "${name}" for signature`} confirmLabel={sent ? "Resend" : "Send"} recipients={1} confirming={run.loading}
120
+ description={<div className="space-y-2 text-left"><p>Emails {text(d.signer_contact_email ?? d.signer_email)} a link to review and sign.</p>
121
+ <Input aria-label="Email subject" placeholder="Subject (optional)" value={subject} onChange={(e) => setSubject(e.target.value)} />
122
+ <Textarea aria-label="Email message" rows={3} placeholder="Message (optional)" value={message} onChange={(e) => setMessage(e.target.value)} /></div>}
123
+ onConfirm={() => void run.mutate("send")} onCancel={() => setPending(null)} />
124
+ ) : null}
125
+ {pending === "edit" ? (
126
+ <ConfirmSpendDialog open tier="external" title={`Edit "${name}" as a new draft`} confirmLabel="Void and edit" confirming={run.loading}
127
+ description="The signing link already sent stops working and a new draft is created. Send the draft again when it is ready." onConfirm={() => void run.mutate("edit")} onCancel={() => setPending(null)} />
128
+ ) : null}
129
+ {pending === "delete" ? (
130
+ <ConfirmSpendDialog open tier="destructive" title={`Delete "${name}"`} affected={1} confirming={run.loading}
131
+ description="The draft is removed. Sent quotes cannot be deleted." onConfirm={() => void run.mutate("delete")} onCancel={() => setPending(null)} />
132
+ ) : null}
133
+ </div>
134
+ );
135
+ }
@@ -0,0 +1,77 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SignatureStatus: where a quote's e-signature stands: sent, viewed, signed or declined, who still has to sign,
5
+ * and a log of views and signatures. Offers a reminder while the envelope is waiting.
6
+ *
7
+ * SDK operations, in the order the screen uses them:
8
+ * 1. getQuote GET /quotes/{id} (read; carries `envelope`, `signers` and audit events)
9
+ * 2. resendEnvelope POST /envelopes/{envelope_id}/resend (external: emails the signer again)
10
+ *
11
+ * The envelope only exists after the quote was sent. Pass `quote` (the object QuoteViewer already loaded) to skip
12
+ * the extra read. A reminder goes through ConfirmSpendDialog. The envelope id is `envelope.id` of the quote.
13
+ */
14
+
15
+ import { useState } from "react";
16
+ import { Button, Card, CardContent, CardHeader, CardTitle, Skeleton } from "@graph8/sdk/ui";
17
+ import { g8Get, g8Post } from "@/g8/lib/g8-client";
18
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
19
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
20
+ import { ApiError } from "@/g8/components/api-error";
21
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
22
+ import { asRec, body, text } from "../lib/quotes";
23
+ import type { Rec } from "../lib/quotes";
24
+
25
+ export interface SignatureStatusProps {
26
+ quoteId: string;
27
+ /** The loaded quote detail, if the caller has it. */
28
+ quote?: Rec;
29
+ onChanged?: () => void;
30
+ className?: string;
31
+ }
32
+
33
+ const WAITING = ["sent", "viewed"];
34
+ const when = (v: unknown) => (v ? new Date(text(v)).toLocaleString() : "");
35
+
36
+ export function SignatureStatus({ quoteId, quote, onChanged, className }: SignatureStatusProps) {
37
+ const q = useG8Query((signal) => g8Get(`/quotes/${encodeURIComponent(quoteId)}`, undefined, { signal }), [quoteId], { enabled: !quote });
38
+ const [confirm, setConfirm] = useState(false);
39
+ const detail = quote ?? body(q.data);
40
+ const env = asRec(detail.envelope);
41
+ const envelopeId = text(env.id);
42
+ const remind = useG8Mutation(() => g8Post(`/envelopes/${encodeURIComponent(envelopeId)}/resend`), { onSuccess: () => { setConfirm(false); if (!quote) void q.refetch(); onChanged?.(); } });
43
+
44
+ if (!quote && q.loading && !q.data) return <div className={className} aria-busy="true"><Skeleton className="h-24 w-full" /></div>;
45
+ if (!quote && q.error) return <ApiError error={q.error} onRetry={() => void q.refetch()} />;
46
+ if (!envelopeId) return <p className={`text-muted-foreground text-sm ${className ?? ""}`}>Not sent for signature yet.</p>;
47
+
48
+ const status = text(env.status);
49
+ const signers = (Array.isArray(detail.signers) ? detail.signers : []) as Rec[];
50
+ const events = (Array.isArray(env.audit_events) ? env.audit_events : []) as Rec[];
51
+ return (
52
+ <Card className={className}>
53
+ <CardHeader><CardTitle className="text-sm">E-signature: <span data-testid="envelope-status" className="capitalize">{status}</span></CardTitle></CardHeader>
54
+ <CardContent className="space-y-3 text-sm">
55
+ <p className="text-muted-foreground">
56
+ {[env.sent_at && `Sent ${when(env.sent_at)}`, env.viewed_at && `viewed ${when(env.viewed_at)}`, env.signed_at && `signed ${when(env.signed_at)}`, env.declined_at && `declined ${when(env.declined_at)}`].filter(Boolean).join(", ")}
57
+ </p>
58
+ {env.decline_reason ? <p>Reason: {text(env.decline_reason)}</p> : null}
59
+ {typeof env.signer_count === "number" ? <p>{text(env.signed_count, "0")} of {text(env.signer_count)} signed</p> : null}
60
+ <ul className="space-y-1">
61
+ {signers.map((s) => <li key={text(s.id ?? s.email)}>{text(s.name) || text(s.email)} <span className="text-muted-foreground">({text(s.status)})</span></li>)}
62
+ </ul>
63
+ {events.length ? (
64
+ <details><summary className="cursor-pointer">Activity ({events.length})</summary>
65
+ <ul className="mt-1 space-y-1 text-xs">{events.map((e, i) => <li key={i}>{text(e.action)} · {when(e.created_at)}</li>)}</ul>
66
+ </details>
67
+ ) : null}
68
+ {remind.error ? <ApiError error={remind.error} title="Could not send the reminder" /> : null}
69
+ {WAITING.includes(status) ? <Button variant="outline" onClick={() => setConfirm(true)}>Send reminder</Button> : null}
70
+ </CardContent>
71
+ {confirm ? (
72
+ <ConfirmSpendDialog open tier="external" title="Remind the signer" confirmLabel="Send reminder" recipients={Math.max(1, signers.filter((s) => text(s.status) !== "signed").length)}
73
+ description="The signer gets the signing email again, with the same link." confirming={remind.loading} onConfirm={() => void remind.mutate(undefined)} onCancel={() => setConfirm(false)} />
74
+ ) : null}
75
+ </Card>
76
+ );
77
+ }
@@ -0,0 +1,13 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+
5
+ /** The value, `ms` after it stopped changing. Search boxes use it so each keystroke is not a request. */
6
+ export function useDebounced<T>(value: T, ms = 300): T {
7
+ const [settled, setSettled] = useState(value);
8
+ useEffect(() => {
9
+ const t = setTimeout(() => setSettled(value), ms);
10
+ return () => clearTimeout(t);
11
+ }, [value, ms]);
12
+ return settled;
13
+ }