@aglyn/plugins-crm 1.0.0-beta.218 → 1.0.0-beta.220
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 +2 -0
- package/package.json +14 -14
- package/src/index.d.ts +1 -0
- package/src/index.js.map +1 -1
- package/src/lib/components/companies-bulk-bar.js +12 -4
- package/src/lib/components/companies-bulk-bar.js.map +1 -1
- package/src/lib/components/company-detail-page.js +11 -0
- package/src/lib/components/company-detail-page.js.map +1 -1
- package/src/lib/components/company-edit-drawer.js +12 -2
- package/src/lib/components/company-edit-drawer.js.map +1 -1
- package/src/lib/components/company-import-drawer.d.ts +1 -1
- package/src/lib/components/company-picker.js +10 -3
- package/src/lib/components/company-picker.js.map +1 -1
- package/src/lib/components/contact-associations-card.js +4 -3
- package/src/lib/components/contact-associations-card.js.map +1 -1
- package/src/lib/components/contact-detail-page.d.ts +4 -4
- package/src/lib/components/contact-detail-page.js +50 -53
- package/src/lib/components/contact-detail-page.js.map +1 -1
- package/src/lib/components/contact-list-columns.d.ts +3 -0
- package/src/lib/components/contact-list-columns.js +21 -5
- package/src/lib/components/contact-list-columns.js.map +1 -1
- package/src/lib/components/contact-merge-dialog.js +2 -1
- package/src/lib/components/contact-merge-dialog.js.map +1 -1
- package/src/lib/components/contact-timeline-card.js +4 -3
- package/src/lib/components/contact-timeline-card.js.map +1 -1
- package/src/lib/components/contacts-bulk-bar.d.ts +3 -3
- package/src/lib/components/contacts-bulk-bar.js +24 -51
- package/src/lib/components/contacts-bulk-bar.js.map +1 -1
- package/src/lib/components/contacts-section.js +9 -3
- package/src/lib/components/contacts-section.js.map +1 -1
- package/src/lib/components/crm-booking-zone.d.ts +8 -2
- package/src/lib/components/crm-booking-zone.js.map +1 -1
- package/src/lib/components/crm-export-all-button.d.ts +1 -1
- package/src/lib/components/crm-export-all-button.js.map +1 -1
- package/src/lib/components/crm-glance-card.js +2 -1
- package/src/lib/components/crm-glance-card.js.map +1 -1
- package/src/lib/components/crm-send-email-dialog.js +9 -6
- package/src/lib/components/crm-send-email-dialog.js.map +1 -1
- package/src/lib/components/csv-import-drawer.js +2 -2
- package/src/lib/components/csv-import-drawer.js.map +1 -1
- package/src/lib/components/deal-detail-page.js +18 -2
- package/src/lib/components/deal-detail-page.js.map +1 -1
- package/src/lib/components/deal-edit-drawer.js +10 -0
- package/src/lib/components/deal-edit-drawer.js.map +1 -1
- package/src/lib/components/deal-products-card.js +27 -16
- package/src/lib/components/deal-products-card.js.map +1 -1
- package/src/lib/components/deals-bulk-bar.js +13 -5
- package/src/lib/components/deals-bulk-bar.js.map +1 -1
- package/src/lib/components/lead-campaigns-card.d.ts +4 -4
- package/src/lib/components/lead-campaigns-card.js +15 -8
- package/src/lib/components/lead-campaigns-card.js.map +1 -1
- package/src/lib/components/lead-detail-page.js +25 -5
- package/src/lib/components/lead-detail-page.js.map +1 -1
- package/src/lib/components/lead-properties-card.js +10 -0
- package/src/lib/components/lead-properties-card.js.map +1 -1
- package/src/lib/components/lead-status-chip.js +3 -1
- package/src/lib/components/lead-status-chip.js.map +1 -1
- package/src/lib/components/lead-surfaces-note.js +4 -5
- package/src/lib/components/lead-surfaces-note.js.map +1 -1
- package/src/lib/components/lead-unqualify-dialog.js +6 -0
- package/src/lib/components/lead-unqualify-dialog.js.map +1 -1
- package/src/lib/components/leads-bulk-bar.js +34 -11
- package/src/lib/components/leads-bulk-bar.js.map +1 -1
- package/src/lib/components/leads-section.js +41 -8
- package/src/lib/components/leads-section.js.map +1 -1
- package/src/lib/components/new-lead-drawer.js +5 -4
- package/src/lib/components/new-lead-drawer.js.map +1 -1
- package/src/lib/components/org-lead-surfaces-note.js +6 -2
- package/src/lib/components/org-lead-surfaces-note.js.map +1 -1
- package/src/lib/components/recipes-card.js +9 -4
- package/src/lib/components/recipes-card.js.map +1 -1
- package/src/lib/components/record-sharing-card.d.ts +57 -0
- package/src/lib/components/record-sharing-card.js +244 -0
- package/src/lib/components/record-sharing-card.js.map +1 -0
- package/src/lib/components/reports/activity-card.js +4 -3
- package/src/lib/components/reports/activity-card.js.map +1 -1
- package/src/lib/components/reports/closed-deals-card.js +4 -3
- package/src/lib/components/reports/closed-deals-card.js.map +1 -1
- package/src/lib/components/reports/contacts-mix-card.js +3 -2
- package/src/lib/components/reports/contacts-mix-card.js.map +1 -1
- package/src/lib/components/reports/contacts-trend-card.js +2 -1
- package/src/lib/components/reports/contacts-trend-card.js.map +1 -1
- package/src/lib/components/reports/forecast-card.js +3 -2
- package/src/lib/components/reports/forecast-card.js.map +1 -1
- package/src/lib/components/reports/lead-funnel-card.js +8 -6
- package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
- package/src/lib/components/reports/pipeline-card.js +4 -3
- package/src/lib/components/reports/pipeline-card.js.map +1 -1
- package/src/lib/components/reports/report-format.d.ts +1 -1
- package/src/lib/components/reports/report-format.js.map +1 -1
- package/src/lib/components/reports/report-scope.d.ts +1 -1
- package/src/lib/components/reports/report-scope.js.map +1 -1
- package/src/lib/components/reports/source-conversion-card.js +2 -1
- package/src/lib/components/reports/source-conversion-card.js.map +1 -1
- package/src/lib/components/reports/tasks-card.js +2 -1
- package/src/lib/components/reports/tasks-card.js.map +1 -1
- package/src/lib/components/reports/won-lost-by-owner-card.js +3 -2
- package/src/lib/components/reports/won-lost-by-owner-card.js.map +1 -1
- package/src/lib/components/reports-section.js +4 -4
- package/src/lib/components/reports-section.js.map +1 -1
- package/src/lib/components/settings-section.js +7 -0
- package/src/lib/components/settings-section.js.map +1 -1
- package/src/lib/components/share-records-dialog.d.ts +28 -0
- package/src/lib/components/share-records-dialog.js +184 -0
- package/src/lib/components/share-records-dialog.js.map +1 -0
- package/src/lib/components/sharing-rule-drawer.d.ts +37 -0
- package/src/lib/components/sharing-rule-drawer.js +374 -0
- package/src/lib/components/sharing-rule-drawer.js.map +1 -0
- package/src/lib/components/sharing-rules-card.d.ts +24 -0
- package/src/lib/components/sharing-rules-card.js +287 -0
- package/src/lib/components/sharing-rules-card.js.map +1 -0
- package/src/lib/components/use-crm-api.d.ts +1 -1
- package/src/lib/components/use-crm-api.js.map +1 -1
- package/src/lib/components/use-site-route-context.d.ts +25 -0
- package/src/lib/components/use-site-route-context.js +58 -0
- package/src/lib/components/use-site-route-context.js.map +1 -0
- package/src/lib/constants/api-routes.d.ts +31 -0
- package/src/lib/constants/api-routes.js +27 -1
- package/src/lib/constants/api-routes.js.map +1 -1
- package/src/lib/constants/bundle-common.d.ts +2 -0
- package/src/lib/constants/bundle-common.js +1 -0
- package/src/lib/constants/bundle-common.js.map +1 -1
- package/src/lib/declarations.console-server.d.ts +41 -0
- package/src/lib/declarations.console-server.js +115 -0
- package/src/lib/declarations.console-server.js.map +1 -0
- package/src/lib/declarations.server.d.ts +12 -0
- package/src/lib/declarations.server.js +84 -1
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/hooks/use-contact-remove.d.ts +35 -0
- package/src/lib/hooks/use-contact-remove.js +77 -0
- package/src/lib/hooks/use-contact-remove.js.map +1 -0
- package/src/lib/hooks/use-crm-campaigns.d.ts +2 -2
- package/src/lib/hooks/use-crm-campaigns.js +3 -3
- package/src/lib/hooks/use-crm-campaigns.js.map +1 -1
- package/src/lib/hooks/use-crm-sharing.d.ts +30 -0
- package/src/lib/hooks/use-crm-sharing.js +109 -0
- package/src/lib/hooks/use-crm-sharing.js.map +1 -0
- package/src/lib/model/campaign-filing-activity.d.ts +1 -1
- package/src/lib/model/campaign-filing-activity.js +1 -1
- package/src/lib/model/campaign-filing-activity.js.map +1 -1
- package/src/lib/model/companies-csv.d.ts +1 -1
- package/src/lib/model/companies-csv.js +2 -2
- package/src/lib/model/companies-csv.js.map +1 -1
- package/src/lib/model/contact-holder.d.ts +68 -0
- package/src/lib/model/contact-holder.js +129 -0
- package/src/lib/model/contact-holder.js.map +1 -0
- package/src/lib/model/contact-interaction-links.d.ts +46 -0
- package/src/lib/model/contact-interaction-links.js +65 -0
- package/src/lib/model/contact-interaction-links.js.map +1 -0
- package/src/lib/model/contact-merge.d.ts +136 -0
- package/src/lib/model/contact-merge.js +412 -0
- package/src/lib/model/contact-merge.js.map +1 -0
- package/src/lib/model/contact-record.d.ts +7 -1
- package/src/lib/model/contact-record.js +7 -6
- package/src/lib/model/contact-record.js.map +1 -1
- package/src/lib/model/contact-remove.d.ts +48 -0
- package/src/lib/model/contact-remove.js +22 -0
- package/src/lib/model/contact-remove.js.map +1 -0
- package/src/lib/model/contacts-bulk-writes.d.ts +2 -71
- package/src/lib/model/contacts-bulk-writes.js +2 -82
- package/src/lib/model/contacts-bulk-writes.js.map +1 -1
- package/src/lib/model/contacts-csv.d.ts +2 -2
- package/src/lib/model/contacts-csv.js +2 -2
- package/src/lib/model/contacts-csv.js.map +1 -1
- package/src/lib/model/crm-csv.d.ts +239 -0
- package/src/lib/model/crm-csv.js +467 -0
- package/src/lib/model/crm-csv.js.map +1 -0
- package/src/lib/model/crm-digest.d.ts +227 -0
- package/src/lib/model/crm-digest.js +336 -0
- package/src/lib/model/crm-digest.js.map +1 -0
- package/src/lib/model/crm-import.d.ts +7 -1
- package/src/lib/model/crm-import.js +2 -1
- package/src/lib/model/crm-import.js.map +1 -1
- package/src/lib/model/crm-inbound.d.ts +162 -0
- package/src/lib/model/crm-inbound.js +208 -0
- package/src/lib/model/crm-inbound.js.map +1 -0
- package/src/lib/model/crm-lead-import.d.ts +1 -1
- package/src/lib/model/crm-lead-import.js +4 -4
- package/src/lib/model/crm-lead-import.js.map +1 -1
- package/src/lib/model/crm-record-routes.d.ts +5 -0
- package/src/lib/model/crm-record-routes.js +7 -0
- package/src/lib/model/crm-record-routes.js.map +1 -1
- package/src/lib/model/crm-reports.d.ts +467 -0
- package/src/lib/model/crm-reports.js +667 -0
- package/src/lib/model/crm-reports.js.map +1 -0
- package/src/lib/model/crm-routes.d.ts +6 -5
- package/src/lib/model/crm-routes.js +7 -7
- package/src/lib/model/crm-routes.js.map +1 -1
- package/src/lib/model/crm-search-sources.d.ts +15 -0
- package/src/lib/model/crm-search-sources.js +162 -0
- package/src/lib/model/crm-search-sources.js.map +1 -0
- package/src/lib/model/crm-sharing-api.d.ts +32 -0
- package/src/lib/model/crm-sharing-api.js +59 -0
- package/src/lib/model/crm-sharing-api.js.map +1 -0
- package/src/lib/model/crm-sharing.d.ts +333 -0
- package/src/lib/model/crm-sharing.js +603 -0
- package/src/lib/model/crm-sharing.js.map +1 -0
- package/src/lib/model/deals-csv.d.ts +1 -1
- package/src/lib/model/deals-csv.js +4 -5
- package/src/lib/model/deals-csv.js.map +1 -1
- package/src/lib/model/lead-filters.d.ts +5 -4
- package/src/lib/model/lead-filters.js +7 -5
- package/src/lib/model/lead-filters.js.map +1 -1
- package/src/lib/model/leads-csv.d.ts +1 -1
- package/src/lib/model/leads-csv.js +1 -1
- package/src/lib/model/leads-csv.js.map +1 -1
- package/src/lib/model/tasks-csv.d.ts +1 -1
- package/src/lib/model/tasks-csv.js +1 -1
- package/src/lib/model/tasks-csv.js.map +1 -1
- package/src/lib/plan-entitlements.d.ts +75 -0
- package/src/lib/plan-entitlements.js +56 -0
- package/src/lib/plan-entitlements.js.map +1 -0
- package/src/lib/plugin.js +39 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/api-v1/contacts-merge.d.ts +20 -0
- package/src/lib/server/api-v1/contacts-merge.js +120 -0
- package/src/lib/server/api-v1/contacts-merge.js.map +1 -0
- package/src/lib/server/api-v1/contacts.d.ts +18 -0
- package/src/lib/server/api-v1/contacts.js +1076 -0
- package/src/lib/server/api-v1/contacts.js.map +1 -0
- package/src/lib/server/api-v1/crm-activities.d.ts +18 -0
- package/src/lib/server/api-v1/crm-activities.js +269 -0
- package/src/lib/server/api-v1/crm-activities.js.map +1 -0
- package/src/lib/server/api-v1/crm-companies.d.ts +18 -0
- package/src/lib/server/api-v1/crm-companies.js +391 -0
- package/src/lib/server/api-v1/crm-companies.js.map +1 -0
- package/src/lib/server/api-v1/crm-deals.d.ts +18 -0
- package/src/lib/server/api-v1/crm-deals.js +631 -0
- package/src/lib/server/api-v1/crm-deals.js.map +1 -0
- package/src/lib/server/api-v1/crm-email-templates.d.ts +18 -0
- package/src/lib/server/api-v1/crm-email-templates.js +336 -0
- package/src/lib/server/api-v1/crm-email-templates.js.map +1 -0
- package/src/lib/server/api-v1/crm-leads.d.ts +18 -0
- package/src/lib/server/api-v1/crm-leads.js +833 -0
- package/src/lib/server/api-v1/crm-leads.js.map +1 -0
- package/src/lib/server/api-v1/crm-pipelines.d.ts +76 -0
- package/src/lib/server/api-v1/crm-pipelines.js +146 -0
- package/src/lib/server/api-v1/crm-pipelines.js.map +1 -0
- package/src/lib/server/api-v1/crm-shared.d.ts +271 -0
- package/src/lib/server/api-v1/crm-shared.js +436 -0
- package/src/lib/server/api-v1/crm-shared.js.map +1 -0
- package/src/lib/server/api-v1/crm-tasks.d.ts +18 -0
- package/src/lib/server/api-v1/crm-tasks.js +398 -0
- package/src/lib/server/api-v1/crm-tasks.js.map +1 -0
- package/src/lib/server/api-v1/openapi.d.ts +29 -0
- package/src/lib/server/api-v1/openapi.js +985 -0
- package/src/lib/server/api-v1/openapi.js.map +1 -0
- package/src/lib/server/api-v1/usage.d.ts +26 -0
- package/src/lib/server/api-v1/usage.js +73 -0
- package/src/lib/server/api-v1/usage.js.map +1 -0
- package/src/lib/server/campaign-filing-activity.d.ts +1 -1
- package/src/lib/server/campaign-filing-activity.js +7 -11
- package/src/lib/server/campaign-filing-activity.js.map +1 -1
- package/src/lib/server/capture-contact.js +15 -4
- package/src/lib/server/capture-contact.js.map +1 -1
- package/src/lib/server/consent-group-participant.js +1 -1
- package/src/lib/server/consent-group-participant.js.map +1 -1
- package/src/lib/server/contact-email-engagement.d.ts +33 -0
- package/src/lib/server/contact-email-engagement.js +146 -0
- package/src/lib/server/contact-email-engagement.js.map +1 -0
- package/src/lib/server/contact-lifecycle-floor.d.ts +89 -0
- package/src/lib/server/contact-lifecycle-floor.js +91 -0
- package/src/lib/server/contact-lifecycle-floor.js.map +1 -0
- package/src/lib/server/contact-remove.d.ts +75 -0
- package/src/lib/server/contact-remove.js +244 -0
- package/src/lib/server/contact-remove.js.map +1 -0
- package/src/lib/server/contact-update.js +4 -3
- package/src/lib/server/contact-update.js.map +1 -1
- package/src/lib/server/contacts-import.d.ts +47 -11
- package/src/lib/server/contacts-import.js +171 -71
- package/src/lib/server/contacts-import.js.map +1 -1
- package/src/lib/server/contacts-merge.d.ts +1 -1
- package/src/lib/server/contacts-merge.js +3 -2
- package/src/lib/server/contacts-merge.js.map +1 -1
- package/src/lib/server/container-detach.d.ts +31 -0
- package/src/lib/server/container-detach.js +115 -0
- package/src/lib/server/container-detach.js.map +1 -0
- package/src/lib/server/convert-host-lead.d.ts +189 -0
- package/src/lib/server/convert-host-lead.js +518 -0
- package/src/lib/server/convert-host-lead.js.map +1 -0
- package/src/lib/server/crm-inbound-email.d.ts +162 -0
- package/src/lib/server/crm-inbound-email.js +259 -0
- package/src/lib/server/crm-inbound-email.js.map +1 -0
- package/src/lib/server/crm-records-meter.d.ts +50 -0
- package/src/lib/server/crm-records-meter.js +126 -0
- package/src/lib/server/crm-records-meter.js.map +1 -0
- package/src/lib/server/crm-sharing.d.ts +108 -0
- package/src/lib/server/crm-sharing.js +790 -0
- package/src/lib/server/crm-sharing.js.map +1 -0
- package/src/lib/server/daily-digest-route.d.ts +101 -0
- package/src/lib/server/daily-digest-route.js +578 -0
- package/src/lib/server/daily-digest-route.js.map +1 -0
- package/src/lib/server/export-route.d.ts +60 -0
- package/src/lib/server/export-route.js +447 -0
- package/src/lib/server/export-route.js.map +1 -0
- package/src/lib/server/inbound-address.js +5 -3
- package/src/lib/server/inbound-address.js.map +1 -1
- package/src/lib/server/inbound-route.d.ts +74 -0
- package/src/lib/server/inbound-route.js +260 -0
- package/src/lib/server/inbound-route.js.map +1 -0
- package/src/lib/server/lead-campaign-carry.js +3 -3
- package/src/lib/server/lead-campaign-carry.js.map +1 -1
- package/src/lib/server/lead-convert.d.ts +1 -1
- package/src/lib/server/lead-convert.js +2 -2
- package/src/lib/server/lead-convert.js.map +1 -1
- package/src/lib/server/lead-create.d.ts +1 -1
- package/src/lib/server/lead-create.js +10 -15
- package/src/lib/server/lead-create.js.map +1 -1
- package/src/lib/server/lead-nurturing.d.ts +23 -0
- package/src/lib/server/lead-nurturing.js +82 -0
- package/src/lib/server/lead-nurturing.js.map +1 -0
- package/src/lib/server/leads-import.js +7 -7
- package/src/lib/server/leads-import.js.map +1 -1
- package/src/lib/server/merge-contacts.d.ts +55 -0
- package/src/lib/server/merge-contacts.js +274 -0
- package/src/lib/server/merge-contacts.js.map +1 -0
- package/src/lib/server/pipeline-record-index.d.ts +36 -0
- package/src/lib/server/pipeline-record-index.js +87 -0
- package/src/lib/server/pipeline-record-index.js.map +1 -0
- package/src/lib/server/record-email-state.js +23 -0
- package/src/lib/server/record-email-state.js.map +1 -1
- package/src/lib/server/record-facts.js +2 -1
- package/src/lib/server/record-facts.js.map +1 -1
- package/src/lib/server/record-timeline.d.ts +12 -0
- package/src/lib/server/record-timeline.js +127 -15
- package/src/lib/server/record-timeline.js.map +1 -1
- package/src/lib/server/task-reminders-route.d.ts +82 -0
- package/src/lib/server/task-reminders-route.js +499 -0
- package/src/lib/server/task-reminders-route.js.map +1 -0
- package/src/lib/server/task-routes.d.ts +2 -0
- package/src/lib/server/task-routes.js +22 -3
- package/src/lib/server/task-routes.js.map +1 -1
- package/src/lib/server-deal-stage.js +2 -1
- package/src/lib/server-deal-stage.js.map +1 -1
- package/src/lib/server.d.ts +5 -2
- package/src/lib/server.js +47 -2
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/stand-in-site-record-routes.d.ts +26 -0
- package/src/lib/testing/stand-in-site-record-routes.js +55 -0
- package/src/lib/testing/stand-in-site-record-routes.js.map +1 -0
- package/src/lib/usage-axes.d.ts +35 -0
- package/src/lib/usage-axes.js +75 -0
- package/src/lib/usage-axes.js.map +1 -0
- package/src/lib/components/use-emails-hub-path.d.ts +0 -24
- package/src/lib/components/use-emails-hub-path.js +0 -59
- package/src/lib/components/use-emails-hub-path.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/deals-bulk-bar.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\n/**\n * The bar over the deals table, for whatever rows are ticked (AGL-2621).\n *\n * Move them to a stage, mark them lost, hand them to an owner, take them\n * into a spreadsheet, or delete them. The owner and the delete are\n * document writes and go through the shared runner in batches; the stage\n * and the loss go through `crm/deal-stage`, ONE REQUEST PER DEAL, because\n * a stage change is what an automation listens for and only the route\n * emits the event — a batch that wrote `stageId` would move forty deals\n * and tell no workflow. Each refusal carries the route's own sentence,\n * named by the deal.\n *\n * ## Set stage needs one pipeline\n *\n * A stage belongs to a pipeline, and a selection that spans two has no\n * one list of stages to offer. The dialog says so rather than offering the\n * first pipeline's stages to a deal in the second, which the route would\n * refuse one row at a time.\n */\n\nimport { CRM_COLLECTIONS, dealStageById, crmMemberPickerLabel } from '@aglyn/aglyn'\nimport { useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport { useFirestore, useHostActivityLogger } from '@aglyn/tenant-feature-instance'\nimport { Button, MenuItem, TextField, Typography } from '@mui/material'\nimport { deleteField, doc } from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport { useCrmBulkApply } from '../hooks/use-crm-bulk-apply'\nimport type { DealStageApi } from '../hooks/use-deal-stage-api'\nimport type { OrgMemberDirectory } from '../hooks/use-org-member-directory'\nimport { downloadTextFile } from '../model/contacts-csv'\nimport {\n type CrmBulkPlan,\n type CrmBulkWrite,\n crmBulkWriters,\n runCrmBulkCalls,\n runCrmBulkWrites,\n} from '../model/crm-bulk-writes'\nimport {\n type DealDoc,\n type PipelineDoc,\n closingStage,\n openStages,\n} from '../model/deal-board-model'\nimport { type DealCsvOptions, dealsCsv } from '../model/deals-csv'\nimport {\n type CrmBulkNoun,\n CrmBulkBarFrame,\n CrmBulkValueDialog,\n countNoun,\n} from './crm-bulk-bar-frame'\nimport CrmExportAllButton from './crm-export-all-button'\nimport { LostReasonDialog } from './lost-reason-dialog'\n\nexport interface DealsBulkBarProps {\n /**\n * The site whose activity feed the bar's acts are logged in, or `null`\n * at the organization level (AGL-2630), where the selection spans sites\n * and the org's feed takes one line for the action instead (AGL-2634);\n * a stage move runs as the org there and is logged per deal by the route.\n */\n hostId: string | null\n /** `['orgs', orgId]`, or `null` while the org is unresolved. */\n scope: readonly ['orgs', string] | null\n rows: readonly DealDoc[]\n selected: readonly string[]\n onSelectedChange: (ids: string[]) => void\n /** The pipeline a deal's `pipelineId` names, for the stage list. */\n pipelineById: (id: string | undefined) => PipelineDoc | null\n /** The section's roster — already read for the Owner column. */\n roster: OrgMemberDirectory\n /** The one door a stage change goes through. */\n api: DealStageApi\n /** How the export names the pipeline, the stage and the owner — the table's own. */\n csv?: DealCsvOptions\n}\n\nconst NOUN: CrmBulkNoun = { singular: 'deal', plural: 'deals' }\n\ntype PendingAction = 'stage' | 'owner'\n\nconst ACTION_TITLES: Record<PendingAction, string> = {\n stage: 'Set the stage',\n owner: 'Set the owner',\n}\n\n/** The label a report lists a deal under. */\nconst labelOf = (deal: DealDoc): string => deal.title || 'Untitled deal'\n\nexport function DealsBulkBar(props: DealsBulkBarProps) {\n if (!props.selected.length) return null\n return <DealsBulkBarBody {...props} />\n}\nDealsBulkBar.displayName = 'DealsBulkBar'\n\nfunction DealsBulkBarBody(props: DealsBulkBarProps) {\n const { hostId, scope, rows, selected, onSelectedChange, pipelineById, roster, api, csv } =\n props\n const firestore = useFirestore()\n const { confirm } = useConfirmationContext()\n const logActivity = useHostActivityLogger(hostId ?? undefined)\n const { busy, report, apply, dismissReport } = useCrmBulkApply({ recordKind: 'deal' })\n\n const selectedRows = useMemo(() => {\n const chosen = new Set(selected)\n return rows.filter((row) => chosen.has(row.$id))\n }, [rows, selected])\n\n /*\n * The one pipeline the selection is in, or `null` when it spans more\n * than one — in which case Set stage has no list to offer.\n */\n const pipeline = useMemo(() => {\n const ids = new Set(selectedRows.map((deal) => String(deal.pipelineId ?? '')))\n return ids.size === 1 ? pipelineById([...ids][0]) : null\n }, [selectedRows, pipelineById])\n const stages = useMemo(() => {\n if (!pipeline) return []\n const won = closingStage(pipeline, 'won')\n return [...openStages(pipeline), ...(won ? [won] : [])]\n }, [pipeline])\n\n const [pending, setPending] = useState<PendingAction | null>(null)\n const [value, setValue] = useState('')\n const [losing, setLosing] = useState(false)\n\n const writers = useMemo(\n () =>\n crmBulkWriters(firestore, (id) =>\n doc(firestore, scope?.[0] ?? 'orgs', scope?.[1] ?? '', CRM_COLLECTIONS.deals, id),\n ),\n [firestore, scope],\n )\n\n const openAction = (action: PendingAction) => {\n setValue('')\n setPending(action)\n }\n\n const runPlan = useCallback(\n (plan: CrmBulkPlan, done: (count: number) => string) =>\n apply({\n attempted: plan.writes.length,\n skipped: plan.skipped,\n job: () => runCrmBulkWrites(writers, plan.writes, (write) => write.label),\n done,\n }),\n [apply, writers],\n )\n\n /** One route request per deal, in order, named by title. */\n const runCalls = useCallback(\n (call: (deal: DealDoc) => Promise<unknown>, done: (count: number) => string) =>\n apply({\n attempted: selectedRows.length,\n skipped: [],\n job: () => runCrmBulkCalls(selectedRows, labelOf, call),\n done,\n // The stage route writes the org's line per deal (AGL-2634).\n loggedByRoute: true,\n }),\n [apply, selectedRows],\n )\n\n const handleApply = useCallback(async () => {\n if (!pending || !scope) return\n const action = pending\n setPending(null)\n if (action === 'stage') {\n const stage = dealStageById(pipeline, value)\n if (!stage) return\n await runCalls(\n (deal) => api.moveToStage(deal, stage.id),\n (count) => `Moved ${countNoun(count, NOUN)} to ${stage.name}`,\n )\n return\n }\n const nowMs = Date.now()\n const writes: CrmBulkWrite[] = selectedRows.map((deal) => ({\n id: deal.$id,\n label: labelOf(deal),\n kind: 'update',\n data: {\n ownerUid: value ? value : deleteField(),\n updatedAt: new Date(nowMs),\n },\n }))\n await runPlan({ writes, skipped: [] }, (count) => `Owner set on ${countNoun(count, NOUN)}`)\n }, [pending, scope, value, pipeline, selectedRows, api, runCalls, runPlan])\n\n const handleLost = useCallback(\n async (reason: string) => {\n setLosing(false)\n await runCalls(\n (deal) => api.markLost(deal, reason),\n (count) => `Marked ${countNoun(count, NOUN)} lost`,\n )\n },\n [api, runCalls],\n )\n\n const handleExport = useCallback(() => {\n downloadTextFile('deals-selected.csv', 'text/csv', dealsCsv(selectedRows, csv))\n }, [selectedRows, csv])\n\n const handleDelete = useCallback(async () => {\n if (!scope || !selectedRows.length) return\n const count = selectedRows.length\n const confirmed = await confirm({\n title: count === 1 ? 'Delete this deal?' : `Delete ${count} deals?`,\n description:\n `${count === 1 ? 'It is' : 'They are'} removed from the pipeline along ` +\n 'with their notes. The contacts and the companies they name are not ' +\n 'touched.',\n confirmationText: count === 1 ? 'Delete deal' : 'Delete deals',\n confirmationButtonProps: { color: 'error' },\n })\n // `confirm` resolves with no value and REJECTS on cancel.\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n const writes: CrmBulkWrite[] = selectedRows.map((deal) => ({\n id: deal.$id,\n label: labelOf(deal),\n kind: 'delete',\n }))\n const outcome = await runPlan(\n { writes, skipped: [] },\n (done) => `Deleted ${countNoun(done, NOUN)}`,\n )\n // One line per deal that went, as its own page would write.\n const refused = new Set(outcome.refused.map((row) => row.label))\n for (const deal of selectedRows) {\n if (!refused.has(labelOf(deal))) {\n logActivity('Deleted deal', { type: 'deal', id: deal.$id, name: deal.title })\n }\n }\n onSelectedChange(\n selectedRows.filter((deal) => refused.has(labelOf(deal))).map((deal) => deal.$id),\n )\n }, [scope, selectedRows, confirm, runPlan, logActivity, onSelectedChange])\n\n return (\n <CrmBulkBarFrame\n count={selected.length}\n noun={NOUN}\n busy={busy}\n onClear={() => onSelectedChange([])}\n report={report}\n onDismissReport={dismissReport}\n extras={\n <>\n <CrmBulkValueDialog\n open={pending !== null}\n title={pending ? ACTION_TITLES[pending] : ''}\n count={selected.length}\n noun={NOUN}\n busy={busy}\n canApply={pending === 'owner' || Boolean(value)}\n onClose={() => setPending(null)}\n onApply={() => void handleApply()}\n >\n {pending === 'stage' ? (\n pipeline ? (\n <TextField\n select\n size=\"small\"\n label=\"Stage\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n helperText={`${pipeline.name} — each move fires its event, as a drag does`}\n >\n {stages.map((stage) => (\n <MenuItem key={stage.id} value={stage.id}>\n {stage.name}\n </MenuItem>\n ))}\n </TextField>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'The selected deals are in different pipelines, so there is ' +\n 'no one list of stages to move them to. Select deals from ' +\n 'one pipeline.'}\n </Typography>\n )\n ) : pending === 'owner' ? (\n <TextField\n select\n size=\"small\"\n label=\"Owner\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n disabled={roster.loading}\n error={Boolean(roster.error)}\n helperText={roster.error ?? (roster.loading ? 'Loading the team…' : undefined)}\n >\n <MenuItem value=\"\">{'Nobody — clear the owner'}</MenuItem>\n {roster.members.map((member) => (\n <MenuItem key={member.uid} value={member.uid}>\n {crmMemberPickerLabel(member)}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n </CrmBulkValueDialog>\n <LostReasonDialog\n open={losing}\n dealTitle={selectedRows[0]?.title ?? ''}\n count={selectedRows.length}\n busy={busy}\n onClose={() => setLosing(false)}\n onConfirm={(reason) => void handleLost(reason)}\n />\n </>\n }\n >\n <Button size=\"small\" disabled={busy || !scope} onClick={() => openAction('stage')}>\n {'Set stage'}\n </Button>\n <Button size=\"small\" disabled={busy || !scope} onClick={() => openAction('owner')}>\n {'Set owner'}\n </Button>\n <Button size=\"small\" disabled={busy || !scope} onClick={() => setLosing(true)}>\n {'Mark lost'}\n </Button>\n <Button size=\"small\" disabled={busy} onClick={handleExport}>\n {'Export CSV'}\n </Button>\n {/*\n The selection's file is the rows on screen; this one is the whole\n collection, streamed by the server (AGL-2662).\n */}\n <CrmExportAllButton\n resource=\"deals\"\n orgId={scope?.[1] ?? null}\n hostId={hostId}\n disabled={busy}\n />\n <Button\n size=\"small\"\n color=\"error\"\n disabled={busy || !scope}\n onClick={() => void handleDelete()}\n >\n {'Delete'}\n </Button>\n </CrmBulkBarFrame>\n )\n}\nDealsBulkBarBody.displayName = 'DealsBulkBarBody'\n\nexport default DealsBulkBar\n"],"names":["CRM_COLLECTIONS","dealStageById","crmMemberPickerLabel","useConfirmationContext","useFirestore","useHostActivityLogger","Button","MenuItem","TextField","Typography","deleteField","doc","useCallback","useMemo","useState","useCrmBulkApply","downloadTextFile","crmBulkWriters","runCrmBulkCalls","runCrmBulkWrites","closingStage","openStages","dealsCsv","CrmBulkBarFrame","CrmBulkValueDialog","countNoun","CrmExportAllButton","LostReasonDialog","NOUN","singular","plural","ACTION_TITLES","stage","owner","labelOf","deal","title","DealsBulkBar","props","selected","length","DealsBulkBarBody","displayName","roster","selectedRows","hostId","scope","rows","onSelectedChange","pipelineById","api","csv","firestore","confirm","logActivity","undefined","busy","report","apply","dismissReport","recordKind","chosen","Set","filter","row","has","$id","pipeline","ids","map","String","pipelineId","size","stages","won","pending","setPending","value","setValue","losing","setLosing","writers","id","deals","openAction","action","runPlan","plan","done","attempted","writes","skipped","job","write","label","runCalls","call","loggedByRoute","handleApply","moveToStage","count","name","nowMs","Date","now","kind","data","ownerUid","updatedAt","handleLost","reason","markLost","handleExport","handleDelete","confirmed","description","confirmationText","confirmationButtonProps","color","then","catch","outcome","refused","type","noun","onClear","onDismissReport","extras","open","canApply","Boolean","onClose","onApply","select","onChange","event","target","helperText","variant","disabled","loading","error","members","member","uid","dealTitle","onConfirm","onClick","resource","orgId"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/deals-bulk-bar.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\n/**\n * The bar over the deals table, for whatever rows are ticked (AGL-2621).\n *\n * Move them to a stage, mark them lost, hand them to an owner, take them\n * into a spreadsheet, or delete them. The owner and the delete are\n * document writes and go through the shared runner in batches; the stage\n * and the loss go through `crm/deal-stage`, ONE REQUEST PER DEAL, because\n * a stage change is what an automation listens for and only the route\n * emits the event — a batch that wrote `stageId` would move forty deals\n * and tell no workflow. Each refusal carries the route's own sentence,\n * named by the deal.\n *\n * ## Set stage needs one pipeline\n *\n * A stage belongs to a pipeline, and a selection that spans two has no\n * one list of stages to offer. The dialog says so rather than offering the\n * first pipeline's stages to a deal in the second, which the route would\n * refuse one row at a time.\n */\n\nimport { CRM_COLLECTIONS, dealStageById, crmMemberPickerLabel } from '@aglyn/aglyn'\nimport { useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport { useFirestore, useHostActivityLogger } from '@aglyn/tenant-feature-instance'\nimport { Button, MenuItem, TextField, Typography } from '@mui/material'\nimport { deleteField, doc } from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport { useCrmBulkApply } from '../hooks/use-crm-bulk-apply'\nimport type { DealStageApi } from '../hooks/use-deal-stage-api'\nimport type { OrgMemberDirectory } from '../hooks/use-org-member-directory'\nimport { downloadTextFile } from '../model/contacts-csv'\nimport {\n type CrmBulkPlan,\n type CrmBulkWrite,\n crmBulkWriters,\n runCrmBulkCalls,\n runCrmBulkWrites,\n} from '../model/crm-bulk-writes'\nimport {\n type DealDoc,\n type PipelineDoc,\n closingStage,\n openStages,\n} from '../model/deal-board-model'\nimport { type DealCsvOptions, dealsCsv } from '../model/deals-csv'\nimport {\n type CrmBulkNoun,\n CrmBulkBarFrame,\n CrmBulkValueDialog,\n countNoun,\n} from './crm-bulk-bar-frame'\nimport CrmExportAllButton from './crm-export-all-button'\nimport { LostReasonDialog } from './lost-reason-dialog'\nimport { useCrmSharingFollowUp } from '../hooks/use-crm-sharing'\n\nexport interface DealsBulkBarProps {\n /**\n * The site whose activity feed the bar's acts are logged in, or `null`\n * at the organization level (AGL-2630), where the selection spans sites\n * and the org's feed takes one line for the action instead (AGL-2634);\n * a stage move runs as the org there and is logged per deal by the route.\n */\n hostId: string | null\n /** `['orgs', orgId]`, or `null` while the org is unresolved. */\n scope: readonly ['orgs', string] | null\n rows: readonly DealDoc[]\n selected: readonly string[]\n onSelectedChange: (ids: string[]) => void\n /** The pipeline a deal's `pipelineId` names, for the stage list. */\n pipelineById: (id: string | undefined) => PipelineDoc | null\n /** The section's roster — already read for the Owner column. */\n roster: OrgMemberDirectory\n /** The one door a stage change goes through. */\n api: DealStageApi\n /** How the export names the pipeline, the stage and the owner — the table's own. */\n csv?: DealCsvOptions\n}\n\nconst NOUN: CrmBulkNoun = { singular: 'deal', plural: 'deals' }\n\ntype PendingAction = 'stage' | 'owner'\n\nconst ACTION_TITLES: Record<PendingAction, string> = {\n stage: 'Set the stage',\n owner: 'Set the owner',\n}\n\n/** The label a report lists a deal under. */\nconst labelOf = (deal: DealDoc): string => deal.title || 'Untitled deal'\n\nexport function DealsBulkBar(props: DealsBulkBarProps) {\n if (!props.selected.length) return null\n return <DealsBulkBarBody {...props} />\n}\nDealsBulkBar.displayName = 'DealsBulkBar'\n\nfunction DealsBulkBarBody(props: DealsBulkBarProps) {\n const { hostId, scope, rows, selected, onSelectedChange, pipelineById, roster, api, csv } =\n props\n const firestore = useFirestore()\n const { confirm } = useConfirmationContext()\n const followUpSharing = useCrmSharingFollowUp(hostId ?? null, scope?.[1] ?? null)\n const logActivity = useHostActivityLogger(hostId ?? undefined)\n const { busy, report, apply, dismissReport } = useCrmBulkApply({ recordKind: 'deal' })\n\n const selectedRows = useMemo(() => {\n const chosen = new Set(selected)\n return rows.filter((row) => chosen.has(row.$id))\n }, [rows, selected])\n\n /*\n * The one pipeline the selection is in, or `null` when it spans more\n * than one — in which case Set stage has no list to offer.\n */\n const pipeline = useMemo(() => {\n const ids = new Set(selectedRows.map((deal) => String(deal.pipelineId ?? '')))\n return ids.size === 1 ? pipelineById([...ids][0]) : null\n }, [selectedRows, pipelineById])\n const stages = useMemo(() => {\n if (!pipeline) return []\n const won = closingStage(pipeline, 'won')\n return [...openStages(pipeline), ...(won ? [won] : [])]\n }, [pipeline])\n\n const [pending, setPending] = useState<PendingAction | null>(null)\n const [value, setValue] = useState('')\n const [losing, setLosing] = useState(false)\n\n const writers = useMemo(\n () =>\n crmBulkWriters(firestore, (id) =>\n doc(firestore, scope?.[0] ?? 'orgs', scope?.[1] ?? '', CRM_COLLECTIONS.deals, id),\n ),\n [firestore, scope],\n )\n\n const openAction = (action: PendingAction) => {\n setValue('')\n setPending(action)\n }\n\n const runPlan = useCallback(\n (plan: CrmBulkPlan, done: (count: number) => string) =>\n apply({\n attempted: plan.writes.length,\n skipped: plan.skipped,\n job: async () => {\n const outcome = await runCrmBulkWrites(writers, plan.writes, (write) => write.label)\n // A client-direct write owes the sharing rules a re-evaluation (AGL-3336).\n followUpSharing(\n 'deals',\n plan.writes.filter((write) => write.kind === 'update').map((write) => write.id),\n )\n return outcome\n },\n done,\n }),\n [apply, writers, followUpSharing],\n )\n\n /** One route request per deal, in order, named by title. */\n const runCalls = useCallback(\n (call: (deal: DealDoc) => Promise<unknown>, done: (count: number) => string) =>\n apply({\n attempted: selectedRows.length,\n skipped: [],\n job: () => runCrmBulkCalls(selectedRows, labelOf, call),\n done,\n // The stage route writes the org's line per deal (AGL-2634).\n loggedByRoute: true,\n }),\n [apply, selectedRows],\n )\n\n const handleApply = useCallback(async () => {\n if (!pending || !scope) return\n const action = pending\n setPending(null)\n if (action === 'stage') {\n const stage = dealStageById(pipeline, value)\n if (!stage) return\n await runCalls(\n (deal) => api.moveToStage(deal, stage.id),\n (count) => `Moved ${countNoun(count, NOUN)} to ${stage.name}`,\n )\n return\n }\n const nowMs = Date.now()\n const writes: CrmBulkWrite[] = selectedRows.map((deal) => ({\n id: deal.$id,\n label: labelOf(deal),\n kind: 'update',\n data: {\n ownerUid: value ? value : deleteField(),\n updatedAt: new Date(nowMs),\n },\n }))\n await runPlan({ writes, skipped: [] }, (count) => `Owner set on ${countNoun(count, NOUN)}`)\n }, [pending, scope, value, pipeline, selectedRows, api, runCalls, runPlan])\n\n const handleLost = useCallback(\n async (reason: string) => {\n setLosing(false)\n await runCalls(\n (deal) => api.markLost(deal, reason),\n (count) => `Marked ${countNoun(count, NOUN)} lost`,\n )\n },\n [api, runCalls],\n )\n\n const handleExport = useCallback(() => {\n downloadTextFile('deals-selected.csv', 'text/csv', dealsCsv(selectedRows, csv))\n }, [selectedRows, csv])\n\n const handleDelete = useCallback(async () => {\n if (!scope || !selectedRows.length) return\n const count = selectedRows.length\n const confirmed = await confirm({\n title: count === 1 ? 'Delete this deal?' : `Delete ${count} deals?`,\n description:\n `${count === 1 ? 'It is' : 'They are'} removed from the pipeline along ` +\n 'with their notes. The contacts and the companies they name are not ' +\n 'touched.',\n confirmationText: count === 1 ? 'Delete deal' : 'Delete deals',\n confirmationButtonProps: { color: 'error' },\n })\n // `confirm` resolves with no value and REJECTS on cancel.\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n const writes: CrmBulkWrite[] = selectedRows.map((deal) => ({\n id: deal.$id,\n label: labelOf(deal),\n kind: 'delete',\n }))\n const outcome = await runPlan(\n { writes, skipped: [] },\n (done) => `Deleted ${countNoun(done, NOUN)}`,\n )\n // One line per deal that went, as its own page would write.\n const refused = new Set(outcome.refused.map((row) => row.label))\n for (const deal of selectedRows) {\n if (!refused.has(labelOf(deal))) {\n logActivity('Deleted deal', { type: 'deal', id: deal.$id, name: deal.title })\n }\n }\n onSelectedChange(\n selectedRows.filter((deal) => refused.has(labelOf(deal))).map((deal) => deal.$id),\n )\n }, [scope, selectedRows, confirm, runPlan, logActivity, onSelectedChange])\n\n return (\n <CrmBulkBarFrame\n count={selected.length}\n noun={NOUN}\n busy={busy}\n onClear={() => onSelectedChange([])}\n report={report}\n onDismissReport={dismissReport}\n extras={\n <>\n <CrmBulkValueDialog\n open={pending !== null}\n title={pending ? ACTION_TITLES[pending] : ''}\n count={selected.length}\n noun={NOUN}\n busy={busy}\n canApply={pending === 'owner' || Boolean(value)}\n onClose={() => setPending(null)}\n onApply={() => void handleApply()}\n >\n {pending === 'stage' ? (\n pipeline ? (\n <TextField\n select\n size=\"small\"\n label=\"Stage\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n helperText={`${pipeline.name} — each move fires its event, as a drag does`}\n >\n {stages.map((stage) => (\n <MenuItem key={stage.id} value={stage.id}>\n {stage.name}\n </MenuItem>\n ))}\n </TextField>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'The selected deals are in different pipelines, so there is ' +\n 'no one list of stages to move them to. Select deals from ' +\n 'one pipeline.'}\n </Typography>\n )\n ) : pending === 'owner' ? (\n <TextField\n select\n size=\"small\"\n label=\"Owner\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n disabled={roster.loading}\n error={Boolean(roster.error)}\n helperText={roster.error ?? (roster.loading ? 'Loading the team…' : undefined)}\n >\n <MenuItem value=\"\">{'Nobody — clear the owner'}</MenuItem>\n {roster.members.map((member) => (\n <MenuItem key={member.uid} value={member.uid}>\n {crmMemberPickerLabel(member)}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n </CrmBulkValueDialog>\n <LostReasonDialog\n open={losing}\n dealTitle={selectedRows[0]?.title ?? ''}\n count={selectedRows.length}\n busy={busy}\n onClose={() => setLosing(false)}\n onConfirm={(reason) => void handleLost(reason)}\n />\n </>\n }\n >\n <Button size=\"small\" disabled={busy || !scope} onClick={() => openAction('stage')}>\n {'Set stage'}\n </Button>\n <Button size=\"small\" disabled={busy || !scope} onClick={() => openAction('owner')}>\n {'Set owner'}\n </Button>\n <Button size=\"small\" disabled={busy || !scope} onClick={() => setLosing(true)}>\n {'Mark lost'}\n </Button>\n <Button size=\"small\" disabled={busy} onClick={handleExport}>\n {'Export CSV'}\n </Button>\n {/*\n The selection's file is the rows on screen; this one is the whole\n collection, streamed by the server (AGL-2662).\n */}\n <CrmExportAllButton\n resource=\"deals\"\n orgId={scope?.[1] ?? null}\n hostId={hostId}\n disabled={busy}\n />\n <Button\n size=\"small\"\n color=\"error\"\n disabled={busy || !scope}\n onClick={() => void handleDelete()}\n >\n {'Delete'}\n </Button>\n </CrmBulkBarFrame>\n )\n}\nDealsBulkBarBody.displayName = 'DealsBulkBarBody'\n\nexport default DealsBulkBar\n"],"names":["CRM_COLLECTIONS","dealStageById","crmMemberPickerLabel","useConfirmationContext","useFirestore","useHostActivityLogger","Button","MenuItem","TextField","Typography","deleteField","doc","useCallback","useMemo","useState","useCrmBulkApply","downloadTextFile","crmBulkWriters","runCrmBulkCalls","runCrmBulkWrites","closingStage","openStages","dealsCsv","CrmBulkBarFrame","CrmBulkValueDialog","countNoun","CrmExportAllButton","LostReasonDialog","useCrmSharingFollowUp","NOUN","singular","plural","ACTION_TITLES","stage","owner","labelOf","deal","title","DealsBulkBar","props","selected","length","DealsBulkBarBody","displayName","roster","selectedRows","hostId","scope","rows","onSelectedChange","pipelineById","api","csv","firestore","confirm","followUpSharing","logActivity","undefined","busy","report","apply","dismissReport","recordKind","chosen","Set","filter","row","has","$id","pipeline","ids","map","String","pipelineId","size","stages","won","pending","setPending","value","setValue","losing","setLosing","writers","id","deals","openAction","action","runPlan","plan","done","attempted","writes","skipped","job","outcome","write","label","kind","runCalls","call","loggedByRoute","handleApply","moveToStage","count","name","nowMs","Date","now","data","ownerUid","updatedAt","handleLost","reason","markLost","handleExport","handleDelete","confirmed","description","confirmationText","confirmationButtonProps","color","then","catch","refused","type","noun","onClear","onDismissReport","extras","open","canApply","Boolean","onClose","onApply","select","onChange","event","target","helperText","variant","disabled","loading","error","members","member","uid","dealTitle","onConfirm","onClick","resource","orgId"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type ContainerPickerOption } from '@aglyn/tenant-feature-instance/components/container-picker';
|
|
2
2
|
import { type FirestoreDocStatus } from '@aglyn/tenant-feature-instance';
|
|
3
3
|
/** The helper line under the picker — the contact card's, word for word. */
|
|
4
4
|
export declare const LEAD_CAMPAIGNS_HELPER_TEXT = "Your own filing. It never adds anyone to a send \u2014 a campaign mails its lists.";
|
|
@@ -11,7 +11,7 @@ export declare const LEAD_CAMPAIGNS_HELPER_TEXT = "Your own filing. It never add
|
|
|
11
11
|
* looks like a fault. The picker below keeps such an id visible as itself,
|
|
12
12
|
* so the filing is never silently lost; the header only ever names.
|
|
13
13
|
*/
|
|
14
|
-
export declare function leadCampaignNames(lead: Record<string, unknown> | null | undefined, options: readonly
|
|
14
|
+
export declare function leadCampaignNames(lead: Record<string, unknown> | null | undefined, options: readonly ContainerPickerOption[]): string[];
|
|
15
15
|
export interface LeadCampaignsCardProps {
|
|
16
16
|
/**
|
|
17
17
|
* The lead's own site, or `null` at the organization level for a lead no
|
|
@@ -26,7 +26,7 @@ export interface LeadCampaignsCardProps {
|
|
|
26
26
|
/** The listener has not confirmed this document with the server yet. */
|
|
27
27
|
fromCache: boolean;
|
|
28
28
|
/** The site's campaigns, read once by the page for this card and the header. */
|
|
29
|
-
options: readonly
|
|
29
|
+
options: readonly ContainerPickerOption[];
|
|
30
30
|
/** The campaign read has answered — false while it settles. */
|
|
31
31
|
optionsReady: boolean;
|
|
32
32
|
/**
|
|
@@ -51,7 +51,7 @@ export interface LeadCampaignsCardProps {
|
|
|
51
51
|
* is the lead's own client door — a one-field `update` on
|
|
52
52
|
* `hosts/{hostId}/leads`, as the properties card writes status and notes —
|
|
53
53
|
* guarded by the seed like every draft edited over a cached read, and
|
|
54
|
-
* `
|
|
54
|
+
* `containerMembershipUnchanged` keeps Save quiet while the picker only
|
|
55
55
|
* reorders what is stored. **Save filing** sits in the card header.
|
|
56
56
|
*
|
|
57
57
|
* It is FILING, not audience: the helper says so, in the sentence the
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
*/ 'use client';
|
|
17
17
|
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
18
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
19
|
-
import {
|
|
20
|
-
import
|
|
19
|
+
import { containerMembershipUnchanged, containerMembershipValue, pluginDocsHelp, readContainerIds } from "@aglyn/aglyn";
|
|
20
|
+
import ContainerPicker from "@aglyn/tenant-feature-instance/components/container-picker";
|
|
21
21
|
import { CardDisplay } from "@aglyn/shared-ui-jsx";
|
|
22
22
|
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
23
23
|
import { useFirestore, writeGuardedBySeed } from "@aglyn/tenant-feature-instance";
|
|
@@ -26,6 +26,7 @@ import { doc, serverTimestamp, updateDoc } from "firebase/firestore";
|
|
|
26
26
|
import { useEffect, useState } from "react";
|
|
27
27
|
import { useCrmScope } from "../hooks/use-crm-scope.js";
|
|
28
28
|
import { crmClientListFields } from "../model/crm-list-query.js";
|
|
29
|
+
import { useCrmSharingFollowUp } from "../hooks/use-crm-sharing.js";
|
|
29
30
|
/** The list field a campaign filing moves (AGL-3321). */ const LEAD_CAMPAIGN_LIST_FIELDS = [
|
|
30
31
|
'scopedCampaignIds'
|
|
31
32
|
];
|
|
@@ -43,7 +44,7 @@ import { crmClientListFields } from "../model/crm-list-query.js";
|
|
|
43
44
|
option.value,
|
|
44
45
|
option.label
|
|
45
46
|
]));
|
|
46
|
-
return
|
|
47
|
+
return readContainerIds(lead, 'campaign').map((id)=>labels.get(id)).filter((name)=>Boolean(name));
|
|
47
48
|
}
|
|
48
49
|
/**
|
|
49
50
|
* WHICH CAMPAIGNS THIS LEAD IS FILED UNDER (AGL-3274).
|
|
@@ -57,7 +58,7 @@ import { crmClientListFields } from "../model/crm-list-query.js";
|
|
|
57
58
|
* is the lead's own client door — a one-field `update` on
|
|
58
59
|
* `hosts/{hostId}/leads`, as the properties card writes status and notes —
|
|
59
60
|
* guarded by the seed like every draft edited over a cached read, and
|
|
60
|
-
* `
|
|
61
|
+
* `containerMembershipUnchanged` keeps Save quiet while the picker only
|
|
61
62
|
* reorders what is stored. **Save filing** sits in the card header.
|
|
62
63
|
*
|
|
63
64
|
* It is FILING, not audience: the helper says so, in the sentence the
|
|
@@ -70,9 +71,10 @@ import { crmClientListFields } from "../model/crm-list-query.js";
|
|
|
70
71
|
const { orgId } = useCrmScope({
|
|
71
72
|
hostId
|
|
72
73
|
});
|
|
74
|
+
const followUpSharing = useCrmSharingFollowUp(hostId, orgId);
|
|
73
75
|
const firestore = useFirestore();
|
|
74
76
|
const { enqueueSnackbar } = useSnackbar();
|
|
75
|
-
const stored =
|
|
77
|
+
const stored = readContainerIds(lead, 'campaign');
|
|
76
78
|
const [selected, setSelected] = useState(stored);
|
|
77
79
|
const [dirty, setDirty] = useState(false);
|
|
78
80
|
const [saving, setSaving] = useState(false);
|
|
@@ -87,7 +89,7 @@ import { crmClientListFields } from "../model/crm-list-query.js";
|
|
|
87
89
|
storedKey,
|
|
88
90
|
dirty
|
|
89
91
|
]);
|
|
90
|
-
const unchanged =
|
|
92
|
+
const unchanged = containerMembershipUnchanged(stored, selected);
|
|
91
93
|
const empty = optionsReady && !options.length;
|
|
92
94
|
const save = async ()=>{
|
|
93
95
|
if (unchanged) return;
|
|
@@ -99,7 +101,7 @@ import { crmClientListFields } from "../model/crm-list-query.js";
|
|
|
99
101
|
return;
|
|
100
102
|
}
|
|
101
103
|
setSaving(true);
|
|
102
|
-
const next =
|
|
104
|
+
const next = containerMembershipValue(selected);
|
|
103
105
|
const verdict = await writeGuardedBySeed({
|
|
104
106
|
subject: 'lead',
|
|
105
107
|
fromCache,
|
|
@@ -114,6 +116,10 @@ import { crmClientListFields } from "../model/crm-list-query.js";
|
|
|
114
116
|
}));
|
|
115
117
|
});
|
|
116
118
|
setSaving(false);
|
|
119
|
+
// A rule may share by campaign (AGL-3336).
|
|
120
|
+
if (verdict.ok) followUpSharing('leads', [
|
|
121
|
+
leadId
|
|
122
|
+
]);
|
|
117
123
|
if (!verdict.ok) {
|
|
118
124
|
var _verdict_message;
|
|
119
125
|
enqueueSnackbar((_verdict_message = verdict.message) != null ? _verdict_message : 'The filing could not be saved.', {
|
|
@@ -156,7 +162,8 @@ import { crmClientListFields } from "../model/crm-list-query.js";
|
|
|
156
162
|
},
|
|
157
163
|
children: /*#__PURE__*/ _jsx(Stack, {
|
|
158
164
|
spacing: 2,
|
|
159
|
-
children: /*#__PURE__*/ _jsx(
|
|
165
|
+
children: /*#__PURE__*/ _jsx(ContainerPicker, {
|
|
166
|
+
kind: "campaign",
|
|
160
167
|
options: options,
|
|
161
168
|
value: selected,
|
|
162
169
|
onChange: (next)=>{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-campaigns-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n campaignMembershipUnchanged,\n campaignMembershipValue,\n pluginDocsHelp,\n readCampaignIds,\n} from '@aglyn/aglyn'\nimport CampaignPicker, {\n type CampaignPickerOption,\n} from '@aglyn/shared-ui-email-campaigns/components/campaign-picker.component'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n type FirestoreDocStatus,\n useFirestore,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport { Button, Stack } from '@mui/material'\nimport { doc, serverTimestamp, updateDoc } from 'firebase/firestore'\nimport { useEffect, useState } from 'react'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { crmClientListFields } from '../model/crm-list-query'\n\n/** The list field a campaign filing moves (AGL-3321). */\nconst LEAD_CAMPAIGN_LIST_FIELDS = ['scopedCampaignIds'] as const\n\n/** The helper line under the picker — the contact card's, word for word. */\nexport const LEAD_CAMPAIGNS_HELPER_TEXT =\n 'Your own filing. It never adds anyone to a send — a campaign mails its lists.'\n\n/**\n * The campaigns a lead is filed under, as NAMES for the record header.\n *\n * Resolved from the site's containers; an id with no live container — a\n * campaign the console deleted, or one still loading — is left out rather\n * than shown, because a document id on a chip tells a reader nothing and\n * looks like a fault. The picker below keeps such an id visible as itself,\n * so the filing is never silently lost; the header only ever names.\n */\nexport function leadCampaignNames(\n lead: Record<string, unknown> | null | undefined,\n options: readonly CampaignPickerOption[],\n): string[] {\n const labels = new Map(options.map((option) => [option.value, option.label]))\n return readCampaignIds(lead)\n .map((id) => labels.get(id))\n .filter((name): name is string => Boolean(name))\n}\n\nexport interface LeadCampaignsCardProps {\n /**\n * The lead's own site, or `null` at the organization level for a lead no\n * site captured (AGL-3278). Read for the org root alone — the containers\n * themselves are handed down as `options` — so the card serves both\n * levels; the org-level mount answers the same org without a site.\n */\n hostId: string | null\n leadId: string\n lead: Record<string, unknown>\n leadStatus: FirestoreDocStatus\n /** The listener has not confirmed this document with the server yet. */\n fromCache: boolean\n /** The site's campaigns, read once by the page for this card and the header. */\n options: readonly CampaignPickerOption[]\n /** The campaign read has answered — false while it settles. */\n optionsReady: boolean\n /**\n * What a saved filing changed, once it has landed: the ids added to and\n * removed from the lead's membership. The page files the activity\n * entries (AGL-3274); the card only knows the document.\n */\n onFiled?: (change: { added: string[]; removed: string[] }) => void\n}\n\n/**\n * WHICH CAMPAIGNS THIS LEAD IS FILED UNDER (AGL-3274).\n *\n * The contact's Relationship card has carried this picker since AGL-2596;\n * the lead's page had none, so a lead filed under a campaign by the New\n * lead drawer, the bulk bar or a sequence's enroll showed nothing of it —\n * the one field on the record the reader had no way to see or change.\n *\n * The same control, the same value helper, the same helper line. The save\n * is the lead's own client door — a one-field `update` on\n * `hosts/{hostId}/leads`, as the properties card writes status and notes —\n * guarded by the seed like every draft edited over a cached read, and\n * `campaignMembershipUnchanged` keeps Save quiet while the picker only\n * reorders what is stored. **Save filing** sits in the card header.\n *\n * It is FILING, not audience: the helper says so, in the sentence the\n * contact card uses, because a reader who finds a campaign picker on a\n * person's page assumes the opposite.\n */\nexport function LeadCampaignsCard(props: LeadCampaignsCardProps) {\n const { hostId, leadId, lead, leadStatus, fromCache, options, optionsReady, onFiled } = props\n // The lead's collection is the org's now (AGL-3275); the site still names\n // the surface, and the scope hook resolves the org from it.\n const { orgId } = useCrmScope({ hostId })\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const stored = readCampaignIds(lead)\n const [selected, setSelected] = useState<string[]>(stored)\n const [dirty, setDirty] = useState(false)\n const [saving, setSaving] = useState(false)\n // A newer membership from the server replaces an UNEDITED selection — a\n // sequence's enroll or a bulk add lands while the page is open — and an\n // edited one is the reader's, decided by the guard on save.\n const storedKey = stored.join(',')\n useEffect(() => {\n if (!dirty) setSelected(stored)\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [storedKey, dirty])\n\n const unchanged = campaignMembershipUnchanged(stored, selected)\n const empty = optionsReady && !options.length\n\n const save = async () => {\n if (unchanged) return\n if (!orgId) {\n enqueueSnackbar('Still loading this workspace — try again in a moment.', {\n variant: 'warning',\n persist: false,\n })\n return\n }\n setSaving(true)\n const next = campaignMembershipValue(selected)\n const verdict = await writeGuardedBySeed(\n { subject: 'lead', fromCache, unreadable: leadStatus === 'error' },\n async () => {\n await updateDoc(doc(firestore, 'orgs', orgId, 'leads', leadId), {\n campaignIds: next,\n // The Leads list's Campaign filter under a site reads these\n // (AGL-3321), from the lead with its new campaigns.\n ...crmClientListFields('leads', lead, { campaignIds: next }, LEAD_CAMPAIGN_LIST_FIELDS),\n updatedAt: serverTimestamp(),\n })\n },\n )\n setSaving(false)\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message ?? 'The filing could not be saved.', { variant: 'warning' })\n return\n }\n setDirty(false)\n enqueueSnackbar('Filing saved', { variant: 'success', persist: false })\n onFiled?.({\n added: next.filter((id) => !stored.includes(id)),\n removed: stored.filter((id) => !next.includes(id)),\n })\n }\n\n return (\n <CardDisplay\n header={'Campaigns'}\n help={pluginDocsHelp('crmLeads', { anchor: '#a-leads-page' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: empty ? null : (\n <Button\n size=\"small\"\n variant=\"outlined\"\n disabled={saving || unchanged}\n onClick={() => void save().catch((error: unknown) => {\n console.error(error)\n enqueueSnackbar(\n error instanceof Error && error.message ? error.message : 'An error has occurred',\n { variant: 'error', allowDuplicate: true },\n )\n setSaving(false)\n })}\n >\n {saving ? 'Saving…' : 'Save filing'}\n </Button>\n ),\n }}\n >\n <Stack spacing={2}>\n <CampaignPicker\n options={options}\n value={selected}\n onChange={(next) => {\n setSelected(next)\n setDirty(true)\n }}\n label=\"Filed under campaigns\"\n helperText={LEAD_CAMPAIGNS_HELPER_TEXT}\n disabled={saving}\n empty={empty}\n emptyText=\"There are no campaigns to file this lead under yet. Create one from Marketing.\"\n />\n </Stack>\n </CardDisplay>\n )\n}\nLeadCampaignsCard.displayName = 'LeadCampaignsCard'\n\nexport default LeadCampaignsCard\n"],"names":["campaignMembershipUnchanged","campaignMembershipValue","pluginDocsHelp","readCampaignIds","CampaignPicker","CardDisplay","useSnackbar","useFirestore","writeGuardedBySeed","Button","Stack","doc","serverTimestamp","updateDoc","useEffect","useState","useCrmScope","crmClientListFields","LEAD_CAMPAIGN_LIST_FIELDS","LEAD_CAMPAIGNS_HELPER_TEXT","leadCampaignNames","lead","options","labels","Map","map","option","value","label","id","get","filter","name","Boolean","LeadCampaignsCard","props","hostId","leadId","leadStatus","fromCache","optionsReady","onFiled","orgId","firestore","enqueueSnackbar","stored","selected","setSelected","dirty","setDirty","saving","setSaving","storedKey","join","unchanged","empty","length","save","variant","persist","next","verdict","subject","unreadable","campaignIds","updatedAt","ok","message","added","includes","removed","header","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","size","disabled","onClick","catch","error","console","Error","allowDuplicate","spacing","onChange","helperText","emptyText","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-campaigns-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n containerMembershipUnchanged,\n containerMembershipValue,\n pluginDocsHelp,\n readContainerIds,\n} from '@aglyn/aglyn'\nimport ContainerPicker, {\n type ContainerPickerOption,\n} from '@aglyn/tenant-feature-instance/components/container-picker'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n type FirestoreDocStatus,\n useFirestore,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport { Button, Stack } from '@mui/material'\nimport { doc, serverTimestamp, updateDoc } from 'firebase/firestore'\nimport { useEffect, useState } from 'react'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { crmClientListFields } from '../model/crm-list-query'\nimport { useCrmSharingFollowUp } from '../hooks/use-crm-sharing'\n\n/** The list field a campaign filing moves (AGL-3321). */\nconst LEAD_CAMPAIGN_LIST_FIELDS = ['scopedCampaignIds'] as const\n\n/** The helper line under the picker — the contact card's, word for word. */\nexport const LEAD_CAMPAIGNS_HELPER_TEXT =\n 'Your own filing. It never adds anyone to a send — a campaign mails its lists.'\n\n/**\n * The campaigns a lead is filed under, as NAMES for the record header.\n *\n * Resolved from the site's containers; an id with no live container — a\n * campaign the console deleted, or one still loading — is left out rather\n * than shown, because a document id on a chip tells a reader nothing and\n * looks like a fault. The picker below keeps such an id visible as itself,\n * so the filing is never silently lost; the header only ever names.\n */\nexport function leadCampaignNames(\n lead: Record<string, unknown> | null | undefined,\n options: readonly ContainerPickerOption[],\n): string[] {\n const labels = new Map(options.map((option) => [option.value, option.label]))\n return readContainerIds(lead, 'campaign')\n .map((id) => labels.get(id))\n .filter((name): name is string => Boolean(name))\n}\n\nexport interface LeadCampaignsCardProps {\n /**\n * The lead's own site, or `null` at the organization level for a lead no\n * site captured (AGL-3278). Read for the org root alone — the containers\n * themselves are handed down as `options` — so the card serves both\n * levels; the org-level mount answers the same org without a site.\n */\n hostId: string | null\n leadId: string\n lead: Record<string, unknown>\n leadStatus: FirestoreDocStatus\n /** The listener has not confirmed this document with the server yet. */\n fromCache: boolean\n /** The site's campaigns, read once by the page for this card and the header. */\n options: readonly ContainerPickerOption[]\n /** The campaign read has answered — false while it settles. */\n optionsReady: boolean\n /**\n * What a saved filing changed, once it has landed: the ids added to and\n * removed from the lead's membership. The page files the activity\n * entries (AGL-3274); the card only knows the document.\n */\n onFiled?: (change: { added: string[]; removed: string[] }) => void\n}\n\n/**\n * WHICH CAMPAIGNS THIS LEAD IS FILED UNDER (AGL-3274).\n *\n * The contact's Relationship card has carried this picker since AGL-2596;\n * the lead's page had none, so a lead filed under a campaign by the New\n * lead drawer, the bulk bar or a sequence's enroll showed nothing of it —\n * the one field on the record the reader had no way to see or change.\n *\n * The same control, the same value helper, the same helper line. The save\n * is the lead's own client door — a one-field `update` on\n * `hosts/{hostId}/leads`, as the properties card writes status and notes —\n * guarded by the seed like every draft edited over a cached read, and\n * `containerMembershipUnchanged` keeps Save quiet while the picker only\n * reorders what is stored. **Save filing** sits in the card header.\n *\n * It is FILING, not audience: the helper says so, in the sentence the\n * contact card uses, because a reader who finds a campaign picker on a\n * person's page assumes the opposite.\n */\nexport function LeadCampaignsCard(props: LeadCampaignsCardProps) {\n const { hostId, leadId, lead, leadStatus, fromCache, options, optionsReady, onFiled } = props\n // The lead's collection is the org's now (AGL-3275); the site still names\n // the surface, and the scope hook resolves the org from it.\n const { orgId } = useCrmScope({ hostId })\n const followUpSharing = useCrmSharingFollowUp(hostId, orgId)\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const stored = readContainerIds(lead, 'campaign')\n const [selected, setSelected] = useState<string[]>(stored)\n const [dirty, setDirty] = useState(false)\n const [saving, setSaving] = useState(false)\n // A newer membership from the server replaces an UNEDITED selection — a\n // sequence's enroll or a bulk add lands while the page is open — and an\n // edited one is the reader's, decided by the guard on save.\n const storedKey = stored.join(',')\n useEffect(() => {\n if (!dirty) setSelected(stored)\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [storedKey, dirty])\n\n const unchanged = containerMembershipUnchanged(stored, selected)\n const empty = optionsReady && !options.length\n\n const save = async () => {\n if (unchanged) return\n if (!orgId) {\n enqueueSnackbar('Still loading this workspace — try again in a moment.', {\n variant: 'warning',\n persist: false,\n })\n return\n }\n setSaving(true)\n const next = containerMembershipValue(selected)\n const verdict = await writeGuardedBySeed(\n { subject: 'lead', fromCache, unreadable: leadStatus === 'error' },\n async () => {\n await updateDoc(doc(firestore, 'orgs', orgId, 'leads', leadId), {\n campaignIds: next,\n // The Leads list's Campaign filter under a site reads these\n // (AGL-3321), from the lead with its new campaigns.\n ...crmClientListFields('leads', lead, { campaignIds: next }, LEAD_CAMPAIGN_LIST_FIELDS),\n updatedAt: serverTimestamp(),\n })\n },\n )\n setSaving(false)\n // A rule may share by campaign (AGL-3336).\n if (verdict.ok) followUpSharing('leads', [leadId])\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message ?? 'The filing could not be saved.', { variant: 'warning' })\n return\n }\n setDirty(false)\n enqueueSnackbar('Filing saved', { variant: 'success', persist: false })\n onFiled?.({\n added: next.filter((id) => !stored.includes(id)),\n removed: stored.filter((id) => !next.includes(id)),\n })\n }\n\n return (\n <CardDisplay\n header={'Campaigns'}\n help={pluginDocsHelp('crmLeads', { anchor: '#a-leads-page' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: empty ? null : (\n <Button\n size=\"small\"\n variant=\"outlined\"\n disabled={saving || unchanged}\n onClick={() => void save().catch((error: unknown) => {\n console.error(error)\n enqueueSnackbar(\n error instanceof Error && error.message ? error.message : 'An error has occurred',\n { variant: 'error', allowDuplicate: true },\n )\n setSaving(false)\n })}\n >\n {saving ? 'Saving…' : 'Save filing'}\n </Button>\n ),\n }}\n >\n <Stack spacing={2}>\n <ContainerPicker\n kind=\"campaign\"\n options={options}\n value={selected}\n onChange={(next) => {\n setSelected(next)\n setDirty(true)\n }}\n label=\"Filed under campaigns\"\n helperText={LEAD_CAMPAIGNS_HELPER_TEXT}\n disabled={saving}\n empty={empty}\n emptyText=\"There are no campaigns to file this lead under yet. Create one from Marketing.\"\n />\n </Stack>\n </CardDisplay>\n )\n}\nLeadCampaignsCard.displayName = 'LeadCampaignsCard'\n\nexport default LeadCampaignsCard\n"],"names":["containerMembershipUnchanged","containerMembershipValue","pluginDocsHelp","readContainerIds","ContainerPicker","CardDisplay","useSnackbar","useFirestore","writeGuardedBySeed","Button","Stack","doc","serverTimestamp","updateDoc","useEffect","useState","useCrmScope","crmClientListFields","useCrmSharingFollowUp","LEAD_CAMPAIGN_LIST_FIELDS","LEAD_CAMPAIGNS_HELPER_TEXT","leadCampaignNames","lead","options","labels","Map","map","option","value","label","id","get","filter","name","Boolean","LeadCampaignsCard","props","hostId","leadId","leadStatus","fromCache","optionsReady","onFiled","orgId","followUpSharing","firestore","enqueueSnackbar","stored","selected","setSelected","dirty","setDirty","saving","setSaving","storedKey","join","unchanged","empty","length","save","variant","persist","next","verdict","subject","unreadable","campaignIds","updatedAt","ok","message","added","includes","removed","header","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","size","disabled","onClick","catch","error","console","Error","allowDuplicate","spacing","kind","onChange","helperText","emptyText","displayName"],"mappings":"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"}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ 'use client';
|
|
17
17
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
18
|
-
import { pluginDocsHelp,
|
|
18
|
+
import { pluginDocsHelp, normalizeContactEmail, readErasureRequestedAtMs } from "@aglyn/aglyn";
|
|
19
19
|
import { useFirestore, useFirestoreDoc } from "@aglyn/tenant-feature-instance";
|
|
20
20
|
import { Stack, Typography } from "@mui/material";
|
|
21
21
|
import { doc } from "firebase/firestore";
|
|
@@ -35,6 +35,9 @@ import { LeadHistoryCard } from "./lead-history-card.js";
|
|
|
35
35
|
import { LeadPropertiesCard } from "./lead-properties-card.js";
|
|
36
36
|
import { LeadUnqualifyDialog } from "./lead-unqualify-dialog.js";
|
|
37
37
|
import { RecordActivityCard } from "./record-activity-card.js";
|
|
38
|
+
import { CrmShareChipView, RecordSharingCard } from "./record-sharing-card.js";
|
|
39
|
+
import { crmShareChipFor } from "../model/crm-sharing.js";
|
|
40
|
+
import { leadPrimaryGroup } from "../model/contact-holder.js";
|
|
38
41
|
/**
|
|
39
42
|
* `/crm/leads/{leadId}` — one lead (AGL-2608).
|
|
40
43
|
*
|
|
@@ -58,6 +61,7 @@ import { RecordActivityCard } from "./record-activity-card.js";
|
|
|
58
61
|
* the same answer `contactPrimaryGroup` gives the contact page, and the
|
|
59
62
|
* same one the Leads list already converts a row as.
|
|
60
63
|
*/ export function LeadDetailPage(props) {
|
|
64
|
+
var _ref;
|
|
61
65
|
const { id, hostId, org, basePath } = props;
|
|
62
66
|
const firestore = useFirestore();
|
|
63
67
|
const routes = crmRoutes(basePath);
|
|
@@ -186,10 +190,17 @@ import { RecordActivityCard } from "./record-activity-card.js";
|
|
|
186
190
|
// The campaigns the lead is filed under (AGL-3274), by name
|
|
187
191
|
// beside the status. An id no container answers for draws no
|
|
188
192
|
// chip: the card below keeps it, the header only names.
|
|
189
|
-
extraChips:
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
+
extraChips: [
|
|
194
|
+
// Seen here only through a share (AGL-3336): who shared it.
|
|
195
|
+
/*#__PURE__*/ _jsx(CrmShareChipView, {
|
|
196
|
+
chip: crmShareChipFor(lead, (_ref = viewingGroup == null ? void 0 : viewingGroup.hostIds) != null ? _ref : []),
|
|
197
|
+
org: org
|
|
198
|
+
}, "shared"),
|
|
199
|
+
...leadCampaignNames(lead, campaigns.options).map((name)=>/*#__PURE__*/ _jsx(CrmRecordChip, {
|
|
200
|
+
label: "Campaign",
|
|
201
|
+
value: name
|
|
202
|
+
}, name))
|
|
203
|
+
]
|
|
193
204
|
}),
|
|
194
205
|
/*#__PURE__*/ _jsx(CrmRecordInsightsZone, {
|
|
195
206
|
hostId: siteHostId,
|
|
@@ -228,6 +239,15 @@ import { RecordActivityCard } from "./record-activity-card.js";
|
|
|
228
239
|
leadId: id,
|
|
229
240
|
lead: lead
|
|
230
241
|
}),
|
|
242
|
+
/*#__PURE__*/ _jsx(RecordSharingCard, {
|
|
243
|
+
object: "leads",
|
|
244
|
+
id: id,
|
|
245
|
+
record: lead,
|
|
246
|
+
hostId: hostId != null ? hostId : null,
|
|
247
|
+
orgId: orgId,
|
|
248
|
+
org: org,
|
|
249
|
+
viewingHostIds: viewingGroup == null ? void 0 : viewingGroup.hostIds
|
|
250
|
+
}),
|
|
231
251
|
/*#__PURE__*/ _jsx(RecordActivityCard, {
|
|
232
252
|
hostId: siteHostId,
|
|
233
253
|
org: org,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-detail-page.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n pluginDocsHelp,\n type CrmLeadFields,\n leadPrimaryGroup,\n normalizeContactEmail,\n readErasureRequestedAtMs,\n} from '@aglyn/aglyn'\nimport { useFirestore, useFirestoreDoc } from '@aglyn/tenant-feature-instance'\nimport { Stack, Typography } from '@mui/material'\nimport { doc } from 'firebase/firestore'\nimport { useMemo, useState } from 'react'\nimport { useCampaignFilingLog } from '../hooks/use-campaign-filing-log'\nimport { useCrmCampaigns } from '../hooks/use-crm-campaigns'\nimport { CrmCreateSiteDefault } from '../hooks/use-crm-org-mount'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { useOrgMemberOptions } from '../hooks/use-org-member-options'\nimport { type CrmDetailPageProps, crmRoutes } from '../model/crm-routes'\nimport { CrmRecordChip, CrmRecordHeader } from './crm-record-header'\nimport { CrmRecordInsightsZone } from './crm-record-insights-zone'\nimport { useErasePersonAction } from './erase-person-action'\nimport { LeadCampaignsCard, leadCampaignNames } from './lead-campaigns-card'\nimport { LeadConvertDialog } from './lead-convert-dialog'\nimport { LeadHistoryCard } from './lead-history-card'\nimport { LeadPropertiesCard } from './lead-properties-card'\nimport { LeadUnqualifyDialog } from './lead-unqualify-dialog'\nimport { RecordActivityCard } from './record-activity-card'\n\ntype LeadDocument = Record<string, unknown> & CrmLeadFields\n\n/**\n * `/crm/leads/{leadId}` — one lead (AGL-2608).\n *\n * The record behind a row of the Leads section: what the person did on the\n * site, who is working them, and the conversion that turns them into a\n * contact, a company and a deal. One document listen and one roster request\n * for the owner picker; the convert dialog's reads open only when it does.\n *\n * ## The two levels, and the site a lead is acted on as (AGL-3278)\n *\n * The org root comes from `useCrmScope`, as it does on every other CRM\n * record page: under a site it is looked up from that site, and at the\n * organization level it is the mount's, known at once. It used to come\n * from `useOrgDataScope({ hostId })` — correct only under a site, because\n * an absent host settles at `orgId: null`, and a null org builds no\n * reference, opens no listener and leaves the page loading forever.\n *\n * Several surfaces still need ONE site: the feed an activity is filed in, the booking door, the consent basis, the\n * conversion. Under a site that is the mounted one. At the organization\n * level it is the lead's own first capturing site — `leadPrimaryGroup` —\n * the same answer `contactPrimaryGroup` gives the contact page, and the\n * same one the Leads list already converts a row as.\n */\nexport function LeadDetailPage(props: CrmDetailPageProps) {\n const { id, hostId, org, basePath } = props\n const firestore = useFirestore()\n const routes = crmRoutes(basePath)\n /*\n * Resolved BEFORE the read that needs it (AGL-3275). A lead is an org row\n * now, so the org has to be known to address one — and `scope` is null\n * while the host-index lookup settles. Handing that null to `doc()` is a\n * thrown `TypeError`, which the page renders as a 500 rather than as a\n * record still loading.\n */\n const { scope, orgId, ready, consentGroup: viewingGroup } = useCrmScope({\n hostId,\n org,\n })\n const {\n data: lead,\n status,\n fromCache,\n } = useFirestoreDoc<LeadDocument>(\n () => (scope ? doc(firestore, scope[0], scope[1], 'leads', id) : null),\n [firestore, scope, id],\n )\n /*\n * The group this lead is READ through: the mounted site's under a site,\n * and the lead's own first capturing site at the organization level\n * (AGL-3278). The consent basis is read against it — a refusal recorded\n * for any sibling in a declared group stands against every one of them,\n * so a group of one would report a person as opted in who had\n * unsubscribed from the brand beside this one.\n */\n const leadGroup = useMemo(\n () => viewingGroup ?? leadPrimaryGroup(lead, org as Record<string, unknown>),\n [viewingGroup, lead, org],\n )\n /** The one site this page acts as, or null for a lead no site captured. */\n const siteHostId = hostId ?? (leadGroup.hostId || null)\n const roster = useOrgMemberOptions(orgId)\n /*\n * The campaigns, read once for the page (AGL-3274): the header names the\n * ones the lead is filed under and the Campaigns card offers them. One\n * listener, not one per surface. Under a site, the campaigns placed on\n * it; at the organization level the lead is the org's record and so is\n * every campaign it may be filed under.\n */\n const campaigns = useCrmCampaigns({ hostId, orgId }, { enabled: true })\n // A saved filing is written on the lead's Activity too (AGL-3274), one\n // entry per campaign added or removed, by the member who saved it.\n const logFiling = useCampaignFilingLog({\n orgId,\n hostId: siteHostId,\n org: org as Record<string, unknown> | undefined,\n })\n const [converting, setConverting] = useState(false)\n const [unqualifying, setUnqualifying] = useState(false)\n // The privacy erasure (AGL-2623), offered from the lead as from the\n // contact: the same request, filed by the lead's address.\n const leadEmail = lead ? normalizeContactEmail(lead['email']) : null\n const erase = useErasePersonAction({\n hostId: siteHostId,\n orgId,\n subject: lead && leadEmail ? { kind: 'lead', id, email: leadEmail } : null,\n requestedAtMs: readErasureRequestedAtMs(lead),\n })\n\n const label = lead ? String(lead['name'] || lead['email'] || id) : undefined\n\n /*\n * A SETTLED lookup with no org is an answer, not a wait (AGL-3278). The\n * spinner below is for a read in flight; standing on it once `ready` has\n * said there is no org to read from is the endless \"Loading…\" this page\n * shipped with.\n */\n const noOrg = ready && !scope\n if (noOrg || status === 'error' || (status === 'success' && !lead)) {\n return (\n <CrmRecordHeader\n kind=\"Lead\"\n title={undefined}\n help={pluginDocsHelp('crmLeads', { anchor: '#a-leads-page' })}\n backHref={routes.section('leads')}\n backLabel=\"Back to leads\"\n >\n <Typography variant=\"body2\" color=\"text.secondary\">\n {noOrg || status === 'error'\n ? 'This lead could not be read.'\n : 'This lead no longer exists — it may have been removed from the Inbox.'}\n </Typography>\n </CrmRecordHeader>\n )\n }\n if (!lead) {\n return (\n <CrmRecordHeader\n kind=\"Lead\"\n title={undefined}\n help={pluginDocsHelp('crmLeads', { anchor: '#a-leads-page' })}\n backHref={routes.section('leads')}\n backLabel=\"Back to leads\"\n loading\n />\n )\n }\n\n return (\n /* A task or an activity created from this page defaults to the lead's\n own site, not to the site the reader last picked in a list's drawer\n (AGL-2630) — as on every other record page. */\n <CrmCreateSiteDefault hostId={siteHostId}>\n {/* The properties card is the record's lead card: it publishes the\n page heading and the trail, so the history card under it says what\n it holds rather than repeating the name. */}\n <Stack spacing={3}>\n <LeadPropertiesCard\n hostId={siteHostId}\n consentGroup={leadGroup}\n orgId={orgId}\n leadId={id}\n lead={lead}\n leadStatus={status}\n fromCache={fromCache}\n basePath={basePath}\n roster={roster}\n onConvert={() => setConverting(true)}\n onUnqualify={() => setUnqualifying(true)}\n extraMenuItems={erase.menuItems}\n banner={erase.banner}\n erasurePending={erase.pendingSinceMs !== null}\n org={org}\n // The campaigns the lead is filed under (AGL-3274), by name\n // beside the status. An id no container answers for draws no\n // chip: the card below keeps it, the header only names.\n extraChips={leadCampaignNames(lead, campaigns.options).map((name) => (\n <CrmRecordChip key={name} label=\"Campaign\" value={name} />\n ))}\n />\n {/* What an assistant says about where the lead stands (AGL-2917): read on its own site. */}\n <CrmRecordInsightsZone\n hostId={siteHostId}\n org={org as Record<string, unknown> | undefined}\n kind=\"lead\"\n recordId={id}\n name={label ?? ''}\n />\n <LeadCampaignsCard\n hostId={siteHostId}\n leadId={id}\n lead={lead}\n leadStatus={status}\n fromCache={fromCache}\n options={campaigns.options}\n optionsReady={campaigns.ready}\n onFiled={({ added, removed }) => {\n const named = (ids: string[]) =>\n ids.map((campaignId) => ({\n id: campaignId,\n name: campaigns.options.find((option) => option.value === campaignId)?.label ?? '',\n }))\n void logFiling({ leadId: id }, { filed: named(added), removed: named(removed) })\n }}\n />\n <LeadHistoryCard hostId={siteHostId} leadId={id} lead={lead} />\n <RecordActivityCard hostId={siteHostId} org={org} leadId={id} />\n </Stack>\n <LeadConvertDialog\n open={converting}\n onClose={() => setConverting(false)}\n hostId={siteHostId}\n orgId={orgId}\n org={org as Record<string, unknown> | undefined}\n leadId={id}\n lead={lead}\n basePath={basePath}\n roster={roster}\n />\n <LeadUnqualifyDialog\n open={unqualifying}\n onClose={() => setUnqualifying(false)}\n hostId={siteHostId}\n leadId={id}\n leadLabel={label ?? id}\n />\n {erase.dialog}\n </CrmCreateSiteDefault>\n )\n}\nLeadDetailPage.displayName = 'LeadDetailPage'\n\nexport default LeadDetailPage\n"],"names":["pluginDocsHelp","leadPrimaryGroup","normalizeContactEmail","readErasureRequestedAtMs","useFirestore","useFirestoreDoc","Stack","Typography","doc","useMemo","useState","useCampaignFilingLog","useCrmCampaigns","CrmCreateSiteDefault","useCrmScope","useOrgMemberOptions","crmRoutes","CrmRecordChip","CrmRecordHeader","CrmRecordInsightsZone","useErasePersonAction","LeadCampaignsCard","leadCampaignNames","LeadConvertDialog","LeadHistoryCard","LeadPropertiesCard","LeadUnqualifyDialog","RecordActivityCard","LeadDetailPage","props","id","hostId","org","basePath","firestore","routes","scope","orgId","ready","consentGroup","viewingGroup","data","lead","status","fromCache","leadGroup","siteHostId","roster","campaigns","enabled","logFiling","converting","setConverting","unqualifying","setUnqualifying","leadEmail","erase","subject","kind","email","requestedAtMs","label","String","undefined","noOrg","title","help","anchor","backHref","section","backLabel","variant","color","loading","spacing","leadId","leadStatus","onConvert","onUnqualify","extraMenuItems","menuItems","banner","erasurePending","pendingSinceMs","extraChips","options","map","name","value","recordId","optionsReady","onFiled","added","removed","named","ids","campaignId","find","option","filed","open","onClose","leadLabel","dialog","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-detail-page.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n pluginDocsHelp,\n type CrmLeadFields,\n normalizeContactEmail,\n readErasureRequestedAtMs,\n} from '@aglyn/aglyn'\nimport { useFirestore, useFirestoreDoc } from '@aglyn/tenant-feature-instance'\nimport { Stack, Typography } from '@mui/material'\nimport { doc } from 'firebase/firestore'\nimport { useMemo, useState } from 'react'\nimport { useCampaignFilingLog } from '../hooks/use-campaign-filing-log'\nimport { useCrmCampaigns } from '../hooks/use-crm-campaigns'\nimport { CrmCreateSiteDefault } from '../hooks/use-crm-org-mount'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { useOrgMemberOptions } from '../hooks/use-org-member-options'\nimport { type CrmDetailPageProps, crmRoutes } from '../model/crm-routes'\nimport { CrmRecordChip, CrmRecordHeader } from './crm-record-header'\nimport { CrmRecordInsightsZone } from './crm-record-insights-zone'\nimport { useErasePersonAction } from './erase-person-action'\nimport { LeadCampaignsCard, leadCampaignNames } from './lead-campaigns-card'\nimport { LeadConvertDialog } from './lead-convert-dialog'\nimport { LeadHistoryCard } from './lead-history-card'\nimport { LeadPropertiesCard } from './lead-properties-card'\nimport { LeadUnqualifyDialog } from './lead-unqualify-dialog'\nimport { RecordActivityCard } from './record-activity-card'\nimport { CrmShareChipView, RecordSharingCard } from './record-sharing-card'\nimport { crmShareChipFor } from '../model/crm-sharing'\nimport { leadPrimaryGroup } from '../model/contact-holder'\n\ntype LeadDocument = Record<string, unknown> & CrmLeadFields\n\n/**\n * `/crm/leads/{leadId}` — one lead (AGL-2608).\n *\n * The record behind a row of the Leads section: what the person did on the\n * site, who is working them, and the conversion that turns them into a\n * contact, a company and a deal. One document listen and one roster request\n * for the owner picker; the convert dialog's reads open only when it does.\n *\n * ## The two levels, and the site a lead is acted on as (AGL-3278)\n *\n * The org root comes from `useCrmScope`, as it does on every other CRM\n * record page: under a site it is looked up from that site, and at the\n * organization level it is the mount's, known at once. It used to come\n * from `useOrgDataScope({ hostId })` — correct only under a site, because\n * an absent host settles at `orgId: null`, and a null org builds no\n * reference, opens no listener and leaves the page loading forever.\n *\n * Several surfaces still need ONE site: the feed an activity is filed in, the booking door, the consent basis, the\n * conversion. Under a site that is the mounted one. At the organization\n * level it is the lead's own first capturing site — `leadPrimaryGroup` —\n * the same answer `contactPrimaryGroup` gives the contact page, and the\n * same one the Leads list already converts a row as.\n */\nexport function LeadDetailPage(props: CrmDetailPageProps) {\n const { id, hostId, org, basePath } = props\n const firestore = useFirestore()\n const routes = crmRoutes(basePath)\n /*\n * Resolved BEFORE the read that needs it (AGL-3275). A lead is an org row\n * now, so the org has to be known to address one — and `scope` is null\n * while the host-index lookup settles. Handing that null to `doc()` is a\n * thrown `TypeError`, which the page renders as a 500 rather than as a\n * record still loading.\n */\n const { scope, orgId, ready, consentGroup: viewingGroup } = useCrmScope({\n hostId,\n org,\n })\n const {\n data: lead,\n status,\n fromCache,\n } = useFirestoreDoc<LeadDocument>(\n () => (scope ? doc(firestore, scope[0], scope[1], 'leads', id) : null),\n [firestore, scope, id],\n )\n /*\n * The group this lead is READ through: the mounted site's under a site,\n * and the lead's own first capturing site at the organization level\n * (AGL-3278). The consent basis is read against it — a refusal recorded\n * for any sibling in a declared group stands against every one of them,\n * so a group of one would report a person as opted in who had\n * unsubscribed from the brand beside this one.\n */\n const leadGroup = useMemo(\n () => viewingGroup ?? leadPrimaryGroup(lead, org as Record<string, unknown>),\n [viewingGroup, lead, org],\n )\n /** The one site this page acts as, or null for a lead no site captured. */\n const siteHostId = hostId ?? (leadGroup.hostId || null)\n const roster = useOrgMemberOptions(orgId)\n /*\n * The campaigns, read once for the page (AGL-3274): the header names the\n * ones the lead is filed under and the Campaigns card offers them. One\n * listener, not one per surface. Under a site, the campaigns placed on\n * it; at the organization level the lead is the org's record and so is\n * every campaign it may be filed under.\n */\n const campaigns = useCrmCampaigns({ hostId, orgId }, { enabled: true })\n // A saved filing is written on the lead's Activity too (AGL-3274), one\n // entry per campaign added or removed, by the member who saved it.\n const logFiling = useCampaignFilingLog({\n orgId,\n hostId: siteHostId,\n org: org as Record<string, unknown> | undefined,\n })\n const [converting, setConverting] = useState(false)\n const [unqualifying, setUnqualifying] = useState(false)\n // The privacy erasure (AGL-2623), offered from the lead as from the\n // contact: the same request, filed by the lead's address.\n const leadEmail = lead ? normalizeContactEmail(lead['email']) : null\n const erase = useErasePersonAction({\n hostId: siteHostId,\n orgId,\n subject: lead && leadEmail ? { kind: 'lead', id, email: leadEmail } : null,\n requestedAtMs: readErasureRequestedAtMs(lead),\n })\n\n const label = lead ? String(lead['name'] || lead['email'] || id) : undefined\n\n /*\n * A SETTLED lookup with no org is an answer, not a wait (AGL-3278). The\n * spinner below is for a read in flight; standing on it once `ready` has\n * said there is no org to read from is the endless \"Loading…\" this page\n * shipped with.\n */\n const noOrg = ready && !scope\n if (noOrg || status === 'error' || (status === 'success' && !lead)) {\n return (\n <CrmRecordHeader\n kind=\"Lead\"\n title={undefined}\n help={pluginDocsHelp('crmLeads', { anchor: '#a-leads-page' })}\n backHref={routes.section('leads')}\n backLabel=\"Back to leads\"\n >\n <Typography variant=\"body2\" color=\"text.secondary\">\n {noOrg || status === 'error'\n ? 'This lead could not be read.'\n : 'This lead no longer exists — it may have been removed from the Inbox.'}\n </Typography>\n </CrmRecordHeader>\n )\n }\n if (!lead) {\n return (\n <CrmRecordHeader\n kind=\"Lead\"\n title={undefined}\n help={pluginDocsHelp('crmLeads', { anchor: '#a-leads-page' })}\n backHref={routes.section('leads')}\n backLabel=\"Back to leads\"\n loading\n />\n )\n }\n\n return (\n /* A task or an activity created from this page defaults to the lead's\n own site, not to the site the reader last picked in a list's drawer\n (AGL-2630) — as on every other record page. */\n <CrmCreateSiteDefault hostId={siteHostId}>\n {/* The properties card is the record's lead card: it publishes the\n page heading and the trail, so the history card under it says what\n it holds rather than repeating the name. */}\n <Stack spacing={3}>\n <LeadPropertiesCard\n hostId={siteHostId}\n consentGroup={leadGroup}\n orgId={orgId}\n leadId={id}\n lead={lead}\n leadStatus={status}\n fromCache={fromCache}\n basePath={basePath}\n roster={roster}\n onConvert={() => setConverting(true)}\n onUnqualify={() => setUnqualifying(true)}\n extraMenuItems={erase.menuItems}\n banner={erase.banner}\n erasurePending={erase.pendingSinceMs !== null}\n org={org}\n // The campaigns the lead is filed under (AGL-3274), by name\n // beside the status. An id no container answers for draws no\n // chip: the card below keeps it, the header only names.\n extraChips={[\n // Seen here only through a share (AGL-3336): who shared it.\n <CrmShareChipView\n key=\"shared\"\n chip={crmShareChipFor(lead, viewingGroup?.hostIds ?? [])}\n org={org}\n />,\n ...leadCampaignNames(lead, campaigns.options).map((name) => (\n <CrmRecordChip key={name} label=\"Campaign\" value={name} />\n )),\n ]}\n />\n {/* What an assistant says about where the lead stands (AGL-2917): read on its own site. */}\n <CrmRecordInsightsZone\n hostId={siteHostId}\n org={org as Record<string, unknown> | undefined}\n kind=\"lead\"\n recordId={id}\n name={label ?? ''}\n />\n <LeadCampaignsCard\n hostId={siteHostId}\n leadId={id}\n lead={lead}\n leadStatus={status}\n fromCache={fromCache}\n options={campaigns.options}\n optionsReady={campaigns.ready}\n onFiled={({ added, removed }) => {\n const named = (ids: string[]) =>\n ids.map((campaignId) => ({\n id: campaignId,\n name: campaigns.options.find((option) => option.value === campaignId)?.label ?? '',\n }))\n void logFiling({ leadId: id }, { filed: named(added), removed: named(removed) })\n }}\n />\n <LeadHistoryCard hostId={siteHostId} leadId={id} lead={lead} />\n {/* Where the lead is visible and why, and a manager's share (AGL-3336). */}\n <RecordSharingCard\n object=\"leads\"\n id={id}\n record={lead}\n hostId={hostId ?? null}\n orgId={orgId}\n org={org}\n viewingHostIds={viewingGroup?.hostIds}\n />\n <RecordActivityCard hostId={siteHostId} org={org} leadId={id} />\n </Stack>\n <LeadConvertDialog\n open={converting}\n onClose={() => setConverting(false)}\n hostId={siteHostId}\n orgId={orgId}\n org={org as Record<string, unknown> | undefined}\n leadId={id}\n lead={lead}\n basePath={basePath}\n roster={roster}\n />\n <LeadUnqualifyDialog\n open={unqualifying}\n onClose={() => setUnqualifying(false)}\n hostId={siteHostId}\n leadId={id}\n leadLabel={label ?? id}\n />\n {erase.dialog}\n </CrmCreateSiteDefault>\n )\n}\nLeadDetailPage.displayName = 'LeadDetailPage'\n\nexport default LeadDetailPage\n"],"names":["pluginDocsHelp","normalizeContactEmail","readErasureRequestedAtMs","useFirestore","useFirestoreDoc","Stack","Typography","doc","useMemo","useState","useCampaignFilingLog","useCrmCampaigns","CrmCreateSiteDefault","useCrmScope","useOrgMemberOptions","crmRoutes","CrmRecordChip","CrmRecordHeader","CrmRecordInsightsZone","useErasePersonAction","LeadCampaignsCard","leadCampaignNames","LeadConvertDialog","LeadHistoryCard","LeadPropertiesCard","LeadUnqualifyDialog","RecordActivityCard","CrmShareChipView","RecordSharingCard","crmShareChipFor","leadPrimaryGroup","LeadDetailPage","props","id","hostId","org","basePath","firestore","routes","scope","orgId","ready","consentGroup","viewingGroup","data","lead","status","fromCache","leadGroup","siteHostId","roster","campaigns","enabled","logFiling","converting","setConverting","unqualifying","setUnqualifying","leadEmail","erase","subject","kind","email","requestedAtMs","label","String","undefined","noOrg","title","help","anchor","backHref","section","backLabel","variant","color","loading","spacing","leadId","leadStatus","onConvert","onUnqualify","extraMenuItems","menuItems","banner","erasurePending","pendingSinceMs","extraChips","chip","hostIds","options","map","name","value","recordId","optionsReady","onFiled","added","removed","named","ids","campaignId","find","option","filed","object","record","viewingHostIds","open","onClose","leadLabel","dialog","displayName"],"mappings":"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"}
|
|
@@ -39,6 +39,7 @@ import { LeadOwnerSelect } from "./lead-owner-select.js";
|
|
|
39
39
|
import { LeadSourceSelect } from "./lead-source-select.js";
|
|
40
40
|
import { LeadStatusChip } from "./lead-status-chip.js";
|
|
41
41
|
import { crmClientListFields, CRM_CLIENT_SEARCH_FIELDS } from "../model/crm-list-query.js";
|
|
42
|
+
import { useCrmSharingFollowUp } from "../hooks/use-crm-sharing.js";
|
|
42
43
|
/** The Leads list's fields a profile save rewrites; the verdict key is the server's. */ const LEAD_CLIENT_LIST_FIELDS = [
|
|
43
44
|
...CRM_CLIENT_SEARCH_FIELDS,
|
|
44
45
|
'leadSourceKey'
|
|
@@ -113,6 +114,8 @@ const TEXT_MAX = Aglyn.CRM_LEAD_TEXT_MAX;
|
|
|
113
114
|
const firestore = useFirestore();
|
|
114
115
|
const { enqueueSnackbar } = useSnackbar();
|
|
115
116
|
const routes = crmRoutes(basePath);
|
|
117
|
+
// A client-direct write owes the org's sharing rules a re-evaluation (AGL-3336).
|
|
118
|
+
const followUpSharing = useCrmSharingFollowUp(hostId, orgId);
|
|
116
119
|
/*
|
|
117
120
|
* Built at USE, not at render (AGL-3275). A lead is an org row, so the org
|
|
118
121
|
* has to be known to address one — and `orgId` is null while the lookup
|
|
@@ -202,6 +205,9 @@ const TEXT_MAX = Aglyn.CRM_LEAD_TEXT_MAX;
|
|
|
202
205
|
await updateDoc(ref, _extends({}, fields, {
|
|
203
206
|
updatedAt: serverTimestamp()
|
|
204
207
|
}));
|
|
208
|
+
followUpSharing('leads', [
|
|
209
|
+
leadId
|
|
210
|
+
]);
|
|
205
211
|
enqueueSnackbar(done, {
|
|
206
212
|
variant: 'success',
|
|
207
213
|
persist: false
|
|
@@ -493,6 +499,10 @@ const TEXT_MAX = Aglyn.CRM_LEAD_TEXT_MAX;
|
|
|
493
499
|
value: "new",
|
|
494
500
|
children: Aglyn.CRM_LEAD_STATUS_LABELS.new
|
|
495
501
|
}),
|
|
502
|
+
/*#__PURE__*/ _jsx(MenuItem, {
|
|
503
|
+
value: "nurturing",
|
|
504
|
+
children: Aglyn.CRM_LEAD_STATUS_LABELS.nurturing
|
|
505
|
+
}),
|
|
496
506
|
/*#__PURE__*/ _jsx(MenuItem, {
|
|
497
507
|
value: "working",
|
|
498
508
|
children: Aglyn.CRM_LEAD_STATUS_LABELS.working
|