@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.
Files changed (90) hide show
  1. package/package.json +14 -14
  2. package/src/lib/components/enrollment-actions.d.ts +33 -0
  3. package/src/lib/components/enrollment-actions.js +174 -0
  4. package/src/lib/components/enrollment-actions.js.map +1 -0
  5. package/src/lib/components/enrollment-detail.d.ts +21 -0
  6. package/src/lib/components/enrollment-detail.js +870 -0
  7. package/src/lib/components/enrollment-detail.js.map +1 -0
  8. package/src/lib/components/enrollment-filter-params.d.ts +58 -0
  9. package/src/lib/components/enrollment-filter-params.js +98 -0
  10. package/src/lib/components/enrollment-filter-params.js.map +1 -0
  11. package/src/lib/components/enrollments-table.d.ts +21 -0
  12. package/src/lib/components/enrollments-table.js +249 -210
  13. package/src/lib/components/enrollments-table.js.map +1 -1
  14. package/src/lib/components/outreach-console-page.js +10 -0
  15. package/src/lib/components/outreach-console-page.js.map +1 -1
  16. package/src/lib/components/sequence-detail.d.ts +2 -0
  17. package/src/lib/components/sequence-detail.js +70 -8
  18. package/src/lib/components/sequence-detail.js.map +1 -1
  19. package/src/lib/components/sequence-report-card.d.ts +17 -0
  20. package/src/lib/components/sequence-report-card.js +63 -15
  21. package/src/lib/components/sequence-report-card.js.map +1 -1
  22. package/src/lib/components/sequences-section.d.ts +4 -1
  23. package/src/lib/components/sequences-section.js +12 -1
  24. package/src/lib/components/sequences-section.js.map +1 -1
  25. package/src/lib/components/use-outreach-api.d.ts +3 -1
  26. package/src/lib/components/use-outreach-api.js +6 -0
  27. package/src/lib/components/use-outreach-api.js.map +1 -1
  28. package/src/lib/components/use-outreach-enrollment.d.ts +18 -0
  29. package/src/lib/components/use-outreach-enrollment.js +133 -0
  30. package/src/lib/components/use-outreach-enrollment.js.map +1 -0
  31. package/src/lib/constants/api-routes.d.ts +2 -0
  32. package/src/lib/constants/api-routes.js +1 -0
  33. package/src/lib/constants/api-routes.js.map +1 -1
  34. package/src/lib/engine/gates.d.ts +4 -3
  35. package/src/lib/engine/gates.js.map +1 -1
  36. package/src/lib/engine/gateway-block.d.ts +4 -8
  37. package/src/lib/engine/gateway-block.js +6 -26
  38. package/src/lib/engine/gateway-block.js.map +1 -1
  39. package/src/lib/engine/mail-gateway.d.ts +18 -73
  40. package/src/lib/engine/mail-gateway.js +32 -206
  41. package/src/lib/engine/mail-gateway.js.map +1 -1
  42. package/src/lib/model/enrollment-engagement.d.ts +26 -2
  43. package/src/lib/model/enrollment-engagement.js +40 -3
  44. package/src/lib/model/enrollment-engagement.js.map +1 -1
  45. package/src/lib/model/enrollment-history.d.ts +40 -0
  46. package/src/lib/model/enrollment-history.js +95 -0
  47. package/src/lib/model/enrollment-history.js.map +1 -0
  48. package/src/lib/model/enrollment-list-query.d.ts +6 -0
  49. package/src/lib/model/enrollment-list-query.js +35 -1
  50. package/src/lib/model/enrollment-list-query.js.map +1 -1
  51. package/src/lib/model/enrollment-timeline.d.ts +78 -0
  52. package/src/lib/model/enrollment-timeline.js +430 -0
  53. package/src/lib/model/enrollment-timeline.js.map +1 -0
  54. package/src/lib/model/outreach-api.d.ts +24 -0
  55. package/src/lib/model/outreach-api.js.map +1 -1
  56. package/src/lib/model/outreach.types.d.ts +78 -29
  57. package/src/lib/model/outreach.types.js +33 -8
  58. package/src/lib/model/outreach.types.js.map +1 -1
  59. package/src/lib/routes/enroll-routes.js +26 -3
  60. package/src/lib/routes/enroll-routes.js.map +1 -1
  61. package/src/lib/routes/enrollment-routes.d.ts +10 -0
  62. package/src/lib/routes/enrollment-routes.js +82 -1
  63. package/src/lib/routes/enrollment-routes.js.map +1 -1
  64. package/src/lib/routes/register-routes.js +14 -4
  65. package/src/lib/routes/register-routes.js.map +1 -1
  66. package/src/lib/routes/route-deps.d.ts +3 -1
  67. package/src/lib/routes/route-deps.js.map +1 -1
  68. package/src/lib/runtime/click-events.d.ts +10 -5
  69. package/src/lib/runtime/click-events.js +45 -6
  70. package/src/lib/runtime/click-events.js.map +1 -1
  71. package/src/lib/runtime/gateway-ledger.d.ts +6 -0
  72. package/src/lib/runtime/gateway-ledger.js +9 -3
  73. package/src/lib/runtime/gateway-ledger.js.map +1 -1
  74. package/src/lib/runtime/host-suspension.d.ts +30 -0
  75. package/src/lib/runtime/host-suspension.js +35 -0
  76. package/src/lib/runtime/host-suspension.js.map +1 -0
  77. package/src/lib/runtime/person-erasure.d.ts +4 -1
  78. package/src/lib/runtime/person-erasure.js +14 -3
  79. package/src/lib/runtime/person-erasure.js.map +1 -1
  80. package/src/lib/runtime/platform-runtime-deps.js +34 -3
  81. package/src/lib/runtime/platform-runtime-deps.js.map +1 -1
  82. package/src/lib/runtime/runtime-deps.d.ts +27 -1
  83. package/src/lib/runtime/runtime-deps.js.map +1 -1
  84. package/src/lib/runtime/send-job.js +46 -6
  85. package/src/lib/runtime/send-job.js.map +1 -1
  86. package/src/lib/runtime/sync-job.js +4 -0
  87. package/src/lib/runtime/sync-job.js.map +1 -1
  88. package/src/lib/storage/domain-intel-store.d.ts +61 -43
  89. package/src/lib/storage/domain-intel-store.js +138 -226
  90. package/src/lib/storage/domain-intel-store.js.map +1 -1
