@graph8/sdk 0.370.0 → 0.374.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (646) hide show
  1. package/README.md +22 -3
  2. package/blocks/_foundation/README.md +61 -0
  3. package/blocks/_foundation/block.json +57 -0
  4. package/blocks/_foundation/components/auth-gate.tsx +363 -0
  5. package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
  6. package/blocks/_foundation/components/empty-state.tsx +5 -2
  7. package/blocks/_foundation/components/list-select.tsx +112 -0
  8. package/blocks/_foundation/components/paging-notices.tsx +55 -0
  9. package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
  10. package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
  11. package/blocks/_foundation/components/screen/index.ts +1 -0
  12. package/blocks/_foundation/components/screen/split-view.tsx +1 -1
  13. package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
  14. package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
  15. package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
  16. package/blocks/_foundation/lib/paging.ts +57 -0
  17. package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
  18. package/blocks/_foundation/server/auth.ts +815 -0
  19. package/blocks/agents/README.md +81 -4
  20. package/blocks/agents/block.json +311 -2
  21. package/blocks/agents/components/agent-activity.tsx +112 -0
  22. package/blocks/agents/components/agent-approvals.tsx +135 -0
  23. package/blocks/agents/components/agent-builder.tsx +17 -3
  24. package/blocks/agents/components/agent-conversations.tsx +143 -0
  25. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
  26. package/blocks/agents/components/agent-operators.tsx +164 -0
  27. package/blocks/agents/components/agent-run-detail.tsx +20 -3
  28. package/blocks/agents/components/agent-runs.tsx +3 -3
  29. package/blocks/agents/components/agent-skills-panel.tsx +7 -7
  30. package/blocks/agents/components/agent-test-history.tsx +121 -0
  31. package/blocks/agents/components/agents-screen.tsx +53 -11
  32. package/blocks/agents/components/assistant-sessions.tsx +150 -0
  33. package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
  34. package/blocks/agents/components/from-template-button.tsx +65 -0
  35. package/blocks/agents/components/knowledge-browser.tsx +107 -0
  36. package/blocks/agents/components/knowledge-detail.tsx +156 -0
  37. package/blocks/agents/components/knowledge-form.tsx +69 -0
  38. package/blocks/agents/components/name-dialog.tsx +32 -0
  39. package/blocks/agents/components/receptionist-button.tsx +46 -0
  40. package/blocks/agents/components/save-as-template-button.tsx +32 -0
  41. package/blocks/agents/components/skill-editor.tsx +147 -0
  42. package/blocks/agents/components/skill-library.tsx +111 -0
  43. package/blocks/agents/lib/agent-types.ts +7 -1
  44. package/blocks/agents/lib/facts.tsx +13 -0
  45. package/blocks/agents/lib/knowledge-utils.ts +49 -0
  46. package/blocks/agents/lib/load-more.tsx +141 -0
  47. package/blocks/agents/lib/skill-utils.ts +91 -0
  48. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
  49. package/blocks/analytics/components/analytics-screen.tsx +16 -3
  50. package/blocks/analytics/components/kpi-tiles.tsx +1 -1
  51. package/blocks/analytics/components/report-builder.tsx +15 -2
  52. package/blocks/appointments/README.md +6 -0
  53. package/blocks/appointments/block.json +27 -0
  54. package/blocks/appointments/components/booking-page.tsx +2 -2
  55. package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
  56. package/blocks/appointments/components/meeting-detail.tsx +2 -2
  57. package/blocks/appointments/components/meeting-list.tsx +29 -13
  58. package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
  59. package/blocks/campaigns/README.md +1 -1
  60. package/blocks/campaigns/block.json +34 -1
  61. package/blocks/campaigns/components/campaign-assets.tsx +1 -1
  62. package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
  63. package/blocks/campaigns/components/campaign-links.tsx +180 -0
  64. package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
  65. package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
  66. package/blocks/campaigns/components/icp-detail.tsx +11 -2
  67. package/blocks/campaigns/components/icp-editor.tsx +8 -3
  68. package/blocks/campaigns/components/persona-detail.tsx +11 -2
  69. package/blocks/campaigns/components/persona-editor.tsx +6 -1
  70. package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
  71. package/blocks/coach/components/call-scorecard.tsx +7 -7
  72. package/blocks/company-intelligence/README.md +132 -0
  73. package/blocks/company-intelligence/block.json +318 -0
  74. package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
  75. package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
  76. package/blocks/company-intelligence/components/company-profile.tsx +68 -0
  77. package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
  78. package/blocks/company-intelligence/components/report-detail.tsx +189 -0
  79. package/blocks/company-intelligence/components/research-reports.tsx +136 -0
  80. package/blocks/company-intelligence/components/research-run.tsx +165 -0
  81. package/blocks/company-intelligence/components/source-detail.tsx +117 -0
  82. package/blocks/company-intelligence/components/source-view.tsx +274 -0
  83. package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
  84. package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
  85. package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
  86. package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
  87. package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
  88. package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
  89. package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
  90. package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
  91. package/blocks/copilot/components/copilot-memory.tsx +6 -2
  92. package/blocks/copilot/components/copilot-panel.tsx +1 -1
  93. package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
  94. package/blocks/crm/README.md +25 -0
  95. package/blocks/crm/block.json +257 -9
  96. package/blocks/crm/components/bulk-update.tsx +119 -0
  97. package/blocks/crm/components/companies-screen.tsx +23 -9
  98. package/blocks/crm/components/company-detail.tsx +2 -2
  99. package/blocks/crm/components/contact-detail.tsx +4 -4
  100. package/blocks/crm/components/contact-documents-tab.tsx +144 -0
  101. package/blocks/crm/components/contact-insights-tab.tsx +118 -0
  102. package/blocks/crm/components/contact-lookup-link.tsx +85 -0
  103. package/blocks/crm/components/contact-row-actions.tsx +43 -0
  104. package/blocks/crm/components/contact-tag-chips.tsx +113 -0
  105. package/blocks/crm/components/contacts-screen.tsx +77 -18
  106. package/blocks/crm/components/dedupe-review.tsx +6 -1
  107. package/blocks/crm/components/import-wizard.tsx +3 -7
  108. package/blocks/crm/components/list-select.tsx +20 -0
  109. package/blocks/crm/components/owner-assign.tsx +132 -0
  110. package/blocks/crm/components/record-engagement-tab.tsx +103 -0
  111. package/blocks/crm/components/record-form.tsx +1 -1
  112. package/blocks/crm/components/record-open-links.tsx +79 -0
  113. package/blocks/crm/components/tag-assign.tsx +112 -0
  114. package/blocks/crm/components/tag-manager.tsx +178 -0
  115. package/blocks/crm/components/tasks-screen.tsx +35 -15
  116. package/blocks/crm/components/unlock-info-button.tsx +82 -0
  117. package/blocks/crm/components/verify-email-button.tsx +93 -0
  118. package/blocks/crm/hooks/use-all-lists.ts +18 -0
  119. package/blocks/crm/hooks/use-contact-tags.ts +29 -0
  120. package/blocks/crm/hooks/use-page-query.ts +3 -1
  121. package/blocks/crm/lib/crm-types.ts +14 -0
  122. package/blocks/crm/lib/read-lists.ts +16 -0
  123. package/blocks/crm/lib/record-actions.ts +102 -0
  124. package/blocks/deals/README.md +25 -6
  125. package/blocks/deals/block.json +168 -2
  126. package/blocks/deals/components/close-deal-dialog.tsx +169 -0
  127. package/blocks/deals/components/create-deal-button.tsx +109 -0
  128. package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
  129. package/blocks/deals/components/deal-detail.tsx +7 -5
  130. package/blocks/deals/components/deal-filters.tsx +65 -0
  131. package/blocks/deals/components/deal-line-items.tsx +120 -0
  132. package/blocks/deals/components/deal-notes-tab.tsx +97 -0
  133. package/blocks/deals/components/deal-related.tsx +30 -9
  134. package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
  135. package/blocks/deals/components/deals-screen.tsx +57 -22
  136. package/blocks/deals/components/forecast-view.tsx +4 -4
  137. package/blocks/deals/components/pipeline-board.tsx +8 -6
  138. package/blocks/deals/components/pipeline-editor.tsx +132 -0
  139. package/blocks/deals/components/record-deals-tab.tsx +62 -0
  140. package/blocks/deals/components/stage-editor.tsx +1 -1
  141. package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
  142. package/blocks/deals/lib/deal-extras.ts +156 -0
  143. package/blocks/dialer/README.md +14 -5
  144. package/blocks/dialer/block.json +17 -2
  145. package/blocks/dialer/components/call-contact-button.tsx +26 -9
  146. package/blocks/dialer/components/call-log-screen.tsx +10 -2
  147. package/blocks/dialer/components/call-log.tsx +16 -4
  148. package/blocks/dialer/components/call-queue.tsx +3 -3
  149. package/blocks/dialer/components/dialer-screen.tsx +69 -27
  150. package/blocks/dialer/components/disposition-panel.tsx +1 -1
  151. package/blocks/dialer/components/record-calls-tab.tsx +101 -0
  152. package/blocks/dialer/components/session-start-form.tsx +58 -11
  153. package/blocks/dialer/components/softphone.tsx +1 -1
  154. package/blocks/documents/README.md +86 -0
  155. package/blocks/documents/block.json +378 -0
  156. package/blocks/documents/components/account-bar.tsx +48 -0
  157. package/blocks/documents/components/correspondence-form.tsx +95 -0
  158. package/blocks/documents/components/correspondence-import.tsx +106 -0
  159. package/blocks/documents/components/correspondence-panel.tsx +191 -0
  160. package/blocks/documents/components/documents-screen.tsx +76 -0
  161. package/blocks/documents/components/library-document-detail.tsx +175 -0
  162. package/blocks/documents/components/library-document-form.tsx +105 -0
  163. package/blocks/documents/components/library-folder-form.tsx +74 -0
  164. package/blocks/documents/components/library-panel.tsx +235 -0
  165. package/blocks/documents/components/library-upload.tsx +160 -0
  166. package/blocks/documents/components/question-row.tsx +130 -0
  167. package/blocks/documents/components/questionnaire-detail.tsx +197 -0
  168. package/blocks/documents/components/questionnaires-panel.tsx +173 -0
  169. package/blocks/documents/components/record-documents-tab.tsx +109 -0
  170. package/blocks/documents/lib/documents-types.ts +252 -0
  171. package/blocks/documents/lib/download.ts +64 -0
  172. package/blocks/documents/recipes/follow-a-questionnaire.ts +26 -0
  173. package/blocks/documents/recipes/log-account-correspondence.ts +27 -0
  174. package/blocks/documents/recipes/organize-the-library.ts +25 -0
  175. package/blocks/email-signatures/README.md +123 -0
  176. package/blocks/email-signatures/block.json +341 -0
  177. package/blocks/email-signatures/components/email-signatures-screen.tsx +166 -0
  178. package/blocks/email-signatures/components/signature-assign-template.tsx +119 -0
  179. package/blocks/email-signatures/components/signature-block-editor.tsx +87 -0
  180. package/blocks/email-signatures/components/signature-campaign-form.tsx +165 -0
  181. package/blocks/email-signatures/components/signature-campaigns.tsx +75 -0
  182. package/blocks/email-signatures/components/signature-identity-form.tsx +131 -0
  183. package/blocks/email-signatures/components/signature-mailboxes.tsx +195 -0
  184. package/blocks/email-signatures/components/signature-policy.tsx +130 -0
  185. package/blocks/email-signatures/components/signature-preview.tsx +119 -0
  186. package/blocks/email-signatures/components/signature-report.tsx +77 -0
  187. package/blocks/email-signatures/components/signature-template-details.tsx +112 -0
  188. package/blocks/email-signatures/components/signature-template-form.tsx +109 -0
  189. package/blocks/email-signatures/hooks/use-signatures-data.ts +93 -0
  190. package/blocks/email-signatures/lib/signatures-types.ts +392 -0
  191. package/blocks/email-signatures/recipes/assign-a-signature-to-a-new-hire.ts +32 -0
  192. package/blocks/email-signatures/recipes/create-a-template-and-make-it-default.ts +35 -0
  193. package/blocks/email-signatures/recipes/signature-engagement-report.ts +16 -0
  194. package/blocks/enrichment/README.md +38 -10
  195. package/blocks/enrichment/block.json +164 -12
  196. package/blocks/enrichment/components/auto-enrichment-configs.tsx +134 -0
  197. package/blocks/enrichment/components/auto-enrichment-settings.tsx +82 -0
  198. package/blocks/enrichment/components/bulk-enrich-panel.tsx +79 -30
  199. package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
  200. package/blocks/enrichment/components/enrich-button.tsx +1 -1
  201. package/blocks/enrichment/components/enrichment-activity.tsx +93 -0
  202. package/blocks/enrichment/components/enrichment-job-detail.tsx +18 -0
  203. package/blocks/enrichment/components/enrichment-screen.tsx +103 -19
  204. package/blocks/enrichment/components/list-pipelines.tsx +97 -0
  205. package/blocks/enrichment/components/pipeline-detail.tsx +214 -0
  206. package/blocks/enrichment/components/pipeline-editor.tsx +144 -0
  207. package/blocks/enrichment/components/pipeline-templates.tsx +90 -0
  208. package/blocks/enrichment/components/waterfall-config.tsx +27 -7
  209. package/blocks/enrichment/lib/enrichment-types.ts +51 -0
  210. package/blocks/enrichment/lib/pipeline-types.ts +234 -0
  211. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
  212. package/blocks/forms/README.md +10 -0
  213. package/blocks/forms/block.json +107 -0
  214. package/blocks/forms/components/form-activity.tsx +71 -0
  215. package/blocks/forms/components/form-backfill.tsx +108 -0
  216. package/blocks/forms/components/form-builder.tsx +1 -1
  217. package/blocks/forms/components/form-embed.tsx +1 -1
  218. package/blocks/forms/components/form-groups.tsx +151 -0
  219. package/blocks/forms/components/form-mapping-editor.tsx +6 -9
  220. package/blocks/forms/components/form-route-history.tsx +72 -0
  221. package/blocks/forms/components/form-saved-views.tsx +81 -0
  222. package/blocks/forms/components/forms-screen.tsx +57 -20
  223. package/blocks/forms/components/more-rows.tsx +30 -0
  224. package/blocks/forms/components/submissions-export.tsx +67 -0
  225. package/blocks/forms/components/submissions-table.tsx +15 -6
  226. package/blocks/forms/lib/forms-extra.ts +67 -0
  227. package/blocks/inbox/README.md +45 -4
  228. package/blocks/inbox/block.json +145 -8
  229. package/blocks/inbox/components/blocked-senders.tsx +103 -0
  230. package/blocks/inbox/components/compose-message.tsx +193 -0
  231. package/blocks/inbox/components/inbox-screen.tsx +54 -6
  232. package/blocks/inbox/components/inbox-search.tsx +106 -0
  233. package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
  234. package/blocks/inbox/components/tag-manager.tsx +136 -0
  235. package/blocks/inbox/components/thread-actions.tsx +133 -0
  236. package/blocks/inbox/components/thread-badge.tsx +2 -2
  237. package/blocks/inbox/components/thread-view.tsx +3 -1
  238. package/blocks/inbox/hooks/use-inbox-mailboxes.ts +31 -0
  239. package/blocks/inbox/lib/inbox-manage.ts +227 -0
  240. package/blocks/integrations/README.md +54 -1
  241. package/blocks/integrations/block.json +189 -15
  242. package/blocks/integrations/components/ad-accounts.tsx +82 -0
  243. package/blocks/integrations/components/connection-health.tsx +59 -0
  244. package/blocks/integrations/components/crm-detail.tsx +20 -1
  245. package/blocks/integrations/components/crm-records.tsx +103 -0
  246. package/blocks/integrations/components/field-mappings.tsx +61 -0
  247. package/blocks/integrations/components/integration-list.tsx +4 -4
  248. package/blocks/integrations/components/integrations-screen.tsx +20 -3
  249. package/blocks/integrations/components/migration-panel.tsx +133 -0
  250. package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
  251. package/blocks/integrations/components/sync-status.tsx +6 -6
  252. package/blocks/integrations/components/sync-triggers.tsx +134 -0
  253. package/blocks/integrations/components/webhook-detail.tsx +26 -7
  254. package/blocks/integrations/components/webhook-manager.tsx +30 -17
  255. package/blocks/integrations/components/workspace-tools.tsx +127 -0
  256. package/blocks/integrations/lib/crm-utils.ts +39 -0
  257. package/blocks/integrations/lib/facts.tsx +13 -0
  258. package/blocks/integrations/lib/integrations-utils.ts +5 -0
  259. package/blocks/integrations/lib/load-more.tsx +141 -0
  260. package/blocks/intent/components/account-intent-score.tsx +5 -5
  261. package/blocks/intent/components/intent-feed.tsx +8 -8
  262. package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
  263. package/blocks/intent/lib/intent-utils.ts +4 -4
  264. package/blocks/lifecycle/components/account-related.tsx +8 -1
  265. package/blocks/lifecycle/components/churn-list.tsx +5 -2
  266. package/blocks/lifecycle/components/customer-table.tsx +1 -1
  267. package/blocks/lists/README.md +7 -1
  268. package/blocks/lists/block.json +31 -3
  269. package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
  270. package/blocks/lists/components/add-to-list-button.tsx +17 -7
  271. package/blocks/lists/components/list-picker.tsx +13 -3
  272. package/blocks/lists/components/list-table.tsx +1 -1
  273. package/blocks/lists/components/lists-screen.tsx +4 -2
  274. package/blocks/lists/components/record-lists-tab.tsx +99 -0
  275. package/blocks/lists/components/suppression-manager.tsx +27 -6
  276. package/blocks/lists/components/suppressions-screen.tsx +21 -12
  277. package/blocks/mailboxes/block.json +4 -0
  278. package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
  279. package/blocks/mailboxes/components/domain-detail.tsx +10 -4
  280. package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
  281. package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
  282. package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
  283. package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
  284. package/blocks/mailboxes/components/warmup-health.tsx +20 -20
  285. package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
  286. package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
  287. package/blocks/newsletter/README.md +12 -0
  288. package/blocks/newsletter/block.json +117 -0
  289. package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
  290. package/blocks/newsletter/components/issue-versions.tsx +93 -0
  291. package/blocks/newsletter/components/more-rows.tsx +30 -0
  292. package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
  293. package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
  294. package/blocks/newsletter/components/newsletter-screen.tsx +38 -13
  295. package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
  296. package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
  297. package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
  298. package/blocks/newsletter/components/subscriber-table.tsx +5 -7
  299. package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
  300. package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
  301. package/blocks/nurture/components/nurture-builder.tsx +10 -2
  302. package/blocks/nurture/components/nurture-enroll.tsx +6 -7
  303. package/blocks/nurture/components/nurture-form.tsx +5 -8
  304. package/blocks/nurture/components/nurture-screen.tsx +16 -5
  305. package/blocks/nurture/components/nurture-stats.tsx +5 -4
  306. package/blocks/objects/README.md +1 -1
  307. package/blocks/objects/block.json +13 -5
  308. package/blocks/objects/components/attribute-field-input.tsx +14 -1
  309. package/blocks/objects/components/object-record-detail.tsx +8 -1
  310. package/blocks/objects/components/object-schema-editor.tsx +1 -1
  311. package/blocks/objects/components/record-roles-panel.tsx +15 -5
  312. package/blocks/pages/README.md +14 -0
  313. package/blocks/pages/block.json +123 -2
  314. package/blocks/pages/components/more-rows.tsx +30 -0
  315. package/blocks/pages/components/page-ai-tools.tsx +196 -0
  316. package/blocks/pages/components/page-analytics.tsx +176 -0
  317. package/blocks/pages/components/page-clone.tsx +99 -0
  318. package/blocks/pages/components/page-domains.tsx +167 -0
  319. package/blocks/pages/components/page-editor.tsx +1 -1
  320. package/blocks/pages/components/page-generation-status.tsx +48 -0
  321. package/blocks/pages/components/page-tracking.tsx +137 -0
  322. package/blocks/pages/components/pages-screen.tsx +57 -16
  323. package/blocks/pages/components/seo-report.tsx +6 -1
  324. package/blocks/pages/hooks/use-landing-page-paths.ts +60 -0
  325. package/blocks/pages/lib/studio.ts +7 -0
  326. package/blocks/phone-numbers/README.md +74 -0
  327. package/blocks/phone-numbers/block.json +262 -0
  328. package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
  329. package/blocks/phone-numbers/components/assign-number.tsx +104 -0
  330. package/blocks/phone-numbers/components/number-search.tsx +239 -0
  331. package/blocks/phone-numbers/components/number-settings.tsx +134 -0
  332. package/blocks/phone-numbers/components/number-usage.tsx +97 -0
  333. package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
  334. package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
  335. package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
  336. package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
  337. package/blocks/phone-numbers/components/release-number.tsx +126 -0
  338. package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
  339. package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
  340. package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
  341. package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
  342. package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
  343. package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
  344. package/blocks/pickers/README.md +23 -0
  345. package/blocks/pickers/block.json +103 -0
  346. package/blocks/pickers/components/company-picker.tsx +90 -0
  347. package/blocks/pickers/components/contact-picker.tsx +131 -0
  348. package/blocks/pickers/components/member-picker.tsx +99 -0
  349. package/blocks/pickers/components/search-picker.tsx +98 -0
  350. package/blocks/pickers/hooks/use-current-member.ts +104 -0
  351. package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
  352. package/blocks/pickers/lib/use-debounced.ts +13 -0
  353. package/blocks/programmatic-pages/README.md +82 -0
  354. package/blocks/programmatic-pages/block.json +292 -0
  355. package/blocks/programmatic-pages/components/programmatic-campaign-form.tsx +136 -0
  356. package/blocks/programmatic-pages/components/programmatic-generate.tsx +112 -0
  357. package/blocks/programmatic-pages/components/programmatic-generated-pages.tsx +210 -0
  358. package/blocks/programmatic-pages/components/programmatic-overview.tsx +84 -0
  359. package/blocks/programmatic-pages/components/programmatic-pages-screen.tsx +221 -0
  360. package/blocks/programmatic-pages/components/programmatic-preview.tsx +66 -0
  361. package/blocks/programmatic-pages/components/programmatic-publish.tsx +102 -0
  362. package/blocks/programmatic-pages/components/programmatic-settings.tsx +96 -0
  363. package/blocks/programmatic-pages/components/programmatic-source-fields.tsx +75 -0
  364. package/blocks/programmatic-pages/components/programmatic-source.tsx +106 -0
  365. package/blocks/programmatic-pages/components/programmatic-variables.tsx +84 -0
  366. package/blocks/programmatic-pages/hooks/use-programmatic-workspace.ts +19 -0
  367. package/blocks/programmatic-pages/lib/programmatic-types.ts +330 -0
  368. package/blocks/programmatic-pages/recipes/create-a-programmatic-campaign.ts +23 -0
  369. package/blocks/programmatic-pages/recipes/render-and-follow.ts +21 -0
  370. package/blocks/programmatic-pages/recipes/review-then-publish.ts +17 -0
  371. package/blocks/quotes/README.md +38 -1
  372. package/blocks/quotes/block.json +170 -6
  373. package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
  374. package/blocks/quotes/components/manual-products.tsx +144 -0
  375. package/blocks/quotes/components/quote-batch-actions.tsx +86 -0
  376. package/blocks/quotes/components/quote-builder.tsx +1 -1
  377. package/blocks/quotes/components/quote-documents.tsx +94 -0
  378. package/blocks/quotes/components/quote-evidence.tsx +95 -0
  379. package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
  380. package/blocks/quotes/components/quote-list.tsx +28 -8
  381. package/blocks/quotes/components/quote-related.tsx +3 -2
  382. package/blocks/quotes/components/quote-settings.tsx +184 -0
  383. package/blocks/quotes/components/quote-templates.tsx +189 -0
  384. package/blocks/quotes/components/quote-viewer.tsx +23 -5
  385. package/blocks/quotes/components/quotes-screen.tsx +15 -5
  386. package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
  387. package/blocks/quotes/lib/quote-admin.ts +264 -0
  388. package/blocks/radar/components/competitor-battle-card.tsx +1 -1
  389. package/blocks/radar/components/radar-overview.tsx +4 -4
  390. package/blocks/radar/components/radar-screen.tsx +2 -2
  391. package/blocks/sequencer/README.md +61 -0
  392. package/blocks/sequencer/block.json +200 -6
  393. package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
  394. package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
  395. package/blocks/sequencer/components/enrollment-withdraw.tsx +71 -0
  396. package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
  397. package/blocks/sequencer/components/sequence-builder.tsx +32 -23
  398. package/blocks/sequencer/components/sequence-campaign-link.tsx +78 -0
  399. package/blocks/sequencer/components/sequence-detail.tsx +10 -2
  400. package/blocks/sequencer/components/sequence-edit-form.tsx +111 -0
  401. package/blocks/sequencer/components/sequence-enrollments.tsx +5 -2
  402. package/blocks/sequencer/components/sequence-list.tsx +1 -1
  403. package/blocks/sequencer/components/sequence-manage-actions.tsx +119 -0
  404. package/blocks/sequencer/components/sequence-senders-panel.tsx +84 -0
  405. package/blocks/sequencer/components/sequence-stats.tsx +8 -8
  406. package/blocks/sequencer/components/sequence-templates.tsx +160 -0
  407. package/blocks/sequencer/components/sequences-screen.tsx +39 -6
  408. package/blocks/sequencer/components/step-ai-assist.tsx +196 -0
  409. package/blocks/sequencer/components/step-editor.tsx +21 -12
  410. package/blocks/sequencer/components/step-templates.tsx +224 -0
  411. package/blocks/sequencer/lib/sequencer-manage.ts +235 -0
  412. package/blocks/sequencer/lib/sequencer-utils.ts +9 -5
  413. package/blocks/signals/README.md +39 -0
  414. package/blocks/signals/block.json +287 -0
  415. package/blocks/signals/components/auto-seed-panel.tsx +111 -0
  416. package/blocks/signals/components/listener-form.tsx +184 -0
  417. package/blocks/signals/components/matched-posts.tsx +195 -0
  418. package/blocks/signals/components/post-engagements.tsx +114 -0
  419. package/blocks/signals/components/record-social-tab.tsx +49 -0
  420. package/blocks/signals/components/social-listeners-screen.tsx +250 -0
  421. package/blocks/signals/components/social-settings.tsx +128 -0
  422. package/blocks/signals/components/source-lists.tsx +83 -0
  423. package/blocks/signals/components/top-engagers.tsx +68 -0
  424. package/blocks/signals/components/x-integration.tsx +129 -0
  425. package/blocks/signals/lib/signals-types.ts +228 -0
  426. package/blocks/signals/recipes/preview-and-create-a-listener.ts +16 -0
  427. package/blocks/signals/recipes/spend-against-caps.ts +14 -0
  428. package/blocks/signals/recipes/triage-matched-posts.ts +18 -0
  429. package/blocks/social-ads/README.md +8 -0
  430. package/blocks/social-ads/block.json +137 -4
  431. package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
  432. package/blocks/social-ads/components/audience-sync.tsx +6 -2
  433. package/blocks/social-ads/components/campaign-ads.tsx +166 -0
  434. package/blocks/social-ads/components/campaign-detail.tsx +6 -1
  435. package/blocks/social-ads/components/campaign-editor.tsx +134 -0
  436. package/blocks/social-ads/components/creative-concepts.tsx +74 -0
  437. package/blocks/social-ads/components/creative-detail.tsx +6 -1
  438. package/blocks/social-ads/components/creative-gallery.tsx +20 -2
  439. package/blocks/social-ads/components/creative-revisions.tsx +57 -0
  440. package/blocks/social-ads/components/creative-tools.tsx +176 -0
  441. package/blocks/social-ads/components/post-composer.tsx +4 -2
  442. package/blocks/social-ads/components/post-detail.tsx +3 -0
  443. package/blocks/social-ads/components/social-ideas.tsx +64 -0
  444. package/blocks/social-ads/components/social-performance.tsx +80 -0
  445. package/blocks/social-ads/components/social-screen.tsx +33 -6
  446. package/blocks/social-ads/lib/ads-extra.ts +67 -0
  447. package/blocks/surveys/README.md +91 -0
  448. package/blocks/surveys/block.json +299 -0
  449. package/blocks/surveys/components/account-nps.tsx +69 -0
  450. package/blocks/surveys/components/contact-survey-history.tsx +52 -0
  451. package/blocks/surveys/components/org-summary.tsx +82 -0
  452. package/blocks/surveys/components/sending-domains.tsx +113 -0
  453. package/blocks/surveys/components/survey-form.tsx +126 -0
  454. package/blocks/surveys/components/survey-responses.tsx +145 -0
  455. package/blocks/surveys/components/survey-results.tsx +72 -0
  456. package/blocks/surveys/components/survey-send-history.tsx +58 -0
  457. package/blocks/surveys/components/survey-send.tsx +162 -0
  458. package/blocks/surveys/components/survey-triggers.tsx +166 -0
  459. package/blocks/surveys/components/surveys-screen.tsx +234 -0
  460. package/blocks/surveys/lib/surveys-types.ts +228 -0
  461. package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
  462. package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
  463. package/blocks/surveys/recipes/survey-score-report.ts +27 -0
  464. package/blocks/visitors/components/snippet-install.tsx +18 -9
  465. package/blocks/visitors/components/traffic-chart.tsx +7 -6
  466. package/blocks/visitors/components/visitor-company-card.tsx +24 -14
  467. package/blocks/visitors/components/visitor-feed.tsx +5 -5
  468. package/blocks/web-analytics/README.md +102 -0
  469. package/blocks/web-analytics/block.json +292 -0
  470. package/blocks/web-analytics/components/acquisition-tab.tsx +154 -0
  471. package/blocks/web-analytics/components/analytics-filters.tsx +161 -0
  472. package/blocks/web-analytics/components/audience-tab.tsx +135 -0
  473. package/blocks/web-analytics/components/conversions-tab.tsx +128 -0
  474. package/blocks/web-analytics/components/engagement-tab.tsx +123 -0
  475. package/blocks/web-analytics/components/overview-tab.tsx +102 -0
  476. package/blocks/web-analytics/components/pages-tab.tsx +148 -0
  477. package/blocks/web-analytics/components/ranked-table.tsx +103 -0
  478. package/blocks/web-analytics/components/realtime-tab.tsx +103 -0
  479. package/blocks/web-analytics/components/report-card.tsx +71 -0
  480. package/blocks/web-analytics/components/web-analytics-screen.tsx +90 -0
  481. package/blocks/web-analytics/hooks/use-report.ts +48 -0
  482. package/blocks/web-analytics/lib/analytics-scope.ts +155 -0
  483. package/blocks/web-analytics/lib/page-path.ts +14 -0
  484. package/blocks/web-analytics/lib/web-analytics-types.ts +187 -0
  485. package/blocks/web-analytics/recipes/best-landing-pages.ts +26 -0
  486. package/blocks/web-analytics/recipes/form-conversion-check.ts +19 -0
  487. package/blocks/web-analytics/recipes/weekly-traffic-report.ts +37 -0
  488. package/blocks/webchat/components/chat-widget-config.tsx +1 -1
  489. package/blocks/webchat/components/conversation-list.tsx +1 -1
  490. package/blocks/workbench/README.md +54 -2
  491. package/blocks/workbench/block.json +234 -5
  492. package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
  493. package/blocks/workbench/components/run-progress.tsx +1 -1
  494. package/blocks/workbench/components/workbench-auto-enrich.tsx +88 -0
  495. package/blocks/workbench/components/workbench-automation.tsx +36 -0
  496. package/blocks/workbench/components/workbench-connections.tsx +136 -0
  497. package/blocks/workbench/components/workbench-crm-import.tsx +91 -0
  498. package/blocks/workbench/components/workbench-datafile-import.tsx +154 -0
  499. package/blocks/workbench/components/workbench-grid.tsx +71 -8
  500. package/blocks/workbench/components/workbench-pipelines.tsx +173 -0
  501. package/blocks/workbench/components/workbench-row-actions.tsx +8 -1
  502. package/blocks/workbench/components/workbench-schedule.tsx +191 -0
  503. package/blocks/workbench/components/workbench-screen.tsx +65 -22
  504. package/blocks/workbench/components/workbench-send-to-sequence.tsx +116 -0
  505. package/blocks/workbench/components/workbench-table-list.tsx +14 -2
  506. package/blocks/workbench/components/workbench-table-settings.tsx +53 -0
  507. package/blocks/workbench/components/workbench-triggers.tsx +178 -0
  508. package/blocks/workbench/hooks/use-connected-crms.ts +29 -0
  509. package/blocks/workbench/lib/api-status.ts +8 -0
  510. package/blocks/workbench/lib/automation-types.ts +257 -0
  511. package/blocks/workflows/components/workflow-editor.tsx +1 -1
  512. package/dist/cli.js +744 -394
  513. package/dist/index.browser.js +1 -1
  514. package/dist/index.browser.js.map +1 -1
  515. package/dist/index.browser.mjs +1 -1
  516. package/dist/index.browser.mjs.map +1 -1
  517. package/dist/index.d.mts +2 -2
  518. package/dist/index.d.ts +2 -2
  519. package/dist/index.js +1 -1
  520. package/dist/index.js.map +1 -1
  521. package/dist/index.mjs +1 -1
  522. package/dist/index.mjs.map +1 -1
  523. package/dist/react.browser.js +1 -1
  524. package/dist/react.browser.js.map +1 -1
  525. package/dist/react.browser.mjs +1 -1
  526. package/dist/react.browser.mjs.map +1 -1
  527. package/dist/react.d.mts +2 -2
  528. package/dist/react.d.ts +2 -2
  529. package/dist/react.js +1 -1
  530. package/dist/react.js.map +1 -1
  531. package/dist/react.mjs +1 -1
  532. package/dist/react.mjs.map +1 -1
  533. package/dist/types/blocks/auth.d.ts +45 -0
  534. package/dist/types/blocks/brand.d.ts +11 -2
  535. package/dist/types/blocks/create.d.ts +11 -4
  536. package/dist/types/blocks/doctor.d.ts +1 -0
  537. package/dist/types/blocks/flows.d.ts +7 -1
  538. package/dist/types/blocks/orgBrand.d.ts +34 -0
  539. package/dist/types/cli.d.ts +3 -0
  540. package/dist/types/version.d.ts +2 -2
  541. package/docs/api/desk.d.ts +2 -2
  542. package/docs/api/desk.md +3 -3
  543. package/docs/api/forms.md +1 -1
  544. package/package.json +1 -1
  545. package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
  546. package/stacks/6sense/pages/contacts.tsx.tmpl +21 -0
  547. package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
  548. package/stacks/6sense.json +37 -4
  549. package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
  550. package/stacks/apollo/pages/campaigns.tsx.tmpl +27 -0
  551. package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
  552. package/stacks/apollo/pages/contacts.tsx.tmpl +35 -95
  553. package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
  554. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
  555. package/stacks/apollo/pages/lists.tsx.tmpl +24 -0
  556. package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
  557. package/stacks/apollo/pages/sequences.tsx.tmpl +18 -0
  558. package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
  559. package/stacks/apollo.json +68 -9
  560. package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
  561. package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
  562. package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
  563. package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
  564. package/stacks/calendly.json +8 -4
  565. package/stacks/clay/pages/companies.tsx.tmpl +17 -1
  566. package/stacks/clay/pages/contacts.tsx.tmpl +14 -0
  567. package/stacks/clay/pages/lists.tsx.tmpl +24 -0
  568. package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
  569. package/stacks/clay.json +16 -3
  570. package/stacks/gong/pages/calls.tsx.tmpl +23 -0
  571. package/stacks/gong/pages/companies.tsx.tmpl +22 -0
  572. package/stacks/gong/pages/contacts.tsx.tmpl +34 -0
  573. package/stacks/gong/pages/lists.tsx.tmpl +12 -0
  574. package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
  575. package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
  576. package/stacks/gong.json +23 -6
  577. package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
  578. package/stacks/hootsuite/pages/contacts.tsx.tmpl +30 -0
  579. package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
  580. package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
  581. package/stacks/hootsuite.json +19 -4
  582. package/stacks/hubspot/pages/campaigns.tsx.tmpl +30 -0
  583. package/stacks/hubspot/pages/companies.tsx.tmpl +16 -35
  584. package/stacks/hubspot/pages/contacts.tsx.tmpl +35 -95
  585. package/stacks/hubspot/pages/deals.tsx.tmpl +14 -1
  586. package/stacks/hubspot/pages/forms.tsx.tmpl +36 -0
  587. package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
  588. package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
  589. package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
  590. package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
  591. package/stacks/hubspot/pages/sequences.tsx.tmpl +18 -0
  592. package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
  593. package/stacks/hubspot/pages/web-analytics.tsx.tmpl +14 -0
  594. package/stacks/hubspot.json +66 -7
  595. package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
  596. package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
  597. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
  598. package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
  599. package/stacks/intercom.json +18 -4
  600. package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
  601. package/stacks/mailchimp/pages/contacts.tsx.tmpl +30 -0
  602. package/stacks/mailchimp/pages/forms.tsx.tmpl +36 -0
  603. package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
  604. package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
  605. package/stacks/mailchimp.json +22 -6
  606. package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
  607. package/stacks/outreach/pages/campaigns.tsx.tmpl +27 -0
  608. package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
  609. package/stacks/outreach/pages/contacts.tsx.tmpl +29 -66
  610. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
  611. package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
  612. package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
  613. package/stacks/outreach/pages/sequences.tsx.tmpl +18 -0
  614. package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
  615. package/stacks/outreach.json +38 -9
  616. package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
  617. package/stacks/pipedrive/pages/contacts.tsx.tmpl +17 -35
  618. package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
  619. package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
  620. package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
  621. package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
  622. package/stacks/pipedrive.json +14 -2
  623. package/stacks/salesforce/pages/companies.tsx.tmpl +16 -35
  624. package/stacks/salesforce/pages/contacts.tsx.tmpl +26 -35
  625. package/stacks/salesforce/pages/deals.tsx.tmpl +14 -1
  626. package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
  627. package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
  628. package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
  629. package/stacks/salesforce.json +17 -3
  630. package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
  631. package/stacks/zoominfo/pages/contacts.tsx.tmpl +16 -0
  632. package/stacks/zoominfo/pages/lists.tsx.tmpl +24 -0
  633. package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
  634. package/stacks/zoominfo.json +36 -3
  635. package/styles/styles.css +1 -1
  636. package/templates/auth/auth-route.ts.tmpl +16 -0
  637. package/templates/auth/g8.auth.anonymous.ts.tmpl +15 -0
  638. package/templates/auth/g8.auth.basic.ts.tmpl +17 -0
  639. package/templates/auth/g8.auth.jwt.ts.tmpl +23 -0
  640. package/templates/auth/g8.auth.password.ts.tmpl +18 -0
  641. package/templates/auth/g8.auth.session.ts.tmpl +23 -0
  642. package/templates/auth/middleware.ts.tmpl +14 -0
  643. package/templates/next-app/README.md.tmpl +5 -0
  644. package/templates/next-app/app/globals.css.tmpl +349 -64
  645. package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
  646. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
