@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,361 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DialerScreen: the whole calling page for a rep. Tabs: Session (pick a list, create a session, dial batch by batch,
|
|
5
|
+
* watch live calls, take them in the browser, record outcomes, read the transcript), Sessions (every session as a list;
|
|
6
|
+
* a row opens the full session record with stats, calls, queue, pause / resume / stop; ?tab=sessions&open=<id>&view=full
|
|
7
|
+
* is the full page) and History (the rep's past calls).
|
|
8
|
+
*
|
|
9
|
+
* Flow: choose a list (ListPicker) -> SessionStartForm (caller ID, power or parallel mode; creating it dials nobody)
|
|
10
|
+
* -> CallQueue ("Dial next batch" confirms each batch; pause, stop with confirm, skip contacts, hang up, mute, voicemail
|
|
11
|
+
* drop with confirm) -> Softphone ("Enable calling" confirms the one-hour credential) -> DispositionPanel for the call
|
|
12
|
+
* that just finished -> TranscriptViewer. Resume a session with `?session=<id>`.
|
|
13
|
+
*
|
|
14
|
+
* SDK operations (all through the composed parts): see CallQueue, SessionStartForm, Softphone, DispositionPanel, CallLog.
|
|
15
|
+
* External and destructive steps always open ConfirmSpendDialog; nothing dials by itself and there is no auto-advance.
|
|
16
|
+
* The browser audio vendor is `transport`; by default the Twilio adapter loads on demand (needs @twilio/voice-sdk).
|
|
17
|
+
* Buying or verifying a caller ID number happens in the graph8 app.
|
|
18
|
+
*/
|
|
19
|
+
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
|
20
|
+
import { Phone } from "lucide-react";
|
|
21
|
+
import { Button, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
|
|
22
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
23
|
+
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
24
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
25
|
+
import { useCurrentMember } from "@/g8/pickers/hooks/use-current-member";
|
|
26
|
+
import { MemberPicker } from "@/g8/pickers/components/member-picker";
|
|
27
|
+
import type { MemberOption } from "@/g8/pickers/components/member-picker";
|
|
28
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
29
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
30
|
+
import { ScreenLayout } from "@/g8/components/screen/screen-layout";
|
|
31
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
32
|
+
import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
33
|
+
import { useUrlFilters } from "@/g8/hooks/use-url-filters";
|
|
34
|
+
import { ListSelect } from "@/g8/crm/components/list-select";
|
|
35
|
+
import { MoreFilters, RangeFilter, SelectFilter, TextFilter } from "@/g8/components/screen/filter-controls";
|
|
36
|
+
import { ListPicker, type ListSummary } from "@/g8/lists/components/list-picker";
|
|
37
|
+
import { SplitView } from "@/g8/components/screen/split-view";
|
|
38
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
39
|
+
import { Toolbar } from "@/g8/components/screen/toolbar";
|
|
40
|
+
import { DialerSessionDetail } from "./dialer-session-detail";
|
|
41
|
+
import { dataOf, isLiveCall, normalizeStatus, needsDisposition, type LiveCall, type NextBatchResult, type SessionDetail } from "../lib/dialer-types";
|
|
42
|
+
import type { SoftphoneTransport } from "../lib/softphone-transport";
|
|
43
|
+
import { CallHistory } from "./call-log-screen";
|
|
44
|
+
import { CallQueue } from "./call-queue";
|
|
45
|
+
import { DispositionPanel } from "./disposition-panel";
|
|
46
|
+
import { Softphone } from "./softphone";
|
|
47
|
+
import { TranscriptViewer } from "./transcript-viewer";
|
|
48
|
+
import { rowActivationProps } from "@/g8/hooks/use-row-activation";
|
|
49
|
+
import { RecordLink } from "@/g8/components/screen/record-link";
|
|
50
|
+
|
|
51
|
+
export interface DialerScreenProps {
|
|
52
|
+
/**
|
|
53
|
+
* The rep these calls belong to. Optional: when omitted the rep is the signed-in team member (GET /team-members/me,
|
|
54
|
+
* a personal API key) and a MemberPicker switches rep. A passed `userId` always wins and hides the switcher.
|
|
55
|
+
*/
|
|
56
|
+
userId?: string;
|
|
57
|
+
userEmail?: string;
|
|
58
|
+
/** Skip the list picker. */
|
|
59
|
+
listId?: string;
|
|
60
|
+
listTitle?: string;
|
|
61
|
+
/** Resume this session. */
|
|
62
|
+
sessionId?: string;
|
|
63
|
+
/** Your audio client. Default: the Twilio adapter, loaded on demand. */
|
|
64
|
+
transport?: SoftphoneTransport;
|
|
65
|
+
toolbarActions?: ReactNode;
|
|
66
|
+
renderRowActions?: (call: LiveCall) => ReactNode;
|
|
67
|
+
renderDetailActions?: (call: LiveCall) => ReactNode;
|
|
68
|
+
extraDetailTabs?: Array<{ id: string; label: string; render: (call: LiveCall) => ReactNode }>;
|
|
69
|
+
className?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const TABS = [
|
|
73
|
+
{ id: "session", label: "Session" },
|
|
74
|
+
{ id: "sessions", label: "Sessions" },
|
|
75
|
+
{ id: "history", label: "History" },
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
interface SessionRow {
|
|
79
|
+
session_id: string;
|
|
80
|
+
name?: string | null;
|
|
81
|
+
status?: string | null;
|
|
82
|
+
total_calls?: number | null;
|
|
83
|
+
user_email?: string | null;
|
|
84
|
+
from_phone?: string | null;
|
|
85
|
+
created_at?: unknown;
|
|
86
|
+
session_metadata?: Record<string, unknown> | null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const STATUSES = ["ACTIVE", "PAUSED", "COMPLETED"];
|
|
90
|
+
|
|
91
|
+
function sessionTone(status?: string | null) {
|
|
92
|
+
const s = normalizeStatus(status);
|
|
93
|
+
return s === "active" ? "positive" : s === "paused" ? "warning" : "neutral";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function created(v: unknown): string {
|
|
97
|
+
const d = typeof v === "string" ? new Date(v) : null;
|
|
98
|
+
return d && !Number.isNaN(d.getTime()) ? d.toLocaleString(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }) : "";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** All dialing sessions (GET /voice/dialer/sessions). A row opens the full session record: ?tab=sessions&open=<id>[&view=full]. */
|
|
102
|
+
/** Every filter the sessions API takes (user_email comes from the screen's `userEmail`). */
|
|
103
|
+
const SESSION_FILTERS = ["name", "status", "campaign_id", "list_id", "list_name", "date_from", "date_to", "sort_by", "sort_order"] as const;
|
|
104
|
+
const SESSION_MORE = ["campaign_id", "list_id", "list_name", "date_from", "date_to", "sort_by", "sort_order"] as const;
|
|
105
|
+
|
|
106
|
+
function SessionsTab({ userEmail, onWork }: { userEmail?: string; onWork: (id: string) => void }) {
|
|
107
|
+
// Its own param: `session` belongs to the Session tab (the running session); closing a detail here must never clear it.
|
|
108
|
+
const [openId, setOpenId] = useUrlState("open");
|
|
109
|
+
const [view, setView] = useUrlState("view");
|
|
110
|
+
const [page, setPage] = useState(1);
|
|
111
|
+
// Every filter the sessions API takes lives in the URL (`s_campaign_id=...`): status, name, campaign, list, dates, sort.
|
|
112
|
+
const uf = useUrlFilters(SESSION_FILTERS, { prefix: "s_" });
|
|
113
|
+
const { status, name } = uf.draft;
|
|
114
|
+
const f = uf.filters;
|
|
115
|
+
const search = f.name.trim();
|
|
116
|
+
const q = useG8Query(
|
|
117
|
+
async (signal) => {
|
|
118
|
+
const env = await g8Get("/voice/dialer/sessions", {
|
|
119
|
+
page, page_size: 25, status: f.status || undefined, name: search || undefined, user_email: userEmail,
|
|
120
|
+
campaign_id: f.campaign_id || undefined, list_id: f.list_id || undefined, list_name: f.list_name || undefined,
|
|
121
|
+
date_from: f.date_from ? `${f.date_from}T00:00:00Z` : undefined, date_to: f.date_to ? `${f.date_to}T23:59:59Z` : undefined,
|
|
122
|
+
sort_by: f.sort_by || undefined, sort_order: f.sort_by ? f.sort_order || undefined : undefined,
|
|
123
|
+
}, { signal });
|
|
124
|
+
const d = dataOf<{ sessions?: SessionRow[]; pagination?: { total_items?: number; total_pages?: number } }>(env);
|
|
125
|
+
return { rows: d.sessions ?? [], total: d.pagination?.total_items, pages: d.pagination?.total_pages };
|
|
126
|
+
},
|
|
127
|
+
[page, f, search, userEmail],
|
|
128
|
+
{ enabled: uf.ready },
|
|
129
|
+
);
|
|
130
|
+
const rows = q.data?.rows ?? [];
|
|
131
|
+
useEffect(() => { setPage(1); }, [f]);
|
|
132
|
+
const list = (
|
|
133
|
+
<div className="space-y-3">
|
|
134
|
+
<Toolbar
|
|
135
|
+
search={{ value: name, onChange: (v) => uf.set("name", v), debounceMs: 0, placeholder: "Search by session name" }}
|
|
136
|
+
filters={
|
|
137
|
+
<>
|
|
138
|
+
<select aria-label="Session status" className="h-8 rounded-md border border-border bg-background px-2 text-sm" value={status} onChange={(e) => uf.set("status", e.target.value)}>
|
|
139
|
+
<option value="">Any status</option>
|
|
140
|
+
{STATUSES.map((s) => <option key={s} value={s}>{s.toLowerCase()}</option>)}
|
|
141
|
+
</select>
|
|
142
|
+
<MoreFilters activeCount={SESSION_MORE.filter((k) => uf.draft[k]).length}>
|
|
143
|
+
<TextFilter label="campaign" placeholder="Campaign ID" value={uf.draft.campaign_id} onChange={(v) => uf.set("campaign_id", v)} className="w-40" />
|
|
144
|
+
<ListSelect label="Filter by list" emptyOption="Any list" value={uf.draft.list_id} valueLabel={`List ${uf.draft.list_id}`} pageSize={100} onChange={(l) => uf.set("list_id", l ? String(l.id) : "")} />
|
|
145
|
+
<TextFilter label="list name" placeholder="List name" value={uf.draft.list_name} onChange={(v) => uf.set("list_name", v)} className="w-40" />
|
|
146
|
+
<RangeFilter label="Created" type="date" min={uf.draft.date_from} max={uf.draft.date_to} onMinChange={(v) => uf.set("date_from", v)} onMaxChange={(v) => uf.set("date_to", v)} />
|
|
147
|
+
<SelectFilter label="sort" any="Newest first" value={uf.draft.sort_by} onChange={(v) => uf.set("sort_by", v)} options={[{ value: "created_at", label: "Created" }, { value: "updated_at", label: "Last updated" }, { value: "total_calls", label: "Calls" }]} />
|
|
148
|
+
<SelectFilter label="sort order" any="Descending" value={uf.draft.sort_order} onChange={(v) => uf.set("sort_order", v)} options={[{ value: "asc", label: "Ascending" }]} />
|
|
149
|
+
</MoreFilters>
|
|
150
|
+
{uf.active ? <Button variant="ghost" size="sm" onClick={uf.clear}>Clear filters</Button> : null}
|
|
151
|
+
</>
|
|
152
|
+
}
|
|
153
|
+
/>
|
|
154
|
+
{q.error ? (
|
|
155
|
+
<ApiError error={q.error} title="Could not load sessions" onRetry={() => void q.refetch()} />
|
|
156
|
+
) : q.loading && !q.data ? (
|
|
157
|
+
<Skeleton className="h-32 w-full" aria-label="Loading sessions" />
|
|
158
|
+
) : rows.length === 0 ? (
|
|
159
|
+
<EmptyState icon={<Phone className="size-6" />} title={uf.active ? "No sessions match" : "No sessions yet"} description={uf.active ? "Try a different search or fewer filters." : "Start one in the Session tab. Each session keeps its queue, calls and outcomes."} />
|
|
160
|
+
) : (
|
|
161
|
+
<Table aria-label="Dialing sessions">
|
|
162
|
+
<TableHeader><TableRow><TableHead>Session</TableHead><TableHead>Status</TableHead><TableHead>Calls</TableHead><TableHead>Rep</TableHead><TableHead>Started</TableHead></TableRow></TableHeader>
|
|
163
|
+
<TableBody>
|
|
164
|
+
{rows.map((r) => (
|
|
165
|
+
<TableRow key={r.session_id} data-state={openId === r.session_id ? "selected" : undefined} className="cursor-pointer" {...rowActivationProps(() => setOpenId(r.session_id))}>
|
|
166
|
+
<TableCell className="font-medium"><RecordLink param="open" id={r.session_id} onOpen={() => setOpenId(r.session_id)}>{r.name ?? "Dialing session"}</RecordLink></TableCell>
|
|
167
|
+
<TableCell><StatusPill tone={sessionTone(r.status)}>{normalizeStatus(r.status) || "unknown"}</StatusPill></TableCell>
|
|
168
|
+
<TableCell className="tabular-nums">{r.total_calls ?? ""}</TableCell>
|
|
169
|
+
<TableCell>{r.user_email ?? ""}</TableCell>
|
|
170
|
+
<TableCell>{created(r.created_at)}</TableCell>
|
|
171
|
+
</TableRow>
|
|
172
|
+
))}
|
|
173
|
+
</TableBody>
|
|
174
|
+
</Table>
|
|
175
|
+
)}
|
|
176
|
+
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
|
177
|
+
<span>{q.data?.total != null ? `${q.data.total.toLocaleString()} sessions` : `Page ${page}`}</span>
|
|
178
|
+
<div className="flex gap-2">
|
|
179
|
+
<Button size="sm" variant="outline" disabled={page <= 1} onClick={() => setPage(page - 1)}>Previous</Button>
|
|
180
|
+
<Button size="sm" variant="outline" disabled={q.data?.pages != null ? page >= q.data.pages : rows.length < 25} onClick={() => setPage(page + 1)}>Next</Button>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
const expanded = view === "full" && Boolean(openId);
|
|
186
|
+
return (
|
|
187
|
+
<SplitView
|
|
188
|
+
list={list}
|
|
189
|
+
detailOpen={Boolean(openId)}
|
|
190
|
+
onCloseDetail={() => { setOpenId(null); setView(null); }}
|
|
191
|
+
expanded={expanded}
|
|
192
|
+
onExpandedChange={(next) => setView(next ? "full" : null)}
|
|
193
|
+
detail={openId ? (
|
|
194
|
+
<DialerSessionDetail
|
|
195
|
+
key={openId}
|
|
196
|
+
sessionId={openId}
|
|
197
|
+
userEmail={userEmail}
|
|
198
|
+
onWork={onWork}
|
|
199
|
+
onClose={() => { setOpenId(null); setView(null); }}
|
|
200
|
+
onExpand={expanded ? undefined : () => setView("full")}
|
|
201
|
+
onCollapse={expanded ? () => setView(null) : undefined}
|
|
202
|
+
onChanged={() => void q.refetch()}
|
|
203
|
+
/>
|
|
204
|
+
) : null}
|
|
205
|
+
/>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** The Twilio adapter, created once in the browser; null until loaded or when the optional peer is missing. */
|
|
210
|
+
function useDefaultTransport(provided?: SoftphoneTransport): SoftphoneTransport | null {
|
|
211
|
+
const [loaded, setLoaded] = useState<SoftphoneTransport | null>(null);
|
|
212
|
+
useEffect(() => {
|
|
213
|
+
if (provided) return;
|
|
214
|
+
let live = true;
|
|
215
|
+
import("../lib/twilio-transport").then((m) => { if (live) setLoaded(m.createTwilioTransport()); }).catch(() => undefined);
|
|
216
|
+
return () => { live = false; };
|
|
217
|
+
}, [provided]);
|
|
218
|
+
return provided ?? loaded;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Every field of the session in one card; re-read when the call list changes. */
|
|
222
|
+
function SessionDetails({ sessionId, version }: { sessionId: string; version: number }) {
|
|
223
|
+
const q = useG8Query(async (signal) => dataOf<SessionDetail>(await g8Get(`/dialer/sessions/${encodeURIComponent(sessionId)}`, undefined, { signal })), [sessionId, version]);
|
|
224
|
+
const d = q.data;
|
|
225
|
+
if (q.error) return <ApiError error={q.error} title="Could not load the session" onRetry={() => void q.refetch()} />;
|
|
226
|
+
if (!d) return <Skeleton className="h-24 w-full" aria-label="Loading session details" />;
|
|
227
|
+
const dash = "";
|
|
228
|
+
return (
|
|
229
|
+
<SectionCard title="Session details">
|
|
230
|
+
<KeyValueList
|
|
231
|
+
columns={1}
|
|
232
|
+
items={[
|
|
233
|
+
{ label: "Name", value: d.name ?? dash },
|
|
234
|
+
{ label: "Status", value: String(d.status ?? dash).toLowerCase() },
|
|
235
|
+
{ label: "List", value: d.list_title ?? d.list_id ?? dash },
|
|
236
|
+
{ label: "Mode", value: d.dialing_mode ? `${d.dialing_mode}${d.dialing_mode === "parallel" ? ` (${Math.min(4, Math.max(1, d.parallel_count ?? 2))} at once)` : ""}` : dash },
|
|
237
|
+
{ label: "Caller ID", value: d.from_phone ?? dash },
|
|
238
|
+
{ label: "Contacts", value: d.counts?.total ?? dash },
|
|
239
|
+
{ label: "Dialed", value: d.counts?.total_dialed ?? dash },
|
|
240
|
+
{ label: "Left in queue", value: d.counts?.queue_remaining ?? dash },
|
|
241
|
+
{ label: "Voicemails", value: d.live_state?.voicemail_calls ?? dash },
|
|
242
|
+
]}
|
|
243
|
+
/>
|
|
244
|
+
</SectionCard>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function SessionTab(props: DialerScreenProps & { onCalls: (calls: LiveCall[]) => void }) {
|
|
249
|
+
const [sessionId, setSessionId] = useUrlState("session", props.sessionId ?? "");
|
|
250
|
+
const [list, setList] = useState<{ id?: string; title?: string } | null>(props.listId ? { id: props.listId, title: props.listTitle } : null);
|
|
251
|
+
const [calls, setCalls] = useState<LiveCall[]>([]);
|
|
252
|
+
const [selected, setSelected] = useState<LiveCall | null>(null);
|
|
253
|
+
const [batch, setBatch] = useState<NextBatchResult | null>(null);
|
|
254
|
+
const transport = useDefaultTransport(props.transport);
|
|
255
|
+
const waiting = useMemo(() => calls.find(needsDisposition) ?? null, [calls]);
|
|
256
|
+
const focus = selected ?? waiting;
|
|
257
|
+
const { onCalls } = props;
|
|
258
|
+
useEffect(() => { onCalls(calls); }, [calls, onCalls]);
|
|
259
|
+
|
|
260
|
+
if (!sessionId && !list) {
|
|
261
|
+
return (
|
|
262
|
+
<SectionCard title="Who do you want to call?" description="Pick a contact list. Creating a session dials nobody; each batch is confirmed before it goes out.">
|
|
263
|
+
<ListPicker type="contacts" allowCreate={false} onChange={(l: ListSummary) => setList({ id: String(l.id), title: l.title })} />
|
|
264
|
+
</SectionCard>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
return (
|
|
268
|
+
<div className="grid gap-4 lg:grid-cols-[minmax(0,3fr)_minmax(0,2fr)]">
|
|
269
|
+
<CallQueue
|
|
270
|
+
sessionId={sessionId || undefined}
|
|
271
|
+
userId={props.userId}
|
|
272
|
+
userEmail={props.userEmail}
|
|
273
|
+
listId={list?.id}
|
|
274
|
+
listTitle={list?.title}
|
|
275
|
+
onSessionCreated={setSessionId}
|
|
276
|
+
onBatch={setBatch}
|
|
277
|
+
onCalls={setCalls}
|
|
278
|
+
onSelectCall={setSelected}
|
|
279
|
+
/>
|
|
280
|
+
<div className="space-y-4">
|
|
281
|
+
{props.userId && transport ? (
|
|
282
|
+
<Softphone key={props.userId} userId={props.userId} transport={transport} conferenceName={batch?.conference_name} joinMuted={(batch?.batch?.length ?? 1) > 1} />
|
|
283
|
+
) : props.userId ? (
|
|
284
|
+
<Skeleton className="h-24 w-full" aria-label="Loading softphone" />
|
|
285
|
+
) : (
|
|
286
|
+
<SectionCard title="Softphone"><p className="text-sm text-muted-foreground">Choose the rep above to take calls in this browser. A personal API key is the rep automatically; an organisation key is not a person.</p></SectionCard>
|
|
287
|
+
)}
|
|
288
|
+
{sessionId ? (
|
|
289
|
+
<>
|
|
290
|
+
<SessionDetails sessionId={sessionId} version={calls.length} />
|
|
291
|
+
<DispositionPanel key={`outcome-${focus?.call_id}`} call={focus} onSaved={() => setSelected(null)} />
|
|
292
|
+
{focus ? (
|
|
293
|
+
<>
|
|
294
|
+
<TranscriptViewer key={`transcript-${focus.call_id}`} roomName={focus.room_name ?? focus.call_id} live={isLiveCall(focus)} />
|
|
295
|
+
{props.renderDetailActions ? <div className="flex flex-wrap gap-2">{props.renderDetailActions(focus)}</div> : null}
|
|
296
|
+
{(props.extraDetailTabs ?? []).map((t) => <SectionCard key={t.id} title={t.label}>{t.render(focus)}</SectionCard>)}
|
|
297
|
+
</>
|
|
298
|
+
) : (
|
|
299
|
+
<EmptyState title="Nothing on screen" description="The transcript of the call you select shows here." />
|
|
300
|
+
)}
|
|
301
|
+
</>
|
|
302
|
+
) : null}
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Who is calling, shown with a switcher when the rep was not passed in. Managers can pick a teammate; a personal key can only be itself. */
|
|
309
|
+
function RepSwitcher({ me, picked, onPick }: { me: ReturnType<typeof useCurrentMember>; picked: MemberOption | null; onPick: (m: MemberOption | null) => void }) {
|
|
310
|
+
const current = picked ?? me.member;
|
|
311
|
+
return (
|
|
312
|
+
<div className="flex flex-wrap items-center gap-2 text-sm" aria-label="Rep">
|
|
313
|
+
{me.loading ? <span className="text-muted-foreground">Finding your user...</span> : null}
|
|
314
|
+
{current ? <span>Calling as {current.name}{current.id === me.userId ? " (you)" : ""}</span> : null}
|
|
315
|
+
<MemberPicker label={current ? "Switch rep" : "Rep"} value={current?.id ?? null} onChange={onPick} placeholder="Choose who is calling" />
|
|
316
|
+
{picked && me.member && picked.id !== me.userId ? <Button size="sm" variant="ghost" onClick={() => onPick(null)}>Use me</Button> : null}
|
|
317
|
+
</div>
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function DialerScreen(props: DialerScreenProps) {
|
|
322
|
+
const [tab, setTab] = useUrlState("tab", "session");
|
|
323
|
+
const [, setView] = useUrlState("view");
|
|
324
|
+
const [, setSession] = useUrlState("session");
|
|
325
|
+
const [calls, setCalls] = useState<LiveCall[]>([]);
|
|
326
|
+
// The rep: a passed userId wins (BC). Otherwise the signed-in team member, or a teammate picked in the switcher.
|
|
327
|
+
const me = useCurrentMember({ enabled: !props.userId });
|
|
328
|
+
const [picked, setPicked] = useState<MemberOption | null>(null);
|
|
329
|
+
const rep = props.userId ? null : (picked ?? me.member);
|
|
330
|
+
const userId = props.userId ?? rep?.id;
|
|
331
|
+
const userEmail = props.userId ? props.userEmail : (rep ? (rep.email ?? undefined) : props.userEmail);
|
|
332
|
+
const active = TABS.some((t) => t.id === tab) ? tab : "session";
|
|
333
|
+
// The header counters describe the Session tab's calls; drop them when it unmounts so they are never stale.
|
|
334
|
+
useEffect(() => { if (active !== "session") setCalls([]); }, [active]);
|
|
335
|
+
const live = calls.filter(isLiveCall).length;
|
|
336
|
+
const stats = calls.length > 0
|
|
337
|
+
? [
|
|
338
|
+
{ label: "On calls now", value: live, tone: live > 0 ? ("positive" as const) : ("neutral" as const) },
|
|
339
|
+
{ label: "Need an outcome", value: calls.filter(needsDisposition).length, tone: calls.some(needsDisposition) ? ("warning" as const) : ("neutral" as const) },
|
|
340
|
+
{ label: "Voicemails", value: calls.filter((c) => c.is_voicemail).length },
|
|
341
|
+
{ label: "Calls this session", value: calls.length },
|
|
342
|
+
]
|
|
343
|
+
: undefined;
|
|
344
|
+
return (
|
|
345
|
+
<ScreenLayout
|
|
346
|
+
className={props.className}
|
|
347
|
+
title="Dialer"
|
|
348
|
+
description="Call a list batch by batch, take calls in the browser and record every outcome."
|
|
349
|
+
icon={<Phone className="size-5" />}
|
|
350
|
+
actions={props.userId ? props.toolbarActions : <><RepSwitcher me={me} picked={picked} onPick={setPicked} />{props.toolbarActions}</>}
|
|
351
|
+
tabs={TABS}
|
|
352
|
+
activeTab={active}
|
|
353
|
+
onTabChange={(id) => setTab(id)}
|
|
354
|
+
stats={stats}
|
|
355
|
+
>
|
|
356
|
+
{active === "session" ? <SessionTab {...props} userId={userId} userEmail={userEmail} onCalls={setCalls} /> : null}
|
|
357
|
+
{active === "sessions" ? <SessionsTab userEmail={props.userEmail} onWork={(id) => { setSession(id); setView(null); setTab("session"); }} /> : null}
|
|
358
|
+
{active === "history" ? <CallHistory userEmail={props.userEmail ?? userEmail} /> : null}
|
|
359
|
+
</ScreenLayout>
|
|
360
|
+
);
|
|
361
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DialerSessionDetail: one dialer session as a complete record. Tabs: Overview (stats and every session field), Calls (the
|
|
5
|
+
* calls in the session with hang up, mute and confirmed voicemail drop) and Queue (contacts still waiting, with skip).
|
|
6
|
+
* Header actions: Pause, Resume (dials the next contacts, so it is confirmed), Stop (hangs up live calls, confirmed) and
|
|
7
|
+
* "Work this session" (hands the id to the caller so the calling workspace can take over).
|
|
8
|
+
*
|
|
9
|
+
* SDK operations, in order:
|
|
10
|
+
* 1. GET /dialer/sessions/{id}/queue-stats + GET /dialer/sessions/{id} read (polled every 5s via useDialerSession)
|
|
11
|
+
* 2. POST /dialer/sessions/{id}/pause write (hangs up live calls)
|
|
12
|
+
* 3. POST /voice/dialer/sessions/{id}/resume { max_contacts } EXTERNAL -> ConfirmSpendDialog first (places real calls)
|
|
13
|
+
* 4. POST /dialer/sessions/{id}/stop write -> ConfirmSpendDialog (one-way, hangs up live calls)
|
|
14
|
+
* Calls and queue use LiveCallsTable and WaitingContacts (see those files for their operations and confirmations).
|
|
15
|
+
* Full-page mode: pass `expanded` plus `onExpand` / `onCollapse` (DialerScreen ties them to `?view=full`).
|
|
16
|
+
*/
|
|
17
|
+
import { useState, type ReactNode } from "react";
|
|
18
|
+
import { Phone } from "lucide-react";
|
|
19
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
20
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
21
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
22
|
+
import { DetailPanel } from "@/g8/components/screen/detail-panel";
|
|
23
|
+
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
24
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
25
|
+
import { StatStrip } from "@/g8/components/screen/stat-strip";
|
|
26
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
27
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
28
|
+
import { g8Post } from "@/g8/lib/g8-client";
|
|
29
|
+
import { useDialerSession } from "../hooks/use-dialer-session";
|
|
30
|
+
import { isLiveCall, isSessionOver, needsDisposition, normalizeStatus } from "../lib/dialer-types";
|
|
31
|
+
import { WaitingContacts } from "./call-queue";
|
|
32
|
+
import { LiveCallsTable } from "./live-calls-table";
|
|
33
|
+
import { pluralWord } from "@/g8/lib/plural";
|
|
34
|
+
|
|
35
|
+
export interface DialerSessionDetailProps {
|
|
36
|
+
sessionId: string;
|
|
37
|
+
/** Needed by pause on some orgs; optional. */
|
|
38
|
+
userEmail?: string;
|
|
39
|
+
/** Show "Work this session" and call this with the id. */
|
|
40
|
+
onWork?: (sessionId: string) => void;
|
|
41
|
+
onClose?: () => void;
|
|
42
|
+
onExpand?: () => void;
|
|
43
|
+
onCollapse?: () => void;
|
|
44
|
+
/** Called after pause, resume or stop succeeded (refresh your list). */
|
|
45
|
+
onChanged?: () => void;
|
|
46
|
+
/** Extra buttons in the header. */
|
|
47
|
+
actions?: ReactNode;
|
|
48
|
+
className?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function tone(status: string) {
|
|
52
|
+
return status === "active" ? "positive" : status === "paused" ? "warning" : status === "completed" || status === "stopped" ? "neutral" : "info";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function DialerSessionDetail({ sessionId, userEmail, onWork, onClose, onExpand, onCollapse, onChanged, actions, className }: DialerSessionDetailProps) {
|
|
56
|
+
const session = useDialerSession(sessionId);
|
|
57
|
+
const [version, setVersion] = useState(0);
|
|
58
|
+
const [asking, setAsking] = useState<"resume" | "stop" | null>(null);
|
|
59
|
+
const changed = () => { setVersion((v) => v + 1); session.refresh(); onChanged?.(); };
|
|
60
|
+
const base = `/dialer/sessions/${encodeURIComponent(sessionId)}`;
|
|
61
|
+
const pause = useG8Mutation(() => g8Post(`${base}/pause`, undefined, { query: { user_email: userEmail } }), { onSuccess: changed });
|
|
62
|
+
const stop = useG8Mutation(() => g8Post(`${base}/stop`), { onSuccess: () => { setAsking(null); changed(); }, onError: () => setAsking(null) });
|
|
63
|
+
const detail = session.detail;
|
|
64
|
+
const mode = detail?.dialing_mode ?? "power";
|
|
65
|
+
const count = mode === "parallel" ? Math.min(4, Math.max(1, detail?.parallel_count ?? 2)) : 1;
|
|
66
|
+
const resume = useG8Mutation(() => g8Post(`/voice/dialer/sessions/${encodeURIComponent(sessionId)}/resume`, { max_contacts: count }), { onSuccess: () => { setAsking(null); changed(); }, onError: () => setAsking(null) });
|
|
67
|
+
|
|
68
|
+
if (!session.stats && !session.error) return <Skeleton className="h-48 w-full" aria-label="Loading session" />;
|
|
69
|
+
const status = normalizeStatus(session.stats?.status ?? detail?.status);
|
|
70
|
+
const over = isSessionOver(status);
|
|
71
|
+
const calls = session.calls;
|
|
72
|
+
const failure = session.error ?? pause.error ?? stop.error ?? resume.error;
|
|
73
|
+
|
|
74
|
+
const header = (
|
|
75
|
+
<>
|
|
76
|
+
{onWork && !over ? <Button size="sm" variant="outline" onClick={() => onWork(sessionId)}><Phone className="mr-1 size-4" />Work this session</Button> : null}
|
|
77
|
+
{status === "active" ? <Button size="sm" variant="outline" disabled={pause.loading} loading={pause.loading} onClick={() => void pause.mutate(undefined)}>Pause</Button> : null}
|
|
78
|
+
{status === "paused" ? <Button size="sm" variant="outline" onClick={() => setAsking("resume")}>Resume</Button> : null}
|
|
79
|
+
{!over ? <Button size="sm" variant="outline" onClick={() => setAsking("stop")}>Stop</Button> : null}
|
|
80
|
+
{actions}
|
|
81
|
+
</>
|
|
82
|
+
);
|
|
83
|
+
const overview = (
|
|
84
|
+
<div className="space-y-4">
|
|
85
|
+
<StatStrip
|
|
86
|
+
items={[
|
|
87
|
+
{ label: "Left in queue", value: session.stats?.queue_remaining ?? detail?.counts?.queue_remaining ?? 0 },
|
|
88
|
+
{ label: "On calls now", value: calls.filter(isLiveCall).length, tone: calls.some(isLiveCall) ? "positive" : "neutral" },
|
|
89
|
+
{ label: "Need an outcome", value: calls.filter(needsDisposition).length, tone: calls.some(needsDisposition) ? "warning" : "neutral" },
|
|
90
|
+
{ label: "Voicemails", value: detail?.live_state?.voicemail_calls ?? calls.filter((c) => c.is_voicemail).length },
|
|
91
|
+
]}
|
|
92
|
+
/>
|
|
93
|
+
<SectionCard title="Session details">
|
|
94
|
+
<KeyValueList
|
|
95
|
+
columns={2}
|
|
96
|
+
items={[
|
|
97
|
+
{ label: "Name", value: detail?.name ?? "" },
|
|
98
|
+
{ label: "Status", value: status },
|
|
99
|
+
{ label: "List", value: detail?.list_title ?? detail?.list_id ?? "" },
|
|
100
|
+
{ label: "Mode", value: detail?.dialing_mode ? `${detail.dialing_mode}${detail.dialing_mode === "parallel" ? ` (${count} at once)` : ""}` : "" },
|
|
101
|
+
{ label: "Caller ID", value: detail?.from_phone ?? "" },
|
|
102
|
+
{ label: "Contacts", value: detail?.counts?.total },
|
|
103
|
+
{ label: "Dialed", value: detail?.counts?.total_dialed },
|
|
104
|
+
{ label: "Not dialed yet", value: detail?.counts?.total_undialed },
|
|
105
|
+
{ label: "Session id", value: sessionId, mono: true, wide: true },
|
|
106
|
+
]}
|
|
107
|
+
/>
|
|
108
|
+
</SectionCard>
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div className={className}>
|
|
114
|
+
{failure ? <ApiError error={failure} title="Session problem" onRetry={() => session.refresh()} /> : null}
|
|
115
|
+
<DetailPanel
|
|
116
|
+
title={detail?.name ?? "Dialing session"}
|
|
117
|
+
subtitle={detail?.list_title ?? undefined}
|
|
118
|
+
badges={<StatusPill tone={tone(status)} dot>{status || "unknown"}</StatusPill>}
|
|
119
|
+
actions={header}
|
|
120
|
+
onClose={onClose}
|
|
121
|
+
onExpand={onExpand}
|
|
122
|
+
onCollapse={onCollapse}
|
|
123
|
+
tabs={[
|
|
124
|
+
{ id: "overview", label: "Overview", content: overview },
|
|
125
|
+
{ id: "calls", label: "Calls", count: calls.length, content: <LiveCallsTable sessionId={sessionId} calls={calls} onChanged={changed} /> },
|
|
126
|
+
{ id: "queue", label: "Queue", content: <WaitingContacts sessionId={sessionId} version={version} onSkipped={changed} /> },
|
|
127
|
+
]}
|
|
128
|
+
/>
|
|
129
|
+
<ConfirmSpendDialog
|
|
130
|
+
open={asking === "resume"}
|
|
131
|
+
tier="external"
|
|
132
|
+
title={count > 1 ? `Resume and dial the next ${count} ${pluralWord(count, "contact")}?` : "Resume and dial the next contact?"}
|
|
133
|
+
description="This places real phone calls to people on the list and reserves credits for each attempt."
|
|
134
|
+
confirmLabel="Resume and dial"
|
|
135
|
+
recipients={count}
|
|
136
|
+
confirming={resume.loading}
|
|
137
|
+
onConfirm={() => void resume.mutate(undefined)}
|
|
138
|
+
onCancel={() => setAsking(null)}
|
|
139
|
+
/>
|
|
140
|
+
<ConfirmSpendDialog
|
|
141
|
+
open={asking === "stop"}
|
|
142
|
+
tier="destructive"
|
|
143
|
+
title="Stop this session?"
|
|
144
|
+
description="Every live call is hung up and the session is completed. It cannot be resumed."
|
|
145
|
+
confirmLabel="Stop session"
|
|
146
|
+
affected={calls.filter(isLiveCall).length}
|
|
147
|
+
confirming={stop.loading}
|
|
148
|
+
onConfirm={() => void stop.mutate(undefined)}
|
|
149
|
+
onCancel={() => setAsking(null)}
|
|
150
|
+
/>
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
@@ -54,7 +54,7 @@ export function DispositionPanel({ call, onSaved, className }: DispositionPanelP
|
|
|
54
54
|
return (
|
|
55
55
|
<section className={`space-y-3 rounded-lg border p-4 ${className ?? ""}`} aria-label="Call outcome">
|
|
56
56
|
<header>
|
|
57
|
-
<h3 className="text-sm font-
|
|
57
|
+
<h3 className="text-sm font-medium">How did the call with {contactName(call)} go?</h3>
|
|
58
58
|
{call.disposition ? <p className="text-xs text-muted-foreground">Currently: {call.disposition}</p> : null}
|
|
59
59
|
</header>
|
|
60
60
|
<div role="radiogroup" aria-label="Outcome" className="flex flex-wrap gap-2">
|
|
@@ -18,8 +18,12 @@ import { EmptyState } from "@/g8/components/empty-state";
|
|
|
18
18
|
import { g8Post } from "@/g8/lib/g8-client";
|
|
19
19
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
20
20
|
import { contactName, isLiveCall, normalizeStatus, type LiveCall } from "../lib/dialer-types";
|
|
21
|
+
import { rowActivationProps } from "@/g8/hooks/use-row-activation";
|
|
22
|
+
import { RecordLink } from "@/g8/components/screen/record-link";
|
|
21
23
|
|
|
22
24
|
export interface LiveCallsTableProps {
|
|
25
|
+
/** URL param the host screen reads for this record type. With it the name is a real link (Cmd-click opens it in a new tab); without it a plain button. */
|
|
26
|
+
linkParam?: string;
|
|
23
27
|
sessionId: string;
|
|
24
28
|
calls: LiveCall[];
|
|
25
29
|
/** Called after a control succeeded; refresh the session here. */
|
|
@@ -29,7 +33,7 @@ export interface LiveCallsTableProps {
|
|
|
29
33
|
className?: string;
|
|
30
34
|
}
|
|
31
35
|
|
|
32
|
-
export function LiveCallsTable({ sessionId, calls, onChanged, onSelect, className }: LiveCallsTableProps) {
|
|
36
|
+
export function LiveCallsTable({ linkParam, sessionId, calls, onChanged, onSelect, className }: LiveCallsTableProps) {
|
|
33
37
|
const [voicemailFor, setVoicemailFor] = useState<LiveCall | null>(null);
|
|
34
38
|
// Per-row busy state: only the row being acted on is disabled, not every call in the table.
|
|
35
39
|
const [busy, setBusy] = useState<ReadonlySet<string>>(new Set());
|
|
@@ -71,8 +75,8 @@ export function LiveCallsTable({ sessionId, calls, onChanged, onSelect, classNam
|
|
|
71
75
|
const live = isLiveCall(call);
|
|
72
76
|
const rowBusy = busy.has(call.call_id);
|
|
73
77
|
return (
|
|
74
|
-
<TableRow key={call.call_id} className={onSelect ? "cursor-pointer" : undefined}
|
|
75
|
-
<TableCell className="font-medium">{contactName(call)}</TableCell>
|
|
78
|
+
<TableRow key={call.call_id} className={onSelect ? "cursor-pointer" : undefined} {...(onSelect ? rowActivationProps(() => onSelect(call)) : {})}>
|
|
79
|
+
<TableCell className="font-medium">{onSelect ? <RecordLink param={linkParam} id={call.call_id} onOpen={() => onSelect(call)}>{contactName(call)}</RecordLink> : contactName(call)}</TableCell>
|
|
76
80
|
<TableCell>{call.to_phone}</TableCell>
|
|
77
81
|
<TableCell className="capitalize">{call.is_voicemail ? "voicemail" : normalizeStatus(call.status)}{call.disposition ? ` · ${call.disposition}` : ""}</TableCell>
|
|
78
82
|
<TableCell className="space-x-1 text-right" onClick={(e) => e.stopPropagation()}>
|