@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/contact-timeline-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 * as Aglyn from '@aglyn/aglyn'\nimport type {\n ContactCampaignEmail,\n ContactInteraction,\n ContactSource,\n CrmActivityRow,\n} from '@aglyn/aglyn'\nimport { CONTACT_SOURCE_LABELS, pluginDocsHelp } from '@aglyn/aglyn'\nimport {\n mdiAccountCircleOutline,\n mdiAccountEditOutline,\n mdiAccountPlusOutline,\n mdiApi,\n mdiBullhornOutline,\n mdiCalendarCheckOutline,\n mdiCartOutline,\n mdiEmailNewsletter,\n mdiFileImportOutline,\n mdiFormSelect,\n} from '@aglyn/shared-data-mdi'\nimport { AppLink, CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport EmptyStateComponent from '@aglyn/shared-ui-jsx/components/empty-state.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { Button, Chip, Stack, Tooltip, Typography } from '@mui/material'\nimport { useParams } from 'next/navigation'\nimport { useCallback, useMemo, useState } from 'react'\nimport { contactPrimaryGroup } from '../model/contact-record'\nimport { ActivityRow } from './activity-list'\nimport {\n type ActivityRecordLink,\n type CrmOrg,\n useActivityScope,\n useActivityWindow,\n useCanEditActivity,\n} from './activity-queries'\nimport {\n TimelineEntry,\n TimelineExpandAllButton,\n useTimelineExpansion,\n useTimelinePages,\n} from './activity-timeline'\nimport { CrmSuiteLockedButton } from './crm-suite-lock'\nimport { LogActivityDialog } from './log-activity-dialog'\nimport { useContactCampaignEmails } from './use-contact-campaign-emails'\n\n/**\n * One glyph per capture door. Typed against the source union so a door added\n * to it cannot render as the placeholder glyph.\n */\nconst SOURCE_ICONS: Record<ContactSource, { path: string }> = {\n form: mdiFormSelect,\n member: mdiAccountPlusOutline,\n order: mdiCartOutline,\n booking: mdiCalendarCheckOutline,\n newsletter: mdiEmailNewsletter,\n api: mdiApi,\n manual: mdiAccountEditOutline,\n import: mdiFileImportOutline,\n account: mdiAccountCircleOutline,\n}\n\n/**\n * One thing the platform recorded: the door it came through, what the door\n * wrote, when, and the page the person was on.\n *\n * Read-only by nature — nobody logged it, so nobody edits it. The entry\n * point sits beside the timestamp rather than in the summary, the way the\n * contacts drawer draws it: the summary is the sentence the door wrote, the\n * path is a fact the door recorded, and a long path on the sentence's line\n * pushes out the thing the row is about.\n *\n * Everything it holds fits the collapsed row, so it has nothing to open.\n */\nfunction CapturedRow(props: {\n interaction: ContactInteraction\n nowMs: number\n /** The console page the door's record is read on, when it left one. */\n href: string | null\n}) {\n const { interaction, nowMs, href } = props\n const source = interaction.type\n const label = CONTACT_SOURCE_LABELS[source] ?? String(source)\n const icon = SOURCE_ICONS[source]\n const when = new Date(interaction.atMs)\n return (\n <TimelineEntry\n icon={<MdiIcon path={icon?.path} />}\n chips={\n <>\n <Chip label={label} size=\"small\" />\n <Chip label=\"Captured\" size=\"small\" variant=\"outlined\" />\n </>\n }\n title={interaction.summary ?? label}\n meta={\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'baseline', flexWrap: 'wrap', rowGap: 0.25 }}\n >\n <Tooltip title={when.toLocaleString()}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {interaction.path\n ? `${Aglyn.activityTimeLabel(interaction.atMs, nowMs)} · ${interaction.path}`\n : Aglyn.activityTimeLabel(interaction.atMs, nowMs)}\n </Typography>\n </Tooltip>\n {/*\n The record the door left, where the console reads it (AGL-2622):\n the submission in the Inbox reader, the order in its dialog. A door\n that left nothing to open — a newsletter opt-in, an import — has no\n link, and the caption is the whole entry.\n */}\n {href ? (\n <Typography variant=\"caption\">\n <AppLink href={href}>\n {Aglyn.INTERACTION_LINK_LABELS[source] ?? 'Open'}\n </AppLink>\n </Typography>\n ) : null}\n </Stack>\n }\n />\n )\n}\nCapturedRow.displayName = 'CapturedRow'\n\n/**\n * One campaign email the platform sent this person (AGL-2616): which email,\n * and what became of it — `sent · delivered · opened ×2 · clicked`.\n *\n * Read-only like a captured row, and drawn apart from BOTH the captured and\n * the logged rows: its glyph is the campaign's, never the envelope a logged\n * `email` activity carries, and its chips say \"Campaign\" and \"Sent\" where\n * the others say \"Captured\" and \"Logged\". The name links to the email's own\n * report when the page can address it; an email the team has deleted, or\n * one a sibling site sent, keeps its name and loses the link.\n *\n * The delivery line stays on the collapsed row beside the name, so it has\n * nothing to open.\n */\nfunction CampaignEmailRow(props: {\n email: ContactCampaignEmail\n href: string | null\n nowMs: number\n}) {\n const { email, href, nowMs } = props\n const name = email.campaignName ?? email.subject ?? 'Campaign email'\n const summary = Aglyn.campaignEmailSummary(email).join(' · ')\n const when = new Date(email.sentAtMs)\n return (\n <TimelineEntry\n icon={<MdiIcon path={mdiBullhornOutline.path} />}\n chips={\n <>\n <Chip label=\"Campaign\" size=\"small\" />\n <Chip label=\"Sent\" size=\"small\" variant=\"outlined\" />\n </>\n }\n title={\n <>\n {href ? <AppLink href={href}>{name}</AppLink> : name}\n {` · ${summary}`}\n </>\n }\n meta={\n <Tooltip title={when.toLocaleString()}>\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ alignSelf: 'flex-start' }}\n >\n {email.subject && email.subject !== name\n ? `${Aglyn.activityTimeLabel(email.sentAtMs, nowMs)} · ${email.subject}`\n : Aglyn.activityTimeLabel(email.sentAtMs, nowMs)}\n </Typography>\n </Tooltip>\n }\n />\n )\n}\nCampaignEmailRow.displayName = 'CampaignEmailRow'\n\nexport interface ContactTimelineCardProps {\n /** The site the record is read under, or `null` at the organization level. */\n hostId: string | null\n org: CrmOrg\n contactId: string\n /**\n * The RAW contact document, as the page's listener hands it back. The card\n * resolves the viewing group and reads that group's facet itself, so a\n * page cannot hand it another holder's timeline by flattening the row\n * through the wrong group.\n */\n contact: Record<string, unknown> | null | undefined\n /**\n * Where a campaign entry links to — the email's report — or `null` for\n * one the page cannot address. Handed in rather than built here because\n * the report lives on another surface whose path is the MOUNT's to know:\n * a site's record page reaches its own Emails hub, an org-level mount\n * holds the org's site list and can reach every site's. No builder means\n * no links, never a guessed one.\n */\n campaignHref?: (email: ContactCampaignEmail) => string | null\n /**\n * The org's plan lacks the CRM (AGL-2788), which the shell mounts no CRM\n * page for (AGL-2851). The history still draws; logging an activity stands\n * locked, and one already logged cannot be edited.\n */\n suiteLocked?: boolean\n}\n\n/**\n * A person's whole history in one newest-first stream (AGL-2600): what the\n * platform CAPTURED — a form, an order, a booking, a sign-up — what a person\n * LOGGED beside it — a call, an email, a meeting, a note — and, since\n * AGL-2616, the campaign mail the platform SENT them and what became of it.\n *\n * ## Why one stream\n *\n * The histories answer one question, \"what has happened with this person\",\n * and a page showing them as separate lists asks the reader to interleave\n * them by eye. The merge is `mergeContactTimeline`, pure and specced: newest\n * first, stable at a tie, an undated entry last, a campaign at the instant\n * it was sent.\n *\n * Each entry says which it is. A captured entry carries the door's label and\n * a \"Captured\" chip and offers no controls; a logged entry carries its kind\n * and a \"Logged\" chip, and its author may edit or delete it; a campaign\n * entry carries \"Campaign\" and \"Sent\", links to the email's report, and is\n * nobody's to edit.\n *\n * ## What is read\n *\n * The captured side comes off the contact document the page already holds\n * — THIS group's facet, filtered to the sites the group may see, the same\n * projection the contacts list makes — and costs no read. The logged side is\n * the bounded listener every activity surface uses, filtered to this\n * contact, a hundred at a time. The campaign side is one request to\n * `crm/contact-email-history`, which reads down this person's own delivery\n * log, capped, and never a campaign's recipients. The facet's history is\n * capped at fifty by the platform, so past the window the stream is the\n * logged side alone.\n *\n * ## How it reads\n *\n * Every entry is a collapsed row that opens in place, paged by the\n * console's footer over the merged stream; a page turned past the logged\n * window widens it by the next hundred. **Expand all** and **Log activity**\n * sit in the card header, and edit and delete stay on the entry they act on.\n */\nexport function ContactTimelineCard(props: ContactTimelineCardProps) {\n const { hostId, org, contactId, contact, campaignHref, suiteLocked = false } = props\n const scope = useActivityScope(hostId, org)\n /*\n * THIS holder's captured history, and only the visits this group may see.\n * A read off the top of the document, or of another group's facet, would\n * be another business's records — the disclosure the facet shape ended.\n * At the organization level (AGL-2630) the holder is the person's own\n * primary one, the group the record page reads the profile through.\n */\n const consentGroup = useMemo(\n () =>\n scope.consentGroup ??\n contactPrimaryGroup(contact, (org ?? null) as Record<string, unknown> | null),\n [scope.consentGroup, contact, org],\n )\n const interactions = useMemo(() => {\n const facet = Aglyn.readContactFacet(contact, consentGroup.groupId)\n return Aglyn.interactionsForGroup(facet.interactions, consentGroup.hostIds)\n }, [contact, consentGroup])\n const link = useMemo<ActivityRecordLink>(() => ({ contactId }), [contactId])\n const activities = useActivityWindow(scope, link)\n /*\n * Where a captured entry's record is READ (AGL-2622). The site console's\n * two route params are already in the URL this page is on, so the link is\n * built from them rather than from two document reads per timeline. Only\n * an interaction on THIS site links: a sibling site's order is that\n * site's record, read on that site's console, and a link built with this\n * site's segment would 404 or — worse — open the wrong site's row under\n * the same id. An interaction with no site recorded predates the stamp\n * and is left unlinked rather than guessed at. Off a site — the org-level\n * mount — there is no segment to build with, and nothing links.\n */\n const params = useParams<{ orgSlug?: string; host?: string }>()\n const siteContext = useMemo(\n () =>\n params?.orgSlug && params?.host\n ? { orgSlug: String(params.orgSlug), host: String(params.host) }\n : null,\n [params?.orgSlug, params?.host],\n )\n const interactionHref = useCallback(\n (interaction: ContactInteraction): string | null =>\n siteContext && interaction.hostId === hostId\n ? Aglyn.contactInteractionHref(interaction, siteContext)\n : null,\n [siteContext, hostId],\n )\n // The campaign history is asked of a site: the mounted one, or the holder's.\n const campaigns = useContactCampaignEmails(\n hostId ?? (consentGroup.hostId || null),\n contactId,\n )\n // One member read for the whole stream, however many rows it has.\n const canEdit = useCanEditActivity(scope.orgId)\n const entries = useMemo(\n () =>\n Aglyn.mergeContactTimeline(interactions, activities.rows, campaigns.emails),\n [interactions, activities.rows, campaigns.emails],\n )\n // One clock for every row of one paint, so two rows logged a second apart\n // cannot read \"just now\" and \"1 min ago\" from being rendered across a\n // minute boundary.\n const nowMs = Date.now()\n\n const [dialogOpen, setDialogOpen] = useState(false)\n const [editing, setEditing] = useState<CrmActivityRow | null>(null)\n const openNew = useCallback(() => {\n setEditing(null)\n setDialogOpen(true)\n }, [])\n const openEdit = useCallback((activity: CrmActivityRow) => {\n setEditing(activity)\n setDialogOpen(true)\n }, [])\n const close = useCallback(() => setDialogOpen(false), [])\n\n const loggedChip = useMemo(\n () => <Chip label=\"Logged\" size=\"small\" variant=\"outlined\" />,\n [],\n )\n const expansion = useTimelineExpansion()\n const pages = useTimelinePages(entries.length, {\n hasMore: activities.hasMore,\n showMore: activities.showMore,\n loading: activities.status === 'loading',\n })\n const logButton = suiteLocked ? (\n <CrmSuiteLockedButton variant=\"contained\" color=\"primary\">\n {'Log activity'}\n </CrmSuiteLockedButton>\n ) : (\n <Button\n size=\"small\"\n variant=\"contained\"\n color=\"primary\"\n onClick={openNew}\n disabled={!activities.ready}\n >\n {'Log activity'}\n </Button>\n )\n\n return (\n <>\n <CardDisplay\n header={'Timeline'}\n help={pluginDocsHelp('contactActivities', { anchor: '#four-kinds-of-history' })}\n HeaderProps={{\n action: (\n <Stack direction=\"row\" spacing={1}>\n <TimelineExpandAllButton expansion={expansion} disabled={!entries.length} />\n {logButton}\n </Stack>\n ),\n }}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n {activities.status === 'error' ? (\n <Typography variant=\"body2\" color=\"error\">\n {'Logged activity could not be loaded; what follows is what the platform captured.'}\n </Typography>\n ) : null}\n {campaigns.status === 'error' || campaigns.lookupFailed ? (\n // \"Unknown\", not \"none\": an empty campaign history and one that\n // could not be read lead a reader to opposite conclusions.\n <Typography variant=\"body2\" color=\"error\">\n {'Campaign email could not be loaded; the stream below may be missing the mail this person was sent.'}\n </Typography>\n ) : null}\n {entries.length === 0 ? (\n campaigns.status === 'loading' ? (\n // The campaign history is one request on mount; an empty state\n // before it answers would read as \"none\" for a person who was mailed.\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Loading…'}\n </Typography>\n ) : (\n <EmptyStateComponent\n compact\n label={'No history yet'}\n description={'What this person does on the site, the campaigns they are sent, and what you log about them, shows here.'}\n action={logButton}\n />\n )\n ) : (\n pages.slice(entries).map((entry) =>\n entry.kind === 'captured' ? (\n <CapturedRow\n key={entry.key}\n interaction={entry.interaction}\n nowMs={nowMs}\n href={interactionHref(entry.interaction)}\n />\n ) : entry.kind === 'campaign' ? (\n <CampaignEmailRow\n key={entry.key}\n email={entry.email}\n href={campaignHref ? campaignHref(entry.email) : null}\n nowMs={nowMs}\n />\n ) : (\n <ActivityRow\n key={entry.key}\n activity={entry.activity}\n scope={scope}\n onEdit={openEdit}\n subject={loggedChip}\n nowMs={nowMs}\n editable={!suiteLocked && canEdit(entry.activity)}\n expanded={expansion.isExpanded(entry.key)}\n onToggle={() => expansion.toggle(entry.key)}\n />\n ),\n )\n )}\n {entries.length ? <ListPagination {...pages.footer} /> : null}\n </Stack>\n </CardDisplay>\n <LogActivityDialog\n open={dialogOpen}\n onClose={close}\n scope={scope}\n link={link}\n activity={editing}\n />\n </>\n )\n}\nContactTimelineCard.displayName = 'ContactTimelineCard'\n\nexport default ContactTimelineCard\n"],"names":["Aglyn","CONTACT_SOURCE_LABELS","pluginDocsHelp","mdiAccountCircleOutline","mdiAccountEditOutline","mdiAccountPlusOutline","mdiApi","mdiBullhornOutline","mdiCalendarCheckOutline","mdiCartOutline","mdiEmailNewsletter","mdiFileImportOutline","mdiFormSelect","AppLink","CardDisplay","MdiIcon","EmptyStateComponent","ListPagination","Button","Chip","Stack","Tooltip","Typography","useParams","useCallback","useMemo","useState","contactPrimaryGroup","ActivityRow","useActivityScope","useActivityWindow","useCanEditActivity","TimelineEntry","TimelineExpandAllButton","useTimelineExpansion","useTimelinePages","CrmSuiteLockedButton","LogActivityDialog","useContactCampaignEmails","SOURCE_ICONS","form","member","order","booking","newsletter","api","manual","import","account","CapturedRow","props","interaction","nowMs","href","source","type","label","String","icon","when","Date","atMs","path","chips","size","variant","title","summary","meta","direction","spacing","sx","alignItems","flexWrap","rowGap","toLocaleString","color","activityTimeLabel","INTERACTION_LINK_LABELS","displayName","CampaignEmailRow","email","name","campaignName","subject","campaignEmailSummary","join","sentAtMs","alignSelf","ContactTimelineCard","hostId","org","contactId","contact","campaignHref","suiteLocked","scope","consentGroup","interactions","facet","readContactFacet","groupId","interactionsForGroup","hostIds","link","activities","params","siteContext","orgSlug","host","interactionHref","contactInteractionHref","campaigns","canEdit","orgId","entries","mergeContactTimeline","rows","emails","now","dialogOpen","setDialogOpen","editing","setEditing","openNew","openEdit","activity","close","loggedChip","expansion","pages","length","hasMore","showMore","loading","status","logButton","onClick","disabled","ready","header","help","anchor","HeaderProps","action","contentGutterX","contentGutterY","lookupFailed","compact","description","slice","map","entry","kind","key","onEdit","editable","expanded","isExpanded","onToggle","toggle","footer","open","onClose"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contact-timeline-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 * as Aglyn from '@aglyn/aglyn'\nimport type {\n ContactCampaignEmail,\n ContactInteraction,\n ContactSource,\n CrmActivityRow,\n} from '@aglyn/aglyn'\nimport { CONTACT_SOURCE_LABELS, pluginDocsHelp } from '@aglyn/aglyn'\nimport {\n mdiAccountCircleOutline,\n mdiAccountEditOutline,\n mdiAccountPlusOutline,\n mdiApi,\n mdiBullhornOutline,\n mdiCalendarCheckOutline,\n mdiCartOutline,\n mdiEmailNewsletter,\n mdiFileImportOutline,\n mdiFormSelect,\n} from '@aglyn/shared-data-mdi'\nimport { AppLink, CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport EmptyStateComponent from '@aglyn/shared-ui-jsx/components/empty-state.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { Button, Chip, Stack, Tooltip, Typography } from '@mui/material'\nimport { useParams } from 'next/navigation'\nimport { useCallback, useMemo, useState } from 'react'\nimport { contactInteractionHref, INTERACTION_LINK_LABELS } from '../model/contact-interaction-links'\nimport { contactPrimaryGroup } from '../model/contact-record'\nimport { ActivityRow } from './activity-list'\nimport {\n type ActivityRecordLink,\n type CrmOrg,\n useActivityScope,\n useActivityWindow,\n useCanEditActivity,\n} from './activity-queries'\nimport {\n TimelineEntry,\n TimelineExpandAllButton,\n useTimelineExpansion,\n useTimelinePages,\n} from './activity-timeline'\nimport { CrmSuiteLockedButton } from './crm-suite-lock'\nimport { LogActivityDialog } from './log-activity-dialog'\nimport { useContactCampaignEmails } from './use-contact-campaign-emails'\n\n/**\n * One glyph per capture door. Typed against the source union so a door added\n * to it cannot render as the placeholder glyph.\n */\nconst SOURCE_ICONS: Record<ContactSource, { path: string }> = {\n form: mdiFormSelect,\n member: mdiAccountPlusOutline,\n order: mdiCartOutline,\n booking: mdiCalendarCheckOutline,\n newsletter: mdiEmailNewsletter,\n api: mdiApi,\n manual: mdiAccountEditOutline,\n import: mdiFileImportOutline,\n account: mdiAccountCircleOutline,\n}\n\n/**\n * One thing the platform recorded: the door it came through, what the door\n * wrote, when, and the page the person was on.\n *\n * Read-only by nature — nobody logged it, so nobody edits it. The entry\n * point sits beside the timestamp rather than in the summary, the way the\n * contacts drawer draws it: the summary is the sentence the door wrote, the\n * path is a fact the door recorded, and a long path on the sentence's line\n * pushes out the thing the row is about.\n *\n * Everything it holds fits the collapsed row, so it has nothing to open.\n */\nfunction CapturedRow(props: {\n interaction: ContactInteraction\n nowMs: number\n /** The console page the door's record is read on, when it left one. */\n href: string | null\n}) {\n const { interaction, nowMs, href } = props\n const source = interaction.type\n const label = CONTACT_SOURCE_LABELS[source] ?? String(source)\n const icon = SOURCE_ICONS[source]\n const when = new Date(interaction.atMs)\n return (\n <TimelineEntry\n icon={<MdiIcon path={icon?.path} />}\n chips={\n <>\n <Chip label={label} size=\"small\" />\n <Chip label=\"Captured\" size=\"small\" variant=\"outlined\" />\n </>\n }\n title={interaction.summary ?? label}\n meta={\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'baseline', flexWrap: 'wrap', rowGap: 0.25 }}\n >\n <Tooltip title={when.toLocaleString()}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {interaction.path\n ? `${Aglyn.activityTimeLabel(interaction.atMs, nowMs)} · ${interaction.path}`\n : Aglyn.activityTimeLabel(interaction.atMs, nowMs)}\n </Typography>\n </Tooltip>\n {/*\n The record the door left, where the console reads it (AGL-2622):\n the submission in the Inbox reader, the order in its dialog. A door\n that left nothing to open — a newsletter opt-in, an import — has no\n link, and the caption is the whole entry.\n */}\n {href ? (\n <Typography variant=\"caption\">\n <AppLink href={href}>\n {INTERACTION_LINK_LABELS[source] ?? 'Open'}\n </AppLink>\n </Typography>\n ) : null}\n </Stack>\n }\n />\n )\n}\nCapturedRow.displayName = 'CapturedRow'\n\n/**\n * One campaign email the platform sent this person (AGL-2616): which email,\n * and what became of it — `sent · delivered · opened ×2 · clicked`.\n *\n * Read-only like a captured row, and drawn apart from BOTH the captured and\n * the logged rows: its glyph is the campaign's, never the envelope a logged\n * `email` activity carries, and its chips say \"Campaign\" and \"Sent\" where\n * the others say \"Captured\" and \"Logged\". The name links to the email's own\n * report when the page can address it; an email the team has deleted, or\n * one a sibling site sent, keeps its name and loses the link.\n *\n * The delivery line stays on the collapsed row beside the name, so it has\n * nothing to open.\n */\nfunction CampaignEmailRow(props: {\n email: ContactCampaignEmail\n href: string | null\n nowMs: number\n}) {\n const { email, href, nowMs } = props\n const name = email.campaignName ?? email.subject ?? 'Campaign email'\n const summary = Aglyn.campaignEmailSummary(email).join(' · ')\n const when = new Date(email.sentAtMs)\n return (\n <TimelineEntry\n icon={<MdiIcon path={mdiBullhornOutline.path} />}\n chips={\n <>\n <Chip label=\"Campaign\" size=\"small\" />\n <Chip label=\"Sent\" size=\"small\" variant=\"outlined\" />\n </>\n }\n title={\n <>\n {href ? <AppLink href={href}>{name}</AppLink> : name}\n {` · ${summary}`}\n </>\n }\n meta={\n <Tooltip title={when.toLocaleString()}>\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ alignSelf: 'flex-start' }}\n >\n {email.subject && email.subject !== name\n ? `${Aglyn.activityTimeLabel(email.sentAtMs, nowMs)} · ${email.subject}`\n : Aglyn.activityTimeLabel(email.sentAtMs, nowMs)}\n </Typography>\n </Tooltip>\n }\n />\n )\n}\nCampaignEmailRow.displayName = 'CampaignEmailRow'\n\nexport interface ContactTimelineCardProps {\n /** The site the record is read under, or `null` at the organization level. */\n hostId: string | null\n org: CrmOrg\n contactId: string\n /**\n * The RAW contact document, as the page's listener hands it back. The card\n * resolves the viewing group and reads that group's facet itself, so a\n * page cannot hand it another holder's timeline by flattening the row\n * through the wrong group.\n */\n contact: Record<string, unknown> | null | undefined\n /**\n * Where a campaign entry links to — the email's report — or `null` for\n * one the page cannot address. Handed in rather than built here because\n * the report lives on another surface whose path is the MOUNT's to know:\n * a site's record page reaches its own Emails hub, an org-level mount\n * holds the org's site list and can reach every site's. No builder means\n * no links, never a guessed one.\n */\n campaignHref?: (email: ContactCampaignEmail) => string | null\n /**\n * The org's plan lacks the CRM (AGL-2788), which the shell mounts no CRM\n * page for (AGL-2851). The history still draws; logging an activity stands\n * locked, and one already logged cannot be edited.\n */\n suiteLocked?: boolean\n}\n\n/**\n * A person's whole history in one newest-first stream (AGL-2600): what the\n * platform CAPTURED — a form, an order, a booking, a sign-up — what a person\n * LOGGED beside it — a call, an email, a meeting, a note — and, since\n * AGL-2616, the campaign mail the platform SENT them and what became of it.\n *\n * ## Why one stream\n *\n * The histories answer one question, \"what has happened with this person\",\n * and a page showing them as separate lists asks the reader to interleave\n * them by eye. The merge is `mergeContactTimeline`, pure and specced: newest\n * first, stable at a tie, an undated entry last, a campaign at the instant\n * it was sent.\n *\n * Each entry says which it is. A captured entry carries the door's label and\n * a \"Captured\" chip and offers no controls; a logged entry carries its kind\n * and a \"Logged\" chip, and its author may edit or delete it; a campaign\n * entry carries \"Campaign\" and \"Sent\", links to the email's report, and is\n * nobody's to edit.\n *\n * ## What is read\n *\n * The captured side comes off the contact document the page already holds\n * — THIS group's facet, filtered to the sites the group may see, the same\n * projection the contacts list makes — and costs no read. The logged side is\n * the bounded listener every activity surface uses, filtered to this\n * contact, a hundred at a time. The campaign side is one request to\n * `crm/contact-email-history`, which reads down this person's own delivery\n * log, capped, and never a campaign's recipients. The facet's history is\n * capped at fifty by the platform, so past the window the stream is the\n * logged side alone.\n *\n * ## How it reads\n *\n * Every entry is a collapsed row that opens in place, paged by the\n * console's footer over the merged stream; a page turned past the logged\n * window widens it by the next hundred. **Expand all** and **Log activity**\n * sit in the card header, and edit and delete stay on the entry they act on.\n */\nexport function ContactTimelineCard(props: ContactTimelineCardProps) {\n const { hostId, org, contactId, contact, campaignHref, suiteLocked = false } = props\n const scope = useActivityScope(hostId, org)\n /*\n * THIS holder's captured history, and only the visits this group may see.\n * A read off the top of the document, or of another group's facet, would\n * be another business's records — the disclosure the facet shape ended.\n * At the organization level (AGL-2630) the holder is the person's own\n * primary one, the group the record page reads the profile through.\n */\n const consentGroup = useMemo(\n () =>\n scope.consentGroup ??\n contactPrimaryGroup(contact, (org ?? null) as Record<string, unknown> | null),\n [scope.consentGroup, contact, org],\n )\n const interactions = useMemo(() => {\n const facet = Aglyn.readContactFacet(contact, consentGroup.groupId)\n return Aglyn.interactionsForGroup(facet.interactions, consentGroup.hostIds)\n }, [contact, consentGroup])\n const link = useMemo<ActivityRecordLink>(() => ({ contactId }), [contactId])\n const activities = useActivityWindow(scope, link)\n /*\n * Where a captured entry's record is READ (AGL-2622). The site console's\n * two route params are already in the URL this page is on, so the link is\n * built from them rather than from two document reads per timeline. Only\n * an interaction on THIS site links: a sibling site's order is that\n * site's record, read on that site's console, and a link built with this\n * site's segment would 404 or — worse — open the wrong site's row under\n * the same id. An interaction with no site recorded predates the stamp\n * and is left unlinked rather than guessed at. Off a site — the org-level\n * mount — there is no segment to build with, and nothing links.\n */\n const params = useParams<{ orgSlug?: string; host?: string }>()\n const siteContext = useMemo(\n () =>\n params?.orgSlug && params?.host\n ? { orgSlug: String(params.orgSlug), host: String(params.host) }\n : null,\n [params?.orgSlug, params?.host],\n )\n const interactionHref = useCallback(\n (interaction: ContactInteraction): string | null =>\n siteContext && interaction.hostId === hostId\n ? contactInteractionHref(interaction, siteContext)\n : null,\n [siteContext, hostId],\n )\n // The campaign history is asked of a site: the mounted one, or the holder's.\n const campaigns = useContactCampaignEmails(\n hostId ?? (consentGroup.hostId || null),\n contactId,\n )\n // One member read for the whole stream, however many rows it has.\n const canEdit = useCanEditActivity(scope.orgId)\n const entries = useMemo(\n () =>\n Aglyn.mergeContactTimeline(interactions, activities.rows, campaigns.emails),\n [interactions, activities.rows, campaigns.emails],\n )\n // One clock for every row of one paint, so two rows logged a second apart\n // cannot read \"just now\" and \"1 min ago\" from being rendered across a\n // minute boundary.\n const nowMs = Date.now()\n\n const [dialogOpen, setDialogOpen] = useState(false)\n const [editing, setEditing] = useState<CrmActivityRow | null>(null)\n const openNew = useCallback(() => {\n setEditing(null)\n setDialogOpen(true)\n }, [])\n const openEdit = useCallback((activity: CrmActivityRow) => {\n setEditing(activity)\n setDialogOpen(true)\n }, [])\n const close = useCallback(() => setDialogOpen(false), [])\n\n const loggedChip = useMemo(\n () => <Chip label=\"Logged\" size=\"small\" variant=\"outlined\" />,\n [],\n )\n const expansion = useTimelineExpansion()\n const pages = useTimelinePages(entries.length, {\n hasMore: activities.hasMore,\n showMore: activities.showMore,\n loading: activities.status === 'loading',\n })\n const logButton = suiteLocked ? (\n <CrmSuiteLockedButton variant=\"contained\" color=\"primary\">\n {'Log activity'}\n </CrmSuiteLockedButton>\n ) : (\n <Button\n size=\"small\"\n variant=\"contained\"\n color=\"primary\"\n onClick={openNew}\n disabled={!activities.ready}\n >\n {'Log activity'}\n </Button>\n )\n\n return (\n <>\n <CardDisplay\n header={'Timeline'}\n help={pluginDocsHelp('contactActivities', { anchor: '#four-kinds-of-history' })}\n HeaderProps={{\n action: (\n <Stack direction=\"row\" spacing={1}>\n <TimelineExpandAllButton expansion={expansion} disabled={!entries.length} />\n {logButton}\n </Stack>\n ),\n }}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n {activities.status === 'error' ? (\n <Typography variant=\"body2\" color=\"error\">\n {'Logged activity could not be loaded; what follows is what the platform captured.'}\n </Typography>\n ) : null}\n {campaigns.status === 'error' || campaigns.lookupFailed ? (\n // \"Unknown\", not \"none\": an empty campaign history and one that\n // could not be read lead a reader to opposite conclusions.\n <Typography variant=\"body2\" color=\"error\">\n {'Campaign email could not be loaded; the stream below may be missing the mail this person was sent.'}\n </Typography>\n ) : null}\n {entries.length === 0 ? (\n campaigns.status === 'loading' ? (\n // The campaign history is one request on mount; an empty state\n // before it answers would read as \"none\" for a person who was mailed.\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Loading…'}\n </Typography>\n ) : (\n <EmptyStateComponent\n compact\n label={'No history yet'}\n description={'What this person does on the site, the campaigns they are sent, and what you log about them, shows here.'}\n action={logButton}\n />\n )\n ) : (\n pages.slice(entries).map((entry) =>\n entry.kind === 'captured' ? (\n <CapturedRow\n key={entry.key}\n interaction={entry.interaction}\n nowMs={nowMs}\n href={interactionHref(entry.interaction)}\n />\n ) : entry.kind === 'campaign' ? (\n <CampaignEmailRow\n key={entry.key}\n email={entry.email}\n href={campaignHref ? campaignHref(entry.email) : null}\n nowMs={nowMs}\n />\n ) : (\n <ActivityRow\n key={entry.key}\n activity={entry.activity}\n scope={scope}\n onEdit={openEdit}\n subject={loggedChip}\n nowMs={nowMs}\n editable={!suiteLocked && canEdit(entry.activity)}\n expanded={expansion.isExpanded(entry.key)}\n onToggle={() => expansion.toggle(entry.key)}\n />\n ),\n )\n )}\n {entries.length ? <ListPagination {...pages.footer} /> : null}\n </Stack>\n </CardDisplay>\n <LogActivityDialog\n open={dialogOpen}\n onClose={close}\n scope={scope}\n link={link}\n activity={editing}\n />\n </>\n )\n}\nContactTimelineCard.displayName = 'ContactTimelineCard'\n\nexport default ContactTimelineCard\n"],"names":["Aglyn","CONTACT_SOURCE_LABELS","pluginDocsHelp","mdiAccountCircleOutline","mdiAccountEditOutline","mdiAccountPlusOutline","mdiApi","mdiBullhornOutline","mdiCalendarCheckOutline","mdiCartOutline","mdiEmailNewsletter","mdiFileImportOutline","mdiFormSelect","AppLink","CardDisplay","MdiIcon","EmptyStateComponent","ListPagination","Button","Chip","Stack","Tooltip","Typography","useParams","useCallback","useMemo","useState","contactInteractionHref","INTERACTION_LINK_LABELS","contactPrimaryGroup","ActivityRow","useActivityScope","useActivityWindow","useCanEditActivity","TimelineEntry","TimelineExpandAllButton","useTimelineExpansion","useTimelinePages","CrmSuiteLockedButton","LogActivityDialog","useContactCampaignEmails","SOURCE_ICONS","form","member","order","booking","newsletter","api","manual","import","account","CapturedRow","props","interaction","nowMs","href","source","type","label","String","icon","when","Date","atMs","path","chips","size","variant","title","summary","meta","direction","spacing","sx","alignItems","flexWrap","rowGap","toLocaleString","color","activityTimeLabel","displayName","CampaignEmailRow","email","name","campaignName","subject","campaignEmailSummary","join","sentAtMs","alignSelf","ContactTimelineCard","hostId","org","contactId","contact","campaignHref","suiteLocked","scope","consentGroup","interactions","facet","readContactFacet","groupId","interactionsForGroup","hostIds","link","activities","params","siteContext","orgSlug","host","interactionHref","campaigns","canEdit","orgId","entries","mergeContactTimeline","rows","emails","now","dialogOpen","setDialogOpen","editing","setEditing","openNew","openEdit","activity","close","loggedChip","expansion","pages","length","hasMore","showMore","loading","status","logButton","onClick","disabled","ready","header","help","anchor","HeaderProps","action","contentGutterX","contentGutterY","lookupFailed","compact","description","slice","map","entry","kind","key","onEdit","editable","expanded","isExpanded","onToggle","toggle","footer","open","onClose"],"mappings":"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"}
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
* the rows it reaches (`contacts-bulk-writes.ts` decides which), sent in
|
|
25
25
|
* pieces the route accepts. A stage move is `crm/contact-stage`, row by row:
|
|
26
26
|
* a move is what an automation listens for, and only the route that
|
|
27
|
-
* performed it can announce it. Letting the rows go is
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
* performed it can announce it. Letting the rows go is `crm/contact-remove`
|
|
28
|
+
* (AGL-3338): the route decides detach or delete against each stored
|
|
29
|
+
* document and keeps any marketing refusal the person gave.
|
|
30
30
|
*
|
|
31
31
|
* ## A refused row is named
|
|
32
32
|
*
|
|
@@ -42,9 +42,9 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
42
42
|
* the rows it reaches (`contacts-bulk-writes.ts` decides which), sent in
|
|
43
43
|
* pieces the route accepts. A stage move is `crm/contact-stage`, row by row:
|
|
44
44
|
* a move is what an automation listens for, and only the route that
|
|
45
|
-
* performed it can announce it. Letting the rows go is
|
|
46
|
-
*
|
|
47
|
-
*
|
|
45
|
+
* performed it can announce it. Letting the rows go is `crm/contact-remove`
|
|
46
|
+
* (AGL-3338): the route decides detach or delete against each stored
|
|
47
|
+
* document and keeps any marketing refusal the person gave.
|
|
48
48
|
*
|
|
49
49
|
* ## A refused row is named
|
|
50
50
|
*
|
|
@@ -61,23 +61,23 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
61
61
|
*/ import { CONTACT_LIFECYCLE_STAGE_LABELS, CONTACT_LIFECYCLE_STAGES, crmMemberPickerLabel } from "@aglyn/aglyn";
|
|
62
62
|
import { useConfirmationContext } from "@aglyn/shared-ui-jsx";
|
|
63
63
|
import { Button, MenuItem, TextField } from "@mui/material";
|
|
64
|
-
import { deleteDoc, doc, updateDoc, writeBatch } from "firebase/firestore";
|
|
65
64
|
import { useCallback, useMemo, useState } from "react";
|
|
66
|
-
import {
|
|
65
|
+
import { useOrgMemberOptions, useUser } from "@aglyn/tenant-feature-instance";
|
|
66
|
+
import { useContactRemove } from "../hooks/use-contact-remove.js";
|
|
67
67
|
import { useContactUpdate } from "../hooks/use-contact-update.js";
|
|
68
68
|
import { useCrmBulkApply } from "../hooks/use-crm-bulk-apply.js";
|
|
69
69
|
import { useCrmScope } from "../hooks/use-crm-scope.js";
|
|
70
|
+
import { CRM_CONTACT_REMOVE_MAX } from "../model/contact-remove.js";
|
|
70
71
|
import { CRM_CONTACT_UPDATE_MAX } from "../model/contact-update.js";
|
|
71
72
|
import { setContactStage } from "../model/crm-api.js";
|
|
72
|
-
import { contactBulkAddressOf, normalizeBulkTag, planAddTag,
|
|
73
|
+
import { contactBulkAddressOf, normalizeBulkTag, planAddTag, planRemoveTag, planSetCompany } from "../model/contacts-bulk-writes.js";
|
|
73
74
|
import { contactsCsv, downloadTextFile } from "../model/contacts-csv.js";
|
|
74
|
-
import { runCrmBulkBatch, runCrmBulkCalls
|
|
75
|
+
import { runCrmBulkBatch, runCrmBulkCalls } from "../model/crm-bulk-writes.js";
|
|
75
76
|
import AddToListDialog from "./add-to-list-dialog.js";
|
|
76
77
|
import { CompanyPicker, useCompanyOptions, useCreateCompany } from "./company-picker.js";
|
|
77
78
|
import { CrmBulkBarFrame, CrmBulkValueDialog, countNoun } from "./crm-bulk-bar-frame.js";
|
|
78
79
|
import CrmExportAllButton from "./crm-export-all-button.js";
|
|
79
|
-
import {
|
|
80
|
-
import { crmTaskCallScope } from "../model/task-routes.js";
|
|
80
|
+
import { CrmBulkShareButton } from "./record-sharing-card.js";
|
|
81
81
|
import { CrmSuiteLockedButton } from "./crm-suite-lock.js";
|
|
82
82
|
const NOUN = {
|
|
83
83
|
singular: 'contact',
|
|
@@ -124,15 +124,15 @@ const ACTION_TITLES = {
|
|
|
124
124
|
}
|
|
125
125
|
ContactsBulkBar.displayName = 'ContactsBulkBar';
|
|
126
126
|
function ContactsBulkBarBody(props) {
|
|
127
|
-
var _team_error, _ref, _ref1;
|
|
127
|
+
var _team_error, _ref, _ref1, _ref2;
|
|
128
128
|
const { hostId, org, scope, consentGroup, rows, selected, onSelectedChange, csv, suiteLocked = false } = props;
|
|
129
|
-
const firestore = useFirestore();
|
|
130
129
|
const { data: user } = useUser();
|
|
131
130
|
const { confirm } = useConfirmationContext();
|
|
132
131
|
const { busy, report, apply, dismissReport } = useCrmBulkApply({
|
|
133
132
|
recordKind: 'contact'
|
|
134
133
|
});
|
|
135
134
|
const contactUpdate = useContactUpdate(hostId);
|
|
135
|
+
const contactRemove = useContactRemove(hostId);
|
|
136
136
|
const selectedRows = useMemo(()=>{
|
|
137
137
|
const chosen = new Set(selected);
|
|
138
138
|
return rows.filter((row)=>chosen.has(row.$id));
|
|
@@ -172,32 +172,6 @@ function ContactsBulkBarBody(props) {
|
|
|
172
172
|
setCompany(null);
|
|
173
173
|
setPending(action);
|
|
174
174
|
};
|
|
175
|
-
/**
|
|
176
|
-
* The Firestore writers for the one act written client-direct: letting the
|
|
177
|
-
* rows go, a batch at a time and a row at a time for a batch that failed.
|
|
178
|
-
*/ const writers = useMemo(()=>{
|
|
179
|
-
const refFor = (id)=>{
|
|
180
|
-
var _ref, _ref1;
|
|
181
|
-
return doc(firestore, (_ref = scope == null ? void 0 : scope[0]) != null ? _ref : 'orgs', (_ref1 = scope == null ? void 0 : scope[1]) != null ? _ref1 : '', 'contacts', id);
|
|
182
|
-
};
|
|
183
|
-
return {
|
|
184
|
-
commitBatch: async (writes)=>{
|
|
185
|
-
const batch = writeBatch(firestore);
|
|
186
|
-
for (const write of writes){
|
|
187
|
-
if (write.kind === 'delete') batch.delete(refFor(write.id));
|
|
188
|
-
else batch.update(refFor(write.id), write.data);
|
|
189
|
-
}
|
|
190
|
-
await batch.commit();
|
|
191
|
-
},
|
|
192
|
-
commitOne: async (write)=>{
|
|
193
|
-
if (write.kind === 'delete') await deleteDoc(refFor(write.id));
|
|
194
|
-
else await updateDoc(refFor(write.id), write.data);
|
|
195
|
-
}
|
|
196
|
-
};
|
|
197
|
-
}, [
|
|
198
|
-
firestore,
|
|
199
|
-
scope
|
|
200
|
-
]);
|
|
201
175
|
/** One act's request for the rows it reaches, in the pieces the route accepts. */ const saveThroughRoute = useCallback((targets, set)=>runCrmBulkBatch(targets, (row)=>row.$id, contactBulkAddressOf, (piece)=>contactUpdate.updateMany(piece.map((row)=>row.$id), set), CRM_CONTACT_UPDATE_MAX), [
|
|
202
176
|
contactUpdate
|
|
203
177
|
]);
|
|
@@ -305,7 +279,6 @@ function ContactsBulkBarBody(props) {
|
|
|
305
279
|
csv
|
|
306
280
|
]);
|
|
307
281
|
const handleDetach = useCallback(async ()=>{
|
|
308
|
-
var _ref;
|
|
309
282
|
if (!scope || !selectedRows.length || !consentGroup) return;
|
|
310
283
|
const count = selectedRows.length;
|
|
311
284
|
const confirmed = await confirm({
|
|
@@ -318,31 +291,24 @@ function ContactsBulkBarBody(props) {
|
|
|
318
291
|
})// `confirm` resolves with no value and REJECTS on cancel.
|
|
319
292
|
.then(()=>true).catch(()=>false);
|
|
320
293
|
if (!confirmed) return;
|
|
321
|
-
const plan = planDetach(selectedRows, consentGroup, Date.now());
|
|
322
294
|
const outcome = await apply({
|
|
323
|
-
attempted:
|
|
324
|
-
skipped:
|
|
325
|
-
job: ()=>
|
|
295
|
+
attempted: selectedRows.length,
|
|
296
|
+
skipped: [],
|
|
297
|
+
job: ()=>runCrmBulkBatch(selectedRows, (row)=>row.$id, contactBulkAddressOf, (piece)=>contactRemove.removeMany(piece.map((row)=>row.$id)), CRM_CONTACT_REMOVE_MAX),
|
|
326
298
|
done: (done)=>doneSentence('detach', done)
|
|
327
299
|
});
|
|
328
300
|
// The rows that went are gone from the table; the refused ones stay
|
|
329
301
|
// selected, so the reader can see which they are and try again.
|
|
330
302
|
const refused = new Set(outcome.refused.map((row)=>row.label));
|
|
331
|
-
// A detached row still carries the facet keys and scoped search tokens
|
|
332
|
-
// of the holder that let it go; the rules keep a client from restamping
|
|
333
|
-
// them, so the route does (AGL-3321). A deleted row answers `missing`.
|
|
334
|
-
await restampCrmListFields(user, crmTaskCallScope(hostId, (_ref = scope == null ? void 0 : scope[1]) != null ? _ref : null), 'contacts', selectedRows.filter((row)=>!refused.has(contactBulkAddressOf(row))).map((row)=>row.$id));
|
|
335
303
|
onSelectedChange(selectedRows.filter((row)=>refused.has(contactBulkAddressOf(row))).map((row)=>row.$id));
|
|
336
304
|
}, [
|
|
337
305
|
scope,
|
|
338
306
|
selectedRows,
|
|
339
307
|
confirm,
|
|
340
308
|
apply,
|
|
341
|
-
|
|
309
|
+
contactRemove,
|
|
342
310
|
consentGroup,
|
|
343
|
-
onSelectedChange
|
|
344
|
-
user,
|
|
345
|
-
hostId
|
|
311
|
+
onSelectedChange
|
|
346
312
|
]);
|
|
347
313
|
const emails = selectedRows.map((row)=>{
|
|
348
314
|
var _row_email;
|
|
@@ -474,6 +440,13 @@ function ContactsBulkBarBody(props) {
|
|
|
474
440
|
onClick: ()=>setListOpen(true),
|
|
475
441
|
children: 'Add to list'
|
|
476
442
|
}),
|
|
443
|
+
/*#__PURE__*/ _jsx(CrmBulkShareButton, {
|
|
444
|
+
object: "contacts",
|
|
445
|
+
ids: selectedRows.map((row)=>row.$id),
|
|
446
|
+
hostId: hostId != null ? hostId : null,
|
|
447
|
+
orgId: (_ref1 = scope == null ? void 0 : scope[1]) != null ? _ref1 : null,
|
|
448
|
+
disabled: busy
|
|
449
|
+
}),
|
|
477
450
|
/*#__PURE__*/ _jsx(Button, {
|
|
478
451
|
size: "small",
|
|
479
452
|
disabled: busy,
|
|
@@ -482,7 +455,7 @@ function ContactsBulkBarBody(props) {
|
|
|
482
455
|
}),
|
|
483
456
|
/*#__PURE__*/ _jsx(CrmExportAllButton, {
|
|
484
457
|
resource: "contacts",
|
|
485
|
-
orgId: (
|
|
458
|
+
orgId: (_ref2 = scope == null ? void 0 : scope[1]) != null ? _ref2 : null,
|
|
486
459
|
hostId: hostId,
|
|
487
460
|
disabled: busy
|
|
488
461
|
}),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contacts-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 contacts table, for whatever rows are ticked (AGL-2603).\n *\n * The table's selection is opt-in and this is what it is for: a chosen set\n * of people, and one act over all of them. Tag or untag them, hand them to\n * an owner, move them along the funnel, put them on an email audience, take\n * them into a spreadsheet, or let them go from this site's CRM.\n *\n * ## The chrome is the shared frame\n *\n * This bar settled how a bar over a table reads — it appears only for a\n * selection and says how many, its actions sit in one wrapping row with\n * Clear at the end, and whatever the last action could not do is listed by\n * name in an alert under it — and the companies, deals and tasks bars were\n * built on that shape as `CrmBulkBarFrame` and `useCrmBulkApply` (AGL-2621).\n * The contacts bar stands on the same two (AGL-2635): what is its own is the\n * actions, the dialog's field, the audience door, and how each act is sent.\n *\n * ## Each act goes where its field is written (AGL-2804)\n *\n * A contact's tags, owner and company live in the holder's facet, which is\n * the server's to write — the rules cannot tell one field of a facet from\n * another — so each of those acts is ONE request to `crm/contact-update` for\n * the rows it reaches (`contacts-bulk-writes.ts` decides which), sent in\n * pieces the route accepts. A stage move is `crm/contact-stage`, row by row:\n * a move is what an automation listens for, and only the route that\n * performed it can announce it. Letting the rows go is the one act written\n * client-direct, because a holder dropping its own facet is the one facet\n * change the rules leave the browser.\n *\n * ## A refused row is named\n *\n * Whatever the server or the store refused comes back by address into the\n * alert under the bar — not into a count, and not into the console. A\n * merchant who tagged four hundred people and got three hundred and\n * ninety-eight needs the two addresses.\n *\n * ## Add to list goes through the audience's own door\n *\n * The membership routes, with the check-then-attest sequence the Emails\n * console runs — see `add-to-list-dialog.tsx`. Nothing here writes a\n * membership document.\n */\n\nimport {\n type AglynOrgBilling,\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CONTACT_LIFECYCLE_STAGES,\n type ConsentGroup,\n type ContactLifecycleStage,\n crmMemberPickerLabel,\n} from '@aglyn/aglyn'\nimport { useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport { Button, MenuItem, TextField } from '@mui/material'\nimport { deleteDoc, doc, updateDoc, writeBatch } from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport {\n useFirestore,\n useOrgMemberOptions,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport { useContactUpdate } from '../hooks/use-contact-update'\nimport { useCrmBulkApply } from '../hooks/use-crm-bulk-apply'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { CRM_CONTACT_UPDATE_MAX, type ContactUpdateFields } from '../model/contact-update'\nimport { setContactStage } from '../model/crm-api'\nimport {\n contactBulkAddressOf,\n normalizeBulkTag,\n planAddTag,\n planDetach,\n planRemoveTag,\n planSetCompany,\n type ContactBulkRow,\n type ContactBulkSelection,\n type ContactBulkSkip,\n type ContactBulkWrite,\n} from '../model/contacts-bulk-writes'\nimport {\n type ContactCsvOptions,\n type ContactCsvRow,\n contactsCsv,\n downloadTextFile,\n} from '../model/contacts-csv'\nimport {\n type CrmBulkOutcome,\n runCrmBulkBatch,\n runCrmBulkCalls,\n runCrmBulkWrites,\n} from '../model/crm-bulk-writes'\nimport AddToListDialog from './add-to-list-dialog'\nimport {\n CompanyPicker,\n type CompanyOption,\n useCompanyOptions,\n useCreateCompany,\n} from './company-picker'\nimport {\n type CrmBulkNoun,\n CrmBulkBarFrame,\n CrmBulkValueDialog,\n countNoun,\n} from './crm-bulk-bar-frame'\nimport CrmExportAllButton from './crm-export-all-button'\nimport { restampCrmListFields } from '../model/list-fields-api'\nimport { crmTaskCallScope } from '../model/task-routes'\nimport { CrmSuiteLockedButton } from './crm-suite-lock'\n\nexport interface ContactsBulkBarProps {\n /** The site the list is read under, or `null` at the organization level. */\n hostId: string | null\n /** The org document the shell passed, for the company picker's scope. */\n org?: Partial<AglynOrgBilling> | null\n /** `['orgs', orgId]`, or `null` while the org is unresolved. */\n scope: readonly [string, string] | null\n /**\n * The holder these rows are being read AS — whose facet the acts land in.\n * `null` at the organization level (AGL-2630), where each row's own holder\n * (`groupId`) takes the act instead, and \"Remove from this site\" is not\n * offered because there is no site to remove from.\n */\n consentGroup: ConsentGroup | null\n /** The table's rows, already projected through the holder's facet. */\n rows: readonly (ContactBulkRow & ContactCsvRow)[]\n selected: readonly string[]\n onSelectedChange: (ids: string[]) => void\n /**\n * How the export names an owner and which custom fields it carries — the\n * table's own options, so the selection's file and the table's are one\n * format (AGL-2621).\n */\n csv?: ContactCsvOptions\n /**\n * The org's plan lacks the CRM (AGL-2788), which the shell mounts no CRM\n * page for (AGL-2851). The owner, the stage and the company stand locked;\n * tags, the exports, the audience door and removing people from a site\n * draw no lock of their own.\n */\n suiteLocked?: boolean\n}\n\nconst NOUN: CrmBulkNoun = { singular: 'contact', plural: 'contacts' }\n\n/** The one small dialog the value-taking actions share. */\ntype PendingAction = 'add-tag' | 'remove-tag' | 'owner' | 'stage' | 'company'\n\nconst ACTION_TITLES: Record<PendingAction, string> = {\n 'add-tag': 'Add a tag',\n 'remove-tag': 'Remove a tag',\n owner: 'Set the owner',\n stage: 'Set the lifecycle stage',\n company: 'Set the company',\n}\n\n/** How a finished action reads, given how many rows it reached. */\nconst doneSentence = (action: PendingAction | 'detach', done: number): string => {\n const people = countNoun(done, NOUN)\n switch (action) {\n case 'add-tag':\n return `Tagged ${people}`\n case 'remove-tag':\n return `Removed the tag from ${people}`\n case 'owner':\n return `Owner set on ${people}`\n case 'stage':\n return `Stage set on ${people}`\n case 'company':\n return `Company set on ${people}`\n case 'detach':\n return `${people} removed from this site`\n }\n}\n\n/** The report's line for a row an act left out on purpose. */\nconst skippedLine = (row: ContactBulkSkip) => ({ label: row.email, reason: row.reason })\n\n/**\n * The bar, or nothing.\n *\n * The empty state is a separate component boundary on purpose: with no\n * selection the body below is never mounted, so none of its hooks run — no\n * roster hook armed, no dialog state held — on the visit to the list that\n * every reader makes and most never tick a row on.\n */\nexport function ContactsBulkBar(props: ContactsBulkBarProps) {\n if (!props.selected.length) return null\n return <ContactsBulkBarBody {...props} />\n}\nContactsBulkBar.displayName = 'ContactsBulkBar'\n\nfunction ContactsBulkBarBody(props: ContactsBulkBarProps) {\n const {\n hostId,\n org,\n scope,\n consentGroup,\n rows,\n selected,\n onSelectedChange,\n csv,\n suiteLocked = false,\n } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { confirm } = useConfirmationContext()\n const { busy, report, apply, dismissReport } = useCrmBulkApply({ recordKind: 'contact' })\n const contactUpdate = useContactUpdate(hostId)\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 const [pending, setPending] = useState<PendingAction | null>(null)\n const [value, setValue] = useState('')\n const [company, setCompany] = useState<CompanyOption | null>(null)\n const [listOpen, setListOpen] = useState(false)\n\n /*\n * The roster, read only while the owner dialog is open. A bulk bar sits on\n * every visit to the list; the team is a request for a control nobody has\n * opened until they open it. The company list follows the same rule for\n * its own dialog (AGL-2613).\n */\n const team = useOrgMemberOptions(scope?.[1], { enabled: pending === 'owner' })\n const companies = useCompanyOptions({\n hostId,\n org,\n enabled: pending === 'company',\n })\n const createCompany = useCreateCompany({ hostId, org })\n // The site the audiences API is asked from: the mounted one, or at the\n // organization level the one the reader picked (AGL-2630).\n const { createHostId } = useCrmScope({ hostId, org })\n\n const openAction = (action: PendingAction) => {\n setValue('')\n setCompany(null)\n setPending(action)\n }\n\n /**\n * The Firestore writers for the one act written client-direct: letting the\n * rows go, a batch at a time and a row at a time for a batch that failed.\n */\n const writers = useMemo(() => {\n const refFor = (id: string) =>\n doc(firestore, scope?.[0] ?? 'orgs', scope?.[1] ?? '', 'contacts', id)\n return {\n commitBatch: async (writes: readonly ContactBulkWrite[]) => {\n const batch = writeBatch(firestore)\n for (const write of writes) {\n if (write.kind === 'delete') batch.delete(refFor(write.id))\n else batch.update(refFor(write.id), write.data)\n }\n await batch.commit()\n },\n commitOne: async (write: ContactBulkWrite) => {\n if (write.kind === 'delete') await deleteDoc(refFor(write.id))\n else await updateDoc(refFor(write.id), write.data)\n },\n }\n }, [firestore, scope])\n\n /** One act's request for the rows it reaches, in the pieces the route accepts. */\n const saveThroughRoute = useCallback(\n (targets: readonly ContactBulkRow[], set: ContactUpdateFields) =>\n runCrmBulkBatch(\n targets,\n (row) => row.$id,\n contactBulkAddressOf,\n (piece) => contactUpdate.updateMany(piece.map((row) => row.$id), set),\n CRM_CONTACT_UPDATE_MAX,\n ),\n [contactUpdate],\n )\n\n const handleApply = useCallback(async () => {\n if (!pending || !scope) return\n /*\n * A row no site holds has no facet to write. Under a site every selected\n * row is the viewing group's; at the organization level (AGL-2630) each\n * row was flattened through its own primary holder and is written back\n * through it, so a row nobody holds yet is left out and says so.\n */\n const held: ContactBulkRow[] = []\n const unheld: ContactBulkSkip[] = []\n for (const row of selectedRows) {\n if (consentGroup || row.groupId) held.push(row)\n else {\n unheld.push({\n email: contactBulkAddressOf(row),\n reason: 'no site holds this contact yet',\n })\n }\n }\n\n const planned = ((): { selection: ContactBulkSelection; job: () => Promise<CrmBulkOutcome> } | null => {\n if (pending === 'add-tag' || pending === 'remove-tag') {\n const tag = normalizeBulkTag(value)\n if (!tag) return null\n const selection =\n pending === 'add-tag' ? planAddTag(held, tag) : planRemoveTag(held, tag)\n const set: ContactUpdateFields =\n pending === 'add-tag' ? { addTag: tag } : { removeTag: tag }\n return { selection, job: () => saveThroughRoute(selection.rows, set) }\n }\n if (pending === 'owner') {\n return {\n selection: { rows: held, skipped: [] },\n job: () => saveThroughRoute(held, { ownerUid: value }),\n }\n }\n if (pending === 'company') {\n const companyId = company?.id ?? null\n const selection = planSetCompany(held, companyId)\n return { selection, job: () => saveThroughRoute(selection.rows, { companyId }) }\n }\n if (!value) return null\n const stage = value as ContactLifecycleStage\n return {\n selection: { rows: held, skipped: [] },\n /*\n * One row at a time through the one writer of a contact's stage, on\n * the site that holds the row: the mounted site, or at the\n * organization level the row's own holder's.\n */\n job: () =>\n runCrmBulkCalls(held, contactBulkAddressOf, async (row) => {\n const site = hostId ?? (row.holderHostId || null)\n if (!site) throw new Error('no site holds this contact yet')\n await setContactStage(user, site, row.$id, stage)\n }),\n }\n })()\n if (!planned) return\n const action = pending\n setPending(null)\n await apply({\n attempted: planned.selection.rows.length,\n skipped: [...unheld, ...planned.selection.skipped].map(skippedLine),\n job: planned.job,\n done: (count) => doneSentence(action, count),\n })\n }, [\n pending,\n scope,\n selectedRows,\n consentGroup,\n value,\n company,\n saveThroughRoute,\n hostId,\n user,\n apply,\n ])\n\n const handleExport = useCallback(() => {\n downloadTextFile(\n 'contacts-selected.csv',\n 'text/csv',\n contactsCsv(selectedRows, csv),\n )\n }, [selectedRows, csv])\n\n const handleDetach = useCallback(async () => {\n if (!scope || !selectedRows.length || !consentGroup) return\n const count = selectedRows.length\n const confirmed = await confirm({\n title: count === 1 ? 'Remove this contact?' : `Remove ${count} contacts?`,\n description:\n `${count === 1 ? 'This person is' : `These ${count} people are`} ` +\n \"removed from this site's Contacts, along with their notes, tags and \" +\n 'timeline. Other sites that captured the same people keep their own ' +\n 'records. Their form submissions, orders, bookings, and membership ' +\n 'records are separate — delete those from their own pages if the ' +\n 'request covers them.',\n confirmationText: count === 1 ? 'Remove contact' : 'Remove contacts',\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 plan = planDetach(selectedRows, consentGroup, Date.now())\n const outcome = await apply({\n attempted: plan.writes.length,\n skipped: plan.skipped.map(skippedLine),\n job: () => runCrmBulkWrites(writers, plan.writes, (write) => write.email),\n done: (done) => doneSentence('detach', done),\n })\n // The rows that went are gone from the table; the refused ones stay\n // selected, so the reader can see which they are and try again.\n const refused = new Set(outcome.refused.map((row) => row.label))\n // A detached row still carries the facet keys and scoped search tokens\n // of the holder that let it go; the rules keep a client from restamping\n // them, so the route does (AGL-3321). A deleted row answers `missing`.\n await restampCrmListFields(\n user,\n crmTaskCallScope(hostId, scope?.[1] ?? null),\n 'contacts',\n selectedRows\n .filter((row) => !refused.has(contactBulkAddressOf(row)))\n .map((row) => row.$id),\n )\n onSelectedChange(\n selectedRows\n .filter((row) => refused.has(contactBulkAddressOf(row)))\n .map((row) => row.$id),\n )\n }, [scope, selectedRows, confirm, apply, writers, consentGroup, onSelectedChange, user, hostId])\n\n const emails = selectedRows\n .map((row) => String(row.email ?? '').trim())\n .filter(Boolean)\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={\n !(pending === 'stage' && !value) &&\n !(\n (pending === 'add-tag' || pending === 'remove-tag') &&\n !normalizeBulkTag(value)\n )\n }\n onClose={() => setPending(null)}\n onApply={() => void handleApply()}\n >\n {pending === 'add-tag' || pending === 'remove-tag' ? (\n <TextField\n autoFocus\n size=\"small\"\n label=\"Tag\"\n placeholder=\"vip\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n helperText={\n pending === 'add-tag'\n ? 'Lowercased, like the tags on a profile'\n : 'Removed wherever it is present'\n }\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 error={Boolean(team.error)}\n helperText={team.error ?? (team.ready ? undefined : 'Loading the team…')}\n >\n <MenuItem value=\"\">{'Nobody — clear the owner'}</MenuItem>\n {team.options.map((member) => (\n <MenuItem key={member.uid} value={member.uid}>\n {crmMemberPickerLabel(member)}\n </MenuItem>\n ))}\n </TextField>\n ) : pending === 'stage' ? (\n <TextField\n select\n size=\"small\"\n label=\"Lifecycle stage\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n >\n {CONTACT_LIFECYCLE_STAGES.map((stage) => (\n <MenuItem key={stage} value={stage}>\n {CONTACT_LIFECYCLE_STAGE_LABELS[stage]}\n </MenuItem>\n ))}\n </TextField>\n ) : pending === 'company' ? (\n <CompanyPicker\n options={companies.options}\n ready={companies.ready}\n truncated={companies.truncated}\n value={company?.id ?? null}\n onChange={(_id, picked) => setCompany(picked)}\n onCreate={createCompany}\n helperText=\"Leave it empty to unlink the selected contacts from their companies.\"\n />\n ) : null}\n </CrmBulkValueDialog>\n {listOpen ? (\n <AddToListDialog\n open\n onClose={() => setListOpen(false)}\n hostId={createHostId}\n scope={scope}\n emails={emails}\n />\n ) : null}\n </>\n }\n >\n <Button size=\"small\" disabled={busy || !scope} onClick={() => openAction('add-tag')}>\n {'Add tag'}\n </Button>\n <Button\n size=\"small\"\n disabled={busy || !scope}\n onClick={() => openAction('remove-tag')}\n >\n {'Remove tag'}\n </Button>\n {suiteLocked ? (\n <>\n <CrmSuiteLockedButton>{'Set owner'}</CrmSuiteLockedButton>\n <CrmSuiteLockedButton>{'Set stage'}</CrmSuiteLockedButton>\n <CrmSuiteLockedButton>{'Set company'}</CrmSuiteLockedButton>\n </>\n ) : (\n <>\n <Button size=\"small\" disabled={busy || !scope} onClick={() => openAction('owner')}>\n {'Set owner'}\n </Button>\n <Button size=\"small\" disabled={busy || !scope} onClick={() => openAction('stage')}>\n {'Set stage'}\n </Button>\n <Button\n size=\"small\"\n disabled={busy || !scope}\n onClick={() => openAction('company')}\n >\n {'Set company'}\n </Button>\n </>\n )}\n <Button\n size=\"small\"\n disabled={busy || !scope || !emails.length || !createHostId}\n onClick={() => setListOpen(true)}\n >\n {'Add to list'}\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=\"contacts\"\n orgId={scope?.[1] ?? null}\n hostId={hostId}\n disabled={busy}\n />\n {/*\n Only under a site. The act is a DETACH from the viewing site's\n CRM, and at the organization level there is no viewing site — an\n org-wide member removes a person from a site by opening the record\n under that site.\n */}\n {consentGroup ? (\n <Button\n size=\"small\"\n color=\"error\"\n disabled={busy || !scope}\n onClick={() => void handleDetach()}\n >\n {'Remove from this site'}\n </Button>\n ) : null}\n </CrmBulkBarFrame>\n )\n}\nContactsBulkBarBody.displayName = 'ContactsBulkBarBody'\n\nexport default ContactsBulkBar\n"],"names":["CONTACT_LIFECYCLE_STAGE_LABELS","CONTACT_LIFECYCLE_STAGES","crmMemberPickerLabel","useConfirmationContext","Button","MenuItem","TextField","deleteDoc","doc","updateDoc","writeBatch","useCallback","useMemo","useState","useFirestore","useOrgMemberOptions","useUser","useContactUpdate","useCrmBulkApply","useCrmScope","CRM_CONTACT_UPDATE_MAX","setContactStage","contactBulkAddressOf","normalizeBulkTag","planAddTag","planDetach","planRemoveTag","planSetCompany","contactsCsv","downloadTextFile","runCrmBulkBatch","runCrmBulkCalls","runCrmBulkWrites","AddToListDialog","CompanyPicker","useCompanyOptions","useCreateCompany","CrmBulkBarFrame","CrmBulkValueDialog","countNoun","CrmExportAllButton","restampCrmListFields","crmTaskCallScope","CrmSuiteLockedButton","NOUN","singular","plural","ACTION_TITLES","owner","stage","company","doneSentence","action","done","people","skippedLine","row","label","email","reason","ContactsBulkBar","props","selected","length","ContactsBulkBarBody","displayName","team","hostId","org","scope","consentGroup","rows","onSelectedChange","csv","suiteLocked","firestore","data","user","confirm","busy","report","apply","dismissReport","recordKind","contactUpdate","selectedRows","chosen","Set","filter","has","$id","pending","setPending","value","setValue","setCompany","listOpen","setListOpen","enabled","companies","createCompany","createHostId","openAction","writers","refFor","id","commitBatch","writes","batch","write","kind","delete","update","commit","commitOne","saveThroughRoute","targets","set","piece","updateMany","map","handleApply","held","unheld","groupId","push","planned","tag","selection","addTag","removeTag","job","skipped","ownerUid","companyId","site","holderHostId","Error","attempted","count","handleExport","handleDetach","confirmed","title","description","confirmationText","confirmationButtonProps","color","then","catch","plan","Date","now","outcome","refused","emails","String","trim","Boolean","noun","onClear","onDismissReport","extras","open","canApply","onClose","onApply","autoFocus","size","placeholder","onChange","event","target","helperText","select","error","ready","undefined","options","member","uid","truncated","_id","picked","onCreate","disabled","onClick","resource","orgId"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contacts-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 contacts table, for whatever rows are ticked (AGL-2603).\n *\n * The table's selection is opt-in and this is what it is for: a chosen set\n * of people, and one act over all of them. Tag or untag them, hand them to\n * an owner, move them along the funnel, put them on an email audience, take\n * them into a spreadsheet, or let them go from this site's CRM.\n *\n * ## The chrome is the shared frame\n *\n * This bar settled how a bar over a table reads — it appears only for a\n * selection and says how many, its actions sit in one wrapping row with\n * Clear at the end, and whatever the last action could not do is listed by\n * name in an alert under it — and the companies, deals and tasks bars were\n * built on that shape as `CrmBulkBarFrame` and `useCrmBulkApply` (AGL-2621).\n * The contacts bar stands on the same two (AGL-2635): what is its own is the\n * actions, the dialog's field, the audience door, and how each act is sent.\n *\n * ## Each act goes where its field is written (AGL-2804)\n *\n * A contact's tags, owner and company live in the holder's facet, which is\n * the server's to write — the rules cannot tell one field of a facet from\n * another — so each of those acts is ONE request to `crm/contact-update` for\n * the rows it reaches (`contacts-bulk-writes.ts` decides which), sent in\n * pieces the route accepts. A stage move is `crm/contact-stage`, row by row:\n * a move is what an automation listens for, and only the route that\n * performed it can announce it. Letting the rows go is `crm/contact-remove`\n * (AGL-3338): the route decides detach or delete against each stored\n * document and keeps any marketing refusal the person gave.\n *\n * ## A refused row is named\n *\n * Whatever the server or the store refused comes back by address into the\n * alert under the bar — not into a count, and not into the console. A\n * merchant who tagged four hundred people and got three hundred and\n * ninety-eight needs the two addresses.\n *\n * ## Add to list goes through the audience's own door\n *\n * The membership routes, with the check-then-attest sequence the Emails\n * console runs — see `add-to-list-dialog.tsx`. Nothing here writes a\n * membership document.\n */\n\nimport {\n type AglynOrgBilling,\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CONTACT_LIFECYCLE_STAGES,\n type ConsentGroup,\n type ContactLifecycleStage,\n crmMemberPickerLabel,\n} from '@aglyn/aglyn'\nimport { useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport { Button, MenuItem, TextField } from '@mui/material'\nimport { useCallback, useMemo, useState } from 'react'\nimport { useOrgMemberOptions, useUser } from '@aglyn/tenant-feature-instance'\nimport { useContactRemove } from '../hooks/use-contact-remove'\nimport { useContactUpdate } from '../hooks/use-contact-update'\nimport { useCrmBulkApply } from '../hooks/use-crm-bulk-apply'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { CRM_CONTACT_REMOVE_MAX } from '../model/contact-remove'\nimport { CRM_CONTACT_UPDATE_MAX, type ContactUpdateFields } from '../model/contact-update'\nimport { setContactStage } from '../model/crm-api'\nimport {\n contactBulkAddressOf,\n normalizeBulkTag,\n planAddTag,\n planRemoveTag,\n planSetCompany,\n type ContactBulkRow,\n type ContactBulkSelection,\n type ContactBulkSkip,\n} from '../model/contacts-bulk-writes'\nimport {\n type ContactCsvOptions,\n type ContactCsvRow,\n contactsCsv,\n downloadTextFile,\n} from '../model/contacts-csv'\nimport {\n type CrmBulkOutcome,\n runCrmBulkBatch,\n runCrmBulkCalls,\n} from '../model/crm-bulk-writes'\nimport AddToListDialog from './add-to-list-dialog'\nimport {\n CompanyPicker,\n type CompanyOption,\n useCompanyOptions,\n useCreateCompany,\n} from './company-picker'\nimport {\n type CrmBulkNoun,\n CrmBulkBarFrame,\n CrmBulkValueDialog,\n countNoun,\n} from './crm-bulk-bar-frame'\nimport CrmExportAllButton from './crm-export-all-button'\nimport { CrmBulkShareButton } from './record-sharing-card'\nimport { CrmSuiteLockedButton } from './crm-suite-lock'\n\nexport interface ContactsBulkBarProps {\n /** The site the list is read under, or `null` at the organization level. */\n hostId: string | null\n /** The org document the shell passed, for the company picker's scope. */\n org?: Partial<AglynOrgBilling> | null\n /** `['orgs', orgId]`, or `null` while the org is unresolved. */\n scope: readonly [string, string] | null\n /**\n * The holder these rows are being read AS — whose facet the acts land in.\n * `null` at the organization level (AGL-2630), where each row's own holder\n * (`groupId`) takes the act instead, and \"Remove from this site\" is not\n * offered because there is no site to remove from.\n */\n consentGroup: ConsentGroup | null\n /** The table's rows, already projected through the holder's facet. */\n rows: readonly (ContactBulkRow & ContactCsvRow)[]\n selected: readonly string[]\n onSelectedChange: (ids: string[]) => void\n /**\n * How the export names an owner and which custom fields it carries — the\n * table's own options, so the selection's file and the table's are one\n * format (AGL-2621).\n */\n csv?: ContactCsvOptions\n /**\n * The org's plan lacks the CRM (AGL-2788), which the shell mounts no CRM\n * page for (AGL-2851). The owner, the stage and the company stand locked;\n * tags, the exports, the audience door and removing people from a site\n * draw no lock of their own.\n */\n suiteLocked?: boolean\n}\n\nconst NOUN: CrmBulkNoun = { singular: 'contact', plural: 'contacts' }\n\n/** The one small dialog the value-taking actions share. */\ntype PendingAction = 'add-tag' | 'remove-tag' | 'owner' | 'stage' | 'company'\n\nconst ACTION_TITLES: Record<PendingAction, string> = {\n 'add-tag': 'Add a tag',\n 'remove-tag': 'Remove a tag',\n owner: 'Set the owner',\n stage: 'Set the lifecycle stage',\n company: 'Set the company',\n}\n\n/** How a finished action reads, given how many rows it reached. */\nconst doneSentence = (action: PendingAction | 'detach', done: number): string => {\n const people = countNoun(done, NOUN)\n switch (action) {\n case 'add-tag':\n return `Tagged ${people}`\n case 'remove-tag':\n return `Removed the tag from ${people}`\n case 'owner':\n return `Owner set on ${people}`\n case 'stage':\n return `Stage set on ${people}`\n case 'company':\n return `Company set on ${people}`\n case 'detach':\n return `${people} removed from this site`\n }\n}\n\n/** The report's line for a row an act left out on purpose. */\nconst skippedLine = (row: ContactBulkSkip) => ({ label: row.email, reason: row.reason })\n\n/**\n * The bar, or nothing.\n *\n * The empty state is a separate component boundary on purpose: with no\n * selection the body below is never mounted, so none of its hooks run — no\n * roster hook armed, no dialog state held — on the visit to the list that\n * every reader makes and most never tick a row on.\n */\nexport function ContactsBulkBar(props: ContactsBulkBarProps) {\n if (!props.selected.length) return null\n return <ContactsBulkBarBody {...props} />\n}\nContactsBulkBar.displayName = 'ContactsBulkBar'\n\nfunction ContactsBulkBarBody(props: ContactsBulkBarProps) {\n const {\n hostId,\n org,\n scope,\n consentGroup,\n rows,\n selected,\n onSelectedChange,\n csv,\n suiteLocked = false,\n } = props\n const { data: user } = useUser()\n const { confirm } = useConfirmationContext()\n const { busy, report, apply, dismissReport } = useCrmBulkApply({ recordKind: 'contact' })\n const contactUpdate = useContactUpdate(hostId)\n const contactRemove = useContactRemove(hostId)\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 const [pending, setPending] = useState<PendingAction | null>(null)\n const [value, setValue] = useState('')\n const [company, setCompany] = useState<CompanyOption | null>(null)\n const [listOpen, setListOpen] = useState(false)\n\n /*\n * The roster, read only while the owner dialog is open. A bulk bar sits on\n * every visit to the list; the team is a request for a control nobody has\n * opened until they open it. The company list follows the same rule for\n * its own dialog (AGL-2613).\n */\n const team = useOrgMemberOptions(scope?.[1], { enabled: pending === 'owner' })\n const companies = useCompanyOptions({\n hostId,\n org,\n enabled: pending === 'company',\n })\n const createCompany = useCreateCompany({ hostId, org })\n // The site the audiences API is asked from: the mounted one, or at the\n // organization level the one the reader picked (AGL-2630).\n const { createHostId } = useCrmScope({ hostId, org })\n\n const openAction = (action: PendingAction) => {\n setValue('')\n setCompany(null)\n setPending(action)\n }\n\n /** One act's request for the rows it reaches, in the pieces the route accepts. */\n const saveThroughRoute = useCallback(\n (targets: readonly ContactBulkRow[], set: ContactUpdateFields) =>\n runCrmBulkBatch(\n targets,\n (row) => row.$id,\n contactBulkAddressOf,\n (piece) => contactUpdate.updateMany(piece.map((row) => row.$id), set),\n CRM_CONTACT_UPDATE_MAX,\n ),\n [contactUpdate],\n )\n\n const handleApply = useCallback(async () => {\n if (!pending || !scope) return\n /*\n * A row no site holds has no facet to write. Under a site every selected\n * row is the viewing group's; at the organization level (AGL-2630) each\n * row was flattened through its own primary holder and is written back\n * through it, so a row nobody holds yet is left out and says so.\n */\n const held: ContactBulkRow[] = []\n const unheld: ContactBulkSkip[] = []\n for (const row of selectedRows) {\n if (consentGroup || row.groupId) held.push(row)\n else {\n unheld.push({\n email: contactBulkAddressOf(row),\n reason: 'no site holds this contact yet',\n })\n }\n }\n\n const planned = ((): { selection: ContactBulkSelection; job: () => Promise<CrmBulkOutcome> } | null => {\n if (pending === 'add-tag' || pending === 'remove-tag') {\n const tag = normalizeBulkTag(value)\n if (!tag) return null\n const selection =\n pending === 'add-tag' ? planAddTag(held, tag) : planRemoveTag(held, tag)\n const set: ContactUpdateFields =\n pending === 'add-tag' ? { addTag: tag } : { removeTag: tag }\n return { selection, job: () => saveThroughRoute(selection.rows, set) }\n }\n if (pending === 'owner') {\n return {\n selection: { rows: held, skipped: [] },\n job: () => saveThroughRoute(held, { ownerUid: value }),\n }\n }\n if (pending === 'company') {\n const companyId = company?.id ?? null\n const selection = planSetCompany(held, companyId)\n return { selection, job: () => saveThroughRoute(selection.rows, { companyId }) }\n }\n if (!value) return null\n const stage = value as ContactLifecycleStage\n return {\n selection: { rows: held, skipped: [] },\n /*\n * One row at a time through the one writer of a contact's stage, on\n * the site that holds the row: the mounted site, or at the\n * organization level the row's own holder's.\n */\n job: () =>\n runCrmBulkCalls(held, contactBulkAddressOf, async (row) => {\n const site = hostId ?? (row.holderHostId || null)\n if (!site) throw new Error('no site holds this contact yet')\n await setContactStage(user, site, row.$id, stage)\n }),\n }\n })()\n if (!planned) return\n const action = pending\n setPending(null)\n await apply({\n attempted: planned.selection.rows.length,\n skipped: [...unheld, ...planned.selection.skipped].map(skippedLine),\n job: planned.job,\n done: (count) => doneSentence(action, count),\n })\n }, [\n pending,\n scope,\n selectedRows,\n consentGroup,\n value,\n company,\n saveThroughRoute,\n hostId,\n user,\n apply,\n ])\n\n const handleExport = useCallback(() => {\n downloadTextFile(\n 'contacts-selected.csv',\n 'text/csv',\n contactsCsv(selectedRows, csv),\n )\n }, [selectedRows, csv])\n\n const handleDetach = useCallback(async () => {\n if (!scope || !selectedRows.length || !consentGroup) return\n const count = selectedRows.length\n const confirmed = await confirm({\n title: count === 1 ? 'Remove this contact?' : `Remove ${count} contacts?`,\n description:\n `${count === 1 ? 'This person is' : `These ${count} people are`} ` +\n \"removed from this site's Contacts, along with their notes, tags and \" +\n 'timeline. Other sites that captured the same people keep their own ' +\n 'records. Their form submissions, orders, bookings, and membership ' +\n 'records are separate — delete those from their own pages if the ' +\n 'request covers them.',\n confirmationText: count === 1 ? 'Remove contact' : 'Remove contacts',\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 outcome = await apply({\n attempted: selectedRows.length,\n skipped: [],\n job: () =>\n runCrmBulkBatch(\n selectedRows,\n (row) => row.$id,\n contactBulkAddressOf,\n (piece) => contactRemove.removeMany(piece.map((row) => row.$id)),\n CRM_CONTACT_REMOVE_MAX,\n ),\n done: (done) => doneSentence('detach', done),\n })\n // The rows that went are gone from the table; the refused ones stay\n // selected, so the reader can see which they are and try again.\n const refused = new Set(outcome.refused.map((row) => row.label))\n onSelectedChange(\n selectedRows\n .filter((row) => refused.has(contactBulkAddressOf(row)))\n .map((row) => row.$id),\n )\n }, [scope, selectedRows, confirm, apply, contactRemove, consentGroup, onSelectedChange])\n\n const emails = selectedRows\n .map((row) => String(row.email ?? '').trim())\n .filter(Boolean)\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={\n !(pending === 'stage' && !value) &&\n !(\n (pending === 'add-tag' || pending === 'remove-tag') &&\n !normalizeBulkTag(value)\n )\n }\n onClose={() => setPending(null)}\n onApply={() => void handleApply()}\n >\n {pending === 'add-tag' || pending === 'remove-tag' ? (\n <TextField\n autoFocus\n size=\"small\"\n label=\"Tag\"\n placeholder=\"vip\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n helperText={\n pending === 'add-tag'\n ? 'Lowercased, like the tags on a profile'\n : 'Removed wherever it is present'\n }\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 error={Boolean(team.error)}\n helperText={team.error ?? (team.ready ? undefined : 'Loading the team…')}\n >\n <MenuItem value=\"\">{'Nobody — clear the owner'}</MenuItem>\n {team.options.map((member) => (\n <MenuItem key={member.uid} value={member.uid}>\n {crmMemberPickerLabel(member)}\n </MenuItem>\n ))}\n </TextField>\n ) : pending === 'stage' ? (\n <TextField\n select\n size=\"small\"\n label=\"Lifecycle stage\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n >\n {CONTACT_LIFECYCLE_STAGES.map((stage) => (\n <MenuItem key={stage} value={stage}>\n {CONTACT_LIFECYCLE_STAGE_LABELS[stage]}\n </MenuItem>\n ))}\n </TextField>\n ) : pending === 'company' ? (\n <CompanyPicker\n options={companies.options}\n ready={companies.ready}\n truncated={companies.truncated}\n value={company?.id ?? null}\n onChange={(_id, picked) => setCompany(picked)}\n onCreate={createCompany}\n helperText=\"Leave it empty to unlink the selected contacts from their companies.\"\n />\n ) : null}\n </CrmBulkValueDialog>\n {listOpen ? (\n <AddToListDialog\n open\n onClose={() => setListOpen(false)}\n hostId={createHostId}\n scope={scope}\n emails={emails}\n />\n ) : null}\n </>\n }\n >\n <Button size=\"small\" disabled={busy || !scope} onClick={() => openAction('add-tag')}>\n {'Add tag'}\n </Button>\n <Button\n size=\"small\"\n disabled={busy || !scope}\n onClick={() => openAction('remove-tag')}\n >\n {'Remove tag'}\n </Button>\n {suiteLocked ? (\n <>\n <CrmSuiteLockedButton>{'Set owner'}</CrmSuiteLockedButton>\n <CrmSuiteLockedButton>{'Set stage'}</CrmSuiteLockedButton>\n <CrmSuiteLockedButton>{'Set company'}</CrmSuiteLockedButton>\n </>\n ) : (\n <>\n <Button size=\"small\" disabled={busy || !scope} onClick={() => openAction('owner')}>\n {'Set owner'}\n </Button>\n <Button size=\"small\" disabled={busy || !scope} onClick={() => openAction('stage')}>\n {'Set stage'}\n </Button>\n <Button\n size=\"small\"\n disabled={busy || !scope}\n onClick={() => openAction('company')}\n >\n {'Set company'}\n </Button>\n </>\n )}\n <Button\n size=\"small\"\n disabled={busy || !scope || !emails.length || !createHostId}\n onClick={() => setListOpen(true)}\n >\n {'Add to list'}\n </Button>\n <CrmBulkShareButton\n object=\"contacts\"\n ids={selectedRows.map((row) => row.$id)}\n hostId={hostId ?? null}\n orgId={scope?.[1] ?? null}\n disabled={busy}\n />\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=\"contacts\"\n orgId={scope?.[1] ?? null}\n hostId={hostId}\n disabled={busy}\n />\n {/*\n Only under a site. The act is a DETACH from the viewing site's\n CRM, and at the organization level there is no viewing site — an\n org-wide member removes a person from a site by opening the record\n under that site.\n */}\n {consentGroup ? (\n <Button\n size=\"small\"\n color=\"error\"\n disabled={busy || !scope}\n onClick={() => void handleDetach()}\n >\n {'Remove from this site'}\n </Button>\n ) : null}\n </CrmBulkBarFrame>\n )\n}\nContactsBulkBarBody.displayName = 'ContactsBulkBarBody'\n\nexport default ContactsBulkBar\n"],"names":["CONTACT_LIFECYCLE_STAGE_LABELS","CONTACT_LIFECYCLE_STAGES","crmMemberPickerLabel","useConfirmationContext","Button","MenuItem","TextField","useCallback","useMemo","useState","useOrgMemberOptions","useUser","useContactRemove","useContactUpdate","useCrmBulkApply","useCrmScope","CRM_CONTACT_REMOVE_MAX","CRM_CONTACT_UPDATE_MAX","setContactStage","contactBulkAddressOf","normalizeBulkTag","planAddTag","planRemoveTag","planSetCompany","contactsCsv","downloadTextFile","runCrmBulkBatch","runCrmBulkCalls","AddToListDialog","CompanyPicker","useCompanyOptions","useCreateCompany","CrmBulkBarFrame","CrmBulkValueDialog","countNoun","CrmExportAllButton","CrmBulkShareButton","CrmSuiteLockedButton","NOUN","singular","plural","ACTION_TITLES","owner","stage","company","doneSentence","action","done","people","skippedLine","row","label","email","reason","ContactsBulkBar","props","selected","length","ContactsBulkBarBody","displayName","team","hostId","org","scope","consentGroup","rows","onSelectedChange","csv","suiteLocked","data","user","confirm","busy","report","apply","dismissReport","recordKind","contactUpdate","contactRemove","selectedRows","chosen","Set","filter","has","$id","pending","setPending","value","setValue","setCompany","listOpen","setListOpen","enabled","companies","createCompany","createHostId","openAction","saveThroughRoute","targets","set","piece","updateMany","map","handleApply","held","unheld","groupId","push","planned","tag","selection","addTag","removeTag","job","skipped","ownerUid","companyId","id","site","holderHostId","Error","attempted","count","handleExport","handleDetach","confirmed","title","description","confirmationText","confirmationButtonProps","color","then","catch","outcome","removeMany","refused","emails","String","trim","Boolean","noun","onClear","onDismissReport","extras","open","canApply","onClose","onApply","autoFocus","size","placeholder","onChange","event","target","helperText","select","error","ready","undefined","options","member","uid","truncated","_id","picked","onCreate","disabled","onClick","object","ids","orgId","resource"],"mappings":"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"}
|
|
@@ -27,6 +27,8 @@ import { crmAskClauses, crmQueryRefusals } from "../model/crm-list-query.js";
|
|
|
27
27
|
import { useCrmFoldsScope, useCrmListQuery } from "../hooks/use-crm-list-query.js";
|
|
28
28
|
import { contactPrimaryGroup, contactRecordFromDoc } from "../model/contact-record.js";
|
|
29
29
|
import { useCrmOrgMount } from "../hooks/use-crm-org-mount.js";
|
|
30
|
+
import { useCrmSharingRules } from "../hooks/use-crm-sharing.js";
|
|
31
|
+
import { crmShareChipLabel } from "../model/crm-sharing.js";
|
|
30
32
|
import { useCrmScope } from "../hooks/use-crm-scope.js";
|
|
31
33
|
import { contactsListSeed } from "../model/contacts-list-seed.js";
|
|
32
34
|
import { crmRoutes } from "../model/crm-routes.js";
|
|
@@ -535,18 +537,22 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
|
|
|
535
537
|
filterHeaders,
|
|
536
538
|
filterOptions
|
|
537
539
|
]);
|
|
538
|
-
/* One row grammar, the console's (AGL-2501) — the same table everywhere. */
|
|
540
|
+
/* One row grammar, the console's (AGL-2501) — the same table everywhere. */ // The org's sharing rules, to name the rule behind a shared row (AGL-3336).
|
|
541
|
+
const sharingRules = useCrmSharingRules(org);
|
|
542
|
+
const contactColumns = useMemo(()=>[
|
|
539
543
|
...contactListColumns({
|
|
540
544
|
memberName: members.memberName,
|
|
541
545
|
// "Known by", at the organization level only (AGL-2630).
|
|
542
|
-
siteName: mount ? mount.siteName : undefined
|
|
546
|
+
siteName: mount ? mount.siteName : undefined,
|
|
547
|
+
shareChipLabel: (chip)=>crmShareChipLabel(chip, sharingRules)
|
|
543
548
|
}),
|
|
544
549
|
// The org's custom fields as optional columns (AGL-2601).
|
|
545
550
|
...customFieldColumns(customFields.active)
|
|
546
551
|
], [
|
|
547
552
|
members.memberName,
|
|
548
553
|
mount,
|
|
549
|
-
customFields.active
|
|
554
|
+
customFields.active,
|
|
555
|
+
sharingRules
|
|
550
556
|
]);
|
|
551
557
|
/*
|
|
552
558
|
* The grid's column and sort models are the VIEW'S (AGL-2617). The
|