@aglyn/plugins-outreach 1.0.0-beta.200 → 1.0.0-beta.202

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
@@ -14,7 +14,8 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */ 'use client';
17
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
17
+ import { _ as _extends } from "@swc/helpers/_/_extends";
18
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
18
19
  import EmptyStateComponent from "@aglyn/shared-ui-jsx/components/empty-state.component";
19
20
  import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
20
21
  import ListQueryNotices, { listQueryRefusals } from "@aglyn/shared-ui-jsx/components/list-query-notices.component";
@@ -23,13 +24,13 @@ import { ListRowActions, ListTable, listActionsColumn } from "@aglyn/shared-ui-j
23
24
  import { hiddenFilterVisibility } from "@aglyn/shared-ui-jsx/const/list-filter";
24
25
  import { listFilterGridColumns } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
25
26
  import { useListGridFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-grid-filter";
26
- import { useSnackbar } from "@aglyn/shared-ui-snackstack";
27
- import { Button, Card, CardContent, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Stack, TextField, Typography, useMediaQuery, useTheme, Chip } from "@mui/material";
28
- import { useState } from "react";
29
- import { OutreachCurateStepDialog, outreachNextEmailStepIndex } from "./curate-step-dialog.js";
30
- import { readOutreachEngagement } from "../model/enrollment-engagement.js";
31
- import { OUTREACH_ENROLLMENT_LIST_QUERY } from "../model/enrollment-list-query.js";
27
+ import { Card, CardContent, Chip, Link, Stack, Tooltip, Typography, useMediaQuery, useTheme } from "@mui/material";
28
+ import { useMemo } from "react";
29
+ import { outreachClickCountLabel, outreachClickSummary } from "../model/enrollment-engagement.js";
30
+ import { OUTREACH_ENROLLMENT_CLICK_FIELDS, OUTREACH_ENROLLMENT_LIST_QUERY } from "../model/enrollment-list-query.js";
32
31
  import { OUTREACH_ENROLLMENT_STATUSES, OUTREACH_STOP_REASONS, OUTREACH_TASK_KIND_LABELS } from "../model/outreach.types.js";
32
+ import { useOutreachEnrollmentActions } from "./enrollment-actions.js";
33
+ import { OUTREACH_CLICKED_FILTER_VALUE, OUTREACH_UNCLICKED_FILTER_VALUE } from "./enrollment-filter-params.js";
33
34
  import { formatOutreachTime, OUTREACH_ENROLLMENT_STATUS_LABELS, OUTREACH_STOP_REASON_LABELS, OutreachEnrollmentStatusChip, OutreachLoading, OutreachLoadProblem } from "./outreach-ui.js";
34
35
  import { useOutreachEnrollmentList } from "./use-outreach-data.js";
35
36
  /** The step an enrollment is on, as a person reads it. */ export function outreachCurrentStepLabel(enrollment, steps) {
@@ -48,43 +49,10 @@ import { useOutreachEnrollmentList } from "./use-outreach-data.js";
48
49
  var _enrollment_lastSentAtMs, _enrollment_stoppedAtMs, _enrollment_createdAtMs;
49
50
  return Math.max((_enrollment_lastSentAtMs = enrollment.lastSentAtMs) != null ? _enrollment_lastSentAtMs : 0, (_enrollment_stoppedAtMs = enrollment.stoppedAtMs) != null ? _enrollment_stoppedAtMs : 0, (_enrollment_createdAtMs = enrollment.createdAtMs) != null ? _enrollment_createdAtMs : 0);
50
51
  }
51
- /** What a member may do to an enrollment in the status it is in. */ function actionsFor(enrollment, steps) {
52
- const actions = [];
53
- // Rewrite the next email for this one person, while one is still to go.
54
- if (outreachNextEmailStepIndex(enrollment, steps) !== null) actions.push('curate');
55
- if (enrollment.status === 'active') actions.push('pause', 'stop');
56
- if (enrollment.status === 'paused') actions.push('resume', 'stop');
57
- if (enrollment.status !== 'opted_out') actions.push('do_not_contact');
58
- return actions;
59
- }
60
- const ACTION_LABELS = {
61
- curate: 'Curate next step',
62
- pause: 'Pause',
63
- resume: 'Resume',
64
- stop: 'Stop',
65
- do_not_contact: 'Mark do-not-contact'
66
- };
67
52
  /** Whether the step the enrollment is on goes out as this person's own copy (AGL-3324). */ export function outreachStepIsCurated(enrollment) {
68
53
  var _enrollment_stepOverrides;
69
54
  return Boolean((_enrollment_stepOverrides = enrollment.stepOverrides) == null ? void 0 : _enrollment_stepOverrides[String(enrollment.stepIndex)]);
70
55
  }
71
- /** What the snackbar says once an action lands. */ const DONE_LABELS = {
72
- pause: 'Paused.',
73
- resume: 'Resumed.',
74
- stop: 'Stopped.'
75
- };
76
- /** The two actions that end something for good ask first. */ const CONFIRM = {
77
- stop: {
78
- title: 'Stop this enrollment?',
79
- body: 'They won’t get any more steps from this sequence, and they can’t be enrolled in it again.',
80
- label: 'Stop'
81
- },
82
- do_not_contact: {
83
- title: 'Mark do-not-contact?',
84
- body: 'The address goes on your organization’s do-not-contact list, which every sequence checks before ' + 'every send, and they are stopped in every sequence they are in.',
85
- label: 'Mark do-not-contact'
86
- }
87
- };
88
56
  /*
89
57
  * What the enrollments grid's Filters panel offers (AGL-3317), each field on
90
58
  * the table's Firestore query (AGL-3321, `OUTREACH_ENROLLMENT_LIST_QUERY`).
@@ -92,15 +60,23 @@ const ACTION_LABELS = {
92
60
  * address is typed whole. `target` and `email` are no column of their own,
93
61
  * so they are hidden columns the panel can still reach; the search box
94
62
  * reads the person's name and address.
95
- */ const ENROLLMENT_FILTER_FIELDS = OUTREACH_ENROLLMENT_LIST_QUERY.fields;
63
+ *
64
+ * A sequence that counts clicks adds two more (AGL-3332), on the same query:
65
+ * whether the person clicked, and a destination they followed — matched
66
+ * whole against the destinations stored on the enrollment, never by a piece
67
+ * of its address, so "followed /pricing" does not also list the people who
68
+ * followed "/pricing/enterprise".
69
+ */ const BASE_FILTER_FIELDS = OUTREACH_ENROLLMENT_LIST_QUERY.fields.filter((field)=>!OUTREACH_ENROLLMENT_CLICK_FIELDS.includes(field.column));
96
70
  const ENROLLMENT_FILTER_HEADERS = {
97
71
  status: 'Status',
98
72
  target: 'Enrolled as',
99
73
  stopReason: 'Stop reason',
100
74
  email: 'Email',
101
- enrolled: 'Enrolled'
75
+ enrolled: 'Enrolled',
76
+ clicked: 'Clicked',
77
+ link: 'Link followed'
102
78
  };
103
- const ENROLLMENT_FILTER_OPTIONS = {
79
+ const BASE_FILTER_OPTIONS = {
104
80
  status: OUTREACH_ENROLLMENT_STATUSES.map((status)=>({
105
81
  value: status,
106
82
  label: OUTREACH_ENROLLMENT_STATUS_LABELS[status]
@@ -120,18 +96,76 @@ const ENROLLMENT_FILTER_OPTIONS = {
120
96
  label: OUTREACH_STOP_REASON_LABELS[reason]
121
97
  }))
122
98
  };
123
- /** The filter-only columns never show. */ const ENROLLMENT_HIDDEN_COLUMNS = hiddenFilterVisibility(ENROLLMENT_FILTER_FIELDS, [
124
- 'status',
125
- 'stopReason'
126
- ]);
99
+ const CLICKED_OPTIONS = [
100
+ {
101
+ value: OUTREACH_CLICKED_FILTER_VALUE,
102
+ label: 'Yes'
103
+ },
104
+ {
105
+ value: OUTREACH_UNCLICKED_FILTER_VALUE,
106
+ label: 'No'
107
+ }
108
+ ];
109
+ /** Columns a reader can show from the column chooser, off until they do (AGL-3332). */ const CLICK_DETAIL_COLUMNS = [
110
+ 'linksFollowed',
111
+ 'lastClick',
112
+ 'scannerClicks'
113
+ ];
114
+ /** The table's grammar, with the click fields when the sequence counts clicks. */ function filterFieldsFor(trackClicks) {
115
+ return trackClicks ? OUTREACH_ENROLLMENT_LIST_QUERY.fields : BASE_FILTER_FIELDS;
116
+ }
117
+ /** What the grid's cells and its export read off one enrollment. */ function filterRow(enrollment, steps, clicks) {
118
+ return {
119
+ enrollment,
120
+ status: enrollment.status,
121
+ target: enrollment.target === 'lead' ? 'lead' : 'contact',
122
+ contactName: enrollment.contactName,
123
+ email: enrollment.email,
124
+ step: outreachCurrentStepLabel(enrollment, steps),
125
+ stopReason: outreachStopLabel(enrollment),
126
+ clicked: clicks.clicks > 0 ? OUTREACH_CLICKED_FILTER_VALUE : OUTREACH_UNCLICKED_FILTER_VALUE,
127
+ link: clicks.followed
128
+ };
129
+ }
130
+ /** The stop reason: its short label, with every word of it on hover and on the person's page. */ function StopReasonCell(props) {
131
+ var _OUTREACH_STOP_REASON_LABELS_enrollment_stopReason;
132
+ const { enrollment } = props;
133
+ if (!enrollment.stopReason) {
134
+ return /*#__PURE__*/ _jsx(Typography, {
135
+ variant: "body2",
136
+ color: "text.secondary",
137
+ children: "—"
138
+ });
139
+ }
140
+ const label = (_OUTREACH_STOP_REASON_LABELS_enrollment_stopReason = OUTREACH_STOP_REASON_LABELS[enrollment.stopReason]) != null ? _OUTREACH_STOP_REASON_LABELS_enrollment_stopReason : '';
141
+ const text = /*#__PURE__*/ _jsx(Typography, {
142
+ variant: "body2",
143
+ sx: _extends({
144
+ whiteSpace: 'normal',
145
+ lineHeight: 1.3
146
+ }, enrollment.stopDetail ? {
147
+ textDecoration: 'underline dotted',
148
+ textUnderlineOffset: 3,
149
+ cursor: 'help'
150
+ } : {}),
151
+ children: label
152
+ });
153
+ return enrollment.stopDetail ? /*#__PURE__*/ _jsx(Tooltip, {
154
+ title: outreachStopLabel(enrollment),
155
+ describeChild: true,
156
+ children: text
157
+ }) : text;
158
+ }
127
159
  /** The table's columns; the person and the actions are drawn from the row's enrollment. */ function columns(rowActions, trackClicks) {
128
160
  return [
129
161
  {
130
162
  field: 'person',
131
163
  headerName: 'Person',
132
- flex: 1,
133
- minWidth: 200,
164
+ flex: 1.2,
165
+ minWidth: 190,
134
166
  sortable: false,
167
+ // What the export writes for a column that is drawn rather than read.
168
+ valueGetter: (_value, row)=>row.enrollment.contactName ? `${row.enrollment.contactName} <${row.enrollment.email}>` : row.enrollment.email,
135
169
  renderCell: ({ row })=>/*#__PURE__*/ _jsxs(Stack, {
136
170
  spacing: 0,
137
171
  sx: {
@@ -172,7 +206,7 @@ const ENROLLMENT_FILTER_OPTIONS = {
172
206
  {
173
207
  field: 'status',
174
208
  headerName: 'Status',
175
- width: 120,
209
+ width: 110,
176
210
  renderCell: ({ row })=>/*#__PURE__*/ _jsx(OutreachEnrollmentStatusChip, {
177
211
  status: row.status
178
212
  })
@@ -208,132 +242,172 @@ const ENROLLMENT_FILTER_OPTIONS = {
208
242
  {
209
243
  field: 'nextSend',
210
244
  headerName: 'Next send',
211
- width: 190,
245
+ width: 170,
212
246
  sortable: false
213
247
  },
214
248
  {
215
249
  field: 'lastActivity',
216
250
  headerName: 'Last activity',
217
- width: 190,
251
+ width: 170,
218
252
  sortable: false
219
253
  },
220
254
  ...trackClicks ? [
221
255
  {
222
256
  field: 'clicks',
223
257
  headerName: 'Clicks',
224
- width: 130,
258
+ description: 'A person’s clicks, and how many different links they followed. Scanners are not counted.',
259
+ width: 110,
225
260
  sortable: false,
226
- renderCell: ({ row })=>{
227
- const engagement = readOutreachEngagement(row.enrollment.engagement);
228
- if (!engagement.clicks) {
229
- return /*#__PURE__*/ _jsx(Typography, {
230
- variant: "body2",
231
- color: "text.secondary",
232
- children: "—"
233
- });
234
- }
235
- return /*#__PURE__*/ _jsxs(Stack, {
236
- spacing: 0,
237
- sx: {
238
- justifyContent: 'center',
239
- height: '100%'
240
- },
241
- children: [
242
- /*#__PURE__*/ _jsx(Typography, {
243
- variant: "body2",
244
- children: engagement.clicks === 1 ? '1 click' : `${engagement.clicks} clicks`
245
- }),
246
- engagement.lastClickUrl ? /*#__PURE__*/ _jsx(Typography, {
247
- variant: "caption",
248
- color: "text.secondary",
249
- noWrap: true,
250
- children: engagement.lastClickUrl
251
- }) : null
252
- ]
253
- });
254
- }
261
+ renderCell: ({ row })=>/*#__PURE__*/ _jsx(Typography, {
262
+ variant: "body2",
263
+ color: row.clicks === '—' ? 'text.secondary' : undefined,
264
+ children: row.clicks
265
+ })
266
+ },
267
+ {
268
+ field: 'linksFollowed',
269
+ headerName: 'Links followed',
270
+ flex: 1,
271
+ minWidth: 220,
272
+ sortable: false
273
+ },
274
+ {
275
+ field: 'lastClick',
276
+ headerName: 'Last click',
277
+ width: 170,
278
+ sortable: false
279
+ },
280
+ {
281
+ field: 'scannerClicks',
282
+ headerName: 'Scanner clicks',
283
+ description: 'Clicks a security scanner made, which are not counted as the person’s.',
284
+ width: 130,
285
+ sortable: false
255
286
  }
256
287
  ] : [],
257
288
  {
258
289
  field: 'stopReason',
259
290
  headerName: 'Stop reason',
260
291
  flex: 1,
261
- minWidth: 170
292
+ minWidth: 170,
293
+ renderCell: ({ row })=>/*#__PURE__*/ _jsx(StopReasonCell, {
294
+ enrollment: row.enrollment
295
+ })
262
296
  },
263
297
  listActionsColumn((row)=>rowActions(row.enrollment), {
264
298
  width: 72
265
299
  })
266
300
  ];
267
301
  }
302
+ /** A plain click on a link, which the page handles itself; any other is the browser's. */ const plainClick = (event)=>!event.defaultPrevented && event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey;
268
303
  /**
269
304
  * A sequence's enrollments (AGL-2980): who is in it, where each person
270
305
  * stands, when their next step goes out in the mailbox's timezone, and the
271
306
  * four things a member can do about it — pause, resume, stop, and mark
272
307
  * do-not-contact. A table on wider screens, a list of cards on a phone.
308
+ *
309
+ * Each row opens the person's own page (AGL-3332), which holds everything
310
+ * else known about them: so a cell says one thing, whole, and the detail
311
+ * behind it is a click away rather than cut off at the column's edge.
273
312
  */ export function OutreachEnrollmentsTable(props) {
274
- var _CONFIRM_confirming_action, _CONFIRM_confirming_action1, _CONFIRM_confirming_action2;
275
- const { steps, timeZone, api } = props;
313
+ const { steps, timeZone, api, onOpen, hrefFor } = props;
276
314
  const trackClicks = props.trackClicks === true;
277
315
  const theme = useTheme();
278
316
  const narrow = useMediaQuery(theme.breakpoints.down('md'));
279
- const { enqueueSnackbar } = useSnackbar();
280
- const [confirming, setConfirming] = useState(null);
281
- const [detail, setDetail] = useState('');
282
- const [busy, setBusy] = useState(null);
283
- const [curating, setCurating] = useState(null);
284
- const gridFilter = useListGridFilter({
317
+ const actions = useOutreachEnrollmentActions({
318
+ api,
319
+ steps
320
+ });
321
+ const fields = useMemo(()=>filterFieldsFor(trackClicks), [
322
+ trackClicks
323
+ ]);
324
+ const gridFilter = useListGridFilter(_extends({
285
325
  selectFields: [
286
326
  'status',
287
327
  'target',
288
- 'stopReason'
328
+ 'stopReason',
329
+ 'clicked',
330
+ 'link'
289
331
  ]
290
- });
332
+ }, props.clauses !== undefined ? {
333
+ clauses: props.clauses,
334
+ onChange: props.onClausesChange
335
+ } : {}));
291
336
  const filtering = gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word)=>word.trim());
292
337
  /*
293
338
  * One page of the sequence's enrollments, newest enrolled first, with
294
339
  * every clause and the search word on the query (AGL-3321): a filter
295
340
  * reaches the enrollment on page forty as surely as the one on page one,
296
- * and nothing is matched over the rows that happen to be loaded.
341
+ * and nothing is matched over the rows that happen to be loaded. The
342
+ * click filters (AGL-3332) are on it too, answered by the `clicked` flag
343
+ * and the destinations the click route stores on each enrollment.
297
344
  */ const enrollments = useOutreachEnrollmentList(props.orgId, props.sequenceId, {
298
345
  clauses: gridFilter.clauses,
299
346
  search: gridFilter.searchWords
300
347
  });
301
- const act = async (enrollment, action, why)=>{
302
- setBusy(enrollment.id);
303
- try {
304
- const answer = await api.actOnEnrollment(enrollment.id, action, why);
305
- const others = answer.stoppedOthers;
306
- enqueueSnackbar(action === 'do_not_contact' ? `Marked do-not-contact${others ? `, and stopped in ${others} other ${others === 1 ? 'sequence' : 'sequences'}` : ''}.` : answer.changed ? DONE_LABELS[action] : 'Nothing to change.', {
307
- variant: 'success'
308
- });
309
- } catch (error) {
310
- enqueueSnackbar(error.message, {
311
- variant: 'error',
312
- allowDuplicate: true
313
- });
314
- } finally{
315
- setBusy(null);
316
- }
348
+ const loadedData = enrollments.rows;
349
+ const summaries = useMemo(()=>new Map(loadedData.map((enrollment)=>[
350
+ enrollment.id,
351
+ outreachClickSummary(enrollment.engagement)
352
+ ])), [
353
+ loadedData
354
+ ]);
355
+ const clicksOf = (enrollment)=>{
356
+ var _summaries_get;
357
+ return (_summaries_get = summaries.get(enrollment.id)) != null ? _summaries_get : outreachClickSummary(enrollment.engagement);
317
358
  };
318
- const choose = (enrollment, action)=>{
319
- if (action === 'curate') {
320
- setCurating(enrollment);
321
- } else if (CONFIRM[action]) {
322
- setDetail('');
323
- setConfirming({
324
- enrollment,
325
- action
326
- });
327
- } else {
328
- void act(enrollment, action);
359
+ /*
360
+ * The destinations the "Link followed" filter offers: the sequence's own
361
+ * rollup, and any a loaded person followed that it does not list — the
362
+ * rollup is capped, and a filter that could not name a destination a
363
+ * person followed would hide them from it.
364
+ */ const linkOptions = useMemo(()=>{
365
+ var _props_linkOptions;
366
+ const options = new Map();
367
+ for (const option of (_props_linkOptions = props.linkOptions) != null ? _props_linkOptions : [])options.set(option.value, option.label);
368
+ for (const summary of summaries.values()){
369
+ for (const url of summary.followed)if (!options.has(url)) options.set(url, url);
329
370
  }
330
- };
371
+ return [
372
+ ...options
373
+ ].map(([value, label])=>({
374
+ value,
375
+ label
376
+ }));
377
+ }, [
378
+ props.linkOptions,
379
+ summaries
380
+ ]);
381
+ const filterOptions = useMemo(()=>trackClicks ? _extends({}, BASE_FILTER_OPTIONS, {
382
+ clicked: CLICKED_OPTIONS,
383
+ link: linkOptions
384
+ }) : BASE_FILTER_OPTIONS, [
385
+ trackClicks,
386
+ linkOptions
387
+ ]);
388
+ const hiddenColumns = useMemo(()=>_extends({}, hiddenFilterVisibility(fields, [
389
+ 'status',
390
+ 'stopReason'
391
+ ]), trackClicks ? Object.fromEntries(CLICK_DETAIL_COLUMNS.map((column)=>[
392
+ column,
393
+ false
394
+ ])) : {}), [
395
+ fields,
396
+ trackClicks
397
+ ]);
398
+ /*
399
+ * A person who followed this destination before each click was recorded
400
+ * on its own, and another link after it, is known by their LAST link only
401
+ * — so a "Link followed" filter says it may miss them rather than
402
+ * implying the list is whole. Which people those are is exactly what the
403
+ * query cannot see, so the caveat stands whenever the filter does.
404
+ */ const linkFilterMayMiss = trackClicks && gridFilter.clauses.some((clause)=>clause.field === 'link');
331
405
  const chips = /*#__PURE__*/ _jsx(ListFilterChips, {
332
- fields: ENROLLMENT_FILTER_FIELDS,
406
+ fields: fields,
333
407
  headers: ENROLLMENT_FILTER_HEADERS,
334
408
  clauses: gridFilter.clauses,
335
409
  onChange: gridFilter.setClauses,
336
- options: ENROLLMENT_FILTER_OPTIONS
410
+ options: filterOptions
337
411
  });
338
412
  if (enrollments.status === 'loading' && !filtering) return /*#__PURE__*/ _jsx(OutreachLoading, {
339
413
  label: "Loading enrollments…"
@@ -362,15 +436,27 @@ const ENROLLMENT_FILTER_OPTIONS = {
362
436
  }
363
437
  const rowActions = (enrollment)=>/*#__PURE__*/ _jsx(ListRowActions, {
364
438
  label: enrollment.contactName || enrollment.email,
365
- items: actionsFor(enrollment, steps).map((action)=>({
366
- key: action,
367
- label: ACTION_LABELS[action],
368
- onClick: ()=>choose(enrollment, action),
369
- destructive: action === 'stop' || action === 'do_not_contact',
370
- disabled: busy === enrollment.id
371
- }))
439
+ items: actions.menuItems(enrollment)
372
440
  });
373
441
  const nextSend = (enrollment)=>enrollment.status === 'active' || enrollment.status === 'paused' ? formatOutreachTime(enrollment.nextDueAtMs, timeZone) : '—';
442
+ const personName = (enrollment)=>{
443
+ const name = enrollment.contactName || enrollment.email;
444
+ if (!hrefFor) return /*#__PURE__*/ _jsx(Typography, {
445
+ variant: "body2",
446
+ children: name
447
+ });
448
+ return /*#__PURE__*/ _jsx(Link, {
449
+ href: hrefFor(enrollment),
450
+ variant: "body2",
451
+ underline: "hover",
452
+ onClick: (event)=>{
453
+ if (!onOpen || !plainClick(event)) return;
454
+ event.preventDefault();
455
+ onOpen(enrollment);
456
+ },
457
+ children: name
458
+ });
459
+ };
374
460
  const person = (enrollment)=>/*#__PURE__*/ _jsxs(Stack, {
375
461
  spacing: 0,
376
462
  children: [
@@ -381,10 +467,7 @@ const ENROLLMENT_FILTER_OPTIONS = {
381
467
  alignItems: 'center'
382
468
  },
383
469
  children: [
384
- /*#__PURE__*/ _jsx(Typography, {
385
- variant: "body2",
386
- children: enrollment.contactName || enrollment.email
387
- }),
470
+ personName(enrollment),
388
471
  enrollment.target === 'lead' ? /*#__PURE__*/ _jsx(Chip, {
389
472
  size: "small",
390
473
  variant: "outlined",
@@ -406,12 +489,17 @@ const ENROLLMENT_FILTER_OPTIONS = {
406
489
  chips,
407
490
  /*#__PURE__*/ _jsx(ListQueryNotices, {
408
491
  refused: listQueryRefusals(enrollments.plan.refused, {
409
- fields: ENROLLMENT_FILTER_FIELDS,
492
+ fields,
410
493
  headers: ENROLLMENT_FILTER_HEADERS,
411
- options: ENROLLMENT_FILTER_OPTIONS
494
+ options: filterOptions
412
495
  }),
413
496
  notices: enrollments.plan.notices
414
497
  }),
498
+ linkFilterMayMiss ? /*#__PURE__*/ _jsx(Typography, {
499
+ variant: "caption",
500
+ color: "text.secondary",
501
+ children: "Clicks made before each one was recorded on its own kept only the person’s last link, so someone who followed this link and then another back then is not listed here."
502
+ }) : null,
415
503
  narrow ? /*#__PURE__*/ _jsx(Stack, {
416
504
  spacing: 1,
417
505
  component: "ul",
@@ -421,7 +509,9 @@ const ENROLLMENT_FILTER_OPTIONS = {
421
509
  m: 0
422
510
  },
423
511
  "aria-label": "Enrollments",
424
- children: pageRows.map((enrollment)=>/*#__PURE__*/ _jsx(Card, {
512
+ children: pageRows.map((enrollment)=>{
513
+ const clicks = clicksOf(enrollment);
514
+ return /*#__PURE__*/ _jsx(Card, {
425
515
  variant: "outlined",
426
516
  component: "li",
427
517
  children: /*#__PURE__*/ _jsx(CardContent, {
@@ -473,10 +563,10 @@ const ENROLLMENT_FILTER_OPTIONS = {
473
563
  color: "text.secondary",
474
564
  children: `Next send: ${nextSend(enrollment)} · Last activity: ${formatOutreachTime(lastActivityMs(enrollment), timeZone)}`
475
565
  }),
476
- trackClicks && readOutreachEngagement(enrollment.engagement).clicks ? /*#__PURE__*/ _jsx(Typography, {
566
+ trackClicks && clicks.clicks ? /*#__PURE__*/ _jsx(Typography, {
477
567
  variant: "body2",
478
568
  color: "text.secondary",
479
- children: `Clicked ${readOutreachEngagement(enrollment.engagement).clicks} ${readOutreachEngagement(enrollment.engagement).clicks === 1 ? 'time' : 'times'}`
569
+ children: `Clicks: ${outreachClickCountLabel(clicks)}`
480
570
  }) : null,
481
571
  outreachStopLabel(enrollment) ? /*#__PURE__*/ _jsx(Typography, {
482
572
  variant: "body2",
@@ -486,28 +576,34 @@ const ENROLLMENT_FILTER_OPTIONS = {
486
576
  ]
487
577
  })
488
578
  })
489
- }, enrollment.id))
579
+ }, enrollment.id);
580
+ })
490
581
  }) : /*#__PURE__*/ _jsx(ListTable, {
491
582
  "aria-label": "Enrollments",
492
- columns: listFilterGridColumns(columns(rowActions, trackClicks), ENROLLMENT_FILTER_FIELDS, ENROLLMENT_FILTER_OPTIONS, ENROLLMENT_FILTER_HEADERS),
583
+ columns: listFilterGridColumns(columns(rowActions, trackClicks), fields, filterOptions, ENROLLMENT_FILTER_HEADERS),
493
584
  initialState: {
494
585
  columns: {
495
- columnVisibilityModel: ENROLLMENT_HIDDEN_COLUMNS
586
+ columnVisibilityModel: hiddenColumns
496
587
  }
497
588
  },
498
589
  loading: enrollments.status === 'loading',
499
- rows: pageRows.map((enrollment)=>({
500
- $id: enrollment.id,
501
- enrollment,
502
- status: enrollment.status,
503
- target: enrollment.target === 'lead' ? 'lead' : 'contact',
504
- contactName: enrollment.contactName,
505
- email: enrollment.email,
506
- step: outreachCurrentStepLabel(enrollment, steps),
590
+ rows: pageRows.map((enrollment)=>{
591
+ var _enrollment_engagement;
592
+ const clicks = clicksOf(enrollment);
593
+ return _extends({
594
+ $id: enrollment.id
595
+ }, filterRow(enrollment, steps, clicks), {
507
596
  nextSend: nextSend(enrollment),
508
597
  lastActivity: formatOutreachTime(lastActivityMs(enrollment), timeZone),
598
+ // What the export writes; the cells draw these their own way.
599
+ clicks: outreachClickCountLabel(clicks),
600
+ linksFollowed: clicks.followed.join(' ') || '—',
601
+ lastClick: clicks.clicks ? formatOutreachTime((_enrollment_engagement = enrollment.engagement) == null ? void 0 : _enrollment_engagement.lastClickAtMs, timeZone) : '—',
602
+ scannerClicks: clicks.machineClicks ? String(clicks.machineClicks) : '—',
509
603
  stopReason: outreachStopLabel(enrollment) || '—'
510
- })),
604
+ });
605
+ }),
606
+ onOpen: onOpen ? (_id, row)=>onOpen(row.enrollment) : undefined,
511
607
  /*
512
608
  * The panel and the search are the grid's; every clause and the
513
609
  * search word are on the table's query (AGL-3321). The grid
@@ -529,64 +625,7 @@ const ENROLLMENT_FILTER_OPTIONS = {
529
625
  onPageChange: enrollments.setPage,
530
626
  onPageSizeChange: enrollments.setPageSize
531
627
  }),
532
- /*#__PURE__*/ _jsx(OutreachCurateStepDialog, {
533
- enrollment: curating,
534
- steps: steps,
535
- api: api,
536
- onClose: ()=>setCurating(null)
537
- }),
538
- /*#__PURE__*/ _jsx(Dialog, {
539
- open: Boolean(confirming),
540
- onClose: ()=>setConfirming(null),
541
- fullWidth: true,
542
- maxWidth: "xs",
543
- children: confirming ? /*#__PURE__*/ _jsxs(_Fragment, {
544
- children: [
545
- /*#__PURE__*/ _jsx(DialogTitle, {
546
- children: (_CONFIRM_confirming_action = CONFIRM[confirming.action]) == null ? void 0 : _CONFIRM_confirming_action.title
547
- }),
548
- /*#__PURE__*/ _jsx(DialogContent, {
549
- children: /*#__PURE__*/ _jsxs(Stack, {
550
- spacing: 2,
551
- children: [
552
- /*#__PURE__*/ _jsx(DialogContentText, {
553
- children: (_CONFIRM_confirming_action1 = CONFIRM[confirming.action]) == null ? void 0 : _CONFIRM_confirming_action1.body
554
- }),
555
- /*#__PURE__*/ _jsx(TextField, {
556
- label: "Why (optional)",
557
- value: detail,
558
- onChange: (event)=>setDetail(event.target.value),
559
- fullWidth: true,
560
- slotProps: {
561
- htmlInput: {
562
- maxLength: 200
563
- }
564
- }
565
- })
566
- ]
567
- })
568
- }),
569
- /*#__PURE__*/ _jsxs(DialogActions, {
570
- children: [
571
- /*#__PURE__*/ _jsx(Button, {
572
- onClick: ()=>setConfirming(null),
573
- children: "Cancel"
574
- }),
575
- /*#__PURE__*/ _jsx(Button, {
576
- color: "error",
577
- variant: "contained",
578
- onClick: ()=>{
579
- const { enrollment, action } = confirming;
580
- setConfirming(null);
581
- void act(enrollment, action, detail.trim() || undefined);
582
- },
583
- children: (_CONFIRM_confirming_action2 = CONFIRM[confirming.action]) == null ? void 0 : _CONFIRM_confirming_action2.label
584
- })
585
- ]
586
- })
587
- ]
588
- }) : null
589
- })
628
+ actions.dialogs
590
629
  ]
591
630
  });
592
631
  }