@@ -0,0 +1,870 @@
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
+ */ 'use client';
17
+ import { _ as _extends } from "@swc/helpers/_/_extends";
18
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
19
+ import { PageHeaderRecord, pluginDocsHelp } from "@aglyn/aglyn";
20
+ import { crmOrgRecordHref, crmRecordHref } from "@aglyn/aglyn/app-utils/console-record-links";
21
+ import { mdiAccountCancelOutline, mdiAccountOutline, mdiAccountPlusOutline, mdiAlertCircleOutline, mdiCheckCircleOutline, mdiChevronDown, mdiChevronUp, mdiClipboardCheckOutline, mdiContentCopy, mdiCursorDefaultClickOutline, mdiEmailAlertOutline, mdiEmailOffOutline, mdiEmailOutline, mdiHandBackLeftOutline, mdiPauseCircleOutline, mdiPencilOutline, mdiPlayCircleOutline, mdiReply, mdiShieldSearch, mdiStopCircleOutline } from "@aglyn/shared-data-mdi";
22
+ import { AppLink, CardDisplay, MdiIcon } from "@aglyn/shared-ui-jsx";
23
+ import EmptyStateComponent from "@aglyn/shared-ui-jsx/components/empty-state.component";
24
+ import RowActionsMenu from "@aglyn/shared-ui-jsx/components/row-actions-menu.component";
25
+ import { useSnackbar } from "@aglyn/shared-ui-snackstack";
26
+ import { useOrgCampaigns, useOrgMemberOptions } from "@aglyn/tenant-feature-instance";
27
+ import { Alert, Box, Button, Card, Chip, Collapse, Divider, IconButton, Link, List, ListItem, ListItemIcon, ListItemText, Stack, Tooltip, Typography } from "@mui/material";
28
+ import { useCallback, useMemo, useState } from "react";
29
+ import { OUTREACH_MAIL_GATEWAY_LABELS } from "../engine/mail-gateway.js";
30
+ import { outreachClickSummary } from "../model/enrollment-engagement.js";
31
+ import { outreachEnrollmentFigures, outreachEnrollmentTimeline, outreachStepName } from "../model/enrollment-timeline.js";
32
+ import { OUTREACH_ATTESTATION_KINDS, OUTREACH_ATTESTATION_LABELS } from "../model/outreach.types.js";
33
+ import { OUTREACH_ENROLLMENT_ACTION_LABELS, outreachEnrollmentActionsFor, useOutreachEnrollmentActions } from "./enrollment-actions.js";
34
+ import { outreachCurrentStepLabel, outreachStepIsCurated, outreachStopLabel } from "./enrollments-table.js";
35
+ import { formatOutreachTime, OutreachEnrollmentStatusChip, OutreachLoading, OutreachLoadProblem } from "./outreach-ui.js";
36
+ import { OutreachFigure } from "./sequence-report-card.js";
37
+ import { useOutreachApi } from "./use-outreach-api.js";
38
+ import { useOutreachSequence } from "./use-outreach-data.js";
39
+ import { useOutreachEnrollment, useOutreachEnrollmentGateway, useOutreachEnrollmentHistory } from "./use-outreach-enrollment.js";
40
+ /** Where the CRM record the person is lives — the site's own CRM when the site is known. */ export function outreachCrmRecordHref(kind, id, hostId, orgMount) {
41
+ var _orgMount_hosts_find;
42
+ if (!id || !(orgMount == null ? void 0 : orgMount.orgSlug)) return null;
43
+ const host = (_orgMount_hosts_find = orgMount.hosts.find((entry)=>entry.id === hostId)) == null ? void 0 : _orgMount_hosts_find.subdomain;
44
+ return host ? crmRecordHref({
45
+ orgSlug: orgMount.orgSlug,
46
+ host
47
+ }, kind, id) : crmOrgRecordHref(orgMount.orgSlug, kind, id);
48
+ }
49
+ /** A Gmail thread, opened in the mailbox's own account. */ export function outreachGmailThreadUrl(threadId, mailboxEmail) {
50
+ const account = mailboxEmail ? `?authuser=${encodeURIComponent(mailboxEmail)}` : '';
51
+ return `https://mail.google.com/mail/${account}#all/${encodeURIComponent(threadId)}`;
52
+ }
53
+ const KIND_ICONS = {
54
+ enrolled: mdiAccountPlusOutline,
55
+ curated: mdiPencilOutline,
56
+ sent: mdiEmailOutline,
57
+ task: mdiClipboardCheckOutline,
58
+ click: mdiCursorDefaultClickOutline,
59
+ scanner: mdiShieldSearch,
60
+ 'earlier-clicks': mdiCursorDefaultClickOutline,
61
+ paused: mdiPauseCircleOutline,
62
+ resumed: mdiPlayCircleOutline,
63
+ stopped: mdiStopCircleOutline,
64
+ 'do-not-contact': mdiAccountCancelOutline,
65
+ replied: mdiReply,
66
+ bounced: mdiEmailAlertOutline,
67
+ 'opted-out': mdiEmailOffOutline,
68
+ held: mdiHandBackLeftOutline,
69
+ released: mdiPlayCircleOutline,
70
+ failed: mdiAlertCircleOutline,
71
+ finished: mdiCheckCircleOutline
72
+ };
73
+ const TONE_COLORS = {
74
+ default: 'text.secondary',
75
+ success: 'success.main',
76
+ info: 'info.main',
77
+ warning: 'warning.main',
78
+ error: 'error.main'
79
+ };
80
+ /** A destination, whole, opening in a new tab. */ function Destination(props) {
81
+ return /*#__PURE__*/ _jsxs(Typography, {
82
+ variant: "body2",
83
+ component: "span",
84
+ sx: {
85
+ display: 'block',
86
+ wordBreak: 'break-all'
87
+ },
88
+ children: [
89
+ props.label ? `${props.label}: ` : null,
90
+ /*#__PURE__*/ _jsx(Link, {
91
+ href: props.url,
92
+ target: "_blank",
93
+ rel: "noopener noreferrer",
94
+ underline: "hover",
95
+ children: props.url
96
+ })
97
+ ]
98
+ });
99
+ }
100
+ /** One line of the timeline. */ function TimelineRow(props) {
101
+ const { entry } = props;
102
+ return /*#__PURE__*/ _jsxs(ListItem, {
103
+ alignItems: "flex-start",
104
+ divider: props.divider,
105
+ disableGutters: true,
106
+ "data-kind": entry.kind,
107
+ children: [
108
+ /*#__PURE__*/ _jsx(ListItemIcon, {
109
+ sx: {
110
+ minWidth: 36,
111
+ mt: 0.75,
112
+ color: TONE_COLORS[entry.tone]
113
+ },
114
+ children: /*#__PURE__*/ _jsx(MdiIcon, {
115
+ path: KIND_ICONS[entry.kind].path,
116
+ fontSize: "small"
117
+ })
118
+ }),
119
+ /*#__PURE__*/ _jsx(ListItemText, {
120
+ disableTypography: true,
121
+ primary: /*#__PURE__*/ _jsxs(Stack, {
122
+ direction: {
123
+ xs: 'column',
124
+ sm: 'row'
125
+ },
126
+ spacing: {
127
+ xs: 0,
128
+ sm: 1
129
+ },
130
+ sx: {
131
+ justifyContent: 'space-between',
132
+ alignItems: {
133
+ sm: 'baseline'
134
+ }
135
+ },
136
+ children: [
137
+ /*#__PURE__*/ _jsx(Typography, {
138
+ variant: "body2",
139
+ sx: {
140
+ fontWeight: 500
141
+ },
142
+ children: entry.title
143
+ }),
144
+ /*#__PURE__*/ _jsx(Typography, {
145
+ variant: "caption",
146
+ color: "text.secondary",
147
+ sx: {
148
+ whiteSpace: 'nowrap'
149
+ },
150
+ children: formatOutreachTime(entry.atMs, props.timeZone)
151
+ })
152
+ ]
153
+ }),
154
+ secondary: /*#__PURE__*/ _jsxs(Stack, {
155
+ spacing: 0.25,
156
+ sx: {
157
+ mt: 0.25
158
+ },
159
+ children: [
160
+ entry.url ? /*#__PURE__*/ _jsx(Destination, {
161
+ url: entry.url,
162
+ label: entry.urlLabel
163
+ }) : null,
164
+ entry.detail ? /*#__PURE__*/ _jsx(Typography, {
165
+ variant: "body2",
166
+ sx: {
167
+ whiteSpace: 'pre-wrap',
168
+ wordBreak: 'break-word'
169
+ },
170
+ children: entry.detail
171
+ }) : null,
172
+ entry.facts.length || entry.gmailThreadId ? /*#__PURE__*/ _jsxs(Typography, {
173
+ variant: "caption",
174
+ color: "text.secondary",
175
+ component: "div",
176
+ children: [
177
+ entry.facts.join(' · '),
178
+ entry.gmailThreadId ? /*#__PURE__*/ _jsxs(_Fragment, {
179
+ children: [
180
+ entry.facts.length ? ' · ' : null,
181
+ /*#__PURE__*/ _jsx(Link, {
182
+ href: outreachGmailThreadUrl(entry.gmailThreadId, props.mailboxEmail),
183
+ target: "_blank",
184
+ rel: "noopener noreferrer",
185
+ underline: "hover",
186
+ children: "Open thread in Gmail"
187
+ })
188
+ ]
189
+ }) : null
190
+ ]
191
+ }) : null
192
+ ]
193
+ })
194
+ })
195
+ ]
196
+ });
197
+ }
198
+ /** One fact of the Details list. */ function Fact(props) {
199
+ return /*#__PURE__*/ _jsxs(_Fragment, {
200
+ children: [
201
+ /*#__PURE__*/ _jsx(Typography, {
202
+ component: "dt",
203
+ variant: "body2",
204
+ color: "text.secondary",
205
+ children: props.label
206
+ }),
207
+ /*#__PURE__*/ _jsx(Box, {
208
+ component: "dd",
209
+ sx: {
210
+ m: 0,
211
+ minWidth: 0,
212
+ typography: 'body2',
213
+ wordBreak: 'break-word'
214
+ },
215
+ children: props.children
216
+ })
217
+ ]
218
+ });
219
+ }
220
+ /** What mail from the sequence's sending domain met at the person's domain, in a line. */ function gatewaySentence(answer, timeZone) {
221
+ const { intel, standing } = answer;
222
+ if (!intel) return 'Could not be looked up';
223
+ if (intel.status === 'no_mx' || intel.status === 'null_mx') {
224
+ var _answer_domain;
225
+ return `${(_answer_domain = answer.domain) != null ? _answer_domain : 'The domain'} takes no mail · looked up ${formatOutreachTime(intel.resolvedAtMs, timeZone)}`;
226
+ }
227
+ const from = answer.sendingDomain ? ` from ${answer.sendingDomain}` : '';
228
+ const parts = [
229
+ OUTREACH_MAIL_GATEWAY_LABELS[intel.gateway],
230
+ intel.mx.length ? `MX ${intel.mx.join(', ')}` : 'no MX; delivered to its address record',
231
+ standing ? `${standing.delivered30} delivered, ${standing.blocked30} refused${from} in the last 30 days` : null,
232
+ `looked up ${formatOutreachTime(intel.resolvedAtMs, timeZone)}`
233
+ ];
234
+ return parts.filter(Boolean).join(' · ');
235
+ }
236
+ /**
237
+ * Everything else the enrollment holds, all of it, behind one toggle.
238
+ */ function EnrollmentDetails(props) {
239
+ var _enrollment_stepOverrides;
240
+ var _enrollment_campaignIds, _enrollment_engagement, _enrollment_engagement1;
241
+ const { enrollment, sequence, memberName, timeZone } = props;
242
+ const { enqueueSnackbar } = useSnackbar();
243
+ const time = (ms)=>formatOutreachTime(ms, timeZone);
244
+ const leadHref = outreachCrmRecordHref('lead', enrollment.leadId, enrollment.hostId, props.orgMount);
245
+ const contactHref = outreachCrmRecordHref('contact', enrollment.contactId, enrollment.hostId, props.orgMount);
246
+ const clicks = outreachClickSummary(enrollment.engagement);
247
+ const overrides = Object.entries((_enrollment_stepOverrides = enrollment.stepOverrides) != null ? _enrollment_stepOverrides : {}).sort(([a], [b])=>Number(a) - Number(b));
248
+ const attested = OUTREACH_ATTESTATION_KINDS.filter((kind)=>{
249
+ var _enrollment_attestations;
250
+ return (_enrollment_attestations = enrollment.attestations) == null ? void 0 : _enrollment_attestations[kind];
251
+ });
252
+ const threads = enrollment.gmailThreadIds.length ? enrollment.gmailThreadIds : enrollment.gmailThreadId ? [
253
+ enrollment.gmailThreadId
254
+ ] : [];
255
+ const copyId = ()=>{
256
+ var _navigator_clipboard;
257
+ void ((_navigator_clipboard = navigator.clipboard) == null ? void 0 : _navigator_clipboard.writeText(enrollment.id).then(()=>enqueueSnackbar('Enrollment id copied.', {
258
+ variant: 'success'
259
+ })).catch(()=>enqueueSnackbar('The id could not be copied.', {
260
+ variant: 'error'
261
+ })));
262
+ };
263
+ const link = (href, label)=>href ? /*#__PURE__*/ _jsx(Link, _extends({}, {
264
+ component: AppLink,
265
+ componentVariant: 'naked'
266
+ }, {
267
+ href: href,
268
+ underline: "hover",
269
+ children: label
270
+ })) : label;
271
+ return /*#__PURE__*/ _jsxs(Box, {
272
+ component: "dl",
273
+ sx: {
274
+ m: 0,
275
+ display: 'grid',
276
+ gridTemplateColumns: {
277
+ xs: '1fr',
278
+ sm: 'minmax(160px, 220px) 1fr'
279
+ },
280
+ columnGap: 2,
281
+ rowGap: 1.25
282
+ },
283
+ children: [
284
+ /*#__PURE__*/ _jsx(Fact, {
285
+ label: "Personal line",
286
+ children: enrollment.personalLine ? /*#__PURE__*/ _jsx(Typography, {
287
+ variant: "body2",
288
+ sx: {
289
+ whiteSpace: 'pre-wrap'
290
+ },
291
+ children: enrollment.personalLine
292
+ }) : '—'
293
+ }),
294
+ /*#__PURE__*/ _jsx(Fact, {
295
+ label: "Enrolled as",
296
+ children: enrollment.target === 'lead' ? link(leadHref, 'Lead') : enrollment.leadId ? /*#__PURE__*/ _jsxs(_Fragment, {
297
+ children: [
298
+ link(contactHref, 'Contact'),
299
+ ', converted from a ',
300
+ link(leadHref, 'lead')
301
+ ]
302
+ }) : link(contactHref, 'Contact')
303
+ }),
304
+ /*#__PURE__*/ _jsx(Fact, {
305
+ label: "Enrolled",
306
+ children: `${time(enrollment.createdAtMs)}${enrollment.enrolledByUid ? ` by ${memberName(enrollment.enrolledByUid)}` : ''}`
307
+ }),
308
+ /*#__PURE__*/ _jsx(Fact, {
309
+ label: "Cold contact",
310
+ children: enrollment.cold ? 'Yes — no inbound capture behind them, so the member confirmed the address before enrolling' : 'No'
311
+ }),
312
+ /*#__PURE__*/ _jsx(Fact, {
313
+ label: "Attestations",
314
+ children: attested.length ? /*#__PURE__*/ _jsx(Stack, {
315
+ component: "ul",
316
+ spacing: 0.25,
317
+ sx: {
318
+ m: 0,
319
+ pl: 2.5
320
+ },
321
+ children: attested.map((kind)=>{
322
+ var _ref;
323
+ var _enrollment_attestations;
324
+ const entry = (_enrollment_attestations = enrollment.attestations) == null ? void 0 : _enrollment_attestations[kind];
325
+ return /*#__PURE__*/ _jsx("li", {
326
+ children: `${OUTREACH_ATTESTATION_LABELS[kind]} — ${memberName((_ref = entry == null ? void 0 : entry.uid) != null ? _ref : '')}, ${time(entry == null ? void 0 : entry.atMs)}`
327
+ }, kind);
328
+ })
329
+ }) : '—'
330
+ }),
331
+ /*#__PURE__*/ _jsx(Fact, {
332
+ label: "Mailbox",
333
+ children: props.mailbox ? `${props.mailbox.sendAs || props.mailbox.email}${props.mailbox.sendAs && props.mailbox.sendAs !== props.mailbox.email ? ` (${props.mailbox.email})` : ''}` : enrollment.mailboxId ? 'No longer connected' : '—'
334
+ }),
335
+ /*#__PURE__*/ _jsx(Fact, {
336
+ label: "Campaigns",
337
+ children: ((_enrollment_campaignIds = enrollment.campaignIds) == null ? void 0 : _enrollment_campaignIds.length) ? enrollment.campaignIds.map(props.campaignName).join(', ') : '—'
338
+ }),
339
+ /*#__PURE__*/ _jsx(Fact, {
340
+ label: "Mail gateway",
341
+ children: props.gateway ? gatewaySentence(props.gateway, timeZone) : props.gatewayUnread
342
+ }),
343
+ /*#__PURE__*/ _jsxs(Fact, {
344
+ label: "Gateway hold",
345
+ children: [
346
+ enrollment.gatewayHold ? [
347
+ OUTREACH_MAIL_GATEWAY_LABELS[enrollment.gatewayHold.gateway],
348
+ enrollment.gatewayHold.heldAtMs ? `held ${time(enrollment.gatewayHold.heldAtMs)}` : 'never held',
349
+ enrollment.gatewayHold.releasedAtMs ? `released ${time(enrollment.gatewayHold.releasedAtMs)}${enrollment.gatewayHold.releasedByUid ? ` by ${memberName(enrollment.gatewayHold.releasedByUid)}` : ''}` : 'not released'
350
+ ].join(' · ') : '—',
351
+ enrollment.gatewayDeliveredSteps ? ` (${enrollment.gatewayDeliveredSteps} of their emails counted as delivered on the gateway ledger)` : null
352
+ ]
353
+ }),
354
+ /*#__PURE__*/ _jsxs(Fact, {
355
+ label: "Clicks",
356
+ children: [
357
+ clicks.clicks || clicks.machineClicks ? [
358
+ `${clicks.clicks} by the person`,
359
+ `${clicks.machineClicks} by scanners, not counted`,
360
+ `${clicks.clicks - clicks.unloggedClicks + clicks.machineClicks - clicks.unloggedMachineClicks} listed one by one`,
361
+ ((_enrollment_engagement = enrollment.engagement) == null ? void 0 : _enrollment_engagement.firstClickAtMs) ? `first ${time(enrollment.engagement.firstClickAtMs)}` : null,
362
+ ((_enrollment_engagement1 = enrollment.engagement) == null ? void 0 : _enrollment_engagement1.lastClickAtMs) ? `last ${time(enrollment.engagement.lastClickAtMs)}` : null
363
+ ].filter(Boolean).join(' · ') : '—',
364
+ clicks.followed.length ? /*#__PURE__*/ _jsx(Box, {
365
+ sx: {
366
+ mt: 0.5
367
+ },
368
+ children: clicks.followed.map((url)=>/*#__PURE__*/ _jsx(Destination, {
369
+ url: url
370
+ }, url))
371
+ }) : null
372
+ ]
373
+ }),
374
+ /*#__PURE__*/ _jsx(Fact, {
375
+ label: "Curated steps",
376
+ children: overrides.length ? /*#__PURE__*/ _jsx(Stack, {
377
+ spacing: 1.5,
378
+ children: overrides.map(([key, override])=>/*#__PURE__*/ _jsxs(Box, {
379
+ children: [
380
+ /*#__PURE__*/ _jsx(Typography, {
381
+ variant: "body2",
382
+ sx: {
383
+ fontWeight: 500
384
+ },
385
+ children: outreachStepName(sequence.steps, Number(key))
386
+ }),
387
+ /*#__PURE__*/ _jsx(Typography, {
388
+ variant: "caption",
389
+ color: "text.secondary",
390
+ component: "div",
391
+ children: [
392
+ override.source === 'ai' ? override.edited ? 'AI draft, edited' : 'AI draft' : 'Written by a member',
393
+ override.draftedByUid ? `confirmed by ${memberName(override.draftedByUid)}` : null,
394
+ override.draftedAtMs ? time(override.draftedAtMs) : null,
395
+ override.model ? `model ${override.model}` : null
396
+ ].filter(Boolean).join(' · ')
397
+ }),
398
+ override.subject !== undefined ? /*#__PURE__*/ _jsx(Typography, {
399
+ variant: "body2",
400
+ children: `Subject: ${override.subject}`
401
+ }) : null,
402
+ override.body !== undefined ? /*#__PURE__*/ _jsx(Typography, {
403
+ variant: "body2",
404
+ sx: {
405
+ whiteSpace: 'pre-wrap',
406
+ mt: 0.5,
407
+ pl: 1.5,
408
+ borderLeft: 2,
409
+ borderColor: 'divider'
410
+ },
411
+ children: override.body
412
+ }) : null,
413
+ override.prompt ? /*#__PURE__*/ _jsx(Typography, {
414
+ variant: "caption",
415
+ color: "text.secondary",
416
+ component: "div",
417
+ sx: {
418
+ whiteSpace: 'pre-wrap',
419
+ mt: 0.5
420
+ },
421
+ children: `Prompt: ${override.prompt}`
422
+ }) : null
423
+ ]
424
+ }, key))
425
+ }) : '—'
426
+ }),
427
+ /*#__PURE__*/ _jsx(Fact, {
428
+ label: "Gmail threads",
429
+ children: threads.length ? /*#__PURE__*/ _jsxs(Stack, {
430
+ spacing: 0.25,
431
+ children: [
432
+ threads.map((thread)=>{
433
+ var _props_mailbox;
434
+ return /*#__PURE__*/ _jsxs("span", {
435
+ children: [
436
+ /*#__PURE__*/ _jsx(Link, {
437
+ href: outreachGmailThreadUrl(thread, (_props_mailbox = props.mailbox) == null ? void 0 : _props_mailbox.email),
438
+ target: "_blank",
439
+ rel: "noopener noreferrer",
440
+ underline: "hover",
441
+ children: thread
442
+ }),
443
+ thread === enrollment.gmailThreadId ? ' (current)' : null
444
+ ]
445
+ }, thread);
446
+ }),
447
+ enrollment.threadSubject ? /*#__PURE__*/ _jsx(Typography, {
448
+ variant: "caption",
449
+ color: "text.secondary",
450
+ children: `Started as “${enrollment.threadSubject}”`
451
+ }) : null,
452
+ enrollment.messageIds.length ? /*#__PURE__*/ _jsx(Typography, {
453
+ variant: "caption",
454
+ color: "text.secondary",
455
+ sx: {
456
+ wordBreak: 'break-all'
457
+ },
458
+ children: `Message-IDs in the current thread: ${enrollment.messageIds.join(' ')}`
459
+ }) : null
460
+ ]
461
+ }) : '—'
462
+ }),
463
+ /*#__PURE__*/ _jsx(Fact, {
464
+ label: "Stop",
465
+ children: enrollment.stopReason ? `${outreachStopLabel(enrollment)} · ${time(enrollment.stoppedAtMs)}${enrollment.stoppedByUid ? ` · by ${memberName(enrollment.stoppedByUid)}` : ''}` : '—'
466
+ }),
467
+ /*#__PURE__*/ _jsx(Fact, {
468
+ label: "Created · updated",
469
+ children: `${time(enrollment.createdAtMs)} · ${time(enrollment.updatedAtMs)}`
470
+ }),
471
+ /*#__PURE__*/ _jsx(Fact, {
472
+ label: "Enrollment id",
473
+ children: /*#__PURE__*/ _jsxs(Stack, {
474
+ direction: "row",
475
+ spacing: 0.5,
476
+ sx: {
477
+ alignItems: 'center'
478
+ },
479
+ children: [
480
+ /*#__PURE__*/ _jsx(Typography, {
481
+ variant: "body2",
482
+ sx: {
483
+ fontFamily: 'monospace',
484
+ wordBreak: 'break-all'
485
+ },
486
+ children: enrollment.id
487
+ }),
488
+ /*#__PURE__*/ _jsx(Tooltip, {
489
+ title: "Copy the id",
490
+ children: /*#__PURE__*/ _jsx(IconButton, {
491
+ size: "small",
492
+ "aria-label": "Copy the enrollment id",
493
+ onClick: copyId,
494
+ children: /*#__PURE__*/ _jsx(MdiIcon, {
495
+ path: mdiContentCopy.path,
496
+ fontSize: "small"
497
+ })
498
+ })
499
+ })
500
+ ]
501
+ })
502
+ })
503
+ ]
504
+ });
505
+ }
506
+ export function OutreachEnrollmentDetail(props) {
507
+ var _props_mailboxes_mailboxes_find, _ref, _ref1, _enrollment_gmailThreadId;
508
+ var _enrollment_campaignIds, _sequence_stats;
509
+ const { orgId, sectionPath, sequenceId, enrollmentId, orgMount } = props;
510
+ const api = useOutreachApi(orgId);
511
+ const loadedSequence = useOutreachSequence(orgId, sequenceId);
512
+ const loaded = useOutreachEnrollment(orgId, enrollmentId);
513
+ const history = useOutreachEnrollmentHistory(orgId, enrollmentId);
514
+ const gateway = useOutreachEnrollmentGateway(api, enrollmentId);
515
+ const roster = useOrgMemberOptions(orgId, {
516
+ enabled: true
517
+ });
518
+ const [detailsOpen, setDetailsOpen] = useState(false);
519
+ const enrollment = loaded.data;
520
+ const sequence = loadedSequence.data;
521
+ const campaigns = useOrgCampaigns(orgId, {
522
+ enabled: detailsOpen && Boolean(enrollment == null ? void 0 : (_enrollment_campaignIds = enrollment.campaignIds) == null ? void 0 : _enrollment_campaignIds.length)
523
+ });
524
+ const steps = useMemo(()=>{
525
+ var _ref;
526
+ return (_ref = sequence == null ? void 0 : sequence.steps) != null ? _ref : [];
527
+ }, [
528
+ sequence
529
+ ]);
530
+ const actions = useOutreachEnrollmentActions({
531
+ api,
532
+ steps
533
+ });
534
+ const memberName = useCallback((uid)=>{
535
+ var _ref;
536
+ var _roster_options_find;
537
+ return (_ref = (_roster_options_find = roster.options.find((option)=>option.uid === uid)) == null ? void 0 : _roster_options_find.label) != null ? _ref : 'a member';
538
+ }, [
539
+ roster.options
540
+ ]);
541
+ const campaignName = useCallback((id)=>{
542
+ var _ref;
543
+ var _campaigns_options_find;
544
+ return (_ref = (_campaigns_options_find = campaigns.options.find((option)=>option.value === id)) == null ? void 0 : _campaigns_options_find.label) != null ? _ref : id;
545
+ }, [
546
+ campaigns.options
547
+ ]);
548
+ const enrollmentsPath = `${sectionPath}/${sequenceId}/enrollments`;
549
+ const mailbox = (_props_mailboxes_mailboxes_find = props.mailboxes.mailboxes.find((entry)=>entry.id === ((enrollment == null ? void 0 : enrollment.mailboxId) || (sequence == null ? void 0 : sequence.mailboxId)))) != null ? _props_mailboxes_mailboxes_find : null;
550
+ const timeZone = (_ref = mailbox == null ? void 0 : mailbox.timezone) != null ? _ref : null;
551
+ const time = (ms)=>formatOutreachTime(ms, timeZone);
552
+ const timeline = useMemo(()=>{
553
+ var _ref;
554
+ var _gateway_data_intel, _gateway_data;
555
+ return enrollment ? outreachEnrollmentTimeline({
556
+ enrollment,
557
+ steps,
558
+ history: history.status === 'ready' ? history.data : null,
559
+ memberName,
560
+ formatTime: (ms)=>formatOutreachTime(ms, timeZone),
561
+ gateway: (_ref = (_gateway_data = gateway.data) == null ? void 0 : (_gateway_data_intel = _gateway_data.intel) == null ? void 0 : _gateway_data_intel.gateway) != null ? _ref : null
562
+ }) : [];
563
+ }, [
564
+ enrollment,
565
+ steps,
566
+ history,
567
+ memberName,
568
+ timeZone,
569
+ gateway.data
570
+ ]);
571
+ const back = /*#__PURE__*/ _jsx(Button, _extends({}, {
572
+ component: AppLink,
573
+ componentVariant: 'naked',
574
+ nativeButton: false
575
+ }, {
576
+ href: enrollmentsPath,
577
+ size: "small",
578
+ color: "primary",
579
+ children: "Back to enrollments"
580
+ }));
581
+ if (loaded.status === 'loading' || loadedSequence.status === 'loading') {
582
+ return /*#__PURE__*/ _jsx(OutreachLoading, {
583
+ label: "Loading this person…"
584
+ });
585
+ }
586
+ if (loaded.status === 'error' || loaded.status === 'refused') {
587
+ return /*#__PURE__*/ _jsx(OutreachLoadProblem, {
588
+ status: loaded.status,
589
+ what: "enrollment"
590
+ });
591
+ }
592
+ if (loadedSequence.status === 'error' || loadedSequence.status === 'refused') {
593
+ return /*#__PURE__*/ _jsx(OutreachLoadProblem, {
594
+ status: loadedSequence.status,
595
+ what: "sequence"
596
+ });
597
+ }
598
+ // An id from another sequence reads as not here: the URL names both.
599
+ if (!enrollment || !sequence || enrollment.sequenceId !== sequenceId) {
600
+ return /*#__PURE__*/ _jsx(Card, {
601
+ variant: "outlined",
602
+ children: /*#__PURE__*/ _jsx(EmptyStateComponent, {
603
+ label: "This person isn’t in this sequence",
604
+ description: "They may have been removed from the workspace, or the link names a different sequence.",
605
+ action: back
606
+ })
607
+ });
608
+ }
609
+ const name = enrollment.contactName || enrollment.email;
610
+ const kind = enrollment.target === 'lead' ? 'lead' : 'contact';
611
+ const recordHref = outreachCrmRecordHref(kind, kind === 'lead' ? enrollment.leadId : enrollment.contactId, enrollment.hostId, orgMount);
612
+ const figures = outreachEnrollmentFigures(enrollment);
613
+ const trackClicks = sequence.settings.trackClicks || ((_sequence_stats = sequence.stats) == null ? void 0 : _sequence_stats.clickTracked) === true;
614
+ const open = enrollment.status === 'active' || enrollment.status === 'paused';
615
+ const held = enrollment.status === 'paused' && enrollment.stopReason === 'gateway_blocked_here';
616
+ /*
617
+ * The one act that moves the person forward is the button: resuming a
618
+ * paused one, else curating the next email. Everything else is in the
619
+ * menu, the destructive acts included, as on every record page.
620
+ */ const available = outreachEnrollmentActionsFor(enrollment, steps);
621
+ const primary = available.includes('resume') ? 'resume' : available.includes('curate') ? 'curate' : null;
622
+ const menu = actions.menuItems(enrollment, (action)=>action !== primary);
623
+ const dash = (value)=>value ? value.toLocaleString() : '—';
624
+ const notTracked = !trackClicks && !figures.clicks && !figures.scannerClicks ? 'Not tracked' : undefined;
625
+ const thread = (_ref1 = (_enrollment_gmailThreadId = enrollment.gmailThreadId) != null ? _enrollment_gmailThreadId : enrollment.gmailThreadIds.at(-1)) != null ? _ref1 : null;
626
+ const nothingYet = enrollment.status === 'active' && enrollment.nextDueAtMs ? `Nothing yet — step ${enrollment.stepIndex + 1} sends ${time(enrollment.nextDueAtMs)}.` : 'Nothing yet.';
627
+ return /*#__PURE__*/ _jsxs(Stack, {
628
+ spacing: 2,
629
+ children: [
630
+ /*#__PURE__*/ _jsx(PageHeaderRecord, {
631
+ title: name
632
+ }),
633
+ /*#__PURE__*/ _jsx(CardDisplay, {
634
+ header: "Enrollment",
635
+ subheader: /*#__PURE__*/ _jsxs(_Fragment, {
636
+ children: [
637
+ enrollment.contactName ? `${enrollment.email} · ` : '',
638
+ `In ${sequence.name || 'Untitled'}`
639
+ ]
640
+ }),
641
+ help: pluginDocsHelp('sequences', {
642
+ anchor: '#person-history'
643
+ }),
644
+ contentGutterX: true,
645
+ contentGutterY: true,
646
+ HeaderProps: {
647
+ action: /*#__PURE__*/ _jsxs(Stack, {
648
+ direction: "row",
649
+ spacing: 1,
650
+ sx: {
651
+ alignItems: 'center',
652
+ flexWrap: 'wrap',
653
+ rowGap: 1
654
+ },
655
+ children: [
656
+ back,
657
+ primary ? /*#__PURE__*/ _jsx(Button, {
658
+ variant: "outlined",
659
+ size: "small",
660
+ disabled: actions.busy === enrollment.id,
661
+ onClick: ()=>actions.choose(enrollment, primary),
662
+ children: held ? 'Resume and send' : OUTREACH_ENROLLMENT_ACTION_LABELS[primary]
663
+ }) : null,
664
+ menu.length ? /*#__PURE__*/ _jsx(RowActionsMenu, {
665
+ label: name,
666
+ items: menu
667
+ }) : null
668
+ ]
669
+ })
670
+ },
671
+ children: /*#__PURE__*/ _jsxs(Stack, {
672
+ spacing: 2,
673
+ children: [
674
+ /*#__PURE__*/ _jsxs(Stack, {
675
+ direction: "row",
676
+ spacing: 1,
677
+ sx: {
678
+ alignItems: 'center',
679
+ flexWrap: 'wrap',
680
+ rowGap: 1
681
+ },
682
+ children: [
683
+ /*#__PURE__*/ _jsx(Tooltip, {
684
+ describeChild: true,
685
+ title: recordHref ? `Open the ${kind} in the CRM` : `The ${kind}’s record can’t be linked`,
686
+ children: /*#__PURE__*/ _jsx(Chip, _extends({
687
+ size: "small",
688
+ variant: "outlined",
689
+ color: recordHref ? 'primary' : 'default',
690
+ icon: /*#__PURE__*/ _jsx(MdiIcon, {
691
+ path: mdiAccountOutline.path,
692
+ fontSize: "small"
693
+ }),
694
+ label: kind === 'lead' ? 'Lead' : 'Contact'
695
+ }, recordHref ? {
696
+ component: AppLink,
697
+ href: recordHref,
698
+ clickable: true,
699
+ componentVariant: 'naked'
700
+ } : {}))
701
+ }),
702
+ /*#__PURE__*/ _jsx(OutreachEnrollmentStatusChip, {
703
+ status: enrollment.status
704
+ }),
705
+ /*#__PURE__*/ _jsx(Chip, {
706
+ size: "small",
707
+ variant: "outlined",
708
+ label: outreachCurrentStepLabel(enrollment, steps)
709
+ }),
710
+ outreachStepIsCurated(enrollment) ? /*#__PURE__*/ _jsx(Chip, {
711
+ size: "small",
712
+ variant: "outlined",
713
+ color: "success",
714
+ label: "Curated"
715
+ }) : null,
716
+ open && enrollment.nextDueAtMs ? /*#__PURE__*/ _jsx(Chip, {
717
+ size: "small",
718
+ variant: "outlined",
719
+ label: `Next send ${time(enrollment.nextDueAtMs)}`
720
+ }) : null
721
+ ]
722
+ }),
723
+ held ? /*#__PURE__*/ _jsx(Alert, {
724
+ severity: "warning",
725
+ children: enrollment.stopDetail || 'Their mail gateway refused this sender, so the next email is held. Resume to send it anyway.'
726
+ }) : null,
727
+ figures.anything ? /*#__PURE__*/ _jsxs(Stack, {
728
+ direction: "row",
729
+ spacing: 3,
730
+ sx: {
731
+ flexWrap: 'wrap',
732
+ rowGap: 2
733
+ },
734
+ role: "group",
735
+ "aria-label": "In numbers",
736
+ children: [
737
+ /*#__PURE__*/ _jsx(OutreachFigure, {
738
+ label: "Emails sent",
739
+ value: dash(figures.emailsSent)
740
+ }),
741
+ /*#__PURE__*/ _jsx(OutreachFigure, {
742
+ label: "Clicks",
743
+ value: dash(figures.clicks),
744
+ hint: notTracked
745
+ }),
746
+ /*#__PURE__*/ _jsx(OutreachFigure, {
747
+ label: "Links followed",
748
+ value: figures.linksFollowed.count ? `${figures.linksFollowed.count}${figures.linksFollowed.atLeast ? '+' : ''}` : '—',
749
+ hint: notTracked != null ? notTracked : figures.linksFollowed.atLeast && figures.linksFollowed.count ? 'Earlier clicks kept the last link only' : undefined
750
+ }),
751
+ /*#__PURE__*/ _jsx(OutreachFigure, {
752
+ label: "Scanner clicks",
753
+ value: dash(figures.scannerClicks),
754
+ hint: notTracked != null ? notTracked : 'Not counted'
755
+ }),
756
+ /*#__PURE__*/ _jsx(OutreachFigure, {
757
+ label: "Replies",
758
+ value: dash(figures.replies)
759
+ })
760
+ ]
761
+ }) : /*#__PURE__*/ _jsx(Typography, {
762
+ variant: "body2",
763
+ color: "text.secondary",
764
+ children: nothingYet
765
+ })
766
+ ]
767
+ })
768
+ }),
769
+ /*#__PURE__*/ _jsx(CardDisplay, {
770
+ header: "Activity",
771
+ subheader: "Newest first",
772
+ help: pluginDocsHelp('sequences', {
773
+ anchor: '#person-history'
774
+ }),
775
+ contentGutterX: true,
776
+ contentGutterY: true,
777
+ HeaderProps: thread ? {
778
+ action: /*#__PURE__*/ _jsx(Button, {
779
+ size: "small",
780
+ href: outreachGmailThreadUrl(thread, mailbox == null ? void 0 : mailbox.email),
781
+ target: "_blank",
782
+ rel: "noopener noreferrer",
783
+ children: "Open thread in Gmail"
784
+ })
785
+ } : undefined,
786
+ children: /*#__PURE__*/ _jsxs(Stack, {
787
+ spacing: 1,
788
+ children: [
789
+ history.status === 'loading' ? /*#__PURE__*/ _jsx(OutreachLoading, {
790
+ label: "Loading their clicks…"
791
+ }) : null,
792
+ history.status === 'error' || history.status === 'refused' ? /*#__PURE__*/ _jsx(OutreachLoadProblem, {
793
+ status: history.status,
794
+ what: "list of their clicks",
795
+ // Only the one-by-one list is missing: the totals, the sends and
796
+ // every stop above come from the enrollment, which was read.
797
+ message: history.status === 'refused' ? 'Their clicks can’t be listed one by one here; the numbers above still count them.' : 'Their clicks couldn’t be listed one by one. Reload the page to try again.'
798
+ }) : null,
799
+ /*#__PURE__*/ _jsx(List, {
800
+ disablePadding: true,
801
+ "aria-label": "Activity",
802
+ children: timeline.map((entry, index)=>{
803
+ var _ref;
804
+ return /*#__PURE__*/ _jsx(TimelineRow, {
805
+ entry: entry,
806
+ timeZone: timeZone,
807
+ mailboxEmail: (_ref = mailbox == null ? void 0 : mailbox.email) != null ? _ref : null,
808
+ divider: index < timeline.length - 1
809
+ }, entry.key);
810
+ })
811
+ })
812
+ ]
813
+ })
814
+ }),
815
+ /*#__PURE__*/ _jsx(CardDisplay, {
816
+ header: "Details",
817
+ help: pluginDocsHelp('sequences', {
818
+ anchor: '#person-history'
819
+ }),
820
+ contentGutterX: true,
821
+ HeaderProps: {
822
+ action: /*#__PURE__*/ _jsx(Button, {
823
+ size: "small",
824
+ "aria-expanded": detailsOpen,
825
+ "aria-controls": "outreach-enrollment-details",
826
+ endIcon: /*#__PURE__*/ _jsx(MdiIcon, {
827
+ path: (detailsOpen ? mdiChevronUp : mdiChevronDown).path,
828
+ fontSize: "small"
829
+ }),
830
+ onClick: ()=>setDetailsOpen((was)=>!was),
831
+ children: detailsOpen ? 'Hide' : 'Show'
832
+ })
833
+ },
834
+ children: /*#__PURE__*/ _jsx(Collapse, {
835
+ in: detailsOpen,
836
+ unmountOnExit: true,
837
+ children: /*#__PURE__*/ _jsxs(Box, {
838
+ id: "outreach-enrollment-details",
839
+ sx: {
840
+ pb: 2
841
+ },
842
+ children: [
843
+ /*#__PURE__*/ _jsx(Divider, {
844
+ sx: {
845
+ mb: 2
846
+ }
847
+ }),
848
+ /*#__PURE__*/ _jsx(EnrollmentDetails, {
849
+ enrollment: enrollment,
850
+ sequence: sequence,
851
+ mailbox: mailbox,
852
+ gateway: gateway.status === 'ready' ? gateway.data : null,
853
+ gatewayUnread: gateway.status === 'loading' ? 'Looking it up…' : gateway.status === 'refused' ? 'Your role cannot look it up' : 'Could not be looked up',
854
+ campaignName: campaignName,
855
+ memberName: memberName,
856
+ orgMount: orgMount,
857
+ timeZone: timeZone
858
+ })
859
+ ]
860
+ })
861
+ })
862
+ }),
863
+ actions.dialogs
864
+ ]
865
+ });
866
+ }
867
+ OutreachEnrollmentDetail.displayName = 'OutreachEnrollmentDetail';
868
+ export default OutreachEnrollmentDetail;
869
+
870
+ //# sourceMappingURL=enrollment-detail.js.map