@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/company-picker.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 AglynOrgBilling,\n createResourceUid,\n CRM_COLLECTIONS,\n CRM_RECORDS_BAND_FULL_MESSAGE,\n crmNewRecordListFields,\n} from '@aglyn/aglyn'\nimport {\n useFirestore,\n useFirestoreCollection,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Autocomplete,\n Button,\n createFilterOptions,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n doc,\n limit,\n orderBy,\n query,\n serverTimestamp,\n setDoc,\n where,\n} from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport { readCrmRecordsQuota } from '../hooks/use-crm-records-quota'\nimport { crmVisibleToClause, useCrmScope } from '../hooks/use-crm-scope'\nimport {\n type CompanyOption,\n companyDraftFields,\n EMPTY_COMPANY_DRAFT,\n suggestCompanyForEmail,\n} from '../model/companies'\n\nexport { suggestCompanyForEmail, type CompanyOption }\n\n/**\n * How many companies the picker lists.\n *\n * A ceiling and not a page: somebody choosing a company knows which one, and\n * a workspace past two hundred accounts finds it from the company list's own\n * search and links the person from there. Bounded because this read happens\n * on a contact's page, where the company list is a control and not the\n * subject.\n */\nexport const COMPANY_OPTIONS_LIMIT = 200\n\nexport interface CompanyOptions {\n options: CompanyOption[]\n /** The listen has answered at least once. */\n ready: boolean\n /** The ceiling cut the list, so a company can exist and not be offered. */\n truncated: boolean\n}\n\n/**\n * The companies this viewer may link a contact to, alphabetically\n * (AGL-2597).\n *\n * Read under the same `visibleTo` predicate the company list runs, because\n * the rules refuse a listen without it for a scoped member — and because a\n * picker offering a company the reader could not open would let them file a\n * person under a record they cannot see. Ordered by `nameLower`, the key\n * every company write stores, so the picker reads as a list of names rather\n * than as document-id order dressed up.\n *\n * `enabled` gates the listen: a contact's properties card has one picker and\n * a merchant who opened the record to read a note must not pay for the\n * company list.\n */\nexport function useCompanyOptions(props: {\n /** The site whose console is reading, or `null` at the organization level. */\n hostId: string | null\n org?: Partial<AglynOrgBilling> | null\n enabled?: boolean\n}): CompanyOptions {\n const { hostId, org, enabled = true } = props\n const firestore = useFirestore()\n const { scope, visibleTo } = useCrmScope({ hostId, org })\n const { data, status } = useFirestoreCollection<Record<string, unknown>>(\n () =>\n enabled && scope\n ? query(\n collection(firestore, scope[0], scope[1], CRM_COLLECTIONS.companies),\n ...crmVisibleToClause(visibleTo),\n orderBy('nameLower'),\n limit(COMPANY_OPTIONS_LIMIT + 1),\n )\n : null,\n [firestore, scope, visibleTo, enabled],\n { idField: '$id' },\n )\n const options = useMemo<CompanyOption[]>(\n () =>\n (data ?? []).slice(0, COMPANY_OPTIONS_LIMIT).map((row) => ({\n id: String(row['$id']),\n name: String(row['name'] ?? ''),\n domain: typeof row['domain'] === 'string' ? row['domain'] : null,\n })),\n [data],\n )\n return {\n options,\n ready: Boolean(scope) && status !== 'loading',\n truncated: (data?.length ?? 0) > COMPANY_OPTIONS_LIMIT,\n }\n}\n\n/**\n * Create a company by name, answering the option the picker then selects.\n * Rejects with the sentence to show when the name cannot be stored.\n */\nexport type CreateCompany = (name: string) => Promise<CompanyOption>\n\n/**\n * The write behind \"Create “Acme”\" in the picker (AGL-2613).\n *\n * The same document the company drawer writes for a create — the search\n * keys, the scope every CRM creator stamps, the provenance — with only the\n * name filled in, because that is all the picker asked for: the domain, the\n * address and the rest are the company page's to add. The person filing it\n * owns it, as the drawer seeds too. `null` until the org has resolved, so a\n * picker offered before the scope is known offers no create.\n *\n * A company is a record of the band the drawers refuse on (AGL-2644): an\n * org at a full hard band is refused here with the drawers' sentence, and\n * the picker shows it beneath the field the way it shows any create that\n * failed. Measured on the click and not on the mount — see\n * `readCrmRecordsQuota` — and never for an edit, because linking an\n * existing company adds no record.\n */\nexport function useCreateCompany(props: {\n /** The site whose console is creating, or `null` at the organization level. */\n hostId: string | null\n org?: Partial<AglynOrgBilling> | null\n}): CreateCompany | null {\n const { hostId, org } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n // The provenance and the scope both come from the site the record is\n // captured by — the mounted one, or at the organization level the one the\n // reader picked (AGL-2630). No site picked, no create offered.\n const { scope, createTokens, createHostId } = useCrmScope({ hostId, org })\n const uid = user?.uid ?? ''\n return useMemo<CreateCompany | null>(() => {\n if (!scope || !createHostId) return null\n return async (name) => {\n const result = companyDraftFields({\n ...EMPTY_COMPANY_DRAFT,\n name,\n ownerUid: uid,\n })\n if (result.ok === false) throw new Error(result.error)\n const quota = await readCrmRecordsQuota(firestore, scope, org)\n if (!quota.allowed) throw new Error(CRM_RECORDS_BAND_FULL_MESSAGE)\n const id = createResourceUid()\n await setDoc(\n doc(firestore, scope[0], scope[1], CRM_COLLECTIONS.companies, id),\n {\n ...result.set,\n // What the Companies list searches and filters by (AGL-3321).\n ...crmNewRecordListFields('companies', { ...result.set, visibleTo: [...createTokens] }),\n visibleTo: [...createTokens],\n hostId: createHostId,\n createdByUid: uid,\n createdAt: serverTimestamp(),\n updatedAt: serverTimestamp(),\n },\n )\n return { id, name: String(result.set['name']), domain: null }\n }\n }, [scope, createTokens, firestore, createHostId, uid, org])\n}\n\n/** The sentinel id of the \"Create …\" row the list grows when nothing matches. */\nconst CREATE_OPTION_ID = '__create__'\n\ntype PickerOption = CompanyOption | { id: typeof CREATE_OPTION_ID; name: string; create: string }\n\nconst isCreateOption = (\n option: PickerOption,\n): option is Extract<PickerOption, { create: string }> => 'create' in option\n\nconst filterCompanies = createFilterOptions<PickerOption>({\n stringify: (option) =>\n isCreateOption(option)\n ? option.create\n : option.domain\n ? `${option.name} ${option.domain}`\n : option.name,\n})\n\nconst optionLabel = (option: PickerOption): string =>\n isCreateOption(option)\n ? `Create “${option.create}”`\n : option.domain\n ? `${option.name} · ${option.domain}`\n : option.name\n\nexport interface CompanyPickerProps {\n options: readonly CompanyOption[]\n /** The linked company's id, or `null` for none. */\n value: string | null\n /**\n * The choice, with the option behind it when the list — or a create — can\n * name it, so a caller can echo the company's name onto the contact\n * without a second lookup. `null` for a company the list does not carry.\n */\n onChange: (companyId: string | null, company: CompanyOption | null) => void\n /**\n * Create a company from the typed name and select it. Absent — or `null`\n * while the scope is unresolved — the picker offers no create row.\n */\n onCreate?: CreateCompany | null\n label?: string\n helperText?: string\n disabled?: boolean\n /**\n * The contact's email address, when the caller has one. A company whose\n * domain matches it is offered as a suggestion beneath the field — a\n * suggestion and never an automatic link, because the person who typed\n * `@acme.com` may be a contractor, a customer of Acme's, or Acme itself.\n */\n email?: string | null\n /**\n * The company name the record carries as free text while no company is\n * linked — an import, or a save from before the picker existed. Offered\n * beneath the field as the company to link or create, so the label a\n * merchant already typed is one click from becoming the record it names.\n */\n fallbackName?: string | null\n /** The option list has answered; until then the field says it is loading. */\n ready?: boolean\n /** The list was cut at the ceiling, so the field says to narrow by typing. */\n truncated?: boolean\n}\n\n/**\n * One company, chosen from the list a contact may be filed under\n * (AGL-2597, AGL-2613).\n *\n * A search over the loaded list rather than free text, because the value is\n * a document id and a typed name is how two records called \"Acme\" come to\n * exist. Typing narrows by name and domain; a name nothing matches grows the\n * list by one row, \"Create “…”\", which writes the company and selects it —\n * so the person filing a contact never has to leave the form to make the\n * account it belongs to. The clear control is the unlink: a saved contact\n * with no company reads as a decision rather than a field somebody skipped.\n *\n * A stored id the list does not carry — a company past the picker's ceiling,\n * or one this viewer cannot see — still renders as SOMETHING selected, or\n * the field would read empty for a contact that has a company and the next\n * save would silently unlink them.\n */\nexport function CompanyPicker(props: CompanyPickerProps) {\n const {\n options,\n value,\n onChange,\n onCreate,\n label = 'Company',\n helperText,\n disabled,\n email,\n fallbackName,\n ready = true,\n truncated,\n } = props\n const [creating, setCreating] = useState(false)\n const [createError, setCreateError] = useState('')\n\n const suggestion = useMemo(\n () => (email ? suggestCompanyForEmail(email, options) : null),\n [email, options],\n )\n const fallback = useMemo(() => {\n const name = String(fallbackName ?? '').trim()\n if (value || !name) return null\n const lower = name.toLowerCase()\n return {\n name,\n match: options.find((option) => option.name.trim().toLowerCase() === lower) ?? null,\n }\n }, [fallbackName, options, value])\n\n const selected = useMemo<PickerOption | null>(() => {\n if (!value) return null\n return (\n options.find((option) => option.id === value) ?? {\n id: value,\n name: `Company ${value.slice(-6)}`,\n domain: null,\n }\n )\n }, [options, value])\n /*\n * The list the control searches: every option, plus the placeholder for a\n * stored id the list does not carry, so the selected value is always one\n * of the options and the control never reports it as invalid.\n */\n const listed = useMemo<PickerOption[]>(\n () =>\n selected && !options.some((option) => option.id === selected.id)\n ? [selected, ...options]\n : [...options],\n [options, selected],\n )\n\n const create = useCallback(\n async (name: string) => {\n if (!onCreate) return\n setCreating(true)\n setCreateError('')\n try {\n const company = await onCreate(name)\n onChange(company.id, company)\n } catch (error) {\n setCreateError(\n error instanceof Error && error.message\n ? error.message\n : 'The company could not be created.',\n )\n } finally {\n setCreating(false)\n }\n },\n [onCreate, onChange],\n )\n\n const busy = disabled || !ready || creating\n return (\n <Stack spacing={0.5}>\n <Autocomplete<PickerOption, false, false, false>\n size=\"small\"\n options={listed}\n value={selected}\n disabled={busy}\n loading={!ready}\n getOptionLabel={optionLabel}\n isOptionEqualToValue={(option, current) => option.id === current.id}\n filterOptions={(candidates, state) => {\n const filtered = filterCompanies(candidates, state)\n const typed = state.inputValue.trim()\n const exact = typed.toLowerCase()\n const named = candidates.some(\n (option) =>\n !isCreateOption(option) && option.name.trim().toLowerCase() === exact,\n )\n if (onCreate && typed && !named) {\n filtered.push({ id: CREATE_OPTION_ID, name: typed, create: typed })\n }\n return filtered\n }}\n onChange={(_event, next) => {\n if (!next) return onChange(null, null)\n if (isCreateOption(next)) return void create(next.create)\n onChange(next.id, next)\n }}\n renderInput={(params) => (\n <TextField\n {...params}\n label={label}\n error={Boolean(createError)}\n helperText={\n !ready\n ? 'Loading companies…'\n : creating\n ? 'Creating the company…'\n : createError ||\n helperText ||\n (truncated\n ? `The first ${COMPANY_OPTIONS_LIMIT} by name — type to narrow, or create one.`\n : undefined)\n }\n />\n )}\n />\n {suggestion && suggestion.id !== value ? (\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Suggested from the email address: ${suggestion.name}`}\n </Typography>\n <Button\n size=\"small\"\n disabled={busy}\n onClick={() => onChange(suggestion.id, suggestion)}\n >\n {'Use'}\n </Button>\n </Stack>\n ) : fallback ? (\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {fallback.match\n ? `Recorded as “${fallback.name}” — a company by that name exists.`\n : `Recorded as “${fallback.name}” without a link.`}\n </Typography>\n {fallback.match ? (\n <Button\n size=\"small\"\n disabled={busy}\n onClick={() => onChange(fallback.match?.id ?? null, fallback.match)}\n >\n {'Use'}\n </Button>\n ) : onCreate ? (\n <Button size=\"small\" disabled={busy} onClick={() => void create(fallback.name)}>\n {`Create “${fallback.name}”`}\n </Button>\n ) : null}\n </Stack>\n ) : null}\n </Stack>\n )\n}\nCompanyPicker.displayName = 'CompanyPicker'\n\nexport default CompanyPicker\n"],"names":["createResourceUid","CRM_COLLECTIONS","CRM_RECORDS_BAND_FULL_MESSAGE","crmNewRecordListFields","useFirestore","useFirestoreCollection","useUser","Autocomplete","Button","createFilterOptions","Stack","TextField","Typography","collection","doc","limit","orderBy","query","serverTimestamp","setDoc","useCallback","useMemo","useState","readCrmRecordsQuota","crmVisibleToClause","useCrmScope","companyDraftFields","EMPTY_COMPANY_DRAFT","suggestCompanyForEmail","COMPANY_OPTIONS_LIMIT","useCompanyOptions","props","hostId","org","enabled","firestore","scope","visibleTo","data","status","companies","idField","options","slice","map","row","id","String","name","domain","ready","Boolean","truncated","length","useCreateCompany","user","createTokens","createHostId","uid","result","ownerUid","ok","Error","error","quota","allowed","set","createdByUid","createdAt","updatedAt","CREATE_OPTION_ID","isCreateOption","option","filterCompanies","stringify","create","optionLabel","CompanyPicker","value","onChange","onCreate","label","helperText","disabled","email","fallbackName","creating","setCreating","createError","setCreateError","suggestion","fallback","trim","lower","toLowerCase","match","find","selected","listed","some","company","message","busy","spacing","size","loading","getOptionLabel","isOptionEqualToValue","current","filterOptions","candidates","state","filtered","typed","inputValue","exact","named","push","_event","next","renderInput","params","undefined","direction","sx","alignItems","variant","color","onClick","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/company-picker.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 AglynOrgBilling,\n createResourceUid,\n CRM_COLLECTIONS,\n CRM_RECORDS_BAND_FULL_MESSAGE,\n crmNewRecordListFields,\n} from '@aglyn/aglyn'\nimport {\n useFirestore,\n useFirestoreCollection,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Autocomplete,\n Button,\n createFilterOptions,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n doc,\n limit,\n orderBy,\n query,\n serverTimestamp,\n setDoc,\n where,\n} from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport { readCrmRecordsQuota } from '../hooks/use-crm-records-quota'\nimport { crmVisibleToClause, useCrmScope } from '../hooks/use-crm-scope'\nimport {\n type CompanyOption,\n companyDraftFields,\n EMPTY_COMPANY_DRAFT,\n suggestCompanyForEmail,\n} from '../model/companies'\nimport { useCrmSharingFollowUp } from '../hooks/use-crm-sharing'\n\nexport { suggestCompanyForEmail, type CompanyOption }\n\n/**\n * How many companies the picker lists.\n *\n * A ceiling and not a page: somebody choosing a company knows which one, and\n * a workspace past two hundred accounts finds it from the company list's own\n * search and links the person from there. Bounded because this read happens\n * on a contact's page, where the company list is a control and not the\n * subject.\n */\nexport const COMPANY_OPTIONS_LIMIT = 200\n\nexport interface CompanyOptions {\n options: CompanyOption[]\n /** The listen has answered at least once. */\n ready: boolean\n /** The ceiling cut the list, so a company can exist and not be offered. */\n truncated: boolean\n}\n\n/**\n * The companies this viewer may link a contact to, alphabetically\n * (AGL-2597).\n *\n * Read under the same `visibleTo` predicate the company list runs, because\n * the rules refuse a listen without it for a scoped member — and because a\n * picker offering a company the reader could not open would let them file a\n * person under a record they cannot see. Ordered by `nameLower`, the key\n * every company write stores, so the picker reads as a list of names rather\n * than as document-id order dressed up.\n *\n * `enabled` gates the listen: a contact's properties card has one picker and\n * a merchant who opened the record to read a note must not pay for the\n * company list.\n */\nexport function useCompanyOptions(props: {\n /** The site whose console is reading, or `null` at the organization level. */\n hostId: string | null\n org?: Partial<AglynOrgBilling> | null\n enabled?: boolean\n}): CompanyOptions {\n const { hostId, org, enabled = true } = props\n const firestore = useFirestore()\n const { scope, visibleTo } = useCrmScope({ hostId, org })\n const { data, status } = useFirestoreCollection<Record<string, unknown>>(\n () =>\n enabled && scope\n ? query(\n collection(firestore, scope[0], scope[1], CRM_COLLECTIONS.companies),\n ...crmVisibleToClause(visibleTo),\n orderBy('nameLower'),\n limit(COMPANY_OPTIONS_LIMIT + 1),\n )\n : null,\n [firestore, scope, visibleTo, enabled],\n { idField: '$id' },\n )\n const options = useMemo<CompanyOption[]>(\n () =>\n (data ?? []).slice(0, COMPANY_OPTIONS_LIMIT).map((row) => ({\n id: String(row['$id']),\n name: String(row['name'] ?? ''),\n domain: typeof row['domain'] === 'string' ? row['domain'] : null,\n })),\n [data],\n )\n return {\n options,\n ready: Boolean(scope) && status !== 'loading',\n truncated: (data?.length ?? 0) > COMPANY_OPTIONS_LIMIT,\n }\n}\n\n/**\n * Create a company by name, answering the option the picker then selects.\n * Rejects with the sentence to show when the name cannot be stored.\n */\nexport type CreateCompany = (name: string) => Promise<CompanyOption>\n\n/**\n * The write behind \"Create “Acme”\" in the picker (AGL-2613).\n *\n * The same document the company drawer writes for a create — the search\n * keys, the scope every CRM creator stamps, the provenance — with only the\n * name filled in, because that is all the picker asked for: the domain, the\n * address and the rest are the company page's to add. The person filing it\n * owns it, as the drawer seeds too. `null` until the org has resolved, so a\n * picker offered before the scope is known offers no create.\n *\n * A company is a record of the band the drawers refuse on (AGL-2644): an\n * org at a full hard band is refused here with the drawers' sentence, and\n * the picker shows it beneath the field the way it shows any create that\n * failed. Measured on the click and not on the mount — see\n * `readCrmRecordsQuota` — and never for an edit, because linking an\n * existing company adds no record.\n */\nexport function useCreateCompany(props: {\n /** The site whose console is creating, or `null` at the organization level. */\n hostId: string | null\n org?: Partial<AglynOrgBilling> | null\n}): CreateCompany | null {\n const { hostId, org } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n // The provenance and the scope both come from the site the record is\n // captured by — the mounted one, or at the organization level the one the\n // reader picked (AGL-2630). No site picked, no create offered.\n const { scope, createTokens, createHostId } = useCrmScope({ hostId, org })\n const followUpSharing = useCrmSharingFollowUp(hostId, scope?.[1] ?? null)\n const uid = user?.uid ?? ''\n return useMemo<CreateCompany | null>(() => {\n if (!scope || !createHostId) return null\n return async (name) => {\n const result = companyDraftFields({\n ...EMPTY_COMPANY_DRAFT,\n name,\n ownerUid: uid,\n })\n if (result.ok === false) throw new Error(result.error)\n const quota = await readCrmRecordsQuota(firestore, scope, org)\n if (!quota.allowed) throw new Error(CRM_RECORDS_BAND_FULL_MESSAGE)\n const id = createResourceUid()\n await setDoc(\n doc(firestore, scope[0], scope[1], CRM_COLLECTIONS.companies, id),\n {\n ...result.set,\n // What the Companies list searches and filters by (AGL-3321).\n ...crmNewRecordListFields('companies', { ...result.set, visibleTo: [...createTokens] }),\n visibleTo: [...createTokens],\n hostId: createHostId,\n createdByUid: uid,\n createdAt: serverTimestamp(),\n updatedAt: serverTimestamp(),\n },\n )\n // A rule may share companies created on this site (AGL-3336).\n followUpSharing('companies', [id])\n return { id, name: String(result.set['name']), domain: null }\n }\n }, [scope, createTokens, firestore, createHostId, uid, org, followUpSharing])\n}\n\n/** The sentinel id of the \"Create …\" row the list grows when nothing matches. */\nconst CREATE_OPTION_ID = '__create__'\n\ntype PickerOption = CompanyOption | { id: typeof CREATE_OPTION_ID; name: string; create: string }\n\nconst isCreateOption = (\n option: PickerOption,\n): option is Extract<PickerOption, { create: string }> => 'create' in option\n\nconst filterCompanies = createFilterOptions<PickerOption>({\n stringify: (option) =>\n isCreateOption(option)\n ? option.create\n : option.domain\n ? `${option.name} ${option.domain}`\n : option.name,\n})\n\nconst optionLabel = (option: PickerOption): string =>\n isCreateOption(option)\n ? `Create “${option.create}”`\n : option.domain\n ? `${option.name} · ${option.domain}`\n : option.name\n\nexport interface CompanyPickerProps {\n options: readonly CompanyOption[]\n /** The linked company's id, or `null` for none. */\n value: string | null\n /**\n * The choice, with the option behind it when the list — or a create — can\n * name it, so a caller can echo the company's name onto the contact\n * without a second lookup. `null` for a company the list does not carry.\n */\n onChange: (companyId: string | null, company: CompanyOption | null) => void\n /**\n * Create a company from the typed name and select it. Absent — or `null`\n * while the scope is unresolved — the picker offers no create row.\n */\n onCreate?: CreateCompany | null\n label?: string\n helperText?: string\n disabled?: boolean\n /**\n * The contact's email address, when the caller has one. A company whose\n * domain matches it is offered as a suggestion beneath the field — a\n * suggestion and never an automatic link, because the person who typed\n * `@acme.com` may be a contractor, a customer of Acme's, or Acme itself.\n */\n email?: string | null\n /**\n * The company name the record carries as free text while no company is\n * linked — an import, or a save from before the picker existed. Offered\n * beneath the field as the company to link or create, so the label a\n * merchant already typed is one click from becoming the record it names.\n */\n fallbackName?: string | null\n /** The option list has answered; until then the field says it is loading. */\n ready?: boolean\n /** The list was cut at the ceiling, so the field says to narrow by typing. */\n truncated?: boolean\n}\n\n/**\n * One company, chosen from the list a contact may be filed under\n * (AGL-2597, AGL-2613).\n *\n * A search over the loaded list rather than free text, because the value is\n * a document id and a typed name is how two records called \"Acme\" come to\n * exist. Typing narrows by name and domain; a name nothing matches grows the\n * list by one row, \"Create “…”\", which writes the company and selects it —\n * so the person filing a contact never has to leave the form to make the\n * account it belongs to. The clear control is the unlink: a saved contact\n * with no company reads as a decision rather than a field somebody skipped.\n *\n * A stored id the list does not carry — a company past the picker's ceiling,\n * or one this viewer cannot see — still renders as SOMETHING selected, or\n * the field would read empty for a contact that has a company and the next\n * save would silently unlink them.\n */\nexport function CompanyPicker(props: CompanyPickerProps) {\n const {\n options,\n value,\n onChange,\n onCreate,\n label = 'Company',\n helperText,\n disabled,\n email,\n fallbackName,\n ready = true,\n truncated,\n } = props\n const [creating, setCreating] = useState(false)\n const [createError, setCreateError] = useState('')\n\n const suggestion = useMemo(\n () => (email ? suggestCompanyForEmail(email, options) : null),\n [email, options],\n )\n const fallback = useMemo(() => {\n const name = String(fallbackName ?? '').trim()\n if (value || !name) return null\n const lower = name.toLowerCase()\n return {\n name,\n match: options.find((option) => option.name.trim().toLowerCase() === lower) ?? null,\n }\n }, [fallbackName, options, value])\n\n const selected = useMemo<PickerOption | null>(() => {\n if (!value) return null\n return (\n options.find((option) => option.id === value) ?? {\n id: value,\n name: `Company ${value.slice(-6)}`,\n domain: null,\n }\n )\n }, [options, value])\n /*\n * The list the control searches: every option, plus the placeholder for a\n * stored id the list does not carry, so the selected value is always one\n * of the options and the control never reports it as invalid.\n */\n const listed = useMemo<PickerOption[]>(\n () =>\n selected && !options.some((option) => option.id === selected.id)\n ? [selected, ...options]\n : [...options],\n [options, selected],\n )\n\n const create = useCallback(\n async (name: string) => {\n if (!onCreate) return\n setCreating(true)\n setCreateError('')\n try {\n const company = await onCreate(name)\n onChange(company.id, company)\n } catch (error) {\n setCreateError(\n error instanceof Error && error.message\n ? error.message\n : 'The company could not be created.',\n )\n } finally {\n setCreating(false)\n }\n },\n [onCreate, onChange],\n )\n\n const busy = disabled || !ready || creating\n return (\n <Stack spacing={0.5}>\n <Autocomplete<PickerOption, false, false, false>\n size=\"small\"\n options={listed}\n value={selected}\n disabled={busy}\n loading={!ready}\n getOptionLabel={optionLabel}\n isOptionEqualToValue={(option, current) => option.id === current.id}\n filterOptions={(candidates, state) => {\n const filtered = filterCompanies(candidates, state)\n const typed = state.inputValue.trim()\n const exact = typed.toLowerCase()\n const named = candidates.some(\n (option) =>\n !isCreateOption(option) && option.name.trim().toLowerCase() === exact,\n )\n if (onCreate && typed && !named) {\n filtered.push({ id: CREATE_OPTION_ID, name: typed, create: typed })\n }\n return filtered\n }}\n onChange={(_event, next) => {\n if (!next) return onChange(null, null)\n if (isCreateOption(next)) return void create(next.create)\n onChange(next.id, next)\n }}\n renderInput={(params) => (\n <TextField\n {...params}\n label={label}\n error={Boolean(createError)}\n helperText={\n !ready\n ? 'Loading companies…'\n : creating\n ? 'Creating the company…'\n : createError ||\n helperText ||\n (truncated\n ? `The first ${COMPANY_OPTIONS_LIMIT} by name — type to narrow, or create one.`\n : undefined)\n }\n />\n )}\n />\n {suggestion && suggestion.id !== value ? (\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Suggested from the email address: ${suggestion.name}`}\n </Typography>\n <Button\n size=\"small\"\n disabled={busy}\n onClick={() => onChange(suggestion.id, suggestion)}\n >\n {'Use'}\n </Button>\n </Stack>\n ) : fallback ? (\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {fallback.match\n ? `Recorded as “${fallback.name}” — a company by that name exists.`\n : `Recorded as “${fallback.name}” without a link.`}\n </Typography>\n {fallback.match ? (\n <Button\n size=\"small\"\n disabled={busy}\n onClick={() => onChange(fallback.match?.id ?? null, fallback.match)}\n >\n {'Use'}\n </Button>\n ) : onCreate ? (\n <Button size=\"small\" disabled={busy} onClick={() => void create(fallback.name)}>\n {`Create “${fallback.name}”`}\n </Button>\n ) : null}\n </Stack>\n ) : null}\n </Stack>\n )\n}\nCompanyPicker.displayName = 'CompanyPicker'\n\nexport default CompanyPicker\n"],"names":["createResourceUid","CRM_COLLECTIONS","CRM_RECORDS_BAND_FULL_MESSAGE","crmNewRecordListFields","useFirestore","useFirestoreCollection","useUser","Autocomplete","Button","createFilterOptions","Stack","TextField","Typography","collection","doc","limit","orderBy","query","serverTimestamp","setDoc","useCallback","useMemo","useState","readCrmRecordsQuota","crmVisibleToClause","useCrmScope","companyDraftFields","EMPTY_COMPANY_DRAFT","suggestCompanyForEmail","useCrmSharingFollowUp","COMPANY_OPTIONS_LIMIT","useCompanyOptions","props","hostId","org","enabled","firestore","scope","visibleTo","data","status","companies","idField","options","slice","map","row","id","String","name","domain","ready","Boolean","truncated","length","useCreateCompany","user","createTokens","createHostId","followUpSharing","uid","result","ownerUid","ok","Error","error","quota","allowed","set","createdByUid","createdAt","updatedAt","CREATE_OPTION_ID","isCreateOption","option","filterCompanies","stringify","create","optionLabel","CompanyPicker","value","onChange","onCreate","label","helperText","disabled","email","fallbackName","creating","setCreating","createError","setCreateError","suggestion","fallback","trim","lower","toLowerCase","match","find","selected","listed","some","company","message","busy","spacing","size","loading","getOptionLabel","isOptionEqualToValue","current","filterOptions","candidates","state","filtered","typed","inputValue","exact","named","push","_event","next","renderInput","params","undefined","direction","sx","alignItems","variant","color","onClick","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,SAEEA,iBAAiB,EACjBC,eAAe,EACfC,6BAA6B,EAC7BC,sBAAsB,QACjB,eAAc;AACrB,SACEC,YAAY,EACZC,sBAAsB,EACtBC,OAAO,QACF,iCAAgC;AACvC,SACEC,YAAY,EACZC,MAAM,EACNC,mBAAmB,EACnBC,KAAK,EACLC,SAAS,EACTC,UAAU,QACL,gBAAe;AACtB,SACEC,UAAU,EACVC,GAAG,EACHC,KAAK,EACLC,OAAO,EACPC,KAAK,EACLC,eAAe,EACfC,MAAM,QAED,qBAAoB;AAC3B,SAASC,WAAW,EAAEC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AACtD,SAASC,mBAAmB,QAAQ,oCAAgC;AACpE,SAASC,kBAAkB,EAAEC,WAAW,QAAQ,4BAAwB;AACxE,SAEEC,kBAAkB,EAClBC,mBAAmB,EACnBC,sBAAsB,QACjB,wBAAoB;AAC3B,SAASC,qBAAqB,QAAQ,8BAA0B;AAEhE,SAASD,sBAAsB,GAAsB;AAErD;;;;;;;;CAQC,GACD,OAAO,MAAME,wBAAwB,IAAG;AAUxC;;;;;;;;;;;;;;CAcC,GACD,OAAO,SAASC,kBAAkBC,KAKjC;;IACC,MAAM,EAAEC,MAAM,EAAEC,GAAG,EAAEC,UAAU,IAAI,EAAE,GAAGH;IACxC,MAAMI,YAAYhC;IAClB,MAAM,EAAEiC,KAAK,EAAEC,SAAS,EAAE,GAAGb,YAAY;QAAEQ;QAAQC;IAAI;IACvD,MAAM,EAAEK,IAAI,EAAEC,MAAM,EAAE,GAAGnC,uBACvB,IACE8B,WAAWE,QACPpB,MACEJ,WAAWuB,WAAWC,KAAK,CAAC,EAAE,EAAEA,KAAK,CAAC,EAAE,EAAEpC,gBAAgBwC,SAAS,MAChEjB,mBAAmBc,YACtBtB,QAAQ,cACRD,MAAMe,wBAAwB,MAEhC,MACN;QAACM;QAAWC;QAAOC;QAAWH;KAAQ,EACtC;QAAEO,SAAS;IAAM;IAEnB,MAAMC,UAAUtB,QACd,IACE,CAACkB,eAAAA,OAAQ,EAAE,EAAEK,KAAK,CAAC,GAAGd,uBAAuBe,GAAG,CAAC,CAACC;gBAEnCA;mBAF4C;gBACzDC,IAAIC,OAAOF,GAAG,CAAC,MAAM;gBACrBG,MAAMD,QAAOF,YAAAA,GAAG,CAAC,OAAO,YAAXA,YAAe;gBAC5BI,QAAQ,OAAOJ,GAAG,CAAC,SAAS,KAAK,WAAWA,GAAG,CAAC,SAAS,GAAG;YAC9D;YACF;QAACP;KAAK;IAER,OAAO;QACLI;QACAQ,OAAOC,QAAQf,UAAUG,WAAW;QACpCa,WAAW,SAACd,wBAAAA,KAAMe,MAAM,mBAAI,KAAKxB;IACnC;AACF;AAQA;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,SAASyB,iBAAiBvB,KAIhC;;IACC,MAAM,EAAEC,MAAM,EAAEC,GAAG,EAAE,GAAGF;IACxB,MAAMI,YAAYhC;IAClB,MAAM,EAAEmC,MAAMiB,IAAI,EAAE,GAAGlD;IACvB,qEAAqE;IACrE,0EAA0E;IAC1E,+DAA+D;IAC/D,MAAM,EAAE+B,KAAK,EAAEoB,YAAY,EAAEC,YAAY,EAAE,GAAGjC,YAAY;QAAEQ;QAAQC;IAAI;IACxE,MAAMyB,kBAAkB9B,sBAAsBI,gBAAQI,yBAAAA,KAAO,CAAC,EAAE,mBAAI;IACpE,MAAMuB,eAAMJ,wBAAAA,KAAMI,GAAG,oBAAI;IACzB,OAAOvC,QAA8B;QACnC,IAAI,CAACgB,SAAS,CAACqB,cAAc,OAAO;QACpC,OAAO,OAAOT;YACZ,MAAMY,SAASnC,mBAAmB,aAC7BC;gBACHsB;gBACAa,UAAUF;;YAEZ,IAAIC,OAAOE,EAAE,KAAK,OAAO,MAAM,IAAIC,MAAMH,OAAOI,KAAK;YACrD,MAAMC,QAAQ,MAAM3C,oBAAoBa,WAAWC,OAAOH;YAC1D,IAAI,CAACgC,MAAMC,OAAO,EAAE,MAAM,IAAIH,MAAM9D;YACpC,MAAM6C,KAAK/C;YACX,MAAMmB,OACJL,IAAIsB,WAAWC,KAAK,CAAC,EAAE,EAAEA,KAAK,CAAC,EAAE,EAAEpC,gBAAgBwC,SAAS,EAAEM,KAC9D,aACKc,OAAOO,GAAG,EAEVjE,uBAAuB,aAAa,aAAK0D,OAAOO,GAAG;gBAAE9B,WAAW;uBAAImB;iBAAa;;gBACpFnB,WAAW;uBAAImB;iBAAa;gBAC5BxB,QAAQyB;gBACRW,cAAcT;gBACdU,WAAWpD;gBACXqD,WAAWrD;;YAGf,8DAA8D;YAC9DyC,gBAAgB,aAAa;gBAACZ;aAAG;YACjC,OAAO;gBAAEA;gBAAIE,MAAMD,OAAOa,OAAOO,GAAG,CAAC,OAAO;gBAAGlB,QAAQ;YAAK;QAC9D;IACF,GAAG;QAACb;QAAOoB;QAAcrB;QAAWsB;QAAcE;QAAK1B;QAAKyB;KAAgB;AAC9E;AAEA,+EAA+E,GAC/E,MAAMa,mBAAmB;AAIzB,MAAMC,iBAAiB,CACrBC,SACwD,YAAYA;AAEtE,MAAMC,kBAAkBlE,oBAAkC;IACxDmE,WAAW,CAACF,SACVD,eAAeC,UACXA,OAAOG,MAAM,GACbH,OAAOxB,MAAM,GACX,GAAGwB,OAAOzB,IAAI,CAAC,CAAC,EAAEyB,OAAOxB,MAAM,EAAE,GACjCwB,OAAOzB,IAAI;AACrB;AAEA,MAAM6B,cAAc,CAACJ,SACnBD,eAAeC,UACX,CAAC,QAAQ,EAAEA,OAAOG,MAAM,CAAC,CAAC,CAAC,GAC3BH,OAAOxB,MAAM,GACX,GAAGwB,OAAOzB,IAAI,CAAC,GAAG,EAAEyB,OAAOxB,MAAM,EAAE,GACnCwB,OAAOzB,IAAI;AAwCnB;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,SAAS8B,cAAc/C,KAAyB;IACrD,MAAM,EACJW,OAAO,EACPqC,KAAK,EACLC,QAAQ,EACRC,QAAQ,EACRC,QAAQ,SAAS,EACjBC,UAAU,EACVC,QAAQ,EACRC,KAAK,EACLC,YAAY,EACZpC,QAAQ,IAAI,EACZE,SAAS,EACV,GAAGrB;IACJ,MAAM,CAACwD,UAAUC,YAAY,GAAGnE,SAAS;IACzC,MAAM,CAACoE,aAAaC,eAAe,GAAGrE,SAAS;IAE/C,MAAMsE,aAAavE,QACjB,IAAOiE,QAAQ1D,uBAAuB0D,OAAO3C,WAAW,MACxD;QAAC2C;QAAO3C;KAAQ;IAElB,MAAMkD,WAAWxE,QAAQ;YAMdsB;QALT,MAAMM,OAAOD,OAAOuC,uBAAAA,eAAgB,IAAIO,IAAI;QAC5C,IAAId,SAAS,CAAC/B,MAAM,OAAO;QAC3B,MAAM8C,QAAQ9C,KAAK+C,WAAW;QAC9B,OAAO;YACL/C;YACAgD,KAAK,GAAEtD,gBAAAA,QAAQuD,IAAI,CAAC,CAACxB,SAAWA,OAAOzB,IAAI,CAAC6C,IAAI,GAAGE,WAAW,OAAOD,kBAA9DpD,gBAAwE;QACjF;IACF,GAAG;QAAC4C;QAAc5C;QAASqC;KAAM;IAEjC,MAAMmB,WAAW9E,QAA6B;YAG1CsB;QAFF,IAAI,CAACqC,OAAO,OAAO;QACnB,QACErC,gBAAAA,QAAQuD,IAAI,CAAC,CAACxB,SAAWA,OAAO3B,EAAE,KAAKiC,kBAAvCrC,gBAAiD;YAC/CI,IAAIiC;YACJ/B,MAAM,CAAC,QAAQ,EAAE+B,MAAMpC,KAAK,CAAC,CAAC,IAAI;YAClCM,QAAQ;QACV;IAEJ,GAAG;QAACP;QAASqC;KAAM;IACnB;;;;GAIC,GACD,MAAMoB,SAAS/E,QACb,IACE8E,YAAY,CAACxD,QAAQ0D,IAAI,CAAC,CAAC3B,SAAWA,OAAO3B,EAAE,KAAKoD,SAASpD,EAAE,IAC3D;YAACoD;eAAaxD;SAAQ,GACtB;eAAIA;SAAQ,EAClB;QAACA;QAASwD;KAAS;IAGrB,MAAMtB,SAASzD,YACb,OAAO6B;QACL,IAAI,CAACiC,UAAU;QACfO,YAAY;QACZE,eAAe;QACf,IAAI;YACF,MAAMW,UAAU,MAAMpB,SAASjC;YAC/BgC,SAASqB,QAAQvD,EAAE,EAAEuD;QACvB,EAAE,OAAOrC,OAAO;YACd0B,eACE1B,iBAAiBD,SAASC,MAAMsC,OAAO,GACnCtC,MAAMsC,OAAO,GACb;QAER,SAAU;YACRd,YAAY;QACd;IACF,GACA;QAACP;QAAUD;KAAS;IAGtB,MAAMuB,OAAOnB,YAAY,CAAClC,SAASqC;IACnC,qBACE,MAAC9E;QAAM+F,SAAS;;0BACd,KAAClG;gBACCmG,MAAK;gBACL/D,SAASyD;gBACTpB,OAAOmB;gBACPd,UAAUmB;gBACVG,SAAS,CAACxD;gBACVyD,gBAAgB9B;gBAChB+B,sBAAsB,CAACnC,QAAQoC,UAAYpC,OAAO3B,EAAE,KAAK+D,QAAQ/D,EAAE;gBACnEgE,eAAe,CAACC,YAAYC;oBAC1B,MAAMC,WAAWvC,gBAAgBqC,YAAYC;oBAC7C,MAAME,QAAQF,MAAMG,UAAU,CAACtB,IAAI;oBACnC,MAAMuB,QAAQF,MAAMnB,WAAW;oBAC/B,MAAMsB,QAAQN,WAAWX,IAAI,CAC3B,CAAC3B,SACC,CAACD,eAAeC,WAAWA,OAAOzB,IAAI,CAAC6C,IAAI,GAAGE,WAAW,OAAOqB;oBAEpE,IAAInC,YAAYiC,SAAS,CAACG,OAAO;wBAC/BJ,SAASK,IAAI,CAAC;4BAAExE,IAAIyB;4BAAkBvB,MAAMkE;4BAAOtC,QAAQsC;wBAAM;oBACnE;oBACA,OAAOD;gBACT;gBACAjC,UAAU,CAACuC,QAAQC;oBACjB,IAAI,CAACA,MAAM,OAAOxC,SAAS,MAAM;oBACjC,IAAIR,eAAegD,OAAO,OAAO,KAAK5C,OAAO4C,KAAK5C,MAAM;oBACxDI,SAASwC,KAAK1E,EAAE,EAAE0E;gBACpB;gBACAC,aAAa,CAACC,uBACZ,KAAChH,wBACKgH;wBACJxC,OAAOA;wBACPlB,OAAOb,QAAQsC;wBACfN,YACE,CAACjC,QACG,uBACAqC,WACE,0BACAE,eACAN,cACC/B,CAAAA,YACG,CAAC,UAAU,EAAEvB,sBAAsB,yCAAyC,CAAC,GAC7E8F,SAAQ;;;YAKzBhC,cAAcA,WAAW7C,EAAE,KAAKiC,sBAC/B,MAACtE;gBAAMmH,WAAU;gBAAMpB,SAAS;gBAAGqB,IAAI;oBAAEC,YAAY;gBAAS;;kCAC5D,KAACnH;wBAAWoH,SAAQ;wBAAUC,OAAM;kCACjC,CAAC,kCAAkC,EAAErC,WAAW3C,IAAI,EAAE;;kCAEzD,KAACzC;wBACCkG,MAAK;wBACLrB,UAAUmB;wBACV0B,SAAS,IAAMjD,SAASW,WAAW7C,EAAE,EAAE6C;kCAEtC;;;iBAGHC,yBACF,MAACnF;gBAAMmH,WAAU;gBAAMpB,SAAS;gBAAGqB,IAAI;oBAAEC,YAAY;gBAAS;;kCAC5D,KAACnH;wBAAWoH,SAAQ;wBAAUC,OAAM;kCACjCpC,SAASI,KAAK,GACX,CAAC,aAAa,EAAEJ,SAAS5C,IAAI,CAAC,kCAAkC,CAAC,GACjE,CAAC,aAAa,EAAE4C,SAAS5C,IAAI,CAAC,iBAAiB,CAAC;;oBAErD4C,SAASI,KAAK,iBACb,KAACzF;wBACCkG,MAAK;wBACLrB,UAAUmB;wBACV0B,SAAS;;gCAAerC;mCAATZ,kBAASY,kBAAAA,SAASI,KAAK,qBAAdJ,gBAAgB9C,EAAE,mBAAI,MAAM8C,SAASI,KAAK;;kCAEjE;yBAEDf,yBACF,KAAC1E;wBAAOkG,MAAK;wBAAQrB,UAAUmB;wBAAM0B,SAAS,IAAM,KAAKrD,OAAOgB,SAAS5C,IAAI;kCAC1E,CAAC,QAAQ,EAAE4C,SAAS5C,IAAI,CAAC,CAAC,CAAC;yBAE5B;;iBAEJ;;;AAGV;AACA8B,cAAcoD,WAAW,GAAG;AAE5B,eAAepD,cAAa"}
|
|
@@ -24,7 +24,7 @@ import { personKeyInBrowser } from "../model/person-key-web.js";
|
|
|
24
24
|
* point the tenant's loader imports to activate the plugin's SITE half, so a
|
|
25
25
|
* console card named there ships to every published page.
|
|
26
26
|
*/ import { CrmRecordAttributionZone } from "./crm-attribution-zone.js";
|
|
27
|
-
import
|
|
27
|
+
import ContainerPicker from "@aglyn/tenant-feature-instance/components/container-picker";
|
|
28
28
|
import { AppLink, CardDisplay } from "@aglyn/shared-ui-jsx";
|
|
29
29
|
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
30
30
|
import { useFirestore, writeGuardedBySeed } from "@aglyn/tenant-feature-instance";
|
|
@@ -140,7 +140,7 @@ import { useCrmCampaigns } from "../hooks/use-crm-campaigns.js";
|
|
|
140
140
|
// so "filed under no campaign" has one shape here and in the pass
|
|
141
141
|
// that detaches a deleted campaign.
|
|
142
142
|
()=>contactUpdate.updateOne(record.$id, {
|
|
143
|
-
campaignIds: Aglyn.
|
|
143
|
+
campaignIds: Aglyn.containerMembershipValue(campaigns)
|
|
144
144
|
}));
|
|
145
145
|
if (!verdict.ok) {
|
|
146
146
|
return void enqueueSnackbar(verdict.message, {
|
|
@@ -298,7 +298,8 @@ import { useCrmCampaigns } from "../hooks/use-crm-campaigns.js";
|
|
|
298
298
|
]
|
|
299
299
|
}),
|
|
300
300
|
/*#__PURE__*/ _jsx(Divider, {}),
|
|
301
|
-
/*#__PURE__*/ _jsx(
|
|
301
|
+
/*#__PURE__*/ _jsx(ContainerPicker, {
|
|
302
|
+
kind: "campaign",
|
|
302
303
|
options: siteCampaigns.options,
|
|
303
304
|
value: campaigns,
|
|
304
305
|
onChange: setCampaigns,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contact-associations-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport {\n CONTACT_SOURCE_LABELS,\n type ConsentGroup,\n type ContactSource,\n MARKETING_BASIS_LABELS,\n readMarketingBasis,\n} from '@aglyn/aglyn'\nimport { personKeyInBrowser } from '../model/person-key-web'\n/*\n * The component path and NOT the marketing barrel: that barrel is the entry\n * point the tenant's loader imports to activate the plugin's SITE half, so a\n * console card named there ships to every published page.\n */\nimport { CrmRecordAttributionZone } from './crm-attribution-zone'\nimport CampaignPicker from '@aglyn/shared-ui-email-campaigns/components/campaign-picker.component'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport { Button, Chip, Divider, Stack, Typography } from '@mui/material'\nimport { doc, getDoc } from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useContactUpdate } from '../hooks/use-contact-update'\nimport type { ContactRecord } from '../model/contact-record'\nimport { crmRoutes } from '../model/crm-routes'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { useCrmCampaigns } from '../hooks/use-crm-campaigns'\n\nexport interface ContactAssociationsCardProps {\n /**\n * The site whose lead, campaigns and attribution are shown — the mounted\n * site, or at the organization level the holder's own; `null` for a row\n * no site has captured, when none of the three has an address.\n */\n hostId: string | null\n /** The row, flattened through the viewing group's facet. */\n record: ContactRecord\n /** The document as read, for the consent fields the projection leaves out. */\n row: Record<string, unknown>\n consentGroup: ConsentGroup\n /** The listener's verdict on the row, for the stale-seed guard. */\n seed: { status: 'loading' | 'success' | 'error'; fromCache: boolean }\n /** The hub's own path, for the link to this person's lead. */\n basePath: string\n /**\n * The site's Bookings page narrowed to this person's address (AGL-2660),\n * or nothing where the site has no booking door — the page decides, and\n * the row is simply not drawn.\n */\n bookingsHref?: string | null\n}\n\n/**\n * WHERE THIS PERSON CAME FROM AND WHAT THEY ARE FILED UNDER (AGL-2596).\n *\n * Four facts that were in the v1 drawer and are not properties of the\n * person so much as of the RELATIONSHIP:\n *\n * - The SOURCES say which mechanism created the record — a form, a\n * checkout, an import.\n * - The ATTRIBUTION says which campaign led to it, recorded from the link\n * they followed and never editable. One keyed read, paid on opening the\n * page rather than per row of the list.\n * - The CONSENT is the basis this controller holds for marketing email:\n * opted in, opted out, or no record — three states that never collapse\n * into two, because \"no record\" is the commonest and is not a refusal.\n * - The FILING is which campaigns the team has PUT them in, a working set\n * kept by hand. It adds nobody to a send: a campaign's audience is its\n * lists and each email's own picker, and the helper says so where a\n * reader would otherwise assume the opposite.\n *\n * The filing is the one editable thing here and has its own **Save filing**\n * in the card header, through the same stale-seed guard the properties card\n * uses: a campaign membership\n * written over a cached read could revert somebody else's filing. The save\n * goes to `crm/contact-update` (AGL-2804), because the filing is this\n * holder's facet and a facet is the server's to write; like every field that\n * route writes, it is refused to a plan without the CRM (AGL-2851).\n *\n * ## The lead (AGL-2612)\n *\n * A LEAD is the sales record a lead surface — a sign-up, a booking, a form\n * with lead routing — files beside the contact, under the capturing site at\n * `hosts/{hostId}/leads/{personKey}`. The lead's page links the contact\n * once converted; nothing linked back, so the two halves of one person\n * read as unrelated. One `getDoc` by the key the capture derives — the\n * address, normalized and hashed, computed here on WebCrypto — is what\n * answers \"does this site hold a lead for this person\", and it is a read\n * this page pays because opening the record IS the ask. Absence is a fact\n * too: a contact captured by an order or an import has no lead, and the\n * chip is simply not there.\n */\nexport function ContactAssociationsCard(props: ContactAssociationsCardProps) {\n const { hostId, record, row, consentGroup, seed, basePath, bookingsHref } = props\n // The lead's collection is the org's now (AGL-3275); the site still names\n // the surface, and the scope hook resolves the org from it.\n const { orgId } = useCrmScope({ hostId })\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const contactUpdate = useContactUpdate(hostId)\n const routes = crmRoutes(basePath)\n\n const [leadKey, setLeadKey] = useState<string | null>(null)\n const email = record.email\n useEffect(() => {\n let active = true\n setLeadKey(null)\n if (!hostId || !orgId) return undefined\n void (async () => {\n const key = await personKeyInBrowser(email)\n if (!key || !active) return\n const snapshot = await getDoc(\n doc(firestore, 'orgs', orgId, 'leads', key),\n ).catch(() => null)\n if (active && snapshot?.exists()) setLeadKey(key)\n })().catch(() => undefined)\n return () => {\n active = false\n }\n }, [firestore, hostId, email])\n\n const [campaigns, setCampaigns] = useState<string[]>(record.campaignIds)\n const [saving, setSaving] = useState(false)\n const recordId = record.$id\n useEffect(() => {\n setCampaigns(record.campaignIds)\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [recordId])\n\n /*\n * The campaigns, read because this page IS the ask: one picker on one\n * record, and nothing in the list ever paid for the campaign list. Under\n * a site, the ones placed on it; at the organization level, every one.\n */\n const siteCampaigns = useCrmCampaigns({ hostId, orgId }, { enabled: true })\n\n const consent = useMemo(\n () => readMarketingBasis(row, consentGroup),\n [row, consentGroup],\n )\n\n const handleSaveFiling = useCallback(async () => {\n setSaving(true)\n try {\n const verdict = await writeGuardedBySeed(\n {\n subject: 'contact',\n unreadable: seed.status === 'error',\n fromCache: seed.fromCache,\n },\n // An empty selection is sent as an empty list rather than left out,\n // so \"filed under no campaign\" has one shape here and in the pass\n // that detaches a deleted campaign.\n () =>\n contactUpdate.updateOne(record.$id, {\n campaignIds: Aglyn.campaignMembershipValue(campaigns),\n }),\n )\n if (!verdict.ok) {\n return void enqueueSnackbar(verdict.message, {\n variant: 'warning',\n persist: false,\n })\n }\n enqueueSnackbar('Filing saved', { variant: 'success', persist: false })\n } catch (error) {\n console.error(error)\n enqueueSnackbar(\n error instanceof Error && error.message ? error.message : 'An error has occurred',\n { variant: 'error', allowDuplicate: true },\n )\n } finally {\n setSaving(false)\n }\n }, [campaigns, contactUpdate, enqueueSnackbar, record.$id, seed.fromCache, seed.status])\n\n const sources = Object.keys(record.sources ?? {}) as ContactSource[]\n const consentDetail = [\n consent.basisAtMs\n ? `recorded ${new Date(consent.basisAtMs).toLocaleDateString()}`\n : null,\n consent.assertedBy === 'operator' ? 'entered by your team' : null,\n consent.otherGrant === 'other-host'\n ? 'opted in to another site in this workspace'\n : null,\n ].filter(Boolean)\n\n return (\n <CardDisplay\n header={'Relationship'}\n help={Aglyn.pluginDocsHelp('contactRecord', { anchor: '#what-each-site-keeps-to-itself' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Button\n size=\"small\"\n variant=\"outlined\"\n disabled={saving || (siteCampaigns.ready && !siteCampaigns.options.length)}\n onClick={() => void handleSaveFiling()}\n >\n {saving ? 'Saving…' : 'Save filing'}\n </Button>\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">{'Sources'}</Typography>\n {sources.length || leadKey ? (\n <Stack direction=\"row\" spacing={0.5} sx={{ flexWrap: 'wrap', rowGap: 0.5 }}>\n {sources.map((source) => (\n <Chip\n key={source}\n label={CONTACT_SOURCE_LABELS[source] ?? source}\n size=\"small\"\n />\n ))}\n {/*\n This site's lead for the person, when it holds one — a link,\n in the shape the lead's page uses to point back here.\n */}\n {leadKey ? (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.lead(leadKey)}\n size=\"small\"\n variant=\"outlined\"\n sx={{ py: 0, minHeight: 24, lineHeight: 1.5 }}\n >\n {'Lead on this site'}\n </Button>\n ) : null}\n </Stack>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No capture recorded.'}\n </Typography>\n )}\n </Stack>\n {/*\n The person's bookings on this site (AGL-2660): every booking\n taken with their address, past ones too, on the Bookings page —\n the appointments are that page's records, and this card only\n points at them.\n */}\n {bookingsHref ? (\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">{'Bookings'}</Typography>\n <Typography variant=\"body2\">\n <AppLink href={bookingsHref}>{'Bookings on this site'}</AppLink>\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Every booking made with this address, upcoming and past.'}\n </Typography>\n </Stack>\n ) : null}\n {hostId ? (\n <Stack spacing={0.5}>\n <CrmRecordAttributionZone\n hostId={hostId}\n recordKind=\"contact\"\n recordId={record.$id}\n />\n {/* Attribution is the campaign that brought the person; the\n picker below is the team's own filing (AGL-3274). */}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Filing is separate — see Filed under campaigns.'}\n </Typography>\n </Stack>\n ) : null}\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">{'Marketing email'}</Typography>\n <Typography variant=\"body2\">\n {MARKETING_BASIS_LABELS[consent.basis]}\n {consentDetail.length ? ` · ${consentDetail.join(' · ')}` : ''}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {consent.basis === 'unrecorded'\n ? 'Nothing was recorded either way — not a refusal, and not permission.'\n : `The basis this ${\n consentGroup.declared ? 'group' : 'site'\n } holds. Other sites in the workspace keep their own.`}\n </Typography>\n </Stack>\n <Divider />\n <CampaignPicker\n options={siteCampaigns.options}\n value={campaigns}\n onChange={setCampaigns}\n label=\"Filed under campaigns\"\n helperText=\"Your own filing. It never adds anyone to a send — a campaign mails its lists.\"\n empty={siteCampaigns.ready && !siteCampaigns.options.length}\n emptyText={hostId ? 'This site has no campaigns yet.' : 'There are no campaigns yet.'}\n />\n </Stack>\n </CardDisplay>\n )\n}\nContactAssociationsCard.displayName = 'ContactAssociationsCard'\n\nexport default ContactAssociationsCard\n"],"names":["Aglyn","CONTACT_SOURCE_LABELS","MARKETING_BASIS_LABELS","readMarketingBasis","personKeyInBrowser","CrmRecordAttributionZone","CampaignPicker","AppLink","CardDisplay","useSnackbar","useFirestore","writeGuardedBySeed","Button","Chip","Divider","Stack","Typography","doc","getDoc","useCallback","useEffect","useMemo","useState","useContactUpdate","crmRoutes","useCrmScope","useCrmCampaigns","ContactAssociationsCard","props","record","hostId","row","consentGroup","seed","basePath","bookingsHref","orgId","firestore","enqueueSnackbar","contactUpdate","routes","leadKey","setLeadKey","email","active","undefined","key","snapshot","catch","exists","campaigns","setCampaigns","campaignIds","saving","setSaving","recordId","$id","siteCampaigns","enabled","consent","handleSaveFiling","verdict","subject","unreadable","status","fromCache","updateOne","campaignMembershipValue","ok","message","variant","persist","error","console","Error","allowDuplicate","sources","Object","keys","consentDetail","basisAtMs","Date","toLocaleDateString","assertedBy","otherGrant","filter","Boolean","header","help","pluginDocsHelp","anchor","contentGutterX","contentGutterY","HeaderProps","action","size","disabled","ready","options","length","onClick","spacing","direction","sx","flexWrap","rowGap","map","source","label","component","componentVariant","nativeButton","href","lead","py","minHeight","lineHeight","color","recordKind","basis","join","declared","value","onChange","helperText","empty","emptyText","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contact-associations-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport {\n CONTACT_SOURCE_LABELS,\n type ConsentGroup,\n type ContactSource,\n MARKETING_BASIS_LABELS,\n readMarketingBasis,\n} from '@aglyn/aglyn'\nimport { personKeyInBrowser } from '../model/person-key-web'\n/*\n * The component path and NOT the marketing barrel: that barrel is the entry\n * point the tenant's loader imports to activate the plugin's SITE half, so a\n * console card named there ships to every published page.\n */\nimport { CrmRecordAttributionZone } from './crm-attribution-zone'\nimport ContainerPicker from '@aglyn/tenant-feature-instance/components/container-picker'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport { Button, Chip, Divider, Stack, Typography } from '@mui/material'\nimport { doc, getDoc } from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useContactUpdate } from '../hooks/use-contact-update'\nimport type { ContactRecord } from '../model/contact-record'\nimport { crmRoutes } from '../model/crm-routes'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { useCrmCampaigns } from '../hooks/use-crm-campaigns'\n\nexport interface ContactAssociationsCardProps {\n /**\n * The site whose lead, campaigns and attribution are shown — the mounted\n * site, or at the organization level the holder's own; `null` for a row\n * no site has captured, when none of the three has an address.\n */\n hostId: string | null\n /** The row, flattened through the viewing group's facet. */\n record: ContactRecord\n /** The document as read, for the consent fields the projection leaves out. */\n row: Record<string, unknown>\n consentGroup: ConsentGroup\n /** The listener's verdict on the row, for the stale-seed guard. */\n seed: { status: 'loading' | 'success' | 'error'; fromCache: boolean }\n /** The hub's own path, for the link to this person's lead. */\n basePath: string\n /**\n * The site's Bookings page narrowed to this person's address (AGL-2660),\n * or nothing where the site has no booking door — the page decides, and\n * the row is simply not drawn.\n */\n bookingsHref?: string | null\n}\n\n/**\n * WHERE THIS PERSON CAME FROM AND WHAT THEY ARE FILED UNDER (AGL-2596).\n *\n * Four facts that were in the v1 drawer and are not properties of the\n * person so much as of the RELATIONSHIP:\n *\n * - The SOURCES say which mechanism created the record — a form, a\n * checkout, an import.\n * - The ATTRIBUTION says which campaign led to it, recorded from the link\n * they followed and never editable. One keyed read, paid on opening the\n * page rather than per row of the list.\n * - The CONSENT is the basis this controller holds for marketing email:\n * opted in, opted out, or no record — three states that never collapse\n * into two, because \"no record\" is the commonest and is not a refusal.\n * - The FILING is which campaigns the team has PUT them in, a working set\n * kept by hand. It adds nobody to a send: a campaign's audience is its\n * lists and each email's own picker, and the helper says so where a\n * reader would otherwise assume the opposite.\n *\n * The filing is the one editable thing here and has its own **Save filing**\n * in the card header, through the same stale-seed guard the properties card\n * uses: a campaign membership\n * written over a cached read could revert somebody else's filing. The save\n * goes to `crm/contact-update` (AGL-2804), because the filing is this\n * holder's facet and a facet is the server's to write; like every field that\n * route writes, it is refused to a plan without the CRM (AGL-2851).\n *\n * ## The lead (AGL-2612)\n *\n * A LEAD is the sales record a lead surface — a sign-up, a booking, a form\n * with lead routing — files beside the contact, under the capturing site at\n * `hosts/{hostId}/leads/{personKey}`. The lead's page links the contact\n * once converted; nothing linked back, so the two halves of one person\n * read as unrelated. One `getDoc` by the key the capture derives — the\n * address, normalized and hashed, computed here on WebCrypto — is what\n * answers \"does this site hold a lead for this person\", and it is a read\n * this page pays because opening the record IS the ask. Absence is a fact\n * too: a contact captured by an order or an import has no lead, and the\n * chip is simply not there.\n */\nexport function ContactAssociationsCard(props: ContactAssociationsCardProps) {\n const { hostId, record, row, consentGroup, seed, basePath, bookingsHref } = props\n // The lead's collection is the org's now (AGL-3275); the site still names\n // the surface, and the scope hook resolves the org from it.\n const { orgId } = useCrmScope({ hostId })\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const contactUpdate = useContactUpdate(hostId)\n const routes = crmRoutes(basePath)\n\n const [leadKey, setLeadKey] = useState<string | null>(null)\n const email = record.email\n useEffect(() => {\n let active = true\n setLeadKey(null)\n if (!hostId || !orgId) return undefined\n void (async () => {\n const key = await personKeyInBrowser(email)\n if (!key || !active) return\n const snapshot = await getDoc(\n doc(firestore, 'orgs', orgId, 'leads', key),\n ).catch(() => null)\n if (active && snapshot?.exists()) setLeadKey(key)\n })().catch(() => undefined)\n return () => {\n active = false\n }\n }, [firestore, hostId, email])\n\n const [campaigns, setCampaigns] = useState<string[]>(record.campaignIds)\n const [saving, setSaving] = useState(false)\n const recordId = record.$id\n useEffect(() => {\n setCampaigns(record.campaignIds)\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [recordId])\n\n /*\n * The campaigns, read because this page IS the ask: one picker on one\n * record, and nothing in the list ever paid for the campaign list. Under\n * a site, the ones placed on it; at the organization level, every one.\n */\n const siteCampaigns = useCrmCampaigns({ hostId, orgId }, { enabled: true })\n\n const consent = useMemo(\n () => readMarketingBasis(row, consentGroup),\n [row, consentGroup],\n )\n\n const handleSaveFiling = useCallback(async () => {\n setSaving(true)\n try {\n const verdict = await writeGuardedBySeed(\n {\n subject: 'contact',\n unreadable: seed.status === 'error',\n fromCache: seed.fromCache,\n },\n // An empty selection is sent as an empty list rather than left out,\n // so \"filed under no campaign\" has one shape here and in the pass\n // that detaches a deleted campaign.\n () =>\n contactUpdate.updateOne(record.$id, {\n campaignIds: Aglyn.containerMembershipValue(campaigns),\n }),\n )\n if (!verdict.ok) {\n return void enqueueSnackbar(verdict.message, {\n variant: 'warning',\n persist: false,\n })\n }\n enqueueSnackbar('Filing saved', { variant: 'success', persist: false })\n } catch (error) {\n console.error(error)\n enqueueSnackbar(\n error instanceof Error && error.message ? error.message : 'An error has occurred',\n { variant: 'error', allowDuplicate: true },\n )\n } finally {\n setSaving(false)\n }\n }, [campaigns, contactUpdate, enqueueSnackbar, record.$id, seed.fromCache, seed.status])\n\n const sources = Object.keys(record.sources ?? {}) as ContactSource[]\n const consentDetail = [\n consent.basisAtMs\n ? `recorded ${new Date(consent.basisAtMs).toLocaleDateString()}`\n : null,\n consent.assertedBy === 'operator' ? 'entered by your team' : null,\n consent.otherGrant === 'other-host'\n ? 'opted in to another site in this workspace'\n : null,\n ].filter(Boolean)\n\n return (\n <CardDisplay\n header={'Relationship'}\n help={Aglyn.pluginDocsHelp('contactRecord', { anchor: '#what-each-site-keeps-to-itself' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Button\n size=\"small\"\n variant=\"outlined\"\n disabled={saving || (siteCampaigns.ready && !siteCampaigns.options.length)}\n onClick={() => void handleSaveFiling()}\n >\n {saving ? 'Saving…' : 'Save filing'}\n </Button>\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">{'Sources'}</Typography>\n {sources.length || leadKey ? (\n <Stack direction=\"row\" spacing={0.5} sx={{ flexWrap: 'wrap', rowGap: 0.5 }}>\n {sources.map((source) => (\n <Chip\n key={source}\n label={CONTACT_SOURCE_LABELS[source] ?? source}\n size=\"small\"\n />\n ))}\n {/*\n This site's lead for the person, when it holds one — a link,\n in the shape the lead's page uses to point back here.\n */}\n {leadKey ? (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.lead(leadKey)}\n size=\"small\"\n variant=\"outlined\"\n sx={{ py: 0, minHeight: 24, lineHeight: 1.5 }}\n >\n {'Lead on this site'}\n </Button>\n ) : null}\n </Stack>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No capture recorded.'}\n </Typography>\n )}\n </Stack>\n {/*\n The person's bookings on this site (AGL-2660): every booking\n taken with their address, past ones too, on the Bookings page —\n the appointments are that page's records, and this card only\n points at them.\n */}\n {bookingsHref ? (\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">{'Bookings'}</Typography>\n <Typography variant=\"body2\">\n <AppLink href={bookingsHref}>{'Bookings on this site'}</AppLink>\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Every booking made with this address, upcoming and past.'}\n </Typography>\n </Stack>\n ) : null}\n {hostId ? (\n <Stack spacing={0.5}>\n <CrmRecordAttributionZone\n hostId={hostId}\n recordKind=\"contact\"\n recordId={record.$id}\n />\n {/* Attribution is the campaign that brought the person; the\n picker below is the team's own filing (AGL-3274). */}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Filing is separate — see Filed under campaigns.'}\n </Typography>\n </Stack>\n ) : null}\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">{'Marketing email'}</Typography>\n <Typography variant=\"body2\">\n {MARKETING_BASIS_LABELS[consent.basis]}\n {consentDetail.length ? ` · ${consentDetail.join(' · ')}` : ''}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {consent.basis === 'unrecorded'\n ? 'Nothing was recorded either way — not a refusal, and not permission.'\n : `The basis this ${\n consentGroup.declared ? 'group' : 'site'\n } holds. Other sites in the workspace keep their own.`}\n </Typography>\n </Stack>\n <Divider />\n <ContainerPicker\n kind=\"campaign\"\n options={siteCampaigns.options}\n value={campaigns}\n onChange={setCampaigns}\n label=\"Filed under campaigns\"\n helperText=\"Your own filing. It never adds anyone to a send — a campaign mails its lists.\"\n empty={siteCampaigns.ready && !siteCampaigns.options.length}\n emptyText={hostId ? 'This site has no campaigns yet.' : 'There are no campaigns yet.'}\n />\n </Stack>\n </CardDisplay>\n )\n}\nContactAssociationsCard.displayName = 'ContactAssociationsCard'\n\nexport default ContactAssociationsCard\n"],"names":["Aglyn","CONTACT_SOURCE_LABELS","MARKETING_BASIS_LABELS","readMarketingBasis","personKeyInBrowser","CrmRecordAttributionZone","ContainerPicker","AppLink","CardDisplay","useSnackbar","useFirestore","writeGuardedBySeed","Button","Chip","Divider","Stack","Typography","doc","getDoc","useCallback","useEffect","useMemo","useState","useContactUpdate","crmRoutes","useCrmScope","useCrmCampaigns","ContactAssociationsCard","props","record","hostId","row","consentGroup","seed","basePath","bookingsHref","orgId","firestore","enqueueSnackbar","contactUpdate","routes","leadKey","setLeadKey","email","active","undefined","key","snapshot","catch","exists","campaigns","setCampaigns","campaignIds","saving","setSaving","recordId","$id","siteCampaigns","enabled","consent","handleSaveFiling","verdict","subject","unreadable","status","fromCache","updateOne","containerMembershipValue","ok","message","variant","persist","error","console","Error","allowDuplicate","sources","Object","keys","consentDetail","basisAtMs","Date","toLocaleDateString","assertedBy","otherGrant","filter","Boolean","header","help","pluginDocsHelp","anchor","contentGutterX","contentGutterY","HeaderProps","action","size","disabled","ready","options","length","onClick","spacing","direction","sx","flexWrap","rowGap","map","source","label","component","componentVariant","nativeButton","href","lead","py","minHeight","lineHeight","color","recordKind","basis","join","declared","kind","value","onChange","helperText","empty","emptyText","displayName"],"mappings":"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"}
|
|
@@ -35,10 +35,10 @@ import { type CrmDetailPageProps } from '../model/crm-routes';
|
|
|
35
35
|
* The row is shared by every site that has captured this person, so the
|
|
36
36
|
* action in the overflow menu drops THIS group's facet, consent, attribution
|
|
37
37
|
* and scope tokens, and deletes the document only when nobody else is left
|
|
38
|
-
* holding it
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
38
|
+
* holding it. `crm/contact-remove` decides which against the stored
|
|
39
|
+
* document and keeps any marketing refusal the person gave (AGL-3338); the
|
|
40
|
+
* rules refuse a client the write. Not the erasure path: a privacy erasure
|
|
41
|
+
* removes the person regardless of who else holds them.
|
|
42
42
|
*/
|
|
43
43
|
export declare function ContactDetailPage(props: CrmDetailPageProps): import("react").JSX.Element;
|
|
44
44
|
export declare namespace ContactDetailPage {
|
|
@@ -18,18 +18,19 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
18
18
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
19
19
|
import * as Aglyn from "@aglyn/aglyn";
|
|
20
20
|
import { CONTACT_LIFECYCLE_STAGE_LABELS, pluginDocsHelp } from "@aglyn/aglyn";
|
|
21
|
+
import { pluginRecordByEmailHref, pluginRecordHref } from "@aglyn/aglyn/plugin-manager/plugin-record-routes";
|
|
21
22
|
import { mdiDeleteOutline, mdiMerge } from "@aglyn/shared-data-mdi";
|
|
22
23
|
import { AppLink, MdiIcon, useConfirmationContext } from "@aglyn/shared-ui-jsx";
|
|
23
24
|
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
24
|
-
import { useFirestore, useFirestoreDoc
|
|
25
|
-
import { restampCrmListFields } from "../model/list-fields-api.js";
|
|
26
|
-
import { crmTaskCallScope } from "../model/task-routes.js";
|
|
25
|
+
import { useFirestore, useFirestoreDoc } from "@aglyn/tenant-feature-instance";
|
|
27
26
|
import { Stack, Tooltip, Typography } from "@mui/material";
|
|
28
|
-
import {
|
|
27
|
+
import { doc } from "firebase/firestore";
|
|
29
28
|
import { useParams, useRouter } from "next/navigation";
|
|
30
29
|
import { useCallback, useMemo } from "react";
|
|
30
|
+
import { useContactRemove } from "../hooks/use-contact-remove.js";
|
|
31
31
|
import { useCrmActivityLogger } from "../hooks/use-crm-activity-logger.js";
|
|
32
32
|
import { CrmCreateSiteDefault, useCrmOrgMount } from "../hooks/use-crm-org-mount.js";
|
|
33
|
+
import { crmShareChipFor } from "../model/crm-sharing.js";
|
|
33
34
|
import { useCrmScope } from "../hooks/use-crm-scope.js";
|
|
34
35
|
import { contactPrimaryGroup, contactRecordFromDoc } from "../model/contact-record.js";
|
|
35
36
|
import { crmRoutes } from "../model/crm-routes.js";
|
|
@@ -51,8 +52,9 @@ import { CrmRecordInsightsZone } from "./crm-record-insights-zone.js";
|
|
|
51
52
|
import { CrmSuiteNotice, crmSuiteIncluded, crmSuiteLockedReason } from "./crm-suite-lock.js";
|
|
52
53
|
import { useErasePersonAction } from "./erase-person-action.js";
|
|
53
54
|
import RecordFilesCard from "./record-files-card.js";
|
|
55
|
+
import { CrmShareChipView, RecordSharingCard } from "./record-sharing-card.js";
|
|
54
56
|
import { RecordTasksCard } from "./record-tasks-card.js";
|
|
55
|
-
import {
|
|
57
|
+
import { useSiteRouteContext } from "./use-site-route-context.js";
|
|
56
58
|
import { useOrgMembers } from "./use-org-members.js";
|
|
57
59
|
const contactDocsHelp = pluginDocsHelp('contacts', {
|
|
58
60
|
anchor: '#the-contacts-page',
|
|
@@ -94,12 +96,12 @@ const contactDocsHelp = pluginDocsHelp('contacts', {
|
|
|
94
96
|
* The row is shared by every site that has captured this person, so the
|
|
95
97
|
* action in the overflow menu drops THIS group's facet, consent, attribution
|
|
96
98
|
* and scope tokens, and deletes the document only when nobody else is left
|
|
97
|
-
* holding it
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
99
|
+
* holding it. `crm/contact-remove` decides which against the stored
|
|
100
|
+
* document and keeps any marketing refusal the person gave (AGL-3338); the
|
|
101
|
+
* rules refuse a client the write. Not the erasure path: a privacy erasure
|
|
102
|
+
* removes the person regardless of who else holds them.
|
|
101
103
|
*/ export function ContactDetailPage(props) {
|
|
102
|
-
var _ref;
|
|
104
|
+
var _ref, _ref1;
|
|
103
105
|
const { hostId, org, id, basePath } = props;
|
|
104
106
|
const routes = crmRoutes(basePath);
|
|
105
107
|
const router = useRouter();
|
|
@@ -169,15 +171,16 @@ const contactDocsHelp = pluginDocsHelp('contacts', {
|
|
|
169
171
|
});
|
|
170
172
|
/*
|
|
171
173
|
* Where this person's ORDERS are read (AGL-2622): the site's orders list,
|
|
172
|
-
* narrowed to their address
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
* org-level mount has no
|
|
174
|
+
* narrowed to their address, at the address the plugin that keeps orders
|
|
175
|
+
* publishes. The count on the header is the number; the list is the rows.
|
|
176
|
+
* Built from the route params already in the URL, so no document is read
|
|
177
|
+
* to draw a link, and absent off a site — the org-level mount has no
|
|
178
|
+
* orders list to point at — and where no plugin keeps orders.
|
|
176
179
|
*/ const params = useParams();
|
|
177
|
-
const ordersHref = (params == null ? void 0 : params.orgSlug) && (params == null ? void 0 : params.host) && (record == null ? void 0 : record.email) ?
|
|
180
|
+
const ordersHref = (params == null ? void 0 : params.orgSlug) && (params == null ? void 0 : params.host) && (record == null ? void 0 : record.email) ? pluginRecordByEmailHref('order', {
|
|
178
181
|
orgSlug: String(params.orgSlug),
|
|
179
182
|
host: String(params.host)
|
|
180
|
-
}
|
|
183
|
+
}, record.email) : null;
|
|
181
184
|
/*
|
|
182
185
|
* Where this person's BOOKINGS are read (AGL-2660): the site's Bookings
|
|
183
186
|
* page narrowed to their address, under the same match rule the booking
|
|
@@ -187,22 +190,24 @@ const contactDocsHelp = pluginDocsHelp('contacts', {
|
|
|
187
190
|
* the subdomain the mount knows it by.
|
|
188
191
|
*/ const bookingDoor = useBookingDoor(siteHostId, org);
|
|
189
192
|
const siteSlug = (params == null ? void 0 : params.host) ? String(params.host) : siteHostId && mount ? mount.siteSubdomain(siteHostId) : null;
|
|
190
|
-
const bookingsHref = bookingDoor.open && (params == null ? void 0 : params.orgSlug) && siteSlug && (record == null ? void 0 : record.email) ?
|
|
193
|
+
const bookingsHref = bookingDoor.open && (params == null ? void 0 : params.orgSlug) && siteSlug && (record == null ? void 0 : record.email) ? pluginRecordByEmailHref('booking', {
|
|
191
194
|
orgSlug: String(params.orgSlug),
|
|
192
195
|
host: siteSlug
|
|
193
|
-
}
|
|
196
|
+
}, record.email) : null;
|
|
194
197
|
/*
|
|
195
198
|
* Where a campaign entry on the timeline links to: the email's own report
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
199
|
+
* (AGL-2616), at the address the plugin that keeps messages publishes as
|
|
200
|
+
* `emailMessage` (AGL-3080). Only a campaign THIS site sent can be
|
|
201
|
+
* addressed — a sibling site in the same consent group has pages of its
|
|
202
|
+
* own under a subdomain this page does not know — so the builder answers
|
|
203
|
+
* `null` for those, and where nothing publishes the kind, and the entry
|
|
204
|
+
* draws unlinked.
|
|
205
|
+
*/ const siteRoute = useSiteRouteContext();
|
|
206
|
+
const campaignHref = useCallback((email)=>siteRoute && email.hostId === hostId ? pluginRecordHref('emailMessage', siteRoute, email.campaignId) : null, [
|
|
207
|
+
siteRoute,
|
|
203
208
|
hostId
|
|
204
209
|
]);
|
|
205
|
-
const
|
|
210
|
+
const contactRemove = useContactRemove(hostId);
|
|
206
211
|
const handleRemove = useCallback(async ()=>{
|
|
207
212
|
var _ref;
|
|
208
213
|
if (!row || !scope) return;
|
|
@@ -216,26 +221,9 @@ const contactDocsHelp = pluginDocsHelp('contacts', {
|
|
|
216
221
|
}).then(()=>true).catch(()=>false);
|
|
217
222
|
if (!confirmed) return;
|
|
218
223
|
try {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
await deleteDoc(ref);
|
|
223
|
-
} else {
|
|
224
|
-
await updateDoc(ref, _extends({}, Object.fromEntries(plan.remove.map((path)=>[
|
|
225
|
-
path,
|
|
226
|
-
deleteField()
|
|
227
|
-
])), {
|
|
228
|
-
visibleTo: arrayRemove(...plan.removeTokens),
|
|
229
|
-
capturedByHostIds: arrayRemove(...plan.removeHostIds),
|
|
230
|
-
updatedAt: new Date()
|
|
231
|
-
}));
|
|
232
|
-
// The facet and the scope just let go of are what the Contacts list
|
|
233
|
-
// filters and searches by; the rules keep a client from restamping
|
|
234
|
-
// them, so the route does (AGL-3321).
|
|
235
|
-
await restampCrmListFields(user, crmTaskCallScope(hostId, orgId), 'contacts', [
|
|
236
|
-
id
|
|
237
|
-
]);
|
|
238
|
-
}
|
|
224
|
+
// The route decides against the stored document, keeps any refusal,
|
|
225
|
+
// and restamps what the Contacts list filters by (AGL-3338).
|
|
226
|
+
const removed = await contactRemove.removeOne(id);
|
|
239
227
|
// `siteLabel` and not "this site": at the organization level the line
|
|
240
228
|
// lands in a feed that spans every site, so it has to name the one.
|
|
241
229
|
logActivity(`Removed contact from ${siteLabel}`, {
|
|
@@ -243,7 +231,7 @@ const contactDocsHelp = pluginDocsHelp('contacts', {
|
|
|
243
231
|
id,
|
|
244
232
|
name: (record == null ? void 0 : record.name) || (record == null ? void 0 : record.email)
|
|
245
233
|
});
|
|
246
|
-
enqueueSnackbar(
|
|
234
|
+
enqueueSnackbar(removed === 'deleted' ? 'Contact deleted' : `Contact removed from ${siteLabel}`, {
|
|
247
235
|
variant: 'success',
|
|
248
236
|
persist: false
|
|
249
237
|
});
|
|
@@ -257,9 +245,8 @@ const contactDocsHelp = pluginDocsHelp('contacts', {
|
|
|
257
245
|
}
|
|
258
246
|
}, [
|
|
259
247
|
confirm,
|
|
260
|
-
|
|
248
|
+
contactRemove,
|
|
261
249
|
enqueueSnackbar,
|
|
262
|
-
firestore,
|
|
263
250
|
id,
|
|
264
251
|
logActivity,
|
|
265
252
|
record,
|
|
@@ -267,10 +254,7 @@ const contactDocsHelp = pluginDocsHelp('contacts', {
|
|
|
267
254
|
routes,
|
|
268
255
|
row,
|
|
269
256
|
scope,
|
|
270
|
-
siteLabel
|
|
271
|
-
user,
|
|
272
|
-
hostId,
|
|
273
|
-
orgId
|
|
257
|
+
siteLabel
|
|
274
258
|
]);
|
|
275
259
|
const overflowItems = [
|
|
276
260
|
/*
|
|
@@ -394,6 +378,10 @@ const contactDocsHelp = pluginDocsHelp('contacts', {
|
|
|
394
378
|
/*#__PURE__*/ _jsx(CrmEmailStateChip, {
|
|
395
379
|
state: record.emailState
|
|
396
380
|
}),
|
|
381
|
+
/*#__PURE__*/ _jsx(CrmShareChipView, {
|
|
382
|
+
chip: crmShareChipFor(row, (_ref1 = viewingGroup == null ? void 0 : viewingGroup.hostIds) != null ? _ref1 : []),
|
|
383
|
+
org: org
|
|
384
|
+
}),
|
|
397
385
|
/*#__PURE__*/ _jsx(CrmEmailGatewayChip, {
|
|
398
386
|
hostId: siteHostId,
|
|
399
387
|
email: record.email,
|
|
@@ -450,6 +438,15 @@ const contactDocsHelp = pluginDocsHelp('contacts', {
|
|
|
450
438
|
contactId: id,
|
|
451
439
|
org: org
|
|
452
440
|
}) : null,
|
|
441
|
+
/*#__PURE__*/ _jsx(RecordSharingCard, {
|
|
442
|
+
object: "contacts",
|
|
443
|
+
id: id,
|
|
444
|
+
record: row,
|
|
445
|
+
hostId: hostId != null ? hostId : null,
|
|
446
|
+
orgId: orgId,
|
|
447
|
+
org: org,
|
|
448
|
+
viewingHostIds: viewingGroup == null ? void 0 : viewingGroup.hostIds
|
|
449
|
+
}),
|
|
453
450
|
/*#__PURE__*/ _jsx(ContactPropertiesCard, {
|
|
454
451
|
hostId: hostId,
|
|
455
452
|
org: org,
|