@aglyn/plugins-outreach 1.0.0-beta.195 → 1.0.0-beta.196

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 (57) hide show
  1. package/package.json +14 -14
  2. package/src/lib/components/curate-step-dialog.d.ts +50 -0
  3. package/src/lib/components/curate-step-dialog.js +235 -0
  4. package/src/lib/components/curate-step-dialog.js.map +1 -0
  5. package/src/lib/components/curated-step-editor.d.ts +80 -0
  6. package/src/lib/components/curated-step-editor.js +255 -0
  7. package/src/lib/components/curated-step-editor.js.map +1 -0
  8. package/src/lib/components/enroll-dialog.d.ts +15 -0
  9. package/src/lib/components/enroll-dialog.js +154 -13
  10. package/src/lib/components/enroll-dialog.js.map +1 -1
  11. package/src/lib/components/enrollments-table.d.ts +2 -0
  12. package/src/lib/components/enrollments-table.js +62 -8
  13. package/src/lib/components/enrollments-table.js.map +1 -1
  14. package/src/lib/components/use-outreach-api.d.ts +8 -1
  15. package/src/lib/components/use-outreach-api.js +12 -0
  16. package/src/lib/components/use-outreach-api.js.map +1 -1
  17. package/src/lib/constants/api-routes.d.ts +8 -0
  18. package/src/lib/constants/api-routes.js +6 -0
  19. package/src/lib/constants/api-routes.js.map +1 -1
  20. package/src/lib/engine/compose.d.ts +12 -2
  21. package/src/lib/engine/compose.js +32 -6
  22. package/src/lib/engine/compose.js.map +1 -1
  23. package/src/lib/engine/curation-prompt.d.ts +73 -0
  24. package/src/lib/engine/curation-prompt.js +151 -0
  25. package/src/lib/engine/curation-prompt.js.map +1 -0
  26. package/src/lib/engine/enrollment-activity.d.ts +24 -0
  27. package/src/lib/engine/enrollment-activity.js +25 -0
  28. package/src/lib/engine/enrollment-activity.js.map +1 -1
  29. package/src/lib/engine/sequence-validation.d.ts +10 -1
  30. package/src/lib/engine/sequence-validation.js +54 -0
  31. package/src/lib/engine/sequence-validation.js.map +1 -1
  32. package/src/lib/enrollment/step-overrides.d.ts +53 -0
  33. package/src/lib/enrollment/step-overrides.js +140 -0
  34. package/src/lib/enrollment/step-overrides.js.map +1 -0
  35. package/src/lib/model/outreach-api.d.ts +69 -2
  36. package/src/lib/model/outreach-api.js.map +1 -1
  37. package/src/lib/model/outreach.types.d.ts +36 -0
  38. package/src/lib/model/outreach.types.js +18 -0
  39. package/src/lib/model/outreach.types.js.map +1 -1
  40. package/src/lib/model/step-preview.d.ts +3 -1
  41. package/src/lib/model/step-preview.js +6 -3
  42. package/src/lib/model/step-preview.js.map +1 -1
  43. package/src/lib/model/stored-records.d.ts +9 -1
  44. package/src/lib/model/stored-records.js +47 -2
  45. package/src/lib/model/stored-records.js.map +1 -1
  46. package/src/lib/routes/curate-routes.d.ts +63 -0
  47. package/src/lib/routes/curate-routes.js +424 -0
  48. package/src/lib/routes/curate-routes.js.map +1 -0
  49. package/src/lib/routes/enroll-routes.d.ts +7 -0
  50. package/src/lib/routes/enroll-routes.js +58 -4
  51. package/src/lib/routes/enroll-routes.js.map +1 -1
  52. package/src/lib/routes/preview-routes.js +15 -1
  53. package/src/lib/routes/preview-routes.js.map +1 -1
  54. package/src/lib/routes/register-routes.js +17 -0
  55. package/src/lib/routes/register-routes.js.map +1 -1
  56. package/src/lib/runtime/send-job.js +2 -0
  57. package/src/lib/runtime/send-job.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/enrollments-table.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 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 ListRowActions,\n ListTable,\n listActionsColumn,\n type ListTableProps,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport { hiddenFilterVisibility } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport {\n filterListRows,\n inMemoryListField,\n listFilterGridColumns,\n} from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { TABLE_PAGE_SIZE_DEFAULT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Button,\n Card,\n CardContent,\n Dialog,\n DialogActions,\n DialogContent,\n DialogContentText,\n DialogTitle,\n Stack,\n TextField,\n Typography,\n useMediaQuery,\n useTheme,\n Chip,\n} from '@mui/material'\nimport { type ReactNode, useEffect, useMemo, useState } from 'react'\nimport { readOutreachEngagement } from '../model/enrollment-engagement'\nimport type { OutreachEnrollmentAction } from '../model/outreach-api'\nimport {\n OUTREACH_ENROLLMENT_STATUSES,\n OUTREACH_TASK_KIND_LABELS,\n type OutreachEnrollment,\n type OutreachSequenceStep,\n} from '../model/outreach.types'\nimport {\n formatOutreachTime,\n OUTREACH_ENROLLMENT_STATUS_LABELS,\n OUTREACH_STOP_REASON_LABELS,\n OutreachEnrollmentStatusChip,\n OutreachLoading,\n OutreachLoadProblem,\n} from './outreach-ui'\nimport type { OutreachApi } from './use-outreach-api'\nimport type { OutreachEnrollmentsLoad } from './use-outreach-data'\n\nexport interface OutreachEnrollmentsTableProps {\n enrollments: OutreachEnrollmentsLoad\n steps: readonly OutreachSequenceStep[]\n /**\n * Whether this sequence counts link clicks (AGL-3239). The Clicks column\n * is shown only when it does: a column of dashes for a sequence that\n * measures nothing reads as \"nobody clicked\", which is a different claim\n * and a false one.\n */\n trackClicks?: boolean\n /** The mailbox's IANA zone, which next sends are read in; `null` for the reader's own. */\n timeZone: string | null\n api: OutreachApi\n /** Shown in the empty state when the sequence takes people. */\n enrollAction?: ReactNode\n}\n\n/** The step an enrollment is on, as a person reads it. */\nexport function outreachCurrentStepLabel(\n enrollment: Pick<OutreachEnrollment, 'status' | 'stepIndex'>,\n steps: readonly OutreachSequenceStep[],\n): string {\n if (enrollment.status === 'finished' || enrollment.stepIndex >= steps.length)\n return 'All steps done'\n const step = steps[enrollment.stepIndex]\n const kind =\n step?.kind === 'email'\n ? 'Email'\n : step?.kind === 'task'\n ? OUTREACH_TASK_KIND_LABELS[step.taskKind]\n : 'Step'\n return `Step ${enrollment.stepIndex + 1} of ${steps.length} · ${kind}`\n}\n\n/** Why an enrollment stopped, as the table says it; `''` while it runs. */\nexport function outreachStopLabel(\n enrollment: Pick<OutreachEnrollment, 'stopReason' | 'stopDetail'>,\n): string {\n if (!enrollment.stopReason) return ''\n const reason = OUTREACH_STOP_REASON_LABELS[enrollment.stopReason] ?? ''\n return enrollment.stopDetail ? `${reason} — ${enrollment.stopDetail}` : reason\n}\n\n/** The last thing that happened: a send, a stop, or the enrollment itself. */\nfunction lastActivityMs(enrollment: OutreachEnrollment): number {\n return Math.max(\n enrollment.lastSentAtMs ?? 0,\n enrollment.stoppedAtMs ?? 0,\n enrollment.createdAtMs ?? 0,\n )\n}\n\n/** What a member may do to an enrollment in the status it is in. */\nfunction actionsFor(\n enrollment: OutreachEnrollment,\n): OutreachEnrollmentAction[] {\n const actions: OutreachEnrollmentAction[] = []\n if (enrollment.status === 'active') actions.push('pause', 'stop')\n if (enrollment.status === 'paused') actions.push('resume', 'stop')\n if (enrollment.status !== 'opted_out') actions.push('do_not_contact')\n return actions\n}\n\nconst ACTION_LABELS: Record<OutreachEnrollmentAction, string> = {\n pause: 'Pause',\n resume: 'Resume',\n stop: 'Stop',\n do_not_contact: 'Mark do-not-contact',\n}\n\n/** What the snackbar says once an action lands. */\nconst DONE_LABELS: Record<\n Exclude<OutreachEnrollmentAction, 'do_not_contact'>,\n string\n> = {\n pause: 'Paused.',\n resume: 'Resumed.',\n stop: 'Stopped.',\n}\n\n/** The two actions that end something for good ask first. */\nconst CONFIRM: Partial<\n Record<\n OutreachEnrollmentAction,\n { title: string; body: string; label: string }\n >\n> = {\n stop: {\n title: 'Stop this enrollment?',\n body: 'They won’t get any more steps from this sequence, and they can’t be enrolled in it again.',\n label: 'Stop',\n },\n do_not_contact: {\n title: 'Mark do-not-contact?',\n body:\n 'The address goes on your organization’s do-not-contact list, which every sequence checks before ' +\n 'every send, and they are stopped in every sequence they are in.',\n label: 'Mark do-not-contact',\n },\n}\n\n/*\n * What the enrollments grid's Filters panel offers (AGL-3317). Status and\n * whether the person is a lead are picked; the name, the address and the\n * stop reason are typed. `target` and `email` are no column of their own,\n * so they are hidden columns the panel can still reach.\n */\nconst ENROLLMENT_FILTER_FIELDS = [\n inMemoryListField('status', 'select'),\n inMemoryListField('target', 'select'),\n inMemoryListField('contactName', 'text'),\n inMemoryListField('email', 'text'),\n inMemoryListField('stopReason', 'text'),\n]\nconst ENROLLMENT_FILTER_HEADERS: Readonly<Record<string, string>> = {\n status: 'Status',\n target: 'Enrolled as',\n contactName: 'Name',\n email: 'Email',\n stopReason: 'Stop reason',\n}\nconst ENROLLMENT_FILTER_OPTIONS = {\n status: OUTREACH_ENROLLMENT_STATUSES.map((status) => ({\n value: status,\n label: OUTREACH_ENROLLMENT_STATUS_LABELS[status],\n })),\n target: [\n { value: 'contact', label: 'Contact' },\n { value: 'lead', label: 'Lead' },\n ],\n}\n/** What the quick search reads on an enrollment row. */\nconst ENROLLMENT_SEARCH_FIELDS = ['contactName', 'email', 'step', 'stopReason'] as const\n/** The filter-only columns never show. */\nconst ENROLLMENT_HIDDEN_COLUMNS = hiddenFilterVisibility(ENROLLMENT_FILTER_FIELDS, [\n 'status',\n 'stopReason',\n])\n\n/** The table's columns; the person and the actions are drawn from the row's enrollment. */\nfunction columns(\n rowActions: (enrollment: OutreachEnrollment) => ReactNode,\n trackClicks: boolean,\n): NonNullable<ListTableProps['columns']> {\n return [\n {\n field: 'person',\n headerName: 'Person',\n flex: 1,\n minWidth: 200,\n sortable: false,\n renderCell: ({ row }) => (\n <Stack\n spacing={0}\n sx={{ justifyContent: 'center', height: '100%', minWidth: 0 }}\n >\n <Stack direction=\"row\" spacing={0.75} sx={{ alignItems: 'center', minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {row.enrollment.contactName || row.enrollment.email}\n </Typography>\n {row.enrollment.target === 'lead' ? (\n <Chip size=\"small\" variant=\"outlined\" label=\"Lead\" />\n ) : null}\n </Stack>\n {row.enrollment.contactName ? (\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {row.enrollment.email}\n </Typography>\n ) : null}\n </Stack>\n ),\n },\n {\n field: 'status',\n headerName: 'Status',\n width: 120,\n renderCell: ({ row }) => (\n <OutreachEnrollmentStatusChip status={row.status} />\n ),\n },\n { field: 'step', headerName: 'Current step', flex: 1, minWidth: 170 },\n { field: 'nextSend', headerName: 'Next send', width: 190, sortable: false },\n {\n field: 'lastActivity',\n headerName: 'Last activity',\n width: 190,\n sortable: false,\n },\n ...(trackClicks\n ? [\n {\n field: 'clicks',\n headerName: 'Clicks',\n width: 130,\n sortable: false,\n renderCell: ({ row }: { row: { enrollment: OutreachEnrollment } }) => {\n const engagement = readOutreachEngagement(row.enrollment.engagement)\n if (!engagement.clicks) {\n return (\n <Typography variant=\"body2\" color=\"text.secondary\">\n —\n </Typography>\n )\n }\n return (\n <Stack spacing={0} sx={{ justifyContent: 'center', height: '100%' }}>\n <Typography variant=\"body2\">\n {engagement.clicks === 1 ? '1 click' : `${engagement.clicks} clicks`}\n </Typography>\n {engagement.lastClickUrl ? (\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {engagement.lastClickUrl}\n </Typography>\n ) : null}\n </Stack>\n )\n },\n },\n ]\n : []),\n { field: 'stopReason', headerName: 'Stop reason', flex: 1, minWidth: 170 },\n listActionsColumn((row) => rowActions(row.enrollment), { width: 72 }),\n ]\n}\n\n/**\n * A sequence's enrollments (AGL-2980): who is in it, where each person\n * stands, when their next step goes out in the mailbox's timezone, and the\n * four things a member can do about it — pause, resume, stop, and mark\n * do-not-contact. A table on wider screens, a list of cards on a phone.\n */\nexport function OutreachEnrollmentsTable(props: OutreachEnrollmentsTableProps) {\n const { enrollments, steps, timeZone, api } = props\n const trackClicks = props.trackClicks === true\n const theme = useTheme()\n const narrow = useMediaQuery(theme.breakpoints.down('md'))\n const { enqueueSnackbar } = useSnackbar()\n const [confirming, setConfirming] = useState<{\n enrollment: OutreachEnrollment\n action: OutreachEnrollmentAction\n } | null>(null)\n const [detail, setDetail] = useState('')\n const [busy, setBusy] = useState<string | null>(null)\n const [page, setPage] = useState(0)\n const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT)\n const gridFilter = useListGridFilter({ selectFields: ['status', 'target'] })\n const filtering = gridFilter.clauses.length > 0 || gridFilter.searchWords.length > 0\n /*\n * The rows the grid is handed, every clause and the search answered over\n * the enrollments read so far — a window that grows as the reader pages\n * past it, so a filter reaches further the further they go.\n */\n const searchKey = gridFilter.searchWords.join(' ')\n const loadedData = enrollments.data\n const matching = useMemo(\n () =>\n filterListRows(\n loadedData.map((enrollment) => ({\n enrollment,\n status: enrollment.status,\n target: enrollment.target === 'lead' ? 'lead' : 'contact',\n contactName: enrollment.contactName,\n email: enrollment.email,\n step: outreachCurrentStepLabel(enrollment, steps),\n stopReason: outreachStopLabel(enrollment),\n })),\n ENROLLMENT_FILTER_FIELDS,\n gridFilter.clauses,\n { paths: ENROLLMENT_SEARCH_FIELDS, words: gridFilter.searchWords },\n ).map((row) => row.enrollment),\n // `searchKey` stands for the words, which are a new array each render.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [loadedData, steps, gridFilter.clauses, searchKey],\n )\n // A narrowed list starts again at its first page.\n useEffect(() => setPage(0), [gridFilter.clauses, searchKey])\n\n const act = async (\n enrollment: OutreachEnrollment,\n action: OutreachEnrollmentAction,\n why?: string,\n ) => {\n setBusy(enrollment.id)\n try {\n const answer = await api.actOnEnrollment(enrollment.id, action, why)\n const others = answer.stoppedOthers\n enqueueSnackbar(\n action === 'do_not_contact'\n ? `Marked do-not-contact${others ? `, and stopped in ${others} other ${others === 1 ? 'sequence' : 'sequences'}` : ''}.`\n : answer.changed\n ? DONE_LABELS[action]\n : 'Nothing to change.',\n { variant: 'success' },\n )\n } catch (error) {\n enqueueSnackbar((error as Error).message, {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setBusy(null)\n }\n }\n\n const choose = (\n enrollment: OutreachEnrollment,\n action: OutreachEnrollmentAction,\n ) => {\n if (CONFIRM[action]) {\n setDetail('')\n setConfirming({ enrollment, action })\n } else {\n void act(enrollment, action)\n }\n }\n\n if (enrollments.status === 'loading')\n return <OutreachLoading label=\"Loading enrollments…\" />\n if (enrollments.status === 'error' || enrollments.status === 'refused') {\n return (\n <OutreachLoadProblem status={enrollments.status} what=\"enrollments\" />\n )\n }\n if (!enrollments.data.length) {\n return (\n <Card variant=\"outlined\">\n <EmptyStateComponent\n label=\"Nobody is enrolled yet\"\n description=\"People you enroll show here, with where each of them is in the sequence.\"\n action={props.enrollAction}\n />\n </Card>\n )\n }\n\n const rowActions = (enrollment: OutreachEnrollment) => (\n <ListRowActions\n label={enrollment.contactName || enrollment.email}\n items={actionsFor(enrollment).map((action) => ({\n key: action,\n label: ACTION_LABELS[action],\n onClick: () => choose(enrollment, action),\n destructive: action === 'stop' || action === 'do_not_contact',\n disabled: busy === enrollment.id,\n }))}\n />\n )\n const nextSend = (enrollment: OutreachEnrollment) =>\n enrollment.status === 'active' || enrollment.status === 'paused'\n ? formatOutreachTime(enrollment.nextDueAtMs, timeZone)\n : '—'\n const person = (enrollment: OutreachEnrollment) => (\n <Stack spacing={0}>\n <Stack direction=\"row\" spacing={0.75} sx={{ alignItems: 'center' }}>\n <Typography variant=\"body2\">\n {enrollment.contactName || enrollment.email}\n </Typography>\n {enrollment.target === 'lead' ? (\n <Chip size=\"small\" variant=\"outlined\" label=\"Lead\" />\n ) : null}\n </Stack>\n {enrollment.contactName ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {enrollment.email}\n </Typography>\n ) : null}\n </Stack>\n )\n\n const loaded = matching\n const pageRows = loaded.slice(page * pageSize, (page + 1) * pageSize)\n const lastLoadedPage = Math.max(0, Math.ceil(loaded.length / pageSize) - 1)\n /** Past the rows read so far, the next page reads another window first. */\n const turnTo = (next: number) => {\n if ((next + 1) * pageSize > loaded.length && enrollments.hasMore)\n enrollments.showMore()\n setPage(next)\n }\n\n return (\n <Stack spacing={1.5}>\n <ListFilterChips\n fields={ENROLLMENT_FILTER_FIELDS}\n headers={ENROLLMENT_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={ENROLLMENT_FILTER_OPTIONS}\n />\n {filtering && enrollments.hasMore ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Filtering the ${enrollments.data.length} enrollments read so far — the next page reads more.`}\n </Typography>\n ) : null}\n {narrow ? (\n <Stack\n spacing={1}\n component=\"ul\"\n sx={{ listStyle: 'none', p: 0, m: 0 }}\n aria-label=\"Enrollments\"\n >\n {pageRows.map((enrollment) => (\n <Card key={enrollment.id} variant=\"outlined\" component=\"li\">\n <CardContent>\n <Stack spacing={1}>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'flex-start' }}\n >\n <Stack sx={{ flexGrow: 1, minWidth: 0 }}>\n {person(enrollment)}\n </Stack>\n <OutreachEnrollmentStatusChip status={enrollment.status} />\n {rowActions(enrollment)}\n </Stack>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {outreachCurrentStepLabel(enrollment, steps)}\n </Typography>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`Next send: ${nextSend(enrollment)} · Last activity: ${formatOutreachTime(lastActivityMs(enrollment), timeZone)}`}\n </Typography>\n {trackClicks && readOutreachEngagement(enrollment.engagement).clicks ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`Clicked ${readOutreachEngagement(enrollment.engagement).clicks} ${\n readOutreachEngagement(enrollment.engagement).clicks === 1 ? 'time' : 'times'\n }`}\n </Typography>\n ) : null}\n {outreachStopLabel(enrollment) ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {outreachStopLabel(enrollment)}\n </Typography>\n ) : null}\n </Stack>\n </CardContent>\n </Card>\n ))}\n </Stack>\n ) : (\n <ListTable\n aria-label=\"Enrollments\"\n columns={listFilterGridColumns(\n columns(rowActions, trackClicks),\n ENROLLMENT_FILTER_FIELDS,\n ENROLLMENT_FILTER_OPTIONS,\n ENROLLMENT_FILTER_HEADERS,\n )}\n initialState={{ columns: { columnVisibilityModel: ENROLLMENT_HIDDEN_COLUMNS } }}\n rows={pageRows.map((enrollment) => ({\n $id: enrollment.id,\n enrollment,\n status: enrollment.status,\n target: enrollment.target === 'lead' ? 'lead' : 'contact',\n contactName: enrollment.contactName,\n email: enrollment.email,\n step: outreachCurrentStepLabel(enrollment, steps),\n nextSend: nextSend(enrollment),\n lastActivity: formatOutreachTime(\n lastActivityMs(enrollment),\n timeZone,\n ),\n stopReason: outreachStopLabel(enrollment) || '—',\n }))}\n /*\n * The panel and the search are the grid's; the table answers them\n * over the enrollments it read (AGL-3317).\n */\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n noRowsLabel=\"No enrollments match these filters\"\n hideFooter\n />\n )}\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={pageRows.length}\n count={enrollments.hasMore ? undefined : loaded.length}\n hasMore={enrollments.hasMore || page < lastLoadedPage}\n onPageChange={turnTo}\n onPageSizeChange={setPageSize}\n />\n\n <Dialog\n open={Boolean(confirming)}\n onClose={() => setConfirming(null)}\n fullWidth\n maxWidth=\"xs\"\n >\n {confirming ? (\n <>\n <DialogTitle>{CONFIRM[confirming.action]?.title}</DialogTitle>\n <DialogContent>\n <Stack spacing={2}>\n <DialogContentText>\n {CONFIRM[confirming.action]?.body}\n </DialogContentText>\n <TextField\n label=\"Why (optional)\"\n value={detail}\n onChange={(event) => setDetail(event.target.value)}\n fullWidth\n slotProps={{ htmlInput: { maxLength: 200 } }}\n />\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setConfirming(null)}>Cancel</Button>\n <Button\n color=\"error\"\n variant=\"contained\"\n onClick={() => {\n const { enrollment, action } = confirming\n setConfirming(null)\n void act(enrollment, action, detail.trim() || undefined)\n }}\n >\n {CONFIRM[confirming.action]?.label}\n </Button>\n </DialogActions>\n </>\n ) : null}\n </Dialog>\n </Stack>\n )\n}\nOutreachEnrollmentsTable.displayName = 'OutreachEnrollmentsTable'\n\nexport default OutreachEnrollmentsTable\n"],"names":["EmptyStateComponent","ListFilterChips","ListPagination","ListRowActions","ListTable","listActionsColumn","hiddenFilterVisibility","filterListRows","inMemoryListField","listFilterGridColumns","TABLE_PAGE_SIZE_DEFAULT","useListGridFilter","useSnackbar","Button","Card","CardContent","Dialog","DialogActions","DialogContent","DialogContentText","DialogTitle","Stack","TextField","Typography","useMediaQuery","useTheme","Chip","useEffect","useMemo","useState","readOutreachEngagement","OUTREACH_ENROLLMENT_STATUSES","OUTREACH_TASK_KIND_LABELS","formatOutreachTime","OUTREACH_ENROLLMENT_STATUS_LABELS","OUTREACH_STOP_REASON_LABELS","OutreachEnrollmentStatusChip","OutreachLoading","OutreachLoadProblem","outreachCurrentStepLabel","enrollment","steps","status","stepIndex","length","step","kind","taskKind","outreachStopLabel","stopReason","reason","stopDetail","lastActivityMs","Math","max","lastSentAtMs","stoppedAtMs","createdAtMs","actionsFor","actions","push","ACTION_LABELS","pause","resume","stop","do_not_contact","DONE_LABELS","CONFIRM","title","body","label","ENROLLMENT_FILTER_FIELDS","ENROLLMENT_FILTER_HEADERS","target","contactName","email","ENROLLMENT_FILTER_OPTIONS","map","value","ENROLLMENT_SEARCH_FIELDS","ENROLLMENT_HIDDEN_COLUMNS","columns","rowActions","trackClicks","field","headerName","flex","minWidth","sortable","renderCell","row","spacing","sx","justifyContent","height","direction","alignItems","variant","noWrap","size","color","width","engagement","clicks","lastClickUrl","OutreachEnrollmentsTable","props","enrollments","timeZone","api","theme","narrow","breakpoints","down","enqueueSnackbar","confirming","setConfirming","detail","setDetail","busy","setBusy","page","setPage","pageSize","setPageSize","gridFilter","selectFields","filtering","clauses","searchWords","searchKey","join","loadedData","data","matching","paths","words","act","action","why","id","answer","actOnEnrollment","others","stoppedOthers","changed","error","message","allowDuplicate","choose","what","description","enrollAction","items","key","onClick","destructive","disabled","nextSend","nextDueAtMs","person","loaded","pageRows","slice","lastLoadedPage","ceil","turnTo","next","hasMore","showMore","fields","headers","onChange","setClauses","options","component","listStyle","p","m","aria-label","flexGrow","initialState","columnVisibilityModel","rows","$id","lastActivity","filterMode","filterModel","onFilterModelChange","quickFilter","noRowsLabel","hideFooter","rowCount","count","undefined","onPageChange","onPageSizeChange","open","Boolean","onClose","fullWidth","maxWidth","event","slotProps","htmlInput","maxLength","trim","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/enrollments-table.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 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 ListRowActions,\n ListTable,\n listActionsColumn,\n type ListTableProps,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport { hiddenFilterVisibility } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport {\n filterListRows,\n inMemoryListField,\n listFilterGridColumns,\n} from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { TABLE_PAGE_SIZE_DEFAULT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Button,\n Card,\n CardContent,\n Dialog,\n DialogActions,\n DialogContent,\n DialogContentText,\n DialogTitle,\n Stack,\n TextField,\n Typography,\n useMediaQuery,\n useTheme,\n Chip,\n} from '@mui/material'\nimport { type ReactNode, useEffect, useMemo, useState } from 'react'\nimport { OutreachCurateStepDialog, outreachNextEmailStepIndex } from './curate-step-dialog'\nimport { readOutreachEngagement } from '../model/enrollment-engagement'\nimport type { OutreachEnrollmentAction } from '../model/outreach-api'\nimport {\n OUTREACH_ENROLLMENT_STATUSES,\n OUTREACH_TASK_KIND_LABELS,\n type OutreachEnrollment,\n type OutreachSequenceStep,\n} from '../model/outreach.types'\nimport {\n formatOutreachTime,\n OUTREACH_ENROLLMENT_STATUS_LABELS,\n OUTREACH_STOP_REASON_LABELS,\n OutreachEnrollmentStatusChip,\n OutreachLoading,\n OutreachLoadProblem,\n} from './outreach-ui'\nimport type { OutreachApi } from './use-outreach-api'\nimport type { OutreachEnrollmentsLoad } from './use-outreach-data'\n\nexport interface OutreachEnrollmentsTableProps {\n enrollments: OutreachEnrollmentsLoad\n steps: readonly OutreachSequenceStep[]\n /**\n * Whether this sequence counts link clicks (AGL-3239). The Clicks column\n * is shown only when it does: a column of dashes for a sequence that\n * measures nothing reads as \"nobody clicked\", which is a different claim\n * and a false one.\n */\n trackClicks?: boolean\n /** The mailbox's IANA zone, which next sends are read in; `null` for the reader's own. */\n timeZone: string | null\n api: OutreachApi\n /** Shown in the empty state when the sequence takes people. */\n enrollAction?: ReactNode\n}\n\n/** The step an enrollment is on, as a person reads it. */\nexport function outreachCurrentStepLabel(\n enrollment: Pick<OutreachEnrollment, 'status' | 'stepIndex'>,\n steps: readonly OutreachSequenceStep[],\n): string {\n if (enrollment.status === 'finished' || enrollment.stepIndex >= steps.length)\n return 'All steps done'\n const step = steps[enrollment.stepIndex]\n const kind =\n step?.kind === 'email'\n ? 'Email'\n : step?.kind === 'task'\n ? OUTREACH_TASK_KIND_LABELS[step.taskKind]\n : 'Step'\n return `Step ${enrollment.stepIndex + 1} of ${steps.length} · ${kind}`\n}\n\n/** Why an enrollment stopped, as the table says it; `''` while it runs. */\nexport function outreachStopLabel(\n enrollment: Pick<OutreachEnrollment, 'stopReason' | 'stopDetail'>,\n): string {\n if (!enrollment.stopReason) return ''\n const reason = OUTREACH_STOP_REASON_LABELS[enrollment.stopReason] ?? ''\n return enrollment.stopDetail ? `${reason} — ${enrollment.stopDetail}` : reason\n}\n\n/** The last thing that happened: a send, a stop, or the enrollment itself. */\nfunction lastActivityMs(enrollment: OutreachEnrollment): number {\n return Math.max(\n enrollment.lastSentAtMs ?? 0,\n enrollment.stoppedAtMs ?? 0,\n enrollment.createdAtMs ?? 0,\n )\n}\n\n/** The row's actions: the four the action route takes, and curating the next step (AGL-3324). */\ntype RowAction = OutreachEnrollmentAction | 'curate'\n\n/** What a member may do to an enrollment in the status it is in. */\nfunction actionsFor(\n enrollment: OutreachEnrollment,\n steps: readonly OutreachSequenceStep[],\n): RowAction[] {\n const actions: RowAction[] = []\n // Rewrite the next email for this one person, while one is still to go.\n if (outreachNextEmailStepIndex(enrollment, steps) !== null) actions.push('curate')\n if (enrollment.status === 'active') actions.push('pause', 'stop')\n if (enrollment.status === 'paused') actions.push('resume', 'stop')\n if (enrollment.status !== 'opted_out') actions.push('do_not_contact')\n return actions\n}\n\nconst ACTION_LABELS: Record<RowAction, string> = {\n curate: 'Curate next step',\n pause: 'Pause',\n resume: 'Resume',\n stop: 'Stop',\n do_not_contact: 'Mark do-not-contact',\n}\n\n/** Whether the step the enrollment is on goes out as this person's own copy (AGL-3324). */\nexport function outreachStepIsCurated(\n enrollment: Pick<OutreachEnrollment, 'stepIndex' | 'stepOverrides'>,\n): boolean {\n return Boolean(enrollment.stepOverrides?.[String(enrollment.stepIndex)])\n}\n\n/** What the snackbar says once an action lands. */\nconst DONE_LABELS: Record<\n Exclude<OutreachEnrollmentAction, 'do_not_contact'>,\n string\n> = {\n pause: 'Paused.',\n resume: 'Resumed.',\n stop: 'Stopped.',\n}\n\n/** The two actions that end something for good ask first. */\nconst CONFIRM: Partial<\n Record<\n OutreachEnrollmentAction,\n { title: string; body: string; label: string }\n >\n> = {\n stop: {\n title: 'Stop this enrollment?',\n body: 'They won’t get any more steps from this sequence, and they can’t be enrolled in it again.',\n label: 'Stop',\n },\n do_not_contact: {\n title: 'Mark do-not-contact?',\n body:\n 'The address goes on your organization’s do-not-contact list, which every sequence checks before ' +\n 'every send, and they are stopped in every sequence they are in.',\n label: 'Mark do-not-contact',\n },\n}\n\n/*\n * What the enrollments grid's Filters panel offers (AGL-3317). Status and\n * whether the person is a lead are picked; the name, the address and the\n * stop reason are typed. `target` and `email` are no column of their own,\n * so they are hidden columns the panel can still reach.\n */\nconst ENROLLMENT_FILTER_FIELDS = [\n inMemoryListField('status', 'select'),\n inMemoryListField('target', 'select'),\n inMemoryListField('contactName', 'text'),\n inMemoryListField('email', 'text'),\n inMemoryListField('stopReason', 'text'),\n]\nconst ENROLLMENT_FILTER_HEADERS: Readonly<Record<string, string>> = {\n status: 'Status',\n target: 'Enrolled as',\n contactName: 'Name',\n email: 'Email',\n stopReason: 'Stop reason',\n}\nconst ENROLLMENT_FILTER_OPTIONS = {\n status: OUTREACH_ENROLLMENT_STATUSES.map((status) => ({\n value: status,\n label: OUTREACH_ENROLLMENT_STATUS_LABELS[status],\n })),\n target: [\n { value: 'contact', label: 'Contact' },\n { value: 'lead', label: 'Lead' },\n ],\n}\n/** What the quick search reads on an enrollment row. */\nconst ENROLLMENT_SEARCH_FIELDS = ['contactName', 'email', 'step', 'stopReason'] as const\n/** The filter-only columns never show. */\nconst ENROLLMENT_HIDDEN_COLUMNS = hiddenFilterVisibility(ENROLLMENT_FILTER_FIELDS, [\n 'status',\n 'stopReason',\n])\n\n/** The table's columns; the person and the actions are drawn from the row's enrollment. */\nfunction columns(\n rowActions: (enrollment: OutreachEnrollment) => ReactNode,\n trackClicks: boolean,\n): NonNullable<ListTableProps['columns']> {\n return [\n {\n field: 'person',\n headerName: 'Person',\n flex: 1,\n minWidth: 200,\n sortable: false,\n renderCell: ({ row }) => (\n <Stack\n spacing={0}\n sx={{ justifyContent: 'center', height: '100%', minWidth: 0 }}\n >\n <Stack direction=\"row\" spacing={0.75} sx={{ alignItems: 'center', minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {row.enrollment.contactName || row.enrollment.email}\n </Typography>\n {row.enrollment.target === 'lead' ? (\n <Chip size=\"small\" variant=\"outlined\" label=\"Lead\" />\n ) : null}\n </Stack>\n {row.enrollment.contactName ? (\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {row.enrollment.email}\n </Typography>\n ) : null}\n </Stack>\n ),\n },\n {\n field: 'status',\n headerName: 'Status',\n width: 120,\n renderCell: ({ row }) => (\n <OutreachEnrollmentStatusChip status={row.status} />\n ),\n },\n {\n field: 'step',\n headerName: 'Current step',\n flex: 1,\n minWidth: 170,\n renderCell: ({ row }) => (\n <Stack direction=\"row\" spacing={0.75} sx={{ alignItems: 'center', height: '100%', minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {row.step}\n </Typography>\n {outreachStepIsCurated(row.enrollment) ? (\n <Chip size=\"small\" variant=\"outlined\" color=\"success\" label=\"Curated\" />\n ) : null}\n </Stack>\n ),\n },\n { field: 'nextSend', headerName: 'Next send', width: 190, sortable: false },\n {\n field: 'lastActivity',\n headerName: 'Last activity',\n width: 190,\n sortable: false,\n },\n ...(trackClicks\n ? [\n {\n field: 'clicks',\n headerName: 'Clicks',\n width: 130,\n sortable: false,\n renderCell: ({ row }: { row: { enrollment: OutreachEnrollment } }) => {\n const engagement = readOutreachEngagement(row.enrollment.engagement)\n if (!engagement.clicks) {\n return (\n <Typography variant=\"body2\" color=\"text.secondary\">\n —\n </Typography>\n )\n }\n return (\n <Stack spacing={0} sx={{ justifyContent: 'center', height: '100%' }}>\n <Typography variant=\"body2\">\n {engagement.clicks === 1 ? '1 click' : `${engagement.clicks} clicks`}\n </Typography>\n {engagement.lastClickUrl ? (\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {engagement.lastClickUrl}\n </Typography>\n ) : null}\n </Stack>\n )\n },\n },\n ]\n : []),\n { field: 'stopReason', headerName: 'Stop reason', flex: 1, minWidth: 170 },\n listActionsColumn((row) => rowActions(row.enrollment), { width: 72 }),\n ]\n}\n\n/**\n * A sequence's enrollments (AGL-2980): who is in it, where each person\n * stands, when their next step goes out in the mailbox's timezone, and the\n * four things a member can do about it — pause, resume, stop, and mark\n * do-not-contact. A table on wider screens, a list of cards on a phone.\n */\nexport function OutreachEnrollmentsTable(props: OutreachEnrollmentsTableProps) {\n const { enrollments, steps, timeZone, api } = props\n const trackClicks = props.trackClicks === true\n const theme = useTheme()\n const narrow = useMediaQuery(theme.breakpoints.down('md'))\n const { enqueueSnackbar } = useSnackbar()\n const [confirming, setConfirming] = useState<{\n enrollment: OutreachEnrollment\n action: OutreachEnrollmentAction\n } | null>(null)\n const [detail, setDetail] = useState('')\n const [busy, setBusy] = useState<string | null>(null)\n const [curating, setCurating] = useState<OutreachEnrollment | null>(null)\n const [page, setPage] = useState(0)\n const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT)\n const gridFilter = useListGridFilter({ selectFields: ['status', 'target'] })\n const filtering = gridFilter.clauses.length > 0 || gridFilter.searchWords.length > 0\n /*\n * The rows the grid is handed, every clause and the search answered over\n * the enrollments read so far — a window that grows as the reader pages\n * past it, so a filter reaches further the further they go.\n */\n const searchKey = gridFilter.searchWords.join(' ')\n const loadedData = enrollments.data\n const matching = useMemo(\n () =>\n filterListRows(\n loadedData.map((enrollment) => ({\n enrollment,\n status: enrollment.status,\n target: enrollment.target === 'lead' ? 'lead' : 'contact',\n contactName: enrollment.contactName,\n email: enrollment.email,\n step: outreachCurrentStepLabel(enrollment, steps),\n stopReason: outreachStopLabel(enrollment),\n })),\n ENROLLMENT_FILTER_FIELDS,\n gridFilter.clauses,\n { paths: ENROLLMENT_SEARCH_FIELDS, words: gridFilter.searchWords },\n ).map((row) => row.enrollment),\n // `searchKey` stands for the words, which are a new array each render.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [loadedData, steps, gridFilter.clauses, searchKey],\n )\n // A narrowed list starts again at its first page.\n useEffect(() => setPage(0), [gridFilter.clauses, searchKey])\n\n const act = async (\n enrollment: OutreachEnrollment,\n action: OutreachEnrollmentAction,\n why?: string,\n ) => {\n setBusy(enrollment.id)\n try {\n const answer = await api.actOnEnrollment(enrollment.id, action, why)\n const others = answer.stoppedOthers\n enqueueSnackbar(\n action === 'do_not_contact'\n ? `Marked do-not-contact${others ? `, and stopped in ${others} other ${others === 1 ? 'sequence' : 'sequences'}` : ''}.`\n : answer.changed\n ? DONE_LABELS[action]\n : 'Nothing to change.',\n { variant: 'success' },\n )\n } catch (error) {\n enqueueSnackbar((error as Error).message, {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setBusy(null)\n }\n }\n\n const choose = (\n enrollment: OutreachEnrollment,\n action: RowAction,\n ) => {\n if (action === 'curate') {\n setCurating(enrollment)\n } else if (CONFIRM[action]) {\n setDetail('')\n setConfirming({ enrollment, action })\n } else {\n void act(enrollment, action)\n }\n }\n\n if (enrollments.status === 'loading')\n return <OutreachLoading label=\"Loading enrollments…\" />\n if (enrollments.status === 'error' || enrollments.status === 'refused') {\n return (\n <OutreachLoadProblem status={enrollments.status} what=\"enrollments\" />\n )\n }\n if (!enrollments.data.length) {\n return (\n <Card variant=\"outlined\">\n <EmptyStateComponent\n label=\"Nobody is enrolled yet\"\n description=\"People you enroll show here, with where each of them is in the sequence.\"\n action={props.enrollAction}\n />\n </Card>\n )\n }\n\n const rowActions = (enrollment: OutreachEnrollment) => (\n <ListRowActions\n label={enrollment.contactName || enrollment.email}\n items={actionsFor(enrollment, steps).map((action) => ({\n key: action,\n label: ACTION_LABELS[action],\n onClick: () => choose(enrollment, action),\n destructive: action === 'stop' || action === 'do_not_contact',\n disabled: busy === enrollment.id,\n }))}\n />\n )\n const nextSend = (enrollment: OutreachEnrollment) =>\n enrollment.status === 'active' || enrollment.status === 'paused'\n ? formatOutreachTime(enrollment.nextDueAtMs, timeZone)\n : '—'\n const person = (enrollment: OutreachEnrollment) => (\n <Stack spacing={0}>\n <Stack direction=\"row\" spacing={0.75} sx={{ alignItems: 'center' }}>\n <Typography variant=\"body2\">\n {enrollment.contactName || enrollment.email}\n </Typography>\n {enrollment.target === 'lead' ? (\n <Chip size=\"small\" variant=\"outlined\" label=\"Lead\" />\n ) : null}\n </Stack>\n {enrollment.contactName ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {enrollment.email}\n </Typography>\n ) : null}\n </Stack>\n )\n\n const loaded = matching\n const pageRows = loaded.slice(page * pageSize, (page + 1) * pageSize)\n const lastLoadedPage = Math.max(0, Math.ceil(loaded.length / pageSize) - 1)\n /** Past the rows read so far, the next page reads another window first. */\n const turnTo = (next: number) => {\n if ((next + 1) * pageSize > loaded.length && enrollments.hasMore)\n enrollments.showMore()\n setPage(next)\n }\n\n return (\n <Stack spacing={1.5}>\n <ListFilterChips\n fields={ENROLLMENT_FILTER_FIELDS}\n headers={ENROLLMENT_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={ENROLLMENT_FILTER_OPTIONS}\n />\n {filtering && enrollments.hasMore ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Filtering the ${enrollments.data.length} enrollments read so far — the next page reads more.`}\n </Typography>\n ) : null}\n {narrow ? (\n <Stack\n spacing={1}\n component=\"ul\"\n sx={{ listStyle: 'none', p: 0, m: 0 }}\n aria-label=\"Enrollments\"\n >\n {pageRows.map((enrollment) => (\n <Card key={enrollment.id} variant=\"outlined\" component=\"li\">\n <CardContent>\n <Stack spacing={1}>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'flex-start' }}\n >\n <Stack sx={{ flexGrow: 1, minWidth: 0 }}>\n {person(enrollment)}\n </Stack>\n <OutreachEnrollmentStatusChip status={enrollment.status} />\n {rowActions(enrollment)}\n </Stack>\n <Stack direction=\"row\" spacing={0.75} sx={{ alignItems: 'center' }}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {outreachCurrentStepLabel(enrollment, steps)}\n </Typography>\n {outreachStepIsCurated(enrollment) ? (\n <Chip size=\"small\" variant=\"outlined\" color=\"success\" label=\"Curated\" />\n ) : null}\n </Stack>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`Next send: ${nextSend(enrollment)} · Last activity: ${formatOutreachTime(lastActivityMs(enrollment), timeZone)}`}\n </Typography>\n {trackClicks && readOutreachEngagement(enrollment.engagement).clicks ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`Clicked ${readOutreachEngagement(enrollment.engagement).clicks} ${\n readOutreachEngagement(enrollment.engagement).clicks === 1 ? 'time' : 'times'\n }`}\n </Typography>\n ) : null}\n {outreachStopLabel(enrollment) ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {outreachStopLabel(enrollment)}\n </Typography>\n ) : null}\n </Stack>\n </CardContent>\n </Card>\n ))}\n </Stack>\n ) : (\n <ListTable\n aria-label=\"Enrollments\"\n columns={listFilterGridColumns(\n columns(rowActions, trackClicks),\n ENROLLMENT_FILTER_FIELDS,\n ENROLLMENT_FILTER_OPTIONS,\n ENROLLMENT_FILTER_HEADERS,\n )}\n initialState={{ columns: { columnVisibilityModel: ENROLLMENT_HIDDEN_COLUMNS } }}\n rows={pageRows.map((enrollment) => ({\n $id: enrollment.id,\n enrollment,\n status: enrollment.status,\n target: enrollment.target === 'lead' ? 'lead' : 'contact',\n contactName: enrollment.contactName,\n email: enrollment.email,\n step: outreachCurrentStepLabel(enrollment, steps),\n nextSend: nextSend(enrollment),\n lastActivity: formatOutreachTime(\n lastActivityMs(enrollment),\n timeZone,\n ),\n stopReason: outreachStopLabel(enrollment) || '—',\n }))}\n /*\n * The panel and the search are the grid's; the table answers them\n * over the enrollments it read (AGL-3317).\n */\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n noRowsLabel=\"No enrollments match these filters\"\n hideFooter\n />\n )}\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={pageRows.length}\n count={enrollments.hasMore ? undefined : loaded.length}\n hasMore={enrollments.hasMore || page < lastLoadedPage}\n onPageChange={turnTo}\n onPageSizeChange={setPageSize}\n />\n\n <OutreachCurateStepDialog\n enrollment={curating}\n steps={steps}\n api={api}\n onClose={() => setCurating(null)}\n />\n\n <Dialog\n open={Boolean(confirming)}\n onClose={() => setConfirming(null)}\n fullWidth\n maxWidth=\"xs\"\n >\n {confirming ? (\n <>\n <DialogTitle>{CONFIRM[confirming.action]?.title}</DialogTitle>\n <DialogContent>\n <Stack spacing={2}>\n <DialogContentText>\n {CONFIRM[confirming.action]?.body}\n </DialogContentText>\n <TextField\n label=\"Why (optional)\"\n value={detail}\n onChange={(event) => setDetail(event.target.value)}\n fullWidth\n slotProps={{ htmlInput: { maxLength: 200 } }}\n />\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setConfirming(null)}>Cancel</Button>\n <Button\n color=\"error\"\n variant=\"contained\"\n onClick={() => {\n const { enrollment, action } = confirming\n setConfirming(null)\n void act(enrollment, action, detail.trim() || undefined)\n }}\n >\n {CONFIRM[confirming.action]?.label}\n </Button>\n </DialogActions>\n </>\n ) : null}\n </Dialog>\n </Stack>\n )\n}\nOutreachEnrollmentsTable.displayName = 'OutreachEnrollmentsTable'\n\nexport default OutreachEnrollmentsTable\n"],"names":["EmptyStateComponent","ListFilterChips","ListPagination","ListRowActions","ListTable","listActionsColumn","hiddenFilterVisibility","filterListRows","inMemoryListField","listFilterGridColumns","TABLE_PAGE_SIZE_DEFAULT","useListGridFilter","useSnackbar","Button","Card","CardContent","Dialog","DialogActions","DialogContent","DialogContentText","DialogTitle","Stack","TextField","Typography","useMediaQuery","useTheme","Chip","useEffect","useMemo","useState","OutreachCurateStepDialog","outreachNextEmailStepIndex","readOutreachEngagement","OUTREACH_ENROLLMENT_STATUSES","OUTREACH_TASK_KIND_LABELS","formatOutreachTime","OUTREACH_ENROLLMENT_STATUS_LABELS","OUTREACH_STOP_REASON_LABELS","OutreachEnrollmentStatusChip","OutreachLoading","OutreachLoadProblem","outreachCurrentStepLabel","enrollment","steps","status","stepIndex","length","step","kind","taskKind","outreachStopLabel","stopReason","reason","stopDetail","lastActivityMs","Math","max","lastSentAtMs","stoppedAtMs","createdAtMs","actionsFor","actions","push","ACTION_LABELS","curate","pause","resume","stop","do_not_contact","outreachStepIsCurated","Boolean","stepOverrides","String","DONE_LABELS","CONFIRM","title","body","label","ENROLLMENT_FILTER_FIELDS","ENROLLMENT_FILTER_HEADERS","target","contactName","email","ENROLLMENT_FILTER_OPTIONS","map","value","ENROLLMENT_SEARCH_FIELDS","ENROLLMENT_HIDDEN_COLUMNS","columns","rowActions","trackClicks","field","headerName","flex","minWidth","sortable","renderCell","row","spacing","sx","justifyContent","height","direction","alignItems","variant","noWrap","size","color","width","engagement","clicks","lastClickUrl","OutreachEnrollmentsTable","props","enrollments","timeZone","api","theme","narrow","breakpoints","down","enqueueSnackbar","confirming","setConfirming","detail","setDetail","busy","setBusy","curating","setCurating","page","setPage","pageSize","setPageSize","gridFilter","selectFields","filtering","clauses","searchWords","searchKey","join","loadedData","data","matching","paths","words","act","action","why","id","answer","actOnEnrollment","others","stoppedOthers","changed","error","message","allowDuplicate","choose","what","description","enrollAction","items","key","onClick","destructive","disabled","nextSend","nextDueAtMs","person","loaded","pageRows","slice","lastLoadedPage","ceil","turnTo","next","hasMore","showMore","fields","headers","onChange","setClauses","options","component","listStyle","p","m","aria-label","flexGrow","initialState","columnVisibilityModel","rows","$id","lastActivity","filterMode","filterModel","onFilterModelChange","quickFilter","noRowsLabel","hideFooter","rowCount","count","undefined","onPageChange","onPageSizeChange","onClose","open","fullWidth","maxWidth","event","slotProps","htmlInput","maxLength","trim","displayName"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import type { OutreachComplianceIssue } from '../model/compliance-settings';
2
- import type { OutreachDoNotContactDomainRequest, OutreachDoNotContactDomainResponse, OutreachDoNotContactDomainsResponse, OutreachEnrollmentAction, OutreachEnrollmentActionResponse, OutreachEnrollPersonRequest, OutreachEnrollPreviewResponse, OutreachEnrollResponse, OutreachEnrollSource, OutreachLinkDomainRequest, OutreachLinkDomainResponse, OutreachLinkDomainsResponse, OutreachPreviewRequest, OutreachPreviewResponse, OutreachRouteRefusalReason, OutreachSequenceAction, OutreachSequenceSaveResponse, OutreachSequenceStatusResponse, OutreachSettingsResponse, OutreachSettingsSaveRequest, OutreachSettingsSaveResponse } from '../model/outreach-api';
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 } 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
@@ -27,6 +27,13 @@ export interface OutreachApi {
27
27
  enroll(sequenceId: string, people: OutreachEnrollPersonRequest[]): Promise<OutreachEnrollResponse>;
28
28
  actOnEnrollment(enrollmentId: string, action: OutreachEnrollmentAction, detail?: string): Promise<OutreachEnrollmentActionResponse>;
29
29
  previewEmail(input: Omit<OutreachPreviewRequest, 'orgId'>): Promise<OutreachPreviewResponse>;
30
+ /**
31
+ * One person's own copies of the sequence's email steps, drafted by the
32
+ * workspace's AI (AGL-3324); nothing is stored until the member confirms.
33
+ */
34
+ curateDrafts(input: Omit<OutreachCurateDraftRequest, 'orgId'>): Promise<OutreachCurateDraftResponse>;
35
+ /** Stores a confirmed copy of one step on an enrollment, or clears it with `null`. */
36
+ saveCuratedStep(enrollmentId: string, stepIndex: number, override: OutreachCurateSaveRequest['override']): Promise<OutreachCurateSaveResponse>;
30
37
  /** The domains on the do-not-contact list (AGL-3244). */
31
38
  readDoNotContactDomains(): Promise<OutreachDoNotContactDomainsResponse>;
32
39
  /** Puts a domain on the list, or takes one off. */
@@ -131,6 +131,18 @@ import { OUTREACH_API_ROUTES } from "../constants/api-routes.js";
131
131
  method: 'POST',
132
132
  body: _extends({}, input)
133
133
  }),
134
+ curateDrafts: (input)=>call(OUTREACH_API_ROUTES.curateDraft, {
135
+ method: 'POST',
136
+ body: _extends({}, input)
137
+ }),
138
+ saveCuratedStep: (enrollmentId, stepIndex, override)=>call(OUTREACH_API_ROUTES.curateSave, {
139
+ method: 'POST',
140
+ body: {
141
+ enrollmentId,
142
+ stepIndex,
143
+ override
144
+ }
145
+ }),
134
146
  readDoNotContactDomains: ()=>call(OUTREACH_API_ROUTES.doNotContactDomains, {
135
147
  method: 'GET'
136
148
  }),
@@ -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 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} 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 /** 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 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","readDoNotContactDomains","doNotContactDomains","changeDoNotContactDomain","domain","readLinkDomains","linkDomains","changeLinkDomain"],"mappings":"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"}
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} 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 * 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 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","curateDrafts","curateDraft","saveCuratedStep","stepIndex","override","curateSave","readDoNotContactDomains","doNotContactDomains","changeDoNotContactDomain","domain","readLinkDomains","linkDomains","changeLinkDomain"],"mappings":"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"}
@@ -74,6 +74,14 @@ export declare const OUTREACH_API_ROUTES: {
74
74
  readonly linkDomains: "outreach/link-domains";
75
75
  /** `POST` — one email of a saved sequence, for a contact or a sample person. */
76
76
  readonly preview: "outreach/preview";
77
+ /**
78
+ * `POST` — one person's own copies of a sequence's email steps, drafted by
79
+ * the workspace's AI (AGL-3324): for a person about to be enrolled, or for
80
+ * the next step of an enrollment. Nothing is stored.
81
+ */
82
+ readonly curateDraft: "outreach/curate/draft";
83
+ /** `POST` — stores a member's confirmed copy of one step on an enrollment, or clears it. */
84
+ readonly curateSave: "outreach/curate/save";
77
85
  /** `GET` and the RFC 8058 one-click `POST` — `?t=<signed token>`. */
78
86
  readonly unsubscribe: "outreach/unsubscribe";
79
87
  /**
@@ -57,6 +57,12 @@
57
57
  * it. Setting up and removing are an owner's or admin's.
58
58
  */ linkDomains: 'outreach/link-domains',
59
59
  /** `POST` — one email of a saved sequence, for a contact or a sample person. */ preview: 'outreach/preview',
60
+ /**
61
+ * `POST` — one person's own copies of a sequence's email steps, drafted by
62
+ * the workspace's AI (AGL-3324): for a person about to be enrolled, or for
63
+ * the next step of an enrollment. Nothing is stored.
64
+ */ curateDraft: 'outreach/curate/draft',
65
+ /** `POST` — stores a member's confirmed copy of one step on an enrollment, or clears it. */ curateSave: 'outreach/curate/save',
60
66
  // The one route a RECIPIENT reaches (AGL-2981): no session, a signed
61
67
  // token instead, and registered as a recipient link so it answers whether
62
68
  // or not Outreach is released for the organization now.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/constants/api-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Outreach's console API routes, as the dispatcher keys them (AGL-2974).\n *\n * One table both halves import, because the server registers the path and the\n * client fetches it, and a route spelled twice is a route that 404s the day\n * one spelling changes. Every path sits under the `outreach` prefix that\n * `plugins.config.json` gives this plugin, which is what lets the console's\n * `/api/[...pluginApi]` dispatcher gate a request on the plugin being enabled\n * and released before the handler runs. Client-safe: constants only.\n */\nexport const OUTREACH_API_ROUTES = {\n /** `GET` — proves the server bundle loaded and registered; see `server.ts`. */\n ping: 'outreach/ping',\n // Mailboxes (AGL-2978). Every one names its org (`orgId`, query or body)\n // so the dispatcher's release gate asks about that organization.\n /** `GET ?orgId` — whether this deployment can connect a Google mailbox. */\n mailboxesAvailability: 'outreach/mailboxes/availability',\n /** `POST` — Google's consent address for a new connect. */\n mailboxesConnect: 'outreach/mailboxes/connect',\n /**\n * `GET` — Google's redirect back. It carries no bearer token: the signed\n * state names the org and the member, and the route hands the code to the\n * Mailboxes page in a fragment rather than acting on it.\n */\n mailboxesOAuthCallback: 'outreach/mailboxes/oauth/callback',\n /** `POST` — finishes a connect with the member's own session. */\n mailboxesConnectComplete: 'outreach/mailboxes/connect/complete',\n /** `POST` — send-as, display name, daily cap, sending window, timezone. */\n mailboxesSettings: 'outreach/mailboxes/settings',\n /** `POST` — pause or resume. */\n mailboxesStatus: 'outreach/mailboxes/status',\n /** `POST` — a plain-text test to the account's own address. */\n mailboxesTest: 'outreach/mailboxes/test',\n /** `POST` — revoke the grant at Google and delete the mailbox. */\n mailboxesDisconnect: 'outreach/mailboxes/disconnect',\n // Settings, sequences and enrollments (AGL-2980). Every one names its org\n // (`orgId`, query or body); the contract is `model/outreach-api.ts`.\n /** `GET ?orgId` — the compliance settings; `POST` — save them. */\n settings: 'outreach/settings',\n /** `POST` — create a sequence, or save an edit to one. */\n sequencesSave: 'outreach/sequences/save',\n /** `POST` — activate, pause or archive a sequence. */\n sequencesStatus: 'outreach/sequences/status',\n /** `POST` — delete a draft nobody was enrolled in. */\n sequencesDelete: 'outreach/sequences/delete',\n /** `POST` — who a saved view or a search would enroll, and where each stands. */\n enrollPreview: 'outreach/enroll/preview',\n /** `POST` — enroll them, every gate checked again. */\n enroll: 'outreach/enroll',\n /** `POST` — pause, resume, stop, or mark do-not-contact. */\n enrollmentsAction: 'outreach/enrollments/action',\n /**\n * `GET ?orgId` — the domains on the organization's do-not-contact list\n * (AGL-3244); `POST` — add one, or take one off.\n */\n doNotContactDomains: 'outreach/do-not-contact/domains',\n /**\n * `GET ?orgId` — each connected mailbox domain's click-tracking host\n * (AGL-3306), `links.<domain>`; `POST` — set one up, check it, or remove\n * it. Setting up and removing are an owner's or admin's.\n */\n linkDomains: 'outreach/link-domains',\n /** `POST` — one email of a saved sequence, for a contact or a sample person. */\n preview: 'outreach/preview',\n // The one route a RECIPIENT reaches (AGL-2981): no session, a signed\n // token instead, and registered as a recipient link so it answers whether\n // or not Outreach is released for the organization now.\n /** `GET` and the RFC 8058 one-click `POST` — `?t=<signed token>`. */\n unsubscribe: 'outreach/unsubscribe',\n /**\n * `GET` — a link in a tracked sequence email (AGL-3239). `?t=<signed\n * token>`; answers a redirect to the destination the token names, and\n * records the click afterwards. A recipient link like the unsubscribe\n * one: no session, and it answers whether or not Outreach is released to\n * the organization now, because a link in an email that already left must\n * not stop working when a rollout is paused.\n */\n click: 'outreach/click',\n /**\n * `GET` — the short tracking link (AGL-3297): `outreach/l/<id>`, where the\n * id names a stored `outreachLinks` document holding the destination. It\n * replaced the signed `click` link for every send since; `click` keeps\n * answering the links already in inboxes. A recipient link, for `click`'s\n * reasons.\n */\n shortLink: 'outreach/l/:linkId',\n} as const\n"],"names":["OUTREACH_API_ROUTES","ping","mailboxesAvailability","mailboxesConnect","mailboxesOAuthCallback","mailboxesConnectComplete","mailboxesSettings","mailboxesStatus","mailboxesTest","mailboxesDisconnect","settings","sequencesSave","sequencesStatus","sequencesDelete","enrollPreview","enroll","enrollmentsAction","doNotContactDomains","linkDomains","preview","unsubscribe","click","shortLink"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;CASC,GACD,OAAO,MAAMA,sBAAsB;IACjC,6EAA6E,GAC7EC,MAAM;IACN,yEAAyE;IACzE,iEAAiE;IACjE,yEAAyE,GACzEC,uBAAuB;IACvB,yDAAyD,GACzDC,kBAAkB;IAClB;;;;GAIC,GACDC,wBAAwB;IACxB,+DAA+D,GAC/DC,0BAA0B;IAC1B,yEAAyE,GACzEC,mBAAmB;IACnB,8BAA8B,GAC9BC,iBAAiB;IACjB,6DAA6D,GAC7DC,eAAe;IACf,gEAAgE,GAChEC,qBAAqB;IACrB,0EAA0E;IAC1E,qEAAqE;IACrE,gEAAgE,GAChEC,UAAU;IACV,wDAAwD,GACxDC,eAAe;IACf,oDAAoD,GACpDC,iBAAiB;IACjB,oDAAoD,GACpDC,iBAAiB;IACjB,+EAA+E,GAC/EC,eAAe;IACf,oDAAoD,GACpDC,QAAQ;IACR,0DAA0D,GAC1DC,mBAAmB;IACnB;;;GAGC,GACDC,qBAAqB;IACrB;;;;GAIC,GACDC,aAAa;IACb,8EAA8E,GAC9EC,SAAS;IACT,qEAAqE;IACrE,0EAA0E;IAC1E,wDAAwD;IACxD,mEAAmE,GACnEC,aAAa;IACb;;;;;;;GAOC,GACDC,OAAO;IACP;;;;;;GAMC,GACDC,WAAW;AACb,EAAU"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/constants/api-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Outreach's console API routes, as the dispatcher keys them (AGL-2974).\n *\n * One table both halves import, because the server registers the path and the\n * client fetches it, and a route spelled twice is a route that 404s the day\n * one spelling changes. Every path sits under the `outreach` prefix that\n * `plugins.config.json` gives this plugin, which is what lets the console's\n * `/api/[...pluginApi]` dispatcher gate a request on the plugin being enabled\n * and released before the handler runs. Client-safe: constants only.\n */\nexport const OUTREACH_API_ROUTES = {\n /** `GET` — proves the server bundle loaded and registered; see `server.ts`. */\n ping: 'outreach/ping',\n // Mailboxes (AGL-2978). Every one names its org (`orgId`, query or body)\n // so the dispatcher's release gate asks about that organization.\n /** `GET ?orgId` — whether this deployment can connect a Google mailbox. */\n mailboxesAvailability: 'outreach/mailboxes/availability',\n /** `POST` — Google's consent address for a new connect. */\n mailboxesConnect: 'outreach/mailboxes/connect',\n /**\n * `GET` — Google's redirect back. It carries no bearer token: the signed\n * state names the org and the member, and the route hands the code to the\n * Mailboxes page in a fragment rather than acting on it.\n */\n mailboxesOAuthCallback: 'outreach/mailboxes/oauth/callback',\n /** `POST` — finishes a connect with the member's own session. */\n mailboxesConnectComplete: 'outreach/mailboxes/connect/complete',\n /** `POST` — send-as, display name, daily cap, sending window, timezone. */\n mailboxesSettings: 'outreach/mailboxes/settings',\n /** `POST` — pause or resume. */\n mailboxesStatus: 'outreach/mailboxes/status',\n /** `POST` — a plain-text test to the account's own address. */\n mailboxesTest: 'outreach/mailboxes/test',\n /** `POST` — revoke the grant at Google and delete the mailbox. */\n mailboxesDisconnect: 'outreach/mailboxes/disconnect',\n // Settings, sequences and enrollments (AGL-2980). Every one names its org\n // (`orgId`, query or body); the contract is `model/outreach-api.ts`.\n /** `GET ?orgId` — the compliance settings; `POST` — save them. */\n settings: 'outreach/settings',\n /** `POST` — create a sequence, or save an edit to one. */\n sequencesSave: 'outreach/sequences/save',\n /** `POST` — activate, pause or archive a sequence. */\n sequencesStatus: 'outreach/sequences/status',\n /** `POST` — delete a draft nobody was enrolled in. */\n sequencesDelete: 'outreach/sequences/delete',\n /** `POST` — who a saved view or a search would enroll, and where each stands. */\n enrollPreview: 'outreach/enroll/preview',\n /** `POST` — enroll them, every gate checked again. */\n enroll: 'outreach/enroll',\n /** `POST` — pause, resume, stop, or mark do-not-contact. */\n enrollmentsAction: 'outreach/enrollments/action',\n /**\n * `GET ?orgId` — the domains on the organization's do-not-contact list\n * (AGL-3244); `POST` — add one, or take one off.\n */\n doNotContactDomains: 'outreach/do-not-contact/domains',\n /**\n * `GET ?orgId` — each connected mailbox domain's click-tracking host\n * (AGL-3306), `links.<domain>`; `POST` — set one up, check it, or remove\n * it. Setting up and removing are an owner's or admin's.\n */\n linkDomains: 'outreach/link-domains',\n /** `POST` — one email of a saved sequence, for a contact or a sample person. */\n preview: 'outreach/preview',\n /**\n * `POST` — one person's own copies of a sequence's email steps, drafted by\n * the workspace's AI (AGL-3324): for a person about to be enrolled, or for\n * the next step of an enrollment. Nothing is stored.\n */\n curateDraft: 'outreach/curate/draft',\n /** `POST` — stores a member's confirmed copy of one step on an enrollment, or clears it. */\n curateSave: 'outreach/curate/save',\n // The one route a RECIPIENT reaches (AGL-2981): no session, a signed\n // token instead, and registered as a recipient link so it answers whether\n // or not Outreach is released for the organization now.\n /** `GET` and the RFC 8058 one-click `POST` — `?t=<signed token>`. */\n unsubscribe: 'outreach/unsubscribe',\n /**\n * `GET` — a link in a tracked sequence email (AGL-3239). `?t=<signed\n * token>`; answers a redirect to the destination the token names, and\n * records the click afterwards. A recipient link like the unsubscribe\n * one: no session, and it answers whether or not Outreach is released to\n * the organization now, because a link in an email that already left must\n * not stop working when a rollout is paused.\n */\n click: 'outreach/click',\n /**\n * `GET` — the short tracking link (AGL-3297): `outreach/l/<id>`, where the\n * id names a stored `outreachLinks` document holding the destination. It\n * replaced the signed `click` link for every send since; `click` keeps\n * answering the links already in inboxes. A recipient link, for `click`'s\n * reasons.\n */\n shortLink: 'outreach/l/:linkId',\n} as const\n"],"names":["OUTREACH_API_ROUTES","ping","mailboxesAvailability","mailboxesConnect","mailboxesOAuthCallback","mailboxesConnectComplete","mailboxesSettings","mailboxesStatus","mailboxesTest","mailboxesDisconnect","settings","sequencesSave","sequencesStatus","sequencesDelete","enrollPreview","enroll","enrollmentsAction","doNotContactDomains","linkDomains","preview","curateDraft","curateSave","unsubscribe","click","shortLink"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;CASC,GACD,OAAO,MAAMA,sBAAsB;IACjC,6EAA6E,GAC7EC,MAAM;IACN,yEAAyE;IACzE,iEAAiE;IACjE,yEAAyE,GACzEC,uBAAuB;IACvB,yDAAyD,GACzDC,kBAAkB;IAClB;;;;GAIC,GACDC,wBAAwB;IACxB,+DAA+D,GAC/DC,0BAA0B;IAC1B,yEAAyE,GACzEC,mBAAmB;IACnB,8BAA8B,GAC9BC,iBAAiB;IACjB,6DAA6D,GAC7DC,eAAe;IACf,gEAAgE,GAChEC,qBAAqB;IACrB,0EAA0E;IAC1E,qEAAqE;IACrE,gEAAgE,GAChEC,UAAU;IACV,wDAAwD,GACxDC,eAAe;IACf,oDAAoD,GACpDC,iBAAiB;IACjB,oDAAoD,GACpDC,iBAAiB;IACjB,+EAA+E,GAC/EC,eAAe;IACf,oDAAoD,GACpDC,QAAQ;IACR,0DAA0D,GAC1DC,mBAAmB;IACnB;;;GAGC,GACDC,qBAAqB;IACrB;;;;GAIC,GACDC,aAAa;IACb,8EAA8E,GAC9EC,SAAS;IACT;;;;GAIC,GACDC,aAAa;IACb,0FAA0F,GAC1FC,YAAY;IACZ,qEAAqE;IACrE,0EAA0E;IAC1E,wDAAwD;IACxD,mEAAmE,GACnEC,aAAa;IACb;;;;;;;GAOC,GACDC,OAAO;IACP;;;;;;GAMC,GACDC,WAAW;AACb,EAAU"}
@@ -15,7 +15,7 @@
15
15
  * limitations under the License.
16
16
  */
17
17
  import { type CrmMergeContext } from '@aglyn/aglyn/app-utils/crm-email-templates';
18
- import type { OutreachEnrollment, OutreachOrgSettings, OutreachSequence } from '../model/outreach.types';
18
+ import type { OutreachEnrollment, OutreachOrgSettings, OutreachSequence, OutreachStepOverride } from '../model/outreach.types';
19
19
  /** The way out every email offers, word for word. */
20
20
  export declare const OUTREACH_OPT_OUT_LINE = "Not interested? Reply \"no\" and I won't email again.";
21
21
  /**
@@ -60,7 +60,7 @@ export interface OutreachComposeError {
60
60
  }
61
61
  export interface ComposeOutreachEmailInput {
62
62
  sequence: Pick<OutreachSequence, 'steps'>;
63
- enrollment: Pick<OutreachEnrollment, 'email' | 'stepIndex' | 'personalLine' | 'threadSubject' | 'messageIds' | 'gmailThreadId'>;
63
+ enrollment: Pick<OutreachEnrollment, 'email' | 'stepIndex' | 'personalLine' | 'threadSubject' | 'messageIds' | 'gmailThreadId'> & Partial<Pick<OutreachEnrollment, 'stepOverrides'>>;
64
64
  orgSettings: Pick<OutreachOrgSettings, 'legalName' | 'brandName' | 'postalAddress'> | null | undefined;
65
65
  /**
66
66
  * What the CRM resolver reads: the contact, the sending site's group id,
@@ -98,7 +98,17 @@ export interface OutreachComposeResult {
98
98
  error: OutreachComposeError | null;
99
99
  /** Merge fields that rendered empty, unique, in order of appearance. */
100
100
  unresolvedFields: string[];
101
+ /**
102
+ * The person's own copy of the step the email was written from
103
+ * (AGL-3324), or `null` when the step was sent as the sequence wrote it.
104
+ */
105
+ override: OutreachStepOverride | null;
101
106
  }