@@ -0,0 +1,184 @@
1
+ "use client";
2
+
3
+ /**
4
+ * QuoteSettings: the org-wide quote defaults: company details and branding, terms, numbering, who quotes are sent as,
5
+ * the billing provider, deal automation, required billing fields and saved terms. The document library sits under it.
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. getQuoteSettings GET /quote-settings read - the values plus `inherited_branding` (what a blank field falls back to)
9
+ * 2. updateQuoteSettings PUT /quote-settings external tier (org admin) -> ConfirmSpendDialog when it changes who quotes are sent as,
10
+ * the billing provider or the deal automation
11
+ * QuoteDocuments (child) owns the library calls.
12
+ *
13
+ * The API only writes values that are not null, so a field that has a value CANNOT be cleared through it: the form
14
+ * says so and holds Save until that field has a value again, instead of reporting a save that changed nothing. Only the
15
+ * fields that changed are sent, and the form reloads from the API after a save. Only Manual and Stripe are offered as
16
+ * billing providers: QuickBooks, Xero and Chargebee are registered but not built, so choosing one would fail.
17
+ */
18
+ import { useEffect, useRef, useState } from "react";
19
+ import { Settings2, Trash2 } from "lucide-react";
20
+ import { Button, Input, Label, Skeleton, Textarea } from "@graph8/sdk/ui";
21
+ import { ApiError } from "@/g8/components/api-error";
22
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
23
+ import { SectionCard } from "@/g8/components/screen/section-card";
24
+ import { useBrand } from "@/g8/hooks/use-brand";
25
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
26
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
27
+ import { g8Get, g8Put } from "@/g8/lib/g8-client";
28
+ import {
29
+ BILLING_FIELDS, BILLING_PROVIDERS, FIELD_LABEL, draftFromSettings, settingsChanges, settingsEffects, settingsProblems, unclearableSettings,
30
+ type SettingsBool, type SettingsDraft, type SettingsField,
31
+ } from "../lib/quote-admin";
32
+ import { PAYMENT_TERMS, asRec, body, text } from "../lib/quotes";
33
+ import { QuoteDocuments } from "./quote-documents";
34
+
35
+ const BOOL_LABEL: Record<SettingsBool, string> = {
36
+ auto_close_deal_on_acceptance: "Move the deal to Closed Won when a quote is accepted",
37
+ auto_advance_deal_on_quote_send: "Move an earlier open deal to Proposal Sent when a quote is sent",
38
+ quote_acceptance_roam_notifications_enabled: "Post to a Roam channel when a quote is accepted",
39
+ };
40
+ const words = (s: string) => s.replace(/_/g, " ");
41
+
42
+ export function QuoteSettings({ className }: { className?: string }) {
43
+ const brand = useBrand();
44
+ const q = useG8Query((signal) => g8Get("/quote-settings", undefined, { signal }), []);
45
+ const row = body(q.data);
46
+ const [was, setWas] = useState<SettingsDraft | null>(null);
47
+ const [now, setNow] = useState<SettingsDraft | null>(null);
48
+ const [confirm, setConfirm] = useState(false);
49
+ const inherited = asRec(row.inherited_branding);
50
+
51
+ // Take the API's answer as the new baseline whenever it lands (first load, and after a save). Typing that happened while a
52
+ // reload was in flight is kept: the form is only replaced when the user had not changed anything since the last baseline.
53
+ const baseline = useRef<SettingsDraft | null>(null);
54
+ useEffect(() => {
55
+ if (!q.data) return;
56
+ const d = draftFromSettings(body(q.data));
57
+ const old = baseline.current;
58
+ baseline.current = d;
59
+ setWas(d);
60
+ setNow((cur) => (cur && old && JSON.stringify(cur) !== JSON.stringify(old) ? cur : d));
61
+ }, [q.data]);
62
+
63
+ const changes = was && now ? settingsChanges(was, now) : {};
64
+ const blocked = was && now ? unclearableSettings(was, now) : [];
65
+ const problems = now ? settingsProblems(now) : [];
66
+ const effects = was ? settingsEffects(changes, was) : [];
67
+ const save = useG8Mutation(() => g8Put("/quote-settings", changes), { onSuccess: () => { setConfirm(false); void q.refetch(); } });
68
+
69
+ if (q.error && !q.data) return <ApiError error={q.error} title="Could not load quote settings" onRetry={() => void q.refetch()} />;
70
+ if (!now || !was) return <Skeleton className="h-64 w-full" aria-label="Loading quote settings" />;
71
+
72
+ const text_ = (k: SettingsField, v: string) => setNow({ ...now, text: { ...now.text, [k]: v } });
73
+ const dirty = Object.keys(changes).length > 0;
74
+ // Anything typed counts for Discard, including a blanked field that Save will refuse (it is not a change the API can take).
75
+ const edited = JSON.stringify(now) !== JSON.stringify(was);
76
+ const canSave = dirty && blocked.length === 0 && problems.length === 0;
77
+ const submit = () => (effects.length > 0 ? setConfirm(true) : void save.mutate(undefined));
78
+
79
+ // A render function, not a component: a component defined here would remount (and drop focus) on every keystroke.
80
+ const field = (k: SettingsField, { area, placeholder, type }: { area?: boolean; placeholder?: string; type?: string } = {}) => (
81
+ <div className="space-y-1" key={k}>
82
+ <Label htmlFor={`qs-${k}`}>{FIELD_LABEL[k]}</Label>
83
+ {area ? <Textarea id={`qs-${k}`} rows={3} value={now.text[k]} placeholder={placeholder} onChange={(e) => text_(k, e.target.value)} /> : <Input id={`qs-${k}`} type={type} value={now.text[k]} placeholder={placeholder} onChange={(e) => text_(k, e.target.value)} />}
84
+ {blocked.includes(k) ? <p role="alert" className="text-xs text-destructive">The API cannot clear this field. Enter a new value, or clear it in {brand.linkLabel}.</p> : null}
85
+ </div>
86
+ );
87
+
88
+ return (
89
+ <div className={`space-y-4 ${className ?? ""}`}>
90
+ {q.error ? <ApiError error={q.error} title="Could not refresh quote settings" onRetry={() => void q.refetch()} /> : null}
91
+ <form className="space-y-4" onSubmit={(e) => { e.preventDefault(); if (canSave) submit(); }}>
92
+ <SectionCard title="Company and branding" icon={<Settings2 />} description="Shown on every quote. A blank field falls back to your company profile.">
93
+ <div className="grid gap-3 sm:grid-cols-2">
94
+ {field("company_name", { placeholder: text(inherited.company_name) })}
95
+ {field("company_email", { placeholder: text(inherited.company_email) })}
96
+ {field("company_phone", { placeholder: text(inherited.company_phone) })}
97
+ {field("accent_color", { placeholder: "#111111" })}
98
+ <div className="sm:col-span-2">{field("company_address", { area: true, placeholder: text(inherited.company_address) })}</div>
99
+ <div className="sm:col-span-2">{field("company_logo_url", { placeholder: text(inherited.company_logo_url) })}</div>
100
+ <div className="sm:col-span-2">{field("footer_text", { area: true })}</div>
101
+ </div>
102
+ </SectionCard>
103
+
104
+ <SectionCard title="Terms and numbering">
105
+ <div className="grid gap-3 sm:grid-cols-2">
106
+ {field("quote_number_prefix")}
107
+ <div className="space-y-1"><Label htmlFor="qs-valid">Default valid days</Label>
108
+ <Input id="qs-valid" type="number" min={1} value={now.default_valid_days} onChange={(e) => setNow({ ...now, default_valid_days: e.target.value })} />
109
+ {blocked.includes("default_valid_days") ? <p role="alert" className="text-xs text-destructive">The API cannot clear this field. Enter a number.</p> : null}</div>
110
+ <div className="space-y-1"><Label htmlFor="qs-pay">{FIELD_LABEL.default_payment_terms}</Label>
111
+ <select id="qs-pay" className="h-9 w-full rounded-md border bg-background px-2 text-sm" value={now.text.default_payment_terms} onChange={(e) => text_("default_payment_terms", e.target.value)}>
112
+ {now.text.default_payment_terms === "" ? <option value="">Not set</option> : null}
113
+ {!(PAYMENT_TERMS as readonly string[]).includes(now.text.default_payment_terms) && now.text.default_payment_terms ? <option value={now.text.default_payment_terms}>{words(now.text.default_payment_terms)}</option> : null}
114
+ {PAYMENT_TERMS.map((t) => <option key={t} value={t}>{words(t)}</option>)}
115
+ </select></div>
116
+ <div className="sm:col-span-2">{field("default_terms", { area: true })}</div>
117
+ <div className="sm:col-span-2">{field("default_privacy", { area: true })}</div>
118
+ {field("terms_url", { type: "url" })}
119
+ {field("privacy_url", { type: "url" })}
120
+ </div>
121
+ <fieldset className="mt-4 space-y-2">
122
+ <legend className="text-sm font-medium">Saved terms</legend>
123
+ {now.terms_library.length === 0 ? <p className="text-xs text-muted-foreground">No saved terms. Add the clauses reps pick from when they build a quote.</p> : null}
124
+ {now.terms_library.map((t, i) => {
125
+ const set = (patch: Partial<typeof t>) => setNow({ ...now, terms_library: now.terms_library.map((x, j) => (j === i ? { ...x, ...patch } : x)) });
126
+ return (
127
+ <div key={i} className="grid gap-2 rounded-md border border-border p-3 sm:grid-cols-[1fr_9rem_auto]">
128
+ <Input aria-label={`Terms name ${i + 1}`} placeholder="Name" value={t.name} onChange={(e) => set({ name: e.target.value })} />
129
+ <select aria-label={`Terms payment terms ${i + 1}`} className="h-9 rounded-md border bg-background px-2 text-sm" value={t.payment_terms} onChange={(e) => set({ payment_terms: e.target.value })}>
130
+ <option value="">Any</option>{PAYMENT_TERMS.map((p) => <option key={p} value={p}>{words(p)}</option>)}</select>
131
+ <Button type="button" variant="ghost" size="sm" aria-label={`Remove terms ${i + 1}`} onClick={() => setNow({ ...now, terms_library: now.terms_library.filter((_, j) => j !== i) })}><Trash2 className="size-4" aria-hidden /></Button>
132
+ <Textarea className="sm:col-span-3" aria-label={`Terms text ${i + 1}`} rows={3} placeholder="Clause text" value={t.content} onChange={(e) => set({ content: e.target.value })} />
133
+ </div>
134
+ );
135
+ })}
136
+ <Button type="button" size="sm" variant="outline" onClick={() => setNow({ ...now, terms_library: [...now.terms_library, { name: "", payment_terms: "", content: "" }] })}>Add terms</Button>
137
+ </fieldset>
138
+ </SectionCard>
139
+
140
+ <SectionCard title="Who quotes are sent as" description="Quote emails use this identity and reply address.">
141
+ <div className="grid gap-3 sm:grid-cols-2">
142
+ <div className="space-y-1 sm:col-span-2"><Label htmlFor="qs-mode">{FIELD_LABEL.sender_mode}</Label>
143
+ <select id="qs-mode" className="h-9 w-full rounded-md border bg-background px-2 text-sm" value={now.text.sender_mode} onChange={(e) => text_("sender_mode", e.target.value)}>
144
+ {now.text.sender_mode === "" ? <option value="">Not set</option> : null}
145
+ <option value="fixed">The sender below, on every quote</option><option value="creator">The person who sends each quote</option></select></div>
146
+ {field("sender_name")}{field("sender_email", { type: "email" })}
147
+ </div>
148
+ </SectionCard>
149
+
150
+ <SectionCard title="Billing and deals" description="What happens when a quote is signed.">
151
+ <div className="space-y-3">
152
+ <div className="space-y-1"><Label htmlFor="qs-provider">{FIELD_LABEL.default_billing_provider}</Label>
153
+ <select id="qs-provider" className="h-9 w-full rounded-md border bg-background px-2 text-sm" value={now.text.default_billing_provider} onChange={(e) => text_("default_billing_provider", e.target.value)}>
154
+ {!BILLING_PROVIDERS.some(([v]) => v === now.text.default_billing_provider) ? <option value={now.text.default_billing_provider}>{now.text.default_billing_provider ? `${words(now.text.default_billing_provider)} (not available here)` : "Not set"}</option> : null}
155
+ {BILLING_PROVIDERS.map(([v, l]) => <option key={v} value={v}>{l}</option>)}</select></div>
156
+ <fieldset className="space-y-1"><legend className="text-sm font-medium">Required billing details</legend>
157
+ {BILLING_FIELDS.map((f) => (
158
+ <label key={f} className="flex items-center gap-2 text-sm"><input type="checkbox" checked={now.required_billing_fields.includes(f)}
159
+ onChange={(e) => setNow({ ...now, required_billing_fields: e.target.checked ? [...now.required_billing_fields, f] : now.required_billing_fields.filter((x) => x !== f) })} />{words(f)}</label>
160
+ ))}</fieldset>
161
+ {(Object.keys(BOOL_LABEL) as SettingsBool[]).map((k) => (
162
+ <label key={k} className="flex items-center gap-2 text-sm"><input type="checkbox" checked={now.flags[k]} onChange={(e) => setNow({ ...now, flags: { ...now.flags, [k]: e.target.checked } })} />{BOOL_LABEL[k]}</label>
163
+ ))}
164
+ {now.flags.quote_acceptance_roam_notifications_enabled ? <div className="grid gap-3 sm:grid-cols-2">{field("quote_acceptance_roam_channel_name")}{field("quote_acceptance_roam_channel_id")}</div> : null}
165
+ </div>
166
+ </SectionCard>
167
+
168
+ {problems.length ? <ul role="alert" className="list-disc pl-5 text-sm text-destructive">{problems.map((p) => <li key={p}>{p}</li>)}</ul> : null}
169
+ {save.error ? <ApiError error={save.error} title="Could not save quote settings" /> : null}
170
+ <div className="flex items-center justify-end gap-2">
171
+ <Button type="button" variant="outline" disabled={!edited} onClick={() => setNow(was)}>Discard changes</Button>
172
+ <Button type="submit" disabled={!canSave} loading={save.loading && !confirm}>Save settings</Button>
173
+ </div>
174
+ </form>
175
+ <QuoteDocuments />
176
+
177
+ {confirm ? (
178
+ <ConfirmSpendDialog open tier="external" title="Save these quote settings?" confirmLabel="Save settings" confirming={save.loading}
179
+ description={<ul className="list-disc space-y-1 pl-5">{effects.map((e) => <li key={e}>{e}</li>)}</ul>}
180
+ onConfirm={() => void save.mutate(undefined)} onCancel={() => setConfirm(false)} />
181
+ ) : null}
182
+ </div>
183
+ );
184
+ }
@@ -0,0 +1,189 @@
1
+ "use client";
2
+
3
+ /**
4
+ * QuoteTemplates: the reusable starting points for a quote (payment terms, validity, contract length, terms text,
5
+ * attached legal documents and preset line items), listed, created, edited and deleted.
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. listQuoteTemplates GET /quote-templates read
9
+ * 2. listQuoteDocuments GET /quote-settings/documents read - the library a template attaches from
10
+ * 3. createQuoteTemplate POST /quote-templates external tier, stores data only (org admin)
11
+ * 4. updateQuoteTemplate PUT /quote-templates/{id} external tier (org admin): REPLACES the template
12
+ * 5. deleteQuoteTemplate DELETE /quote-templates/{id} DESTRUCTIVE (org admin) -> ConfirmSpendDialog
13
+ * Plus listQuotableProducts (read) inside ProductPicker, for the preset line items.
14
+ *
15
+ * Creating and saving a template send nobody anything, so they run on one click, like saving a draft quote. Two things
16
+ * still go through ConfirmSpendDialog: Delete, and a Save that would reset template settings the API cannot carry
17
+ * (payment methods, marketing rights, contract start on signature: the confirm names exactly which ones this template
18
+ * has). Writes need an organization admin; a refusal is shown as the API's own message.
19
+ */
20
+ import { useState } from "react";
21
+ import { FileStack, Pencil, Plus, Trash2 } from "lucide-react";
22
+ import { Button, Input, Label, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Textarea } from "@graph8/sdk/ui";
23
+ import { ApiError } from "@/g8/components/api-error";
24
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
25
+ import { EmptyState } from "@/g8/components/empty-state";
26
+ import { FormSheet } from "@/g8/components/screen/form-sheet";
27
+ import { SectionCard } from "@/g8/components/screen/section-card";
28
+ import { StatusPill } from "@/g8/components/screen/status-pill";
29
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
30
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
31
+ import { useBrand } from "@/g8/hooks/use-brand";
32
+ import { g8Delete, g8Get, g8Post, g8Put } from "@/g8/lib/g8-client";
33
+ import { DURATION_UNITS, draftFromTemplate, emptyTemplateDraft, fieldsLostOnTemplateSave, templateBody, templateProblems, type TemplateDraft } from "../lib/quote-admin";
34
+ import { PAYMENT_TERMS, body, linesFromQuote, text, toRequestLines, type LineItem, type Rec } from "../lib/quotes";
35
+ import { ProductPicker } from "./product-picker";
36
+
37
+ const label = (s: string) => s.replace(/_/g, " ");
38
+ const plain = (cents: number) => (cents / 100).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
39
+
40
+ function LinesEditor({ lines, onChange }: { lines: LineItem[]; onChange: (next: LineItem[]) => void }) {
41
+ const [picking, setPicking] = useState(false);
42
+ const set = (key: string, patch: Partial<LineItem>) => onChange(lines.map((l) => (l.key === key ? { ...l, ...patch } : l)));
43
+ return (
44
+ <div className="space-y-2">
45
+ <div className="flex items-center justify-between"><Label>Preset line items</Label><Button type="button" size="sm" variant="outline" onClick={() => setPicking(!picking)}>{picking ? "Hide products" : "Add a product"}</Button></div>
46
+ {picking ? <ProductPicker onPick={(l) => onChange([...lines, l])} pageSize={10} /> : null}
47
+ {lines.length === 0 ? <p className="text-xs text-muted-foreground">No preset lines. A quote built from this template starts empty.</p> : (
48
+ <Table>
49
+ <TableHeader><TableRow><TableHead>Product</TableHead><TableHead>Qty</TableHead><TableHead className="text-right">Unit</TableHead><TableHead><span className="sr-only">Remove</span></TableHead></TableRow></TableHeader>
50
+ <TableBody>
51
+ {lines.map((l) => (
52
+ <TableRow key={l.key}>
53
+ <TableCell>{l.product_name}</TableCell>
54
+ <TableCell><Input aria-label={`Quantity for ${l.product_name}`} type="number" min={1} max={10000} className="h-8 w-20" value={l.quantity} onChange={(e) => set(l.key, { quantity: Math.max(1, Math.min(10000, Math.floor(Number(e.target.value) || 1))) })} /></TableCell>
55
+ <TableCell className="text-right tabular-nums">{plain(l.unit_amount)}</TableCell>
56
+ <TableCell><Button type="button" size="sm" variant="ghost" aria-label={`Remove ${l.product_name}`} onClick={() => onChange(lines.filter((x) => x.key !== l.key))}><Trash2 className="size-4" aria-hidden /></Button></TableCell>
57
+ </TableRow>
58
+ ))}
59
+ </TableBody>
60
+ </Table>
61
+ )}
62
+ </div>
63
+ );
64
+ }
65
+
66
+ function DocumentsPicker({ selected, onChange }: { selected: string[]; onChange: (ids: string[]) => void }) {
67
+ const docs = useG8Query((signal) => g8Get("/quote-settings/documents", undefined, { signal }), []);
68
+ const items = (Array.isArray(body(docs.data).items) ? body(docs.data).items : []) as Rec[];
69
+ if (docs.error) return <ApiError error={docs.error} title="Could not load the document library" onRetry={() => void docs.refetch()} />;
70
+ const known = new Set(items.map((d) => text(d.id)));
71
+ const missing = selected.filter((id) => !known.has(id));
72
+ return (
73
+ <fieldset className="space-y-1">
74
+ <legend className="text-sm font-medium">Attached documents</legend>
75
+ {docs.loading && !docs.data ? <Skeleton className="h-8 w-full" /> : items.length === 0 ? <p className="text-xs text-muted-foreground">The document library is empty. Upload an NDA or MSA in Settings.</p> : items.map((d) => (
76
+ <label key={text(d.id)} className="flex items-center gap-2 text-sm">
77
+ <input type="checkbox" checked={selected.includes(text(d.id))} onChange={(e) => onChange(e.target.checked ? [...selected, text(d.id)] : selected.filter((x) => x !== text(d.id)))} />
78
+ {text(d.title, text(d.file_name))} <span className="text-xs text-muted-foreground">{text(d.doc_type).toUpperCase()}</span>
79
+ </label>
80
+ ))}
81
+ {missing.length > 0 ? <p className="text-xs text-muted-foreground">{missing.length} attached {missing.length === 1 ? "document is" : "documents are"} no longer in the library. Saving keeps {missing.length === 1 ? "it" : "them"} attached.</p> : null}
82
+ </fieldset>
83
+ );
84
+ }
85
+
86
+ function TemplateForm({ draft, setDraft }: { draft: TemplateDraft; setDraft: (d: TemplateDraft) => void }) {
87
+ const up = (patch: Partial<TemplateDraft>) => setDraft({ ...draft, ...patch });
88
+ return (
89
+ <div className="space-y-4">
90
+ <div className="space-y-1"><Label htmlFor="tpl-name">Name</Label><Input id="tpl-name" value={draft.name} maxLength={255} onChange={(e) => up({ name: e.target.value })} /></div>
91
+ <div className="space-y-1"><Label htmlFor="tpl-desc">Description</Label><Textarea id="tpl-desc" rows={2} value={draft.description} onChange={(e) => up({ description: e.target.value })} /></div>
92
+ <div className="grid grid-cols-2 gap-3">
93
+ <div className="space-y-1"><Label htmlFor="tpl-terms">Payment terms</Label>
94
+ <select id="tpl-terms" className="h-9 w-full rounded-md border bg-background px-2 text-sm" value={draft.default_payment_terms} onChange={(e) => up({ default_payment_terms: e.target.value })}>
95
+ <option value="">Use the workspace default</option>{PAYMENT_TERMS.map((t) => <option key={t} value={t}>{label(t)}</option>)}
96
+ </select></div>
97
+ <div className="space-y-1"><Label htmlFor="tpl-valid">Valid for (days)</Label><Input id="tpl-valid" type="number" min={1} max={365} value={draft.default_valid_days} onChange={(e) => up({ default_valid_days: e.target.value })} /></div>
98
+ <div className="space-y-1"><Label htmlFor="tpl-len">Contract length</Label><Input id="tpl-len" type="number" min={0.1} step="any" value={draft.contract_duration_value} onChange={(e) => up({ contract_duration_value: e.target.value })} /></div>
99
+ <div className="space-y-1"><Label htmlFor="tpl-unit">Length unit</Label>
100
+ <select id="tpl-unit" className="h-9 w-full rounded-md border bg-background px-2 text-sm" value={draft.contract_duration_unit} onChange={(e) => up({ contract_duration_unit: e.target.value as TemplateDraft["contract_duration_unit"] })}>
101
+ <option value="">No fixed length</option>{DURATION_UNITS.map((u) => <option key={u} value={u}>{u}</option>)}
102
+ </select></div>
103
+ </div>
104
+ <div className="space-y-1"><Label htmlFor="tpl-override">Terms text for this template</Label><Textarea id="tpl-override" rows={4} placeholder="Leave blank to use the workspace default terms" value={draft.terms_override} onChange={(e) => up({ terms_override: e.target.value })} /></div>
105
+ <label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={draft.is_default} onChange={(e) => up({ is_default: e.target.checked })} />Use as the default template for new quotes</label>
106
+ <DocumentsPicker selected={draft.document_ids} onChange={(document_ids) => up({ document_ids })} />
107
+ <LinesEditor lines={draft.lines} onChange={(lines) => up({ lines })} />
108
+ </div>
109
+ );
110
+ }
111
+
112
+ type Confirm = { kind: "delete"; row: Rec } | { kind: "lost"; fields: string[] };
113
+
114
+ export function QuoteTemplates({ className }: { className?: string }) {
115
+ const brand = useBrand();
116
+ const q = useG8Query((signal) => g8Get("/quote-templates", undefined, { signal }), []);
117
+ const rows = (Array.isArray(body(q.data).items) ? body(q.data).items : []) as Rec[];
118
+ const [editing, setEditing] = useState<{ row: Rec | null } | null>(null);
119
+ const [draft, setDraft] = useState<TemplateDraft>(emptyTemplateDraft());
120
+ const [confirm, setConfirm] = useState<Confirm | null>(null);
121
+ const [tried, setTried] = useState(false);
122
+ const problems = templateProblems(draft);
123
+
124
+ const open = (row: Rec | null) => { setDraft(row ? draftFromTemplate(row, linesFromQuote(row.default_line_items)) : emptyTemplateDraft()); setTried(false); setEditing({ row }); };
125
+ const close = () => { setEditing(null); setConfirm(null); };
126
+ const save = useG8Mutation(async () => {
127
+ const payload = templateBody(draft, toRequestLines(draft.lines));
128
+ return editing?.row ? g8Put(`/quote-templates/${encodeURIComponent(text(editing.row.id))}`, payload) : g8Post("/quote-templates", payload);
129
+ }, { onSuccess: () => { close(); void q.refetch(); } });
130
+ const remove = useG8Mutation((id: string) => g8Delete(`/quote-templates/${encodeURIComponent(id)}`), { onSuccess: () => { setConfirm(null); void q.refetch(); } });
131
+
132
+ const submit = () => {
133
+ setTried(true);
134
+ if (problems.length) return;
135
+ const lost = editing?.row ? fieldsLostOnTemplateSave(editing.row) : [];
136
+ if (lost.length) setConfirm({ kind: "lost", fields: lost });
137
+ else void save.mutate(undefined);
138
+ };
139
+
140
+ const rowName = (r: Rec) => text(r.name, "Untitled template");
141
+ return (
142
+ <SectionCard className={className} title="Quote templates" icon={<FileStack />} description="Starting points for new quotes: terms, validity, contract length, documents and preset lines."
143
+ actions={<Button size="sm" onClick={() => open(null)}><Plus className="size-4" aria-hidden />New template</Button>} flush>
144
+ {q.error ? <div className="p-4"><ApiError error={q.error} title="Could not load templates" onRetry={() => void q.refetch()} /></div> : null}
145
+ {q.loading && !q.data ? <div className="p-4"><Skeleton className="h-24 w-full" aria-label="Loading templates" /></div> : !q.error && rows.length === 0 ? (
146
+ <EmptyState compact title="No quote templates yet" description="Save the terms and lines you reuse, then start a quote from them." action={<Button onClick={() => open(null)}>New template</Button>} />
147
+ ) : rows.length > 0 ? (
148
+ <Table>
149
+ <TableHeader><TableRow><TableHead>Template</TableHead><TableHead>Payment terms</TableHead><TableHead>Valid</TableHead><TableHead className="text-right">Lines</TableHead><TableHead><span className="sr-only">Actions</span></TableHead></TableRow></TableHeader>
150
+ <TableBody>
151
+ {rows.map((r) => (
152
+ <TableRow key={text(r.id)}>
153
+ <TableCell><span className="font-medium">{rowName(r)}</span>{r.is_default === true ? <StatusPill tone="info" className="ml-2">Default</StatusPill> : null}{r.description ? <div className="text-xs text-muted-foreground">{text(r.description)}</div> : null}</TableCell>
154
+ <TableCell>{label(text(r.default_payment_terms, "workspace default"))}</TableCell>
155
+ <TableCell>{r.default_valid_days ? `${text(r.default_valid_days)} days` : "-"}</TableCell>
156
+ <TableCell className="text-right tabular-nums">{Array.isArray(r.default_line_items) ? r.default_line_items.length : 0}</TableCell>
157
+ <TableCell className="whitespace-nowrap text-right">
158
+ <Button size="sm" variant="ghost" aria-label={`Edit ${rowName(r)}`} onClick={() => open(r)}><Pencil className="size-4" aria-hidden /></Button>
159
+ <Button size="sm" variant="ghost" aria-label={`Delete ${rowName(r)}`} onClick={() => setConfirm({ kind: "delete", row: r })}><Trash2 className="size-4" aria-hidden /></Button>
160
+ </TableCell>
161
+ </TableRow>
162
+ ))}
163
+ </TableBody>
164
+ </Table>
165
+ ) : null}
166
+ {remove.error ? <div className="p-4"><ApiError error={remove.error} title="Could not delete the template" /></div> : null}
167
+
168
+ <FormSheet open={editing != null} onOpenChange={(o) => { if (!o) close(); }} width="lg" title={editing?.row ? "Edit template" : "New template"}
169
+ description={editing?.row ? "Saving replaces the whole template with what is in this form." : "Needs an organization admin."}
170
+ onSubmit={submit}
171
+ footer={<><Button type="button" variant="outline" onClick={close}>Cancel</Button><Button type="submit" loading={save.loading}>{editing?.row ? "Save template" : "Create template"}</Button></>}>
172
+ {editing ? <TemplateForm draft={draft} setDraft={setDraft} /> : null}
173
+ {tried && problems.length ? <ul role="alert" className="list-disc pl-5 text-sm text-destructive">{problems.map((p) => <li key={p}>{p}</li>)}</ul> : null}
174
+ {save.error ? <ApiError error={save.error} title="Could not save the template" /> : null}
175
+ </FormSheet>
176
+
177
+ {confirm?.kind === "delete" ? (
178
+ <ConfirmSpendDialog open tier="destructive" title={`Delete "${rowName(confirm.row)}"?`} confirmLabel="Delete template" affected={1} confirming={remove.loading}
179
+ description="The template is removed and can no longer start a quote. Quotes already built from it are not changed."
180
+ onConfirm={() => void remove.mutate(text(confirm.row.id))} onCancel={() => setConfirm(null)} />
181
+ ) : null}
182
+ {confirm?.kind === "lost" ? (
183
+ <ConfirmSpendDialog open tier="destructive" title="Saving here resets some template settings" confirmLabel="Save and reset" confirming={save.loading}
184
+ description={<p>This template has {confirm.fields.join(", ")}. The API cannot carry {confirm.fields.length === 1 ? "that setting" : "those settings"}, so saving from here resets {confirm.fields.length === 1 ? "it" : "them"}. Edit the template in {brand.linkLabel} to keep {confirm.fields.length === 1 ? "it" : "them"}.</p>}
185
+ onConfirm={() => void save.mutate(undefined)} onCancel={() => setConfirm(null)} />
186
+ ) : null}
187
+ </SectionCard>
188
+ );
189
+ }
@@ -10,6 +10,10 @@
10
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
11
  * 4. duplicateQuote POST /quotes/{id}/duplicate (external tier, but only copies into a new draft)
