@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,239 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* NumberSearch: find a number a carrier has available, then buy it.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations, in order:
|
|
7
|
+
* 1. searchAvailablePhoneNumbers POST /phone-numbers/search (read; nothing is held or charged)
|
|
8
|
+
* 2. purchasePhoneNumber POST /phone-numbers/purchase (billable: SPENDS CREDITS AND REAL MONEY, so it opens
|
|
9
|
+
* ConfirmSpendDialog with the price first; sent with an Idempotency-Key)
|
|
10
|
+
*
|
|
11
|
+
* Ids passed between steps: the `phone_number` of a search result (E.164) is the body of step 2. The price is an
|
|
12
|
+
* ESTIMATE (250 credits held on purchase, then about 250 credits each month): the API has no price read. A 402 means
|
|
13
|
+
* not enough credits; the number was not bought and nothing was charged. A 409 means the number is already taken.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { useRef, useState } from "react";
|
|
17
|
+
import { Phone, Search } from "lucide-react";
|
|
18
|
+
import { Button, Input, Label } from "@graph8/sdk/ui";
|
|
19
|
+
import { g8Post } from "@/g8/lib/g8-client";
|
|
20
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
21
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
22
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
23
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
24
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
25
|
+
import { plural } from "@/g8/lib/plural";
|
|
26
|
+
import { MemberPicker } from "@/g8/pickers/components/member-picker";
|
|
27
|
+
import type { MemberOption } from "@/g8/pickers/components/member-picker";
|
|
28
|
+
import { MONTHLY_RENTAL_CREDITS, PROVIDERS, PURCHASE_CREDITS, asObject, formatNumber, newIdempotencyKey } from "../lib/phone-numbers-utils";
|
|
29
|
+
import type { AvailableNumber, Provider } from "../lib/phone-numbers-utils";
|
|
30
|
+
|
|
31
|
+
export interface NumberSearchProps {
|
|
32
|
+
/** Called after a purchase went through, so the list can refresh. */
|
|
33
|
+
onPurchased?: (phoneNumber: string) => void;
|
|
34
|
+
className?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type Use = "dialer" | "ai" | "work";
|
|
38
|
+
|
|
39
|
+
const USE_LABEL: Record<Use, string> = { dialer: "Outbound dialer", ai: "Voice AI agent", work: "Personal work phone" };
|
|
40
|
+
const SELECT = "h-9 w-full rounded-md border border-border bg-background px-2 text-sm";
|
|
41
|
+
|
|
42
|
+
function resultsOf(envelope: unknown): AvailableNumber[] {
|
|
43
|
+
const data = asObject(envelope).data;
|
|
44
|
+
const rows = Array.isArray(data) ? data : asObject(data).items;
|
|
45
|
+
return (Array.isArray(rows) ? rows : []).filter((r): r is AvailableNumber => typeof asObject(r).phone_number === "string");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function Field({ id, label, children }: { id: string; label: string; children: React.ReactNode }) {
|
|
49
|
+
return (
|
|
50
|
+
<div className="space-y-1">
|
|
51
|
+
<Label htmlFor={id}>{label}</Label>
|
|
52
|
+
{children}
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function NumberSearch({ onPurchased, className }: NumberSearchProps) {
|
|
58
|
+
const [provider, setProvider] = useState<Provider>("twilio");
|
|
59
|
+
const [country, setCountry] = useState("US");
|
|
60
|
+
const [numberType, setNumberType] = useState<"local" | "toll_free">("local");
|
|
61
|
+
const [areaCode, setAreaCode] = useState("");
|
|
62
|
+
const [by, setBy] = useState<"locality" | "pattern">("pattern");
|
|
63
|
+
const [text, setText] = useState("");
|
|
64
|
+
const [match, setMatch] = useState<"first" | "anywhere" | "last">("first");
|
|
65
|
+
// The results remember the carrier they were searched on: a number is bought from THAT carrier, even if the select moved since.
|
|
66
|
+
const [found, setFound] = useState<{ provider: Provider; rows: AvailableNumber[] } | null>(null);
|
|
67
|
+
const results = found?.rows ?? null;
|
|
68
|
+
const boughtFrom = found?.provider ?? provider;
|
|
69
|
+
const [picked, setPicked] = useState<AvailableNumber | null>(null);
|
|
70
|
+
const [use, setUse] = useState<Use>("dialer");
|
|
71
|
+
const [assignee, setAssignee] = useState<MemberOption | null>(null);
|
|
72
|
+
const [confirming, setConfirming] = useState(false);
|
|
73
|
+
const [done, setDone] = useState<string | null>(null);
|
|
74
|
+
const idem = useRef<{ sig: string; key: string } | null>(null);
|
|
75
|
+
|
|
76
|
+
const search = useG8Mutation(async () => {
|
|
77
|
+
const body = {
|
|
78
|
+
provider,
|
|
79
|
+
country: country.trim().toUpperCase() || "US",
|
|
80
|
+
number_type: numberType,
|
|
81
|
+
...(areaCode.trim() ? { area_code: areaCode.trim() } : {}),
|
|
82
|
+
...(text.trim() ? (by === "pattern" ? { pattern: text.trim(), match } : { locality: text.trim() }) : {}),
|
|
83
|
+
};
|
|
84
|
+
setPicked(null);
|
|
85
|
+
setDone(null);
|
|
86
|
+
setFound({ provider, rows: resultsOf(await g8Post("/phone-numbers/search", body)) });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const purchaseBody = picked
|
|
90
|
+
? {
|
|
91
|
+
phone_number: picked.phone_number,
|
|
92
|
+
provider: boughtFrom,
|
|
93
|
+
dialer: use !== "ai",
|
|
94
|
+
work: use === "work",
|
|
95
|
+
...(assignee?.email ? { assignee: assignee.email } : {}),
|
|
96
|
+
}
|
|
97
|
+
: null;
|
|
98
|
+
|
|
99
|
+
const buy = useG8Mutation(async () => {
|
|
100
|
+
if (!purchaseBody) return;
|
|
101
|
+
const sig = JSON.stringify(purchaseBody);
|
|
102
|
+
if (idem.current?.sig !== sig) idem.current = { sig, key: newIdempotencyKey() };
|
|
103
|
+
await g8Post("/phone-numbers/purchase", purchaseBody, { idempotencyKey: idem.current.key });
|
|
104
|
+
setDone(`${formatNumber(purchaseBody.phone_number)} is yours. It can take a minute to appear in your numbers.`);
|
|
105
|
+
setFound((f) => (f ? { ...f, rows: f.rows.filter((x) => x.phone_number !== purchaseBody.phone_number) } : f));
|
|
106
|
+
setPicked(null);
|
|
107
|
+
setConfirming(false);
|
|
108
|
+
onPurchased?.(purchaseBody.phone_number);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const status = (buy.error as { status?: number } | undefined)?.status;
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div className={`space-y-5 ${className ?? ""}`}>
|
|
115
|
+
<form
|
|
116
|
+
className="space-y-3"
|
|
117
|
+
onSubmit={(e) => {
|
|
118
|
+
e.preventDefault();
|
|
119
|
+
void search.mutate(undefined);
|
|
120
|
+
}}
|
|
121
|
+
>
|
|
122
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
123
|
+
<Field id="ns-provider" label="Carrier">
|
|
124
|
+
<select id="ns-provider" className={SELECT} value={provider} onChange={(e) => setProvider(e.target.value as Provider)}>
|
|
125
|
+
{PROVIDERS.map((p) => (
|
|
126
|
+
<option key={p} value={p}>{p === "twilio" ? "Twilio" : "Telnyx"}</option>
|
|
127
|
+
))}
|
|
128
|
+
</select>
|
|
129
|
+
</Field>
|
|
130
|
+
<Field id="ns-type" label="Number type">
|
|
131
|
+
<select id="ns-type" className={SELECT} value={numberType} onChange={(e) => setNumberType(e.target.value as "local" | "toll_free")}>
|
|
132
|
+
<option value="local">Local</option>
|
|
133
|
+
<option value="toll_free">Toll free</option>
|
|
134
|
+
</select>
|
|
135
|
+
</Field>
|
|
136
|
+
<Field id="ns-country" label="Country (ISO code)">
|
|
137
|
+
<Input id="ns-country" value={country} maxLength={2} onChange={(e) => setCountry(e.target.value)} />
|
|
138
|
+
</Field>
|
|
139
|
+
<Field id="ns-area" label="Area code">
|
|
140
|
+
<Input id="ns-area" inputMode="numeric" placeholder="415" value={areaCode} onChange={(e) => setAreaCode(e.target.value)} />
|
|
141
|
+
</Field>
|
|
142
|
+
</div>
|
|
143
|
+
<div className="grid gap-3 sm:grid-cols-[10rem_1fr_9rem]">
|
|
144
|
+
<Field id="ns-by" label="Narrow by">
|
|
145
|
+
<select id="ns-by" className={SELECT} value={by} onChange={(e) => setBy(e.target.value as "locality" | "pattern")}>
|
|
146
|
+
<option value="pattern">Digits or phrase</option>
|
|
147
|
+
<option value="locality">City</option>
|
|
148
|
+
</select>
|
|
149
|
+
</Field>
|
|
150
|
+
<Field id="ns-text" label={by === "pattern" ? "Digits or phrase" : "City"}>
|
|
151
|
+
<Input id="ns-text" placeholder={by === "pattern" ? "555 or 0100" : "San Francisco"} value={text} onChange={(e) => setText(e.target.value)} />
|
|
152
|
+
</Field>
|
|
153
|
+
{by === "pattern" ? (
|
|
154
|
+
<Field id="ns-match" label="Where it appears">
|
|
155
|
+
<select id="ns-match" className={SELECT} value={match} onChange={(e) => setMatch(e.target.value as "first" | "anywhere" | "last")}>
|
|
156
|
+
<option value="first">At the start</option>
|
|
157
|
+
<option value="anywhere">Anywhere</option>
|
|
158
|
+
<option value="last">At the end</option>
|
|
159
|
+
</select>
|
|
160
|
+
</Field>
|
|
161
|
+
) : null}
|
|
162
|
+
</div>
|
|
163
|
+
<Button type="submit" loading={search.loading}>
|
|
164
|
+
<Search className="mr-1 h-4 w-4" aria-hidden /> Search numbers
|
|
165
|
+
</Button>
|
|
166
|
+
<p className="text-xs text-muted-foreground">Searching is free. Nothing is held or charged until you confirm a purchase.</p>
|
|
167
|
+
</form>
|
|
168
|
+
|
|
169
|
+
{search.error ? <ApiError error={search.error} onRetry={search.reset} /> : null}
|
|
170
|
+
{done ? <p role="status" className="rounded-md border border-border bg-muted px-3 py-2 text-sm">{done}</p> : null}
|
|
171
|
+
|
|
172
|
+
{results && results.length === 0 ? (
|
|
173
|
+
<EmptyState compact title="No numbers available" description="Try another area code, a shorter pattern, or the other carrier." />
|
|
174
|
+
) : null}
|
|
175
|
+
{results && results.length > 0 ? (
|
|
176
|
+
<section aria-label="Available numbers" className="space-y-2">
|
|
177
|
+
<h3 className="text-sm font-medium">{plural(results.length, "number")} available</h3>
|
|
178
|
+
<ul className="divide-y rounded-md border border-border">
|
|
179
|
+
{results.map((r) => (
|
|
180
|
+
<li key={r.phone_number} className={`flex flex-wrap items-center justify-between gap-2 px-3 py-2 text-sm ${picked?.phone_number === r.phone_number ? "bg-muted" : ""}`}>
|
|
181
|
+
<span className="flex flex-wrap items-center gap-2">
|
|
182
|
+
<Phone className="h-4 w-4 text-muted-foreground" aria-hidden />
|
|
183
|
+
<span className="font-medium tabular-nums">{formatNumber(r.phone_number)}</span>
|
|
184
|
+
<span className="text-xs text-muted-foreground">{[r.locality, r.region].filter(Boolean).join(", ")}</span>
|
|
185
|
+
{r.capabilities?.voice ? <StatusPill tone="neutral">Voice</StatusPill> : null}
|
|
186
|
+
{r.capabilities?.sms ? <StatusPill tone="neutral">SMS</StatusPill> : null}
|
|
187
|
+
</span>
|
|
188
|
+
<Button size="sm" variant="outline" aria-label={`Choose ${r.phone_number}`} onClick={() => { setPicked(r); setDone(null); buy.reset(); }}>
|
|
189
|
+
{picked?.phone_number === r.phone_number ? "Chosen" : "Choose"}
|
|
190
|
+
</Button>
|
|
191
|
+
</li>
|
|
192
|
+
))}
|
|
193
|
+
</ul>
|
|
194
|
+
</section>
|
|
195
|
+
) : null}
|
|
196
|
+
|
|
197
|
+
{picked ? (
|
|
198
|
+
<section aria-label="Buy this number" className="space-y-3 rounded-md border border-border p-3">
|
|
199
|
+
<h3 className="text-sm font-medium">Buy {formatNumber(picked.phone_number)}</h3>
|
|
200
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
201
|
+
<Field id="ns-use" label="What it is for">
|
|
202
|
+
<select id="ns-use" className={SELECT} value={use} onChange={(e) => setUse(e.target.value as Use)}>
|
|
203
|
+
{(Object.keys(USE_LABEL) as Use[]).map((u) => (
|
|
204
|
+
<option key={u} value={u}>{USE_LABEL[u]}</option>
|
|
205
|
+
))}
|
|
206
|
+
</select>
|
|
207
|
+
</Field>
|
|
208
|
+
<Field id="ns-assignee" label="Assign to (optional)">
|
|
209
|
+
<MemberPicker id="ns-assignee" label="Assign to" value={assignee?.id ?? null} selectedName={assignee?.name} onChange={setAssignee} />
|
|
210
|
+
</Field>
|
|
211
|
+
</div>
|
|
212
|
+
{use === "work" && !assignee ? <p className="text-xs text-muted-foreground">A personal work phone rings for one person: pick who, or it is yours.</p> : null}
|
|
213
|
+
{buy.error ? (
|
|
214
|
+
<ApiError
|
|
215
|
+
error={buy.error}
|
|
216
|
+
title={status === 402 ? "Not enough credits. Nothing was bought and nothing was charged." : status === 409 ? "That number is no longer available" : "The purchase did not go through"}
|
|
217
|
+
onRetry={buy.reset}
|
|
218
|
+
/>
|
|
219
|
+
) : null}
|
|
220
|
+
<Button onClick={() => setConfirming(true)}>Buy for about {PURCHASE_CREDITS} credits</Button>
|
|
221
|
+
</section>
|
|
222
|
+
) : null}
|
|
223
|
+
|
|
224
|
+
{picked && confirming ? (
|
|
225
|
+
<ConfirmSpendDialog
|
|
226
|
+
open
|
|
227
|
+
tier="billable"
|
|
228
|
+
title={`Buy ${formatNumber(picked.phone_number)} from ${boughtFrom === "twilio" ? "Twilio" : "Telnyx"}`}
|
|
229
|
+
description={`This spends credits now, whether or not the number is ever used. It is then rented for about ${MONTHLY_RENTAL_CREDITS} credits a month until you release it. These are estimates: the exact rate comes from your plan.`}
|
|
230
|
+
estimatedCredits={PURCHASE_CREDITS}
|
|
231
|
+
confirmLabel="Buy number"
|
|
232
|
+
confirming={buy.loading}
|
|
233
|
+
onCancel={() => setConfirming(false)}
|
|
234
|
+
onConfirm={() => void buy.mutate(undefined).then(() => setConfirming(false))}
|
|
235
|
+
/>
|
|
236
|
+
) : null}
|
|
237
|
+
</div>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* NumberSettings: how one number behaves. Answering mode, caller-ID verification, and who picks up.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations, in order:
|
|
7
|
+
* 1. getVoiceAnsweringAgent GET /voice/numbers/answering-agent?number= (read; who picks up when it is called)
|
|
8
|
+
* 2. updatePhoneCapabilities PATCH /phone-numbers/update-capabilities (write; switches a number between the
|
|
9
|
+
* dialer and a voice AI agent and re-provisions it; admin-only)
|
|
10
|
+
* 3. updateVoiceInboundType PATCH /voice/numbers/inbound-type (write; retypes among ai, sdr, dialer
|
|
11
|
+
* and work on the voice side)
|
|
12
|
+
* 4. verifyVoiceCallerId POST /voice/numbers/verify-caller-id/{number} (write; Telnyx numbers only)
|
|
13
|
+
*
|
|
14
|
+
* Mapping: "Voice AI agent" and "Outbound dialer" go through 2 (the app's own derivation, which also updates the CRM
|
|
15
|
+
* record); "Rep answers" and "Personal work phone" go through 3. Changing the mode tears down and rebuilds the number's
|
|
16
|
+
* routing, so the number answers nothing for a moment: the change takes a second click on a button that says so.
|
|
17
|
+
* Call forwarding, voicemail greetings and ring-group routing have no API operations, so they are not offered here.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { useState } from "react";
|
|
21
|
+
import { ExternalLink } from "lucide-react";
|
|
22
|
+
import { Button } from "@graph8/sdk/ui";
|
|
23
|
+
import { g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
24
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
25
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
26
|
+
import { useBrand, brandUrl } from "@/g8/hooks/use-brand";
|
|
27
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
28
|
+
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
29
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
30
|
+
import { INBOUND_LABEL, INBOUND_TYPES, dataOf, formatNumber, scalarItems, typeLabel } from "../lib/phone-numbers-utils";
|
|
31
|
+
import type { InboundType, NumberRow } from "../lib/phone-numbers-utils";
|
|
32
|
+
|
|
33
|
+
export interface NumberSettingsProps {
|
|
34
|
+
row: NumberRow;
|
|
35
|
+
onChanged?: () => void;
|
|
36
|
+
className?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const SELECT = "h-9 rounded-md border border-border bg-background px-2 text-sm";
|
|
40
|
+
|
|
41
|
+
export function NumberSettings({ row, onChanged, className }: NumberSettingsProps) {
|
|
42
|
+
const brand = useBrand();
|
|
43
|
+
// An unreported mode is "" (never guessed as voice AI), so every real mode can be applied to it.
|
|
44
|
+
const current: InboundType | "" = (INBOUND_TYPES as readonly string[]).includes(row.inboundType) ? (row.inboundType as InboundType) : "";
|
|
45
|
+
const [mode, setMode] = useState<InboundType | "">(current);
|
|
46
|
+
const [armed, setArmed] = useState(false);
|
|
47
|
+
const [note, setNote] = useState<string | null>(null);
|
|
48
|
+
|
|
49
|
+
const who = useG8Query(async (signal) => dataOf(await g8Get("/voice/numbers/answering-agent", { number: row.number }, { signal })), [row.number]);
|
|
50
|
+
|
|
51
|
+
const retype = useG8Mutation(async (next: InboundType) => {
|
|
52
|
+
if (next === "ai" || next === "dialer") await g8Patch("/phone-numbers/update-capabilities", { phone_number: row.number, dialer: next === "dialer", voice: next === "ai" });
|
|
53
|
+
else await g8Patch("/voice/numbers/inbound-type", { number: row.number, inbound_type: next });
|
|
54
|
+
setNote(`${formatNumber(row.number)} is now: ${INBOUND_LABEL[next]}.`);
|
|
55
|
+
setArmed(false);
|
|
56
|
+
onChanged?.();
|
|
57
|
+
void who.refetch();
|
|
58
|
+
});
|
|
59
|
+
const verify = useG8Mutation(async () => {
|
|
60
|
+
const res = dataOf(await g8Post(`/voice/numbers/verify-caller-id/${encodeURIComponent(row.number)}`, {}));
|
|
61
|
+
const state = typeof res.status === "string" ? res.status : typeof res.result === "string" ? res.result : "verification_started";
|
|
62
|
+
setNote(state.includes("already") ? "Already verified as a caller ID." : "Verification started. It takes a few minutes; this page shows the result once it lands.");
|
|
63
|
+
onChanged?.();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const changed = mode !== "" && mode !== current;
|
|
67
|
+
const telnyx = row.provider === "telnyx";
|
|
68
|
+
const answering = scalarItems(who.data ?? {});
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className={`space-y-6 ${className ?? ""}`}>
|
|
72
|
+
<section className="space-y-2" aria-label="Answering mode">
|
|
73
|
+
<h3 className="text-sm font-medium">Answering mode</h3>
|
|
74
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
75
|
+
<select aria-label="Answering mode" className={SELECT} value={mode} onChange={(e) => { setMode(e.target.value as InboundType | ""); setArmed(false); retype.reset(); }}>
|
|
76
|
+
{current === "" ? <option value="" disabled>Not reported</option> : null}
|
|
77
|
+
{INBOUND_TYPES.map((t) => (
|
|
78
|
+
<option key={t} value={t}>{INBOUND_LABEL[t]}</option>
|
|
79
|
+
))}
|
|
80
|
+
</select>
|
|
81
|
+
{!armed ? (
|
|
82
|
+
<Button variant="outline" disabled={!changed} onClick={() => setArmed(true)}>Change mode</Button>
|
|
83
|
+
) : (
|
|
84
|
+
<Button loading={retype.loading} onClick={() => mode !== "" && void retype.mutate(mode)}>Switch now: the number answers nothing for a moment</Button>
|
|
85
|
+
)}
|
|
86
|
+
{armed ? <Button variant="ghost" onClick={() => setArmed(false)}>Cancel</Button> : null}
|
|
87
|
+
</div>
|
|
88
|
+
<p className="text-xs text-muted-foreground">
|
|
89
|
+
Now: {typeLabel(row.inboundType)}. Switching rebuilds how calls reach the number, so calls in that moment are missed. Admin-only.
|
|
90
|
+
</p>
|
|
91
|
+
{retype.error ? <ApiError error={retype.error} title="The mode did not change" onRetry={retype.reset} /> : null}
|
|
92
|
+
</section>
|
|
93
|
+
|
|
94
|
+
<section className="space-y-2" aria-label="Caller ID">
|
|
95
|
+
<h3 className="text-sm font-medium">Caller ID</h3>
|
|
96
|
+
{telnyx ? (
|
|
97
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
98
|
+
<StatusPill tone={row.callerIdVerified === true ? "positive" : row.callerIdVerified === false ? "warning" : "neutral"} dot>
|
|
99
|
+
{row.callerIdVerified === true ? "Verified" : row.callerIdVerified === false ? "Not verified" : "Unknown"}
|
|
100
|
+
</StatusPill>
|
|
101
|
+
<Button size="sm" variant="outline" loading={verify.loading} disabled={row.callerIdVerified === true} onClick={() => void verify.mutate(undefined)}>
|
|
102
|
+
Verify as caller ID
|
|
103
|
+
</Button>
|
|
104
|
+
</div>
|
|
105
|
+
) : row.provider === "twilio" ? (
|
|
106
|
+
<p className="text-sm text-muted-foreground">Twilio numbers are valid caller IDs as they are. Nothing to verify.</p>
|
|
107
|
+
) : (
|
|
108
|
+
<p className="text-sm text-muted-foreground">The carrier is not reported for this number, so caller-ID verification cannot be offered.</p>
|
|
109
|
+
)}
|
|
110
|
+
{telnyx ? <p className="text-xs text-muted-foreground">An unverified number cannot place calls, so it stays out of the dialer rotation.</p> : null}
|
|
111
|
+
{verify.error ? <ApiError error={verify.error} title="Verification did not start" onRetry={verify.reset} /> : null}
|
|
112
|
+
</section>
|
|
113
|
+
|
|
114
|
+
<section className="space-y-2" aria-label="Who answers">
|
|
115
|
+
<h3 className="text-sm font-medium">Who answers</h3>
|
|
116
|
+
{who.error ? (
|
|
117
|
+
<ApiError compact error={who.error} onRetry={() => void who.refetch()} />
|
|
118
|
+
) : answering.length > 0 ? (
|
|
119
|
+
<KeyValueList items={answering} />
|
|
120
|
+
) : (
|
|
121
|
+
<p className="text-sm text-muted-foreground">{who.loading ? "Looking up who answers." : "No answering agent is set for this number."}</p>
|
|
122
|
+
)}
|
|
123
|
+
</section>
|
|
124
|
+
|
|
125
|
+
{note ? <p role="status" className="text-sm">{note}</p> : null}
|
|
126
|
+
<p className="text-xs text-muted-foreground">
|
|
127
|
+
Call forwarding, voicemail greetings and ring-group routing are not available through the API yet.{" "}
|
|
128
|
+
<a className="inline-flex items-center gap-1 underline" href={brandUrl(brand)} target="_blank" rel="noreferrer">
|
|
129
|
+
Manage them in {brand.linkLabel} <ExternalLink className="h-3 w-3" aria-hidden />
|
|
130
|
+
</a>
|
|
131
|
+
</p>
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* NumberUsage: how hard one number is being used today, against the daily cap that keeps carriers from flagging it.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations:
|
|
7
|
+
* resetVoiceDailyCounts DELETE /voice/numbers/daily-counts (destructive: clears today's outbound call counters for EVERY
|
|
8
|
+
* number in the workspace, which lets numbers dial past a cap that exists to protect their
|
|
9
|
+
* reputation. Admin-only. Confirmed with ConfirmSpendDialog.)
|
|
10
|
+
*
|
|
11
|
+
* The figures come from the number list (GET /voice/dialer/numbers), so this component reads nothing itself. Counters
|
|
12
|
+
* reset on their own at midnight US Eastern. Costs: the API has no per-number cost read; the rental is shown as an
|
|
13
|
+
* estimate on the purchase dialog only, and a number the platform provisioned for you is never rental-billed.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { useState } from "react";
|
|
17
|
+
import { Button } from "@graph8/sdk/ui";
|
|
18
|
+
import { g8Delete } from "@/g8/lib/g8-client";
|
|
19
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
20
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
21
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
22
|
+
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
23
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
24
|
+
import { dataOf, numberHealth, percent } from "../lib/phone-numbers-utils";
|
|
25
|
+
import type { NumberRow } from "../lib/phone-numbers-utils";
|
|
26
|
+
|
|
27
|
+
export interface NumberUsageProps {
|
|
28
|
+
row: NumberRow;
|
|
29
|
+
onChanged?: () => void;
|
|
30
|
+
className?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const show = (n: number | null) => (n == null ? "-" : n.toLocaleString());
|
|
34
|
+
|
|
35
|
+
export function NumberUsage({ row, onChanged, className }: NumberUsageProps) {
|
|
36
|
+
const [confirming, setConfirming] = useState(false);
|
|
37
|
+
const [note, setNote] = useState<string | null>(null);
|
|
38
|
+
const reset = useG8Mutation(async () => {
|
|
39
|
+
const out = dataOf(await g8Delete("/voice/numbers/daily-counts"));
|
|
40
|
+
const n = typeof out.deleted === "number" ? out.deleted : null;
|
|
41
|
+
setNote(n === null ? "Today's counters were cleared." : `Cleared ${n.toLocaleString()} counter${n === 1 ? "" : "s"}.`);
|
|
42
|
+
setConfirming(false);
|
|
43
|
+
onChanged?.();
|
|
44
|
+
});
|
|
45
|
+
const health = numberHealth(row);
|
|
46
|
+
const used = row.callsToday ?? 0;
|
|
47
|
+
const pct = row.dailyLimit ? Math.min(100, Math.round((used / row.dailyLimit) * 100)) : 0;
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div className={`space-y-5 ${className ?? ""}`}>
|
|
51
|
+
<div className="flex items-center gap-2">
|
|
52
|
+
<StatusPill tone={health.tone} dot>{health.label}</StatusPill>
|
|
53
|
+
{row.isValid === false ? <span className="text-xs text-muted-foreground">The carrier would refuse calls from this number.</span> : null}
|
|
54
|
+
</div>
|
|
55
|
+
<div className="space-y-1">
|
|
56
|
+
<div className="flex items-baseline justify-between text-sm">
|
|
57
|
+
<span>Outbound calls today</span>
|
|
58
|
+
<span className="tabular-nums">{show(row.callsToday)} of {row.dailyLimit ? row.dailyLimit.toLocaleString() : "no limit set"}</span>
|
|
59
|
+
</div>
|
|
60
|
+
<div role="progressbar" aria-label="Daily limit used" aria-valuenow={pct} aria-valuemin={0} aria-valuemax={100} className="h-2 overflow-hidden rounded-full bg-muted">
|
|
61
|
+
<div className={`h-full ${pct >= 100 ? "bg-error" : pct >= 80 ? "bg-warning" : "bg-primary"}`} style={{ width: `${pct}%` }} />
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
<KeyValueList
|
|
65
|
+
columns={2}
|
|
66
|
+
items={[
|
|
67
|
+
{ label: "Calls, last 7 days", value: show(row.calls7d) },
|
|
68
|
+
{ label: "Connect rate, 7 days", value: percent(row.connectRate7d) },
|
|
69
|
+
{ label: "Daily limit", value: row.dailyLimit ? show(row.dailyLimit) : "-" },
|
|
70
|
+
{ label: "Carrier", value: row.provider ? (row.provider === "telnyx" ? "Telnyx" : "Twilio") : "-" },
|
|
71
|
+
]}
|
|
72
|
+
/>
|
|
73
|
+
<section className="space-y-2 rounded-md border border-border p-3" aria-label="Reset counters">
|
|
74
|
+
<h3 className="text-sm font-medium">Reset today's counters</h3>
|
|
75
|
+
<p className="text-sm text-muted-foreground">
|
|
76
|
+
Clears today's call counts for every number in the workspace, not only this one, so they can dial past the daily limit that protects them from being flagged
|
|
77
|
+
as spam. They reset on their own at midnight US Eastern. Admin-only.
|
|
78
|
+
</p>
|
|
79
|
+
<Button variant="outline" onClick={() => setConfirming(true)}>Reset counters</Button>
|
|
80
|
+
{reset.error ? <ApiError error={reset.error} title="The counters were not reset" onRetry={reset.reset} /> : null}
|
|
81
|
+
{note ? <p role="status" className="text-sm">{note}</p> : null}
|
|
82
|
+
</section>
|
|
83
|
+
{confirming ? (
|
|
84
|
+
<ConfirmSpendDialog
|
|
85
|
+
open
|
|
86
|
+
tier="destructive"
|
|
87
|
+
title="Reset today's call counters for every number"
|
|
88
|
+
description="Numbers that have reached their daily limit can dial again today. The limit exists to keep carriers from flagging a number as spam."
|
|
89
|
+
confirmLabel="Reset counters"
|
|
90
|
+
confirming={reset.loading}
|
|
91
|
+
onCancel={() => setConfirming(false)}
|
|
92
|
+
onConfirm={() => void reset.mutate(undefined).then(() => setConfirming(false))}
|
|
93
|
+
/>
|
|
94
|
+
) : null}
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PhoneNumberDetail: one number in full, as tabs: Overview, Assign, Usage, Settings and Release. Each tab is its own
|
|
5
|
+
* component (AssignNumber, NumberUsage, NumberSettings, ReleaseNumber) that owns its operations and confirm dialogs.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations: none of its own (the composed parts call them).
|
|
8
|
+
*
|
|
9
|
+
* Billing, stated honestly: the API reports no rental per number, so Overview does not guess one. A number bought here is
|
|
10
|
+
* rented monthly after purchase (see the purchase dialog for the estimate); a number the platform set up for you, such as
|
|
11
|
+
* the onboarding receptionist line, is never rental-billed.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { ReactNode } from "react";
|
|
15
|
+
import { Phone } from "lucide-react";
|
|
16
|
+
import { DetailPanel } from "@/g8/components/screen/detail-panel";
|
|
17
|
+
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
18
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
19
|
+
import { AssignNumber } from "./assign-number";
|
|
20
|
+
import { NumberSettings } from "./number-settings";
|
|
21
|
+
import { NumberUsage } from "./number-usage";
|
|
22
|
+
import { ReleaseNumber } from "./release-number";
|
|
23
|
+
import { formatNumber, numberHealth, shortDate, typeLabel } from "../lib/phone-numbers-utils";
|
|
24
|
+
import type { NumberRow } from "../lib/phone-numbers-utils";
|
|
25
|
+
|
|
26
|
+
export interface PhoneNumberDetailProps {
|
|
27
|
+
row: NumberRow;
|
|
28
|
+
tab?: string;
|
|
29
|
+
onTabChange?: (tab: string) => void;
|
|
30
|
+
onClose?: () => void;
|
|
31
|
+
onExpand?: () => void;
|
|
32
|
+
onCollapse?: () => void;
|
|
33
|
+
/** The list should reload (something about the number changed). */
|
|
34
|
+
onChanged?: () => void;
|
|
35
|
+
/** The number was released: the record is gone. */
|
|
36
|
+
onReleased?: () => void;
|
|
37
|
+
renderActions?: (row: NumberRow) => ReactNode;
|
|
38
|
+
extraTabs?: Array<{ id: string; label: string; render: (row: NumberRow) => ReactNode }>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function PhoneNumberDetail({ row, tab, onTabChange, onClose, onExpand, onCollapse, onChanged, onReleased, renderActions, extraTabs }: PhoneNumberDetailProps) {
|
|
42
|
+
const health = numberHealth(row);
|
|
43
|
+
const overview = (
|
|
44
|
+
<div className="space-y-4">
|
|
45
|
+
<KeyValueList
|
|
46
|
+
columns={2}
|
|
47
|
+
items={[
|
|
48
|
+
{ label: "Number", value: row.number, mono: true },
|
|
49
|
+
{ label: "Use", value: typeLabel(row.inboundType) },
|
|
50
|
+
{ label: "Carrier", value: row.provider === "telnyx" ? "Telnyx" : row.provider === "twilio" ? "Twilio" : "-" },
|
|
51
|
+
{ label: "Assigned to", value: row.assignedTo ?? "-" },
|
|
52
|
+
{ label: "Voice agent", value: row.agentName ?? "-" },
|
|
53
|
+
{ label: "Added", value: shortDate(row.createdAt) },
|
|
54
|
+
]}
|
|
55
|
+
/>
|
|
56
|
+
<p className="text-sm text-muted-foreground">
|
|
57
|
+
The API does not report a rental price per number. A number bought here is rented monthly after purchase; a number the platform set up for you is never
|
|
58
|
+
rental-billed.
|
|
59
|
+
</p>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
return (
|
|
63
|
+
<DetailPanel
|
|
64
|
+
title={formatNumber(row.number)}
|
|
65
|
+
subtitle={typeLabel(row.inboundType)}
|
|
66
|
+
avatar={<Phone className="h-5 w-5" aria-hidden />}
|
|
67
|
+
badges={<StatusPill tone={health.tone} dot>{health.label}</StatusPill>}
|
|
68
|
+
actions={renderActions?.(row)}
|
|
69
|
+
activeTab={tab || "overview"}
|
|
70
|
+
onTabChange={onTabChange}
|
|
71
|
+
onClose={onClose}
|
|
72
|
+
onExpand={onExpand}
|
|
73
|
+
onCollapse={onCollapse}
|
|
74
|
+
tabs={[
|
|
75
|
+
{ id: "overview", label: "Overview", content: overview },
|
|
76
|
+
{ id: "assign", label: "Assign", content: <AssignNumber row={row} onChanged={onChanged} /> },
|
|
77
|
+
{ id: "usage", label: "Usage", content: <NumberUsage row={row} onChanged={onChanged} /> },
|
|
78
|
+
{ id: "settings", label: "Settings", content: <NumberSettings row={row} onChanged={onChanged} /> },
|
|
79
|
+
{ id: "release", label: "Release", content: <ReleaseNumber row={row} onChanged={onChanged} onReleased={onReleased} /> },
|
|
80
|
+
...(extraTabs ?? []).map((t) => ({ id: t.id, label: t.label, content: t.render(row) })),
|
|
81
|
+
]}
|
|
82
|
+
/>
|
|
83
|
+
);
|
|
84
|
+
}
|