@graph8/sdk 0.370.0 → 0.373.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/blocks/_foundation/README.md +14 -0
- package/blocks/_foundation/block.json +44 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
- package/blocks/_foundation/components/empty-state.tsx +5 -2
- package/blocks/_foundation/components/list-select.tsx +112 -0
- package/blocks/_foundation/components/paging-notices.tsx +55 -0
- package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
- package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
- package/blocks/_foundation/components/screen/index.ts +1 -0
- package/blocks/_foundation/components/screen/split-view.tsx +1 -1
- package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
- package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
- package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
- package/blocks/_foundation/lib/paging.ts +57 -0
- package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
- package/blocks/agents/README.md +81 -4
- package/blocks/agents/block.json +311 -2
- package/blocks/agents/components/agent-activity.tsx +112 -0
- package/blocks/agents/components/agent-approvals.tsx +135 -0
- package/blocks/agents/components/agent-builder.tsx +17 -3
- package/blocks/agents/components/agent-conversations.tsx +143 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
- package/blocks/agents/components/agent-operators.tsx +164 -0
- package/blocks/agents/components/agent-run-detail.tsx +20 -3
- package/blocks/agents/components/agent-runs.tsx +3 -3
- package/blocks/agents/components/agent-skills-panel.tsx +7 -7
- package/blocks/agents/components/agent-test-history.tsx +121 -0
- package/blocks/agents/components/agents-screen.tsx +53 -11
- package/blocks/agents/components/assistant-sessions.tsx +150 -0
- package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
- package/blocks/agents/components/from-template-button.tsx +65 -0
- package/blocks/agents/components/knowledge-browser.tsx +107 -0
- package/blocks/agents/components/knowledge-detail.tsx +156 -0
- package/blocks/agents/components/knowledge-form.tsx +69 -0
- package/blocks/agents/components/name-dialog.tsx +32 -0
- package/blocks/agents/components/receptionist-button.tsx +46 -0
- package/blocks/agents/components/save-as-template-button.tsx +32 -0
- package/blocks/agents/components/skill-editor.tsx +147 -0
- package/blocks/agents/components/skill-library.tsx +111 -0
- package/blocks/agents/lib/agent-types.ts +7 -1
- package/blocks/agents/lib/facts.tsx +13 -0
- package/blocks/agents/lib/knowledge-utils.ts +49 -0
- package/blocks/agents/lib/load-more.tsx +141 -0
- package/blocks/agents/lib/skill-utils.ts +91 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
- package/blocks/analytics/components/analytics-screen.tsx +16 -3
- package/blocks/analytics/components/kpi-tiles.tsx +1 -1
- package/blocks/analytics/components/report-builder.tsx +15 -2
- package/blocks/appointments/README.md +6 -0
- package/blocks/appointments/block.json +27 -0
- package/blocks/appointments/components/booking-page.tsx +2 -2
- package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
- package/blocks/appointments/components/meeting-detail.tsx +2 -2
- package/blocks/appointments/components/meeting-list.tsx +29 -13
- package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
- package/blocks/campaigns/components/campaign-assets.tsx +1 -1
- package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
- package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
- package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
- package/blocks/campaigns/components/icp-detail.tsx +11 -2
- package/blocks/campaigns/components/icp-editor.tsx +8 -3
- package/blocks/campaigns/components/persona-detail.tsx +11 -2
- package/blocks/campaigns/components/persona-editor.tsx +6 -1
- package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
- package/blocks/coach/components/call-scorecard.tsx +7 -7
- package/blocks/company-intelligence/README.md +132 -0
- package/blocks/company-intelligence/block.json +318 -0
- package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
- package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
- package/blocks/company-intelligence/components/company-profile.tsx +68 -0
- package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
- package/blocks/company-intelligence/components/report-detail.tsx +189 -0
- package/blocks/company-intelligence/components/research-reports.tsx +136 -0
- package/blocks/company-intelligence/components/research-run.tsx +161 -0
- package/blocks/company-intelligence/components/source-detail.tsx +117 -0
- package/blocks/company-intelligence/components/source-view.tsx +274 -0
- package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
- package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
- package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
- package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
- package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
- package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
- package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
- package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
- package/blocks/copilot/components/copilot-memory.tsx +6 -2
- package/blocks/copilot/components/copilot-panel.tsx +1 -1
- package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
- package/blocks/crm/README.md +6 -0
- package/blocks/crm/block.json +53 -4
- package/blocks/crm/components/companies-screen.tsx +23 -9
- package/blocks/crm/components/company-detail.tsx +2 -2
- package/blocks/crm/components/contact-detail.tsx +4 -4
- package/blocks/crm/components/contact-lookup-link.tsx +44 -0
- package/blocks/crm/components/contacts-screen.tsx +31 -13
- package/blocks/crm/components/dedupe-review.tsx +6 -1
- package/blocks/crm/components/import-wizard.tsx +3 -7
- package/blocks/crm/components/list-select.tsx +20 -0
- package/blocks/crm/components/record-engagement-tab.tsx +103 -0
- package/blocks/crm/components/record-form.tsx +1 -1
- package/blocks/crm/components/tasks-screen.tsx +35 -15
- package/blocks/crm/hooks/use-all-lists.ts +18 -0
- package/blocks/crm/hooks/use-page-query.ts +3 -1
- package/blocks/crm/lib/crm-types.ts +14 -0
- package/blocks/crm/lib/read-lists.ts +16 -0
- package/blocks/deals/README.md +24 -6
- package/blocks/deals/block.json +151 -2
- package/blocks/deals/components/close-deal-dialog.tsx +169 -0
- package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
- package/blocks/deals/components/deal-detail.tsx +2 -2
- package/blocks/deals/components/deal-filters.tsx +65 -0
- package/blocks/deals/components/deal-line-items.tsx +120 -0
- package/blocks/deals/components/deal-notes-tab.tsx +97 -0
- package/blocks/deals/components/deal-related.tsx +30 -9
- package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
- package/blocks/deals/components/deals-screen.tsx +57 -22
- package/blocks/deals/components/forecast-view.tsx +4 -4
- package/blocks/deals/components/pipeline-board.tsx +8 -6
- package/blocks/deals/components/pipeline-editor.tsx +132 -0
- package/blocks/deals/components/record-deals-tab.tsx +62 -0
- package/blocks/deals/components/stage-editor.tsx +1 -1
- package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
- package/blocks/deals/lib/deal-extras.ts +156 -0
- package/blocks/dialer/README.md +14 -5
- package/blocks/dialer/block.json +17 -2
- package/blocks/dialer/components/call-contact-button.tsx +26 -9
- package/blocks/dialer/components/call-log-screen.tsx +4 -1
- package/blocks/dialer/components/call-log.tsx +16 -4
- package/blocks/dialer/components/call-queue.tsx +3 -3
- package/blocks/dialer/components/dialer-screen.tsx +69 -27
- package/blocks/dialer/components/disposition-panel.tsx +1 -1
- package/blocks/dialer/components/record-calls-tab.tsx +101 -0
- package/blocks/dialer/components/session-start-form.tsx +58 -11
- package/blocks/dialer/components/softphone.tsx +1 -1
- package/blocks/enrichment/README.md +16 -6
- package/blocks/enrichment/block.json +19 -7
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +76 -27
- package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
- package/blocks/enrichment/components/enrich-button.tsx +1 -1
- package/blocks/enrichment/components/enrichment-screen.tsx +70 -17
- package/blocks/enrichment/components/waterfall-config.tsx +27 -7
- package/blocks/enrichment/lib/enrichment-types.ts +39 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
- package/blocks/forms/README.md +8 -0
- package/blocks/forms/block.json +107 -0
- package/blocks/forms/components/form-activity.tsx +71 -0
- package/blocks/forms/components/form-backfill.tsx +108 -0
- package/blocks/forms/components/form-builder.tsx +1 -1
- package/blocks/forms/components/form-embed.tsx +1 -1
- package/blocks/forms/components/form-groups.tsx +151 -0
- package/blocks/forms/components/form-mapping-editor.tsx +6 -9
- package/blocks/forms/components/form-route-history.tsx +72 -0
- package/blocks/forms/components/form-saved-views.tsx +81 -0
- package/blocks/forms/components/forms-screen.tsx +51 -17
- package/blocks/forms/components/more-rows.tsx +30 -0
- package/blocks/forms/components/submissions-export.tsx +67 -0
- package/blocks/forms/components/submissions-table.tsx +15 -6
- package/blocks/forms/lib/forms-extra.ts +67 -0
- package/blocks/inbox/README.md +5 -0
- package/blocks/inbox/block.json +15 -0
- package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
- package/blocks/inbox/components/thread-badge.tsx +2 -2
- package/blocks/inbox/components/thread-view.tsx +1 -1
- package/blocks/integrations/README.md +54 -1
- package/blocks/integrations/block.json +189 -15
- package/blocks/integrations/components/ad-accounts.tsx +82 -0
- package/blocks/integrations/components/connection-health.tsx +59 -0
- package/blocks/integrations/components/crm-detail.tsx +20 -1
- package/blocks/integrations/components/crm-records.tsx +103 -0
- package/blocks/integrations/components/field-mappings.tsx +61 -0
- package/blocks/integrations/components/integration-list.tsx +4 -4
- package/blocks/integrations/components/integrations-screen.tsx +20 -3
- package/blocks/integrations/components/migration-panel.tsx +133 -0
- package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
- package/blocks/integrations/components/sync-status.tsx +6 -6
- package/blocks/integrations/components/sync-triggers.tsx +134 -0
- package/blocks/integrations/components/webhook-detail.tsx +26 -7
- package/blocks/integrations/components/webhook-manager.tsx +30 -17
- package/blocks/integrations/components/workspace-tools.tsx +127 -0
- package/blocks/integrations/lib/crm-utils.ts +39 -0
- package/blocks/integrations/lib/facts.tsx +13 -0
- package/blocks/integrations/lib/integrations-utils.ts +5 -0
- package/blocks/integrations/lib/load-more.tsx +141 -0
- package/blocks/intent/components/account-intent-score.tsx +5 -5
- package/blocks/intent/components/intent-feed.tsx +8 -8
- package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
- package/blocks/intent/lib/intent-utils.ts +4 -4
- package/blocks/lifecycle/components/account-related.tsx +8 -1
- package/blocks/lifecycle/components/churn-list.tsx +5 -2
- package/blocks/lifecycle/components/customer-table.tsx +1 -1
- package/blocks/lists/README.md +7 -1
- package/blocks/lists/block.json +31 -3
- package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
- package/blocks/lists/components/add-to-list-button.tsx +17 -7
- package/blocks/lists/components/list-picker.tsx +13 -3
- package/blocks/lists/components/list-table.tsx +1 -1
- package/blocks/lists/components/lists-screen.tsx +4 -2
- package/blocks/lists/components/record-lists-tab.tsx +99 -0
- package/blocks/lists/components/suppression-manager.tsx +27 -6
- package/blocks/lists/components/suppressions-screen.tsx +21 -12
- package/blocks/mailboxes/block.json +4 -0
- package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
- package/blocks/mailboxes/components/domain-detail.tsx +10 -4
- package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
- package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
- package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
- package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
- package/blocks/mailboxes/components/warmup-health.tsx +20 -20
- package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
- package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
- package/blocks/newsletter/README.md +12 -0
- package/blocks/newsletter/block.json +117 -0
- package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
- package/blocks/newsletter/components/issue-versions.tsx +93 -0
- package/blocks/newsletter/components/more-rows.tsx +30 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
- package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
- package/blocks/newsletter/components/newsletter-screen.tsx +36 -13
- package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
- package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
- package/blocks/newsletter/components/subscriber-table.tsx +5 -7
- package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
- package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
- package/blocks/nurture/components/nurture-builder.tsx +10 -2
- package/blocks/nurture/components/nurture-enroll.tsx +6 -7
- package/blocks/nurture/components/nurture-form.tsx +5 -8
- package/blocks/nurture/components/nurture-screen.tsx +16 -5
- package/blocks/nurture/components/nurture-stats.tsx +5 -4
- package/blocks/objects/README.md +1 -1
- package/blocks/objects/block.json +13 -5
- package/blocks/objects/components/attribute-field-input.tsx +14 -1
- package/blocks/objects/components/object-record-detail.tsx +8 -1
- package/blocks/objects/components/object-schema-editor.tsx +1 -1
- package/blocks/objects/components/record-roles-panel.tsx +15 -5
- package/blocks/pages/README.md +14 -0
- package/blocks/pages/block.json +113 -1
- package/blocks/pages/components/more-rows.tsx +30 -0
- package/blocks/pages/components/page-ai-tools.tsx +196 -0
- package/blocks/pages/components/page-analytics.tsx +176 -0
- package/blocks/pages/components/page-clone.tsx +99 -0
- package/blocks/pages/components/page-domains.tsx +167 -0
- package/blocks/pages/components/page-editor.tsx +1 -1
- package/blocks/pages/components/page-generation-status.tsx +48 -0
- package/blocks/pages/components/page-tracking.tsx +137 -0
- package/blocks/pages/components/pages-screen.tsx +57 -16
- package/blocks/pages/components/seo-report.tsx +6 -1
- package/blocks/pages/lib/studio.ts +7 -0
- package/blocks/phone-numbers/README.md +74 -0
- package/blocks/phone-numbers/block.json +262 -0
- package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
- package/blocks/phone-numbers/components/assign-number.tsx +104 -0
- package/blocks/phone-numbers/components/number-search.tsx +239 -0
- package/blocks/phone-numbers/components/number-settings.tsx +134 -0
- package/blocks/phone-numbers/components/number-usage.tsx +97 -0
- package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
- package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
- package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
- package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
- package/blocks/phone-numbers/components/release-number.tsx +126 -0
- package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
- package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
- package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
- package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
- package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
- package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
- package/blocks/pickers/README.md +23 -0
- package/blocks/pickers/block.json +103 -0
- package/blocks/pickers/components/company-picker.tsx +90 -0
- package/blocks/pickers/components/contact-picker.tsx +131 -0
- package/blocks/pickers/components/member-picker.tsx +99 -0
- package/blocks/pickers/components/search-picker.tsx +98 -0
- package/blocks/pickers/hooks/use-current-member.ts +104 -0
- package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
- package/blocks/pickers/lib/use-debounced.ts +13 -0
- package/blocks/quotes/README.md +13 -0
- package/blocks/quotes/block.json +48 -1
- package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
- package/blocks/quotes/components/quote-builder.tsx +1 -1
- package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
- package/blocks/quotes/components/quote-list.tsx +5 -3
- package/blocks/quotes/components/quote-related.tsx +3 -2
- package/blocks/quotes/components/quote-viewer.tsx +7 -2
- package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
- package/blocks/radar/components/competitor-battle-card.tsx +1 -1
- package/blocks/radar/components/radar-overview.tsx +4 -4
- package/blocks/radar/components/radar-screen.tsx +2 -2
- package/blocks/sequencer/README.md +6 -0
- package/blocks/sequencer/block.json +28 -1
- package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
- package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
- package/blocks/sequencer/components/sequence-builder.tsx +19 -20
- package/blocks/sequencer/components/sequence-detail.tsx +4 -2
- package/blocks/sequencer/components/sequence-enrollments.tsx +4 -2
- package/blocks/sequencer/components/sequence-list.tsx +1 -1
- package/blocks/sequencer/components/sequence-stats.tsx +8 -8
- package/blocks/sequencer/components/sequences-screen.tsx +3 -0
- package/blocks/sequencer/components/step-editor.tsx +11 -11
- package/blocks/sequencer/lib/sequencer-utils.ts +5 -5
- package/blocks/social-ads/README.md +8 -0
- package/blocks/social-ads/block.json +137 -4
- package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
- package/blocks/social-ads/components/audience-sync.tsx +6 -2
- package/blocks/social-ads/components/campaign-ads.tsx +166 -0
- package/blocks/social-ads/components/campaign-detail.tsx +6 -1
- package/blocks/social-ads/components/campaign-editor.tsx +134 -0
- package/blocks/social-ads/components/creative-concepts.tsx +74 -0
- package/blocks/social-ads/components/creative-detail.tsx +6 -1
- package/blocks/social-ads/components/creative-gallery.tsx +20 -2
- package/blocks/social-ads/components/creative-revisions.tsx +57 -0
- package/blocks/social-ads/components/creative-tools.tsx +176 -0
- package/blocks/social-ads/components/post-composer.tsx +4 -2
- package/blocks/social-ads/components/post-detail.tsx +3 -0
- package/blocks/social-ads/components/social-ideas.tsx +64 -0
- package/blocks/social-ads/components/social-performance.tsx +80 -0
- package/blocks/social-ads/components/social-screen.tsx +33 -6
- package/blocks/social-ads/lib/ads-extra.ts +67 -0
- package/blocks/surveys/README.md +91 -0
- package/blocks/surveys/block.json +299 -0
- package/blocks/surveys/components/account-nps.tsx +69 -0
- package/blocks/surveys/components/contact-survey-history.tsx +52 -0
- package/blocks/surveys/components/org-summary.tsx +82 -0
- package/blocks/surveys/components/sending-domains.tsx +113 -0
- package/blocks/surveys/components/survey-form.tsx +126 -0
- package/blocks/surveys/components/survey-responses.tsx +145 -0
- package/blocks/surveys/components/survey-results.tsx +72 -0
- package/blocks/surveys/components/survey-send-history.tsx +58 -0
- package/blocks/surveys/components/survey-send.tsx +162 -0
- package/blocks/surveys/components/survey-triggers.tsx +166 -0
- package/blocks/surveys/components/surveys-screen.tsx +234 -0
- package/blocks/surveys/lib/surveys-types.ts +228 -0
- package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
- package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
- package/blocks/surveys/recipes/survey-score-report.ts +27 -0
- package/blocks/visitors/components/snippet-install.tsx +18 -9
- package/blocks/visitors/components/traffic-chart.tsx +4 -4
- package/blocks/visitors/components/visitor-company-card.tsx +24 -14
- package/blocks/visitors/components/visitor-feed.tsx +5 -5
- package/blocks/webchat/components/chat-widget-config.tsx +1 -1
- package/blocks/webchat/components/conversation-list.tsx +1 -1
- package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
- package/blocks/workbench/components/run-progress.tsx +1 -1
- package/blocks/workbench/components/workbench-grid.tsx +1 -1
- package/blocks/workbench/components/workbench-screen.tsx +15 -5
- package/blocks/workbench/components/workbench-table-list.tsx +14 -2
- package/blocks/workflows/components/workflow-editor.tsx +1 -1
- package/dist/cli.js +536 -392
- package/dist/index.browser.js +1 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +1 -1
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +1 -1
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +1 -1
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +1 -1
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/brand.d.ts +11 -2
- package/dist/types/blocks/create.d.ts +7 -3
- package/dist/types/blocks/doctor.d.ts +1 -0
- package/dist/types/blocks/flows.d.ts +7 -1
- package/dist/types/blocks/orgBrand.d.ts +34 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/version.d.ts +2 -2
- package/docs/api/desk.d.ts +2 -2
- package/docs/api/desk.md +3 -3
- package/package.json +1 -1
- package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
- package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
- package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
- package/stacks/6sense.json +17 -4
- package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
- package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
- package/stacks/apollo/pages/contacts.tsx.tmpl +29 -95
- package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
- package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
- package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
- package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
- package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
- package/stacks/apollo.json +36 -8
- package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
- package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
- package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
- package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
- package/stacks/calendly.json +8 -4
- package/stacks/clay/pages/companies.tsx.tmpl +17 -1
- package/stacks/clay/pages/contacts.tsx.tmpl +12 -0
- package/stacks/clay/pages/lists.tsx.tmpl +12 -0
- package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
- package/stacks/clay.json +16 -3
- package/stacks/gong/pages/calls.tsx.tmpl +23 -0
- package/stacks/gong/pages/companies.tsx.tmpl +22 -0
- package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
- package/stacks/gong/pages/lists.tsx.tmpl +12 -0
- package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
- package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
- package/stacks/gong.json +23 -6
- package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
- package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
- package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
- package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
- package/stacks/hootsuite.json +9 -4
- package/stacks/hubspot/pages/companies.tsx.tmpl +14 -35
- package/stacks/hubspot/pages/contacts.tsx.tmpl +29 -95
- package/stacks/hubspot/pages/deals.tsx.tmpl +10 -1
- package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
- package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
- package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
- package/stacks/hubspot.json +21 -5
- package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
- package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
- package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
- package/stacks/intercom.json +8 -4
- package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
- package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
- package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
- package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
- package/stacks/mailchimp.json +20 -5
- package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
- package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
- package/stacks/outreach/pages/contacts.tsx.tmpl +27 -66
- package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
- package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
- package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
- package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
- package/stacks/outreach.json +25 -7
- package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +15 -35
- package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
- package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
- package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
- package/stacks/pipedrive.json +4 -2
- package/stacks/salesforce/pages/companies.tsx.tmpl +14 -35
- package/stacks/salesforce/pages/contacts.tsx.tmpl +20 -35
- package/stacks/salesforce/pages/deals.tsx.tmpl +10 -1
- package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
- package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
- package/stacks/salesforce.json +7 -3
- package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +12 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
- package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +16 -3
- package/styles/styles.css +1 -1
- package/templates/next-app/app/globals.css.tmpl +349 -64
- package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
package/blocks/deals/README.md
CHANGED
|
@@ -11,19 +11,33 @@ compose under `DealDetail`'s `children`). Drag and drop needs `npm i @dnd-kit/co
|
|
|
11
11
|
|
|
12
12
|
- Board and List views, search by name, a pipeline switcher when there is more than one pipeline.
|
|
13
13
|
- Headline numbers: open deals, open value, estimated weighted value, won value.
|
|
14
|
-
- Open a deal (board card or list row, deep link `?id=<deal id>`): Details (edit, stage history, confirmed delete)
|
|
15
|
-
|
|
14
|
+
- Open a deal (board card or list row, deep link `?id=<deal id>`): Details (edit, stage history, confirmed delete),
|
|
15
|
+
Contacts (add with a role, change a role, make primary, remove), Line items (add, edit, remove, totals), Notes (add,
|
|
16
|
+
edit, delete), Tasks (add, complete), Quotes and Activity tabs. Moving a card into a won or lost stage asks for the
|
|
17
|
+
close date and amount first.
|
|
18
|
+
- **Close deal** (detail header): won or lost, with a close date, an amount for won, and a reason picked from the org's
|
|
19
|
+
close reasons (add one on the spot). **API gap:** no deal operation accepts a close reason (`closed_lost_reason` is
|
|
20
|
+
read only), so the reason is saved as a deal note, "Closed lost. Reason: Price". **Move to pipeline** is next to it.
|
|
21
|
+
- Filters on Board and List: owner, stage, outcome (open, won, lost), stale (no activity for 7 to 90 days, or never),
|
|
22
|
+
activity, close-date range and "no contact". `filterQuery(state)` turns them into the `GET /deals` parameters.
|
|
23
|
+
- Pipeline settings: create, rename, make default and delete (confirmed) pipelines, add or archive close reasons,
|
|
24
|
+
then the stage summary and StageEditor.
|
|
25
|
+
- Quotes tab: the deal's quotes. To offer "Create quote from this deal", pass the quotes block's button:
|
|
26
|
+
`<DealsScreen renderQuoteActions={(deal) => <CreateQuoteFromDeal dealId={deal.id} />} />` (quotes depends on deals,
|
|
27
|
+
so the deals block cannot import it).
|
|
16
28
|
- **New deal**: name, amount, close date, stage, an **owner** and **at least one contact**. The API requires both
|
|
17
29
|
(`owner_id`, and `contact_ids` with one or more mashup contact ids that all work at one company; the older note that
|
|
18
30
|
the list may be empty was wrong). A company that already has a deal is refused with 409 unless "Allow another deal"
|
|
19
31
|
is ticked.
|
|
20
|
-
- Forecast tab (totals, win rate, stuck deals, estimated weighted pipeline) and Pipeline settings tab (
|
|
21
|
-
probabilities, won/lost types, order, delete).
|
|
32
|
+
- Forecast tab (totals, win rate, stuck deals, estimated weighted pipeline) and Pipeline settings tab (pipelines, close
|
|
33
|
+
reasons, stage names, probabilities, won/lost types, order, delete).
|
|
22
34
|
- Slots for cross-block glue, all optional: `renderRowActions(deal)`, `renderDetailActions(deal)`,
|
|
23
35
|
`extraDetailTabs`, `toolbarActions`. Example: add a Quotes tab with `<QuoteList dealId={deal.id} />`.
|
|
24
36
|
|
|
25
|
-
Scopes the screen needs on top of the lists below: `team:read` (owner default and picker)
|
|
26
|
-
(contact search)
|
|
37
|
+
Scopes the screen needs on top of the lists below: `team:read` (owner default and picker), `contacts:read`
|
|
38
|
+
(contact search), `notes:read` and `notes:write` (Notes tab), `tasks:read` and `tasks:write` (Tasks tab) and
|
|
39
|
+
`deals:delete` (remove a contact, delete a note, archive a close reason).
|
|
40
|
+
Proxy `allowedPrefixes` must include `deals`, `pipelines`, `team-members`, `contacts`, `tasks` (and `activity`
|
|
27
41
|
for the timeline).
|
|
28
42
|
|
|
29
43
|
|
|
@@ -107,3 +121,7 @@ create), `deal.id` + `deal.revision` (card -> detail -> update/delete).
|
|
|
107
121
|
- `01-create-deal-and-advance.ts`: create, move, close as won.
|
|
108
122
|
- `02-pipeline-report.ts`: totals, win rate, stall points, estimated weighted forecast.
|
|
109
123
|
- `03-stale-deals-sweep.ts`: find neglected open deals, optionally flag them with one bulk write.
|
|
124
|
+
|
|
125
|
+
## Cross-block glue
|
|
126
|
+
|
|
127
|
+
- `RecordDealsTab` (`components/record-deals-tab.tsx`): the deals of one contact or company (`GET /contacts/{id}/deals`, `GET /companies/{id}/deals`), each opening in the deals screen. Use it as an `extraDetailTabs` entry of `ContactsScreen` / `CompaniesScreen`. `DealsScreen` also takes optional `onOpenContact` / `onOpenCompany`: the Contacts tab gains "Open in contacts" and the contact sheet gains "Open company".
|
package/blocks/deals/block.json
CHANGED
|
@@ -8,30 +8,56 @@
|
|
|
8
8
|
"optimistic stage moves with rollback and closed-stage rules handled",
|
|
9
9
|
"deal create, edit, stage history and confirmed delete",
|
|
10
10
|
"stage editor: names, probabilities, won/lost types, order",
|
|
11
|
-
"forecast: totals, win rate, stuck deals and an estimated weighted pipeline"
|
|
11
|
+
"forecast: totals, win rate, stuck deals and an estimated weighted pipeline",
|
|
12
|
+
"deal notes, tasks, contacts with roles and primary, and line items with totals",
|
|
13
|
+
"close a deal as won or lost with a reason",
|
|
14
|
+
"create, rename, default and delete pipelines, and manage close reasons",
|
|
15
|
+
"list filters: owner, stage, outcome, stale, activity, close-date range, no contact"
|
|
12
16
|
],
|
|
13
17
|
"dependsOn": [
|
|
14
18
|
"crm"
|
|
15
19
|
],
|
|
16
20
|
"operations": [
|
|
21
|
+
"add_deal_contact_deals__deal_id__contacts_post",
|
|
22
|
+
"archive_deal_close_reason_deals_close_reasons__reason_id__delete",
|
|
23
|
+
"create_deal_close_reason_deals_close_reasons_post",
|
|
17
24
|
"create_deal_deals_post",
|
|
25
|
+
"create_deal_note_deals__deal_id__notes_post",
|
|
26
|
+
"create_global_task_tasks_post",
|
|
27
|
+
"create_pipeline_pipelines_post",
|
|
18
28
|
"create_stage_pipelines__pipeline_id__stages_post",
|
|
19
29
|
"delete_deal_deals__deal_id__delete",
|
|
30
|
+
"delete_deal_note_deals__deal_id__notes__note_id__delete",
|
|
31
|
+
"delete_pipeline_pipelines__pipeline_id__delete",
|
|
20
32
|
"delete_stage_pipelines__pipeline_id__stages__stage_id__delete",
|
|
33
|
+
"get_company_deals_companies__company_id__deals_get",
|
|
34
|
+
"get_contact_deals_contacts__contact_id__deals_get",
|
|
21
35
|
"get_deal_analytics_deals_analytics_get",
|
|
22
36
|
"get_deal_deals__deal_id__get",
|
|
23
37
|
"get_deal_stage_velocity_deals_stage_velocity_get",
|
|
24
38
|
"get_my_team_member_team_members_me_get",
|
|
25
39
|
"get_pipeline_pipelines__pipeline_id__get",
|
|
40
|
+
"list_all_tasks_tasks_get",
|
|
26
41
|
"list_contacts_contacts_get",
|
|
42
|
+
"list_deal_close_reasons_deals_close_reasons_get",
|
|
27
43
|
"list_deal_history_deals__deal_id__history_get",
|
|
44
|
+
"list_deal_line_items_deals__deal_id__line_items_get",
|
|
45
|
+
"list_deal_notes_deals__deal_id__notes_get",
|
|
28
46
|
"list_deals_deals_get",
|
|
29
47
|
"list_pipelines_deals_pipelines_get",
|
|
30
48
|
"list_quotes_quotes_get",
|
|
31
49
|
"list_team_members_team_members_get",
|
|
50
|
+
"move_deal_to_pipeline_deals__deal_id__pipeline_put",
|
|
51
|
+
"remove_deal_contact_deals__deal_id__contacts__person_id__delete",
|
|
32
52
|
"reorder_stages_pipelines__pipeline_id__stages_reorder_put",
|
|
53
|
+
"replace_deal_line_items_deals__deal_id__line_items_put",
|
|
54
|
+
"set_deal_contact_role_deals__deal_id__contacts__contact_id__put",
|
|
55
|
+
"set_primary_deal_contact_deals__deal_id__contacts__person_id__primary_put",
|
|
33
56
|
"update_deal_deals__deal_id__patch",
|
|
34
|
-
"
|
|
57
|
+
"update_deal_note_deals__deal_id__notes__note_id__put",
|
|
58
|
+
"update_pipeline_pipelines__pipeline_id__put",
|
|
59
|
+
"update_stage_pipelines__pipeline_id__stages__stage_id__put",
|
|
60
|
+
"update_task_tasks__task_id__patch"
|
|
35
61
|
],
|
|
36
62
|
"components": [
|
|
37
63
|
{
|
|
@@ -120,6 +146,121 @@
|
|
|
120
146
|
"tiers": [
|
|
121
147
|
"read"
|
|
122
148
|
]
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"name": "DealNotesTab",
|
|
152
|
+
"file": "components/deal-notes-tab.tsx",
|
|
153
|
+
"purpose": "Notes of one deal: list, add, edit (only when the text changed) and confirmed delete; synced notes are read only.",
|
|
154
|
+
"operations": [
|
|
155
|
+
"create_deal_note_deals__deal_id__notes_post",
|
|
156
|
+
"delete_deal_note_deals__deal_id__notes__note_id__delete",
|
|
157
|
+
"list_deal_notes_deals__deal_id__notes_get",
|
|
158
|
+
"update_deal_note_deals__deal_id__notes__note_id__put"
|
|
159
|
+
],
|
|
160
|
+
"tiers": [
|
|
161
|
+
"read",
|
|
162
|
+
"write",
|
|
163
|
+
"destructive"
|
|
164
|
+
]
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"name": "DealTasksTab",
|
|
168
|
+
"file": "components/deal-tasks-tab.tsx",
|
|
169
|
+
"purpose": "Follow-up tasks linked to one deal: list (open or all), add with due date and priority, complete and reopen.",
|
|
170
|
+
"operations": [
|
|
171
|
+
"create_global_task_tasks_post",
|
|
172
|
+
"list_all_tasks_tasks_get",
|
|
173
|
+
"update_task_tasks__task_id__patch"
|
|
174
|
+
],
|
|
175
|
+
"tiers": [
|
|
176
|
+
"read",
|
|
177
|
+
"write"
|
|
178
|
+
]
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"name": "DealContactsEditor",
|
|
182
|
+
"file": "components/deal-contacts-editor.tsx",
|
|
183
|
+
"purpose": "Buying committee of one deal: add a contact with a role, change a role, make one primary, remove one (confirmed).",
|
|
184
|
+
"operations": [
|
|
185
|
+
"add_deal_contact_deals__deal_id__contacts_post",
|
|
186
|
+
"get_deal_deals__deal_id__get",
|
|
187
|
+
"list_contacts_contacts_get",
|
|
188
|
+
"remove_deal_contact_deals__deal_id__contacts__person_id__delete",
|
|
189
|
+
"set_deal_contact_role_deals__deal_id__contacts__contact_id__put",
|
|
190
|
+
"set_primary_deal_contact_deals__deal_id__contacts__person_id__primary_put"
|
|
191
|
+
],
|
|
192
|
+
"tiers": [
|
|
193
|
+
"read",
|
|
194
|
+
"write",
|
|
195
|
+
"destructive"
|
|
196
|
+
]
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
"name": "DealLineItems",
|
|
200
|
+
"file": "components/deal-line-items.tsx",
|
|
201
|
+
"purpose": "Products on a deal: add, edit and remove lines (dollars in, cents sent), per-line totals, the deal total and the quote-mismatch warning.",
|
|
202
|
+
"operations": [
|
|
203
|
+
"list_deal_line_items_deals__deal_id__line_items_get",
|
|
204
|
+
"replace_deal_line_items_deals__deal_id__line_items_put"
|
|
205
|
+
],
|
|
206
|
+
"tiers": [
|
|
207
|
+
"read",
|
|
208
|
+
"write"
|
|
209
|
+
]
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
"name": "DealActions",
|
|
213
|
+
"file": "components/close-deal-dialog.tsx",
|
|
214
|
+
"purpose": "Deal header actions: Close deal as won or lost with a reason (saved as a note, the API has no reason field), add close reasons, and Move to pipeline. CloseDealDialog is the dialog alone.",
|
|
215
|
+
"operations": [
|
|
216
|
+
"create_deal_close_reason_deals_close_reasons_post",
|
|
217
|
+
"create_deal_note_deals__deal_id__notes_post",
|
|
218
|
+
"list_deal_close_reasons_deals_close_reasons_get",
|
|
219
|
+
"move_deal_to_pipeline_deals__deal_id__pipeline_put",
|
|
220
|
+
"update_deal_deals__deal_id__patch"
|
|
221
|
+
],
|
|
222
|
+
"tiers": [
|
|
223
|
+
"read",
|
|
224
|
+
"write"
|
|
225
|
+
]
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
"name": "PipelineEditor",
|
|
229
|
+
"file": "components/pipeline-editor.tsx",
|
|
230
|
+
"purpose": "Create, rename, make default and delete (confirmed) pipelines, and add or archive (confirmed) close reasons. Stages are edited by StageEditor.",
|
|
231
|
+
"operations": [
|
|
232
|
+
"archive_deal_close_reason_deals_close_reasons__reason_id__delete",
|
|
233
|
+
"create_deal_close_reason_deals_close_reasons_post",
|
|
234
|
+
"create_pipeline_pipelines_post",
|
|
235
|
+
"delete_pipeline_pipelines__pipeline_id__delete",
|
|
236
|
+
"list_deal_close_reasons_deals_close_reasons_get",
|
|
237
|
+
"list_pipelines_deals_pipelines_get",
|
|
238
|
+
"update_pipeline_pipelines__pipeline_id__put"
|
|
239
|
+
],
|
|
240
|
+
"tiers": [
|
|
241
|
+
"read",
|
|
242
|
+
"write",
|
|
243
|
+
"destructive"
|
|
244
|
+
]
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
"name": "DealFilterBar",
|
|
248
|
+
"file": "components/deal-filters.tsx",
|
|
249
|
+
"purpose": "List filters: owner, stage, outcome, stale (7 to 90 days), activity, close-date range and no contact. Pass filterQuery(state) to usePipelineDeals.",
|
|
250
|
+
"operations": [],
|
|
251
|
+
"tiers": []
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
"name": "RecordDealsTab",
|
|
255
|
+
"file": "components/record-deals-tab.tsx",
|
|
256
|
+
"purpose": "The deals linked to one contact or company, each opening in the deals screen; an extraDetailTabs entry for ContactsScreen and CompaniesScreen.",
|
|
257
|
+
"operations": [
|
|
258
|
+
"get_contact_deals_contacts__contact_id__deals_get",
|
|
259
|
+
"get_company_deals_companies__company_id__deals_get"
|
|
260
|
+
],
|
|
261
|
+
"tiers": [
|
|
262
|
+
"read"
|
|
263
|
+
]
|
|
123
264
|
}
|
|
124
265
|
],
|
|
125
266
|
"hooks": [
|
|
@@ -154,6 +295,10 @@
|
|
|
154
295
|
{
|
|
155
296
|
"file": "lib/deals-types.ts",
|
|
156
297
|
"purpose": "Deal, pipeline and stage types, stage kind, money formatting, weighted-pipeline estimate and closed-stage error markers."
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
"file": "lib/deal-extras.ts",
|
|
301
|
+
"purpose": "Note, task, line-item and close-reason shapes, contact roles, money in cents, and the deal filter state with its GET /deals query."
|
|
157
302
|
}
|
|
158
303
|
],
|
|
159
304
|
"recipes": [
|
|
@@ -201,7 +346,11 @@
|
|
|
201
346
|
"deals:delete",
|
|
202
347
|
"deals:read",
|
|
203
348
|
"deals:write",
|
|
349
|
+
"notes:read",
|
|
350
|
+
"notes:write",
|
|
204
351
|
"quotes:read",
|
|
352
|
+
"tasks:read",
|
|
353
|
+
"tasks:write",
|
|
205
354
|
"team:read"
|
|
206
355
|
],
|
|
207
356
|
"defaults": [
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DealActions: the header actions of one deal: "Close deal" (won or lost, with a reason) and "Move to pipeline".
|
|
5
|
+
* CloseDealDialog is the dialog behind the first one and can be mounted on its own.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* 1. GET /deals/close-reasons?kind=won|lost (deals.listDealCloseReasons) read - the reasons the org keeps
|
|
9
|
+
* POST /deals/close-reasons { kind, label } (deals.createDealCloseReason) write - add a reason while closing
|
|
10
|
+
* 2. PATCH /deals/{id} { stage_id, close_date, amount?, expected_revision } (deals.updateDeal) write - the close itself
|
|
11
|
+
* 3. POST /deals/{id}/notes { content } (notes.createDealNote) write - records the reason on the deal
|
|
12
|
+
* 4. PUT /deals/{id}/pipeline { target_pipeline_id } (deals.moveDealToPipeline) write
|
|
13
|
+
* API GAP: no deal operation accepts a close reason (`closed_lost_reason` is read-only on the deal), so the chosen
|
|
14
|
+
* reason is saved as a note, "Closed lost. Reason: Price", right after the stage moves. If the move succeeded but the
|
|
15
|
+
* note failed, the dialog says so and retries only the note (the deal is never closed twice).
|
|
16
|
+
* A won deal needs a close date and an amount; the API also needs an owner. Date-only values are sent as YYYY-MM-DD.
|
|
17
|
+
*/
|
|
18
|
+
import { useState } from "react";
|
|
19
|
+
import { Button, Dialog, DialogContent, DialogDescription, DialogTitle, Input, Label } from "@graph8/sdk/ui";
|
|
20
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
21
|
+
import { g8Get, g8Patch, g8Post, g8Put } from "@/g8/lib/g8-client";
|
|
22
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
23
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
24
|
+
import { closedStageMarker, stageKind, type Deal, type Pipeline, type Stage } from "../lib/deals-types";
|
|
25
|
+
import { reasonsOf } from "../lib/deal-extras";
|
|
26
|
+
import { localToday } from "./pipeline-board";
|
|
27
|
+
|
|
28
|
+
export interface CloseDealDialogProps {
|
|
29
|
+
deal: Deal;
|
|
30
|
+
/** The stages of the deal's pipeline. */
|
|
31
|
+
stages: Stage[];
|
|
32
|
+
open: boolean;
|
|
33
|
+
onClose: () => void;
|
|
34
|
+
/** Called once the deal is closed (and the reason recorded). */
|
|
35
|
+
onClosed?: () => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type Outcome = "won" | "lost";
|
|
39
|
+
|
|
40
|
+
export function CloseDealDialog({ deal, stages, open, onClose, onClosed }: CloseDealDialogProps) {
|
|
41
|
+
const [outcome, setOutcome] = useState<Outcome>("won");
|
|
42
|
+
const [stageId, setStageId] = useState("");
|
|
43
|
+
const [date, setDate] = useState(deal.close_date?.slice(0, 10) ?? localToday());
|
|
44
|
+
const [amount, setAmount] = useState(deal.amount != null ? String(deal.amount) : "");
|
|
45
|
+
const [reasonId, setReasonId] = useState("");
|
|
46
|
+
const [newReason, setNewReason] = useState("");
|
|
47
|
+
// Retry-safe: once the stage move succeeded, a retry only records the reason.
|
|
48
|
+
const [moved, setMoved] = useState(false);
|
|
49
|
+
const targets = stages.filter((s) => stageKind(s) === outcome);
|
|
50
|
+
const stage = targets.find((s) => s.id === stageId) ?? targets[0];
|
|
51
|
+
const reasons = useG8Query(async (signal) => reasonsOf(await g8Get("/deals/close-reasons", { kind: outcome }, { signal })), [outcome], { enabled: open, key: ["deals", "close-reasons", outcome] });
|
|
52
|
+
const options = (reasons.data ?? []).filter((r) => !r.kind || r.kind === outcome);
|
|
53
|
+
const reason = options.find((r) => r.id === reasonId);
|
|
54
|
+
|
|
55
|
+
const addReason = useG8Mutation(async () => {
|
|
56
|
+
await g8Post("/deals/close-reasons", { kind: outcome, label: newReason.trim() });
|
|
57
|
+
const label = newReason.trim();
|
|
58
|
+
setNewReason("");
|
|
59
|
+
return label;
|
|
60
|
+
}, { invalidates: [["deals", "close-reasons"]], onSuccess: (label) => { void reasons.refetch().then((r) => { const found = r?.find((x) => x.label === label); if (found) setReasonId(found.id); }); } });
|
|
61
|
+
|
|
62
|
+
const close = useG8Mutation(async () => {
|
|
63
|
+
if (!moved) {
|
|
64
|
+
const body: Record<string, unknown> = { stage_id: stage?.id, close_date: date, expected_revision: deal.revision ?? undefined };
|
|
65
|
+
// Won always carries the amount (the API requires it); lost sends it only when it was changed.
|
|
66
|
+
if (amount !== "" && (outcome === "won" || Number(amount) !== deal.amount)) body.amount = Number(amount);
|
|
67
|
+
await g8Patch(`/deals/${encodeURIComponent(deal.id)}`, body);
|
|
68
|
+
setMoved(true);
|
|
69
|
+
}
|
|
70
|
+
if (reason) await g8Post(`/deals/${encodeURIComponent(deal.id)}/notes`, { content: `Closed ${outcome}. Reason: ${reason.label}` });
|
|
71
|
+
}, { invalidates: [["deals"]], onSuccess: () => { onClosed?.(); onClose(); } });
|
|
72
|
+
|
|
73
|
+
const valid = Boolean(stage) && Boolean(date) && (outcome !== "won" || amount !== "");
|
|
74
|
+
const marker = closedStageMarker(close.error);
|
|
75
|
+
return (
|
|
76
|
+
<Dialog open={open} onOpenChange={(o) => { if (!o && !close.loading) onClose(); }}>
|
|
77
|
+
<DialogContent>
|
|
78
|
+
<DialogTitle>Close "{deal.name ?? "this deal"}"</DialogTitle>
|
|
79
|
+
<DialogDescription>Moves the deal to a closed stage. The close date is required, and the amount for a won deal.</DialogDescription>
|
|
80
|
+
<form className="space-y-3" onSubmit={(e) => { e.preventDefault(); if (valid) void close.mutate(undefined); }}>
|
|
81
|
+
<div className="space-y-1">
|
|
82
|
+
<Label htmlFor="close-outcome">Outcome</Label>
|
|
83
|
+
<select id="close-outcome" className="h-9 w-full rounded-md border bg-background px-2 text-sm" disabled={moved} value={outcome} onChange={(e) => { setOutcome(e.target.value as Outcome); setStageId(""); setReasonId(""); }}>
|
|
84
|
+
<option value="won">Won</option>
|
|
85
|
+
<option value="lost">Lost</option>
|
|
86
|
+
</select>
|
|
87
|
+
</div>
|
|
88
|
+
{targets.length === 0 ? <p className="text-sm text-muted-foreground">This pipeline has no {outcome} stage. Add one under Pipeline settings (stage type "Closed {outcome}").</p> : null}
|
|
89
|
+
{targets.length > 1 ? (
|
|
90
|
+
<div className="space-y-1">
|
|
91
|
+
<Label htmlFor="close-stage">Stage</Label>
|
|
92
|
+
<select id="close-stage" className="h-9 w-full rounded-md border bg-background px-2 text-sm" disabled={moved} value={stage?.id ?? ""} onChange={(e) => setStageId(e.target.value)}>
|
|
93
|
+
{targets.map((s) => <option key={s.id} value={s.id}>{s.name}</option>)}
|
|
94
|
+
</select>
|
|
95
|
+
</div>
|
|
96
|
+
) : null}
|
|
97
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
98
|
+
<div className="space-y-1"><Label htmlFor="close-date">Close date</Label><Input id="close-date" type="date" disabled={moved} value={date} onChange={(e) => setDate(e.target.value)} /></div>
|
|
99
|
+
<div className="space-y-1"><Label htmlFor="close-amount">Amount{outcome === "won" ? "" : " (optional)"}</Label><Input id="close-amount" type="number" min={0} disabled={moved} value={amount} onChange={(e) => setAmount(e.target.value)} /></div>
|
|
100
|
+
</div>
|
|
101
|
+
<div className="space-y-1">
|
|
102
|
+
<Label htmlFor="close-reason">{outcome === "won" ? "Why did you win?" : "Why was it lost?"}</Label>
|
|
103
|
+
<select id="close-reason" className="h-9 w-full rounded-md border bg-background px-2 text-sm" value={reasonId} onChange={(e) => setReasonId(e.target.value)}>
|
|
104
|
+
<option value="">{reasons.loading ? "Loading reasons" : "No reason"}</option>
|
|
105
|
+
{options.map((r) => <option key={r.id} value={r.id}>{r.label}</option>)}
|
|
106
|
+
</select>
|
|
107
|
+
{reasons.error ? <ApiError error={reasons.error} compact title="Could not load reasons" onRetry={() => void reasons.refetch()} /> : null}
|
|
108
|
+
<div className="flex gap-2">
|
|
109
|
+
<Input aria-label="New reason" placeholder="Add a reason" value={newReason} onChange={(e) => setNewReason(e.target.value)} />
|
|
110
|
+
<Button type="button" variant="outline" loading={addReason.loading} disabled={!newReason.trim()} onClick={() => void addReason.mutate(undefined)}>Add reason</Button>
|
|
111
|
+
</div>
|
|
112
|
+
{addReason.error ? <ApiError error={addReason.error} title="Could not add the reason" /> : null}
|
|
113
|
+
</div>
|
|
114
|
+
{moved && close.error ? <p role="status" className="text-sm text-muted-foreground">The deal is closed, but the reason was not saved. Try again to save only the reason.</p> : null}
|
|
115
|
+
{close.error ? <ApiError error={marker ? new Error(`The API needs more before this deal can close (${marker.replace(/_/g, " ")}).`) : close.error} title={moved ? "Could not save the reason" : "Could not close the deal"} /> : null}
|
|
116
|
+
<div className="flex justify-end gap-2">
|
|
117
|
+
<Button type="button" variant="outline" disabled={close.loading} onClick={onClose}>Cancel</Button>
|
|
118
|
+
<Button type="submit" loading={close.loading} disabled={!valid}>{moved ? "Save reason" : `Close as ${outcome}`}</Button>
|
|
119
|
+
</div>
|
|
120
|
+
</form>
|
|
121
|
+
</DialogContent>
|
|
122
|
+
</Dialog>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface DealActionsProps {
|
|
127
|
+
deal: Deal;
|
|
128
|
+
pipelines: Pipeline[];
|
|
129
|
+
onChanged?: () => void;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function DealActions({ deal, pipelines, onChanged }: DealActionsProps) {
|
|
133
|
+
const [closing, setClosing] = useState(false);
|
|
134
|
+
const [moving, setMoving] = useState(false);
|
|
135
|
+
const [target, setTarget] = useState("");
|
|
136
|
+
const current = pipelines.find((p) => p.id === deal.pipeline_id);
|
|
137
|
+
const stages = current?.stages ?? [];
|
|
138
|
+
const isOpen = stageKind(stages.find((s) => s.id === deal.stage_id)) === "open";
|
|
139
|
+
const others = pipelines.filter((p) => p.id !== deal.pipeline_id);
|
|
140
|
+
const move = useG8Mutation(() => g8Put(`/deals/${encodeURIComponent(deal.id)}/pipeline`, { target_pipeline_id: target }), {
|
|
141
|
+
invalidates: [["deals"]],
|
|
142
|
+
onSuccess: () => { setMoving(false); setTarget(""); onChanged?.(); },
|
|
143
|
+
});
|
|
144
|
+
return (
|
|
145
|
+
<>
|
|
146
|
+
{isOpen && stages.length > 0 ? <Button size="sm" onClick={() => setClosing(true)}>Close deal</Button> : null}
|
|
147
|
+
{others.length > 0 ? <Button size="sm" variant="outline" onClick={() => setMoving(true)}>Move to pipeline</Button> : null}
|
|
148
|
+
{closing ? <CloseDealDialog key={deal.id} deal={deal} stages={stages} open onClose={() => setClosing(false)} onClosed={onChanged} /> : null}
|
|
149
|
+
<Dialog open={moving} onOpenChange={(o) => { if (!o && !move.loading) setMoving(false); }}>
|
|
150
|
+
<DialogContent>
|
|
151
|
+
<DialogTitle>Move "{deal.name ?? "this deal"}" to another pipeline</DialogTitle>
|
|
152
|
+
<DialogDescription>The API places it in a stage of the pipeline you pick; check the board afterwards.</DialogDescription>
|
|
153
|
+
<div className="space-y-1">
|
|
154
|
+
<Label htmlFor="move-pipeline">Pipeline</Label>
|
|
155
|
+
<select id="move-pipeline" className="h-9 w-full rounded-md border bg-background px-2 text-sm" value={target} onChange={(e) => setTarget(e.target.value)}>
|
|
156
|
+
<option value="">Choose a pipeline</option>
|
|
157
|
+
{others.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
|
|
158
|
+
</select>
|
|
159
|
+
</div>
|
|
160
|
+
{move.error ? <ApiError error={move.error} title="Could not move the deal" /> : null}
|
|
161
|
+
<div className="flex justify-end gap-2">
|
|
162
|
+
<Button type="button" variant="outline" disabled={move.loading} onClick={() => setMoving(false)}>Cancel</Button>
|
|
163
|
+
<Button type="button" loading={move.loading} disabled={!target} onClick={() => void move.mutate(undefined)}>Move deal</Button>
|
|
164
|
+
</div>
|
|
165
|
+
</DialogContent>
|
|
166
|
+
</Dialog>
|
|
167
|
+
</>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DealContactsEditor: the buying committee of one deal. Add a contact (with an optional role), change a role, make
|
|
5
|
+
* a contact the primary one, and remove a contact. The list itself is DealContactsTab; open a row for the full CRM record.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* 1. GET /deals/{id} (deals.getDeal) read - the contacts, roles and primary (via DealContactsTab)
|
|
9
|
+
* 2. GET /contacts?name= (crm contacts.listContacts) read - find a contact to add
|
|
10
|
+
* 3. POST /deals/{id}/contacts { person_id, role } (deals.addDealContact) write - the contact must work at the deal's company
|
|
11
|
+
* 4. PUT /deals/{id}/contacts/{contact_id} { role } (deals.setDealContactRole) write - null clears the role
|
|
12
|
+
* 5. PUT /deals/{id}/contacts/{person_id}/primary (deals.setPrimaryDealContact) write
|
|
13
|
+
* 6. DELETE /deals/{id}/contacts/{person_id} (deals.removeDealContact) DESTRUCTIVE -> ConfirmSpendDialog first
|
|
14
|
+
* Roles are champion, decision_maker, influencer, blocker, coach and end_user. A deal keeps at least one contact:
|
|
15
|
+
* the API refuses to remove the last one and the message is shown.
|
|
16
|
+
*/
|
|
17
|
+
import { useState } from "react";
|
|
18
|
+
import { Search } from "lucide-react";
|
|
19
|
+
import { Button, Input } from "@graph8/sdk/ui";
|
|
20
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
21
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
22
|
+
import { EntityAvatar } from "@/g8/components/screen/entity-avatar";
|
|
23
|
+
import { g8Delete, g8Get, g8Post, g8Put } from "@/g8/lib/g8-client";
|
|
24
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
25
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
26
|
+
import { useDebouncedValue } from "@/g8/crm/hooks/use-debounced";
|
|
27
|
+
import { contactName, type ContactRecord } from "@/g8/crm/lib/crm-types";
|
|
28
|
+
import { dataOf, type Deal, type DealContact } from "../lib/deals-types";
|
|
29
|
+
import { CONTACT_ROLES, roleLabel } from "../lib/deal-extras";
|
|
30
|
+
import { DealContactsTab } from "./deal-related";
|
|
31
|
+
|
|
32
|
+
export interface DealContactsEditorProps {
|
|
33
|
+
dealId: string;
|
|
34
|
+
/** Called after any change, so a screen can refresh its numbers. */
|
|
35
|
+
onChanged?: () => void;
|
|
36
|
+
/** Open a contact in your contacts screen (see DealContactsTab). */
|
|
37
|
+
onOpenContact?: (contactId: number) => void;
|
|
38
|
+
/** Open a contact's company in your companies screen. */
|
|
39
|
+
onOpenCompany?: (companyId: number) => void;
|
|
40
|
+
className?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function AddContact({ dealId, existing, onAdded }: { dealId: string; existing: ReadonlySet<number>; onAdded: () => void }) {
|
|
44
|
+
const [text, setText] = useState("");
|
|
45
|
+
const [role, setRole] = useState("");
|
|
46
|
+
const term = useDebouncedValue(text.trim());
|
|
47
|
+
const found = useG8Query(async (signal) => (await g8Get<ContactRecord[]>("/contacts", { name: term, limit: 8, page: 1 }, { signal })).data ?? [], [term], { enabled: term.length > 1 });
|
|
48
|
+
const add = useG8Mutation(
|
|
49
|
+
(c: ContactRecord) => g8Post(`/deals/${encodeURIComponent(dealId)}/contacts`, { person_id: String(c.id), ...(role ? { role } : {}) }),
|
|
50
|
+
{ invalidates: [["deals", "detail", dealId], ["deals", "list"]], onSuccess: () => { setText(""); setRole(""); onAdded(); } },
|
|
51
|
+
);
|
|
52
|
+
const choices = (found.data ?? []).filter((c) => !existing.has(c.id));
|
|
53
|
+
return (
|
|
54
|
+
<div className="space-y-2">
|
|
55
|
+
<div className="flex flex-wrap items-end gap-2">
|
|
56
|
+
<div className="relative min-w-48 flex-1">
|
|
57
|
+
<Search className="pointer-events-none absolute left-2 top-2.5 size-4 text-muted-foreground" aria-hidden />
|
|
58
|
+
<Input aria-label="Add a contact to this deal" className="pl-8" placeholder="Search contacts to add" value={text} onChange={(e) => setText(e.target.value)} />
|
|
59
|
+
</div>
|
|
60
|
+
<select aria-label="Role for the new contact" className="h-9 rounded-md border bg-background px-2 text-sm" value={role} onChange={(e) => setRole(e.target.value)}>
|
|
61
|
+
<option value="">No role</option>
|
|
62
|
+
{CONTACT_ROLES.map((r) => <option key={r} value={r}>{roleLabel(r)}</option>)}
|
|
63
|
+
</select>
|
|
64
|
+
</div>
|
|
65
|
+
{add.error ? <ApiError error={add.error} title="Could not add the contact" /> : null}
|
|
66
|
+
{found.error ? <ApiError error={found.error} title="Could not search contacts" /> : null}
|
|
67
|
+
{term.length > 1 && !found.loading && !found.error && choices.length === 0 ? <p className="text-xs text-muted-foreground">No other contacts match.</p> : null}
|
|
68
|
+
<ul className="divide-y rounded-md border empty:hidden" aria-label="Contacts to add">
|
|
69
|
+
{choices.map((c) => (
|
|
70
|
+
<li key={c.id}>
|
|
71
|
+
<button type="button" className="flex w-full items-center gap-2 p-2 text-left text-sm hover:bg-muted disabled:opacity-60" disabled={add.loading} aria-label={`Add ${contactName(c)}`} onClick={() => void add.mutate(c)}>
|
|
72
|
+
<EntityAvatar name={contactName(c)} size="sm" />
|
|
73
|
+
<span className="font-medium">{contactName(c)}</span>
|
|
74
|
+
<span className="text-xs text-muted-foreground">{[c.job_title, c.company_name].filter(Boolean).join(" at ")}</span>
|
|
75
|
+
</button>
|
|
76
|
+
</li>
|
|
77
|
+
))}
|
|
78
|
+
</ul>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function DealContactsEditor({ dealId, onChanged, onOpenContact, onOpenCompany, className }: DealContactsEditorProps) {
|
|
84
|
+
const base = `/deals/${encodeURIComponent(dealId)}/contacts`;
|
|
85
|
+
const [busyId, setBusyId] = useState<number | null>(null);
|
|
86
|
+
const [removing, setRemoving] = useState<DealContact | null>(null);
|
|
87
|
+
// Same query key as DealContactsTab: one request, and the ids already on the deal are hidden from the search results.
|
|
88
|
+
const deal = useG8Query(async (signal) => dataOf<Deal>(await g8Get(`/deals/${encodeURIComponent(dealId)}`, undefined, { signal })), [dealId], { key: ["deals", "detail", dealId] });
|
|
89
|
+
const existing = new Set((deal.data?.contacts ?? []).map((c) => c.id));
|
|
90
|
+
const done = () => onChanged?.();
|
|
91
|
+
const refresh = [["deals", "detail", dealId], ["deals", "list"]] as const;
|
|
92
|
+
|
|
93
|
+
const role = useG8Mutation(async (v: { contact: DealContact; role: string }) => {
|
|
94
|
+
setBusyId(v.contact.id);
|
|
95
|
+
try { await g8Put(`${base}/${v.contact.id}`, { role: v.role || null }); } finally { setBusyId(null); }
|
|
96
|
+
}, { invalidates: refresh, onSuccess: done });
|
|
97
|
+
const primary = useG8Mutation(async (c: DealContact) => {
|
|
98
|
+
setBusyId(c.id);
|
|
99
|
+
try { await g8Put(`${base}/${encodeURIComponent(String(c.id))}/primary`); } finally { setBusyId(null); }
|
|
100
|
+
}, { invalidates: refresh, onSuccess: done });
|
|
101
|
+
const remove = useG8Mutation(async (c: DealContact) => {
|
|
102
|
+
setBusyId(c.id);
|
|
103
|
+
try { await g8Delete(`${base}/${encodeURIComponent(String(c.id))}`); } finally { setBusyId(null); }
|
|
104
|
+
}, { invalidates: refresh, onSuccess: () => { setRemoving(null); done(); }, onError: () => setRemoving(null) });
|
|
105
|
+
const failure = role.error ?? primary.error ?? remove.error;
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div className={`space-y-4 ${className ?? ""}`}>
|
|
109
|
+
<AddContact dealId={dealId} existing={existing} onAdded={done} />
|
|
110
|
+
{failure ? <ApiError error={failure} title="That contact change failed" /> : null}
|
|
111
|
+
<DealContactsTab
|
|
112
|
+
dealId={dealId}
|
|
113
|
+
onOpenContact={onOpenContact}
|
|
114
|
+
onOpenCompany={onOpenCompany}
|
|
115
|
+
renderContactActions={(c, deal) => {
|
|
116
|
+
const isPrimary = deal.primary_contact?.id === c.id;
|
|
117
|
+
const name = c.name || c.email || `contact ${c.id}`;
|
|
118
|
+
const options = c.role && !CONTACT_ROLES.includes(c.role as (typeof CONTACT_ROLES)[number]) ? [c.role, ...CONTACT_ROLES] : CONTACT_ROLES;
|
|
119
|
+
return (
|
|
120
|
+
<span className="flex shrink-0 items-center gap-1">
|
|
121
|
+
<select aria-label={`Role of ${name}`} className="h-8 rounded-md border bg-background px-1 text-xs" disabled={busyId === c.id} value={c.role ?? ""} onChange={(e) => void role.mutate({ contact: c, role: e.target.value })}>
|
|
122
|
+
<option value="">No role</option>
|
|
123
|
+
{options.map((r) => <option key={r} value={r}>{roleLabel(r)}</option>)}
|
|
124
|
+
</select>
|
|
125
|
+
<Button size="sm" variant="ghost" disabled={isPrimary || busyId === c.id} aria-label={`Make ${name} primary`} onClick={() => void primary.mutate(c)}>{isPrimary ? "Primary" : "Make primary"}</Button>
|
|
126
|
+
<Button size="sm" variant="ghost" disabled={busyId === c.id} aria-label={`Remove ${name}`} onClick={() => setRemoving(c)}>Remove</Button>
|
|
127
|
+
</span>
|
|
128
|
+
);
|
|
129
|
+
}}
|
|
130
|
+
/>
|
|
131
|
+
<ConfirmSpendDialog open={removing != null} tier="destructive" title={`Remove ${removing?.name || removing?.email || "this contact"} from the deal?`} description="The contact stays in your CRM; only the link to this deal is removed." confirmLabel="Remove contact" affected={1} confirming={remove.loading} onConfirm={() => removing && void remove.mutate(removing)} onCancel={() => setRemoving(null)} />
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
@@ -128,14 +128,14 @@ export function DealDetail({ dealId, ownerId, contactIds = [], pipelineId, allow
|
|
|
128
128
|
|
|
129
129
|
{d ? (
|
|
130
130
|
<section aria-label="Contacts" className="space-y-1 text-sm">
|
|
131
|
-
<h3 className="font-
|
|
131
|
+
<h3 className="font-medium">Contacts ({d.contact_count ?? d.contacts?.length ?? 0})</h3>
|
|
132
132
|
{(d.contacts ?? []).map((c) => <p key={c.id}>{c.name ?? c.email}{c.role ? ` · ${c.role}` : ""}{d.primary_contact?.id === c.id ? " (primary)" : ""}</p>)}
|
|
133
133
|
<p className="text-muted-foreground">Owner: {d.owner_name ?? d.owner_email ?? "none"} · {formatMoney(d.amount, d.currency)}</p>
|
|
134
134
|
</section>
|
|
135
135
|
) : null}
|
|
136
136
|
{dealId ? (
|
|
137
137
|
<section aria-label="Stage history" className="space-y-1 text-sm">
|
|
138
|
-
<h3 className="font-
|
|
138
|
+
<h3 className="font-medium">Stage history</h3>
|
|
139
139
|
{history.error ? <ApiError error={history.error} title="Could not load history" onRetry={() => void history.refetch()} /> : null}
|
|
140
140
|
{(history.data ?? []).length === 0 && !history.loading ? <p className="text-muted-foreground">No stage changes yet.</p> : null}
|
|
141
141
|
{(history.data ?? []).map((h, i) => <p key={i}>{h.from_stage_name ?? "New"} to {h.to_stage_name ?? "unknown"} <span className="text-muted-foreground">{h.changed_at ?? h.created_at ?? ""}</span></p>)}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DealFilterBar: the deal list filters beside the search box: owner, stage, outcome, stale, activity, close-date
|
|
5
|
+
* range and "no contact". It only edits a DealFilterState; `filterQuery(state)` turns that into the `usePipelineDeals`
|
|
6
|
+
* filters (GET /deals owner_id, stage_id, outcome, stale_before, has_activity, close_after, close_before, has_contact).
|
|
7
|
+
*
|
|
8
|
+
* SDK operations: none directly (the owners come from the deals block's useDefaultOwner: GET /team-members).
|
|
9
|
+
* Dates are date-only (YYYY-MM-DD); the "to" date is inclusive, the API's close_before is exclusive, so
|
|
10
|
+
* `filterQuery` adds one day in UTC.
|
|
11
|
+
*/
|
|
12
|
+
import { Button } from "@graph8/sdk/ui";
|
|
13
|
+
import { NO_FILTERS, filtersActive, type DealFilterState } from "../lib/deal-extras";
|
|
14
|
+
import type { Stage } from "../lib/deals-types";
|
|
15
|
+
import type { OwnerOption } from "../hooks/use-default-owner";
|
|
16
|
+
|
|
17
|
+
export interface DealFilterBarProps {
|
|
18
|
+
value: DealFilterState;
|
|
19
|
+
onChange: (next: DealFilterState) => void;
|
|
20
|
+
stages: Stage[];
|
|
21
|
+
owners: OwnerOption[];
|
|
22
|
+
className?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const SELECT = "h-9 rounded-md border bg-background px-2 text-sm";
|
|
26
|
+
const DATE = "h-9 rounded-md border bg-background px-2 text-sm";
|
|
27
|
+
|
|
28
|
+
export function DealFilterBar({ value, onChange, stages, owners, className }: DealFilterBarProps) {
|
|
29
|
+
const set = <K extends keyof DealFilterState>(key: K, v: DealFilterState[K]) => onChange({ ...value, [key]: v });
|
|
30
|
+
return (
|
|
31
|
+
<div className={`flex flex-wrap items-center gap-2 ${className ?? ""}`} role="group" aria-label="Deal filters">
|
|
32
|
+
<select aria-label="Filter by owner" className={SELECT} value={value.ownerId} onChange={(e) => set("ownerId", e.target.value)}>
|
|
33
|
+
<option value="">Any owner</option>
|
|
34
|
+
{owners.map((o) => <option key={o.id} value={o.id}>{o.name}</option>)}
|
|
35
|
+
</select>
|
|
36
|
+
<select aria-label="Filter by stage" className={SELECT} value={value.stageId} onChange={(e) => set("stageId", e.target.value)}>
|
|
37
|
+
<option value="">Any stage</option>
|
|
38
|
+
{stages.map((s) => <option key={s.id} value={s.id}>{s.name}</option>)}
|
|
39
|
+
</select>
|
|
40
|
+
<select aria-label="Filter by outcome" className={SELECT} value={value.outcome} onChange={(e) => set("outcome", e.target.value as DealFilterState["outcome"])}>
|
|
41
|
+
<option value="">Open, won and lost</option>
|
|
42
|
+
<option value="open">Open</option>
|
|
43
|
+
<option value="won">Won</option>
|
|
44
|
+
<option value="lost">Lost</option>
|
|
45
|
+
</select>
|
|
46
|
+
<select aria-label="Filter by staleness" className={SELECT} value={value.staleDays} onChange={(e) => set("staleDays", e.target.value as DealFilterState["staleDays"])}>
|
|
47
|
+
<option value="">Any activity age</option>
|
|
48
|
+
{(["7", "14", "30", "60", "90"] as const).map((d) => <option key={d} value={d}>Stale {d}+ days</option>)}
|
|
49
|
+
</select>
|
|
50
|
+
<select aria-label="Filter by activity" className={SELECT} value={value.activity} onChange={(e) => set("activity", e.target.value as DealFilterState["activity"])}>
|
|
51
|
+
<option value="">With or without activity</option>
|
|
52
|
+
<option value="yes">Has activity</option>
|
|
53
|
+
<option value="no">No activity</option>
|
|
54
|
+
</select>
|
|
55
|
+
<label className="flex items-center gap-1 text-xs text-muted-foreground">Closes from
|
|
56
|
+
<input aria-label="Close date from" type="date" className={DATE} value={value.closeFrom} max={value.closeTo || undefined} onChange={(e) => set("closeFrom", e.target.value)} />
|
|
57
|
+
</label>
|
|
58
|
+
<label className="flex items-center gap-1 text-xs text-muted-foreground">to
|
|
59
|
+
<input aria-label="Close date to" type="date" className={DATE} value={value.closeTo} min={value.closeFrom || undefined} onChange={(e) => set("closeTo", e.target.value)} />
|
|
60
|
+
</label>
|
|
61
|
+
<label className="flex items-center gap-1 text-sm"><input type="checkbox" checked={value.orphaned} onChange={(e) => set("orphaned", e.target.checked)} />No contact</label>
|
|
62
|
+
{filtersActive(value) ? <Button size="sm" variant="ghost" onClick={() => onChange(NO_FILTERS)}>Clear filters</Button> : null}
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|