@graph8/sdk 0.370.0 → 0.374.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -3
- package/blocks/_foundation/README.md +61 -0
- package/blocks/_foundation/block.json +57 -0
- package/blocks/_foundation/components/auth-gate.tsx +363 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
- package/blocks/_foundation/components/empty-state.tsx +5 -2
- package/blocks/_foundation/components/list-select.tsx +112 -0
- package/blocks/_foundation/components/paging-notices.tsx +55 -0
- package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
- package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
- package/blocks/_foundation/components/screen/index.ts +1 -0
- package/blocks/_foundation/components/screen/split-view.tsx +1 -1
- package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
- package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
- package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
- package/blocks/_foundation/lib/paging.ts +57 -0
- package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
- package/blocks/_foundation/server/auth.ts +815 -0
- package/blocks/agents/README.md +81 -4
- package/blocks/agents/block.json +311 -2
- package/blocks/agents/components/agent-activity.tsx +112 -0
- package/blocks/agents/components/agent-approvals.tsx +135 -0
- package/blocks/agents/components/agent-builder.tsx +17 -3
- package/blocks/agents/components/agent-conversations.tsx +143 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
- package/blocks/agents/components/agent-operators.tsx +164 -0
- package/blocks/agents/components/agent-run-detail.tsx +20 -3
- package/blocks/agents/components/agent-runs.tsx +3 -3
- package/blocks/agents/components/agent-skills-panel.tsx +7 -7
- package/blocks/agents/components/agent-test-history.tsx +121 -0
- package/blocks/agents/components/agents-screen.tsx +53 -11
- package/blocks/agents/components/assistant-sessions.tsx +150 -0
- package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
- package/blocks/agents/components/from-template-button.tsx +65 -0
- package/blocks/agents/components/knowledge-browser.tsx +107 -0
- package/blocks/agents/components/knowledge-detail.tsx +156 -0
- package/blocks/agents/components/knowledge-form.tsx +69 -0
- package/blocks/agents/components/name-dialog.tsx +32 -0
- package/blocks/agents/components/receptionist-button.tsx +46 -0
- package/blocks/agents/components/save-as-template-button.tsx +32 -0
- package/blocks/agents/components/skill-editor.tsx +147 -0
- package/blocks/agents/components/skill-library.tsx +111 -0
- package/blocks/agents/lib/agent-types.ts +7 -1
- package/blocks/agents/lib/facts.tsx +13 -0
- package/blocks/agents/lib/knowledge-utils.ts +49 -0
- package/blocks/agents/lib/load-more.tsx +141 -0
- package/blocks/agents/lib/skill-utils.ts +91 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
- package/blocks/analytics/components/analytics-screen.tsx +16 -3
- package/blocks/analytics/components/kpi-tiles.tsx +1 -1
- package/blocks/analytics/components/report-builder.tsx +15 -2
- package/blocks/appointments/README.md +6 -0
- package/blocks/appointments/block.json +27 -0
- package/blocks/appointments/components/booking-page.tsx +2 -2
- package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
- package/blocks/appointments/components/meeting-detail.tsx +2 -2
- package/blocks/appointments/components/meeting-list.tsx +29 -13
- package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
- package/blocks/campaigns/README.md +1 -1
- package/blocks/campaigns/block.json +34 -1
- package/blocks/campaigns/components/campaign-assets.tsx +1 -1
- package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
- package/blocks/campaigns/components/campaign-links.tsx +180 -0
- package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
- package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
- package/blocks/campaigns/components/icp-detail.tsx +11 -2
- package/blocks/campaigns/components/icp-editor.tsx +8 -3
- package/blocks/campaigns/components/persona-detail.tsx +11 -2
- package/blocks/campaigns/components/persona-editor.tsx +6 -1
- package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
- package/blocks/coach/components/call-scorecard.tsx +7 -7
- package/blocks/company-intelligence/README.md +132 -0
- package/blocks/company-intelligence/block.json +318 -0
- package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
- package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
- package/blocks/company-intelligence/components/company-profile.tsx +68 -0
- package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
- package/blocks/company-intelligence/components/report-detail.tsx +189 -0
- package/blocks/company-intelligence/components/research-reports.tsx +136 -0
- package/blocks/company-intelligence/components/research-run.tsx +165 -0
- package/blocks/company-intelligence/components/source-detail.tsx +117 -0
- package/blocks/company-intelligence/components/source-view.tsx +274 -0
- package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
- package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
- package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
- package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
- package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
- package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
- package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
- package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
- package/blocks/copilot/components/copilot-memory.tsx +6 -2
- package/blocks/copilot/components/copilot-panel.tsx +1 -1
- package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
- package/blocks/crm/README.md +25 -0
- package/blocks/crm/block.json +257 -9
- package/blocks/crm/components/bulk-update.tsx +119 -0
- package/blocks/crm/components/companies-screen.tsx +23 -9
- package/blocks/crm/components/company-detail.tsx +2 -2
- package/blocks/crm/components/contact-detail.tsx +4 -4
- package/blocks/crm/components/contact-documents-tab.tsx +144 -0
- package/blocks/crm/components/contact-insights-tab.tsx +118 -0
- package/blocks/crm/components/contact-lookup-link.tsx +85 -0
- package/blocks/crm/components/contact-row-actions.tsx +43 -0
- package/blocks/crm/components/contact-tag-chips.tsx +113 -0
- package/blocks/crm/components/contacts-screen.tsx +77 -18
- package/blocks/crm/components/dedupe-review.tsx +6 -1
- package/blocks/crm/components/import-wizard.tsx +3 -7
- package/blocks/crm/components/list-select.tsx +20 -0
- package/blocks/crm/components/owner-assign.tsx +132 -0
- package/blocks/crm/components/record-engagement-tab.tsx +103 -0
- package/blocks/crm/components/record-form.tsx +1 -1
- package/blocks/crm/components/record-open-links.tsx +79 -0
- package/blocks/crm/components/tag-assign.tsx +112 -0
- package/blocks/crm/components/tag-manager.tsx +178 -0
- package/blocks/crm/components/tasks-screen.tsx +35 -15
- package/blocks/crm/components/unlock-info-button.tsx +82 -0
- package/blocks/crm/components/verify-email-button.tsx +93 -0
- package/blocks/crm/hooks/use-all-lists.ts +18 -0
- package/blocks/crm/hooks/use-contact-tags.ts +29 -0
- package/blocks/crm/hooks/use-page-query.ts +3 -1
- package/blocks/crm/lib/crm-types.ts +14 -0
- package/blocks/crm/lib/read-lists.ts +16 -0
- package/blocks/crm/lib/record-actions.ts +102 -0
- package/blocks/deals/README.md +25 -6
- package/blocks/deals/block.json +168 -2
- package/blocks/deals/components/close-deal-dialog.tsx +169 -0
- package/blocks/deals/components/create-deal-button.tsx +109 -0
- package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
- package/blocks/deals/components/deal-detail.tsx +7 -5
- package/blocks/deals/components/deal-filters.tsx +65 -0
- package/blocks/deals/components/deal-line-items.tsx +120 -0
- package/blocks/deals/components/deal-notes-tab.tsx +97 -0
- package/blocks/deals/components/deal-related.tsx +30 -9
- package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
- package/blocks/deals/components/deals-screen.tsx +57 -22
- package/blocks/deals/components/forecast-view.tsx +4 -4
- package/blocks/deals/components/pipeline-board.tsx +8 -6
- package/blocks/deals/components/pipeline-editor.tsx +132 -0
- package/blocks/deals/components/record-deals-tab.tsx +62 -0
- package/blocks/deals/components/stage-editor.tsx +1 -1
- package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
- package/blocks/deals/lib/deal-extras.ts +156 -0
- package/blocks/dialer/README.md +14 -5
- package/blocks/dialer/block.json +17 -2
- package/blocks/dialer/components/call-contact-button.tsx +26 -9
- package/blocks/dialer/components/call-log-screen.tsx +10 -2
- package/blocks/dialer/components/call-log.tsx +16 -4
- package/blocks/dialer/components/call-queue.tsx +3 -3
- package/blocks/dialer/components/dialer-screen.tsx +69 -27
- package/blocks/dialer/components/disposition-panel.tsx +1 -1
- package/blocks/dialer/components/record-calls-tab.tsx +101 -0
- package/blocks/dialer/components/session-start-form.tsx +58 -11
- package/blocks/dialer/components/softphone.tsx +1 -1
- package/blocks/documents/README.md +86 -0
- package/blocks/documents/block.json +378 -0
- package/blocks/documents/components/account-bar.tsx +48 -0
- package/blocks/documents/components/correspondence-form.tsx +95 -0
- package/blocks/documents/components/correspondence-import.tsx +106 -0
- package/blocks/documents/components/correspondence-panel.tsx +191 -0
- package/blocks/documents/components/documents-screen.tsx +76 -0
- package/blocks/documents/components/library-document-detail.tsx +175 -0
- package/blocks/documents/components/library-document-form.tsx +105 -0
- package/blocks/documents/components/library-folder-form.tsx +74 -0
- package/blocks/documents/components/library-panel.tsx +235 -0
- package/blocks/documents/components/library-upload.tsx +160 -0
- package/blocks/documents/components/question-row.tsx +130 -0
- package/blocks/documents/components/questionnaire-detail.tsx +197 -0
- package/blocks/documents/components/questionnaires-panel.tsx +173 -0
- package/blocks/documents/components/record-documents-tab.tsx +109 -0
- package/blocks/documents/lib/documents-types.ts +252 -0
- package/blocks/documents/lib/download.ts +64 -0
- package/blocks/documents/recipes/follow-a-questionnaire.ts +26 -0
- package/blocks/documents/recipes/log-account-correspondence.ts +27 -0
- package/blocks/documents/recipes/organize-the-library.ts +25 -0
- package/blocks/email-signatures/README.md +123 -0
- package/blocks/email-signatures/block.json +341 -0
- package/blocks/email-signatures/components/email-signatures-screen.tsx +166 -0
- package/blocks/email-signatures/components/signature-assign-template.tsx +119 -0
- package/blocks/email-signatures/components/signature-block-editor.tsx +87 -0
- package/blocks/email-signatures/components/signature-campaign-form.tsx +165 -0
- package/blocks/email-signatures/components/signature-campaigns.tsx +75 -0
- package/blocks/email-signatures/components/signature-identity-form.tsx +131 -0
- package/blocks/email-signatures/components/signature-mailboxes.tsx +195 -0
- package/blocks/email-signatures/components/signature-policy.tsx +130 -0
- package/blocks/email-signatures/components/signature-preview.tsx +119 -0
- package/blocks/email-signatures/components/signature-report.tsx +77 -0
- package/blocks/email-signatures/components/signature-template-details.tsx +112 -0
- package/blocks/email-signatures/components/signature-template-form.tsx +109 -0
- package/blocks/email-signatures/hooks/use-signatures-data.ts +93 -0
- package/blocks/email-signatures/lib/signatures-types.ts +392 -0
- package/blocks/email-signatures/recipes/assign-a-signature-to-a-new-hire.ts +32 -0
- package/blocks/email-signatures/recipes/create-a-template-and-make-it-default.ts +35 -0
- package/blocks/email-signatures/recipes/signature-engagement-report.ts +16 -0
- package/blocks/enrichment/README.md +38 -10
- package/blocks/enrichment/block.json +164 -12
- package/blocks/enrichment/components/auto-enrichment-configs.tsx +134 -0
- package/blocks/enrichment/components/auto-enrichment-settings.tsx +82 -0
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +79 -30
- package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
- package/blocks/enrichment/components/enrich-button.tsx +1 -1
- package/blocks/enrichment/components/enrichment-activity.tsx +93 -0
- package/blocks/enrichment/components/enrichment-job-detail.tsx +18 -0
- package/blocks/enrichment/components/enrichment-screen.tsx +103 -19
- package/blocks/enrichment/components/list-pipelines.tsx +97 -0
- package/blocks/enrichment/components/pipeline-detail.tsx +214 -0
- package/blocks/enrichment/components/pipeline-editor.tsx +144 -0
- package/blocks/enrichment/components/pipeline-templates.tsx +90 -0
- package/blocks/enrichment/components/waterfall-config.tsx +27 -7
- package/blocks/enrichment/lib/enrichment-types.ts +51 -0
- package/blocks/enrichment/lib/pipeline-types.ts +234 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
- package/blocks/forms/README.md +10 -0
- package/blocks/forms/block.json +107 -0
- package/blocks/forms/components/form-activity.tsx +71 -0
- package/blocks/forms/components/form-backfill.tsx +108 -0
- package/blocks/forms/components/form-builder.tsx +1 -1
- package/blocks/forms/components/form-embed.tsx +1 -1
- package/blocks/forms/components/form-groups.tsx +151 -0
- package/blocks/forms/components/form-mapping-editor.tsx +6 -9
- package/blocks/forms/components/form-route-history.tsx +72 -0
- package/blocks/forms/components/form-saved-views.tsx +81 -0
- package/blocks/forms/components/forms-screen.tsx +57 -20
- package/blocks/forms/components/more-rows.tsx +30 -0
- package/blocks/forms/components/submissions-export.tsx +67 -0
- package/blocks/forms/components/submissions-table.tsx +15 -6
- package/blocks/forms/lib/forms-extra.ts +67 -0
- package/blocks/inbox/README.md +45 -4
- package/blocks/inbox/block.json +145 -8
- package/blocks/inbox/components/blocked-senders.tsx +103 -0
- package/blocks/inbox/components/compose-message.tsx +193 -0
- package/blocks/inbox/components/inbox-screen.tsx +54 -6
- package/blocks/inbox/components/inbox-search.tsx +106 -0
- package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
- package/blocks/inbox/components/tag-manager.tsx +136 -0
- package/blocks/inbox/components/thread-actions.tsx +133 -0
- package/blocks/inbox/components/thread-badge.tsx +2 -2
- package/blocks/inbox/components/thread-view.tsx +3 -1
- package/blocks/inbox/hooks/use-inbox-mailboxes.ts +31 -0
- package/blocks/inbox/lib/inbox-manage.ts +227 -0
- package/blocks/integrations/README.md +54 -1
- package/blocks/integrations/block.json +189 -15
- package/blocks/integrations/components/ad-accounts.tsx +82 -0
- package/blocks/integrations/components/connection-health.tsx +59 -0
- package/blocks/integrations/components/crm-detail.tsx +20 -1
- package/blocks/integrations/components/crm-records.tsx +103 -0
- package/blocks/integrations/components/field-mappings.tsx +61 -0
- package/blocks/integrations/components/integration-list.tsx +4 -4
- package/blocks/integrations/components/integrations-screen.tsx +20 -3
- package/blocks/integrations/components/migration-panel.tsx +133 -0
- package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
- package/blocks/integrations/components/sync-status.tsx +6 -6
- package/blocks/integrations/components/sync-triggers.tsx +134 -0
- package/blocks/integrations/components/webhook-detail.tsx +26 -7
- package/blocks/integrations/components/webhook-manager.tsx +30 -17
- package/blocks/integrations/components/workspace-tools.tsx +127 -0
- package/blocks/integrations/lib/crm-utils.ts +39 -0
- package/blocks/integrations/lib/facts.tsx +13 -0
- package/blocks/integrations/lib/integrations-utils.ts +5 -0
- package/blocks/integrations/lib/load-more.tsx +141 -0
- package/blocks/intent/components/account-intent-score.tsx +5 -5
- package/blocks/intent/components/intent-feed.tsx +8 -8
- package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
- package/blocks/intent/lib/intent-utils.ts +4 -4
- package/blocks/lifecycle/components/account-related.tsx +8 -1
- package/blocks/lifecycle/components/churn-list.tsx +5 -2
- package/blocks/lifecycle/components/customer-table.tsx +1 -1
- package/blocks/lists/README.md +7 -1
- package/blocks/lists/block.json +31 -3
- package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
- package/blocks/lists/components/add-to-list-button.tsx +17 -7
- package/blocks/lists/components/list-picker.tsx +13 -3
- package/blocks/lists/components/list-table.tsx +1 -1
- package/blocks/lists/components/lists-screen.tsx +4 -2
- package/blocks/lists/components/record-lists-tab.tsx +99 -0
- package/blocks/lists/components/suppression-manager.tsx +27 -6
- package/blocks/lists/components/suppressions-screen.tsx +21 -12
- package/blocks/mailboxes/block.json +4 -0
- package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
- package/blocks/mailboxes/components/domain-detail.tsx +10 -4
- package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
- package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
- package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
- package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
- package/blocks/mailboxes/components/warmup-health.tsx +20 -20
- package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
- package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
- package/blocks/newsletter/README.md +12 -0
- package/blocks/newsletter/block.json +117 -0
- package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
- package/blocks/newsletter/components/issue-versions.tsx +93 -0
- package/blocks/newsletter/components/more-rows.tsx +30 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
- package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
- package/blocks/newsletter/components/newsletter-screen.tsx +38 -13
- package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
- package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
- package/blocks/newsletter/components/subscriber-table.tsx +5 -7
- package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
- package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
- package/blocks/nurture/components/nurture-builder.tsx +10 -2
- package/blocks/nurture/components/nurture-enroll.tsx +6 -7
- package/blocks/nurture/components/nurture-form.tsx +5 -8
- package/blocks/nurture/components/nurture-screen.tsx +16 -5
- package/blocks/nurture/components/nurture-stats.tsx +5 -4
- package/blocks/objects/README.md +1 -1
- package/blocks/objects/block.json +13 -5
- package/blocks/objects/components/attribute-field-input.tsx +14 -1
- package/blocks/objects/components/object-record-detail.tsx +8 -1
- package/blocks/objects/components/object-schema-editor.tsx +1 -1
- package/blocks/objects/components/record-roles-panel.tsx +15 -5
- package/blocks/pages/README.md +14 -0
- package/blocks/pages/block.json +123 -2
- package/blocks/pages/components/more-rows.tsx +30 -0
- package/blocks/pages/components/page-ai-tools.tsx +196 -0
- package/blocks/pages/components/page-analytics.tsx +176 -0
- package/blocks/pages/components/page-clone.tsx +99 -0
- package/blocks/pages/components/page-domains.tsx +167 -0
- package/blocks/pages/components/page-editor.tsx +1 -1
- package/blocks/pages/components/page-generation-status.tsx +48 -0
- package/blocks/pages/components/page-tracking.tsx +137 -0
- package/blocks/pages/components/pages-screen.tsx +57 -16
- package/blocks/pages/components/seo-report.tsx +6 -1
- package/blocks/pages/hooks/use-landing-page-paths.ts +60 -0
- package/blocks/pages/lib/studio.ts +7 -0
- package/blocks/phone-numbers/README.md +74 -0
- package/blocks/phone-numbers/block.json +262 -0
- package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
- package/blocks/phone-numbers/components/assign-number.tsx +104 -0
- package/blocks/phone-numbers/components/number-search.tsx +239 -0
- package/blocks/phone-numbers/components/number-settings.tsx +134 -0
- package/blocks/phone-numbers/components/number-usage.tsx +97 -0
- package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
- package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
- package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
- package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
- package/blocks/phone-numbers/components/release-number.tsx +126 -0
- package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
- package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
- package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
- package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
- package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
- package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
- package/blocks/pickers/README.md +23 -0
- package/blocks/pickers/block.json +103 -0
- package/blocks/pickers/components/company-picker.tsx +90 -0
- package/blocks/pickers/components/contact-picker.tsx +131 -0
- package/blocks/pickers/components/member-picker.tsx +99 -0
- package/blocks/pickers/components/search-picker.tsx +98 -0
- package/blocks/pickers/hooks/use-current-member.ts +104 -0
- package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
- package/blocks/pickers/lib/use-debounced.ts +13 -0
- package/blocks/programmatic-pages/README.md +82 -0
- package/blocks/programmatic-pages/block.json +292 -0
- package/blocks/programmatic-pages/components/programmatic-campaign-form.tsx +136 -0
- package/blocks/programmatic-pages/components/programmatic-generate.tsx +112 -0
- package/blocks/programmatic-pages/components/programmatic-generated-pages.tsx +210 -0
- package/blocks/programmatic-pages/components/programmatic-overview.tsx +84 -0
- package/blocks/programmatic-pages/components/programmatic-pages-screen.tsx +221 -0
- package/blocks/programmatic-pages/components/programmatic-preview.tsx +66 -0
- package/blocks/programmatic-pages/components/programmatic-publish.tsx +102 -0
- package/blocks/programmatic-pages/components/programmatic-settings.tsx +96 -0
- package/blocks/programmatic-pages/components/programmatic-source-fields.tsx +75 -0
- package/blocks/programmatic-pages/components/programmatic-source.tsx +106 -0
- package/blocks/programmatic-pages/components/programmatic-variables.tsx +84 -0
- package/blocks/programmatic-pages/hooks/use-programmatic-workspace.ts +19 -0
- package/blocks/programmatic-pages/lib/programmatic-types.ts +330 -0
- package/blocks/programmatic-pages/recipes/create-a-programmatic-campaign.ts +23 -0
- package/blocks/programmatic-pages/recipes/render-and-follow.ts +21 -0
- package/blocks/programmatic-pages/recipes/review-then-publish.ts +17 -0
- package/blocks/quotes/README.md +38 -1
- package/blocks/quotes/block.json +170 -6
- package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
- package/blocks/quotes/components/manual-products.tsx +144 -0
- package/blocks/quotes/components/quote-batch-actions.tsx +86 -0
- package/blocks/quotes/components/quote-builder.tsx +1 -1
- package/blocks/quotes/components/quote-documents.tsx +94 -0
- package/blocks/quotes/components/quote-evidence.tsx +95 -0
- package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
- package/blocks/quotes/components/quote-list.tsx +28 -8
- package/blocks/quotes/components/quote-related.tsx +3 -2
- package/blocks/quotes/components/quote-settings.tsx +184 -0
- package/blocks/quotes/components/quote-templates.tsx +189 -0
- package/blocks/quotes/components/quote-viewer.tsx +23 -5
- package/blocks/quotes/components/quotes-screen.tsx +15 -5
- package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
- package/blocks/quotes/lib/quote-admin.ts +264 -0
- package/blocks/radar/components/competitor-battle-card.tsx +1 -1
- package/blocks/radar/components/radar-overview.tsx +4 -4
- package/blocks/radar/components/radar-screen.tsx +2 -2
- package/blocks/sequencer/README.md +61 -0
- package/blocks/sequencer/block.json +200 -6
- package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
- package/blocks/sequencer/components/enrollment-withdraw.tsx +71 -0
- package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
- package/blocks/sequencer/components/sequence-builder.tsx +32 -23
- package/blocks/sequencer/components/sequence-campaign-link.tsx +78 -0
- package/blocks/sequencer/components/sequence-detail.tsx +10 -2
- package/blocks/sequencer/components/sequence-edit-form.tsx +111 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +5 -2
- package/blocks/sequencer/components/sequence-list.tsx +1 -1
- package/blocks/sequencer/components/sequence-manage-actions.tsx +119 -0
- package/blocks/sequencer/components/sequence-senders-panel.tsx +84 -0
- package/blocks/sequencer/components/sequence-stats.tsx +8 -8
- package/blocks/sequencer/components/sequence-templates.tsx +160 -0
- package/blocks/sequencer/components/sequences-screen.tsx +39 -6
- package/blocks/sequencer/components/step-ai-assist.tsx +196 -0
- package/blocks/sequencer/components/step-editor.tsx +21 -12
- package/blocks/sequencer/components/step-templates.tsx +224 -0
- package/blocks/sequencer/lib/sequencer-manage.ts +235 -0
- package/blocks/sequencer/lib/sequencer-utils.ts +9 -5
- package/blocks/signals/README.md +39 -0
- package/blocks/signals/block.json +287 -0
- package/blocks/signals/components/auto-seed-panel.tsx +111 -0
- package/blocks/signals/components/listener-form.tsx +184 -0
- package/blocks/signals/components/matched-posts.tsx +195 -0
- package/blocks/signals/components/post-engagements.tsx +114 -0
- package/blocks/signals/components/record-social-tab.tsx +49 -0
- package/blocks/signals/components/social-listeners-screen.tsx +250 -0
- package/blocks/signals/components/social-settings.tsx +128 -0
- package/blocks/signals/components/source-lists.tsx +83 -0
- package/blocks/signals/components/top-engagers.tsx +68 -0
- package/blocks/signals/components/x-integration.tsx +129 -0
- package/blocks/signals/lib/signals-types.ts +228 -0
- package/blocks/signals/recipes/preview-and-create-a-listener.ts +16 -0
- package/blocks/signals/recipes/spend-against-caps.ts +14 -0
- package/blocks/signals/recipes/triage-matched-posts.ts +18 -0
- package/blocks/social-ads/README.md +8 -0
- package/blocks/social-ads/block.json +137 -4
- package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
- package/blocks/social-ads/components/audience-sync.tsx +6 -2
- package/blocks/social-ads/components/campaign-ads.tsx +166 -0
- package/blocks/social-ads/components/campaign-detail.tsx +6 -1
- package/blocks/social-ads/components/campaign-editor.tsx +134 -0
- package/blocks/social-ads/components/creative-concepts.tsx +74 -0
- package/blocks/social-ads/components/creative-detail.tsx +6 -1
- package/blocks/social-ads/components/creative-gallery.tsx +20 -2
- package/blocks/social-ads/components/creative-revisions.tsx +57 -0
- package/blocks/social-ads/components/creative-tools.tsx +176 -0
- package/blocks/social-ads/components/post-composer.tsx +4 -2
- package/blocks/social-ads/components/post-detail.tsx +3 -0
- package/blocks/social-ads/components/social-ideas.tsx +64 -0
- package/blocks/social-ads/components/social-performance.tsx +80 -0
- package/blocks/social-ads/components/social-screen.tsx +33 -6
- package/blocks/social-ads/lib/ads-extra.ts +67 -0
- package/blocks/surveys/README.md +91 -0
- package/blocks/surveys/block.json +299 -0
- package/blocks/surveys/components/account-nps.tsx +69 -0
- package/blocks/surveys/components/contact-survey-history.tsx +52 -0
- package/blocks/surveys/components/org-summary.tsx +82 -0
- package/blocks/surveys/components/sending-domains.tsx +113 -0
- package/blocks/surveys/components/survey-form.tsx +126 -0
- package/blocks/surveys/components/survey-responses.tsx +145 -0
- package/blocks/surveys/components/survey-results.tsx +72 -0
- package/blocks/surveys/components/survey-send-history.tsx +58 -0
- package/blocks/surveys/components/survey-send.tsx +162 -0
- package/blocks/surveys/components/survey-triggers.tsx +166 -0
- package/blocks/surveys/components/surveys-screen.tsx +234 -0
- package/blocks/surveys/lib/surveys-types.ts +228 -0
- package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
- package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
- package/blocks/surveys/recipes/survey-score-report.ts +27 -0
- package/blocks/visitors/components/snippet-install.tsx +18 -9
- package/blocks/visitors/components/traffic-chart.tsx +7 -6
- package/blocks/visitors/components/visitor-company-card.tsx +24 -14
- package/blocks/visitors/components/visitor-feed.tsx +5 -5
- package/blocks/web-analytics/README.md +102 -0
- package/blocks/web-analytics/block.json +292 -0
- package/blocks/web-analytics/components/acquisition-tab.tsx +154 -0
- package/blocks/web-analytics/components/analytics-filters.tsx +161 -0
- package/blocks/web-analytics/components/audience-tab.tsx +135 -0
- package/blocks/web-analytics/components/conversions-tab.tsx +128 -0
- package/blocks/web-analytics/components/engagement-tab.tsx +123 -0
- package/blocks/web-analytics/components/overview-tab.tsx +102 -0
- package/blocks/web-analytics/components/pages-tab.tsx +148 -0
- package/blocks/web-analytics/components/ranked-table.tsx +103 -0
- package/blocks/web-analytics/components/realtime-tab.tsx +103 -0
- package/blocks/web-analytics/components/report-card.tsx +71 -0
- package/blocks/web-analytics/components/web-analytics-screen.tsx +90 -0
- package/blocks/web-analytics/hooks/use-report.ts +48 -0
- package/blocks/web-analytics/lib/analytics-scope.ts +155 -0
- package/blocks/web-analytics/lib/page-path.ts +14 -0
- package/blocks/web-analytics/lib/web-analytics-types.ts +187 -0
- package/blocks/web-analytics/recipes/best-landing-pages.ts +26 -0
- package/blocks/web-analytics/recipes/form-conversion-check.ts +19 -0
- package/blocks/web-analytics/recipes/weekly-traffic-report.ts +37 -0
- package/blocks/webchat/components/chat-widget-config.tsx +1 -1
- package/blocks/webchat/components/conversation-list.tsx +1 -1
- package/blocks/workbench/README.md +54 -2
- package/blocks/workbench/block.json +234 -5
- package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
- package/blocks/workbench/components/run-progress.tsx +1 -1
- package/blocks/workbench/components/workbench-auto-enrich.tsx +88 -0
- package/blocks/workbench/components/workbench-automation.tsx +36 -0
- package/blocks/workbench/components/workbench-connections.tsx +136 -0
- package/blocks/workbench/components/workbench-crm-import.tsx +91 -0
- package/blocks/workbench/components/workbench-datafile-import.tsx +154 -0
- package/blocks/workbench/components/workbench-grid.tsx +71 -8
- package/blocks/workbench/components/workbench-pipelines.tsx +173 -0
- package/blocks/workbench/components/workbench-row-actions.tsx +8 -1
- package/blocks/workbench/components/workbench-schedule.tsx +191 -0
- package/blocks/workbench/components/workbench-screen.tsx +65 -22
- package/blocks/workbench/components/workbench-send-to-sequence.tsx +116 -0
- package/blocks/workbench/components/workbench-table-list.tsx +14 -2
- package/blocks/workbench/components/workbench-table-settings.tsx +53 -0
- package/blocks/workbench/components/workbench-triggers.tsx +178 -0
- package/blocks/workbench/hooks/use-connected-crms.ts +29 -0
- package/blocks/workbench/lib/api-status.ts +8 -0
- package/blocks/workbench/lib/automation-types.ts +257 -0
- package/blocks/workflows/components/workflow-editor.tsx +1 -1
- package/dist/cli.js +744 -394
- package/dist/index.browser.js +1 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +1 -1
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +1 -1
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +1 -1
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +1 -1
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/auth.d.ts +45 -0
- package/dist/types/blocks/brand.d.ts +11 -2
- package/dist/types/blocks/create.d.ts +11 -4
- package/dist/types/blocks/doctor.d.ts +1 -0
- package/dist/types/blocks/flows.d.ts +7 -1
- package/dist/types/blocks/orgBrand.d.ts +34 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/version.d.ts +2 -2
- package/docs/api/desk.d.ts +2 -2
- package/docs/api/desk.md +3 -3
- package/docs/api/forms.md +1 -1
- package/package.json +1 -1
- package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
- package/stacks/6sense/pages/contacts.tsx.tmpl +21 -0
- package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
- package/stacks/6sense.json +37 -4
- package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
- package/stacks/apollo/pages/campaigns.tsx.tmpl +27 -0
- package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
- package/stacks/apollo/pages/contacts.tsx.tmpl +35 -95
- package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
- package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/apollo/pages/lists.tsx.tmpl +24 -0
- package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
- package/stacks/apollo/pages/sequences.tsx.tmpl +18 -0
- package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
- package/stacks/apollo.json +68 -9
- package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
- package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
- package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
- package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
- package/stacks/calendly.json +8 -4
- package/stacks/clay/pages/companies.tsx.tmpl +17 -1
- package/stacks/clay/pages/contacts.tsx.tmpl +14 -0
- package/stacks/clay/pages/lists.tsx.tmpl +24 -0
- package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
- package/stacks/clay.json +16 -3
- package/stacks/gong/pages/calls.tsx.tmpl +23 -0
- package/stacks/gong/pages/companies.tsx.tmpl +22 -0
- package/stacks/gong/pages/contacts.tsx.tmpl +34 -0
- package/stacks/gong/pages/lists.tsx.tmpl +12 -0
- package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
- package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
- package/stacks/gong.json +23 -6
- package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
- package/stacks/hootsuite/pages/contacts.tsx.tmpl +30 -0
- package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
- package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
- package/stacks/hootsuite.json +19 -4
- package/stacks/hubspot/pages/campaigns.tsx.tmpl +30 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +16 -35
- package/stacks/hubspot/pages/contacts.tsx.tmpl +35 -95
- package/stacks/hubspot/pages/deals.tsx.tmpl +14 -1
- package/stacks/hubspot/pages/forms.tsx.tmpl +36 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
- package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
- package/stacks/hubspot/pages/sequences.tsx.tmpl +18 -0
- package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
- package/stacks/hubspot/pages/web-analytics.tsx.tmpl +14 -0
- package/stacks/hubspot.json +66 -7
- package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
- package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
- package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
- package/stacks/intercom.json +18 -4
- package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
- package/stacks/mailchimp/pages/contacts.tsx.tmpl +30 -0
- package/stacks/mailchimp/pages/forms.tsx.tmpl +36 -0
- package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
- package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
- package/stacks/mailchimp.json +22 -6
- package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
- package/stacks/outreach/pages/campaigns.tsx.tmpl +27 -0
- package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
- package/stacks/outreach/pages/contacts.tsx.tmpl +29 -66
- package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
- package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
- package/stacks/outreach/pages/sequences.tsx.tmpl +18 -0
- package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
- package/stacks/outreach.json +38 -9
- package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +17 -35
- package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
- package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
- package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
- package/stacks/pipedrive.json +14 -2
- package/stacks/salesforce/pages/companies.tsx.tmpl +16 -35
- package/stacks/salesforce/pages/contacts.tsx.tmpl +26 -35
- package/stacks/salesforce/pages/deals.tsx.tmpl +14 -1
- package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
- package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
- package/stacks/salesforce.json +17 -3
- package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +16 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +24 -0
- package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +36 -3
- package/styles/styles.css +1 -1
- package/templates/auth/auth-route.ts.tmpl +16 -0
- package/templates/auth/g8.auth.anonymous.ts.tmpl +15 -0
- package/templates/auth/g8.auth.basic.ts.tmpl +17 -0
- package/templates/auth/g8.auth.jwt.ts.tmpl +23 -0
- package/templates/auth/g8.auth.password.ts.tmpl +18 -0
- package/templates/auth/g8.auth.session.ts.tmpl +23 -0
- package/templates/auth/middleware.ts.tmpl +14 -0
- package/templates/next-app/README.md.tmpl +5 -0
- package/templates/next-app/app/globals.css.tmpl +349 -64
- package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PhoneNumberList: the workspace's numbers as a table, paged on the client with a "Load more" so a long inventory is never
|
|
5
|
+
* cut off silently. It reads nothing itself: the screen passes the filtered rows from usePhoneNumbers.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations: none (rows come from usePhoneNumbers: listDialerNumbers + listAgentPhoneNumbers).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useEffect, useState } from "react";
|
|
11
|
+
import type { ReactNode } from "react";
|
|
12
|
+
import { Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
|
|
13
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
14
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
15
|
+
import { LoadMore } from "@/g8/components/paging-notices";
|
|
16
|
+
import { RecordLink } from "@/g8/components/screen/record-link";
|
|
17
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
18
|
+
import { rowActivationProps } from "@/g8/hooks/use-row-activation";
|
|
19
|
+
import { formatNumber, numberHealth, typeLabel } from "../lib/phone-numbers-utils";
|
|
20
|
+
import type { NumberRow } from "../lib/phone-numbers-utils";
|
|
21
|
+
|
|
22
|
+
export const PAGE_SIZE = 25;
|
|
23
|
+
const COLUMNS = 6;
|
|
24
|
+
|
|
25
|
+
export interface PhoneNumberListProps {
|
|
26
|
+
rows: NumberRow[];
|
|
27
|
+
loading?: boolean;
|
|
28
|
+
error?: unknown;
|
|
29
|
+
onRetry?: () => void;
|
|
30
|
+
/** The open number (E.164). */
|
|
31
|
+
selected?: string;
|
|
32
|
+
onSelect: (row: NumberRow) => void;
|
|
33
|
+
/** Extra buttons in each row's last cell. */
|
|
34
|
+
renderRowActions?: (row: NumberRow) => ReactNode;
|
|
35
|
+
/** Shown when there are no rows at all. */
|
|
36
|
+
emptyAction?: ReactNode;
|
|
37
|
+
/** Shown instead of the empty state when filters hid every row. */
|
|
38
|
+
filtered?: boolean;
|
|
39
|
+
/** Bump to go back to the first page (filters changed). */
|
|
40
|
+
resetKey?: string;
|
|
41
|
+
className?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function PhoneNumberList({ rows, loading, error, onRetry, selected, onSelect, renderRowActions, emptyAction, filtered, resetKey, className }: PhoneNumberListProps) {
|
|
45
|
+
const [shown, setShown] = useState(PAGE_SIZE);
|
|
46
|
+
useEffect(() => setShown(PAGE_SIZE), [resetKey]);
|
|
47
|
+
|
|
48
|
+
if (error) return <ApiError error={error} onRetry={onRetry} className={className} />;
|
|
49
|
+
if (loading && rows.length === 0) {
|
|
50
|
+
return (
|
|
51
|
+
<Table className={className}>
|
|
52
|
+
<TableBody>
|
|
53
|
+
{Array.from({ length: 5 }, (_, i) => (
|
|
54
|
+
<TableRow key={i}>
|
|
55
|
+
<TableCell colSpan={COLUMNS}><Skeleton className="h-5 w-full" /></TableCell>
|
|
56
|
+
</TableRow>
|
|
57
|
+
))}
|
|
58
|
+
</TableBody>
|
|
59
|
+
</Table>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (rows.length === 0) {
|
|
63
|
+
return filtered ? (
|
|
64
|
+
<EmptyState className={className} title="No numbers match" description="Clear a filter or change the search to see more." />
|
|
65
|
+
) : (
|
|
66
|
+
<EmptyState className={className} title="No phone numbers yet" description="Buy a number to call from, to give a voice agent, or as a rep's work phone." action={emptyAction} />
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
const visible = rows.slice(0, shown);
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div className={`space-y-2 ${className ?? ""}`}>
|
|
73
|
+
<Table>
|
|
74
|
+
<TableHeader>
|
|
75
|
+
<TableRow>
|
|
76
|
+
<TableHead>Number</TableHead>
|
|
77
|
+
<TableHead>Use</TableHead>
|
|
78
|
+
<TableHead>Assigned to</TableHead>
|
|
79
|
+
<TableHead>Carrier</TableHead>
|
|
80
|
+
<TableHead className="text-right">Calls today</TableHead>
|
|
81
|
+
<TableHead>Status</TableHead>
|
|
82
|
+
</TableRow>
|
|
83
|
+
</TableHeader>
|
|
84
|
+
<TableBody>
|
|
85
|
+
{visible.map((r) => {
|
|
86
|
+
const health = numberHealth(r);
|
|
87
|
+
return (
|
|
88
|
+
<TableRow key={r.number} data-state={selected === r.number ? "selected" : undefined} className="cursor-pointer" {...rowActivationProps(() => onSelect(r))}>
|
|
89
|
+
<TableCell className="font-medium tabular-nums">
|
|
90
|
+
<RecordLink param="id" id={r.number} onOpen={() => onSelect(r)}>{formatNumber(r.number)}</RecordLink>
|
|
91
|
+
</TableCell>
|
|
92
|
+
<TableCell>{typeLabel(r.inboundType)}{r.agentName ? <span className="block text-xs text-muted-foreground">{r.agentName}</span> : null}</TableCell>
|
|
93
|
+
<TableCell>{r.assignedTo ?? <span className="text-muted-foreground">-</span>}</TableCell>
|
|
94
|
+
<TableCell>{r.provider === "telnyx" ? "Telnyx" : r.provider === "twilio" ? "Twilio" : <span className="text-muted-foreground">-</span>}</TableCell>
|
|
95
|
+
<TableCell className="text-right tabular-nums">{r.callsToday ?? "-"}{r.dailyLimit ? <span className="text-xs text-muted-foreground"> / {r.dailyLimit}</span> : null}</TableCell>
|
|
96
|
+
<TableCell>
|
|
97
|
+
<span className="flex items-center justify-between gap-2">
|
|
98
|
+
<StatusPill tone={health.tone} dot>{health.label}</StatusPill>
|
|
99
|
+
{renderRowActions ? <span data-row-ignore>{renderRowActions(r)}</span> : null}
|
|
100
|
+
</span>
|
|
101
|
+
</TableCell>
|
|
102
|
+
</TableRow>
|
|
103
|
+
);
|
|
104
|
+
})}
|
|
105
|
+
</TableBody>
|
|
106
|
+
</Table>
|
|
107
|
+
<LoadMore shown={visible.length} noun={`of ${rows.length.toLocaleString()} numbers`} hasMore={visible.length < rows.length} onLoadMore={() => setShown((n) => n + PAGE_SIZE)} />
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PhoneNumbersScreen: the finished "Phone numbers" page. Every number the workspace owns, who has it, what it is for, and
|
|
5
|
+
* how to get, assign, configure and give back numbers.
|
|
6
|
+
*
|
|
7
|
+
* Composes: PhoneNumberList, PhoneNumberDetail (Overview, Assign, Usage, Settings, Release), NumberSearch (search and
|
|
8
|
+
* buy), RegisterOwnedNumber, A2pRegistration.
|
|
9
|
+
*
|
|
10
|
+
* User flows:
|
|
11
|
+
* - See the inventory at a glance (stat strip), search it and filter by use, carrier, assignment and status. The list is
|
|
12
|
+
* paged on the client ("Load more"); the API returns the whole inventory, so nothing is cut off.
|
|
13
|
+
* - Open a number (deep link `?id=<number>`, `?tab=`, full page with `?view=full`): assign it to a teammate (MemberPicker,
|
|
14
|
+
* "Assign to me", unassign), see today's usage against the daily limit, change what answers it, verify a Telnyx caller
|
|
15
|
+
* ID, see who answers, remove its voice routing (confirmed) or release it (type the number, then confirm).
|
|
16
|
+
* - "Buy a number" opens a sheet: search a carrier by area code, digits or city, choose one, and buy it. The purchase is
|
|
17
|
+
* billable, so it opens ConfirmSpendDialog with the price first (an estimate: the API has no price read).
|
|
18
|
+
* - "Add a number you own" registers an owned number with the voice platform (spends nothing).
|
|
19
|
+
* - Text registration tab: A2P 10DLC status, a draft of the business details, a confirmed submit, and a retry for stuck numbers.
|
|
20
|
+
*
|
|
21
|
+
* SDK operations: none of its own. The list is read by usePhoneNumbers; billable, external and destructive calls live in the
|
|
22
|
+
* composed parts, each of which opens ConfirmSpendDialog itself.
|
|
23
|
+
*
|
|
24
|
+
* Billing: numbers the platform provisioned for you (the onboarding receptionist line) are never rental-billed, and nothing
|
|
25
|
+
* here labels any number as billable. The purchase dialog is the only place a price appears.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { useState } from "react";
|
|
29
|
+
import type { ReactNode } from "react";
|
|
30
|
+
import { Phone, Plus } from "lucide-react";
|
|
31
|
+
import { Button } from "@graph8/sdk/ui";
|
|
32
|
+
import { FormSheet } from "@/g8/components/screen/form-sheet";
|
|
33
|
+
import { SelectFilter } from "@/g8/components/screen/filter-controls";
|
|
34
|
+
import { ScreenLayout } from "@/g8/components/screen/screen-layout";
|
|
35
|
+
import { SplitView } from "@/g8/components/screen/split-view";
|
|
36
|
+
import type { StatItem } from "@/g8/components/screen/stat-strip";
|
|
37
|
+
import { Toolbar } from "@/g8/components/screen/toolbar";
|
|
38
|
+
import { useUrlFilters } from "@/g8/hooks/use-url-filters";
|
|
39
|
+
import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
40
|
+
import { usePhoneNumbers } from "../hooks/use-phone-numbers";
|
|
41
|
+
import { digits, numberHealth, typeLabel, INBOUND_TYPES } from "../lib/phone-numbers-utils";
|
|
42
|
+
import type { NumberRow } from "../lib/phone-numbers-utils";
|
|
43
|
+
import { A2pRegistration } from "./a2p-registration";
|
|
44
|
+
import { NumberSearch } from "./number-search";
|
|
45
|
+
import { PhoneNumberDetail } from "./phone-number-detail";
|
|
46
|
+
import { PhoneNumberList } from "./phone-number-list";
|
|
47
|
+
import { RegisterOwnedNumber } from "./register-owned-number";
|
|
48
|
+
|
|
49
|
+
export interface PhoneNumbersScreenProps {
|
|
50
|
+
/** Extra buttons in the toolbar. */
|
|
51
|
+
toolbarActions?: ReactNode;
|
|
52
|
+
/** Extra buttons in each row. */
|
|
53
|
+
renderRowActions?: (row: NumberRow) => ReactNode;
|
|
54
|
+
/** Extra buttons in the open number's header. */
|
|
55
|
+
renderDetailActions?: (row: NumberRow) => ReactNode;
|
|
56
|
+
/** Extra tabs in the open number's panel. */
|
|
57
|
+
extraDetailTabs?: Array<{ id: string; label: string; render: (row: NumberRow) => ReactNode }>;
|
|
58
|
+
className?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const TABS = [
|
|
62
|
+
{ id: "numbers", label: "Numbers" },
|
|
63
|
+
{ id: "texting", label: "Text registration" },
|
|
64
|
+
];
|
|
65
|
+
const FILTERS = ["q", "use", "carrier", "assigned", "status"] as const;
|
|
66
|
+
|
|
67
|
+
/** Rows that match the filters (all client-side: the API returns the whole inventory). */
|
|
68
|
+
export function filterNumbers(rows: NumberRow[], f: Record<(typeof FILTERS)[number], string>): NumberRow[] {
|
|
69
|
+
const q = f.q.trim().toLowerCase();
|
|
70
|
+
const qDigits = digits(f.q);
|
|
71
|
+
return rows.filter((r) => {
|
|
72
|
+
if (q) {
|
|
73
|
+
const hay = [r.assignedTo, r.agentName, typeLabel(r.inboundType)].join(" ").toLowerCase();
|
|
74
|
+
if (!(hay.includes(q) || (qDigits && digits(r.number).includes(qDigits)))) return false;
|
|
75
|
+
}
|
|
76
|
+
if (f.use && r.inboundType !== f.use) return false;
|
|
77
|
+
if (f.carrier && r.provider !== f.carrier) return false;
|
|
78
|
+
if (f.assigned === "assigned" && !r.assignedTo) return false;
|
|
79
|
+
if (f.assigned === "unassigned" && r.assignedTo) return false;
|
|
80
|
+
if (f.status && numberHealth(r).label !== f.status) return false;
|
|
81
|
+
return true;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function PhoneNumbersScreen({ toolbarActions, renderRowActions, renderDetailActions, extraDetailTabs, className }: PhoneNumbersScreenProps) {
|
|
86
|
+
const [tab, setTab] = useUrlState("tab", "numbers");
|
|
87
|
+
const [selected, setSelected] = useUrlState("id", "");
|
|
88
|
+
const [detailTab, setDetailTab] = useUrlState("dtab", "");
|
|
89
|
+
const [view, setView] = useUrlState("view", "");
|
|
90
|
+
const [sheet, setSheet] = useState<"buy" | "own" | null>(null);
|
|
91
|
+
const numbers = usePhoneNumbers();
|
|
92
|
+
const uf = useUrlFilters(FILTERS, { prefix: "n_" });
|
|
93
|
+
const rows = filterNumbers(numbers.rows, uf.filters);
|
|
94
|
+
const open = numbers.rows.find((r) => digits(r.number) === digits(selected));
|
|
95
|
+
|
|
96
|
+
const close = () => { setSelected(null); setDetailTab(null); setView(null); };
|
|
97
|
+
const openRow = (r: NumberRow) => { setSelected(r.number); setDetailTab(null); setView(null); };
|
|
98
|
+
const assigned = numbers.rows.filter((r) => r.assignedTo).length;
|
|
99
|
+
const stats: StatItem[] = numbers.loaded
|
|
100
|
+
? [
|
|
101
|
+
{ label: "Numbers", value: numbers.rows.length.toLocaleString() },
|
|
102
|
+
{ label: "Assigned", value: assigned.toLocaleString() },
|
|
103
|
+
{ label: "Unassigned", value: (numbers.rows.length - assigned).toLocaleString(), tone: numbers.rows.length - assigned > 0 ? "warning" : undefined },
|
|
104
|
+
{ label: "Calls today", value: numbers.rows.reduce((n, r) => n + (r.callsToday ?? 0), 0).toLocaleString() },
|
|
105
|
+
]
|
|
106
|
+
: [];
|
|
107
|
+
|
|
108
|
+
const actions = (
|
|
109
|
+
<>
|
|
110
|
+
<Button variant="outline" onClick={() => setSheet("own")}>Add a number you own</Button>
|
|
111
|
+
<Button onClick={() => setSheet("buy")}>
|
|
112
|
+
<Plus className="mr-1 h-4 w-4" aria-hidden /> Buy a number
|
|
113
|
+
</Button>
|
|
114
|
+
</>
|
|
115
|
+
);
|
|
116
|
+
const resetKey = JSON.stringify(uf.filters);
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<ScreenLayout
|
|
120
|
+
className={className}
|
|
121
|
+
title="Phone numbers"
|
|
122
|
+
description="The numbers you call from, answer on and text from: buy, assign, configure and release."
|
|
123
|
+
icon={<Phone className="h-5 w-5" aria-hidden />}
|
|
124
|
+
actions={actions}
|
|
125
|
+
tabs={TABS}
|
|
126
|
+
activeTab={TABS.some((t) => t.id === tab) ? tab : "numbers"}
|
|
127
|
+
onTabChange={(id) => { setTab(id === "numbers" ? null : id); close(); }}
|
|
128
|
+
stats={stats.length > 0 ? stats : undefined}
|
|
129
|
+
statsLoading={numbers.loading && !numbers.loaded}
|
|
130
|
+
>
|
|
131
|
+
{tab === "texting" ? (
|
|
132
|
+
<A2pRegistration />
|
|
133
|
+
) : (
|
|
134
|
+
<div className="space-y-3">
|
|
135
|
+
<Toolbar
|
|
136
|
+
search={{ value: uf.draft.q, onChange: (v) => uf.set("q", v), debounceMs: 0, placeholder: "Search by number, person or agent" }}
|
|
137
|
+
actions={toolbarActions}
|
|
138
|
+
filters={
|
|
139
|
+
<>
|
|
140
|
+
<SelectFilter label="use" value={uf.draft.use} onChange={(v) => uf.set("use", v)} any="Any use" options={INBOUND_TYPES.map((t) => ({ value: t, label: typeLabel(t) }))} />
|
|
141
|
+
<SelectFilter label="carrier" value={uf.draft.carrier} onChange={(v) => uf.set("carrier", v)} any="Any carrier" options={[{ value: "twilio", label: "Twilio" }, { value: "telnyx", label: "Telnyx" }]} />
|
|
142
|
+
<SelectFilter label="assignment" value={uf.draft.assigned} onChange={(v) => uf.set("assigned", v)} any="Assigned or not" options={[{ value: "assigned", label: "Assigned" }, { value: "unassigned", label: "Unassigned" }]} />
|
|
143
|
+
<SelectFilter label="status" value={uf.draft.status} onChange={(v) => uf.set("status", v)} any="Any status" options={["Ready", "Near limit", "At daily limit", "Cannot dial"]} />
|
|
144
|
+
</>
|
|
145
|
+
}
|
|
146
|
+
/>
|
|
147
|
+
{numbers.agentsUnavailable ? (
|
|
148
|
+
<p role="note" className="text-xs text-muted-foreground">
|
|
149
|
+
Numbers held by voice agents could not be read with this key, so they may be missing from the list.
|
|
150
|
+
</p>
|
|
151
|
+
) : null}
|
|
152
|
+
<SplitView
|
|
153
|
+
expanded={view === "full"}
|
|
154
|
+
onExpandedChange={(e) => setView(e ? "full" : null)}
|
|
155
|
+
detailOpen={Boolean(selected)}
|
|
156
|
+
onCloseDetail={close}
|
|
157
|
+
list={
|
|
158
|
+
<PhoneNumberList
|
|
159
|
+
rows={rows}
|
|
160
|
+
loading={numbers.loading && !numbers.loaded}
|
|
161
|
+
error={numbers.error}
|
|
162
|
+
onRetry={numbers.refetch}
|
|
163
|
+
selected={open?.number}
|
|
164
|
+
onSelect={openRow}
|
|
165
|
+
renderRowActions={renderRowActions}
|
|
166
|
+
filtered={uf.active}
|
|
167
|
+
resetKey={resetKey}
|
|
168
|
+
emptyAction={<Button onClick={() => setSheet("buy")}>Buy a number</Button>}
|
|
169
|
+
/>
|
|
170
|
+
}
|
|
171
|
+
detail={
|
|
172
|
+
selected && open ? (
|
|
173
|
+
<PhoneNumberDetail
|
|
174
|
+
key={open.number}
|
|
175
|
+
row={open}
|
|
176
|
+
tab={detailTab || undefined}
|
|
177
|
+
onTabChange={(t) => setDetailTab(t === "overview" ? null : t)}
|
|
178
|
+
onClose={close}
|
|
179
|
+
onExpand={view === "full" ? undefined : () => setView("full")}
|
|
180
|
+
onCollapse={view === "full" ? () => setView(null) : undefined}
|
|
181
|
+
onChanged={numbers.refetch}
|
|
182
|
+
onReleased={() => { close(); numbers.refetch(); }}
|
|
183
|
+
renderActions={renderDetailActions}
|
|
184
|
+
extraTabs={extraDetailTabs}
|
|
185
|
+
/>
|
|
186
|
+
) : selected && numbers.loaded ? (
|
|
187
|
+
<p className="p-4 text-sm text-muted-foreground">No number matches this link. It may have been released.</p>
|
|
188
|
+
) : null
|
|
189
|
+
}
|
|
190
|
+
/>
|
|
191
|
+
</div>
|
|
192
|
+
)}
|
|
193
|
+
<FormSheet width="xl" open={sheet === "buy"} onOpenChange={(o) => !o && setSheet(null)} title="Buy a number" description="Search for a number, choose one, and confirm the purchase.">
|
|
194
|
+
{sheet === "buy" ? <NumberSearch onPurchased={() => numbers.refetch()} /> : null}
|
|
195
|
+
</FormSheet>
|
|
196
|
+
<FormSheet width="md" open={sheet === "own"} onOpenChange={(o) => !o && setSheet(null)} title="Add a number you own" description="Register a number you already hold with the voice platform.">
|
|
197
|
+
{sheet === "own" ? <RegisterOwnedNumber onRegistered={() => { setSheet(null); numbers.refetch(); }} /> : null}
|
|
198
|
+
</FormSheet>
|
|
199
|
+
</ScreenLayout>
|
|
200
|
+
);
|
|
201
|
+
}
|