@graph8/sdk 0.362.0 → 0.370.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 +14 -10
- package/blocks/_foundation/README.md +64 -1
- package/blocks/_foundation/block.json +79 -6
- package/blocks/_foundation/components/api-error.tsx +9 -7
- package/blocks/_foundation/components/brand-provider.tsx +23 -0
- package/blocks/_foundation/components/confirmed-action.tsx +90 -0
- package/blocks/_foundation/components/connect-graph8.tsx +280 -0
- package/blocks/_foundation/components/screen/index.ts +1 -0
- package/blocks/_foundation/components/screen/record-link.tsx +76 -0
- 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-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-paginated.ts +120 -0
- package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
- package/blocks/_foundation/lib/g8-cache.ts +454 -0
- package/blocks/_foundation/lib/g8-client.ts +1 -1
- package/blocks/_foundation/lib/plural.ts +23 -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 +14 -0
- package/blocks/agents/block.json +33 -1
- package/blocks/agents/components/agent-builder.tsx +13 -8
- package/blocks/agents/components/agent-knowledge-panel.tsx +2 -1
- package/blocks/agents/components/agent-run-detail.tsx +102 -0
- package/blocks/agents/components/agent-runs.tsx +16 -7
- package/blocks/agents/components/agents-screen.tsx +184 -0
- package/blocks/analytics/README.md +13 -0
- package/blocks/analytics/block.json +45 -1
- package/blocks/analytics/components/analytics-screen.tsx +209 -0
- package/blocks/analytics/components/leaderboard.tsx +14 -7
- package/blocks/analytics/components/report-builder.tsx +23 -9
- package/blocks/analytics/components/report-detail.tsx +84 -0
- package/blocks/analytics/components/trend-chart.tsx +62 -0
- package/blocks/appointments/block.json +6 -2
- package/blocks/appointments/components/availability-editor.tsx +17 -5
- 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-list.tsx +9 -5
- package/blocks/appointments/components/meetings-screen.tsx +7 -4
- 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 +16 -5
- package/blocks/campaigns/components/campaign-launch-step.tsx +102 -19
- package/blocks/campaigns/components/campaign-list.tsx +7 -3
- package/blocks/campaigns/components/campaign-wizard.tsx +43 -10
- package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
- package/blocks/campaigns/components/icp-detail.tsx +154 -0
- package/blocks/campaigns/components/icp-editor.tsx +15 -15
- package/blocks/campaigns/components/persona-detail.tsx +172 -0
- package/blocks/campaigns/components/persona-editor.tsx +14 -11
- package/blocks/campaigns/lib/campaigns-utils.ts +134 -1
- 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/coach-screen.tsx +222 -0
- package/blocks/copilot/README.md +13 -0
- package/blocks/copilot/block.json +14 -1
- 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 +7 -5
- package/blocks/crm/README.md +28 -0
- package/blocks/crm/block.json +58 -2
- package/blocks/crm/components/companies-screen.tsx +345 -0
- package/blocks/crm/components/company-detail.tsx +8 -6
- package/blocks/crm/components/company-table.tsx +11 -4
- package/blocks/crm/components/contact-detail.tsx +6 -3
- package/blocks/crm/components/contact-table.tsx +10 -3
- package/blocks/crm/components/contacts-screen.tsx +359 -0
- package/blocks/crm/components/dedupe-review.tsx +7 -5
- package/blocks/crm/components/import-wizard.tsx +4 -2
- package/blocks/crm/components/notes-panel.tsx +4 -4
- package/blocks/crm/components/record-form.tsx +5 -3
- package/blocks/crm/components/task-list.tsx +4 -4
- package/blocks/crm/components/tasks-screen.tsx +379 -0
- package/blocks/crm/hooks/use-custom-fields.ts +6 -1
- package/blocks/crm/hooks/use-page-query.ts +7 -1
- package/blocks/crm/lib/crm-types.ts +12 -2
- package/blocks/crm/lib/screen-parts.tsx +105 -0
- package/blocks/deals/block.json +9 -3
- package/blocks/deals/components/deal-detail.tsx +23 -11
- package/blocks/deals/components/deal-related.tsx +1 -1
- package/blocks/deals/components/deals-screen.tsx +10 -5
- package/blocks/deals/components/stage-editor.tsx +4 -4
- package/blocks/deals/hooks/use-pipeline-deals.ts +5 -1
- package/blocks/deals/hooks/use-pipelines.ts +1 -1
- package/blocks/dialer/README.md +34 -2
- package/blocks/dialer/block.json +67 -1
- package/blocks/dialer/components/call-contact-button.tsx +252 -0
- package/blocks/dialer/components/call-log-screen.tsx +205 -0
- package/blocks/dialer/components/call-log.tsx +7 -3
- package/blocks/dialer/components/call-queue.tsx +1 -1
- package/blocks/dialer/components/dialer-screen.tsx +319 -0
- package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
- package/blocks/dialer/components/live-calls-table.tsx +7 -3
- package/blocks/dialer/components/session-start-form.tsx +4 -2
- package/blocks/dialer/components/softphone.tsx +3 -1
- package/blocks/enrichment/README.md +14 -2
- package/blocks/enrichment/block.json +32 -4
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +10 -5
- package/blocks/enrichment/components/email-verify-badge.tsx +28 -8
- 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 +288 -0
- package/blocks/enrichment/components/waterfall-config.tsx +39 -10
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
- package/blocks/forms/block.json +32 -18
- package/blocks/forms/components/form-builder.tsx +3 -1
- package/blocks/forms/components/form-embed.tsx +3 -1
- package/blocks/forms/components/forms-screen.tsx +5 -2
- package/blocks/forms/components/submissions-table.tsx +3 -1
- package/blocks/inbox/README.md +27 -3
- package/blocks/inbox/block.json +14 -2
- package/blocks/inbox/components/inbox-list.tsx +7 -3
- package/blocks/inbox/components/inbox-screen.tsx +294 -27
- package/blocks/inbox/components/reply-composer.tsx +65 -9
- package/blocks/inbox/components/tag-and-assign.tsx +4 -2
- 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 +9 -0
- package/blocks/integrations/block.json +46 -1
- package/blocks/integrations/components/crm-detail.tsx +100 -0
- package/blocks/integrations/components/integration-list.tsx +9 -4
- package/blocks/integrations/components/integrations-screen.tsx +135 -0
- package/blocks/integrations/components/sync-status.tsx +5 -3
- package/blocks/integrations/components/webhook-detail.tsx +160 -0
- package/blocks/integrations/components/webhook-manager.tsx +13 -8
- package/blocks/integrations/lib/integrations-utils.ts +6 -1
- package/blocks/intent/README.md +14 -0
- package/blocks/intent/block.json +27 -1
- package/blocks/intent/components/intent-feed.tsx +5 -2
- package/blocks/intent/components/intent-keyword-manager.tsx +8 -4
- package/blocks/intent/components/intent-screen.tsx +265 -0
- package/blocks/intent/components/keyword-detail.tsx +124 -0
- package/blocks/lifecycle/block.json +13 -8
- 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 +13 -0
- package/blocks/lists/block.json +39 -1
- package/blocks/lists/components/add-to-list-button.tsx +3 -2
- package/blocks/lists/components/list-picker.tsx +5 -1
- package/blocks/lists/components/list-table.tsx +11 -5
- package/blocks/lists/components/lists-screen.tsx +344 -0
- package/blocks/lists/components/suppression-manager.tsx +6 -2
- package/blocks/lists/components/suppressions-screen.tsx +255 -0
- package/blocks/mailboxes/README.md +17 -0
- package/blocks/mailboxes/block.json +65 -17
- package/blocks/mailboxes/components/connect-mailbox.tsx +23 -13
- package/blocks/mailboxes/components/domain-detail.tsx +142 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +6 -3
- package/blocks/mailboxes/components/mailbox-detail.tsx +156 -0
- package/blocks/mailboxes/components/mailboxes-screen.tsx +223 -0
- package/blocks/mailboxes/components/warmup-health.tsx +4 -2
- package/blocks/mailboxes/lib/mailboxes-utils.ts +22 -1
- package/blocks/newsletter/block.json +40 -24
- package/blocks/newsletter/components/issue-scheduler.tsx +2 -1
- package/blocks/newsletter/components/newsletter-screen.tsx +3 -1
- package/blocks/nurture/block.json +28 -14
- package/blocks/nurture/components/nurture-builder.tsx +3 -1
- package/blocks/nurture/components/nurture-enroll.tsx +6 -3
- package/blocks/nurture/components/nurture-form.tsx +23 -3
- package/blocks/nurture/components/nurture-screen.tsx +10 -6
- package/blocks/nurture/lib/nurture-types.ts +1 -1
- package/blocks/objects/README.md +17 -0
- package/blocks/objects/block.json +29 -16
- package/blocks/objects/components/object-not-found.tsx +3 -1
- package/blocks/objects/components/object-record-detail.tsx +2 -1
- package/blocks/objects/components/object-schema-editor.tsx +3 -1
- package/blocks/objects/components/object-table.tsx +13 -3
- package/blocks/objects/components/objects-screen.tsx +147 -0
- package/blocks/objects/lib/object-types.ts +1 -1
- package/blocks/pages/block.json +28 -23
- package/blocks/pages/components/brand-kit-editor.tsx +5 -2
- package/blocks/pages/components/content-library.tsx +9 -4
- package/blocks/pages/components/page-list.tsx +2 -1
- package/blocks/quotes/block.json +17 -12
- package/blocks/quotes/components/product-picker.tsx +3 -1
- package/blocks/quotes/components/quote-related.tsx +5 -2
- package/blocks/radar/README.md +10 -0
- package/blocks/radar/block.json +41 -1
- package/blocks/radar/components/competitor-detail.tsx +117 -0
- package/blocks/radar/components/competitor-tracker.tsx +3 -1
- 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 +49 -9
- package/blocks/sequencer/block.json +99 -19
- package/blocks/sequencer/components/enroll-dialog.tsx +9 -3
- package/blocks/sequencer/components/sequence-builder.tsx +211 -46
- package/blocks/sequencer/components/sequence-detail.tsx +259 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +114 -0
- package/blocks/sequencer/components/sequence-list.tsx +33 -8
- package/blocks/sequencer/components/sequence-stats.tsx +3 -2
- package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
- package/blocks/sequencer/components/sequences-screen.tsx +159 -0
- package/blocks/sequencer/components/step-editor.tsx +3 -1
- package/blocks/sequencer/lib/sequencer-utils.ts +128 -5
- 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 +11 -0
- package/blocks/social-ads/block.json +101 -2
- 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 +188 -0
- package/blocks/social-ads/components/audience-sync.tsx +11 -7
- package/blocks/social-ads/components/campaign-detail.tsx +133 -0
- package/blocks/social-ads/components/creative-detail.tsx +150 -0
- package/blocks/social-ads/components/creative-gallery.tsx +15 -5
- package/blocks/social-ads/components/post-composer.tsx +3 -1
- package/blocks/social-ads/components/post-detail.tsx +160 -0
- package/blocks/social-ads/components/social-post-list.tsx +11 -3
- package/blocks/social-ads/components/social-screen.tsx +160 -0
- package/blocks/visitors/README.md +10 -0
- package/blocks/visitors/block.json +25 -1
- package/blocks/visitors/components/snippet-install.tsx +3 -1
- package/blocks/visitors/components/visitor-company-card.tsx +2 -2
- package/blocks/visitors/components/visitor-detail.tsx +105 -0
- package/blocks/visitors/components/visitor-feed.tsx +11 -2
- 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 +7 -4
- package/blocks/webchat/components/conversation-list.tsx +7 -3
- 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 +4 -2
- package/blocks/workbench/components/workbench-grid.tsx +9 -3
- package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
- package/blocks/workbench/components/workbench-screen.tsx +192 -0
- package/blocks/workbench/components/workbench-sources.tsx +194 -0
- 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 +3 -1
- package/blocks/workflows/components/workflow-list.tsx +13 -6
- package/blocks/workflows/components/workflows-screen.tsx +193 -0
- package/dist/cli.js +2709 -1721
- 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 +113 -0
- package/dist/types/blocks/compose.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +22 -19
- package/dist/types/blocks/doctor.d.ts +5 -1
- package/dist/types/blocks/navfile.d.ts +35 -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 +2 -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.json +41 -16
- package/stacks/apollo/pages/companies.tsx.tmpl +50 -0
- package/stacks/apollo/pages/contacts.tsx.tmpl +117 -0
- package/stacks/apollo.json +118 -36
- package/stacks/calendly.json +60 -0
- package/stacks/clay/pages/companies.tsx.tmpl +15 -0
- package/stacks/clay/pages/contacts.tsx.tmpl +19 -0
- package/stacks/clay.json +59 -26
- package/stacks/gong.json +81 -35
- package/stacks/hootsuite.json +99 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +47 -0
- package/stacks/hubspot/pages/contacts.tsx.tmpl +111 -0
- package/stacks/hubspot/pages/deals.tsx.tmpl +11 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +92 -0
- package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
- package/stacks/hubspot.json +167 -66
- package/stacks/intercom.json +48 -20
- package/stacks/mailchimp.json +52 -33
- package/stacks/outreach/pages/contacts.tsx.tmpl +85 -0
- package/stacks/outreach.json +88 -25
- package/stacks/pipedrive/pages/companies.tsx.tmpl +47 -0
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +47 -0
- package/stacks/pipedrive/pages/deals.tsx.tmpl +11 -0
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +92 -0
- package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
- package/stacks/pipedrive.json +115 -0
- package/stacks/salesforce/pages/companies.tsx.tmpl +47 -0
- package/stacks/salesforce/pages/contacts.tsx.tmpl +47 -0
- package/stacks/salesforce/pages/deals.tsx.tmpl +11 -0
- package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
- package/stacks/salesforce.json +81 -31
- package/stacks/zoominfo/pages/companies.tsx.tmpl +15 -0
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +102 -0
- package/styles/styles.css +1 -1
- package/templates/next-app/README.md.tmpl +40 -9
- package/templates/next-app/app/layout.tsx.tmpl +7 -2
- package/templates/next-app/components/app-shell.tsx.tmpl +99 -89
- package/templates/next-app/dot-gitignore.tmpl +2 -0
- package/templates/next-app/package.json.tmpl +1 -1
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* 5. DELETE /deals/{id} (deals.deleteDeal) DESTRUCTIVE -> ConfirmSpendDialog first
|
|
13
13
|
* Compose notes, tasks and the activity timeline from the crm block under `children`.
|
|
14
14
|
*/
|
|
15
|
-
import { useEffect, useState, type ReactNode } from "react";
|
|
15
|
+
import { useEffect, useRef, useState, type ReactNode } from "react";
|
|
16
16
|
import { Button, Input, Label, Skeleton, Textarea } from "@graph8/sdk/ui";
|
|
17
17
|
import { ApiError } from "@/g8/components/api-error";
|
|
18
18
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
@@ -21,6 +21,7 @@ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
|
21
21
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
22
22
|
import { usePipelines } from "../hooks/use-pipelines";
|
|
23
23
|
import { dataOf, formatMoney, type Deal } from "../lib/deals-types";
|
|
24
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
24
25
|
|
|
25
26
|
export interface DealDetailProps {
|
|
26
27
|
/** Edit this deal; omit to create one. */
|
|
@@ -53,6 +54,7 @@ function fromDeal(d: Deal): Form {
|
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
export function DealDetail({ dealId, ownerId, contactIds = [], pipelineId, allowDuplicate, onSaved, onDeleted, children, className }: DealDetailProps) {
|
|
57
|
+
const brand = useBrand();
|
|
56
58
|
const { pipelines, defaultPipeline } = usePipelines();
|
|
57
59
|
const [form, setForm] = useState<Form>(EMPTY);
|
|
58
60
|
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
@@ -60,14 +62,23 @@ export function DealDetail({ dealId, ownerId, contactIds = [], pipelineId, allow
|
|
|
60
62
|
const [createdId, setCreatedId] = useState<string | null>(null);
|
|
61
63
|
const [createdRevision, setCreatedRevision] = useState<number | null>(null);
|
|
62
64
|
const effectiveId = dealId ?? createdId ?? undefined;
|
|
63
|
-
const deal = useG8Query(async (signal) => dataOf<Deal>(await g8Get(`/deals/${encodeURIComponent(dealId as string)}`, undefined, { signal })), [dealId], {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
const deal = useG8Query(async (signal) => dataOf<Deal>(await g8Get(`/deals/${encodeURIComponent(dealId as string)}`, undefined, { signal })), [dealId], {
|
|
66
|
+
enabled: Boolean(dealId),
|
|
67
|
+
key: ["deals", "detail", dealId ?? null],
|
|
68
|
+
// The form is seeded from this record: never swap it under someone typing on a window refocus.
|
|
69
|
+
refetchOnFocus: false,
|
|
70
|
+
});
|
|
71
|
+
const history = useG8Query(async (signal) => dataOf<{ items?: HistoryItem[] }>(await g8Get(`/deals/${encodeURIComponent(dealId as string)}/history`, { limit: 20 }, { signal })).items ?? [], [dealId], { enabled: Boolean(dealId), key: ["deals", "history", dealId ?? null] });
|
|
72
|
+
// Unsaved edits win over a refreshed record (focus, invalidation from a board move, refetch): only an
|
|
73
|
+
// untouched form follows the server. Saving or switching deals makes the form untouched again.
|
|
74
|
+
const dirty = useRef(false);
|
|
75
|
+
// A different deal (or create mode again) must not show the previous one's fields. Declared BEFORE the
|
|
76
|
+
// seeding effect so that, when the record is already cached, the reset runs first and the seed wins.
|
|
77
|
+
useEffect(() => { dirty.current = false; setForm(EMPTY); setCreatedId(null); setCreatedRevision(null); setConfirmDelete(false); }, [dealId]);
|
|
78
|
+
useEffect(() => { if (deal.data && !dirty.current) setForm(fromDeal(deal.data)); }, [deal.data]);
|
|
68
79
|
|
|
69
80
|
const pipeline = pipelines.find((p) => p.id === (deal.data?.pipeline_id ?? pipelineId)) ?? defaultPipeline;
|
|
70
|
-
const set = <K extends keyof Form>(k: K, v: Form[K]) => setForm((f) => ({ ...f, [k]: v }));
|
|
81
|
+
const set = <K extends keyof Form>(k: K, v: Form[K]) => { dirty.current = true; setForm((f) => ({ ...f, [k]: v })); };
|
|
71
82
|
|
|
72
83
|
const save = useG8Mutation(async () => {
|
|
73
84
|
const body = {
|
|
@@ -81,11 +92,12 @@ export function DealDetail({ dealId, ownerId, contactIds = [], pipelineId, allow
|
|
|
81
92
|
? await g8Patch(`/deals/${encodeURIComponent(effectiveId)}`, { ...body, expected_revision: (dealId ? deal.data?.revision : createdRevision) ?? undefined })
|
|
82
93
|
: await g8Post("/deals", { ...body, owner_id: ownerId, contact_ids: contactIds, allow_duplicate: allowDuplicate || undefined, pipeline_id: pipeline?.id, amount: body.amount ?? undefined, close_date: body.close_date ?? undefined });
|
|
83
94
|
return dataOf<Deal>(env);
|
|
84
|
-
}, { onSuccess: (saved) => { if (!dealId && saved.id) { setCreatedId(saved.id); setCreatedRevision(saved.revision ?? null); } if (dealId) { void deal.refetch(); void history.refetch(); } onSaved?.(saved); } });
|
|
85
|
-
const remove = useG8Mutation(async () => { await g8Delete(`/deals/${encodeURIComponent(dealId as string)}`); }, { onSuccess: () => { setConfirmDelete(false); onDeleted?.(dealId as string); }, onError: () => setConfirmDelete(false) });
|
|
95
|
+
}, { invalidates: [["deals", "list"], ["deals", "history"]], onSuccess: (saved) => { dirty.current = false; if (!dealId && saved.id) { setCreatedId(saved.id); setCreatedRevision(saved.revision ?? null); } if (dealId) { void deal.refetch(); void history.refetch(); } onSaved?.(saved); } });
|
|
96
|
+
const remove = useG8Mutation(async () => { await g8Delete(`/deals/${encodeURIComponent(dealId as string)}`); }, { invalidates: [["deals", "list"]], onSuccess: () => { setConfirmDelete(false); onDeleted?.(dealId as string); }, onError: () => setConfirmDelete(false) });
|
|
86
97
|
|
|
87
98
|
if (deal.error) return <ApiError error={deal.error} title="Could not load the deal" onRetry={() => void deal.refetch()} />;
|
|
88
|
-
|
|
99
|
+
// The hook keeps the previous deal's data while the next loads: wait for THIS deal, so a blank form is never shown (or saved) over it.
|
|
100
|
+
if (dealId && (!deal.data || String(deal.data.id) !== String(dealId))) return <Skeleton className="h-56 w-full" aria-label="Loading deal" />;
|
|
89
101
|
const d = deal.data;
|
|
90
102
|
const canCreate = Boolean(ownerId);
|
|
91
103
|
|
|
@@ -105,7 +117,7 @@ export function DealDetail({ dealId, ownerId, contactIds = [], pipelineId, allow
|
|
|
105
117
|
</div>
|
|
106
118
|
</div>
|
|
107
119
|
<div className="space-y-1"><Label htmlFor="deal-desc">Description</Label><Textarea id="deal-desc" rows={3} value={form.description} onChange={(e) => set("description", e.target.value)} /></div>
|
|
108
|
-
{!dealId && !canCreate ? <p className="text-sm text-muted-foreground">Pass ownerId (a
|
|
120
|
+
{!dealId && !canCreate ? <p className="text-sm text-muted-foreground">Pass ownerId (a {brand.linkLabel} user id) to create deals: the API requires an owner.</p> : null}
|
|
109
121
|
{save.error ? <ApiError error={save.error} title="Could not save the deal" /> : null}
|
|
110
122
|
{remove.error ? <ApiError error={remove.error} title="Could not delete the deal" /> : null}
|
|
111
123
|
<div className="flex justify-between">
|
|
@@ -24,7 +24,7 @@ import { dataOf, type Deal } from "../lib/deals-types";
|
|
|
24
24
|
|
|
25
25
|
export function DealContactsTab({ dealId }: { dealId: string }) {
|
|
26
26
|
const [open, setOpen] = useState<number | null>(null);
|
|
27
|
-
const q = useG8Query(async (signal) => dataOf<Deal>(await g8Get(`/deals/${encodeURIComponent(dealId)}`, undefined, { signal })), [dealId]);
|
|
27
|
+
const q = useG8Query(async (signal) => dataOf<Deal>(await g8Get(`/deals/${encodeURIComponent(dealId)}`, undefined, { signal })), [dealId], { key: ["deals", "detail", dealId] });
|
|
28
28
|
if (q.error) return <ApiError error={q.error} title="Could not load the deal's contacts" onRetry={() => void q.refetch()} />;
|
|
29
29
|
if (!q.data) return <Skeleton className="h-24 w-full" aria-label="Loading contacts" />;
|
|
30
30
|
const contacts = q.data.contacts ?? [];
|
|
@@ -43,6 +43,8 @@ import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
|
43
43
|
import { ActivityTimeline } from "@/g8/crm/components/activity-timeline";
|
|
44
44
|
import { useDebouncedValue } from "@/g8/crm/hooks/use-debounced";
|
|
45
45
|
import { contactName, type ContactRecord } from "@/g8/crm/lib/crm-types";
|
|
46
|
+
import { rowActivationProps } from "@/g8/hooks/use-row-activation";
|
|
47
|
+
import { RecordLink } from "@/g8/components/screen/record-link";
|
|
46
48
|
import { useDefaultOwner } from "../hooks/use-default-owner";
|
|
47
49
|
import { usePipelineDeals } from "../hooks/use-pipeline-deals";
|
|
48
50
|
import { usePipelines } from "../hooks/use-pipelines";
|
|
@@ -52,6 +54,7 @@ import { DealContactsTab, DealQuotesTab } from "./deal-related";
|
|
|
52
54
|
import { ForecastView } from "./forecast-view";
|
|
53
55
|
import { PipelineBoard } from "./pipeline-board";
|
|
54
56
|
import { StageEditor } from "./stage-editor";
|
|
57
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
55
58
|
|
|
56
59
|
export interface DealsScreenProps {
|
|
57
60
|
/** Start on this pipeline; defaults to the org's default one. */
|
|
@@ -84,6 +87,7 @@ function stageTone(stage: Stage | undefined): "neutral" | "positive" | "negative
|
|
|
84
87
|
|
|
85
88
|
/** Contact search + chips for the new-deal sheet. The API requires at least one contact. */
|
|
86
89
|
function ContactPicker({ value, onChange }: { value: ContactRecord[]; onChange: (next: ContactRecord[]) => void }) {
|
|
90
|
+
const brand = useBrand();
|
|
87
91
|
const [text, setText] = useState("");
|
|
88
92
|
const term = useDebouncedValue(text.trim());
|
|
89
93
|
const found = useG8Query(async (signal) => {
|
|
@@ -119,7 +123,7 @@ function ContactPicker({ value, onChange }: { value: ContactRecord[]; onChange:
|
|
|
119
123
|
</li>
|
|
120
124
|
))}
|
|
121
125
|
</ul>
|
|
122
|
-
<p className="text-xs text-muted-foreground">
|
|
126
|
+
<p className="text-xs text-muted-foreground">{brand.linkLabel} needs at least one contact on a deal. Every contact must work at the same company.</p>
|
|
123
127
|
</div>
|
|
124
128
|
);
|
|
125
129
|
}
|
|
@@ -182,8 +186,8 @@ function DealRows({ deals, stages, selectedId, onOpen, renderRowActions }: { dea
|
|
|
182
186
|
{deals.map((d) => {
|
|
183
187
|
const stage = d.stage_id ? byId.get(d.stage_id) : undefined;
|
|
184
188
|
return (
|
|
185
|
-
<TableRow key={d.id} data-state={selectedId === d.id ? "selected" : undefined} className="cursor-pointer"
|
|
186
|
-
<TableCell><
|
|
189
|
+
<TableRow key={d.id} data-state={selectedId === d.id ? "selected" : undefined} className="cursor-pointer" {...rowActivationProps(() => onOpen(d))}>
|
|
190
|
+
<TableCell><RecordLink param="id" id={d.id} onOpen={() => onOpen(d)}>{d.name || "Untitled deal"}</RecordLink></TableCell>
|
|
187
191
|
<TableCell><StatusPill tone={stageTone(stage)} dot>{stage?.name ?? d.stage_name ?? "Unknown"}</StatusPill></TableCell>
|
|
188
192
|
<TableCell className="text-right tabular-nums">{formatMoney(d.amount, d.currency)}</TableCell>
|
|
189
193
|
<TableCell><span className="inline-flex items-center gap-2"><EntityAvatar name={d.owner_name ?? d.owner_email ?? "?"} size="sm" />{d.owner_name ?? d.owner_email ?? "No owner"}</span></TableCell>
|
|
@@ -198,6 +202,7 @@ function DealRows({ deals, stages, selectedId, onOpen, renderRowActions }: { dea
|
|
|
198
202
|
}
|
|
199
203
|
|
|
200
204
|
export function DealsScreen({ pipelineId: initialPipelineId, renderRowActions, renderDetailActions, extraDetailTabs, toolbarActions, onOpenQuote }: DealsScreenProps) {
|
|
205
|
+
const brand = useBrand();
|
|
201
206
|
const [viewParam, setViewParam] = useUrlState("tab", "board");
|
|
202
207
|
const [selectedId, setSelectedId] = useUrlState("id");
|
|
203
208
|
const [mode, setMode] = useUrlState("view");
|
|
@@ -228,7 +233,7 @@ export function DealsScreen({ pipelineId: initialPipelineId, renderRowActions, r
|
|
|
228
233
|
|
|
229
234
|
const listed = data.deals.find((d) => d.id === selectedId);
|
|
230
235
|
// A deep link can point at a deal in another pipeline or hidden by the search: resolve it directly.
|
|
231
|
-
const fetched = useG8Query(async (signal) => dataOf<Deal>(await g8Get(`/deals/${encodeURIComponent(selectedId ?? "")}`, undefined, { signal })), [selectedId, version], { enabled: Boolean(selectedId) && !listed && !data.loading });
|
|
236
|
+
const fetched = useG8Query(async (signal) => dataOf<Deal>(await g8Get(`/deals/${encodeURIComponent(selectedId ?? "")}`, undefined, { signal })), [selectedId, version], { enabled: Boolean(selectedId) && !listed && !data.loading, key: ["deals", "detail", selectedId ?? null] });
|
|
232
237
|
const selected = listed ?? (fetched.data?.id === selectedId ? fetched.data : undefined);
|
|
233
238
|
const open = (id: string) => setSelectedId(id);
|
|
234
239
|
const close = () => { setSelectedId(null); setMode(null); };
|
|
@@ -259,7 +264,7 @@ export function DealsScreen({ pipelineId: initialPipelineId, renderRowActions, r
|
|
|
259
264
|
|
|
260
265
|
let body: ReactNode;
|
|
261
266
|
if (view === "forecast") body = <ForecastView pipelineId={pipeline?.id} />;
|
|
262
|
-
else if (view === "stages") body = pipeline ? <div className="space-y-6"><StageSummary stages={stages} deals={data.deals} /><StageEditor pipelineId={pipeline.id} onChanged={refresh} /></div> : <EmptyState title="No pipeline yet" description=
|
|
267
|
+
else if (view === "stages") body = pipeline ? <div className="space-y-6"><StageSummary stages={stages} deals={data.deals} /><StageEditor pipelineId={pipeline.id} onChanged={refresh} /></div> : <EmptyState title="No pipeline yet" description={`Create a pipeline in ${brand.linkLabel} first.`} />;
|
|
263
268
|
else body = <SplitView detailOpen={Boolean(selectedId)} onCloseDetail={close} expanded={mode === "full" && Boolean(selectedId)} onExpandedChange={(full) => setMode(full ? "full" : null)} detailWidth="lg" detail={detail} list={view === "board" ? <PipelineBoard key={`${pipeline?.id}-${version}`} pipelineId={pipeline?.id} search={search} onSelectDeal={(d) => open(d.id)} onChanged={() => void data.reload()} /> : list} />;
|
|
264
269
|
|
|
265
270
|
return (
|
|
@@ -45,21 +45,21 @@ export function StageEditor({ pipelineId, onChanged, className }: StageEditorPro
|
|
|
45
45
|
const saved = dataOf<Stage>(await g8Put(`${base}/stages/${encodeURIComponent(s.id)}`, { name: s.name, probability: s.probability ?? 0, stage_type: s.stage_type ?? "open", color: s.color ?? undefined }));
|
|
46
46
|
done(stages.map((x) => (x.id === saved.id ? { ...x, ...saved } : x)));
|
|
47
47
|
} finally { setSavingId(null); }
|
|
48
|
-
});
|
|
48
|
+
}, { invalidates: [["deals", "pipelines"]] });
|
|
49
49
|
const add = useG8Mutation(async (name: string) => {
|
|
50
50
|
const created = dataOf<Stage>(await g8Post(`${base}/stages`, { name, probability: 0, stage_type: "open" }));
|
|
51
51
|
done([...stages, created]);
|
|
52
52
|
setNewName("");
|
|
53
|
-
});
|
|
53
|
+
}, { invalidates: [["deals", "pipelines"]] });
|
|
54
54
|
const reorder = useG8Mutation(async (next: Stage[]) => {
|
|
55
55
|
await g8Put(`${base}/stages/reorder`, { stage_ids: next.map((s) => s.id) });
|
|
56
56
|
done(next);
|
|
57
|
-
});
|
|
57
|
+
}, { invalidates: [["deals", "pipelines"]] });
|
|
58
58
|
const remove = useG8Mutation(async (s: Stage) => {
|
|
59
59
|
await g8Delete(`${base}/stages/${encodeURIComponent(s.id)}`);
|
|
60
60
|
done(stages.filter((x) => x.id !== s.id));
|
|
61
61
|
setRemoving(null);
|
|
62
|
-
}, { onError: () => setRemoving(null) });
|
|
62
|
+
}, { invalidates: [["deals", "pipelines"]], onError: () => setRemoving(null) });
|
|
63
63
|
|
|
64
64
|
const patch = (id: string, change: Partial<Stage>) => setStages((all) => all.map((s) => (s.id === id ? { ...s, ...change } : s)));
|
|
65
65
|
const swap = (i: number, j: number) => { const next = [...stages]; [next[i], next[j]] = [next[j], next[i]]; void reorder.mutate(next); };
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { useCallback, useEffect, useState } from "react";
|
|
16
16
|
import { g8Get, g8Patch } from "@/g8/lib/g8-client";
|
|
17
17
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
18
|
+
import { invalidate } from "@/g8/lib/g8-cache";
|
|
18
19
|
import type { Deal } from "../lib/deals-types";
|
|
19
20
|
import { dataOf } from "../lib/deals-types";
|
|
20
21
|
|
|
@@ -42,7 +43,7 @@ export function usePipelineDeals(pipelineId: string | undefined, filters: Pipeli
|
|
|
42
43
|
return all.slice(0, maxDeals);
|
|
43
44
|
},
|
|
44
45
|
[pipelineId, filters.search, filters.ownerId, maxDeals],
|
|
45
|
-
{ enabled: Boolean(pipelineId) },
|
|
46
|
+
{ enabled: Boolean(pipelineId), key: ["deals", "list", "board", { pipelineId, search: filters.search || undefined, ownerId: filters.ownerId || undefined, maxDeals }] },
|
|
46
47
|
);
|
|
47
48
|
useEffect(() => { if (query.data) setDeals(query.data); }, [query.data]);
|
|
48
49
|
|
|
@@ -53,6 +54,9 @@ export function usePipelineDeals(pipelineId: string | undefined, filters: Pipeli
|
|
|
53
54
|
try {
|
|
54
55
|
const saved = dataOf<Deal>(await g8Patch(`/deals/${encodeURIComponent(deal.id)}`, { stage_id: stageId, expected_revision: deal.revision ?? undefined, ...extra }));
|
|
55
56
|
setDeals((all) => all.map((d) => (d.id === deal.id ? { ...d, ...saved } : d)));
|
|
57
|
+
// The deal's revision changed: a cached detail or history of it is out of date.
|
|
58
|
+
void invalidate(["deals", "detail"]);
|
|
59
|
+
void invalidate(["deals", "history"]);
|
|
56
60
|
return saved;
|
|
57
61
|
} catch (err) {
|
|
58
62
|
// Roll back by re-reading the server's truth: a snapshot taken before the move could itself be stale.
|
|
@@ -13,7 +13,7 @@ export function usePipelines() {
|
|
|
13
13
|
const q = useG8Query(async (signal) => {
|
|
14
14
|
const env = await g8Get<Pipeline[]>("/deals/pipelines", undefined, { signal });
|
|
15
15
|
return (env.data ?? []).map((p) => ({ ...p, stages: sortedStages(p) }));
|
|
16
|
-
}, []);
|
|
16
|
+
}, [], { key: ["deals", "pipelines"] });
|
|
17
17
|
const pipelines = q.data ?? [];
|
|
18
18
|
return { ...q, pipelines, defaultPipeline: pipelines.find((p) => p.is_default) ?? pipelines[0] };
|
|
19
19
|
}
|
package/blocks/dialer/README.md
CHANGED
|
@@ -9,6 +9,26 @@ browser, record outcomes, and review transcripts. Copy `DialerWorkspace` for the
|
|
|
9
9
|
> voicemail, or mints a softphone credential without a confirm dialog first, and the block never auto-advances to
|
|
10
10
|
> the next batch.
|
|
11
11
|
|
|
12
|
+
## Screens
|
|
13
|
+
|
|
14
|
+
- `DialerScreen` (`components/dialer-screen.tsx`): **Session** tab picks a list (ListPicker), creates a session (dials
|
|
15
|
+
nobody), then runs the queue: every batch, voicemail drop, softphone credential and stop is confirmed; finished calls
|
|
16
|
+
open the outcome panel and transcript. **Sessions** tab lists every session; a row opens `DialerSessionDetail` (stats, details, calls, queue; pause, confirmed resume, confirmed stop; `?tab=sessions&session=<id>`). **History** tab lists the rep's calls. Props are optional: `userId` (needed to
|
|
17
|
+
enable the browser softphone), `userEmail`, `listId`, `sessionId`, `transport` (default: the Twilio adapter, loaded
|
|
18
|
+
on demand, needs `@twilio/voice-sdk`). Resume a session with `?session=<id>`. Add `&view=full` to any detail (session, call, and the same in the other screens) to open it as a full page.
|
|
19
|
+
- `CallLogScreen` (`components/call-log-screen.tsx`): history of one rep, contact or session with outcome filter and search;
|
|
20
|
+
open a call for recording, transcript and AI grade (`?id=<room_name>`), and **Call back** the number (`CallContactButton`). Needs a rep email (it asks) or a prop.
|
|
21
|
+
- Slots on both: `toolbarActions`, `renderRowActions`, `renderDetailActions`, `extraDetailTabs`.
|
|
22
|
+
- `CallContactButton` (`components/call-contact-button.tsx`): **call one contact from any screen**. Drop it into a
|
|
23
|
+
contact page's `renderDetailActions` (the apollo and outreach stacks do) or a row slot: `<CallContactButton contact={c} />`.
|
|
24
|
+
Closed, it loads nothing. Open: pick the number (masked `***` numbers are never offered), a PAUSED or COMPLETED
|
|
25
|
+
session to log the call under, and the caller ID; turn on the softphone; confirm (external, 1 recipient). It calls
|
|
26
|
+
`POST /dialer/parallel-calls/manual-dial` and the softphone joins the returned `conference_name`. The rep is
|
|
27
|
+
`userId` if passed, else the personal key's user (`GET /team-members/me`); an organisation key is not a rep, so it
|
|
28
|
+
then asks for `userId`. Why not `next-batch` with `force_next_contact`: that only picks a contact already in the
|
|
29
|
+
session's list and dials a whole batch.
|
|
30
|
+
Scopes: `voice:read`, `voice:write`, `voice:run` (and `team:read` for `CallContactButton` without `userId`). A phone number must exist (buy or verify it in the graph8 app).
|
|
31
|
+
|
|
12
32
|
## How it works (read this before building)
|
|
13
33
|
|
|
14
34
|
- graph8 places the calls **from its servers** into a conference. The rep's browser only **joins that
|
|
@@ -81,13 +101,22 @@ and the call log for a rep email. Pass `userId` / `userEmail` from your session
|
|
|
81
101
|
- Voicemail drop plays the rep's cloned voice to a stranger's phone. The confirm dialog names the contact.
|
|
82
102
|
- Another tenant's session id is a 404, the same as a missing one.
|
|
83
103
|
- Manual dial (`POST /dialer/parallel-calls/manual-dial`, external) needs a PAUSED or COMPLETED session and can
|
|
84
|
-
create a contact
|
|
104
|
+
create a contact (an unknown number is matched to a contact or one is created). `CallContactButton` uses it.
|
|
105
|
+
- **Late join.** The conference name of a batch is only known from the `next-batch` (or `manual-dial`) response; the
|
|
106
|
+
app learns the NEXT batch's name earlier over a WebSocket the API does not have, and the session read does not
|
|
107
|
+
expose it. Voice waits up to 2 s for the rep only when the name was announced, so an SDK softphone joins after the
|
|
108
|
+
dials go out and the first second of an instant answer can be missed (voice's late-join reconciliation still
|
|
109
|
+
connects the audio). Keep the softphone registered BEFORE dialing (`CallContactButton` only enables the call once
|
|
110
|
+
it is ready) so the join is a single `joinConference` the moment the response lands.
|
|
111
|
+
- `next-batch` `user_email`: with a personal key the dialer's rep is the key's own user (naming someone else is a
|
|
112
|
+
403); with an organisation key the name is passed through but not vouched for, so voice keeps dialing without
|
|
113
|
+
moving the session to that rep.
|
|
85
114
|
|
|
86
115
|
## Operations
|
|
87
116
|
|
|
88
117
|
| Operation | Method and path | Tier | Used by |
|
|
89
118
|
|---|---|---|---|
|
|
90
|
-
| `voice.listDialerNumbers` | GET /voice/dialer/numbers | read | SessionStartForm |
|
|
119
|
+
| `voice.listDialerNumbers` | GET /voice/dialer/numbers | read | SessionStartForm, CallContactButton |
|
|
91
120
|
| `dialer.createSessionDraft` | POST /dialer/sessions/draft | write | SessionStartForm |
|
|
92
121
|
| `dialer.readQueueStats` | GET /dialer/sessions/{id}/queue-stats | read | useDialerSession |
|
|
93
122
|
| `voice.getDialerSession` | GET /dialer/sessions/{id} | read | useDialerSession |
|
|
@@ -106,6 +135,9 @@ and the call log for a rep email. Pass `userId` / `userEmail` from your session
|
|
|
106
135
|
| `voice.getCallTranscript` | GET /voice/dialer/calls/{room}/transcript | read | TranscriptViewer |
|
|
107
136
|
| `voice.getCallGrading` | GET /voice/dialer/calls/{room}/grading | read | TranscriptViewer |
|
|
108
137
|
| `voice.downloadCallRecording` | GET /voice/calls/{id}/recording | read | TranscriptViewer |
|
|
138
|
+
| `teamMembers.getMyTeamMember` | GET /team-members/me | read | CallContactButton (only without `userId`) |
|
|
139
|
+
| `voice.listDialerSessions` | GET /voice/dialer/sessions | read | CallContactButton |
|
|
140
|
+
| `dialer.manualDial` | POST /dialer/parallel-calls/manual-dial | external | CallContactButton |
|
|
109
141
|
|
|
110
142
|
## Recipes (server side)
|
|
111
143
|
|
package/blocks/dialer/block.json
CHANGED
|
@@ -21,15 +21,19 @@
|
|
|
21
21
|
"get_call_grading_voice_dialer_calls__room_name__grading_get",
|
|
22
22
|
"get_call_transcript_voice_dialer_calls__room_name__transcript_get",
|
|
23
23
|
"get_dialer_session_dialer_sessions__session_id__get",
|
|
24
|
+
"get_my_team_member_team_members_me_get",
|
|
24
25
|
"hangup_session_call_dialer_sessions_calls_hangup_post",
|
|
25
26
|
"list_calls_for_contact_voice_dialer_calls_get",
|
|
26
27
|
"list_dialer_numbers_voice_dialer_numbers_get",
|
|
28
|
+
"list_dialer_sessions_voice_dialer_sessions_get",
|
|
27
29
|
"list_voice_dispositions_voice_dispositions_get",
|
|
30
|
+
"manual_dial_dialer_parallel_calls_manual_dial_post",
|
|
28
31
|
"mint_softphone_token_dialer_sessions_token_get",
|
|
29
32
|
"mute_call_dialer_sessions_calls_mute_post",
|
|
30
33
|
"pause_dialer_session_dialer_sessions__session_id__pause_post",
|
|
31
34
|
"read_queue_stats_dialer_sessions__session_id__queue_stats_get",
|
|
32
35
|
"read_session_grid_dialer_sessions__session_id__grid_post",
|
|
36
|
+
"resume_dialer_session_voice_dialer_sessions__session_id__resume_post",
|
|
33
37
|
"set_call_disposition_dialer_parallel_calls_calls__livekit_room_name__disposition_patch",
|
|
34
38
|
"skip_contact_dialer_sessions__session_id__calls_skip_contact_post",
|
|
35
39
|
"stop_dialer_session_dialer_sessions__session_id__stop_post"
|
|
@@ -135,6 +139,61 @@
|
|
|
135
139
|
"purpose": "The whole rep screen wiring queue, softphone, outcome panel and transcript together.",
|
|
136
140
|
"operations": [],
|
|
137
141
|
"tiers": []
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"name": "DialerScreen",
|
|
145
|
+
"file": "components/dialer-screen.tsx",
|
|
146
|
+
"purpose": "Finished calling page: pick a list, run a session, softphone, outcomes, transcript and history.",
|
|
147
|
+
"operations": [
|
|
148
|
+
"get_dialer_session_dialer_sessions__session_id__get",
|
|
149
|
+
"list_dialer_sessions_voice_dialer_sessions_get"
|
|
150
|
+
],
|
|
151
|
+
"tiers": [
|
|
152
|
+
"read"
|
|
153
|
+
]
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"name": "CallLogScreen",
|
|
157
|
+
"file": "components/call-log-screen.tsx",
|
|
158
|
+
"purpose": "Finished call history page with outcome filter, recording, transcript and AI grade, and Call back on a call (CallContactButton, confirmed); also exports CallHistory.",
|
|
159
|
+
"operations": [
|
|
160
|
+
"list_calls_for_contact_voice_dialer_calls_get"
|
|
161
|
+
],
|
|
162
|
+
"tiers": [
|
|
163
|
+
"read"
|
|
164
|
+
]
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"name": "DialerSessionDetail",
|
|
168
|
+
"file": "components/dialer-session-detail.tsx",
|
|
169
|
+
"purpose": "One dialing session as a full record: stats, details, calls and queue, with pause, confirmed resume and confirmed stop.",
|
|
170
|
+
"operations": [
|
|
171
|
+
"get_dialer_session_dialer_sessions__session_id__get",
|
|
172
|
+
"pause_dialer_session_dialer_sessions__session_id__pause_post",
|
|
173
|
+
"read_queue_stats_dialer_sessions__session_id__queue_stats_get",
|
|
174
|
+
"resume_dialer_session_voice_dialer_sessions__session_id__resume_post",
|
|
175
|
+
"stop_dialer_session_dialer_sessions__session_id__stop_post"
|
|
176
|
+
],
|
|
177
|
+
"tiers": [
|
|
178
|
+
"read",
|
|
179
|
+
"write",
|
|
180
|
+
"external"
|
|
181
|
+
]
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"name": "CallContactButton",
|
|
185
|
+
"file": "components/call-contact-button.tsx",
|
|
186
|
+
"purpose": "Call one contact from any screen (contact page, table row): pick the number, a paused or completed session and the caller ID, turn on the softphone, confirm (external), and the softphone joins the call.",
|
|
187
|
+
"operations": [
|
|
188
|
+
"get_my_team_member_team_members_me_get",
|
|
189
|
+
"list_dialer_sessions_voice_dialer_sessions_get",
|
|
190
|
+
"list_dialer_numbers_voice_dialer_numbers_get",
|
|
191
|
+
"manual_dial_dialer_parallel_calls_manual_dial_post"
|
|
192
|
+
],
|
|
193
|
+
"tiers": [
|
|
194
|
+
"read",
|
|
195
|
+
"external"
|
|
196
|
+
]
|
|
138
197
|
}
|
|
139
198
|
],
|
|
140
199
|
"hooks": [
|
|
@@ -222,6 +281,7 @@
|
|
|
222
281
|
}
|
|
223
282
|
],
|
|
224
283
|
"scopes": [
|
|
284
|
+
"team:read",
|
|
225
285
|
"voice:read",
|
|
226
286
|
"voice:run",
|
|
227
287
|
"voice:write"
|
|
@@ -251,10 +311,16 @@
|
|
|
251
311
|
"decision": "Browser audio vendor for the Softphone",
|
|
252
312
|
"default": "Twilio via lib/twilio-transport.ts; graph8's dialer runs on Twilio conferences. For another vendor write a SoftphoneTransport and delete twilio-transport.ts",
|
|
253
313
|
"askHuman": true
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
"decision": "How a single contact is called outside a dialing session",
|
|
317
|
+
"default": "manual-dial under a PAUSED or COMPLETED session the rep picks (same call log and analytics), only once the softphone is ready; the rep is the personal key's user unless userId is passed",
|
|
318
|
+
"askHuman": false
|
|
254
319
|
}
|
|
255
320
|
],
|
|
256
321
|
"peerDependencies": {
|
|
257
322
|
"react": ">=18",
|
|
258
|
-
"@twilio/voice-sdk": "^2.18.5"
|
|
323
|
+
"@twilio/voice-sdk": "^2.18.5",
|
|
324
|
+
"lucide-react": ">=0.400.0"
|
|
259
325
|
}
|
|
260
326
|
}
|