@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,94 @@
1
+ "use client";
2
+
3
+ /**
4
+ * QuoteDocuments: the org's reusable legal documents (NDA, MSA, other PDFs) that a quote template can attach.
5
+ *
6
+ * SDK operations, in order:
7
+ * 1. listQuoteDocuments GET /quote-settings/documents read
8
+ * 2. uploadQuoteDocument POST /quote-settings/documents external tier, stores a PDF (org admin); multipart: file, title, doc_type
9
+ * 3. deleteQuoteDocument DELETE /quote-settings/documents/{id} DESTRUCTIVE (org admin) -> ConfirmSpendDialog
10
+ *
11
+ * Uploading sends nobody anything, so it runs on one click. The file is checked here first: PDF only, and under the app
12
+ * proxy's request cap (about 5 MB; the API itself allows 10 MB). The API silently files an unknown type as `other`, so the
13
+ * form only offers nda, msa and other. Delete removes the library entry: it can no longer be attached to a template or a quote.
14
+ */
15
+ import { useRef, useState } from "react";
16
+ import { FileText, Trash2, Upload } from "lucide-react";
17
+ import { Button, Input, Label, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
18
+ import { ApiError } from "@/g8/components/api-error";
19
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
20
+ import { EmptyState } from "@/g8/components/empty-state";
21
+ import { SectionCard } from "@/g8/components/screen/section-card";
22
+ import { StatusPill } from "@/g8/components/screen/status-pill";
23
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
24
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
25
+ import { g8Delete, g8Get, g8Post } from "@/g8/lib/g8-client";
26
+ import { UPLOAD_LIMIT_LABEL, fileSize, pdfProblem } from "../lib/quote-admin";
27
+ import { body, text, type Rec } from "../lib/quotes";
28
+
29
+ const DOC_TYPES: Array<[string, string]> = [["nda", "NDA"], ["msa", "MSA"], ["other", "Other"]];
30
+
31
+ export function QuoteDocuments({ className }: { className?: string }) {
32
+ const q = useG8Query((signal) => g8Get("/quote-settings/documents", undefined, { signal }), []);
33
+ const rows = (Array.isArray(body(q.data).items) ? body(q.data).items : []) as Rec[];
34
+ const [title, setTitle] = useState("");
35
+ const [docType, setDocType] = useState("other");
36
+ const [file, setFile] = useState<File | null>(null);
37
+ const [problem, setProblem] = useState<string | null>(null);
38
+ const [removing, setRemoving] = useState<Rec | null>(null);
39
+ const input = useRef<HTMLInputElement>(null);
40
+
41
+ const upload = useG8Mutation(async () => {
42
+ const form = new FormData();
43
+ form.append("file", file as File);
44
+ form.append("title", title.trim());
45
+ form.append("doc_type", docType);
46
+ return g8Post("/quote-settings/documents", form);
47
+ }, { onSuccess: () => { setTitle(""); setFile(null); setDocType("other"); if (input.current) input.current.value = ""; void q.refetch(); } });
48
+ const remove = useG8Mutation((id: string) => g8Delete(`/quote-settings/documents/${encodeURIComponent(id)}`), { onSuccess: () => { setRemoving(null); void q.refetch(); } });
49
+
50
+ const submit = () => {
51
+ const bad = pdfProblem(file) ?? (title.trim() ? null : "Give the document a title.");
52
+ setProblem(bad);
53
+ if (!bad) void upload.mutate(undefined);
54
+ };
55
+
56
+ return (
57
+ <SectionCard className={className} title="Document library" icon={<FileText />} description={`PDFs a template can attach to its quotes, up to ${UPLOAD_LIMIT_LABEL} each. Needs an organization admin.`} flush>
58
+ {q.error ? <div className="p-4"><ApiError error={q.error} title="Could not load documents" onRetry={() => void q.refetch()} /></div> : null}
59
+ {q.loading && !q.data ? <div className="p-4"><Skeleton className="h-16 w-full" aria-label="Loading documents" /></div> : !q.error && rows.length === 0 ? (
60
+ <EmptyState compact bordered title="No documents yet" description="Upload an NDA or MSA to attach it to quote templates." />
61
+ ) : rows.length > 0 ? (
62
+ <Table>
63
+ <TableHeader><TableRow><TableHead>Title</TableHead><TableHead>Type</TableHead><TableHead>File</TableHead><TableHead><span className="sr-only">Actions</span></TableHead></TableRow></TableHeader>
64
+ <TableBody>
65
+ {rows.map((d) => (
66
+ <TableRow key={text(d.id)}>
67
+ <TableCell className="font-medium">{text(d.title, "Untitled")}</TableCell>
68
+ <TableCell><StatusPill tone="neutral">{text(d.doc_type, "other").toUpperCase()}</StatusPill></TableCell>
69
+ <TableCell className="text-muted-foreground">{text(d.file_name)} {fileSize(d.file_size)}</TableCell>
70
+ <TableCell className="text-right"><Button size="sm" variant="ghost" aria-label={`Delete ${text(d.title, "document")}`} onClick={() => setRemoving(d)}><Trash2 className="size-4" aria-hidden /></Button></TableCell>
71
+ </TableRow>
72
+ ))}
73
+ </TableBody>
74
+ </Table>
75
+ ) : null}
76
+ <form className="grid gap-3 border-t border-border p-4 sm:grid-cols-[1fr_8rem]" onSubmit={(e) => { e.preventDefault(); submit(); }}>
77
+ <div className="space-y-1"><Label htmlFor="doc-title">Document title</Label><Input id="doc-title" value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Mutual NDA" /></div>
78
+ <div className="space-y-1"><Label htmlFor="doc-type">Type</Label>
79
+ <select id="doc-type" className="h-9 w-full rounded-md border bg-background px-2 text-sm" value={docType} onChange={(e) => setDocType(e.target.value)}>{DOC_TYPES.map(([v, l]) => <option key={v} value={v}>{l}</option>)}</select></div>
80
+ <div className="space-y-1 sm:col-span-2"><Label htmlFor="doc-file">PDF file</Label>
81
+ <input id="doc-file" ref={input} type="file" accept="application/pdf,.pdf" className="block w-full text-sm" onChange={(e) => { setFile(e.target.files?.[0] ?? null); setProblem(null); }} /></div>
82
+ <div className="sm:col-span-2"><Button type="submit" loading={upload.loading}><Upload className="size-4" aria-hidden />Upload document</Button></div>
83
+ {problem ? <p role="alert" className="text-sm text-destructive sm:col-span-2">{problem}</p> : null}
84
+ {upload.error ? <div className="sm:col-span-2"><ApiError error={upload.error} title="Could not upload the document" /></div> : null}
85
+ </form>
86
+ {remove.error ? <div className="p-4"><ApiError error={remove.error} title="Could not delete the document" /></div> : null}
87
+ {removing ? (
88
+ <ConfirmSpendDialog open tier="destructive" title={`Delete "${text(removing.title, "this document")}"?`} confirmLabel="Delete document" affected={1} confirming={remove.loading}
89
+ description="The document leaves the library, so it can no longer be attached to a template or a quote. Check your templates after: one that listed it keeps pointing at a document that is gone."
90
+ onConfirm={() => void remove.mutate(text(removing.id))} onCancel={() => setRemoving(null)} />
91
+ ) : null}
92
+ </SectionCard>
93
+ );
94
+ }
@@ -0,0 +1,95 @@
1
+ "use client";
2
+
3
+ /**
4
+ * QuoteEvidence: the PDFs attached to a quote as internal evidence (a countersigned copy received outside the product,
5
+ * a purchase order). They are for your team only: the customer never sees them and nothing is sent.
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. listQuoteEvidence GET /quotes/{id}/evidence read - `{ items: [{ id, file_name, created_at, uploaded_by }] }`
9
+ * 2. uploadQuoteEvidence POST /quotes/{id}/evidence write - multipart `file`, PDF only; answers `{ id }`
10
+ * 3. downloadQuoteEvidence GET /quotes/{id}/evidence/{evidence_id} read - answers `{ url }`, a link that expires after 60 seconds
11
+ *
12
+ * The API has no delete for evidence, so an attached PDF stays: the form says so before the upload. The file is checked
13
+ * here first (PDF header and end marker, which the API also demands, and the app proxy's request cap) so a bad file is
14
+ * refused with a reason instead of a 400 or 413.
15
+ */
16
+ import { useRef, useState } from "react";
17
+ import { Download, Paperclip, Upload } from "lucide-react";
18
+ import { Button, Skeleton } from "@graph8/sdk/ui";
19
+ import { ApiError } from "@/g8/components/api-error";
20
+ import { EmptyState } from "@/g8/components/empty-state";
21
+ import { SectionCard } from "@/g8/components/screen/section-card";
22
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
23
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
24
+ import { g8Get, g8Post } from "@/g8/lib/g8-client";
25
+ import { UPLOAD_LIMIT_LABEL, pdfProblem } from "../lib/quote-admin";
26
+ import { body, text, type Rec } from "../lib/quotes";
27
+
28
+ /** The API rejects a file that does not start with %PDF- or has no %%EOF in its last KB. Check both before uploading. */
29
+ export async function pdfStructureProblem(file: File): Promise<string | null> {
30
+ const bytes = (blob: Blob) => new Promise<Uint8Array>((resolve, reject) => {
31
+ const reader = new FileReader();
32
+ reader.onload = () => resolve(new Uint8Array(reader.result as ArrayBuffer));
33
+ reader.onerror = () => reject(reader.error);
34
+ reader.readAsArrayBuffer(blob);
35
+ });
36
+ const head = new TextDecoder().decode(await bytes(file.slice(0, 5)));
37
+ const tail = new TextDecoder("latin1").decode(await bytes(file.slice(Math.max(0, file.size - 1024))));
38
+ if (!head.startsWith("%PDF-")) return "That file is not a PDF.";
39
+ if (!tail.includes("%%EOF")) return "That PDF looks incomplete (no end marker). Export it again and retry.";
40
+ return null;
41
+ }
42
+
43
+ export function QuoteEvidence({ quoteId, className }: { quoteId: string; className?: string }) {
44
+ const base = `/quotes/${encodeURIComponent(quoteId)}/evidence`;
45
+ const q = useG8Query((signal) => g8Get(base, undefined, { signal }), [quoteId]);
46
+ const rows = (Array.isArray(body(q.data).items) ? body(q.data).items : []) as Rec[];
47
+ const [file, setFile] = useState<File | null>(null);
48
+ const [problem, setProblem] = useState<string | null>(null);
49
+ const input = useRef<HTMLInputElement>(null);
50
+
51
+ const upload = useG8Mutation(async () => {
52
+ const form = new FormData();
53
+ form.append("file", file as File);
54
+ return g8Post(base, form);
55
+ }, { onSuccess: () => { setFile(null); if (input.current) input.current.value = ""; void q.refetch(); } });
56
+ const download = useG8Mutation(async (id: string) => {
57
+ const res = body(await g8Get(`${base}/${encodeURIComponent(id)}`));
58
+ const url = text(res.url);
59
+ if (!/^https?:\/\//.test(url)) throw new Error("The API returned no download link for this file.");
60
+ window.open(url, "_blank", "noopener,noreferrer");
61
+ });
62
+
63
+ const submit = async () => {
64
+ const bad = pdfProblem(file) ?? (file ? await pdfStructureProblem(file) : null);
65
+ setProblem(bad);
66
+ if (!bad) void upload.mutate(undefined);
67
+ };
68
+
69
+ return (
70
+ <SectionCard className={className} title="Evidence" icon={<Paperclip />} description="Internal PDFs for this quote. The customer does not see them." flush>
71
+ {q.error ? <div className="p-4"><ApiError error={q.error} title="Could not load evidence" onRetry={() => void q.refetch()} /></div> : null}
72
+ {q.loading && !q.data ? <div className="p-4"><Skeleton className="h-12 w-full" aria-label="Loading evidence" /></div> : !q.error && rows.length === 0 ? (
73
+ <EmptyState compact title="No evidence attached" description="Attach a countersigned copy or a purchase order." />
74
+ ) : (
75
+ <ul className="divide-y divide-border">
76
+ {rows.map((r) => (
77
+ <li key={text(r.id)} className="flex items-center justify-between gap-2 px-4 py-2 text-sm">
78
+ <span><span className="font-medium">{text(r.file_name, "evidence.pdf")}</span><span className="block text-xs text-muted-foreground">{r.created_at ? new Date(text(r.created_at)).toLocaleString() : ""}{r.uploaded_by ? ` · ${text(r.uploaded_by)}` : ""}</span></span>
79
+ <Button size="sm" variant="outline" loading={download.loading} aria-label={`Download ${text(r.file_name, "evidence")}`} onClick={() => void download.mutate(text(r.id))}><Download className="size-4" aria-hidden />Download</Button>
80
+ </li>
81
+ ))}
82
+ </ul>
83
+ )}
84
+ {download.error ? <div className="px-4 pb-3"><ApiError error={download.error} title="Could not get the file" /></div> : null}
85
+ <form className="space-y-2 border-t border-border p-4" onSubmit={(e) => { e.preventDefault(); void submit(); }}>
86
+ <label className="block text-sm font-medium" htmlFor={`ev-file-${quoteId}`}>Attach a PDF</label>
87
+ <input id={`ev-file-${quoteId}`} ref={input} type="file" accept="application/pdf,.pdf" className="block w-full text-sm" onChange={(e) => { setFile(e.target.files?.[0] ?? null); setProblem(null); }} />
88
+ <p className="text-xs text-muted-foreground">PDF only, up to {UPLOAD_LIMIT_LABEL}. An attached file cannot be removed afterwards.</p>
89
+ <Button type="submit" loading={upload.loading}><Upload className="size-4" aria-hidden />Attach PDF</Button>
90
+ {problem ? <p role="alert" className="text-sm text-destructive">{problem}</p> : null}
91
+ {upload.error ? <ApiError error={upload.error} title="Could not attach the PDF" /> : null}
92
+ </form>
93
+ </SectionCard>
94
+ );
95
+ }
@@ -0,0 +1,146 @@
1
+ "use client";
2
+
3
+ /**
4
+ * QuoteLifecycleActions: what you can do with a quote after it exists: download the PDF, archive or restore it, and
5
+ * for a quote that is out for signature, expire it, revoke it or mark it accepted. QuoteViewer renders it under the
6
+ * header; it can also be mounted on its own for a quote you already loaded.
7
+ *
8
+ * SDK operations, in order:
9
+ * 1. GET /quotes/{id}/pdf (quotes.getQuotePdf) read - the document as the customer sees it
10
+ * 2. POST /quotes/{id}/archive (quotes.archiveQuote) DESTRUCTIVE - off the active list, reversible -> ConfirmSpendDialog
11
+ * POST /quotes/{id}/unarchive (quotes.unarchiveQuote) external -> ConfirmSpendDialog
12
+ * 3. POST /quotes/{id}/expire (quotes.expireQuote) DESTRUCTIVE - terminal, the customer's link stops working -> ConfirmSpendDialog
13
+ * 4. POST /quotes/{id}/revoke (quotes.revokeQuote) DESTRUCTIVE - terminal, withdraws a sent quote -> ConfirmSpendDialog
14
+ * 5. POST /quotes/{id}/mark-accepted { evidence_type, evidence_note } (quotes.markQuoteAccepted) external - RAISES AN INVOICE -> ConfirmSpendDialog
15
+ * Status rules (the API enforces them too): expire, revoke and mark accepted apply to sent or viewed quotes; archive
16
+ * to any quote that is not archived. Marking accepted runs the same provisioning as a signed envelope, so use it only
17
+ * when the customer paid or signed outside the product; the evidence is stored on the audit trail.
18
+ * The PDF endpoint answers JSON: a link (`url`, `pdf_url`, `download_url`) opens in a new tab, base64 content downloads.
19
+ */
20
+ import { useState } from "react";
21
+ import { Download } from "lucide-react";
22
+ import { Button, Textarea } from "@graph8/sdk/ui";
23
+ import { ApiError } from "@/g8/components/api-error";
24
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
25
+ import { g8Get, g8Post } from "@/g8/lib/g8-client";
26
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
27
+ import { asRec, text } from "../lib/quotes";
28
+
29
+ export interface QuoteLifecycleActionsProps {
30
+ quoteId: string;
31
+ /** The quote's status: draft, sent, viewed, accepted, declined, voided, expired or archived. */
32
+ status: string;
33
+ /** True when the quote is archived (a status of "archived", or an archived flag on the record). */
34
+ archived?: boolean;
35
+ /** Shown in the dialogs and used for the PDF file name. */
36
+ title?: string;
37
+ quoteNumber?: string;
38
+ /** Called after any lifecycle change, so the caller reloads the quote. */
39
+ onChanged?: () => void;
40
+ className?: string;
41
+ }
42
+
43
+ type Pending = "archive" | "unarchive" | "expire" | "revoke" | "accept";
44
+
45
+ export const EVIDENCE_TYPES: Array<[string, string]> = [
46
+ ["offline_signature", "Signed offline"],
47
+ ["customer_email", "Customer confirmed by email"],
48
+ ["verbal_authorization", "Verbal authorization"],
49
+ ["payment_received", "Payment received"],
50
+ ];
51
+
52
+ function base64Bytes(b64: string): Uint8Array {
53
+ const bin = atob(b64.replace(/^data:application\/pdf;base64,/, "").replace(/\s/g, ""));
54
+ return Uint8Array.from(bin, (c) => c.charCodeAt(0));
55
+ }
56
+
57
+ /** Open or save the PDF an `/pdf` response describes. Throws when it carries neither a link nor content. */
58
+ export function deliverPdf(res: unknown, filename: string): void {
59
+ const raw = asRec(res);
60
+ const data = "data" in raw ? raw.data : raw;
61
+ const rec = asRec(data);
62
+ const link = typeof data === "string" && /^https?:/.test(data) ? data : text(rec.pdf_url ?? rec.download_url ?? rec.signed_url ?? rec.url ?? rec.link);
63
+ if (link) {
64
+ window.open(link, "_blank", "noopener,noreferrer");
65
+ return;
66
+ }
67
+ const b64 = typeof data === "string" ? data : text(rec.pdf_base64 ?? rec.content_base64 ?? rec.base64 ?? rec.pdf ?? rec.content);
68
+ if (!b64) throw new Error("The API returned no PDF for this quote (no link and no file content).");
69
+ const url = URL.createObjectURL(new Blob([base64Bytes(b64) as BlobPart], { type: "application/pdf" }));
70
+ const a = document.createElement("a");
71
+ a.href = url;
72
+ a.download = filename;
73
+ a.click();
74
+ setTimeout(() => URL.revokeObjectURL(url), 10_000);
75
+ }
76
+
77
+ export function QuoteLifecycleActions({ quoteId, status, archived = false, title, quoteNumber, onChanged, className }: QuoteLifecycleActionsProps) {
78
+ const base = `/quotes/${encodeURIComponent(quoteId)}`;
79
+ const [pending, setPending] = useState<Pending | null>(null);
80
+ const [evidenceType, setEvidenceType] = useState(EVIDENCE_TYPES[0][0]);
81
+ const [evidenceNote, setEvidenceNote] = useState("");
82
+ const [needNote, setNeedNote] = useState(false);
83
+ const name = title || "this quote";
84
+ const out = status === "sent" || status === "viewed";
85
+ const isArchived = archived || status === "archived";
86
+
87
+ const pdf = useG8Mutation(async () => { deliverPdf(await g8Get(`${base}/pdf`), `${quoteNumber || "quote"}.pdf`); });
88
+ const run = useG8Mutation(async (kind: Pending) => {
89
+ if (kind === "accept") return g8Post(`${base}/mark-accepted`, { evidence_type: evidenceType, evidence_note: evidenceNote.trim() });
90
+ return g8Post(`${base}/${kind}`);
91
+ }, {
92
+ invalidates: [["quotes"]],
93
+ onSuccess: () => { setPending(null); setEvidenceNote(""); setNeedNote(false); onChanged?.(); },
94
+ });
95
+ const cancel = () => { setPending(null); setNeedNote(false); };
96
+ const confirm = (kind: Pending) => {
97
+ if (kind === "accept" && !evidenceNote.trim()) { setNeedNote(true); return; }
98
+ void run.mutate(kind);
99
+ };
100
+
101
+ return (
102
+ <div className={className}>
103
+ <div className="flex flex-wrap items-center gap-2" role="group" aria-label="Quote lifecycle">
104
+ {isArchived ? <span className="text-xs text-muted-foreground">Archived</span> : null}
105
+ <Button size="sm" variant="outline" loading={pdf.loading} onClick={() => void pdf.mutate(undefined)}><Download className="size-4" aria-hidden />Download PDF</Button>
106
+ {out ? <Button size="sm" variant="outline" onClick={() => setPending("accept")}>Mark accepted</Button> : null}
107
+ {out ? <Button size="sm" variant="ghost" onClick={() => setPending("revoke")}>Revoke</Button> : null}
108
+ {out ? <Button size="sm" variant="ghost" onClick={() => setPending("expire")}>Expire</Button> : null}
109
+ {isArchived ? <Button size="sm" variant="ghost" onClick={() => setPending("unarchive")}>Unarchive</Button> : <Button size="sm" variant="ghost" onClick={() => setPending("archive")}>Archive</Button>}
110
+ </div>
111
+ {pdf.error ? <ApiError error={pdf.error} title="Could not get the PDF" /> : null}
112
+ {run.error ? <ApiError error={run.error} title="That quote action failed" /> : null}
113
+
114
+ {pending === "archive" ? (
115
+ <ConfirmSpendDialog open tier="destructive" title={`Archive "${name}"?`} confirmLabel="Archive" affected={1} confirming={run.loading}
116
+ description="The quote leaves the active list. Nothing is sent and you can unarchive it later." onConfirm={() => confirm("archive")} onCancel={cancel} />
117
+ ) : null}
118
+ {pending === "unarchive" ? (
119
+ <ConfirmSpendDialog open tier="external" title={`Unarchive "${name}"?`} confirmLabel="Unarchive" confirming={run.loading}
120
+ description="The quote returns to the active list." onConfirm={() => confirm("unarchive")} onCancel={cancel} />
121
+ ) : null}
122
+ {pending === "expire" ? (
123
+ <ConfirmSpendDialog open tier="destructive" title={`Expire "${name}"?`} confirmLabel="Expire quote" affected={1} confirming={run.loading}
124
+ description="The customer's link stops working. This is final: create a new quote to offer it again." onConfirm={() => confirm("expire")} onCancel={cancel} />
125
+ ) : null}
126
+ {pending === "revoke" ? (
127
+ <ConfirmSpendDialog open tier="destructive" title={`Revoke "${name}"?`} confirmLabel="Revoke quote" affected={1} confirming={run.loading}
128
+ description="Withdraws the quote you already sent, so the customer can no longer sign it. This is final: use it when the offer itself was wrong." onConfirm={() => confirm("revoke")} onCancel={cancel} />
129
+ ) : null}
130
+ {pending === "accept" ? (
131
+ <ConfirmSpendDialog open tier="external" title={`Mark "${name}" accepted?`} confirmLabel="Mark accepted" recipients={1} confirming={run.loading}
132
+ description={(
133
+ <div className="space-y-2 text-left">
134
+ <p>Accepts the quote on the customer&apos;s behalf. This raises an invoice or subscription with your billing provider, the same as a signed quote. Use it only when the customer paid or signed outside the product.</p>
135
+ <select aria-label="Evidence type" className="h-9 w-full rounded-md border bg-background px-2 text-sm" value={evidenceType} onChange={(e) => setEvidenceType(e.target.value)}>
136
+ {EVIDENCE_TYPES.map(([v, label]) => <option key={v} value={v}>{label}</option>)}
137
+ </select>
138
+ <Textarea aria-label="Evidence note" rows={3} placeholder="Describe the evidence (required), for example the email date or payment reference" value={evidenceNote} onChange={(e) => { setEvidenceNote(e.target.value); setNeedNote(false); }} />
139
+ {needNote ? <p role="alert" className="text-sm text-destructive">Add a note describing the evidence before accepting.</p> : null}
140
+ </div>
141
+ )}
142
+ onConfirm={() => confirm("accept")} onCancel={cancel} />
143
+ ) : null}
144
+ </div>
145
+ );
146
+ }
@@ -4,13 +4,17 @@
4
4
  * QuoteList: quotes with a status filter, totals in the quote's own currency, and paging.
5
5
  *
6
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 }`)
7
+ * 1. listQuotes GET /quotes?status&include_archived&mashup_company_id&deal_id&page&limit (read; `{ items, total, page, limit }`)
8
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.
9
+ * Reading needs no confirmation. Pass `dealId` or `companyId` to show the quotes of one deal or company (for a tab on a
10
+ * deal page). `onOpen` receives the quote id for QuoteViewer.
11
+ *
12
+ * With `selectable`, each row gets a checkbox and the ticked quotes can be archived, expired or revoked together
13
+ * (QuoteBatchActions: id-addressed, confirmed, result read row by row). The selection is cleared when the filter or page
14
+ * changes and pruned to the quotes still on the page after every reload. `onChanged` tells the caller a batch ran.
11
15
  */
12
16
 
13
- import { useState } from "react";
17
+ import { useEffect, useState } from "react";
14
18
  import { Button, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
15
19
  import { g8Get } from "@/g8/lib/g8-client";
16
20
  import { useG8Query } from "@/g8/hooks/use-g8-query";
@@ -18,6 +22,7 @@ import { ApiError } from "@/g8/components/api-error";
18
22
  import { EmptyState } from "@/g8/components/empty-state";
19
23
  import { body, money, statusLabel, text } from "../lib/quotes";
20
24
  import type { Rec } from "../lib/quotes";
25
+ import { QuoteBatchActions } from "./quote-batch-actions";
21
26
 
22
27
  export interface QuoteListProps {
23
28
  dealId?: string;
@@ -29,21 +34,33 @@ export interface QuoteListProps {
29
34
  refreshKey?: number;
30
35
  /** Mark this quote row as the open one. */
31
36
  selectedId?: string | null;
37
+ /** Tick quotes to archive, expire or revoke them together. */
38
+ selectable?: boolean;
39
+ /** Called after a batch action ran, so the screen can reload its numbers. */
40
+ onChanged?: () => void;
32
41
  className?: string;
33
42
  }
34
43
 
35
44
  const STATUSES = ["", "draft", "sent", "viewed", "accepted", "declined", "voided", "expired", "archived"];
36
45
 
37
- export function QuoteList({ dealId, companyId, onOpen, onNew, pageSize = 25, refreshKey, selectedId, className }: QuoteListProps) {
46
+ export function QuoteList({ dealId, companyId, onOpen, onNew, pageSize = 25, refreshKey, selectedId, selectable, onChanged, className }: QuoteListProps) {
38
47
  const [status, setStatus] = useState("");
48
+ const [archived, setArchived] = useState(false);
39
49
  const [page, setPage] = useState(1);
40
50
  const q = useG8Query(
41
- (signal) => g8Get("/quotes", { status: status || undefined, deal_id: dealId, mashup_company_id: companyId, page, limit: pageSize }, { signal }),
42
- [status, dealId, companyId, page, pageSize, refreshKey],
51
+ (signal) => g8Get("/quotes", { status: status || undefined, include_archived: archived || undefined, deal_id: dealId, mashup_company_id: companyId, page, limit: pageSize }, { signal }),
52
+ [status, archived, dealId, companyId, page, pageSize, refreshKey],
43
53
  );
44
54
  const data = body(q.data);
45
55
  const rows = (Array.isArray(data.items) ? data.items : []) as Rec[];
46
56
  const total = typeof data.total === "number" ? data.total : rows.length;
57
+ const [ticked, setTicked] = useState<string[]>([]);
58
+ // A new filter or page is a different set of rows: forget the ticks. After a reload keep only quotes still on the page.
59
+ useEffect(() => { setTicked([]); }, [status, archived, page, dealId, companyId]);
60
+ const idsOnPage = rows.map((r) => text(r.id)).join(",");
61
+ useEffect(() => { setTicked((cur) => { const here = new Set(idsOnPage.split(",")); const next = cur.filter((id) => here.has(id)); return next.length === cur.length ? cur : next; }); }, [idsOnPage]);
62
+ const tickedRows = rows.filter((r) => ticked.includes(text(r.id)));
63
+ const allTicked = rows.length > 0 && tickedRows.length === rows.length;
47
64
 
48
65
  return (
49
66
  <div className={className}>
@@ -51,17 +68,20 @@ export function QuoteList({ dealId, companyId, onOpen, onNew, pageSize = 25, ref
51
68
  <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">
52
69
  {STATUSES.map((s) => <option key={s} value={s}>{s ? statusLabel(s) : "All statuses"}</option>)}
53
70
  </select>
71
+ <label className="flex items-center gap-1 text-sm text-muted-foreground"><input type="checkbox" checked={archived} onChange={(e) => { setArchived(e.target.checked); setPage(1); }} />Include archived</label>
54
72
  {onNew ? <Button className="ml-auto" onClick={onNew}>New quote</Button> : null}
55
73
  </div>
56
74
  {q.error ? <ApiError error={q.error} onRetry={() => void q.refetch()} /> : null}
75
+ {selectable ? <QuoteBatchActions selected={tickedRows} onDone={() => { void q.refetch(); onChanged?.(); }} /> : null}
57
76
  {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 ? (
58
77
  <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} />
59
78
  ) : (
60
79
  <Table>
61
- <TableHeader><TableRow><TableHead>Quote</TableHead><TableHead>Status</TableHead><TableHead className="text-right">Total</TableHead><TableHead>Sent</TableHead></TableRow></TableHeader>
80
+ <TableHeader><TableRow>{selectable ? <TableHead className="w-8"><input type="checkbox" aria-label="Select all quotes on this page" checked={allTicked} onChange={(e) => setTicked(e.target.checked ? rows.map((r) => text(r.id)) : [])} /></TableHead> : null}<TableHead>Quote</TableHead><TableHead>Status</TableHead><TableHead className="text-right">Total</TableHead><TableHead>Sent</TableHead></TableRow></TableHeader>
62
81
  <TableBody>
63
82
  {rows.map((r) => (
64
83
  <TableRow key={text(r.id)} data-state={selectedId === text(r.id) ? "selected" : undefined}>
84
+ {selectable ? <TableCell><input type="checkbox" aria-label={`Select ${text(r.title, "Untitled quote")}`} checked={ticked.includes(text(r.id))} onChange={(e) => setTicked(e.target.checked ? [...ticked, text(r.id)] : ticked.filter((x) => x !== text(r.id)))} /></TableCell> : null}
65
85
  <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>
66
86
  <TableCell>{statusLabel(r.status)}</TableCell>
67
87
  <TableCell className="text-right">{money(r.total, text(r.currency, "USD"))}</TableCell>
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * ProductCatalog: every quotable product (Stripe and manual) as a list, with a full product record (all fields and
7
7
  * every price) in a detail panel, and a "Start a quote" action.
8
- * QuoteDealTab: the deal a quote belongs to, as a tab of the quote's detail.
8
+ * QuoteDealTab: the deal a quote belongs to, as a tab of the quote's detail, with "Create another quote" (CreateQuoteFromDeal).
9
9
  *
10
10
  * SDK operations, in order:
11
11
  * ProductCatalog: GET /quotable-products?page&limit (quotes.listQuotableProducts) read
@@ -28,6 +28,7 @@ import { g8Get } from "@/g8/lib/g8-client";
28
28
  import { useG8Query } from "@/g8/hooks/use-g8-query";
29
29
  import { formatMoney, type Deal } from "@/g8/deals/lib/deals-types";
30
30
  import { rowActivationProps } from "@/g8/hooks/use-row-activation";
31
+ import { CreateQuoteFromDeal } from "./create-quote-from-deal";
31
32
  import { body, frequencyOf, money, text, type QuotableProduct, type ProductPrice } from "../lib/quotes";
32
33
  import { useBrand } from "@/g8/hooks/use-brand";
33
34
 
@@ -113,7 +114,7 @@ export function QuoteDealTab({ quoteId, onOpenDeal }: { quoteId: string; onOpenD
113
114
  const d = q.data;
114
115
  if (!d) return <EmptyState title="No deal on this quote" />;
115
116
  return (
116
- <SectionCard title={d.name || "Deal"} actions={onOpenDeal ? <Button size="sm" variant="outline" onClick={() => onOpenDeal(d.id)}>Open deal</Button> : undefined}>
117
+ <SectionCard title={d.name || "Deal"} actions={<>{onOpenDeal ? <Button size="sm" variant="outline" onClick={() => onOpenDeal(d.id)}>Open deal</Button> : null}<CreateQuoteFromDeal dealId={d.id} label="Create another quote" /></>}>
117
118
  <KeyValueList columns={2} items={[
118
119
  { label: "Stage", value: d.stage_name }, { label: "Amount", value: d.amount == null ? null : formatMoney(d.amount, d.currency) }, { label: "Owner", value: d.owner_name ?? d.owner_email },
119
120
  { label: "Close date", value: d.close_date?.slice(0, 10) }, { label: "Pipeline", value: d.pipeline_name }, { label: "Contacts", value: d.contact_count },