@aglyn/plugins-outreach 1.0.0-beta.200 → 1.0.0-beta.201
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 -14
- package/src/lib/components/enrollment-actions.d.ts +33 -0
- package/src/lib/components/enrollment-actions.js +174 -0
- package/src/lib/components/enrollment-actions.js.map +1 -0
- package/src/lib/components/enrollment-detail.d.ts +21 -0
- package/src/lib/components/enrollment-detail.js +870 -0
- package/src/lib/components/enrollment-detail.js.map +1 -0
- package/src/lib/components/enrollment-filter-params.d.ts +58 -0
- package/src/lib/components/enrollment-filter-params.js +98 -0
- package/src/lib/components/enrollment-filter-params.js.map +1 -0
- package/src/lib/components/enrollments-table.d.ts +21 -0
- package/src/lib/components/enrollments-table.js +249 -210
- package/src/lib/components/enrollments-table.js.map +1 -1
- package/src/lib/components/outreach-console-page.js +10 -0
- package/src/lib/components/outreach-console-page.js.map +1 -1
- package/src/lib/components/sequence-detail.d.ts +2 -0
- package/src/lib/components/sequence-detail.js +70 -8
- package/src/lib/components/sequence-detail.js.map +1 -1
- package/src/lib/components/sequence-report-card.d.ts +17 -0
- package/src/lib/components/sequence-report-card.js +63 -15
- package/src/lib/components/sequence-report-card.js.map +1 -1
- package/src/lib/components/sequences-section.d.ts +4 -1
- package/src/lib/components/sequences-section.js +12 -1
- package/src/lib/components/sequences-section.js.map +1 -1
- package/src/lib/components/use-outreach-api.d.ts +3 -1
- package/src/lib/components/use-outreach-api.js +6 -0
- package/src/lib/components/use-outreach-api.js.map +1 -1
- package/src/lib/components/use-outreach-enrollment.d.ts +18 -0
- package/src/lib/components/use-outreach-enrollment.js +133 -0
- package/src/lib/components/use-outreach-enrollment.js.map +1 -0
- package/src/lib/constants/api-routes.d.ts +2 -0
- package/src/lib/constants/api-routes.js +1 -0
- package/src/lib/constants/api-routes.js.map +1 -1
- package/src/lib/engine/gates.d.ts +4 -3
- package/src/lib/engine/gates.js.map +1 -1
- package/src/lib/engine/gateway-block.d.ts +4 -8
- package/src/lib/engine/gateway-block.js +6 -26
- package/src/lib/engine/gateway-block.js.map +1 -1
- package/src/lib/engine/mail-gateway.d.ts +18 -73
- package/src/lib/engine/mail-gateway.js +32 -206
- package/src/lib/engine/mail-gateway.js.map +1 -1
- package/src/lib/model/enrollment-engagement.d.ts +26 -2
- package/src/lib/model/enrollment-engagement.js +40 -3
- package/src/lib/model/enrollment-engagement.js.map +1 -1
- package/src/lib/model/enrollment-history.d.ts +40 -0
- package/src/lib/model/enrollment-history.js +95 -0
- package/src/lib/model/enrollment-history.js.map +1 -0
- package/src/lib/model/enrollment-list-query.d.ts +6 -0
- package/src/lib/model/enrollment-list-query.js +35 -1
- package/src/lib/model/enrollment-list-query.js.map +1 -1
- package/src/lib/model/enrollment-timeline.d.ts +78 -0
- package/src/lib/model/enrollment-timeline.js +430 -0
- package/src/lib/model/enrollment-timeline.js.map +1 -0
- package/src/lib/model/outreach-api.d.ts +24 -0
- package/src/lib/model/outreach-api.js.map +1 -1
- package/src/lib/model/outreach.types.d.ts +78 -29
- package/src/lib/model/outreach.types.js +33 -8
- package/src/lib/model/outreach.types.js.map +1 -1
- package/src/lib/routes/enroll-routes.js +26 -3
- package/src/lib/routes/enroll-routes.js.map +1 -1
- package/src/lib/routes/enrollment-routes.d.ts +10 -0
- package/src/lib/routes/enrollment-routes.js +82 -1
- package/src/lib/routes/enrollment-routes.js.map +1 -1
- package/src/lib/routes/register-routes.js +14 -4
- package/src/lib/routes/register-routes.js.map +1 -1
- package/src/lib/routes/route-deps.d.ts +3 -1
- package/src/lib/routes/route-deps.js.map +1 -1
- package/src/lib/runtime/click-events.d.ts +10 -5
- package/src/lib/runtime/click-events.js +45 -6
- package/src/lib/runtime/click-events.js.map +1 -1
- package/src/lib/runtime/gateway-ledger.d.ts +6 -0
- package/src/lib/runtime/gateway-ledger.js +9 -3
- package/src/lib/runtime/gateway-ledger.js.map +1 -1
- package/src/lib/runtime/host-suspension.d.ts +30 -0
- package/src/lib/runtime/host-suspension.js +35 -0
- package/src/lib/runtime/host-suspension.js.map +1 -0
- package/src/lib/runtime/person-erasure.d.ts +4 -1
- package/src/lib/runtime/person-erasure.js +14 -3
- package/src/lib/runtime/person-erasure.js.map +1 -1
- package/src/lib/runtime/platform-runtime-deps.js +34 -3
- package/src/lib/runtime/platform-runtime-deps.js.map +1 -1
- package/src/lib/runtime/runtime-deps.d.ts +27 -1
- package/src/lib/runtime/runtime-deps.js.map +1 -1
- package/src/lib/runtime/send-job.js +46 -6
- package/src/lib/runtime/send-job.js.map +1 -1
- package/src/lib/runtime/sync-job.js +4 -0
- package/src/lib/runtime/sync-job.js.map +1 -1
- package/src/lib/storage/domain-intel-store.d.ts +61 -43
- package/src/lib/storage/domain-intel-store.js +138 -226
- package/src/lib/storage/domain-intel-store.js.map +1 -1
|
@@ -34,12 +34,29 @@
|
|
|
34
34
|
* search `searchTokens`: every prefix of the person's name and of
|
|
35
35
|
* their address and its parts, stamped at enrollment by the
|
|
36
36
|
* enroll route and by `tools/scripts/backfill-outreach-list-search.mjs`
|
|
37
|
-
* for the enrollments made before it
|
|
37
|
+
* for the enrollments made before it;
|
|
38
|
+
* clicked (AGL-3332) whether the person clicked a link: `clicked`,
|
|
39
|
+
* `false` at enrollment and set by the click route's
|
|
40
|
+
* transaction on a person's click (never a scanner's), and
|
|
41
|
+
* stamped from `engagement.clicks` by the same backfill;
|
|
42
|
+
* link (AGL-3332) a destination they followed, whole:
|
|
43
|
+
* `engagement.links`, the capped array the click route keeps,
|
|
44
|
+
* asked with `array-contains-any` — the query's one array
|
|
45
|
+
* clause, so it and the search cannot stand together and the
|
|
46
|
+
* plan refuses the pair by name. The backfill seeds it with an
|
|
47
|
+
* earlier click's `lastClickUrl`, the one destination such a
|
|
48
|
+
* click is known by.
|
|
38
49
|
*
|
|
39
50
|
* The step, the next send and the last activity are not offered: each is
|
|
40
51
|
* derived from the sequence's steps or from several fields, and a query can
|
|
41
52
|
* ask none of them.
|
|
42
53
|
*/ /** The array the search box reads, stamped by the enroll route. */ export const OUTREACH_ENROLLMENT_SEARCH_TOKENS = 'searchTokens';
|
|
54
|
+
/** Whether the person clicked (AGL-3332): what the "Clicked" filter asks. */ export const OUTREACH_ENROLLMENT_CLICKED = 'clicked';
|
|
55
|
+
/** The destinations followed (AGL-3332): what the "Link followed" filter asks. */ export const OUTREACH_ENROLLMENT_LINKS_PATH = 'engagement.links';
|
|
56
|
+
/** The fields the table offers only for a sequence that counts clicks. */ export const OUTREACH_ENROLLMENT_CLICK_FIELDS = [
|
|
57
|
+
'clicked',
|
|
58
|
+
'link'
|
|
59
|
+
];
|
|
43
60
|
export const OUTREACH_ENROLLMENT_LIST_QUERY = {
|
|
44
61
|
fields: [
|
|
45
62
|
{
|
|
@@ -92,6 +109,23 @@ export const OUTREACH_ENROLLMENT_LIST_QUERY = {
|
|
|
92
109
|
'before',
|
|
93
110
|
'onOrBefore'
|
|
94
111
|
]
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
column: 'clicked',
|
|
115
|
+
kind: 'boolean',
|
|
116
|
+
path: OUTREACH_ENROLLMENT_CLICKED,
|
|
117
|
+
operators: [
|
|
118
|
+
'equals'
|
|
119
|
+
]
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
column: 'link',
|
|
123
|
+
kind: 'exact',
|
|
124
|
+
path: OUTREACH_ENROLLMENT_LINKS_PATH,
|
|
125
|
+
tokensPath: OUTREACH_ENROLLMENT_LINKS_PATH,
|
|
126
|
+
operators: [
|
|
127
|
+
'isAnyOf'
|
|
128
|
+
]
|
|
95
129
|
}
|
|
96
130
|
],
|
|
97
131
|
sorts: [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/model/enrollment-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 type {\n ListQueryDeclaration,\n ListQueryFilter,\n} from '@aglyn/shared-ui-jsx/const/list-query-plan'\n\n/*\n * WHAT A SEQUENCE'S ENROLLMENTS TABLE ASKS FIRESTORE (AGL-3321).\n *\n * One query over `orgs/{orgId}/outreachEnrollments`, narrowed to the\n * sequence (`sequenceId ==`, the list's base) and ordered newest enrolled\n * first, paged by that query. Every clause the Filters panel offers and the\n * search box are predicates on it:\n *\n * status the stored status, on every enrollment;\n * target `contact` or `lead`, on every enrollment (the ones made\n * before leads could be sequenced gain `contact` from the\n * backfill);\n * stopReason the stored reason code — the column shows its label and the\n * stop detail, the query asks by code;\n * email the whole address, stored normalized, so `equals` is exact;\n * enrolled the table's ONE range: when the person was enrolled, the\n * field the table is already ordered by, so it costs no index\n * of its own;\n * search `searchTokens`: every prefix of the person's name and of\n * their address and its parts, stamped at enrollment by the\n * enroll route and by `tools/scripts/backfill-outreach-list-search.mjs`\n * for the enrollments made before it.\n *\n * The step, the next send and the last activity are not offered: each is\n * derived from the sequence's steps or from several fields, and a query can\n * ask none of them.\n */\n\n/** The array the search box reads, stamped by the enroll route. */\nexport const OUTREACH_ENROLLMENT_SEARCH_TOKENS = 'searchTokens'\n\nexport const OUTREACH_ENROLLMENT_LIST_QUERY: ListQueryDeclaration = {\n fields: [\n { column: 'status', kind: 'exact', path: 'status', presence: 'always', operators: ['equals', 'isAnyOf'] },\n { column: 'target', kind: 'exact', path: 'target', presence: 'always', operators: ['equals'] },\n { column: 'stopReason', kind: 'exact', path: 'stopReason', operators: ['equals', 'isAnyOf'] },\n { column: 'email', kind: 'text', path: 'email', lowerPath: 'email', operators: ['equals'] },\n {\n column: 'enrolled',\n kind: 'date',\n path: 'createdAtMs',\n storedAs: 'millis',\n presence: 'always',\n operators: ['is', 'after', 'onOrAfter', 'before', 'onOrBefore'],\n },\n ],\n sorts: [{ path: 'createdAtMs', direction: 'desc' }],\n search: { tokensPath: OUTREACH_ENROLLMENT_SEARCH_TOKENS },\n}\n\n/** The scope every page of the table carries: this sequence's people. */\nexport function outreachEnrollmentListBase(sequenceId: string): ListQueryFilter[] {\n return [{ path: 'sequenceId', op: '==', value: sequenceId }]\n}\n"],"names":["OUTREACH_ENROLLMENT_SEARCH_TOKENS","OUTREACH_ENROLLMENT_LIST_QUERY","fields","column","kind","path","presence","operators","lowerPath","storedAs","sorts","direction","search","
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/model/enrollment-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 type {\n ListQueryDeclaration,\n ListQueryFilter,\n} from '@aglyn/shared-ui-jsx/const/list-query-plan'\n\n/*\n * WHAT A SEQUENCE'S ENROLLMENTS TABLE ASKS FIRESTORE (AGL-3321).\n *\n * One query over `orgs/{orgId}/outreachEnrollments`, narrowed to the\n * sequence (`sequenceId ==`, the list's base) and ordered newest enrolled\n * first, paged by that query. Every clause the Filters panel offers and the\n * search box are predicates on it:\n *\n * status the stored status, on every enrollment;\n * target `contact` or `lead`, on every enrollment (the ones made\n * before leads could be sequenced gain `contact` from the\n * backfill);\n * stopReason the stored reason code — the column shows its label and the\n * stop detail, the query asks by code;\n * email the whole address, stored normalized, so `equals` is exact;\n * enrolled the table's ONE range: when the person was enrolled, the\n * field the table is already ordered by, so it costs no index\n * of its own;\n * search `searchTokens`: every prefix of the person's name and of\n * their address and its parts, stamped at enrollment by the\n * enroll route and by `tools/scripts/backfill-outreach-list-search.mjs`\n * for the enrollments made before it;\n * clicked (AGL-3332) whether the person clicked a link: `clicked`,\n * `false` at enrollment and set by the click route's\n * transaction on a person's click (never a scanner's), and\n * stamped from `engagement.clicks` by the same backfill;\n * link (AGL-3332) a destination they followed, whole:\n * `engagement.links`, the capped array the click route keeps,\n * asked with `array-contains-any` — the query's one array\n * clause, so it and the search cannot stand together and the\n * plan refuses the pair by name. The backfill seeds it with an\n * earlier click's `lastClickUrl`, the one destination such a\n * click is known by.\n *\n * The step, the next send and the last activity are not offered: each is\n * derived from the sequence's steps or from several fields, and a query can\n * ask none of them.\n */\n\n/** The array the search box reads, stamped by the enroll route. */\nexport const OUTREACH_ENROLLMENT_SEARCH_TOKENS = 'searchTokens'\n\n/** Whether the person clicked (AGL-3332): what the \"Clicked\" filter asks. */\nexport const OUTREACH_ENROLLMENT_CLICKED = 'clicked'\n\n/** The destinations followed (AGL-3332): what the \"Link followed\" filter asks. */\nexport const OUTREACH_ENROLLMENT_LINKS_PATH = 'engagement.links'\n\n/** The fields the table offers only for a sequence that counts clicks. */\nexport const OUTREACH_ENROLLMENT_CLICK_FIELDS = ['clicked', 'link'] as const\n\nexport const OUTREACH_ENROLLMENT_LIST_QUERY: ListQueryDeclaration = {\n fields: [\n { column: 'status', kind: 'exact', path: 'status', presence: 'always', operators: ['equals', 'isAnyOf'] },\n { column: 'target', kind: 'exact', path: 'target', presence: 'always', operators: ['equals'] },\n { column: 'stopReason', kind: 'exact', path: 'stopReason', operators: ['equals', 'isAnyOf'] },\n { column: 'email', kind: 'text', path: 'email', lowerPath: 'email', operators: ['equals'] },\n {\n column: 'enrolled',\n kind: 'date',\n path: 'createdAtMs',\n storedAs: 'millis',\n presence: 'always',\n operators: ['is', 'after', 'onOrAfter', 'before', 'onOrBefore'],\n },\n { column: 'clicked', kind: 'boolean', path: OUTREACH_ENROLLMENT_CLICKED, operators: ['equals'] },\n {\n column: 'link',\n kind: 'exact',\n path: OUTREACH_ENROLLMENT_LINKS_PATH,\n tokensPath: OUTREACH_ENROLLMENT_LINKS_PATH,\n operators: ['isAnyOf'],\n },\n ],\n sorts: [{ path: 'createdAtMs', direction: 'desc' }],\n search: { tokensPath: OUTREACH_ENROLLMENT_SEARCH_TOKENS },\n}\n\n/** The scope every page of the table carries: this sequence's people. */\nexport function outreachEnrollmentListBase(sequenceId: string): ListQueryFilter[] {\n return [{ path: 'sequenceId', op: '==', value: sequenceId }]\n}\n"],"names":["OUTREACH_ENROLLMENT_SEARCH_TOKENS","OUTREACH_ENROLLMENT_CLICKED","OUTREACH_ENROLLMENT_LINKS_PATH","OUTREACH_ENROLLMENT_CLICK_FIELDS","OUTREACH_ENROLLMENT_LIST_QUERY","fields","column","kind","path","presence","operators","lowerPath","storedAs","tokensPath","sorts","direction","search","outreachEnrollmentListBase","sequenceId","op","value"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAOD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqCC,GAED,iEAAiE,GACjE,OAAO,MAAMA,oCAAoC,eAAc;AAE/D,2EAA2E,GAC3E,OAAO,MAAMC,8BAA8B,UAAS;AAEpD,gFAAgF,GAChF,OAAO,MAAMC,iCAAiC,mBAAkB;AAEhE,wEAAwE,GACxE,OAAO,MAAMC,mCAAmC;IAAC;IAAW;CAAO,CAAS;AAE5E,OAAO,MAAMC,iCAAuD;IAClEC,QAAQ;QACN;YAAEC,QAAQ;YAAUC,MAAM;YAASC,MAAM;YAAUC,UAAU;YAAUC,WAAW;gBAAC;gBAAU;aAAU;QAAC;QACxG;YAAEJ,QAAQ;YAAUC,MAAM;YAASC,MAAM;YAAUC,UAAU;YAAUC,WAAW;gBAAC;aAAS;QAAC;QAC7F;YAAEJ,QAAQ;YAAcC,MAAM;YAASC,MAAM;YAAcE,WAAW;gBAAC;gBAAU;aAAU;QAAC;QAC5F;YAAEJ,QAAQ;YAASC,MAAM;YAAQC,MAAM;YAASG,WAAW;YAASD,WAAW;gBAAC;aAAS;QAAC;QAC1F;YACEJ,QAAQ;YACRC,MAAM;YACNC,MAAM;YACNI,UAAU;YACVH,UAAU;YACVC,WAAW;gBAAC;gBAAM;gBAAS;gBAAa;gBAAU;aAAa;QACjE;QACA;YAAEJ,QAAQ;YAAWC,MAAM;YAAWC,MAAMP;YAA6BS,WAAW;gBAAC;aAAS;QAAC;QAC/F;YACEJ,QAAQ;YACRC,MAAM;YACNC,MAAMN;YACNW,YAAYX;YACZQ,WAAW;gBAAC;aAAU;QACxB;KACD;IACDI,OAAO;QAAC;YAAEN,MAAM;YAAeO,WAAW;QAAO;KAAE;IACnDC,QAAQ;QAAEH,YAAYb;IAAkC;AAC1D,EAAC;AAED,uEAAuE,GACvE,OAAO,SAASiB,2BAA2BC,UAAkB;IAC3D,OAAO;QAAC;YAAEV,MAAM;YAAcW,IAAI;YAAMC,OAAOF;QAAW;KAAE;AAC9D"}
|
|
@@ -0,0 +1,78 @@
|
|
|
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 { OutreachClickMachineReason } from '../engine/click-tracking';
|
|
18
|
+
import { type OutreachMailGateway } from '../engine/mail-gateway';
|
|
19
|
+
import { type OutreachEnrollment, type OutreachEnrollmentHistoryEntry, type OutreachSequenceStep } from './outreach.types';
|
|
20
|
+
/** What kind of thing an entry is, which the view draws an icon for. */
|
|
21
|
+
export type OutreachTimelineKind = 'enrolled' | 'curated' | 'sent' | 'task' | 'click' | 'scanner' | 'earlier-clicks' | 'paused' | 'resumed' | 'stopped' | 'do-not-contact' | 'replied' | 'bounced' | 'opted-out' | 'held' | 'released' | 'failed' | 'finished';
|
|
22
|
+
/** How loudly the view draws an entry. */
|
|
23
|
+
export type OutreachTimelineTone = 'default' | 'success' | 'info' | 'warning' | 'error';
|
|
24
|
+
/** One line of the timeline. */
|
|
25
|
+
export interface OutreachTimelineEntry {
|
|
26
|
+
/** Stable across renders, for React and for a spec. */
|
|
27
|
+
key: string;
|
|
28
|
+
kind: OutreachTimelineKind;
|
|
29
|
+
atMs: number;
|
|
30
|
+
/** What happened, in a few words. */
|
|
31
|
+
title: string;
|
|
32
|
+
/** The one fact that matters most, shown in full: a subject, a reason, a diagnostic. */
|
|
33
|
+
detail?: string | null;
|
|
34
|
+
/** The rest, each short: the step, who, a scanner's reason. */
|
|
35
|
+
facts: string[];
|
|
36
|
+
/** A destination the person followed, shown whole and linkable. */
|
|
37
|
+
url?: string | null;
|
|
38
|
+
/** What `url` is, when it is not simply "the link they followed". */
|
|
39
|
+
urlLabel?: string;
|
|
40
|
+
/** The Gmail thread this entry opened, for an "Open in Gmail" link. */
|
|
41
|
+
gmailThreadId?: string | null;
|
|
42
|
+
tone: OutreachTimelineTone;
|
|
43
|
+
}
|
|
44
|
+
export interface OutreachTimelineInput {
|
|
45
|
+
enrollment: OutreachEnrollment;
|
|
46
|
+
steps: readonly OutreachSequenceStep[];
|
|
47
|
+
/** The history rows, newest or oldest first; `null` while unread or unreadable. */
|
|
48
|
+
history: readonly OutreachEnrollmentHistoryEntry[] | null;
|
|
49
|
+
/** A member's name for a stored uid; the uid itself when the roster does not know it. */
|
|
50
|
+
memberName: (uid: string) => string;
|
|
51
|
+
/** A moment as the reader reads it — in the mailbox's zone. */
|
|
52
|
+
formatTime: (ms: number) => string;
|
|
53
|
+
/** The mail gateway in front of the person's domain, when it is known. */
|
|
54
|
+
gateway?: OutreachMailGateway | null;
|
|
55
|
+
}
|
|
56
|
+
/** "Email 2 · step 3" for an email step, "Step 2 · Call" for a task, "Step 4" for one the sequence no longer has. */
|
|
57
|
+
export declare function outreachStepName(steps: readonly OutreachSequenceStep[], stepIndex: number): string;
|
|
58
|
+
/** Why a click was read as a scanner's, as the timeline says it. */
|
|
59
|
+
export declare const OUTREACH_MACHINE_REASON_LABELS: Record<OutreachClickMachineReason, string>;
|
|
60
|
+
/**
|
|
61
|
+
* The timeline, newest first — see the module note.
|
|
62
|
+
*/
|
|
63
|
+
export declare function outreachEnrollmentTimeline(input: OutreachTimelineInput): OutreachTimelineEntry[];
|
|
64
|
+
/** The five figures above the timeline. */
|
|
65
|
+
export interface OutreachEnrollmentFigures {
|
|
66
|
+
emailsSent: number;
|
|
67
|
+
clicks: number;
|
|
68
|
+
/** Distinct destinations followed; `atLeast` when only a floor is known. */
|
|
69
|
+
linksFollowed: {
|
|
70
|
+
count: number;
|
|
71
|
+
atLeast: boolean;
|
|
72
|
+
};
|
|
73
|
+
scannerClicks: number;
|
|
74
|
+
replies: number;
|
|
75
|
+
/** Whether anything has happened to them beyond being enrolled. */
|
|
76
|
+
anything: boolean;
|
|
77
|
+
}
|
|
78
|
+
export declare function outreachEnrollmentFigures(enrollment: OutreachEnrollment): OutreachEnrollmentFigures;
|
|
@@ -0,0 +1,430 @@
|
|
|
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
|
+
* EVERYTHING THAT HAPPENED TO ONE PERSON IN ONE SEQUENCE (AGL-3332).
|
|
18
|
+
*
|
|
19
|
+
* The enrollment's detail view is one list, newest first, and this builds
|
|
20
|
+
* it from what is stored — the enrollment itself, and its history rows —
|
|
21
|
+
* without inventing anything that is not:
|
|
22
|
+
*
|
|
23
|
+
* - **Each send** is a step record: the step, the time, the subject as
|
|
24
|
+
* sent, and whether it went out as the person's curated copy.
|
|
25
|
+
* - **Each click** is a history row: the destination, the step whose email
|
|
26
|
+
* carried it, and a person's or a scanner's. Clicks from before the
|
|
27
|
+
* history began exist only as totals, and are ONE row saying so — never
|
|
28
|
+
* a row per click, which would be a fabrication with a timestamp on it.
|
|
29
|
+
* - **Each member's act** — pause, resume, stop, do-not-contact — is a
|
|
30
|
+
* history row with who and why. An enrollment holds only its current
|
|
31
|
+
* stop, so the row is the only record of a pause that was resumed.
|
|
32
|
+
* - **The current stop** — a reply, a bounce, an opt-out, a hold — comes
|
|
33
|
+
* from the enrollment's own fields, with its full reason, unless a
|
|
34
|
+
* history row already says it.
|
|
35
|
+
* - **The gateway hold** (AGL-3326), and its release.
|
|
36
|
+
* - **The enrollment**, with who enrolled them, and each curated step.
|
|
37
|
+
*
|
|
38
|
+
* Client-safe and free of React: the view renders these entries, and the
|
|
39
|
+
* spec reads them.
|
|
40
|
+
*=========================================*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
41
|
+
import { OUTREACH_MAIL_GATEWAY_LABELS } from "../engine/mail-gateway.js";
|
|
42
|
+
import { outreachClickSummary } from "./enrollment-engagement.js";
|
|
43
|
+
import { OUTREACH_ENROLLMENT_HISTORY_MAX, OUTREACH_TASK_KIND_LABELS } from "./outreach.types.js";
|
|
44
|
+
/** "Email 2 · step 3" for an email step, "Step 2 · Call" for a task, "Step 4" for one the sequence no longer has. */ export function outreachStepName(steps, stepIndex) {
|
|
45
|
+
const step = steps[stepIndex];
|
|
46
|
+
if ((step == null ? void 0 : step.kind) === 'email') {
|
|
47
|
+
const position = steps.slice(0, stepIndex + 1).filter((entry)=>entry.kind === 'email').length;
|
|
48
|
+
return `Email ${position} · step ${stepIndex + 1}`;
|
|
49
|
+
}
|
|
50
|
+
if ((step == null ? void 0 : step.kind) === 'task') return `Step ${stepIndex + 1} · ${OUTREACH_TASK_KIND_LABELS[step.taskKind]}`;
|
|
51
|
+
return `Step ${stepIndex + 1}`;
|
|
52
|
+
}
|
|
53
|
+
/** Why a click was read as a scanner's, as the timeline says it. */ export const OUTREACH_MACHINE_REASON_LABELS = {
|
|
54
|
+
agent: 'the request named itself as a scanner or a link preview',
|
|
55
|
+
too_soon: 'it came within 30 seconds of the email arriving, faster than anyone reads',
|
|
56
|
+
method: 'it checked the link without opening the page'
|
|
57
|
+
};
|
|
58
|
+
const ACTION_TITLES = {
|
|
59
|
+
pause: 'Paused',
|
|
60
|
+
resume: 'Resumed',
|
|
61
|
+
stop: 'Stopped by a member',
|
|
62
|
+
do_not_contact: 'Marked do-not-contact'
|
|
63
|
+
};
|
|
64
|
+
const ACTION_KINDS = {
|
|
65
|
+
pause: 'paused',
|
|
66
|
+
resume: 'resumed',
|
|
67
|
+
stop: 'stopped',
|
|
68
|
+
do_not_contact: 'do-not-contact'
|
|
69
|
+
};
|
|
70
|
+
/** Which of two entries at the same moment reads first: the outcome above what led to it. */ const TIE_ORDER = {
|
|
71
|
+
finished: 0,
|
|
72
|
+
replied: 1,
|
|
73
|
+
bounced: 1,
|
|
74
|
+
'opted-out': 1,
|
|
75
|
+
failed: 1,
|
|
76
|
+
stopped: 2,
|
|
77
|
+
'do-not-contact': 2,
|
|
78
|
+
paused: 2,
|
|
79
|
+
held: 2,
|
|
80
|
+
released: 3,
|
|
81
|
+
resumed: 3,
|
|
82
|
+
click: 4,
|
|
83
|
+
scanner: 4,
|
|
84
|
+
'earlier-clicks': 4,
|
|
85
|
+
task: 5,
|
|
86
|
+
sent: 5,
|
|
87
|
+
curated: 6,
|
|
88
|
+
enrolled: 7
|
|
89
|
+
};
|
|
90
|
+
const plural = (count, one, many)=>`${count} ${count === 1 ? one : many}`;
|
|
91
|
+
const byWhom = (uid, memberName)=>uid ? `By ${memberName(uid)}` : null;
|
|
92
|
+
/** The step records that are emails, in the order they went. */ function emailRecords(enrollment) {
|
|
93
|
+
var _enrollment_stepRecords;
|
|
94
|
+
return ((_enrollment_stepRecords = enrollment.stepRecords) != null ? _enrollment_stepRecords : []).filter((record)=>(record == null ? void 0 : record.kind) === 'email');
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The timeline, newest first — see the module note.
|
|
98
|
+
*/ export function outreachEnrollmentTimeline(input) {
|
|
99
|
+
var _input_history, _enrollment_stepOverrides, _enrollment_stepRecords;
|
|
100
|
+
const { enrollment, steps, memberName } = input;
|
|
101
|
+
const entries = [];
|
|
102
|
+
const history = (_input_history = input.history) != null ? _input_history : [];
|
|
103
|
+
const gatewayName = input.gateway ? OUTREACH_MAIL_GATEWAY_LABELS[input.gateway] : null;
|
|
104
|
+
// ── The enrollment ──────────────────────────────────────────────────────
|
|
105
|
+
if (enrollment.createdAtMs) {
|
|
106
|
+
entries.push({
|
|
107
|
+
key: 'enrolled',
|
|
108
|
+
kind: 'enrolled',
|
|
109
|
+
atMs: enrollment.createdAtMs,
|
|
110
|
+
title: 'Enrolled',
|
|
111
|
+
facts: [
|
|
112
|
+
byWhom(enrollment.enrolledByUid, memberName),
|
|
113
|
+
enrollment.target === 'lead' ? 'As a lead' : 'As a contact',
|
|
114
|
+
enrollment.cold ? 'Cold — confirmed by the member who enrolled them' : null
|
|
115
|
+
].filter((fact)=>Boolean(fact)),
|
|
116
|
+
tone: 'default'
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
// ── Curated copies (AGL-3324) ───────────────────────────────────────────
|
|
120
|
+
for (const [key, override] of Object.entries((_enrollment_stepOverrides = enrollment.stepOverrides) != null ? _enrollment_stepOverrides : {})){
|
|
121
|
+
if (!override.draftedAtMs) continue;
|
|
122
|
+
entries.push({
|
|
123
|
+
key: `curated-${key}`,
|
|
124
|
+
kind: 'curated',
|
|
125
|
+
atMs: override.draftedAtMs,
|
|
126
|
+
title: `Curated ${outreachStepName(steps, Number(key))}`,
|
|
127
|
+
facts: [
|
|
128
|
+
override.source === 'ai' ? override.edited ? 'AI draft, edited' : 'AI draft' : 'Written by a member',
|
|
129
|
+
byWhom(override.draftedByUid, memberName)
|
|
130
|
+
].filter((fact)=>Boolean(fact)),
|
|
131
|
+
tone: 'default'
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
// ── Sends and tasks ─────────────────────────────────────────────────────
|
|
135
|
+
let previousThread = null;
|
|
136
|
+
((_enrollment_stepRecords = enrollment.stepRecords) != null ? _enrollment_stepRecords : []).forEach((record, index)=>{
|
|
137
|
+
var _record_gmailThreadId;
|
|
138
|
+
var _record_links;
|
|
139
|
+
if (!record || typeof record.atMs !== 'number') return;
|
|
140
|
+
if (record.kind === 'task') {
|
|
141
|
+
var _steps_find;
|
|
142
|
+
const step = (_steps_find = steps.find((entry)=>entry.id === record.stepId)) != null ? _steps_find : steps[record.stepIndex];
|
|
143
|
+
entries.push({
|
|
144
|
+
key: `task-${index}`,
|
|
145
|
+
kind: 'task',
|
|
146
|
+
atMs: record.atMs,
|
|
147
|
+
title: (step == null ? void 0 : step.kind) === 'task' ? `${OUTREACH_TASK_KIND_LABELS[step.taskKind]} task filed` : 'Task filed',
|
|
148
|
+
detail: (step == null ? void 0 : step.kind) === 'task' ? step.title : null,
|
|
149
|
+
facts: [
|
|
150
|
+
outreachStepName(steps, record.stepIndex),
|
|
151
|
+
record.taskId === null ? 'The CRM could not file it' : null
|
|
152
|
+
].filter((fact)=>Boolean(fact)),
|
|
153
|
+
tone: 'default'
|
|
154
|
+
});
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const thread = (_record_gmailThreadId = record.gmailThreadId) != null ? _record_gmailThreadId : null;
|
|
158
|
+
// The link to Gmail sits on the email that STARTED a thread, once.
|
|
159
|
+
const startsThread = Boolean(thread) && thread !== previousThread;
|
|
160
|
+
previousThread = thread != null ? thread : previousThread;
|
|
161
|
+
entries.push({
|
|
162
|
+
key: `sent-${index}`,
|
|
163
|
+
kind: 'sent',
|
|
164
|
+
atMs: record.atMs,
|
|
165
|
+
title: `${outreachStepName(steps, record.stepIndex)} sent`,
|
|
166
|
+
detail: record.subject ? `Subject: ${record.subject}` : null,
|
|
167
|
+
facts: [
|
|
168
|
+
record.curated === 'ai' ? 'Curated copy — AI draft' : record.curated === 'member' ? 'Curated copy — written by a member' : null,
|
|
169
|
+
((_record_links = record.links) == null ? void 0 : _record_links.length) ? plural(record.links.length, 'tracked link', 'tracked links') : null
|
|
170
|
+
].filter((fact)=>Boolean(fact)),
|
|
171
|
+
gmailThreadId: startsThread ? thread : null,
|
|
172
|
+
tone: 'default'
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
// ── Clicks ──────────────────────────────────────────────────────────────
|
|
176
|
+
for (const row of history){
|
|
177
|
+
if (row.kind !== 'click') continue;
|
|
178
|
+
entries.push(row.human ? {
|
|
179
|
+
key: `history-${row.id}`,
|
|
180
|
+
kind: 'click',
|
|
181
|
+
atMs: row.atMs,
|
|
182
|
+
title: 'Clicked a link',
|
|
183
|
+
url: row.url,
|
|
184
|
+
facts: [
|
|
185
|
+
`From ${outreachStepName(steps, row.stepIndex)}`
|
|
186
|
+
],
|
|
187
|
+
tone: 'success'
|
|
188
|
+
} : {
|
|
189
|
+
key: `history-${row.id}`,
|
|
190
|
+
kind: 'scanner',
|
|
191
|
+
atMs: row.atMs,
|
|
192
|
+
title: 'A scanner followed a link — not counted',
|
|
193
|
+
url: row.url,
|
|
194
|
+
facts: [
|
|
195
|
+
`From ${outreachStepName(steps, row.stepIndex)}`,
|
|
196
|
+
row.machineReason ? `Read as a scanner: ${OUTREACH_MACHINE_REASON_LABELS[row.machineReason]}` : null
|
|
197
|
+
].filter((fact)=>Boolean(fact)),
|
|
198
|
+
tone: 'default'
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
/*
|
|
202
|
+
* Clicks with no row of their own: counted before the per-click history
|
|
203
|
+
* began, or past its limit. ONE entry, placed at the first of them when
|
|
204
|
+
* that is known, saying what the totals hold and nothing more.
|
|
205
|
+
*/ const clicks = outreachClickSummary(enrollment.engagement);
|
|
206
|
+
const engagement = enrollment.engagement;
|
|
207
|
+
if (clicks.unloggedClicks > 0 || clicks.unloggedMachineClicks > 0) {
|
|
208
|
+
var _ref, _ref1, _ref2, _ref3, _ref4, _ref5;
|
|
209
|
+
var _emailRecords_;
|
|
210
|
+
const everyHumanUnlogged = clicks.unloggedClicks === clicks.clicks;
|
|
211
|
+
const loggedRows = clicks.clicks - clicks.unloggedClicks + (clicks.machineClicks - clicks.unloggedMachineClicks);
|
|
212
|
+
const facts = [];
|
|
213
|
+
const first = (_ref = engagement == null ? void 0 : engagement.firstClickAtMs) != null ? _ref : null;
|
|
214
|
+
const last = (_ref1 = engagement == null ? void 0 : engagement.lastClickAtMs) != null ? _ref1 : null;
|
|
215
|
+
if (clicks.unloggedClicks > 0 && first !== null && everyHumanUnlogged && last !== null) {
|
|
216
|
+
facts.push(clicks.unloggedClicks === 1 ? `At ${input.formatTime(first)}` : first === last ? `First and last at ${input.formatTime(first)}` : `First at ${input.formatTime(first)}, last at ${input.formatTime(last)}`);
|
|
217
|
+
} else if (clicks.unloggedClicks > 0 && first !== null) {
|
|
218
|
+
// Later clicks have rows of their own, so only the first is one of these.
|
|
219
|
+
facts.push(`First at ${input.formatTime(first)}`);
|
|
220
|
+
}
|
|
221
|
+
if (clicks.unloggedMachineClicks > 0) {
|
|
222
|
+
facts.push(plural(clicks.unloggedMachineClicks, 'scanner click, not counted', 'scanner clicks, not counted'));
|
|
223
|
+
}
|
|
224
|
+
entries.push({
|
|
225
|
+
key: 'earlier-clicks',
|
|
226
|
+
kind: 'earlier-clicks',
|
|
227
|
+
atMs: (_ref2 = (_ref3 = (_ref4 = clicks.unloggedClicks > 0 ? engagement == null ? void 0 : engagement.firstClickAtMs : null) != null ? _ref4 : engagement == null ? void 0 : engagement.lastClickAtMs) != null ? _ref3 : (_emailRecords_ = emailRecords(enrollment)[0]) == null ? void 0 : _emailRecords_.atMs) != null ? _ref2 : enrollment.createdAtMs,
|
|
228
|
+
title: clicks.unloggedClicks > 0 ? `${plural(clicks.unloggedClicks, 'earlier click', 'earlier clicks')}, kept as a total` : `${plural(clicks.unloggedMachineClicks, 'earlier scanner click', 'earlier scanner clicks')}, kept as a total`,
|
|
229
|
+
detail: loggedRows >= OUTREACH_ENROLLMENT_HISTORY_MAX ? `Each click is listed on its own up to ${OUTREACH_ENROLLMENT_HISTORY_MAX}; these came before per-click history began, or after this person reached that limit.` : 'These came before each click was recorded on its own, so only their count and times were kept.',
|
|
230
|
+
// The last destination is theirs only when every counted click was.
|
|
231
|
+
url: clicks.unloggedClicks > 0 && everyHumanUnlogged ? (_ref5 = engagement == null ? void 0 : engagement.lastClickUrl) != null ? _ref5 : null : null,
|
|
232
|
+
urlLabel: clicks.unloggedClicks === 1 ? 'Link' : 'Last link',
|
|
233
|
+
facts,
|
|
234
|
+
tone: clicks.unloggedClicks > 0 ? 'success' : 'default'
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
// ── Members' acts ───────────────────────────────────────────────────────
|
|
238
|
+
const actionTimes = new Set();
|
|
239
|
+
for (const row of history){
|
|
240
|
+
var _enrollment_gatewayHold;
|
|
241
|
+
if (row.kind !== 'action') continue;
|
|
242
|
+
actionTimes.add(row.atMs);
|
|
243
|
+
const released = row.action === 'resume' && ((_enrollment_gatewayHold = enrollment.gatewayHold) == null ? void 0 : _enrollment_gatewayHold.releasedAtMs) === row.atMs;
|
|
244
|
+
if (released) continue;
|
|
245
|
+
entries.push({
|
|
246
|
+
key: `history-${row.id}`,
|
|
247
|
+
kind: ACTION_KINDS[row.action],
|
|
248
|
+
atMs: row.atMs,
|
|
249
|
+
title: ACTION_TITLES[row.action],
|
|
250
|
+
detail: row.detail,
|
|
251
|
+
facts: [
|
|
252
|
+
byWhom(row.byUid, memberName)
|
|
253
|
+
].filter((fact)=>Boolean(fact)),
|
|
254
|
+
tone: row.action === 'resume' ? 'default' : row.action === 'pause' ? 'warning' : 'default'
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
// ── The gateway hold (AGL-3326) ─────────────────────────────────────────
|
|
258
|
+
const hold = enrollment.gatewayHold;
|
|
259
|
+
const holdGateway = hold ? OUTREACH_MAIL_GATEWAY_LABELS[hold.gateway] : null;
|
|
260
|
+
if (hold == null ? void 0 : hold.heldAtMs) {
|
|
261
|
+
entries.push({
|
|
262
|
+
key: 'held',
|
|
263
|
+
kind: 'held',
|
|
264
|
+
atMs: hold.heldAtMs,
|
|
265
|
+
title: `Held — ${holdGateway} refused this sender`,
|
|
266
|
+
detail: enrollment.stopReason === 'gateway_blocked_here' && enrollment.stoppedAtMs === hold.heldAtMs ? enrollment.stopDetail : null,
|
|
267
|
+
facts: [
|
|
268
|
+
'Nothing was sent to them while it was held'
|
|
269
|
+
],
|
|
270
|
+
tone: 'warning'
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
if (hold == null ? void 0 : hold.releasedAtMs) {
|
|
274
|
+
entries.push({
|
|
275
|
+
key: 'released',
|
|
276
|
+
kind: 'released',
|
|
277
|
+
atMs: hold.releasedAtMs,
|
|
278
|
+
title: hold.heldAtMs ? `Resumed past the ${holdGateway} hold — sent anyway` : `Enrolled past the ${holdGateway} warning`,
|
|
279
|
+
facts: [
|
|
280
|
+
byWhom(hold.releasedByUid, memberName)
|
|
281
|
+
].filter((fact)=>Boolean(fact)),
|
|
282
|
+
tone: 'default'
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
// ── The current stop, from the enrollment ───────────────────────────────
|
|
286
|
+
const stoppedAtMs = enrollment.stoppedAtMs;
|
|
287
|
+
const alreadySaid = stoppedAtMs !== null && (actionTimes.has(stoppedAtMs) || enrollment.stopReason === 'gateway_blocked_here' && (hold == null ? void 0 : hold.heldAtMs) === stoppedAtMs);
|
|
288
|
+
if (stoppedAtMs && !alreadySaid && enrollment.status !== 'active' && enrollment.status !== 'finished') {
|
|
289
|
+
const stop = currentStop(enrollment, memberName, gatewayName);
|
|
290
|
+
if (stop) entries.push(_extends({
|
|
291
|
+
key: 'stop',
|
|
292
|
+
atMs: stoppedAtMs
|
|
293
|
+
}, stop));
|
|
294
|
+
}
|
|
295
|
+
// ── Finished ────────────────────────────────────────────────────────────
|
|
296
|
+
if (enrollment.status === 'finished') {
|
|
297
|
+
var _enrollment_stepRecords1, _ref6;
|
|
298
|
+
const last = ((_enrollment_stepRecords1 = enrollment.stepRecords) != null ? _enrollment_stepRecords1 : []).at(-1);
|
|
299
|
+
entries.push({
|
|
300
|
+
key: 'finished',
|
|
301
|
+
kind: 'finished',
|
|
302
|
+
atMs: (_ref6 = last == null ? void 0 : last.atMs) != null ? _ref6 : enrollment.updatedAtMs,
|
|
303
|
+
title: 'Finished — every step ran',
|
|
304
|
+
facts: [],
|
|
305
|
+
tone: 'success'
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
return entries.sort((a, b)=>b.atMs - a.atMs || TIE_ORDER[a.kind] - TIE_ORDER[b.kind]);
|
|
309
|
+
}
|
|
310
|
+
/** The entry for the stop the enrollment is in now. */ function currentStop(enrollment, memberName, gatewayName) {
|
|
311
|
+
const detail = enrollment.stopDetail;
|
|
312
|
+
const by = byWhom(enrollment.stoppedByUid, memberName);
|
|
313
|
+
const facts = (...values)=>values.filter((fact)=>Boolean(fact));
|
|
314
|
+
switch(enrollment.stopReason){
|
|
315
|
+
case 'reply':
|
|
316
|
+
var _ref, _enrollment_gmailThreadId;
|
|
317
|
+
return {
|
|
318
|
+
kind: 'replied',
|
|
319
|
+
title: 'Replied',
|
|
320
|
+
detail,
|
|
321
|
+
facts: [
|
|
322
|
+
'The sequence stopped for them'
|
|
323
|
+
],
|
|
324
|
+
gmailThreadId: (_ref = (_enrollment_gmailThreadId = enrollment.gmailThreadId) != null ? _enrollment_gmailThreadId : enrollment.gmailThreadIds.at(-1)) != null ? _ref : null,
|
|
325
|
+
tone: 'info'
|
|
326
|
+
};
|
|
327
|
+
case 'hard_bounce':
|
|
328
|
+
return {
|
|
329
|
+
kind: 'bounced',
|
|
330
|
+
title: 'Bounced',
|
|
331
|
+
detail,
|
|
332
|
+
facts: facts(gatewayName ? `Their mail gateway: ${gatewayName}` : null, 'The address is on the do-not-contact list'),
|
|
333
|
+
tone: 'error'
|
|
334
|
+
};
|
|
335
|
+
case 'opt_out_reply':
|
|
336
|
+
return {
|
|
337
|
+
kind: 'opted-out',
|
|
338
|
+
title: 'Asked not to be emailed, in a reply',
|
|
339
|
+
detail,
|
|
340
|
+
facts: [],
|
|
341
|
+
tone: 'warning'
|
|
342
|
+
};
|
|
343
|
+
case 'unsubscribe':
|
|
344
|
+
return {
|
|
345
|
+
kind: 'opted-out',
|
|
346
|
+
title: 'Unsubscribed',
|
|
347
|
+
detail,
|
|
348
|
+
facts: [],
|
|
349
|
+
tone: 'warning'
|
|
350
|
+
};
|
|
351
|
+
case 'do_not_contact':
|
|
352
|
+
return {
|
|
353
|
+
kind: 'do-not-contact',
|
|
354
|
+
title: 'Put on the do-not-contact list',
|
|
355
|
+
detail,
|
|
356
|
+
facts: facts(by),
|
|
357
|
+
tone: 'warning'
|
|
358
|
+
};
|
|
359
|
+
case 'manual':
|
|
360
|
+
return enrollment.status === 'paused' ? {
|
|
361
|
+
kind: 'paused',
|
|
362
|
+
title: 'Paused',
|
|
363
|
+
detail,
|
|
364
|
+
facts: facts(by),
|
|
365
|
+
tone: 'warning'
|
|
366
|
+
} : {
|
|
367
|
+
kind: 'stopped',
|
|
368
|
+
title: 'Stopped by a member',
|
|
369
|
+
detail,
|
|
370
|
+
facts: facts(by),
|
|
371
|
+
tone: 'default'
|
|
372
|
+
};
|
|
373
|
+
case 'gate':
|
|
374
|
+
return {
|
|
375
|
+
kind: 'stopped',
|
|
376
|
+
title: 'Stopped — no longer eligible',
|
|
377
|
+
detail,
|
|
378
|
+
facts: [],
|
|
379
|
+
tone: 'default'
|
|
380
|
+
};
|
|
381
|
+
case 'sequence_archived':
|
|
382
|
+
return {
|
|
383
|
+
kind: 'stopped',
|
|
384
|
+
title: 'Stopped — the sequence was archived',
|
|
385
|
+
detail,
|
|
386
|
+
facts: [],
|
|
387
|
+
tone: 'default'
|
|
388
|
+
};
|
|
389
|
+
case 'gateway_blocked_here':
|
|
390
|
+
return {
|
|
391
|
+
kind: 'held',
|
|
392
|
+
title: 'Held — their mail gateway refused this sender',
|
|
393
|
+
detail,
|
|
394
|
+
facts: [],
|
|
395
|
+
tone: 'warning'
|
|
396
|
+
};
|
|
397
|
+
case 'send_failed':
|
|
398
|
+
return {
|
|
399
|
+
kind: 'failed',
|
|
400
|
+
title: 'An email couldn’t be sent',
|
|
401
|
+
detail,
|
|
402
|
+
facts: [],
|
|
403
|
+
tone: 'error'
|
|
404
|
+
};
|
|
405
|
+
default:
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
export function outreachEnrollmentFigures(enrollment) {
|
|
410
|
+
var _enrollment_stepRecords;
|
|
411
|
+
const clicks = outreachClickSummary(enrollment.engagement);
|
|
412
|
+
const emailsSent = emailRecords(enrollment).length;
|
|
413
|
+
const replies = enrollment.status === 'replied' || enrollment.stopReason === 'reply' || enrollment.stopReason === 'opt_out_reply' ? 1 : 0;
|
|
414
|
+
return {
|
|
415
|
+
emailsSent,
|
|
416
|
+
clicks: clicks.clicks,
|
|
417
|
+
linksFollowed: clicks.linkCount === null ? {
|
|
418
|
+
count: clicks.followed.length,
|
|
419
|
+
atLeast: true
|
|
420
|
+
} : {
|
|
421
|
+
count: clicks.linkCount,
|
|
422
|
+
atLeast: clicks.linksCapped
|
|
423
|
+
},
|
|
424
|
+
scannerClicks: clicks.machineClicks,
|
|
425
|
+
replies,
|
|
426
|
+
anything: emailsSent > 0 || ((_enrollment_stepRecords = enrollment.stepRecords) != null ? _enrollment_stepRecords : []).length > 0 || clicks.clicks > 0 || clicks.machineClicks > 0 || enrollment.stoppedAtMs !== null
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
//# sourceMappingURL=enrollment-timeline.js.map
|