107
+ /**
108
+ * The enrollment's own copy of the step at `stepIndex` (AGL-3324), or
109
+ * `null`. A copy with neither a subject nor a body is no copy.
110
+ */
111
+ export declare function outreachStepOverrideFor(enrollment: Partial<Pick<OutreachEnrollment, 'stepOverrides'>> | null | undefined, stepIndex: number): OutreachStepOverride | null;
102
112
  /**
103
113
  * The footer every email ends with, or the reason it cannot be written.
104
114
  *
@@ -49,6 +49,15 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
49
49
  * `{{enrollment.personalLine}}` through the resolver's `extra` fields.
50
50
  * Fields that render empty are returned beside the message, for the
51
51
  * runtime to decide whether "Hi ," is worth sending.
52
+ *
53
+ * ## A curated step
54
+ *
55
+ * An enrollment may carry its own copy of a step (AGL-3324): a subject and
56
+ * a body drafted for that one person. The composer reads the copy in place
57
+ * of the step's subject and body — and of the template the step names —
58
+ * and then does everything it does to a step: the merge, the threading, the
59
+ * click tracking, the footer. A curated in-thread email still goes out as
60
+ * `Re:` the thread's subject, since it answers the same thread.
52
61
  *==========================================*/ import { normalizeContactEmail } from "@aglyn/aglyn/app-utils/contacts";
