@graph8/sdk 0.362.0 → 0.373.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 +33 -10
- package/blocks/_foundation/README.md +78 -1
- package/blocks/_foundation/block.json +123 -6
- package/blocks/_foundation/components/api-error.tsx +9 -7
- package/blocks/_foundation/components/brand-provider.tsx +23 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
- package/blocks/_foundation/components/confirmed-action.tsx +90 -0
- package/blocks/_foundation/components/connect-graph8.tsx +280 -0
- 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 +2 -0
- package/blocks/_foundation/components/screen/record-link.tsx +76 -0
- package/blocks/_foundation/components/screen/split-view.tsx +1 -1
- package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
- package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
- package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
- package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
- package/blocks/_foundation/hooks/use-brand.ts +52 -0
- package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
- package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
- package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
- package/blocks/_foundation/hooks/use-paginated.ts +120 -0
- package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
- package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
- package/blocks/_foundation/lib/g8-cache.ts +454 -0
- package/blocks/_foundation/lib/g8-client.ts +1 -1
- package/blocks/_foundation/lib/paging.ts +57 -0
- package/blocks/_foundation/lib/plural.ts +23 -0
- package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
- package/blocks/_foundation/server/connect-route.ts +15 -0
- package/blocks/_foundation/server/connect.ts +127 -0
- package/blocks/_foundation/server/local-key.ts +70 -0
- package/blocks/_foundation/server/proxy.ts +103 -3
- package/blocks/_foundation/server/route.ts +16 -4
- package/blocks/agents/README.md +93 -2
- package/blocks/agents/block.json +343 -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 +29 -10
- package/blocks/agents/components/agent-conversations.tsx +143 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +4 -3
- package/blocks/agents/components/agent-operators.tsx +164 -0
- package/blocks/agents/components/agent-run-detail.tsx +119 -0
- package/blocks/agents/components/agent-runs.tsx +19 -10
- 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 +226 -0
- 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/README.md +13 -0
- package/blocks/analytics/block.json +45 -1
- package/blocks/analytics/components/analytics-screen.tsx +222 -0
- package/blocks/analytics/components/kpi-tiles.tsx +1 -1
- package/blocks/analytics/components/leaderboard.tsx +14 -7
- package/blocks/analytics/components/report-builder.tsx +38 -11
- package/blocks/analytics/components/report-detail.tsx +84 -0
- package/blocks/analytics/components/trend-chart.tsx +62 -0
- package/blocks/appointments/README.md +6 -0
- package/blocks/appointments/block.json +33 -2
- package/blocks/appointments/components/availability-editor.tsx +17 -5
- package/blocks/appointments/components/booking-page.tsx +2 -2
- package/blocks/appointments/components/event-type-editor.tsx +2 -1
- package/blocks/appointments/components/event-type-related.tsx +4 -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 +38 -18
- package/blocks/appointments/components/meetings-screen.tsx +7 -4
- package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
- package/blocks/appointments/components/scheduling-screen.tsx +2 -1
- package/blocks/block.schema.json +18 -0
- package/blocks/block.types.ts +17 -0
- package/blocks/campaigns/README.md +41 -11
- package/blocks/campaigns/block.json +105 -42
- package/blocks/campaigns/components/campaign-assets.tsx +17 -6
- package/blocks/campaigns/components/campaign-launch-step.tsx +114 -26
- package/blocks/campaigns/components/campaign-list.tsx +7 -3
- package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
- package/blocks/campaigns/components/campaign-wizard.tsx +52 -14
- package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
- package/blocks/campaigns/components/icp-detail.tsx +163 -0
- package/blocks/campaigns/components/icp-editor.tsx +22 -17
- package/blocks/campaigns/components/persona-detail.tsx +181 -0
- package/blocks/campaigns/components/persona-editor.tsx +19 -11
- package/blocks/campaigns/lib/campaigns-utils.ts +138 -5
- package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
- package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
- package/blocks/coach/README.md +10 -0
- package/blocks/coach/block.json +16 -1
- package/blocks/coach/components/call-scorecard.tsx +7 -7
- package/blocks/coach/components/coach-screen.tsx +222 -0
- 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 +161 -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/README.md +13 -0
- package/blocks/copilot/block.json +14 -1
- package/blocks/copilot/components/copilot-memory.tsx +6 -2
- package/blocks/copilot/components/copilot-panel.tsx +14 -5
- package/blocks/copilot/components/copilot-screen.tsx +148 -0
- package/blocks/copilot/components/copilot-team-chat.tsx +13 -6
- package/blocks/crm/README.md +34 -0
- package/blocks/crm/block.json +107 -2
- package/blocks/crm/components/companies-screen.tsx +359 -0
- package/blocks/crm/components/company-detail.tsx +10 -8
- package/blocks/crm/components/company-table.tsx +11 -4
- package/blocks/crm/components/contact-detail.tsx +10 -7
- package/blocks/crm/components/contact-lookup-link.tsx +44 -0
- package/blocks/crm/components/contact-table.tsx +10 -3
- package/blocks/crm/components/contacts-screen.tsx +377 -0
- package/blocks/crm/components/dedupe-review.tsx +13 -6
- package/blocks/crm/components/import-wizard.tsx +7 -9
- package/blocks/crm/components/list-select.tsx +20 -0
- package/blocks/crm/components/notes-panel.tsx +4 -4
- package/blocks/crm/components/record-engagement-tab.tsx +103 -0
- package/blocks/crm/components/record-form.tsx +6 -4
- package/blocks/crm/components/task-list.tsx +4 -4
- package/blocks/crm/components/tasks-screen.tsx +399 -0
- package/blocks/crm/hooks/use-all-lists.ts +18 -0
- package/blocks/crm/hooks/use-custom-fields.ts +6 -1
- package/blocks/crm/hooks/use-page-query.ts +9 -1
- package/blocks/crm/lib/crm-types.ts +26 -2
- package/blocks/crm/lib/read-lists.ts +16 -0
- package/blocks/crm/lib/screen-parts.tsx +105 -0
- package/blocks/deals/README.md +24 -6
- package/blocks/deals/block.json +160 -5
- package/blocks/deals/components/close-deal-dialog.tsx +169 -0
- package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
- package/blocks/deals/components/deal-detail.tsx +25 -13
- 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 +31 -10
- package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
- package/blocks/deals/components/deals-screen.tsx +66 -26
- 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 +5 -5
- package/blocks/deals/hooks/use-pipeline-deals.ts +21 -4
- package/blocks/deals/hooks/use-pipelines.ts +1 -1
- package/blocks/deals/lib/deal-extras.ts +156 -0
- package/blocks/dialer/README.md +45 -4
- package/blocks/dialer/block.json +84 -3
- package/blocks/dialer/components/call-contact-button.tsx +269 -0
- package/blocks/dialer/components/call-log-screen.tsx +208 -0
- package/blocks/dialer/components/call-log.tsx +23 -7
- package/blocks/dialer/components/call-queue.tsx +4 -4
- package/blocks/dialer/components/dialer-screen.tsx +361 -0
- package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
- package/blocks/dialer/components/disposition-panel.tsx +1 -1
- package/blocks/dialer/components/live-calls-table.tsx +7 -3
- package/blocks/dialer/components/record-calls-tab.tsx +101 -0
- package/blocks/dialer/components/session-start-form.tsx +61 -12
- package/blocks/dialer/components/softphone.tsx +4 -2
- package/blocks/enrichment/README.md +29 -7
- package/blocks/enrichment/block.json +49 -9
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +83 -29
- package/blocks/enrichment/components/email-verify-badge.tsx +31 -11
- package/blocks/enrichment/components/enrich-button.tsx +32 -9
- package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
- package/blocks/enrichment/components/enrichment-screen.tsx +341 -0
- package/blocks/enrichment/components/waterfall-config.tsx +66 -17
- package/blocks/enrichment/lib/enrichment-types.ts +39 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
- package/blocks/forms/README.md +8 -0
- package/blocks/forms/block.json +139 -18
- 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 +4 -2
- package/blocks/forms/components/form-embed.tsx +4 -2
- 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 +56 -19
- 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 +17 -6
- package/blocks/forms/lib/forms-extra.ts +67 -0
- package/blocks/inbox/README.md +32 -3
- package/blocks/inbox/block.json +29 -2
- package/blocks/inbox/components/inbox-list.tsx +7 -3
- package/blocks/inbox/components/inbox-screen.tsx +294 -27
- package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
- package/blocks/inbox/components/reply-composer.tsx +65 -9
- package/blocks/inbox/components/tag-and-assign.tsx +4 -2
- package/blocks/inbox/components/thread-badge.tsx +2 -2
- package/blocks/inbox/components/thread-view.tsx +1 -1
- package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
- package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
- package/blocks/inbox/lib/inbox-types.ts +47 -1
- package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
- package/blocks/integrations/README.md +63 -1
- package/blocks/integrations/block.json +234 -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 +119 -0
- 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 +13 -8
- package/blocks/integrations/components/integrations-screen.tsx +152 -0
- 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 +11 -9
- package/blocks/integrations/components/sync-triggers.tsx +134 -0
- package/blocks/integrations/components/webhook-detail.tsx +179 -0
- package/blocks/integrations/components/webhook-manager.tsx +43 -25
- 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 +11 -1
- package/blocks/integrations/lib/load-more.tsx +141 -0
- package/blocks/intent/README.md +14 -0
- package/blocks/intent/block.json +27 -1
- package/blocks/intent/components/account-intent-score.tsx +5 -5
- package/blocks/intent/components/intent-feed.tsx +13 -10
- package/blocks/intent/components/intent-keyword-manager.tsx +16 -12
- package/blocks/intent/components/intent-screen.tsx +265 -0
- package/blocks/intent/components/keyword-detail.tsx +124 -0
- package/blocks/intent/lib/intent-utils.ts +4 -4
- package/blocks/lifecycle/block.json +13 -8
- 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/lifecycle/components/lifecycle-board.tsx +3 -1
- package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
- package/blocks/lifecycle/lib/lifecycle.ts +1 -1
- package/blocks/lists/README.md +20 -1
- package/blocks/lists/block.json +69 -3
- package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
- package/blocks/lists/components/add-to-list-button.tsx +20 -9
- package/blocks/lists/components/list-picker.tsx +17 -3
- package/blocks/lists/components/list-table.tsx +12 -6
- package/blocks/lists/components/lists-screen.tsx +346 -0
- package/blocks/lists/components/record-lists-tab.tsx +99 -0
- package/blocks/lists/components/suppression-manager.tsx +33 -8
- package/blocks/lists/components/suppressions-screen.tsx +264 -0
- package/blocks/mailboxes/README.md +17 -0
- package/blocks/mailboxes/block.json +69 -17
- package/blocks/mailboxes/components/connect-mailbox.tsx +32 -21
- package/blocks/mailboxes/components/domain-detail.tsx +148 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +20 -14
- package/blocks/mailboxes/components/mailbox-detail.tsx +157 -0
- package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
- package/blocks/mailboxes/components/mailboxes-screen.tsx +228 -0
- package/blocks/mailboxes/components/warmup-health.tsx +24 -22
- package/blocks/mailboxes/lib/mailboxes-utils.ts +26 -5
- package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
- package/blocks/newsletter/README.md +12 -0
- package/blocks/newsletter/block.json +157 -24
- package/blocks/newsletter/components/issue-scheduler.tsx +4 -3
- 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 +39 -14
- 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/block.json +28 -14
- package/blocks/nurture/components/nurture-builder.tsx +13 -3
- package/blocks/nurture/components/nurture-enroll.tsx +12 -10
- package/blocks/nurture/components/nurture-form.tsx +27 -10
- package/blocks/nurture/components/nurture-screen.tsx +26 -11
- package/blocks/nurture/components/nurture-stats.tsx +5 -4
- package/blocks/nurture/lib/nurture-types.ts +1 -1
- package/blocks/objects/README.md +18 -1
- package/blocks/objects/block.json +42 -21
- package/blocks/objects/components/attribute-field-input.tsx +14 -1
- package/blocks/objects/components/object-not-found.tsx +3 -1
- package/blocks/objects/components/object-record-detail.tsx +10 -2
- package/blocks/objects/components/object-schema-editor.tsx +4 -2
- package/blocks/objects/components/object-table.tsx +13 -3
- package/blocks/objects/components/objects-screen.tsx +147 -0
- package/blocks/objects/components/record-roles-panel.tsx +15 -5
- package/blocks/objects/lib/object-types.ts +1 -1
- package/blocks/pages/README.md +14 -0
- package/blocks/pages/block.json +140 -23
- package/blocks/pages/components/brand-kit-editor.tsx +5 -2
- package/blocks/pages/components/content-library.tsx +9 -4
- 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-list.tsx +2 -1
- 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/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/quotes/README.md +13 -0
- package/blocks/quotes/block.json +65 -13
- package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
- package/blocks/quotes/components/product-picker.tsx +3 -1
- package/blocks/quotes/components/quote-builder.tsx +1 -1
- package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
- package/blocks/quotes/components/quote-list.tsx +5 -3
- package/blocks/quotes/components/quote-related.tsx +8 -4
- package/blocks/quotes/components/quote-viewer.tsx +7 -2
- package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
- package/blocks/radar/README.md +10 -0
- package/blocks/radar/block.json +41 -1
- package/blocks/radar/components/competitor-battle-card.tsx +1 -1
- package/blocks/radar/components/competitor-detail.tsx +117 -0
- package/blocks/radar/components/competitor-tracker.tsx +3 -1
- package/blocks/radar/components/radar-overview.tsx +4 -4
- package/blocks/radar/components/radar-report.tsx +4 -2
- package/blocks/radar/components/radar-screen.tsx +214 -0
- package/blocks/radar/components/report-detail.tsx +104 -0
- package/blocks/search/README.md +10 -0
- package/blocks/search/block.json +16 -1
- package/blocks/search/components/filter-builder.tsx +3 -3
- package/blocks/search/components/prospect-results.tsx +5 -3
- package/blocks/search/components/prospect-search-screen.tsx +235 -0
- package/blocks/search/components/saved-searches.tsx +4 -1
- package/blocks/search/hooks/use-filter-options.ts +20 -6
- package/blocks/search/lib/search-types.ts +2 -0
- package/blocks/sequencer/README.md +55 -9
- package/blocks/sequencer/block.json +126 -19
- package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +11 -5
- package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
- package/blocks/sequencer/components/sequence-builder.tsx +224 -60
- package/blocks/sequencer/components/sequence-detail.tsx +261 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +116 -0
- package/blocks/sequencer/components/sequence-list.tsx +34 -9
- package/blocks/sequencer/components/sequence-stats.tsx +11 -10
- package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
- package/blocks/sequencer/components/sequences-screen.tsx +162 -0
- package/blocks/sequencer/components/step-editor.tsx +14 -12
- package/blocks/sequencer/lib/sequencer-utils.ts +133 -10
- package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
- package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
- package/blocks/social-ads/README.md +19 -0
- package/blocks/social-ads/block.json +233 -1
- package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
- package/blocks/social-ads/components/ads-screen.tsx +152 -0
- package/blocks/social-ads/components/audience-sync-detail.tsx +194 -0
- package/blocks/social-ads/components/audience-sync.tsx +17 -9
- package/blocks/social-ads/components/campaign-ads.tsx +166 -0
- package/blocks/social-ads/components/campaign-detail.tsx +138 -0
- 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 +155 -0
- package/blocks/social-ads/components/creative-gallery.tsx +35 -7
- 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 +7 -3
- package/blocks/social-ads/components/post-detail.tsx +163 -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-post-list.tsx +11 -3
- package/blocks/social-ads/components/social-screen.tsx +187 -0
- 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/README.md +10 -0
- package/blocks/visitors/block.json +25 -1
- package/blocks/visitors/components/snippet-install.tsx +21 -10
- package/blocks/visitors/components/traffic-chart.tsx +4 -4
- package/blocks/visitors/components/visitor-company-card.tsx +26 -16
- package/blocks/visitors/components/visitor-detail.tsx +105 -0
- package/blocks/visitors/components/visitor-feed.tsx +16 -7
- package/blocks/visitors/components/visitors-screen.tsx +166 -0
- package/blocks/webchat/README.md +10 -0
- package/blocks/webchat/block.json +21 -7
- package/blocks/webchat/components/chat-widget-config.tsx +8 -5
- package/blocks/webchat/components/conversation-list.tsx +8 -4
- package/blocks/webchat/components/webchat-screen.tsx +245 -0
- package/blocks/workbench/README.md +11 -0
- package/blocks/workbench/block.json +48 -1
- package/blocks/workbench/components/column-recipe-editor.tsx +5 -3
- package/blocks/workbench/components/run-progress.tsx +1 -1
- package/blocks/workbench/components/workbench-grid.tsx +10 -4
- package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
- package/blocks/workbench/components/workbench-screen.tsx +202 -0
- package/blocks/workbench/components/workbench-sources.tsx +194 -0
- package/blocks/workbench/components/workbench-table-list.tsx +14 -2
- package/blocks/workflows/README.md +13 -0
- package/blocks/workflows/block.json +28 -9
- package/blocks/workflows/components/run-history.tsx +31 -9
- package/blocks/workflows/components/trigger-config.tsx +3 -1
- package/blocks/workflows/components/workflow-editor.tsx +4 -2
- package/blocks/workflows/components/workflow-list.tsx +13 -6
- package/blocks/workflows/components/workflows-screen.tsx +193 -0
- package/dist/cli.js +3109 -1977
- package/dist/errors.d.mts +34 -0
- package/dist/errors.d.ts +34 -0
- package/dist/errors.js +46 -0
- package/dist/errors.js.map +1 -0
- package/dist/errors.mjs +21 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/index.browser.js +11 -5
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +11 -5
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +164 -25
- package/dist/index.d.ts +164 -25
- package/dist/index.js +11 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +11 -5
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +10 -4
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +10 -4
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +142 -5
- package/dist/react.d.ts +142 -5
- package/dist/react.js +10 -4
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +10 -4
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/add.d.ts +11 -4
- package/dist/types/blocks/brand.d.ts +122 -0
- package/dist/types/blocks/compose.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +27 -20
- package/dist/types/blocks/doctor.d.ts +6 -1
- package/dist/types/blocks/flows.d.ts +7 -1
- package/dist/types/blocks/navfile.d.ts +35 -0
- package/dist/types/blocks/orgBrand.d.ts +34 -0
- package/dist/types/blocks/paths.d.ts +4 -0
- package/dist/types/blocks/prefixes.d.ts +37 -0
- package/dist/types/blocks/registry.d.ts +17 -0
- package/dist/types/blocks/screens.d.ts +53 -0
- package/dist/types/cli.d.ts +5 -0
- package/dist/types/errors.d.ts +33 -0
- package/dist/types/http.d.ts +2 -18
- package/dist/types/version.d.ts +2 -2
- package/docs/agents.md +15 -4
- package/docs/api/README.md +2 -2
- package/docs/api/by-path.md +1 -0
- package/docs/api/desk.d.ts +98 -2
- package/docs/api/desk.md +123 -1
- package/docs/api/inbox.d.ts +10 -2
- package/docs/api/inbox.md +10 -2
- package/docs/api/seo.d.ts +19 -1
- package/docs/api/seo.md +24 -1
- package/package.json +16 -2
- package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
- package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
- package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
- package/stacks/6sense.json +55 -17
- package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
- package/stacks/apollo/pages/companies.tsx.tmpl +33 -0
- package/stacks/apollo/pages/contacts.tsx.tmpl +51 -0
- 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 +12 -0
- package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
- package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
- package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
- package/stacks/apollo.json +147 -37
- 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 +64 -0
- package/stacks/clay/pages/companies.tsx.tmpl +31 -0
- package/stacks/clay/pages/contacts.tsx.tmpl +31 -0
- package/stacks/clay/pages/lists.tsx.tmpl +12 -0
- package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
- package/stacks/clay.json +73 -27
- 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 +32 -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 +98 -35
- package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
- package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
- package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
- package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
- package/stacks/hootsuite.json +104 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +26 -0
- package/stacks/hubspot/pages/contacts.tsx.tmpl +45 -0
- package/stacks/hubspot/pages/deals.tsx.tmpl +20 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +103 -0
- 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/quotes.tsx.tmpl +11 -0
- package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
- package/stacks/hubspot.json +181 -64
- 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 +52 -20
- package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
- package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
- package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
- package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
- package/stacks/mailchimp.json +68 -34
- package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
- package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
- package/stacks/outreach/pages/contacts.tsx.tmpl +46 -0
- 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 +12 -0
- package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
- package/stacks/outreach.json +106 -25
- package/stacks/pipedrive/pages/companies.tsx.tmpl +23 -0
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +27 -0
- package/stacks/pipedrive/pages/deals.tsx.tmpl +20 -0
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +103 -0
- package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
- package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
- package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
- package/stacks/pipedrive.json +117 -0
- package/stacks/salesforce/pages/companies.tsx.tmpl +26 -0
- package/stacks/salesforce/pages/contacts.tsx.tmpl +32 -0
- package/stacks/salesforce/pages/deals.tsx.tmpl +20 -0
- package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
- package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
- package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
- package/stacks/salesforce.json +85 -31
- package/stacks/zoominfo/pages/companies.tsx.tmpl +31 -0
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +31 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
- package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +115 -0
- package/styles/styles.css +1 -1
- package/templates/next-app/README.md.tmpl +40 -9
- package/templates/next-app/app/globals.css.tmpl +349 -64
- package/templates/next-app/app/layout.tsx.tmpl +7 -2
- package/templates/next-app/components/app-shell.tsx.tmpl +104 -88
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
- package/templates/next-app/dot-gitignore.tmpl +2 -0
- package/templates/next-app/package.json.tmpl +1 -1
|
@@ -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
|
+
}
|
|
@@ -22,6 +22,7 @@ import { ApiError } from "@/g8/components/api-error";
|
|
|
22
22
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
23
23
|
import { body, frequencyOf, lineFromProduct, money, text } from "../lib/quotes";
|
|
24
24
|
import type { LineItem, ProductPrice, QuotableProduct } from "../lib/quotes";
|
|
25
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
25
26
|
|
|
26
27
|
export interface ProductPickerProps {
|
|
27
28
|
/** Only offer prices in this currency (the quote's currency). */
|
|
@@ -34,6 +35,7 @@ export interface ProductPickerProps {
|
|
|
34
35
|
const intervalLabel = (p: ProductPrice) => (frequencyOf(p.recurring_interval) === "one_time" ? "one time" : `per ${p.recurring_interval}`);
|
|
35
36
|
|
|
36
37
|
export function ProductPicker({ currency, onPick, pageSize = 25, className }: ProductPickerProps) {
|
|
38
|
+
const brand = useBrand();
|
|
37
39
|
const [page, setPage] = useState(1);
|
|
38
40
|
const [search, setSearch] = useState("");
|
|
39
41
|
const q = useG8Query((signal) => g8Get("/quotable-products", { currency: currency?.toUpperCase(), page, limit: pageSize }, { signal }), [currency, page, pageSize]);
|
|
@@ -49,7 +51,7 @@ export function ProductPicker({ currency, onPick, pageSize = 25, className }: Pr
|
|
|
49
51
|
{q.error ? <ApiError error={q.error} onRetry={() => void q.refetch()} /> : null}
|
|
50
52
|
{q.loading && !q.data ? <div className="space-y-2" aria-busy="true">{[0, 1, 2].map((i) => <Skeleton key={i} className="h-12 w-full" />)}</div> : null}
|
|
51
53
|
{!q.loading && !q.error && shown.length === 0 ? (
|
|
52
|
-
<EmptyState title="No quotable products" description=
|
|
54
|
+
<EmptyState title="No quotable products" description={`Mark a Stripe product as quotable, or add a manual product, in ${brand.linkLabel}'s quote settings.`} />
|
|
53
55
|
) : null}
|
|
54
56
|
<ul className="divide-y rounded-md border">
|
|
55
57
|
{shown.map((p) => (
|
|
@@ -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>
|
|
@@ -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'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,7 +4,7 @@
|
|
|
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
9
|
* Read-only: no billable, external or destructive call, so no confirmation. Pass `dealId` or `companyId` to show the
|
|
10
10
|
* quotes of one deal or company (for a tab on a deal page). `onOpen` receives the quote id for QuoteViewer.
|
|
@@ -36,10 +36,11 @@ const STATUSES = ["", "draft", "sent", "viewed", "accepted", "declined", "voided
|
|
|
36
36
|
|
|
37
37
|
export function QuoteList({ dealId, companyId, onOpen, onNew, pageSize = 25, refreshKey, selectedId, className }: QuoteListProps) {
|
|
38
38
|
const [status, setStatus] = useState("");
|
|
39
|
+
const [archived, setArchived] = useState(false);
|
|
39
40
|
const [page, setPage] = useState(1);
|
|
40
41
|
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],
|
|
42
|
+
(signal) => g8Get("/quotes", { status: status || undefined, include_archived: archived || undefined, deal_id: dealId, mashup_company_id: companyId, page, limit: pageSize }, { signal }),
|
|
43
|
+
[status, archived, dealId, companyId, page, pageSize, refreshKey],
|
|
43
44
|
);
|
|
44
45
|
const data = body(q.data);
|
|
45
46
|
const rows = (Array.isArray(data.items) ? data.items : []) as Rec[];
|
|
@@ -51,6 +52,7 @@ export function QuoteList({ dealId, companyId, onOpen, onNew, pageSize = 25, ref
|
|
|
51
52
|
<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
53
|
{STATUSES.map((s) => <option key={s} value={s}>{s ? statusLabel(s) : "All statuses"}</option>)}
|
|
53
54
|
</select>
|
|
55
|
+
<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
56
|
{onNew ? <Button className="ml-auto" onClick={onNew}>New quote</Button> : null}
|
|
55
57
|
</div>
|
|
56
58
|
{q.error ? <ApiError error={q.error} onRetry={() => void q.refetch()} /> : null}
|
|
@@ -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
|
|
@@ -27,7 +27,10 @@ import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
|
27
27
|
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
|
+
import { rowActivationProps } from "@/g8/hooks/use-row-activation";
|
|
31
|
+
import { CreateQuoteFromDeal } from "./create-quote-from-deal";
|
|
30
32
|
import { body, frequencyOf, money, text, type QuotableProduct, type ProductPrice } from "../lib/quotes";
|
|
33
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
31
34
|
|
|
32
35
|
const productKey = (p: QuotableProduct) => p.stripe_product_id ?? p.manual_product_id ?? p.name;
|
|
33
36
|
const cadence = (p: ProductPrice) => (frequencyOf(p.recurring_interval) === "one_time" ? "One time" : `Per ${p.recurring_interval}`);
|
|
@@ -39,6 +42,7 @@ export interface ProductCatalogProps {
|
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
export function ProductCatalog({ onStartQuote, pageSize = 50 }: ProductCatalogProps) {
|
|
45
|
+
const brand = useBrand();
|
|
42
46
|
const [selected, setSelected] = useUrlState("product");
|
|
43
47
|
const [mode, setMode] = useUrlState("view");
|
|
44
48
|
const q = useG8Query((signal) => g8Get("/quotable-products", { page: 1, limit: pageSize }, { signal }), [pageSize]);
|
|
@@ -48,13 +52,13 @@ export function ProductCatalog({ onStartQuote, pageSize = 50 }: ProductCatalogPr
|
|
|
48
52
|
const close = () => { setSelected(null); setMode(null); };
|
|
49
53
|
|
|
50
54
|
const list = q.error ? <ApiError error={q.error} title="Could not load products" onRetry={() => void q.refetch()} /> : q.loading && !q.data ? <Skeleton className="h-40 w-full" aria-label="Loading products" /> : items.length === 0 ? (
|
|
51
|
-
<EmptyState icon={<Package className="size-8" />} title="No quotable products" description=
|
|
55
|
+
<EmptyState icon={<Package className="size-8" />} title="No quotable products" description={`Mark a Stripe product as quotable, or add a manual product, in ${brand.linkLabel}'s quote settings.`} />
|
|
52
56
|
) : (
|
|
53
57
|
<Table>
|
|
54
58
|
<TableHeader><TableRow><TableHead>Product</TableHead><TableHead>Source</TableHead><TableHead className="text-right">Prices</TableHead></TableRow></TableHeader>
|
|
55
59
|
<TableBody>
|
|
56
60
|
{items.map((p) => (
|
|
57
|
-
<TableRow key={productKey(p)} className="cursor-pointer" data-state={productKey(p) === selected ? "selected" : undefined}
|
|
61
|
+
<TableRow key={productKey(p)} className="cursor-pointer" data-state={productKey(p) === selected ? "selected" : undefined} {...rowActivationProps(() => setSelected(productKey(p)))}>
|
|
58
62
|
<TableCell><button type="button" className="font-medium hover:underline" onClick={(e) => { e.stopPropagation(); setSelected(productKey(p)); }}>{p.name}</button><div className="text-xs text-muted-foreground">{p.category}</div></TableCell>
|
|
59
63
|
<TableCell><StatusPill tone="neutral">{p.source === "stripe" ? "Stripe" : "Manual"}</StatusPill></TableCell>
|
|
60
64
|
<TableCell className="text-right tabular-nums">{p.prices.length}</TableCell>
|
|
@@ -110,7 +114,7 @@ export function QuoteDealTab({ quoteId, onOpenDeal }: { quoteId: string; onOpenD
|
|
|
110
114
|
const d = q.data;
|
|
111
115
|
if (!d) return <EmptyState title="No deal on this quote" />;
|
|
112
116
|
return (
|
|
113
|
-
<SectionCard title={d.name || "Deal"} actions={onOpenDeal ? <Button size="sm" variant="outline" onClick={() => onOpenDeal(d.id)}>Open deal</Button> :
|
|
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" /></>}>
|
|
114
118
|
<KeyValueList columns={2} items={[
|
|
115
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 },
|
|
116
120
|
{ label: "Close date", value: d.close_date?.slice(0, 10) }, { label: "Pipeline", value: d.pipeline_name }, { label: "Contacts", value: d.contact_count },
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
* 4. duplicateQuote POST /quotes/{id}/duplicate (external tier, but only copies into a new draft)
|
|
12
12
|
* 5. deleteQuote DELETE /quotes/{id} (destructive; drafts only)
|
|
13
13
|
*
|
|
14
|
+
* The lifecycle row (QuoteLifecycleActions): download the PDF, archive or unarchive, and for a sent quote expire, revoke or
|
|
15
|
+
* mark accepted. Those operations live in that component and each risky one is confirmed there.
|
|
16
|
+
*
|
|
14
17
|
* Send, edit-as-draft and delete go through ConfirmSpendDialog. Duplicate creates an unsent draft and emails
|
|
15
18
|
* nobody, so it runs on one click (same reasoning as saving a draft in QuoteBuilder).
|
|
16
19
|
* Send needs a signer on the quote; without one the button explains what is missing instead of failing.
|
|
@@ -24,6 +27,7 @@ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
|
24
27
|
import { ApiError } from "@/g8/components/api-error";
|
|
25
28
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
26
29
|
import { asRec, body, lineTotal, linesFromQuote, money, statusLabel, text } from "../lib/quotes";
|
|
30
|
+
import { QuoteLifecycleActions } from "./quote-lifecycle-actions";
|
|
27
31
|
import { SignatureStatus } from "./signature-status";
|
|
28
32
|
|
|
29
33
|
export interface QuoteViewerProps {
|
|
@@ -75,7 +79,7 @@ export function QuoteViewer({ quoteId, onEdit, onOpenQuote, onDeleted, className
|
|
|
75
79
|
return (
|
|
76
80
|
<div className={className}>
|
|
77
81
|
<div className="mb-4 flex flex-wrap items-center gap-2">
|
|
78
|
-
<h2 className="text-lg font-
|
|
82
|
+
<h2 className="text-lg font-medium">{text(d.title, "Untitled quote")}</h2>
|
|
79
83
|
<span className="text-muted-foreground text-sm" data-testid="quote-status">{statusLabel(status)} · {text(d.quote_number)}</span>
|
|
80
84
|
<div className="ml-auto flex flex-wrap gap-2">
|
|
81
85
|
{draft ? <Button variant="outline" onClick={() => onEdit?.(quoteId)}>Edit</Button> : null}
|
|
@@ -85,6 +89,7 @@ export function QuoteViewer({ quoteId, onEdit, onOpenQuote, onDeleted, className
|
|
|
85
89
|
{draft || sent ? <Button disabled={!hasSigner} title={hasSigner ? undefined : "Add a signer in the builder first"} onClick={() => setPending("send")}>{sent ? "Resend" : "Send for signature"}</Button> : null}
|
|
86
90
|
</div>
|
|
87
91
|
</div>
|
|
92
|
+
<QuoteLifecycleActions className="mb-4" quoteId={quoteId} status={status} archived={Boolean(d.archived_at || d.is_archived || d.archived === true)} title={text(d.title)} quoteNumber={text(d.quote_number)} onChanged={() => void q.refetch()} />
|
|
88
93
|
{run.error ? <ApiError error={run.error} title="That action failed" /> : null}
|
|
89
94
|
{copy.error ? <ApiError error={copy.error} title="Could not duplicate" /> : null}
|
|
90
95
|
{draft && !hasSigner ? <p className="text-muted-foreground mb-3 text-sm">Add a signer before sending.</p> : null}
|
|
@@ -104,7 +109,7 @@ export function QuoteViewer({ quoteId, onEdit, onOpenQuote, onDeleted, className
|
|
|
104
109
|
<div>Subtotal {money(d.subtotal, currency)}</div>
|
|
105
110
|
{Number(d.discount_amount) ? <div>Discount -{money(d.discount_amount, currency)}</div> : null}
|
|
106
111
|
<div>Tax {money(d.tax_amount, currency)}</div>
|
|
107
|
-
<div className="font-
|
|
112
|
+
<div className="font-medium">Total {money(d.total, currency)}</div>
|
|
108
113
|
</div>
|
|
109
114
|
</CardContent>
|
|
110
115
|
</Card>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* RecordQuotesTab: the quotes tied to one contact (as the signer) or one company, each opening in the quotes screen. Use it
|
|
5
|
+
* as an `extraDetailTabs` entry of ContactsScreen / CompaniesScreen:
|
|
6
|
+
* { id: "quotes", label: "Quotes", render: (c) => <RecordQuotesTab kind="contacts" id={c.id} onOpenQuote={(id) => router.push(`/quotes?id=${id}`)} /> }
|
|
7
|
+
*
|
|
8
|
+
* SDK operations, in order:
|
|
9
|
+
* 1. getContactQuotes GET /contacts/{id}/quotes (read) when kind="contacts"
|
|
10
|
+
* getCompanyQuotes GET /companies/{id}/quotes (read) when kind="companies"
|
|
11
|
+
* Both answer every matching quote in one read (the API does not page them). Nothing here is billable, external or
|
|
12
|
+
* destructive; creating and sending a quote happens in the quotes screen.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { Skeleton } from "@graph8/sdk/ui";
|
|
16
|
+
import { FileText } from "lucide-react";
|
|
17
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
18
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
19
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
20
|
+
import type { Tone } from "@/g8/components/screen/status-pill";
|
|
21
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
22
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
23
|
+
import { asRec, linesFromQuote, money, statusLabel, subtotal, text } from "../lib/quotes";
|
|
24
|
+
import type { Rec } from "../lib/quotes";
|
|
25
|
+
|
|
26
|
+
const TONE: Record<string, Tone> = { sent: "info", viewed: "info", accepted: "positive", declined: "negative", voided: "negative", expired: "warning", draft: "neutral", archived: "neutral" };
|
|
27
|
+
|
|
28
|
+
/** The quote's total: the API's own `total` when it sends one, else the sum of its lines. */
|
|
29
|
+
function totalOf(q: Rec): string {
|
|
30
|
+
const currency = text(q.currency, "USD");
|
|
31
|
+
if (typeof q.total === "number") return money(q.total, currency);
|
|
32
|
+
try {
|
|
33
|
+
const sum = subtotal(linesFromQuote(q.line_items));
|
|
34
|
+
return sum > 0 ? money(sum, currency) : "";
|
|
35
|
+
} catch {
|
|
36
|
+
return "";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface RecordQuotesTabProps {
|
|
41
|
+
kind: "contacts" | "companies";
|
|
42
|
+
id: number;
|
|
43
|
+
/** Route to the quote in your quotes screen. Without it the rows link to `${quotesPath}?id=<quote id>`. */
|
|
44
|
+
onOpenQuote?: (quoteId: string) => void;
|
|
45
|
+
/** Path of your quotes screen for the plain links. Default "/quotes". */
|
|
46
|
+
quotesPath?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function RecordQuotesTab({ kind, id, onOpenQuote, quotesPath = "/quotes" }: RecordQuotesTabProps) {
|
|
50
|
+
const q = useG8Query(async (signal) => {
|
|
51
|
+
const env = asRec(await g8Get(`/${kind}/${id}/quotes`, undefined, { signal }));
|
|
52
|
+
const inner = env.data;
|
|
53
|
+
const rows = Array.isArray(inner) ? inner : (asRec(inner).items ?? []);
|
|
54
|
+
return (rows as unknown[]).map(asRec).filter((r) => text(r.id) !== "");
|
|
55
|
+
}, [kind, id]);
|
|
56
|
+
if (q.error) return <ApiError error={q.error} title="Could not load quotes" onRetry={() => void q.refetch()} />;
|
|
57
|
+
if (!q.data) return <Skeleton className="h-24 w-full" aria-label="Loading quotes" />;
|
|
58
|
+
if (q.data.length === 0) return <EmptyState compact icon={<FileText className="size-8" />} title="No quotes yet" description={kind === "contacts" ? "Quotes this contact signs show up here." : "Quotes for this company show up here."} />;
|
|
59
|
+
return (
|
|
60
|
+
<ul className="divide-y rounded-md border" aria-label="Quotes">
|
|
61
|
+
{q.data.map((r) => {
|
|
62
|
+
const quoteId = text(r.id);
|
|
63
|
+
const status = text(r.status);
|
|
64
|
+
const total = totalOf(r);
|
|
65
|
+
return (
|
|
66
|
+
<li key={quoteId}>
|
|
67
|
+
<a
|
|
68
|
+
href={`${quotesPath}?id=${encodeURIComponent(quoteId)}`}
|
|
69
|
+
className="flex items-center justify-between gap-3 p-3 text-sm hover:bg-muted"
|
|
70
|
+
onClick={(e) => {
|
|
71
|
+
if (!onOpenQuote || e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
onOpenQuote(quoteId);
|
|
74
|
+
}}
|
|
75
|
+
>
|
|
76
|
+
<span className="min-w-0">
|
|
77
|
+
<span className="block truncate font-medium">{text(r.title, "Untitled quote")}</span>
|
|
78
|
+
{text(r.quote_number) ? <span className="block text-xs text-muted-foreground">{text(r.quote_number)}</span> : null}
|
|
79
|
+
</span>
|
|
80
|
+
<span className="flex shrink-0 items-center gap-2">
|
|
81
|
+
{total ? <span className="tabular-nums text-muted-foreground">{total}</span> : null}
|
|
82
|
+
<StatusPill tone={TONE[status] ?? "neutral"} dot>{statusLabel(status)}</StatusPill>
|
|
83
|
+
</span>
|
|
84
|
+
</a>
|
|
85
|
+
</li>
|
|
86
|
+
);
|
|
87
|
+
})}
|
|
88
|
+
</ul>
|
|
89
|
+
);
|
|
90
|
+
}
|
package/blocks/radar/README.md
CHANGED
|
@@ -5,6 +5,16 @@ deep-dive reports. Depends only on `foundation`.
|
|
|
5
5
|
|
|
6
6
|
Install: `npx @graph8/sdk add radar`.
|
|
7
7
|
|
|
8
|
+
## Screens
|
|
9
|
+
|
|
10
|
+
`RadarScreen` (`components/radar-screen.tsx`) is the finished page: copy it as your route. No props are required.
|
|
11
|
+
|
|
12
|
+
- **Overview tab**: stat strip, threat board (click a row to open its battle card), alerts and recent moves.
|
|
13
|
+
- **Competitors tab**: add, bulk-add detected, scan, pause, resume and remove competitors; the selected one opens its battle card
|
|
14
|
+
in a side panel. Deep link: `?id=<competitor id>`. **Reports tab**: generate and open deep-dive reports.
|
|
15
|
+
- Scans and reports spend credits and are confirmed; removing a competitor is confirmed as destructive.
|
|
16
|
+
- Slots: `toolbarActions`, `renderDetailActions(record)`, `extraDetailTabs` (each `{ id, label, render(record) }`), where `record` is `{ kind: "competitor" | "report", id }`.
|
|
17
|
+
|
|
8
18
|
## What you get
|
|
9
19
|
|
|
10
20
|
| Component | Use it for | Props |
|
package/blocks/radar/block.json
CHANGED
|
@@ -87,6 +87,45 @@
|
|
|
87
87
|
"read",
|
|
88
88
|
"billable"
|
|
89
89
|
]
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"name": "RadarScreen",
|
|
93
|
+
"file": "components/radar-screen.tsx",
|
|
94
|
+
"purpose": "Finished competitive-radar page: overview with threat board, competitor roster with battle-card panel, and reports.",
|
|
95
|
+
"operations": [
|
|
96
|
+
"get_marketing_overview_analytics_marketing_overview_get",
|
|
97
|
+
"overview_radar_overview_get"
|
|
98
|
+
],
|
|
99
|
+
"tiers": [
|
|
100
|
+
"read"
|
|
101
|
+
]
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"name": "CompetitorDetail",
|
|
105
|
+
"file": "components/competitor-detail.tsx",
|
|
106
|
+
"purpose": "Complete screen for one competitor: every field, battle card, scan, pause or resume, remove.",
|
|
107
|
+
"operations": [
|
|
108
|
+
"scan_competitor_radar_competitors__competitor_id__scan_post",
|
|
109
|
+
"update_competitor_radar_competitors__competitor_id__patch",
|
|
110
|
+
"delete_competitor_radar_competitors__competitor_id__delete"
|
|
111
|
+
],
|
|
112
|
+
"tiers": [
|
|
113
|
+
"billable",
|
|
114
|
+
"destructive"
|
|
115
|
+
]
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"name": "ReportDetail",
|
|
119
|
+
"file": "components/report-detail.tsx",
|
|
120
|
+
"purpose": "Complete screen for one radar report: status, preview, HTML and PDF links, regenerate.",
|
|
121
|
+
"operations": [
|
|
122
|
+
"list_radar_reports_radar_reports_get",
|
|
123
|
+
"create_radar_report_radar_reports_post"
|
|
124
|
+
],
|
|
125
|
+
"tiers": [
|
|
126
|
+
"read",
|
|
127
|
+
"billable"
|
|
128
|
+
]
|
|
90
129
|
}
|
|
91
130
|
],
|
|
92
131
|
"hooks": [
|
|
@@ -184,6 +223,7 @@
|
|
|
184
223
|
}
|
|
185
224
|
],
|
|
186
225
|
"peerDependencies": {
|
|
187
|
-
"react": ">=18"
|
|
226
|
+
"react": ">=18",
|
|
227
|
+
"lucide-react": ">=0.400"
|
|
188
228
|
}
|
|
189
229
|
}
|
|
@@ -34,7 +34,7 @@ export function CompetitorBattleCard({ competitorId, className }: CompetitorBatt
|
|
|
34
34
|
if (!card) return <Skeleton className="h-40 w-full" />;
|
|
35
35
|
return (
|
|
36
36
|
<article className={`flex flex-col gap-4 ${className ?? ""}`} aria-busy={query.loading}>
|
|
37
|
-
<header><h3 className="text-base font-
|
|
37
|
+
<header><h3 className="text-base font-medium">{card.competitor.name || card.competitor.domain}</h3><p className="text-sm text-muted-foreground">{card.summary}</p></header>
|
|
38
38
|
<section aria-label="Lead with"><h4 className="text-sm font-medium">Lead with</h4>
|
|
39
39
|
{card.talk_track.lead_with.length ? <ul className="list-disc pl-5 text-sm">{card.talk_track.lead_with.map((t) => <li key={t}>{t}</li>)}</ul> : <p className="text-sm text-muted-foreground">No proven advantage yet. Add proof to your catalog or show unverified ones.</p>}
|
|
40
40
|
</section>
|