@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-detail-page.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { CONTACT_LIFECYCLE_STAGE_LABELS, pluginDocsHelp } from '@aglyn/aglyn'\nimport { mdiDeleteOutline, mdiMerge } from '@aglyn/shared-data-mdi'\nimport { AppLink, MdiIcon, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport type { RowActionsMenuItem } from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useFirestore, useFirestoreDoc, useUser } from '@aglyn/tenant-feature-instance'\nimport { restampCrmListFields } from '../model/list-fields-api'\nimport { crmTaskCallScope } from '../model/task-routes'\nimport { Stack, Tooltip, Typography } from '@mui/material'\nimport {\n arrayRemove,\n deleteDoc,\n deleteField,\n doc,\n updateDoc,\n} from 'firebase/firestore'\nimport { useParams, useRouter } from 'next/navigation'\nimport { useCallback, useMemo } from 'react'\nimport { useCrmActivityLogger } from '../hooks/use-crm-activity-logger'\nimport { CrmCreateSiteDefault, useCrmOrgMount } from '../hooks/use-crm-org-mount'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { contactPrimaryGroup, contactRecordFromDoc } from '../model/contact-record'\nimport { type CrmDetailPageProps, crmRoutes } from '../model/crm-routes'\nimport ContactAssociationsCard from './contact-associations-card'\nimport ContactDuplicatesCard from './contact-duplicates-card'\nimport ContactKnownByCard from './contact-known-by-card'\nimport ContactMergeDialog, { useContactMergeDialog } from './contact-merge-dialog'\nimport ContactPropertiesCard from './contact-properties-card'\nimport ContactTimelineCard from './contact-timeline-card'\nimport { AddToListButton } from './add-to-list-button'\nimport { useBookingDoor } from './crm-booking-zone'\nimport { CrmSendEmailButton } from './crm-send-email-button'\nimport { ContactDealsCard } from './contact-deals-card'\nimport { CrmCallButton } from './crm-call-actions'\nimport { CrmEmailStateChip } from './crm-email-state-chip'\nimport { CrmEmailGatewayChip } from './crm-email-check'\nimport { CrmRecordChip, CrmRecordHeader } from './crm-record-header'\nimport { CrmRecordInsightsZone } from './crm-record-insights-zone'\nimport { CrmSuiteNotice, crmSuiteIncluded, crmSuiteLockedReason } from './crm-suite-lock'\nimport { useErasePersonAction } from './erase-person-action'\nimport RecordFilesCard from './record-files-card'\nimport { RecordTasksCard } from './record-tasks-card'\nimport { useEmailsHubPath } from './use-emails-hub-path'\nimport { useOrgMembers } from './use-org-members'\n\nconst contactDocsHelp = pluginDocsHelp('contacts', {\n anchor: '#the-contacts-page',\n excerpt:\n \"One person's record: the profile your team keeps on them, where they \" +\n 'came from, what they are filed under, and what the site recorded.',\n})\n\n/**\n * `/crm/contacts/{contactId}` — one person (AGL-2596).\n *\n * A ROUTE rather than the v1 drawer, because a person is the thing every\n * other CRM record points at: a deal, a task and an activity all name a\n * contact, and a link that opens a drawer on top of a list is not a link\n * anybody can paste.\n *\n * ## What this page reads\n *\n * ONE document, by id, through the scoped rules — a single-document get is\n * judged on that document's own `visibleTo`, so no `array-contains-any`\n * filter is needed here the way it is on the list. The row is then flattened\n * through the viewing group's facet ONCE (`contactRecordFromDoc`) and the\n * cards below read the flat record: none of them reaches into\n * `facets.{groupId}` for itself, which is what keeps a card from ever\n * showing another holder's notes.\n *\n * Beyond the document: the team roster (one request, for the owner picker)\n * and the site's campaigns (one listen, for the filing picker) — both paid\n * here because opening a record IS the ask for them, and neither is paid by\n * the list.\n *\n * ## The cards\n *\n * Each in its own file, so a card can grow without this page growing:\n * properties (the editable profile, with the org's custom fields under it),\n * the relationship (sources, attribution, consent, campaign filing) and the\n * timeline. Other CRM sections add a card here by inserting one import and\n * one line.\n *\n * ## Delete is a detach\n *\n * The row is shared by every site that has captured this person, so the\n * action in the overflow menu drops THIS group's facet, consent, attribution\n * and scope tokens, and deletes the document only when nobody else is left\n * holding it — `planContactDetach` does the counting off `visibleTo`, which\n * is what both enforcement layers evaluate, and the rules refuse a delete by\n * a caller who is not the sole holder. Not the erasure path: a privacy\n * erasure removes the person regardless of who else holds them.\n */\nexport function ContactDetailPage(props: CrmDetailPageProps) {\n const { hostId, org, id, basePath } = props\n const routes = crmRoutes(basePath)\n const router = useRouter()\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n // The org root, the viewing group and what this viewer may list — the\n // merge search's and the duplicates query's one clause — from the one\n // scope hook (AGL-2614); at the organization level the group is `null`\n // and resolved per person below, and the listeners carry no clause\n // (AGL-2630).\n const {\n scope,\n orgId,\n consentGroup: viewingGroup,\n visibleTo,\n } = useCrmScope({ hostId, org })\n const mount = useCrmOrgMount()\n const merge = useContactMergeDialog()\n /*\n * THE CRM (AGL-2788). This page is the CRM's, and the shell mounts no CRM\n * page for a plan without it (AGL-2851). What locks below — owner, stage\n * and company; tasks, deals, files and custom fields; a logged call or\n * activity; one-to-one email; a merge — draws only for an org whose plan\n * lacks the CRM. The shell mounted this page once the org had settled, so\n * this is the plan, not a paint of the org still loading.\n */\n const suiteIncluded = crmSuiteIncluded(org)\n\n const {\n data: row,\n status,\n fromCache,\n } = useFirestoreDoc<Record<string, any>>(\n () => (scope ? doc(firestore, scope[0], scope[1], 'contacts', id) : null),\n [firestore, scope, id],\n { idField: '$id' },\n )\n /*\n * The controller this page is showing. Under a site: the sites declared\n * to be one sender, or this site alone — resolved from the org document\n * the shell already passed, so it costs no read. At the ORGANIZATION\n * level: the person's own PRIMARY holder, the first capturing site's\n * group, so an org-wide member reads the profile that site keeps and an\n * edit goes back to the same facet; the \"Known by\" card says which other\n * sites hold one.\n */\n const consentGroup = useMemo(\n () => viewingGroup ?? contactPrimaryGroup(row, org as Record<string, unknown>),\n [viewingGroup, row, org],\n )\n /**\n * The SITE the page acts as, for what needs one — the lead lookup, the\n * campaign filing, the site's activity feed: the mounted site, or the\n * primary holder's, or nothing for a row no site has captured.\n */\n const siteHostId = hostId ?? (consentGroup.hostId || null)\n /*\n * The feed the act is logged in, decided by the level it was performed\n * at (AGL-2738) and so given the MOUNTED site rather than `siteHostId`:\n * at the org hub a client-direct append to the holder's own site would\n * face a gate the record write never had to pass.\n */\n const logActivity = useCrmActivityLogger(hostId)\n const record = useMemo(\n () => (row ? contactRecordFromDoc(row, consentGroup) : null),\n [row, consentGroup],\n )\n const notFound = Boolean(scope) && status !== 'loading' && !row\n /** How the site the delete detaches from is named — see `handleRemove`. */\n const siteLabel = hostId\n ? 'this site'\n : siteHostId\n ? (mount?.siteName(siteHostId) ?? siteHostId)\n : 'its site'\n\n // The roster, for the owner picker and the owner's name — read because a\n // record page is the one place both are shown.\n const members = useOrgMembers(orgId, { enabled: Boolean(record) })\n /*\n * Where this person's ORDERS are read (AGL-2622): the site's orders list,\n * narrowed to their address. The count on the header is the number; the\n * list is the rows. Built from the route params already in the URL, so\n * no document is read to draw a link, and absent off a site — the\n * org-level mount has no orders list to point at.\n */\n const params = useParams<{ orgSlug?: string; host?: string }>()\n const ordersHref =\n params?.orgSlug && params?.host && record?.email\n ? Aglyn.siteRecordLinks({\n orgSlug: String(params.orgSlug),\n host: String(params.host),\n }).ordersByCustomer(record.email)\n : null\n /*\n * Where this person's BOOKINGS are read (AGL-2660): the site's Bookings\n * page narrowed to their address, under the same match rule the booking\n * row's own \"View in CRM\" uses. Offered only where the site has a booking\n * door — a site that never enabled Bookings should not carry the row. At\n * the organization level the capturing site's page, addressed through\n * the subdomain the mount knows it by.\n */\n const bookingDoor = useBookingDoor(siteHostId, org)\n const siteSlug = params?.host\n ? String(params.host)\n : siteHostId && mount\n ? mount.siteSubdomain(siteHostId)\n : null\n const bookingsHref =\n bookingDoor.open && params?.orgSlug && siteSlug && record?.email\n ? Aglyn.siteRecordLinks({\n orgSlug: String(params.orgSlug),\n host: siteSlug,\n }).bookingsByBooker(record.email)\n : null\n\n /*\n * Where a campaign entry on the timeline links to: the email's own report\n * on this site's Emails hub (AGL-2616). Only a campaign THIS site sent can\n * be addressed — a sibling site in the same consent group has an Emails\n * hub of its own under a subdomain this page does not know — so the\n * builder answers `null` for those and the entry draws unlinked.\n */\n const emailsHub = useEmailsHubPath()\n const campaignHref = useCallback(\n (email: Aglyn.ContactCampaignEmail) =>\n emailsHub && email.hostId === hostId\n ? `${emailsHub}/messages/${encodeURIComponent(email.campaignId)}`\n : null,\n [emailsHub, hostId],\n )\n\n const { data: user } = useUser()\n const handleRemove = useCallback(async () => {\n if (!row || !scope) return\n const confirmed = await confirm({\n title: 'Delete this contact?',\n description:\n `\"${record?.email ?? id}\" is removed from ${siteLabel}'s ` +\n 'Contacts, along with its notes, tags and timeline. Other sites ' +\n 'that captured the same person keep their own records. Their form ' +\n 'submissions, orders, bookings, and membership records are ' +\n 'separate — delete those from their own pages if the request ' +\n 'covers them.',\n confirmationText: 'Delete contact',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n try {\n const ref = doc(firestore, scope[0], scope[1], 'contacts', id)\n const plan = Aglyn.planContactDetach(row, consentGroup)\n if (plan.action === 'delete') {\n await deleteDoc(ref)\n } else {\n await updateDoc(ref, {\n ...Object.fromEntries(plan.remove.map((path) => [path, deleteField()])),\n visibleTo: arrayRemove(...plan.removeTokens),\n capturedByHostIds: arrayRemove(...plan.removeHostIds),\n updatedAt: new Date(),\n })\n // The facet and the scope just let go of are what the Contacts list\n // filters and searches by; the rules keep a client from restamping\n // them, so the route does (AGL-3321).\n await restampCrmListFields(user, crmTaskCallScope(hostId, orgId), 'contacts', [id])\n }\n // `siteLabel` and not \"this site\": at the organization level the line\n // lands in a feed that spans every site, so it has to name the one.\n logActivity(`Removed contact from ${siteLabel}`, {\n type: 'contact',\n id,\n name: record?.name || record?.email,\n })\n enqueueSnackbar(\n plan.action === 'delete'\n ? 'Contact deleted'\n : `Contact removed from ${siteLabel}`,\n { variant: 'success', persist: false },\n )\n router.push(routes.section('contacts'))\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n }, [\n confirm,\n consentGroup,\n enqueueSnackbar,\n firestore,\n id,\n logActivity,\n record,\n router,\n routes,\n row,\n scope,\n siteLabel,\n user,\n hostId,\n orgId,\n ])\n\n const overflowItems: RowActionsMenuItem[] = [\n /*\n * Two records for one person become one (AGL-2625). Opened empty, to be\n * searched, with the OTHER record surviving: a reader on the record they\n * want gone says \"merge this into…\". The dialog lets them switch.\n */\n {\n key: 'merge',\n label: 'Merge into…',\n icon: <MdiIcon path={mdiMerge.path} size={0.8} />,\n disabled: !row || !suiteIncluded,\n disabledReason: suiteIncluded ? 'The contact has not loaded' : crmSuiteLockedReason(),\n onClick: () => merge.open(),\n },\n {\n key: 'remove',\n label: 'Delete contact',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n disabled: !row,\n disabledReason: 'The contact has not loaded',\n onClick: () => void handleRemove(),\n },\n ]\n // The privacy erasure (AGL-2623): its item joins the overflow, its banner\n // sits under the header while a request waits, its dialog renders once.\n const erase = useErasePersonAction({\n // Filed as the site the page acts as — at the organization level the\n // person's primary holder (AGL-2630).\n hostId: siteHostId,\n orgId,\n subject: record ? { kind: 'contact', id, email: record.email } : null,\n requestedAtMs: Aglyn.readErasureRequestedAtMs(row),\n })\n\n if (notFound) {\n return (\n <CrmRecordHeader\n kind=\"Contact\"\n title={undefined}\n help={contactDocsHelp}\n backHref={routes.section('contacts')}\n backLabel=\"Back to contacts\"\n >\n {/*\n * Not \"no data\". A contact that cannot be read is a different\n * situation from one with an empty profile — it may have been\n * removed from this site, or be held only by a site this reader\n * cannot see.\n */}\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This contact could not be loaded. It may have been removed from this site.'}\n </Typography>\n </CrmRecordHeader>\n )\n }\n\n return (\n // A create opened from this page — a task, an activity, a deal — files\n // under the site that captured this person unless the reader says\n // otherwise (AGL-2630).\n <CrmCreateSiteDefault hostId={siteHostId}>\n <Stack spacing={3}>\n <CrmRecordHeader\n kind=\"Contact\"\n title={record ? record.name || record.email : undefined}\n // The identity, and the other addresses a merge folded into it.\n subtitle={\n record\n ? [record.email, ...record.alternateEmails.map((email) => `also ${email}`)].join(' · ')\n : undefined\n }\n help={contactDocsHelp}\n backHref={routes.section('contacts')}\n backLabel=\"Back to contacts\"\n actions={\n record ? (\n <>\n <AddToListButton hostId={hostId} org={org} contactId={id} email={record.email} />\n {/* Dial the number, and log the call it was (AGL-2661). */}\n <CrmCallButton\n hostId={siteHostId}\n org={org}\n link={{ contactId: id }}\n phone={record.phone}\n suiteLocked={!suiteIncluded}\n />\n <CrmSendEmailButton\n hostId={siteHostId}\n org={org}\n contactId={id}\n email={record.email}\n name={record.name}\n emailState={record.emailState}\n suiteLocked={!suiteIncluded}\n />\n </>\n ) : null\n }\n menuItems={[...overflowItems, ...erase.menuItems]}\n // The booking door (AGL-2660): this site's services, or at the\n // organization level the capturing site's.\n booking={record ? { hostId: siteHostId, org, kind: 'contact', recordId: id } : undefined}\n loading={!record}\n chips={\n record ? (\n <>\n {/* The verdict on the address (AGL-3245), first: a bounce or a\n do-not-contact mark is what a person deciding to write must see. */}\n <CrmEmailStateChip state={record.emailState} />\n {/* The mail gateway in front of the address (AGL-3328). */}\n <CrmEmailGatewayChip\n hostId={siteHostId}\n email={record.email}\n emailState={record.emailState}\n enabled={suiteIncluded}\n />\n <CrmRecordChip\n label=\"Stage\"\n value={\n record.lifecycleStage\n ? CONTACT_LIFECYCLE_STAGE_LABELS[record.lifecycleStage]\n : undefined\n }\n />\n <CrmRecordChip\n label=\"Owner\"\n value={record.ownerUid ? members.memberName(record.ownerUid) : undefined}\n />\n {/*\n The last time they opened or clicked one of this site's\n campaigns (AGL-2616) — the relationship's pulse, beside the\n owner who keeps it.\n */}\n <CrmRecordChip\n label=\"Last engaged\"\n value={\n record.lastEmailEngagementAtMs ? (\n <Tooltip title={new Date(record.lastEmailEngagementAtMs).toLocaleString()}>\n <span>\n {Aglyn.activityTimeLabel(record.lastEmailEngagementAtMs, Date.now())}\n </span>\n </Tooltip>\n ) : undefined\n }\n />\n <CrmRecordChip\n label=\"Orders\"\n value={\n record.ordersCount > 0 ? (\n ordersHref ? (\n <AppLink href={ordersHref} color=\"inherit\" underline=\"hover\">\n {`${record.ordersCount.toLocaleString()} · $${(record.ltvCents / 100).toFixed(2)} lifetime`}\n </AppLink>\n ) : (\n `${record.ordersCount.toLocaleString()} · $${(record.ltvCents / 100).toFixed(2)} lifetime`\n )\n ) : undefined\n }\n />\n </>\n ) : null\n }\n >\n {erase.banner}\n </CrmRecordHeader>\n {record && row && scope ? (\n <>\n {suiteIncluded ? null : (\n <CrmSuiteNotice>\n {'Working this contact — the owner, stage and company, tasks, ' +\n 'deals, files and custom fields, logging a call or an activity, ' +\n 'one-to-one email and merging duplicates — is part of the CRM.'}\n </CrmSuiteNotice>\n )}\n {/*\n The cross-site fact, at the organization level only (AGL-2630):\n which sites know this person, and their consent for each. Under\n a site the page IS one site's view and the card would say so on\n every record.\n */}\n {/* What an assistant proposes about the person (AGL-2917), where the plan works a contact. */}\n {suiteIncluded ? (\n <CrmRecordInsightsZone\n hostId={hostId}\n org={org as Record<string, unknown> | undefined}\n kind=\"contact\"\n recordId={id}\n name={record.name || record.email}\n taskLink={{ contactId: id }}\n />\n ) : null}\n {mount ? <ContactKnownByCard row={row} contactId={id} org={org} /> : null}\n <ContactPropertiesCard\n hostId={hostId}\n org={org}\n record={record}\n orgId={orgId}\n consentGroup={consentGroup}\n seed={{ status, fromCache }}\n members={members}\n suiteLocked={!suiteIncluded}\n />\n <ContactAssociationsCard\n hostId={siteHostId}\n record={record}\n row={row}\n consentGroup={consentGroup}\n seed={{ status, fromCache }}\n basePath={basePath}\n bookingsHref={bookingsHref}\n />\n <ContactTimelineCard\n hostId={hostId}\n org={org}\n contactId={id}\n contact={row}\n campaignHref={campaignHref}\n suiteLocked={!suiteIncluded}\n />\n {/*\n On a plan without the CRM, the cards that only work a record —\n deals, tasks, files — are named in the notice above rather than\n drawn as upsells in a row.\n */}\n {suiteIncluded ? (\n <>\n <ContactDealsCard\n hostId={hostId}\n org={org}\n basePath={basePath}\n contactId={id}\n contactName={record.name || record.email}\n />\n <RecordTasksCard hostId={hostId} org={org} basePath={basePath} contactId={id} />\n {/*\n The files are the VIEWING holder's, like everything else on this\n page: the write is a facet path, so a contract one brand filed\n never appears on a sibling brand's copy of the person.\n */}\n <RecordFilesCard\n scope={scope}\n collection=\"contacts\"\n recordId={id}\n hostId={hostId}\n facetGroupId={consentGroup.groupId}\n mediaIds={record.mediaIds}\n topic=\"contactRecord\"\n />\n </>\n ) : null}\n <ContactDuplicatesCard\n current={{ id, doc: row }}\n scope={scope}\n consentGroup={consentGroup}\n visibleTo={visibleTo}\n basePath={basePath}\n onMerge={(candidate) => merge.open({ other: candidate, keep: 'current' })}\n suiteLocked={!suiteIncluded}\n />\n <ContactMergeDialog\n {...merge.state}\n onClose={merge.close}\n hostId={siteHostId}\n current={{ id, doc: row }}\n scope={scope}\n consentGroup={consentGroup}\n visibleTo={visibleTo}\n memberName={members.memberName}\n // The page's own record was folded away: its address now opens\n // the survivor, which is where the reader is taken.\n onMerged={(survivorId) => {\n if (survivorId !== id) router.replace(routes.contact(survivorId))\n }}\n />\n </>\n ) : null}\n {erase.dialog}\n </Stack>\n </CrmCreateSiteDefault>\n )\n}\nContactDetailPage.displayName = 'ContactDetailPage'\n\nexport default ContactDetailPage\n"],"names":["Aglyn","CONTACT_LIFECYCLE_STAGE_LABELS","pluginDocsHelp","mdiDeleteOutline","mdiMerge","AppLink","MdiIcon","useConfirmationContext","useSnackbar","useFirestore","useFirestoreDoc","useUser","restampCrmListFields","crmTaskCallScope","Stack","Tooltip","Typography","arrayRemove","deleteDoc","deleteField","doc","updateDoc","useParams","useRouter","useCallback","useMemo","useCrmActivityLogger","CrmCreateSiteDefault","useCrmOrgMount","useCrmScope","contactPrimaryGroup","contactRecordFromDoc","crmRoutes","ContactAssociationsCard","ContactDuplicatesCard","ContactKnownByCard","ContactMergeDialog","useContactMergeDialog","ContactPropertiesCard","ContactTimelineCard","AddToListButton","useBookingDoor","CrmSendEmailButton","ContactDealsCard","CrmCallButton","CrmEmailStateChip","CrmEmailGatewayChip","CrmRecordChip","CrmRecordHeader","CrmRecordInsightsZone","CrmSuiteNotice","crmSuiteIncluded","crmSuiteLockedReason","useErasePersonAction","RecordFilesCard","RecordTasksCard","useEmailsHubPath","useOrgMembers","contactDocsHelp","anchor","excerpt","ContactDetailPage","props","hostId","org","id","basePath","routes","router","firestore","enqueueSnackbar","confirm","scope","orgId","consentGroup","viewingGroup","visibleTo","mount","merge","suiteIncluded","data","row","status","fromCache","idField","siteHostId","logActivity","record","notFound","Boolean","siteLabel","siteName","members","enabled","params","ordersHref","orgSlug","host","email","siteRecordLinks","String","ordersByCustomer","bookingDoor","siteSlug","siteSubdomain","bookingsHref","open","bookingsByBooker","emailsHub","campaignHref","encodeURIComponent","campaignId","user","handleRemove","confirmed","title","description","confirmationText","confirmationButtonProps","color","then","catch","ref","plan","planContactDetach","action","Object","fromEntries","remove","map","path","removeTokens","capturedByHostIds","removeHostIds","updatedAt","Date","type","name","variant","persist","push","section","error","console","allowDuplicate","overflowItems","key","label","icon","size","disabled","disabledReason","onClick","destructive","erase","subject","kind","requestedAtMs","readErasureRequestedAtMs","undefined","help","backHref","backLabel","spacing","subtitle","alternateEmails","join","actions","contactId","link","phone","suiteLocked","emailState","menuItems","booking","recordId","loading","chips","state","value","lifecycleStage","ownerUid","memberName","lastEmailEngagementAtMs","toLocaleString","span","activityTimeLabel","now","ordersCount","href","underline","ltvCents","toFixed","banner","taskLink","seed","contact","contactName","collection","facetGroupId","groupId","mediaIds","topic","current","onMerge","candidate","other","keep","onClose","close","onMerged","survivorId","replace","dialog","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contact-detail-page.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { CONTACT_LIFECYCLE_STAGE_LABELS, pluginDocsHelp } from '@aglyn/aglyn'\nimport {\n pluginRecordByEmailHref,\n pluginRecordHref,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport { mdiDeleteOutline, mdiMerge } from '@aglyn/shared-data-mdi'\nimport { AppLink, MdiIcon, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport type { RowActionsMenuItem } from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useFirestore, useFirestoreDoc } from '@aglyn/tenant-feature-instance'\nimport { Stack, Tooltip, Typography } from '@mui/material'\nimport { doc } from 'firebase/firestore'\nimport { useParams, useRouter } from 'next/navigation'\nimport { useCallback, useMemo } from 'react'\nimport { useContactRemove } from '../hooks/use-contact-remove'\nimport { useCrmActivityLogger } from '../hooks/use-crm-activity-logger'\nimport { CrmCreateSiteDefault, useCrmOrgMount } from '../hooks/use-crm-org-mount'\nimport { crmShareChipFor } from '../model/crm-sharing'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { contactPrimaryGroup, contactRecordFromDoc } from '../model/contact-record'\nimport { type CrmDetailPageProps, crmRoutes } from '../model/crm-routes'\nimport ContactAssociationsCard from './contact-associations-card'\nimport ContactDuplicatesCard from './contact-duplicates-card'\nimport ContactKnownByCard from './contact-known-by-card'\nimport ContactMergeDialog, { useContactMergeDialog } from './contact-merge-dialog'\nimport ContactPropertiesCard from './contact-properties-card'\nimport ContactTimelineCard from './contact-timeline-card'\nimport { AddToListButton } from './add-to-list-button'\nimport { useBookingDoor } from './crm-booking-zone'\nimport { CrmSendEmailButton } from './crm-send-email-button'\nimport { ContactDealsCard } from './contact-deals-card'\nimport { CrmCallButton } from './crm-call-actions'\nimport { CrmEmailStateChip } from './crm-email-state-chip'\nimport { CrmEmailGatewayChip } from './crm-email-check'\nimport { CrmRecordChip, CrmRecordHeader } from './crm-record-header'\nimport { CrmRecordInsightsZone } from './crm-record-insights-zone'\nimport { CrmSuiteNotice, crmSuiteIncluded, crmSuiteLockedReason } from './crm-suite-lock'\nimport { useErasePersonAction } from './erase-person-action'\nimport RecordFilesCard from './record-files-card'\nimport { CrmShareChipView, RecordSharingCard } from './record-sharing-card'\nimport { RecordTasksCard } from './record-tasks-card'\nimport { useSiteRouteContext } from './use-site-route-context'\nimport { useOrgMembers } from './use-org-members'\n\nconst contactDocsHelp = pluginDocsHelp('contacts', {\n anchor: '#the-contacts-page',\n excerpt:\n \"One person's record: the profile your team keeps on them, where they \" +\n 'came from, what they are filed under, and what the site recorded.',\n})\n\n/**\n * `/crm/contacts/{contactId}` — one person (AGL-2596).\n *\n * A ROUTE rather than the v1 drawer, because a person is the thing every\n * other CRM record points at: a deal, a task and an activity all name a\n * contact, and a link that opens a drawer on top of a list is not a link\n * anybody can paste.\n *\n * ## What this page reads\n *\n * ONE document, by id, through the scoped rules — a single-document get is\n * judged on that document's own `visibleTo`, so no `array-contains-any`\n * filter is needed here the way it is on the list. The row is then flattened\n * through the viewing group's facet ONCE (`contactRecordFromDoc`) and the\n * cards below read the flat record: none of them reaches into\n * `facets.{groupId}` for itself, which is what keeps a card from ever\n * showing another holder's notes.\n *\n * Beyond the document: the team roster (one request, for the owner picker)\n * and the site's campaigns (one listen, for the filing picker) — both paid\n * here because opening a record IS the ask for them, and neither is paid by\n * the list.\n *\n * ## The cards\n *\n * Each in its own file, so a card can grow without this page growing:\n * properties (the editable profile, with the org's custom fields under it),\n * the relationship (sources, attribution, consent, campaign filing) and the\n * timeline. Other CRM sections add a card here by inserting one import and\n * one line.\n *\n * ## Delete is a detach\n *\n * The row is shared by every site that has captured this person, so the\n * action in the overflow menu drops THIS group's facet, consent, attribution\n * and scope tokens, and deletes the document only when nobody else is left\n * holding it. `crm/contact-remove` decides which against the stored\n * document and keeps any marketing refusal the person gave (AGL-3338); the\n * rules refuse a client the write. Not the erasure path: a privacy erasure\n * removes the person regardless of who else holds them.\n */\nexport function ContactDetailPage(props: CrmDetailPageProps) {\n const { hostId, org, id, basePath } = props\n const routes = crmRoutes(basePath)\n const router = useRouter()\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n // The org root, the viewing group and what this viewer may list — the\n // merge search's and the duplicates query's one clause — from the one\n // scope hook (AGL-2614); at the organization level the group is `null`\n // and resolved per person below, and the listeners carry no clause\n // (AGL-2630).\n const {\n scope,\n orgId,\n consentGroup: viewingGroup,\n visibleTo,\n } = useCrmScope({ hostId, org })\n const mount = useCrmOrgMount()\n const merge = useContactMergeDialog()\n /*\n * THE CRM (AGL-2788). This page is the CRM's, and the shell mounts no CRM\n * page for a plan without it (AGL-2851). What locks below — owner, stage\n * and company; tasks, deals, files and custom fields; a logged call or\n * activity; one-to-one email; a merge — draws only for an org whose plan\n * lacks the CRM. The shell mounted this page once the org had settled, so\n * this is the plan, not a paint of the org still loading.\n */\n const suiteIncluded = crmSuiteIncluded(org)\n\n const {\n data: row,\n status,\n fromCache,\n } = useFirestoreDoc<Record<string, any>>(\n () => (scope ? doc(firestore, scope[0], scope[1], 'contacts', id) : null),\n [firestore, scope, id],\n { idField: '$id' },\n )\n /*\n * The controller this page is showing. Under a site: the sites declared\n * to be one sender, or this site alone — resolved from the org document\n * the shell already passed, so it costs no read. At the ORGANIZATION\n * level: the person's own PRIMARY holder, the first capturing site's\n * group, so an org-wide member reads the profile that site keeps and an\n * edit goes back to the same facet; the \"Known by\" card says which other\n * sites hold one.\n */\n const consentGroup = useMemo(\n () => viewingGroup ?? contactPrimaryGroup(row, org as Record<string, unknown>),\n [viewingGroup, row, org],\n )\n /**\n * The SITE the page acts as, for what needs one — the lead lookup, the\n * campaign filing, the site's activity feed: the mounted site, or the\n * primary holder's, or nothing for a row no site has captured.\n */\n const siteHostId = hostId ?? (consentGroup.hostId || null)\n /*\n * The feed the act is logged in, decided by the level it was performed\n * at (AGL-2738) and so given the MOUNTED site rather than `siteHostId`:\n * at the org hub a client-direct append to the holder's own site would\n * face a gate the record write never had to pass.\n */\n const logActivity = useCrmActivityLogger(hostId)\n const record = useMemo(\n () => (row ? contactRecordFromDoc(row, consentGroup) : null),\n [row, consentGroup],\n )\n const notFound = Boolean(scope) && status !== 'loading' && !row\n /** How the site the delete detaches from is named — see `handleRemove`. */\n const siteLabel = hostId\n ? 'this site'\n : siteHostId\n ? (mount?.siteName(siteHostId) ?? siteHostId)\n : 'its site'\n\n // The roster, for the owner picker and the owner's name — read because a\n // record page is the one place both are shown.\n const members = useOrgMembers(orgId, { enabled: Boolean(record) })\n /*\n * Where this person's ORDERS are read (AGL-2622): the site's orders list,\n * narrowed to their address, at the address the plugin that keeps orders\n * publishes. The count on the header is the number; the list is the rows.\n * Built from the route params already in the URL, so no document is read\n * to draw a link, and absent off a site — the org-level mount has no\n * orders list to point at — and where no plugin keeps orders.\n */\n const params = useParams<{ orgSlug?: string; host?: string }>()\n const ordersHref =\n params?.orgSlug && params?.host && record?.email\n ? pluginRecordByEmailHref(\n 'order',\n { orgSlug: String(params.orgSlug), host: String(params.host) },\n record.email,\n )\n : null\n /*\n * Where this person's BOOKINGS are read (AGL-2660): the site's Bookings\n * page narrowed to their address, under the same match rule the booking\n * row's own \"View in CRM\" uses. Offered only where the site has a booking\n * door — a site that never enabled Bookings should not carry the row. At\n * the organization level the capturing site's page, addressed through\n * the subdomain the mount knows it by.\n */\n const bookingDoor = useBookingDoor(siteHostId, org)\n const siteSlug = params?.host\n ? String(params.host)\n : siteHostId && mount\n ? mount.siteSubdomain(siteHostId)\n : null\n const bookingsHref =\n bookingDoor.open && params?.orgSlug && siteSlug && record?.email\n ? pluginRecordByEmailHref(\n 'booking',\n { orgSlug: String(params.orgSlug), host: siteSlug },\n record.email,\n )\n : null\n\n /*\n * Where a campaign entry on the timeline links to: the email's own report\n * (AGL-2616), at the address the plugin that keeps messages publishes as\n * `emailMessage` (AGL-3080). Only a campaign THIS site sent can be\n * addressed — a sibling site in the same consent group has pages of its\n * own under a subdomain this page does not know — so the builder answers\n * `null` for those, and where nothing publishes the kind, and the entry\n * draws unlinked.\n */\n const siteRoute = useSiteRouteContext()\n const campaignHref = useCallback(\n (email: Aglyn.ContactCampaignEmail) =>\n siteRoute && email.hostId === hostId\n ? pluginRecordHref('emailMessage', siteRoute, email.campaignId)\n : null,\n [siteRoute, hostId],\n )\n\n const contactRemove = useContactRemove(hostId)\n const handleRemove = useCallback(async () => {\n if (!row || !scope) return\n const confirmed = await confirm({\n title: 'Delete this contact?',\n description:\n `\"${record?.email ?? id}\" is removed from ${siteLabel}'s ` +\n 'Contacts, along with its notes, tags and timeline. Other sites ' +\n 'that captured the same person keep their own records. Their form ' +\n 'submissions, orders, bookings, and membership records are ' +\n 'separate — delete those from their own pages if the request ' +\n 'covers them.',\n confirmationText: 'Delete contact',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n try {\n // The route decides against the stored document, keeps any refusal,\n // and restamps what the Contacts list filters by (AGL-3338).\n const removed = await contactRemove.removeOne(id)\n // `siteLabel` and not \"this site\": at the organization level the line\n // lands in a feed that spans every site, so it has to name the one.\n logActivity(`Removed contact from ${siteLabel}`, {\n type: 'contact',\n id,\n name: record?.name || record?.email,\n })\n enqueueSnackbar(\n removed === 'deleted'\n ? 'Contact deleted'\n : `Contact removed from ${siteLabel}`,\n { variant: 'success', persist: false },\n )\n router.push(routes.section('contacts'))\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n }, [\n confirm,\n contactRemove,\n enqueueSnackbar,\n id,\n logActivity,\n record,\n router,\n routes,\n row,\n scope,\n siteLabel,\n ])\n\n const overflowItems: RowActionsMenuItem[] = [\n /*\n * Two records for one person become one (AGL-2625). Opened empty, to be\n * searched, with the OTHER record surviving: a reader on the record they\n * want gone says \"merge this into…\". The dialog lets them switch.\n */\n {\n key: 'merge',\n label: 'Merge into…',\n icon: <MdiIcon path={mdiMerge.path} size={0.8} />,\n disabled: !row || !suiteIncluded,\n disabledReason: suiteIncluded ? 'The contact has not loaded' : crmSuiteLockedReason(),\n onClick: () => merge.open(),\n },\n {\n key: 'remove',\n label: 'Delete contact',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n disabled: !row,\n disabledReason: 'The contact has not loaded',\n onClick: () => void handleRemove(),\n },\n ]\n // The privacy erasure (AGL-2623): its item joins the overflow, its banner\n // sits under the header while a request waits, its dialog renders once.\n const erase = useErasePersonAction({\n // Filed as the site the page acts as — at the organization level the\n // person's primary holder (AGL-2630).\n hostId: siteHostId,\n orgId,\n subject: record ? { kind: 'contact', id, email: record.email } : null,\n requestedAtMs: Aglyn.readErasureRequestedAtMs(row),\n })\n\n if (notFound) {\n return (\n <CrmRecordHeader\n kind=\"Contact\"\n title={undefined}\n help={contactDocsHelp}\n backHref={routes.section('contacts')}\n backLabel=\"Back to contacts\"\n >\n {/*\n * Not \"no data\". A contact that cannot be read is a different\n * situation from one with an empty profile — it may have been\n * removed from this site, or be held only by a site this reader\n * cannot see.\n */}\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This contact could not be loaded. It may have been removed from this site.'}\n </Typography>\n </CrmRecordHeader>\n )\n }\n\n return (\n // A create opened from this page — a task, an activity, a deal — files\n // under the site that captured this person unless the reader says\n // otherwise (AGL-2630).\n <CrmCreateSiteDefault hostId={siteHostId}>\n <Stack spacing={3}>\n <CrmRecordHeader\n kind=\"Contact\"\n title={record ? record.name || record.email : undefined}\n // The identity, and the other addresses a merge folded into it.\n subtitle={\n record\n ? [record.email, ...record.alternateEmails.map((email) => `also ${email}`)].join(' · ')\n : undefined\n }\n help={contactDocsHelp}\n backHref={routes.section('contacts')}\n backLabel=\"Back to contacts\"\n actions={\n record ? (\n <>\n <AddToListButton hostId={hostId} org={org} contactId={id} email={record.email} />\n {/* Dial the number, and log the call it was (AGL-2661). */}\n <CrmCallButton\n hostId={siteHostId}\n org={org}\n link={{ contactId: id }}\n phone={record.phone}\n suiteLocked={!suiteIncluded}\n />\n <CrmSendEmailButton\n hostId={siteHostId}\n org={org}\n contactId={id}\n email={record.email}\n name={record.name}\n emailState={record.emailState}\n suiteLocked={!suiteIncluded}\n />\n </>\n ) : null\n }\n menuItems={[...overflowItems, ...erase.menuItems]}\n // The booking door (AGL-2660): this site's services, or at the\n // organization level the capturing site's.\n booking={record ? { hostId: siteHostId, org, kind: 'contact', recordId: id } : undefined}\n loading={!record}\n chips={\n record ? (\n <>\n {/* The verdict on the address (AGL-3245), first: a bounce or a\n do-not-contact mark is what a person deciding to write must see. */}\n <CrmEmailStateChip state={record.emailState} />\n {/* Seen here only through a share (AGL-3336): who shared it. */}\n <CrmShareChipView chip={crmShareChipFor(row, viewingGroup?.hostIds ?? [])} org={org} />\n {/* The mail gateway in front of the address (AGL-3328). */}\n <CrmEmailGatewayChip\n hostId={siteHostId}\n email={record.email}\n emailState={record.emailState}\n enabled={suiteIncluded}\n />\n <CrmRecordChip\n label=\"Stage\"\n value={\n record.lifecycleStage\n ? CONTACT_LIFECYCLE_STAGE_LABELS[record.lifecycleStage]\n : undefined\n }\n />\n <CrmRecordChip\n label=\"Owner\"\n value={record.ownerUid ? members.memberName(record.ownerUid) : undefined}\n />\n {/*\n The last time they opened or clicked one of this site's\n campaigns (AGL-2616) — the relationship's pulse, beside the\n owner who keeps it.\n */}\n <CrmRecordChip\n label=\"Last engaged\"\n value={\n record.lastEmailEngagementAtMs ? (\n <Tooltip title={new Date(record.lastEmailEngagementAtMs).toLocaleString()}>\n <span>\n {Aglyn.activityTimeLabel(record.lastEmailEngagementAtMs, Date.now())}\n </span>\n </Tooltip>\n ) : undefined\n }\n />\n <CrmRecordChip\n label=\"Orders\"\n value={\n record.ordersCount > 0 ? (\n ordersHref ? (\n <AppLink href={ordersHref} color=\"inherit\" underline=\"hover\">\n {`${record.ordersCount.toLocaleString()} · $${(record.ltvCents / 100).toFixed(2)} lifetime`}\n </AppLink>\n ) : (\n `${record.ordersCount.toLocaleString()} · $${(record.ltvCents / 100).toFixed(2)} lifetime`\n )\n ) : undefined\n }\n />\n </>\n ) : null\n }\n >\n {erase.banner}\n </CrmRecordHeader>\n {record && row && scope ? (\n <>\n {suiteIncluded ? null : (\n <CrmSuiteNotice>\n {'Working this contact — the owner, stage and company, tasks, ' +\n 'deals, files and custom fields, logging a call or an activity, ' +\n 'one-to-one email and merging duplicates — is part of the CRM.'}\n </CrmSuiteNotice>\n )}\n {/*\n The cross-site fact, at the organization level only (AGL-2630):\n which sites know this person, and their consent for each. Under\n a site the page IS one site's view and the card would say so on\n every record.\n */}\n {/* What an assistant proposes about the person (AGL-2917), where the plan works a contact. */}\n {suiteIncluded ? (\n <CrmRecordInsightsZone\n hostId={hostId}\n org={org as Record<string, unknown> | undefined}\n kind=\"contact\"\n recordId={id}\n name={record.name || record.email}\n taskLink={{ contactId: id }}\n />\n ) : null}\n {mount ? <ContactKnownByCard row={row} contactId={id} org={org} /> : null}\n {/* Where the person is visible and why, and a manager's share (AGL-3336). */}\n <RecordSharingCard\n object=\"contacts\"\n id={id}\n record={row}\n hostId={hostId ?? null}\n orgId={orgId}\n org={org}\n viewingHostIds={viewingGroup?.hostIds}\n />\n <ContactPropertiesCard\n hostId={hostId}\n org={org}\n record={record}\n orgId={orgId}\n consentGroup={consentGroup}\n seed={{ status, fromCache }}\n members={members}\n suiteLocked={!suiteIncluded}\n />\n <ContactAssociationsCard\n hostId={siteHostId}\n record={record}\n row={row}\n consentGroup={consentGroup}\n seed={{ status, fromCache }}\n basePath={basePath}\n bookingsHref={bookingsHref}\n />\n <ContactTimelineCard\n hostId={hostId}\n org={org}\n contactId={id}\n contact={row}\n campaignHref={campaignHref}\n suiteLocked={!suiteIncluded}\n />\n {/*\n On a plan without the CRM, the cards that only work a record —\n deals, tasks, files — are named in the notice above rather than\n drawn as upsells in a row.\n */}\n {suiteIncluded ? (\n <>\n <ContactDealsCard\n hostId={hostId}\n org={org}\n basePath={basePath}\n contactId={id}\n contactName={record.name || record.email}\n />\n <RecordTasksCard hostId={hostId} org={org} basePath={basePath} contactId={id} />\n {/*\n The files are the VIEWING holder's, like everything else on this\n page: the write is a facet path, so a contract one brand filed\n never appears on a sibling brand's copy of the person.\n */}\n <RecordFilesCard\n scope={scope}\n collection=\"contacts\"\n recordId={id}\n hostId={hostId}\n facetGroupId={consentGroup.groupId}\n mediaIds={record.mediaIds}\n topic=\"contactRecord\"\n />\n </>\n ) : null}\n <ContactDuplicatesCard\n current={{ id, doc: row }}\n scope={scope}\n consentGroup={consentGroup}\n visibleTo={visibleTo}\n basePath={basePath}\n onMerge={(candidate) => merge.open({ other: candidate, keep: 'current' })}\n suiteLocked={!suiteIncluded}\n />\n <ContactMergeDialog\n {...merge.state}\n onClose={merge.close}\n hostId={siteHostId}\n current={{ id, doc: row }}\n scope={scope}\n consentGroup={consentGroup}\n visibleTo={visibleTo}\n memberName={members.memberName}\n // The page's own record was folded away: its address now opens\n // the survivor, which is where the reader is taken.\n onMerged={(survivorId) => {\n if (survivorId !== id) router.replace(routes.contact(survivorId))\n }}\n />\n </>\n ) : null}\n {erase.dialog}\n </Stack>\n </CrmCreateSiteDefault>\n )\n}\nContactDetailPage.displayName = 'ContactDetailPage'\n\nexport default ContactDetailPage\n"],"names":["Aglyn","CONTACT_LIFECYCLE_STAGE_LABELS","pluginDocsHelp","pluginRecordByEmailHref","pluginRecordHref","mdiDeleteOutline","mdiMerge","AppLink","MdiIcon","useConfirmationContext","useSnackbar","useFirestore","useFirestoreDoc","Stack","Tooltip","Typography","doc","useParams","useRouter","useCallback","useMemo","useContactRemove","useCrmActivityLogger","CrmCreateSiteDefault","useCrmOrgMount","crmShareChipFor","useCrmScope","contactPrimaryGroup","contactRecordFromDoc","crmRoutes","ContactAssociationsCard","ContactDuplicatesCard","ContactKnownByCard","ContactMergeDialog","useContactMergeDialog","ContactPropertiesCard","ContactTimelineCard","AddToListButton","useBookingDoor","CrmSendEmailButton","ContactDealsCard","CrmCallButton","CrmEmailStateChip","CrmEmailGatewayChip","CrmRecordChip","CrmRecordHeader","CrmRecordInsightsZone","CrmSuiteNotice","crmSuiteIncluded","crmSuiteLockedReason","useErasePersonAction","RecordFilesCard","CrmShareChipView","RecordSharingCard","RecordTasksCard","useSiteRouteContext","useOrgMembers","contactDocsHelp","anchor","excerpt","ContactDetailPage","props","hostId","org","id","basePath","routes","router","firestore","enqueueSnackbar","confirm","scope","orgId","consentGroup","viewingGroup","visibleTo","mount","merge","suiteIncluded","data","row","status","fromCache","idField","siteHostId","logActivity","record","notFound","Boolean","siteLabel","siteName","members","enabled","params","ordersHref","orgSlug","host","email","String","bookingDoor","siteSlug","siteSubdomain","bookingsHref","open","siteRoute","campaignHref","campaignId","contactRemove","handleRemove","confirmed","title","description","confirmationText","confirmationButtonProps","color","then","catch","removed","removeOne","type","name","variant","persist","push","section","error","console","allowDuplicate","overflowItems","key","label","icon","path","size","disabled","disabledReason","onClick","destructive","erase","subject","kind","requestedAtMs","readErasureRequestedAtMs","undefined","help","backHref","backLabel","spacing","subtitle","alternateEmails","map","join","actions","contactId","link","phone","suiteLocked","emailState","menuItems","booking","recordId","loading","chips","state","chip","hostIds","value","lifecycleStage","ownerUid","memberName","lastEmailEngagementAtMs","Date","toLocaleString","span","activityTimeLabel","now","ordersCount","href","underline","ltvCents","toFixed","banner","taskLink","object","viewingHostIds","seed","contact","contactName","collection","facetGroupId","groupId","mediaIds","topic","current","onMerge","candidate","other","keep","onClose","close","onMerged","survivorId","replace","dialog","displayName"],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { GridColDef } from '@mui/x-data-grid';
|
|
2
|
+
import type { CrmShareChip } from '../model/crm-sharing';
|
|
2
3
|
/**
|
|
3
4
|
* The filterable fields that get a column. The rest of
|
|
4
5
|
* `CONTACT_LIST_FILTER_FIELDS` still reaches the filter panel, hidden.
|
|
@@ -30,6 +31,8 @@ export interface ContactListColumnOptions {
|
|
|
30
31
|
* is known by the site and the column would say so on every line.
|
|
31
32
|
*/
|
|
32
33
|
siteName?: (hostId: string) => string;
|
|
34
|
+
/** The words of a row's "Shared by…" chip (AGL-3336); absent draws none. */
|
|
35
|
+
shareChipLabel?: (chip: CrmShareChip) => string;
|
|
33
36
|
/**
|
|
34
37
|
* One clock for every row of one paint, so two contacts engaged a second
|
|
35
38
|
* apart cannot read "just now" and "1 min ago". Defaults to the paint's
|
|
@@ -63,7 +63,7 @@ import { nextActivityColumn } from "./crm-next-activity-column.js";
|
|
|
63
63
|
* (`contactQueryClause`, AGL-3321).
|
|
64
64
|
*/ export function contactListColumns(options) {
|
|
65
65
|
var _options_nowMs;
|
|
66
|
-
const { memberName, siteName } = options;
|
|
66
|
+
const { memberName, siteName, shareChipLabel } = options;
|
|
67
67
|
const nowMs = (_options_nowMs = options.nowMs) != null ? _options_nowMs : Date.now();
|
|
68
68
|
return [
|
|
69
69
|
_extends({
|
|
@@ -80,12 +80,28 @@ import { nextActivityColumn } from "./crm-next-activity-column.js";
|
|
|
80
80
|
lineHeight: 1.25
|
|
81
81
|
},
|
|
82
82
|
children: [
|
|
83
|
-
/*#__PURE__*/
|
|
84
|
-
|
|
83
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
84
|
+
direction: "row",
|
|
85
|
+
spacing: 1,
|
|
85
86
|
sx: {
|
|
86
|
-
|
|
87
|
+
alignItems: 'center',
|
|
88
|
+
minWidth: 0
|
|
87
89
|
},
|
|
88
|
-
children:
|
|
90
|
+
children: [
|
|
91
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
92
|
+
variant: "body2",
|
|
93
|
+
sx: {
|
|
94
|
+
lineHeight: 1.25
|
|
95
|
+
},
|
|
96
|
+
children: row.name || row.email
|
|
97
|
+
}),
|
|
98
|
+
row.shareChip && shareChipLabel ? /*#__PURE__*/ _jsx(Chip, {
|
|
99
|
+
size: "small",
|
|
100
|
+
variant: "outlined",
|
|
101
|
+
color: "info",
|
|
102
|
+
label: shareChipLabel(row.shareChip)
|
|
103
|
+
}) : null
|
|
104
|
+
]
|
|
89
105
|
}),
|
|
90
106
|
row.name ? /*#__PURE__*/ _jsx(Typography, {
|
|
91
107
|
variant: "caption",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contact-list-columns.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n activityTimeLabel,\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CONTACT_SOURCE_LABELS,\n EMAIL_STATE_LABELS,\n type ContactSource,\n} from '@aglyn/aglyn'\nimport {\n hiddenFilterColumns,\n listFilterColumn,\n} from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { Chip, Stack, Tooltip, Typography } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport {\n CONTACT_LIST_FILTER_FIELDS,\n CONTACT_LIST_FILTER_HEADERS,\n} from '../constants/contact-filters'\nimport type { ContactRecord } from '../model/contact-record'\nimport { CrmEmailStateChip } from './crm-email-state-chip'\nimport { nextActivityColumn } from './crm-next-activity-column'\n\n/**\n * The filterable fields that get a column. The rest of\n * `CONTACT_LIST_FILTER_FIELDS` still reaches the filter panel, hidden.\n */\n/**\n * The declared filter fields that ARE table columns, so the hidden\n * filter-only columns are built for the others alone. Owner and Stage\n * joined it when the grammar learned them (AGL-2617): each is a shown\n * column already, and a second, hidden one under the same field would be\n * a duplicate the grid refuses.\n */\nexport const CONTACT_FILTER_COLUMNS = [\n 'name',\n 'ownerUid',\n 'lifecycleStage',\n 'sources',\n 'tags',\n 'updatedAt',\n 'nextTaskAtMs',\n 'emailState',\n]\n\n/**\n * The columns that ship HIDDEN, for the grid's visibility model (AGL-2616).\n *\n * \"Last engaged\" is a fact most lists do not need on screen and every list\n * can turn on from the column menu; shipping it visible would widen a\n * table that already carries seven columns for a figure only a marketing\n * reader scans.\n */\nexport const CONTACT_OPTIONAL_COLUMNS = ['lastEmailEngagementAtMs']\n\nexport interface ContactListColumnOptions {\n /** The owner's name for a uid — the roster's, or the uid itself. */\n memberName: (uid: string) => string\n /**\n * How a site reads, by document id — handed in at the ORGANIZATION level\n * only (AGL-2630), where it turns on the \"Known by\" column: the sites\n * that have captured each person. Absent under a site, where every row\n * is known by the site and the column would say so on every line.\n */\n siteName?: (hostId: string) => string\n /**\n * One clock for every row of one paint, so two contacts engaged a second\n * apart cannot read \"just now\" and \"1 min ago\". Defaults to the paint's\n * own `Date.now()`.\n */\n nowMs?: number\n}\n\n/**\n * The contacts list's columns (AGL-2596), in their own module so the list\n * file stays about the QUERY and the toolbar while other surfaces — a bulk\n * bar, an import — grow beside it.\n *\n * Owner and Stage read off the flattened row, which is already this\n * holder's facet (`contactRecordFromDoc`), so neither column can show\n * another holder's assignment. Filtering by either is the list's: the panel\n * names the field and the list asks the holder's facet key of its query\n * (`contactQueryClause`, AGL-3321).\n */\nexport function contactListColumns(\n options: ContactListColumnOptions,\n): GridColDef[] {\n const { memberName, siteName } = options\n const nowMs = options.nowMs ?? Date.now()\n return [\n {\n field: 'name',\n headerName: 'Contact',\n flex: 1.6,\n minWidth: 220,\n ...listFilterColumn(CONTACT_LIST_FILTER_FIELDS, 'name'),\n valueGetter: (_value, row: ContactRecord) =>\n String(row.name || row.email || ''),\n renderCell: ({ row }: { row: ContactRecord }) => (\n <Stack sx={{ justifyContent: 'center', height: '100%', lineHeight: 1.25 }}>\n <Typography variant=\"body2\" sx={{ lineHeight: 1.25 }}>\n {row.name || row.email}\n </Typography>\n {row.name ? (\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ lineHeight: 1.25 }}\n noWrap\n >\n {row.email}\n </Typography>\n ) : null}\n </Stack>\n ),\n },\n {\n field: 'ownerUid',\n headerName: 'Owner',\n flex: 0.9,\n minWidth: 140,\n filterable: false,\n valueGetter: (_value, row: ContactRecord) =>\n row.ownerUid ? memberName(row.ownerUid) : '',\n renderCell: ({ row }: { row: ContactRecord }) =>\n row.ownerUid ? (\n <Typography variant=\"body2\" noWrap>\n {memberName(row.ownerUid)}\n </Typography>\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'—'}\n </Typography>\n ),\n },\n {\n field: 'lifecycleStage',\n headerName: 'Stage',\n flex: 0.9,\n minWidth: 150,\n filterable: false,\n valueGetter: (_value, row: ContactRecord) =>\n row.lifecycleStage ? CONTACT_LIFECYCLE_STAGE_LABELS[row.lifecycleStage] : '',\n renderCell: ({ row }: { row: ContactRecord }) =>\n row.lifecycleStage ? (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n label={CONTACT_LIFECYCLE_STAGE_LABELS[row.lifecycleStage]}\n />\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'—'}\n </Typography>\n ),\n },\n /*\n * KNOWN BY (AGL-2630): the sites that have captured this person, which\n * is the cross-site fact the organization-level list exists to show —\n * one deduplicated row per person, and which of the org's sites know\n * them. `capturedByHostIds` is a top-level array precisely so the\n * question can be answered without opening a facet. Not filterable from\n * the panel: an `array-contains` on it would displace the scope clause\n * under a site, and a search or a facet filter at the org level (one\n * array clause per query, AGL-3321). A row that names no site predates attribution and says so,\n * rather than reading as \"every site\".\n */\n ...(siteName\n ? [\n {\n field: 'capturedByHostIds',\n headerName: 'Known by',\n flex: 1.1,\n minWidth: 200,\n sortable: false,\n filterable: false,\n valueGetter: (_value: unknown, row: ContactRecord) =>\n row.capturedByHostIds.map(siteName).join(', '),\n renderCell: ({ row }: { row: ContactRecord }) =>\n row.capturedByHostIds.length ? (\n <Stack\n direction=\"row\"\n spacing={0.5}\n sx={{ alignItems: 'center', height: '100%', flexWrap: 'nowrap' }}\n >\n {row.capturedByHostIds.slice(0, 2).map((hostId) => (\n <Chip key={hostId} size=\"small\" label={siteName(hostId)} />\n ))}\n {row.capturedByHostIds.length > 2 ? (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n label={`+${row.capturedByHostIds.length - 2}`}\n />\n ) : null}\n </Stack>\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'No site recorded'}\n </Typography>\n ),\n } satisfies GridColDef,\n ]\n : []),\n {\n field: 'sources',\n headerName: 'Sources',\n flex: 1,\n minWidth: 160,\n // A map of provenance flags, not a scalar — `sources.form == true` is\n // queryable one key at a time, which is a menu of its own rather than\n // a filter on this column.\n filterable: false,\n sortable: false,\n valueGetter: (_value, row: ContactRecord) =>\n Object.keys(row.sources ?? {}).join(', '),\n renderCell: ({ row }: { row: ContactRecord }) => (\n <Stack\n direction=\"row\"\n spacing={0.5}\n sx={{ alignItems: 'center', height: '100%' }}\n >\n {Object.keys(row.sources ?? {}).map((source) => (\n <Chip\n key={source}\n label={CONTACT_SOURCE_LABELS[source as ContactSource] ?? source}\n size=\"small\"\n />\n ))}\n </Stack>\n ),\n },\n {\n field: 'tags',\n headerName: 'Tags',\n flex: 1,\n minWidth: 150,\n ...listFilterColumn(CONTACT_LIST_FILTER_FIELDS, 'tags'),\n sortable: false,\n valueGetter: (_value, row: ContactRecord) => (row.tags ?? []).join(', '),\n renderCell: ({ row }: { row: ContactRecord }) =>\n (row.tags ?? []).slice(0, 3).join(', '),\n },\n {\n field: 'updatedAt',\n headerName: 'Last activity',\n flex: 0.8,\n minWidth: 150,\n // `type: 'date'` is what gives the panel a date PICKER rather than a\n // free-text box for a value the query reads as a day.\n type: 'date',\n ...listFilterColumn(CONTACT_LIST_FILTER_FIELDS, 'updatedAt'),\n valueGetter: (_value, row: ContactRecord) => {\n const seconds = (row.updatedAt as { seconds?: number } | undefined)?.seconds\n return seconds ? new Date(seconds * 1000) : null\n },\n renderCell: ({ row }: { row: ContactRecord }) => (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {row.interactions?.[0]\n ? new Date(row.interactions[0].atMs).toLocaleDateString()\n : '—'}\n </Typography>\n ),\n },\n // When the earliest open task against the person is due (AGL-2661).\n nextActivityColumn(nowMs),\n {\n /*\n * When the person last opened or clicked one of this holder's\n * campaigns (AGL-2616). Off the facet like Owner and Stage, so it can\n * never show another holder's readers; `filterable: false` for the\n * same reason theirs are — a facet path is not a query the panel can\n * make. A date column, so a sort on it orders by the instant and an\n * export gets a date; the cell prints the relative form the timeline\n * uses and carries the full stamp in its tooltip.\n */\n field: 'lastEmailEngagementAtMs',\n headerName: 'Last engaged',\n flex: 0.8,\n minWidth: 140,\n type: 'date',\n filterable: false,\n valueGetter: (_value, row: ContactRecord) =>\n row.lastEmailEngagementAtMs ? new Date(row.lastEmailEngagementAtMs) : null,\n renderCell: ({ row }: { row: ContactRecord }) =>\n row.lastEmailEngagementAtMs ? (\n <Tooltip title={new Date(row.lastEmailEngagementAtMs).toLocaleString()}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {activityTimeLabel(row.lastEmailEngagementAtMs, nowMs)}\n </Typography>\n </Tooltip>\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'—'}\n </Typography>\n ),\n },\n {\n /*\n * The verdict on the address (AGL-3245): the same chip the record page\n * carries, so a bounced or blocked person is told apart in the list a\n * member picks recipients from. Sorted and exported by its label;\n * filtered through the bar's `emailState` clause, not the panel.\n */\n field: 'emailState',\n headerName: 'Email',\n flex: 0.8,\n minWidth: 140,\n filterable: false,\n valueGetter: (_value, row: ContactRecord) =>\n row.emailState ? EMAIL_STATE_LABELS[row.emailState.status] : '',\n renderCell: ({ row }: { row: ContactRecord }) =>\n row.emailState ? (\n <CrmEmailStateChip state={row.emailState} />\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'—'}\n </Typography>\n ),\n },\n ...hiddenFilterColumns(\n CONTACT_LIST_FILTER_FIELDS,\n CONTACT_FILTER_COLUMNS,\n CONTACT_LIST_FILTER_HEADERS,\n ),\n ]\n}\n"],"names":["activityTimeLabel","CONTACT_LIFECYCLE_STAGE_LABELS","CONTACT_SOURCE_LABELS","EMAIL_STATE_LABELS","hiddenFilterColumns","listFilterColumn","Chip","Stack","Tooltip","Typography","CONTACT_LIST_FILTER_FIELDS","CONTACT_LIST_FILTER_HEADERS","CrmEmailStateChip","nextActivityColumn","CONTACT_FILTER_COLUMNS","CONTACT_OPTIONAL_COLUMNS","contactListColumns","options","memberName","siteName","nowMs","Date","now","field","headerName","flex","minWidth","valueGetter","_value","row","String","name","email","renderCell","sx","justifyContent","height","lineHeight","variant","color","noWrap","filterable","ownerUid","lifecycleStage","size","label","sortable","capturedByHostIds","map","join","length","direction","spacing","alignItems","flexWrap","slice","hostId","Object","keys","sources","source","tags","type","seconds","updatedAt","interactions","atMs","toLocaleDateString","lastEmailEngagementAtMs","title","toLocaleString","emailState","status","state"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contact-list-columns.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n activityTimeLabel,\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CONTACT_SOURCE_LABELS,\n EMAIL_STATE_LABELS,\n type ContactSource,\n} from '@aglyn/aglyn'\nimport {\n hiddenFilterColumns,\n listFilterColumn,\n} from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { Chip, Stack, Tooltip, Typography } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport {\n CONTACT_LIST_FILTER_FIELDS,\n CONTACT_LIST_FILTER_HEADERS,\n} from '../constants/contact-filters'\nimport type { ContactRecord } from '../model/contact-record'\nimport type { CrmShareChip } from '../model/crm-sharing'\nimport { CrmEmailStateChip } from './crm-email-state-chip'\nimport { nextActivityColumn } from './crm-next-activity-column'\n\n/**\n * The filterable fields that get a column. The rest of\n * `CONTACT_LIST_FILTER_FIELDS` still reaches the filter panel, hidden.\n */\n/**\n * The declared filter fields that ARE table columns, so the hidden\n * filter-only columns are built for the others alone. Owner and Stage\n * joined it when the grammar learned them (AGL-2617): each is a shown\n * column already, and a second, hidden one under the same field would be\n * a duplicate the grid refuses.\n */\nexport const CONTACT_FILTER_COLUMNS = [\n 'name',\n 'ownerUid',\n 'lifecycleStage',\n 'sources',\n 'tags',\n 'updatedAt',\n 'nextTaskAtMs',\n 'emailState',\n]\n\n/**\n * The columns that ship HIDDEN, for the grid's visibility model (AGL-2616).\n *\n * \"Last engaged\" is a fact most lists do not need on screen and every list\n * can turn on from the column menu; shipping it visible would widen a\n * table that already carries seven columns for a figure only a marketing\n * reader scans.\n */\nexport const CONTACT_OPTIONAL_COLUMNS = ['lastEmailEngagementAtMs']\n\nexport interface ContactListColumnOptions {\n /** The owner's name for a uid — the roster's, or the uid itself. */\n memberName: (uid: string) => string\n /**\n * How a site reads, by document id — handed in at the ORGANIZATION level\n * only (AGL-2630), where it turns on the \"Known by\" column: the sites\n * that have captured each person. Absent under a site, where every row\n * is known by the site and the column would say so on every line.\n */\n siteName?: (hostId: string) => string\n /** The words of a row's \"Shared by…\" chip (AGL-3336); absent draws none. */\n shareChipLabel?: (chip: CrmShareChip) => string\n /**\n * One clock for every row of one paint, so two contacts engaged a second\n * apart cannot read \"just now\" and \"1 min ago\". Defaults to the paint's\n * own `Date.now()`.\n */\n nowMs?: number\n}\n\n/**\n * The contacts list's columns (AGL-2596), in their own module so the list\n * file stays about the QUERY and the toolbar while other surfaces — a bulk\n * bar, an import — grow beside it.\n *\n * Owner and Stage read off the flattened row, which is already this\n * holder's facet (`contactRecordFromDoc`), so neither column can show\n * another holder's assignment. Filtering by either is the list's: the panel\n * names the field and the list asks the holder's facet key of its query\n * (`contactQueryClause`, AGL-3321).\n */\nexport function contactListColumns(\n options: ContactListColumnOptions,\n): GridColDef[] {\n const { memberName, siteName, shareChipLabel } = options\n const nowMs = options.nowMs ?? Date.now()\n return [\n {\n field: 'name',\n headerName: 'Contact',\n flex: 1.6,\n minWidth: 220,\n ...listFilterColumn(CONTACT_LIST_FILTER_FIELDS, 'name'),\n valueGetter: (_value, row: ContactRecord) =>\n String(row.name || row.email || ''),\n renderCell: ({ row }: { row: ContactRecord }) => (\n <Stack sx={{ justifyContent: 'center', height: '100%', lineHeight: 1.25 }}>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center', minWidth: 0 }}>\n <Typography variant=\"body2\" sx={{ lineHeight: 1.25 }}>\n {row.name || row.email}\n </Typography>\n {/* Seen on this site only through a share (AGL-3336). */}\n {row.shareChip && shareChipLabel ? (\n <Chip size=\"small\" variant=\"outlined\" color=\"info\" label={shareChipLabel(row.shareChip)} />\n ) : null}\n </Stack>\n {row.name ? (\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ lineHeight: 1.25 }}\n noWrap\n >\n {row.email}\n </Typography>\n ) : null}\n </Stack>\n ),\n },\n {\n field: 'ownerUid',\n headerName: 'Owner',\n flex: 0.9,\n minWidth: 140,\n filterable: false,\n valueGetter: (_value, row: ContactRecord) =>\n row.ownerUid ? memberName(row.ownerUid) : '',\n renderCell: ({ row }: { row: ContactRecord }) =>\n row.ownerUid ? (\n <Typography variant=\"body2\" noWrap>\n {memberName(row.ownerUid)}\n </Typography>\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'—'}\n </Typography>\n ),\n },\n {\n field: 'lifecycleStage',\n headerName: 'Stage',\n flex: 0.9,\n minWidth: 150,\n filterable: false,\n valueGetter: (_value, row: ContactRecord) =>\n row.lifecycleStage ? CONTACT_LIFECYCLE_STAGE_LABELS[row.lifecycleStage] : '',\n renderCell: ({ row }: { row: ContactRecord }) =>\n row.lifecycleStage ? (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n label={CONTACT_LIFECYCLE_STAGE_LABELS[row.lifecycleStage]}\n />\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'—'}\n </Typography>\n ),\n },\n /*\n * KNOWN BY (AGL-2630): the sites that have captured this person, which\n * is the cross-site fact the organization-level list exists to show —\n * one deduplicated row per person, and which of the org's sites know\n * them. `capturedByHostIds` is a top-level array precisely so the\n * question can be answered without opening a facet. Not filterable from\n * the panel: an `array-contains` on it would displace the scope clause\n * under a site, and a search or a facet filter at the org level (one\n * array clause per query, AGL-3321). A row that names no site predates attribution and says so,\n * rather than reading as \"every site\".\n */\n ...(siteName\n ? [\n {\n field: 'capturedByHostIds',\n headerName: 'Known by',\n flex: 1.1,\n minWidth: 200,\n sortable: false,\n filterable: false,\n valueGetter: (_value: unknown, row: ContactRecord) =>\n row.capturedByHostIds.map(siteName).join(', '),\n renderCell: ({ row }: { row: ContactRecord }) =>\n row.capturedByHostIds.length ? (\n <Stack\n direction=\"row\"\n spacing={0.5}\n sx={{ alignItems: 'center', height: '100%', flexWrap: 'nowrap' }}\n >\n {row.capturedByHostIds.slice(0, 2).map((hostId) => (\n <Chip key={hostId} size=\"small\" label={siteName(hostId)} />\n ))}\n {row.capturedByHostIds.length > 2 ? (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n label={`+${row.capturedByHostIds.length - 2}`}\n />\n ) : null}\n </Stack>\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'No site recorded'}\n </Typography>\n ),\n } satisfies GridColDef,\n ]\n : []),\n {\n field: 'sources',\n headerName: 'Sources',\n flex: 1,\n minWidth: 160,\n // A map of provenance flags, not a scalar — `sources.form == true` is\n // queryable one key at a time, which is a menu of its own rather than\n // a filter on this column.\n filterable: false,\n sortable: false,\n valueGetter: (_value, row: ContactRecord) =>\n Object.keys(row.sources ?? {}).join(', '),\n renderCell: ({ row }: { row: ContactRecord }) => (\n <Stack\n direction=\"row\"\n spacing={0.5}\n sx={{ alignItems: 'center', height: '100%' }}\n >\n {Object.keys(row.sources ?? {}).map((source) => (\n <Chip\n key={source}\n label={CONTACT_SOURCE_LABELS[source as ContactSource] ?? source}\n size=\"small\"\n />\n ))}\n </Stack>\n ),\n },\n {\n field: 'tags',\n headerName: 'Tags',\n flex: 1,\n minWidth: 150,\n ...listFilterColumn(CONTACT_LIST_FILTER_FIELDS, 'tags'),\n sortable: false,\n valueGetter: (_value, row: ContactRecord) => (row.tags ?? []).join(', '),\n renderCell: ({ row }: { row: ContactRecord }) =>\n (row.tags ?? []).slice(0, 3).join(', '),\n },\n {\n field: 'updatedAt',\n headerName: 'Last activity',\n flex: 0.8,\n minWidth: 150,\n // `type: 'date'` is what gives the panel a date PICKER rather than a\n // free-text box for a value the query reads as a day.\n type: 'date',\n ...listFilterColumn(CONTACT_LIST_FILTER_FIELDS, 'updatedAt'),\n valueGetter: (_value, row: ContactRecord) => {\n const seconds = (row.updatedAt as { seconds?: number } | undefined)?.seconds\n return seconds ? new Date(seconds * 1000) : null\n },\n renderCell: ({ row }: { row: ContactRecord }) => (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {row.interactions?.[0]\n ? new Date(row.interactions[0].atMs).toLocaleDateString()\n : '—'}\n </Typography>\n ),\n },\n // When the earliest open task against the person is due (AGL-2661).\n nextActivityColumn(nowMs),\n {\n /*\n * When the person last opened or clicked one of this holder's\n * campaigns (AGL-2616). Off the facet like Owner and Stage, so it can\n * never show another holder's readers; `filterable: false` for the\n * same reason theirs are — a facet path is not a query the panel can\n * make. A date column, so a sort on it orders by the instant and an\n * export gets a date; the cell prints the relative form the timeline\n * uses and carries the full stamp in its tooltip.\n */\n field: 'lastEmailEngagementAtMs',\n headerName: 'Last engaged',\n flex: 0.8,\n minWidth: 140,\n type: 'date',\n filterable: false,\n valueGetter: (_value, row: ContactRecord) =>\n row.lastEmailEngagementAtMs ? new Date(row.lastEmailEngagementAtMs) : null,\n renderCell: ({ row }: { row: ContactRecord }) =>\n row.lastEmailEngagementAtMs ? (\n <Tooltip title={new Date(row.lastEmailEngagementAtMs).toLocaleString()}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {activityTimeLabel(row.lastEmailEngagementAtMs, nowMs)}\n </Typography>\n </Tooltip>\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'—'}\n </Typography>\n ),\n },\n {\n /*\n * The verdict on the address (AGL-3245): the same chip the record page\n * carries, so a bounced or blocked person is told apart in the list a\n * member picks recipients from. Sorted and exported by its label;\n * filtered through the bar's `emailState` clause, not the panel.\n */\n field: 'emailState',\n headerName: 'Email',\n flex: 0.8,\n minWidth: 140,\n filterable: false,\n valueGetter: (_value, row: ContactRecord) =>\n row.emailState ? EMAIL_STATE_LABELS[row.emailState.status] : '',\n renderCell: ({ row }: { row: ContactRecord }) =>\n row.emailState ? (\n <CrmEmailStateChip state={row.emailState} />\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'—'}\n </Typography>\n ),\n },\n ...hiddenFilterColumns(\n CONTACT_LIST_FILTER_FIELDS,\n CONTACT_FILTER_COLUMNS,\n CONTACT_LIST_FILTER_HEADERS,\n ),\n ]\n}\n"],"names":["activityTimeLabel","CONTACT_LIFECYCLE_STAGE_LABELS","CONTACT_SOURCE_LABELS","EMAIL_STATE_LABELS","hiddenFilterColumns","listFilterColumn","Chip","Stack","Tooltip","Typography","CONTACT_LIST_FILTER_FIELDS","CONTACT_LIST_FILTER_HEADERS","CrmEmailStateChip","nextActivityColumn","CONTACT_FILTER_COLUMNS","CONTACT_OPTIONAL_COLUMNS","contactListColumns","options","memberName","siteName","shareChipLabel","nowMs","Date","now","field","headerName","flex","minWidth","valueGetter","_value","row","String","name","email","renderCell","sx","justifyContent","height","lineHeight","direction","spacing","alignItems","variant","shareChip","size","color","label","noWrap","filterable","ownerUid","lifecycleStage","sortable","capturedByHostIds","map","join","length","flexWrap","slice","hostId","Object","keys","sources","source","tags","type","seconds","updatedAt","interactions","atMs","toLocaleDateString","lastEmailEngagementAtMs","title","toLocaleString","emailState","status","state"],"mappings":"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"}
|
|
@@ -16,10 +16,11 @@
|
|
|
16
16
|
*/ 'use client';
|
|
17
17
|
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
18
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
19
|
-
import { contactDisplayName,
|
|
19
|
+
import { contactDisplayName, nameSearchKey, normalizeContactEmail } from "@aglyn/aglyn";
|
|
20
20
|
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
21
21
|
import { useFirestore } from "@aglyn/tenant-feature-instance";
|
|
22
22
|
import { ScrollTable } from "@aglyn/shared-ui-jsx/components/scroll-table.component";
|
|
23
|
+
import { contactMergePreview } from "../model/contact-merge.js";
|
|
23
24
|
import { Alert, Button, Dialog, DialogActions, DialogContent, DialogTitle, FormControl, FormControlLabel, FormLabel, List, ListItemButton, ListItemText, Radio, RadioGroup, Stack, TableBody, TableCell, TableHead, TableRow, TextField, Typography } from "@mui/material";
|
|
24
25
|
import { collection, endAt, getDocs, limit, orderBy, query, startAt, where } from "firebase/firestore";
|
|
25
26
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contact-merge-dialog.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n type ConsentGroup,\n type ContactMergePreviewRow,\n contactDisplayName,\n contactMergePreview,\n nameSearchKey,\n normalizeContactEmail,\n} from '@aglyn/aglyn'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport {\n Alert,\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n FormControl,\n FormControlLabel,\n FormLabel,\n List,\n ListItemButton,\n ListItemText,\n Radio,\n RadioGroup,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n endAt,\n getDocs,\n limit,\n orderBy,\n query,\n startAt,\n where,\n} from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useCrmOrgMount } from '../hooks/use-crm-org-mount'\nimport { crmVisibleToClause } from '../hooks/use-crm-scope'\nimport { useCrmApi } from './use-crm-api'\n\n/** One contact as the dialog holds it: its id and the document as read. */\nexport interface ContactPick {\n id: string\n doc: Record<string, unknown>\n}\n\n/** Which of the two records stays. */\nexport type ContactMergeKeep = 'current' | 'other'\n\n/** How many rows a search answers — a picker, not a list. */\nconst SEARCH_LIMIT = 8\n/** The high code point that closes a `nameLower` prefix range. */\nconst HIGH = ''\n\nexport interface ContactMergeDialogState {\n open: boolean\n /** The record to merge with, when the opener already knows it. */\n other: ContactPick | null\n keep: ContactMergeKeep\n}\n\n/**\n * The dialog's open state, held by the page so two openers — the record's\n * overflow menu and the likely-duplicates card — share one dialog.\n *\n * The menu opens it empty, to be searched, with the OTHER record surviving:\n * \"merge this into…\" is what a reader means when they are on the record\n * they want gone. The duplicates card opens it with the candidate already\n * picked and THIS record surviving: the reader is on the record they keep,\n * looking at the copies. Either way the dialog lets them switch.\n */\nexport function useContactMergeDialog() {\n const [state, setState] = useState<ContactMergeDialogState>({\n open: false,\n other: null,\n keep: 'other',\n })\n const open = useCallback(\n (options: { other?: ContactPick; keep?: ContactMergeKeep } = {}) =>\n setState({\n open: true,\n other: options.other ?? null,\n keep: options.keep ?? (options.other ? 'current' : 'other'),\n }),\n [],\n )\n const close = useCallback(\n () => setState((previous) => ({ ...previous, open: false })),\n [],\n )\n return { state, open, close }\n}\n\nexport interface ContactMergeDialogProps extends ContactMergeDialogState {\n onClose: () => void\n /**\n * The site the merge runs as — the route logs it on that site's feed. At\n * the organization level the record's own capturing site (AGL-2630), or\n * `null` for a row no site has captured: beneath the org hub's mount the\n * route's org variant merges it anyway (AGL-2634), and only a surface\n * mounted nowhere holds the button.\n */\n hostId: string | null\n /** The record whose page this is. */\n current: ContactPick\n /** `['orgs', orgId]` — where the contacts live. */\n scope: readonly [string, string]\n consentGroup: ConsentGroup\n /**\n * What this viewer may list — the search's `array-contains-any` — or\n * `null` at the organization level, where the search carries no clause\n * (AGL-2630).\n */\n visibleTo: readonly string[] | null\n /** The owner's name for a uid, for the preview. */\n memberName?: (uid: string) => string\n /** Told which record survived, once the server has merged. */\n onMerged?: (survivorId: string) => void\n}\n\nconst label = (pick: ContactPick, groupId: string) =>\n contactDisplayName(pick.doc, groupId) || String(pick.doc['email'] ?? pick.id)\n\n/**\n * MERGE TWO RECORDS FOR ONE PERSON (AGL-2625).\n *\n * Two steps in one dialog. First the OTHER record, found by address or by\n * name among the contacts this viewer may list — the same two searches the\n * company's contacts card runs, one read each, on ask. Then the two records\n * side by side, field by field, with the value the surviving record will\n * carry and where it came from — `contactMergePreview` is the same rule the\n * server writes with, so what the reader is shown is what lands.\n *\n * The reader picks which record survives. The survivor keeps its address;\n * the other record's address becomes an alternate on it, its deals, tasks,\n * activities and leads move, and the document is deleted. The act is one\n * server route, because it is one transaction over two documents and a\n * repoint of rows a browser may not be able to list.\n */\nexport function ContactMergeDialog(props: ContactMergeDialogProps) {\n const {\n open,\n onClose,\n hostId,\n current,\n scope,\n consentGroup,\n visibleTo,\n memberName,\n onMerged,\n } = props\n const firestore = useFirestore()\n const callCrm = useCrmApi(hostId)\n const { enqueueSnackbar } = useSnackbar()\n const groupId = consentGroup.groupId\n const orgMount = useCrmOrgMount()\n // A site to run as, or the org to run as (AGL-2634): one of the two is\n // what the route authorizes against, and with neither there is no door.\n const canMerge = Boolean(hostId) || Boolean(orgMount)\n\n const [other, setOther] = useState<ContactPick | null>(props.other)\n const [keep, setKeep] = useState<ContactMergeKeep>(props.keep)\n const [term, setTerm] = useState('')\n const [searching, setSearching] = useState(false)\n const [results, setResults] = useState<ContactPick[] | null>(null)\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n // Reset on every open to what the opener handed over.\n useEffect(() => {\n if (!open) return\n setOther(props.other)\n setKeep(props.keep)\n setTerm('')\n setResults(null)\n setError(null)\n setBusy(false)\n }, [open, props.other, props.keep])\n\n const search = useCallback(async () => {\n const raw = term.trim()\n if (!raw || searching) return\n setSearching(true)\n setError(null)\n try {\n const contactsRef = collection(firestore, scope[0], scope[1], 'contacts')\n const scoped = crmVisibleToClause(visibleTo)\n let found\n if (raw.includes('@')) {\n const email = normalizeContactEmail(raw)\n if (!email) {\n setResults([])\n return\n }\n found = await getDocs(\n query(contactsRef, ...scoped, where('email', '==', email), limit(SEARCH_LIMIT)),\n )\n } else {\n const key = nameSearchKey(raw)\n found = await getDocs(\n query(\n contactsRef,\n ...scoped,\n orderBy('nameLower'),\n startAt(key),\n endAt(`${key}${HIGH}`),\n limit(SEARCH_LIMIT),\n ),\n )\n }\n setResults(\n found.docs\n .filter((snapshot) => snapshot.id !== current.id)\n .map((snapshot) => ({ id: snapshot.id, doc: snapshot.data() })),\n )\n } catch (searchError) {\n console.error(searchError)\n setResults(null)\n setError('The search could not be run.')\n } finally {\n setSearching(false)\n }\n }, [term, searching, firestore, scope, visibleTo, current.id])\n\n const survivor = keep === 'current' ? current : other\n const merged = keep === 'current' ? other : current\n const rows: ContactMergePreviewRow[] = useMemo(\n () =>\n survivor && merged\n ? contactMergePreview(survivor.doc, merged.doc, groupId, { memberName })\n : [],\n [survivor, merged, groupId, memberName],\n )\n\n const submit = useCallback(async () => {\n if (!survivor || !merged || busy || !canMerge) return\n setBusy(true)\n setError(null)\n try {\n const { response, payload } = await callCrm('contacts-merge', {\n survivorId: survivor.id,\n mergedId: merged.id,\n })\n if (!response.ok) {\n setError(String(payload['error'] ?? 'The contacts could not be merged.'))\n return\n }\n enqueueSnackbar('Contacts merged', { variant: 'success', persist: false })\n onClose()\n onMerged?.(survivor.id)\n } catch (submitError) {\n console.error(submitError)\n setError('The contacts could not be merged.')\n } finally {\n setBusy(false)\n }\n }, [survivor, merged, busy, canMerge, callCrm, enqueueSnackbar, onClose, onMerged])\n\n const currentLabel = label(current, groupId)\n const otherLabel = other ? label(other, groupId) : ''\n\n return (\n <Dialog open={open} onClose={busy ? undefined : onClose} maxWidth=\"md\" fullWidth>\n <DialogTitle>{'Merge contacts'}</DialogTitle>\n <DialogContent>\n <Stack spacing={2} sx={{ pt: 1 }}>\n {!other ? (\n <>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`Find the other record for ${currentLabel} — by email address, or by name.`}\n </Typography>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'flex-start' }}>\n <TextField\n label=\"Email or name\"\n value={term}\n onChange={(event) => setTerm(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === 'Enter') {\n event.preventDefault()\n void search()\n }\n }}\n size=\"small\"\n fullWidth\n autoFocus\n />\n <Button\n variant=\"outlined\"\n onClick={() => void search()}\n disabled={!term.trim() || searching}\n >\n {searching ? 'Searching…' : 'Search'}\n </Button>\n </Stack>\n {results ? (\n results.length ? (\n <List dense disablePadding aria-label=\"Matching contacts\">\n {results.map((pick) => (\n <ListItemButton key={pick.id} onClick={() => setOther(pick)}>\n <ListItemText\n primary={label(pick, groupId)}\n secondary={String(pick.doc['email'] ?? '')}\n />\n </ListItemButton>\n ))}\n </List>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No other contact matches among the ones you can see.'}\n </Typography>\n )\n ) : null}\n </>\n ) : (\n <>\n <FormControl>\n <FormLabel id=\"contact-merge-keep\">{'Keep'}</FormLabel>\n <RadioGroup\n aria-labelledby=\"contact-merge-keep\"\n value={keep}\n onChange={(event) => setKeep(event.target.value as ContactMergeKeep)}\n >\n <FormControlLabel\n value=\"current\"\n control={<Radio size=\"small\" />}\n label={`${currentLabel} (${String(current.doc['email'] ?? '')})`}\n />\n <FormControlLabel\n value=\"other\"\n control={<Radio size=\"small\" />}\n label={`${otherLabel} (${String(other.doc['email'] ?? '')})`}\n />\n </RadioGroup>\n </FormControl>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'The kept record keeps every value it has; empty fields fill from the ' +\n 'other record. Tags, filings and the timeline are combined, and the ' +\n \"other record's deals, tasks, activities and leads move across. Its \" +\n 'address becomes an alternate on the kept record, so a later capture ' +\n 'on it lands here — and the other record is deleted.'}\n </Typography>\n <ScrollTable size=\"small\" aria-label=\"What the merge keeps\">\n <TableHead>\n <TableRow>\n <TableCell>{'Field'}</TableCell>\n <TableCell>{'Kept'}</TableCell>\n <TableCell>{'Merged'}</TableCell>\n <TableCell>{'After'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {rows.map((row) => (\n <TableRow key={row.key}>\n <TableCell component=\"th\" scope=\"row\">\n {row.label}\n </TableCell>\n <TableCell sx={{ whiteSpace: 'pre-wrap' }}>{row.survivor}</TableCell>\n <TableCell sx={{ whiteSpace: 'pre-wrap' }}>{row.merged}</TableCell>\n <TableCell\n sx={{\n whiteSpace: 'pre-wrap',\n fontWeight: row.from === 'merged' || row.from === 'both' ? 600 : undefined,\n }}\n >\n {row.result}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n {!props.other ? (\n <Button size=\"small\" onClick={() => setOther(null)} sx={{ alignSelf: 'flex-start' }}>\n {'Pick a different contact'}\n </Button>\n ) : null}\n </>\n )}\n {error ? <Alert severity=\"error\">{error}</Alert> : null}\n {!canMerge ? (\n <Alert severity=\"info\">\n {'No site has captured this contact, so there is no site to ' +\n 'merge it from. Open it from one of your sites instead.'}\n </Alert>\n ) : null}\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n <Button\n variant=\"contained\"\n onClick={() => void submit()}\n disabled={!other || busy || !canMerge}\n >\n {busy ? 'Merging…' : 'Merge'}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nContactMergeDialog.displayName = 'ContactMergeDialog'\n\nexport default ContactMergeDialog\n"],"names":["contactDisplayName","contactMergePreview","nameSearchKey","normalizeContactEmail","useSnackbar","useFirestore","ScrollTable","Alert","Button","Dialog","DialogActions","DialogContent","DialogTitle","FormControl","FormControlLabel","FormLabel","List","ListItemButton","ListItemText","Radio","RadioGroup","Stack","TableBody","TableCell","TableHead","TableRow","TextField","Typography","collection","endAt","getDocs","limit","orderBy","query","startAt","where","useCallback","useEffect","useMemo","useState","useCrmOrgMount","crmVisibleToClause","useCrmApi","SEARCH_LIMIT","HIGH","useContactMergeDialog","state","setState","open","other","keep","options","close","previous","label","pick","groupId","doc","String","id","ContactMergeDialog","props","current","onClose","hostId","scope","consentGroup","visibleTo","memberName","onMerged","firestore","callCrm","enqueueSnackbar","orgMount","canMerge","Boolean","setOther","setKeep","term","setTerm","searching","setSearching","results","setResults","busy","setBusy","error","setError","search","raw","trim","contactsRef","scoped","found","includes","email","key","docs","filter","snapshot","map","data","searchError","console","survivor","merged","rows","submit","response","payload","survivorId","mergedId","ok","variant","persist","submitError","currentLabel","otherLabel","undefined","maxWidth","fullWidth","spacing","sx","pt","color","direction","alignItems","value","onChange","event","target","onKeyDown","preventDefault","size","autoFocus","onClick","disabled","length","dense","disablePadding","aria-label","primary","secondary","aria-labelledby","control","row","component","whiteSpace","fontWeight","from","result","alignSelf","severity","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contact-merge-dialog.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n type ConsentGroup,\n contactDisplayName,\n nameSearchKey,\n normalizeContactEmail,\n} from '@aglyn/aglyn'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { type ContactMergePreviewRow, contactMergePreview } from '../model/contact-merge'\nimport {\n Alert,\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n FormControl,\n FormControlLabel,\n FormLabel,\n List,\n ListItemButton,\n ListItemText,\n Radio,\n RadioGroup,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n endAt,\n getDocs,\n limit,\n orderBy,\n query,\n startAt,\n where,\n} from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useCrmOrgMount } from '../hooks/use-crm-org-mount'\nimport { crmVisibleToClause } from '../hooks/use-crm-scope'\nimport { useCrmApi } from './use-crm-api'\n\n/** One contact as the dialog holds it: its id and the document as read. */\nexport interface ContactPick {\n id: string\n doc: Record<string, unknown>\n}\n\n/** Which of the two records stays. */\nexport type ContactMergeKeep = 'current' | 'other'\n\n/** How many rows a search answers — a picker, not a list. */\nconst SEARCH_LIMIT = 8\n/** The high code point that closes a `nameLower` prefix range. */\nconst HIGH = ''\n\nexport interface ContactMergeDialogState {\n open: boolean\n /** The record to merge with, when the opener already knows it. */\n other: ContactPick | null\n keep: ContactMergeKeep\n}\n\n/**\n * The dialog's open state, held by the page so two openers — the record's\n * overflow menu and the likely-duplicates card — share one dialog.\n *\n * The menu opens it empty, to be searched, with the OTHER record surviving:\n * \"merge this into…\" is what a reader means when they are on the record\n * they want gone. The duplicates card opens it with the candidate already\n * picked and THIS record surviving: the reader is on the record they keep,\n * looking at the copies. Either way the dialog lets them switch.\n */\nexport function useContactMergeDialog() {\n const [state, setState] = useState<ContactMergeDialogState>({\n open: false,\n other: null,\n keep: 'other',\n })\n const open = useCallback(\n (options: { other?: ContactPick; keep?: ContactMergeKeep } = {}) =>\n setState({\n open: true,\n other: options.other ?? null,\n keep: options.keep ?? (options.other ? 'current' : 'other'),\n }),\n [],\n )\n const close = useCallback(\n () => setState((previous) => ({ ...previous, open: false })),\n [],\n )\n return { state, open, close }\n}\n\nexport interface ContactMergeDialogProps extends ContactMergeDialogState {\n onClose: () => void\n /**\n * The site the merge runs as — the route logs it on that site's feed. At\n * the organization level the record's own capturing site (AGL-2630), or\n * `null` for a row no site has captured: beneath the org hub's mount the\n * route's org variant merges it anyway (AGL-2634), and only a surface\n * mounted nowhere holds the button.\n */\n hostId: string | null\n /** The record whose page this is. */\n current: ContactPick\n /** `['orgs', orgId]` — where the contacts live. */\n scope: readonly [string, string]\n consentGroup: ConsentGroup\n /**\n * What this viewer may list — the search's `array-contains-any` — or\n * `null` at the organization level, where the search carries no clause\n * (AGL-2630).\n */\n visibleTo: readonly string[] | null\n /** The owner's name for a uid, for the preview. */\n memberName?: (uid: string) => string\n /** Told which record survived, once the server has merged. */\n onMerged?: (survivorId: string) => void\n}\n\nconst label = (pick: ContactPick, groupId: string) =>\n contactDisplayName(pick.doc, groupId) || String(pick.doc['email'] ?? pick.id)\n\n/**\n * MERGE TWO RECORDS FOR ONE PERSON (AGL-2625).\n *\n * Two steps in one dialog. First the OTHER record, found by address or by\n * name among the contacts this viewer may list — the same two searches the\n * company's contacts card runs, one read each, on ask. Then the two records\n * side by side, field by field, with the value the surviving record will\n * carry and where it came from — `contactMergePreview` is the same rule the\n * server writes with, so what the reader is shown is what lands.\n *\n * The reader picks which record survives. The survivor keeps its address;\n * the other record's address becomes an alternate on it, its deals, tasks,\n * activities and leads move, and the document is deleted. The act is one\n * server route, because it is one transaction over two documents and a\n * repoint of rows a browser may not be able to list.\n */\nexport function ContactMergeDialog(props: ContactMergeDialogProps) {\n const {\n open,\n onClose,\n hostId,\n current,\n scope,\n consentGroup,\n visibleTo,\n memberName,\n onMerged,\n } = props\n const firestore = useFirestore()\n const callCrm = useCrmApi(hostId)\n const { enqueueSnackbar } = useSnackbar()\n const groupId = consentGroup.groupId\n const orgMount = useCrmOrgMount()\n // A site to run as, or the org to run as (AGL-2634): one of the two is\n // what the route authorizes against, and with neither there is no door.\n const canMerge = Boolean(hostId) || Boolean(orgMount)\n\n const [other, setOther] = useState<ContactPick | null>(props.other)\n const [keep, setKeep] = useState<ContactMergeKeep>(props.keep)\n const [term, setTerm] = useState('')\n const [searching, setSearching] = useState(false)\n const [results, setResults] = useState<ContactPick[] | null>(null)\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n // Reset on every open to what the opener handed over.\n useEffect(() => {\n if (!open) return\n setOther(props.other)\n setKeep(props.keep)\n setTerm('')\n setResults(null)\n setError(null)\n setBusy(false)\n }, [open, props.other, props.keep])\n\n const search = useCallback(async () => {\n const raw = term.trim()\n if (!raw || searching) return\n setSearching(true)\n setError(null)\n try {\n const contactsRef = collection(firestore, scope[0], scope[1], 'contacts')\n const scoped = crmVisibleToClause(visibleTo)\n let found\n if (raw.includes('@')) {\n const email = normalizeContactEmail(raw)\n if (!email) {\n setResults([])\n return\n }\n found = await getDocs(\n query(contactsRef, ...scoped, where('email', '==', email), limit(SEARCH_LIMIT)),\n )\n } else {\n const key = nameSearchKey(raw)\n found = await getDocs(\n query(\n contactsRef,\n ...scoped,\n orderBy('nameLower'),\n startAt(key),\n endAt(`${key}${HIGH}`),\n limit(SEARCH_LIMIT),\n ),\n )\n }\n setResults(\n found.docs\n .filter((snapshot) => snapshot.id !== current.id)\n .map((snapshot) => ({ id: snapshot.id, doc: snapshot.data() })),\n )\n } catch (searchError) {\n console.error(searchError)\n setResults(null)\n setError('The search could not be run.')\n } finally {\n setSearching(false)\n }\n }, [term, searching, firestore, scope, visibleTo, current.id])\n\n const survivor = keep === 'current' ? current : other\n const merged = keep === 'current' ? other : current\n const rows: ContactMergePreviewRow[] = useMemo(\n () =>\n survivor && merged\n ? contactMergePreview(survivor.doc, merged.doc, groupId, { memberName })\n : [],\n [survivor, merged, groupId, memberName],\n )\n\n const submit = useCallback(async () => {\n if (!survivor || !merged || busy || !canMerge) return\n setBusy(true)\n setError(null)\n try {\n const { response, payload } = await callCrm('contacts-merge', {\n survivorId: survivor.id,\n mergedId: merged.id,\n })\n if (!response.ok) {\n setError(String(payload['error'] ?? 'The contacts could not be merged.'))\n return\n }\n enqueueSnackbar('Contacts merged', { variant: 'success', persist: false })\n onClose()\n onMerged?.(survivor.id)\n } catch (submitError) {\n console.error(submitError)\n setError('The contacts could not be merged.')\n } finally {\n setBusy(false)\n }\n }, [survivor, merged, busy, canMerge, callCrm, enqueueSnackbar, onClose, onMerged])\n\n const currentLabel = label(current, groupId)\n const otherLabel = other ? label(other, groupId) : ''\n\n return (\n <Dialog open={open} onClose={busy ? undefined : onClose} maxWidth=\"md\" fullWidth>\n <DialogTitle>{'Merge contacts'}</DialogTitle>\n <DialogContent>\n <Stack spacing={2} sx={{ pt: 1 }}>\n {!other ? (\n <>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`Find the other record for ${currentLabel} — by email address, or by name.`}\n </Typography>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'flex-start' }}>\n <TextField\n label=\"Email or name\"\n value={term}\n onChange={(event) => setTerm(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === 'Enter') {\n event.preventDefault()\n void search()\n }\n }}\n size=\"small\"\n fullWidth\n autoFocus\n />\n <Button\n variant=\"outlined\"\n onClick={() => void search()}\n disabled={!term.trim() || searching}\n >\n {searching ? 'Searching…' : 'Search'}\n </Button>\n </Stack>\n {results ? (\n results.length ? (\n <List dense disablePadding aria-label=\"Matching contacts\">\n {results.map((pick) => (\n <ListItemButton key={pick.id} onClick={() => setOther(pick)}>\n <ListItemText\n primary={label(pick, groupId)}\n secondary={String(pick.doc['email'] ?? '')}\n />\n </ListItemButton>\n ))}\n </List>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No other contact matches among the ones you can see.'}\n </Typography>\n )\n ) : null}\n </>\n ) : (\n <>\n <FormControl>\n <FormLabel id=\"contact-merge-keep\">{'Keep'}</FormLabel>\n <RadioGroup\n aria-labelledby=\"contact-merge-keep\"\n value={keep}\n onChange={(event) => setKeep(event.target.value as ContactMergeKeep)}\n >\n <FormControlLabel\n value=\"current\"\n control={<Radio size=\"small\" />}\n label={`${currentLabel} (${String(current.doc['email'] ?? '')})`}\n />\n <FormControlLabel\n value=\"other\"\n control={<Radio size=\"small\" />}\n label={`${otherLabel} (${String(other.doc['email'] ?? '')})`}\n />\n </RadioGroup>\n </FormControl>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'The kept record keeps every value it has; empty fields fill from the ' +\n 'other record. Tags, filings and the timeline are combined, and the ' +\n \"other record's deals, tasks, activities and leads move across. Its \" +\n 'address becomes an alternate on the kept record, so a later capture ' +\n 'on it lands here — and the other record is deleted.'}\n </Typography>\n <ScrollTable size=\"small\" aria-label=\"What the merge keeps\">\n <TableHead>\n <TableRow>\n <TableCell>{'Field'}</TableCell>\n <TableCell>{'Kept'}</TableCell>\n <TableCell>{'Merged'}</TableCell>\n <TableCell>{'After'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {rows.map((row) => (\n <TableRow key={row.key}>\n <TableCell component=\"th\" scope=\"row\">\n {row.label}\n </TableCell>\n <TableCell sx={{ whiteSpace: 'pre-wrap' }}>{row.survivor}</TableCell>\n <TableCell sx={{ whiteSpace: 'pre-wrap' }}>{row.merged}</TableCell>\n <TableCell\n sx={{\n whiteSpace: 'pre-wrap',\n fontWeight: row.from === 'merged' || row.from === 'both' ? 600 : undefined,\n }}\n >\n {row.result}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n {!props.other ? (\n <Button size=\"small\" onClick={() => setOther(null)} sx={{ alignSelf: 'flex-start' }}>\n {'Pick a different contact'}\n </Button>\n ) : null}\n </>\n )}\n {error ? <Alert severity=\"error\">{error}</Alert> : null}\n {!canMerge ? (\n <Alert severity=\"info\">\n {'No site has captured this contact, so there is no site to ' +\n 'merge it from. Open it from one of your sites instead.'}\n </Alert>\n ) : null}\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n <Button\n variant=\"contained\"\n onClick={() => void submit()}\n disabled={!other || busy || !canMerge}\n >\n {busy ? 'Merging…' : 'Merge'}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nContactMergeDialog.displayName = 'ContactMergeDialog'\n\nexport default ContactMergeDialog\n"],"names":["contactDisplayName","nameSearchKey","normalizeContactEmail","useSnackbar","useFirestore","ScrollTable","contactMergePreview","Alert","Button","Dialog","DialogActions","DialogContent","DialogTitle","FormControl","FormControlLabel","FormLabel","List","ListItemButton","ListItemText","Radio","RadioGroup","Stack","TableBody","TableCell","TableHead","TableRow","TextField","Typography","collection","endAt","getDocs","limit","orderBy","query","startAt","where","useCallback","useEffect","useMemo","useState","useCrmOrgMount","crmVisibleToClause","useCrmApi","SEARCH_LIMIT","HIGH","useContactMergeDialog","state","setState","open","other","keep","options","close","previous","label","pick","groupId","doc","String","id","ContactMergeDialog","props","current","onClose","hostId","scope","consentGroup","visibleTo","memberName","onMerged","firestore","callCrm","enqueueSnackbar","orgMount","canMerge","Boolean","setOther","setKeep","term","setTerm","searching","setSearching","results","setResults","busy","setBusy","error","setError","search","raw","trim","contactsRef","scoped","found","includes","email","key","docs","filter","snapshot","map","data","searchError","console","survivor","merged","rows","submit","response","payload","survivorId","mergedId","ok","variant","persist","submitError","currentLabel","otherLabel","undefined","maxWidth","fullWidth","spacing","sx","pt","color","direction","alignItems","value","onChange","event","target","onKeyDown","preventDefault","size","autoFocus","onClick","disabled","length","dense","disablePadding","aria-label","primary","secondary","aria-labelledby","control","row","component","whiteSpace","fontWeight","from","result","alignSelf","severity","displayName"],"mappings":"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"}
|
|
@@ -25,6 +25,7 @@ import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.
|
|
|
25
25
|
import { Button, Chip, Stack, Tooltip, Typography } from "@mui/material";
|
|
26
26
|
import { useParams } from "next/navigation";
|
|
27
27
|
import { useCallback, useMemo, useState } from "react";
|
|
28
|
+
import { contactInteractionHref, INTERACTION_LINK_LABELS } from "../model/contact-interaction-links.js";
|
|
28
29
|
import { contactPrimaryGroup } from "../model/contact-record.js";
|
|
29
30
|
import { ActivityRow } from "./activity-list.js";
|
|
30
31
|
import { useActivityScope, useActivityWindow, useCanEditActivity } from "./activity-queries.js";
|
|
@@ -58,7 +59,7 @@ import { useContactCampaignEmails } from "./use-contact-campaign-emails.js";
|
|
|
58
59
|
*
|
|
59
60
|
* Everything it holds fits the collapsed row, so it has nothing to open.
|
|
60
61
|
*/ function CapturedRow(props) {
|
|
61
|
-
var _CONTACT_SOURCE_LABELS_source, _interaction_summary,
|
|
62
|
+
var _CONTACT_SOURCE_LABELS_source, _interaction_summary, _INTERACTION_LINK_LABELS_source;
|
|
62
63
|
const { interaction, nowMs, href } = props;
|
|
63
64
|
const source = interaction.type;
|
|
64
65
|
const label = (_CONTACT_SOURCE_LABELS_source = CONTACT_SOURCE_LABELS[source]) != null ? _CONTACT_SOURCE_LABELS_source : String(source);
|
|
@@ -103,7 +104,7 @@ import { useContactCampaignEmails } from "./use-contact-campaign-emails.js";
|
|
|
103
104
|
variant: "caption",
|
|
104
105
|
children: /*#__PURE__*/ _jsx(AppLink, {
|
|
105
106
|
href: href,
|
|
106
|
-
children: (
|
|
107
|
+
children: (_INTERACTION_LINK_LABELS_source = INTERACTION_LINK_LABELS[source]) != null ? _INTERACTION_LINK_LABELS_source : 'Open'
|
|
107
108
|
})
|
|
108
109
|
}) : null
|
|
109
110
|
]
|
|
@@ -256,7 +257,7 @@ CampaignEmailRow.displayName = 'CampaignEmailRow';
|
|
|
256
257
|
params == null ? void 0 : params.orgSlug,
|
|
257
258
|
params == null ? void 0 : params.host
|
|
258
259
|
]);
|
|
259
|
-
const interactionHref = useCallback((interaction)=>siteContext && interaction.hostId === hostId ?
|
|
260
|
+
const interactionHref = useCallback((interaction)=>siteContext && interaction.hostId === hostId ? contactInteractionHref(interaction, siteContext) : null, [
|
|
260
261
|
siteContext,
|
|
261
262
|
hostId
|
|
262
263
|
]);
|