53
62
  import { CRM_EMAIL_SUBJECT_MAX } from "@aglyn/aglyn/app-utils/crm";
54
63
  import { resolveCrmMergeFields } from "@aglyn/aglyn/app-utils/crm-email-templates";
@@ -65,6 +74,17 @@ import { isInThreadEmailStep, OUTREACH_PERSONAL_LINE_FIELD, outreachSubjectHasRe
65
74
  */ export function outreachSolicitationStatement(senderName) {
66
75
  return `This is a sales email from ${senderName}.`;
67
76
  }
77
+ /**
78
+ * The enrollment's own copy of the step at `stepIndex` (AGL-3324), or
79
+ * `null`. A copy with neither a subject nor a body is no copy.
80
+ */ export function outreachStepOverrideFor(enrollment, stepIndex) {
81
+ var _enrollment_stepOverrides;
82
+ const override = enrollment == null ? void 0 : (_enrollment_stepOverrides = enrollment.stepOverrides) == null ? void 0 : _enrollment_stepOverrides[String(stepIndex)];
83
+ if (!override || typeof override !== 'object') return null;
84
+ const hasSubject = typeof override.subject === 'string';
85
+ const hasBody = typeof override.body === 'string';
86
+ return hasSubject || hasBody ? override : null;
87
+ }
68
88
  const oneLine = (value)=>typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '';
69
89
  /**
70
90
  * The footer every email ends with, or the reason it cannot be written.
@@ -137,7 +157,8 @@ const refuse = (code, message)=>({
137
157
  code,
138
158
  message
139
159
  },
140
- unresolvedFields: []
160
+ unresolvedFields: [],
161
+ override: null
141
162
  });
142
163
  /** The email one step sends to one person — see the module note. */ export function composeOutreachEmail(input) {
143
164
  var _ref, _input_merge, _ref1, _input_templateBody, _step_body;
@@ -146,7 +167,8 @@ const refuse = (code, message)=>({
146
167
  if (error) return {
147
168
  email: null,
148
169
  error,
149
- unresolvedFields: []
170
+ unresolvedFields: [],
171
+ override: null
150
172
  };
151
173
  const steps = (_ref = (_input_sequence = input.sequence) == null ? void 0 : _input_sequence.steps) != null ? _ref : [];
152
174
  const stepIndex = (_input_enrollment = input.enrollment) == null ? void 0 : _input_enrollment.stepIndex;
@@ -169,6 +191,9 @@ const refuse = (code, message)=>({
169
191
  for (const key of result.unresolved)if (!unresolved.includes(key)) unresolved.push(key);
170
192
  return result.text;
171
193
  };
194
+ // This person's own copy of the step (AGL-3324), read in place of the
195
+ // step's own words — and of its template — from here on.
196
+ const override = outreachStepOverrideFor(input.enrollment, stepIndex);
172
197
  const email = {
173
198
  to,
174
199
  subject: '',
@@ -188,15 +213,15 @@ const refuse = (code, message)=>({
188
213
  email.inReplyTo = messageIds[messageIds.length - 1];
189
214
  email.references = messageIds.join(' ');
190
215
  } else {
191
- var _step_subject;
192
- const subject = oneLine(render(String((_step_subject = step.subject) != null ? _step_subject : '')));
216
+ var _ref2, _ref3;
217
+ const subject = oneLine(render(String((_ref2 = (_ref3 = override == null ? void 0 : override.subject) != null ? _ref3 : step.subject) != null ? _ref2 : '')));
193
218
  if (!subject) return refuse('missing_subject', 'This email has no subject.');
194
219
  if (outreachSubjectHasReplyPrefix(subject)) {
195
220
  return refuse('subject_reply_prefix', 'This email starts a thread, so its subject may not begin with "Re:" or "Fwd:".');
196
221
  }
197
222
  email.subject = subject.slice(0, CRM_EMAIL_SUBJECT_MAX);
198
223
  }
199
- const source = step.templateId ? String((_input_templateBody = input.templateBody) != null ? _input_templateBody : '') : String((_step_body = step.body) != null ? _step_body : '');
224
+ const source = typeof (override == null ? void 0 : override.body) === 'string' ? override.body : step.templateId ? String((_input_templateBody = input.templateBody) != null ? _input_templateBody : '') : String((_step_body = step.body) != null ? _step_body : '');
200
225
  const body = render(source).replace(/\r\n?/g, '\n').split('\n').map((line)=>line.replace(/[ \t]+$/, '')).join('\n').replace(/^\n+|\n+$/g, '');
201
226
  if (!body.trim()) return refuse('missing_body', 'This email has no body.');
202
227
  /*
@@ -224,7 +249,8 @@ const refuse = (code, message)=>({
224
249
  return {
225
250
  email,
226
251
  error: null,
227
- unresolvedFields: unresolved
252
+ unresolvedFields: unresolved,
253
+ override
228
254
  };
229
255
  }
230
256