@aglyn/plugins-crm 1.0.0-beta.221 → 1.0.0-beta.222
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 -1
- package/package.json +13 -13
- package/src/lib/components/activity-list.js +12 -5
- package/src/lib/components/activity-list.js.map +1 -1
- package/src/lib/components/companies-bulk-bar.d.ts +1 -4
- package/src/lib/components/companies-bulk-bar.js +7 -20
- package/src/lib/components/companies-bulk-bar.js.map +1 -1
- package/src/lib/components/companies-section.d.ts +3 -3
- package/src/lib/components/companies-section.js +149 -46
- package/src/lib/components/companies-section.js.map +1 -1
- package/src/lib/components/company-edit-drawer.js +281 -20
- package/src/lib/components/company-edit-drawer.js.map +1 -1
- package/src/lib/components/company-properties-card.js +99 -8
- package/src/lib/components/company-properties-card.js.map +1 -1
- package/src/lib/components/contact-detail-page.js +3 -1
- package/src/lib/components/contact-detail-page.js.map +1 -1
- package/src/lib/components/contact-merge-dialog.js +33 -10
- package/src/lib/components/contact-merge-dialog.js.map +1 -1
- package/src/lib/components/contact-properties-card.d.ts +0 -69
- package/src/lib/components/contact-properties-card.js +350 -60
- package/src/lib/components/contact-properties-card.js.map +1 -1
- package/src/lib/components/contact-reports-to-field.d.ts +46 -0
- package/src/lib/components/contact-reports-to-field.js +67 -0
- package/src/lib/components/contact-reports-to-field.js.map +1 -0
- package/src/lib/components/contacts-bulk-bar.d.ts +1 -8
- package/src/lib/components/contacts-bulk-bar.js +7 -19
- package/src/lib/components/contacts-bulk-bar.js.map +1 -1
- package/src/lib/components/contacts-section.js +46 -33
- package/src/lib/components/contacts-section.js.map +1 -1
- package/src/lib/components/crm-call-actions.d.ts +10 -0
- package/src/lib/components/crm-call-actions.js +26 -9
- package/src/lib/components/crm-call-actions.js.map +1 -1
- package/src/lib/components/crm-record-picker.d.ts +15 -0
- package/src/lib/components/crm-record-picker.js +16 -10
- package/src/lib/components/crm-record-picker.js.map +1 -1
- package/src/lib/components/crm-transfer-buttons.d.ts +54 -0
- package/src/lib/components/crm-transfer-buttons.js +167 -0
- package/src/lib/components/crm-transfer-buttons.js.map +1 -0
- package/src/lib/components/deal-contact-roles-card.d.ts +46 -0
- package/src/lib/components/deal-contact-roles-card.js +368 -0
- package/src/lib/components/deal-contact-roles-card.js.map +1 -0
- package/src/lib/components/deal-detail-page.d.ts +3 -2
- package/src/lib/components/deal-detail-page.js +25 -6
- package/src/lib/components/deal-detail-page.js.map +1 -1
- package/src/lib/components/deal-edit-drawer.d.ts +10 -0
- package/src/lib/components/deal-edit-drawer.js +219 -10
- package/src/lib/components/deal-edit-drawer.js.map +1 -1
- package/src/lib/components/deal-properties-card.d.ts +7 -0
- package/src/lib/components/deal-properties-card.js +34 -3
- package/src/lib/components/deal-properties-card.js.map +1 -1
- package/src/lib/components/deal-stage-card.d.ts +4 -0
- package/src/lib/components/deal-stage-card.js +10 -2
- package/src/lib/components/deal-stage-card.js.map +1 -1
- package/src/lib/components/deals-bulk-bar.d.ts +0 -3
- package/src/lib/components/deals-bulk-bar.js +7 -20
- package/src/lib/components/deals-bulk-bar.js.map +1 -1
- package/src/lib/components/deals-section.d.ts +3 -3
- package/src/lib/components/deals-section.js +123 -63
- package/src/lib/components/deals-section.js.map +1 -1
- package/src/lib/components/fields-section.js +31 -17
- package/src/lib/components/fields-section.js.map +1 -1
- package/src/lib/components/lead-convert-dialog.d.ts +4 -0
- package/src/lib/components/lead-convert-dialog.js +23 -1
- package/src/lib/components/lead-convert-dialog.js.map +1 -1
- package/src/lib/components/lead-detail-page.js +8 -2
- package/src/lib/components/lead-detail-page.js.map +1 -1
- package/src/lib/components/lead-profile-fields.d.ts +71 -0
- package/src/lib/components/lead-profile-fields.js +386 -0
- package/src/lib/components/lead-profile-fields.js.map +1 -0
- package/src/lib/components/lead-properties-card.d.ts +2 -1
- package/src/lib/components/lead-properties-card.js +85 -176
- package/src/lib/components/lead-properties-card.js.map +1 -1
- package/src/lib/components/lead-source-select.d.ts +4 -23
- package/src/lib/components/lead-source-select.js +9 -73
- package/src/lib/components/lead-source-select.js.map +1 -1
- package/src/lib/components/lead-status-chip.d.ts +9 -3
- package/src/lib/components/lead-status-chip.js +8 -4
- package/src/lib/components/lead-status-chip.js.map +1 -1
- package/src/lib/components/lead-status-options.d.ts +20 -0
- package/src/lib/components/lead-status-options.js +51 -0
- package/src/lib/components/lead-status-options.js.map +1 -0
- package/src/lib/components/lead-unqualify-dialog.d.ts +5 -0
- package/src/lib/components/lead-unqualify-dialog.js +50 -18
- package/src/lib/components/lead-unqualify-dialog.js.map +1 -1
- package/src/lib/components/leads-bulk-bar.d.ts +0 -3
- package/src/lib/components/leads-bulk-bar.js +29 -29
- package/src/lib/components/leads-bulk-bar.js.map +1 -1
- package/src/lib/components/leads-section.js +120 -68
- package/src/lib/components/leads-section.js.map +1 -1
- package/src/lib/components/linked-deals-card.d.ts +4 -0
- package/src/lib/components/linked-deals-card.js +47 -3
- package/src/lib/components/linked-deals-card.js.map +1 -1
- package/src/lib/components/log-activity-dialog.js +43 -3
- package/src/lib/components/log-activity-dialog.js.map +1 -1
- package/src/lib/components/new-contact-drawer.d.ts +8 -0
- package/src/lib/components/new-contact-drawer.js +108 -6
- package/src/lib/components/new-contact-drawer.js.map +1 -1
- package/src/lib/components/new-lead-drawer.d.ts +9 -2
- package/src/lib/components/new-lead-drawer.js +88 -121
- package/src/lib/components/new-lead-drawer.js.map +1 -1
- package/src/lib/components/picklist-select.d.ts +36 -0
- package/src/lib/components/picklist-select.js +114 -0
- package/src/lib/components/picklist-select.js.map +1 -0
- package/src/lib/components/picklist-values-card.d.ts +34 -0
- package/src/lib/components/{lead-source-values-card.js → picklist-values-card.js} +241 -58
- package/src/lib/components/picklist-values-card.js.map +1 -0
- package/src/lib/components/pipeline-stages-dialog.d.ts +7 -0
- package/src/lib/components/pipeline-stages-dialog.js +26 -4
- package/src/lib/components/pipeline-stages-dialog.js.map +1 -1
- package/src/lib/components/pipelines-dialog.js +15 -6
- package/src/lib/components/pipelines-dialog.js.map +1 -1
- package/src/lib/components/recent-activity-feed.js +20 -5
- package/src/lib/components/recent-activity-feed.js.map +1 -1
- package/src/lib/components/recipes-card.js +4 -2
- package/src/lib/components/recipes-card.js.map +1 -1
- package/src/lib/components/record-tasks-card.js +15 -8
- package/src/lib/components/record-tasks-card.js.map +1 -1
- package/src/lib/components/reports/forecast-card.d.ts +6 -0
- package/src/lib/components/reports/forecast-card.js +101 -3
- package/src/lib/components/reports/forecast-card.js.map +1 -1
- package/src/lib/components/reports/lead-funnel-card.d.ts +12 -0
- package/src/lib/components/reports/lead-funnel-card.js +9 -5
- package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
- package/src/lib/components/reports/lead-source-card.d.ts +23 -0
- package/src/lib/components/reports/lead-source-card.js +318 -0
- package/src/lib/components/reports/lead-source-card.js.map +1 -0
- package/src/lib/components/reports/report-export.js +1 -1
- package/src/lib/components/reports/report-export.js.map +1 -1
- package/src/lib/components/reports-section.js +8 -0
- package/src/lib/components/reports-section.js.map +1 -1
- package/src/lib/components/sharing-rule-drawer.d.ts +5 -0
- package/src/lib/components/sharing-rule-drawer.js +108 -19
- package/src/lib/components/sharing-rule-drawer.js.map +1 -1
- package/src/lib/components/sharing-rules-card.js +11 -3
- package/src/lib/components/sharing-rules-card.js.map +1 -1
- package/src/lib/components/task-cells.d.ts +9 -1
- package/src/lib/components/task-cells.js +9 -4
- package/src/lib/components/task-cells.js.map +1 -1
- package/src/lib/components/task-edit-drawer.js +111 -43
- package/src/lib/components/task-edit-drawer.js.map +1 -1
- package/src/lib/components/tasks-bulk-bar.d.ts +0 -3
- package/src/lib/components/tasks-bulk-bar.js +7 -20
- package/src/lib/components/tasks-bulk-bar.js.map +1 -1
- package/src/lib/components/tasks-calendar.d.ts +3 -0
- package/src/lib/components/tasks-calendar.js +8 -1
- package/src/lib/components/tasks-calendar.js.map +1 -1
- package/src/lib/components/tasks-section.d.ts +3 -3
- package/src/lib/components/tasks-section.js +58 -43
- package/src/lib/components/tasks-section.js.map +1 -1
- package/src/lib/constants/api-routes.d.ts +2 -9
- package/src/lib/constants/api-routes.js +1 -7
- package/src/lib/constants/api-routes.js.map +1 -1
- package/src/lib/constants/bundle-common.d.ts +6 -0
- package/src/lib/constants/bundle-common.js +11 -0
- package/src/lib/constants/bundle-common.js.map +1 -1
- package/src/lib/constants/company-filters.d.ts +14 -0
- package/src/lib/constants/company-filters.js +51 -1
- package/src/lib/constants/company-filters.js.map +1 -1
- package/src/lib/constants/contact-filters.d.ts +2 -0
- package/src/lib/constants/contact-filters.js +18 -1
- package/src/lib/constants/contact-filters.js.map +1 -1
- package/src/lib/constants/deal-filters.d.ts +22 -1
- package/src/lib/constants/deal-filters.js +67 -4
- package/src/lib/constants/deal-filters.js.map +1 -1
- package/src/lib/declarations.console-server.d.ts +3 -0
- package/src/lib/declarations.console-server.js +35 -0
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/declarations.d.ts +26 -0
- package/src/lib/declarations.js +35 -0
- package/src/lib/declarations.js.map +1 -0
- package/src/lib/declarations.server.d.ts +11 -0
- package/src/lib/declarations.server.js +92 -1
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/hooks/use-company-picklists.d.ts +15 -0
- package/src/lib/hooks/use-company-picklists.js +55 -0
- package/src/lib/hooks/use-company-picklists.js.map +1 -0
- package/src/lib/hooks/use-crm-picklist.d.ts +25 -0
- package/src/lib/hooks/use-crm-picklist.js +53 -0
- package/src/lib/hooks/use-crm-picklist.js.map +1 -0
- package/src/lib/hooks/use-crm-record-names.d.ts +16 -0
- package/src/lib/hooks/use-crm-record-names.js +45 -19
- package/src/lib/hooks/use-crm-record-names.js.map +1 -1
- package/src/lib/hooks/use-crm-task-picklists.d.ts +11 -0
- package/src/lib/hooks/use-crm-task-picklists.js +45 -0
- package/src/lib/hooks/use-crm-task-picklists.js.map +1 -0
- package/src/lib/hooks/use-deals.d.ts +14 -0
- package/src/lib/hooks/use-deals.js +29 -0
- package/src/lib/hooks/use-deals.js.map +1 -1
- package/src/lib/hooks/use-lead-picklists.d.ts +14 -0
- package/src/lib/hooks/use-lead-picklists.js +46 -0
- package/src/lib/hooks/use-lead-picklists.js.map +1 -0
- package/src/lib/hooks/use-lead-source-picklist.d.ts +5 -19
- package/src/lib/hooks/use-lead-source-picklist.js +12 -22
- package/src/lib/hooks/use-lead-source-picklist.js.map +1 -1
- package/src/lib/hooks/use-lead-status-picklist.d.ts +9 -0
- package/src/lib/hooks/use-lead-status-picklist.js +40 -0
- package/src/lib/hooks/use-lead-status-picklist.js.map +1 -0
- package/src/lib/model/companies.d.ts +32 -3
- package/src/lib/model/companies.js +133 -16
- package/src/lib/model/companies.js.map +1 -1
- package/src/lib/model/contact-merge.d.ts +27 -3
- package/src/lib/model/contact-merge.js +81 -7
- package/src/lib/model/contact-merge.js.map +1 -1
- package/src/lib/model/contact-record.d.ts +17 -0
- package/src/lib/model/contact-record.js +15 -1
- package/src/lib/model/contact-record.js.map +1 -1
- package/src/lib/model/contact-update.d.ts +31 -2
- package/src/lib/model/contact-update.js +14 -0
- package/src/lib/model/contact-update.js.map +1 -1
- package/src/lib/model/crm-list-query.d.ts +5 -0
- package/src/lib/model/crm-list-query.js +8 -0
- package/src/lib/model/crm-list-query.js.map +1 -1
- package/src/lib/model/crm-recipes.d.ts +40 -0
- package/src/lib/model/crm-recipes.js +244 -0
- package/src/lib/model/crm-recipes.js.map +1 -0
- package/src/lib/model/crm-record-counts.d.ts +36 -0
- package/src/lib/model/crm-record-counts.js +54 -0
- package/src/lib/model/crm-record-counts.js.map +1 -0
- package/src/lib/model/crm-record-lists.d.ts +23 -0
- package/src/lib/model/crm-record-lists.js +161 -0
- package/src/lib/model/crm-record-lists.js.map +1 -0
- package/src/lib/model/crm-reports.d.ts +73 -3
- package/src/lib/model/crm-reports.js +120 -1
- package/src/lib/model/crm-reports.js.map +1 -1
- package/src/lib/model/crm-sharing.d.ts +21 -3
- package/src/lib/model/crm-sharing.js +27 -6
- package/src/lib/model/crm-sharing.js.map +1 -1
- package/src/lib/model/deal-board-model.d.ts +8 -2
- package/src/lib/model/deal-board-model.js +15 -2
- package/src/lib/model/deal-board-model.js.map +1 -1
- package/src/lib/model/deal-contact-roles.d.ts +45 -0
- package/src/lib/model/deal-contact-roles.js +73 -0
- package/src/lib/model/deal-contact-roles.js.map +1 -0
- package/src/lib/model/deal-form-model.d.ts +56 -0
- package/src/lib/model/deal-form-model.js +79 -7
- package/src/lib/model/deal-form-model.js.map +1 -1
- package/src/lib/model/download-text-file.d.ts +23 -0
- package/src/lib/model/download-text-file.js +34 -0
- package/src/lib/model/download-text-file.js.map +1 -0
- package/src/lib/model/lead-filters.d.ts +36 -2
- package/src/lib/model/lead-filters.js +123 -2
- package/src/lib/model/lead-filters.js.map +1 -1
- package/src/lib/model/lead-source-direction.d.ts +38 -0
- package/src/lib/model/lead-source-direction.js +52 -0
- package/src/lib/model/lead-source-direction.js.map +1 -0
- package/src/lib/model/picklist-values.d.ts +119 -0
- package/src/lib/model/picklist-values.js +256 -0
- package/src/lib/model/picklist-values.js.map +1 -0
- package/src/lib/model/record-facts-disclosed.d.ts +81 -0
- package/src/lib/model/record-facts-disclosed.js +281 -0
- package/src/lib/model/record-facts-disclosed.js.map +1 -0
- package/src/lib/model/record-facts.d.ts +215 -34
- package/src/lib/model/record-facts.js +307 -128
- package/src/lib/model/record-facts.js.map +1 -1
- package/src/lib/model/task-picklists.d.ts +52 -0
- package/src/lib/model/task-picklists.js +83 -0
- package/src/lib/model/task-picklists.js.map +1 -0
- package/src/lib/model/task-routes.d.ts +13 -0
- package/src/lib/model/task-routes.js +23 -4
- package/src/lib/model/task-routes.js.map +1 -1
- package/src/lib/plan-entitlements.js +6 -4
- package/src/lib/plan-entitlements.js.map +1 -1
- package/src/lib/plugin.js +17 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/api-v1/contacts.js +220 -14
- package/src/lib/server/api-v1/contacts.js.map +1 -1
- package/src/lib/server/api-v1/crm-activities.js +14 -1
- package/src/lib/server/api-v1/crm-activities.js.map +1 -1
- package/src/lib/server/api-v1/crm-companies.js +123 -14
- package/src/lib/server/api-v1/crm-companies.js.map +1 -1
- package/src/lib/server/api-v1/crm-deals.js +291 -26
- package/src/lib/server/api-v1/crm-deals.js.map +1 -1
- package/src/lib/server/api-v1/crm-leads.js +144 -38
- package/src/lib/server/api-v1/crm-leads.js.map +1 -1
- package/src/lib/server/api-v1/crm-pipelines.d.ts +6 -0
- package/src/lib/server/api-v1/crm-pipelines.js +8 -2
- package/src/lib/server/api-v1/crm-pipelines.js.map +1 -1
- package/src/lib/server/api-v1/crm-tasks.js +75 -33
- package/src/lib/server/api-v1/crm-tasks.js.map +1 -1
- package/src/lib/server/api-v1/openapi.d.ts +0 -11
- package/src/lib/server/api-v1/openapi.js +171 -16
- package/src/lib/server/api-v1/openapi.js.map +1 -1
- package/src/lib/server/assign-contact-owner.d.ts +162 -0
- package/src/lib/server/assign-contact-owner.js +340 -0
- package/src/lib/server/assign-contact-owner.js.map +1 -0
- package/src/lib/server/associate-company-by-domain.d.ts +70 -0
- package/src/lib/server/associate-company-by-domain.js +153 -0
- package/src/lib/server/associate-company-by-domain.js.map +1 -0
- package/src/lib/server/automation-steps.d.ts +139 -0
- package/src/lib/server/automation-steps.js +472 -0
- package/src/lib/server/automation-steps.js.map +1 -0
- package/src/lib/server/capture-contact.js +65 -21
- package/src/lib/server/capture-contact.js.map +1 -1
- package/src/lib/server/capture-host-contact.d.ts +104 -0
- package/src/lib/server/capture-host-contact.js +151 -0
- package/src/lib/server/capture-host-contact.js.map +1 -0
- package/src/lib/server/company-children.d.ts +25 -0
- package/src/lib/server/company-children.js +42 -0
- package/src/lib/server/company-children.js.map +1 -0
- package/src/lib/server/company-delete.d.ts +18 -0
- package/src/lib/server/company-delete.js +44 -21
- package/src/lib/server/company-delete.js.map +1 -1
- package/src/lib/server/contact-email-history.js +21 -19
- package/src/lib/server/contact-email-history.js.map +1 -1
- package/src/lib/server/contact-remove.js +27 -2
- package/src/lib/server/contact-remove.js.map +1 -1
- package/src/lib/server/contact-reports-to.d.ts +61 -0
- package/src/lib/server/contact-reports-to.js +114 -0
- package/src/lib/server/contact-reports-to.js.map +1 -0
- package/src/lib/server/contact-update.d.ts +42 -1
- package/src/lib/server/contact-update.js +204 -23
- package/src/lib/server/contact-update.js.map +1 -1
- package/src/lib/server/convert-host-lead.d.ts +46 -1
- package/src/lib/server/convert-host-lead.js +173 -15
- package/src/lib/server/convert-host-lead.js.map +1 -1
- package/src/lib/server/convert-open-lead.js +12 -5
- package/src/lib/server/convert-open-lead.js.map +1 -1
- package/src/lib/server/crm-record-indexes.d.ts +45 -0
- package/src/lib/server/crm-record-indexes.js +78 -0
- package/src/lib/server/crm-record-indexes.js.map +1 -0
- package/src/lib/server/crm-sharing.d.ts +9 -1
- package/src/lib/server/crm-sharing.js +23 -3
- package/src/lib/server/crm-sharing.js.map +1 -1
- package/src/lib/server/deal-contact-roles.d.ts +27 -0
- package/src/lib/server/deal-contact-roles.js +89 -0
- package/src/lib/server/deal-contact-roles.js.map +1 -0
- package/src/lib/server/lead-convert.d.ts +5 -0
- package/src/lib/server/lead-convert.js +19 -3
- package/src/lib/server/lead-convert.js.map +1 -1
- package/src/lib/server/lead-create.d.ts +17 -2
- package/src/lib/server/lead-create.js +46 -14
- package/src/lib/server/lead-create.js.map +1 -1
- package/src/lib/server/lead-nurturing.d.ts +6 -0
- package/src/lib/server/lead-nurturing.js +57 -20
- package/src/lib/server/lead-nurturing.js.map +1 -1
- package/src/lib/server/lead-picklists.d.ts +35 -0
- package/src/lib/server/lead-picklists.js +52 -0
- package/src/lib/server/lead-picklists.js.map +1 -0
- package/src/lib/server/lead-source-picklist.d.ts +1 -13
- package/src/lib/server/lead-source-picklist.js +6 -33
- package/src/lib/server/lead-source-picklist.js.map +1 -1
- package/src/lib/server/merge-contacts.js +22 -4
- package/src/lib/server/merge-contacts.js.map +1 -1
- package/src/lib/server/person-eraser.d.ts +53 -0
- package/src/lib/server/person-eraser.js +149 -0
- package/src/lib/server/person-eraser.js.map +1 -0
- package/src/lib/server/person-records.d.ts +69 -0
- package/src/lib/server/person-records.js +317 -0
- package/src/lib/server/person-records.js.map +1 -0
- package/src/lib/server/person-refund.d.ts +97 -0
- package/src/lib/server/person-refund.js +221 -0
- package/src/lib/server/person-refund.js.map +1 -0
- package/src/lib/server/picklist-values.d.ts +21 -0
- package/src/lib/server/picklist-values.js +308 -0
- package/src/lib/server/picklist-values.js.map +1 -0
- package/src/lib/server/read-picklist.d.ts +44 -0
- package/src/lib/server/read-picklist.js +65 -0
- package/src/lib/server/read-picklist.js.map +1 -0
- package/src/lib/server/recipe-routes.js +15 -7
- package/src/lib/server/recipe-routes.js.map +1 -1
- package/src/lib/server/record-email-state.js +7 -1
- package/src/lib/server/record-email-state.js.map +1 -1
- package/src/lib/server/record-facts.d.ts +3 -0
- package/src/lib/server/record-facts.js +233 -31
- package/src/lib/server/record-facts.js.map +1 -1
- package/src/lib/server/record-origin.d.ts +27 -0
- package/src/lib/server/record-origin.js +104 -0
- package/src/lib/server/record-origin.js.map +1 -0
- package/src/lib/server/record-timeline.js +15 -2
- package/src/lib/server/record-timeline.js.map +1 -1
- package/src/lib/server/suite-gate.js +4 -3
- package/src/lib/server/suite-gate.js.map +1 -1
- package/src/lib/server/task-routes.js +30 -16
- package/src/lib/server/task-routes.js.map +1 -1
- package/src/lib/server-deal-stage.d.ts +5 -1
- package/src/lib/server-deal-stage.js +10 -5
- package/src/lib/server-deal-stage.js.map +1 -1
- package/src/lib/server.d.ts +8 -3
- package/src/lib/server.js +47 -43
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/memory-firestore.d.ts +55 -0
- package/src/lib/testing/memory-firestore.js +335 -0
- package/src/lib/testing/memory-firestore.js.map +1 -0
- package/src/lib/testing/stand-in-email-send-index.d.ts +25 -0
- package/src/lib/testing/stand-in-email-send-index.js +49 -0
- package/src/lib/testing/stand-in-email-send-index.js.map +1 -0
- package/src/lib/transfer/activities.d.ts +30 -0
- package/src/lib/transfer/activities.js +132 -0
- package/src/lib/transfer/activities.js.map +1 -0
- package/src/lib/transfer/aliases.d.ts +22 -0
- package/src/lib/transfer/aliases.js +1342 -0
- package/src/lib/transfer/aliases.js.map +1 -0
- package/src/lib/transfer/common.d.ts +222 -0
- package/src/lib/transfer/common.js +832 -0
- package/src/lib/transfer/common.js.map +1 -0
- package/src/lib/transfer/companies.d.ts +43 -0
- package/src/lib/transfer/companies.js +549 -0
- package/src/lib/transfer/companies.js.map +1 -0
- package/src/lib/transfer/contacts.d.ts +31 -0
- package/src/lib/transfer/contacts.js +727 -0
- package/src/lib/transfer/contacts.js.map +1 -0
- package/src/lib/transfer/deals.d.ts +66 -0
- package/src/lib/transfer/deals.js +698 -0
- package/src/lib/transfer/deals.js.map +1 -0
- package/src/lib/transfer/email-templates-package.d.ts +40 -0
- package/src/lib/transfer/email-templates-package.js +76 -0
- package/src/lib/transfer/email-templates-package.js.map +1 -0
- package/src/lib/transfer/email-templates-package.server.d.ts +32 -0
- package/src/lib/transfer/email-templates-package.server.js +176 -0
- package/src/lib/transfer/email-templates-package.server.js.map +1 -0
- package/src/lib/transfer/fields.d.ts +102 -0
- package/src/lib/transfer/fields.js +1755 -0
- package/src/lib/transfer/fields.js.map +1 -0
- package/src/lib/transfer/leads.d.ts +30 -0
- package/src/lib/transfer/leads.js +582 -0
- package/src/lib/transfer/leads.js.map +1 -0
- package/src/lib/transfer/pipelines.d.ts +21 -0
- package/src/lib/transfer/pipelines.js +172 -0
- package/src/lib/transfer/pipelines.js.map +1 -0
- package/src/lib/transfer/register.d.ts +47 -0
- package/src/lib/transfer/register.js +277 -0
- package/src/lib/transfer/register.js.map +1 -0
- package/src/lib/transfer/tasks.d.ts +30 -0
- package/src/lib/transfer/tasks.js +418 -0
- package/src/lib/transfer/tasks.js.map +1 -0
- package/src/lib/transfer/ui.d.ts +20 -0
- package/src/lib/transfer/ui.js +42 -0
- package/src/lib/transfer/ui.js.map +1 -0
- package/src/lib/components/company-import-drawer.d.ts +0 -42
- package/src/lib/components/company-import-drawer.js +0 -99
- package/src/lib/components/company-import-drawer.js.map +0 -1
- package/src/lib/components/contact-import-drawer.d.ts +0 -46
- package/src/lib/components/contact-import-drawer.js +0 -149
- package/src/lib/components/contact-import-drawer.js.map +0 -1
- package/src/lib/components/crm-export-all-button.d.ts +0 -39
- package/src/lib/components/crm-export-all-button.js +0 -98
- package/src/lib/components/crm-export-all-button.js.map +0 -1
- package/src/lib/components/csv-import-drawer.d.ts +0 -139
- package/src/lib/components/csv-import-drawer.js +0 -757
- package/src/lib/components/csv-import-drawer.js.map +0 -1
- package/src/lib/components/deal-import-drawer.d.ts +0 -24
- package/src/lib/components/deal-import-drawer.js +0 -83
- package/src/lib/components/deal-import-drawer.js.map +0 -1
- package/src/lib/components/lead-import-drawer.d.ts +0 -27
- package/src/lib/components/lead-import-drawer.js +0 -118
- package/src/lib/components/lead-import-drawer.js.map +0 -1
- package/src/lib/components/lead-source-values-card.d.ts +0 -27
- package/src/lib/components/lead-source-values-card.js.map +0 -1
- package/src/lib/components/task-import-drawer.d.ts +0 -24
- package/src/lib/components/task-import-drawer.js +0 -82
- package/src/lib/components/task-import-drawer.js.map +0 -1
- package/src/lib/model/companies-csv.d.ts +0 -36
- package/src/lib/model/companies-csv.js +0 -38
- package/src/lib/model/companies-csv.js.map +0 -1
- package/src/lib/model/contacts-csv.d.ts +0 -54
- package/src/lib/model/contacts-csv.js +0 -67
- package/src/lib/model/contacts-csv.js.map +0 -1
- package/src/lib/model/crm-company-import.d.ts +0 -154
- package/src/lib/model/crm-company-import.js +0 -331
- package/src/lib/model/crm-company-import.js.map +0 -1
- package/src/lib/model/crm-csv.d.ts +0 -239
- package/src/lib/model/crm-csv.js +0 -467
- package/src/lib/model/crm-csv.js.map +0 -1
- package/src/lib/model/crm-deal-import.d.ts +0 -112
- package/src/lib/model/crm-deal-import.js +0 -263
- package/src/lib/model/crm-deal-import.js.map +0 -1
- package/src/lib/model/crm-import.d.ts +0 -250
- package/src/lib/model/crm-import.js +0 -467
- package/src/lib/model/crm-import.js.map +0 -1
- package/src/lib/model/crm-lead-import.d.ts +0 -138
- package/src/lib/model/crm-lead-import.js +0 -446
- package/src/lib/model/crm-lead-import.js.map +0 -1
- package/src/lib/model/crm-task-import.d.ts +0 -94
- package/src/lib/model/crm-task-import.js +0 -275
- package/src/lib/model/crm-task-import.js.map +0 -1
- package/src/lib/model/deals-csv.d.ts +0 -32
- package/src/lib/model/deals-csv.js +0 -36
- package/src/lib/model/deals-csv.js.map +0 -1
- package/src/lib/model/import-column-shapes.d.ts +0 -36
- package/src/lib/model/import-column-shapes.js +0 -76
- package/src/lib/model/import-column-shapes.js.map +0 -1
- package/src/lib/model/lead-source-values.d.ts +0 -82
- package/src/lib/model/lead-source-values.js +0 -149
- package/src/lib/model/lead-source-values.js.map +0 -1
- package/src/lib/model/leads-csv.d.ts +0 -40
- package/src/lib/model/leads-csv.js +0 -40
- package/src/lib/model/leads-csv.js.map +0 -1
- package/src/lib/model/tasks-csv.d.ts +0 -28
- package/src/lib/model/tasks-csv.js +0 -28
- package/src/lib/model/tasks-csv.js.map +0 -1
- package/src/lib/server/companies-import.d.ts +0 -57
- package/src/lib/server/companies-import.js +0 -242
- package/src/lib/server/companies-import.js.map +0 -1
- package/src/lib/server/contacts-import.d.ts +0 -109
- package/src/lib/server/contacts-import.js +0 -393
- package/src/lib/server/contacts-import.js.map +0 -1
- package/src/lib/server/deals-import.d.ts +0 -76
- package/src/lib/server/deals-import.js +0 -248
- package/src/lib/server/deals-import.js.map +0 -1
- package/src/lib/server/export-route.d.ts +0 -60
- package/src/lib/server/export-route.js +0 -447
- package/src/lib/server/export-route.js.map +0 -1
- package/src/lib/server/import-context.d.ts +0 -87
- package/src/lib/server/import-context.js +0 -182
- package/src/lib/server/import-context.js.map +0 -1
- package/src/lib/server/lead-source-values.d.ts +0 -50
- package/src/lib/server/lead-source-values.js +0 -191
- package/src/lib/server/lead-source-values.js.map +0 -1
- package/src/lib/server/leads-import.d.ts +0 -84
- package/src/lib/server/leads-import.js +0 -367
- package/src/lib/server/leads-import.js.map +0 -1
- package/src/lib/server/tasks-import.d.ts +0 -54
- package/src/lib/server/tasks-import.js +0 -160
- package/src/lib/server/tasks-import.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/contact-merge.ts"],"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\n/**\n * MERGING TWO CONTACT DOCUMENTS INTO ONE (AGL-2625) — the pure half.\n *\n * Contacts are keyed by email, so exact duplicates cannot exist; one human\n * with `jane@acme.com` and `jane@gmail.com` is two documents, with tags,\n * deals, tasks and a timeline split between them. A merge folds the second\n * into the first: the SURVIVOR keeps its address as the identity, the MERGED\n * record's address becomes an alternate on it, and every field is combined\n * by one rule the dialog can show before the write happens.\n *\n * ## The rule\n *\n * - A scalar — a name, a phone, a title, a stage, an owner — is the\n * survivor's where the survivor has one, and fills from the merged record\n * where the survivor is empty. Nothing the survivor holds is overwritten.\n * - A list — tags, campaign filings, the timeline, the sites that captured\n * the person, the forms, the companies — is the UNION, deduplicated and\n * bounded where the write paths bound it.\n * - A figure that counts something — orders, lifetime value, refunds — is\n * the SUM: two records of one person's purchases are one person's\n * purchases. A \"first\" is the earlier, a \"last\" is the later.\n * - Notes are appended, survivor first: a note is a list of sentences, and\n * dropping the merged record's would be the one silent loss in a merge\n * that is otherwise lossless.\n * - A recorded REFUSAL of marketing mail on either record stands on the\n * survivor. The merge asserts the two are one person, and that person\n * said no.\n *\n * Applied per holder: each group's facet on the merged record is folded\n * into the same group's facet on the survivor, and a group only the merged\n * record had comes across whole. No holder's records reach another holder's\n * facet, which is the guarantee the facet shape exists for.\n *\n * ## Why this is a plan and not a write\n *\n * The console shows the two records side by side and says which value wins\n * before anything is committed; the server performs the write. Both read\n * this function, so the preview cannot show one outcome and the write land\n * another. The plan is plain values — no sentinels — so it can be rendered,\n * and it is shaped for a merge-`set`, which deep-merges the facet map one\n * holder at a time and replaces an array whole: exactly the two behaviors\n * the rule above needs.\n */\n\nimport {\n CONTACT_ALTERNATE_EMAILS_CAP,\n CONTACT_ALTERNATE_EMAILS_FIELD,\n CONTACT_FACETS_FIELD,\n CONTACT_FORM_IDS_CAP,\n CONTACT_FORM_IDS_FIELD,\n CONTACT_INTERACTIONS_CAP,\n type ContactFacet,\n type ContactInteraction,\n contactDisplayName,\n contactEmails,\n normalizeContactEmail,\n readContactFacet,\n} from '@aglyn/aglyn/app-utils/contacts'\nimport { CONTACT_COMPANY_IDS_FIELD, CONTACT_LIFECYCLE_STAGE_LABELS } from '@aglyn/aglyn/app-utils/crm'\nimport {\n MARKETING_CONSENT_BY_HOST_FIELD,\n MARKETING_CONSENT_FIELD,\n} from '@aglyn/aglyn/app-utils/marketing-consent'\nimport { nameSearchFields } from '@aglyn/aglyn/app-utils/name-search'\n\n/** The tags cap every writer applies, so a union cannot exceed it. */\nconst TAGS_CAP = 20\n/** The notes cap the record page applies to its About box. */\nconst NOTES_CAP = 5000\n\n/** Facet figures that add up: two records of one buyer are one buyer. */\nconst SUM_FIELDS = [\n 'ltvCents',\n 'ordersCount',\n 'refundedCents',\n 'refundedOrdersCount',\n] as const\n/** Facet instants where the later one is the truth. */\nconst LATEST_FIELDS = [\n 'lastPurchaseAtMs',\n 'lastRefundAtMs',\n 'lastEmailEngagementAtMs',\n] as const\n/** Facet instants where the earlier one is the truth. */\nconst EARLIEST_FIELDS = ['firstPurchaseAtMs'] as const\n\nexport interface ContactMergePlan {\n /**\n * What the survivor becomes, as a merge-`set` payload: every derived\n * field with its full value, and only the groups the merged record held\n * under `facets`. Plain values throughout — the writer adds its own\n * `updatedAt`.\n */\n survivor: Record<string, unknown>\n /** Every address the survivor answers to afterwards, primary first. */\n emails: string[]\n /** The merged record's addresses — what its leads and index entries are keyed by. */\n mergedEmails: string[]\n /**\n * The companies that lose a contact: those BOTH records were filed under,\n * which counted two people and now count one. A company only one record\n * named keeps its count — the survivor still names it.\n */\n companyCounts: Array<{ companyId: string; delta: -1 }>\n /** The union of both records' scope tokens — who may read the survivor. */\n visibleTo: string[]\n}\n\ntype Doc = Record<string, unknown>\n\n/** Whether a value counts as \"nothing here\" for the fill rule. */\nfunction isEmpty(value: unknown): boolean {\n if (value === undefined || value === null) return true\n if (typeof value === 'string') return value.trim() === ''\n if (Array.isArray(value)) return value.length === 0\n if (typeof value === 'object') return Object.keys(value as object).length === 0\n return false\n}\n\nfunction text(value: unknown): string {\n return typeof value === 'string' ? value : ''\n}\n\nfunction strings(value: unknown): string[] {\n return Array.isArray(value)\n ? value.filter((item): item is string => typeof item === 'string' && item !== '')\n : []\n}\n\nfunction number(value: unknown): number | null {\n return typeof value === 'number' && Number.isFinite(value) ? value : null\n}\n\n/** The union of two string lists, first list's order first, deduplicated. */\nfunction union(a: unknown, b: unknown, cap = Number.POSITIVE_INFINITY): string[] {\n return [...new Set([...strings(a), ...strings(b)])].slice(0, cap)\n}\n\n/** Survivor's value where it has one, the merged record's otherwise. */\nfunction fill<T>(survivor: T, merged: T): T {\n return isEmpty(survivor) ? merged : survivor\n}\n\n/** Notes appended, survivor first, one blank line between, bounded. */\nfunction joinNotes(survivor: unknown, merged: unknown): string {\n const a = text(survivor).trim()\n const b = text(merged).trim()\n if (!b || a === b) return a\n if (!a) return b\n return `${a}\\n\\n${b}`.slice(0, NOTES_CAP)\n}\n\n/** What makes two timeline entries the same event. */\nfunction interactionKey(entry: ContactInteraction): string {\n return [entry.type, entry.refId ?? '', entry.atMs, entry.hostId ?? ''].join('|')\n}\n\n/**\n * Both timelines as one: deduplicated by event, newest first, and capped\n * the way every capture caps it — so a merge of two full timelines keeps\n * the fifty most recent visits rather than growing the document past what\n * a capture may write.\n */\nexport function mergeInteractions(\n survivor: readonly ContactInteraction[] | undefined,\n merged: readonly ContactInteraction[] | undefined,\n): ContactInteraction[] {\n const seen = new Set<string>()\n const out: ContactInteraction[] = []\n for (const entry of [...(survivor ?? []), ...(merged ?? [])]) {\n if (!entry || typeof entry !== 'object') continue\n const key = interactionKey(entry)\n if (seen.has(key)) continue\n seen.add(key)\n out.push(entry)\n }\n return out\n .sort((a, b) => (number(b.atMs) ?? 0) - (number(a.atMs) ?? 0))\n .slice(0, CONTACT_INTERACTIONS_CAP)\n}\n\n/** The custom values per key: the survivor's where set, the merged record's otherwise. */\nfunction mergeCustom(survivor: unknown, merged: unknown): Record<string, unknown> {\n const a = (survivor && typeof survivor === 'object' ? survivor : {}) as Doc\n const b = (merged && typeof merged === 'object' ? merged : {}) as Doc\n const out: Doc = { ...b }\n for (const [key, value] of Object.entries(a)) {\n if (!isEmpty(value) || !(key in b)) out[key] = value\n }\n return out\n}\n\n/**\n * One holder's facet after the merge.\n *\n * `survivor` is that holder's facet on the surviving record — possibly\n * absent, when only the merged record was held by this group — and `merged`\n * the same holder's facet on the record being folded in.\n */\nexport function mergeContactFacet(\n survivor: Doc | undefined,\n merged: Doc | undefined,\n): Doc {\n if (!merged) return { ...(survivor ?? {}) }\n if (!survivor) return { ...merged }\n const out: Doc = { ...merged }\n // Every scalar the survivor holds stands; every one it lacks fills.\n for (const [key, value] of Object.entries(survivor)) {\n out[key] = fill(value, merged[key])\n }\n const a = survivor as unknown as ContactFacet\n const b = merged as unknown as ContactFacet\n out['sources'] = { ...(b.sources ?? {}), ...(a.sources ?? {}) }\n out['interactions'] = mergeInteractions(a.interactions, b.interactions)\n const tags = union(a.tags, b.tags, TAGS_CAP)\n if (tags.length) out['tags'] = tags\n const campaignIds = union(a.campaignIds, b.campaignIds)\n if (campaignIds.length) out['campaignIds'] = campaignIds\n const notes = joinNotes(a.notes, b.notes)\n if (notes) out['notes'] = notes\n if (!isEmpty(a.custom) || !isEmpty(b.custom)) {\n out['custom'] = mergeCustom(a.custom, b.custom)\n }\n for (const field of SUM_FIELDS) {\n const left = number(a[field])\n const right = number(b[field])\n if (left !== null || right !== null) out[field] = (left ?? 0) + (right ?? 0)\n }\n for (const field of LATEST_FIELDS) {\n const left = number(a[field])\n const right = number(b[field])\n if (left !== null || right !== null) {\n out[field] = Math.max(left ?? 0, right ?? 0)\n }\n }\n for (const field of EARLIEST_FIELDS) {\n const left = number(a[field])\n const right = number(b[field])\n if (left !== null && right !== null) out[field] = Math.min(left, right)\n else if (left !== null || right !== null) out[field] = left ?? right\n }\n return out\n}\n\nfunction facetsOf(doc: Doc): Record<string, Doc> {\n const facets = doc[CONTACT_FACETS_FIELD]\n return facets && typeof facets === 'object' && !Array.isArray(facets)\n ? (facets as Record<string, Doc>)\n : {}\n}\n\nfunction mapOf(value: unknown): Doc {\n return value && typeof value === 'object' && !Array.isArray(value)\n ? (value as Doc)\n : {}\n}\n\n/**\n * A refusal one record holds for a site, written over the other record's grant\n * for the same site (AGL-3320).\n *\n * The refusal outranks the grant, as it does when one person's refusal and\n * grant sit on two sites of one group: two records of one person who said no\n * to a site are a person who said no to it, and a merge that kept the grant\n * would start mailing them. The grant is kept beside it as `supersededEntry`,\n * so what the survivor held is still on the record.\n *\n * The merge is written with a merging `set`, which keeps every field of the\n * survivor's entry it does not name — so the grant's own fields are named\n * here, empty, or the refusal would read as carrying the grant's disclosure\n * and whoever recorded it.\n */\nfunction refusalOver(refusal: Doc, grant: Doc): Doc {\n const entry: Doc = { ...refusal, supersededEntry: grant }\n for (const key of Object.keys(grant)) {\n if (!(key in entry)) entry[key] = null\n }\n return entry\n}\n\n/**\n * The plan for folding `merged` into `survivor`.\n *\n * Both are the stored documents as read. The survivor's `email` is the\n * identity that stands; the merged record's becomes an alternate.\n */\nexport function planContactMerge(survivor: Doc, merged: Doc): ContactMergePlan {\n const survivorEmail = normalizeContactEmail(survivor['email']) ?? ''\n const mergedEmails = contactEmails(merged)\n const alternates = [\n ...new Set([\n ...strings(survivor[CONTACT_ALTERNATE_EMAILS_FIELD]),\n ...mergedEmails,\n ]),\n ]\n .filter((email) => email !== survivorEmail)\n .slice(0, CONTACT_ALTERNATE_EMAILS_CAP)\n\n const out: Doc = {\n [CONTACT_ALTERNATE_EMAILS_FIELD]: alternates,\n }\n // The canonical name, with its search keys, only when the survivor had none.\n const survivorName = text(survivor['name']).trim()\n const mergedName = text(merged['name']).trim()\n if (!survivorName && mergedName) Object.assign(out, nameSearchFields(mergedName))\n // The search echoes, on the same fill rule.\n for (const key of ['phone', 'companyName'] as const) {\n if (isEmpty(survivor[key]) && !isEmpty(merged[key])) out[key] = merged[key]\n }\n // The shared lists: who may read the row, who captured it, which forms and\n // companies it is filed under.\n const visibleTo = union(survivor['visibleTo'], merged['visibleTo'])\n out['visibleTo'] = visibleTo\n const capturedBy = union(survivor['capturedByHostIds'], merged['capturedByHostIds'])\n if (capturedBy.length) out['capturedByHostIds'] = capturedBy\n const formIds = union(\n survivor[CONTACT_FORM_IDS_FIELD],\n merged[CONTACT_FORM_IDS_FIELD],\n CONTACT_FORM_IDS_CAP,\n )\n if (formIds.length) out[CONTACT_FORM_IDS_FIELD] = formIds\n const survivorCompanies = strings(survivor[CONTACT_COMPANY_IDS_FIELD])\n const mergedCompanies = strings(merged[CONTACT_COMPANY_IDS_FIELD])\n const companyIds = [...new Set([...survivorCompanies, ...mergedCompanies])]\n if (companyIds.length) out[CONTACT_COMPANY_IDS_FIELD] = companyIds\n const companyCounts = survivorCompanies\n .filter((id) => mergedCompanies.includes(id))\n .map((companyId) => ({ companyId, delta: -1 as const }))\n // The organization-level fields the REST API reads and writes.\n const tags = union(survivor['tags'], merged['tags'])\n if (tags.length) out['tags'] = tags\n const notes = joinNotes(survivor['notes'], merged['notes'])\n if (notes) out['notes'] = notes\n if (!isEmpty(survivor['custom']) || !isEmpty(merged['custom'])) {\n out['custom'] = mergeCustom(survivor['custom'], merged['custom'])\n }\n // Consent: a grant fills where the survivor has none for that site, and a\n // refusal on either record stands — per site as well as unscoped (AGL-3320).\n const survivorConsent = mapOf(survivor[MARKETING_CONSENT_BY_HOST_FIELD])\n const mergedConsent = mapOf(merged[MARKETING_CONSENT_BY_HOST_FIELD])\n const consentByHost: Doc = { ...mergedConsent, ...survivorConsent }\n for (const [hostId, value] of Object.entries(mergedConsent)) {\n const refusal = mapOf(value)\n if (refusal[MARKETING_CONSENT_FIELD] !== false) continue\n if (!(hostId in survivorConsent)) continue\n const standing = mapOf(survivorConsent[hostId])\n if (standing[MARKETING_CONSENT_FIELD] === false) continue\n consentByHost[hostId] = refusalOver(refusal, standing)\n }\n if (Object.keys(consentByHost).length) {\n out[MARKETING_CONSENT_BY_HOST_FIELD] = consentByHost\n }\n if (survivor['marketingConsent'] === false || merged['marketingConsent'] === false) {\n out['marketingConsent'] = false\n } else if (survivor['marketingConsent'] === true || merged['marketingConsent'] === true) {\n out['marketingConsent'] = true\n }\n // Each holder's facet — only the groups the merged record held need a write.\n const survivorFacets = facetsOf(survivor)\n const mergedFacets = facetsOf(merged)\n const facets: Record<string, Doc> = {}\n for (const [groupId, facet] of Object.entries(mergedFacets)) {\n facets[groupId] = mergeContactFacet(survivorFacets[groupId], facet)\n }\n if (Object.keys(facets).length) out[CONTACT_FACETS_FIELD] = facets\n\n return {\n survivor: out,\n emails: [survivorEmail, ...alternates].filter(Boolean),\n mergedEmails,\n companyCounts,\n visibleTo,\n }\n}\n\n/** Where a previewed value comes from. */\nexport type ContactMergeSource = 'survivor' | 'merged' | 'both' | 'none'\n\n/** One line of the side-by-side the dialog shows. */\nexport interface ContactMergePreviewRow {\n key: string\n label: string\n /** The surviving record's value, as text. */\n survivor: string\n /** The merged record's value, as text. */\n merged: string\n /** What the survivor carries afterwards. */\n result: string\n from: ContactMergeSource\n}\n\nexport interface ContactMergePreviewOptions {\n /** The owner's display name for a uid; the uid itself when absent. */\n memberName?: (uid: string) => string\n}\n\nfunction addressLine(value: unknown): string {\n const address = mapOf(value)\n return ['line1', 'line2', 'city', 'state', 'postalCode', 'country']\n .map((key) => text(address[key]).trim())\n .filter(Boolean)\n .join(', ')\n}\n\nfunction sourceOf(survivor: string, merged: string, result: string): ContactMergeSource {\n if (!result) return 'none'\n if (survivor && merged && survivor !== merged && result !== survivor && result !== merged) {\n return 'both'\n }\n if (result === survivor && survivor === merged) return 'both'\n return result === survivor ? 'survivor' : 'merged'\n}\n\n/**\n * The merge as the viewing group would see it, field by field.\n *\n * Reads through ONE group's facet, the way the record page does, so a\n * holder previewing a merge sees its own values and never another holder's.\n * Every row carries the survivor's value, the merged record's, and the\n * result the plan would write, so the dialog is a rendering and not a second\n * opinion.\n */\nexport function contactMergePreview(\n survivor: Doc,\n merged: Doc,\n groupId: string,\n options: ContactMergePreviewOptions = {},\n): ContactMergePreviewRow[] {\n const plan = planContactMerge(survivor, merged)\n const after: Doc = {\n ...survivor,\n ...plan.survivor,\n [CONTACT_FACETS_FIELD]: {\n ...facetsOf(survivor),\n ...mapOf(plan.survivor[CONTACT_FACETS_FIELD]),\n },\n }\n const a = readContactFacet(survivor, groupId)\n const b = readContactFacet(merged, groupId)\n const c = readContactFacet(after, groupId)\n const memberName = options.memberName ?? ((uid: string) => uid)\n const stage = (value: unknown) =>\n typeof value === 'string' && value in CONTACT_LIFECYCLE_STAGE_LABELS\n ? CONTACT_LIFECYCLE_STAGE_LABELS[value as keyof typeof CONTACT_LIFECYCLE_STAGE_LABELS]\n : ''\n const owner = (value: unknown) => (text(value) ? memberName(text(value)) : '')\n const list = (value: unknown) => strings(value).join(', ')\n const money = (facet: ContactFacet) =>\n facet.ordersCount\n ? `${facet.ordersCount} · $${((facet.ltvCents ?? 0) / 100).toFixed(2)}`\n : ''\n const row = (\n key: string,\n label: string,\n left: string,\n right: string,\n result: string,\n ): ContactMergePreviewRow => ({\n key,\n label,\n survivor: left,\n merged: right,\n result,\n from: sourceOf(left, right, result),\n })\n const rows: ContactMergePreviewRow[] = [\n row(\n 'name',\n 'Name',\n contactDisplayName(survivor, groupId),\n contactDisplayName(merged, groupId),\n contactDisplayName(after, groupId),\n ),\n row(\n 'email',\n 'Email',\n contactEmails(survivor).join(', '),\n contactEmails(merged).join(', '),\n plan.emails.join(', '),\n ),\n row('phone', 'Phone', text(a.phone), text(b.phone), text(c.phone)),\n row('jobTitle', 'Job title', text(a.jobTitle), text(b.jobTitle), text(c.jobTitle)),\n row(\n 'company',\n 'Company',\n text(a.companyName),\n text(b.companyName),\n text(c.companyName),\n ),\n row(\n 'lifecycleStage',\n 'Stage',\n stage(a.lifecycleStage),\n stage(b.lifecycleStage),\n stage(c.lifecycleStage),\n ),\n row('owner', 'Owner', owner(a.ownerUid), owner(b.ownerUid), owner(c.ownerUid)),\n row('tags', 'Tags', list(a.tags), list(b.tags), list(c.tags)),\n row('notes', 'Notes', text(a.notes), text(b.notes), text(c.notes)),\n row(\n 'address',\n 'Address',\n addressLine(a.address),\n addressLine(b.address),\n addressLine(c.address),\n ),\n row('orders', 'Orders', money(a), money(b), money(c)),\n row(\n 'timeline',\n 'Timeline entries',\n a.interactions.length ? String(a.interactions.length) : '',\n b.interactions.length ? String(b.interactions.length) : '',\n c.interactions.length ? String(c.interactions.length) : '',\n ),\n ]\n const customKeys = [\n ...new Set([...Object.keys(mapOf(a.custom)), ...Object.keys(mapOf(b.custom))]),\n ].sort()\n for (const key of customKeys) {\n const show = (facet: ContactFacet) => {\n const value = mapOf(facet.custom)[key]\n return value === null || value === undefined ? '' : String(value)\n }\n rows.push(row(`custom.${key}`, key, show(a), show(b), show(c)))\n }\n return rows\n}\n"],"names":["CONTACT_ALTERNATE_EMAILS_CAP","CONTACT_ALTERNATE_EMAILS_FIELD","CONTACT_FACETS_FIELD","CONTACT_FORM_IDS_CAP","CONTACT_FORM_IDS_FIELD","CONTACT_INTERACTIONS_CAP","contactDisplayName","contactEmails","normalizeContactEmail","readContactFacet","CONTACT_COMPANY_IDS_FIELD","CONTACT_LIFECYCLE_STAGE_LABELS","MARKETING_CONSENT_BY_HOST_FIELD","MARKETING_CONSENT_FIELD","nameSearchFields","TAGS_CAP","NOTES_CAP","SUM_FIELDS","LATEST_FIELDS","EARLIEST_FIELDS","isEmpty","value","undefined","trim","Array","isArray","length","Object","keys","text","strings","filter","item","number","Number","isFinite","union","a","b","cap","POSITIVE_INFINITY","Set","slice","fill","survivor","merged","joinNotes","interactionKey","entry","type","refId","atMs","hostId","join","mergeInteractions","seen","out","key","has","add","push","sort","mergeCustom","entries","mergeContactFacet","sources","interactions","tags","campaignIds","notes","custom","field","left","right","Math","max","min","facetsOf","doc","facets","mapOf","refusalOver","refusal","grant","supersededEntry","planContactMerge","survivorEmail","mergedEmails","alternates","email","survivorName","mergedName","assign","visibleTo","capturedBy","formIds","survivorCompanies","mergedCompanies","companyIds","companyCounts","id","includes","map","companyId","delta","survivorConsent","mergedConsent","consentByHost","standing","survivorFacets","mergedFacets","groupId","facet","emails","Boolean","addressLine","address","sourceOf","result","contactMergePreview","options","plan","after","c","memberName","uid","stage","owner","list","money","ordersCount","ltvCents","toFixed","row","label","from","rows","phone","jobTitle","companyName","lifecycleStage","ownerUid","String","customKeys","show"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/contact-merge.ts"],"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\n/**\n * MERGING TWO CONTACT DOCUMENTS INTO ONE (AGL-2625) — the pure half.\n *\n * Contacts are keyed by email, so exact duplicates cannot exist; one human\n * with `jane@acme.com` and `jane@gmail.com` is two documents, with tags,\n * deals, tasks and a timeline split between them. A merge folds the second\n * into the first: the SURVIVOR keeps its address as the identity, the MERGED\n * record's address becomes an alternate on it, and every field is combined\n * by one rule the dialog can show before the write happens.\n *\n * ## The rule\n *\n * - A scalar — a name, a phone, a title, a stage, an owner — is the\n * survivor's where the survivor has one, and fills from the merged record\n * where the survivor is empty. Nothing the survivor holds is overwritten.\n * - A list — tags, campaign filings, the timeline, the sites that captured\n * the person, the forms, the companies — is the UNION, deduplicated and\n * bounded where the write paths bound it.\n * - A figure that counts something — orders, lifetime value, refunds — is\n * the SUM: two records of one person's purchases are one person's\n * purchases. A \"first\" is the earlier, a \"last\" is the later.\n * - Notes are appended, survivor first: a note is a list of sentences, and\n * dropping the merged record's would be the one silent loss in a merge\n * that is otherwise lossless.\n * - A recorded REFUSAL of marketing mail on either record stands on the\n * survivor. The merge asserts the two are one person, and that person\n * said no. So does a do-not-call (AGL-3515), for the same reason.\n * - The name is ONE value made of three fields (AGL-3515): a holder's\n * `name`, `firstName` and `lastName` come across together, from the\n * survivor when it names the person at all and from the merged record\n * only when it does not — so a survivor called \"Bob\" never gains a first\n * name of \"Robert\" that its name does not show.\n * - A reports-to that would point at the survivor itself — the survivor's\n * pointing at the merged record, or the merged record's at the survivor —\n * is cleared (`null`) rather than kept as a person reporting to\n * themselves.\n *\n * Applied per holder: each group's facet on the merged record is folded\n * into the same group's facet on the survivor, and a group only the merged\n * record had comes across whole. No holder's records reach another holder's\n * facet, which is the guarantee the facet shape exists for.\n *\n * ## Why this is a plan and not a write\n *\n * The console shows the two records side by side and says which value wins\n * before anything is committed; the server performs the write. Both read\n * this function, so the preview cannot show one outcome and the write land\n * another. The plan is plain values — no sentinels — so it can be rendered,\n * and it is shaped for a merge-`set`, which deep-merges the facet map one\n * holder at a time and replaces an array whole: exactly the two behaviors\n * the rule above needs.\n */\n\nimport {\n CONTACT_ALTERNATE_EMAILS_CAP,\n CONTACT_ALTERNATE_EMAILS_FIELD,\n CONTACT_FACETS_FIELD,\n CONTACT_FORM_IDS_CAP,\n CONTACT_FORM_IDS_FIELD,\n CONTACT_INTERACTIONS_CAP,\n type ContactFacet,\n type ContactInteraction,\n contactDisplayName,\n contactEmails,\n normalizeContactEmail,\n readContactFacet,\n} from '@aglyn/aglyn/app-utils/contacts'\nimport { CONTACT_COMPANY_IDS_FIELD, CONTACT_LIFECYCLE_STAGE_LABELS } from '@aglyn/aglyn/app-utils/crm'\nimport {\n MARKETING_CONSENT_BY_HOST_FIELD,\n MARKETING_CONSENT_FIELD,\n} from '@aglyn/aglyn/app-utils/marketing-consent'\nimport { nameSearchFields } from '@aglyn/aglyn/app-utils/name-search'\n\n/** The tags cap every writer applies, so a union cannot exceed it. */\nconst TAGS_CAP = 20\n/** The notes cap the record page applies to its About box. */\nconst NOTES_CAP = 5000\n\n/** Facet figures that add up: two records of one buyer are one buyer. */\nconst SUM_FIELDS = [\n 'ltvCents',\n 'ordersCount',\n 'refundedCents',\n 'refundedOrdersCount',\n] as const\n/** Facet instants where the later one is the truth. */\nconst LATEST_FIELDS = [\n 'lastPurchaseAtMs',\n 'lastRefundAtMs',\n 'lastEmailEngagementAtMs',\n] as const\n/** Facet instants where the earlier one is the truth. */\nconst EARLIEST_FIELDS = ['firstPurchaseAtMs'] as const\n/** The facet fields that together name the person, merged as one (AGL-3515). */\nconst NAME_FIELDS = ['name', 'firstName', 'lastName'] as const\n\n/** The two records' ids, when the caller knows them — what a self-pointer is judged by. */\nexport interface ContactMergeIds {\n survivorId: string\n mergedId: string\n}\n\nexport interface ContactMergePlan {\n /**\n * What the survivor becomes, as a merge-`set` payload: every derived\n * field with its full value, and only the groups the merged record held\n * under `facets`. Plain values throughout — the writer adds its own\n * `updatedAt`.\n */\n survivor: Record<string, unknown>\n /** Every address the survivor answers to afterwards, primary first. */\n emails: string[]\n /** The merged record's addresses — what its leads and index entries are keyed by. */\n mergedEmails: string[]\n /**\n * The companies that lose a contact: those BOTH records were filed under,\n * which counted two people and now count one. A company only one record\n * named keeps its count — the survivor still names it.\n */\n companyCounts: Array<{ companyId: string; delta: -1 }>\n /** The union of both records' scope tokens — who may read the survivor. */\n visibleTo: string[]\n}\n\ntype Doc = Record<string, unknown>\n\n/** Whether a value counts as \"nothing here\" for the fill rule. */\nfunction isEmpty(value: unknown): boolean {\n if (value === undefined || value === null) return true\n if (typeof value === 'string') return value.trim() === ''\n if (Array.isArray(value)) return value.length === 0\n if (typeof value === 'object') return Object.keys(value as object).length === 0\n return false\n}\n\nfunction text(value: unknown): string {\n return typeof value === 'string' ? value : ''\n}\n\nfunction strings(value: unknown): string[] {\n return Array.isArray(value)\n ? value.filter((item): item is string => typeof item === 'string' && item !== '')\n : []\n}\n\nfunction number(value: unknown): number | null {\n return typeof value === 'number' && Number.isFinite(value) ? value : null\n}\n\n/** The union of two string lists, first list's order first, deduplicated. */\nfunction union(a: unknown, b: unknown, cap = Number.POSITIVE_INFINITY): string[] {\n return [...new Set([...strings(a), ...strings(b)])].slice(0, cap)\n}\n\n/** Survivor's value where it has one, the merged record's otherwise. */\nfunction fill<T>(survivor: T, merged: T): T {\n return isEmpty(survivor) ? merged : survivor\n}\n\n/** Notes appended, survivor first, one blank line between, bounded. */\nfunction joinNotes(survivor: unknown, merged: unknown): string {\n const a = text(survivor).trim()\n const b = text(merged).trim()\n if (!b || a === b) return a\n if (!a) return b\n return `${a}\\n\\n${b}`.slice(0, NOTES_CAP)\n}\n\n/** What makes two timeline entries the same event. */\nfunction interactionKey(entry: ContactInteraction): string {\n return [entry.type, entry.refId ?? '', entry.atMs, entry.hostId ?? ''].join('|')\n}\n\n/**\n * Both timelines as one: deduplicated by event, newest first, and capped\n * the way every capture caps it — so a merge of two full timelines keeps\n * the fifty most recent visits rather than growing the document past what\n * a capture may write.\n */\nexport function mergeInteractions(\n survivor: readonly ContactInteraction[] | undefined,\n merged: readonly ContactInteraction[] | undefined,\n): ContactInteraction[] {\n const seen = new Set<string>()\n const out: ContactInteraction[] = []\n for (const entry of [...(survivor ?? []), ...(merged ?? [])]) {\n if (!entry || typeof entry !== 'object') continue\n const key = interactionKey(entry)\n if (seen.has(key)) continue\n seen.add(key)\n out.push(entry)\n }\n return out\n .sort((a, b) => (number(b.atMs) ?? 0) - (number(a.atMs) ?? 0))\n .slice(0, CONTACT_INTERACTIONS_CAP)\n}\n\n/** The custom values per key: the survivor's where set, the merged record's otherwise. */\nfunction mergeCustom(survivor: unknown, merged: unknown): Record<string, unknown> {\n const a = (survivor && typeof survivor === 'object' ? survivor : {}) as Doc\n const b = (merged && typeof merged === 'object' ? merged : {}) as Doc\n const out: Doc = { ...b }\n for (const [key, value] of Object.entries(a)) {\n if (!isEmpty(value) || !(key in b)) out[key] = value\n }\n return out\n}\n\n/**\n * One holder's facet after the merge.\n *\n * `survivor` is that holder's facet on the surviving record — possibly\n * absent, when only the merged record was held by this group — and `merged`\n * the same holder's facet on the record being folded in.\n */\nexport function mergeContactFacet(\n survivor: Doc | undefined,\n merged: Doc | undefined,\n ids?: ContactMergeIds,\n): Doc {\n const out = foldContactFacet(survivor, merged)\n /*\n * Nobody reports to themselves — see the rule above. Written as `null`\n * rather than left out, because the plan is a merge-`set`, which keeps a\n * stored key it is not given.\n */\n if (ids && [ids.survivorId, ids.mergedId].includes(text(out['reportsToContactId']))) {\n out['reportsToContactId'] = null\n }\n return out\n}\n\nfunction foldContactFacet(survivor: Doc | undefined, merged: Doc | undefined): Doc {\n if (!merged) return { ...(survivor ?? {}) }\n if (!survivor) return { ...merged }\n const out: Doc = { ...merged }\n // Every scalar the survivor holds stands; every one it lacks fills.\n for (const [key, value] of Object.entries(survivor)) {\n out[key] = fill(value, merged[key])\n }\n // The name's three fields, from one record — see the rule above.\n const named = NAME_FIELDS.some((key) => !isEmpty(survivor[key]))\n for (const key of NAME_FIELDS) {\n const value = named ? survivor[key] : merged[key]\n if (isEmpty(value)) delete out[key]\n else out[key] = value\n }\n if (survivor['doNotCall'] === true || merged['doNotCall'] === true) out['doNotCall'] = true\n const a = survivor as unknown as ContactFacet\n const b = merged as unknown as ContactFacet\n out['sources'] = { ...(b.sources ?? {}), ...(a.sources ?? {}) }\n out['interactions'] = mergeInteractions(a.interactions, b.interactions)\n const tags = union(a.tags, b.tags, TAGS_CAP)\n if (tags.length) out['tags'] = tags\n const campaignIds = union(a.campaignIds, b.campaignIds)\n if (campaignIds.length) out['campaignIds'] = campaignIds\n const notes = joinNotes(a.notes, b.notes)\n if (notes) out['notes'] = notes\n if (!isEmpty(a.custom) || !isEmpty(b.custom)) {\n out['custom'] = mergeCustom(a.custom, b.custom)\n }\n for (const field of SUM_FIELDS) {\n const left = number(a[field])\n const right = number(b[field])\n if (left !== null || right !== null) out[field] = (left ?? 0) + (right ?? 0)\n }\n for (const field of LATEST_FIELDS) {\n const left = number(a[field])\n const right = number(b[field])\n if (left !== null || right !== null) {\n out[field] = Math.max(left ?? 0, right ?? 0)\n }\n }\n for (const field of EARLIEST_FIELDS) {\n const left = number(a[field])\n const right = number(b[field])\n if (left !== null && right !== null) out[field] = Math.min(left, right)\n else if (left !== null || right !== null) out[field] = left ?? right\n }\n return out\n}\n\nfunction facetsOf(doc: Doc): Record<string, Doc> {\n const facets = doc[CONTACT_FACETS_FIELD]\n return facets && typeof facets === 'object' && !Array.isArray(facets)\n ? (facets as Record<string, Doc>)\n : {}\n}\n\nfunction mapOf(value: unknown): Doc {\n return value && typeof value === 'object' && !Array.isArray(value)\n ? (value as Doc)\n : {}\n}\n\n/**\n * A refusal one record holds for a site, written over the other record's grant\n * for the same site (AGL-3320).\n *\n * The refusal outranks the grant, as it does when one person's refusal and\n * grant sit on two sites of one group: two records of one person who said no\n * to a site are a person who said no to it, and a merge that kept the grant\n * would start mailing them. The grant is kept beside it as `supersededEntry`,\n * so what the survivor held is still on the record.\n *\n * The merge is written with a merging `set`, which keeps every field of the\n * survivor's entry it does not name — so the grant's own fields are named\n * here, empty, or the refusal would read as carrying the grant's disclosure\n * and whoever recorded it.\n */\nfunction refusalOver(refusal: Doc, grant: Doc): Doc {\n const entry: Doc = { ...refusal, supersededEntry: grant }\n for (const key of Object.keys(grant)) {\n if (!(key in entry)) entry[key] = null\n }\n return entry\n}\n\n/**\n * The plan for folding `merged` into `survivor`.\n *\n * Both are the stored documents as read. The survivor's `email` is the\n * identity that stands; the merged record's becomes an alternate.\n */\nexport function planContactMerge(\n survivor: Doc,\n merged: Doc,\n ids?: ContactMergeIds,\n): ContactMergePlan {\n const survivorEmail = normalizeContactEmail(survivor['email']) ?? ''\n const mergedEmails = contactEmails(merged)\n const alternates = [\n ...new Set([\n ...strings(survivor[CONTACT_ALTERNATE_EMAILS_FIELD]),\n ...mergedEmails,\n ]),\n ]\n .filter((email) => email !== survivorEmail)\n .slice(0, CONTACT_ALTERNATE_EMAILS_CAP)\n\n const out: Doc = {\n [CONTACT_ALTERNATE_EMAILS_FIELD]: alternates,\n }\n // The canonical name, with its search keys, only when the survivor had none.\n const survivorName = text(survivor['name']).trim()\n const mergedName = text(merged['name']).trim()\n if (!survivorName && mergedName) Object.assign(out, nameSearchFields(mergedName))\n // The search echoes, on the same fill rule.\n for (const key of ['phone', 'companyName'] as const) {\n if (isEmpty(survivor[key]) && !isEmpty(merged[key])) out[key] = merged[key]\n }\n // The shared lists: who may read the row, who captured it, which forms and\n // companies it is filed under.\n const visibleTo = union(survivor['visibleTo'], merged['visibleTo'])\n out['visibleTo'] = visibleTo\n const capturedBy = union(survivor['capturedByHostIds'], merged['capturedByHostIds'])\n if (capturedBy.length) out['capturedByHostIds'] = capturedBy\n const formIds = union(\n survivor[CONTACT_FORM_IDS_FIELD],\n merged[CONTACT_FORM_IDS_FIELD],\n CONTACT_FORM_IDS_CAP,\n )\n if (formIds.length) out[CONTACT_FORM_IDS_FIELD] = formIds\n const survivorCompanies = strings(survivor[CONTACT_COMPANY_IDS_FIELD])\n const mergedCompanies = strings(merged[CONTACT_COMPANY_IDS_FIELD])\n const companyIds = [...new Set([...survivorCompanies, ...mergedCompanies])]\n if (companyIds.length) out[CONTACT_COMPANY_IDS_FIELD] = companyIds\n const companyCounts = survivorCompanies\n .filter((id) => mergedCompanies.includes(id))\n .map((companyId) => ({ companyId, delta: -1 as const }))\n // The organization-level fields the REST API reads and writes.\n const tags = union(survivor['tags'], merged['tags'])\n if (tags.length) out['tags'] = tags\n const notes = joinNotes(survivor['notes'], merged['notes'])\n if (notes) out['notes'] = notes\n if (!isEmpty(survivor['custom']) || !isEmpty(merged['custom'])) {\n out['custom'] = mergeCustom(survivor['custom'], merged['custom'])\n }\n // Consent: a grant fills where the survivor has none for that site, and a\n // refusal on either record stands — per site as well as unscoped (AGL-3320).\n const survivorConsent = mapOf(survivor[MARKETING_CONSENT_BY_HOST_FIELD])\n const mergedConsent = mapOf(merged[MARKETING_CONSENT_BY_HOST_FIELD])\n const consentByHost: Doc = { ...mergedConsent, ...survivorConsent }\n for (const [hostId, value] of Object.entries(mergedConsent)) {\n const refusal = mapOf(value)\n if (refusal[MARKETING_CONSENT_FIELD] !== false) continue\n if (!(hostId in survivorConsent)) continue\n const standing = mapOf(survivorConsent[hostId])\n if (standing[MARKETING_CONSENT_FIELD] === false) continue\n consentByHost[hostId] = refusalOver(refusal, standing)\n }\n if (Object.keys(consentByHost).length) {\n out[MARKETING_CONSENT_BY_HOST_FIELD] = consentByHost\n }\n if (survivor['marketingConsent'] === false || merged['marketingConsent'] === false) {\n out['marketingConsent'] = false\n } else if (survivor['marketingConsent'] === true || merged['marketingConsent'] === true) {\n out['marketingConsent'] = true\n }\n // Each holder's facet — only the groups the merged record held need a write.\n const survivorFacets = facetsOf(survivor)\n const mergedFacets = facetsOf(merged)\n const facets: Record<string, Doc> = {}\n for (const [groupId, facet] of Object.entries(mergedFacets)) {\n facets[groupId] = mergeContactFacet(survivorFacets[groupId], facet, ids)\n }\n // A survivor facet the merged record did not hold may still point at it.\n if (ids) {\n for (const [groupId, facet] of Object.entries(survivorFacets)) {\n if (groupId in facets || facet['reportsToContactId'] !== ids.mergedId) continue\n facets[groupId] = mergeContactFacet(facet, undefined, ids)\n }\n }\n if (Object.keys(facets).length) out[CONTACT_FACETS_FIELD] = facets\n\n return {\n survivor: out,\n emails: [survivorEmail, ...alternates].filter(Boolean),\n mergedEmails,\n companyCounts,\n visibleTo,\n }\n}\n\n/** Where a previewed value comes from. */\nexport type ContactMergeSource = 'survivor' | 'merged' | 'both' | 'none'\n\n/** One line of the side-by-side the dialog shows. */\nexport interface ContactMergePreviewRow {\n key: string\n label: string\n /** The surviving record's value, as text. */\n survivor: string\n /** The merged record's value, as text. */\n merged: string\n /** What the survivor carries afterwards. */\n result: string\n from: ContactMergeSource\n}\n\nexport interface ContactMergePreviewOptions {\n /** The owner's display name for a uid; the uid itself when absent. */\n memberName?: (uid: string) => string\n /**\n * The two records' ids, so the preview applies the rule the write does\n * to a reports-to (AGL-3537): a pointer at either record would be the\n * person reporting to themselves, and becomes nobody.\n */\n ids?: ContactMergeIds\n /** A contact's name for an id, for Reports to; the id itself when absent. */\n contactName?: (contactId: string) => string\n}\n\n/** What Reports to reads when the merge leaves the person reporting to nobody because the pointer was at themselves. */\nexport const CONTACT_MERGE_REPORTS_TO_SELF = 'Nobody — it named one of these two records'\n\nfunction addressLine(value: unknown): string {\n const address = mapOf(value)\n return ['line1', 'line2', 'city', 'state', 'postalCode', 'country']\n .map((key) => text(address[key]).trim())\n .filter(Boolean)\n .join(', ')\n}\n\nfunction sourceOf(survivor: string, merged: string, result: string): ContactMergeSource {\n if (!result) return 'none'\n if (survivor && merged && survivor !== merged && result !== survivor && result !== merged) {\n return 'both'\n }\n if (result === survivor && survivor === merged) return 'both'\n return result === survivor ? 'survivor' : 'merged'\n}\n\n/**\n * The merge as the viewing group would see it, field by field.\n *\n * Reads through ONE group's facet, the way the record page does, so a\n * holder previewing a merge sees its own values and never another holder's.\n * Every row carries the survivor's value, the merged record's, and the\n * result the plan would write, so the dialog is a rendering and not a second\n * opinion.\n */\nexport function contactMergePreview(\n survivor: Doc,\n merged: Doc,\n groupId: string,\n options: ContactMergePreviewOptions = {},\n): ContactMergePreviewRow[] {\n const plan = planContactMerge(survivor, merged, options.ids)\n const after: Doc = {\n ...survivor,\n ...plan.survivor,\n [CONTACT_FACETS_FIELD]: {\n ...facetsOf(survivor),\n ...mapOf(plan.survivor[CONTACT_FACETS_FIELD]),\n },\n }\n const a = readContactFacet(survivor, groupId)\n const b = readContactFacet(merged, groupId)\n const c = readContactFacet(after, groupId)\n const memberName = options.memberName ?? ((uid: string) => uid)\n const stage = (value: unknown) =>\n typeof value === 'string' && value in CONTACT_LIFECYCLE_STAGE_LABELS\n ? CONTACT_LIFECYCLE_STAGE_LABELS[value as keyof typeof CONTACT_LIFECYCLE_STAGE_LABELS]\n : ''\n const owner = (value: unknown) => (text(value) ? memberName(text(value)) : '')\n const contactName = options.contactName ?? ((contactId: string) => contactId)\n const person = (value: unknown) => (text(value) ? contactName(text(value)) : '')\n /*\n * Reports to (AGL-3537), as the write leaves it: the kept record's own\n * manager stands, an empty one fills from the other record's, and a\n * pointer at either of the two records — which after the merge are one\n * person — becomes nobody, and says why.\n */\n const pair = options.ids ? [options.ids.survivorId, options.ids.mergedId] : []\n const reportsToResult =\n person(c.reportsToContactId) ||\n ([a.reportsToContactId, b.reportsToContactId].some((id) => pair.includes(text(id)))\n ? CONTACT_MERGE_REPORTS_TO_SELF\n : '')\n const list = (value: unknown) => strings(value).join(', ')\n const flag = (value: unknown) => (value === true ? 'Yes' : '')\n const money = (facet: ContactFacet) =>\n facet.ordersCount\n ? `${facet.ordersCount} · $${((facet.ltvCents ?? 0) / 100).toFixed(2)}`\n : ''\n const row = (\n key: string,\n label: string,\n left: string,\n right: string,\n result: string,\n ): ContactMergePreviewRow => ({\n key,\n label,\n survivor: left,\n merged: right,\n result,\n from: sourceOf(left, right, result),\n })\n const rows: ContactMergePreviewRow[] = [\n row(\n 'name',\n 'Name',\n contactDisplayName(survivor, groupId),\n contactDisplayName(merged, groupId),\n contactDisplayName(after, groupId),\n ),\n row(\n 'email',\n 'Email',\n contactEmails(survivor).join(', '),\n contactEmails(merged).join(', '),\n plan.emails.join(', '),\n ),\n row('salutation', 'Salutation', text(a.salutation), text(b.salutation), text(c.salutation)),\n row('phone', 'Phone', text(a.phone), text(b.phone), text(c.phone)),\n row(\n 'mobilePhone',\n 'Mobile phone',\n text(a.mobilePhone),\n text(b.mobilePhone),\n text(c.mobilePhone),\n ),\n row('homePhone', 'Home phone', text(a.homePhone), text(b.homePhone), text(c.homePhone)),\n row('otherPhone', 'Other phone', text(a.otherPhone), text(b.otherPhone), text(c.otherPhone)),\n row('fax', 'Fax', text(a.fax), text(b.fax), text(c.fax)),\n row('doNotCall', 'Do not call', flag(a.doNotCall), flag(b.doNotCall), flag(c.doNotCall)),\n row('jobTitle', 'Job title', text(a.jobTitle), text(b.jobTitle), text(c.jobTitle)),\n row('department', 'Department', text(a.department), text(b.department), text(c.department)),\n {\n ...row(\n 'reportsTo',\n 'Reports to',\n person(a.reportsToContactId),\n person(b.reportsToContactId),\n reportsToResult,\n ),\n // Nobody, by the rule — taken from neither record.\n ...(reportsToResult === CONTACT_MERGE_REPORTS_TO_SELF ? { from: 'none' as const } : {}),\n },\n row(\n 'company',\n 'Company',\n text(a.companyName),\n text(b.companyName),\n text(c.companyName),\n ),\n row(\n 'lifecycleStage',\n 'Stage',\n stage(a.lifecycleStage),\n stage(b.lifecycleStage),\n stage(c.lifecycleStage),\n ),\n row('owner', 'Owner', owner(a.ownerUid), owner(b.ownerUid), owner(c.ownerUid)),\n row('tags', 'Tags', list(a.tags), list(b.tags), list(c.tags)),\n row('notes', 'Notes', text(a.notes), text(b.notes), text(c.notes)),\n row(\n 'address',\n 'Mailing address',\n addressLine(a.address),\n addressLine(b.address),\n addressLine(c.address),\n ),\n row(\n 'otherAddress',\n 'Other address',\n addressLine(a.otherAddress),\n addressLine(b.otherAddress),\n addressLine(c.otherAddress),\n ),\n row('birthdate', 'Birthdate', text(a.birthdate), text(b.birthdate), text(c.birthdate)),\n row(\n 'assistant',\n 'Assistant',\n text(a.assistantName),\n text(b.assistantName),\n text(c.assistantName),\n ),\n row(\n 'assistantPhone',\n 'Assistant phone',\n text(a.assistantPhone),\n text(b.assistantPhone),\n text(c.assistantPhone),\n ),\n row('orders', 'Orders', money(a), money(b), money(c)),\n row(\n 'timeline',\n 'Timeline entries',\n a.interactions.length ? String(a.interactions.length) : '',\n b.interactions.length ? String(b.interactions.length) : '',\n c.interactions.length ? String(c.interactions.length) : '',\n ),\n ]\n const customKeys = [\n ...new Set([...Object.keys(mapOf(a.custom)), ...Object.keys(mapOf(b.custom))]),\n ].sort()\n for (const key of customKeys) {\n const show = (facet: ContactFacet) => {\n const value = mapOf(facet.custom)[key]\n return value === null || value === undefined ? '' : String(value)\n }\n rows.push(row(`custom.${key}`, key, show(a), show(b), show(c)))\n }\n return rows\n}\n"],"names":["CONTACT_ALTERNATE_EMAILS_CAP","CONTACT_ALTERNATE_EMAILS_FIELD","CONTACT_FACETS_FIELD","CONTACT_FORM_IDS_CAP","CONTACT_FORM_IDS_FIELD","CONTACT_INTERACTIONS_CAP","contactDisplayName","contactEmails","normalizeContactEmail","readContactFacet","CONTACT_COMPANY_IDS_FIELD","CONTACT_LIFECYCLE_STAGE_LABELS","MARKETING_CONSENT_BY_HOST_FIELD","MARKETING_CONSENT_FIELD","nameSearchFields","TAGS_CAP","NOTES_CAP","SUM_FIELDS","LATEST_FIELDS","EARLIEST_FIELDS","NAME_FIELDS","isEmpty","value","undefined","trim","Array","isArray","length","Object","keys","text","strings","filter","item","number","Number","isFinite","union","a","b","cap","POSITIVE_INFINITY","Set","slice","fill","survivor","merged","joinNotes","interactionKey","entry","type","refId","atMs","hostId","join","mergeInteractions","seen","out","key","has","add","push","sort","mergeCustom","entries","mergeContactFacet","ids","foldContactFacet","survivorId","mergedId","includes","named","some","sources","interactions","tags","campaignIds","notes","custom","field","left","right","Math","max","min","facetsOf","doc","facets","mapOf","refusalOver","refusal","grant","supersededEntry","planContactMerge","survivorEmail","mergedEmails","alternates","email","survivorName","mergedName","assign","visibleTo","capturedBy","formIds","survivorCompanies","mergedCompanies","companyIds","companyCounts","id","map","companyId","delta","survivorConsent","mergedConsent","consentByHost","standing","survivorFacets","mergedFacets","groupId","facet","emails","Boolean","CONTACT_MERGE_REPORTS_TO_SELF","addressLine","address","sourceOf","result","contactMergePreview","options","plan","after","c","memberName","uid","stage","owner","contactName","contactId","person","pair","reportsToResult","reportsToContactId","list","flag","money","ordersCount","ltvCents","toFixed","row","label","from","rows","salutation","phone","mobilePhone","homePhone","otherPhone","fax","doNotCall","jobTitle","department","companyName","lifecycleStage","ownerUid","otherAddress","birthdate","assistantName","assistantPhone","String","customKeys","show"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmDC,GAED,SACEA,4BAA4B,EAC5BC,8BAA8B,EAC9BC,oBAAoB,EACpBC,oBAAoB,EACpBC,sBAAsB,EACtBC,wBAAwB,EAGxBC,kBAAkB,EAClBC,aAAa,EACbC,qBAAqB,EACrBC,gBAAgB,QACX,kCAAiC;AACxC,SAASC,yBAAyB,EAAEC,8BAA8B,QAAQ,6BAA4B;AACtG,SACEC,+BAA+B,EAC/BC,uBAAuB,QAClB,2CAA0C;AACjD,SAASC,gBAAgB,QAAQ,qCAAoC;AAErE,oEAAoE,GACpE,MAAMC,WAAW;AACjB,4DAA4D,GAC5D,MAAMC,YAAY;AAElB,uEAAuE,GACvE,MAAMC,aAAa;IACjB;IACA;IACA;IACA;CACD;AACD,qDAAqD,GACrD,MAAMC,gBAAgB;IACpB;IACA;IACA;CACD;AACD,uDAAuD,GACvD,MAAMC,kBAAkB;IAAC;CAAoB;AAC7C,8EAA8E,GAC9E,MAAMC,cAAc;IAAC;IAAQ;IAAa;CAAW;AAgCrD,gEAAgE,GAChE,SAASC,QAAQC,KAAc;IAC7B,IAAIA,UAAUC,aAAaD,UAAU,MAAM,OAAO;IAClD,IAAI,OAAOA,UAAU,UAAU,OAAOA,MAAME,IAAI,OAAO;IACvD,IAAIC,MAAMC,OAAO,CAACJ,QAAQ,OAAOA,MAAMK,MAAM,KAAK;IAClD,IAAI,OAAOL,UAAU,UAAU,OAAOM,OAAOC,IAAI,CAACP,OAAiBK,MAAM,KAAK;IAC9E,OAAO;AACT;AAEA,SAASG,KAAKR,KAAc;IAC1B,OAAO,OAAOA,UAAU,WAAWA,QAAQ;AAC7C;AAEA,SAASS,QAAQT,KAAc;IAC7B,OAAOG,MAAMC,OAAO,CAACJ,SACjBA,MAAMU,MAAM,CAAC,CAACC,OAAyB,OAAOA,SAAS,YAAYA,SAAS,MAC5E,EAAE;AACR;AAEA,SAASC,OAAOZ,KAAc;IAC5B,OAAO,OAAOA,UAAU,YAAYa,OAAOC,QAAQ,CAACd,SAASA,QAAQ;AACvE;AAEA,2EAA2E,GAC3E,SAASe,MAAMC,CAAU,EAAEC,CAAU,EAAEC,MAAML,OAAOM,iBAAiB;IACnE,OAAO;WAAI,IAAIC,IAAI;eAAIX,QAAQO;eAAOP,QAAQQ;SAAG;KAAE,CAACI,KAAK,CAAC,GAAGH;AAC/D;AAEA,sEAAsE,GACtE,SAASI,KAAQC,QAAW,EAAEC,MAAS;IACrC,OAAOzB,QAAQwB,YAAYC,SAASD;AACtC;AAEA,qEAAqE,GACrE,SAASE,UAAUF,QAAiB,EAAEC,MAAe;IACnD,MAAMR,IAAIR,KAAKe,UAAUrB,IAAI;IAC7B,MAAMe,IAAIT,KAAKgB,QAAQtB,IAAI;IAC3B,IAAI,CAACe,KAAKD,MAAMC,GAAG,OAAOD;IAC1B,IAAI,CAACA,GAAG,OAAOC;IACf,OAAO,GAAGD,EAAE,IAAI,EAAEC,GAAG,CAACI,KAAK,CAAC,GAAG3B;AACjC;AAEA,oDAAoD,GACpD,SAASgC,eAAeC,KAAyB;QAC3BA,cAA+BA;IAAnD,OAAO;QAACA,MAAMC,IAAI;SAAED,eAAAA,MAAME,KAAK,YAAXF,eAAe;QAAIA,MAAMG,IAAI;SAAEH,gBAAAA,MAAMI,MAAM,YAAZJ,gBAAgB;KAAG,CAACK,IAAI,CAAC;AAC9E;AAEA;;;;;CAKC,GACD,OAAO,SAASC,kBACdV,QAAmD,EACnDC,MAAiD;IAEjD,MAAMU,OAAO,IAAId;IACjB,MAAMe,MAA4B,EAAE;IACpC,KAAK,MAAMR,SAAS;WAAKJ,mBAAAA,WAAY,EAAE;WAAOC,iBAAAA,SAAU,EAAE;KAAE,CAAE;QAC5D,IAAI,CAACG,SAAS,OAAOA,UAAU,UAAU;QACzC,MAAMS,MAAMV,eAAeC;QAC3B,IAAIO,KAAKG,GAAG,CAACD,MAAM;QACnBF,KAAKI,GAAG,CAACF;QACTD,IAAII,IAAI,CAACZ;IACX;IACA,OAAOQ,IACJK,IAAI,CAAC,CAACxB,GAAGC;YAAOL,SAAwBA;eAAzB,EAACA,UAAAA,OAAOK,EAAEa,IAAI,aAAblB,UAAkB,OAAMA,WAAAA,OAAOI,EAAEc,IAAI,aAAblB,WAAkB;OAC1DS,KAAK,CAAC,GAAGtC;AACd;AAEA,wFAAwF,GACxF,SAAS0D,YAAYlB,QAAiB,EAAEC,MAAe;IACrD,MAAMR,IAAKO,YAAY,OAAOA,aAAa,WAAWA,WAAW,CAAC;IAClE,MAAMN,IAAKO,UAAU,OAAOA,WAAW,WAAWA,SAAS,CAAC;IAC5D,MAAMW,MAAW,aAAKlB;IACtB,KAAK,MAAM,CAACmB,KAAKpC,MAAM,IAAIM,OAAOoC,OAAO,CAAC1B,GAAI;QAC5C,IAAI,CAACjB,QAAQC,UAAU,CAAEoC,CAAAA,OAAOnB,CAAAA,GAAIkB,GAAG,CAACC,IAAI,GAAGpC;IACjD;IACA,OAAOmC;AACT;AAEA;;;;;;CAMC,GACD,OAAO,SAASQ,kBACdpB,QAAyB,EACzBC,MAAuB,EACvBoB,GAAqB;IAErB,MAAMT,MAAMU,iBAAiBtB,UAAUC;IACvC;;;;GAIC,GACD,IAAIoB,OAAO;QAACA,IAAIE,UAAU;QAAEF,IAAIG,QAAQ;KAAC,CAACC,QAAQ,CAACxC,KAAK2B,GAAG,CAAC,qBAAqB,IAAI;QACnFA,GAAG,CAAC,qBAAqB,GAAG;IAC9B;IACA,OAAOA;AACT;AAEA,SAASU,iBAAiBtB,QAAyB,EAAEC,MAAuB;QAkBnDP,YAAsBD;IAjB7C,IAAI,CAACQ,QAAQ,OAAO,aAAMD,mBAAAA,WAAY,CAAC;IACvC,IAAI,CAACA,UAAU,OAAO,aAAKC;IAC3B,MAAMW,MAAW,aAAKX;IACtB,oEAAoE;IACpE,KAAK,MAAM,CAACY,KAAKpC,MAAM,IAAIM,OAAOoC,OAAO,CAACnB,UAAW;QACnDY,GAAG,CAACC,IAAI,GAAGd,KAAKtB,OAAOwB,MAAM,CAACY,IAAI;IACpC;IACA,iEAAiE;IACjE,MAAMa,QAAQnD,YAAYoD,IAAI,CAAC,CAACd,MAAQ,CAACrC,QAAQwB,QAAQ,CAACa,IAAI;IAC9D,KAAK,MAAMA,OAAOtC,YAAa;QAC7B,MAAME,QAAQiD,QAAQ1B,QAAQ,CAACa,IAAI,GAAGZ,MAAM,CAACY,IAAI;QACjD,IAAIrC,QAAQC,QAAQ,OAAOmC,GAAG,CAACC,IAAI;aAC9BD,GAAG,CAACC,IAAI,GAAGpC;IAClB;IACA,IAAIuB,QAAQ,CAAC,YAAY,KAAK,QAAQC,MAAM,CAAC,YAAY,KAAK,MAAMW,GAAG,CAAC,YAAY,GAAG;IACvF,MAAMnB,IAAIO;IACV,MAAMN,IAAIO;IACVW,GAAG,CAAC,UAAU,GAAG,cAAMlB,aAAAA,EAAEkC,OAAO,YAATlC,aAAa,CAAC,IAAQD,aAAAA,EAAEmC,OAAO,YAATnC,aAAa,CAAC;IAC3DmB,GAAG,CAAC,eAAe,GAAGF,kBAAkBjB,EAAEoC,YAAY,EAAEnC,EAAEmC,YAAY;IACtE,MAAMC,OAAOtC,MAAMC,EAAEqC,IAAI,EAAEpC,EAAEoC,IAAI,EAAE5D;IACnC,IAAI4D,KAAKhD,MAAM,EAAE8B,GAAG,CAAC,OAAO,GAAGkB;IAC/B,MAAMC,cAAcvC,MAAMC,EAAEsC,WAAW,EAAErC,EAAEqC,WAAW;IACtD,IAAIA,YAAYjD,MAAM,EAAE8B,GAAG,CAAC,cAAc,GAAGmB;IAC7C,MAAMC,QAAQ9B,UAAUT,EAAEuC,KAAK,EAAEtC,EAAEsC,KAAK;IACxC,IAAIA,OAAOpB,GAAG,CAAC,QAAQ,GAAGoB;IAC1B,IAAI,CAACxD,QAAQiB,EAAEwC,MAAM,KAAK,CAACzD,QAAQkB,EAAEuC,MAAM,GAAG;QAC5CrB,GAAG,CAAC,SAAS,GAAGM,YAAYzB,EAAEwC,MAAM,EAAEvC,EAAEuC,MAAM;IAChD;IACA,KAAK,MAAMC,SAAS9D,WAAY;QAC9B,MAAM+D,OAAO9C,OAAOI,CAAC,CAACyC,MAAM;QAC5B,MAAME,QAAQ/C,OAAOK,CAAC,CAACwC,MAAM;QAC7B,IAAIC,SAAS,QAAQC,UAAU,MAAMxB,GAAG,CAACsB,MAAM,GAAG,CAACC,eAAAA,OAAQ,MAAMC,gBAAAA,QAAS;IAC5E;IACA,KAAK,MAAMF,SAAS7D,cAAe;QACjC,MAAM8D,OAAO9C,OAAOI,CAAC,CAACyC,MAAM;QAC5B,MAAME,QAAQ/C,OAAOK,CAAC,CAACwC,MAAM;QAC7B,IAAIC,SAAS,QAAQC,UAAU,MAAM;YACnCxB,GAAG,CAACsB,MAAM,GAAGG,KAAKC,GAAG,CAACH,eAAAA,OAAQ,GAAGC,gBAAAA,QAAS;QAC5C;IACF;IACA,KAAK,MAAMF,SAAS5D,gBAAiB;QACnC,MAAM6D,OAAO9C,OAAOI,CAAC,CAACyC,MAAM;QAC5B,MAAME,QAAQ/C,OAAOK,CAAC,CAACwC,MAAM;QAC7B,IAAIC,SAAS,QAAQC,UAAU,MAAMxB,GAAG,CAACsB,MAAM,GAAGG,KAAKE,GAAG,CAACJ,MAAMC;aAC5D,IAAID,SAAS,QAAQC,UAAU,MAAMxB,GAAG,CAACsB,MAAM,GAAGC,eAAAA,OAAQC;IACjE;IACA,OAAOxB;AACT;AAEA,SAAS4B,SAASC,GAAQ;IACxB,MAAMC,SAASD,GAAG,CAACpF,qBAAqB;IACxC,OAAOqF,UAAU,OAAOA,WAAW,YAAY,CAAC9D,MAAMC,OAAO,CAAC6D,UACzDA,SACD,CAAC;AACP;AAEA,SAASC,MAAMlE,KAAc;IAC3B,OAAOA,SAAS,OAAOA,UAAU,YAAY,CAACG,MAAMC,OAAO,CAACJ,SACvDA,QACD,CAAC;AACP;AAEA;;;;;;;;;;;;;;CAcC,GACD,SAASmE,YAAYC,OAAY,EAAEC,KAAU;IAC3C,MAAM1C,QAAa,aAAKyC;QAASE,iBAAiBD;;IAClD,KAAK,MAAMjC,OAAO9B,OAAOC,IAAI,CAAC8D,OAAQ;QACpC,IAAI,CAAEjC,CAAAA,OAAOT,KAAI,GAAIA,KAAK,CAACS,IAAI,GAAG;IACpC;IACA,OAAOT;AACT;AAEA;;;;;CAKC,GACD,OAAO,SAAS4C,iBACdhD,QAAa,EACbC,MAAW,EACXoB,GAAqB;QAEC1D;IAAtB,MAAMsF,iBAAgBtF,yBAAAA,sBAAsBqC,QAAQ,CAAC,QAAQ,aAAvCrC,yBAA4C;IAClE,MAAMuF,eAAexF,cAAcuC;IACnC,MAAMkD,aAAa;WACd,IAAItD,IAAI;eACNX,QAAQc,QAAQ,CAAC5C,+BAA+B;eAChD8F;SACJ;KACF,CACE/D,MAAM,CAAC,CAACiE,QAAUA,UAAUH,eAC5BnD,KAAK,CAAC,GAAG3C;IAEZ,MAAMyD,MAAW;QACf,CAACxD,+BAA+B,EAAE+F;IACpC;IACA,6EAA6E;IAC7E,MAAME,eAAepE,KAAKe,QAAQ,CAAC,OAAO,EAAErB,IAAI;IAChD,MAAM2E,aAAarE,KAAKgB,MAAM,CAAC,OAAO,EAAEtB,IAAI;IAC5C,IAAI,CAAC0E,gBAAgBC,YAAYvE,OAAOwE,MAAM,CAAC3C,KAAK3C,iBAAiBqF;IACrE,4CAA4C;IAC5C,KAAK,MAAMzC,OAAO;QAAC;QAAS;KAAc,CAAW;QACnD,IAAIrC,QAAQwB,QAAQ,CAACa,IAAI,KAAK,CAACrC,QAAQyB,MAAM,CAACY,IAAI,GAAGD,GAAG,CAACC,IAAI,GAAGZ,MAAM,CAACY,IAAI;IAC7E;IACA,2EAA2E;IAC3E,+BAA+B;IAC/B,MAAM2C,YAAYhE,MAAMQ,QAAQ,CAAC,YAAY,EAAEC,MAAM,CAAC,YAAY;IAClEW,GAAG,CAAC,YAAY,GAAG4C;IACnB,MAAMC,aAAajE,MAAMQ,QAAQ,CAAC,oBAAoB,EAAEC,MAAM,CAAC,oBAAoB;IACnF,IAAIwD,WAAW3E,MAAM,EAAE8B,GAAG,CAAC,oBAAoB,GAAG6C;IAClD,MAAMC,UAAUlE,MACdQ,QAAQ,CAACzC,uBAAuB,EAChC0C,MAAM,CAAC1C,uBAAuB,EAC9BD;IAEF,IAAIoG,QAAQ5E,MAAM,EAAE8B,GAAG,CAACrD,uBAAuB,GAAGmG;IAClD,MAAMC,oBAAoBzE,QAAQc,QAAQ,CAACnC,0BAA0B;IACrE,MAAM+F,kBAAkB1E,QAAQe,MAAM,CAACpC,0BAA0B;IACjE,MAAMgG,aAAa;WAAI,IAAIhE,IAAI;eAAI8D;eAAsBC;SAAgB;KAAE;IAC3E,IAAIC,WAAW/E,MAAM,EAAE8B,GAAG,CAAC/C,0BAA0B,GAAGgG;IACxD,MAAMC,gBAAgBH,kBACnBxE,MAAM,CAAC,CAAC4E,KAAOH,gBAAgBnC,QAAQ,CAACsC,KACxCC,GAAG,CAAC,CAACC,YAAe,CAAA;YAAEA;YAAWC,OAAO,CAAC;QAAW,CAAA;IACvD,+DAA+D;IAC/D,MAAMpC,OAAOtC,MAAMQ,QAAQ,CAAC,OAAO,EAAEC,MAAM,CAAC,OAAO;IACnD,IAAI6B,KAAKhD,MAAM,EAAE8B,GAAG,CAAC,OAAO,GAAGkB;IAC/B,MAAME,QAAQ9B,UAAUF,QAAQ,CAAC,QAAQ,EAAEC,MAAM,CAAC,QAAQ;IAC1D,IAAI+B,OAAOpB,GAAG,CAAC,QAAQ,GAAGoB;IAC1B,IAAI,CAACxD,QAAQwB,QAAQ,CAAC,SAAS,KAAK,CAACxB,QAAQyB,MAAM,CAAC,SAAS,GAAG;QAC9DW,GAAG,CAAC,SAAS,GAAGM,YAAYlB,QAAQ,CAAC,SAAS,EAAEC,MAAM,CAAC,SAAS;IAClE;IACA,0EAA0E;IAC1E,6EAA6E;IAC7E,MAAMkE,kBAAkBxB,MAAM3C,QAAQ,CAACjC,gCAAgC;IACvE,MAAMqG,gBAAgBzB,MAAM1C,MAAM,CAAClC,gCAAgC;IACnE,MAAMsG,gBAAqB,aAAKD,eAAkBD;IAClD,KAAK,MAAM,CAAC3D,QAAQ/B,MAAM,IAAIM,OAAOoC,OAAO,CAACiD,eAAgB;QAC3D,MAAMvB,UAAUF,MAAMlE;QACtB,IAAIoE,OAAO,CAAC7E,wBAAwB,KAAK,OAAO;QAChD,IAAI,CAAEwC,CAAAA,UAAU2D,eAAc,GAAI;QAClC,MAAMG,WAAW3B,MAAMwB,eAAe,CAAC3D,OAAO;QAC9C,IAAI8D,QAAQ,CAACtG,wBAAwB,KAAK,OAAO;QACjDqG,aAAa,CAAC7D,OAAO,GAAGoC,YAAYC,SAASyB;IAC/C;IACA,IAAIvF,OAAOC,IAAI,CAACqF,eAAevF,MAAM,EAAE;QACrC8B,GAAG,CAAC7C,gCAAgC,GAAGsG;IACzC;IACA,IAAIrE,QAAQ,CAAC,mBAAmB,KAAK,SAASC,MAAM,CAAC,mBAAmB,KAAK,OAAO;QAClFW,GAAG,CAAC,mBAAmB,GAAG;IAC5B,OAAO,IAAIZ,QAAQ,CAAC,mBAAmB,KAAK,QAAQC,MAAM,CAAC,mBAAmB,KAAK,MAAM;QACvFW,GAAG,CAAC,mBAAmB,GAAG;IAC5B;IACA,6EAA6E;IAC7E,MAAM2D,iBAAiB/B,SAASxC;IAChC,MAAMwE,eAAehC,SAASvC;IAC9B,MAAMyC,SAA8B,CAAC;IACrC,KAAK,MAAM,CAAC+B,SAASC,MAAM,IAAI3F,OAAOoC,OAAO,CAACqD,cAAe;QAC3D9B,MAAM,CAAC+B,QAAQ,GAAGrD,kBAAkBmD,cAAc,CAACE,QAAQ,EAAEC,OAAOrD;IACtE;IACA,yEAAyE;IACzE,IAAIA,KAAK;QACP,KAAK,MAAM,CAACoD,SAASC,MAAM,IAAI3F,OAAOoC,OAAO,CAACoD,gBAAiB;YAC7D,IAAIE,WAAW/B,UAAUgC,KAAK,CAAC,qBAAqB,KAAKrD,IAAIG,QAAQ,EAAE;YACvEkB,MAAM,CAAC+B,QAAQ,GAAGrD,kBAAkBsD,OAAOhG,WAAW2C;QACxD;IACF;IACA,IAAItC,OAAOC,IAAI,CAAC0D,QAAQ5D,MAAM,EAAE8B,GAAG,CAACvD,qBAAqB,GAAGqF;IAE5D,OAAO;QACL1C,UAAUY;QACV+D,QAAQ;YAAC1B;eAAkBE;SAAW,CAAChE,MAAM,CAACyF;QAC9C1B;QACAY;QACAN;IACF;AACF;AA+BA,sHAAsH,GACtH,OAAO,MAAMqB,gCAAgC,6CAA4C;AAEzF,SAASC,YAAYrG,KAAc;IACjC,MAAMsG,UAAUpC,MAAMlE;IACtB,OAAO;QAAC;QAAS;QAAS;QAAQ;QAAS;QAAc;KAAU,CAChEuF,GAAG,CAAC,CAACnD,MAAQ5B,KAAK8F,OAAO,CAAClE,IAAI,EAAElC,IAAI,IACpCQ,MAAM,CAACyF,SACPnE,IAAI,CAAC;AACV;AAEA,SAASuE,SAAShF,QAAgB,EAAEC,MAAc,EAAEgF,MAAc;IAChE,IAAI,CAACA,QAAQ,OAAO;IACpB,IAAIjF,YAAYC,UAAUD,aAAaC,UAAUgF,WAAWjF,YAAYiF,WAAWhF,QAAQ;QACzF,OAAO;IACT;IACA,IAAIgF,WAAWjF,YAAYA,aAAaC,QAAQ,OAAO;IACvD,OAAOgF,WAAWjF,WAAW,aAAa;AAC5C;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASkF,oBACdlF,QAAa,EACbC,MAAW,EACXwE,OAAe,EACfU,UAAsC,CAAC,CAAC;QAcrBA,qBAMCA;IAlBpB,MAAMC,OAAOpC,iBAAiBhD,UAAUC,QAAQkF,QAAQ9D,GAAG;IAC3D,MAAMgE,QAAa,aACdrF,UACAoF,KAAKpF,QAAQ;QAChB,CAAC3C,qBAAqB,EAAE,aACnBmF,SAASxC,WACT2C,MAAMyC,KAAKpF,QAAQ,CAAC3C,qBAAqB;;IAGhD,MAAMoC,IAAI7B,iBAAiBoC,UAAUyE;IACrC,MAAM/E,IAAI9B,iBAAiBqC,QAAQwE;IACnC,MAAMa,IAAI1H,iBAAiByH,OAAOZ;IAClC,MAAMc,cAAaJ,sBAAAA,QAAQI,UAAU,YAAlBJ,sBAAuB,CAACK,MAAgBA;IAC3D,MAAMC,QAAQ,CAAChH,QACb,OAAOA,UAAU,YAAYA,SAASX,iCAClCA,8BAA8B,CAACW,MAAqD,GACpF;IACN,MAAMiH,QAAQ,CAACjH,QAAoBQ,KAAKR,SAAS8G,WAAWtG,KAAKR,UAAU;IAC3E,MAAMkH,eAAcR,uBAAAA,QAAQQ,WAAW,YAAnBR,uBAAwB,CAACS,YAAsBA;IACnE,MAAMC,SAAS,CAACpH,QAAoBQ,KAAKR,SAASkH,YAAY1G,KAAKR,UAAU;IAC7E;;;;;GAKC,GACD,MAAMqH,OAAOX,QAAQ9D,GAAG,GAAG;QAAC8D,QAAQ9D,GAAG,CAACE,UAAU;QAAE4D,QAAQ9D,GAAG,CAACG,QAAQ;KAAC,GAAG,EAAE;IAC9E,MAAMuE,kBACJF,OAAOP,EAAEU,kBAAkB,KAC1B,CAAA;QAACvG,EAAEuG,kBAAkB;QAAEtG,EAAEsG,kBAAkB;KAAC,CAACrE,IAAI,CAAC,CAACoC,KAAO+B,KAAKrE,QAAQ,CAACxC,KAAK8E,QAC1Ec,gCACA,EAAC;IACP,MAAMoB,OAAO,CAACxH,QAAmBS,QAAQT,OAAOgC,IAAI,CAAC;IACrD,MAAMyF,OAAO,CAACzH,QAAoBA,UAAU,OAAO,QAAQ;IAC3D,MAAM0H,QAAQ,CAACzB;YAEoBA;eADjCA,MAAM0B,WAAW,GACb,GAAG1B,MAAM0B,WAAW,CAAC,IAAI,EAAE,AAAC,CAAA,EAAC1B,kBAAAA,MAAM2B,QAAQ,YAAd3B,kBAAkB,KAAK,GAAE,EAAG4B,OAAO,CAAC,IAAI,GACrE;;IACN,MAAMC,MAAM,CACV1F,KACA2F,OACArE,MACAC,OACA6C,SAC4B,CAAA;YAC5BpE;YACA2F;YACAxG,UAAUmC;YACVlC,QAAQmC;YACR6C;YACAwB,MAAMzB,SAAS7C,MAAMC,OAAO6C;QAC9B,CAAA;IACA,MAAMyB,OAAiC;QACrCH,IACE,QACA,QACA9I,mBAAmBuC,UAAUyE,UAC7BhH,mBAAmBwC,QAAQwE,UAC3BhH,mBAAmB4H,OAAOZ;QAE5B8B,IACE,SACA,SACA7I,cAAcsC,UAAUS,IAAI,CAAC,OAC7B/C,cAAcuC,QAAQQ,IAAI,CAAC,OAC3B2E,KAAKT,MAAM,CAAClE,IAAI,CAAC;QAEnB8F,IAAI,cAAc,cAActH,KAAKQ,EAAEkH,UAAU,GAAG1H,KAAKS,EAAEiH,UAAU,GAAG1H,KAAKqG,EAAEqB,UAAU;QACzFJ,IAAI,SAAS,SAAStH,KAAKQ,EAAEmH,KAAK,GAAG3H,KAAKS,EAAEkH,KAAK,GAAG3H,KAAKqG,EAAEsB,KAAK;QAChEL,IACE,eACA,gBACAtH,KAAKQ,EAAEoH,WAAW,GAClB5H,KAAKS,EAAEmH,WAAW,GAClB5H,KAAKqG,EAAEuB,WAAW;QAEpBN,IAAI,aAAa,cAActH,KAAKQ,EAAEqH,SAAS,GAAG7H,KAAKS,EAAEoH,SAAS,GAAG7H,KAAKqG,EAAEwB,SAAS;QACrFP,IAAI,cAAc,eAAetH,KAAKQ,EAAEsH,UAAU,GAAG9H,KAAKS,EAAEqH,UAAU,GAAG9H,KAAKqG,EAAEyB,UAAU;QAC1FR,IAAI,OAAO,OAAOtH,KAAKQ,EAAEuH,GAAG,GAAG/H,KAAKS,EAAEsH,GAAG,GAAG/H,KAAKqG,EAAE0B,GAAG;QACtDT,IAAI,aAAa,eAAeL,KAAKzG,EAAEwH,SAAS,GAAGf,KAAKxG,EAAEuH,SAAS,GAAGf,KAAKZ,EAAE2B,SAAS;QACtFV,IAAI,YAAY,aAAatH,KAAKQ,EAAEyH,QAAQ,GAAGjI,KAAKS,EAAEwH,QAAQ,GAAGjI,KAAKqG,EAAE4B,QAAQ;QAChFX,IAAI,cAAc,cAActH,KAAKQ,EAAE0H,UAAU,GAAGlI,KAAKS,EAAEyH,UAAU,GAAGlI,KAAKqG,EAAE6B,UAAU;QACzF,aACKZ,IACD,aACA,cACAV,OAAOpG,EAAEuG,kBAAkB,GAC3BH,OAAOnG,EAAEsG,kBAAkB,GAC3BD,kBAGEA,oBAAoBlB,gCAAgC;YAAE4B,MAAM;QAAgB,IAAI,CAAC;QAEvFF,IACE,WACA,WACAtH,KAAKQ,EAAE2H,WAAW,GAClBnI,KAAKS,EAAE0H,WAAW,GAClBnI,KAAKqG,EAAE8B,WAAW;QAEpBb,IACE,kBACA,SACAd,MAAMhG,EAAE4H,cAAc,GACtB5B,MAAM/F,EAAE2H,cAAc,GACtB5B,MAAMH,EAAE+B,cAAc;QAExBd,IAAI,SAAS,SAASb,MAAMjG,EAAE6H,QAAQ,GAAG5B,MAAMhG,EAAE4H,QAAQ,GAAG5B,MAAMJ,EAAEgC,QAAQ;QAC5Ef,IAAI,QAAQ,QAAQN,KAAKxG,EAAEqC,IAAI,GAAGmE,KAAKvG,EAAEoC,IAAI,GAAGmE,KAAKX,EAAExD,IAAI;QAC3DyE,IAAI,SAAS,SAAStH,KAAKQ,EAAEuC,KAAK,GAAG/C,KAAKS,EAAEsC,KAAK,GAAG/C,KAAKqG,EAAEtD,KAAK;QAChEuE,IACE,WACA,mBACAzB,YAAYrF,EAAEsF,OAAO,GACrBD,YAAYpF,EAAEqF,OAAO,GACrBD,YAAYQ,EAAEP,OAAO;QAEvBwB,IACE,gBACA,iBACAzB,YAAYrF,EAAE8H,YAAY,GAC1BzC,YAAYpF,EAAE6H,YAAY,GAC1BzC,YAAYQ,EAAEiC,YAAY;QAE5BhB,IAAI,aAAa,aAAatH,KAAKQ,EAAE+H,SAAS,GAAGvI,KAAKS,EAAE8H,SAAS,GAAGvI,KAAKqG,EAAEkC,SAAS;QACpFjB,IACE,aACA,aACAtH,KAAKQ,EAAEgI,aAAa,GACpBxI,KAAKS,EAAE+H,aAAa,GACpBxI,KAAKqG,EAAEmC,aAAa;QAEtBlB,IACE,kBACA,mBACAtH,KAAKQ,EAAEiI,cAAc,GACrBzI,KAAKS,EAAEgI,cAAc,GACrBzI,KAAKqG,EAAEoC,cAAc;QAEvBnB,IAAI,UAAU,UAAUJ,MAAM1G,IAAI0G,MAAMzG,IAAIyG,MAAMb;QAClDiB,IACE,YACA,oBACA9G,EAAEoC,YAAY,CAAC/C,MAAM,GAAG6I,OAAOlI,EAAEoC,YAAY,CAAC/C,MAAM,IAAI,IACxDY,EAAEmC,YAAY,CAAC/C,MAAM,GAAG6I,OAAOjI,EAAEmC,YAAY,CAAC/C,MAAM,IAAI,IACxDwG,EAAEzD,YAAY,CAAC/C,MAAM,GAAG6I,OAAOrC,EAAEzD,YAAY,CAAC/C,MAAM,IAAI;KAE3D;IACD,MAAM8I,aAAa;WACd,IAAI/H,IAAI;eAAId,OAAOC,IAAI,CAAC2D,MAAMlD,EAAEwC,MAAM;eAAOlD,OAAOC,IAAI,CAAC2D,MAAMjD,EAAEuC,MAAM;SAAG;KAC9E,CAAChB,IAAI;IACN,KAAK,MAAMJ,OAAO+G,WAAY;QAC5B,MAAMC,OAAO,CAACnD;YACZ,MAAMjG,QAAQkE,MAAM+B,MAAMzC,MAAM,CAAC,CAACpB,IAAI;YACtC,OAAOpC,UAAU,QAAQA,UAAUC,YAAY,KAAKiJ,OAAOlJ;QAC7D;QACAiI,KAAK1F,IAAI,CAACuF,IAAI,CAAC,OAAO,EAAE1F,KAAK,EAAEA,KAAKgH,KAAKpI,IAAIoI,KAAKnI,IAAImI,KAAKvC;IAC7D;IACA,OAAOoB;AACT"}
|
|
@@ -71,6 +71,22 @@ export interface ContactRecord {
|
|
|
71
71
|
jobTitle: string;
|
|
72
72
|
/** This holder's lead source label (AGL-3298), `''` for none. */
|
|
73
73
|
leadSource: string;
|
|
74
|
+
salutation: string;
|
|
75
|
+
firstName: string;
|
|
76
|
+
lastName: string;
|
|
77
|
+
department: string;
|
|
78
|
+
mobilePhone: string;
|
|
79
|
+
homePhone: string;
|
|
80
|
+
otherPhone: string;
|
|
81
|
+
fax: string;
|
|
82
|
+
/** `YYYY-MM-DD`. */
|
|
83
|
+
birthdate: string;
|
|
84
|
+
assistantName: string;
|
|
85
|
+
assistantPhone: string;
|
|
86
|
+
/** The contact this person reports to, by id. */
|
|
87
|
+
reportsToContactId: string;
|
|
88
|
+
otherAddress: AglynPostalAddress | null;
|
|
89
|
+
doNotCall: boolean;
|
|
74
90
|
companyName: string;
|
|
75
91
|
companyId: string;
|
|
76
92
|
/**
|
|
@@ -81,6 +97,7 @@ export interface ContactRecord {
|
|
|
81
97
|
* already holds, and nothing renders it.
|
|
82
98
|
*/
|
|
83
99
|
companyLink: Aglyn.ContactCompanyLinkState;
|
|
100
|
+
/** The mailing address. */
|
|
84
101
|
address: AglynPostalAddress | null;
|
|
85
102
|
/** This holder's custom field values, keyed by definition key (AGL-2601). */
|
|
86
103
|
custom?: Record<string, Aglyn.ContactCustomValue>;
|
|
@@ -21,7 +21,7 @@ import { crmShareChipFor } from "./crm-sharing.js";
|
|
|
21
21
|
* the file and the organization-level list flatten a contact one way.
|
|
22
22
|
*/ export { contactPrimaryGroup, NO_HOLDER_GROUP } from "./contact-holder.js";
|
|
23
23
|
/** A document off the wire, flattened through one group's facet. */ export function contactRecordFromDoc(row, group) {
|
|
24
|
-
var _row_$id, _facet_name, _facet_tags, _facet_notes, _facet_ltvCents, _facet_ordersCount, _facet_phone, _facet_jobTitle, _facet_leadSource, _facet_companyName, _facet_companyId, _facet_address, _facet_custom, _facet_mediaIds, _facet_ownerUid;
|
|
24
|
+
var _row_$id, _facet_name, _facet_tags, _facet_notes, _facet_ltvCents, _facet_ordersCount, _facet_phone, _facet_jobTitle, _facet_leadSource, _facet_salutation, _facet_firstName, _facet_lastName, _facet_department, _facet_mobilePhone, _facet_homePhone, _facet_otherPhone, _facet_fax, _facet_birthdate, _facet_assistantName, _facet_assistantPhone, _facet_reportsToContactId, _facet_otherAddress, _facet_companyName, _facet_companyId, _facet_address, _facet_custom, _facet_mediaIds, _facet_ownerUid;
|
|
25
25
|
const facet = Aglyn.readContactFacet(row, group.groupId);
|
|
26
26
|
return {
|
|
27
27
|
$id: String((_row_$id = row['$id']) != null ? _row_$id : ''),
|
|
@@ -43,6 +43,20 @@ import { crmShareChipFor } from "./crm-sharing.js";
|
|
|
43
43
|
phone: (_facet_phone = facet.phone) != null ? _facet_phone : '',
|
|
44
44
|
jobTitle: (_facet_jobTitle = facet.jobTitle) != null ? _facet_jobTitle : '',
|
|
45
45
|
leadSource: (_facet_leadSource = facet.leadSource) != null ? _facet_leadSource : '',
|
|
46
|
+
salutation: (_facet_salutation = facet.salutation) != null ? _facet_salutation : '',
|
|
47
|
+
firstName: (_facet_firstName = facet.firstName) != null ? _facet_firstName : '',
|
|
48
|
+
lastName: (_facet_lastName = facet.lastName) != null ? _facet_lastName : '',
|
|
49
|
+
department: (_facet_department = facet.department) != null ? _facet_department : '',
|
|
50
|
+
mobilePhone: (_facet_mobilePhone = facet.mobilePhone) != null ? _facet_mobilePhone : '',
|
|
51
|
+
homePhone: (_facet_homePhone = facet.homePhone) != null ? _facet_homePhone : '',
|
|
52
|
+
otherPhone: (_facet_otherPhone = facet.otherPhone) != null ? _facet_otherPhone : '',
|
|
53
|
+
fax: (_facet_fax = facet.fax) != null ? _facet_fax : '',
|
|
54
|
+
birthdate: (_facet_birthdate = facet.birthdate) != null ? _facet_birthdate : '',
|
|
55
|
+
assistantName: (_facet_assistantName = facet.assistantName) != null ? _facet_assistantName : '',
|
|
56
|
+
assistantPhone: (_facet_assistantPhone = facet.assistantPhone) != null ? _facet_assistantPhone : '',
|
|
57
|
+
reportsToContactId: (_facet_reportsToContactId = facet.reportsToContactId) != null ? _facet_reportsToContactId : '',
|
|
58
|
+
otherAddress: (_facet_otherAddress = facet.otherAddress) != null ? _facet_otherAddress : null,
|
|
59
|
+
doNotCall: facet.doNotCall === true,
|
|
46
60
|
companyName: (_facet_companyName = facet.companyName) != null ? _facet_companyName : '',
|
|
47
61
|
companyId: (_facet_companyId = facet.companyId) != null ? _facet_companyId : '',
|
|
48
62
|
companyLink: Aglyn.readContactCompanyLink(row, group.groupId),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/contact-record.ts"],"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\nimport * as Aglyn from '@aglyn/aglyn'\nimport { type CrmShareChip, crmShareChipFor } from './crm-sharing'\nimport type {\n AglynPostalAddress,\n ConsentGroup,\n ContactInteraction,\n ContactLifecycleStage,\n ContactSource,\n} from '@aglyn/aglyn'\n\n/**\n * One contact, as the VIEWING GROUP may see it (AGL-2596).\n *\n * A contact document is shared by every site in the org, and almost nothing\n * on it is legitimately shared: the notes, tags, timeline, profile and\n * commercial figures are the holder's own records and live in that holder's\n * facet. This is the row after the facet read — flat, so the list, the\n * record page and the export can render fields by name without any of them\n * reaching into `facets.{groupId}` for itself. One projection rather than a\n * facet read at every field is what keeps a surface from ever showing\n * another holder's records: there is no field on this shape that came from\n * the top of the document except the two that ARE shared, the address and\n * the canonical name.\n */\nexport interface ContactRecord {\n $id: string\n /**\n * The facet this row was flattened through — the viewing group under a\n * site, the person's primary holder at the org level (AGL-2630). A write\n * that edits the row goes back to the same facet, so a surface that has\n * only the flat record can still address the holder's own fields.\n */\n groupId: string\n /**\n * A site of that holder's group — the viewing site under a site, and at\n * the organization level the site the primary holder was resolved from —\n * which is where a stage move for this row is made and announced. `''` for\n * a row no site holds.\n */\n holderHostId: string\n /** Every site that has captured this person, sorted — the org-level \"Known by\". */\n capturedByHostIds: string[]\n /** The shared identity and the dedupe key. */\n email: string\n /** The other addresses a merge folded into this record (AGL-2625). */\n alternateEmails: string[]\n /** What THIS holder sees — their own override, or the canonical name. */\n name: string\n /** The canonical name: the identity of last resort, shared by every holder. */\n canonicalName: string\n /** This holder's own name for the person, when they have set one. */\n nameOverride: string\n sources: Partial<Record<ContactSource, true>>\n /** This holder's timeline, narrowed to the sites the group covers. */\n interactions: ContactInteraction[]\n tags: string[]\n notes: string\n campaignIds: string[]\n ltvCents: number\n ordersCount: number\n phone: string\n jobTitle: string\n /** This holder's lead source label (AGL-3298), `''` for none. */\n leadSource: string\n companyName: string\n companyId: string\n /**\n * What linking this person to a company has to know — this holder's link,\n * the shared mirror and the other holders' ids — so the properties card\n * and the bulk bar can plan a link from the row without the document. Not\n * a display field: the mirror is an index every reader of the document\n * already holds, and nothing renders it.\n */\n companyLink: Aglyn.ContactCompanyLinkState\n address: AglynPostalAddress | null\n /** This holder's custom field values, keyed by definition key (AGL-2601). */\n custom?: Record<string, Aglyn.ContactCustomValue>\n /** This holder's attached org-library files, by media id (AGL-2662). */\n mediaIds?: string[]\n ownerUid: string\n /** Empty when the holder has not placed the person in the funnel. */\n lifecycleStage: ContactLifecycleStage | ''\n /**\n * When the person last opened or clicked one of this holder's campaigns\n * (AGL-2616), or `null` when they never have since the stamp shipped.\n */\n lastEmailEngagementAtMs: number | null\n /**\n * When the earliest open task against this person is due (AGL-2661), or\n * `null` when nothing is scheduled. SHARED, not the holder's: a task is\n * filed against the contact document, and every holder reading the row\n * sees the same next step.\n */\n nextTaskAtMs: number | null\n /**\n * The last verdict on the address (AGL-3245) — bounced, blocked,\n * unsubscribed, do-not-contact — or `null` when nothing is known. SHARED\n * like the address it is about.\n */\n emailState: Aglyn.EmailState | null\n /**\n * Why the viewing group sees this person when the answer is a share\n * (AGL-3336) — `null` when it holds them, and at the organization level.\n */\n shareChip?: CrmShareChip | null\n createdAt?: unknown\n updatedAt?: unknown\n}\n\n/*\n * Which holder a cross-site reader sees one contact through (AGL-2630) —\n * `contact-holder.ts`, shared with the server's whole-collection export so\n * the file and the organization-level list flatten a contact one way.\n */\nexport { contactPrimaryGroup, NO_HOLDER_GROUP } from './contact-holder'\n\n/** A document off the wire, flattened through one group's facet. */\nexport function contactRecordFromDoc(\n row: Record<string, any>,\n group: ConsentGroup,\n): ContactRecord {\n const facet = Aglyn.readContactFacet(row, group.groupId)\n return {\n $id: String(row['$id'] ?? ''),\n groupId: group.groupId,\n holderHostId: group.hostId,\n capturedByHostIds: Aglyn.contactCaptureHostIds(row),\n email: typeof row['email'] === 'string' ? row['email'] : '',\n alternateEmails: Array.isArray(row[Aglyn.CONTACT_ALTERNATE_EMAILS_FIELD])\n ? (row[Aglyn.CONTACT_ALTERNATE_EMAILS_FIELD] as unknown[]).filter(\n (email): email is string => typeof email === 'string',\n )\n : [],\n name: Aglyn.contactDisplayName(row, group.groupId),\n canonicalName: typeof row['name'] === 'string' ? row['name'] : '',\n nameOverride: facet.name ?? '',\n sources: facet.sources,\n interactions: Aglyn.interactionsForGroup(facet.interactions, group.hostIds),\n tags: facet.tags ?? [],\n notes: facet.notes ?? '',\n campaignIds: Aglyn.readContactContainerIds(row, group.groupId, 'campaign'),\n ltvCents: facet.ltvCents ?? 0,\n ordersCount: facet.ordersCount ?? 0,\n phone: facet.phone ?? '',\n jobTitle: facet.jobTitle ?? '',\n leadSource: facet.leadSource ?? '',\n companyName: facet.companyName ?? '',\n companyId: facet.companyId ?? '',\n companyLink: Aglyn.readContactCompanyLink(row, group.groupId),\n address: facet.address ?? null,\n custom: facet.custom ?? {},\n mediaIds: facet.mediaIds ?? [],\n ownerUid: facet.ownerUid ?? '',\n lifecycleStage: Aglyn.isContactLifecycleStage(facet.lifecycleStage)\n ? facet.lifecycleStage\n : '',\n lastEmailEngagementAtMs:\n typeof facet.lastEmailEngagementAtMs === 'number' &&\n Number.isFinite(facet.lastEmailEngagementAtMs) &&\n facet.lastEmailEngagementAtMs > 0\n ? facet.lastEmailEngagementAtMs\n : null,\n nextTaskAtMs: Aglyn.readNextTaskAtMs(row as { nextTaskAtMs?: unknown }),\n emailState: Aglyn.readEmailState(row),\n shareChip: crmShareChipFor(row, group.hostIds),\n createdAt: row['createdAt'],\n updatedAt: row['updatedAt'],\n }\n}\n\n/**\n * Tags as typed — comma-separated — into the shape the facet stores:\n * lower-cased, trimmed, deduplicated and capped, so a tag typed on the\n * record page and one typed in the create drawer are the same tag to the\n * segment filter that matches on them.\n */\nexport function parseContactTags(input: string): string[] {\n return [\n ...new Set(\n input\n .split(',')\n .map((tag) => tag.trim().toLowerCase().slice(0, 40))\n .filter(Boolean)\n .slice(0, 20),\n ),\n ]\n}\n"],"names":["Aglyn","crmShareChipFor","contactPrimaryGroup","NO_HOLDER_GROUP","contactRecordFromDoc","row","group","facet","readContactFacet","groupId","$id","String","holderHostId","hostId","capturedByHostIds","contactCaptureHostIds","email","alternateEmails","Array","isArray","CONTACT_ALTERNATE_EMAILS_FIELD","filter","name","contactDisplayName","canonicalName","nameOverride","sources","interactions","interactionsForGroup","hostIds","tags","notes","campaignIds","readContactContainerIds","ltvCents","ordersCount","phone","jobTitle","leadSource","companyName","companyId","companyLink","readContactCompanyLink","address","custom","mediaIds","ownerUid","lifecycleStage","isContactLifecycleStage","lastEmailEngagementAtMs","Number","isFinite","nextTaskAtMs","readNextTaskAtMs","emailState","readEmailState","shareChip","createdAt","updatedAt","parseContactTags","input","Set","split","map","tag","trim","toLowerCase","slice","Boolean"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,YAAYA,WAAW,eAAc;AACrC,SAA4BC,eAAe,QAAQ,mBAAe;AA4GlE;;;;CAIC,GACD,SAASC,mBAAmB,EAAEC,eAAe,QAAQ,sBAAkB;AAEvE,kEAAkE,GAClE,OAAO,SAASC,qBACdC,GAAwB,EACxBC,KAAmB;QAILD,UAYEE,aAGRA,aACCA,cAEGA,iBACGA,oBACNA,cACGA,iBACEA,mBACCA,oBACFA,kBAEFA,gBACDA,eACEA,iBACAA;IA/BZ,MAAMA,QAAQP,MAAMQ,gBAAgB,CAACH,KAAKC,MAAMG,OAAO;IACvD,OAAO;QACLC,KAAKC,QAAON,WAAAA,GAAG,CAAC,MAAM,YAAVA,WAAc;QAC1BI,SAASH,MAAMG,OAAO;QACtBG,cAAcN,MAAMO,MAAM;QAC1BC,mBAAmBd,MAAMe,qBAAqB,CAACV;QAC/CW,OAAO,OAAOX,GAAG,CAAC,QAAQ,KAAK,WAAWA,GAAG,CAAC,QAAQ,GAAG;QACzDY,iBAAiBC,MAAMC,OAAO,CAACd,GAAG,CAACL,MAAMoB,8BAA8B,CAAC,IACpE,AAACf,GAAG,CAACL,MAAMoB,8BAA8B,CAAC,CAAeC,MAAM,CAC7D,CAACL,QAA2B,OAAOA,UAAU,YAE/C,EAAE;QACNM,MAAMtB,MAAMuB,kBAAkB,CAAClB,KAAKC,MAAMG,OAAO;QACjDe,eAAe,OAAOnB,GAAG,CAAC,OAAO,KAAK,WAAWA,GAAG,CAAC,OAAO,GAAG;QAC/DoB,YAAY,GAAElB,cAAAA,MAAMe,IAAI,YAAVf,cAAc;QAC5BmB,SAASnB,MAAMmB,OAAO;QACtBC,cAAc3B,MAAM4B,oBAAoB,CAACrB,MAAMoB,YAAY,EAAErB,MAAMuB,OAAO;QAC1EC,IAAI,GAAEvB,cAAAA,MAAMuB,IAAI,YAAVvB,cAAc,EAAE;QACtBwB,KAAK,GAAExB,eAAAA,MAAMwB,KAAK,YAAXxB,eAAe;QACtByB,aAAahC,MAAMiC,uBAAuB,CAAC5B,KAAKC,MAAMG,OAAO,EAAE;QAC/DyB,QAAQ,GAAE3B,kBAAAA,MAAM2B,QAAQ,YAAd3B,kBAAkB;QAC5B4B,WAAW,GAAE5B,qBAAAA,MAAM4B,WAAW,YAAjB5B,qBAAqB;QAClC6B,KAAK,GAAE7B,eAAAA,MAAM6B,KAAK,YAAX7B,eAAe;QACtB8B,QAAQ,GAAE9B,kBAAAA,MAAM8B,QAAQ,YAAd9B,kBAAkB;QAC5B+B,UAAU,GAAE/B,oBAAAA,MAAM+B,UAAU,YAAhB/B,oBAAoB;QAChCgC,WAAW,GAAEhC,qBAAAA,MAAMgC,WAAW,YAAjBhC,qBAAqB;QAClCiC,SAAS,GAAEjC,mBAAAA,MAAMiC,SAAS,YAAfjC,mBAAmB;QAC9BkC,aAAazC,MAAM0C,sBAAsB,CAACrC,KAAKC,MAAMG,OAAO;QAC5DkC,OAAO,GAAEpC,iBAAAA,MAAMoC,OAAO,YAAbpC,iBAAiB;QAC1BqC,MAAM,GAAErC,gBAAAA,MAAMqC,MAAM,YAAZrC,gBAAgB,CAAC;QACzBsC,QAAQ,GAAEtC,kBAAAA,MAAMsC,QAAQ,YAAdtC,kBAAkB,EAAE;QAC9BuC,QAAQ,GAAEvC,kBAAAA,MAAMuC,QAAQ,YAAdvC,kBAAkB;QAC5BwC,gBAAgB/C,MAAMgD,uBAAuB,CAACzC,MAAMwC,cAAc,IAC9DxC,MAAMwC,cAAc,GACpB;QACJE,yBACE,OAAO1C,MAAM0C,uBAAuB,KAAK,YACzCC,OAAOC,QAAQ,CAAC5C,MAAM0C,uBAAuB,KAC7C1C,MAAM0C,uBAAuB,GAAG,IAC5B1C,MAAM0C,uBAAuB,GAC7B;QACNG,cAAcpD,MAAMqD,gBAAgB,CAAChD;QACrCiD,YAAYtD,MAAMuD,cAAc,CAAClD;QACjCmD,WAAWvD,gBAAgBI,KAAKC,MAAMuB,OAAO;QAC7C4B,WAAWpD,GAAG,CAAC,YAAY;QAC3BqD,WAAWrD,GAAG,CAAC,YAAY;IAC7B;AACF;AAEA;;;;;CAKC,GACD,OAAO,SAASsD,iBAAiBC,KAAa;IAC5C,OAAO;WACF,IAAIC,IACLD,MACGE,KAAK,CAAC,KACNC,GAAG,CAAC,CAACC,MAAQA,IAAIC,IAAI,GAAGC,WAAW,GAAGC,KAAK,CAAC,GAAG,KAC/C9C,MAAM,CAAC+C,SACPD,KAAK,CAAC,GAAG;KAEf;AACH"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/contact-record.ts"],"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\nimport * as Aglyn from '@aglyn/aglyn'\nimport { type CrmShareChip, crmShareChipFor } from './crm-sharing'\nimport type {\n AglynPostalAddress,\n ConsentGroup,\n ContactInteraction,\n ContactLifecycleStage,\n ContactSource,\n} from '@aglyn/aglyn'\n\n/**\n * One contact, as the VIEWING GROUP may see it (AGL-2596).\n *\n * A contact document is shared by every site in the org, and almost nothing\n * on it is legitimately shared: the notes, tags, timeline, profile and\n * commercial figures are the holder's own records and live in that holder's\n * facet. This is the row after the facet read — flat, so the list, the\n * record page and the export can render fields by name without any of them\n * reaching into `facets.{groupId}` for itself. One projection rather than a\n * facet read at every field is what keeps a surface from ever showing\n * another holder's records: there is no field on this shape that came from\n * the top of the document except the two that ARE shared, the address and\n * the canonical name.\n */\nexport interface ContactRecord {\n $id: string\n /**\n * The facet this row was flattened through — the viewing group under a\n * site, the person's primary holder at the org level (AGL-2630). A write\n * that edits the row goes back to the same facet, so a surface that has\n * only the flat record can still address the holder's own fields.\n */\n groupId: string\n /**\n * A site of that holder's group — the viewing site under a site, and at\n * the organization level the site the primary holder was resolved from —\n * which is where a stage move for this row is made and announced. `''` for\n * a row no site holds.\n */\n holderHostId: string\n /** Every site that has captured this person, sorted — the org-level \"Known by\". */\n capturedByHostIds: string[]\n /** The shared identity and the dedupe key. */\n email: string\n /** The other addresses a merge folded into this record (AGL-2625). */\n alternateEmails: string[]\n /** What THIS holder sees — their own override, or the canonical name. */\n name: string\n /** The canonical name: the identity of last resort, shared by every holder. */\n canonicalName: string\n /** This holder's own name for the person, when they have set one. */\n nameOverride: string\n sources: Partial<Record<ContactSource, true>>\n /** This holder's timeline, narrowed to the sites the group covers. */\n interactions: ContactInteraction[]\n tags: string[]\n notes: string\n campaignIds: string[]\n ltvCents: number\n ordersCount: number\n phone: string\n jobTitle: string\n /** This holder's lead source label (AGL-3298), `''` for none. */\n leadSource: string\n /*\n * Salesforce's standard contact fields (AGL-3515), this holder's, each\n * `''` (or `null`, `false`) for none — see `ContactFacet`.\n */\n salutation: string\n firstName: string\n lastName: string\n department: string\n mobilePhone: string\n homePhone: string\n otherPhone: string\n fax: string\n /** `YYYY-MM-DD`. */\n birthdate: string\n assistantName: string\n assistantPhone: string\n /** The contact this person reports to, by id. */\n reportsToContactId: string\n otherAddress: AglynPostalAddress | null\n doNotCall: boolean\n companyName: string\n companyId: string\n /**\n * What linking this person to a company has to know — this holder's link,\n * the shared mirror and the other holders' ids — so the properties card\n * and the bulk bar can plan a link from the row without the document. Not\n * a display field: the mirror is an index every reader of the document\n * already holds, and nothing renders it.\n */\n companyLink: Aglyn.ContactCompanyLinkState\n /** The mailing address. */\n address: AglynPostalAddress | null\n /** This holder's custom field values, keyed by definition key (AGL-2601). */\n custom?: Record<string, Aglyn.ContactCustomValue>\n /** This holder's attached org-library files, by media id (AGL-2662). */\n mediaIds?: string[]\n ownerUid: string\n /** Empty when the holder has not placed the person in the funnel. */\n lifecycleStage: ContactLifecycleStage | ''\n /**\n * When the person last opened or clicked one of this holder's campaigns\n * (AGL-2616), or `null` when they never have since the stamp shipped.\n */\n lastEmailEngagementAtMs: number | null\n /**\n * When the earliest open task against this person is due (AGL-2661), or\n * `null` when nothing is scheduled. SHARED, not the holder's: a task is\n * filed against the contact document, and every holder reading the row\n * sees the same next step.\n */\n nextTaskAtMs: number | null\n /**\n * The last verdict on the address (AGL-3245) — bounced, blocked,\n * unsubscribed, do-not-contact — or `null` when nothing is known. SHARED\n * like the address it is about.\n */\n emailState: Aglyn.EmailState | null\n /**\n * Why the viewing group sees this person when the answer is a share\n * (AGL-3336) — `null` when it holds them, and at the organization level.\n */\n shareChip?: CrmShareChip | null\n createdAt?: unknown\n updatedAt?: unknown\n}\n\n/*\n * Which holder a cross-site reader sees one contact through (AGL-2630) —\n * `contact-holder.ts`, shared with the server's whole-collection export so\n * the file and the organization-level list flatten a contact one way.\n */\nexport { contactPrimaryGroup, NO_HOLDER_GROUP } from './contact-holder'\n\n/** A document off the wire, flattened through one group's facet. */\nexport function contactRecordFromDoc(\n row: Record<string, any>,\n group: ConsentGroup,\n): ContactRecord {\n const facet = Aglyn.readContactFacet(row, group.groupId)\n return {\n $id: String(row['$id'] ?? ''),\n groupId: group.groupId,\n holderHostId: group.hostId,\n capturedByHostIds: Aglyn.contactCaptureHostIds(row),\n email: typeof row['email'] === 'string' ? row['email'] : '',\n alternateEmails: Array.isArray(row[Aglyn.CONTACT_ALTERNATE_EMAILS_FIELD])\n ? (row[Aglyn.CONTACT_ALTERNATE_EMAILS_FIELD] as unknown[]).filter(\n (email): email is string => typeof email === 'string',\n )\n : [],\n name: Aglyn.contactDisplayName(row, group.groupId),\n canonicalName: typeof row['name'] === 'string' ? row['name'] : '',\n nameOverride: facet.name ?? '',\n sources: facet.sources,\n interactions: Aglyn.interactionsForGroup(facet.interactions, group.hostIds),\n tags: facet.tags ?? [],\n notes: facet.notes ?? '',\n campaignIds: Aglyn.readContactContainerIds(row, group.groupId, 'campaign'),\n ltvCents: facet.ltvCents ?? 0,\n ordersCount: facet.ordersCount ?? 0,\n phone: facet.phone ?? '',\n jobTitle: facet.jobTitle ?? '',\n leadSource: facet.leadSource ?? '',\n salutation: facet.salutation ?? '',\n firstName: facet.firstName ?? '',\n lastName: facet.lastName ?? '',\n department: facet.department ?? '',\n mobilePhone: facet.mobilePhone ?? '',\n homePhone: facet.homePhone ?? '',\n otherPhone: facet.otherPhone ?? '',\n fax: facet.fax ?? '',\n birthdate: facet.birthdate ?? '',\n assistantName: facet.assistantName ?? '',\n assistantPhone: facet.assistantPhone ?? '',\n reportsToContactId: facet.reportsToContactId ?? '',\n otherAddress: facet.otherAddress ?? null,\n doNotCall: facet.doNotCall === true,\n companyName: facet.companyName ?? '',\n companyId: facet.companyId ?? '',\n companyLink: Aglyn.readContactCompanyLink(row, group.groupId),\n address: facet.address ?? null,\n custom: facet.custom ?? {},\n mediaIds: facet.mediaIds ?? [],\n ownerUid: facet.ownerUid ?? '',\n lifecycleStage: Aglyn.isContactLifecycleStage(facet.lifecycleStage)\n ? facet.lifecycleStage\n : '',\n lastEmailEngagementAtMs:\n typeof facet.lastEmailEngagementAtMs === 'number' &&\n Number.isFinite(facet.lastEmailEngagementAtMs) &&\n facet.lastEmailEngagementAtMs > 0\n ? facet.lastEmailEngagementAtMs\n : null,\n nextTaskAtMs: Aglyn.readNextTaskAtMs(row as { nextTaskAtMs?: unknown }),\n emailState: Aglyn.readEmailState(row),\n shareChip: crmShareChipFor(row, group.hostIds),\n createdAt: row['createdAt'],\n updatedAt: row['updatedAt'],\n }\n}\n\n/**\n * Tags as typed — comma-separated — into the shape the facet stores:\n * lower-cased, trimmed, deduplicated and capped, so a tag typed on the\n * record page and one typed in the create drawer are the same tag to the\n * segment filter that matches on them.\n */\nexport function parseContactTags(input: string): string[] {\n return [\n ...new Set(\n input\n .split(',')\n .map((tag) => tag.trim().toLowerCase().slice(0, 40))\n .filter(Boolean)\n .slice(0, 20),\n ),\n ]\n}\n"],"names":["Aglyn","crmShareChipFor","contactPrimaryGroup","NO_HOLDER_GROUP","contactRecordFromDoc","row","group","facet","readContactFacet","groupId","$id","String","holderHostId","hostId","capturedByHostIds","contactCaptureHostIds","email","alternateEmails","Array","isArray","CONTACT_ALTERNATE_EMAILS_FIELD","filter","name","contactDisplayName","canonicalName","nameOverride","sources","interactions","interactionsForGroup","hostIds","tags","notes","campaignIds","readContactContainerIds","ltvCents","ordersCount","phone","jobTitle","leadSource","salutation","firstName","lastName","department","mobilePhone","homePhone","otherPhone","fax","birthdate","assistantName","assistantPhone","reportsToContactId","otherAddress","doNotCall","companyName","companyId","companyLink","readContactCompanyLink","address","custom","mediaIds","ownerUid","lifecycleStage","isContactLifecycleStage","lastEmailEngagementAtMs","Number","isFinite","nextTaskAtMs","readNextTaskAtMs","emailState","readEmailState","shareChip","createdAt","updatedAt","parseContactTags","input","Set","split","map","tag","trim","toLowerCase","slice","Boolean"],"mappings":"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"}
|
|
@@ -41,11 +41,40 @@ export declare const CRM_CONTACT_UPDATE_MAX = 200;
|
|
|
41
41
|
* A text field sent as `''` is CLEARED — removed from the facet — except
|
|
42
42
|
* `notes`, which are stored as typed. A list replaces the stored list;
|
|
43
43
|
* `addTag` and `removeTag` change one tag and leave the rest as the server
|
|
44
|
-
* holds them.
|
|
44
|
+
* holds them. An address or the reports-to sent as `null` clears it, and
|
|
45
|
+
* `doNotCall: false` clears the flag.
|
|
45
46
|
*/
|
|
46
47
|
export interface ContactUpdateFields {
|
|
47
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* This holder's own name for the person. The canonical name is never
|
|
50
|
+
* written. While the holder keeps a first or last name, the name is their
|
|
51
|
+
* composition, and a `name` that differs from it is refused (AGL-3515).
|
|
52
|
+
*/
|
|
48
53
|
name?: string;
|
|
54
|
+
/** One of the org's salutation values, by label, or `''` to clear (AGL-3515). */
|
|
55
|
+
salutation?: string;
|
|
56
|
+
/** Given and family names; the facet's `name` follows them — see `ContactFacet.firstName`. */
|
|
57
|
+
firstName?: string;
|
|
58
|
+
lastName?: string;
|
|
59
|
+
department?: string;
|
|
60
|
+
/** As typed; stored as E.164, and refused when it cannot be read as a number. */
|
|
61
|
+
mobilePhone?: string;
|
|
62
|
+
homePhone?: string;
|
|
63
|
+
otherPhone?: string;
|
|
64
|
+
fax?: string;
|
|
65
|
+
assistantPhone?: string;
|
|
66
|
+
/** `YYYY-MM-DD`, not in the future, or `''` to clear. */
|
|
67
|
+
birthdate?: string;
|
|
68
|
+
assistantName?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Another contact the holder can see, or `null` to clear. Refused when it
|
|
71
|
+
* is the contact itself or when its own chain leads back to the contact.
|
|
72
|
+
*/
|
|
73
|
+
reportsToContactId?: string | null;
|
|
74
|
+
/** The second postal address — `address` is the mailing one. */
|
|
75
|
+
otherAddress?: AglynPostalAddress | null;
|
|
76
|
+
/** The person asked not to be phoned; `false` clears the flag. */
|
|
77
|
+
doNotCall?: boolean;
|
|
49
78
|
/** As typed; stored as E.164, and refused when it cannot be read as a number. */
|
|
50
79
|
phone?: string;
|
|
51
80
|
jobTitle?: string;
|
|
@@ -32,10 +32,24 @@
|
|
|
32
32
|
*/ export const CRM_CONTACT_UPDATE_MAX = 200;
|
|
33
33
|
/** Every field the route reads. A body naming any other is refused, not trimmed. */ export const CONTACT_UPDATE_FIELDS = [
|
|
34
34
|
'name',
|
|
35
|
+
'salutation',
|
|
36
|
+
'firstName',
|
|
37
|
+
'lastName',
|
|
35
38
|
'phone',
|
|
39
|
+
'mobilePhone',
|
|
40
|
+
'homePhone',
|
|
41
|
+
'otherPhone',
|
|
42
|
+
'fax',
|
|
36
43
|
'jobTitle',
|
|
44
|
+
'department',
|
|
45
|
+
'birthdate',
|
|
46
|
+
'assistantName',
|
|
47
|
+
'assistantPhone',
|
|
48
|
+
'reportsToContactId',
|
|
37
49
|
'leadSource',
|
|
38
50
|
'address',
|
|
51
|
+
'otherAddress',
|
|
52
|
+
'doNotCall',
|
|
39
53
|
'notes',
|
|
40
54
|
'tags',
|
|
41
55
|
'addTag',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/contact-update.ts"],"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\nimport type { AglynPostalAddress, ContactCustomValue } from '@aglyn/aglyn'\n\n/**\n * THE CONTACT PROFILE ROUTE'S CONTRACT (AGL-2804), in one module both halves\n * import, so the field the console sends and the field the handler reads\n * cannot drift apart.\n *\n * `crm/contact-update` is the console's one writer of a contact's facets.\n * The Firestore rules cannot tell one field of a holder's facet from another\n * — the holder is a map key a rule cannot address — so a client update may\n * not change a facet at all beyond letting a holder go, and every edit the\n * console makes to one arrives here: the record page's profile, custom\n * fields, files and campaign filing, the company page's link, and the bulk\n * bar's tags, owner and company. A lifecycle stage has a route of its own,\n * `crm/contact-stage`, because moving it is an event.\n */\n\n/**\n * The most contacts one request names — the contacts list's window, which is\n * as much as a selection reaches. A larger selection is sent in pieces.\n */\nexport const CRM_CONTACT_UPDATE_MAX = 200\n\n/**\n * What one request sets, for ONE holder: the viewing site's group under a\n * site, and at the organization level each contact's own primary holder.\n *\n * A text field sent as `''` is CLEARED — removed from the facet — except\n * `notes`, which are stored as typed. A list replaces the stored list;\n * `addTag` and `removeTag` change one tag and leave the rest as the server\n * holds them.\n */\nexport interface ContactUpdateFields {\n
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/contact-update.ts"],"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\nimport type { AglynPostalAddress, ContactCustomValue } from '@aglyn/aglyn'\n\n/**\n * THE CONTACT PROFILE ROUTE'S CONTRACT (AGL-2804), in one module both halves\n * import, so the field the console sends and the field the handler reads\n * cannot drift apart.\n *\n * `crm/contact-update` is the console's one writer of a contact's facets.\n * The Firestore rules cannot tell one field of a holder's facet from another\n * — the holder is a map key a rule cannot address — so a client update may\n * not change a facet at all beyond letting a holder go, and every edit the\n * console makes to one arrives here: the record page's profile, custom\n * fields, files and campaign filing, the company page's link, and the bulk\n * bar's tags, owner and company. A lifecycle stage has a route of its own,\n * `crm/contact-stage`, because moving it is an event.\n */\n\n/**\n * The most contacts one request names — the contacts list's window, which is\n * as much as a selection reaches. A larger selection is sent in pieces.\n */\nexport const CRM_CONTACT_UPDATE_MAX = 200\n\n/**\n * What one request sets, for ONE holder: the viewing site's group under a\n * site, and at the organization level each contact's own primary holder.\n *\n * A text field sent as `''` is CLEARED — removed from the facet — except\n * `notes`, which are stored as typed. A list replaces the stored list;\n * `addTag` and `removeTag` change one tag and leave the rest as the server\n * holds them. An address or the reports-to sent as `null` clears it, and\n * `doNotCall: false` clears the flag.\n */\nexport interface ContactUpdateFields {\n /**\n * This holder's own name for the person. The canonical name is never\n * written. While the holder keeps a first or last name, the name is their\n * composition, and a `name` that differs from it is refused (AGL-3515).\n */\n name?: string\n /** One of the org's salutation values, by label, or `''` to clear (AGL-3515). */\n salutation?: string\n /** Given and family names; the facet's `name` follows them — see `ContactFacet.firstName`. */\n firstName?: string\n lastName?: string\n department?: string\n /** As typed; stored as E.164, and refused when it cannot be read as a number. */\n mobilePhone?: string\n homePhone?: string\n otherPhone?: string\n fax?: string\n assistantPhone?: string\n /** `YYYY-MM-DD`, not in the future, or `''` to clear. */\n birthdate?: string\n assistantName?: string\n /**\n * Another contact the holder can see, or `null` to clear. Refused when it\n * is the contact itself or when its own chain leads back to the contact.\n */\n reportsToContactId?: string | null\n /** The second postal address — `address` is the mailing one. */\n otherAddress?: AglynPostalAddress | null\n /** The person asked not to be phoned; `false` clears the flag. */\n doNotCall?: boolean\n /** As typed; stored as E.164, and refused when it cannot be read as a number. */\n phone?: string\n jobTitle?: string\n /**\n * One of the org's lead source values, by label (AGL-3298), or `''` to\n * clear. Refused when it is not an active value and not the one the\n * contact already holds.\n */\n leadSource?: string\n address?: AglynPostalAddress | null\n notes?: string\n tags?: string[]\n addTag?: string\n removeTag?: string\n campaignIds?: string[]\n /** A member of the organization, or `''` for nobody. */\n ownerUid?: string\n /** The company to file the person under, or `null` to unlink them. */\n companyId?: string | null\n /**\n * The company's label on the record. Left out beside `companyId`, the\n * linked company's own name is written, and an unlink clears it.\n */\n companyName?: string\n /** Only the keys that changed, each judged against its definition; `null` clears. */\n custom?: Record<string, ContactCustomValue>\n mediaIds?: string[]\n}\n\n/** Every field the route reads. A body naming any other is refused, not trimmed. */\nexport const CONTACT_UPDATE_FIELDS: ReadonlyArray<keyof ContactUpdateFields> = [\n 'name',\n 'salutation',\n 'firstName',\n 'lastName',\n 'phone',\n 'mobilePhone',\n 'homePhone',\n 'otherPhone',\n 'fax',\n 'jobTitle',\n 'department',\n 'birthdate',\n 'assistantName',\n 'assistantPhone',\n 'reportsToContactId',\n 'leadSource',\n 'address',\n 'otherAddress',\n 'doNotCall',\n 'notes',\n 'tags',\n 'addTag',\n 'removeTag',\n 'campaignIds',\n 'ownerUid',\n 'companyId',\n 'companyName',\n 'custom',\n 'mediaIds',\n]\n\nexport interface ContactUpdateRequest {\n /** The mounted site; at the organization level the record's own site, or nothing. */\n hostId?: string | null\n /** The organization, which makes the call the route's organization variant. */\n orgId?: string\n contactIds: string[]\n set: ContactUpdateFields\n}\n\n/** One contact's answer: saved, or refused with a sentence written for the reader. */\nexport type ContactUpdateOutcome =\n | { contactId: string; ok: true }\n | { contactId: string; ok: false; error: string }\n\nexport interface ContactUpdateResponse {\n ok: true\n results: ContactUpdateOutcome[]\n}\n"],"names":["CRM_CONTACT_UPDATE_MAX","CONTACT_UPDATE_FIELDS"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAID;;;;;;;;;;;;;CAaC,GAED;;;CAGC,GACD,OAAO,MAAMA,yBAAyB,IAAG;AAwEzC,kFAAkF,GAClF,OAAO,MAAMC,wBAAkE;IAC7E;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAAA"}
|
|
@@ -89,3 +89,8 @@ export declare function crmSelectField(column: string, path?: string): ListFilte
|
|
|
89
89
|
export declare function crmClientListFields(collection: CrmListCollection, record: Readonly<Record<string, unknown>>, patch: Readonly<Record<string, unknown>>, fields: readonly string[]): Record<string, unknown>;
|
|
90
90
|
/** The search fields every CRM record carries — what a client edit of searched text rewrites. */
|
|
91
91
|
export declare const CRM_CLIENT_SEARCH_FIELDS: readonly ["searchTokens", "scopedSearchTokens"];
|
|
92
|
+
/**
|
|
93
|
+
* The list fields a deal's contact roles are found by (AGL-3521) — what a
|
|
94
|
+
* client write of `contactRoles` or `contactId` rewrites beside them.
|
|
95
|
+
*/
|
|
96
|
+
export declare const CRM_DEAL_CONTACT_ROLE_LIST_FIELDS: readonly ["contactRoleContactIds", "scopedContactRoleContactIds", "contactRoleKeys"];
|
|
@@ -178,5 +178,13 @@ const sameClause = (a, b)=>a.field === b.field && a.op === b.op && a.value === b
|
|
|
178
178
|
'searchTokens',
|
|
179
179
|
'scopedSearchTokens'
|
|
180
180
|
];
|
|
181
|
+
/**
|
|
182
|
+
* The list fields a deal's contact roles are found by (AGL-3521) — what a
|
|
183
|
+
* client write of `contactRoles` or `contactId` rewrites beside them.
|
|
184
|
+
*/ export const CRM_DEAL_CONTACT_ROLE_LIST_FIELDS = [
|
|
185
|
+
'contactRoleContactIds',
|
|
186
|
+
'scopedContactRoleContactIds',
|
|
187
|
+
'contactRoleKeys'
|
|
188
|
+
];
|
|
181
189
|
|
|
182
190
|
//# sourceMappingURL=crm-list-query.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/crm-list-query.ts"],"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\nimport {\n CRM_SCOPED_SEARCH_TOKENS_FIELD,\n CRM_SEARCH_TOKENS_FIELD,\n type CrmListCollection,\n crmListFields,\n} from '@aglyn/aglyn'\nimport { scopedSearch } from '@aglyn/aglyn/app-utils/name-search'\nimport type { ListFilterField, ListFilterRequest } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport type { ListFilterOption } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { listQueryRefusals } from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport type {\n ListQueryDeclaration,\n ListQueryFilter,\n ListQueryPlan,\n} from '@aglyn/shared-ui-jsx/const/list-query-plan'\n\n/*\n * THE CRM LISTS ON THEIR QUERIES (AGL-3321).\n *\n * Every CRM list — Leads, Contacts, Companies, Deals, Tasks — declares its\n * filterable fields, its one order and its search as a\n * `ListQueryDeclaration`, and `planListQuery` puts every clause and the\n * search word on ONE Firestore query paged by `useListQuery`. What a query\n * cannot hold is refused by name (`ListQueryNotices`), never matched over\n * the rows a page happened to load.\n *\n * This module is what the five share: the search every CRM record carries\n * (`searchTokens`, and `scopedSearchTokens` under a site — see\n * `crmListFields` in `@aglyn/aglyn`), the scope clause as the plan's base,\n * a stored clause translated to the one its query asks, and the refusals\n * read back through the clause the reader set.\n */\n\n/** Every CRM list's quick search: the tokens every writer stamps. */\nexport const CRM_LIST_SEARCH: NonNullable<ListQueryDeclaration['search']> = {\n tokensPath: CRM_SEARCH_TOKENS_FIELD,\n scoped: scopedSearch(CRM_SCOPED_SEARCH_TOKENS_FIELD),\n}\n\n/**\n * The scope clause as the plan's base: `visibleTo array-contains-any` over\n * the reader's tokens under a site, nothing at the organization level\n * (`null` tokens), where an org-wide member's listeners carry none.\n */\nexport function crmListBase(visibleTo: readonly string[] | null): ListQueryFilter[] {\n return visibleTo ? [{ path: 'visibleTo', op: 'array-contains-any', value: [...visibleTo] }] : []\n}\n\n/** The base's shape, for `listQueryIndexes`: the scope clause is an array clause. */\nexport const CRM_LIST_BASE_INDEX = [{ path: 'visibleTo', array: true }] as const\n\n/**\n * The declaration a reader may run.\n *\n * Under a site the search folds into the scope clause (`search.scoped`),\n * and a query without `visibleTo` is one the rules can prove only for an\n * ORG-WIDE member — `canReadScoped()` short-circuits on it, and for anybody\n * else inspects `visibleTo`, which the folded query no longer constrains.\n * A member whose reach is some sites keeps the scope clause, and the plan\n * refuses their search by name rather than sending a query the rules deny.\n */\nexport function crmListDeclarationFor(\n declaration: ListQueryDeclaration,\n foldsScope: boolean,\n): ListQueryDeclaration {\n if (foldsScope || !declaration.search?.scoped) return declaration\n return { ...declaration, search: { tokensPath: declaration.search.tokensPath } }\n}\n\n/*------------------------------------------\n * WHAT A LIST STORES, AND WHAT IT ASKS.\n *\n * A saved view stores clauses in the grid's grammar — \"Status is Open\",\n * \"Owner is Dana\", \"Lead source is none\" — and some of them name no stored\n * field one query can ask as it is: Open is two statuses, an owner under a\n * site is that holder's facet key, a lead source is compared by its key. A\n * list translates each stored clause into the clause its query asks, plans\n * those, and reads the plan's refusals back through the stored clause, so\n * the notice names what the reader set (\"Status is Open is not applied\")\n * and never the field it was translated to.\n *-----------------------------------------*/\n\n/** How one stored clause is asked: a query clause, or why it cannot be. */\nexport type CrmClauseAsked = ListFilterRequest | { refused: string }\n\nexport interface CrmAskedClauses {\n /** What the plan is asked, in the order the reader set them. */\n clauses: ListFilterRequest[]\n /** The stored clause each asked one stands for, index for index. */\n origins: ListFilterRequest[]\n /** Stored clauses refused before the plan saw them, with why. */\n refused: Array<{ clause: ListFilterRequest; reason: string }>\n}\n\n/** Every stored clause through `ask`, keeping where each came from. */\nexport function crmAskClauses(\n stored: readonly ListFilterRequest[],\n ask: (clause: ListFilterRequest) => CrmClauseAsked,\n): CrmAskedClauses {\n const clauses: ListFilterRequest[] = []\n const origins: ListFilterRequest[] = []\n const refused: CrmAskedClauses['refused'] = []\n for (const clause of stored) {\n const asked = ask(clause)\n if ('refused' in asked) {\n refused.push({ clause, reason: asked.refused })\n } else {\n clauses.push(asked)\n origins.push(clause)\n }\n }\n return { clauses, origins, refused }\n}\n\nconst sameClause = (a: ListFilterRequest, b: ListFilterRequest): boolean =>\n a.field === b.field && a.op === b.op && a.value === b.value\n\n/**\n * The plan's refusals and the translation's, each named by the clause the\n * reader SET, as `ListQueryNotices` takes them — through the platform's one\n * wording (`listQueryRefusals`), so a notice and its chip read alike.\n */\nexport function crmQueryRefusals(\n plan: Pick<ListQueryPlan, 'refused'>,\n asked: CrmAskedClauses,\n context: {\n fields: readonly ListFilterField[]\n headers?: Readonly<Record<string, string>>\n options?: Readonly<Record<string, readonly ListFilterOption[]>>\n },\n): Array<{ label: string; reason: string }> {\n const planned = plan.refused.map((entry) => {\n if (entry.clause === 'search') return entry\n const clause = entry.clause\n const at = asked.clauses.findIndex((candidate) => sameClause(candidate, clause))\n return { clause: at >= 0 ? asked.origins[at] : clause, reason: entry.reason }\n })\n return listQueryRefusals([...asked.refused, ...planned], context)\n}\n\n/** The comma-separated values of an `isAnyOf`, or the one value of anything else. */\nexport function crmClauseValues(clause: Pick<ListFilterRequest, 'op' | 'value'>): string[] {\n const raw = clause.op === 'isAnyOf' ? clause.value.split(',') : [clause.value]\n return [...new Set(raw.map((value) => value.trim()).filter(Boolean))]\n}\n\n/** One value as `equals`, several as `isAnyOf` — the shape the plan takes a list in. */\nexport function crmAnyOf(field: string, values: readonly string[]): CrmClauseAsked {\n const unique = [...new Set(values)]\n if (!unique.length) return { refused: 'no value yet' }\n return unique.length === 1\n ? { field, op: 'equals', value: unique[0] }\n : { field, op: 'isAnyOf', value: unique.join(',') }\n}\n\n/** A `select` field: equality and \"any of\" over stored values, nothing else. */\nexport function crmSelectField(column: string, path: string = column): ListFilterField {\n return { column, kind: 'exact', path, operators: ['equals', 'isAnyOf'] }\n}\n\n/**\n * The list fields a CLIENT edit writes beside its patch (AGL-3321): the\n * record as the listener holds it with the patch laid over — a `null` or a\n * sentinel in the patch reads as the field removed — through\n * `crmListFields`, narrowed to `fields`. A client never writes a field the\n * rules keep for the server (a lead's `emailStatus`), which is why the\n * caller names the ones it may.\n */\nexport function crmClientListFields(\n collection: CrmListCollection,\n record: Readonly<Record<string, unknown>>,\n patch: Readonly<Record<string, unknown>>,\n fields: readonly string[],\n): Record<string, unknown> {\n const merged: Record<string, unknown> = { ...record }\n for (const [key, value] of Object.entries(patch)) {\n // A dotted path is a map entry (`custom.tier`), never a searched field.\n if (key.includes('.')) continue\n merged[key] = value === null || isSentinel(value) ? undefined : value\n }\n const computed = crmListFields(collection, merged)\n return Object.fromEntries(fields.filter((field) => field in computed).map((field) => [field, computed[field]]))\n}\n\n/** A web-SDK `FieldValue` — `deleteField()`, the one a client edit of these fields sends. */\nconst isSentinel = (value: unknown): boolean =>\n typeof value === 'object' &&\n value !== null &&\n !Array.isArray(value) &&\n !(value instanceof Date) &&\n typeof (value as { isEqual?: unknown }).isEqual === 'function'\n\n/** The search fields every CRM record carries — what a client edit of searched text rewrites. */\nexport const CRM_CLIENT_SEARCH_FIELDS = ['searchTokens', 'scopedSearchTokens'] as const\n\n"],"names":["CRM_SCOPED_SEARCH_TOKENS_FIELD","CRM_SEARCH_TOKENS_FIELD","crmListFields","scopedSearch","listQueryRefusals","CRM_LIST_SEARCH","tokensPath","scoped","crmListBase","visibleTo","path","op","value","CRM_LIST_BASE_INDEX","array","crmListDeclarationFor","declaration","foldsScope","search","crmAskClauses","stored","ask","clauses","origins","refused","clause","asked","push","reason","sameClause","a","b","field","crmQueryRefusals","plan","context","planned","map","entry","at","findIndex","candidate","crmClauseValues","raw","split","Set","trim","filter","Boolean","crmAnyOf","values","unique","length","join","crmSelectField","column","kind","operators","crmClientListFields","collection","record","patch","fields","merged","key","Object","entries","includes","isSentinel","undefined","computed","fromEntries","Array","isArray","Date","isEqual","CRM_CLIENT_SEARCH_FIELDS"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/crm-list-query.ts"],"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\nimport {\n CRM_SCOPED_SEARCH_TOKENS_FIELD,\n CRM_SEARCH_TOKENS_FIELD,\n type CrmListCollection,\n crmListFields,\n} from '@aglyn/aglyn'\nimport { scopedSearch } from '@aglyn/aglyn/app-utils/name-search'\nimport type { ListFilterField, ListFilterRequest } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport type { ListFilterOption } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { listQueryRefusals } from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport type {\n ListQueryDeclaration,\n ListQueryFilter,\n ListQueryPlan,\n} from '@aglyn/shared-ui-jsx/const/list-query-plan'\n\n/*\n * THE CRM LISTS ON THEIR QUERIES (AGL-3321).\n *\n * Every CRM list — Leads, Contacts, Companies, Deals, Tasks — declares its\n * filterable fields, its one order and its search as a\n * `ListQueryDeclaration`, and `planListQuery` puts every clause and the\n * search word on ONE Firestore query paged by `useListQuery`. What a query\n * cannot hold is refused by name (`ListQueryNotices`), never matched over\n * the rows a page happened to load.\n *\n * This module is what the five share: the search every CRM record carries\n * (`searchTokens`, and `scopedSearchTokens` under a site — see\n * `crmListFields` in `@aglyn/aglyn`), the scope clause as the plan's base,\n * a stored clause translated to the one its query asks, and the refusals\n * read back through the clause the reader set.\n */\n\n/** Every CRM list's quick search: the tokens every writer stamps. */\nexport const CRM_LIST_SEARCH: NonNullable<ListQueryDeclaration['search']> = {\n tokensPath: CRM_SEARCH_TOKENS_FIELD,\n scoped: scopedSearch(CRM_SCOPED_SEARCH_TOKENS_FIELD),\n}\n\n/**\n * The scope clause as the plan's base: `visibleTo array-contains-any` over\n * the reader's tokens under a site, nothing at the organization level\n * (`null` tokens), where an org-wide member's listeners carry none.\n */\nexport function crmListBase(visibleTo: readonly string[] | null): ListQueryFilter[] {\n return visibleTo ? [{ path: 'visibleTo', op: 'array-contains-any', value: [...visibleTo] }] : []\n}\n\n/** The base's shape, for `listQueryIndexes`: the scope clause is an array clause. */\nexport const CRM_LIST_BASE_INDEX = [{ path: 'visibleTo', array: true }] as const\n\n/**\n * The declaration a reader may run.\n *\n * Under a site the search folds into the scope clause (`search.scoped`),\n * and a query without `visibleTo` is one the rules can prove only for an\n * ORG-WIDE member — `canReadScoped()` short-circuits on it, and for anybody\n * else inspects `visibleTo`, which the folded query no longer constrains.\n * A member whose reach is some sites keeps the scope clause, and the plan\n * refuses their search by name rather than sending a query the rules deny.\n */\nexport function crmListDeclarationFor(\n declaration: ListQueryDeclaration,\n foldsScope: boolean,\n): ListQueryDeclaration {\n if (foldsScope || !declaration.search?.scoped) return declaration\n return { ...declaration, search: { tokensPath: declaration.search.tokensPath } }\n}\n\n/*------------------------------------------\n * WHAT A LIST STORES, AND WHAT IT ASKS.\n *\n * A saved view stores clauses in the grid's grammar — \"Status is Open\",\n * \"Owner is Dana\", \"Lead source is none\" — and some of them name no stored\n * field one query can ask as it is: Open is two statuses, an owner under a\n * site is that holder's facet key, a lead source is compared by its key. A\n * list translates each stored clause into the clause its query asks, plans\n * those, and reads the plan's refusals back through the stored clause, so\n * the notice names what the reader set (\"Status is Open is not applied\")\n * and never the field it was translated to.\n *-----------------------------------------*/\n\n/** How one stored clause is asked: a query clause, or why it cannot be. */\nexport type CrmClauseAsked = ListFilterRequest | { refused: string }\n\nexport interface CrmAskedClauses {\n /** What the plan is asked, in the order the reader set them. */\n clauses: ListFilterRequest[]\n /** The stored clause each asked one stands for, index for index. */\n origins: ListFilterRequest[]\n /** Stored clauses refused before the plan saw them, with why. */\n refused: Array<{ clause: ListFilterRequest; reason: string }>\n}\n\n/** Every stored clause through `ask`, keeping where each came from. */\nexport function crmAskClauses(\n stored: readonly ListFilterRequest[],\n ask: (clause: ListFilterRequest) => CrmClauseAsked,\n): CrmAskedClauses {\n const clauses: ListFilterRequest[] = []\n const origins: ListFilterRequest[] = []\n const refused: CrmAskedClauses['refused'] = []\n for (const clause of stored) {\n const asked = ask(clause)\n if ('refused' in asked) {\n refused.push({ clause, reason: asked.refused })\n } else {\n clauses.push(asked)\n origins.push(clause)\n }\n }\n return { clauses, origins, refused }\n}\n\nconst sameClause = (a: ListFilterRequest, b: ListFilterRequest): boolean =>\n a.field === b.field && a.op === b.op && a.value === b.value\n\n/**\n * The plan's refusals and the translation's, each named by the clause the\n * reader SET, as `ListQueryNotices` takes them — through the platform's one\n * wording (`listQueryRefusals`), so a notice and its chip read alike.\n */\nexport function crmQueryRefusals(\n plan: Pick<ListQueryPlan, 'refused'>,\n asked: CrmAskedClauses,\n context: {\n fields: readonly ListFilterField[]\n headers?: Readonly<Record<string, string>>\n options?: Readonly<Record<string, readonly ListFilterOption[]>>\n },\n): Array<{ label: string; reason: string }> {\n const planned = plan.refused.map((entry) => {\n if (entry.clause === 'search') return entry\n const clause = entry.clause\n const at = asked.clauses.findIndex((candidate) => sameClause(candidate, clause))\n return { clause: at >= 0 ? asked.origins[at] : clause, reason: entry.reason }\n })\n return listQueryRefusals([...asked.refused, ...planned], context)\n}\n\n/** The comma-separated values of an `isAnyOf`, or the one value of anything else. */\nexport function crmClauseValues(clause: Pick<ListFilterRequest, 'op' | 'value'>): string[] {\n const raw = clause.op === 'isAnyOf' ? clause.value.split(',') : [clause.value]\n return [...new Set(raw.map((value) => value.trim()).filter(Boolean))]\n}\n\n/** One value as `equals`, several as `isAnyOf` — the shape the plan takes a list in. */\nexport function crmAnyOf(field: string, values: readonly string[]): CrmClauseAsked {\n const unique = [...new Set(values)]\n if (!unique.length) return { refused: 'no value yet' }\n return unique.length === 1\n ? { field, op: 'equals', value: unique[0] }\n : { field, op: 'isAnyOf', value: unique.join(',') }\n}\n\n/** A `select` field: equality and \"any of\" over stored values, nothing else. */\nexport function crmSelectField(column: string, path: string = column): ListFilterField {\n return { column, kind: 'exact', path, operators: ['equals', 'isAnyOf'] }\n}\n\n/**\n * The list fields a CLIENT edit writes beside its patch (AGL-3321): the\n * record as the listener holds it with the patch laid over — a `null` or a\n * sentinel in the patch reads as the field removed — through\n * `crmListFields`, narrowed to `fields`. A client never writes a field the\n * rules keep for the server (a lead's `emailStatus`), which is why the\n * caller names the ones it may.\n */\nexport function crmClientListFields(\n collection: CrmListCollection,\n record: Readonly<Record<string, unknown>>,\n patch: Readonly<Record<string, unknown>>,\n fields: readonly string[],\n): Record<string, unknown> {\n const merged: Record<string, unknown> = { ...record }\n for (const [key, value] of Object.entries(patch)) {\n // A dotted path is a map entry (`custom.tier`), never a searched field.\n if (key.includes('.')) continue\n merged[key] = value === null || isSentinel(value) ? undefined : value\n }\n const computed = crmListFields(collection, merged)\n return Object.fromEntries(fields.filter((field) => field in computed).map((field) => [field, computed[field]]))\n}\n\n/** A web-SDK `FieldValue` — `deleteField()`, the one a client edit of these fields sends. */\nconst isSentinel = (value: unknown): boolean =>\n typeof value === 'object' &&\n value !== null &&\n !Array.isArray(value) &&\n !(value instanceof Date) &&\n typeof (value as { isEqual?: unknown }).isEqual === 'function'\n\n/** The search fields every CRM record carries — what a client edit of searched text rewrites. */\nexport const CRM_CLIENT_SEARCH_FIELDS = ['searchTokens', 'scopedSearchTokens'] as const\n\n\n/**\n * The list fields a deal's contact roles are found by (AGL-3521) — what a\n * client write of `contactRoles` or `contactId` rewrites beside them.\n */\nexport const CRM_DEAL_CONTACT_ROLE_LIST_FIELDS = [\n 'contactRoleContactIds',\n 'scopedContactRoleContactIds',\n 'contactRoleKeys',\n] as const\n"],"names":["CRM_SCOPED_SEARCH_TOKENS_FIELD","CRM_SEARCH_TOKENS_FIELD","crmListFields","scopedSearch","listQueryRefusals","CRM_LIST_SEARCH","tokensPath","scoped","crmListBase","visibleTo","path","op","value","CRM_LIST_BASE_INDEX","array","crmListDeclarationFor","declaration","foldsScope","search","crmAskClauses","stored","ask","clauses","origins","refused","clause","asked","push","reason","sameClause","a","b","field","crmQueryRefusals","plan","context","planned","map","entry","at","findIndex","candidate","crmClauseValues","raw","split","Set","trim","filter","Boolean","crmAnyOf","values","unique","length","join","crmSelectField","column","kind","operators","crmClientListFields","collection","record","patch","fields","merged","key","Object","entries","includes","isSentinel","undefined","computed","fromEntries","Array","isArray","Date","isEqual","CRM_CLIENT_SEARCH_FIELDS","CRM_DEAL_CONTACT_ROLE_LIST_FIELDS"],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { InteractionRecipe, InteractionRecipeInput } from '@aglyn/aglyn/plugin-manager/interaction-recipes';
|
|
18
|
+
export declare const CRM_ACTION_RECIPE_IDS: readonly ["welcomeNewLead", "followUpWonDeal", "reengageStaleLead", "tagByForm"];
|
|
19
|
+
export type CrmActionRecipeId = (typeof CRM_ACTION_RECIPE_IDS)[number];
|
|
20
|
+
/** What a recipe is handed before it builds — today only the form `tagByForm` reads. */
|
|
21
|
+
export type CrmActionRecipeInput = InteractionRecipeInput;
|
|
22
|
+
export interface CrmActionRecipe extends InteractionRecipe {
|
|
23
|
+
id: CrmActionRecipeId;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* How long the stale-lead recipe holds before it decides a lead has gone
|
|
27
|
+
* quiet. A week: long enough that a rep who is working the lead has had a
|
|
28
|
+
* chance to move its stage, short enough that a lead nobody touched is
|
|
29
|
+
* still warm when the reminder lands.
|
|
30
|
+
*/
|
|
31
|
+
export declare const STALE_LEAD_WAIT_MINUTES: number;
|
|
32
|
+
/**
|
|
33
|
+
* The tag a form's captures get: the form's own name, cut to the tag cap.
|
|
34
|
+
* The name is what the person calls the form, so it is the tag they would
|
|
35
|
+
* have typed; the editor is open to change it before anything is saved.
|
|
36
|
+
*/
|
|
37
|
+
export declare function crmRecipeTagForForm(formName: string): string;
|
|
38
|
+
export declare const CRM_ACTION_RECIPES: readonly CrmActionRecipe[];
|
|
39
|
+
/** The recipe with this id, or null for a string that names none. */
|
|
40
|
+
export declare function crmActionRecipe(id: unknown): CrmActionRecipe | null;
|