12
12
  * 5. deleteQuote DELETE /quotes/{id} (destructive; drafts only)
13
+ * 6. previewQuoteSend POST /quotes/{id}/send-preview (read; renders the exact email and sends nothing: "Preview email" in the send confirm)
14
+ *
15
+ * The lifecycle row (QuoteLifecycleActions): download the PDF, archive or unarchive, and for a sent quote expire, revoke or
16
+ * mark accepted. Those operations live in that component and each risky one is confirmed there.
13
17
  *
14
18
  * Send, edit-as-draft and delete go through ConfirmSpendDialog. Duplicate creates an unsent draft and emails
15
19
  * nobody, so it runs on one click (same reasoning as saving a draft in QuoteBuilder).
@@ -24,6 +28,7 @@ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
24
28
  import { ApiError } from "@/g8/components/api-error";
25
29
  import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
26
30
  import { asRec, body, lineTotal, linesFromQuote, money, statusLabel, text } from "../lib/quotes";
31
+ import { QuoteLifecycleActions } from "./quote-lifecycle-actions";
27
32
  import { SignatureStatus } from "./signature-status";
28
33
 
29
34
  export interface QuoteViewerProps {
@@ -58,6 +63,8 @@ export function QuoteViewer({ quoteId, onEdit, onOpenQuote, onDeleted, className
58
63
  void q.refetch();
59
64
  },
60
65
  });
