@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
@@ -15,24 +15,25 @@
15
15
  * limitations under the License.
16
16
  */ 'use client';
17
17
  import { _ as _extends } from "@swc/helpers/_/_extends";
18
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
19
- import { Alert, Card, CardContent, Divider, Stack, TableBody, TableCell, TableHead, TableRow, Typography } from "@mui/material";
18
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
19
+ import { Alert, ButtonBase, Card, CardContent, Divider, Link, Stack, TableBody, TableCell, TableHead, TableRow, Tooltip, Typography } from "@mui/material";
20
20
  // Not a raw MUI `Table`: a destination is a long string, and a raw table is
21
21
  // cut off by the card around it with nothing able to scroll to what is past
22
22
  // the edge (AGL-3045).
23
23
  import { ScrollTable } from "@aglyn/shared-ui-jsx/components/scroll-table.component";
24
24
  import { outreachSequenceLinkReport, outreachSequenceReport } from "../model/sequence-report.js";
25
25
  import { formatOutreachTime, OutreachLoadProblem } from "./outreach-ui.js";
26
- /** One figure, with what it counts under it. */ function Figure(props) {
27
- return /*#__PURE__*/ _jsxs(Stack, {
28
- spacing: 0,
29
- sx: {
30
- minWidth: 110
31
- },
26
+ /**
27
+ * One figure, with what it counts under it — and where it leads, when it
28
+ * leads somewhere. The sequence's Results and one person's page (AGL-3332)
29
+ * draw their numbers alike.
30
+ */ export function OutreachFigure(props) {
31
+ const body = /*#__PURE__*/ _jsxs(_Fragment, {
32
32
  children: [
33
33
  /*#__PURE__*/ _jsx(Typography, {
34
34
  variant: "h5",
35
35
  component: "p",
36
+ color: props.onClick ? 'primary' : undefined,
36
37
  children: props.value
37
38
  }),
38
39
  /*#__PURE__*/ _jsx(Typography, {
@@ -46,6 +47,36 @@ import { formatOutreachTime, OutreachLoadProblem } from "./outreach-ui.js";
46
47
  }) : null
47
48
  ]
48
49
  });
50
+ if (!props.onClick) {
51
+ return /*#__PURE__*/ _jsx(Stack, {
52
+ spacing: 0,
53
+ sx: {
54
+ minWidth: 110
55
+ },
56
+ children: body
57
+ });
58
+ }
59
+ return /*#__PURE__*/ _jsx(Tooltip, {
60
+ title: props.action,
61
+ children: /*#__PURE__*/ _jsx(ButtonBase, {
62
+ onClick: props.onClick,
63
+ "aria-label": `${props.value} ${props.label}. ${props.action}`,
64
+ sx: {
65
+ minWidth: 110,
66
+ display: 'flex',
67
+ flexDirection: 'column',
68
+ alignItems: 'flex-start',
69
+ textAlign: 'left',
70
+ borderRadius: 1,
71
+ px: 1,
72
+ mx: -1,
73
+ '&:hover, &.Mui-focusVisible': {
74
+ bgcolor: 'action.hover'
75
+ }
76
+ },
77
+ children: body
78
+ })
79
+ });
49
80
  }
50
81
  /** A whole number as the card prints one. */ const count = (value)=>value.toLocaleString();
51
82
  /**
@@ -86,27 +117,30 @@ export function OutreachSequenceReportCard(props) {
86
117
  rowGap: 2
87
118
  },
88
119
  children: [
89
- /*#__PURE__*/ _jsx(Figure, {
120
+ /*#__PURE__*/ _jsx(OutreachFigure, {
90
121
  label: "Emails sent",
91
122
  value: count(report.sent)
92
123
  }),
93
- /*#__PURE__*/ _jsx(Figure, _extends({
124
+ /*#__PURE__*/ _jsx(OutreachFigure, _extends({
94
125
  label: "People emailed",
95
126
  value: report.people === null ? '—' : count(report.people)
96
127
  }, report.people === null ? {
97
128
  hint: 'Not counted for this sequence'
98
129
  } : {})),
99
- /*#__PURE__*/ _jsx(Figure, {
130
+ /*#__PURE__*/ _jsx(OutreachFigure, _extends({
100
131
  label: "Clicked",
101
132
  value: count(report.uniqueClicks),
102
133
  hint: report.clicks === report.uniqueClicks ? undefined : `${count(report.clicks)} clicks in all`
103
- }),
104
- /*#__PURE__*/ _jsx(Figure, {
134
+ }, props.onShowClickers && report.uniqueClicks > 0 ? {
135
+ onClick: props.onShowClickers,
136
+ action: 'Show who clicked'
137
+ } : {})),
138
+ /*#__PURE__*/ _jsx(OutreachFigure, {
105
139
  label: "Click rate",
106
140
  value: clickRate.value,
107
141
  hint: clickRate.hint
108
142
  }),
109
- report.machineClicks > 0 ? /*#__PURE__*/ _jsx(Figure, {
143
+ report.machineClicks > 0 ? /*#__PURE__*/ _jsx(OutreachFigure, {
110
144
  label: "Scanner clicks",
111
145
  value: count(report.machineClicks),
112
146
  hint: "Not in the rate"
@@ -156,12 +190,25 @@ export function OutreachSequenceReportCard(props) {
156
190
  }),
157
191
  /*#__PURE__*/ _jsx(TableBody, {
158
192
  children: links.rows.map((row)=>/*#__PURE__*/ _jsxs(TableRow, {
193
+ hover: Boolean(props.onShowLink),
159
194
  children: [
160
195
  /*#__PURE__*/ _jsx(TableCell, {
161
196
  sx: {
162
197
  wordBreak: 'break-all'
163
198
  },
164
- children: row.url
199
+ children: props.onShowLink ? /*#__PURE__*/ _jsx(Link, {
200
+ component: "button",
201
+ type: "button",
202
+ variant: "body2",
203
+ underline: "hover",
204
+ onClick: ()=>props.onShowLink == null ? void 0 : props.onShowLink.call(props, row.url),
205
+ "aria-label": `Show who followed ${row.url}`,
206
+ sx: {
207
+ textAlign: 'left',
208
+ wordBreak: 'break-all'
209
+ },
210
+ children: row.url
211
+ }) : row.url
165
212
  }),
166
213
  /*#__PURE__*/ _jsx(TableCell, {
167
214
  align: "right",
@@ -176,6 +223,7 @@ export function OutreachSequenceReportCard(props) {
176
223
  variant: "caption",
177
224
  color: "text.secondary",
178
225
  children: [
226
+ props.onShowLink ? 'Select a destination to see who followed it. ' : '',
179
227
  "Two links to the same page are counted as one destination, whatever tracking parameters follow the address.",
180
228
  links.truncated ? ` ${count(links.overflowClicks)} more clicks went to destinations past the ones listed.` : ''
181
229
  ]
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/sequence-report-card.tsx"],"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'use client'\n\nimport type { CampaignRate } from '@aglyn/shared-ui-email-campaigns/model'\nimport {\n Alert,\n Card,\n CardContent,\n Divider,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\n// Not a raw MUI `Table`: a destination is a long string, and a raw table is\n// cut off by the card around it with nothing able to scroll to what is past\n// the edge (AGL-3045).\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport type { OutreachSequence } from '../model/outreach.types'\nimport {\n outreachSequenceLinkReport,\n outreachSequenceReport,\n} from '../model/sequence-report'\nimport { formatOutreachTime, OutreachLoadProblem } from './outreach-ui'\nimport type { OutreachSequenceLinksLoad } from './use-outreach-data'\n\n/**\n * WHAT A SEQUENCE MEASURED (AGL-3239).\n *\n * Four figures and one rate, and the sentences saying what each of them is\n * not. The decisions are all in `model/sequence-report.ts`; this renders\n * them and adds no arithmetic of its own — in particular it never divides,\n * so there is no second place a percentage can be computed differently from\n * the way a campaign's report computes it.\n *\n * The caveats are rendered as prominently as the numbers rather than as\n * small print. \"Opens aren't measured\" is the thing a reader coming from\n * the campaign report most needs told, and a blank where an open rate would\n * be says the opposite of what is true.\n */\n\nexport interface OutreachSequenceReportCardProps {\n sequence: Pick<OutreachSequence, 'stats' | 'settings'>\n links: OutreachSequenceLinksLoad\n /** The mailbox's zone, which times are read in; `null` for the reader's own. */\n timeZone: string | null\n}\n\n/** One figure, with what it counts under it. */\nfunction Figure(props: { label: string; value: string; hint?: string }) {\n return (\n <Stack spacing={0} sx={{ minWidth: 110 }}>\n <Typography variant=\"h5\" component=\"p\">\n {props.value}\n </Typography>\n <Typography variant=\"body2\">{props.label}</Typography>\n {props.hint ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {props.hint}\n </Typography>\n ) : null}\n </Stack>\n )\n}\n\n/** A whole number as the card prints one. */\nconst count = (value: number): string => value.toLocaleString()\n\n/**\n * A rate and the denominator it was taken over, or the em dash that stands\n * for a rate the model refused to take. The denominator is printed beside\n * the percentage, never dropped: \"12% of people emailed\" and \"12% of people\n * who opened\" are different claims and a bare \"12%\" is neither.\n */\nfunction rateText(rate: CampaignRate | null): { value: string; hint?: string } {\n if (!rate) return { value: '—' }\n return {\n value: `${(rate.value * 100).toFixed(1)}%`,\n hint: `${count(rate.numerator)} of ${count(rate.denominator)} ${rate.denominatorLabel}`,\n }\n}\n\nexport function OutreachSequenceReportCard(props: OutreachSequenceReportCardProps) {\n const report = outreachSequenceReport(\n props.sequence.stats,\n props.sequence.settings.trackClicks,\n )\n const clickRate = rateText(report.rates.click)\n const links = outreachSequenceLinkReport(\n props.links.status === 'ready' ? (props.links.data ?? undefined) : undefined,\n )\n\n return (\n <Card variant=\"outlined\">\n <CardContent>\n <Stack spacing={2}>\n <Typography variant=\"h6\" component=\"h2\">\n Results\n </Typography>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap', rowGap: 2 }}>\n <Figure label=\"Emails sent\" value={count(report.sent)} />\n <Figure\n label=\"People emailed\"\n value={report.people === null ? '—' : count(report.people)}\n {...(report.people === null ? { hint: 'Not counted for this sequence' } : {})}\n />\n <Figure\n label=\"Clicked\"\n value={count(report.uniqueClicks)}\n hint={\n report.clicks === report.uniqueClicks\n ? undefined\n : `${count(report.clicks)} clicks in all`\n }\n />\n <Figure label=\"Click rate\" value={clickRate.value} hint={clickRate.hint} />\n {report.machineClicks > 0 ? (\n <Figure\n label=\"Scanner clicks\"\n value={count(report.machineClicks)}\n hint=\"Not in the rate\"\n />\n ) : null}\n </Stack>\n {report.lastClickAtMs === null ? null : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n Last click {formatOutreachTime(report.lastClickAtMs, props.timeZone)}\n </Typography>\n )}\n\n {report.caveats.map((entry) => (\n <Alert key={entry.id} severity=\"info\" variant=\"outlined\">\n {entry.message}\n </Alert>\n ))}\n\n {props.links.status === 'error' || props.links.status === 'refused' ? (\n <OutreachLoadProblem status={props.links.status} what=\"the link report\" />\n ) : links.rows.length ? (\n <Stack spacing={1}>\n <Divider />\n <Typography variant=\"subtitle2\" component=\"h3\">\n Links followed\n </Typography>\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>Destination</TableCell>\n <TableCell align=\"right\">Clicks</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {links.rows.map((row) => (\n <TableRow key={row.url}>\n <TableCell sx={{ wordBreak: 'break-all' }}>{row.url}</TableCell>\n <TableCell align=\"right\">{count(row.clicks)}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n <Typography variant=\"caption\" color=\"text.secondary\">\n Two links to the same page are counted as one destination, whatever\n tracking parameters follow the address.\n {links.truncated\n ? ` ${count(links.overflowClicks)} more clicks went to destinations past the ones listed.`\n : ''}\n </Typography>\n </Stack>\n ) : null}\n </Stack>\n </CardContent>\n </Card>\n )\n}\n"],"names":["Alert","Card","CardContent","Divider","Stack","TableBody","TableCell","TableHead","TableRow","Typography","ScrollTable","outreachSequenceLinkReport","outreachSequenceReport","formatOutreachTime","OutreachLoadProblem","Figure","props","spacing","sx","minWidth","variant","component","value","label","hint","color","count","toLocaleString","rateText","rate","toFixed","numerator","denominator","denominatorLabel","OutreachSequenceReportCard","report","sequence","stats","settings","trackClicks","clickRate","rates","click","links","status","data","undefined","direction","flexWrap","rowGap","sent","people","uniqueClicks","clicks","machineClicks","lastClickAtMs","timeZone","caveats","map","entry","severity","message","id","what","rows","length","size","align","row","wordBreak","url","truncated","overflowClicks"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAGA,SACEA,KAAK,EACLC,IAAI,EACJC,WAAW,EACXC,OAAO,EACPC,KAAK,EACLC,SAAS,EACTC,SAAS,EACTC,SAAS,EACTC,QAAQ,EACRC,UAAU,QACL,gBAAe;AACtB,4EAA4E;AAC5E,4EAA4E;AAC5E,uBAAuB;AACvB,SAASC,WAAW,QAAQ,yDAAwD;AAEpF,SACEC,0BAA0B,EAC1BC,sBAAsB,QACjB,8BAA0B;AACjC,SAASC,kBAAkB,EAAEC,mBAAmB,QAAQ,mBAAe;AAyBvE,8CAA8C,GAC9C,SAASC,OAAOC,KAAsD;IACpE,qBACE,MAACZ;QAAMa,SAAS;QAAGC,IAAI;YAAEC,UAAU;QAAI;;0BACrC,KAACV;gBAAWW,SAAQ;gBAAKC,WAAU;0BAChCL,MAAMM,KAAK;;0BAEd,KAACb;gBAAWW,SAAQ;0BAASJ,MAAMO,KAAK;;YACvCP,MAAMQ,IAAI,iBACT,KAACf;gBAAWW,SAAQ;gBAAUK,OAAM;0BACjCT,MAAMQ,IAAI;iBAEX;;;AAGV;AAEA,2CAA2C,GAC3C,MAAME,QAAQ,CAACJ,QAA0BA,MAAMK,cAAc;AAE7D;;;;;CAKC,GACD,SAASC,SAASC,IAAyB;IACzC,IAAI,CAACA,MAAM,OAAO;QAAEP,OAAO;IAAI;IAC/B,OAAO;QACLA,OAAO,GAAG,AAACO,CAAAA,KAAKP,KAAK,GAAG,GAAE,EAAGQ,OAAO,CAAC,GAAG,CAAC,CAAC;QAC1CN,MAAM,GAAGE,MAAMG,KAAKE,SAAS,EAAE,IAAI,EAAEL,MAAMG,KAAKG,WAAW,EAAE,CAAC,EAAEH,KAAKI,gBAAgB,EAAE;IACzF;AACF;AAEA,OAAO,SAASC,2BAA2BlB,KAAsC;QAO3CA;IANpC,MAAMmB,SAASvB,uBACbI,MAAMoB,QAAQ,CAACC,KAAK,EACpBrB,MAAMoB,QAAQ,CAACE,QAAQ,CAACC,WAAW;IAErC,MAAMC,YAAYZ,SAASO,OAAOM,KAAK,CAACC,KAAK;IAC7C,MAAMC,QAAQhC,2BACZK,MAAM2B,KAAK,CAACC,MAAM,KAAK,WAAW5B,oBAAAA,MAAM2B,KAAK,CAACE,IAAI,YAAhB7B,oBAAoB8B,YAAaA;IAGrE,qBACE,KAAC7C;QAAKmB,SAAQ;kBACZ,cAAA,KAAClB;sBACC,cAAA,MAACE;gBAAMa,SAAS;;kCACd,KAACR;wBAAWW,SAAQ;wBAAKC,WAAU;kCAAK;;kCAGxC,MAACjB;wBAAM2C,WAAU;wBAAM9B,SAAS;wBAAGC,IAAI;4BAAE8B,UAAU;4BAAQC,QAAQ;wBAAE;;0CACnE,KAAClC;gCAAOQ,OAAM;gCAAcD,OAAOI,MAAMS,OAAOe,IAAI;;0CACpD,KAACnC;gCACCQ,OAAM;gCACND,OAAOa,OAAOgB,MAAM,KAAK,OAAO,MAAMzB,MAAMS,OAAOgB,MAAM;+BACpDhB,OAAOgB,MAAM,KAAK,OAAO;gCAAE3B,MAAM;4BAAgC,IAAI,CAAC;0CAE7E,KAACT;gCACCQ,OAAM;gCACND,OAAOI,MAAMS,OAAOiB,YAAY;gCAChC5B,MACEW,OAAOkB,MAAM,KAAKlB,OAAOiB,YAAY,GACjCN,YACA,GAAGpB,MAAMS,OAAOkB,MAAM,EAAE,cAAc,CAAC;;0CAG/C,KAACtC;gCAAOQ,OAAM;gCAAaD,OAAOkB,UAAUlB,KAAK;gCAAEE,MAAMgB,UAAUhB,IAAI;;4BACtEW,OAAOmB,aAAa,GAAG,kBACtB,KAACvC;gCACCQ,OAAM;gCACND,OAAOI,MAAMS,OAAOmB,aAAa;gCACjC9B,MAAK;iCAEL;;;oBAELW,OAAOoB,aAAa,KAAK,OAAO,qBAC/B,MAAC9C;wBAAWW,SAAQ;wBAAQK,OAAM;;4BAAiB;4BACrCZ,mBAAmBsB,OAAOoB,aAAa,EAAEvC,MAAMwC,QAAQ;;;oBAItErB,OAAOsB,OAAO,CAACC,GAAG,CAAC,CAACC,sBACnB,KAAC3D;4BAAqB4D,UAAS;4BAAOxC,SAAQ;sCAC3CuC,MAAME,OAAO;2BADJF,MAAMG,EAAE;oBAKrB9C,MAAM2B,KAAK,CAACC,MAAM,KAAK,WAAW5B,MAAM2B,KAAK,CAACC,MAAM,KAAK,0BACxD,KAAC9B;wBAAoB8B,QAAQ5B,MAAM2B,KAAK,CAACC,MAAM;wBAAEmB,MAAK;yBACpDpB,MAAMqB,IAAI,CAACC,MAAM,iBACnB,MAAC7D;wBAAMa,SAAS;;0CACd,KAACd;0CACD,KAACM;gCAAWW,SAAQ;gCAAYC,WAAU;0CAAK;;0CAG/C,MAACX;gCAAYwD,MAAK;;kDAChB,KAAC3D;kDACC,cAAA,MAACC;;8DACC,KAACF;8DAAU;;8DACX,KAACA;oDAAU6D,OAAM;8DAAQ;;;;;kDAG7B,KAAC9D;kDACEsC,MAAMqB,IAAI,CAACN,GAAG,CAAC,CAACU,oBACf,MAAC5D;;kEACC,KAACF;wDAAUY,IAAI;4DAAEmD,WAAW;wDAAY;kEAAID,IAAIE,GAAG;;kEACnD,KAAChE;wDAAU6D,OAAM;kEAASzC,MAAM0C,IAAIf,MAAM;;;+CAF7Be,IAAIE,GAAG;;;;0CAO5B,MAAC7D;gCAAWW,SAAQ;gCAAUK,OAAM;;oCAAiB;oCAGlDkB,MAAM4B,SAAS,GACZ,CAAC,CAAC,EAAE7C,MAAMiB,MAAM6B,cAAc,EAAE,uDAAuD,CAAC,GACxF;;;;yBAGN;;;;;AAKd"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/sequence-report-card.tsx"],"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'use client'\n\nimport type { CampaignRate } from '@aglyn/shared-ui-email-campaigns/model'\nimport {\n Alert,\n ButtonBase,\n Card,\n CardContent,\n Divider,\n Link,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport type { ReactNode } from 'react'\n// Not a raw MUI `Table`: a destination is a long string, and a raw table is\n// cut off by the card around it with nothing able to scroll to what is past\n// the edge (AGL-3045).\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport type { OutreachSequence } from '../model/outreach.types'\nimport {\n outreachSequenceLinkReport,\n outreachSequenceReport,\n} from '../model/sequence-report'\nimport { formatOutreachTime, OutreachLoadProblem } from './outreach-ui'\nimport type { OutreachSequenceLinksLoad } from './use-outreach-data'\n\n/**\n * WHAT A SEQUENCE MEASURED (AGL-3239).\n *\n * Four figures and one rate, and the sentences saying what each of them is\n * not. The decisions are all in `model/sequence-report.ts`; this renders\n * them and adds no arithmetic of its own — in particular it never divides,\n * so there is no second place a percentage can be computed differently from\n * the way a campaign's report computes it.\n *\n * The caveats are rendered as prominently as the numbers rather than as\n * small print. \"Opens aren't measured\" is the thing a reader coming from\n * the campaign report most needs told, and a blank where an open rate would\n * be says the opposite of what is true.\n */\n\nexport interface OutreachSequenceReportCardProps {\n sequence: Pick<OutreachSequence, 'stats' | 'settings'>\n links: OutreachSequenceLinksLoad\n /** The mailbox's zone, which times are read in; `null` for the reader's own. */\n timeZone: string | null\n /** Lists the people who clicked, on the Enrollments tab (AGL-3332). */\n onShowClickers?: () => void\n /** Lists the people who followed one destination, on the Enrollments tab (AGL-3332). */\n onShowLink?: (url: string) => void\n}\n\n/**\n * One figure, with what it counts under it — and where it leads, when it\n * leads somewhere. The sequence's Results and one person's page (AGL-3332)\n * draw their numbers alike.\n */\nexport function OutreachFigure(props: {\n label: string\n value: string\n hint?: string\n /** Opens the list behind the figure; named for a screen reader by `action`. */\n onClick?: () => void\n action?: string\n}) {\n const body: ReactNode = (\n <>\n <Typography variant=\"h5\" component=\"p\" color={props.onClick ? 'primary' : undefined}>\n {props.value}\n </Typography>\n <Typography variant=\"body2\">{props.label}</Typography>\n {props.hint ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {props.hint}\n </Typography>\n ) : null}\n </>\n )\n if (!props.onClick) {\n return (\n <Stack spacing={0} sx={{ minWidth: 110 }}>\n {body}\n </Stack>\n )\n }\n return (\n <Tooltip title={props.action}>\n <ButtonBase\n onClick={props.onClick}\n aria-label={`${props.value} ${props.label}. ${props.action}`}\n sx={{\n minWidth: 110,\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'flex-start',\n textAlign: 'left',\n borderRadius: 1,\n px: 1,\n mx: -1,\n '&:hover, &.Mui-focusVisible': { bgcolor: 'action.hover' },\n }}\n >\n {body}\n </ButtonBase>\n </Tooltip>\n )\n}\n\n/** A whole number as the card prints one. */\nconst count = (value: number): string => value.toLocaleString()\n\n/**\n * A rate and the denominator it was taken over, or the em dash that stands\n * for a rate the model refused to take. The denominator is printed beside\n * the percentage, never dropped: \"12% of people emailed\" and \"12% of people\n * who opened\" are different claims and a bare \"12%\" is neither.\n */\nfunction rateText(rate: CampaignRate | null): { value: string; hint?: string } {\n if (!rate) return { value: '—' }\n return {\n value: `${(rate.value * 100).toFixed(1)}%`,\n hint: `${count(rate.numerator)} of ${count(rate.denominator)} ${rate.denominatorLabel}`,\n }\n}\n\nexport function OutreachSequenceReportCard(props: OutreachSequenceReportCardProps) {\n const report = outreachSequenceReport(\n props.sequence.stats,\n props.sequence.settings.trackClicks,\n )\n const clickRate = rateText(report.rates.click)\n const links = outreachSequenceLinkReport(\n props.links.status === 'ready' ? (props.links.data ?? undefined) : undefined,\n )\n\n return (\n <Card variant=\"outlined\">\n <CardContent>\n <Stack spacing={2}>\n <Typography variant=\"h6\" component=\"h2\">\n Results\n </Typography>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap', rowGap: 2 }}>\n <OutreachFigure label=\"Emails sent\" value={count(report.sent)} />\n <OutreachFigure\n label=\"People emailed\"\n value={report.people === null ? '—' : count(report.people)}\n {...(report.people === null ? { hint: 'Not counted for this sequence' } : {})}\n />\n <OutreachFigure\n label=\"Clicked\"\n value={count(report.uniqueClicks)}\n hint={\n report.clicks === report.uniqueClicks\n ? undefined\n : `${count(report.clicks)} clicks in all`\n }\n {...(props.onShowClickers && report.uniqueClicks > 0\n ? { onClick: props.onShowClickers, action: 'Show who clicked' }\n : {})}\n />\n <OutreachFigure label=\"Click rate\" value={clickRate.value} hint={clickRate.hint} />\n {report.machineClicks > 0 ? (\n <OutreachFigure\n label=\"Scanner clicks\"\n value={count(report.machineClicks)}\n hint=\"Not in the rate\"\n />\n ) : null}\n </Stack>\n {report.lastClickAtMs === null ? null : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n Last click {formatOutreachTime(report.lastClickAtMs, props.timeZone)}\n </Typography>\n )}\n\n {report.caveats.map((entry) => (\n <Alert key={entry.id} severity=\"info\" variant=\"outlined\">\n {entry.message}\n </Alert>\n ))}\n\n {props.links.status === 'error' || props.links.status === 'refused' ? (\n <OutreachLoadProblem status={props.links.status} what=\"the link report\" />\n ) : links.rows.length ? (\n <Stack spacing={1}>\n <Divider />\n <Typography variant=\"subtitle2\" component=\"h3\">\n Links followed\n </Typography>\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>Destination</TableCell>\n <TableCell align=\"right\">Clicks</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {links.rows.map((row) => (\n <TableRow key={row.url} hover={Boolean(props.onShowLink)}>\n <TableCell sx={{ wordBreak: 'break-all' }}>\n {props.onShowLink ? (\n <Link\n component=\"button\"\n type=\"button\"\n variant=\"body2\"\n underline=\"hover\"\n onClick={() => props.onShowLink?.(row.url)}\n aria-label={`Show who followed ${row.url}`}\n sx={{ textAlign: 'left', wordBreak: 'break-all' }}\n >\n {row.url}\n </Link>\n ) : (\n row.url\n )}\n </TableCell>\n <TableCell align=\"right\">{count(row.clicks)}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {props.onShowLink ? 'Select a destination to see who followed it. ' : ''}\n Two links to the same page are counted as one destination, whatever\n tracking parameters follow the address.\n {links.truncated\n ? ` ${count(links.overflowClicks)} more clicks went to destinations past the ones listed.`\n : ''}\n </Typography>\n </Stack>\n ) : null}\n </Stack>\n </CardContent>\n </Card>\n )\n}\n"],"names":["Alert","ButtonBase","Card","CardContent","Divider","Link","Stack","TableBody","TableCell","TableHead","TableRow","Tooltip","Typography","ScrollTable","outreachSequenceLinkReport","outreachSequenceReport","formatOutreachTime","OutreachLoadProblem","OutreachFigure","props","body","variant","component","color","onClick","undefined","value","label","hint","spacing","sx","minWidth","title","action","aria-label","display","flexDirection","alignItems","textAlign","borderRadius","px","mx","bgcolor","count","toLocaleString","rateText","rate","toFixed","numerator","denominator","denominatorLabel","OutreachSequenceReportCard","report","sequence","stats","settings","trackClicks","clickRate","rates","click","links","status","data","direction","flexWrap","rowGap","sent","people","uniqueClicks","clicks","onShowClickers","machineClicks","lastClickAtMs","timeZone","caveats","map","entry","severity","message","id","what","rows","length","size","align","row","hover","Boolean","onShowLink","wordBreak","type","underline","url","truncated","overflowClicks"],"mappings":"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"}
@@ -7,7 +7,10 @@ export interface OutreachSequencesSectionProps {
7
7
  org?: Record<string, unknown> | null;
8
8
  /** The section's own path: `/[orgSlug]/outreach/sequences`. */
9
9
  sectionPath: string;
10
- /** The path below the section: `[]`, `['new']`, `[id]` or `[id, 'enrollments']`. */
10
+ /**
11
+ * The path below the section: `[]`, `['new']`, `[id]`, `[id, 'enrollments']`
12
+ * or `[id, 'enrollments', enrollmentId]` — one person's page (AGL-3332).
13
+ */
11
14
  subpath: readonly string[];
12
15
  /** The Mailboxes section: `/[orgSlug]/outreach/mailboxes`. */
13
16
  mailboxesPath: string;
@@ -32,6 +32,7 @@ import { useMemo } from "react";
32
32
  import { OUTREACH_SEQUENCE_STATUSES } from "../model/outreach.types.js";
33
33
  import { OUTREACH_SEQUENCE_LIST_QUERY } from "../model/sequence-list-query.js";
34
34
  import { OUTREACH_SEQUENCE_STATUS_LABELS, OutreachLink, OutreachLoading, OutreachLoadProblem, OutreachSequenceStatusChip, useOutreachNavigate } from "./outreach-ui.js";
35
+ import { OutreachEnrollmentDetail } from "./enrollment-detail.js";
35
36
  import { OutreachSequenceDetail } from "./sequence-detail.js";
36
37
  import { OutreachSequenceEditor } from "./sequence-editor.js";
37
38
  import { useOutreachApi } from "./use-outreach-api.js";
@@ -52,7 +53,7 @@ import { useOutreachComplianceSettings } from "./use-outreach-settings.js";
52
53
  if (!orgId) return /*#__PURE__*/ _jsx(OutreachLoading, {
53
54
  label: "Loading sequences…"
54
55
  });
55
- const [first, second] = subpath;
56
+ const [first, second, third] = subpath;
56
57
  if (first === OUTREACH_NEW_SEQUENCE_SEGMENT) {
57
58
  return /*#__PURE__*/ _jsxs(Stack, {
58
59
  spacing: 2,
@@ -79,6 +80,16 @@ import { useOutreachComplianceSettings } from "./use-outreach-settings.js";
79
80
  ]
80
81
  });
81
82
  }
83
+ if (first && second === 'enrollments' && third) {
84
+ return /*#__PURE__*/ _jsx(OutreachEnrollmentDetail, {
85
+ orgId: orgId,
86
+ orgMount: props.orgMount,
87
+ sectionPath: sectionPath,
88
+ sequenceId: first,
89
+ enrollmentId: third,
90
+ mailboxes: mailboxes
91
+ });
92
+ }
82
93
  if (first) {
83
94
  return /*#__PURE__*/ _jsx(OutreachSequenceDetail, {
84
95
  orgId: orgId,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/sequences-section.tsx"],"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'use client'\n\nimport { pluginDocsHelp, type ConsolePluginOrgMount } from '@aglyn/aglyn'\nimport { mdiPlus } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport EmptyStateComponent from '@aglyn/shared-ui-jsx/components/empty-state.component'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport {\n ListTable,\n type ListTableProps,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport ListQueryNotices, {\n listQueryRefusals,\n} from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport { hiddenFilterVisibility } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport { listFilterGridColumns } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport {\n Button,\n Card,\n CardContent,\n Chip,\n Stack,\n Typography,\n useMediaQuery,\n useTheme,\n} from '@mui/material'\nimport { useMemo } from 'react'\nimport {\n OUTREACH_SEQUENCE_STATUSES,\n type OutreachMailbox,\n type OutreachSequence,\n} from '../model/outreach.types'\nimport { OUTREACH_SEQUENCE_LIST_QUERY } from '../model/sequence-list-query'\nimport {\n OUTREACH_SEQUENCE_STATUS_LABELS,\n OutreachLink,\n OutreachLoading,\n OutreachLoadProblem,\n OutreachSequenceStatusChip,\n useOutreachNavigate,\n} from './outreach-ui'\nimport {\n OutreachSequenceDetail,\n type OutreachSequenceTab,\n} from './sequence-detail'\nimport { OutreachSequenceEditor } from './sequence-editor'\nimport { useOutreachApi } from './use-outreach-api'\nimport {\n type OutreachSequenceCounts,\n useOutreachSequenceCounts,\n useOutreachSequenceList,\n} from './use-outreach-data'\nimport { useOutreachMailboxes } from './use-outreach-mailboxes'\nimport { useOutreachComplianceSettings } from './use-outreach-settings'\n\nexport interface OutreachSequencesSectionProps {\n /** The organization the hub is mounted under; `null` until the shell knows it. */\n orgId: string | null\n orgMount?: ConsolePluginOrgMount\n org?: Record<string, unknown> | null\n /** The section's own path: `/[orgSlug]/outreach/sequences`. */\n sectionPath: string\n /** The path below the section: `[]`, `['new']`, `[id]` or `[id, 'enrollments']`. */\n subpath: readonly string[]\n /** The Mailboxes section: `/[orgSlug]/outreach/mailboxes`. */\n mailboxesPath: string\n /** The Compliance section: `/[orgSlug]/outreach/compliance`. */\n compliancePath: string\n}\n\n/** The new-sequence page's segment below the section. */\nexport const OUTREACH_NEW_SEQUENCE_SEGMENT = 'new'\n\n/**\n * The Sequences section (AGL-2980), and the pages below it: the list, a new\n * sequence, and one sequence's steps and enrollments. Each is a real URL, so\n * a sequence is a link a rep can keep and the back button walks them.\n */\nexport function OutreachSequencesSection(props: OutreachSequencesSectionProps) {\n const { orgId, sectionPath, subpath } = props\n const api = useOutreachApi(orgId)\n const settings = useOutreachComplianceSettings(api, orgId)\n const mailboxes = useOutreachMailboxes(orgId)\n const navigate = useOutreachNavigate()\n\n if (!orgId) return <OutreachLoading label=\"Loading sequences…\" />\n const [first, second] = subpath\n if (first === OUTREACH_NEW_SEQUENCE_SEGMENT) {\n return (\n <Stack spacing={2}>\n <OutreachLink href={sectionPath}>Sequences</OutreachLink>\n <Typography variant=\"h5\" component=\"h2\">\n New sequence\n </Typography>\n <OutreachSequenceEditor\n orgId={orgId}\n orgMount={props.orgMount}\n sequence={null}\n settings={settings}\n mailboxes={mailboxes}\n mailboxesPath={props.mailboxesPath}\n onSaved={(sequence) => navigate(`${sectionPath}/${sequence.id}`)}\n onCancel={() => navigate(sectionPath)}\n />\n </Stack>\n )\n }\n if (first) {\n return (\n <OutreachSequenceDetail\n orgId={orgId}\n orgMount={props.orgMount}\n org={props.org}\n sectionPath={sectionPath}\n sequenceId={first}\n tab={\n (second === 'enrollments'\n ? 'enrollments'\n : 'steps') as OutreachSequenceTab\n }\n settings={settings}\n mailboxes={mailboxes}\n mailboxesPath={props.mailboxesPath}\n compliancePath={props.compliancePath}\n />\n )\n }\n return (\n <OutreachSequenceList\n orgId={orgId}\n sectionPath={sectionPath}\n mailboxes={mailboxes.mailboxes}\n />\n )\n}\nOutreachSequencesSection.displayName = 'OutreachSequencesSection'\n\nconst COUNT_COLUMNS: ReadonlyArray<[keyof OutreachSequenceCounts, string]> = [\n ['enrolled', 'Enrolled'],\n ['active', 'Active'],\n ['replied', 'Replied'],\n ['bounced', 'Bounced'],\n ['optedOut', 'Opted out'],\n]\n\n/*\n * What the sequences grid's Filters panel offers (AGL-3317), each field on\n * the list's Firestore query (AGL-3321, `OUTREACH_SEQUENCE_LIST_QUERY`).\n */\nconst SEQUENCE_FILTER_FIELDS = OUTREACH_SEQUENCE_LIST_QUERY.fields\nconst SEQUENCE_FILTER_HEADERS: Readonly<Record<string, string>> = {\n name: 'Name',\n mailbox: 'Mailbox',\n status: 'Status',\n created: 'Created',\n}\n/** `created` is no column of its own: a filter-only column, never shown. */\nconst SEQUENCE_HIDDEN_COLUMNS = hiddenFilterVisibility(SEQUENCE_FILTER_FIELDS, [\n 'name',\n 'mailbox',\n 'status',\n])\nconst SEQUENCE_STATUS_OPTIONS = OUTREACH_SEQUENCE_STATUSES.map((status) => ({\n value: status,\n label: OUTREACH_SEQUENCE_STATUS_LABELS[status],\n}))\n\n/** How a mailbox reads in the list: the address it sends as. */\nconst mailboxName = (mailbox: OutreachMailbox) => mailbox.sendAs || mailbox.email\n\n/**\n * Every sequence: its mailbox, its status, and how many people it enrolled\n * and where they stand — a table on wider screens, cards on a phone.\n *\n * ## Asked of Firestore, a page at a time\n *\n * The Filters panel and the search box are the grid's; the clauses and the\n * words go onto ONE query over the organization's sequences, newest first,\n * and each page is a page of that query's matches (AGL-3321). A clause the\n * query cannot hold beside the others is not applied, and the notice above\n * the list says which and why — never answered over the sequences that\n * happen to be loaded.\n */\nexport function OutreachSequenceList(props: {\n orgId: string\n sectionPath: string\n mailboxes: readonly OutreachMailbox[]\n}) {\n const { orgId, sectionPath } = props\n const theme = useTheme()\n const narrow = useMediaQuery(theme.breakpoints.down('md'))\n const navigate = useOutreachNavigate()\n const gridFilter = useListGridFilter({ selectFields: ['status', 'mailbox'] })\n const sequences = useOutreachSequenceList(orgId, {\n clauses: gridFilter.clauses,\n search: gridFilter.searchWords,\n })\n const filtering =\n gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word) => word.trim())\n const filterOptions = useMemo(\n () => ({\n status: SEQUENCE_STATUS_OPTIONS,\n mailbox: props.mailboxes.map((mailbox) => ({\n value: mailbox.id,\n label: mailboxName(mailbox),\n })),\n }),\n [props.mailboxes],\n )\n const mailboxLabel = (sequence: OutreachSequence) => {\n const mailbox = props.mailboxes.find(\n (entry) => entry.id === sequence.mailboxId,\n )\n return mailbox\n ? mailboxName(mailbox)\n : sequence.mailboxId\n ? 'Mailbox removed'\n : 'No mailbox yet'\n }\n const pageRows = sequences.rows\n // Counted for the page on screen only: five aggregations a sequence.\n const counts = useOutreachSequenceCounts(\n orgId,\n pageRows.map((sequence) => sequence.id),\n )\n const footer = (\n <ListPagination\n page={sequences.page}\n pageSize={sequences.pageSize}\n rowCount={pageRows.length}\n hasMore={sequences.hasMore}\n onPageChange={sequences.setPage}\n onPageSizeChange={sequences.setPageSize}\n />\n )\n const newHref = `${sectionPath}/${OUTREACH_NEW_SEQUENCE_SEGMENT}`\n const newButton = (\n <Button\n variant=\"contained\"\n startIcon={<MdiIcon path={mdiPlus.path} fontSize=\"small\" />}\n href={newHref}\n onClick={(event) => {\n event.preventDefault()\n navigate(newHref)\n }}\n >\n New sequence\n </Button>\n )\n const count = (\n sequence: OutreachSequence,\n key: keyof OutreachSequenceCounts,\n ) => (counts[sequence.id] ? String(counts[sequence.id][key]) : '—')\n\n const chips = (\n <ListFilterChips\n fields={SEQUENCE_FILTER_FIELDS}\n headers={SEQUENCE_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={filterOptions}\n />\n )\n const notices = (\n <ListQueryNotices\n refused={listQueryRefusals(sequences.plan.refused, {\n fields: SEQUENCE_FILTER_FIELDS,\n headers: SEQUENCE_FILTER_HEADERS,\n options: filterOptions,\n })}\n notices={sequences.plan.notices}\n />\n )\n /** Nothing stored at all — not a filter that matched nothing. */\n const none =\n sequences.status === 'ready' &&\n !pageRows.length &&\n !filtering &&\n sequences.page === 0\n let body\n if (sequences.status === 'loading' && !filtering)\n body = <OutreachLoading label=\"Loading sequences…\" />\n else if (sequences.status === 'error' || sequences.status === 'refused') {\n body = (\n <Stack spacing={1}>\n {chips}\n <OutreachLoadProblem status={sequences.status} what=\"sequences\" />\n </Stack>\n )\n } else if (none) {\n body = (\n <Card variant=\"outlined\">\n <EmptyStateComponent\n label=\"No sequences yet\"\n description=\"A sequence is the emails, and the tasks between them, one person gets from one rep.\"\n action={newButton}\n />\n </Card>\n )\n } else if (narrow) {\n body = (\n <Stack spacing={1}>\n {chips}\n {notices}\n {pageRows.length ? (\n <Stack\n spacing={1}\n component=\"ul\"\n sx={{ listStyle: 'none', p: 0, m: 0 }}\n aria-label=\"Sequences\"\n >\n {pageRows.map((sequence) => (\n <Card key={sequence.id} variant=\"outlined\" component=\"li\">\n <CardContent>\n <Stack spacing={1}>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{\n alignItems: 'center',\n justifyContent: 'space-between',\n }}\n >\n <OutreachLink href={`${sectionPath}/${sequence.id}`}>\n {sequence.name || 'Untitled'}\n </OutreachLink>\n <OutreachSequenceStatusChip status={sequence.status} />\n </Stack>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {mailboxLabel(sequence)}\n </Typography>\n <Stack\n direction=\"row\"\n spacing={0.5}\n sx={{ flexWrap: 'wrap', rowGap: 0.5 }}\n >\n {COUNT_COLUMNS.map(([key, label]) => (\n <Chip\n key={key}\n size=\"small\"\n variant=\"outlined\"\n label={`${label} ${count(sequence, key)}`}\n />\n ))}\n </Stack>\n </Stack>\n </CardContent>\n </Card>\n ))}\n </Stack>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {sequences.status === 'loading'\n ? 'Loading sequences…'\n : 'No sequences match these filters'}\n </Typography>\n )}\n {footer}\n </Stack>\n )\n } else {\n const columns: NonNullable<ListTableProps['columns']> = listFilterGridColumns([\n { field: 'name', headerName: 'Name', flex: 1, minWidth: 180 },\n { field: 'mailbox', headerName: 'Mailbox', flex: 1, minWidth: 180 },\n {\n field: 'status',\n headerName: 'Status',\n width: 110,\n renderCell: ({ row }) => (\n <OutreachSequenceStatusChip status={row.status} />\n ),\n },\n ...COUNT_COLUMNS.map(([key, label]) => ({\n field: key,\n headerName: label,\n width: 96,\n align: 'right' as const,\n headerAlign: 'right' as const,\n })),\n ], SEQUENCE_FILTER_FIELDS, filterOptions, SEQUENCE_FILTER_HEADERS)\n body = (\n <Stack spacing={1}>\n {chips}\n {notices}\n <ListTable\n aria-label=\"Sequences\"\n columns={columns}\n initialState={{ columns: { columnVisibilityModel: SEQUENCE_HIDDEN_COLUMNS } }}\n rows={pageRows.map((sequence) => ({\n $id: sequence.id,\n name: sequence.name || 'Untitled',\n mailbox: mailboxLabel(sequence),\n status: sequence.status,\n ...Object.fromEntries(\n COUNT_COLUMNS.map(([key]) => [key, count(sequence, key)]),\n ),\n }))}\n onOpen={(id) => navigate(`${sectionPath}/${id}`)}\n loading={sequences.status === 'loading'}\n /*\n * The panel and the search are the grid's; every clause and the\n * search word are on the list's query (AGL-3321), and the grid\n * neither filters nor sorts the page it is handed: the query's\n * one order, newest first, is the list's.\n */\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n disableColumnSorting\n noRowsLabel=\"No sequences match these filters\"\n hideFooter\n />\n {footer}\n </Stack>\n )\n }\n\n return (\n <CardDisplay\n header=\"Sequences\"\n help={pluginDocsHelp('sequences', { anchor: '#sequences' })}\n HeaderProps={{\n action: none || sequences.status === 'loading' ? undefined : newButton,\n }}\n contentGutterX\n contentGutterY\n >\n {body}\n </CardDisplay>\n )\n}\nOutreachSequenceList.displayName = 'OutreachSequenceList'\n\nexport default OutreachSequencesSection\n"],"names":["pluginDocsHelp","mdiPlus","CardDisplay","MdiIcon","EmptyStateComponent","ListFilterChips","ListPagination","ListTable","ListQueryNotices","listQueryRefusals","hiddenFilterVisibility","listFilterGridColumns","useListGridFilter","Button","Card","CardContent","Chip","Stack","Typography","useMediaQuery","useTheme","useMemo","OUTREACH_SEQUENCE_STATUSES","OUTREACH_SEQUENCE_LIST_QUERY","OUTREACH_SEQUENCE_STATUS_LABELS","OutreachLink","OutreachLoading","OutreachLoadProblem","OutreachSequenceStatusChip","useOutreachNavigate","OutreachSequenceDetail","OutreachSequenceEditor","useOutreachApi","useOutreachSequenceCounts","useOutreachSequenceList","useOutreachMailboxes","useOutreachComplianceSettings","OUTREACH_NEW_SEQUENCE_SEGMENT","OutreachSequencesSection","props","orgId","sectionPath","subpath","api","settings","mailboxes","navigate","label","first","second","spacing","href","variant","component","orgMount","sequence","mailboxesPath","onSaved","id","onCancel","org","sequenceId","tab","compliancePath","OutreachSequenceList","displayName","COUNT_COLUMNS","SEQUENCE_FILTER_FIELDS","fields","SEQUENCE_FILTER_HEADERS","name","mailbox","status","created","SEQUENCE_HIDDEN_COLUMNS","SEQUENCE_STATUS_OPTIONS","map","value","mailboxName","sendAs","email","theme","narrow","breakpoints","down","gridFilter","selectFields","sequences","clauses","search","searchWords","filtering","length","some","word","trim","filterOptions","mailboxLabel","find","entry","mailboxId","pageRows","rows","counts","footer","page","pageSize","rowCount","hasMore","onPageChange","setPage","onPageSizeChange","setPageSize","newHref","newButton","startIcon","path","fontSize","onClick","event","preventDefault","count","key","String","chips","headers","onChange","setClauses","options","notices","refused","plan","none","body","what","description","action","sx","listStyle","p","m","aria-label","direction","alignItems","justifyContent","color","flexWrap","rowGap","size","columns","field","headerName","flex","minWidth","width","renderCell","row","align","headerAlign","initialState","columnVisibilityModel","$id","Object","fromEntries","onOpen","loading","filterMode","filterModel","onFilterModelChange","quickFilter","disableColumnSorting","noRowsLabel","hideFooter","header","help","anchor","HeaderProps","undefined","contentGutterX","contentGutterY"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/sequences-section.tsx"],"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'use client'\n\nimport { pluginDocsHelp, type ConsolePluginOrgMount } from '@aglyn/aglyn'\nimport { mdiPlus } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport EmptyStateComponent from '@aglyn/shared-ui-jsx/components/empty-state.component'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport {\n ListTable,\n type ListTableProps,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport ListQueryNotices, {\n listQueryRefusals,\n} from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport { hiddenFilterVisibility } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport { listFilterGridColumns } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport {\n Button,\n Card,\n CardContent,\n Chip,\n Stack,\n Typography,\n useMediaQuery,\n useTheme,\n} from '@mui/material'\nimport { useMemo } from 'react'\nimport {\n OUTREACH_SEQUENCE_STATUSES,\n type OutreachMailbox,\n type OutreachSequence,\n} from '../model/outreach.types'\nimport { OUTREACH_SEQUENCE_LIST_QUERY } from '../model/sequence-list-query'\nimport {\n OUTREACH_SEQUENCE_STATUS_LABELS,\n OutreachLink,\n OutreachLoading,\n OutreachLoadProblem,\n OutreachSequenceStatusChip,\n useOutreachNavigate,\n} from './outreach-ui'\nimport { OutreachEnrollmentDetail } from './enrollment-detail'\nimport {\n OutreachSequenceDetail,\n type OutreachSequenceTab,\n} from './sequence-detail'\nimport { OutreachSequenceEditor } from './sequence-editor'\nimport { useOutreachApi } from './use-outreach-api'\nimport {\n type OutreachSequenceCounts,\n useOutreachSequenceCounts,\n useOutreachSequenceList,\n} from './use-outreach-data'\nimport { useOutreachMailboxes } from './use-outreach-mailboxes'\nimport { useOutreachComplianceSettings } from './use-outreach-settings'\n\nexport interface OutreachSequencesSectionProps {\n /** The organization the hub is mounted under; `null` until the shell knows it. */\n orgId: string | null\n orgMount?: ConsolePluginOrgMount\n org?: Record<string, unknown> | null\n /** The section's own path: `/[orgSlug]/outreach/sequences`. */\n sectionPath: string\n /**\n * The path below the section: `[]`, `['new']`, `[id]`, `[id, 'enrollments']`\n * or `[id, 'enrollments', enrollmentId]` — one person's page (AGL-3332).\n */\n subpath: readonly string[]\n /** The Mailboxes section: `/[orgSlug]/outreach/mailboxes`. */\n mailboxesPath: string\n /** The Compliance section: `/[orgSlug]/outreach/compliance`. */\n compliancePath: string\n}\n\n/** The new-sequence page's segment below the section. */\nexport const OUTREACH_NEW_SEQUENCE_SEGMENT = 'new'\n\n/**\n * The Sequences section (AGL-2980), and the pages below it: the list, a new\n * sequence, and one sequence's steps and enrollments. Each is a real URL, so\n * a sequence is a link a rep can keep and the back button walks them.\n */\nexport function OutreachSequencesSection(props: OutreachSequencesSectionProps) {\n const { orgId, sectionPath, subpath } = props\n const api = useOutreachApi(orgId)\n const settings = useOutreachComplianceSettings(api, orgId)\n const mailboxes = useOutreachMailboxes(orgId)\n const navigate = useOutreachNavigate()\n\n if (!orgId) return <OutreachLoading label=\"Loading sequences…\" />\n const [first, second, third] = subpath\n if (first === OUTREACH_NEW_SEQUENCE_SEGMENT) {\n return (\n <Stack spacing={2}>\n <OutreachLink href={sectionPath}>Sequences</OutreachLink>\n <Typography variant=\"h5\" component=\"h2\">\n New sequence\n </Typography>\n <OutreachSequenceEditor\n orgId={orgId}\n orgMount={props.orgMount}\n sequence={null}\n settings={settings}\n mailboxes={mailboxes}\n mailboxesPath={props.mailboxesPath}\n onSaved={(sequence) => navigate(`${sectionPath}/${sequence.id}`)}\n onCancel={() => navigate(sectionPath)}\n />\n </Stack>\n )\n }\n if (first && second === 'enrollments' && third) {\n return (\n <OutreachEnrollmentDetail\n orgId={orgId}\n orgMount={props.orgMount}\n sectionPath={sectionPath}\n sequenceId={first}\n enrollmentId={third}\n mailboxes={mailboxes}\n />\n )\n }\n if (first) {\n return (\n <OutreachSequenceDetail\n orgId={orgId}\n orgMount={props.orgMount}\n org={props.org}\n sectionPath={sectionPath}\n sequenceId={first}\n tab={\n (second === 'enrollments'\n ? 'enrollments'\n : 'steps') as OutreachSequenceTab\n }\n settings={settings}\n mailboxes={mailboxes}\n mailboxesPath={props.mailboxesPath}\n compliancePath={props.compliancePath}\n />\n )\n }\n return (\n <OutreachSequenceList\n orgId={orgId}\n sectionPath={sectionPath}\n mailboxes={mailboxes.mailboxes}\n />\n )\n}\nOutreachSequencesSection.displayName = 'OutreachSequencesSection'\n\nconst COUNT_COLUMNS: ReadonlyArray<[keyof OutreachSequenceCounts, string]> = [\n ['enrolled', 'Enrolled'],\n ['active', 'Active'],\n ['replied', 'Replied'],\n ['bounced', 'Bounced'],\n ['optedOut', 'Opted out'],\n]\n\n/*\n * What the sequences grid's Filters panel offers (AGL-3317), each field on\n * the list's Firestore query (AGL-3321, `OUTREACH_SEQUENCE_LIST_QUERY`).\n */\nconst SEQUENCE_FILTER_FIELDS = OUTREACH_SEQUENCE_LIST_QUERY.fields\nconst SEQUENCE_FILTER_HEADERS: Readonly<Record<string, string>> = {\n name: 'Name',\n mailbox: 'Mailbox',\n status: 'Status',\n created: 'Created',\n}\n/** `created` is no column of its own: a filter-only column, never shown. */\nconst SEQUENCE_HIDDEN_COLUMNS = hiddenFilterVisibility(SEQUENCE_FILTER_FIELDS, [\n 'name',\n 'mailbox',\n 'status',\n])\nconst SEQUENCE_STATUS_OPTIONS = OUTREACH_SEQUENCE_STATUSES.map((status) => ({\n value: status,\n label: OUTREACH_SEQUENCE_STATUS_LABELS[status],\n}))\n\n/** How a mailbox reads in the list: the address it sends as. */\nconst mailboxName = (mailbox: OutreachMailbox) => mailbox.sendAs || mailbox.email\n\n/**\n * Every sequence: its mailbox, its status, and how many people it enrolled\n * and where they stand — a table on wider screens, cards on a phone.\n *\n * ## Asked of Firestore, a page at a time\n *\n * The Filters panel and the search box are the grid's; the clauses and the\n * words go onto ONE query over the organization's sequences, newest first,\n * and each page is a page of that query's matches (AGL-3321). A clause the\n * query cannot hold beside the others is not applied, and the notice above\n * the list says which and why — never answered over the sequences that\n * happen to be loaded.\n */\nexport function OutreachSequenceList(props: {\n orgId: string\n sectionPath: string\n mailboxes: readonly OutreachMailbox[]\n}) {\n const { orgId, sectionPath } = props\n const theme = useTheme()\n const narrow = useMediaQuery(theme.breakpoints.down('md'))\n const navigate = useOutreachNavigate()\n const gridFilter = useListGridFilter({ selectFields: ['status', 'mailbox'] })\n const sequences = useOutreachSequenceList(orgId, {\n clauses: gridFilter.clauses,\n search: gridFilter.searchWords,\n })\n const filtering =\n gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word) => word.trim())\n const filterOptions = useMemo(\n () => ({\n status: SEQUENCE_STATUS_OPTIONS,\n mailbox: props.mailboxes.map((mailbox) => ({\n value: mailbox.id,\n label: mailboxName(mailbox),\n })),\n }),\n [props.mailboxes],\n )\n const mailboxLabel = (sequence: OutreachSequence) => {\n const mailbox = props.mailboxes.find(\n (entry) => entry.id === sequence.mailboxId,\n )\n return mailbox\n ? mailboxName(mailbox)\n : sequence.mailboxId\n ? 'Mailbox removed'\n : 'No mailbox yet'\n }\n const pageRows = sequences.rows\n // Counted for the page on screen only: five aggregations a sequence.\n const counts = useOutreachSequenceCounts(\n orgId,\n pageRows.map((sequence) => sequence.id),\n )\n const footer = (\n <ListPagination\n page={sequences.page}\n pageSize={sequences.pageSize}\n rowCount={pageRows.length}\n hasMore={sequences.hasMore}\n onPageChange={sequences.setPage}\n onPageSizeChange={sequences.setPageSize}\n />\n )\n const newHref = `${sectionPath}/${OUTREACH_NEW_SEQUENCE_SEGMENT}`\n const newButton = (\n <Button\n variant=\"contained\"\n startIcon={<MdiIcon path={mdiPlus.path} fontSize=\"small\" />}\n href={newHref}\n onClick={(event) => {\n event.preventDefault()\n navigate(newHref)\n }}\n >\n New sequence\n </Button>\n )\n const count = (\n sequence: OutreachSequence,\n key: keyof OutreachSequenceCounts,\n ) => (counts[sequence.id] ? String(counts[sequence.id][key]) : '—')\n\n const chips = (\n <ListFilterChips\n fields={SEQUENCE_FILTER_FIELDS}\n headers={SEQUENCE_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={filterOptions}\n />\n )\n const notices = (\n <ListQueryNotices\n refused={listQueryRefusals(sequences.plan.refused, {\n fields: SEQUENCE_FILTER_FIELDS,\n headers: SEQUENCE_FILTER_HEADERS,\n options: filterOptions,\n })}\n notices={sequences.plan.notices}\n />\n )\n /** Nothing stored at all — not a filter that matched nothing. */\n const none =\n sequences.status === 'ready' &&\n !pageRows.length &&\n !filtering &&\n sequences.page === 0\n let body\n if (sequences.status === 'loading' && !filtering)\n body = <OutreachLoading label=\"Loading sequences…\" />\n else if (sequences.status === 'error' || sequences.status === 'refused') {\n body = (\n <Stack spacing={1}>\n {chips}\n <OutreachLoadProblem status={sequences.status} what=\"sequences\" />\n </Stack>\n )\n } else if (none) {\n body = (\n <Card variant=\"outlined\">\n <EmptyStateComponent\n label=\"No sequences yet\"\n description=\"A sequence is the emails, and the tasks between them, one person gets from one rep.\"\n action={newButton}\n />\n </Card>\n )\n } else if (narrow) {\n body = (\n <Stack spacing={1}>\n {chips}\n {notices}\n {pageRows.length ? (\n <Stack\n spacing={1}\n component=\"ul\"\n sx={{ listStyle: 'none', p: 0, m: 0 }}\n aria-label=\"Sequences\"\n >\n {pageRows.map((sequence) => (\n <Card key={sequence.id} variant=\"outlined\" component=\"li\">\n <CardContent>\n <Stack spacing={1}>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{\n alignItems: 'center',\n justifyContent: 'space-between',\n }}\n >\n <OutreachLink href={`${sectionPath}/${sequence.id}`}>\n {sequence.name || 'Untitled'}\n </OutreachLink>\n <OutreachSequenceStatusChip status={sequence.status} />\n </Stack>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {mailboxLabel(sequence)}\n </Typography>\n <Stack\n direction=\"row\"\n spacing={0.5}\n sx={{ flexWrap: 'wrap', rowGap: 0.5 }}\n >\n {COUNT_COLUMNS.map(([key, label]) => (\n <Chip\n key={key}\n size=\"small\"\n variant=\"outlined\"\n label={`${label} ${count(sequence, key)}`}\n />\n ))}\n </Stack>\n </Stack>\n </CardContent>\n </Card>\n ))}\n </Stack>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {sequences.status === 'loading'\n ? 'Loading sequences…'\n : 'No sequences match these filters'}\n </Typography>\n )}\n {footer}\n </Stack>\n )\n } else {\n const columns: NonNullable<ListTableProps['columns']> = listFilterGridColumns([\n { field: 'name', headerName: 'Name', flex: 1, minWidth: 180 },\n { field: 'mailbox', headerName: 'Mailbox', flex: 1, minWidth: 180 },\n {\n field: 'status',\n headerName: 'Status',\n width: 110,\n renderCell: ({ row }) => (\n <OutreachSequenceStatusChip status={row.status} />\n ),\n },\n ...COUNT_COLUMNS.map(([key, label]) => ({\n field: key,\n headerName: label,\n width: 96,\n align: 'right' as const,\n headerAlign: 'right' as const,\n })),\n ], SEQUENCE_FILTER_FIELDS, filterOptions, SEQUENCE_FILTER_HEADERS)\n body = (\n <Stack spacing={1}>\n {chips}\n {notices}\n <ListTable\n aria-label=\"Sequences\"\n columns={columns}\n initialState={{ columns: { columnVisibilityModel: SEQUENCE_HIDDEN_COLUMNS } }}\n rows={pageRows.map((sequence) => ({\n $id: sequence.id,\n name: sequence.name || 'Untitled',\n mailbox: mailboxLabel(sequence),\n status: sequence.status,\n ...Object.fromEntries(\n COUNT_COLUMNS.map(([key]) => [key, count(sequence, key)]),\n ),\n }))}\n onOpen={(id) => navigate(`${sectionPath}/${id}`)}\n loading={sequences.status === 'loading'}\n /*\n * The panel and the search are the grid's; every clause and the\n * search word are on the list's query (AGL-3321), and the grid\n * neither filters nor sorts the page it is handed: the query's\n * one order, newest first, is the list's.\n */\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n disableColumnSorting\n noRowsLabel=\"No sequences match these filters\"\n hideFooter\n />\n {footer}\n </Stack>\n )\n }\n\n return (\n <CardDisplay\n header=\"Sequences\"\n help={pluginDocsHelp('sequences', { anchor: '#sequences' })}\n HeaderProps={{\n action: none || sequences.status === 'loading' ? undefined : newButton,\n }}\n contentGutterX\n contentGutterY\n >\n {body}\n </CardDisplay>\n )\n}\nOutreachSequenceList.displayName = 'OutreachSequenceList'\n\nexport default OutreachSequencesSection\n"],"names":["pluginDocsHelp","mdiPlus","CardDisplay","MdiIcon","EmptyStateComponent","ListFilterChips","ListPagination","ListTable","ListQueryNotices","listQueryRefusals","hiddenFilterVisibility","listFilterGridColumns","useListGridFilter","Button","Card","CardContent","Chip","Stack","Typography","useMediaQuery","useTheme","useMemo","OUTREACH_SEQUENCE_STATUSES","OUTREACH_SEQUENCE_LIST_QUERY","OUTREACH_SEQUENCE_STATUS_LABELS","OutreachLink","OutreachLoading","OutreachLoadProblem","OutreachSequenceStatusChip","useOutreachNavigate","OutreachEnrollmentDetail","OutreachSequenceDetail","OutreachSequenceEditor","useOutreachApi","useOutreachSequenceCounts","useOutreachSequenceList","useOutreachMailboxes","useOutreachComplianceSettings","OUTREACH_NEW_SEQUENCE_SEGMENT","OutreachSequencesSection","props","orgId","sectionPath","subpath","api","settings","mailboxes","navigate","label","first","second","third","spacing","href","variant","component","orgMount","sequence","mailboxesPath","onSaved","id","onCancel","sequenceId","enrollmentId","org","tab","compliancePath","OutreachSequenceList","displayName","COUNT_COLUMNS","SEQUENCE_FILTER_FIELDS","fields","SEQUENCE_FILTER_HEADERS","name","mailbox","status","created","SEQUENCE_HIDDEN_COLUMNS","SEQUENCE_STATUS_OPTIONS","map","value","mailboxName","sendAs","email","theme","narrow","breakpoints","down","gridFilter","selectFields","sequences","clauses","search","searchWords","filtering","length","some","word","trim","filterOptions","mailboxLabel","find","entry","mailboxId","pageRows","rows","counts","footer","page","pageSize","rowCount","hasMore","onPageChange","setPage","onPageSizeChange","setPageSize","newHref","newButton","startIcon","path","fontSize","onClick","event","preventDefault","count","key","String","chips","headers","onChange","setClauses","options","notices","refused","plan","none","body","what","description","action","sx","listStyle","p","m","aria-label","direction","alignItems","justifyContent","color","flexWrap","rowGap","size","columns","field","headerName","flex","minWidth","width","renderCell","row","align","headerAlign","initialState","columnVisibilityModel","$id","Object","fromEntries","onOpen","loading","filterMode","filterModel","onFilterModelChange","quickFilter","disableColumnSorting","noRowsLabel","hideFooter","header","help","anchor","HeaderProps","undefined","contentGutterX","contentGutterY"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import type { OutreachComplianceIssue } from '../model/compliance-settings';
2
- import type { OutreachCurateDraftRequest, OutreachCurateDraftResponse, OutreachCurateSaveRequest, OutreachCurateSaveResponse, OutreachDoNotContactDomainRequest, OutreachDoNotContactDomainResponse, OutreachDoNotContactDomainsResponse, OutreachEnrollmentAction, OutreachEnrollmentActionResponse, OutreachEnrollPersonRequest, OutreachEnrollPreviewResponse, OutreachEnrollResponse, OutreachEnrollSource, OutreachLinkDomainRequest, OutreachLinkDomainResponse, OutreachLinkDomainsResponse, OutreachPreviewRequest, OutreachPreviewResponse, OutreachRouteRefusalReason, OutreachSequenceAction, OutreachSequenceSaveResponse, OutreachSequenceStatusResponse, OutreachSettingsResponse, OutreachSettingsSaveRequest, OutreachSettingsSaveResponse, OutreachStepTestRequest, OutreachStepTestResponse } from '../model/outreach-api';
2
+ import type { OutreachCurateDraftRequest, OutreachCurateDraftResponse, OutreachCurateSaveRequest, OutreachCurateSaveResponse, OutreachDoNotContactDomainRequest, OutreachDoNotContactDomainResponse, OutreachDoNotContactDomainsResponse, OutreachEnrollmentAction, OutreachEnrollmentActionResponse, OutreachEnrollmentGatewayResponse, OutreachEnrollPersonRequest, OutreachEnrollPreviewResponse, OutreachEnrollResponse, OutreachEnrollSource, OutreachLinkDomainRequest, OutreachLinkDomainResponse, OutreachLinkDomainsResponse, OutreachPreviewRequest, OutreachPreviewResponse, OutreachRouteRefusalReason, OutreachSequenceAction, OutreachSequenceSaveResponse, OutreachSequenceStatusResponse, OutreachSettingsResponse, OutreachSettingsSaveRequest, OutreachSettingsSaveResponse, OutreachStepTestRequest, OutreachStepTestResponse } from '../model/outreach-api';
3
3
  import type { OutreachSequenceDraft, OutreachSequenceIssue } from '../model/sequence-draft';
4
4
  /**
5
5
  * A route's refusal, carrying its sentence, its stable reason and any
@@ -26,6 +26,8 @@ export interface OutreachApi {
26
26
  previewEnrollment(sequenceId: string, source: OutreachEnrollSource): Promise<OutreachEnrollPreviewResponse>;
27
27
  enroll(sequenceId: string, people: OutreachEnrollPersonRequest[]): Promise<OutreachEnrollResponse>;
28
28
  actOnEnrollment(enrollmentId: string, action: OutreachEnrollmentAction, detail?: string): Promise<OutreachEnrollmentActionResponse>;
29
+ /** The mail gateway in front of one enrolled person's domain (AGL-3332). */
30
+ readEnrollmentGateway(enrollmentId: string): Promise<OutreachEnrollmentGatewayResponse>;
29
31
  previewEmail(input: Omit<OutreachPreviewRequest, 'orgId'>): Promise<OutreachPreviewResponse>;
30
32
  /**
31
33
  * Sends one step of a saved sequence to the member as a test (AGL-3325):
@@ -127,6 +127,12 @@ import { OUTREACH_API_ROUTES } from "../constants/api-routes.js";
127
127
  detail
128
128
  } : {})
129
129
  }),
130
+ readEnrollmentGateway: (enrollmentId)=>call(OUTREACH_API_ROUTES.enrollmentsGateway, {
131
+ method: 'POST',
132
+ body: {
133
+ enrollmentId
134
+ }
135
+ }),
130
136
  previewEmail: (input)=>call(OUTREACH_API_ROUTES.preview, {
131
137
  method: 'POST',
132
138
  body: _extends({}, input)
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/use-outreach-api.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'use client'\n\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport { useCallback, useMemo } from 'react'\nimport { OUTREACH_API_ROUTES } from '../constants/api-routes'\nimport type { OutreachComplianceIssue } from '../model/compliance-settings'\nimport type {\n OutreachCurateDraftRequest,\n OutreachCurateDraftResponse,\n OutreachCurateSaveRequest,\n OutreachCurateSaveResponse,\n OutreachDoNotContactDomainRequest,\n OutreachDoNotContactDomainResponse,\n OutreachDoNotContactDomainsResponse,\n OutreachEnrollmentAction,\n OutreachEnrollmentActionResponse,\n OutreachEnrollPersonRequest,\n OutreachEnrollPreviewResponse,\n OutreachEnrollResponse,\n OutreachEnrollSource,\n OutreachLinkDomainRequest,\n OutreachLinkDomainResponse,\n OutreachLinkDomainsResponse,\n OutreachPreviewRequest,\n OutreachPreviewResponse,\n OutreachRouteRefusal,\n OutreachRouteRefusalReason,\n OutreachSequenceAction,\n OutreachSequenceSaveResponse,\n OutreachSequenceStatusResponse,\n OutreachSettingsResponse,\n OutreachSettingsSaveRequest,\n OutreachSettingsSaveResponse,\n OutreachStepTestRequest,\n OutreachStepTestResponse,\n} from '../model/outreach-api'\nimport type {\n OutreachSequenceDraft,\n OutreachSequenceIssue,\n} from '../model/sequence-draft'\n\n/**\n * A route's refusal, carrying its sentence, its stable reason and any\n * field-level issues. `refused` is the one a page shows as \"you can't use\n * this here\" rather than as something that went wrong: no permission, no\n * entitlement, or the release gate answering as if the route did not exist.\n */\nexport class OutreachRouteError extends Error {\n readonly reason: OutreachRouteRefusalReason | 'unreachable' | 'not-found'\n readonly status: number\n readonly issues: ReadonlyArray<\n OutreachComplianceIssue | OutreachSequenceIssue\n >\n\n constructor(\n message: string,\n reason: OutreachRouteError['reason'],\n status: number,\n issues: OutreachRouteError['issues'] = [],\n ) {\n super(message)\n this.name = 'OutreachRouteError'\n this.reason = reason\n this.status = status\n this.issues = issues\n }\n\n get refused(): boolean {\n return (\n this.reason === 'permission' ||\n this.reason === 'entitlement' ||\n this.reason === 'not-org-wide' ||\n this.reason === 'not-a-member' ||\n // The dispatcher answers a route whose plugin is not released to this\n // workspace as though it did not exist.\n this.reason === 'not-found'\n )\n }\n}\n\n/** The routes, as the Outreach pages call them. */\nexport interface OutreachApi {\n readSettings(): Promise<OutreachSettingsResponse>\n saveSettings(\n input: Omit<OutreachSettingsSaveRequest, 'orgId'>,\n ): Promise<OutreachSettingsSaveResponse>\n saveSequence(\n sequenceId: string | null,\n sequence: OutreachSequenceDraft,\n ): Promise<OutreachSequenceSaveResponse>\n setSequenceStatus(\n sequenceId: string,\n action: OutreachSequenceAction,\n ): Promise<OutreachSequenceStatusResponse>\n deleteSequence(sequenceId: string): Promise<{ ok: true }>\n previewEnrollment(\n sequenceId: string,\n source: OutreachEnrollSource,\n ): Promise<OutreachEnrollPreviewResponse>\n enroll(\n sequenceId: string,\n people: OutreachEnrollPersonRequest[],\n ): Promise<OutreachEnrollResponse>\n actOnEnrollment(\n enrollmentId: string,\n action: OutreachEnrollmentAction,\n detail?: string,\n ): Promise<OutreachEnrollmentActionResponse>\n previewEmail(\n input: Omit<OutreachPreviewRequest, 'orgId'>,\n ): Promise<OutreachPreviewResponse>\n /**\n * Sends one step of a saved sequence to the member as a test (AGL-3325):\n * the preview's email, through the sequence's mailbox, `[Test]` in front.\n */\n sendStepTest(\n input: Omit<OutreachStepTestRequest, 'orgId'>,\n ): Promise<OutreachStepTestResponse & { unresolvedFields?: string[] }>\n /**\n * One person's own copies of the sequence's email steps, drafted by the\n * workspace's AI (AGL-3324); nothing is stored until the member confirms.\n */\n curateDrafts(\n input: Omit<OutreachCurateDraftRequest, 'orgId'>,\n ): Promise<OutreachCurateDraftResponse>\n /** Stores a confirmed copy of one step on an enrollment, or clears it with `null`. */\n saveCuratedStep(\n enrollmentId: string,\n stepIndex: number,\n override: OutreachCurateSaveRequest['override'],\n ): Promise<OutreachCurateSaveResponse>\n /** The domains on the do-not-contact list (AGL-3244). */\n readDoNotContactDomains(): Promise<OutreachDoNotContactDomainsResponse>\n /** Puts a domain on the list, or takes one off. */\n changeDoNotContactDomain(\n action: OutreachDoNotContactDomainRequest['action'],\n domain: string,\n detail?: string,\n ): Promise<OutreachDoNotContactDomainResponse>\n /** Each mailbox domain's click-tracking host (AGL-3306). */\n readLinkDomains(): Promise<OutreachLinkDomainsResponse>\n /** Sets a link domain up, checks it, or removes it. */\n changeLinkDomain(\n action: OutreachLinkDomainRequest['action'],\n domain: string,\n ): Promise<OutreachLinkDomainResponse>\n}\n\n/**\n * The Outreach pages' one door to the settings, sequence and enrollment\n * routes (AGL-2980), with the member's session attached and the\n * organization named on every call. A refusal is thrown as an\n * {@link OutreachRouteError} carrying the route's own sentence, which is\n * written for the person reading it.\n */\nexport function useOutreachApi(orgId: string | null): OutreachApi {\n const { data: user } = useUser()\n\n const call = useCallback(\n async <T>(\n route: string,\n init: { method: 'GET' | 'POST'; body?: Record<string, unknown> },\n ): Promise<T> => {\n if (!orgId)\n throw new OutreachRouteError(\n 'Open an organization first.',\n 'org-required',\n 400,\n )\n const path =\n init.method === 'GET'\n ? `/api/${route}?orgId=${encodeURIComponent(orgId)}`\n : `/api/${route}`\n let response: Response\n try {\n response = await authorizedFetch(user, path, {\n method: init.method,\n ...(init.method === 'POST'\n ? {\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ orgId, ...init.body }),\n }\n : {}),\n })\n } catch {\n throw new OutreachRouteError(\n 'Sequences could not be reached. Try again.',\n 'unreachable',\n 0,\n )\n }\n const payload = (await response.json().catch(() => null)) as\n (T & { error?: undefined }) | Partial<OutreachRouteRefusal> | null\n if (!response.ok || !payload) {\n const refusal = (payload ?? {}) as Partial<OutreachRouteRefusal>\n throw new OutreachRouteError(\n refusal.error && refusal.reason\n ? refusal.error\n : response.status === 404\n ? \"Sequences isn't available to this workspace yet.\"\n : 'Sequences could not be reached. Try again.',\n refusal.reason ??\n (response.status === 404 ? 'not-found' : 'unreachable'),\n response.status,\n refusal.issues ?? [],\n )\n }\n return payload as T\n },\n [user, orgId],\n )\n\n return useMemo<OutreachApi>(\n () => ({\n readSettings: () => call(OUTREACH_API_ROUTES.settings, { method: 'GET' }),\n saveSettings: (input) =>\n call(OUTREACH_API_ROUTES.settings, {\n method: 'POST',\n body: { ...input },\n }),\n saveSequence: (sequenceId, sequence) =>\n call(OUTREACH_API_ROUTES.sequencesSave, {\n method: 'POST',\n body: { ...(sequenceId ? { sequenceId } : {}), sequence },\n }),\n setSequenceStatus: (sequenceId, action) =>\n call(OUTREACH_API_ROUTES.sequencesStatus, {\n method: 'POST',\n body: { sequenceId, action },\n }),\n deleteSequence: (sequenceId) =>\n call(OUTREACH_API_ROUTES.sequencesDelete, {\n method: 'POST',\n body: { sequenceId },\n }),\n previewEnrollment: (sequenceId, source) =>\n call(OUTREACH_API_ROUTES.enrollPreview, {\n method: 'POST',\n body: { sequenceId, source },\n }),\n enroll: (sequenceId, people) =>\n call(OUTREACH_API_ROUTES.enroll, {\n method: 'POST',\n body: { sequenceId, people },\n }),\n actOnEnrollment: (enrollmentId, action, detail) =>\n call(OUTREACH_API_ROUTES.enrollmentsAction, {\n method: 'POST',\n body: { enrollmentId, action, ...(detail ? { detail } : {}) },\n }),\n previewEmail: (input) =>\n call(OUTREACH_API_ROUTES.preview, {\n method: 'POST',\n body: { ...input },\n }),\n sendStepTest: (input) =>\n call(OUTREACH_API_ROUTES.stepTest, {\n method: 'POST',\n body: { ...input },\n }),\n curateDrafts: (input) =>\n call(OUTREACH_API_ROUTES.curateDraft, {\n method: 'POST',\n body: { ...input },\n }),\n saveCuratedStep: (enrollmentId, stepIndex, override) =>\n call(OUTREACH_API_ROUTES.curateSave, {\n method: 'POST',\n body: { enrollmentId, stepIndex, override },\n }),\n readDoNotContactDomains: () =>\n call(OUTREACH_API_ROUTES.doNotContactDomains, { method: 'GET' }),\n changeDoNotContactDomain: (action, domain, detail) =>\n call(OUTREACH_API_ROUTES.doNotContactDomains, {\n method: 'POST',\n body: { action, domain, ...(detail ? { detail } : {}) },\n }),\n readLinkDomains: () => call(OUTREACH_API_ROUTES.linkDomains, { method: 'GET' }),\n changeLinkDomain: (action, domain) =>\n call(OUTREACH_API_ROUTES.linkDomains, {\n method: 'POST',\n body: { action, domain },\n }),\n }),\n [call],\n )\n}\n"],"names":["authorizedFetch","useUser","useCallback","useMemo","OUTREACH_API_ROUTES","OutreachRouteError","Error","refused","reason","message","status","issues","name","useOutreachApi","orgId","data","user","call","route","init","path","method","encodeURIComponent","response","headers","body","JSON","stringify","payload","json","catch","ok","refusal","error","readSettings","settings","saveSettings","input","saveSequence","sequenceId","sequence","sequencesSave","setSequenceStatus","action","sequencesStatus","deleteSequence","sequencesDelete","previewEnrollment","source","enrollPreview","enroll","people","actOnEnrollment","enrollmentId","detail","enrollmentsAction","previewEmail","preview","sendStepTest","stepTest","curateDrafts","curateDraft","saveCuratedStep","stepIndex","override","curateSave","readDoNotContactDomains","doNotContactDomains","changeDoNotContactDomain","domain","readLinkDomains","linkDomains","changeLinkDomain"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAASA,eAAe,QAAQ,2CAA0C;AAC1E,SAASC,OAAO,QAAQ,iCAAgC;AACxD,SAASC,WAAW,EAAEC,OAAO,QAAQ,QAAO;AAC5C,SAASC,mBAAmB,QAAQ,6BAAyB;AAqC7D;;;;;CAKC,GACD,OAAO,MAAMC,2BAA2BC;IAoBtC,IAAIC,UAAmB;QACrB,OACE,IAAI,CAACC,MAAM,KAAK,gBAChB,IAAI,CAACA,MAAM,KAAK,iBAChB,IAAI,CAACA,MAAM,KAAK,kBAChB,IAAI,CAACA,MAAM,KAAK,kBAChB,sEAAsE;QACtE,wCAAwC;QACxC,IAAI,CAACA,MAAM,KAAK;IAEpB;IAvBA,YACEC,OAAe,EACfD,MAAoC,EACpCE,MAAc,EACdC,SAAuC,EAAE,CACzC;QACA,KAAK,CAACF;QACN,IAAI,CAACG,IAAI,GAAG;QACZ,IAAI,CAACJ,MAAM,GAAGA;QACd,IAAI,CAACE,MAAM,GAAGA;QACd,IAAI,CAACC,MAAM,GAAGA;IAChB;AAaF;AAsEA;;;;;;CAMC,GACD,OAAO,SAASE,eAAeC,KAAoB;IACjD,MAAM,EAAEC,MAAMC,IAAI,EAAE,GAAGf;IAEvB,MAAMgB,OAAOf,YACX,OACEgB,OACAC;QAEA,IAAI,CAACL,OACH,MAAM,IAAIT,mBACR,+BACA,gBACA;QAEJ,MAAMe,OACJD,KAAKE,MAAM,KAAK,QACZ,CAAC,KAAK,EAAEH,MAAM,OAAO,EAAEI,mBAAmBR,QAAQ,GAClD,CAAC,KAAK,EAAEI,OAAO;QACrB,IAAIK;QACJ,IAAI;YACFA,WAAW,MAAMvB,gBAAgBgB,MAAMI,MAAM;gBAC3CC,QAAQF,KAAKE,MAAM;eACfF,KAAKE,MAAM,KAAK,SAChB;gBACEG,SAAS;oBAAE,gBAAgB;gBAAmB;gBAC9CC,MAAMC,KAAKC,SAAS,CAAC;oBAAEb;mBAAUK,KAAKM,IAAI;YAC5C,IACA,CAAC;QAET,EAAE,eAAM;YACN,MAAM,IAAIpB,mBACR,8CACA,eACA;QAEJ;QACA,MAAMuB,UAAW,MAAML,SAASM,IAAI,GAAGC,KAAK,CAAC,IAAM;QAEnD,IAAI,CAACP,SAASQ,EAAE,IAAI,CAACH,SAAS;gBAQ1BI,iBAGAA;YAVF,MAAMA,UAAWJ,kBAAAA,UAAW,CAAC;YAC7B,MAAM,IAAIvB,mBACR2B,QAAQC,KAAK,IAAID,QAAQxB,MAAM,GAC3BwB,QAAQC,KAAK,GACbV,SAASb,MAAM,KAAK,MAClB,qDACA,+CACNsB,kBAAAA,QAAQxB,MAAM,YAAdwB,kBACGT,SAASb,MAAM,KAAK,MAAM,cAAc,eAC3Ca,SAASb,MAAM,GACfsB,kBAAAA,QAAQrB,MAAM,YAAdqB,kBAAkB,EAAE;QAExB;QACA,OAAOJ;IACT,GACA;QAACZ;QAAMF;KAAM;IAGf,OAAOX,QACL,IAAO,CAAA;YACL+B,cAAc,IAAMjB,KAAKb,oBAAoB+B,QAAQ,EAAE;oBAAEd,QAAQ;gBAAM;YACvEe,cAAc,CAACC,QACbpB,KAAKb,oBAAoB+B,QAAQ,EAAE;oBACjCd,QAAQ;oBACRI,MAAM,aAAKY;gBACb;YACFC,cAAc,CAACC,YAAYC,WACzBvB,KAAKb,oBAAoBqC,aAAa,EAAE;oBACtCpB,QAAQ;oBACRI,MAAM,aAAMc,aAAa;wBAAEA;oBAAW,IAAI,CAAC;wBAAIC;;gBACjD;YACFE,mBAAmB,CAACH,YAAYI,SAC9B1B,KAAKb,oBAAoBwC,eAAe,EAAE;oBACxCvB,QAAQ;oBACRI,MAAM;wBAAEc;wBAAYI;oBAAO;gBAC7B;YACFE,gBAAgB,CAACN,aACftB,KAAKb,oBAAoB0C,eAAe,EAAE;oBACxCzB,QAAQ;oBACRI,MAAM;wBAAEc;oBAAW;gBACrB;YACFQ,mBAAmB,CAACR,YAAYS,SAC9B/B,KAAKb,oBAAoB6C,aAAa,EAAE;oBACtC5B,QAAQ;oBACRI,MAAM;wBAAEc;wBAAYS;oBAAO;gBAC7B;YACFE,QAAQ,CAACX,YAAYY,SACnBlC,KAAKb,oBAAoB8C,MAAM,EAAE;oBAC/B7B,QAAQ;oBACRI,MAAM;wBAAEc;wBAAYY;oBAAO;gBAC7B;YACFC,iBAAiB,CAACC,cAAcV,QAAQW,SACtCrC,KAAKb,oBAAoBmD,iBAAiB,EAAE;oBAC1ClC,QAAQ;oBACRI,MAAM;wBAAE4B;wBAAcV;uBAAYW,SAAS;wBAAEA;oBAAO,IAAI,CAAC;gBAC3D;YACFE,cAAc,CAACnB,QACbpB,KAAKb,oBAAoBqD,OAAO,EAAE;oBAChCpC,QAAQ;oBACRI,MAAM,aAAKY;gBACb;YACFqB,cAAc,CAACrB,QACbpB,KAAKb,oBAAoBuD,QAAQ,EAAE;oBACjCtC,QAAQ;oBACRI,MAAM,aAAKY;gBACb;YACFuB,cAAc,CAACvB,QACbpB,KAAKb,oBAAoByD,WAAW,EAAE;oBACpCxC,QAAQ;oBACRI,MAAM,aAAKY;gBACb;YACFyB,iBAAiB,CAACT,cAAcU,WAAWC,WACzC/C,KAAKb,oBAAoB6D,UAAU,EAAE;oBACnC5C,QAAQ;oBACRI,MAAM;wBAAE4B;wBAAcU;wBAAWC;oBAAS;gBAC5C;YACFE,yBAAyB,IACvBjD,KAAKb,oBAAoB+D,mBAAmB,EAAE;oBAAE9C,QAAQ;gBAAM;YAChE+C,0BAA0B,CAACzB,QAAQ0B,QAAQf,SACzCrC,KAAKb,oBAAoB+D,mBAAmB,EAAE;oBAC5C9C,QAAQ;oBACRI,MAAM;wBAAEkB;wBAAQ0B;uBAAYf,SAAS;wBAAEA;oBAAO,IAAI,CAAC;gBACrD;YACFgB,iBAAiB,IAAMrD,KAAKb,oBAAoBmE,WAAW,EAAE;oBAAElD,QAAQ;gBAAM;YAC7EmD,kBAAkB,CAAC7B,QAAQ0B,SACzBpD,KAAKb,oBAAoBmE,WAAW,EAAE;oBACpClD,QAAQ;oBACRI,MAAM;wBAAEkB;wBAAQ0B;oBAAO;gBACzB;QACJ,CAAA,GACA;QAACpD;KAAK;AAEV"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/use-outreach-api.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'use client'\n\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport { useCallback, useMemo } from 'react'\nimport { OUTREACH_API_ROUTES } from '../constants/api-routes'\nimport type { OutreachComplianceIssue } from '../model/compliance-settings'\nimport type {\n OutreachCurateDraftRequest,\n OutreachCurateDraftResponse,\n OutreachCurateSaveRequest,\n OutreachCurateSaveResponse,\n OutreachDoNotContactDomainRequest,\n OutreachDoNotContactDomainResponse,\n OutreachDoNotContactDomainsResponse,\n OutreachEnrollmentAction,\n OutreachEnrollmentActionResponse,\n OutreachEnrollmentGatewayResponse,\n OutreachEnrollPersonRequest,\n OutreachEnrollPreviewResponse,\n OutreachEnrollResponse,\n OutreachEnrollSource,\n OutreachLinkDomainRequest,\n OutreachLinkDomainResponse,\n OutreachLinkDomainsResponse,\n OutreachPreviewRequest,\n OutreachPreviewResponse,\n OutreachRouteRefusal,\n OutreachRouteRefusalReason,\n OutreachSequenceAction,\n OutreachSequenceSaveResponse,\n OutreachSequenceStatusResponse,\n OutreachSettingsResponse,\n OutreachSettingsSaveRequest,\n OutreachSettingsSaveResponse,\n OutreachStepTestRequest,\n OutreachStepTestResponse,\n} from '../model/outreach-api'\nimport type {\n OutreachSequenceDraft,\n OutreachSequenceIssue,\n} from '../model/sequence-draft'\n\n/**\n * A route's refusal, carrying its sentence, its stable reason and any\n * field-level issues. `refused` is the one a page shows as \"you can't use\n * this here\" rather than as something that went wrong: no permission, no\n * entitlement, or the release gate answering as if the route did not exist.\n */\nexport class OutreachRouteError extends Error {\n readonly reason: OutreachRouteRefusalReason | 'unreachable' | 'not-found'\n readonly status: number\n readonly issues: ReadonlyArray<\n OutreachComplianceIssue | OutreachSequenceIssue\n >\n\n constructor(\n message: string,\n reason: OutreachRouteError['reason'],\n status: number,\n issues: OutreachRouteError['issues'] = [],\n ) {\n super(message)\n this.name = 'OutreachRouteError'\n this.reason = reason\n this.status = status\n this.issues = issues\n }\n\n get refused(): boolean {\n return (\n this.reason === 'permission' ||\n this.reason === 'entitlement' ||\n this.reason === 'not-org-wide' ||\n this.reason === 'not-a-member' ||\n // The dispatcher answers a route whose plugin is not released to this\n // workspace as though it did not exist.\n this.reason === 'not-found'\n )\n }\n}\n\n/** The routes, as the Outreach pages call them. */\nexport interface OutreachApi {\n readSettings(): Promise<OutreachSettingsResponse>\n saveSettings(\n input: Omit<OutreachSettingsSaveRequest, 'orgId'>,\n ): Promise<OutreachSettingsSaveResponse>\n saveSequence(\n sequenceId: string | null,\n sequence: OutreachSequenceDraft,\n ): Promise<OutreachSequenceSaveResponse>\n setSequenceStatus(\n sequenceId: string,\n action: OutreachSequenceAction,\n ): Promise<OutreachSequenceStatusResponse>\n deleteSequence(sequenceId: string): Promise<{ ok: true }>\n previewEnrollment(\n sequenceId: string,\n source: OutreachEnrollSource,\n ): Promise<OutreachEnrollPreviewResponse>\n enroll(\n sequenceId: string,\n people: OutreachEnrollPersonRequest[],\n ): Promise<OutreachEnrollResponse>\n actOnEnrollment(\n enrollmentId: string,\n action: OutreachEnrollmentAction,\n detail?: string,\n ): Promise<OutreachEnrollmentActionResponse>\n /** The mail gateway in front of one enrolled person's domain (AGL-3332). */\n readEnrollmentGateway(enrollmentId: string): Promise<OutreachEnrollmentGatewayResponse>\n previewEmail(\n input: Omit<OutreachPreviewRequest, 'orgId'>,\n ): Promise<OutreachPreviewResponse>\n /**\n * Sends one step of a saved sequence to the member as a test (AGL-3325):\n * the preview's email, through the sequence's mailbox, `[Test]` in front.\n */\n sendStepTest(\n input: Omit<OutreachStepTestRequest, 'orgId'>,\n ): Promise<OutreachStepTestResponse & { unresolvedFields?: string[] }>\n /**\n * One person's own copies of the sequence's email steps, drafted by the\n * workspace's AI (AGL-3324); nothing is stored until the member confirms.\n */\n curateDrafts(\n input: Omit<OutreachCurateDraftRequest, 'orgId'>,\n ): Promise<OutreachCurateDraftResponse>\n /** Stores a confirmed copy of one step on an enrollment, or clears it with `null`. */\n saveCuratedStep(\n enrollmentId: string,\n stepIndex: number,\n override: OutreachCurateSaveRequest['override'],\n ): Promise<OutreachCurateSaveResponse>\n /** The domains on the do-not-contact list (AGL-3244). */\n readDoNotContactDomains(): Promise<OutreachDoNotContactDomainsResponse>\n /** Puts a domain on the list, or takes one off. */\n changeDoNotContactDomain(\n action: OutreachDoNotContactDomainRequest['action'],\n domain: string,\n detail?: string,\n ): Promise<OutreachDoNotContactDomainResponse>\n /** Each mailbox domain's click-tracking host (AGL-3306). */\n readLinkDomains(): Promise<OutreachLinkDomainsResponse>\n /** Sets a link domain up, checks it, or removes it. */\n changeLinkDomain(\n action: OutreachLinkDomainRequest['action'],\n domain: string,\n ): Promise<OutreachLinkDomainResponse>\n}\n\n/**\n * The Outreach pages' one door to the settings, sequence and enrollment\n * routes (AGL-2980), with the member's session attached and the\n * organization named on every call. A refusal is thrown as an\n * {@link OutreachRouteError} carrying the route's own sentence, which is\n * written for the person reading it.\n */\nexport function useOutreachApi(orgId: string | null): OutreachApi {\n const { data: user } = useUser()\n\n const call = useCallback(\n async <T>(\n route: string,\n init: { method: 'GET' | 'POST'; body?: Record<string, unknown> },\n ): Promise<T> => {\n if (!orgId)\n throw new OutreachRouteError(\n 'Open an organization first.',\n 'org-required',\n 400,\n )\n const path =\n init.method === 'GET'\n ? `/api/${route}?orgId=${encodeURIComponent(orgId)}`\n : `/api/${route}`\n let response: Response\n try {\n response = await authorizedFetch(user, path, {\n method: init.method,\n ...(init.method === 'POST'\n ? {\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ orgId, ...init.body }),\n }\n : {}),\n })\n } catch {\n throw new OutreachRouteError(\n 'Sequences could not be reached. Try again.',\n 'unreachable',\n 0,\n )\n }\n const payload = (await response.json().catch(() => null)) as\n (T & { error?: undefined }) | Partial<OutreachRouteRefusal> | null\n if (!response.ok || !payload) {\n const refusal = (payload ?? {}) as Partial<OutreachRouteRefusal>\n throw new OutreachRouteError(\n refusal.error && refusal.reason\n ? refusal.error\n : response.status === 404\n ? \"Sequences isn't available to this workspace yet.\"\n : 'Sequences could not be reached. Try again.',\n refusal.reason ??\n (response.status === 404 ? 'not-found' : 'unreachable'),\n response.status,\n refusal.issues ?? [],\n )\n }\n return payload as T\n },\n [user, orgId],\n )\n\n return useMemo<OutreachApi>(\n () => ({\n readSettings: () => call(OUTREACH_API_ROUTES.settings, { method: 'GET' }),\n saveSettings: (input) =>\n call(OUTREACH_API_ROUTES.settings, {\n method: 'POST',\n body: { ...input },\n }),\n saveSequence: (sequenceId, sequence) =>\n call(OUTREACH_API_ROUTES.sequencesSave, {\n method: 'POST',\n body: { ...(sequenceId ? { sequenceId } : {}), sequence },\n }),\n setSequenceStatus: (sequenceId, action) =>\n call(OUTREACH_API_ROUTES.sequencesStatus, {\n method: 'POST',\n body: { sequenceId, action },\n }),\n deleteSequence: (sequenceId) =>\n call(OUTREACH_API_ROUTES.sequencesDelete, {\n method: 'POST',\n body: { sequenceId },\n }),\n previewEnrollment: (sequenceId, source) =>\n call(OUTREACH_API_ROUTES.enrollPreview, {\n method: 'POST',\n body: { sequenceId, source },\n }),\n enroll: (sequenceId, people) =>\n call(OUTREACH_API_ROUTES.enroll, {\n method: 'POST',\n body: { sequenceId, people },\n }),\n actOnEnrollment: (enrollmentId, action, detail) =>\n call(OUTREACH_API_ROUTES.enrollmentsAction, {\n method: 'POST',\n body: { enrollmentId, action, ...(detail ? { detail } : {}) },\n }),\n readEnrollmentGateway: (enrollmentId) =>\n call(OUTREACH_API_ROUTES.enrollmentsGateway, {\n method: 'POST',\n body: { enrollmentId },\n }),\n previewEmail: (input) =>\n call(OUTREACH_API_ROUTES.preview, {\n method: 'POST',\n body: { ...input },\n }),\n sendStepTest: (input) =>\n call(OUTREACH_API_ROUTES.stepTest, {\n method: 'POST',\n body: { ...input },\n }),\n curateDrafts: (input) =>\n call(OUTREACH_API_ROUTES.curateDraft, {\n method: 'POST',\n body: { ...input },\n }),\n saveCuratedStep: (enrollmentId, stepIndex, override) =>\n call(OUTREACH_API_ROUTES.curateSave, {\n method: 'POST',\n body: { enrollmentId, stepIndex, override },\n }),\n readDoNotContactDomains: () =>\n call(OUTREACH_API_ROUTES.doNotContactDomains, { method: 'GET' }),\n changeDoNotContactDomain: (action, domain, detail) =>\n call(OUTREACH_API_ROUTES.doNotContactDomains, {\n method: 'POST',\n body: { action, domain, ...(detail ? { detail } : {}) },\n }),\n readLinkDomains: () => call(OUTREACH_API_ROUTES.linkDomains, { method: 'GET' }),\n changeLinkDomain: (action, domain) =>\n call(OUTREACH_API_ROUTES.linkDomains, {\n method: 'POST',\n body: { action, domain },\n }),\n }),\n [call],\n )\n}\n"],"names":["authorizedFetch","useUser","useCallback","useMemo","OUTREACH_API_ROUTES","OutreachRouteError","Error","refused","reason","message","status","issues","name","useOutreachApi","orgId","data","user","call","route","init","path","method","encodeURIComponent","response","headers","body","JSON","stringify","payload","json","catch","ok","refusal","error","readSettings","settings","saveSettings","input","saveSequence","sequenceId","sequence","sequencesSave","setSequenceStatus","action","sequencesStatus","deleteSequence","sequencesDelete","previewEnrollment","source","enrollPreview","enroll","people","actOnEnrollment","enrollmentId","detail","enrollmentsAction","readEnrollmentGateway","enrollmentsGateway","previewEmail","preview","sendStepTest","stepTest","curateDrafts","curateDraft","saveCuratedStep","stepIndex","override","curateSave","readDoNotContactDomains","doNotContactDomains","changeDoNotContactDomain","domain","readLinkDomains","linkDomains","changeLinkDomain"],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import { type OutreachEnrollment, type OutreachEnrollmentHistoryEntry } from '../model/outreach.types';
2
+ import type { OutreachEnrollmentGatewayResponse } from '../model/outreach-api';
3
+ import type { OutreachApi } from './use-outreach-api';
4
+ import type { OutreachLoad } from './use-outreach-data';
5
+ /** One enrollment, live, or `null` once it is known not to exist. */
6
+ export declare function useOutreachEnrollment(orgId: string | null, enrollmentId: string | null): OutreachLoad<OutreachEnrollment | null>;
7
+ /**
8
+ * The enrollment's history rows, newest first — every row it can hold, which
9
+ * is bounded by `OUTREACH_ENROLLMENT_HISTORY_MAX` on the writing side.
10
+ */
11
+ export declare function useOutreachEnrollmentHistory(orgId: string | null, enrollmentId: string | null): OutreachLoad<OutreachEnrollmentHistoryEntry[]>;
12
+ /**
13
+ * The mail gateway in front of the person's domain (AGL-3326, AGL-3328),
14
+ * and what it did with mail from the enrollment's sending domain — asked of
15
+ * the server once per page, because the domain's MX lives in the platform's
16
+ * cache, which no member reads.
17
+ */
18
+ export declare function useOutreachEnrollmentGateway(api: Pick<OutreachApi, 'readEnrollmentGateway'>, enrollmentId: string | null): OutreachLoad<OutreachEnrollmentGatewayResponse | null>;