@graph8/sdk 0.361.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 +172 -1
- package/blocks/_foundation/block.json +234 -13
- package/blocks/_foundation/components/api-error.tsx +71 -25
- 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/empty-state.tsx +39 -7
- package/blocks/_foundation/components/screen/bar-list.tsx +71 -0
- package/blocks/_foundation/components/screen/detail-panel.tsx +114 -0
- package/blocks/_foundation/components/screen/entity-avatar.tsx +56 -0
- package/blocks/_foundation/components/screen/form-sheet.tsx +73 -0
- package/blocks/_foundation/components/screen/index.ts +22 -0
- package/blocks/_foundation/components/screen/key-value-list.tsx +65 -0
- package/blocks/_foundation/components/screen/record-link.tsx +76 -0
- package/blocks/_foundation/components/screen/screen-layout.tsx +124 -0
- package/blocks/_foundation/components/screen/section-card.tsx +47 -0
- package/blocks/_foundation/components/screen/sparkline.tsx +77 -0
- package/blocks/_foundation/components/screen/split-view.tsx +125 -0
- package/blocks/_foundation/components/screen/stat-strip.tsx +121 -0
- package/blocks/_foundation/components/screen/status-pill.tsx +104 -0
- package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
- package/blocks/_foundation/components/screen/time-series-chart.tsx +32 -0
- package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
- package/blocks/_foundation/components/screen/toolbar.tsx +144 -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/hooks/use-url-state.ts +74 -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/README.md +21 -1
- package/blocks/appointments/block.json +43 -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 +77 -0
- package/blocks/appointments/components/meeting-list.tsx +14 -6
- package/blocks/appointments/components/meetings-screen.tsx +157 -0
- package/blocks/appointments/components/scheduling-screen.tsx +122 -0
- package/blocks/appointments/hooks/use-event-types.ts +17 -0
- package/blocks/block.schema.json +18 -0
- package/blocks/block.types.ts +26 -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/README.md +34 -3
- package/blocks/deals/block.json +45 -4
- package/blocks/deals/components/deal-detail.tsx +27 -13
- package/blocks/deals/components/deal-related.tsx +87 -0
- package/blocks/deals/components/deals-screen.tsx +298 -0
- package/blocks/deals/components/pipeline-board.tsx +4 -2
- package/blocks/deals/components/stage-editor.tsx +4 -4
- package/blocks/deals/hooks/use-default-owner.ts +60 -0
- package/blocks/deals/hooks/use-pipeline-deals.ts +5 -1
- package/blocks/deals/hooks/use-pipelines.ts +1 -1
- package/blocks/deals/lib/deals-types.ts +14 -0
- 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/README.md +12 -0
- package/blocks/forms/block.json +39 -17
- 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 +353 -0
- package/blocks/forms/components/submissions-table.tsx +16 -6
- 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/README.md +16 -0
- package/blocks/lifecycle/block.json +34 -7
- package/blocks/lifecycle/components/account-related.tsx +66 -0
- package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
- package/blocks/lifecycle/components/lifecycle-screen.tsx +149 -0
- 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/README.md +13 -0
- package/blocks/newsletter/block.json +47 -23
- package/blocks/newsletter/components/issue-scheduler.tsx +2 -1
- package/blocks/newsletter/components/newsletter-screen.tsx +404 -0
- package/blocks/newsletter/components/subscriber-table.tsx +12 -6
- package/blocks/nurture/README.md +12 -0
- package/blocks/nurture/block.json +35 -13
- 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 +283 -0
- 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/README.md +12 -0
- package/blocks/pages/block.json +35 -22
- package/blocks/pages/components/brand-kit-editor.tsx +5 -2
- package/blocks/pages/components/content-library.tsx +46 -11
- package/blocks/pages/components/page-list.tsx +6 -3
- package/blocks/pages/components/pages-screen.tsx +154 -0
- package/blocks/quotes/README.md +17 -0
- package/blocks/quotes/block.json +38 -9
- package/blocks/quotes/components/product-picker.tsx +3 -1
- package/blocks/quotes/components/quote-list.tsx +7 -3
- package/blocks/quotes/components/quote-related.tsx +123 -0
- package/blocks/quotes/components/quotes-screen.tsx +169 -0
- 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 +2785 -1637
- 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 +25 -8
- package/dist/types/blocks/doctor.d.ts +6 -1
- package/dist/types/blocks/flows.d.ts +20 -0
- 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 +32 -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/stack.schema.json +6 -1
- 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 +55 -8
- package/templates/next-app/app/globals.css.tmpl +105 -0
- package/templates/next-app/app/layout.tsx.tmpl +18 -4
- package/templates/next-app/components/app-shell.tsx.tmpl +248 -23
- package/templates/next-app/components/shell/connection-status.tsx.tmpl +109 -0
- package/templates/next-app/components/shell/nav-model.ts.tmpl +65 -0
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +131 -0
- package/templates/next-app/components/shell/theme-toggle.tsx.tmpl +40 -0
- package/templates/next-app/dot-gitignore.tmpl +2 -0
- package/templates/next-app/lib/nav.ts.tmpl +1 -1
- package/templates/next-app/package.json.tmpl +1 -1
package/stacks/outreach.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "./stack.schema.json",
|
|
3
3
|
"id": "outreach",
|
|
4
4
|
"title": "Outreach-style sales engagement",
|
|
5
|
-
"summary": "
|
|
5
|
+
"summary": "Sequences, campaigns, shared mailboxes, inbox, dialer and meetings with analytics on top.",
|
|
6
6
|
"status": "available",
|
|
7
7
|
"blocks": [
|
|
8
8
|
"foundation",
|
|
@@ -10,77 +10,140 @@
|
|
|
10
10
|
"lists",
|
|
11
11
|
"mailboxes",
|
|
12
12
|
"sequencer",
|
|
13
|
+
"campaigns",
|
|
13
14
|
"inbox",
|
|
14
15
|
"dialer",
|
|
15
16
|
"appointments",
|
|
16
17
|
"analytics"
|
|
17
18
|
],
|
|
19
|
+
"home": "/analytics",
|
|
18
20
|
"nav": [
|
|
21
|
+
{
|
|
22
|
+
"label": "Analytics",
|
|
23
|
+
"path": "/analytics",
|
|
24
|
+
"block": "analytics",
|
|
25
|
+
"component": "AnalyticsScreen",
|
|
26
|
+
"icon": "BarChart3",
|
|
27
|
+
"group": "Insights",
|
|
28
|
+
"description": "Pipeline, engagement and team KPIs"
|
|
29
|
+
},
|
|
19
30
|
{
|
|
20
31
|
"label": "Contacts",
|
|
21
32
|
"path": "/contacts",
|
|
22
33
|
"block": "crm",
|
|
23
|
-
"component": "
|
|
34
|
+
"component": "ContactsScreen",
|
|
35
|
+
"icon": "Users",
|
|
36
|
+
"group": "Prospect",
|
|
37
|
+
"description": "People you sell to, with notes, tasks and activity",
|
|
38
|
+
"page": "pages/contacts.tsx.tmpl"
|
|
24
39
|
},
|
|
25
40
|
{
|
|
26
41
|
"label": "Companies",
|
|
27
42
|
"path": "/companies",
|
|
28
43
|
"block": "crm",
|
|
29
|
-
"component": "
|
|
44
|
+
"component": "CompaniesScreen",
|
|
45
|
+
"icon": "Building2",
|
|
46
|
+
"group": "Prospect",
|
|
47
|
+
"description": "Accounts and the people at them"
|
|
30
48
|
},
|
|
31
49
|
{
|
|
32
50
|
"label": "Lists",
|
|
33
51
|
"path": "/lists",
|
|
34
52
|
"block": "lists",
|
|
35
|
-
"component": "
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
"path": "/suppressions",
|
|
40
|
-
"block": "lists",
|
|
41
|
-
"component": "SuppressionManager"
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
"label": "Mailboxes",
|
|
45
|
-
"path": "/mailboxes",
|
|
46
|
-
"block": "mailboxes",
|
|
47
|
-
"component": "MailboxList"
|
|
53
|
+
"component": "ListsScreen",
|
|
54
|
+
"icon": "ListChecks",
|
|
55
|
+
"group": "Prospect",
|
|
56
|
+
"description": "Saved audiences you can sequence or sync"
|
|
48
57
|
},
|
|
49
58
|
{
|
|
50
59
|
"label": "Sequences",
|
|
51
60
|
"path": "/sequences",
|
|
52
61
|
"block": "sequencer",
|
|
53
|
-
"component": "
|
|
62
|
+
"component": "SequencesScreen",
|
|
63
|
+
"icon": "Workflow",
|
|
64
|
+
"group": "Engage",
|
|
65
|
+
"description": "Multi-step outreach cadences"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"label": "Campaigns",
|
|
69
|
+
"path": "/campaigns",
|
|
70
|
+
"block": "campaigns",
|
|
71
|
+
"component": "CampaignsScreen",
|
|
72
|
+
"icon": "Rocket",
|
|
73
|
+
"group": "Engage",
|
|
74
|
+
"description": "Campaigns from brief to launch"
|
|
54
75
|
},
|
|
55
76
|
{
|
|
56
77
|
"label": "Inbox",
|
|
57
78
|
"path": "/inbox",
|
|
58
79
|
"block": "inbox",
|
|
59
|
-
"component": "
|
|
80
|
+
"component": "InboxScreen",
|
|
81
|
+
"icon": "Inbox",
|
|
82
|
+
"group": "Engage",
|
|
83
|
+
"description": "Replies from every channel in one place"
|
|
60
84
|
},
|
|
61
85
|
{
|
|
62
86
|
"label": "Dialer",
|
|
63
87
|
"path": "/dialer",
|
|
64
88
|
"block": "dialer",
|
|
65
|
-
"component": "
|
|
89
|
+
"component": "DialerScreen",
|
|
90
|
+
"icon": "Phone",
|
|
91
|
+
"group": "Engage",
|
|
92
|
+
"description": "Call queues and live sessions"
|
|
66
93
|
},
|
|
67
94
|
{
|
|
68
95
|
"label": "Call log",
|
|
69
96
|
"path": "/calls",
|
|
70
97
|
"block": "dialer",
|
|
71
|
-
"component": "
|
|
98
|
+
"component": "CallLogScreen",
|
|
99
|
+
"icon": "PhoneCall",
|
|
100
|
+
"group": "Engage",
|
|
101
|
+
"description": "Every call with transcript and outcome"
|
|
72
102
|
},
|
|
73
103
|
{
|
|
74
104
|
"label": "Meetings",
|
|
75
105
|
"path": "/meetings",
|
|
76
106
|
"block": "appointments",
|
|
77
|
-
"component": "
|
|
107
|
+
"component": "MeetingsScreen",
|
|
108
|
+
"icon": "CalendarCheck",
|
|
109
|
+
"group": "Engage",
|
|
110
|
+
"description": "Booked, held and missed meetings"
|
|
78
111
|
},
|
|
79
112
|
{
|
|
80
|
-
"label": "
|
|
81
|
-
"path": "/
|
|
82
|
-
"block": "
|
|
83
|
-
"component": "
|
|
113
|
+
"label": "Scheduling",
|
|
114
|
+
"path": "/scheduling",
|
|
115
|
+
"block": "appointments",
|
|
116
|
+
"component": "SchedulingScreen",
|
|
117
|
+
"icon": "CalendarClock",
|
|
118
|
+
"group": "Setup",
|
|
119
|
+
"description": "Booking links and event types"
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"label": "Tasks",
|
|
123
|
+
"path": "/tasks",
|
|
124
|
+
"block": "crm",
|
|
125
|
+
"component": "TasksScreen",
|
|
126
|
+
"icon": "CheckSquare",
|
|
127
|
+
"group": "Work",
|
|
128
|
+
"description": "Follow-ups due across your records"
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"label": "Mailboxes",
|
|
132
|
+
"path": "/mailboxes",
|
|
133
|
+
"block": "mailboxes",
|
|
134
|
+
"component": "MailboxesScreen",
|
|
135
|
+
"icon": "Mail",
|
|
136
|
+
"group": "Setup",
|
|
137
|
+
"description": "Sending mailboxes and warmup health"
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"label": "Suppressions",
|
|
141
|
+
"path": "/suppressions",
|
|
142
|
+
"block": "lists",
|
|
143
|
+
"component": "SuppressionsScreen",
|
|
144
|
+
"icon": "ShieldOff",
|
|
145
|
+
"group": "Setup",
|
|
146
|
+
"description": "People and domains you never contact"
|
|
84
147
|
}
|
|
85
148
|
]
|
|
86
149
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// Generated by `graph8-sdk create {{stackId}}`. The {{block}} block's {{component}} (source in {{blockDir}}), with the
|
|
4
|
+
// other blocks of this app plugged into its slots. Edit freely: this file is yours.
|
|
5
|
+
import Link from "next/link";
|
|
6
|
+
import { Skeleton } from "@graph8/sdk/ui";
|
|
7
|
+
import { Handshake } from "lucide-react";
|
|
8
|
+
import { ApiError } from "@/components/g8/_foundation/api-error";
|
|
9
|
+
import { EmptyState } from "@/components/g8/_foundation/empty-state";
|
|
10
|
+
import { useG8Query } from "@/components/g8/_foundation/hooks/use-g8-query";
|
|
11
|
+
import { g8Get } from "@/components/g8/_foundation/lib/g8-client";
|
|
12
|
+
import { CompaniesScreen } from "@/components/g8/crm/companies-screen";
|
|
13
|
+
import { dataOf, formatMoney } from "@/components/g8/deals/lib/deals-types";
|
|
14
|
+
|
|
15
|
+
/** One row of GET /contacts/{id}/deals and GET /companies/{id}/deals (the API's ContactDealResponse). */
|
|
16
|
+
interface LinkedDeal { deal_id?: string | null; name?: string | null; stage?: string | null; value?: number | null; currency?: string | null; role?: string | null }
|
|
17
|
+
|
|
18
|
+
/** The deals linked to one contact or company, each opening in the Deals page. */
|
|
19
|
+
function LinkedDeals({ kind, id }: { kind: "contacts" | "companies"; id: number }) {
|
|
20
|
+
const q = useG8Query(async (signal) => {
|
|
21
|
+
const inner = dataOf<{ items?: LinkedDeal[] } | LinkedDeal[]>(await g8Get(`/${kind}/${id}/deals`, undefined, { signal }));
|
|
22
|
+
return (Array.isArray(inner) ? inner : (inner.items ?? [])).filter((d) => d.deal_id);
|
|
23
|
+
}, [kind, id]);
|
|
24
|
+
if (q.error) return <ApiError error={q.error} title="Could not load deals" onRetry={() => void q.refetch()} />;
|
|
25
|
+
if (!q.data) return <Skeleton className="h-24 w-full" aria-label="Loading deals" />;
|
|
26
|
+
if (q.data.length === 0) return <EmptyState compact icon={<Handshake className="size-8" />} title="No deals yet" description="Deals linked to this record show up here." />;
|
|
27
|
+
return (
|
|
28
|
+
<ul className="divide-y rounded-md border" aria-label="Deals">
|
|
29
|
+
{q.data.map((d) => (
|
|
30
|
+
<li key={d.deal_id}>
|
|
31
|
+
<Link href={`/deals?id=${encodeURIComponent(d.deal_id as string)}`} className="flex items-center justify-between gap-3 p-3 text-sm hover:bg-muted">
|
|
32
|
+
<span className="min-w-0 truncate font-medium">{d.name || "Untitled deal"}</span>
|
|
33
|
+
<span className="shrink-0 text-xs text-muted-foreground">{[d.stage, d.value != null ? formatMoney(d.value, d.currency) : null].filter(Boolean).join(" · ")}</span>
|
|
34
|
+
</Link>
|
|
35
|
+
</li>
|
|
36
|
+
))}
|
|
37
|
+
</ul>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default function Page() {
|
|
42
|
+
return (
|
|
43
|
+
<CompaniesScreen
|
|
44
|
+
extraDetailTabs={[{ id: "deals", label: "Deals", render: (c) => <LinkedDeals kind="companies" id={c.id} /> }]}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// Generated by `graph8-sdk create {{stackId}}`. The {{block}} block's {{component}} (source in {{blockDir}}), with the
|
|
4
|
+
// other blocks of this app plugged into its slots. Edit freely: this file is yours.
|
|
5
|
+
import Link from "next/link";
|
|
6
|
+
import { Skeleton } from "@graph8/sdk/ui";
|
|
7
|
+
import { Handshake } from "lucide-react";
|
|
8
|
+
import { ApiError } from "@/components/g8/_foundation/api-error";
|
|
9
|
+
import { EmptyState } from "@/components/g8/_foundation/empty-state";
|
|
10
|
+
import { useG8Query } from "@/components/g8/_foundation/hooks/use-g8-query";
|
|
11
|
+
import { g8Get } from "@/components/g8/_foundation/lib/g8-client";
|
|
12
|
+
import { ContactsScreen } from "@/components/g8/crm/contacts-screen";
|
|
13
|
+
import { dataOf, formatMoney } from "@/components/g8/deals/lib/deals-types";
|
|
14
|
+
|
|
15
|
+
/** One row of GET /contacts/{id}/deals and GET /companies/{id}/deals (the API's ContactDealResponse). */
|
|
16
|
+
interface LinkedDeal { deal_id?: string | null; name?: string | null; stage?: string | null; value?: number | null; currency?: string | null; role?: string | null }
|
|
17
|
+
|
|
18
|
+
/** The deals linked to one contact or company, each opening in the Deals page. */
|
|
19
|
+
function LinkedDeals({ kind, id }: { kind: "contacts" | "companies"; id: number }) {
|
|
20
|
+
const q = useG8Query(async (signal) => {
|
|
21
|
+
const inner = dataOf<{ items?: LinkedDeal[] } | LinkedDeal[]>(await g8Get(`/${kind}/${id}/deals`, undefined, { signal }));
|
|
22
|
+
return (Array.isArray(inner) ? inner : (inner.items ?? [])).filter((d) => d.deal_id);
|
|
23
|
+
}, [kind, id]);
|
|
24
|
+
if (q.error) return <ApiError error={q.error} title="Could not load deals" onRetry={() => void q.refetch()} />;
|
|
25
|
+
if (!q.data) return <Skeleton className="h-24 w-full" aria-label="Loading deals" />;
|
|
26
|
+
if (q.data.length === 0) return <EmptyState compact icon={<Handshake className="size-8" />} title="No deals yet" description="Deals linked to this record show up here." />;
|
|
27
|
+
return (
|
|
28
|
+
<ul className="divide-y rounded-md border" aria-label="Deals">
|
|
29
|
+
{q.data.map((d) => (
|
|
30
|
+
<li key={d.deal_id}>
|
|
31
|
+
<Link href={`/deals?id=${encodeURIComponent(d.deal_id as string)}`} className="flex items-center justify-between gap-3 p-3 text-sm hover:bg-muted">
|
|
32
|
+
<span className="min-w-0 truncate font-medium">{d.name || "Untitled deal"}</span>
|
|
33
|
+
<span className="shrink-0 text-xs text-muted-foreground">{[d.stage, d.value != null ? formatMoney(d.value, d.currency) : null].filter(Boolean).join(" · ")}</span>
|
|
34
|
+
</Link>
|
|
35
|
+
</li>
|
|
36
|
+
))}
|
|
37
|
+
</ul>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default function Page() {
|
|
42
|
+
return (
|
|
43
|
+
<ContactsScreen
|
|
44
|
+
extraDetailTabs={[{ id: "deals", label: "Deals", render: (c) => <LinkedDeals kind="contacts" id={c.id} /> }]}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// Generated by `graph8-sdk create {{stackId}}`. The {{block}} block's {{component}} (source in {{blockDir}}), with the
|
|
4
|
+
// other blocks of this app plugged into its slots. Edit freely: this file is yours.
|
|
5
|
+
import { useRouter } from "next/navigation";
|
|
6
|
+
import { DealsScreen } from "@/components/g8/deals/deals-screen";
|
|
7
|
+
|
|
8
|
+
export default function Page() {
|
|
9
|
+
const router = useRouter();
|
|
10
|
+
return <DealsScreen onOpenQuote={(quoteId) => router.push(`/quotes?id=${encodeURIComponent(quoteId)}`)} />;
|
|
11
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// Generated by `graph8-sdk create {{stackId}}`. The {{block}} block's {{component}} (source in {{blockDir}}), with the
|
|
4
|
+
// other blocks of this app plugged into its slots. Edit freely: this file is yours.
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { useRouter } from "next/navigation";
|
|
7
|
+
import { Button, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, Skeleton } from "@graph8/sdk/ui";
|
|
8
|
+
import { Handshake } from "lucide-react";
|
|
9
|
+
import { ApiError } from "@/components/g8/_foundation/api-error";
|
|
10
|
+
import { EmptyState } from "@/components/g8/_foundation/empty-state";
|
|
11
|
+
import { useG8Query } from "@/components/g8/_foundation/hooks/use-g8-query";
|
|
12
|
+
import { g8Get } from "@/components/g8/_foundation/lib/g8-client";
|
|
13
|
+
import { DealDetail } from "@/components/g8/deals/deal-detail";
|
|
14
|
+
import { useDefaultOwner } from "@/components/g8/deals/hooks/use-default-owner";
|
|
15
|
+
import { InboxScreen } from "@/components/g8/inbox/inbox-screen";
|
|
16
|
+
import type { InboxThread } from "@/components/g8/inbox/lib/inbox-types";
|
|
17
|
+
|
|
18
|
+
interface Match { id: number; full_name?: string | null; first_name?: string | null; last_name?: string | null; work_email?: string | null; job_title?: string | null }
|
|
19
|
+
const nameOf = (m: Match): string => m.full_name || [m.first_name, m.last_name].filter(Boolean).join(" ") || m.work_email || `Contact ${m.id}`;
|
|
20
|
+
|
|
21
|
+
/** The new deal's form. Mounted only while the sheet is open, so browsing threads never loads the team. When the key has no person, the owner is chosen here instead of silently being the first member. */
|
|
22
|
+
function NewDeal({ contactId, onSaved }: { contactId: number; onSaved: (dealId: string) => void }) {
|
|
23
|
+
const owner = useDefaultOwner();
|
|
24
|
+
const [ownerId, setOwnerId] = useState<string | null>(null);
|
|
25
|
+
if (owner.loading) return <Skeleton className="h-24 w-full" aria-label="Loading owners" />;
|
|
26
|
+
const effective = ownerId ?? owner.defaultOwner?.id ?? "";
|
|
27
|
+
if (!effective) return <ApiError error={owner.error ?? new Error("No team member found to own this deal.")} title="A deal needs an owner" onRetry={() => void owner.reload()} />;
|
|
28
|
+
return (
|
|
29
|
+
<div className="space-y-3">
|
|
30
|
+
{owner.source === "first-member" ? (
|
|
31
|
+
<div className="space-y-1">
|
|
32
|
+
<label htmlFor="inbox-deal-owner" className="text-sm font-medium">Owner</label>
|
|
33
|
+
<select id="inbox-deal-owner" className="h-9 w-full rounded-md border bg-background px-2 text-sm" value={effective} onChange={(e) => setOwnerId(e.target.value)}>
|
|
34
|
+
{owner.owners.map((o) => <option key={o.id} value={o.id}>{o.name}{o.email && o.email !== o.name ? ` (${o.email})` : ""}</option>)}
|
|
35
|
+
</select>
|
|
36
|
+
<p className="text-xs text-muted-foreground">This key has no person of its own, so choose who owns the deal.</p>
|
|
37
|
+
</div>
|
|
38
|
+
) : null}
|
|
39
|
+
<DealDetail ownerId={effective} contactIds={[contactId]} onSaved={(deal) => onSaved(deal.id)} />
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Create a deal from a reply: find the sender in your CRM, pick them, then fill in the deal. */
|
|
45
|
+
function CreateDeal({ thread }: { thread: InboxThread }) {
|
|
46
|
+
const router = useRouter();
|
|
47
|
+
const [open, setOpen] = useState(false);
|
|
48
|
+
const [contactId, setContactId] = useState<number | null>(null);
|
|
49
|
+
const name = thread.contact?.name?.trim() ?? "";
|
|
50
|
+
const matches = useG8Query(async (signal) => ((await g8Get<Match[]>("/contacts", { name, limit: 8, page: 1 }, { signal })).data ?? []), [name], { enabled: open && name.length > 1 });
|
|
51
|
+
const email = thread.contact?.email?.toLowerCase();
|
|
52
|
+
const list = matches.data ?? [];
|
|
53
|
+
return (
|
|
54
|
+
<>
|
|
55
|
+
<Button size="sm" variant="outline" onClick={() => { setContactId(null); setOpen(true); }}>
|
|
56
|
+
<Handshake className="mr-1 size-4" aria-hidden />
|
|
57
|
+
Create deal
|
|
58
|
+
</Button>
|
|
59
|
+
<Sheet open={open} onOpenChange={setOpen}>
|
|
60
|
+
<SheetContent className="w-full overflow-y-auto sm:max-w-xl">
|
|
61
|
+
<SheetHeader>
|
|
62
|
+
<SheetTitle>Create deal</SheetTitle>
|
|
63
|
+
<SheetDescription>A deal needs a contact in your CRM. Pick the person who wrote this reply.</SheetDescription>
|
|
64
|
+
</SheetHeader>
|
|
65
|
+
<div className="space-y-4 p-4">
|
|
66
|
+
{name.length < 2 ? <EmptyState compact title="No sender name on this thread" description="Open the contact in Contacts and create the deal from the Deals page." /> : null}
|
|
67
|
+
{matches.error ? <ApiError error={matches.error} title="Could not search contacts" onRetry={() => void matches.refetch()} /> : null}
|
|
68
|
+
{matches.loading && !matches.data ? <Skeleton className="h-16 w-full" aria-label="Searching contacts" /> : null}
|
|
69
|
+
{matches.data && list.length === 0 ? <EmptyState compact title="Not in your CRM yet" description={`Add ${name} in Contacts first, then come back.`} /> : null}
|
|
70
|
+
{list.length > 0 ? (
|
|
71
|
+
<ul className="divide-y rounded-md border" aria-label="Matching contacts">
|
|
72
|
+
{list.map((m) => (
|
|
73
|
+
<li key={m.id}>
|
|
74
|
+
<button type="button" aria-pressed={contactId === m.id} className="flex w-full items-center justify-between gap-3 p-3 text-left text-sm hover:bg-muted aria-pressed:bg-muted" onClick={() => setContactId(m.id)}>
|
|
75
|
+
<span className="min-w-0 truncate font-medium">{nameOf(m)}</span>
|
|
76
|
+
<span className="shrink-0 text-xs text-muted-foreground">{[m.job_title, m.work_email && m.work_email.toLowerCase() === email ? "Same email" : m.work_email].filter(Boolean).join(" · ")}</span>
|
|
77
|
+
</button>
|
|
78
|
+
</li>
|
|
79
|
+
))}
|
|
80
|
+
</ul>
|
|
81
|
+
) : null}
|
|
82
|
+
{contactId != null ? <NewDeal key={contactId} contactId={contactId} onSaved={(id) => router.push(`/deals?id=${encodeURIComponent(id)}`)} /> : null}
|
|
83
|
+
</div>
|
|
84
|
+
</SheetContent>
|
|
85
|
+
</Sheet>
|
|
86
|
+
</>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export default function Page() {
|
|
91
|
+
return <InboxScreen renderDetailActions={(thread) => <CreateDeal thread={thread} />} />;
|
|
92
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// Generated by `graph8-sdk create {{stackId}}`. The {{block}} block's {{component}} (source in {{blockDir}}), with the
|
|
4
|
+
// other blocks of this app plugged into its slots. Edit freely: this file is yours.
|
|
5
|
+
import { useRouter } from "next/navigation";
|
|
6
|
+
import { QuotesScreen } from "@/components/g8/quotes/quotes-screen";
|
|
7
|
+
|
|
8
|
+
export default function Page() {
|
|
9
|
+
const router = useRouter();
|
|
10
|
+
return <QuotesScreen onOpenDeal={(dealId) => router.push(`/deals?id=${encodeURIComponent(dealId)}`)} />;
|
|
11
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./stack.schema.json",
|
|
3
|
+
"id": "pipedrive",
|
|
4
|
+
"title": "Pipedrive-style sales pipeline",
|
|
5
|
+
"summary": "Deals and quotes with mailboxes, inbox, meetings and analytics over your contacts.",
|
|
6
|
+
"status": "available",
|
|
7
|
+
"blocks": [
|
|
8
|
+
"foundation",
|
|
9
|
+
"crm",
|
|
10
|
+
"mailboxes",
|
|
11
|
+
"inbox",
|
|
12
|
+
"appointments",
|
|
13
|
+
"deals",
|
|
14
|
+
"quotes",
|
|
15
|
+
"analytics"
|
|
16
|
+
],
|
|
17
|
+
"home": "/analytics",
|
|
18
|
+
"nav": [
|
|
19
|
+
{
|
|
20
|
+
"label": "Analytics",
|
|
21
|
+
"path": "/analytics",
|
|
22
|
+
"block": "analytics",
|
|
23
|
+
"component": "AnalyticsScreen",
|
|
24
|
+
"icon": "BarChart3",
|
|
25
|
+
"group": "Insights",
|
|
26
|
+
"description": "Pipeline, engagement and team KPIs"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"label": "Deals",
|
|
30
|
+
"path": "/deals",
|
|
31
|
+
"block": "deals",
|
|
32
|
+
"component": "DealsScreen",
|
|
33
|
+
"icon": "Handshake",
|
|
34
|
+
"group": "Pipeline",
|
|
35
|
+
"description": "Pipeline board, forecast and deal detail",
|
|
36
|
+
"page": "pages/deals.tsx.tmpl"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"label": "Quotes",
|
|
40
|
+
"path": "/quotes",
|
|
41
|
+
"block": "quotes",
|
|
42
|
+
"component": "QuotesScreen",
|
|
43
|
+
"icon": "FileText",
|
|
44
|
+
"group": "Pipeline",
|
|
45
|
+
"description": "Build, send and track quotes",
|
|
46
|
+
"page": "pages/quotes.tsx.tmpl"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"label": "Contacts",
|
|
50
|
+
"path": "/contacts",
|
|
51
|
+
"block": "crm",
|
|
52
|
+
"component": "ContactsScreen",
|
|
53
|
+
"icon": "Users",
|
|
54
|
+
"group": "Prospect",
|
|
55
|
+
"description": "People you sell to, with notes, tasks and activity",
|
|
56
|
+
"page": "pages/contacts.tsx.tmpl"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"label": "Companies",
|
|
60
|
+
"path": "/companies",
|
|
61
|
+
"block": "crm",
|
|
62
|
+
"component": "CompaniesScreen",
|
|
63
|
+
"icon": "Building2",
|
|
64
|
+
"group": "Prospect",
|
|
65
|
+
"description": "Accounts and the people at them",
|
|
66
|
+
"page": "pages/companies.tsx.tmpl"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"label": "Inbox",
|
|
70
|
+
"path": "/inbox",
|
|
71
|
+
"block": "inbox",
|
|
72
|
+
"component": "InboxScreen",
|
|
73
|
+
"icon": "Inbox",
|
|
74
|
+
"group": "Engage",
|
|
75
|
+
"description": "Replies from every channel in one place",
|
|
76
|
+
"page": "pages/inbox.tsx.tmpl"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"label": "Meetings",
|
|
80
|
+
"path": "/meetings",
|
|
81
|
+
"block": "appointments",
|
|
82
|
+
"component": "MeetingsScreen",
|
|
83
|
+
"icon": "CalendarCheck",
|
|
84
|
+
"group": "Engage",
|
|
85
|
+
"description": "Booked, held and missed meetings"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"label": "Scheduling",
|
|
89
|
+
"path": "/scheduling",
|
|
90
|
+
"block": "appointments",
|
|
91
|
+
"component": "SchedulingScreen",
|
|
92
|
+
"icon": "CalendarClock",
|
|
93
|
+
"group": "Setup",
|
|
94
|
+
"description": "Booking links and event types"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"label": "Tasks",
|
|
98
|
+
"path": "/tasks",
|
|
99
|
+
"block": "crm",
|
|
100
|
+
"component": "TasksScreen",
|
|
101
|
+
"icon": "CheckSquare",
|
|
102
|
+
"group": "Work",
|
|
103
|
+
"description": "Follow-ups due across your records"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"label": "Mailboxes",
|
|
107
|
+
"path": "/mailboxes",
|
|
108
|
+
"block": "mailboxes",
|
|
109
|
+
"component": "MailboxesScreen",
|
|
110
|
+
"icon": "Mail",
|
|
111
|
+
"group": "Setup",
|
|
112
|
+
"description": "Sending mailboxes and warmup health"
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// Generated by `graph8-sdk create {{stackId}}`. The {{block}} block's {{component}} (source in {{blockDir}}), with the
|
|
4
|
+
// other blocks of this app plugged into its slots. Edit freely: this file is yours.
|
|
5
|
+
import Link from "next/link";
|
|
6
|
+
import { Skeleton } from "@graph8/sdk/ui";
|
|
7
|
+
import { Handshake } from "lucide-react";
|
|
8
|
+
import { ApiError } from "@/components/g8/_foundation/api-error";
|
|
9
|
+
import { EmptyState } from "@/components/g8/_foundation/empty-state";
|
|
10
|
+
import { useG8Query } from "@/components/g8/_foundation/hooks/use-g8-query";
|
|
11
|
+
import { g8Get } from "@/components/g8/_foundation/lib/g8-client";
|
|
12
|
+
import { CompaniesScreen } from "@/components/g8/crm/companies-screen";
|
|
13
|
+
import { dataOf, formatMoney } from "@/components/g8/deals/lib/deals-types";
|
|
14
|
+
|
|
15
|
+
/** One row of GET /contacts/{id}/deals and GET /companies/{id}/deals (the API's ContactDealResponse). */
|
|
16
|
+
interface LinkedDeal { deal_id?: string | null; name?: string | null; stage?: string | null; value?: number | null; currency?: string | null; role?: string | null }
|
|
17
|
+
|
|
18
|
+
/** The deals linked to one contact or company, each opening in the Deals page. */
|
|
19
|
+
function LinkedDeals({ kind, id }: { kind: "contacts" | "companies"; id: number }) {
|
|
20
|
+
const q = useG8Query(async (signal) => {
|
|
21
|
+
const inner = dataOf<{ items?: LinkedDeal[] } | LinkedDeal[]>(await g8Get(`/${kind}/${id}/deals`, undefined, { signal }));
|
|
22
|
+
return (Array.isArray(inner) ? inner : (inner.items ?? [])).filter((d) => d.deal_id);
|
|
23
|
+
}, [kind, id]);
|
|
24
|
+
if (q.error) return <ApiError error={q.error} title="Could not load deals" onRetry={() => void q.refetch()} />;
|
|
25
|
+
if (!q.data) return <Skeleton className="h-24 w-full" aria-label="Loading deals" />;
|
|
26
|
+
if (q.data.length === 0) return <EmptyState compact icon={<Handshake className="size-8" />} title="No deals yet" description="Deals linked to this record show up here." />;
|
|
27
|
+
return (
|
|
28
|
+
<ul className="divide-y rounded-md border" aria-label="Deals">
|
|
29
|
+
{q.data.map((d) => (
|
|
30
|
+
<li key={d.deal_id}>
|
|
31
|
+
<Link href={`/deals?id=${encodeURIComponent(d.deal_id as string)}`} className="flex items-center justify-between gap-3 p-3 text-sm hover:bg-muted">
|
|
32
|
+
<span className="min-w-0 truncate font-medium">{d.name || "Untitled deal"}</span>
|
|
33
|
+
<span className="shrink-0 text-xs text-muted-foreground">{[d.stage, d.value != null ? formatMoney(d.value, d.currency) : null].filter(Boolean).join(" · ")}</span>
|
|
34
|
+
</Link>
|
|
35
|
+
</li>
|
|
36
|
+
))}
|
|
37
|
+
</ul>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default function Page() {
|
|
42
|
+
return (
|
|
43
|
+
<CompaniesScreen
|
|
44
|
+
extraDetailTabs={[{ id: "deals", label: "Deals", render: (c) => <LinkedDeals kind="companies" id={c.id} /> }]}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// Generated by `graph8-sdk create {{stackId}}`. The {{block}} block's {{component}} (source in {{blockDir}}), with the
|
|
4
|
+
// other blocks of this app plugged into its slots. Edit freely: this file is yours.
|
|
5
|
+
import Link from "next/link";
|
|
6
|
+
import { Skeleton } from "@graph8/sdk/ui";
|
|
7
|
+
import { Handshake } from "lucide-react";
|
|
8
|
+
import { ApiError } from "@/components/g8/_foundation/api-error";
|
|
9
|
+
import { EmptyState } from "@/components/g8/_foundation/empty-state";
|
|
10
|
+
import { useG8Query } from "@/components/g8/_foundation/hooks/use-g8-query";
|
|
11
|
+
import { g8Get } from "@/components/g8/_foundation/lib/g8-client";
|
|
12
|
+
import { ContactsScreen } from "@/components/g8/crm/contacts-screen";
|
|
13
|
+
import { dataOf, formatMoney } from "@/components/g8/deals/lib/deals-types";
|
|
14
|
+
|
|
15
|
+
/** One row of GET /contacts/{id}/deals and GET /companies/{id}/deals (the API's ContactDealResponse). */
|
|
16
|
+
interface LinkedDeal { deal_id?: string | null; name?: string | null; stage?: string | null; value?: number | null; currency?: string | null; role?: string | null }
|
|
17
|
+
|
|
18
|
+
/** The deals linked to one contact or company, each opening in the Deals page. */
|
|
19
|
+
function LinkedDeals({ kind, id }: { kind: "contacts" | "companies"; id: number }) {
|
|
20
|
+
const q = useG8Query(async (signal) => {
|
|
21
|
+
const inner = dataOf<{ items?: LinkedDeal[] } | LinkedDeal[]>(await g8Get(`/${kind}/${id}/deals`, undefined, { signal }));
|
|
22
|
+
return (Array.isArray(inner) ? inner : (inner.items ?? [])).filter((d) => d.deal_id);
|
|
23
|
+
}, [kind, id]);
|
|
24
|
+
if (q.error) return <ApiError error={q.error} title="Could not load deals" onRetry={() => void q.refetch()} />;
|
|
25
|
+
if (!q.data) return <Skeleton className="h-24 w-full" aria-label="Loading deals" />;
|
|
26
|
+
if (q.data.length === 0) return <EmptyState compact icon={<Handshake className="size-8" />} title="No deals yet" description="Deals linked to this record show up here." />;
|
|
27
|
+
return (
|
|
28
|
+
<ul className="divide-y rounded-md border" aria-label="Deals">
|
|
29
|
+
{q.data.map((d) => (
|
|
30
|
+
<li key={d.deal_id}>
|
|
31
|
+
<Link href={`/deals?id=${encodeURIComponent(d.deal_id as string)}`} className="flex items-center justify-between gap-3 p-3 text-sm hover:bg-muted">
|
|
32
|
+
<span className="min-w-0 truncate font-medium">{d.name || "Untitled deal"}</span>
|
|
33
|
+
<span className="shrink-0 text-xs text-muted-foreground">{[d.stage, d.value != null ? formatMoney(d.value, d.currency) : null].filter(Boolean).join(" · ")}</span>
|
|
34
|
+
</Link>
|
|
35
|
+
</li>
|
|
36
|
+
))}
|
|
37
|
+
</ul>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default function Page() {
|
|
42
|
+
return (
|
|
43
|
+
<ContactsScreen
|
|
44
|
+
extraDetailTabs={[{ id: "deals", label: "Deals", render: (c) => <LinkedDeals kind="contacts" id={c.id} /> }]}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// Generated by `graph8-sdk create {{stackId}}`. The {{block}} block's {{component}} (source in {{blockDir}}), with the
|
|
4
|
+
// other blocks of this app plugged into its slots. Edit freely: this file is yours.
|
|
5
|
+
import { useRouter } from "next/navigation";
|
|
6
|
+
import { DealsScreen } from "@/components/g8/deals/deals-screen";
|
|
7
|
+
|
|
8
|
+
export default function Page() {
|
|
9
|
+
const router = useRouter();
|
|
10
|
+
return <DealsScreen onOpenQuote={(quoteId) => router.push(`/quotes?id=${encodeURIComponent(quoteId)}`)} />;
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// Generated by `graph8-sdk create {{stackId}}`. The {{block}} block's {{component}} (source in {{blockDir}}), with the
|
|
4
|
+
// other blocks of this app plugged into its slots. Edit freely: this file is yours.
|
|
5
|
+
import { useRouter } from "next/navigation";
|
|
6
|
+
import { QuotesScreen } from "@/components/g8/quotes/quotes-screen";
|
|
7
|
+
|
|
8
|
+
export default function Page() {
|
|
9
|
+
const router = useRouter();
|
|
10
|
+
return <QuotesScreen onOpenDeal={(dealId) => router.push(`/deals?id=${encodeURIComponent(dealId)}`)} />;
|
|
11
|
+
}
|