@aglyn/plugins-crm 1.0.0-beta.217 → 1.0.0-beta.219
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/package.json +14 -13
- package/src/index.d.ts +1 -0
- package/src/index.js.map +1 -1
- package/src/lib/components/companies-bulk-bar.js +12 -4
- package/src/lib/components/companies-bulk-bar.js.map +1 -1
- package/src/lib/components/company-detail-page.js +11 -0
- package/src/lib/components/company-detail-page.js.map +1 -1
- package/src/lib/components/company-edit-drawer.js +12 -2
- package/src/lib/components/company-edit-drawer.js.map +1 -1
- package/src/lib/components/company-import-drawer.d.ts +1 -1
- package/src/lib/components/company-picker.js +10 -3
- package/src/lib/components/company-picker.js.map +1 -1
- package/src/lib/components/contact-detail-page.d.ts +4 -4
- package/src/lib/components/contact-detail-page.js +50 -53
- package/src/lib/components/contact-detail-page.js.map +1 -1
- package/src/lib/components/contact-list-columns.d.ts +3 -0
- package/src/lib/components/contact-list-columns.js +21 -5
- package/src/lib/components/contact-list-columns.js.map +1 -1
- package/src/lib/components/contact-merge-dialog.js +2 -1
- package/src/lib/components/contact-merge-dialog.js.map +1 -1
- package/src/lib/components/contact-timeline-card.js +4 -3
- package/src/lib/components/contact-timeline-card.js.map +1 -1
- package/src/lib/components/contacts-bulk-bar.d.ts +3 -3
- package/src/lib/components/contacts-bulk-bar.js +24 -51
- package/src/lib/components/contacts-bulk-bar.js.map +1 -1
- package/src/lib/components/contacts-section.js +9 -3
- package/src/lib/components/contacts-section.js.map +1 -1
- package/src/lib/components/crm-export-all-button.d.ts +1 -1
- package/src/lib/components/crm-export-all-button.js.map +1 -1
- package/src/lib/components/crm-send-email-dialog.js +9 -6
- package/src/lib/components/crm-send-email-dialog.js.map +1 -1
- package/src/lib/components/csv-import-drawer.js +2 -2
- package/src/lib/components/csv-import-drawer.js.map +1 -1
- package/src/lib/components/deal-detail-page.js +18 -2
- package/src/lib/components/deal-detail-page.js.map +1 -1
- package/src/lib/components/deal-edit-drawer.js +10 -0
- package/src/lib/components/deal-edit-drawer.js.map +1 -1
- package/src/lib/components/deals-bulk-bar.js +13 -5
- package/src/lib/components/deals-bulk-bar.js.map +1 -1
- package/src/lib/components/lead-campaigns-card.js +6 -0
- package/src/lib/components/lead-campaigns-card.js.map +1 -1
- package/src/lib/components/lead-detail-page.js +25 -5
- package/src/lib/components/lead-detail-page.js.map +1 -1
- package/src/lib/components/lead-properties-card.js +6 -0
- package/src/lib/components/lead-properties-card.js.map +1 -1
- package/src/lib/components/lead-surfaces-note.js +4 -5
- package/src/lib/components/lead-surfaces-note.js.map +1 -1
- package/src/lib/components/lead-unqualify-dialog.js +6 -0
- package/src/lib/components/lead-unqualify-dialog.js.map +1 -1
- package/src/lib/components/leads-bulk-bar.js +28 -3
- package/src/lib/components/leads-bulk-bar.js.map +1 -1
- package/src/lib/components/leads-section.js +36 -7
- package/src/lib/components/leads-section.js.map +1 -1
- package/src/lib/components/org-lead-surfaces-note.js +6 -2
- package/src/lib/components/org-lead-surfaces-note.js.map +1 -1
- package/src/lib/components/recipes-card.js +9 -4
- package/src/lib/components/recipes-card.js.map +1 -1
- package/src/lib/components/record-sharing-card.d.ts +57 -0
- package/src/lib/components/record-sharing-card.js +244 -0
- package/src/lib/components/record-sharing-card.js.map +1 -0
- package/src/lib/components/settings-section.js +7 -0
- package/src/lib/components/settings-section.js.map +1 -1
- package/src/lib/components/share-records-dialog.d.ts +28 -0
- package/src/lib/components/share-records-dialog.js +184 -0
- package/src/lib/components/share-records-dialog.js.map +1 -0
- package/src/lib/components/sharing-rule-drawer.d.ts +37 -0
- package/src/lib/components/sharing-rule-drawer.js +374 -0
- package/src/lib/components/sharing-rule-drawer.js.map +1 -0
- package/src/lib/components/sharing-rules-card.d.ts +24 -0
- package/src/lib/components/sharing-rules-card.js +287 -0
- package/src/lib/components/sharing-rules-card.js.map +1 -0
- package/src/lib/components/use-crm-api.d.ts +1 -1
- package/src/lib/components/use-crm-api.js.map +1 -1
- package/src/lib/components/use-site-route-context.d.ts +25 -0
- package/src/lib/components/use-site-route-context.js +58 -0
- package/src/lib/components/use-site-route-context.js.map +1 -0
- package/src/lib/constants/api-routes.d.ts +31 -0
- package/src/lib/constants/api-routes.js +27 -1
- package/src/lib/constants/api-routes.js.map +1 -1
- package/src/lib/declarations.console-server.d.ts +37 -0
- package/src/lib/declarations.console-server.js +103 -0
- package/src/lib/declarations.console-server.js.map +1 -0
- package/src/lib/declarations.server.d.ts +6 -0
- package/src/lib/declarations.server.js +33 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/hooks/use-contact-remove.d.ts +35 -0
- package/src/lib/hooks/use-contact-remove.js +77 -0
- package/src/lib/hooks/use-contact-remove.js.map +1 -0
- package/src/lib/hooks/use-crm-sharing.d.ts +30 -0
- package/src/lib/hooks/use-crm-sharing.js +109 -0
- package/src/lib/hooks/use-crm-sharing.js.map +1 -0
- package/src/lib/model/companies-csv.d.ts +1 -1
- package/src/lib/model/companies-csv.js +2 -2
- package/src/lib/model/companies-csv.js.map +1 -1
- package/src/lib/model/contact-holder.d.ts +68 -0
- package/src/lib/model/contact-holder.js +129 -0
- package/src/lib/model/contact-holder.js.map +1 -0
- package/src/lib/model/contact-interaction-links.d.ts +46 -0
- package/src/lib/model/contact-interaction-links.js +65 -0
- package/src/lib/model/contact-interaction-links.js.map +1 -0
- package/src/lib/model/contact-merge.d.ts +136 -0
- package/src/lib/model/contact-merge.js +412 -0
- package/src/lib/model/contact-merge.js.map +1 -0
- package/src/lib/model/contact-record.d.ts +7 -1
- package/src/lib/model/contact-record.js +6 -5
- package/src/lib/model/contact-record.js.map +1 -1
- package/src/lib/model/contact-remove.d.ts +48 -0
- package/src/lib/model/contact-remove.js +22 -0
- package/src/lib/model/contact-remove.js.map +1 -0
- package/src/lib/model/contacts-bulk-writes.d.ts +2 -71
- package/src/lib/model/contacts-bulk-writes.js +2 -82
- package/src/lib/model/contacts-bulk-writes.js.map +1 -1
- package/src/lib/model/contacts-csv.d.ts +2 -2
- package/src/lib/model/contacts-csv.js +2 -2
- package/src/lib/model/contacts-csv.js.map +1 -1
- package/src/lib/model/crm-csv.d.ts +239 -0
- package/src/lib/model/crm-csv.js +467 -0
- package/src/lib/model/crm-csv.js.map +1 -0
- package/src/lib/model/crm-digest.d.ts +185 -0
- package/src/lib/model/crm-digest.js +272 -0
- package/src/lib/model/crm-digest.js.map +1 -0
- package/src/lib/model/crm-import.d.ts +7 -1
- package/src/lib/model/crm-import.js +2 -1
- package/src/lib/model/crm-import.js.map +1 -1
- package/src/lib/model/crm-record-routes.d.ts +5 -0
- package/src/lib/model/crm-record-routes.js +7 -0
- package/src/lib/model/crm-record-routes.js.map +1 -1
- package/src/lib/model/crm-routes.d.ts +6 -5
- package/src/lib/model/crm-routes.js +7 -7
- package/src/lib/model/crm-routes.js.map +1 -1
- package/src/lib/model/crm-search-sources.d.ts +15 -0
- package/src/lib/model/crm-search-sources.js +162 -0
- package/src/lib/model/crm-search-sources.js.map +1 -0
- package/src/lib/model/crm-sharing-api.d.ts +32 -0
- package/src/lib/model/crm-sharing-api.js +59 -0
- package/src/lib/model/crm-sharing-api.js.map +1 -0
- package/src/lib/model/crm-sharing.d.ts +333 -0
- package/src/lib/model/crm-sharing.js +603 -0
- package/src/lib/model/crm-sharing.js.map +1 -0
- package/src/lib/model/deals-csv.d.ts +1 -1
- package/src/lib/model/deals-csv.js +4 -5
- package/src/lib/model/deals-csv.js.map +1 -1
- package/src/lib/model/lead-filters.d.ts +1 -1
- package/src/lib/model/leads-csv.d.ts +1 -1
- package/src/lib/model/leads-csv.js +1 -1
- package/src/lib/model/leads-csv.js.map +1 -1
- package/src/lib/model/tasks-csv.d.ts +1 -1
- package/src/lib/model/tasks-csv.js +1 -1
- package/src/lib/model/tasks-csv.js.map +1 -1
- package/src/lib/plan-entitlements.d.ts +75 -0
- package/src/lib/plan-entitlements.js +56 -0
- package/src/lib/plan-entitlements.js.map +1 -0
- package/src/lib/plugin.js +3 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/api-v1/contacts-merge.d.ts +20 -0
- package/src/lib/server/api-v1/contacts-merge.js +120 -0
- package/src/lib/server/api-v1/contacts-merge.js.map +1 -0
- package/src/lib/server/api-v1/contacts.d.ts +18 -0
- package/src/lib/server/api-v1/contacts.js +1076 -0
- package/src/lib/server/api-v1/contacts.js.map +1 -0
- package/src/lib/server/api-v1/crm-activities.d.ts +18 -0
- package/src/lib/server/api-v1/crm-activities.js +269 -0
- package/src/lib/server/api-v1/crm-activities.js.map +1 -0
- package/src/lib/server/api-v1/crm-companies.d.ts +18 -0
- package/src/lib/server/api-v1/crm-companies.js +391 -0
- package/src/lib/server/api-v1/crm-companies.js.map +1 -0
- package/src/lib/server/api-v1/crm-deals.d.ts +18 -0
- package/src/lib/server/api-v1/crm-deals.js +631 -0
- package/src/lib/server/api-v1/crm-deals.js.map +1 -0
- package/src/lib/server/api-v1/crm-email-templates.d.ts +18 -0
- package/src/lib/server/api-v1/crm-email-templates.js +336 -0
- package/src/lib/server/api-v1/crm-email-templates.js.map +1 -0
- package/src/lib/server/api-v1/crm-leads.d.ts +18 -0
- package/src/lib/server/api-v1/crm-leads.js +832 -0
- package/src/lib/server/api-v1/crm-leads.js.map +1 -0
- package/src/lib/server/api-v1/crm-pipelines.d.ts +76 -0
- package/src/lib/server/api-v1/crm-pipelines.js +146 -0
- package/src/lib/server/api-v1/crm-pipelines.js.map +1 -0
- package/src/lib/server/api-v1/crm-shared.d.ts +271 -0
- package/src/lib/server/api-v1/crm-shared.js +436 -0
- package/src/lib/server/api-v1/crm-shared.js.map +1 -0
- package/src/lib/server/api-v1/crm-tasks.d.ts +18 -0
- package/src/lib/server/api-v1/crm-tasks.js +398 -0
- package/src/lib/server/api-v1/crm-tasks.js.map +1 -0
- package/src/lib/server/api-v1/openapi.d.ts +29 -0
- package/src/lib/server/api-v1/openapi.js +985 -0
- package/src/lib/server/api-v1/openapi.js.map +1 -0
- package/src/lib/server/api-v1/usage.d.ts +26 -0
- package/src/lib/server/api-v1/usage.js +73 -0
- package/src/lib/server/api-v1/usage.js.map +1 -0
- package/src/lib/server/consent-group-participant.js +1 -1
- package/src/lib/server/consent-group-participant.js.map +1 -1
- package/src/lib/server/contact-lifecycle-floor.d.ts +89 -0
- package/src/lib/server/contact-lifecycle-floor.js +91 -0
- package/src/lib/server/contact-lifecycle-floor.js.map +1 -0
- package/src/lib/server/contact-remove.d.ts +75 -0
- package/src/lib/server/contact-remove.js +244 -0
- package/src/lib/server/contact-remove.js.map +1 -0
- package/src/lib/server/contact-update.js +2 -1
- package/src/lib/server/contact-update.js.map +1 -1
- package/src/lib/server/contacts-import.d.ts +47 -11
- package/src/lib/server/contacts-import.js +171 -71
- package/src/lib/server/contacts-import.js.map +1 -1
- package/src/lib/server/contacts-merge.d.ts +1 -1
- package/src/lib/server/contacts-merge.js +3 -2
- package/src/lib/server/contacts-merge.js.map +1 -1
- package/src/lib/server/convert-host-lead.d.ts +189 -0
- package/src/lib/server/convert-host-lead.js +518 -0
- package/src/lib/server/convert-host-lead.js.map +1 -0
- package/src/lib/server/crm-sharing.d.ts +108 -0
- package/src/lib/server/crm-sharing.js +790 -0
- package/src/lib/server/crm-sharing.js.map +1 -0
- package/src/lib/server/daily-digest-route.d.ts +107 -0
- package/src/lib/server/daily-digest-route.js +588 -0
- package/src/lib/server/daily-digest-route.js.map +1 -0
- package/src/lib/server/export-route.d.ts +60 -0
- package/src/lib/server/export-route.js +447 -0
- package/src/lib/server/export-route.js.map +1 -0
- package/src/lib/server/inbound-route.d.ts +74 -0
- package/src/lib/server/inbound-route.js +257 -0
- package/src/lib/server/inbound-route.js.map +1 -0
- package/src/lib/server/lead-convert.d.ts +1 -1
- package/src/lib/server/lead-convert.js +2 -2
- package/src/lib/server/lead-convert.js.map +1 -1
- package/src/lib/server/merge-contacts.d.ts +55 -0
- package/src/lib/server/merge-contacts.js +274 -0
- package/src/lib/server/merge-contacts.js.map +1 -0
- package/src/lib/server/pipeline-record-index.d.ts +36 -0
- package/src/lib/server/pipeline-record-index.js +87 -0
- package/src/lib/server/pipeline-record-index.js.map +1 -0
- package/src/lib/server/record-facts.js +2 -1
- package/src/lib/server/record-facts.js.map +1 -1
- package/src/lib/server/task-reminders-route.d.ts +82 -0
- package/src/lib/server/task-reminders-route.js +497 -0
- package/src/lib/server/task-reminders-route.js.map +1 -0
- package/src/lib/server-deal-stage.js +2 -1
- package/src/lib/server-deal-stage.js.map +1 -1
- package/src/lib/server.d.ts +5 -2
- package/src/lib/server.js +47 -2
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/stand-in-site-record-routes.d.ts +26 -0
- package/src/lib/testing/stand-in-site-record-routes.js +55 -0
- package/src/lib/testing/stand-in-site-record-routes.js.map +1 -0
- package/src/lib/usage-axes.d.ts +35 -0
- package/src/lib/usage-axes.js +68 -0
- package/src/lib/usage-axes.js.map +1 -0
- package/src/lib/components/use-emails-hub-path.d.ts +0 -24
- package/src/lib/components/use-emails-hub-path.js +0 -59
- package/src/lib/components/use-emails-hub-path.js.map +0 -1
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
/**
|
|
18
|
+
* The contract of `crm/contact-remove` (AGL-3338), client-safe: what the
|
|
19
|
+
* record page's Delete and the contacts bar's Remove send, and what each
|
|
20
|
+
* contact is answered with.
|
|
21
|
+
*/
|
|
22
|
+
/** The most contacts one request may name — the bar sends a selection in pieces this size. */
|
|
23
|
+
export declare const CRM_CONTACT_REMOVE_MAX = 200;
|
|
24
|
+
export interface ContactRemoveRequest {
|
|
25
|
+
/** The mounted site; at the organization level the record's own site, or nothing. */
|
|
26
|
+
hostId?: string | null;
|
|
27
|
+
/** The organization, which makes the call the route's organization variant. */
|
|
28
|
+
orgId?: string;
|
|
29
|
+
contactIds: string[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* One contact's answer: `detached` when other sites still hold the person
|
|
33
|
+
* and only this holder's half went, `deleted` when nobody else did and the
|
|
34
|
+
* document went with it — or refused, with a sentence written for the reader.
|
|
35
|
+
*/
|
|
36
|
+
export type ContactRemoveOutcome = {
|
|
37
|
+
contactId: string;
|
|
38
|
+
ok: true;
|
|
39
|
+
removed: 'detached' | 'deleted';
|
|
40
|
+
} | {
|
|
41
|
+
contactId: string;
|
|
42
|
+
ok: false;
|
|
43
|
+
error: string;
|
|
44
|
+
};
|
|
45
|
+
export interface ContactRemoveResponse {
|
|
46
|
+
ok: true;
|
|
47
|
+
results: ContactRemoveOutcome[];
|
|
48
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
* The contract of `crm/contact-remove` (AGL-3338), client-safe: what the
|
|
18
|
+
* record page's Delete and the contacts bar's Remove send, and what each
|
|
19
|
+
* contact is answered with.
|
|
20
|
+
*/ /** The most contacts one request may name — the bar sends a selection in pieces this size. */ export const CRM_CONTACT_REMOVE_MAX = 200;
|
|
21
|
+
|
|
22
|
+
//# sourceMappingURL=contact-remove.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/contact-remove.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 * The contract of `crm/contact-remove` (AGL-3338), client-safe: what the\n * record page's Delete and the contacts bar's Remove send, and what each\n * contact is answered with.\n */\n\n/** The most contacts one request may name — the bar sends a selection in pieces this size. */\nexport const CRM_CONTACT_REMOVE_MAX = 200\n\nexport interface ContactRemoveRequest {\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}\n\n/**\n * One contact's answer: `detached` when other sites still hold the person\n * and only this holder's half went, `deleted` when nobody else did and the\n * document went with it — or refused, with a sentence written for the reader.\n */\nexport type ContactRemoveOutcome =\n | { contactId: string; ok: true; removed: 'detached' | 'deleted' }\n | { contactId: string; ok: false; error: string }\n\nexport interface ContactRemoveResponse {\n ok: true\n results: ContactRemoveOutcome[]\n}\n"],"names":["CRM_CONTACT_REMOVE_MAX"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;CAIC,GAED,4FAA4F,GAC5F,OAAO,MAAMA,yBAAyB,IAAG"}
|
|
@@ -22,8 +22,7 @@
|
|
|
22
22
|
* to an owner, file them under a company, move them along the funnel, let
|
|
23
23
|
* them go. A contact's tags, owner and company are one holder's records in
|
|
24
24
|
* that holder's facet, and a facet is the server's to write: the rules cannot
|
|
25
|
-
* tell one field of it from another
|
|
26
|
-
* but letting a holder go. So every act but that one is a request to a route,
|
|
25
|
+
* tell one field of it from another. So every act is a request to a route,
|
|
27
26
|
* and what this module decides is WHICH rows the request names — the bar
|
|
28
27
|
* holds only the controls.
|
|
29
28
|
*
|
|
@@ -37,23 +36,8 @@
|
|
|
37
36
|
* "398 of 400" is a number nobody can act on; an address is. The route asks
|
|
38
37
|
* the same questions of the stored document, so a row that changed since the
|
|
39
38
|
* table read it is still refused by name rather than slipped past.
|
|
40
|
-
*
|
|
41
|
-
* ## Letting go is still written here
|
|
42
|
-
*
|
|
43
|
-
* Removing a contact from a site drops that holder's facet, consent entries,
|
|
44
|
-
* capture attribution and scope tokens — the one change to a facet a holder
|
|
45
|
-
* may make client-direct. Those patches are built here and applied in
|
|
46
|
-
* batches with a per-row fallback, so a refused row is named rather than
|
|
47
|
-
* failing the other 399 with it.
|
|
48
|
-
*
|
|
49
|
-
* Pure apart from the Firestore sentinels the detach carries: the runner takes
|
|
50
|
-
* its writers as ports, so the bar wires `writeBatch`/`updateDoc`/`deleteDoc`
|
|
51
|
-
* in and a spec wires a ledger in.
|
|
52
39
|
*/
|
|
53
40
|
import { type ContactCompanyLinkState } from '@aglyn/aglyn';
|
|
54
|
-
import { chunked } from './crm-bulk-writes';
|
|
55
|
-
export declare const CONTACT_BULK_WRITE_CHUNK = 400;
|
|
56
|
-
export { chunked };
|
|
57
41
|
/** The record page's cap on a holder's tags — the same number, so the two agree. */
|
|
58
42
|
export declare const CONTACT_TAGS_CAP = 20;
|
|
59
43
|
/** As much of a table row as a bulk act reads. */
|
|
@@ -71,31 +55,16 @@ export interface ContactBulkRow {
|
|
|
71
55
|
holderHostId?: string;
|
|
72
56
|
/** THIS holder's tags, already read through the facet by the table. */
|
|
73
57
|
tags?: string[];
|
|
74
|
-
/** The holder tokens
|
|
58
|
+
/** The holder tokens the row is shared with. */
|
|
75
59
|
visibleTo?: string[];
|
|
76
60
|
/** The link state the company planner reads — see `ContactRecord.companyLink`. */
|
|
77
61
|
companyLink?: ContactCompanyLinkState;
|
|
78
62
|
}
|
|
79
|
-
/** One client-direct write to one contact document, named by the address it is about. */
|
|
80
|
-
export type ContactBulkWrite = {
|
|
81
|
-
id: string;
|
|
82
|
-
email: string;
|
|
83
|
-
} & ({
|
|
84
|
-
kind: 'update';
|
|
85
|
-
data: Record<string, unknown>;
|
|
86
|
-
} | {
|
|
87
|
-
kind: 'delete';
|
|
88
|
-
});
|
|
89
63
|
/** A row an action deliberately left alone, and why. */
|
|
90
64
|
export interface ContactBulkSkip {
|
|
91
65
|
email: string;
|
|
92
66
|
reason: string;
|
|
93
67
|
}
|
|
94
|
-
/** What a letting-go wants written, and what it declined to. */
|
|
95
|
-
export interface ContactBulkPlan {
|
|
96
|
-
writes: ContactBulkWrite[];
|
|
97
|
-
skipped: ContactBulkSkip[];
|
|
98
|
-
}
|
|
99
68
|
/** The rows a request to a route names, and the rows it leaves out by name. */
|
|
100
69
|
export interface ContactBulkSelection {
|
|
101
70
|
rows: ContactBulkRow[];
|
|
@@ -133,41 +102,3 @@ export declare function planRemoveTag(rows: readonly ContactBulkRow[], tag: stri
|
|
|
133
102
|
* request is not the place to guess.
|
|
134
103
|
*/
|
|
135
104
|
export declare function planSetCompany(rows: readonly ContactBulkRow[], companyId: string | null): ContactBulkSelection;
|
|
136
|
-
/**
|
|
137
|
-
* Let every row go — the record page's DELETE IS A DETACH, per row.
|
|
138
|
-
*
|
|
139
|
-
* `planContactDetach` decides for each document whether this holder is the
|
|
140
|
-
* last one (delete) or one of several (drop this group's facet, consent
|
|
141
|
-
* entries, capture attribution and scope tokens). The `arrayRemove` and
|
|
142
|
-
* `deleteField` sentinels are the ones the record page writes, so a row
|
|
143
|
-
* detached here and a row detached there are the same document afterwards.
|
|
144
|
-
*
|
|
145
|
-
* ⛔ Not the erasure path: a privacy erasure removes the person everywhere
|
|
146
|
-
* regardless of holders.
|
|
147
|
-
*/
|
|
148
|
-
export declare function planDetach(rows: readonly ContactBulkRow[], group: {
|
|
149
|
-
groupId: string;
|
|
150
|
-
hostIds: readonly string[];
|
|
151
|
-
}, nowMs: number): ContactBulkPlan;
|
|
152
|
-
/** The writers the runner needs — Firestore's, or a spec's ledger. */
|
|
153
|
-
export interface ContactBulkWriters {
|
|
154
|
-
/** Apply every write atomically, or throw. */
|
|
155
|
-
commitBatch: (writes: readonly ContactBulkWrite[]) => Promise<void>;
|
|
156
|
-
/** Apply one write, or throw. */
|
|
157
|
-
commitOne: (write: ContactBulkWrite) => Promise<void>;
|
|
158
|
-
}
|
|
159
|
-
export interface ContactBulkOutcome {
|
|
160
|
-
/** Rows written. */
|
|
161
|
-
done: number;
|
|
162
|
-
/** Rows the store refused, by address, with the reason it gave. */
|
|
163
|
-
refused: Array<{
|
|
164
|
-
email: string;
|
|
165
|
-
error: string;
|
|
166
|
-
}>;
|
|
167
|
-
}
|
|
168
|
-
/**
|
|
169
|
-
* The shared runner over contact writes — batched, with a per-row pass for
|
|
170
|
-
* the chunk that failed (`crm-bulk-writes.ts`) — reporting by ADDRESS, which
|
|
171
|
-
* is the name a contacts report lists a row under.
|
|
172
|
-
*/
|
|
173
|
-
export declare function runContactBulkWrites(writers: ContactBulkWriters, writes: readonly ContactBulkWrite[], chunkSize?: number): Promise<ContactBulkOutcome>;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
1
|
/**
|
|
3
2
|
* @license
|
|
4
3
|
* Copyright 2026 Aglyn LLC
|
|
@@ -22,8 +21,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
22
21
|
* to an owner, file them under a company, move them along the funnel, let
|
|
23
22
|
* them go. A contact's tags, owner and company are one holder's records in
|
|
24
23
|
* that holder's facet, and a facet is the server's to write: the rules cannot
|
|
25
|
-
* tell one field of it from another
|
|
26
|
-
* but letting a holder go. So every act but that one is a request to a route,
|
|
24
|
+
* tell one field of it from another. So every act is a request to a route,
|
|
27
25
|
* and what this module decides is WHICH rows the request names — the bar
|
|
28
26
|
* holds only the controls.
|
|
29
27
|
*
|
|
@@ -37,27 +35,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
37
35
|
* "398 of 400" is a number nobody can act on; an address is. The route asks
|
|
38
36
|
* the same questions of the stored document, so a row that changed since the
|
|
39
37
|
* table read it is still refused by name rather than slipped past.
|
|
40
|
-
|
|
41
|
-
* ## Letting go is still written here
|
|
42
|
-
*
|
|
43
|
-
* Removing a contact from a site drops that holder's facet, consent entries,
|
|
44
|
-
* capture attribution and scope tokens — the one change to a facet a holder
|
|
45
|
-
* may make client-direct. Those patches are built here and applied in
|
|
46
|
-
* batches with a per-row fallback, so a refused row is named rather than
|
|
47
|
-
* failing the other 399 with it.
|
|
48
|
-
*
|
|
49
|
-
* Pure apart from the Firestore sentinels the detach carries: the runner takes
|
|
50
|
-
* its writers as ports, so the bar wires `writeBatch`/`updateDoc`/`deleteDoc`
|
|
51
|
-
* in and a spec wires a ledger in.
|
|
52
|
-
*/ import { planContactCompanyLink, planContactDetach } from "@aglyn/aglyn";
|
|
53
|
-
import { arrayRemove, deleteField } from "firebase/firestore";
|
|
54
|
-
import { CRM_BULK_WRITE_CHUNK, chunked, runCrmBulkWrites } from "./crm-bulk-writes.js";
|
|
55
|
-
/*
|
|
56
|
-
* The runner and its chunk size are the shared ones (`crm-bulk-writes.ts`,
|
|
57
|
-
* AGL-2621), under the names this module has always exported: the batch
|
|
58
|
-
* cap and the per-row fallback are about Firestore, not about people.
|
|
59
|
-
*/ export const CONTACT_BULK_WRITE_CHUNK = CRM_BULK_WRITE_CHUNK;
|
|
60
|
-
export { chunked };
|
|
38
|
+
*/ import { planContactCompanyLink } from "@aglyn/aglyn";
|
|
61
39
|
/** The record page's cap on a holder's tags — the same number, so the two agree. */ export const CONTACT_TAGS_CAP = 20;
|
|
62
40
|
/** The address a report names a row by, falling back to the id. */ export const contactBulkAddressOf = (row)=>String(row.email || row.$id);
|
|
63
41
|
/**
|
|
@@ -134,63 +112,5 @@ export { chunked };
|
|
|
134
112
|
skipped
|
|
135
113
|
};
|
|
136
114
|
}
|
|
137
|
-
/**
|
|
138
|
-
* Let every row go — the record page's DELETE IS A DETACH, per row.
|
|
139
|
-
*
|
|
140
|
-
* `planContactDetach` decides for each document whether this holder is the
|
|
141
|
-
* last one (delete) or one of several (drop this group's facet, consent
|
|
142
|
-
* entries, capture attribution and scope tokens). The `arrayRemove` and
|
|
143
|
-
* `deleteField` sentinels are the ones the record page writes, so a row
|
|
144
|
-
* detached here and a row detached there are the same document afterwards.
|
|
145
|
-
*
|
|
146
|
-
* ⛔ Not the erasure path: a privacy erasure removes the person everywhere
|
|
147
|
-
* regardless of holders.
|
|
148
|
-
*/ export function planDetach(rows, group, nowMs) {
|
|
149
|
-
return {
|
|
150
|
-
writes: rows.map((row)=>{
|
|
151
|
-
var _row_visibleTo;
|
|
152
|
-
// Only the holder tokens reach the plan: that is all it reads, and a
|
|
153
|
-
// projected table row carries fields the detach must not consult.
|
|
154
|
-
const plan = planContactDetach({
|
|
155
|
-
visibleTo: (_row_visibleTo = row.visibleTo) != null ? _row_visibleTo : []
|
|
156
|
-
}, group);
|
|
157
|
-
if (plan.action === 'delete') {
|
|
158
|
-
return {
|
|
159
|
-
id: row.$id,
|
|
160
|
-
email: contactBulkAddressOf(row),
|
|
161
|
-
kind: 'delete'
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
return {
|
|
165
|
-
id: row.$id,
|
|
166
|
-
email: contactBulkAddressOf(row),
|
|
167
|
-
kind: 'update',
|
|
168
|
-
data: _extends({}, Object.fromEntries(plan.remove.map((path)=>[
|
|
169
|
-
path,
|
|
170
|
-
deleteField()
|
|
171
|
-
])), {
|
|
172
|
-
visibleTo: arrayRemove(...plan.removeTokens),
|
|
173
|
-
capturedByHostIds: arrayRemove(...plan.removeHostIds),
|
|
174
|
-
updatedAt: new Date(nowMs)
|
|
175
|
-
})
|
|
176
|
-
};
|
|
177
|
-
}),
|
|
178
|
-
skipped: []
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
/**
|
|
182
|
-
* The shared runner over contact writes — batched, with a per-row pass for
|
|
183
|
-
* the chunk that failed (`crm-bulk-writes.ts`) — reporting by ADDRESS, which
|
|
184
|
-
* is the name a contacts report lists a row under.
|
|
185
|
-
*/ export async function runContactBulkWrites(writers, writes, chunkSize = CONTACT_BULK_WRITE_CHUNK) {
|
|
186
|
-
const outcome = await runCrmBulkWrites(writers, writes, (write)=>write.email, chunkSize);
|
|
187
|
-
return {
|
|
188
|
-
done: outcome.done,
|
|
189
|
-
refused: outcome.refused.map((row)=>({
|
|
190
|
-
email: row.label,
|
|
191
|
-
error: row.error
|
|
192
|
-
}))
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
115
|
|
|
196
116
|
//# sourceMappingURL=contacts-bulk-writes.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/contacts-bulk-writes.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 * What a bulk action on the contacts table does to each row (AGL-2603, and\n * through the server since AGL-2804).\n *\n * The bar over the table offers one act over many rows — tag them, hand them\n * to an owner, file them under a company, move them along the funnel, let\n * them go. A contact's tags, owner and company are one holder's records in\n * that holder's facet, and a facet is the server's to write: the rules cannot\n * tell one field of it from another, so they leave a client nothing there\n * but letting a holder go. So every act but that one is a request to a route,\n * and what this module decides is WHICH rows the request names — the bar\n * holds only the controls.\n *\n * ## A row with nothing to change is left out, and a row that cannot be\n * ## reached is REPORTED\n *\n * A row already carrying the tag, or already at the company, is left out of\n * the request silently: the route would write nothing, and a report saying\n * so would be noise. A row already at the tag cap, or whose company link the\n * table could not project, is left out and named by address — a count of\n * \"398 of 400\" is a number nobody can act on; an address is. The route asks\n * the same questions of the stored document, so a row that changed since the\n * table read it is still refused by name rather than slipped past.\n *\n * ## Letting go is still written here\n *\n * Removing a contact from a site drops that holder's facet, consent entries,\n * capture attribution and scope tokens — the one change to a facet a holder\n * may make client-direct. Those patches are built here and applied in\n * batches with a per-row fallback, so a refused row is named rather than\n * failing the other 399 with it.\n *\n * Pure apart from the Firestore sentinels the detach carries: the runner takes\n * its writers as ports, so the bar wires `writeBatch`/`updateDoc`/`deleteDoc`\n * in and a spec wires a ledger in.\n */\n\nimport {\n type ContactCompanyLinkState,\n planContactCompanyLink,\n planContactDetach,\n} from '@aglyn/aglyn'\nimport { arrayRemove, deleteField } from 'firebase/firestore'\nimport {\n CRM_BULK_WRITE_CHUNK,\n chunked,\n runCrmBulkWrites,\n} from './crm-bulk-writes'\n\n/*\n * The runner and its chunk size are the shared ones (`crm-bulk-writes.ts`,\n * AGL-2621), under the names this module has always exported: the batch\n * cap and the per-row fallback are about Firestore, not about people.\n */\nexport const CONTACT_BULK_WRITE_CHUNK = CRM_BULK_WRITE_CHUNK\nexport { chunked }\n\n/** The record page's cap on a holder's tags — the same number, so the two agree. */\nexport const CONTACT_TAGS_CAP = 20\n\n/** As much of a table row as a bulk act reads. */\nexport interface ContactBulkRow {\n $id: string\n email?: string\n /**\n * The holder this row was flattened through (`ContactRecord.groupId`),\n * which a cross-holder reader — the organization-level list (AGL-2630) —\n * writes each row through, since no one viewing group covers every\n * selected person there.\n */\n groupId?: string\n /** A site of that holder's group — where the row's stage is moved (`ContactRecord.holderHostId`). */\n holderHostId?: string\n /** THIS holder's tags, already read through the facet by the table. */\n tags?: string[]\n /** The holder tokens — what `planContactDetach` counts. */\n visibleTo?: string[]\n /** The link state the company planner reads — see `ContactRecord.companyLink`. */\n companyLink?: ContactCompanyLinkState\n}\n\n/** One client-direct write to one contact document, named by the address it is about. */\nexport type ContactBulkWrite = { id: string; email: string } & (\n | { kind: 'update'; data: Record<string, unknown> }\n | { kind: 'delete' }\n)\n\n/** A row an action deliberately left alone, and why. */\nexport interface ContactBulkSkip {\n email: string\n reason: string\n}\n\n/** What a letting-go wants written, and what it declined to. */\nexport interface ContactBulkPlan {\n writes: ContactBulkWrite[]\n skipped: ContactBulkSkip[]\n}\n\n/** The rows a request to a route names, and the rows it leaves out by name. */\nexport interface ContactBulkSelection {\n rows: ContactBulkRow[]\n skipped: ContactBulkSkip[]\n}\n\n/** The address a report names a row by, falling back to the id. */\nexport const contactBulkAddressOf = (row: ContactBulkRow): string =>\n String(row.email || row.$id)\n\n/**\n * A typed tag as the record page stores one, or `null` when nothing survives.\n *\n * Lowercased and trimmed because the record page lowercases and trims, and\n * `contactMatchesSegment` compares lowercased — a bulk \"VIP\" beside a typed\n * \"vip\" would otherwise be two tags on one list and one filter finding half\n * of them.\n */\nexport function normalizeBulkTag(input: string): string | null {\n const tag = input.trim().toLowerCase().slice(0, 60)\n return tag || null\n}\n\n/**\n * The rows adding one tag reaches.\n *\n * A row that already carries the tag is left out silently. A row already at\n * the cap without it is left out AND reported: the cap is the record page's,\n * and a bulk path that slipped past it would leave a tag the page's next\n * save silently drops.\n */\nexport function planAddTag(\n rows: readonly ContactBulkRow[],\n tag: string,\n): ContactBulkSelection {\n const reached: ContactBulkRow[] = []\n const skipped: ContactBulkSkip[] = []\n for (const row of rows) {\n const held = (row.tags ?? []).map((held) => held.toLowerCase())\n if (held.includes(tag)) continue\n if (held.length >= CONTACT_TAGS_CAP) {\n skipped.push({\n email: contactBulkAddressOf(row),\n reason: `already has ${CONTACT_TAGS_CAP} tags`,\n })\n continue\n }\n reached.push(row)\n }\n return { rows: reached, skipped }\n}\n\n/** The rows taking one tag off reaches: the ones that carry it. */\nexport function planRemoveTag(\n rows: readonly ContactBulkRow[],\n tag: string,\n): ContactBulkSelection {\n return {\n rows: rows.filter((row) =>\n (row.tags ?? []).map((held) => held.toLowerCase()).includes(tag),\n ),\n skipped: [],\n }\n}\n\n/**\n * The rows filing under one company reaches — or unlinking, with `null`\n * (AGL-2613).\n *\n * A row already where it was asked to be is left out silently, as a row\n * already tagged is. A row the table could not project a link state for is\n * left out AND named: whether its link would change cannot be told, and the\n * request is not the place to guess.\n */\nexport function planSetCompany(\n rows: readonly ContactBulkRow[],\n companyId: string | null,\n): ContactBulkSelection {\n const reached: ContactBulkRow[] = []\n const skipped: ContactBulkSkip[] = []\n for (const row of rows) {\n if (!row.companyLink) {\n skipped.push({\n email: contactBulkAddressOf(row),\n reason: 'its company link could not be read',\n })\n continue\n }\n if (planContactCompanyLink(row.companyLink, companyId)) reached.push(row)\n }\n return { rows: reached, skipped }\n}\n\n/**\n * Let every row go — the record page's DELETE IS A DETACH, per row.\n *\n * `planContactDetach` decides for each document whether this holder is the\n * last one (delete) or one of several (drop this group's facet, consent\n * entries, capture attribution and scope tokens). The `arrayRemove` and\n * `deleteField` sentinels are the ones the record page writes, so a row\n * detached here and a row detached there are the same document afterwards.\n *\n * ⛔ Not the erasure path: a privacy erasure removes the person everywhere\n * regardless of holders.\n */\nexport function planDetach(\n rows: readonly ContactBulkRow[],\n group: { groupId: string; hostIds: readonly string[] },\n nowMs: number,\n): ContactBulkPlan {\n return {\n writes: rows.map((row) => {\n // Only the holder tokens reach the plan: that is all it reads, and a\n // projected table row carries fields the detach must not consult.\n const plan = planContactDetach({ visibleTo: row.visibleTo ?? [] }, group)\n if (plan.action === 'delete') {\n return { id: row.$id, email: contactBulkAddressOf(row), kind: 'delete' as const }\n }\n return {\n id: row.$id,\n email: contactBulkAddressOf(row),\n kind: 'update' as const,\n data: {\n ...Object.fromEntries(plan.remove.map((path) => [path, deleteField()])),\n visibleTo: arrayRemove(...plan.removeTokens),\n capturedByHostIds: arrayRemove(...plan.removeHostIds),\n updatedAt: new Date(nowMs),\n },\n }\n }),\n skipped: [],\n }\n}\n\n/** The writers the runner needs — Firestore's, or a spec's ledger. */\nexport interface ContactBulkWriters {\n /** Apply every write atomically, or throw. */\n commitBatch: (writes: readonly ContactBulkWrite[]) => Promise<void>\n /** Apply one write, or throw. */\n commitOne: (write: ContactBulkWrite) => Promise<void>\n}\n\nexport interface ContactBulkOutcome {\n /** Rows written. */\n done: number\n /** Rows the store refused, by address, with the reason it gave. */\n refused: Array<{ email: string; error: string }>\n}\n\n/**\n * The shared runner over contact writes — batched, with a per-row pass for\n * the chunk that failed (`crm-bulk-writes.ts`) — reporting by ADDRESS, which\n * is the name a contacts report lists a row under.\n */\nexport async function runContactBulkWrites(\n writers: ContactBulkWriters,\n writes: readonly ContactBulkWrite[],\n chunkSize: number = CONTACT_BULK_WRITE_CHUNK,\n): Promise<ContactBulkOutcome> {\n const outcome = await runCrmBulkWrites(\n writers,\n writes,\n (write) => write.email,\n chunkSize,\n )\n return {\n done: outcome.done,\n refused: outcome.refused.map((row) => ({ email: row.label, error: row.error })),\n }\n}\n"],"names":["planContactCompanyLink","planContactDetach","arrayRemove","deleteField","CRM_BULK_WRITE_CHUNK","chunked","runCrmBulkWrites","CONTACT_BULK_WRITE_CHUNK","CONTACT_TAGS_CAP","contactBulkAddressOf","row","String","email","$id","normalizeBulkTag","input","tag","trim","toLowerCase","slice","planAddTag","rows","reached","skipped","held","tags","map","includes","length","push","reason","planRemoveTag","filter","planSetCompany","companyId","companyLink","planDetach","group","nowMs","writes","plan","visibleTo","action","id","kind","data","Object","fromEntries","remove","path","removeTokens","capturedByHostIds","removeHostIds","updatedAt","Date","runContactBulkWrites","writers","chunkSize","outcome","write","done","refused","label","error"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmCC,GAED,SAEEA,sBAAsB,EACtBC,iBAAiB,QACZ,eAAc;AACrB,SAASC,WAAW,EAAEC,WAAW,QAAQ,qBAAoB;AAC7D,SACEC,oBAAoB,EACpBC,OAAO,EACPC,gBAAgB,QACX,uBAAmB;AAE1B;;;;CAIC,GACD,OAAO,MAAMC,2BAA2BH,qBAAoB;AAC5D,SAASC,OAAO,GAAE;AAElB,kFAAkF,GAClF,OAAO,MAAMG,mBAAmB,GAAE;AA+ClC,iEAAiE,GACjE,OAAO,MAAMC,uBAAuB,CAACC,MACnCC,OAAOD,IAAIE,KAAK,IAAIF,IAAIG,GAAG,EAAC;AAE9B;;;;;;;CAOC,GACD,OAAO,SAASC,iBAAiBC,KAAa;IAC5C,MAAMC,MAAMD,MAAME,IAAI,GAAGC,WAAW,GAAGC,KAAK,CAAC,GAAG;IAChD,OAAOH,OAAO;AAChB;AAEA;;;;;;;CAOC,GACD,OAAO,SAASI,WACdC,IAA+B,EAC/BL,GAAW;IAEX,MAAMM,UAA4B,EAAE;IACpC,MAAMC,UAA6B,EAAE;IACrC,KAAK,MAAMb,OAAOW,KAAM;YACRX;QAAd,MAAMc,OAAO,EAACd,YAAAA,IAAIe,IAAI,YAARf,YAAY,EAAE,EAAEgB,GAAG,CAAC,CAACF,OAASA,KAAKN,WAAW;QAC5D,IAAIM,KAAKG,QAAQ,CAACX,MAAM;QACxB,IAAIQ,KAAKI,MAAM,IAAIpB,kBAAkB;YACnCe,QAAQM,IAAI,CAAC;gBACXjB,OAAOH,qBAAqBC;gBAC5BoB,QAAQ,CAAC,YAAY,EAAEtB,iBAAiB,KAAK,CAAC;YAChD;YACA;QACF;QACAc,QAAQO,IAAI,CAACnB;IACf;IACA,OAAO;QAAEW,MAAMC;QAASC;IAAQ;AAClC;AAEA,iEAAiE,GACjE,OAAO,SAASQ,cACdV,IAA+B,EAC/BL,GAAW;IAEX,OAAO;QACLK,MAAMA,KAAKW,MAAM,CAAC,CAACtB;gBAChBA;mBAAD,EAACA,YAAAA,IAAIe,IAAI,YAARf,YAAY,EAAE,EAAEgB,GAAG,CAAC,CAACF,OAASA,KAAKN,WAAW,IAAIS,QAAQ,CAACX;;QAE9DO,SAAS,EAAE;IACb;AACF;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASU,eACdZ,IAA+B,EAC/Ba,SAAwB;IAExB,MAAMZ,UAA4B,EAAE;IACpC,MAAMC,UAA6B,EAAE;IACrC,KAAK,MAAMb,OAAOW,KAAM;QACtB,IAAI,CAACX,IAAIyB,WAAW,EAAE;YACpBZ,QAAQM,IAAI,CAAC;gBACXjB,OAAOH,qBAAqBC;gBAC5BoB,QAAQ;YACV;YACA;QACF;QACA,IAAI9B,uBAAuBU,IAAIyB,WAAW,EAAED,YAAYZ,QAAQO,IAAI,CAACnB;IACvE;IACA,OAAO;QAAEW,MAAMC;QAASC;IAAQ;AAClC;AAEA;;;;;;;;;;;CAWC,GACD,OAAO,SAASa,WACdf,IAA+B,EAC/BgB,KAAsD,EACtDC,KAAa;IAEb,OAAO;QACLC,QAAQlB,KAAKK,GAAG,CAAC,CAAChB;gBAG4BA;YAF5C,qEAAqE;YACrE,kEAAkE;YAClE,MAAM8B,OAAOvC,kBAAkB;gBAAEwC,SAAS,GAAE/B,iBAAAA,IAAI+B,SAAS,YAAb/B,iBAAiB,EAAE;YAAC,GAAG2B;YACnE,IAAIG,KAAKE,MAAM,KAAK,UAAU;gBAC5B,OAAO;oBAAEC,IAAIjC,IAAIG,GAAG;oBAAED,OAAOH,qBAAqBC;oBAAMkC,MAAM;gBAAkB;YAClF;YACA,OAAO;gBACLD,IAAIjC,IAAIG,GAAG;gBACXD,OAAOH,qBAAqBC;gBAC5BkC,MAAM;gBACNC,MAAM,aACDC,OAAOC,WAAW,CAACP,KAAKQ,MAAM,CAACtB,GAAG,CAAC,CAACuB,OAAS;wBAACA;wBAAM9C;qBAAc;oBACrEsC,WAAWvC,eAAesC,KAAKU,YAAY;oBAC3CC,mBAAmBjD,eAAesC,KAAKY,aAAa;oBACpDC,WAAW,IAAIC,KAAKhB;;YAExB;QACF;QACAf,SAAS,EAAE;IACb;AACF;AAiBA;;;;CAIC,GACD,OAAO,eAAegC,qBACpBC,OAA2B,EAC3BjB,MAAmC,EACnCkB,YAAoBlD,wBAAwB;IAE5C,MAAMmD,UAAU,MAAMpD,iBACpBkD,SACAjB,QACA,CAACoB,QAAUA,MAAM/C,KAAK,EACtB6C;IAEF,OAAO;QACLG,MAAMF,QAAQE,IAAI;QAClBC,SAASH,QAAQG,OAAO,CAACnC,GAAG,CAAC,CAAChB,MAAS,CAAA;gBAAEE,OAAOF,IAAIoD,KAAK;gBAAEC,OAAOrD,IAAIqD,KAAK;YAAC,CAAA;IAC9E;AACF"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/contacts-bulk-writes.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 * What a bulk action on the contacts table does to each row (AGL-2603, and\n * through the server since AGL-2804).\n *\n * The bar over the table offers one act over many rows — tag them, hand them\n * to an owner, file them under a company, move them along the funnel, let\n * them go. A contact's tags, owner and company are one holder's records in\n * that holder's facet, and a facet is the server's to write: the rules cannot\n * tell one field of it from another. So every act is a request to a route,\n * and what this module decides is WHICH rows the request names — the bar\n * holds only the controls.\n *\n * ## A row with nothing to change is left out, and a row that cannot be\n * ## reached is REPORTED\n *\n * A row already carrying the tag, or already at the company, is left out of\n * the request silently: the route would write nothing, and a report saying\n * so would be noise. A row already at the tag cap, or whose company link the\n * table could not project, is left out and named by address — a count of\n * \"398 of 400\" is a number nobody can act on; an address is. The route asks\n * the same questions of the stored document, so a row that changed since the\n * table read it is still refused by name rather than slipped past.\n */\n\nimport {\n type ContactCompanyLinkState,\n planContactCompanyLink,\n} from '@aglyn/aglyn'\n\n/** The record page's cap on a holder's tags — the same number, so the two agree. */\nexport const CONTACT_TAGS_CAP = 20\n\n/** As much of a table row as a bulk act reads. */\nexport interface ContactBulkRow {\n $id: string\n email?: string\n /**\n * The holder this row was flattened through (`ContactRecord.groupId`),\n * which a cross-holder reader — the organization-level list (AGL-2630) —\n * writes each row through, since no one viewing group covers every\n * selected person there.\n */\n groupId?: string\n /** A site of that holder's group — where the row's stage is moved (`ContactRecord.holderHostId`). */\n holderHostId?: string\n /** THIS holder's tags, already read through the facet by the table. */\n tags?: string[]\n /** The holder tokens the row is shared with. */\n visibleTo?: string[]\n /** The link state the company planner reads — see `ContactRecord.companyLink`. */\n companyLink?: ContactCompanyLinkState\n}\n\n/** A row an action deliberately left alone, and why. */\nexport interface ContactBulkSkip {\n email: string\n reason: string\n}\n\n/** The rows a request to a route names, and the rows it leaves out by name. */\nexport interface ContactBulkSelection {\n rows: ContactBulkRow[]\n skipped: ContactBulkSkip[]\n}\n\n/** The address a report names a row by, falling back to the id. */\nexport const contactBulkAddressOf = (row: ContactBulkRow): string =>\n String(row.email || row.$id)\n\n/**\n * A typed tag as the record page stores one, or `null` when nothing survives.\n *\n * Lowercased and trimmed because the record page lowercases and trims, and\n * `contactMatchesSegment` compares lowercased — a bulk \"VIP\" beside a typed\n * \"vip\" would otherwise be two tags on one list and one filter finding half\n * of them.\n */\nexport function normalizeBulkTag(input: string): string | null {\n const tag = input.trim().toLowerCase().slice(0, 60)\n return tag || null\n}\n\n/**\n * The rows adding one tag reaches.\n *\n * A row that already carries the tag is left out silently. A row already at\n * the cap without it is left out AND reported: the cap is the record page's,\n * and a bulk path that slipped past it would leave a tag the page's next\n * save silently drops.\n */\nexport function planAddTag(\n rows: readonly ContactBulkRow[],\n tag: string,\n): ContactBulkSelection {\n const reached: ContactBulkRow[] = []\n const skipped: ContactBulkSkip[] = []\n for (const row of rows) {\n const held = (row.tags ?? []).map((held) => held.toLowerCase())\n if (held.includes(tag)) continue\n if (held.length >= CONTACT_TAGS_CAP) {\n skipped.push({\n email: contactBulkAddressOf(row),\n reason: `already has ${CONTACT_TAGS_CAP} tags`,\n })\n continue\n }\n reached.push(row)\n }\n return { rows: reached, skipped }\n}\n\n/** The rows taking one tag off reaches: the ones that carry it. */\nexport function planRemoveTag(\n rows: readonly ContactBulkRow[],\n tag: string,\n): ContactBulkSelection {\n return {\n rows: rows.filter((row) =>\n (row.tags ?? []).map((held) => held.toLowerCase()).includes(tag),\n ),\n skipped: [],\n }\n}\n\n/**\n * The rows filing under one company reaches — or unlinking, with `null`\n * (AGL-2613).\n *\n * A row already where it was asked to be is left out silently, as a row\n * already tagged is. A row the table could not project a link state for is\n * left out AND named: whether its link would change cannot be told, and the\n * request is not the place to guess.\n */\nexport function planSetCompany(\n rows: readonly ContactBulkRow[],\n companyId: string | null,\n): ContactBulkSelection {\n const reached: ContactBulkRow[] = []\n const skipped: ContactBulkSkip[] = []\n for (const row of rows) {\n if (!row.companyLink) {\n skipped.push({\n email: contactBulkAddressOf(row),\n reason: 'its company link could not be read',\n })\n continue\n }\n if (planContactCompanyLink(row.companyLink, companyId)) reached.push(row)\n }\n return { rows: reached, skipped }\n}\n"],"names":["planContactCompanyLink","CONTACT_TAGS_CAP","contactBulkAddressOf","row","String","email","$id","normalizeBulkTag","input","tag","trim","toLowerCase","slice","planAddTag","rows","reached","skipped","held","tags","map","includes","length","push","reason","planRemoveTag","filter","planSetCompany","companyId","companyLink"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;CAsBC,GAED,SAEEA,sBAAsB,QACjB,eAAc;AAErB,kFAAkF,GAClF,OAAO,MAAMC,mBAAmB,GAAE;AAmClC,iEAAiE,GACjE,OAAO,MAAMC,uBAAuB,CAACC,MACnCC,OAAOD,IAAIE,KAAK,IAAIF,IAAIG,GAAG,EAAC;AAE9B;;;;;;;CAOC,GACD,OAAO,SAASC,iBAAiBC,KAAa;IAC5C,MAAMC,MAAMD,MAAME,IAAI,GAAGC,WAAW,GAAGC,KAAK,CAAC,GAAG;IAChD,OAAOH,OAAO;AAChB;AAEA;;;;;;;CAOC,GACD,OAAO,SAASI,WACdC,IAA+B,EAC/BL,GAAW;IAEX,MAAMM,UAA4B,EAAE;IACpC,MAAMC,UAA6B,EAAE;IACrC,KAAK,MAAMb,OAAOW,KAAM;YACRX;QAAd,MAAMc,OAAO,EAACd,YAAAA,IAAIe,IAAI,YAARf,YAAY,EAAE,EAAEgB,GAAG,CAAC,CAACF,OAASA,KAAKN,WAAW;QAC5D,IAAIM,KAAKG,QAAQ,CAACX,MAAM;QACxB,IAAIQ,KAAKI,MAAM,IAAIpB,kBAAkB;YACnCe,QAAQM,IAAI,CAAC;gBACXjB,OAAOH,qBAAqBC;gBAC5BoB,QAAQ,CAAC,YAAY,EAAEtB,iBAAiB,KAAK,CAAC;YAChD;YACA;QACF;QACAc,QAAQO,IAAI,CAACnB;IACf;IACA,OAAO;QAAEW,MAAMC;QAASC;IAAQ;AAClC;AAEA,iEAAiE,GACjE,OAAO,SAASQ,cACdV,IAA+B,EAC/BL,GAAW;IAEX,OAAO;QACLK,MAAMA,KAAKW,MAAM,CAAC,CAACtB;gBAChBA;mBAAD,EAACA,YAAAA,IAAIe,IAAI,YAARf,YAAY,EAAE,EAAEgB,GAAG,CAAC,CAACF,OAASA,KAAKN,WAAW,IAAIS,QAAQ,CAACX;;QAE9DO,SAAS,EAAE;IACb;AACF;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASU,eACdZ,IAA+B,EAC/Ba,SAAwB;IAExB,MAAMZ,UAA4B,EAAE;IACpC,MAAMC,UAA6B,EAAE;IACrC,KAAK,MAAMb,OAAOW,KAAM;QACtB,IAAI,CAACX,IAAIyB,WAAW,EAAE;YACpBZ,QAAQM,IAAI,CAAC;gBACXjB,OAAOH,qBAAqBC;gBAC5BoB,QAAQ;YACV;YACA;QACF;QACA,IAAIvB,uBAAuBG,IAAIyB,WAAW,EAAED,YAAYZ,QAAQO,IAAI,CAACnB;IACvE;IACA,OAAO;QAAEW,MAAMC;QAASC;IAAQ;AAClC"}
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
* written, because they are what a merchant reads the file for; they map to
|
|
38
38
|
* "Do not import" on the way back.
|
|
39
39
|
*/
|
|
40
|
-
export { CONTACT_CSV_COLUMNS, contactCsvHeader, contactsCsv, type ContactCsvOptions, type ContactCsvRow, } from '
|
|
41
|
-
import { type ContactCsvOptions } from '
|
|
40
|
+
export { CONTACT_CSV_COLUMNS, contactCsvHeader, contactsCsv, type ContactCsvOptions, type ContactCsvRow, } from './crm-csv';
|
|
41
|
+
import { type ContactCsvOptions } from './crm-csv';
|
|
42
42
|
/**
|
|
43
43
|
* The file the Import drawer hands out to start from: the export's header
|
|
44
44
|
* and nothing under it, so a sheet filled in against it maps itself.
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
* the last interaction and the last campaign engagement — are still
|
|
36
36
|
* written, because they are what a merchant reads the file for; they map to
|
|
37
37
|
* "Do not import" on the way back.
|
|
38
|
-
*/ /* The writer
|
|
39
|
-
import { contactsCsv } from "
|
|
38
|
+
*/ /* The writer is `crm-csv.ts`, shared with the server's export — see `deals-csv.ts`. */ export { CONTACT_CSV_COLUMNS, contactCsvHeader, contactsCsv } from "./crm-csv.js";
|
|
39
|
+
import { contactsCsv } from "./crm-csv.js";
|
|
40
40
|
/**
|
|
41
41
|
* The file the Import drawer hands out to start from: the export's header
|
|
42
42
|
* and nothing under it, so a sheet filled in against it maps itself.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/contacts-csv.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 * The contacts CSV — every CRM column, the same file from the table's\n * Export button and the bulk bar's (AGL-2603, widened by AGL-2621).\n *\n * The selection's export and the table's export are the same file over a\n * different set of rows: the same columns in the same order, the same `|`\n * between multi-valued cells, the same quoting. A second column list would\n * be a second file format for one feature, and the person opening both in\n * a spreadsheet would be the one to find the difference.\n *\n * ## The header row is the import's own vocabulary\n *\n * Each column is headed by a name `guessContactImportMapping` recognizes —\n * \"Job title\", \"Address line 1\", \"Lifecycle stage\", a custom field's label\n * — so an export re-imports without a hand mapping, and the template the\n * Import drawer offers IS this header over no rows. The owner column\n * carries the member's ADDRESS rather than a uid for the same reason: the\n * import resolves an owner by email, and a uid is nothing a spreadsheet\n * can read anyway. The three columns the import has no field for — sources,\n * the last interaction and the last campaign engagement — are still\n * written, because they are what a merchant reads the file for; they map to\n * \"Do not import\" on the way back.\n */\n\n/* The writer
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/contacts-csv.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 * The contacts CSV — every CRM column, the same file from the table's\n * Export button and the bulk bar's (AGL-2603, widened by AGL-2621).\n *\n * The selection's export and the table's export are the same file over a\n * different set of rows: the same columns in the same order, the same `|`\n * between multi-valued cells, the same quoting. A second column list would\n * be a second file format for one feature, and the person opening both in\n * a spreadsheet would be the one to find the difference.\n *\n * ## The header row is the import's own vocabulary\n *\n * Each column is headed by a name `guessContactImportMapping` recognizes —\n * \"Job title\", \"Address line 1\", \"Lifecycle stage\", a custom field's label\n * — so an export re-imports without a hand mapping, and the template the\n * Import drawer offers IS this header over no rows. The owner column\n * carries the member's ADDRESS rather than a uid for the same reason: the\n * import resolves an owner by email, and a uid is nothing a spreadsheet\n * can read anyway. The three columns the import has no field for — sources,\n * the last interaction and the last campaign engagement — are still\n * written, because they are what a merchant reads the file for; they map to\n * \"Do not import\" on the way back.\n */\n\n/* The writer is `crm-csv.ts`, shared with the server's export — see `deals-csv.ts`. */\nexport {\n CONTACT_CSV_COLUMNS,\n contactCsvHeader,\n contactsCsv,\n type ContactCsvOptions,\n type ContactCsvRow,\n} from './crm-csv'\n\nimport { contactsCsv, type ContactCsvOptions } from './crm-csv'\n\n/**\n * The file the Import drawer hands out to start from: the export's header\n * and nothing under it, so a sheet filled in against it maps itself.\n */\nexport function contactImportTemplateCsv(\n customFields: ContactCsvOptions['customFields'] = [],\n): string {\n return contactsCsv([], { customFields })\n}\n\n/**\n * Hand the browser a file to save.\n *\n * The one writer that stayed: it touches `URL.createObjectURL` and\n * `document`, so it belongs to the browser and not to the library a server\n * route imports.\n */\nexport function downloadTextFile(name: string, mime: string, text: string): void {\n const url = URL.createObjectURL(new Blob([text], { type: mime }))\n const anchor = document.createElement('a')\n anchor.href = url\n anchor.download = name\n anchor.click()\n URL.revokeObjectURL(url)\n}\n"],"names":["CONTACT_CSV_COLUMNS","contactCsvHeader","contactsCsv","contactImportTemplateCsv","customFields","downloadTextFile","name","mime","text","url","URL","createObjectURL","Blob","type","anchor","document","createElement","href","download","click","revokeObjectURL"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;CAsBC,GAED,qFAAqF,GACrF,SACEA,mBAAmB,EACnBC,gBAAgB,EAChBC,WAAW,QAGN,eAAW;AAElB,SAASA,WAAW,QAAgC,eAAW;AAE/D;;;CAGC,GACD,OAAO,SAASC,yBACdC,eAAkD,EAAE;IAEpD,OAAOF,YAAY,EAAE,EAAE;QAAEE;IAAa;AACxC;AAEA;;;;;;CAMC,GACD,OAAO,SAASC,iBAAiBC,IAAY,EAAEC,IAAY,EAAEC,IAAY;IACvE,MAAMC,MAAMC,IAAIC,eAAe,CAAC,IAAIC,KAAK;QAACJ;KAAK,EAAE;QAAEK,MAAMN;IAAK;IAC9D,MAAMO,SAASC,SAASC,aAAa,CAAC;IACtCF,OAAOG,IAAI,GAAGR;IACdK,OAAOI,QAAQ,GAAGZ;IAClBQ,OAAOK,KAAK;IACZT,IAAIU,eAAe,CAACX;AACtB"}
|
|
@@ -0,0 +1,239 @@
|
|
|
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
|
+
/**
|
|
18
|
+
* EVERY CRM CSV, WRITTEN ONCE (AGL-2662).
|
|
19
|
+
*
|
|
20
|
+
* ## Why these moved out of the plugin
|
|
21
|
+
*
|
|
22
|
+
* Each CRM section already wrote a file from the rows it had LOADED, and
|
|
23
|
+
* the whole-collection export writes the same file from the server over
|
|
24
|
+
* every row there is. The console app may not import a feature plugin —
|
|
25
|
+
* plugins reach an app only through the generated loader manifests — so a
|
|
26
|
+
* server route could not have called the plugin's writers, and writing a
|
|
27
|
+
* second set would have given one feature two file formats, discovered by
|
|
28
|
+
* whichever merchant opened both in a spreadsheet.
|
|
29
|
+
*
|
|
30
|
+
* So the columns and the cells live here, in the library both sides may
|
|
31
|
+
* import. The plugin's `contacts-csv.ts`, `companies-csv.ts`,
|
|
32
|
+
* `deals-csv.ts`, `tasks-csv.ts` and `leads-csv.ts` keep their names and
|
|
33
|
+
* delegate: the sections import what they always did, and there is one
|
|
34
|
+
* implementation under all of it. Their specs are unchanged and are the
|
|
35
|
+
* proof that the move changed no byte of any file.
|
|
36
|
+
*
|
|
37
|
+
* ## What a resolver is for, and what an absent one means
|
|
38
|
+
*
|
|
39
|
+
* A stored document holds ids — an owner uid, a pipeline id, a linked
|
|
40
|
+
* record id — and a spreadsheet cannot read an id. Each writer therefore
|
|
41
|
+
* takes optional resolvers, and every one of them degrades the same way:
|
|
42
|
+
* ABSENT resolver writes the id itself rather than an empty cell, because
|
|
43
|
+
* a row whose owner is a uid is still a row whose owner can be found, and
|
|
44
|
+
* a blank is a claim that nobody owns it.
|
|
45
|
+
*
|
|
46
|
+
* The owner columns carry an ADDRESS where they can, which is also what
|
|
47
|
+
* the contacts and companies imports resolve an owner by — so an exported
|
|
48
|
+
* file re-imports without a hand mapping.
|
|
49
|
+
*/
|
|
50
|
+
import type { AglynPostalAddress } from '@aglyn/aglyn';
|
|
51
|
+
import { type ContactCustomValue, type ContactFieldDefinition, type ContactLifecycleStage, type CrmCompany, type CrmLeadFields, type CrmTask } from '@aglyn/aglyn/app-utils/crm';
|
|
52
|
+
/** Cents as a major-unit decimal — `125000` → `1250.00`; `''` for none. */
|
|
53
|
+
export declare function csvAmount(cents: number | null | undefined): string;
|
|
54
|
+
/** An epoch as an ISO timestamp, or `''` for none. */
|
|
55
|
+
export declare function csvInstant(ms: number | null | undefined): string;
|
|
56
|
+
/**
|
|
57
|
+
* An epoch as a local calendar DATE — `YYYY-MM-DD`.
|
|
58
|
+
*
|
|
59
|
+
* For the fields that are a day rather than a moment: an expected close is
|
|
60
|
+
* stored at local noon precisely so the day survives a timezone round trip,
|
|
61
|
+
* and writing it as an instant would hand a spreadsheet a time nobody
|
|
62
|
+
* chose. The same arithmetic the deal drawer's date field uses, so the
|
|
63
|
+
* file and the field read one day.
|
|
64
|
+
*/
|
|
65
|
+
export declare function csvLocalDate(ms: number | null | undefined): string;
|
|
66
|
+
/** Epoch millis or a Firestore timestamp as an ISO instant, or `''`. */
|
|
67
|
+
export declare function csvStoredInstant(value: unknown): string;
|
|
68
|
+
/** As much of a projected contact row as the file reads. */
|
|
69
|
+
export interface ContactCsvRow {
|
|
70
|
+
email?: string;
|
|
71
|
+
name?: string;
|
|
72
|
+
phone?: string;
|
|
73
|
+
jobTitle?: string;
|
|
74
|
+
companyName?: string;
|
|
75
|
+
ownerUid?: string;
|
|
76
|
+
lifecycleStage?: ContactLifecycleStage | '';
|
|
77
|
+
address?: AglynPostalAddress | null;
|
|
78
|
+
tags?: string[];
|
|
79
|
+
sources?: Record<string, unknown>;
|
|
80
|
+
interactions?: Array<{
|
|
81
|
+
atMs: number;
|
|
82
|
+
}>;
|
|
83
|
+
/** The last open or click on one of the site's campaigns (AGL-2616). */
|
|
84
|
+
lastEmailEngagementAtMs?: number;
|
|
85
|
+
notes?: string;
|
|
86
|
+
/** This holder's custom values, keyed by definition key. */
|
|
87
|
+
custom?: Record<string, ContactCustomValue>;
|
|
88
|
+
}
|
|
89
|
+
export interface ContactCsvOptions {
|
|
90
|
+
/**
|
|
91
|
+
* The owner's address for a stored uid — what the import resolves an
|
|
92
|
+
* owner by. Absent, the uid is written as it is.
|
|
93
|
+
*/
|
|
94
|
+
ownerEmail?: (uid: string) => string;
|
|
95
|
+
/** The org's custom fields, one column each, headed by the field's label. */
|
|
96
|
+
customFields?: readonly Pick<ContactFieldDefinition, 'key' | 'label'>[];
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The standard columns, in order, headed as the import reads them. A
|
|
100
|
+
* custom field's column follows these, headed by its label.
|
|
101
|
+
*/
|
|
102
|
+
export declare const CONTACT_CSV_COLUMNS: readonly ["Email", "Name", "Phone", "Job title", "Company", "Owner", "Lifecycle stage", "Address line 1", "Address line 2", "City", "State", "Postal code", "Country", "Tags", "Sources", "Last interaction", "Last engaged", "Notes"];
|
|
103
|
+
/** The header row, with one column per custom field after the standard ones. */
|
|
104
|
+
export declare function contactCsvHeader(customFields?: ContactCsvOptions['customFields']): string[];
|
|
105
|
+
/** One contact's cells, in the header's order. */
|
|
106
|
+
export declare function contactCsvCells(contact: ContactCsvRow, options?: ContactCsvOptions): unknown[];
|
|
107
|
+
/** The whole file, header first. */
|
|
108
|
+
export declare function contactsCsv(rows: readonly ContactCsvRow[], options?: ContactCsvOptions): string;
|
|
109
|
+
/** As much of a company row as the file reads. */
|
|
110
|
+
export type CompanyCsvRow = Partial<Pick<CrmCompany, 'name' | 'domain' | 'website' | 'phone' | 'industry' | 'ownerUid' | 'tags' | 'notes' | 'contactsCount'>> & {
|
|
111
|
+
address?: AglynPostalAddress | null;
|
|
112
|
+
};
|
|
113
|
+
export interface CompanyCsvOptions {
|
|
114
|
+
/** The owner's address for a stored uid; absent, the uid is written. */
|
|
115
|
+
ownerEmail?: (uid: string) => string;
|
|
116
|
+
}
|
|
117
|
+
/** The columns, in order, headed as the import reads them. */
|
|
118
|
+
export declare const COMPANY_CSV_COLUMNS: readonly ["Company", "Domain", "Website", "Phone", "Industry", "Owner", "Address line 1", "Address line 2", "City", "State", "Postal code", "Country", "Tags", "Notes", "Contacts"];
|
|
119
|
+
/** One company's cells, in the header's order. */
|
|
120
|
+
export declare function companyCsvCells(company: CompanyCsvRow, options?: CompanyCsvOptions): unknown[];
|
|
121
|
+
/** The whole file, header first. */
|
|
122
|
+
export declare function companiesCsv(rows: readonly CompanyCsvRow[], options?: CompanyCsvOptions): string;
|
|
123
|
+
/** The currency a deal with no code of its own is read in. */
|
|
124
|
+
export declare const CSV_DEFAULT_DEAL_CURRENCY = "usd";
|
|
125
|
+
/** How a deal's `status` reads in a file. */
|
|
126
|
+
export declare const CSV_DEAL_STATUS_LABELS: Record<string, string>;
|
|
127
|
+
/** As much of a deal row as the file reads. */
|
|
128
|
+
export interface DealCsvRow {
|
|
129
|
+
title?: string;
|
|
130
|
+
pipelineId?: string;
|
|
131
|
+
stageId?: string;
|
|
132
|
+
status?: string;
|
|
133
|
+
amountCents?: number;
|
|
134
|
+
currency?: string;
|
|
135
|
+
ownerUid?: string;
|
|
136
|
+
expectedCloseAtMs?: number | null;
|
|
137
|
+
closedAtMs?: number | null;
|
|
138
|
+
contactName?: string;
|
|
139
|
+
companyName?: string;
|
|
140
|
+
lostReason?: string;
|
|
141
|
+
notes?: string;
|
|
142
|
+
}
|
|
143
|
+
export interface DealCsvOptions {
|
|
144
|
+
/** The pipeline's name for a stored id; absent, the id is written. */
|
|
145
|
+
pipelineName?: (pipelineId: string) => string | undefined;
|
|
146
|
+
/** The stage's name within its pipeline; absent, the stage id is written. */
|
|
147
|
+
stageName?: (pipelineId: string, stageId: string) => string | undefined;
|
|
148
|
+
/** The owner's address for a stored uid; absent, the uid is written. */
|
|
149
|
+
ownerEmail?: (uid: string) => string;
|
|
150
|
+
}
|
|
151
|
+
export declare const DEAL_CSV_COLUMNS: readonly ["Title", "Pipeline", "Stage", "Amount", "Currency", "Owner", "Expected close", "Status", "Contact", "Company", "Closed", "Lost reason", "Notes"];
|
|
152
|
+
/** One deal's cells, in the header's order. */
|
|
153
|
+
export declare function dealCsvCells(deal: DealCsvRow, options?: DealCsvOptions): unknown[];
|
|
154
|
+
/** The whole file, header first. */
|
|
155
|
+
export declare function dealsCsv(rows: readonly DealCsvRow[], options?: DealCsvOptions): string;
|
|
156
|
+
/** As much of a task row as the file reads. */
|
|
157
|
+
export type TaskCsvRow = Partial<Pick<CrmTask, 'title' | 'kind' | 'priority' | 'status' | 'dueAtMs' | 'completedAtMs' | 'assigneeUid' | 'contactId' | 'companyId' | 'dealId' | 'notes'>>;
|
|
158
|
+
export interface TaskCsvOptions {
|
|
159
|
+
/** The assignee's address for a stored uid; absent, the uid is written. */
|
|
160
|
+
assigneeEmail?: (uid: string) => string;
|
|
161
|
+
/** What a linked record is called; absent, the id is written. */
|
|
162
|
+
recordName?: (kind: 'contact' | 'company' | 'deal', id: string) => string | undefined;
|
|
163
|
+
}
|
|
164
|
+
export declare const TASK_CSV_COLUMNS: readonly ["Title", "Kind", "Priority", "Status", "Due", "Assignee", "Contact", "Company", "Deal", "Completed", "Notes"];
|
|
165
|
+
/** One task's cells, in the header's order. */
|
|
166
|
+
export declare function taskCsvCells(task: TaskCsvRow, options?: TaskCsvOptions): unknown[];
|
|
167
|
+
/** The whole file, header first. */
|
|
168
|
+
export declare function tasksCsv(rows: readonly TaskCsvRow[], options?: TaskCsvOptions): string;
|
|
169
|
+
/** How one capture surface reads in a file. */
|
|
170
|
+
export declare function csvLeadSourceLabel(source: string): string;
|
|
171
|
+
/** Every surface that produced a capture — the array, or the older single field. */
|
|
172
|
+
export declare function csvLeadSources(lead: Record<string, unknown>): string[];
|
|
173
|
+
/** As much of a lead row as the file reads. */
|
|
174
|
+
export type LeadCsvRow = Record<string, unknown> & Pick<CrmLeadFields, 'status' | 'ownerUid' | 'notes' | 'unqualifiedReason' | 'convertedAtMs' | 'company' | 'jobTitle' | 'phone' | 'website' | 'address' | 'tags' | 'leadSource'> & {
|
|
175
|
+
/** The site the lead lives under — what the `Site` column names. */
|
|
176
|
+
hostId?: string;
|
|
177
|
+
};
|
|
178
|
+
export interface LeadCsvOptions {
|
|
179
|
+
/** The owner's address for a stored uid; absent, the uid is written. */
|
|
180
|
+
ownerEmail?: (uid: string) => string;
|
|
181
|
+
/**
|
|
182
|
+
* The site's name for a row's `hostId`. Given, the file carries a `Site`
|
|
183
|
+
* column — the organization-level file; absent, it does not.
|
|
184
|
+
*/
|
|
185
|
+
siteName?: (hostId: string) => string | undefined;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* The columns every leads file carries, in the list's order.
|
|
189
|
+
*
|
|
190
|
+
* The lead's own profile (AGL-3231) — company, title, phone, website, the
|
|
191
|
+
* address parts, tags and the lead source — sits where the contacts file
|
|
192
|
+
* keeps the same facts, under the same headers, so one spreadsheet of
|
|
193
|
+
* people reads into either import with the same mapping.
|
|
194
|
+
*/
|
|
195
|
+
export declare const LEAD_CSV_COLUMNS: readonly ["Email", "Name", "Company", "Job title", "Phone", "Website", "Status", "Owner", "Lead source", "Sources", "First seen", "Last seen", "Captures", "Address line 1", "Address line 2", "City", "State", "Postal code", "Country", "Tags", "Unqualified reason", "Converted", "Notes"];
|
|
196
|
+
/** The header row: the standard columns, with `Site` after `Owner` at the org level. */
|
|
197
|
+
export declare function leadCsvHeader(options?: LeadCsvOptions): string[];
|
|
198
|
+
/** One lead's cells, in the header's order. */
|
|
199
|
+
export declare function leadCsvCells(lead: LeadCsvRow, options?: LeadCsvOptions): unknown[];
|
|
200
|
+
/** The whole file, header first. */
|
|
201
|
+
export declare function leadsCsv(rows: readonly LeadCsvRow[], options?: LeadCsvOptions): string;
|
|
202
|
+
/** The five tables a whole-collection export can be asked for. */
|
|
203
|
+
export declare const CRM_EXPORT_RESOURCES: readonly ["contacts", "companies", "deals", "tasks", "leads"];
|
|
204
|
+
export type CrmExportResource = (typeof CRM_EXPORT_RESOURCES)[number];
|
|
205
|
+
export declare function isCrmExportResource(value: unknown): value is CrmExportResource;
|
|
206
|
+
/** Every resolver any of the five writers takes, in one bag. */
|
|
207
|
+
export type CrmExportOptions = ContactCsvOptions & DealCsvOptions & TaskCsvOptions & LeadCsvOptions & {
|
|
208
|
+
assigneeEmail?: (uid: string) => string;
|
|
209
|
+
};
|
|
210
|
+
/** The header row one resource's file carries. */
|
|
211
|
+
export declare function crmExportHeader(resource: CrmExportResource, options?: CrmExportOptions): string[];
|
|
212
|
+
/**
|
|
213
|
+
* One stored document's cells for one resource's file.
|
|
214
|
+
*
|
|
215
|
+
* The bridge the server route stands on: it holds a raw document rather
|
|
216
|
+
* than the projected row a list built, and this is the one place that
|
|
217
|
+
* turns the first into the second — so the streamed file and the file the
|
|
218
|
+
* Export button writes are the same columns filled the same way.
|
|
219
|
+
*/
|
|
220
|
+
export declare function crmExportCells(resource: CrmExportResource, document: Record<string, unknown>, options?: CrmExportOptions): unknown[];
|
|
221
|
+
/** What the contacts file needs to know about the holder it is written for. */
|
|
222
|
+
export interface CrmExportContactHolder {
|
|
223
|
+
/** The consent group whose facet the file reads — a facet is per holder. */
|
|
224
|
+
groupId: string;
|
|
225
|
+
/** The sites in that group, which bound the timeline the file reads. */
|
|
226
|
+
hostIds: readonly string[];
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* A stored contact document as the contacts file reads it.
|
|
230
|
+
*
|
|
231
|
+
* The projection the Contacts list makes before it writes its own file,
|
|
232
|
+
* spelled here so the server can make the same one: a contact document
|
|
233
|
+
* holds one facet per HOLDER, and every field the file writes but the
|
|
234
|
+
* address itself — the phone, the company, the tags, the notes, the
|
|
235
|
+
* lifecycle stage, the custom values — lives in the viewing holder's facet.
|
|
236
|
+
* Reading them off the top of the document is how one business's file ends
|
|
237
|
+
* up carrying another's notes.
|
|
238
|
+
*/
|
|
239
|
+
export declare function contactCsvRowFromDoc(document: Record<string, unknown>, holder: CrmExportContactHolder): ContactCsvRow;
|