66
+ // What the customer would get, rendered by the API from the same subject and message the confirm holds. Sends nothing.
67
+ const preview = useG8Mutation(async () => body(await g8Post(`${base}/send-preview`, { ...(subject ? { subject } : {}), ...(message ? { message } : {}) })));
61
68
  const copy = useG8Mutation(async () => body(await g8Post(`${base}/duplicate`)), { onSuccess: (res) => { if (text(res.id)) onOpenQuote?.(text(res.id)); } });
62
69
 
63
70
  if (q.loading && !q.data) return <div className={className} aria-busy="true"><Skeleton className="h-48 w-full" /></div>;
@@ -75,16 +82,17 @@ export function QuoteViewer({ quoteId, onEdit, onOpenQuote, onDeleted, className
75
82
  return (
76
83
  <div className={className}>
77
84
  <div className="mb-4 flex flex-wrap items-center gap-2">
78
- <h2 className="text-lg font-semibold">{text(d.title, "Untitled quote")}</h2>
85
+ <h2 className="text-lg font-medium">{text(d.title, "Untitled quote")}</h2>
79
86
  <span className="text-muted-foreground text-sm" data-testid="quote-status">{statusLabel(status)} · {text(d.quote_number)}</span>
80
87
  <div className="ml-auto flex flex-wrap gap-2">
81
88
  {draft ? <Button variant="outline" onClick={() => onEdit?.(quoteId)}>Edit</Button> : null}
82
89
  {status === "sent" ? <Button variant="outline" onClick={() => setPending("edit")}>Edit as draft</Button> : null}
83
90
  <Button variant="outline" loading={copy.loading} onClick={() => void copy.mutate(undefined)}>Duplicate</Button>
84
91
  {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}
92
+ {draft || sent ? <Button disabled={!hasSigner} title={hasSigner ? undefined : "Add a signer in the builder first"} onClick={() => { preview.reset(); setPending("send"); }}>{sent ? "Resend" : "Send for signature"}</Button> : null}
86
93
  </div>
87
94
  </div>
95
+ <QuoteLifecycleActions className="mb-4" quoteId={quoteId} status={status} archived={Boolean(d.archived_at || d.is_archived || d.archived === true)} title={text(d.title)} quoteNumber={text(d.quote_number)} onChanged={() => void q.refetch()} />
88
96
  {run.error ? <ApiError error={run.error} title="That action failed" /> : null}
89
97
  {copy.error ? <ApiError error={copy.error} title="Could not duplicate" /> : null}
90
98
  {draft && !hasSigner ? <p className="text-muted-foreground mb-3 text-sm">Add a signer before sending.</p> : null}
@@ -104,7 +112,7 @@ export function QuoteViewer({ quoteId, onEdit, onOpenQuote, onDeleted, className
104
112
  <div>Subtotal {money(d.subtotal, currency)}</div>
105
113
  {Number(d.discount_amount) ? <div>Discount -{money(d.discount_amount, currency)}</div> : null}
106
114
  <div>Tax {money(d.tax_amount, currency)}</div>
107
- <div className="font-semibold">Total {money(d.total, currency)}</div>
115
+ <div className="font-medium">Total {money(d.total, currency)}</div>
108
116
  </div>
109
117
  </CardContent>
110
118
  </Card>
@@ -118,8 +126,18 @@ export function QuoteViewer({ quoteId, onEdit, onOpenQuote, onDeleted, className
118
126
  {pending === "send" ? (
119
127
  <ConfirmSpendDialog open tier="external" title={`${sent ? "Resend" : "Send"} "${name}" for signature`} confirmLabel={sent ? "Resend" : "Send"} recipients={1} confirming={run.loading}
120
128
  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>}
129
+ <Input aria-label="Email subject" placeholder="Subject (optional)" value={subject} onChange={(e) => { setSubject(e.target.value); preview.reset(); }} />
130
+ <Textarea aria-label="Email message" rows={3} placeholder="Message (optional)" value={message} onChange={(e) => { setMessage(e.target.value); preview.reset(); }} />
131
+ <Button type="button" variant="outline" size="sm" loading={preview.loading} onClick={() => void preview.mutate(undefined)}>Preview email</Button>
132
+ {preview.error ? <ApiError error={preview.error} title="Could not render the preview" /> : null}
133
+ {preview.data ? (
134
+ <div className="space-y-1 rounded-md border border-border p-2" data-testid="send-preview">
135
+ <p className="text-xs text-muted-foreground">From {text(preview.data.sender_name)}{preview.data.reply_to ? ` (replies to ${text(preview.data.reply_to)})` : ""} to {text(preview.data.recipient_name)} {text(preview.data.recipient_email)}</p>
136
+ <p className="text-sm font-medium">{text(preview.data.email_subject, text(preview.data.subject))}</p>
137
+ {preview.data.email_html ? <iframe title="Email preview" sandbox="" srcDoc={text(preview.data.email_html)} className="h-48 w-full rounded border-0 bg-white" /> : null}
138
+ {Array.isArray(preview.data.attached_documents) && preview.data.attached_documents.length ? <p className="text-xs text-muted-foreground">Attached: {preview.data.attached_documents.map((d) => text(asRec(d).title, text(asRec(d).file_name))).join(", ")}</p> : null}
139
+ </div>
140
+ ) : null}</div>}
123
141
  onConfirm={() => void run.mutate("send")} onCancel={() => setPending(null)} />
124
142
  ) : null}
125
143
  {pending === "edit" ? (
@@ -4,7 +4,8 @@
4
4
  * QuotesScreen: the finished quotes product. Every quote with its status, a viewer for the open one, a builder for
5
5
  * new and draft quotes, and e-signature tracking. No props are required.
6
6
  *
7
- * It composes: ProductCatalog (Products tab), QuoteDealTab, QuoteList (status filter, paging), QuoteViewer (totals, send, resend, edit as draft, duplicate,
7
+ * It composes: ProductCatalog and ManualProducts (Products tab), QuoteTemplates (Templates tab), QuoteSettings with the
8
+ * document library (Settings tab), QuoteEvidence (Evidence tab of an open quote), QuoteBatchActions (inside QuoteList), QuoteDealTab, QuoteList (status filter, paging), QuoteViewer (totals, send, resend, edit as draft, duplicate,
8
9
  * delete, e-signature status and reminder), QuoteBuilder (details, signer, line items from ProductPicker) and a deal
9
10
  * picker, because graph8 attaches every quote to a deal.
10
11
  *
@@ -16,6 +17,10 @@
16
17
  * - New quote: choose the deal (search by name; skipped when `dealId` is passed), then the builder. Saving a draft
17
18
  * emails nobody, so it needs no confirmation.
18
19
  * - Edit a draft from the viewer; duplicate; delete a draft (confirmed).
20
+ * - Tick quotes in the list to archive, expire or revoke them together (confirmed, id-addressed, result read per quote).
21
+ * - Templates tab: create, edit and delete quote templates. Settings tab: org-wide quote settings and the legal document library.
22
+ * - Products tab: the quotable catalogue, plus the manual products you add, edit and retire.
23
+ * - Evidence tab of an open quote: attach and download internal PDFs.
19
24
  * - Send for signature, resend, send a signature reminder, and edit a sent quote as a new draft: all external
20
25
  * actions, each behind ConfirmSpendDialog because they reach a real person.
21
26
  *
@@ -40,9 +45,13 @@ import { useG8Query } from "@/g8/hooks/use-g8-query";
40
45
  import { formatMoney, type Deal } from "@/g8/deals/lib/deals-types";
41
46
  import { useDebounced } from "../hooks/use-debounced";
42
47
  import { body } from "../lib/quotes";
48
+ import { ManualProducts } from "./manual-products";
43
49
  import { QuoteBuilder } from "./quote-builder";
50
+ import { QuoteEvidence } from "./quote-evidence";
44
51
  import { QuoteList } from "./quote-list";
45
52
  import { ProductCatalog, QuoteDealTab } from "./quote-related";
53
+ import { QuoteSettings } from "./quote-settings";
54
+ import { QuoteTemplates } from "./quote-templates";
46
55
  import { QuoteViewer } from "./quote-viewer";
47
56
 
48
57
  export interface QuotesScreenProps {
@@ -109,7 +118,7 @@ export function QuotesScreen({ dealId, companyId, renderDetailActions, extraDeta
109
118
  const [selectedId, setSelectedId] = useUrlState("id");
110
119
  const [mode, setMode] = useUrlState("view");
111
120
  const [tabParam, setTab] = useUrlState("tab", "quotes");
112
- const tab = tabParam === "products" ? "products" : "quotes";
121
+ const tab = tabParam === "products" || tabParam === "templates" || tabParam === "settings" ? tabParam : "quotes";
113
122
  const [version, setVersion] = useState(0);
114
123
  const [creating, setCreating] = useState(false);
115
124
  const [pickedDeal, setPickedDeal] = useState<Deal | null>(null);
@@ -132,6 +141,7 @@ export function QuotesScreen({ dealId, companyId, renderDetailActions, extraDeta
132
141
  onCollapse={mode === "full" ? () => setMode(null) : undefined}
133
142
  tabs={[
134
143
  { id: "quote", label: "Quote", content: <QuoteViewer key={selectedId} quoteId={selectedId} onEdit={setEditingId} onOpenQuote={(id) => { refresh(); setSelectedId(id); }} onDeleted={() => { close(); refresh(); }} /> },
144
+ { id: "evidence", label: "Evidence", content: <QuoteEvidence key={selectedId} quoteId={selectedId} /> },
135
145
  { id: "deal", label: "Deal", content: <QuoteDealTab key={selectedId} quoteId={selectedId} onOpenDeal={onOpenDeal} /> },
136
146
  ...(extraDetailTabs ?? []).map((t) => ({ id: t.id, label: t.label, content: t.render(selectedId) })),
137
147
  ]}
@@ -145,18 +155,18 @@ export function QuotesScreen({ dealId, companyId, renderDetailActions, extraDeta
145
155
  icon={<FileText className="size-5" />}
146
156
  actions={<>{toolbarActions}<Button onClick={openNew}><Plus className="size-4" aria-hidden />New quote</Button></>}
147
157
  stats={stats}
148
- tabs={[{ id: "quotes", label: "Quotes" }, { id: "products", label: "Products" }]}
158
+ tabs={[{ id: "quotes", label: "Quotes" }, { id: "products", label: "Products" }, { id: "templates", label: "Templates" }, { id: "settings", label: "Settings" }]}
149
159
  activeTab={tab}
150
160
  onTabChange={(id) => { setTab(id); close(); }}
151
161
  >
152
162
  {totals.error ? <ApiError error={totals.error} title="Could not load quote numbers" onRetry={() => void totals.refetch()} /> : null}
153
- {tab === "products" ? <ProductCatalog onStartQuote={openNew} /> : <SplitView
163
+ {tab === "products" ? <div className="space-y-4"><ProductCatalog onStartQuote={openNew} /><ManualProducts /></div> : tab === "templates" ? <QuoteTemplates /> : tab === "settings" ? <QuoteSettings /> : <SplitView
154
164
  detailOpen={Boolean(selectedId)}
155
165
  onCloseDetail={close}
156
166
  expanded={mode === "full" && Boolean(selectedId)}
157
167
  onExpandedChange={(full) => setMode(full ? "full" : null)}
158
168
  detail={detail}
159
- list={<QuoteList dealId={dealId} companyId={companyId} refreshKey={version} selectedId={selectedId} onOpen={setSelectedId} onNew={openNew} />}
169
+ list={<QuoteList dealId={dealId} companyId={companyId} refreshKey={version} selectedId={selectedId} onOpen={setSelectedId} onNew={openNew} selectable onChanged={refresh} />}
160
170
  />}
161
171
  <FormSheet open={creating} onOpenChange={setCreating} title="New quote" description={pickedDeal ? `For ${pickedDeal.name ?? "this deal"}` : undefined}>
162
172
  {creating ? (builderDeal ? <QuoteBuilder key={builderDeal} dealId={builderDeal} companyId={companyId} onSaved={saved} /> : <DealStep onPick={setPickedDeal} />) : null}