@graph8/sdk 0.303.0 → 0.357.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 +81 -0
- package/blocks/README.md +32 -0
- package/blocks/_foundation/README.md +78 -0
- package/blocks/_foundation/block.json +79 -0
- package/blocks/_foundation/components/api-error.tsx +62 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +142 -0
- package/blocks/_foundation/components/empty-state.tsx +24 -0
- package/blocks/_foundation/hooks/use-g8-mutation.ts +88 -0
- package/blocks/_foundation/hooks/use-g8-query.ts +87 -0
- package/blocks/_foundation/hooks/use-job-poll.ts +130 -0
- package/blocks/_foundation/hooks/use-paginated.ts +105 -0
- package/blocks/_foundation/lib/g8-client.ts +128 -0
- package/blocks/_foundation/recipes/verify-proxy-key.ts +18 -0
- package/blocks/_foundation/server/proxy.ts +233 -0
- package/blocks/_foundation/server/route.ts +30 -0
- package/blocks/agents/README.md +133 -0
- package/blocks/agents/block.json +223 -0
- package/blocks/agents/components/agent-builder.tsx +199 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +144 -0
- package/blocks/agents/components/agent-runs.tsx +175 -0
- package/blocks/agents/components/agent-skills-panel.tsx +126 -0
- package/blocks/agents/components/agent-test-console.tsx +175 -0
- package/blocks/agents/lib/agent-types.ts +114 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +50 -0
- package/blocks/agents/recipes/run-and-follow-agent-run.ts +46 -0
- package/blocks/agents/recipes/test-agent-before-activating.ts +36 -0
- package/blocks/analytics/README.md +73 -0
- package/blocks/analytics/block.json +178 -0
- package/blocks/analytics/components/funnel-chart.tsx +79 -0
- package/blocks/analytics/components/kpi-tiles.tsx +74 -0
- package/blocks/analytics/components/leaderboard.tsx +86 -0
- package/blocks/analytics/components/report-builder.tsx +200 -0
- package/blocks/analytics/hooks/use-report.ts +26 -0
- package/blocks/analytics/lib/analytics-types.ts +108 -0
- package/blocks/analytics/lib/use-debounced.ts +16 -0
- package/blocks/analytics/recipes/01-weekly-outbound-report.ts +32 -0
- package/blocks/analytics/recipes/02-revenue-snapshot.ts +22 -0
- package/blocks/analytics/recipes/03-saved-report.ts +30 -0
- package/blocks/appointments/README.md +80 -0
- package/blocks/appointments/block.json +218 -0
- package/blocks/appointments/components/availability-editor.tsx +192 -0
- package/blocks/appointments/components/booking-page.tsx +154 -0
- package/blocks/appointments/components/event-type-editor.tsx +214 -0
- package/blocks/appointments/components/meeting-detail.tsx +147 -0
- package/blocks/appointments/components/meeting-list.tsx +148 -0
- package/blocks/appointments/hooks/use-available-slots.ts +29 -0
- package/blocks/appointments/hooks/use-schedules.ts +18 -0
- package/blocks/appointments/lib/appointments-types.ts +159 -0
- package/blocks/appointments/lib/idempotency.ts +30 -0
- package/blocks/appointments/recipes/01-create-event-type-and-hours.ts +27 -0
- package/blocks/appointments/recipes/02-find-slots-and-book.ts +34 -0
- package/blocks/appointments/recipes/03-bookings-digest.ts +22 -0
- package/blocks/appointments/recipes/04-calendar-invite.ts +21 -0
- package/blocks/block.schema.json +130 -0
- package/blocks/block.types.ts +109 -0
- package/blocks/campaigns/README.md +118 -0
- package/blocks/campaigns/block.json +361 -0
- package/blocks/campaigns/components/campaign-assets.tsx +180 -0
- package/blocks/campaigns/components/campaign-audience-step.tsx +91 -0
- package/blocks/campaigns/components/campaign-launch-step.tsx +147 -0
- package/blocks/campaigns/components/campaign-list.tsx +120 -0
- package/blocks/campaigns/components/campaign-metrics.tsx +93 -0
- package/blocks/campaigns/components/campaign-select.tsx +52 -0
- package/blocks/campaigns/components/campaign-sequence-step.tsx +158 -0
- package/blocks/campaigns/components/campaign-wizard.tsx +238 -0
- package/blocks/campaigns/components/form-field.tsx +29 -0
- package/blocks/campaigns/components/icp-editor.tsx +230 -0
- package/blocks/campaigns/components/persona-editor.tsx +210 -0
- package/blocks/campaigns/hooks/use-company-profile.ts +34 -0
- package/blocks/campaigns/lib/campaigns-utils.ts +155 -0
- package/blocks/campaigns/recipes/attach-audience-and-launch.ts +33 -0
- package/blocks/campaigns/recipes/campaign-report.ts +22 -0
- package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +51 -0
- package/blocks/campaigns/recipes/set-up-icp-and-personas.ts +37 -0
- package/blocks/coach/README.md +93 -0
- package/blocks/coach/block.json +125 -0
- package/blocks/coach/components/call-scorecard.tsx +126 -0
- package/blocks/coach/components/coaching-feed.tsx +140 -0
- package/blocks/coach/lib/coach-types.ts +116 -0
- package/blocks/coach/recipes/read-a-scorecard.ts +27 -0
- package/blocks/coach/recipes/weekly-coaching-digest.ts +53 -0
- package/blocks/copilot/README.md +99 -0
- package/blocks/copilot/block.json +151 -0
- package/blocks/copilot/components/copilot-memory.tsx +87 -0
- package/blocks/copilot/components/copilot-panel.tsx +189 -0
- package/blocks/copilot/components/copilot-team-chat.tsx +111 -0
- package/blocks/copilot/lib/copilot-types.ts +111 -0
- package/blocks/copilot/recipes/ask-the-copilot.ts +35 -0
- package/blocks/copilot/recipes/post-in-a-work-channel.ts +33 -0
- package/blocks/copilot/recipes/remember-and-recall.ts +19 -0
- package/blocks/crm/README.md +122 -0
- package/blocks/crm/block.json +378 -0
- package/blocks/crm/components/activity-timeline.tsx +99 -0
- package/blocks/crm/components/company-detail.tsx +118 -0
- package/blocks/crm/components/company-table.tsx +173 -0
- package/blocks/crm/components/contact-detail.tsx +135 -0
- package/blocks/crm/components/contact-table.tsx +217 -0
- package/blocks/crm/components/dedupe-review.tsx +164 -0
- package/blocks/crm/components/import-wizard.tsx +193 -0
- package/blocks/crm/components/notes-panel.tsx +112 -0
- package/blocks/crm/components/record-form.tsx +191 -0
- package/blocks/crm/components/task-list.tsx +107 -0
- package/blocks/crm/hooks/use-custom-fields.ts +18 -0
- package/blocks/crm/hooks/use-debounced.ts +13 -0
- package/blocks/crm/hooks/use-page-query.ts +52 -0
- package/blocks/crm/lib/crm-types.ts +136 -0
- package/blocks/crm/recipes/custom-fields-and-values.ts +28 -0
- package/blocks/crm/recipes/dedupe-and-merge.ts +30 -0
- package/blocks/crm/recipes/notes-tasks-and-activity.ts +23 -0
- package/blocks/crm/recipes/upsert-contact-with-company.ts +33 -0
- package/blocks/deals/README.md +78 -0
- package/blocks/deals/block.json +198 -0
- package/blocks/deals/components/deal-detail.tsx +134 -0
- package/blocks/deals/components/forecast-view.tsx +112 -0
- package/blocks/deals/components/pipeline-board.tsx +142 -0
- package/blocks/deals/components/stage-editor.tsx +96 -0
- package/blocks/deals/hooks/use-pipeline-deals.ts +67 -0
- package/blocks/deals/hooks/use-pipelines.ts +19 -0
- package/blocks/deals/lib/deals-types.ts +128 -0
- package/blocks/deals/recipes/01-create-deal-and-advance.ts +32 -0
- package/blocks/deals/recipes/02-pipeline-report.ts +35 -0
- package/blocks/deals/recipes/03-stale-deals-sweep.ts +20 -0
- package/blocks/dialer/README.md +114 -0
- package/blocks/dialer/block.json +260 -0
- package/blocks/dialer/components/call-log.tsx +78 -0
- package/blocks/dialer/components/call-queue.tsx +188 -0
- package/blocks/dialer/components/dialer-workspace.tsx +47 -0
- package/blocks/dialer/components/disposition-panel.tsx +75 -0
- package/blocks/dialer/components/live-calls-table.tsx +107 -0
- package/blocks/dialer/components/session-start-form.tsx +111 -0
- package/blocks/dialer/components/softphone.tsx +122 -0
- package/blocks/dialer/components/transcript-viewer.tsx +82 -0
- package/blocks/dialer/hooks/use-dialer-numbers.ts +19 -0
- package/blocks/dialer/hooks/use-dialer-session.ts +68 -0
- package/blocks/dialer/hooks/use-dispositions.ts +29 -0
- package/blocks/dialer/lib/dialer-types.ts +158 -0
- package/blocks/dialer/lib/softphone-transport.ts +67 -0
- package/blocks/dialer/lib/twilio-transport.ts +74 -0
- package/blocks/dialer/recipes/01-create-session-and-dial.ts +39 -0
- package/blocks/dialer/recipes/02-disposition-calls.ts +26 -0
- package/blocks/dialer/recipes/03-review-a-call.ts +23 -0
- package/blocks/enrichment/README.md +105 -0
- package/blocks/enrichment/block.json +204 -0
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +205 -0
- package/blocks/enrichment/components/email-verify-badge.tsx +82 -0
- package/blocks/enrichment/components/enrich-button.tsx +136 -0
- package/blocks/enrichment/components/waterfall-config.tsx +127 -0
- package/blocks/enrichment/hooks/use-providers.ts +18 -0
- package/blocks/enrichment/lib/enrichment-types.ts +104 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +41 -0
- package/blocks/enrichment/recipes/lookup-and-verify.ts +25 -0
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +26 -0
- package/blocks/forms/README.md +112 -0
- package/blocks/forms/block.json +200 -0
- package/blocks/forms/components/form-builder.tsx +150 -0
- package/blocks/forms/components/form-embed.tsx +103 -0
- package/blocks/forms/components/form-mapping-editor.tsx +125 -0
- package/blocks/forms/components/site-audit-panel.tsx +73 -0
- package/blocks/forms/components/submissions-table.tsx +158 -0
- package/blocks/forms/lib/form-types.ts +127 -0
- package/blocks/forms/recipes/install-snippet-and-form.ts +21 -0
- package/blocks/forms/recipes/retry-failed-deliveries.ts +26 -0
- package/blocks/forms/recipes/route-form-to-list.ts +44 -0
- package/blocks/inbox/README.md +72 -0
- package/blocks/inbox/block.json +194 -0
- package/blocks/inbox/components/inbox-list.tsx +156 -0
- package/blocks/inbox/components/inbox-screen.tsx +49 -0
- package/blocks/inbox/components/reply-composer.tsx +183 -0
- package/blocks/inbox/components/tag-and-assign.tsx +189 -0
- package/blocks/inbox/components/thread-badge.tsx +17 -0
- package/blocks/inbox/components/thread-view.tsx +108 -0
- package/blocks/inbox/hooks/use-inbox-thread.ts +24 -0
- package/blocks/inbox/hooks/use-inbox-threads.ts +40 -0
- package/blocks/inbox/lib/idempotency.ts +30 -0
- package/blocks/inbox/lib/inbox-types.ts +100 -0
- package/blocks/inbox/recipes/01-triage-and-reply.ts +43 -0
- package/blocks/inbox/recipes/02-tag-and-assign.ts +31 -0
- package/blocks/inbox/recipes/03-hot-contacts-digest.ts +16 -0
- package/blocks/integrations/README.md +75 -0
- package/blocks/integrations/block.json +172 -0
- package/blocks/integrations/components/integration-list.tsx +165 -0
- package/blocks/integrations/components/sync-status.tsx +223 -0
- package/blocks/integrations/components/webhook-manager.tsx +300 -0
- package/blocks/integrations/lib/integrations-utils.ts +62 -0
- package/blocks/integrations/recipes/crm-sync-health.ts +19 -0
- package/blocks/integrations/recipes/push-contacts-to-crm.ts +20 -0
- package/blocks/integrations/recipes/register-webhook.ts +18 -0
- package/blocks/integrations/recipes/verify-webhook-route.ts +33 -0
- package/blocks/intent/README.md +57 -0
- package/blocks/intent/block.json +138 -0
- package/blocks/intent/components/account-intent-score.tsx +91 -0
- package/blocks/intent/components/intent-feed.tsx +215 -0
- package/blocks/intent/components/intent-keyword-manager.tsx +331 -0
- package/blocks/intent/hooks/use-account-intent.ts +46 -0
- package/blocks/intent/lib/intent-utils.ts +95 -0
- package/blocks/intent/recipes/rank-intent-accounts.ts +36 -0
- package/blocks/intent/recipes/track-competitor-domain.ts +23 -0
- package/blocks/intent/recipes/who-visited-url.ts +18 -0
- package/blocks/lifecycle/README.md +79 -0
- package/blocks/lifecycle/block.json +198 -0
- package/blocks/lifecycle/components/churn-list.tsx +92 -0
- package/blocks/lifecycle/components/customer-table.tsx +119 -0
- package/blocks/lifecycle/components/lead-queue.tsx +127 -0
- package/blocks/lifecycle/components/lifecycle-board.tsx +102 -0
- package/blocks/lifecycle/components/stage-move-dialog.tsx +75 -0
- package/blocks/lifecycle/hooks/use-debounced.ts +13 -0
- package/blocks/lifecycle/hooks/use-lifecycle-stages.ts +27 -0
- package/blocks/lifecycle/lib/lifecycle.ts +99 -0
- package/blocks/lifecycle/recipes/customer-brief.ts +23 -0
- package/blocks/lifecycle/recipes/move-company-stage.ts +17 -0
- package/blocks/lifecycle/recipes/read-lifecycle-board.ts +21 -0
- package/blocks/lifecycle/recipes/unlock-stage-contacts.ts +25 -0
- package/blocks/lists/README.md +96 -0
- package/blocks/lists/block.json +166 -0
- package/blocks/lists/components/add-to-list-button.tsx +111 -0
- package/blocks/lists/components/list-picker.tsx +94 -0
- package/blocks/lists/components/list-table.tsx +160 -0
- package/blocks/lists/components/suppression-manager.tsx +153 -0
- package/blocks/lists/recipes/create-list-and-add-contacts.ts +35 -0
- package/blocks/lists/recipes/refresh-and-check-lists.ts +21 -0
- package/blocks/lists/recipes/suppress-and-reinstate.ts +30 -0
- package/blocks/mailboxes/README.md +71 -0
- package/blocks/mailboxes/block.json +187 -0
- package/blocks/mailboxes/components/connect-mailbox.tsx +284 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +271 -0
- package/blocks/mailboxes/components/mailbox-list.tsx +185 -0
- package/blocks/mailboxes/components/warmup-health.tsx +232 -0
- package/blocks/mailboxes/lib/mailboxes-utils.ts +69 -0
- package/blocks/mailboxes/recipes/buy-domain-and-mailboxes.ts +64 -0
- package/blocks/mailboxes/recipes/connect-imap-mailbox.ts +38 -0
- package/blocks/mailboxes/recipes/is-it-safe-to-send.ts +17 -0
- package/blocks/mailboxes/recipes/warmup-report.ts +31 -0
- package/blocks/newsletter/README.md +122 -0
- package/blocks/newsletter/block.json +224 -0
- package/blocks/newsletter/components/issue-form.tsx +124 -0
- package/blocks/newsletter/components/issue-scheduler.tsx +152 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +131 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +93 -0
- package/blocks/newsletter/components/subscriber-table.tsx +173 -0
- package/blocks/newsletter/lib/newsletter-types.ts +97 -0
- package/blocks/newsletter/recipes/create-newsletter-and-draft-issue.ts +27 -0
- package/blocks/newsletter/recipes/import-audience-and-send.ts +43 -0
- package/blocks/newsletter/recipes/issue-performance-report.ts +26 -0
- package/blocks/nurture/README.md +110 -0
- package/blocks/nurture/block.json +191 -0
- package/blocks/nurture/components/nurture-builder.tsx +114 -0
- package/blocks/nurture/components/nurture-enroll.tsx +107 -0
- package/blocks/nurture/components/nurture-form.tsx +120 -0
- package/blocks/nurture/components/nurture-stats.tsx +81 -0
- package/blocks/nurture/lib/nurture-types.ts +109 -0
- package/blocks/nurture/recipes/create-a-nurture.ts +34 -0
- package/blocks/nurture/recipes/enroll-contacts-and-launch.ts +24 -0
- package/blocks/nurture/recipes/nurture-health-check.ts +28 -0
- package/blocks/objects/README.md +120 -0
- package/blocks/objects/block.json +243 -0
- package/blocks/objects/components/attribute-field-input.tsx +82 -0
- package/blocks/objects/components/attribute-form-dialog.tsx +159 -0
- package/blocks/objects/components/object-not-found.tsx +34 -0
- package/blocks/objects/components/object-record-detail.tsx +143 -0
- package/blocks/objects/components/object-schema-editor.tsx +197 -0
- package/blocks/objects/components/object-table.tsx +130 -0
- package/blocks/objects/components/record-form-dialog.tsx +112 -0
- package/blocks/objects/components/record-roles-panel.tsx +106 -0
- package/blocks/objects/hooks/use-object-schema.ts +36 -0
- package/blocks/objects/lib/object-types.ts +214 -0
- package/blocks/objects/recipes/define-object-and-add-records.ts +24 -0
- package/blocks/objects/recipes/update-audit-and-archive-records.ts +39 -0
- package/blocks/objects/recipes/upsert-records-from-your-system.ts +30 -0
- package/blocks/pages/README.md +104 -0
- package/blocks/pages/block.json +223 -0
- package/blocks/pages/components/brand-kit-editor.tsx +152 -0
- package/blocks/pages/components/content-library.tsx +163 -0
- package/blocks/pages/components/page-editor.tsx +253 -0
- package/blocks/pages/components/page-list.tsx +159 -0
- package/blocks/pages/components/seo-report.tsx +159 -0
- package/blocks/pages/lib/studio.ts +77 -0
- package/blocks/pages/recipes/audit-site-seo.ts +28 -0
- package/blocks/pages/recipes/create-and-publish-page.ts +26 -0
- package/blocks/pages/recipes/edit-page-section.ts +25 -0
- package/blocks/pages/recipes/refresh-brand-kit.ts +22 -0
- package/blocks/quotes/README.md +79 -0
- package/blocks/quotes/block.json +183 -0
- package/blocks/quotes/components/product-picker.tsx +80 -0
- package/blocks/quotes/components/quote-builder.tsx +195 -0
- package/blocks/quotes/components/quote-list.tsx +79 -0
- package/blocks/quotes/components/quote-viewer.tsx +135 -0
- package/blocks/quotes/components/signature-status.tsx +77 -0
- package/blocks/quotes/hooks/use-debounced.ts +13 -0
- package/blocks/quotes/lib/quotes.ts +142 -0
- package/blocks/quotes/recipes/create-and-send-quote.ts +44 -0
- package/blocks/quotes/recipes/revise-sent-quote.ts +18 -0
- package/blocks/quotes/recipes/track-quote-signature.ts +22 -0
- package/blocks/radar/README.md +106 -0
- package/blocks/radar/block.json +189 -0
- package/blocks/radar/components/competitor-battle-card.tsx +53 -0
- package/blocks/radar/components/competitor-tracker.tsx +158 -0
- package/blocks/radar/components/radar-overview.tsx +67 -0
- package/blocks/radar/components/radar-report.tsx +86 -0
- package/blocks/radar/hooks/use-competitors.ts +23 -0
- package/blocks/radar/lib/radar-types.ts +124 -0
- package/blocks/radar/recipes/battle-card.ts +28 -0
- package/blocks/radar/recipes/deep-dive-report.ts +32 -0
- package/blocks/radar/recipes/marketing-snapshot.ts +18 -0
- package/blocks/radar/recipes/track-and-scan.ts +39 -0
- package/blocks/search/README.md +129 -0
- package/blocks/search/block.json +185 -0
- package/blocks/search/components/filter-builder.tsx +149 -0
- package/blocks/search/components/prospect-results.tsx +175 -0
- package/blocks/search/components/save-to-list-dialog.tsx +135 -0
- package/blocks/search/components/saved-searches.tsx +95 -0
- package/blocks/search/hooks/use-filter-options.ts +41 -0
- package/blocks/search/lib/search-types.ts +149 -0
- package/blocks/search/recipes/explore-filter-values.ts +27 -0
- package/blocks/search/recipes/saved-searches.ts +34 -0
- package/blocks/search/recipes/search-and-save-to-list.ts +37 -0
- package/blocks/sequencer/README.md +87 -0
- package/blocks/sequencer/block.json +214 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +93 -0
- package/blocks/sequencer/components/sequence-builder.tsx +306 -0
- package/blocks/sequencer/components/sequence-list.tsx +200 -0
- package/blocks/sequencer/components/sequence-stats.tsx +256 -0
- package/blocks/sequencer/components/step-editor.tsx +213 -0
- package/blocks/sequencer/lib/sequencer-utils.ts +168 -0
- package/blocks/sequencer/recipes/create-multichannel-sequence.ts +51 -0
- package/blocks/sequencer/recipes/launch-and-enroll.ts +44 -0
- package/blocks/sequencer/recipes/pause-when-unsafe.ts +30 -0
- package/blocks/sequencer/recipes/sequence-report.ts +22 -0
- package/blocks/social-ads/README.md +128 -0
- package/blocks/social-ads/block.json +256 -0
- package/blocks/social-ads/components/ad-campaign-list.tsx +136 -0
- package/blocks/social-ads/components/audience-sync.tsx +150 -0
- package/blocks/social-ads/components/creative-gallery.tsx +121 -0
- package/blocks/social-ads/components/post-composer.tsx +122 -0
- package/blocks/social-ads/components/social-post-list.tsx +116 -0
- package/blocks/social-ads/hooks/use-social-accounts.ts +16 -0
- package/blocks/social-ads/lib/social-ads-types.ts +167 -0
- package/blocks/social-ads/recipes/creative-to-gallery.ts +41 -0
- package/blocks/social-ads/recipes/schedule-post.ts +51 -0
- package/blocks/social-ads/recipes/sync-audience-and-launch.ts +41 -0
- package/blocks/visitors/README.md +56 -0
- package/blocks/visitors/block.json +139 -0
- package/blocks/visitors/components/snippet-install.tsx +169 -0
- package/blocks/visitors/components/traffic-chart.tsx +156 -0
- package/blocks/visitors/components/visitor-company-card.tsx +92 -0
- package/blocks/visitors/components/visitor-feed.tsx +157 -0
- package/blocks/visitors/lib/visitors-utils.ts +78 -0
- package/blocks/visitors/recipes/check-snippet-installed.ts +24 -0
- package/blocks/visitors/recipes/recent-company-visits.ts +27 -0
- package/blocks/visitors/recipes/traffic-summary.ts +24 -0
- package/blocks/webchat/README.md +87 -0
- package/blocks/webchat/block.json +134 -0
- package/blocks/webchat/components/chat-widget-config.tsx +160 -0
- package/blocks/webchat/components/conversation-list.tsx +74 -0
- package/blocks/webchat/lib/webchat-types.ts +83 -0
- package/blocks/webchat/recipes/create-bot-and-style-widget.ts +24 -0
- package/blocks/webchat/recipes/list-chat-visitors.ts +24 -0
- package/blocks/workbench/README.md +135 -0
- package/blocks/workbench/block.json +259 -0
- package/blocks/workbench/components/column-recipe-editor.tsx +183 -0
- package/blocks/workbench/components/run-progress.tsx +132 -0
- package/blocks/workbench/components/workbench-add-rows.tsx +100 -0
- package/blocks/workbench/components/workbench-grid.tsx +159 -0
- package/blocks/workbench/components/workbench-row-actions.tsx +102 -0
- package/blocks/workbench/components/workbench-table-list.tsx +105 -0
- package/blocks/workbench/hooks/use-workbench-table.ts +30 -0
- package/blocks/workbench/lib/waterfall-presets.ts +119 -0
- package/blocks/workbench/lib/workbench-types.ts +191 -0
- package/blocks/workbench/recipes/build-enriched-table.ts +65 -0
- package/blocks/workbench/recipes/import-from-search.ts +42 -0
- package/blocks/workbench/recipes/scrape-into-table.ts +36 -0
- package/blocks/workbench/recipes/waterfall-column.ts +55 -0
- package/blocks/workflows/README.md +100 -0
- package/blocks/workflows/block.json +188 -0
- package/blocks/workflows/components/run-history.tsx +136 -0
- package/blocks/workflows/components/trigger-config.tsx +134 -0
- package/blocks/workflows/components/workflow-editor.tsx +142 -0
- package/blocks/workflows/components/workflow-list.tsx +153 -0
- package/blocks/workflows/hooks/use-workflow.ts +51 -0
- package/blocks/workflows/lib/graph.ts +187 -0
- package/blocks/workflows/recipes/create-workflow.ts +35 -0
- package/blocks/workflows/recipes/find-failed-runs.ts +15 -0
- package/blocks/workflows/recipes/run-workflow-and-wait.ts +32 -0
- package/blocks/workflows/recipes/set-trigger.ts +22 -0
- package/dist/cli.js +5330 -29
- package/dist/index.browser.js +7740 -0
- package/dist/index.browser.js.map +1 -0
- package/dist/index.browser.mjs +7717 -0
- package/dist/index.browser.mjs.map +1 -0
- package/dist/index.d.mts +2220 -23
- package/dist/index.d.ts +2220 -23
- package/dist/index.js +64 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +64 -3
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +7598 -0
- package/dist/react.browser.js.map +1 -0
- package/dist/react.browser.mjs +7568 -0
- package/dist/react.browser.mjs.map +1 -0
- package/dist/react.d.mts +2082 -20
- package/dist/react.d.ts +2082 -20
- package/dist/react.js +63 -2
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +63 -2
- package/dist/react.mjs.map +1 -1
- package/dist/types/agentSetup.d.ts +6 -6
- package/dist/types/blocks/add.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +46 -0
- package/dist/types/blocks/doctor.d.ts +26 -0
- package/dist/types/blocks/registry.d.ts +134 -0
- package/dist/types/cli.d.ts +16 -0
- package/dist/types/core.d.ts +1 -1
- package/dist/types/crypto-browser.d.ts +2 -0
- package/dist/types/crypto-node.d.ts +6 -0
- package/dist/types/react.d.ts +1 -1
- package/dist/types/version.d.ts +2 -2
- package/dist/types/webhooks.d.ts +2 -2
- package/dist/ui/index.d.mts +54 -0
- package/dist/ui/index.d.ts +54 -0
- package/dist/ui/index.js +105 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/index.mjs +44 -0
- package/dist/ui/index.mjs.map +1 -0
- package/dist/ui/lib/compose-refs.d.mts +19 -0
- package/dist/ui/lib/compose-refs.d.ts +19 -0
- package/dist/ui/lib/compose-refs.js +76 -0
- package/dist/ui/lib/compose-refs.js.map +1 -0
- package/dist/ui/lib/compose-refs.mjs +41 -0
- package/dist/ui/lib/compose-refs.mjs.map +1 -0
- package/dist/ui/lib/use-mobile.d.mts +4 -0
- package/dist/ui/lib/use-mobile.d.ts +4 -0
- package/dist/ui/lib/use-mobile.js +57 -0
- package/dist/ui/lib/use-mobile.js.map +1 -0
- package/dist/ui/lib/use-mobile.mjs +22 -0
- package/dist/ui/lib/use-mobile.mjs.map +1 -0
- package/dist/ui/lib/utils.d.mts +10 -0
- package/dist/ui/lib/utils.d.ts +10 -0
- package/dist/ui/lib/utils.js +33 -0
- package/dist/ui/lib/utils.js.map +1 -0
- package/dist/ui/lib/utils.mjs +9 -0
- package/dist/ui/lib/utils.mjs.map +1 -0
- package/dist/ui/primitives/alert-dialog.d.mts +22 -0
- package/dist/ui/primitives/alert-dialog.d.ts +22 -0
- package/dist/ui/primitives/alert-dialog.js +163 -0
- package/dist/ui/primitives/alert-dialog.js.map +1 -0
- package/dist/ui/primitives/alert-dialog.mjs +119 -0
- package/dist/ui/primitives/alert-dialog.mjs.map +1 -0
- package/dist/ui/primitives/avatar.d.mts +8 -0
- package/dist/ui/primitives/avatar.d.ts +8 -0
- package/dist/ui/primitives/avatar.js +91 -0
- package/dist/ui/primitives/avatar.js.map +1 -0
- package/dist/ui/primitives/avatar.mjs +55 -0
- package/dist/ui/primitives/avatar.mjs.map +1 -0
- package/dist/ui/primitives/breadcrumb.d.mts +13 -0
- package/dist/ui/primitives/breadcrumb.d.ts +13 -0
- package/dist/ui/primitives/breadcrumb.js +127 -0
- package/dist/ui/primitives/breadcrumb.js.map +1 -0
- package/dist/ui/primitives/breadcrumb.mjs +97 -0
- package/dist/ui/primitives/breadcrumb.mjs.map +1 -0
- package/dist/ui/primitives/button.d.mts +15 -0
- package/dist/ui/primitives/button.d.ts +15 -0
- package/dist/ui/primitives/button.js +91 -0
- package/dist/ui/primitives/button.js.map +1 -0
- package/dist/ui/primitives/button.mjs +56 -0
- package/dist/ui/primitives/button.mjs.map +1 -0
- package/dist/ui/primitives/card.d.mts +10 -0
- package/dist/ui/primitives/card.d.ts +10 -0
- package/dist/ui/primitives/card.js +101 -0
- package/dist/ui/primitives/card.js.map +1 -0
- package/dist/ui/primitives/card.mjs +62 -0
- package/dist/ui/primitives/card.mjs.map +1 -0
- package/dist/ui/primitives/carousel.d.mts +45 -0
- package/dist/ui/primitives/carousel.d.ts +45 -0
- package/dist/ui/primitives/carousel.js +300 -0
- package/dist/ui/primitives/carousel.js.map +1 -0
- package/dist/ui/primitives/carousel.mjs +261 -0
- package/dist/ui/primitives/carousel.mjs.map +1 -0
- package/dist/ui/primitives/chart.d.mts +68 -0
- package/dist/ui/primitives/chart.d.ts +68 -0
- package/dist/ui/primitives/chart.js +280 -0
- package/dist/ui/primitives/chart.js.map +1 -0
- package/dist/ui/primitives/chart.mjs +241 -0
- package/dist/ui/primitives/chart.mjs.map +1 -0
- package/dist/ui/primitives/collapsible.d.mts +8 -0
- package/dist/ui/primitives/collapsible.d.ts +8 -0
- package/dist/ui/primitives/collapsible.js +72 -0
- package/dist/ui/primitives/collapsible.js.map +1 -0
- package/dist/ui/primitives/collapsible.mjs +36 -0
- package/dist/ui/primitives/collapsible.mjs.map +1 -0
- package/dist/ui/primitives/context-menu.d.mts +29 -0
- package/dist/ui/primitives/context-menu.d.ts +29 -0
- package/dist/ui/primitives/context-menu.js +203 -0
- package/dist/ui/primitives/context-menu.js.map +1 -0
- package/dist/ui/primitives/context-menu.mjs +155 -0
- package/dist/ui/primitives/context-menu.mjs.map +1 -0
- package/dist/ui/primitives/dialog.d.mts +26 -0
- package/dist/ui/primitives/dialog.d.ts +26 -0
- package/dist/ui/primitives/dialog.js +192 -0
- package/dist/ui/primitives/dialog.js.map +1 -0
- package/dist/ui/primitives/dialog.mjs +148 -0
- package/dist/ui/primitives/dialog.mjs.map +1 -0
- package/dist/ui/primitives/drawer.d.mts +26 -0
- package/dist/ui/primitives/drawer.d.ts +26 -0
- package/dist/ui/primitives/drawer.js +146 -0
- package/dist/ui/primitives/drawer.js.map +1 -0
- package/dist/ui/primitives/drawer.mjs +103 -0
- package/dist/ui/primitives/drawer.mjs.map +1 -0
- package/dist/ui/primitives/input.d.mts +5 -0
- package/dist/ui/primitives/input.d.ts +5 -0
- package/dist/ui/primitives/input.js +46 -0
- package/dist/ui/primitives/input.js.map +1 -0
- package/dist/ui/primitives/input.mjs +22 -0
- package/dist/ui/primitives/input.mjs.map +1 -0
- package/dist/ui/primitives/item.d.mts +27 -0
- package/dist/ui/primitives/item.d.ts +27 -0
- package/dist/ui/primitives/item.js +221 -0
- package/dist/ui/primitives/item.js.map +1 -0
- package/dist/ui/primitives/item.mjs +188 -0
- package/dist/ui/primitives/item.mjs.map +1 -0
- package/dist/ui/primitives/kbd.d.mts +15 -0
- package/dist/ui/primitives/kbd.d.ts +15 -0
- package/dist/ui/primitives/kbd.js +45 -0
- package/dist/ui/primitives/kbd.js.map +1 -0
- package/dist/ui/primitives/kbd.mjs +21 -0
- package/dist/ui/primitives/kbd.mjs.map +1 -0
- package/dist/ui/primitives/label.d.mts +6 -0
- package/dist/ui/primitives/label.d.ts +6 -0
- package/dist/ui/primitives/label.js +55 -0
- package/dist/ui/primitives/label.js.map +1 -0
- package/dist/ui/primitives/label.mjs +21 -0
- package/dist/ui/primitives/label.mjs.map +1 -0
- package/dist/ui/primitives/resizable.d.mts +10 -0
- package/dist/ui/primitives/resizable.d.ts +10 -0
- package/dist/ui/primitives/resizable.js +68 -0
- package/dist/ui/primitives/resizable.js.map +1 -0
- package/dist/ui/primitives/resizable.mjs +42 -0
- package/dist/ui/primitives/resizable.mjs.map +1 -0
- package/dist/ui/primitives/save-status.d.mts +31 -0
- package/dist/ui/primitives/save-status.d.ts +31 -0
- package/dist/ui/primitives/save-status.js +150 -0
- package/dist/ui/primitives/save-status.js.map +1 -0
- package/dist/ui/primitives/save-status.mjs +126 -0
- package/dist/ui/primitives/save-status.mjs.map +1 -0
- package/dist/ui/primitives/separator.d.mts +6 -0
- package/dist/ui/primitives/separator.d.ts +6 -0
- package/dist/ui/primitives/separator.js +62 -0
- package/dist/ui/primitives/separator.js.map +1 -0
- package/dist/ui/primitives/separator.mjs +28 -0
- package/dist/ui/primitives/separator.mjs.map +1 -0
- package/dist/ui/primitives/sheet.d.mts +16 -0
- package/dist/ui/primitives/sheet.d.ts +16 -0
- package/dist/ui/primitives/sheet.js +170 -0
- package/dist/ui/primitives/sheet.js.map +1 -0
- package/dist/ui/primitives/sheet.mjs +129 -0
- package/dist/ui/primitives/sheet.mjs.map +1 -0
- package/dist/ui/primitives/sidebar.d.mts +74 -0
- package/dist/ui/primitives/sidebar.d.ts +74 -0
- package/dist/ui/primitives/sidebar.js +697 -0
- package/dist/ui/primitives/sidebar.js.map +1 -0
- package/dist/ui/primitives/sidebar.mjs +651 -0
- package/dist/ui/primitives/sidebar.mjs.map +1 -0
- package/dist/ui/primitives/skeleton.d.mts +5 -0
- package/dist/ui/primitives/skeleton.d.ts +5 -0
- package/dist/ui/primitives/skeleton.js +40 -0
- package/dist/ui/primitives/skeleton.js.map +1 -0
- package/dist/ui/primitives/skeleton.mjs +16 -0
- package/dist/ui/primitives/skeleton.mjs.map +1 -0
- package/dist/ui/primitives/slider.d.mts +20 -0
- package/dist/ui/primitives/slider.d.ts +20 -0
- package/dist/ui/primitives/slider.js +112 -0
- package/dist/ui/primitives/slider.js.map +1 -0
- package/dist/ui/primitives/slider.mjs +78 -0
- package/dist/ui/primitives/slider.mjs.map +1 -0
- package/dist/ui/primitives/sortable.d.mts +50 -0
- package/dist/ui/primitives/sortable.d.ts +50 -0
- package/dist/ui/primitives/sortable.js +456 -0
- package/dist/ui/primitives/sortable.js.map +1 -0
- package/dist/ui/primitives/sortable.mjs +435 -0
- package/dist/ui/primitives/sortable.mjs.map +1 -0
- package/dist/ui/primitives/switch.d.mts +6 -0
- package/dist/ui/primitives/switch.d.ts +6 -0
- package/dist/ui/primitives/switch.js +67 -0
- package/dist/ui/primitives/switch.js.map +1 -0
- package/dist/ui/primitives/switch.mjs +33 -0
- package/dist/ui/primitives/switch.mjs.map +1 -0
- package/dist/ui/primitives/table.d.mts +12 -0
- package/dist/ui/primitives/table.d.ts +12 -0
- package/dist/ui/primitives/table.js +130 -0
- package/dist/ui/primitives/table.js.map +1 -0
- package/dist/ui/primitives/table.mjs +89 -0
- package/dist/ui/primitives/table.mjs.map +1 -0
- package/dist/ui/primitives/tabs.d.mts +9 -0
- package/dist/ui/primitives/tabs.d.ts +9 -0
- package/dist/ui/primitives/tabs.js +86 -0
- package/dist/ui/primitives/tabs.js.map +1 -0
- package/dist/ui/primitives/tabs.mjs +49 -0
- package/dist/ui/primitives/tabs.mjs.map +1 -0
- package/dist/ui/primitives/textarea.d.mts +5 -0
- package/dist/ui/primitives/textarea.d.ts +5 -0
- package/dist/ui/primitives/textarea.js +43 -0
- package/dist/ui/primitives/textarea.js.map +1 -0
- package/dist/ui/primitives/textarea.mjs +19 -0
- package/dist/ui/primitives/textarea.mjs.map +1 -0
- package/dist/ui/primitives/tooltip.d.mts +9 -0
- package/dist/ui/primitives/tooltip.d.ts +9 -0
- package/dist/ui/primitives/tooltip.js +94 -0
- package/dist/ui/primitives/tooltip.js.map +1 -0
- package/dist/ui/primitives/tooltip.mjs +57 -0
- package/dist/ui/primitives/tooltip.mjs.map +1 -0
- package/docs/agents.md +72 -29
- package/docs/api/README.md +7 -3
- package/docs/api/appObjects.d.ts +3 -1
- package/docs/api/appObjects.md +2 -1
- package/docs/api/by-path.md +51 -0
- package/docs/api/dataPipelines.d.ts +149 -0
- package/docs/api/dataPipelines.md +232 -0
- package/docs/api/forms.d.ts +16 -0
- package/docs/api/forms.md +8 -0
- package/docs/api/objects.d.ts +705 -5
- package/docs/api/objects.md +1099 -14
- package/docs/api/ops.d.ts +1 -1
- package/docs/api/ops.md +1 -1
- package/docs/api/processLists.d.ts +247 -0
- package/docs/api/processLists.md +409 -0
- package/docs/api/quotes.d.ts +3 -1
- package/docs/api/quotes.md +7 -2
- package/docs/api/radar.d.ts +4 -0
- package/docs/api/radar.md +1 -0
- package/docs/api/reports.d.ts +476 -0
- package/docs/api/reports.md +539 -0
- package/docs/api/scheduleBuy.d.ts +12 -8
- package/docs/api/scheduleBuy.md +13 -10
- package/docs/api/seo.d.ts +406 -3
- package/docs/api/seo.md +505 -1
- package/docs/api/workspaceBackup.d.ts +324 -0
- package/docs/api/workspaceBackup.md +427 -0
- package/package.json +174 -5
- package/stacks/6sense.json +46 -0
- package/stacks/README.md +20 -0
- package/stacks/apollo.json +128 -0
- package/stacks/clay.json +59 -0
- package/stacks/gong.json +72 -0
- package/stacks/hubspot.json +162 -0
- package/stacks/intercom.json +53 -0
- package/stacks/mailchimp.json +71 -0
- package/stacks/outreach.json +86 -0
- package/stacks/salesforce.json +94 -0
- package/stacks/stack.schema.json +32 -0
- package/stacks/starter.json +22 -0
- package/styles/styles.css +2 -0
- package/styles/theme.css +181 -0
- package/styles/tokens.css +284 -0
- package/templates/next-app/README.md.tmpl +35 -0
- package/templates/next-app/_pages/redirect-page.tsx.tmpl +5 -0
- package/templates/next-app/_pages/route-page.tsx.tmpl +9 -0
- package/templates/next-app/app/globals.css.tmpl +8 -0
- package/templates/next-app/app/layout.tsx.tmpl +19 -0
- package/templates/next-app/components/app-shell.tsx.tmpl +34 -0
- package/templates/next-app/dot-env.local.example.tmpl +6 -0
- package/templates/next-app/dot-gitignore.tmpl +7 -0
- package/templates/next-app/lib/nav.ts.tmpl +2 -0
- package/templates/next-app/next.config.mjs.tmpl +4 -0
- package/templates/next-app/package.json.tmpl +25 -0
- package/templates/next-app/postcss.config.mjs.tmpl +4 -0
- package/templates/next-app/tsconfig.json.tmpl +21 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PipelineBoard: the deals kanban. One column per stage, drag a card to move the deal, column counts and totals.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations, in order:
|
|
7
|
+
* 1. GET /deals/pipelines (deals.listPipelines) read - columns and stage ids
|
|
8
|
+
* 2. GET /deals?pipeline_id=&page=&limit= (deals.listDeals) read - the cards (all pages, capped)
|
|
9
|
+
* 3. PATCH /deals/{id} { stage_id, expected_revision, close_date?, amount? } (deals.updateDeal) write
|
|
10
|
+
* A stage move is an ordinary write (no confirm dialog): it contacts nobody and is easy to move back.
|
|
11
|
+
* Moving into a won/lost stage has server rules: a close date (and an amount for won) must exist. The board asks
|
|
12
|
+
* for them in a small dialog first; if the API still answers with a closed-stage marker it explains and rolls back.
|
|
13
|
+
*
|
|
14
|
+
* Drag and drop uses @dnd-kit/core (an optional peer of @graph8/sdk, declared by this block). Every card also has a
|
|
15
|
+
* "Move to" select, which is the keyboard and screen-reader path and does the exact same PATCH.
|
|
16
|
+
*/
|
|
17
|
+
import { useMemo, useState } from "react";
|
|
18
|
+
import { DndContext, KeyboardSensor, PointerSensor, useDraggable, useDroppable, useSensor, useSensors, type DragEndEvent } from "@dnd-kit/core";
|
|
19
|
+
import { Button, Input, Label, Skeleton } from "@graph8/sdk/ui";
|
|
20
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
21
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
22
|
+
import { usePipelineDeals } from "../hooks/use-pipeline-deals";
|
|
23
|
+
import { usePipelines } from "../hooks/use-pipelines";
|
|
24
|
+
import { closedStageMarker, formatDate, formatMoney, stageKind, sumAmounts, type Deal, type Stage } from "../lib/deals-types";
|
|
25
|
+
|
|
26
|
+
export interface PipelineBoardProps {
|
|
27
|
+
/** Show this pipeline; defaults to the org's default one. */
|
|
28
|
+
pipelineId?: string;
|
|
29
|
+
search?: string;
|
|
30
|
+
ownerId?: string;
|
|
31
|
+
onSelectDeal?: (deal: Deal) => void;
|
|
32
|
+
className?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function Card({ deal, stages, busy, onMove, onSelect }: { deal: Deal; stages: Stage[]; busy: boolean; onMove: (stage: Stage) => void; onSelect?: (deal: Deal) => void }) {
|
|
36
|
+
const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({ id: deal.id, disabled: busy });
|
|
37
|
+
const style = transform ? { transform: `translate3d(${transform.x}px, ${transform.y}px, 0)` } : undefined;
|
|
38
|
+
return (
|
|
39
|
+
<div ref={setNodeRef} style={style} className={`rounded-md border bg-background p-3 text-sm shadow-xs ${isDragging ? "opacity-60" : ""}`} data-deal-id={deal.id}>
|
|
40
|
+
<div className="flex items-start justify-between gap-2">
|
|
41
|
+
<button type="button" className="text-left font-medium hover:underline" onClick={() => onSelect?.(deal)}>{deal.name || "Untitled deal"}</button>
|
|
42
|
+
<button type="button" aria-label={`Drag ${deal.name ?? "deal"}`} className="cursor-grab px-1 text-muted-foreground" {...listeners} {...attributes}>::</button>
|
|
43
|
+
</div>
|
|
44
|
+
<div className="mt-1 text-muted-foreground">{formatMoney(deal.amount, deal.currency)}</div>
|
|
45
|
+
<div className="mt-1 flex justify-between text-xs text-muted-foreground">
|
|
46
|
+
<span>{deal.owner_name ?? deal.owner_email ?? "No owner"}</span>
|
|
47
|
+
<span>{formatDate(deal.close_date)}</span>
|
|
48
|
+
</div>
|
|
49
|
+
<select aria-label={`Move ${deal.name ?? "deal"} to`} aria-busy={busy} disabled={busy} className="mt-2 h-7 w-full rounded border bg-background px-1 text-xs" value={deal.stage_id ?? ""} onChange={(e) => { const s = stages.find((x) => x.id === e.target.value); if (s) onMove(s); }}>
|
|
50
|
+
{stages.map((s) => <option key={s.id} value={s.id}>{s.name}</option>)}
|
|
51
|
+
</select>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function Column({ stage, stages, deals, moving, onMove, onSelect }: { stage: Stage; stages: Stage[]; deals: Deal[]; moving: ReadonlySet<string>; onMove: (deal: Deal, stage: Stage) => void; onSelect?: (deal: Deal) => void }) {
|
|
57
|
+
const { setNodeRef, isOver } = useDroppable({ id: stage.id });
|
|
58
|
+
const kind = stageKind(stage);
|
|
59
|
+
return (
|
|
60
|
+
<section ref={setNodeRef} aria-label={stage.name} className={`flex w-72 shrink-0 flex-col gap-2 rounded-lg border bg-muted/40 p-2 ${isOver ? "ring-2 ring-primary" : ""}`}>
|
|
61
|
+
<header className="flex items-center justify-between px-1">
|
|
62
|
+
<h3 className="text-sm font-semibold">{stage.name}{kind !== "open" ? ` (${kind})` : ""}</h3>
|
|
63
|
+
<span className="text-xs text-muted-foreground">{deals.length} · {formatMoney(sumAmounts(deals), deals[0]?.currency)}</span>
|
|
64
|
+
</header>
|
|
65
|
+
<div className="flex min-h-16 flex-col gap-2">
|
|
66
|
+
{deals.map((d) => <Card key={d.id} deal={d} stages={stages} busy={moving.has(d.id)} onMove={(s) => onMove(d, s)} onSelect={onSelect} />)}
|
|
67
|
+
</div>
|
|
68
|
+
</section>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Today's date in the USER's time zone as YYYY-MM-DD (toISOString would give tomorrow's date in the evening west of UTC). */
|
|
73
|
+
export function localToday(now = new Date()): string {
|
|
74
|
+
const p = (n: number) => String(n).padStart(2, "0");
|
|
75
|
+
return `${now.getFullYear()}-${p(now.getMonth() + 1)}-${p(now.getDate())}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function CloseDialog({ stage, deal, onConfirm, onCancel }: { stage: Stage; deal: Deal; onConfirm: (extra: { close_date: string; amount?: number }) => void; onCancel: () => void }) {
|
|
79
|
+
const won = stageKind(stage) === "won";
|
|
80
|
+
const [date, setDate] = useState(deal.close_date?.slice(0, 10) ?? localToday());
|
|
81
|
+
const [amount, setAmount] = useState(deal.amount != null ? String(deal.amount) : "");
|
|
82
|
+
const valid = Boolean(date) && (!won || amount !== "");
|
|
83
|
+
return (
|
|
84
|
+
<div role="dialog" aria-modal="true" aria-label={`Close deal as ${stageKind(stage)}`} className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4">
|
|
85
|
+
<form className="w-full max-w-sm space-y-3 rounded-lg bg-background p-5 shadow-xl" onSubmit={(e) => { e.preventDefault(); if (valid) onConfirm({ close_date: date, amount: amount === "" ? undefined : Number(amount) }); }}>
|
|
86
|
+
<h2 className="text-base font-semibold">Move "{deal.name}" to {stage.name}</h2>
|
|
87
|
+
<div className="space-y-1"><Label htmlFor="cd-date">Close date</Label><Input id="cd-date" type="date" value={date} onChange={(e) => setDate(e.target.value)} /></div>
|
|
88
|
+
{won ? <div className="space-y-1"><Label htmlFor="cd-amount">Amount</Label><Input id="cd-amount" type="number" min={0} value={amount} onChange={(e) => setAmount(e.target.value)} /></div> : null}
|
|
89
|
+
<div className="flex justify-end gap-2"><Button type="button" variant="outline" onClick={onCancel}>Cancel</Button><Button type="submit" disabled={!valid}>Move deal</Button></div>
|
|
90
|
+
</form>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function PipelineBoard({ pipelineId, search, ownerId, onSelectDeal, className }: PipelineBoardProps) {
|
|
96
|
+
const pipelines = usePipelines();
|
|
97
|
+
const pipeline = pipelines.pipelines.find((p) => p.id === pipelineId) ?? pipelines.defaultPipeline;
|
|
98
|
+
const { deals, loading, error, reload, moveDeal, movingIds } = usePipelineDeals(pipeline?.id, { search, ownerId });
|
|
99
|
+
const [closing, setClosing] = useState<{ deal: Deal; stage: Stage } | null>(null);
|
|
100
|
+
const [moveError, setMoveError] = useState<unknown>(null);
|
|
101
|
+
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } }), useSensor(KeyboardSensor));
|
|
102
|
+
const stages = pipeline?.stages ?? [];
|
|
103
|
+
const byStage = useMemo(() => {
|
|
104
|
+
const map = new Map<string, Deal[]>(stages.map((s) => [s.id, []]));
|
|
105
|
+
for (const d of deals) map.get(d.stage_id ?? "")?.push(d);
|
|
106
|
+
return map;
|
|
107
|
+
}, [deals, stages]);
|
|
108
|
+
|
|
109
|
+
const request = async (deal: Deal, stage: Stage, extra?: { close_date?: string; amount?: number }) => {
|
|
110
|
+
if (deal.stage_id === stage.id || movingIds.has(deal.id)) return;
|
|
111
|
+
setMoveError(null);
|
|
112
|
+
if (stageKind(stage) !== "open" && !extra) { setClosing({ deal, stage }); return; }
|
|
113
|
+
try { await moveDeal(deal, stage.id, extra); } catch (err) {
|
|
114
|
+
const marker = closedStageMarker(err);
|
|
115
|
+
setMoveError(marker ? new Error(`The API needs more before this deal can close (${marker.replace(/_/g, " ")}).`) : err);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const onDragEnd = (e: DragEndEvent) => {
|
|
119
|
+
const deal = deals.find((d) => d.id === e.active.id);
|
|
120
|
+
const stage = stages.find((s) => s.id === e.over?.id);
|
|
121
|
+
if (deal && stage) void request(deal, stage);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
if (pipelines.error) return <ApiError error={pipelines.error} title="Could not load pipelines" onRetry={() => void pipelines.refetch()} />;
|
|
125
|
+
if (pipelines.loading || !pipeline) return pipelines.loading ? <Skeleton className="h-64 w-full" aria-label="Loading pipeline" /> : <EmptyState title="No pipeline yet" description="Create a pipeline with stages, then your deals appear here." />;
|
|
126
|
+
return (
|
|
127
|
+
<div className={`space-y-3 ${className ?? ""}`}>
|
|
128
|
+
<h2 className="text-base font-semibold">{pipeline.name}</h2>
|
|
129
|
+
{error ? <ApiError error={error} title="Could not load deals" onRetry={() => void reload()} /> : null}
|
|
130
|
+
{moveError ? <ApiError error={moveError} title="That move was not saved" /> : null}
|
|
131
|
+
{loading && deals.length === 0 ? <Skeleton className="h-48 w-full" aria-label="Loading deals" /> : (
|
|
132
|
+
<DndContext sensors={sensors} onDragEnd={onDragEnd}>
|
|
133
|
+
<div className="flex gap-3 overflow-x-auto pb-2">
|
|
134
|
+
{stages.map((s) => <Column key={s.id} stage={s} stages={stages} deals={byStage.get(s.id) ?? []} moving={movingIds} onMove={(d, st) => void request(d, st)} onSelect={onSelectDeal} />)}
|
|
135
|
+
</div>
|
|
136
|
+
</DndContext>
|
|
137
|
+
)}
|
|
138
|
+
{!loading && !error && deals.length === 0 ? <EmptyState title="No deals in this pipeline" description="Create a deal and it lands in the first stage." /> : null}
|
|
139
|
+
{closing ? <CloseDialog stage={closing.stage} deal={closing.deal} onCancel={() => setClosing(null)} onConfirm={(extra) => { const c = closing; setClosing(null); void request(c.deal, c.stage, extra); }} /> : null}
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* StageEditor: configure a pipeline's stages: names, win probability, won/lost type, colour and order.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations, in order:
|
|
7
|
+
* 1. GET /pipelines/{id} (cliPipelines.getPipeline) read
|
|
8
|
+
* 2. POST /pipelines/{id}/stages (cliPipelines.createStage) write
|
|
9
|
+
* PUT /pipelines/{id}/stages/{stage_id} (cliPipelines.updateStage) write
|
|
10
|
+
* PUT /pipelines/{id}/stages/reorder (cliPipelines.reorderStages) write - send ALL stage ids in the new order
|
|
11
|
+
* 3. DELETE /pipelines/{id}/stages/{stage_id} (cliPipelines.deleteStage) DESTRUCTIVE -> ConfirmSpendDialog first
|
|
12
|
+
* Set probabilities: the forecast's weighted pipeline treats an unset (0) probability as "not configured".
|
|
13
|
+
* A stage with `stage_type` "won" or "lost" closes the deal and enforces a close date (and amount for won).
|
|
14
|
+
*/
|
|
15
|
+
import { useEffect, useState } from "react";
|
|
16
|
+
import { Button, Input, Skeleton } from "@graph8/sdk/ui";
|
|
17
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
18
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
19
|
+
import { g8Delete, g8Get, g8Post, g8Put } from "@/g8/lib/g8-client";
|
|
20
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
21
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
22
|
+
import { dataOf, sortedStages, type Pipeline, type Stage } from "../lib/deals-types";
|
|
23
|
+
|
|
24
|
+
export interface StageEditorProps {
|
|
25
|
+
pipelineId: string;
|
|
26
|
+
onChanged?: (stages: Stage[]) => void;
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const TYPES = [["open", "Open"], ["won", "Closed won"], ["lost", "Closed lost"]] as const;
|
|
31
|
+
|
|
32
|
+
export function StageEditor({ pipelineId, onChanged, className }: StageEditorProps) {
|
|
33
|
+
const base = `/pipelines/${encodeURIComponent(pipelineId)}`;
|
|
34
|
+
const pipeline = useG8Query(async (signal) => dataOf<Pipeline>(await g8Get(base, undefined, { signal })), [pipelineId]);
|
|
35
|
+
const [stages, setStages] = useState<Stage[]>([]);
|
|
36
|
+
const [newName, setNewName] = useState("");
|
|
37
|
+
const [removing, setRemoving] = useState<Stage | null>(null);
|
|
38
|
+
const [savingId, setSavingId] = useState<string | null>(null);
|
|
39
|
+
useEffect(() => { if (pipeline.data) setStages(sortedStages(pipeline.data)); }, [pipeline.data]);
|
|
40
|
+
|
|
41
|
+
const done = (next: Stage[]) => { setStages(next); onChanged?.(next); };
|
|
42
|
+
const save = useG8Mutation(async (s: Stage) => {
|
|
43
|
+
setSavingId(s.id);
|
|
44
|
+
try {
|
|
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
|
+
done(stages.map((x) => (x.id === saved.id ? { ...x, ...saved } : x)));
|
|
47
|
+
} finally { setSavingId(null); }
|
|
48
|
+
});
|
|
49
|
+
const add = useG8Mutation(async (name: string) => {
|
|
50
|
+
const created = dataOf<Stage>(await g8Post(`${base}/stages`, { name, probability: 0, stage_type: "open" }));
|
|
51
|
+
done([...stages, created]);
|
|
52
|
+
setNewName("");
|
|
53
|
+
});
|
|
54
|
+
const reorder = useG8Mutation(async (next: Stage[]) => {
|
|
55
|
+
await g8Put(`${base}/stages/reorder`, { stage_ids: next.map((s) => s.id) });
|
|
56
|
+
done(next);
|
|
57
|
+
});
|
|
58
|
+
const remove = useG8Mutation(async (s: Stage) => {
|
|
59
|
+
await g8Delete(`${base}/stages/${encodeURIComponent(s.id)}`);
|
|
60
|
+
done(stages.filter((x) => x.id !== s.id));
|
|
61
|
+
setRemoving(null);
|
|
62
|
+
}, { onError: () => setRemoving(null) });
|
|
63
|
+
|
|
64
|
+
const patch = (id: string, change: Partial<Stage>) => setStages((all) => all.map((s) => (s.id === id ? { ...s, ...change } : s)));
|
|
65
|
+
const swap = (i: number, j: number) => { const next = [...stages]; [next[i], next[j]] = [next[j], next[i]]; void reorder.mutate(next); };
|
|
66
|
+
const failure = save.error ?? add.error ?? reorder.error ?? remove.error;
|
|
67
|
+
|
|
68
|
+
if (pipeline.error) return <ApiError error={pipeline.error} title="Could not load the pipeline" onRetry={() => void pipeline.refetch()} />;
|
|
69
|
+
if (pipeline.loading && !pipeline.data) return <Skeleton className="h-48 w-full" aria-label="Loading stages" />;
|
|
70
|
+
return (
|
|
71
|
+
<div className={`space-y-3 ${className ?? ""}`}>
|
|
72
|
+
<h2 className="text-base font-semibold">{pipeline.data?.name} stages</h2>
|
|
73
|
+
{failure ? <ApiError error={failure} title="That stage change failed" /> : null}
|
|
74
|
+
<ol className="space-y-2">
|
|
75
|
+
{stages.map((s, i) => (
|
|
76
|
+
<li key={s.id} className="flex flex-wrap items-center gap-2 rounded-md border p-2">
|
|
77
|
+
<Input aria-label={`Name of ${s.name}`} className="w-48" value={s.name} onChange={(e) => patch(s.id, { name: e.target.value })} />
|
|
78
|
+
<Input aria-label={`Probability of ${s.name}`} type="number" min={0} max={100} className="w-24" value={s.probability ?? 0} onChange={(e) => patch(s.id, { probability: Math.min(100, Math.max(0, Number(e.target.value))) })} />
|
|
79
|
+
<select aria-label={`Type of ${s.name}`} className="h-9 rounded-md border bg-background px-2 text-sm" value={(s.stage_type ?? "open").toLowerCase()} onChange={(e) => patch(s.id, { stage_type: e.target.value })}>
|
|
80
|
+
{TYPES.map(([v, label]) => <option key={v} value={v}>{label}</option>)}
|
|
81
|
+
</select>
|
|
82
|
+
<Button size="sm" variant="outline" loading={savingId === s.id} disabled={savingId === s.id} onClick={() => void save.mutate(s)}>Save</Button>
|
|
83
|
+
<Button size="sm" variant="ghost" aria-label={`Move ${s.name} up`} disabled={i === 0 || reorder.loading} onClick={() => swap(i, i - 1)}>Up</Button>
|
|
84
|
+
<Button size="sm" variant="ghost" aria-label={`Move ${s.name} down`} disabled={i === stages.length - 1 || reorder.loading} onClick={() => swap(i, i + 1)}>Down</Button>
|
|
85
|
+
<Button size="sm" variant="ghost" aria-label={`Delete ${s.name}`} onClick={() => setRemoving(s)}>Delete</Button>
|
|
86
|
+
</li>
|
|
87
|
+
))}
|
|
88
|
+
</ol>
|
|
89
|
+
<div className="flex gap-2">
|
|
90
|
+
<Input aria-label="New stage name" placeholder="New stage name" value={newName} onChange={(e) => setNewName(e.target.value)} />
|
|
91
|
+
<Button disabled={!newName.trim() || add.loading} onClick={() => void add.mutate(newName.trim())}>Add stage</Button>
|
|
92
|
+
</div>
|
|
93
|
+
<ConfirmSpendDialog open={removing != null} tier="destructive" title={`Delete the "${removing?.name}" stage?`} description="Deals sitting in this stage must be moved first; the API refuses to delete a stage that still has deals." confirmLabel="Delete stage" affected={1} confirming={remove.loading} onConfirm={() => removing && void remove.mutate(removing)} onCancel={() => setRemoving(null)} />
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* usePipelineDeals: the deals in one pipeline, all pages up to `maxDeals`, with an optimistic stage move.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations:
|
|
7
|
+
* GET /deals?pipeline_id=&search=&owner_id=&page=&limit= (deals.listDeals) read
|
|
8
|
+
* PATCH /deals/{deal_id} (deals.updateDeal) write - the stage move
|
|
9
|
+
*
|
|
10
|
+
* `moveDeal` updates the board immediately and, if the API refuses, refetches the list (the server is the truth)
|
|
11
|
+
* and rethrows; `movingIds` holds the deals with a move in flight so a card can disable only its own control (a stale revision, or a closed-stage
|
|
12
|
+
* rule such as "a won deal needs a close date and an amount"). Pass those with `extra`.
|
|
13
|
+
* A deal in a closed stage is still returned by the list; filter on the stage kind to hide won/lost.
|
|
14
|
+
*/
|
|
15
|
+
import { useCallback, useEffect, useState } from "react";
|
|
16
|
+
import { g8Get, g8Patch } from "@/g8/lib/g8-client";
|
|
17
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
18
|
+
import type { Deal } from "../lib/deals-types";
|
|
19
|
+
import { dataOf } from "../lib/deals-types";
|
|
20
|
+
|
|
21
|
+
export interface PipelineDealFilters {
|
|
22
|
+
search?: string;
|
|
23
|
+
ownerId?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface MoveExtra {
|
|
27
|
+
close_date?: string;
|
|
28
|
+
amount?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function usePipelineDeals(pipelineId: string | undefined, filters: PipelineDealFilters = {}, maxDeals = 500) {
|
|
32
|
+
const [deals, setDeals] = useState<Deal[]>([]);
|
|
33
|
+
const [movingIds, setMovingIds] = useState<ReadonlySet<string>>(new Set());
|
|
34
|
+
const query = useG8Query(
|
|
35
|
+
async (signal) => {
|
|
36
|
+
const all: Deal[] = [];
|
|
37
|
+
for (let page = 1; all.length < maxDeals; page++) {
|
|
38
|
+
const env = await g8Get<Deal[]>("/deals", { pipeline_id: pipelineId, search: filters.search || undefined, owner_id: filters.ownerId || undefined, page, limit: 100 }, { signal });
|
|
39
|
+
all.push(...(env.data ?? []));
|
|
40
|
+
if (!env.pagination?.has_next) break;
|
|
41
|
+
}
|
|
42
|
+
return all.slice(0, maxDeals);
|
|
43
|
+
},
|
|
44
|
+
[pipelineId, filters.search, filters.ownerId, maxDeals],
|
|
45
|
+
{ enabled: Boolean(pipelineId) },
|
|
46
|
+
);
|
|
47
|
+
useEffect(() => { if (query.data) setDeals(query.data); }, [query.data]);
|
|
48
|
+
|
|
49
|
+
const moveDeal = useCallback(async (deal: Deal, stageId: string, extra: MoveExtra = {}): Promise<Deal> => {
|
|
50
|
+
const mark = (on: boolean) => setMovingIds((ids) => { const next = new Set(ids); if (on) next.add(deal.id); else next.delete(deal.id); return next; });
|
|
51
|
+
mark(true);
|
|
52
|
+
setDeals((all) => all.map((d) => (d.id === deal.id ? { ...d, stage_id: stageId } : d)));
|
|
53
|
+
try {
|
|
54
|
+
const saved = dataOf<Deal>(await g8Patch(`/deals/${encodeURIComponent(deal.id)}`, { stage_id: stageId, expected_revision: deal.revision ?? undefined, ...extra }));
|
|
55
|
+
setDeals((all) => all.map((d) => (d.id === deal.id ? { ...d, ...saved } : d)));
|
|
56
|
+
return saved;
|
|
57
|
+
} catch (err) {
|
|
58
|
+
// Roll back by re-reading the server's truth: a snapshot taken before the move could itself be stale.
|
|
59
|
+
await query.refetch();
|
|
60
|
+
throw err;
|
|
61
|
+
} finally {
|
|
62
|
+
mark(false);
|
|
63
|
+
}
|
|
64
|
+
}, [query]);
|
|
65
|
+
|
|
66
|
+
return { deals, loading: query.loading, error: query.error, reload: query.refetch, moveDeal, movingIds };
|
|
67
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* usePipelines: every deal pipeline with its stages in order. The source of stage ids for everything else.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations: GET /deals/pipelines (deals.listPipelines), read tier.
|
|
7
|
+
*/
|
|
8
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
9
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
10
|
+
import { sortedStages, type Pipeline } from "../lib/deals-types";
|
|
11
|
+
|
|
12
|
+
export function usePipelines() {
|
|
13
|
+
const q = useG8Query(async (signal) => {
|
|
14
|
+
const env = await g8Get<Pipeline[]>("/deals/pipelines", undefined, { signal });
|
|
15
|
+
return (env.data ?? []).map((p) => ({ ...p, stages: sortedStages(p) }));
|
|
16
|
+
}, []);
|
|
17
|
+
const pipelines = q.data ?? [];
|
|
18
|
+
return { ...q, pipelines, defaultPipeline: pipelines.find((p) => p.is_default) ?? pipelines[0] };
|
|
19
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Deal, pipeline and stage shapes plus the money/stage helpers the deals components share.
|
|
5
|
+
*
|
|
6
|
+
* Stage ids ALWAYS come from the API (`GET /deals/pipelines`); never type one in. A stage's `stage_type` is free
|
|
7
|
+
* text on the server ("won", "lost", or anything else for an open stage), so compare it through `stageKind()`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface DealContact {
|
|
11
|
+
id: number;
|
|
12
|
+
name?: string | null;
|
|
13
|
+
email?: string | null;
|
|
14
|
+
role?: string | null;
|
|
15
|
+
title?: string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface Deal {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string | null;
|
|
21
|
+
amount?: number | null;
|
|
22
|
+
currency?: string | null;
|
|
23
|
+
close_date?: string | null;
|
|
24
|
+
description?: string | null;
|
|
25
|
+
owner_id?: string | null;
|
|
26
|
+
owner_name?: string | null;
|
|
27
|
+
owner_email?: string | null;
|
|
28
|
+
pipeline_id?: string | null;
|
|
29
|
+
pipeline_name?: string | null;
|
|
30
|
+
stage_id?: string | null;
|
|
31
|
+
stage_name?: string | null;
|
|
32
|
+
contact_count?: number;
|
|
33
|
+
contacts?: DealContact[] | null;
|
|
34
|
+
primary_contact?: DealContact | null;
|
|
35
|
+
closed_lost_reason?: string | null;
|
|
36
|
+
last_activity_at?: string | null;
|
|
37
|
+
/** Send it back as `expected_revision` so a concurrent edit is rejected instead of overwritten. */
|
|
38
|
+
revision?: number | null;
|
|
39
|
+
updated_at?: string | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface Stage {
|
|
43
|
+
id: string;
|
|
44
|
+
name: string;
|
|
45
|
+
color?: string | null;
|
|
46
|
+
position?: number | null;
|
|
47
|
+
/** 0 to 100. */
|
|
48
|
+
probability?: number | null;
|
|
49
|
+
stage_type?: string | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface Pipeline {
|
|
53
|
+
id: string;
|
|
54
|
+
name: string;
|
|
55
|
+
is_default?: boolean;
|
|
56
|
+
stages: Stage[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type StageKind = "won" | "lost" | "open";
|
|
60
|
+
|
|
61
|
+
export function stageKind(stage: Pick<Stage, "stage_type"> | null | undefined): StageKind {
|
|
62
|
+
const t = (stage?.stage_type ?? "").trim().toLowerCase();
|
|
63
|
+
return t === "won" || t === "lost" ? t : "open";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function sortedStages(pipeline: Pipeline | undefined): Stage[] {
|
|
67
|
+
return [...(pipeline?.stages ?? [])].sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function formatMoney(amount: number | null | undefined, currency?: string | null): string {
|
|
71
|
+
if (amount == null) return "No amount";
|
|
72
|
+
try {
|
|
73
|
+
return new Intl.NumberFormat(undefined, { style: "currency", currency: currency || "USD", maximumFractionDigits: 0 }).format(amount);
|
|
74
|
+
} catch {
|
|
75
|
+
return String(amount);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function sumAmounts(deals: ReadonlyArray<Pick<Deal, "amount">>): number {
|
|
80
|
+
return deals.reduce((total, d) => total + (d.amount ?? 0), 0);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Probability-weighted value of open deals. This is an ESTIMATE computed here: the analytics endpoint refuses to
|
|
85
|
+
* return a weighted total because a stage's probability defaults to 0 when nobody configured it, which would
|
|
86
|
+
* silently drop those deals. `unweighted` counts the deals whose stage has no probability set.
|
|
87
|
+
*/
|
|
88
|
+
export function weightedPipeline(deals: ReadonlyArray<Deal>, stages: ReadonlyArray<Stage>): { value: number; unweighted: number } {
|
|
89
|
+
const byId = new Map(stages.map((s) => [s.id, s]));
|
|
90
|
+
let value = 0;
|
|
91
|
+
let unweighted = 0;
|
|
92
|
+
for (const deal of deals) {
|
|
93
|
+
const stage = deal.stage_id ? byId.get(deal.stage_id) : undefined;
|
|
94
|
+
if (!stage || stageKind(stage) !== "open") continue;
|
|
95
|
+
if (!stage.probability) unweighted++;
|
|
96
|
+
value += (deal.amount ?? 0) * ((stage.probability ?? 0) / 100);
|
|
97
|
+
}
|
|
98
|
+
return { value, unweighted };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Markers the API puts at the start of a 422 `detail` when a deal cannot move into a closed stage. */
|
|
102
|
+
export const CLOSED_STAGE_MARKERS = [
|
|
103
|
+
"close_date_required_for_won_stage",
|
|
104
|
+
"close_date_required_for_lost_stage",
|
|
105
|
+
"amount_required_for_won_stage",
|
|
106
|
+
"owner_required_for_closed_stage",
|
|
107
|
+
] as const;
|
|
108
|
+
|
|
109
|
+
export function closedStageMarker(error: unknown): (typeof CLOSED_STAGE_MARKERS)[number] | null {
|
|
110
|
+
const detail = String((error as { detail?: unknown } | null)?.detail ?? (error as Error | null)?.message ?? "");
|
|
111
|
+
return CLOSED_STAGE_MARKERS.find((m) => detail.startsWith(m)) ?? null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function dataOf<T>(env: unknown): T {
|
|
115
|
+
const e = env as { data?: unknown } | null;
|
|
116
|
+
return (e && typeof e === "object" && "data" in e && e.data != null ? e.data : env) as T;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* A date-only value (a close date) for display. Always UTC: `2026-10-01` must read as the 1st in every time zone,
|
|
121
|
+
* whereas local formatting shows the 30th west of UTC. Send date-only values to the API as `YYYY-MM-DD`, never
|
|
122
|
+
* through `new Date(x).toISOString()`.
|
|
123
|
+
*/
|
|
124
|
+
export function formatDate(value: string | null | undefined): string {
|
|
125
|
+
if (!value) return "";
|
|
126
|
+
const d = new Date(value.length === 10 ? `${value}T00:00:00Z` : value);
|
|
127
|
+
return Number.isNaN(d.getTime()) ? "" : d.toLocaleDateString(undefined, { timeZone: "UTC" });
|
|
128
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Create a deal and move it through the pipeline
|
|
3
|
+
* @when Tracking revenue from an agent or a webhook. Stage ids come from `deals.pipelines()`; never guess them.
|
|
4
|
+
* `owner_id` is a graph8 user id. Moving into a won/lost stage needs a close date (and an amount for won), or the
|
|
5
|
+
* API answers 422 with a marker such as `close_date_required_for_won_stage`. Pass `expected_revision` so a
|
|
6
|
+
* concurrent edit is refused instead of overwritten.
|
|
7
|
+
*/
|
|
8
|
+
import { g8 } from "@graph8/sdk";
|
|
9
|
+
|
|
10
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
11
|
+
|
|
12
|
+
const { data: pipelines } = await g8.deals.pipelines();
|
|
13
|
+
const pipeline = pipelines.find((p) => p.is_default) ?? pipelines[0];
|
|
14
|
+
const stages = [...pipeline.stages].sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
|
|
15
|
+
const won = stages.find((s) => (s.stage_type ?? "").toLowerCase() === "won");
|
|
16
|
+
|
|
17
|
+
const deal = await g8.deals.create({
|
|
18
|
+
name: "Acme - annual plan",
|
|
19
|
+
owner_id: "user_123",
|
|
20
|
+
contact_ids: [101],
|
|
21
|
+
amount: 24000,
|
|
22
|
+
currency: "USD",
|
|
23
|
+
pipeline_id: pipeline.id,
|
|
24
|
+
stage_id: stages[0].id,
|
|
25
|
+
});
|
|
26
|
+
if (!deal.id) throw new Error("graph8 returned a deal without an id");
|
|
27
|
+
|
|
28
|
+
// An ordinary move between open stages.
|
|
29
|
+
const next = await g8.deals.update(deal.id, { stage_id: stages[1].id });
|
|
30
|
+
|
|
31
|
+
// Closing as won: the close date (past dates are allowed) and the amount must exist on the deal or in this call.
|
|
32
|
+
if (won) await g8.deals.update(deal.id, { stage_id: won.id, close_date: new Date().toISOString(), amount: 24000, expected_revision: next.revision ?? undefined });
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Pipeline report: totals, win rate, where deals stall, and an estimated weighted forecast
|
|
3
|
+
* @when A weekly summary for a manager or an agent answering "how is the pipeline?". Read-only. The analytics
|
|
4
|
+
* endpoint returns NO weighted number on purpose (an unset stage probability is 0 and would drop deals); compute
|
|
5
|
+
* the estimate yourself and say how many deals had no probability.
|
|
6
|
+
*/
|
|
7
|
+
import { g8 } from "@graph8/sdk";
|
|
8
|
+
|
|
9
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
10
|
+
|
|
11
|
+
const { data: pipelines } = await g8.deals.pipelines();
|
|
12
|
+
const pipeline = pipelines.find((p) => p.is_default) ?? pipelines[0];
|
|
13
|
+
|
|
14
|
+
const analytics = (await g8.api.deals.getDealAnalytics({ query: { pipeline_id: pipeline.id } })) as {
|
|
15
|
+
data?: { totals?: { open_count: number; open_value?: number | null; win_rate?: number | null }; currency?: { code?: string | null; is_mixed?: boolean } };
|
|
16
|
+
};
|
|
17
|
+
const totals = analytics.data?.totals;
|
|
18
|
+
console.log(`open: ${totals?.open_count} deals, ${totals?.open_value ?? "n/a"} ${analytics.data?.currency?.code ?? ""}`);
|
|
19
|
+
console.log(`win rate (won / (won + lost)): ${totals?.win_rate != null ? Math.round(totals.win_rate * 100) + "%" : "n/a"}`);
|
|
20
|
+
|
|
21
|
+
const velocity = await g8.api.deals.getDealStageVelocity({ query: { pipeline_id: pipeline.id, stuck_threshold_days: 14 } });
|
|
22
|
+
for (const s of velocity.data.by_stage ?? []) console.log(s.stage_name, s.avg_days_in_stage, "days avg,", s.still_in_stage_count, "waiting");
|
|
23
|
+
console.log("stuck deals:", velocity.data.stuck_deals.total_count);
|
|
24
|
+
|
|
25
|
+
// Weighted estimate from open deals x stage probability (0-100).
|
|
26
|
+
const probability = new Map(pipeline.stages.map((s) => [s.id, s.probability ?? 0]));
|
|
27
|
+
const open = await g8.api.deals.listDeals({ query: { pipeline_id: pipeline.id, outcome: "open", limit: 100 } });
|
|
28
|
+
let weighted = 0;
|
|
29
|
+
let noProbability = 0;
|
|
30
|
+
for (const d of open.data) {
|
|
31
|
+
const p = probability.get(d.stage_id ?? "") ?? 0;
|
|
32
|
+
if (p === 0) noProbability++;
|
|
33
|
+
weighted += (d.amount ?? 0) * (p / 100);
|
|
34
|
+
}
|
|
35
|
+
console.log(`weighted estimate: ${Math.round(weighted)} (${noProbability} deals in stages with no probability set)`);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Find open deals with no recent activity and flag them
|
|
3
|
+
* @when A nightly job that surfaces neglected deals. The list is read-only; the flag is one bulk write
|
|
4
|
+
* (`POST /deals/bulk`, up to 100 deals, tier `write`). PUT semantics apply to `change`: null leaves a field alone,
|
|
5
|
+
* an EMPTY STRING clears it, so never send "" unless you mean to erase the value.
|
|
6
|
+
*/
|
|
7
|
+
import { g8 } from "@graph8/sdk";
|
|
8
|
+
|
|
9
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
10
|
+
|
|
11
|
+
const cutoff = new Date(Date.now() - 21 * 86_400_000).toISOString();
|
|
12
|
+
const stale = await g8.api.deals.listDeals({ query: { outcome: "open", stale_before: cutoff, limit: 100 } });
|
|
13
|
+
console.log(`${stale.data.length} open deals untouched since ${cutoff.slice(0, 10)}`);
|
|
14
|
+
for (const d of stale.data) console.log(d.id, d.name, d.stage_name, d.owner_name ?? "no owner");
|
|
15
|
+
|
|
16
|
+
if (stale.data.length > 0 && process.env.FLAG_STALE === "1") {
|
|
17
|
+
const ids = stale.data.map((d) => d.id).filter((id): id is string => Boolean(id));
|
|
18
|
+
const result = await g8.api.deals.bulkUpdateDeals({ body: { deal_ids: ids, change: { next_step: "Stale: follow up or close out" } } });
|
|
19
|
+
console.log(JSON.stringify(result.data)); // read the per-deal result, not just the HTTP status
|
|
20
|
+
}
|