@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.
- package/README.md +22 -3
- package/blocks/_foundation/README.md +61 -0
- package/blocks/_foundation/block.json +57 -0
- package/blocks/_foundation/components/auth-gate.tsx +363 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
- package/blocks/_foundation/components/empty-state.tsx +5 -2
- package/blocks/_foundation/components/list-select.tsx +112 -0
- package/blocks/_foundation/components/paging-notices.tsx +55 -0
- package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
- package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
- package/blocks/_foundation/components/screen/index.ts +1 -0
- package/blocks/_foundation/components/screen/split-view.tsx +1 -1
- package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
- package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
- package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
- package/blocks/_foundation/lib/paging.ts +57 -0
- package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
- package/blocks/_foundation/server/auth.ts +815 -0
- package/blocks/agents/README.md +81 -4
- package/blocks/agents/block.json +311 -2
- package/blocks/agents/components/agent-activity.tsx +112 -0
- package/blocks/agents/components/agent-approvals.tsx +135 -0
- package/blocks/agents/components/agent-builder.tsx +17 -3
- package/blocks/agents/components/agent-conversations.tsx +143 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
- package/blocks/agents/components/agent-operators.tsx +164 -0
- package/blocks/agents/components/agent-run-detail.tsx +20 -3
- package/blocks/agents/components/agent-runs.tsx +3 -3
- package/blocks/agents/components/agent-skills-panel.tsx +7 -7
- package/blocks/agents/components/agent-test-history.tsx +121 -0
- package/blocks/agents/components/agents-screen.tsx +53 -11
- package/blocks/agents/components/assistant-sessions.tsx +150 -0
- package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
- package/blocks/agents/components/from-template-button.tsx +65 -0
- package/blocks/agents/components/knowledge-browser.tsx +107 -0
- package/blocks/agents/components/knowledge-detail.tsx +156 -0
- package/blocks/agents/components/knowledge-form.tsx +69 -0
- package/blocks/agents/components/name-dialog.tsx +32 -0
- package/blocks/agents/components/receptionist-button.tsx +46 -0
- package/blocks/agents/components/save-as-template-button.tsx +32 -0
- package/blocks/agents/components/skill-editor.tsx +147 -0
- package/blocks/agents/components/skill-library.tsx +111 -0
- package/blocks/agents/lib/agent-types.ts +7 -1
- package/blocks/agents/lib/facts.tsx +13 -0
- package/blocks/agents/lib/knowledge-utils.ts +49 -0
- package/blocks/agents/lib/load-more.tsx +141 -0
- package/blocks/agents/lib/skill-utils.ts +91 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
- package/blocks/analytics/components/analytics-screen.tsx +16 -3
- package/blocks/analytics/components/kpi-tiles.tsx +1 -1
- package/blocks/analytics/components/report-builder.tsx +15 -2
- package/blocks/appointments/README.md +6 -0
- package/blocks/appointments/block.json +27 -0
- package/blocks/appointments/components/booking-page.tsx +2 -2
- package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
- package/blocks/appointments/components/meeting-detail.tsx +2 -2
- package/blocks/appointments/components/meeting-list.tsx +29 -13
- package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
- package/blocks/campaigns/README.md +1 -1
- package/blocks/campaigns/block.json +34 -1
- package/blocks/campaigns/components/campaign-assets.tsx +1 -1
- package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
- package/blocks/campaigns/components/campaign-links.tsx +180 -0
- package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
- package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
- package/blocks/campaigns/components/icp-detail.tsx +11 -2
- package/blocks/campaigns/components/icp-editor.tsx +8 -3
- package/blocks/campaigns/components/persona-detail.tsx +11 -2
- package/blocks/campaigns/components/persona-editor.tsx +6 -1
- package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
- package/blocks/coach/components/call-scorecard.tsx +7 -7
- package/blocks/company-intelligence/README.md +132 -0
- package/blocks/company-intelligence/block.json +318 -0
- package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
- package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
- package/blocks/company-intelligence/components/company-profile.tsx +68 -0
- package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
- package/blocks/company-intelligence/components/report-detail.tsx +189 -0
- package/blocks/company-intelligence/components/research-reports.tsx +136 -0
- package/blocks/company-intelligence/components/research-run.tsx +165 -0
- package/blocks/company-intelligence/components/source-detail.tsx +117 -0
- package/blocks/company-intelligence/components/source-view.tsx +274 -0
- package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
- package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
- package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
- package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
- package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
- package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
- package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
- package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
- package/blocks/copilot/components/copilot-memory.tsx +6 -2
- package/blocks/copilot/components/copilot-panel.tsx +1 -1
- package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
- package/blocks/crm/README.md +25 -0
- package/blocks/crm/block.json +257 -9
- package/blocks/crm/components/bulk-update.tsx +119 -0
- package/blocks/crm/components/companies-screen.tsx +23 -9
- package/blocks/crm/components/company-detail.tsx +2 -2
- package/blocks/crm/components/contact-detail.tsx +4 -4
- package/blocks/crm/components/contact-documents-tab.tsx +144 -0
- package/blocks/crm/components/contact-insights-tab.tsx +118 -0
- package/blocks/crm/components/contact-lookup-link.tsx +85 -0
- package/blocks/crm/components/contact-row-actions.tsx +43 -0
- package/blocks/crm/components/contact-tag-chips.tsx +113 -0
- package/blocks/crm/components/contacts-screen.tsx +77 -18
- package/blocks/crm/components/dedupe-review.tsx +6 -1
- package/blocks/crm/components/import-wizard.tsx +3 -7
- package/blocks/crm/components/list-select.tsx +20 -0
- package/blocks/crm/components/owner-assign.tsx +132 -0
- package/blocks/crm/components/record-engagement-tab.tsx +103 -0
- package/blocks/crm/components/record-form.tsx +1 -1
- package/blocks/crm/components/record-open-links.tsx +79 -0
- package/blocks/crm/components/tag-assign.tsx +112 -0
- package/blocks/crm/components/tag-manager.tsx +178 -0
- package/blocks/crm/components/tasks-screen.tsx +35 -15
- package/blocks/crm/components/unlock-info-button.tsx +82 -0
- package/blocks/crm/components/verify-email-button.tsx +93 -0
- package/blocks/crm/hooks/use-all-lists.ts +18 -0
- package/blocks/crm/hooks/use-contact-tags.ts +29 -0
- package/blocks/crm/hooks/use-page-query.ts +3 -1
- package/blocks/crm/lib/crm-types.ts +14 -0
- package/blocks/crm/lib/read-lists.ts +16 -0
- package/blocks/crm/lib/record-actions.ts +102 -0
- package/blocks/deals/README.md +25 -6
- package/blocks/deals/block.json +168 -2
- package/blocks/deals/components/close-deal-dialog.tsx +169 -0
- package/blocks/deals/components/create-deal-button.tsx +109 -0
- package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
- package/blocks/deals/components/deal-detail.tsx +7 -5
- package/blocks/deals/components/deal-filters.tsx +65 -0
- package/blocks/deals/components/deal-line-items.tsx +120 -0
- package/blocks/deals/components/deal-notes-tab.tsx +97 -0
- package/blocks/deals/components/deal-related.tsx +30 -9
- package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
- package/blocks/deals/components/deals-screen.tsx +57 -22
- package/blocks/deals/components/forecast-view.tsx +4 -4
- package/blocks/deals/components/pipeline-board.tsx +8 -6
- package/blocks/deals/components/pipeline-editor.tsx +132 -0
- package/blocks/deals/components/record-deals-tab.tsx +62 -0
- package/blocks/deals/components/stage-editor.tsx +1 -1
- package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
- package/blocks/deals/lib/deal-extras.ts +156 -0
- package/blocks/dialer/README.md +14 -5
- package/blocks/dialer/block.json +17 -2
- package/blocks/dialer/components/call-contact-button.tsx +26 -9
- package/blocks/dialer/components/call-log-screen.tsx +10 -2
- package/blocks/dialer/components/call-log.tsx +16 -4
- package/blocks/dialer/components/call-queue.tsx +3 -3
- package/blocks/dialer/components/dialer-screen.tsx +69 -27
- package/blocks/dialer/components/disposition-panel.tsx +1 -1
- package/blocks/dialer/components/record-calls-tab.tsx +101 -0
- package/blocks/dialer/components/session-start-form.tsx +58 -11
- package/blocks/dialer/components/softphone.tsx +1 -1
- package/blocks/documents/README.md +86 -0
- package/blocks/documents/block.json +378 -0
- package/blocks/documents/components/account-bar.tsx +48 -0
- package/blocks/documents/components/correspondence-form.tsx +95 -0
- package/blocks/documents/components/correspondence-import.tsx +106 -0
- package/blocks/documents/components/correspondence-panel.tsx +191 -0
- package/blocks/documents/components/documents-screen.tsx +76 -0
- package/blocks/documents/components/library-document-detail.tsx +175 -0
- package/blocks/documents/components/library-document-form.tsx +105 -0
- package/blocks/documents/components/library-folder-form.tsx +74 -0
- package/blocks/documents/components/library-panel.tsx +235 -0
- package/blocks/documents/components/library-upload.tsx +160 -0
- package/blocks/documents/components/question-row.tsx +130 -0
- package/blocks/documents/components/questionnaire-detail.tsx +197 -0
- package/blocks/documents/components/questionnaires-panel.tsx +173 -0
- package/blocks/documents/components/record-documents-tab.tsx +109 -0
- package/blocks/documents/lib/documents-types.ts +252 -0
- package/blocks/documents/lib/download.ts +64 -0
- package/blocks/documents/recipes/follow-a-questionnaire.ts +26 -0
- package/blocks/documents/recipes/log-account-correspondence.ts +27 -0
- package/blocks/documents/recipes/organize-the-library.ts +25 -0
- package/blocks/email-signatures/README.md +123 -0
- package/blocks/email-signatures/block.json +341 -0
- package/blocks/email-signatures/components/email-signatures-screen.tsx +166 -0
- package/blocks/email-signatures/components/signature-assign-template.tsx +119 -0
- package/blocks/email-signatures/components/signature-block-editor.tsx +87 -0
- package/blocks/email-signatures/components/signature-campaign-form.tsx +165 -0
- package/blocks/email-signatures/components/signature-campaigns.tsx +75 -0
- package/blocks/email-signatures/components/signature-identity-form.tsx +131 -0
- package/blocks/email-signatures/components/signature-mailboxes.tsx +195 -0
- package/blocks/email-signatures/components/signature-policy.tsx +130 -0
- package/blocks/email-signatures/components/signature-preview.tsx +119 -0
- package/blocks/email-signatures/components/signature-report.tsx +77 -0
- package/blocks/email-signatures/components/signature-template-details.tsx +112 -0
- package/blocks/email-signatures/components/signature-template-form.tsx +109 -0
- package/blocks/email-signatures/hooks/use-signatures-data.ts +93 -0
- package/blocks/email-signatures/lib/signatures-types.ts +392 -0
- package/blocks/email-signatures/recipes/assign-a-signature-to-a-new-hire.ts +32 -0
- package/blocks/email-signatures/recipes/create-a-template-and-make-it-default.ts +35 -0
- package/blocks/email-signatures/recipes/signature-engagement-report.ts +16 -0
- package/blocks/enrichment/README.md +38 -10
- package/blocks/enrichment/block.json +164 -12
- package/blocks/enrichment/components/auto-enrichment-configs.tsx +134 -0
- package/blocks/enrichment/components/auto-enrichment-settings.tsx +82 -0
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +79 -30
- package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
- package/blocks/enrichment/components/enrich-button.tsx +1 -1
- package/blocks/enrichment/components/enrichment-activity.tsx +93 -0
- package/blocks/enrichment/components/enrichment-job-detail.tsx +18 -0
- package/blocks/enrichment/components/enrichment-screen.tsx +103 -19
- package/blocks/enrichment/components/list-pipelines.tsx +97 -0
- package/blocks/enrichment/components/pipeline-detail.tsx +214 -0
- package/blocks/enrichment/components/pipeline-editor.tsx +144 -0
- package/blocks/enrichment/components/pipeline-templates.tsx +90 -0
- package/blocks/enrichment/components/waterfall-config.tsx +27 -7
- package/blocks/enrichment/lib/enrichment-types.ts +51 -0
- package/blocks/enrichment/lib/pipeline-types.ts +234 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
- package/blocks/forms/README.md +10 -0
- package/blocks/forms/block.json +107 -0
- package/blocks/forms/components/form-activity.tsx +71 -0
- package/blocks/forms/components/form-backfill.tsx +108 -0
- package/blocks/forms/components/form-builder.tsx +1 -1
- package/blocks/forms/components/form-embed.tsx +1 -1
- package/blocks/forms/components/form-groups.tsx +151 -0
- package/blocks/forms/components/form-mapping-editor.tsx +6 -9
- package/blocks/forms/components/form-route-history.tsx +72 -0
- package/blocks/forms/components/form-saved-views.tsx +81 -0
- package/blocks/forms/components/forms-screen.tsx +57 -20
- package/blocks/forms/components/more-rows.tsx +30 -0
- package/blocks/forms/components/submissions-export.tsx +67 -0
- package/blocks/forms/components/submissions-table.tsx +15 -6
- package/blocks/forms/lib/forms-extra.ts +67 -0
- package/blocks/inbox/README.md +45 -4
- package/blocks/inbox/block.json +145 -8
- package/blocks/inbox/components/blocked-senders.tsx +103 -0
- package/blocks/inbox/components/compose-message.tsx +193 -0
- package/blocks/inbox/components/inbox-screen.tsx +54 -6
- package/blocks/inbox/components/inbox-search.tsx +106 -0
- package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
- package/blocks/inbox/components/tag-manager.tsx +136 -0
- package/blocks/inbox/components/thread-actions.tsx +133 -0
- package/blocks/inbox/components/thread-badge.tsx +2 -2
- package/blocks/inbox/components/thread-view.tsx +3 -1
- package/blocks/inbox/hooks/use-inbox-mailboxes.ts +31 -0
- package/blocks/inbox/lib/inbox-manage.ts +227 -0
- package/blocks/integrations/README.md +54 -1
- package/blocks/integrations/block.json +189 -15
- package/blocks/integrations/components/ad-accounts.tsx +82 -0
- package/blocks/integrations/components/connection-health.tsx +59 -0
- package/blocks/integrations/components/crm-detail.tsx +20 -1
- package/blocks/integrations/components/crm-records.tsx +103 -0
- package/blocks/integrations/components/field-mappings.tsx +61 -0
- package/blocks/integrations/components/integration-list.tsx +4 -4
- package/blocks/integrations/components/integrations-screen.tsx +20 -3
- package/blocks/integrations/components/migration-panel.tsx +133 -0
- package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
- package/blocks/integrations/components/sync-status.tsx +6 -6
- package/blocks/integrations/components/sync-triggers.tsx +134 -0
- package/blocks/integrations/components/webhook-detail.tsx +26 -7
- package/blocks/integrations/components/webhook-manager.tsx +30 -17
- package/blocks/integrations/components/workspace-tools.tsx +127 -0
- package/blocks/integrations/lib/crm-utils.ts +39 -0
- package/blocks/integrations/lib/facts.tsx +13 -0
- package/blocks/integrations/lib/integrations-utils.ts +5 -0
- package/blocks/integrations/lib/load-more.tsx +141 -0
- package/blocks/intent/components/account-intent-score.tsx +5 -5
- package/blocks/intent/components/intent-feed.tsx +8 -8
- package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
- package/blocks/intent/lib/intent-utils.ts +4 -4
- package/blocks/lifecycle/components/account-related.tsx +8 -1
- package/blocks/lifecycle/components/churn-list.tsx +5 -2
- package/blocks/lifecycle/components/customer-table.tsx +1 -1
- package/blocks/lists/README.md +7 -1
- package/blocks/lists/block.json +31 -3
- package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
- package/blocks/lists/components/add-to-list-button.tsx +17 -7
- package/blocks/lists/components/list-picker.tsx +13 -3
- package/blocks/lists/components/list-table.tsx +1 -1
- package/blocks/lists/components/lists-screen.tsx +4 -2
- package/blocks/lists/components/record-lists-tab.tsx +99 -0
- package/blocks/lists/components/suppression-manager.tsx +27 -6
- package/blocks/lists/components/suppressions-screen.tsx +21 -12
- package/blocks/mailboxes/block.json +4 -0
- package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
- package/blocks/mailboxes/components/domain-detail.tsx +10 -4
- package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
- package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
- package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
- package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
- package/blocks/mailboxes/components/warmup-health.tsx +20 -20
- package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
- package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
- package/blocks/newsletter/README.md +12 -0
- package/blocks/newsletter/block.json +117 -0
- package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
- package/blocks/newsletter/components/issue-versions.tsx +93 -0
- package/blocks/newsletter/components/more-rows.tsx +30 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
- package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
- package/blocks/newsletter/components/newsletter-screen.tsx +38 -13
- package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
- package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
- package/blocks/newsletter/components/subscriber-table.tsx +5 -7
- package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
- package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
- package/blocks/nurture/components/nurture-builder.tsx +10 -2
- package/blocks/nurture/components/nurture-enroll.tsx +6 -7
- package/blocks/nurture/components/nurture-form.tsx +5 -8
- package/blocks/nurture/components/nurture-screen.tsx +16 -5
- package/blocks/nurture/components/nurture-stats.tsx +5 -4
- package/blocks/objects/README.md +1 -1
- package/blocks/objects/block.json +13 -5
- package/blocks/objects/components/attribute-field-input.tsx +14 -1
- package/blocks/objects/components/object-record-detail.tsx +8 -1
- package/blocks/objects/components/object-schema-editor.tsx +1 -1
- package/blocks/objects/components/record-roles-panel.tsx +15 -5
- package/blocks/pages/README.md +14 -0
- package/blocks/pages/block.json +123 -2
- package/blocks/pages/components/more-rows.tsx +30 -0
- package/blocks/pages/components/page-ai-tools.tsx +196 -0
- package/blocks/pages/components/page-analytics.tsx +176 -0
- package/blocks/pages/components/page-clone.tsx +99 -0
- package/blocks/pages/components/page-domains.tsx +167 -0
- package/blocks/pages/components/page-editor.tsx +1 -1
- package/blocks/pages/components/page-generation-status.tsx +48 -0
- package/blocks/pages/components/page-tracking.tsx +137 -0
- package/blocks/pages/components/pages-screen.tsx +57 -16
- package/blocks/pages/components/seo-report.tsx +6 -1
- package/blocks/pages/hooks/use-landing-page-paths.ts +60 -0
- package/blocks/pages/lib/studio.ts +7 -0
- package/blocks/phone-numbers/README.md +74 -0
- package/blocks/phone-numbers/block.json +262 -0
- package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
- package/blocks/phone-numbers/components/assign-number.tsx +104 -0
- package/blocks/phone-numbers/components/number-search.tsx +239 -0
- package/blocks/phone-numbers/components/number-settings.tsx +134 -0
- package/blocks/phone-numbers/components/number-usage.tsx +97 -0
- package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
- package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
- package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
- package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
- package/blocks/phone-numbers/components/release-number.tsx +126 -0
- package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
- package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
- package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
- package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
- package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
- package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
- package/blocks/pickers/README.md +23 -0
- package/blocks/pickers/block.json +103 -0
- package/blocks/pickers/components/company-picker.tsx +90 -0
- package/blocks/pickers/components/contact-picker.tsx +131 -0
- package/blocks/pickers/components/member-picker.tsx +99 -0
- package/blocks/pickers/components/search-picker.tsx +98 -0
- package/blocks/pickers/hooks/use-current-member.ts +104 -0
- package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
- package/blocks/pickers/lib/use-debounced.ts +13 -0
- package/blocks/programmatic-pages/README.md +82 -0
- package/blocks/programmatic-pages/block.json +292 -0
- package/blocks/programmatic-pages/components/programmatic-campaign-form.tsx +136 -0
- package/blocks/programmatic-pages/components/programmatic-generate.tsx +112 -0
- package/blocks/programmatic-pages/components/programmatic-generated-pages.tsx +210 -0
- package/blocks/programmatic-pages/components/programmatic-overview.tsx +84 -0
- package/blocks/programmatic-pages/components/programmatic-pages-screen.tsx +221 -0
- package/blocks/programmatic-pages/components/programmatic-preview.tsx +66 -0
- package/blocks/programmatic-pages/components/programmatic-publish.tsx +102 -0
- package/blocks/programmatic-pages/components/programmatic-settings.tsx +96 -0
- package/blocks/programmatic-pages/components/programmatic-source-fields.tsx +75 -0
- package/blocks/programmatic-pages/components/programmatic-source.tsx +106 -0
- package/blocks/programmatic-pages/components/programmatic-variables.tsx +84 -0
- package/blocks/programmatic-pages/hooks/use-programmatic-workspace.ts +19 -0
- package/blocks/programmatic-pages/lib/programmatic-types.ts +330 -0
- package/blocks/programmatic-pages/recipes/create-a-programmatic-campaign.ts +23 -0
- package/blocks/programmatic-pages/recipes/render-and-follow.ts +21 -0
- package/blocks/programmatic-pages/recipes/review-then-publish.ts +17 -0
- package/blocks/quotes/README.md +38 -1
- package/blocks/quotes/block.json +170 -6
- package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
- package/blocks/quotes/components/manual-products.tsx +144 -0
- package/blocks/quotes/components/quote-batch-actions.tsx +86 -0
- package/blocks/quotes/components/quote-builder.tsx +1 -1
- package/blocks/quotes/components/quote-documents.tsx +94 -0
- package/blocks/quotes/components/quote-evidence.tsx +95 -0
- package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
- package/blocks/quotes/components/quote-list.tsx +28 -8
- package/blocks/quotes/components/quote-related.tsx +3 -2
- package/blocks/quotes/components/quote-settings.tsx +184 -0
- package/blocks/quotes/components/quote-templates.tsx +189 -0
- package/blocks/quotes/components/quote-viewer.tsx +23 -5
- package/blocks/quotes/components/quotes-screen.tsx +15 -5
- package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
- package/blocks/quotes/lib/quote-admin.ts +264 -0
- package/blocks/radar/components/competitor-battle-card.tsx +1 -1
- package/blocks/radar/components/radar-overview.tsx +4 -4
- package/blocks/radar/components/radar-screen.tsx +2 -2
- package/blocks/sequencer/README.md +61 -0
- package/blocks/sequencer/block.json +200 -6
- package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
- package/blocks/sequencer/components/enrollment-withdraw.tsx +71 -0
- package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
- package/blocks/sequencer/components/sequence-builder.tsx +32 -23
- package/blocks/sequencer/components/sequence-campaign-link.tsx +78 -0
- package/blocks/sequencer/components/sequence-detail.tsx +10 -2
- package/blocks/sequencer/components/sequence-edit-form.tsx +111 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +5 -2
- package/blocks/sequencer/components/sequence-list.tsx +1 -1
- package/blocks/sequencer/components/sequence-manage-actions.tsx +119 -0
- package/blocks/sequencer/components/sequence-senders-panel.tsx +84 -0
- package/blocks/sequencer/components/sequence-stats.tsx +8 -8
- package/blocks/sequencer/components/sequence-templates.tsx +160 -0
- package/blocks/sequencer/components/sequences-screen.tsx +39 -6
- package/blocks/sequencer/components/step-ai-assist.tsx +196 -0
- package/blocks/sequencer/components/step-editor.tsx +21 -12
- package/blocks/sequencer/components/step-templates.tsx +224 -0
- package/blocks/sequencer/lib/sequencer-manage.ts +235 -0
- package/blocks/sequencer/lib/sequencer-utils.ts +9 -5
- package/blocks/signals/README.md +39 -0
- package/blocks/signals/block.json +287 -0
- package/blocks/signals/components/auto-seed-panel.tsx +111 -0
- package/blocks/signals/components/listener-form.tsx +184 -0
- package/blocks/signals/components/matched-posts.tsx +195 -0
- package/blocks/signals/components/post-engagements.tsx +114 -0
- package/blocks/signals/components/record-social-tab.tsx +49 -0
- package/blocks/signals/components/social-listeners-screen.tsx +250 -0
- package/blocks/signals/components/social-settings.tsx +128 -0
- package/blocks/signals/components/source-lists.tsx +83 -0
- package/blocks/signals/components/top-engagers.tsx +68 -0
- package/blocks/signals/components/x-integration.tsx +129 -0
- package/blocks/signals/lib/signals-types.ts +228 -0
- package/blocks/signals/recipes/preview-and-create-a-listener.ts +16 -0
- package/blocks/signals/recipes/spend-against-caps.ts +14 -0
- package/blocks/signals/recipes/triage-matched-posts.ts +18 -0
- package/blocks/social-ads/README.md +8 -0
- package/blocks/social-ads/block.json +137 -4
- package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
- package/blocks/social-ads/components/audience-sync.tsx +6 -2
- package/blocks/social-ads/components/campaign-ads.tsx +166 -0
- package/blocks/social-ads/components/campaign-detail.tsx +6 -1
- package/blocks/social-ads/components/campaign-editor.tsx +134 -0
- package/blocks/social-ads/components/creative-concepts.tsx +74 -0
- package/blocks/social-ads/components/creative-detail.tsx +6 -1
- package/blocks/social-ads/components/creative-gallery.tsx +20 -2
- package/blocks/social-ads/components/creative-revisions.tsx +57 -0
- package/blocks/social-ads/components/creative-tools.tsx +176 -0
- package/blocks/social-ads/components/post-composer.tsx +4 -2
- package/blocks/social-ads/components/post-detail.tsx +3 -0
- package/blocks/social-ads/components/social-ideas.tsx +64 -0
- package/blocks/social-ads/components/social-performance.tsx +80 -0
- package/blocks/social-ads/components/social-screen.tsx +33 -6
- package/blocks/social-ads/lib/ads-extra.ts +67 -0
- package/blocks/surveys/README.md +91 -0
- package/blocks/surveys/block.json +299 -0
- package/blocks/surveys/components/account-nps.tsx +69 -0
- package/blocks/surveys/components/contact-survey-history.tsx +52 -0
- package/blocks/surveys/components/org-summary.tsx +82 -0
- package/blocks/surveys/components/sending-domains.tsx +113 -0
- package/blocks/surveys/components/survey-form.tsx +126 -0
- package/blocks/surveys/components/survey-responses.tsx +145 -0
- package/blocks/surveys/components/survey-results.tsx +72 -0
- package/blocks/surveys/components/survey-send-history.tsx +58 -0
- package/blocks/surveys/components/survey-send.tsx +162 -0
- package/blocks/surveys/components/survey-triggers.tsx +166 -0
- package/blocks/surveys/components/surveys-screen.tsx +234 -0
- package/blocks/surveys/lib/surveys-types.ts +228 -0
- package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
- package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
- package/blocks/surveys/recipes/survey-score-report.ts +27 -0
- package/blocks/visitors/components/snippet-install.tsx +18 -9
- package/blocks/visitors/components/traffic-chart.tsx +7 -6
- package/blocks/visitors/components/visitor-company-card.tsx +24 -14
- package/blocks/visitors/components/visitor-feed.tsx +5 -5
- package/blocks/web-analytics/README.md +102 -0
- package/blocks/web-analytics/block.json +292 -0
- package/blocks/web-analytics/components/acquisition-tab.tsx +154 -0
- package/blocks/web-analytics/components/analytics-filters.tsx +161 -0
- package/blocks/web-analytics/components/audience-tab.tsx +135 -0
- package/blocks/web-analytics/components/conversions-tab.tsx +128 -0
- package/blocks/web-analytics/components/engagement-tab.tsx +123 -0
- package/blocks/web-analytics/components/overview-tab.tsx +102 -0
- package/blocks/web-analytics/components/pages-tab.tsx +148 -0
- package/blocks/web-analytics/components/ranked-table.tsx +103 -0
- package/blocks/web-analytics/components/realtime-tab.tsx +103 -0
- package/blocks/web-analytics/components/report-card.tsx +71 -0
- package/blocks/web-analytics/components/web-analytics-screen.tsx +90 -0
- package/blocks/web-analytics/hooks/use-report.ts +48 -0
- package/blocks/web-analytics/lib/analytics-scope.ts +155 -0
- package/blocks/web-analytics/lib/page-path.ts +14 -0
- package/blocks/web-analytics/lib/web-analytics-types.ts +187 -0
- package/blocks/web-analytics/recipes/best-landing-pages.ts +26 -0
- package/blocks/web-analytics/recipes/form-conversion-check.ts +19 -0
- package/blocks/web-analytics/recipes/weekly-traffic-report.ts +37 -0
- package/blocks/webchat/components/chat-widget-config.tsx +1 -1
- package/blocks/webchat/components/conversation-list.tsx +1 -1
- package/blocks/workbench/README.md +54 -2
- package/blocks/workbench/block.json +234 -5
- package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
- package/blocks/workbench/components/run-progress.tsx +1 -1
- package/blocks/workbench/components/workbench-auto-enrich.tsx +88 -0
- package/blocks/workbench/components/workbench-automation.tsx +36 -0
- package/blocks/workbench/components/workbench-connections.tsx +136 -0
- package/blocks/workbench/components/workbench-crm-import.tsx +91 -0
- package/blocks/workbench/components/workbench-datafile-import.tsx +154 -0
- package/blocks/workbench/components/workbench-grid.tsx +71 -8
- package/blocks/workbench/components/workbench-pipelines.tsx +173 -0
- package/blocks/workbench/components/workbench-row-actions.tsx +8 -1
- package/blocks/workbench/components/workbench-schedule.tsx +191 -0
- package/blocks/workbench/components/workbench-screen.tsx +65 -22
- package/blocks/workbench/components/workbench-send-to-sequence.tsx +116 -0
- package/blocks/workbench/components/workbench-table-list.tsx +14 -2
- package/blocks/workbench/components/workbench-table-settings.tsx +53 -0
- package/blocks/workbench/components/workbench-triggers.tsx +178 -0
- package/blocks/workbench/hooks/use-connected-crms.ts +29 -0
- package/blocks/workbench/lib/api-status.ts +8 -0
- package/blocks/workbench/lib/automation-types.ts +257 -0
- package/blocks/workflows/components/workflow-editor.tsx +1 -1
- package/dist/cli.js +744 -394
- package/dist/index.browser.js +1 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +1 -1
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +1 -1
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +1 -1
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +1 -1
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/auth.d.ts +45 -0
- package/dist/types/blocks/brand.d.ts +11 -2
- package/dist/types/blocks/create.d.ts +11 -4
- package/dist/types/blocks/doctor.d.ts +1 -0
- package/dist/types/blocks/flows.d.ts +7 -1
- package/dist/types/blocks/orgBrand.d.ts +34 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/version.d.ts +2 -2
- package/docs/api/desk.d.ts +2 -2
- package/docs/api/desk.md +3 -3
- package/docs/api/forms.md +1 -1
- package/package.json +1 -1
- package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
- package/stacks/6sense/pages/contacts.tsx.tmpl +21 -0
- package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
- package/stacks/6sense.json +37 -4
- package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
- package/stacks/apollo/pages/campaigns.tsx.tmpl +27 -0
- package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
- package/stacks/apollo/pages/contacts.tsx.tmpl +35 -95
- package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
- package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/apollo/pages/lists.tsx.tmpl +24 -0
- package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
- package/stacks/apollo/pages/sequences.tsx.tmpl +18 -0
- package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
- package/stacks/apollo.json +68 -9
- package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
- package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
- package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
- package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
- package/stacks/calendly.json +8 -4
- package/stacks/clay/pages/companies.tsx.tmpl +17 -1
- package/stacks/clay/pages/contacts.tsx.tmpl +14 -0
- package/stacks/clay/pages/lists.tsx.tmpl +24 -0
- package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
- package/stacks/clay.json +16 -3
- package/stacks/gong/pages/calls.tsx.tmpl +23 -0
- package/stacks/gong/pages/companies.tsx.tmpl +22 -0
- package/stacks/gong/pages/contacts.tsx.tmpl +34 -0
- package/stacks/gong/pages/lists.tsx.tmpl +12 -0
- package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
- package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
- package/stacks/gong.json +23 -6
- package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
- package/stacks/hootsuite/pages/contacts.tsx.tmpl +30 -0
- package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
- package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
- package/stacks/hootsuite.json +19 -4
- package/stacks/hubspot/pages/campaigns.tsx.tmpl +30 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +16 -35
- package/stacks/hubspot/pages/contacts.tsx.tmpl +35 -95
- package/stacks/hubspot/pages/deals.tsx.tmpl +14 -1
- package/stacks/hubspot/pages/forms.tsx.tmpl +36 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
- package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
- package/stacks/hubspot/pages/sequences.tsx.tmpl +18 -0
- package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
- package/stacks/hubspot/pages/web-analytics.tsx.tmpl +14 -0
- package/stacks/hubspot.json +66 -7
- package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
- package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
- package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
- package/stacks/intercom.json +18 -4
- package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
- package/stacks/mailchimp/pages/contacts.tsx.tmpl +30 -0
- package/stacks/mailchimp/pages/forms.tsx.tmpl +36 -0
- package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
- package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
- package/stacks/mailchimp.json +22 -6
- package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
- package/stacks/outreach/pages/campaigns.tsx.tmpl +27 -0
- package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
- package/stacks/outreach/pages/contacts.tsx.tmpl +29 -66
- package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
- package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
- package/stacks/outreach/pages/sequences.tsx.tmpl +18 -0
- package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
- package/stacks/outreach.json +38 -9
- package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +17 -35
- package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
- package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
- package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
- package/stacks/pipedrive.json +14 -2
- package/stacks/salesforce/pages/companies.tsx.tmpl +16 -35
- package/stacks/salesforce/pages/contacts.tsx.tmpl +26 -35
- package/stacks/salesforce/pages/deals.tsx.tmpl +14 -1
- package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
- package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
- package/stacks/salesforce.json +17 -3
- package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +16 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +24 -0
- package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +36 -3
- package/styles/styles.css +1 -1
- package/templates/auth/auth-route.ts.tmpl +16 -0
- package/templates/auth/g8.auth.anonymous.ts.tmpl +15 -0
- package/templates/auth/g8.auth.basic.ts.tmpl +17 -0
- package/templates/auth/g8.auth.jwt.ts.tmpl +23 -0
- package/templates/auth/g8.auth.password.ts.tmpl +18 -0
- package/templates/auth/g8.auth.session.ts.tmpl +23 -0
- package/templates/auth/middleware.ts.tmpl +14 -0
- package/templates/next-app/README.md.tmpl +5 -0
- package/templates/next-app/app/globals.css.tmpl +349 -64
- package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
package/blocks/quotes/README.md
CHANGED
|
@@ -10,6 +10,14 @@ Install with `npx @graph8/sdk add quotes`.
|
|
|
10
10
|
| `ProductPicker` | Quotable products (Stripe + manual) and their prices; returns a ready line item. |
|
|
11
11
|
| `QuoteViewer` | One quote with totals and status-based actions: send/resend, edit as draft, duplicate, delete. |
|
|
12
12
|
| `SignatureStatus` | Envelope state, signers, activity, reminder. |
|
|
13
|
+
| `QuoteLifecycleActions` | PDF download, archive and unarchive, and for a sent quote expire, revoke and mark accepted. Each risky action is confirmed. |
|
|
14
|
+
| `QuoteTemplates` | Quote templates: create, edit (a full replace), delete; terms, validity, contract length, attached documents, preset lines. |
|
|
15
|
+
| `QuoteSettings` | Org-wide quote settings: branding, terms, numbering, who quotes are sent as, billing provider, deal automation, saved terms. |
|
|
16
|
+
| `QuoteDocuments` | The legal document library (NDA, MSA, other PDFs) that templates attach. |
|
|
17
|
+
| `QuoteEvidence` | Internal PDF evidence on one quote: list, attach, download. |
|
|
18
|
+
| `QuoteBatchActions` | Archive, expire or revoke the ticked quotes in one confirmed, id-addressed request (mounted by `QuoteList selectable`). |
|
|
19
|
+
| `ManualProducts` | The org's own product catalogue: list, add, edit, retire. |
|
|
20
|
+
| `CreateQuoteFromDeal` | A "Create quote from this deal" button (builder in a side sheet, then the saved quote). Pass it to `DealsScreen`'s `renderQuoteActions`. |
|
|
13
21
|
|
|
14
22
|
## Screens
|
|
15
23
|
|
|
@@ -19,6 +27,30 @@ Install with `npx @graph8/sdk add quotes`.
|
|
|
19
27
|
- Open a quote (deep link `?id=<quote id>`): line items, totals, signer, signature progress, with the actions its status
|
|
20
28
|
allows. Send, resend, signature reminder and edit-as-draft are external and confirmed; delete is destructive and
|
|
21
29
|
confirmed; duplicate and saving a draft email nobody.
|
|
30
|
+
- **Lifecycle** (under the quote header): Download PDF (opens the link the API returns, or saves base64 content), Archive
|
|
31
|
+
and Unarchive (the list has "Include archived"), and for a sent or viewed quote **Expire** and **Revoke** (both final,
|
|
32
|
+
destructive confirms) and **Mark accepted**. Mark accepted raises an invoice with your billing provider, so it is an
|
|
33
|
+
external confirm that also requires an evidence type (signed offline, customer email, verbal authorization, payment
|
|
34
|
+
received) and a note. Use it only when the customer paid or signed outside the product.
|
|
35
|
+
- **Batch** (`QuoteList selectable`, on in the screen): tick quotes, then Archive, Expire or Revoke. The request carries the ticked quote
|
|
36
|
+
ids and nothing else (never a filter); a quote the action does not apply to (expire a draft, revoke a quote that is not out for
|
|
37
|
+
signature, archive an archived one) is left out of the request and out of the confirm count, and the confirm says how many were
|
|
38
|
+
left out. The result is read quote by quote ("N of M archived", with the reason for each skipped or failed one). Ticks clear when the
|
|
39
|
+
filter or page changes and prune to the quotes still listed after a reload. Max 200 per request.
|
|
40
|
+
- **Templates tab**: list, create, edit and delete quote templates. Saving a template REPLACES it, and the API body cannot carry
|
|
41
|
+
accepted payment methods, marketing rights or the contract-start-on-signature setting, so a template that has any of those asks
|
|
42
|
+
first and names which ones the save resets. Delete is a destructive confirm (quotes already built from the template are unchanged).
|
|
43
|
+
- **Settings tab**: org-wide settings and the document library. Only changed fields are sent. The API ignores a null, so a field that has
|
|
44
|
+
a value cannot be cleared from here: the form says so and holds Save instead of reporting a save that changed nothing. Changing who
|
|
45
|
+
quotes are sent as, the billing provider (Manual or Stripe only: QuickBooks, Xero and Chargebee are not built) or the deal automation
|
|
46
|
+
confirms first and states the effect. Document uploads take PDFs only, up to about 5 MB (the app proxy caps a whole request at 5 MiB;
|
|
47
|
+
the API allows 10 MB).
|
|
48
|
+
- **Products tab**: the quotable catalogue, plus the manual products you add, edit (changed fields only) and retire (a soft delete; quotes
|
|
49
|
+
that already include the product keep the line item).
|
|
50
|
+
- **Evidence tab** of an open quote: attach internal PDFs (checked before upload; the API has no delete, so an attached file stays) and
|
|
51
|
+
download them through the API's 60 second link.
|
|
52
|
+
- **Preview email**: inside the send confirm, renders the exact email and attachments without sending anything.
|
|
53
|
+
- The Deal tab of a quote offers "Create another quote" for the same deal.
|
|
22
54
|
- **New quote** asks for the deal first (search by name, skipped when `dealId` is given because graph8 attaches every
|
|
23
55
|
quote to a deal), then opens the builder (details, signer, line items from the quotable products, live totals).
|
|
24
56
|
- Edit a draft from the viewer in a side sheet.
|
|
@@ -48,7 +80,7 @@ The deal search calls `GET /deals`, so the key also needs `deals:read` and the p
|
|
|
48
80
|
|
|
49
81
|
## Prerequisites
|
|
50
82
|
|
|
51
|
-
- `foundation` installed (and `deals`, which supplies the `dealId`); proxy `allowedPrefixes` include `quotes`, `quotable-products`, `quote-contacts`, `envelopes
|
|
83
|
+
- `foundation` installed (and `deals`, which supplies the `dealId`); proxy `allowedPrefixes` include `quotes`, `quotable-products`, `quote-contacts`, `quote-templates`, `quote-settings`, `manual-products`, `envelopes` (derived from the block's operations; `doctor --fix-prefixes` adds them to an existing app). Templates, settings, documents and manual products need an organization admin key to change.
|
|
52
84
|
- `where: "api"`: at least one quotable product.
|
|
53
85
|
- `where: "app"`: quote settings (company legal name, terms, sender) filled in; Stripe connected for Stripe products and subscriptions.
|
|
54
86
|
|
|
@@ -94,3 +126,8 @@ and **delete** (destructive). From code, never call `sendQuote` without the pers
|
|
|
94
126
|
- `recipes/create-and-send-quote.ts`: Create a quote from quotable products and send it for signature
|
|
95
127
|
- `recipes/track-quote-signature.ts`: Wait for a quote to be signed (or declined)
|
|
96
128
|
- `recipes/revise-sent-quote.ts`: Change a quote that was already sent
|
|
129
|
+
|
|
130
|
+
## Cross-block glue
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
- `RecordQuotesTab` (`components/record-quotes-tab.tsx`): the quotes tied to a contact (as signer) or a company (`getContactQuotes` / `getCompanyQuotes`, unpaged), each opening `/quotes?id=<id>`. `extraDetailTabs` entry for ContactsScreen and CompaniesScreen.
|
package/blocks/quotes/block.json
CHANGED
|
@@ -8,31 +8,65 @@
|
|
|
8
8
|
"draft builder with live totals and signer search",
|
|
9
9
|
"quotable product picker (Stripe and manual products)",
|
|
10
10
|
"send, resend and edit-as-draft behind confirms",
|
|
11
|
-
"e-signature status with reminder"
|
|
11
|
+
"e-signature status with reminder",
|
|
12
|
+
"quote lifecycle: PDF download, archive and unarchive, expire, revoke and mark accepted",
|
|
13
|
+
"create a quote from a deal anywhere with one button",
|
|
14
|
+
"quote templates: create, edit and delete, with attached documents and preset lines",
|
|
15
|
+
"org quote settings and the legal document library",
|
|
16
|
+
"internal PDF evidence on a quote",
|
|
17
|
+
"archive, expire or revoke several quotes at once, id-addressed and confirmed",
|
|
18
|
+
"manual product catalogue: add, edit and retire",
|
|
19
|
+
"preview the exact email before sending a quote"
|
|
12
20
|
],
|
|
13
21
|
"dependsOn": [
|
|
14
22
|
"deals"
|
|
15
23
|
],
|
|
16
24
|
"operations": [
|
|
25
|
+
"archive_quote_quotes__quote_id__archive_post",
|
|
26
|
+
"batch_quote_action_quotes_batch_post",
|
|
27
|
+
"create_manual_product_manual_products_post",
|
|
17
28
|
"create_quote_quotes_post",
|
|
29
|
+
"create_quote_template_quote_templates_post",
|
|
30
|
+
"delete_manual_product_manual_products__product_id__delete",
|
|
31
|
+
"delete_quote_document_quote_settings_documents__doc_id__delete",
|
|
18
32
|
"delete_quote_quotes__quote_id__delete",
|
|
33
|
+
"delete_quote_template_quote_templates__template_id__delete",
|
|
34
|
+
"download_quote_evidence_quotes__quote_id__evidence__evidence_id__get",
|
|
19
35
|
"duplicate_quote_quotes__quote_id__duplicate_post",
|
|
20
36
|
"edit_quote_as_draft_quotes__quote_id__edit_as_draft_post",
|
|
37
|
+
"expire_quote_quotes__quote_id__expire_post",
|
|
38
|
+
"get_company_quotes_companies__company_id__quotes_get",
|
|
39
|
+
"get_contact_quotes_contacts__contact_id__quotes_get",
|
|
21
40
|
"get_deal_deals__deal_id__get",
|
|
41
|
+
"get_quote_pdf_quotes__quote_id__pdf_get",
|
|
22
42
|
"get_quote_quotes__quote_id__get",
|
|
43
|
+
"get_quote_settings_quote_settings_get",
|
|
23
44
|
"list_deals_deals_get",
|
|
45
|
+
"list_manual_products_manual_products_get",
|
|
24
46
|
"list_quotable_products_quotable_products_get",
|
|
47
|
+
"list_quote_documents_quote_settings_documents_get",
|
|
48
|
+
"list_quote_evidence_quotes__quote_id__evidence_get",
|
|
49
|
+
"list_quote_templates_quote_templates_get",
|
|
25
50
|
"list_quotes_quotes_get",
|
|
51
|
+
"mark_quote_accepted_quotes__quote_id__mark_accepted_post",
|
|
52
|
+
"preview_quote_send_quotes__quote_id__send_preview_post",
|
|
26
53
|
"resend_envelope_envelopes__envelope_id__resend_post",
|
|
54
|
+
"revoke_quote_quotes__quote_id__revoke_post",
|
|
27
55
|
"search_quote_contacts_quote_contacts_get",
|
|
28
56
|
"send_quote_quotes__quote_id__send_post",
|
|
29
|
-
"
|
|
57
|
+
"unarchive_quote_quotes__quote_id__unarchive_post",
|
|
58
|
+
"update_manual_product_manual_products__product_id__patch",
|
|
59
|
+
"update_quote_quotes__quote_id__patch",
|
|
60
|
+
"update_quote_settings_quote_settings_put",
|
|
61
|
+
"update_quote_template_quote_templates__template_id__put",
|
|
62
|
+
"upload_quote_document_quote_settings_documents_post",
|
|
63
|
+
"upload_quote_evidence_quotes__quote_id__evidence_post"
|
|
30
64
|
],
|
|
31
65
|
"components": [
|
|
32
66
|
{
|
|
33
67
|
"name": "QuoteList",
|
|
34
68
|
"file": "components/quote-list.tsx",
|
|
35
|
-
"purpose": "Quotes with a status filter, totals in the quote currency, and paging; optionally scoped to a deal or company.",
|
|
69
|
+
"purpose": "Quotes with a status filter, totals in the quote currency, and paging; optionally scoped to a deal or company. With selectable, ticked quotes can be archived, expired or revoked together.",
|
|
36
70
|
"operations": [
|
|
37
71
|
"list_quotes_quotes_get"
|
|
38
72
|
],
|
|
@@ -69,13 +103,14 @@
|
|
|
69
103
|
{
|
|
70
104
|
"name": "QuoteViewer",
|
|
71
105
|
"file": "components/quote-viewer.tsx",
|
|
72
|
-
"purpose": "Read a quote with its totals and actions by status: send or resend for signature, edit as draft, duplicate, delete.",
|
|
106
|
+
"purpose": "Read a quote with its totals and actions by status: send or resend for signature, edit as draft, duplicate, delete. Previews the exact email from the send confirm.",
|
|
73
107
|
"operations": [
|
|
74
108
|
"get_quote_quotes__quote_id__get",
|
|
75
109
|
"send_quote_quotes__quote_id__send_post",
|
|
76
110
|
"edit_quote_as_draft_quotes__quote_id__edit_as_draft_post",
|
|
77
111
|
"duplicate_quote_quotes__quote_id__duplicate_post",
|
|
78
|
-
"delete_quote_quotes__quote_id__delete"
|
|
112
|
+
"delete_quote_quotes__quote_id__delete",
|
|
113
|
+
"preview_quote_send_quotes__quote_id__send_preview_post"
|
|
79
114
|
],
|
|
80
115
|
"tiers": [
|
|
81
116
|
"read",
|
|
@@ -108,6 +143,93 @@
|
|
|
108
143
|
"read"
|
|
109
144
|
]
|
|
110
145
|
},
|
|
146
|
+
{
|
|
147
|
+
"name": "QuoteTemplates",
|
|
148
|
+
"file": "components/quote-templates.tsx",
|
|
149
|
+
"purpose": "Quote templates: list, create, edit (a full replace) and delete; terms, validity, contract length, attached documents and preset lines. Delete is confirmed, and so is a save that would reset settings the API cannot carry.",
|
|
150
|
+
"operations": [
|
|
151
|
+
"list_quote_templates_quote_templates_get",
|
|
152
|
+
"create_quote_template_quote_templates_post",
|
|
153
|
+
"update_quote_template_quote_templates__template_id__put",
|
|
154
|
+
"delete_quote_template_quote_templates__template_id__delete",
|
|
155
|
+
"list_quote_documents_quote_settings_documents_get",
|
|
156
|
+
"list_quotable_products_quotable_products_get"
|
|
157
|
+
],
|
|
158
|
+
"tiers": [
|
|
159
|
+
"read",
|
|
160
|
+
"external",
|
|
161
|
+
"destructive"
|
|
162
|
+
]
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"name": "QuoteSettings",
|
|
166
|
+
"file": "components/quote-settings.tsx",
|
|
167
|
+
"purpose": "Org-wide quote settings (branding, terms, numbering, sender identity, billing provider, deal automation, required billing details, saved terms). Sends only changed fields, refuses to pretend a value was cleared, and confirms changes to sender identity, billing provider and deal automation.",
|
|
168
|
+
"operations": [
|
|
169
|
+
"get_quote_settings_quote_settings_get",
|
|
170
|
+
"update_quote_settings_quote_settings_put"
|
|
171
|
+
],
|
|
172
|
+
"tiers": [
|
|
173
|
+
"read",
|
|
174
|
+
"external"
|
|
175
|
+
]
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"name": "QuoteDocuments",
|
|
179
|
+
"file": "components/quote-documents.tsx",
|
|
180
|
+
"purpose": "The legal document library (NDA, MSA, other PDFs): list, upload with client-side PDF and size checks, delete behind a confirm.",
|
|
181
|
+
"operations": [
|
|
182
|
+
"list_quote_documents_quote_settings_documents_get",
|
|
183
|
+
"upload_quote_document_quote_settings_documents_post",
|
|
184
|
+
"delete_quote_document_quote_settings_documents__doc_id__delete"
|
|
185
|
+
],
|
|
186
|
+
"tiers": [
|
|
187
|
+
"read",
|
|
188
|
+
"external",
|
|
189
|
+
"destructive"
|
|
190
|
+
]
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
"name": "QuoteEvidence",
|
|
194
|
+
"file": "components/quote-evidence.tsx",
|
|
195
|
+
"purpose": "Internal PDF evidence on a quote: list, attach (checked before upload; cannot be removed afterwards) and download via the API's short-lived link.",
|
|
196
|
+
"operations": [
|
|
197
|
+
"list_quote_evidence_quotes__quote_id__evidence_get",
|
|
198
|
+
"upload_quote_evidence_quotes__quote_id__evidence_post",
|
|
199
|
+
"download_quote_evidence_quotes__quote_id__evidence__evidence_id__get"
|
|
200
|
+
],
|
|
201
|
+
"tiers": [
|
|
202
|
+
"read",
|
|
203
|
+
"write"
|
|
204
|
+
]
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
"name": "QuoteBatchActions",
|
|
208
|
+
"file": "components/quote-batch-actions.tsx",
|
|
209
|
+
"purpose": "Archive, expire or revoke the ticked quotes in one confirmed request addressed by quote id; reads the per-quote result.",
|
|
210
|
+
"operations": [
|
|
211
|
+
"batch_quote_action_quotes_batch_post"
|
|
212
|
+
],
|
|
213
|
+
"tiers": [
|
|
214
|
+
"external"
|
|
215
|
+
]
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
"name": "ManualProducts",
|
|
219
|
+
"file": "components/manual-products.tsx",
|
|
220
|
+
"purpose": "The org's own product catalogue: list (including inactive), add, edit changed fields only, and retire behind a confirm.",
|
|
221
|
+
"operations": [
|
|
222
|
+
"list_manual_products_manual_products_get",
|
|
223
|
+
"create_manual_product_manual_products_post",
|
|
224
|
+
"update_manual_product_manual_products__product_id__patch",
|
|
225
|
+
"delete_manual_product_manual_products__product_id__delete"
|
|
226
|
+
],
|
|
227
|
+
"tiers": [
|
|
228
|
+
"read",
|
|
229
|
+
"write",
|
|
230
|
+
"destructive"
|
|
231
|
+
]
|
|
232
|
+
},
|
|
111
233
|
{
|
|
112
234
|
"name": "ProductCatalog",
|
|
113
235
|
"file": "components/quote-related.tsx",
|
|
@@ -120,6 +242,43 @@
|
|
|
120
242
|
"tiers": [
|
|
121
243
|
"read"
|
|
122
244
|
]
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
"name": "QuoteLifecycleActions",
|
|
248
|
+
"file": "components/quote-lifecycle-actions.tsx",
|
|
249
|
+
"purpose": "After a quote exists: download the PDF, archive or unarchive, and for a sent quote expire, revoke or mark accepted (evidence required). Each risky action is confirmed.",
|
|
250
|
+
"operations": [
|
|
251
|
+
"archive_quote_quotes__quote_id__archive_post",
|
|
252
|
+
"expire_quote_quotes__quote_id__expire_post",
|
|
253
|
+
"get_quote_pdf_quotes__quote_id__pdf_get",
|
|
254
|
+
"mark_quote_accepted_quotes__quote_id__mark_accepted_post",
|
|
255
|
+
"revoke_quote_quotes__quote_id__revoke_post",
|
|
256
|
+
"unarchive_quote_quotes__quote_id__unarchive_post"
|
|
257
|
+
],
|
|
258
|
+
"tiers": [
|
|
259
|
+
"read",
|
|
260
|
+
"external",
|
|
261
|
+
"destructive"
|
|
262
|
+
]
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
"name": "CreateQuoteFromDeal",
|
|
266
|
+
"file": "components/create-quote-from-deal.tsx",
|
|
267
|
+
"purpose": "A 'Create quote from this deal' button: builder in a side sheet, then the saved quote with its actions. Pass it to the deals screen's renderQuoteActions.",
|
|
268
|
+
"operations": [],
|
|
269
|
+
"tiers": []
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
"name": "RecordQuotesTab",
|
|
273
|
+
"file": "components/record-quotes-tab.tsx",
|
|
274
|
+
"purpose": "The quotes tied to one contact (as signer) or one company, each opening in the quotes screen; an extraDetailTabs entry for ContactsScreen and CompaniesScreen.",
|
|
275
|
+
"operations": [
|
|
276
|
+
"get_contact_quotes_contacts__contact_id__quotes_get",
|
|
277
|
+
"get_company_quotes_companies__company_id__quotes_get"
|
|
278
|
+
],
|
|
279
|
+
"tiers": [
|
|
280
|
+
"read"
|
|
281
|
+
]
|
|
123
282
|
}
|
|
124
283
|
],
|
|
125
284
|
"hooks": [
|
|
@@ -133,6 +292,10 @@
|
|
|
133
292
|
{
|
|
134
293
|
"file": "lib/quotes.ts",
|
|
135
294
|
"purpose": "Line-item math in cents, product and line types, request line mapping, status labels."
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
"file": "lib/quote-admin.ts",
|
|
298
|
+
"purpose": "Rules for the admin screens: template save body and what a save resets, settings diff (only changed fields, what the API cannot clear), PDF upload checks, batch eligibility and result reading."
|
|
136
299
|
}
|
|
137
300
|
],
|
|
138
301
|
"recipes": [
|
|
@@ -176,7 +339,8 @@
|
|
|
176
339
|
"deals:read",
|
|
177
340
|
"quotes:delete",
|
|
178
341
|
"quotes:read",
|
|
179
|
-
"quotes:run"
|
|
342
|
+
"quotes:run",
|
|
343
|
+
"quotes:write"
|
|
180
344
|
],
|
|
181
345
|
"defaults": [
|
|
182
346
|
{
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CreateQuoteFromDeal: a "Create quote from this deal" button. It opens a side sheet with the quote builder for the
|
|
5
|
+
* deal, and after saving the draft shows the quote itself (send, duplicate, lifecycle) in the same sheet.
|
|
6
|
+
*
|
|
7
|
+
* Drop it into the deals screen's Quotes tab: `<DealsScreen renderQuoteActions={(deal) => <CreateQuoteFromDeal dealId={deal.id} />} />`.
|
|
8
|
+
* It cannot live in the deals block because quotes depends on deals, not the other way round.
|
|
9
|
+
*
|
|
10
|
+
* SDK operations (through QuoteBuilder and QuoteViewer): listQuotableProducts, searchQuoteContacts, createQuote and
|
|
11
|
+
* updateQuotePatch (a draft emails nobody, so it saves in one click), then getQuote and the viewer's actions. Sending,
|
|
12
|
+
* editing a sent quote as a draft, deleting and every lifecycle change stay behind ConfirmSpendDialog in QuoteViewer.
|
|
13
|
+
*/
|
|
14
|
+
import { useState } from "react";
|
|
15
|
+
import { FileText } from "lucide-react";
|
|
16
|
+
import { Button } from "@graph8/sdk/ui";
|
|
17
|
+
import { FormSheet } from "@/g8/components/screen/form-sheet";
|
|
18
|
+
import { QuoteBuilder } from "./quote-builder";
|
|
19
|
+
import { QuoteViewer } from "./quote-viewer";
|
|
20
|
+
|
|
21
|
+
export interface CreateQuoteFromDealProps {
|
|
22
|
+
dealId: string;
|
|
23
|
+
/** Company (mashup company id) used to find signer contacts. */
|
|
24
|
+
companyId?: number;
|
|
25
|
+
label?: string;
|
|
26
|
+
/** Called with the new quote's id once the draft is saved. */
|
|
27
|
+
onCreated?: (quoteId: string) => void;
|
|
28
|
+
className?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function CreateQuoteFromDeal({ dealId, companyId, label = "Create quote from this deal", onCreated, className }: CreateQuoteFromDealProps) {
|
|
32
|
+
const [open, setOpen] = useState(false);
|
|
33
|
+
const [quoteId, setQuoteId] = useState<string | null>(null);
|
|
34
|
+
const [editing, setEditing] = useState(false);
|
|
35
|
+
const close = (next: boolean) => { setOpen(next); if (!next) { setQuoteId(null); setEditing(false); } };
|
|
36
|
+
return (
|
|
37
|
+
<>
|
|
38
|
+
<Button className={className} size="sm" onClick={() => setOpen(true)}><FileText className="size-4" aria-hidden />{label}</Button>
|
|
39
|
+
<FormSheet open={open} onOpenChange={close} title={quoteId ? "Quote" : "New quote"} description={quoteId ? undefined : "The quote is attached to this deal."} width="xl">
|
|
40
|
+
{open ? (quoteId && editing
|
|
41
|
+
? <QuoteBuilder key={`edit-${quoteId}`} quoteId={quoteId} onSaved={(id) => { setQuoteId(id); setEditing(false); }} />
|
|
42
|
+
: quoteId
|
|
43
|
+
? <QuoteViewer key={quoteId} quoteId={quoteId} onEdit={() => setEditing(true)} onOpenQuote={setQuoteId} onDeleted={() => close(false)} />
|
|
44
|
+
: <QuoteBuilder key={dealId} dealId={dealId} companyId={companyId} onSaved={(id) => { setQuoteId(id); onCreated?.(id); }} />) : null}
|
|
45
|
+
</FormSheet>
|
|
46
|
+
</>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ManualProducts: the org's own product catalogue for quoting without Stripe: list (including inactive and not-quotable),
|
|
5
|
+
* add, edit and retire.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* 1. listManualProducts GET /manual-products?include_inactive&include_non_quotable&page&limit read, paged `{ items, total, page, limit }`
|
|
9
|
+
* 2. createManualProduct POST /manual-products write (org admin); amounts are cents, so the form takes a price and converts
|
|
10
|
+
* 3. updateManualProduct PATCH /manual-products/{id} write (org admin); only the fields that changed are sent
|
|
11
|
+
* 4. deleteManualProduct DELETE /manual-products/{id} DESTRUCTIVE (org admin) -> ConfirmSpendDialog; a soft delete
|
|
12
|
+
*
|
|
13
|
+
* Only a product that is active AND quotable appears in the quote builder's picker; the table shows both flags. Retiring
|
|
14
|
+
* a product removes it from the catalogue and the picker, and the line items of quotes that already used it keep it.
|
|
15
|
+
* Create and edit store data and contact nobody, so they run on one click.
|
|
16
|
+
*/
|
|
17
|
+
import { useState } from "react";
|
|
18
|
+
import { Package, Pencil, Plus, Trash2 } from "lucide-react";
|
|
19
|
+
import { Button, Input, Label, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Textarea } from "@graph8/sdk/ui";
|
|
20
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
21
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
22
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
23
|
+
import { FormSheet } from "@/g8/components/screen/form-sheet";
|
|
24
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
25
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
26
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
27
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
28
|
+
import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
29
|
+
import { body, money, text, type Rec } from "../lib/quotes";
|
|
30
|
+
|
|
31
|
+
const FREQUENCIES: Array<[string, string]> = [["one_time", "One time"], ["month", "Monthly"], ["quarter", "Quarterly"], ["year", "Yearly"]];
|
|
32
|
+
const PAGE_SIZE = 25;
|
|
33
|
+
|
|
34
|
+
export interface ManualProductDraft { name: string; description: string; price: string; currency: string; billing_frequency: string; category: string; active: boolean; quotable: boolean }
|
|
35
|
+
const blank = (): ManualProductDraft => ({ name: "", description: "", price: "", currency: "USD", billing_frequency: "one_time", category: "", active: true, quotable: true });
|
|
36
|
+
const fromRow = (r: Rec): ManualProductDraft => ({
|
|
37
|
+
name: text(r.name), description: text(r.description), price: typeof r.default_unit_amount === "number" ? (r.default_unit_amount / 100).toFixed(2) : "", currency: text(r.currency, "USD").toUpperCase(),
|
|
38
|
+
billing_frequency: text(r.billing_frequency, "one_time"), category: text(r.category), active: r.active !== false, quotable: r.quotable !== false,
|
|
39
|
+
});
|
|
40
|
+
const cents = (price: string): number | null => {
|
|
41
|
+
const n = Number(price);
|
|
42
|
+
return price.trim() !== "" && Number.isFinite(n) && n >= 0 && n <= 10_000_000 ? Math.round(n * 100) : null;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export function draftProblems(d: ManualProductDraft): string[] {
|
|
46
|
+
const out: string[] = [];
|
|
47
|
+
if (!d.name.trim()) out.push("Give the product a name.");
|
|
48
|
+
if (cents(d.price) == null) out.push("Enter a price of 0 or more.");
|
|
49
|
+
if (!/^[A-Za-z]{3}$/.test(d.currency.trim())) out.push("The currency is a 3-letter code such as USD.");
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The PATCH body: only fields that differ from the stored product. A cleared description or category is sent as null. */
|
|
54
|
+
export function productChanges(was: ManualProductDraft, now: ManualProductDraft): Rec {
|
|
55
|
+
const out: Rec = {};
|
|
56
|
+
if (now.name.trim() !== was.name.trim()) out.name = now.name.trim();
|
|
57
|
+
if (now.description.trim() !== was.description.trim()) out.description = now.description.trim() || null;
|
|
58
|
+
if (now.category.trim() !== was.category.trim()) out.category = now.category.trim() || null;
|
|
59
|
+
if (cents(now.price) !== cents(was.price)) out.default_unit_amount = cents(now.price);
|
|
60
|
+
if (now.currency.trim().toUpperCase() !== was.currency.trim().toUpperCase()) out.currency = now.currency.trim().toUpperCase();
|
|
61
|
+
if (now.billing_frequency !== was.billing_frequency) out.billing_frequency = now.billing_frequency;
|
|
62
|
+
if (now.active !== was.active) out.active = now.active;
|
|
63
|
+
if (now.quotable !== was.quotable) out.quotable = now.quotable;
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function ManualProducts({ className, refreshKey }: { className?: string; refreshKey?: number }) {
|
|
68
|
+
const [page, setPage] = useState(1);
|
|
69
|
+
const q = useG8Query((signal) => g8Get("/manual-products", { include_inactive: true, include_non_quotable: true, page, limit: PAGE_SIZE }, { signal }), [page, refreshKey]);
|
|
70
|
+
const data = body(q.data);
|
|
71
|
+
const rows = (Array.isArray(data.items) ? data.items : []) as Rec[];
|
|
72
|
+
const total = typeof data.total === "number" ? data.total : rows.length;
|
|
73
|
+
const [editing, setEditing] = useState<{ row: Rec | null; was: ManualProductDraft } | null>(null);
|
|
74
|
+
const [draft, setDraft] = useState<ManualProductDraft>(blank());
|
|
75
|
+
const [tried, setTried] = useState(false);
|
|
76
|
+
const [retiring, setRetiring] = useState<Rec | null>(null);
|
|
77
|
+
const problems = draftProblems(draft);
|
|
78
|
+
const changes = editing?.row ? productChanges(editing.was, draft) : {};
|
|
79
|
+
|
|
80
|
+
const open = (row: Rec | null) => { const d = row ? fromRow(row) : blank(); setDraft(d); setTried(false); setEditing({ row, was: d }); };
|
|
81
|
+
const save = useG8Mutation(async () => {
|
|
82
|
+
if (editing?.row) return g8Patch(`/manual-products/${encodeURIComponent(text(editing.row.id))}`, changes);
|
|
83
|
+
return g8Post("/manual-products", {
|
|
84
|
+
name: draft.name.trim(), default_unit_amount: cents(draft.price), currency: draft.currency.trim().toUpperCase(), billing_frequency: draft.billing_frequency,
|
|
85
|
+
active: draft.active, quotable: draft.quotable, ...(draft.description.trim() ? { description: draft.description.trim() } : {}), ...(draft.category.trim() ? { category: draft.category.trim() } : {}),
|
|
86
|
+
});
|
|
87
|
+
}, { onSuccess: () => { setEditing(null); void q.refetch(); } });
|
|
88
|
+
const retire = useG8Mutation((id: string) => g8Delete(`/manual-products/${encodeURIComponent(id)}`), { onSuccess: () => { setRetiring(null); void q.refetch(); } });
|
|
89
|
+
const submit = () => { setTried(true); if (problems.length === 0 && (!editing?.row || Object.keys(changes).length > 0)) void save.mutate(undefined); };
|
|
90
|
+
const up = (patch: Partial<ManualProductDraft>) => setDraft({ ...draft, ...patch });
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<SectionCard className={className} title="Manual products" icon={<Package />} description="Your own catalogue for quoting without Stripe. Needs an organization admin to change."
|
|
94
|
+
actions={<Button size="sm" onClick={() => open(null)}><Plus className="size-4" aria-hidden />New product</Button>} flush
|
|
95
|
+
footer={<div className="flex items-center justify-between"><span>{total} {total === 1 ? "product" : "products"}</span><span className="flex gap-2"><Button size="sm" variant="outline" disabled={page === 1} onClick={() => setPage(page - 1)}>Previous</Button><Button size="sm" variant="outline" disabled={page * PAGE_SIZE >= total} onClick={() => setPage(page + 1)}>Next</Button></span></div>}>
|
|
96
|
+
{q.error ? <div className="p-4"><ApiError error={q.error} title="Could not load manual products" onRetry={() => void q.refetch()} /></div> : null}
|
|
97
|
+
{q.loading && !q.data ? <div className="p-4"><Skeleton className="h-20 w-full" aria-label="Loading manual products" /></div> : !q.error && rows.length === 0 ? (
|
|
98
|
+
<EmptyState compact title="No manual products" description="Add the products you sell, with a price, and they appear in the quote builder." action={<Button onClick={() => open(null)}>New product</Button>} />
|
|
99
|
+
) : rows.length > 0 ? (
|
|
100
|
+
<Table>
|
|
101
|
+
<TableHeader><TableRow><TableHead>Product</TableHead><TableHead>Billing</TableHead><TableHead className="text-right">Price</TableHead><TableHead>In quotes</TableHead><TableHead><span className="sr-only">Actions</span></TableHead></TableRow></TableHeader>
|
|
102
|
+
<TableBody>
|
|
103
|
+
{rows.map((r) => (
|
|
104
|
+
<TableRow key={text(r.id)}>
|
|
105
|
+
<TableCell><span className="font-medium">{text(r.name)}</span><div className="text-xs text-muted-foreground">{text(r.category)}</div></TableCell>
|
|
106
|
+
<TableCell>{(FREQUENCIES.find(([v]) => v === r.billing_frequency)?.[1]) ?? text(r.billing_frequency)}</TableCell>
|
|
107
|
+
<TableCell className="text-right tabular-nums">{money(r.default_unit_amount, text(r.currency, "USD").toUpperCase())}</TableCell>
|
|
108
|
+
<TableCell>{r.active === false ? <StatusPill tone="neutral">Inactive</StatusPill> : r.quotable === false ? <StatusPill tone="warning">Not quotable</StatusPill> : <StatusPill tone="positive">Quotable</StatusPill>}</TableCell>
|
|
109
|
+
<TableCell className="whitespace-nowrap text-right">
|
|
110
|
+
<Button size="sm" variant="ghost" aria-label={`Edit ${text(r.name)}`} onClick={() => open(r)}><Pencil className="size-4" aria-hidden /></Button>
|
|
111
|
+
<Button size="sm" variant="ghost" aria-label={`Retire ${text(r.name)}`} onClick={() => setRetiring(r)}><Trash2 className="size-4" aria-hidden /></Button>
|
|
112
|
+
</TableCell>
|
|
113
|
+
</TableRow>
|
|
114
|
+
))}
|
|
115
|
+
</TableBody>
|
|
116
|
+
</Table>
|
|
117
|
+
) : null}
|
|
118
|
+
{retire.error ? <div className="p-4"><ApiError error={retire.error} title="Could not retire the product" /></div> : null}
|
|
119
|
+
|
|
120
|
+
<FormSheet open={editing != null} onOpenChange={(o) => { if (!o) setEditing(null); }} title={editing?.row ? "Edit product" : "New manual product"} onSubmit={submit}
|
|
121
|
+
footer={<><Button type="button" variant="outline" onClick={() => setEditing(null)}>Cancel</Button><Button type="submit" loading={save.loading} disabled={Boolean(editing?.row) && Object.keys(changes).length === 0}>{editing?.row ? "Save product" : "Add product"}</Button></>}>
|
|
122
|
+
<div className="space-y-1"><Label htmlFor="mp-name">Name</Label><Input id="mp-name" value={draft.name} maxLength={500} onChange={(e) => up({ name: e.target.value })} /></div>
|
|
123
|
+
<div className="space-y-1"><Label htmlFor="mp-desc">Description</Label><Textarea id="mp-desc" rows={3} value={draft.description} onChange={(e) => up({ description: e.target.value })} /></div>
|
|
124
|
+
<div className="grid grid-cols-2 gap-3">
|
|
125
|
+
<div className="space-y-1"><Label htmlFor="mp-price">Price</Label><Input id="mp-price" inputMode="decimal" value={draft.price} onChange={(e) => up({ price: e.target.value })} /></div>
|
|
126
|
+
<div className="space-y-1"><Label htmlFor="mp-cur">Currency</Label><Input id="mp-cur" maxLength={3} value={draft.currency} onChange={(e) => up({ currency: e.target.value })} /></div>
|
|
127
|
+
<div className="space-y-1"><Label htmlFor="mp-freq">Billing</Label>
|
|
128
|
+
<select id="mp-freq" className="h-9 w-full rounded-md border bg-background px-2 text-sm" value={draft.billing_frequency} onChange={(e) => up({ billing_frequency: e.target.value })}>{FREQUENCIES.map(([v, l]) => <option key={v} value={v}>{l}</option>)}</select></div>
|
|
129
|
+
<div className="space-y-1"><Label htmlFor="mp-cat">Category</Label><Input id="mp-cat" maxLength={100} value={draft.category} onChange={(e) => up({ category: e.target.value })} /></div>
|
|
130
|
+
</div>
|
|
131
|
+
<label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={draft.active} onChange={(e) => up({ active: e.target.checked })} />Active</label>
|
|
132
|
+
<label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={draft.quotable} onChange={(e) => up({ quotable: e.target.checked })} />Offer on quotes</label>
|
|
133
|
+
{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}
|
|
134
|
+
{save.error ? <ApiError error={save.error} title="Could not save the product" /> : null}
|
|
135
|
+
</FormSheet>
|
|
136
|
+
|
|
137
|
+
{retiring ? (
|
|
138
|
+
<ConfirmSpendDialog open tier="destructive" title={`Retire "${text(retiring.name)}"?`} confirmLabel="Retire product" affected={1} confirming={retire.loading}
|
|
139
|
+
description="The product leaves your catalogue and the quote builder's picker. Quotes that already include it keep the line item. To keep the product but stop offering it, switch off Active or Offer on quotes instead."
|
|
140
|
+
onConfirm={() => void retire.mutate(text(retiring.id))} onCancel={() => setRetiring(null)} />
|
|
141
|
+
) : null}
|
|
142
|
+
</SectionCard>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* QuoteBatchActions: archive, expire or revoke the quotes ticked in a list, in one request.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations, in order:
|
|
7
|
+
* 1. batchQuoteAction POST /quotes/batch { action: "archive" | "expire" | "revoke", quote_ids: [...] } external tier,
|
|
8
|
+
* DESTRUCTIVE in effect for expire and revoke (both final) -> ConfirmSpendDialog for every action
|
|
9
|
+
*
|
|
10
|
+
* The request is addressed by quote id only, never by a filter: the ids sent are exactly the ticked quotes that the action
|
|
11
|
+
* applies to (expire and revoke only apply to a quote that is out for signature, archive to one that is not archived), and
|
|
12
|
+
* that same array is what the confirm counts. Quotes the action does not apply to are left out and said so, not sent.
|
|
13
|
+
* The API answers a row per quote (`success`, `skipped`, `error`) and a 200 only means the batch ran, so the result is read
|
|
14
|
+
* row by row: "N of M changed", with the reason for each that did not. The caller refetches afterwards (`onDone`).
|
|
15
|
+
*/
|
|
16
|
+
import { useState } from "react";
|
|
17
|
+
import { Button } from "@graph8/sdk/ui";
|
|
18
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
19
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
20
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
21
|
+
import { g8Post } from "@/g8/lib/g8-client";
|
|
22
|
+
import { BATCH_MAX, BATCH_REASON, batchEligible, batchOutcomes, type BatchAction, type BatchOutcome } from "../lib/quote-admin";
|
|
23
|
+
import { text, type Rec } from "../lib/quotes";
|
|
24
|
+
|
|
25
|
+
export interface QuoteBatchActionsProps {
|
|
26
|
+
/** The ticked quotes, loaded (the list rows). */
|
|
27
|
+
selected: Rec[];
|
|
28
|
+
/** Called after the request, so the caller reloads the list and prunes its selection. */
|
|
29
|
+
onDone: () => void;
|
|
30
|
+
className?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const COPY: Record<BatchAction, { button: string; title: (n: number) => string; confirm: string; effect: string }> = {
|
|
34
|
+
archive: { button: "Archive", title: (n) => `Archive ${n} ${n === 1 ? "quote" : "quotes"}?`, confirm: "Archive quotes", effect: "The quotes leave the active list. Nothing is sent and each can be unarchived later." },
|
|
35
|
+
expire: { button: "Expire", title: (n) => `Expire ${n} ${n === 1 ? "quote" : "quotes"}?`, confirm: "Expire quotes", effect: "Each customer's link stops working. This is final: create a new quote to offer it again." },
|
|
36
|
+
revoke: { button: "Revoke", title: (n) => `Revoke ${n} ${n === 1 ? "quote" : "quotes"}?`, confirm: "Revoke quotes", effect: "Withdraws the quotes you already sent, so customers can no longer sign them. This is final." },
|
|
37
|
+
};
|
|
38
|
+
interface Job { action: BatchAction; ids: string[]; names: Record<string, string> }
|
|
39
|
+
const ACTIONS: BatchAction[] = ["archive", "expire", "revoke"];
|
|
40
|
+
|
|
41
|
+
export function QuoteBatchActions({ selected, onDone, className }: QuoteBatchActionsProps) {
|
|
42
|
+
const [pending, setPending] = useState<Job | null>(null);
|
|
43
|
+
const [outcome, setOutcome] = useState<{ action: BatchAction; asked: number; rows: BatchOutcome[]; names: Record<string, string> } | null>(null);
|
|
44
|
+
|
|
45
|
+
const run = useG8Mutation(async (job: Job) => ({ job, res: await g8Post("/quotes/batch", { action: job.action, quote_ids: job.ids }) }), {
|
|
46
|
+
invalidates: [["quotes"]],
|
|
47
|
+
onSuccess: ({ job, res }) => { setPending(null); setOutcome({ action: job.action, asked: job.ids.length, rows: batchOutcomes(res), names: job.names }); onDone(); },
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// The ids a click would send: filtered ONCE here, and this same array is both counted in the confirm and sent.
|
|
51
|
+
const idsFor = (action: BatchAction): string[] => selected.filter((r) => batchEligible(action, r)).map((r) => text(r.id)).filter(Boolean);
|
|
52
|
+
const open = (action: BatchAction) => { setOutcome(null); setPending({ action, ids: idsFor(action), names: Object.fromEntries(selected.map((r) => [text(r.id), text(r.title, text(r.quote_number, "Untitled quote"))])) }); };
|
|
53
|
+
|
|
54
|
+
const done = outcome ? outcome.rows.filter((r) => r.status === "success").length : 0;
|
|
55
|
+
const notDone = outcome ? outcome.rows.filter((r) => r.status !== "success") : [];
|
|
56
|
+
return (
|
|
57
|
+
<div className={className}>
|
|
58
|
+
{selected.length > 0 ? (
|
|
59
|
+
<div className="mb-2 flex flex-wrap items-center gap-2 rounded-md border border-border bg-muted/40 px-3 py-2 text-sm" role="group" aria-label="Batch actions">
|
|
60
|
+
<span className="font-medium tabular-nums">{selected.length} selected</span>
|
|
61
|
+
{ACTIONS.map((a) => {
|
|
62
|
+
const n = idsFor(a).length;
|
|
63
|
+
const tooMany = n > BATCH_MAX;
|
|
64
|
+
return <Button key={a} size="sm" variant="outline" disabled={n === 0 || tooMany} title={n === 0 ? `None of the selected quotes can be ${a === "archive" ? "archived" : a === "expire" ? "expired" : "revoked"}` : tooMany ? `At most ${BATCH_MAX} at a time` : undefined} onClick={() => open(a)}>{COPY[a].button} ({n})</Button>;
|
|
65
|
+
})}
|
|
66
|
+
</div>
|
|
67
|
+
) : null}
|
|
68
|
+
{run.error ? <ApiError error={run.error} title="The batch action failed" /> : null}
|
|
69
|
+
{outcome ? (
|
|
70
|
+
<div role="status" className="mb-2 rounded-md border border-border px-3 py-2 text-sm">
|
|
71
|
+
<p className="font-medium">{done} of {outcome.asked} {outcome.asked === 1 ? "quote" : "quotes"} {outcome.action === "archive" ? "archived" : outcome.action === "expire" ? "expired" : "revoked"}.</p>
|
|
72
|
+
{notDone.length > 0 ? (
|
|
73
|
+
<ul className="mt-1 list-disc pl-5 text-muted-foreground">
|
|
74
|
+
{notDone.map((r) => <li key={r.quote_id}>{outcome.names[r.quote_id] ?? r.quote_id}: {r.status === "skipped" ? `skipped (${(r.detail ?? "not applicable").replace(/_/g, " ")})` : `failed (${BATCH_REASON[r.detail ?? ""] ?? (r.detail ?? "error").replace(/_/g, " ")})`}</li>)}
|
|
75
|
+
</ul>
|
|
76
|
+
) : null}
|
|
77
|
+
</div>
|
|
78
|
+
) : null}
|
|
79
|
+
{pending ? (
|
|
80
|
+
<ConfirmSpendDialog open tier="destructive" title={COPY[pending.action].title(pending.ids.length)} confirmLabel={COPY[pending.action].confirm} affected={pending.ids.length} confirming={run.loading}
|
|
81
|
+
description={<div className="space-y-1"><p>{COPY[pending.action].effect}</p>{selected.length > pending.ids.length ? <p>{selected.length - pending.ids.length} of the {selected.length} selected {selected.length - pending.ids.length === 1 ? "quote does" : "quotes do"} not qualify and {selected.length - pending.ids.length === 1 ? "is" : "are"} left out.</p> : null}</div>}
|
|
82
|
+
onConfirm={() => void run.mutate(pending)} onCancel={() => setPending(null)} />
|
|
83
|
+
) : null}
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -178,7 +178,7 @@ export function QuoteBuilder({ quoteId, dealId, companyId, currency = "USD", onS
|
|
|
178
178
|
<div className="text-right text-sm" data-testid="totals">
|
|
179
179
|
<div>Subtotal {money(sub, form.currency)}</div>
|
|
180
180
|
<div>Tax {money(taxCents, form.currency)}</div>
|
|
181
|
-
<div className="font-
|
|
181
|
+
<div className="font-medium">Total {money(sub + taxCents, form.currency)}</div>
|
|
182
182
|
</div>
|
|
183
183
|
<details>
|
|
184
184
|
<summary className="cursor-pointer text-sm font-medium">Add a product</summary>
|