@aglyn/plugins-outreach 1.0.0-beta.204 → 1.0.0-beta.205
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -14
- package/src/lib/components/enrollment-detail.js +20 -2
- package/src/lib/components/enrollment-detail.js.map +1 -1
- package/src/lib/components/enrollments-table.d.ts +8 -1
- package/src/lib/components/enrollments-table.js +60 -10
- package/src/lib/components/enrollments-table.js.map +1 -1
- package/src/lib/components/mailbox-card.js +8 -0
- package/src/lib/components/mailbox-card.js.map +1 -1
- package/src/lib/components/next-send.d.ts +22 -0
- package/src/lib/components/next-send.js +118 -0
- package/src/lib/components/next-send.js.map +1 -0
- package/src/lib/components/sequence-detail.js +2 -0
- package/src/lib/components/sequence-detail.js.map +1 -1
- package/src/lib/engine/index.d.ts +1 -0
- package/src/lib/engine/index.js +1 -0
- package/src/lib/engine/index.js.map +1 -1
- package/src/lib/engine/next-send.d.ts +59 -0
- package/src/lib/engine/next-send.js +100 -0
- package/src/lib/engine/next-send.js.map +1 -0
|
@@ -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 ListQueryNotices, {\n listQueryRefusals,\n} from '@aglyn/shared-ui-jsx/components/list-query-notices.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, type ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport {\n listFilterGridColumns,\n type ListFilterClause,\n type ListFilterOption,\n} from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport {\n Card,\n CardContent,\n Chip,\n Link,\n Stack,\n Tooltip,\n Typography,\n useMediaQuery,\n useTheme,\n} from '@mui/material'\nimport { type MouseEvent, type ReactNode, useMemo } from 'react'\nimport {\n outreachClickCountLabel,\n outreachClickSummary,\n type OutreachClickSummary,\n} from '../model/enrollment-engagement'\nimport {\n OUTREACH_ENROLLMENT_CLICK_FIELDS,\n OUTREACH_ENROLLMENT_LIST_QUERY,\n} from '../model/enrollment-list-query'\nimport {\n OUTREACH_ENROLLMENT_STATUSES,\n OUTREACH_STOP_REASONS,\n OUTREACH_TASK_KIND_LABELS,\n type OutreachEnrollment,\n type OutreachSequenceStep,\n} from '../model/outreach.types'\nimport { useOutreachEnrollmentActions } from './enrollment-actions'\nimport { OUTREACH_CLICKED_FILTER_VALUE, OUTREACH_UNCLICKED_FILTER_VALUE } from './enrollment-filter-params'\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 { useOutreachEnrollmentList } from './use-outreach-data'\n\nexport interface OutreachEnrollmentsTableProps {\n /** The organization, whose enrollments the table asks for. */\n orgId: string\n /** The sequence whose people these are: every page is narrowed to it. */\n sequenceId: string\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 * Opens one person's page (AGL-3332): a row's click, Enter on a focused\n * row, or the name on a phone's card. The row's menu never opens it.\n */\n onOpen?: (enrollment: OutreachEnrollment) => void\n /** The person's page, for the name on a phone's card, which is a real link. */\n hrefFor?: (enrollment: OutreachEnrollment) => string\n /**\n * The clauses the table is narrowed by, when the page holds them — so the\n * Results card's \"Clicked\" and \"Links followed\" can set one (AGL-3332).\n * Omitted, the table holds its own.\n */\n clauses?: readonly ListFilterClause[]\n onClausesChange?: (clauses: ListFilterClause[]) => void\n /** Destinations the sequence's links went to, for the \"Link followed\" filter's choices. */\n linkOptions?: readonly ListFilterOption[]\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/** 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/*\n * What the enrollments grid's Filters panel offers (AGL-3317), each field on\n * the table's Firestore query (AGL-3321, `OUTREACH_ENROLLMENT_LIST_QUERY`).\n * Status, whether the person is a lead and the stop reason are picked; the\n * address is typed whole. `target` and `email` are no column of their own,\n * so they are hidden columns the panel can still reach; the search box\n * reads the person's name and address.\n *\n * A sequence that counts clicks adds two more (AGL-3332), on the same query:\n * whether the person clicked, and a destination they followed — matched\n * whole against the destinations stored on the enrollment, never by a piece\n * of its address, so \"followed /pricing\" does not also list the people who\n * followed \"/pricing/enterprise\".\n */\nconst BASE_FILTER_FIELDS = OUTREACH_ENROLLMENT_LIST_QUERY.fields.filter(\n (field) => !(OUTREACH_ENROLLMENT_CLICK_FIELDS as readonly string[]).includes(field.column),\n)\nconst ENROLLMENT_FILTER_HEADERS: Readonly<Record<string, string>> = {\n status: 'Status',\n target: 'Enrolled as',\n stopReason: 'Stop reason',\n email: 'Email',\n enrolled: 'Enrolled',\n clicked: 'Clicked',\n link: 'Link followed',\n}\nconst BASE_FILTER_OPTIONS: Record<string, readonly ListFilterOption[]> = {\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 stopReason: OUTREACH_STOP_REASONS.map((reason) => ({\n value: reason,\n label: OUTREACH_STOP_REASON_LABELS[reason],\n })),\n}\nconst CLICKED_OPTIONS: readonly ListFilterOption[] = [\n { value: OUTREACH_CLICKED_FILTER_VALUE, label: 'Yes' },\n { value: OUTREACH_UNCLICKED_FILTER_VALUE, label: 'No' },\n]\n/** Columns a reader can show from the column chooser, off until they do (AGL-3332). */\nconst CLICK_DETAIL_COLUMNS = ['linksFollowed', 'lastClick', 'scannerClicks'] as const\n\n/** The table's grammar, with the click fields when the sequence counts clicks. */\nfunction filterFieldsFor(trackClicks: boolean): readonly ListFilterField[] {\n return trackClicks ? OUTREACH_ENROLLMENT_LIST_QUERY.fields : BASE_FILTER_FIELDS\n}\n\n/** What the grid's cells and its export read off one enrollment. */\nfunction filterRow(\n enrollment: OutreachEnrollment,\n steps: readonly OutreachSequenceStep[],\n clicks: OutreachClickSummary,\n) {\n return {\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 clicked: clicks.clicks > 0 ? OUTREACH_CLICKED_FILTER_VALUE : OUTREACH_UNCLICKED_FILTER_VALUE,\n link: clicks.followed,\n }\n}\n\n/** The stop reason: its short label, with every word of it on hover and on the person's page. */\nfunction StopReasonCell(props: { enrollment: OutreachEnrollment }) {\n const { enrollment } = props\n if (!enrollment.stopReason) {\n return (\n <Typography variant=\"body2\" color=\"text.secondary\">\n —\n </Typography>\n )\n }\n const label = OUTREACH_STOP_REASON_LABELS[enrollment.stopReason] ?? ''\n const text = (\n <Typography\n variant=\"body2\"\n sx={{\n whiteSpace: 'normal',\n lineHeight: 1.3,\n ...(enrollment.stopDetail\n ? { textDecoration: 'underline dotted', textUnderlineOffset: 3, cursor: 'help' }\n : {}),\n }}\n >\n {label}\n </Typography>\n )\n return enrollment.stopDetail ? (\n <Tooltip title={outreachStopLabel(enrollment)} describeChild>\n {text}\n </Tooltip>\n ) : (\n text\n )\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.2,\n minWidth: 190,\n sortable: false,\n // What the export writes for a column that is drawn rather than read.\n valueGetter: (_value: unknown, row: { enrollment: OutreachEnrollment }) =>\n row.enrollment.contactName\n ? `${row.enrollment.contactName} <${row.enrollment.email}>`\n : row.enrollment.email,\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: 110,\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: 170, sortable: false },\n {\n field: 'lastActivity',\n headerName: 'Last activity',\n width: 170,\n sortable: false,\n },\n ...(trackClicks\n ? [\n {\n field: 'clicks',\n headerName: 'Clicks',\n description: 'A person’s clicks, and how many different links they followed. Scanners are not counted.',\n width: 110,\n sortable: false,\n renderCell: ({ row }: { row: { clicks: string } }) => (\n <Typography variant=\"body2\" color={row.clicks === '—' ? 'text.secondary' : undefined}>\n {row.clicks}\n </Typography>\n ),\n },\n {\n field: 'linksFollowed',\n headerName: 'Links followed',\n flex: 1,\n minWidth: 220,\n sortable: false,\n },\n { field: 'lastClick', headerName: 'Last click', width: 170, sortable: false },\n {\n field: 'scannerClicks',\n headerName: 'Scanner clicks',\n description: 'Clicks a security scanner made, which are not counted as the person’s.',\n width: 130,\n sortable: false,\n },\n ]\n : []),\n {\n field: 'stopReason',\n headerName: 'Stop reason',\n flex: 1,\n minWidth: 170,\n renderCell: ({ row }) => <StopReasonCell enrollment={row.enrollment} />,\n },\n listActionsColumn((row) => rowActions(row.enrollment), { width: 72 }),\n ]\n}\n\n/** A plain click on a link, which the page handles itself; any other is the browser's. */\nconst plainClick = (event: MouseEvent) =>\n !event.defaultPrevented &&\n event.button === 0 &&\n !event.metaKey &&\n !event.ctrlKey &&\n !event.shiftKey &&\n !event.altKey\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 *\n * Each row opens the person's own page (AGL-3332), which holds everything\n * else known about them: so a cell says one thing, whole, and the detail\n * behind it is a click away rather than cut off at the column's edge.\n */\nexport function OutreachEnrollmentsTable(props: OutreachEnrollmentsTableProps) {\n const { steps, timeZone, api, onOpen, hrefFor } = props\n const trackClicks = props.trackClicks === true\n const theme = useTheme()\n const narrow = useMediaQuery(theme.breakpoints.down('md'))\n const actions = useOutreachEnrollmentActions({ api, steps })\n const fields = useMemo(() => filterFieldsFor(trackClicks), [trackClicks])\n const gridFilter = useListGridFilter({\n selectFields: ['status', 'target', 'stopReason', 'clicked', 'link'],\n ...(props.clauses !== undefined\n ? { clauses: props.clauses, onChange: props.onClausesChange }\n : {}),\n })\n const filtering =\n gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word) => word.trim())\n /*\n * One page of the sequence's enrollments, newest enrolled first, with\n * every clause and the search word on the query (AGL-3321): a filter\n * reaches the enrollment on page forty as surely as the one on page one,\n * and nothing is matched over the rows that happen to be loaded. The\n * click filters (AGL-3332) are on it too, answered by the `clicked` flag\n * and the destinations the click route stores on each enrollment.\n */\n const enrollments = useOutreachEnrollmentList(props.orgId, props.sequenceId, {\n clauses: gridFilter.clauses,\n search: gridFilter.searchWords,\n })\n const loadedData = enrollments.rows\n const summaries = useMemo(\n () =>\n new Map(\n loadedData.map((enrollment) => [enrollment.id, outreachClickSummary(enrollment.engagement)]),\n ),\n [loadedData],\n )\n const clicksOf = (enrollment: OutreachEnrollment) =>\n summaries.get(enrollment.id) ?? outreachClickSummary(enrollment.engagement)\n\n /*\n * The destinations the \"Link followed\" filter offers: the sequence's own\n * rollup, and any a loaded person followed that it does not list — the\n * rollup is capped, and a filter that could not name a destination a\n * person followed would hide them from it.\n */\n const linkOptions = useMemo(() => {\n const options = new Map<string, string>()\n for (const option of props.linkOptions ?? []) options.set(option.value, option.label)\n for (const summary of summaries.values()) {\n for (const url of summary.followed) if (!options.has(url)) options.set(url, url)\n }\n return [...options].map(([value, label]) => ({ value, label }))\n }, [props.linkOptions, summaries])\n const filterOptions = useMemo(\n () =>\n trackClicks\n ? { ...BASE_FILTER_OPTIONS, clicked: CLICKED_OPTIONS, link: linkOptions }\n : BASE_FILTER_OPTIONS,\n [trackClicks, linkOptions],\n )\n const hiddenColumns = useMemo(\n () => ({\n ...hiddenFilterVisibility(fields, ['status', 'stopReason']),\n ...(trackClicks\n ? Object.fromEntries(CLICK_DETAIL_COLUMNS.map((column) => [column, false]))\n : {}),\n }),\n [fields, trackClicks],\n )\n /*\n * A person who followed this destination before each click was recorded\n * on its own, and another link after it, is known by their LAST link only\n * — so a \"Link followed\" filter says it may miss them rather than\n * implying the list is whole. Which people those are is exactly what the\n * query cannot see, so the caveat stands whenever the filter does.\n */\n const linkFilterMayMiss =\n trackClicks && gridFilter.clauses.some((clause) => clause.field === 'link')\n\n const chips = (\n <ListFilterChips\n fields={fields}\n headers={ENROLLMENT_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={filterOptions}\n />\n )\n if (enrollments.status === 'loading' && !filtering)\n return <OutreachLoading label=\"Loading enrollments…\" />\n if (enrollments.status === 'error' || enrollments.status === 'refused') {\n return (\n <Stack spacing={1.5}>\n {chips}\n <OutreachLoadProblem status={enrollments.status} what=\"enrollments\" />\n </Stack>\n )\n }\n if (\n enrollments.status === 'ready' &&\n !enrollments.rows.length &&\n !filtering &&\n enrollments.page === 0\n ) {\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={actions.menuItems(enrollment)}\n />\n )\n const nextSend = (enrollment: OutreachEnrollment) =>\n enrollment.status === 'active' || enrollment.status === 'paused'\n ? formatOutreachTime(enrollment.nextDueAtMs, timeZone)\n : '—'\n const personName = (enrollment: OutreachEnrollment) => {\n const name = enrollment.contactName || enrollment.email\n if (!hrefFor) return <Typography variant=\"body2\">{name}</Typography>\n return (\n <Link\n href={hrefFor(enrollment)}\n variant=\"body2\"\n underline=\"hover\"\n onClick={(event) => {\n if (!onOpen || !plainClick(event)) return\n event.preventDefault()\n onOpen(enrollment)\n }}\n >\n {name}\n </Link>\n )\n }\n const person = (enrollment: OutreachEnrollment) => (\n <Stack spacing={0}>\n <Stack direction=\"row\" spacing={0.75} sx={{ alignItems: 'center' }}>\n {personName(enrollment)}\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 pageRows = enrollments.rows\n\n return (\n <Stack spacing={1.5}>\n {chips}\n <ListQueryNotices\n refused={listQueryRefusals(enrollments.plan.refused, {\n fields,\n headers: ENROLLMENT_FILTER_HEADERS,\n options: filterOptions,\n })}\n notices={enrollments.plan.notices}\n />\n {linkFilterMayMiss ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n Clicks made before each one was recorded on its own kept only the person’s last link, so\n someone who followed this link and then another back then is not listed here.\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 const clicks = clicksOf(enrollment)\n return (\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 && clicks.clicks ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`Clicks: ${outreachClickCountLabel(clicks)}`}\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 })}\n </Stack>\n ) : (\n <ListTable\n aria-label=\"Enrollments\"\n columns={listFilterGridColumns(\n columns(rowActions, trackClicks),\n fields,\n filterOptions,\n ENROLLMENT_FILTER_HEADERS,\n )}\n initialState={{ columns: { columnVisibilityModel: hiddenColumns } }}\n loading={enrollments.status === 'loading'}\n rows={pageRows.map((enrollment) => {\n const clicks = clicksOf(enrollment)\n return {\n $id: enrollment.id,\n ...filterRow(enrollment, steps, clicks),\n nextSend: nextSend(enrollment),\n lastActivity: formatOutreachTime(\n lastActivityMs(enrollment),\n timeZone,\n ),\n // What the export writes; the cells draw these their own way.\n clicks: outreachClickCountLabel(clicks),\n linksFollowed: clicks.followed.join(' ') || '—',\n lastClick: clicks.clicks\n ? formatOutreachTime(enrollment.engagement?.lastClickAtMs, timeZone)\n : '—',\n scannerClicks: clicks.machineClicks ? String(clicks.machineClicks) : '—',\n stopReason: outreachStopLabel(enrollment) || '—',\n }\n })}\n onOpen={\n onOpen\n ? (_id, row: { enrollment: OutreachEnrollment }) => onOpen(row.enrollment)\n : undefined\n }\n /*\n * The panel and the search are the grid's; every clause and the\n * search word are on the table's query (AGL-3321). The grid\n * neither filters nor sorts the page it is handed: the query's\n * one order, newest enrolled first, is the table's.\n */\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n disableColumnSorting\n noRowsLabel=\"No enrollments match these filters\"\n hideFooter\n />\n )}\n <ListPagination\n page={enrollments.page}\n pageSize={enrollments.pageSize}\n rowCount={pageRows.length}\n hasMore={enrollments.hasMore}\n onPageChange={enrollments.setPage}\n onPageSizeChange={enrollments.setPageSize}\n />\n {actions.dialogs}\n </Stack>\n )\n}\nOutreachEnrollmentsTable.displayName = 'OutreachEnrollmentsTable'\n\nexport default OutreachEnrollmentsTable\n"],"names":["EmptyStateComponent","ListFilterChips","ListQueryNotices","listQueryRefusals","ListPagination","ListRowActions","ListTable","listActionsColumn","hiddenFilterVisibility","listFilterGridColumns","useListGridFilter","Card","CardContent","Chip","Link","Stack","Tooltip","Typography","useMediaQuery","useTheme","useMemo","outreachClickCountLabel","outreachClickSummary","OUTREACH_ENROLLMENT_CLICK_FIELDS","OUTREACH_ENROLLMENT_LIST_QUERY","OUTREACH_ENROLLMENT_STATUSES","OUTREACH_STOP_REASONS","OUTREACH_TASK_KIND_LABELS","useOutreachEnrollmentActions","OUTREACH_CLICKED_FILTER_VALUE","OUTREACH_UNCLICKED_FILTER_VALUE","formatOutreachTime","OUTREACH_ENROLLMENT_STATUS_LABELS","OUTREACH_STOP_REASON_LABELS","OutreachEnrollmentStatusChip","OutreachLoading","OutreachLoadProblem","useOutreachEnrollmentList","outreachCurrentStepLabel","enrollment","steps","status","stepIndex","length","step","kind","taskKind","outreachStopLabel","stopReason","reason","stopDetail","lastActivityMs","Math","max","lastSentAtMs","stoppedAtMs","createdAtMs","outreachStepIsCurated","Boolean","stepOverrides","String","BASE_FILTER_FIELDS","fields","filter","field","includes","column","ENROLLMENT_FILTER_HEADERS","target","email","enrolled","clicked","link","BASE_FILTER_OPTIONS","map","value","label","CLICKED_OPTIONS","CLICK_DETAIL_COLUMNS","filterFieldsFor","trackClicks","filterRow","clicks","contactName","followed","StopReasonCell","props","variant","color","text","sx","whiteSpace","lineHeight","textDecoration","textUnderlineOffset","cursor","title","describeChild","columns","rowActions","headerName","flex","minWidth","sortable","valueGetter","_value","row","renderCell","spacing","justifyContent","height","direction","alignItems","noWrap","size","width","description","undefined","plainClick","event","defaultPrevented","button","metaKey","ctrlKey","shiftKey","altKey","OutreachEnrollmentsTable","timeZone","api","onOpen","hrefFor","theme","narrow","breakpoints","down","actions","gridFilter","selectFields","clauses","onChange","onClausesChange","filtering","searchWords","some","word","trim","enrollments","orgId","sequenceId","search","loadedData","rows","summaries","Map","id","engagement","clicksOf","get","linkOptions","options","option","set","summary","values","url","has","filterOptions","hiddenColumns","Object","fromEntries","linkFilterMayMiss","clause","chips","headers","setClauses","what","page","action","enrollAction","items","menuItems","nextSend","nextDueAtMs","personName","name","href","underline","onClick","preventDefault","person","pageRows","refused","plan","notices","component","listStyle","p","m","aria-label","flexGrow","initialState","columnVisibilityModel","loading","$id","lastActivity","linksFollowed","join","lastClick","lastClickAtMs","scannerClicks","machineClicks","_id","filterMode","filterModel","onFilterModelChange","quickFilter","disableColumnSorting","noRowsLabel","hideFooter","pageSize","rowCount","hasMore","onPageChange","setPage","onPageSizeChange","setPageSize","dialogs","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 ListQueryNotices, {\n listQueryRefusals,\n} from '@aglyn/shared-ui-jsx/components/list-query-notices.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, type ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport {\n listFilterGridColumns,\n type ListFilterClause,\n type ListFilterOption,\n} from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport {\n Card,\n CardContent,\n Chip,\n Link,\n Stack,\n Tooltip,\n Typography,\n useMediaQuery,\n useTheme,\n} from '@mui/material'\nimport { type MouseEvent, type ReactNode, useMemo } from 'react'\nimport {\n outreachClickCountLabel,\n outreachClickSummary,\n type OutreachClickSummary,\n} from '../model/enrollment-engagement'\nimport {\n OUTREACH_ENROLLMENT_CLICK_FIELDS,\n OUTREACH_ENROLLMENT_LIST_QUERY,\n} from '../model/enrollment-list-query'\nimport {\n OUTREACH_ENROLLMENT_STATUSES,\n OUTREACH_STOP_REASONS,\n OUTREACH_TASK_KIND_LABELS,\n type OutreachEnrollment,\n type OutreachMailbox,\n type OutreachSequence,\n type OutreachSequenceStep,\n} from '../model/outreach.types'\nimport { outreachNextSendState, type OutreachNextSendState } from '../engine/next-send'\nimport { useOutreachEnrollmentActions } from './enrollment-actions'\nimport { OUTREACH_CLICKED_FILTER_VALUE, OUTREACH_UNCLICKED_FILTER_VALUE } from './enrollment-filter-params'\nimport {\n OutreachNextSend,\n outreachNextSendDetail,\n outreachNextSendLabel,\n useOutreachNowMs,\n} from './next-send'\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 { useOutreachEnrollmentList } from './use-outreach-data'\n\nexport interface OutreachEnrollmentsTableProps {\n /** The organization, whose enrollments the table asks for. */\n orgId: string\n /** The sequence whose people these are: every page is narrowed to it. */\n sequenceId: string\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 /**\n * The sequence's mailbox, whose pacing a due enrollment waits on\n * (AGL-3366): `undefined` while the mailboxes load, `null` when it has none.\n */\n mailbox?: OutreachMailbox | null\n /** The sequence, whose status and window a due enrollment waits on too. */\n sequence?: Pick<OutreachSequence, 'status' | 'steps' | 'settings'> | null\n api: OutreachApi\n /** Shown in the empty state when the sequence takes people. */\n enrollAction?: ReactNode\n /**\n * Opens one person's page (AGL-3332): a row's click, Enter on a focused\n * row, or the name on a phone's card. The row's menu never opens it.\n */\n onOpen?: (enrollment: OutreachEnrollment) => void\n /** The person's page, for the name on a phone's card, which is a real link. */\n hrefFor?: (enrollment: OutreachEnrollment) => string\n /**\n * The clauses the table is narrowed by, when the page holds them — so the\n * Results card's \"Clicked\" and \"Links followed\" can set one (AGL-3332).\n * Omitted, the table holds its own.\n */\n clauses?: readonly ListFilterClause[]\n onClausesChange?: (clauses: ListFilterClause[]) => void\n /** Destinations the sequence's links went to, for the \"Link followed\" filter's choices. */\n linkOptions?: readonly ListFilterOption[]\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/** 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/*\n * What the enrollments grid's Filters panel offers (AGL-3317), each field on\n * the table's Firestore query (AGL-3321, `OUTREACH_ENROLLMENT_LIST_QUERY`).\n * Status, whether the person is a lead and the stop reason are picked; the\n * address is typed whole. `target` and `email` are no column of their own,\n * so they are hidden columns the panel can still reach; the search box\n * reads the person's name and address.\n *\n * A sequence that counts clicks adds two more (AGL-3332), on the same query:\n * whether the person clicked, and a destination they followed — matched\n * whole against the destinations stored on the enrollment, never by a piece\n * of its address, so \"followed /pricing\" does not also list the people who\n * followed \"/pricing/enterprise\".\n */\nconst BASE_FILTER_FIELDS = OUTREACH_ENROLLMENT_LIST_QUERY.fields.filter(\n (field) => !(OUTREACH_ENROLLMENT_CLICK_FIELDS as readonly string[]).includes(field.column),\n)\nconst ENROLLMENT_FILTER_HEADERS: Readonly<Record<string, string>> = {\n status: 'Status',\n target: 'Enrolled as',\n stopReason: 'Stop reason',\n email: 'Email',\n enrolled: 'Enrolled',\n clicked: 'Clicked',\n link: 'Link followed',\n}\nconst BASE_FILTER_OPTIONS: Record<string, readonly ListFilterOption[]> = {\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 stopReason: OUTREACH_STOP_REASONS.map((reason) => ({\n value: reason,\n label: OUTREACH_STOP_REASON_LABELS[reason],\n })),\n}\nconst CLICKED_OPTIONS: readonly ListFilterOption[] = [\n { value: OUTREACH_CLICKED_FILTER_VALUE, label: 'Yes' },\n { value: OUTREACH_UNCLICKED_FILTER_VALUE, label: 'No' },\n]\n/** Columns a reader can show from the column chooser, off until they do (AGL-3332). */\nconst CLICK_DETAIL_COLUMNS = ['linksFollowed', 'lastClick', 'scannerClicks'] as const\n\n/** The table's grammar, with the click fields when the sequence counts clicks. */\nfunction filterFieldsFor(trackClicks: boolean): readonly ListFilterField[] {\n return trackClicks ? OUTREACH_ENROLLMENT_LIST_QUERY.fields : BASE_FILTER_FIELDS\n}\n\n/** What the grid's cells and its export read off one enrollment. */\nfunction filterRow(\n enrollment: OutreachEnrollment,\n steps: readonly OutreachSequenceStep[],\n clicks: OutreachClickSummary,\n) {\n return {\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 clicked: clicks.clicks > 0 ? OUTREACH_CLICKED_FILTER_VALUE : OUTREACH_UNCLICKED_FILTER_VALUE,\n link: clicks.followed,\n }\n}\n\n/** The stop reason: its short label, with every word of it on hover and on the person's page. */\nfunction StopReasonCell(props: { enrollment: OutreachEnrollment }) {\n const { enrollment } = props\n if (!enrollment.stopReason) {\n return (\n <Typography variant=\"body2\" color=\"text.secondary\">\n —\n </Typography>\n )\n }\n const label = OUTREACH_STOP_REASON_LABELS[enrollment.stopReason] ?? ''\n const text = (\n <Typography\n variant=\"body2\"\n sx={{\n whiteSpace: 'normal',\n lineHeight: 1.3,\n ...(enrollment.stopDetail\n ? { textDecoration: 'underline dotted', textUnderlineOffset: 3, cursor: 'help' }\n : {}),\n }}\n >\n {label}\n </Typography>\n )\n return enrollment.stopDetail ? (\n <Tooltip title={outreachStopLabel(enrollment)} describeChild>\n {text}\n </Tooltip>\n ) : (\n text\n )\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 timeZone: string | null,\n): NonNullable<ListTableProps['columns']> {\n return [\n {\n field: 'person',\n headerName: 'Person',\n flex: 1.2,\n minWidth: 190,\n sortable: false,\n // What the export writes for a column that is drawn rather than read.\n valueGetter: (_value: unknown, row: { enrollment: OutreachEnrollment }) =>\n row.enrollment.contactName\n ? `${row.enrollment.contactName} <${row.enrollment.email}>`\n : row.enrollment.email,\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: 110,\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 {\n field: 'nextSend',\n headerName: 'Next send',\n width: 190,\n sortable: false,\n renderCell: ({ row }: { row: { nextSendState: OutreachNextSendState } }) => (\n <Stack sx={{ justifyContent: 'center', height: '100%' }}>\n <OutreachNextSend state={row.nextSendState} timeZone={timeZone} />\n </Stack>\n ),\n },\n {\n field: 'lastActivity',\n headerName: 'Last activity',\n width: 170,\n sortable: false,\n },\n ...(trackClicks\n ? [\n {\n field: 'clicks',\n headerName: 'Clicks',\n description: 'A person’s clicks, and how many different links they followed. Scanners are not counted.',\n width: 110,\n sortable: false,\n renderCell: ({ row }: { row: { clicks: string } }) => (\n <Typography variant=\"body2\" color={row.clicks === '—' ? 'text.secondary' : undefined}>\n {row.clicks}\n </Typography>\n ),\n },\n {\n field: 'linksFollowed',\n headerName: 'Links followed',\n flex: 1,\n minWidth: 220,\n sortable: false,\n },\n { field: 'lastClick', headerName: 'Last click', width: 170, sortable: false },\n {\n field: 'scannerClicks',\n headerName: 'Scanner clicks',\n description: 'Clicks a security scanner made, which are not counted as the person’s.',\n width: 130,\n sortable: false,\n },\n ]\n : []),\n {\n field: 'stopReason',\n headerName: 'Stop reason',\n flex: 1,\n minWidth: 170,\n renderCell: ({ row }) => <StopReasonCell enrollment={row.enrollment} />,\n },\n listActionsColumn((row) => rowActions(row.enrollment), { width: 72 }),\n ]\n}\n\n/** A plain click on a link, which the page handles itself; any other is the browser's. */\nconst plainClick = (event: MouseEvent) =>\n !event.defaultPrevented &&\n event.button === 0 &&\n !event.metaKey &&\n !event.ctrlKey &&\n !event.shiftKey &&\n !event.altKey\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 *\n * Each row opens the person's own page (AGL-3332), which holds everything\n * else known about them: so a cell says one thing, whole, and the detail\n * behind it is a click away rather than cut off at the column's edge.\n */\nexport function OutreachEnrollmentsTable(props: OutreachEnrollmentsTableProps) {\n const { steps, timeZone, api, onOpen, hrefFor } = props\n const trackClicks = props.trackClicks === true\n const nowMs = useOutreachNowMs()\n const theme = useTheme()\n const narrow = useMediaQuery(theme.breakpoints.down('md'))\n const actions = useOutreachEnrollmentActions({ api, steps })\n const fields = useMemo(() => filterFieldsFor(trackClicks), [trackClicks])\n const gridFilter = useListGridFilter({\n selectFields: ['status', 'target', 'stopReason', 'clicked', 'link'],\n ...(props.clauses !== undefined\n ? { clauses: props.clauses, onChange: props.onClausesChange }\n : {}),\n })\n const filtering =\n gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word) => word.trim())\n /*\n * One page of the sequence's enrollments, newest enrolled first, with\n * every clause and the search word on the query (AGL-3321): a filter\n * reaches the enrollment on page forty as surely as the one on page one,\n * and nothing is matched over the rows that happen to be loaded. The\n * click filters (AGL-3332) are on it too, answered by the `clicked` flag\n * and the destinations the click route stores on each enrollment.\n */\n const enrollments = useOutreachEnrollmentList(props.orgId, props.sequenceId, {\n clauses: gridFilter.clauses,\n search: gridFilter.searchWords,\n })\n const loadedData = enrollments.rows\n const summaries = useMemo(\n () =>\n new Map(\n loadedData.map((enrollment) => [enrollment.id, outreachClickSummary(enrollment.engagement)]),\n ),\n [loadedData],\n )\n const clicksOf = (enrollment: OutreachEnrollment) =>\n summaries.get(enrollment.id) ?? outreachClickSummary(enrollment.engagement)\n\n /*\n * The destinations the \"Link followed\" filter offers: the sequence's own\n * rollup, and any a loaded person followed that it does not list — the\n * rollup is capped, and a filter that could not name a destination a\n * person followed would hide them from it.\n */\n const linkOptions = useMemo(() => {\n const options = new Map<string, string>()\n for (const option of props.linkOptions ?? []) options.set(option.value, option.label)\n for (const summary of summaries.values()) {\n for (const url of summary.followed) if (!options.has(url)) options.set(url, url)\n }\n return [...options].map(([value, label]) => ({ value, label }))\n }, [props.linkOptions, summaries])\n const filterOptions = useMemo(\n () =>\n trackClicks\n ? { ...BASE_FILTER_OPTIONS, clicked: CLICKED_OPTIONS, link: linkOptions }\n : BASE_FILTER_OPTIONS,\n [trackClicks, linkOptions],\n )\n const hiddenColumns = useMemo(\n () => ({\n ...hiddenFilterVisibility(fields, ['status', 'stopReason']),\n ...(trackClicks\n ? Object.fromEntries(CLICK_DETAIL_COLUMNS.map((column) => [column, false]))\n : {}),\n }),\n [fields, trackClicks],\n )\n /*\n * A person who followed this destination before each click was recorded\n * on its own, and another link after it, is known by their LAST link only\n * — so a \"Link followed\" filter says it may miss them rather than\n * implying the list is whole. Which people those are is exactly what the\n * query cannot see, so the caveat stands whenever the filter does.\n */\n const linkFilterMayMiss =\n trackClicks && gridFilter.clauses.some((clause) => clause.field === 'link')\n\n const chips = (\n <ListFilterChips\n fields={fields}\n headers={ENROLLMENT_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={filterOptions}\n />\n )\n if (enrollments.status === 'loading' && !filtering)\n return <OutreachLoading label=\"Loading enrollments…\" />\n if (enrollments.status === 'error' || enrollments.status === 'refused') {\n return (\n <Stack spacing={1.5}>\n {chips}\n <OutreachLoadProblem status={enrollments.status} what=\"enrollments\" />\n </Stack>\n )\n }\n if (\n enrollments.status === 'ready' &&\n !enrollments.rows.length &&\n !filtering &&\n enrollments.page === 0\n ) {\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={actions.menuItems(enrollment)}\n />\n )\n /*\n * A next send whose time has come says it is queued, and why, rather than\n * showing a time already past (AGL-3366): the mailbox sends its share of\n * the day each run, follow-ups first, so a first email can wait its turn\n * for an hour or more without anything having failed.\n */\n const nextSendState = (enrollment: OutreachEnrollment) =>\n outreachNextSendState({\n enrollment,\n sequence: props.sequence ?? null,\n mailbox: props.mailbox,\n nowMs,\n })\n const personName = (enrollment: OutreachEnrollment) => {\n const name = enrollment.contactName || enrollment.email\n if (!hrefFor) return <Typography variant=\"body2\">{name}</Typography>\n return (\n <Link\n href={hrefFor(enrollment)}\n variant=\"body2\"\n underline=\"hover\"\n onClick={(event) => {\n if (!onOpen || !plainClick(event)) return\n event.preventDefault()\n onOpen(enrollment)\n }}\n >\n {name}\n </Link>\n )\n }\n const person = (enrollment: OutreachEnrollment) => (\n <Stack spacing={0}>\n <Stack direction=\"row\" spacing={0.75} sx={{ alignItems: 'center' }}>\n {personName(enrollment)}\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 pageRows = enrollments.rows\n\n return (\n <Stack spacing={1.5}>\n {chips}\n <ListQueryNotices\n refused={listQueryRefusals(enrollments.plan.refused, {\n fields,\n headers: ENROLLMENT_FILTER_HEADERS,\n options: filterOptions,\n })}\n notices={enrollments.plan.notices}\n />\n {linkFilterMayMiss ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n Clicks made before each one was recorded on its own kept only the person’s last link, so\n someone who followed this link and then another back then is not listed here.\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 const clicks = clicksOf(enrollment)\n return (\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 <Stack\n direction=\"row\"\n spacing={0.5}\n sx={{ alignItems: 'baseline', flexWrap: 'wrap', color: 'text.secondary' }}\n >\n <Typography variant=\"body2\">Next send:</Typography>\n <OutreachNextSend state={nextSendState(enrollment)} timeZone={timeZone} />\n <Typography variant=\"body2\">\n {`· Last activity: ${formatOutreachTime(lastActivityMs(enrollment), timeZone)}`}\n </Typography>\n </Stack>\n {trackClicks && clicks.clicks ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`Clicks: ${outreachClickCountLabel(clicks)}`}\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 })}\n </Stack>\n ) : (\n <ListTable\n aria-label=\"Enrollments\"\n columns={listFilterGridColumns(\n columns(rowActions, trackClicks, timeZone),\n fields,\n filterOptions,\n ENROLLMENT_FILTER_HEADERS,\n )}\n initialState={{ columns: { columnVisibilityModel: hiddenColumns } }}\n loading={enrollments.status === 'loading'}\n rows={pageRows.map((enrollment) => {\n const clicks = clicksOf(enrollment)\n const next = nextSendState(enrollment)\n return {\n $id: enrollment.id,\n ...filterRow(enrollment, steps, clicks),\n nextSendState: next,\n // What the export writes: the label, and for a queued send, why.\n nextSend: [outreachNextSendLabel(next, timeZone), outreachNextSendDetail(next, timeZone)]\n .filter(Boolean)\n .join(' — '),\n lastActivity: formatOutreachTime(\n lastActivityMs(enrollment),\n timeZone,\n ),\n // What the export writes; the cells draw these their own way.\n clicks: outreachClickCountLabel(clicks),\n linksFollowed: clicks.followed.join(' ') || '—',\n lastClick: clicks.clicks\n ? formatOutreachTime(enrollment.engagement?.lastClickAtMs, timeZone)\n : '—',\n scannerClicks: clicks.machineClicks ? String(clicks.machineClicks) : '—',\n stopReason: outreachStopLabel(enrollment) || '—',\n }\n })}\n onOpen={\n onOpen\n ? (_id, row: { enrollment: OutreachEnrollment }) => onOpen(row.enrollment)\n : undefined\n }\n /*\n * The panel and the search are the grid's; every clause and the\n * search word are on the table's query (AGL-3321). The grid\n * neither filters nor sorts the page it is handed: the query's\n * one order, newest enrolled first, is the table's.\n */\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n disableColumnSorting\n noRowsLabel=\"No enrollments match these filters\"\n hideFooter\n />\n )}\n <ListPagination\n page={enrollments.page}\n pageSize={enrollments.pageSize}\n rowCount={pageRows.length}\n hasMore={enrollments.hasMore}\n onPageChange={enrollments.setPage}\n onPageSizeChange={enrollments.setPageSize}\n />\n {actions.dialogs}\n </Stack>\n )\n}\nOutreachEnrollmentsTable.displayName = 'OutreachEnrollmentsTable'\n\nexport default OutreachEnrollmentsTable\n"],"names":["EmptyStateComponent","ListFilterChips","ListQueryNotices","listQueryRefusals","ListPagination","ListRowActions","ListTable","listActionsColumn","hiddenFilterVisibility","listFilterGridColumns","useListGridFilter","Card","CardContent","Chip","Link","Stack","Tooltip","Typography","useMediaQuery","useTheme","useMemo","outreachClickCountLabel","outreachClickSummary","OUTREACH_ENROLLMENT_CLICK_FIELDS","OUTREACH_ENROLLMENT_LIST_QUERY","OUTREACH_ENROLLMENT_STATUSES","OUTREACH_STOP_REASONS","OUTREACH_TASK_KIND_LABELS","outreachNextSendState","useOutreachEnrollmentActions","OUTREACH_CLICKED_FILTER_VALUE","OUTREACH_UNCLICKED_FILTER_VALUE","OutreachNextSend","outreachNextSendDetail","outreachNextSendLabel","useOutreachNowMs","formatOutreachTime","OUTREACH_ENROLLMENT_STATUS_LABELS","OUTREACH_STOP_REASON_LABELS","OutreachEnrollmentStatusChip","OutreachLoading","OutreachLoadProblem","useOutreachEnrollmentList","outreachCurrentStepLabel","enrollment","steps","status","stepIndex","length","step","kind","taskKind","outreachStopLabel","stopReason","reason","stopDetail","lastActivityMs","Math","max","lastSentAtMs","stoppedAtMs","createdAtMs","outreachStepIsCurated","Boolean","stepOverrides","String","BASE_FILTER_FIELDS","fields","filter","field","includes","column","ENROLLMENT_FILTER_HEADERS","target","email","enrolled","clicked","link","BASE_FILTER_OPTIONS","map","value","label","CLICKED_OPTIONS","CLICK_DETAIL_COLUMNS","filterFieldsFor","trackClicks","filterRow","clicks","contactName","followed","StopReasonCell","props","variant","color","text","sx","whiteSpace","lineHeight","textDecoration","textUnderlineOffset","cursor","title","describeChild","columns","rowActions","timeZone","headerName","flex","minWidth","sortable","valueGetter","_value","row","renderCell","spacing","justifyContent","height","direction","alignItems","noWrap","size","width","state","nextSendState","description","undefined","plainClick","event","defaultPrevented","button","metaKey","ctrlKey","shiftKey","altKey","OutreachEnrollmentsTable","api","onOpen","hrefFor","nowMs","theme","narrow","breakpoints","down","actions","gridFilter","selectFields","clauses","onChange","onClausesChange","filtering","searchWords","some","word","trim","enrollments","orgId","sequenceId","search","loadedData","rows","summaries","Map","id","engagement","clicksOf","get","linkOptions","options","option","set","summary","values","url","has","filterOptions","hiddenColumns","Object","fromEntries","linkFilterMayMiss","clause","chips","headers","setClauses","what","page","action","enrollAction","items","menuItems","sequence","mailbox","personName","name","href","underline","onClick","preventDefault","person","pageRows","refused","plan","notices","component","listStyle","p","m","aria-label","flexGrow","flexWrap","initialState","columnVisibilityModel","loading","next","$id","nextSend","join","lastActivity","linksFollowed","lastClick","lastClickAtMs","scannerClicks","machineClicks","_id","filterMode","filterModel","onFilterModelChange","quickFilter","disableColumnSorting","noRowsLabel","hideFooter","pageSize","rowCount","hasMore","onPageChange","setPage","onPageSizeChange","setPageSize","dialogs","displayName"],"mappings":"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"}
|
|
@@ -23,6 +23,7 @@ import SenderReadinessPanel from "@aglyn/tenant-feature-instance/components/send
|
|
|
23
23
|
import { Alert, Autocomplete, Box, Button, Chip, FormControlLabel, FormHelperText, MenuItem, Stack, Switch, TextField, ToggleButton, ToggleButtonGroup, Typography } from "@mui/material";
|
|
24
24
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
25
25
|
import { formatMinuteOfDay, isValidTimezone, OUTREACH_DEFAULT_WINDOW, OUTREACH_MAILBOX_STATUS_LABELS, OUTREACH_MAX_DAILY_CAP, OUTREACH_RAMP_STEPS, OUTREACH_WEEKDAY_LABELS, outreachEffectiveDailyCap, outreachLocalDay, summarizeMailboxHealth, validateDailyCap, validateDisplayName, validateSendWindow } from "../mailboxes/mailbox-settings.js";
|
|
26
|
+
import { outreachPacingNote } from "./next-send.js";
|
|
26
27
|
/**
|
|
27
28
|
* What the card says about a mailbox that paused ITSELF (AGL-2981): the
|
|
28
29
|
* engine's own sentence, when it paused, and — after a complaint — the day
|
|
@@ -378,6 +379,13 @@ const draftOf = (mailbox)=>{
|
|
|
378
379
|
color: "text.secondary",
|
|
379
380
|
"aria-label": "Tests today",
|
|
380
381
|
children: `Tests today: ${testsToday}`
|
|
382
|
+
}),
|
|
383
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
384
|
+
variant: "caption",
|
|
385
|
+
color: "text.secondary",
|
|
386
|
+
component: "p",
|
|
387
|
+
"aria-label": "Pacing",
|
|
388
|
+
children: outreachPacingNote(mailbox, nowMs)
|
|
381
389
|
})
|
|
382
390
|
]
|
|
383
391
|
}),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/mailbox-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { pluginDocsHelp } from '@aglyn/aglyn'\nimport { CardDisplay, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport SenderReadinessPanel from '@aglyn/tenant-feature-instance/components/sender-readiness-panel'\nimport {\n Alert,\n Autocomplete,\n Box,\n Button,\n Chip,\n FormControlLabel,\n FormHelperText,\n MenuItem,\n Stack,\n Switch,\n TextField,\n ToggleButton,\n ToggleButtonGroup,\n Typography,\n} from '@mui/material'\nimport { useEffect, useMemo, useRef, useState } from 'react'\nimport {\n formatMinuteOfDay,\n isValidTimezone,\n OUTREACH_DEFAULT_WINDOW,\n OUTREACH_MAILBOX_STATUS_LABELS,\n OUTREACH_MAX_DAILY_CAP,\n OUTREACH_RAMP_STEPS,\n OUTREACH_WEEKDAY_LABELS,\n outreachEffectiveDailyCap,\n outreachLocalDay,\n summarizeMailboxHealth,\n validateDailyCap,\n validateDisplayName,\n validateSendWindow,\n} from '../mailboxes/mailbox-settings'\nimport type { OutreachMailboxReadinessResponse } from '../mailboxes/mailbox-api'\nimport type {\n OutreachMailbox,\n OutreachMailboxStatus,\n OutreachSendWindow,\n} from '../model/outreach.types'\nimport type { OutreachMailboxApi } from './use-outreach-mailbox-api'\n\nexport interface MailboxCardProps {\n mailbox: OutreachMailbox\n /** The viewer is the member who connected it. */\n isMine: boolean\n /** The viewer may change, pause and disconnect it: its member, or an org owner or admin. */\n canManage: boolean\n api: OutreachMailboxApi\n /** Starts a new Google connect, for a mailbox that needs reconnecting. */\n onReconnect(): void\n /** True while that connect is on its way to Google. */\n reconnecting?: boolean\n /** The clock the health window and today's limit are read on. */\n nowMs?: number\n}\n\n/**\n * What the card says about a mailbox that paused ITSELF (AGL-2981): the\n * engine's own sentence, when it paused, and — after a complaint — the day\n * resuming stops being premature. Nothing sends from it until a member\n * resumes it.\n */\nexport function autoPauseSentence(mailbox: Pick<OutreachMailbox, 'autoPause' | 'timezone'>): string {\n const pause = mailbox.autoPause\n if (!pause) return ''\n const day = (atMs: number) => {\n try {\n return new Intl.DateTimeFormat('en-US', { dateStyle: 'medium', timeZone: mailbox.timezone }).format(atMs)\n } catch {\n return new Intl.DateTimeFormat('en-US', { dateStyle: 'medium', timeZone: 'UTC' }).format(atMs)\n }\n }\n const until = typeof pause.untilMs === 'number' ? ` Wait until ${day(pause.untilMs)} before resuming it.` : ''\n return `Sequences paused this mailbox on ${day(pause.atMs)}. ${pause.message}${until} Nothing sends from it until it is resumed.`\n}\n\n/** Accessible names of the card's actions, spelled once for the specs. */\nexport const MAILBOX_ACTION_LABELS = {\n save: 'Save settings',\n pause: 'Pause',\n resume: 'Resume',\n test: 'Send a test to myself',\n testElsewhere: 'Send a test',\n testAddress: 'Test address',\n warmUp: 'Warm up gradually',\n disconnect: 'Disconnect',\n reconnect: 'Reconnect',\n recheckReadiness: 'Check DNS again',\n} as const\n\nconst WEEKDAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n\nconst STATUS_COLOR: Record<OutreachMailboxStatus, 'success' | 'default' | 'warning' | 'error'> = {\n connected: 'success',\n paused: 'default',\n reconnect_required: 'warning',\n disconnected: 'error',\n}\n\n/** Every half hour of the day, and midnight at its end. */\nconst MINUTE_OPTIONS = Array.from({ length: 49 }, (_, index) => index * 30)\n\n/** The zones this runtime knows, the mailbox's own included whatever it is. */\nfunction timezoneOptions(current: string): string[] {\n const supported = (Intl as { supportedValuesOf?: (key: string) => string[] }).supportedValuesOf?.('timeZone') ?? []\n const zones = supported.length ? supported : ['UTC']\n return zones.includes(current) ? zones : [current, ...zones]\n}\n\n/** The ramp, as the cap's helper text reads it. */\nconst RAMP_SENTENCE = `Warm-up: ${OUTREACH_RAMP_STEPS.map((step, index) =>\n `week ${step.week}${index === OUTREACH_RAMP_STEPS.length - 1 ? '+' : ''}: ${step.limit}`,\n).join(' · ')} a day, never above the cap.`\n\ninterface Draft {\n sendAs: string\n displayName: string\n dailyCap: string\n window: OutreachSendWindow\n timezone: string\n /** Whether the warm-up ramp applies — `rampStartedAtMs` as a switch. */\n warmUp: boolean\n}\n\nconst draftOf = (mailbox: OutreachMailbox): Draft => ({\n sendAs: mailbox.sendAs ?? mailbox.email,\n displayName: mailbox.displayName ?? '',\n dailyCap: String(mailbox.dailyCap ?? ''),\n window: mailbox.window ?? OUTREACH_DEFAULT_WINDOW,\n timezone: mailbox.timezone ?? 'UTC',\n warmUp: typeof mailbox.rampStartedAtMs === 'number',\n})\n\n/**\n * One connected mailbox (AGL-2978): its account and status, its last seven\n * days, its settings, and pause, test and disconnect.\n *\n * Every control is offered only where the route will allow it: the settings\n * form and pause and disconnect to the member who connected the mailbox or an\n * org owner or admin, and the test and the reconnect to its member alone,\n * because both act in that member's own Google account.\n */\nexport function MailboxCard(props: MailboxCardProps) {\n const { mailbox, isMine, canManage, api, onReconnect, reconnecting = false, nowMs = Date.now() } = props\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n const [draft, setDraft] = useState<Draft>(() => draftOf(mailbox))\n const [saving, setSaving] = useState(false)\n const [saveError, setSaveError] = useState<string | null>(null)\n const [busy, setBusy] = useState<'status' | 'test' | 'disconnect' | null>(null)\n // Where a test goes when not to the member themself (AGL-3228): a test to\n // one's own address never leaves Google and shows no authentication result.\n const [testAddress, setTestAddress] = useState('')\n\n /*\n * SPF, DKIM and DMARC for the address this mailbox sends as (AGL-3328),\n * read by the server and remembered there briefly. Re-read when the\n * send-as address changes; a failed read shows nothing rather than a\n * verdict nobody made.\n */\n const sendAsAddress = mailbox.sendAs || mailbox.email\n const [readiness, setReadiness] = useState<OutreachMailboxReadinessResponse | null>(null)\n const [readinessBusy, setReadinessBusy] = useState(false)\n useEffect(() => {\n let active = true\n api\n .readiness(mailbox.id)\n .then((answer) => {\n if (active) setReadiness(answer)\n })\n .catch(() => {\n if (active) setReadiness(null)\n })\n return () => {\n active = false\n }\n }, [api, mailbox.id, sendAsAddress])\n const recheckReadiness = async () => {\n setReadinessBusy(true)\n try {\n setReadiness(await api.readiness(mailbox.id, true))\n } catch (error) {\n enqueueSnackbar((error as Error).message, { variant: 'error', allowDuplicate: true })\n } finally {\n setReadinessBusy(false)\n }\n }\n\n // A save here, a change made from another tab, or a reconnect rewrites the\n // stored settings; the form follows them, unless the member has unsaved\n // edits of their own, which are never thrown away.\n const stored = useMemo(() => draftOf(mailbox), [mailbox])\n const storedKey = JSON.stringify(stored)\n const seededFrom = useRef(storedKey)\n const dirty = JSON.stringify(draft) !== storedKey\n useEffect(() => {\n if (seededFrom.current === storedKey) return\n const previous = seededFrom.current\n seededFrom.current = storedKey\n setDraft((current) => (JSON.stringify(current) === previous ? stored : current))\n }, [storedKey, stored])\n\n const health = summarizeMailboxHealth(mailbox.health, mailbox.timezone, nowMs)\n // Tests of a sequence step sent today (AGL-3325): counted beside the\n // sends, never among them, so the cap and the seven-day sum stay honest.\n const testsToday = Math.max(\n 0,\n Number(mailbox.health?.daily?.[outreachLocalDay(nowMs, mailbox.timezone)]?.tests) || 0,\n )\n const needsReconnect = mailbox.status === 'reconnect_required' || mailbox.status === 'disconnected'\n const capCheck = validateDailyCap(draft.dailyCap === '' ? Number.NaN : Number(draft.dailyCap))\n const windowCheck = validateSendWindow(draft.window)\n const nameCheck = validateDisplayName(draft.displayName)\n const timezoneValid = isValidTimezone(draft.timezone)\n const invalid =\n typeof capCheck !== 'number' || !('days' in windowCheck) || typeof nameCheck !== 'string' || !timezoneValid\n const todayLimit = outreachEffectiveDailyCap(\n {\n dailyCap: typeof capCheck === 'number' ? capCheck : mailbox.dailyCap,\n // A ramp switched on in the draft reads as starting today.\n rampStartedAtMs: draft.warmUp ? (mailbox.rampStartedAtMs ?? nowMs) : null,\n timezone: timezoneValid ? draft.timezone : mailbox.timezone,\n },\n nowMs,\n )\n\n const save = async () => {\n setSaving(true)\n setSaveError(null)\n try {\n await api.saveSettings({\n mailboxId: mailbox.id,\n ...(draft.sendAs !== stored.sendAs ? { sendAs: draft.sendAs } : {}),\n ...(draft.displayName !== stored.displayName ? { displayName: draft.displayName } : {}),\n ...(draft.dailyCap !== stored.dailyCap ? { dailyCap: Number(draft.dailyCap) } : {}),\n ...(JSON.stringify(draft.window) !== JSON.stringify(stored.window) ? { window: draft.window } : {}),\n ...(draft.timezone !== stored.timezone ? { timezone: draft.timezone } : {}),\n ...(draft.warmUp !== stored.warmUp ? { warmUp: draft.warmUp } : {}),\n })\n enqueueSnackbar('Mailbox settings saved', { variant: 'success' })\n } catch (error) {\n setSaveError((error as Error).message)\n } finally {\n setSaving(false)\n }\n }\n\n const togglePaused = async () => {\n const paused = mailbox.status !== 'paused'\n setBusy('status')\n try {\n await api.setPaused(mailbox.id, paused)\n enqueueSnackbar(paused ? 'Mailbox paused' : 'Mailbox resumed', { variant: 'success' })\n } catch (error) {\n enqueueSnackbar((error as Error).message, { variant: 'error', allowDuplicate: true })\n } finally {\n setBusy(null)\n }\n }\n\n const sendTest = async () => {\n setBusy('test')\n try {\n const to = testAddress.trim()\n const sent = await api.sendTest(mailbox.id, to || undefined)\n enqueueSnackbar(\n to\n ? `Test sent to ${sent.sentTo}. Its original source shows whether the sender authenticated.`\n : `Test sent to ${sent.sentTo}. Check your inbox.`,\n { variant: 'success' },\n )\n } catch (error) {\n enqueueSnackbar((error as Error).message, { variant: 'error', allowDuplicate: true })\n } finally {\n setBusy(null)\n }\n }\n\n const disconnect = async () => {\n const confirmed = await confirm({\n title: 'Disconnect this mailbox?',\n description:\n 'Disconnecting deletes the access stored for this mailbox and asks Google to revoke it. ' +\n 'Nothing is sent from it again unless it is connected again.',\n confirmationText: MAILBOX_ACTION_LABELS.disconnect,\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n setBusy('disconnect')\n try {\n const { revocation } = await api.disconnect(mailbox.id)\n if (revocation === 'failed') {\n enqueueSnackbar(\n 'Mailbox disconnected, but Google could not confirm the access was revoked. ' +\n 'Remove it from the Google Account’s third-party connections to be sure.',\n { variant: 'warning', persist: true },\n )\n } else if (revocation === 'kept-for-other-mailbox') {\n enqueueSnackbar(\n 'Mailbox disconnected. Google access stays in place for another connected mailbox on the same account.',\n { variant: 'success' },\n )\n } else {\n enqueueSnackbar('Mailbox disconnected', { variant: 'success' })\n }\n } catch (error) {\n enqueueSnackbar((error as Error).message, { variant: 'error', allowDuplicate: true })\n setBusy(null)\n }\n }\n\n const sendAsLabel = (email: string) => {\n const option = mailbox.sendAsOptions?.find((entry) => entry.email === email)\n return option?.displayName ? `${option.displayName} <${option.email}>` : email\n }\n\n return (\n <CardDisplay\n header={mailbox.email}\n subheader={isMine ? 'Your mailbox' : 'A teammate’s mailbox'}\n help={pluginDocsHelp('sequences', { anchor: '#connect-a-mailbox' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Chip\n size=\"small\"\n label={OUTREACH_MAILBOX_STATUS_LABELS[mailbox.status] ?? mailbox.status}\n color={STATUS_COLOR[mailbox.status] ?? 'default'}\n />\n ),\n }}\n >\n <Stack spacing={2}>\n {needsReconnect ? (\n <Alert\n severity=\"warning\"\n action={\n isMine ? (\n <Button color=\"inherit\" size=\"small\" disabled={reconnecting} onClick={onReconnect}>\n {MAILBOX_ACTION_LABELS.reconnect}\n </Button>\n ) : undefined\n }\n >\n {`${\n mailbox.status === 'disconnected'\n ? 'This mailbox was disconnected.'\n : 'Google stopped accepting this mailbox’s connection.'\n } ${\n isMine\n ? 'Nothing sends from it until you connect it again.'\n : 'Only the member who connected it can reconnect it.'\n }`}\n </Alert>\n ) : null}\n {mailbox.status === 'paused' && mailbox.autoPause ? (\n <Alert severity=\"error\" role=\"alert\">\n {autoPauseSentence(mailbox)}\n </Alert>\n ) : null}\n\n <Box>\n <Typography variant=\"subtitle2\">Last 7 days</Typography>\n {health.hasSent ? null : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n No sends yet\n </Typography>\n )}\n <Stack direction=\"row\" spacing={3} sx={{ mt: 0.5 }}>\n {[\n ['Sent', health.sent],\n ['Bounces', health.bounces],\n ['Replies', health.replies],\n ].map(([label, value]) => (\n <Box key={label as string} aria-label={`${label} in the last 7 days`}>\n <Typography variant=\"h6\" component=\"p\">\n {value}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {label}\n </Typography>\n </Box>\n ))}\n </Stack>\n <Typography variant=\"caption\" color=\"text.secondary\" aria-label=\"Tests today\">\n {`Tests today: ${testsToday}`}\n </Typography>\n </Box>\n\n {readiness?.readiness ? (\n <Stack spacing={1}>\n <SenderReadinessPanel readiness={readiness.readiness} />\n <Box>\n <Button size=\"small\" disabled={readinessBusy} onClick={() => void recheckReadiness()}>\n {MAILBOX_ACTION_LABELS.recheckReadiness}\n </Button>\n </Box>\n </Stack>\n ) : readiness ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Google authenticates mail from ${readiness.sendAs.slice(readiness.sendAs.lastIndexOf('@') + 1)} ` +\n 'itself, so there are no records to publish for this address.'}\n </Typography>\n ) : null}\n\n {canManage ? (\n <Stack spacing={2} component=\"form\" onSubmit={(event) => event.preventDefault()}>\n <TextField\n select\n size=\"small\"\n label=\"Send as\"\n value={draft.sendAs}\n onChange={(event) => setDraft({ ...draft, sendAs: event.target.value })}\n helperText=\"Addresses Gmail has verified for this account.\"\n >\n {(mailbox.sendAsOptions?.length ? mailbox.sendAsOptions : [{ email: mailbox.sendAs }]).map((option) => (\n <MenuItem key={option.email} value={option.email}>\n {sendAsLabel(option.email)}\n </MenuItem>\n ))}\n </TextField>\n <TextField\n size=\"small\"\n label=\"Display name\"\n value={draft.displayName}\n onChange={(event) => setDraft({ ...draft, displayName: event.target.value })}\n error={typeof nameCheck !== 'string'}\n helperText={typeof nameCheck !== 'string' ? nameCheck.message : 'The name recipients see beside the address.'}\n />\n <TextField\n size=\"small\"\n type=\"number\"\n label=\"Daily cap\"\n value={draft.dailyCap}\n onChange={(event) => setDraft({ ...draft, dailyCap: event.target.value })}\n error={typeof capCheck !== 'number'}\n slotProps={{ htmlInput: { min: 1, max: OUTREACH_MAX_DAILY_CAP, step: 1 } }}\n helperText={\n typeof capCheck !== 'number'\n ? capCheck.message\n : draft.warmUp\n ? `${RAMP_SENTENCE} Today’s limit: ${todayLimit}.`\n : `No warm-up: today’s limit is the cap, ${todayLimit}.`\n }\n />\n <FormControlLabel\n control={\n <Switch\n checked={draft.warmUp}\n onChange={(event) => setDraft({ ...draft, warmUp: event.target.checked })}\n />\n }\n label={MAILBOX_ACTION_LABELS.warmUp}\n />\n <FormHelperText sx={{ mt: -1 }}>\n {'For a mailbox that is new or has not sent much. One that has sent mail for years already has ' +\n 'its reputation and can start at its cap.'}\n </FormHelperText>\n <Box>\n <Typography variant=\"subtitle2\" gutterBottom>\n Sending window\n </Typography>\n <ToggleButtonGroup\n size=\"small\"\n aria-label=\"Sending days\"\n value={draft.window.days}\n onChange={(_event, days: number[]) =>\n setDraft({ ...draft, window: { ...draft.window, days: [...days].sort((a, b) => a - b) } })\n }\n >\n {OUTREACH_WEEKDAY_LABELS.map((label, day) => (\n <ToggleButton key={label} value={day} aria-label={WEEKDAY_NAMES[day]}>\n {label}\n </ToggleButton>\n ))}\n </ToggleButtonGroup>\n <Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 2 }}>\n <TextField\n select\n size=\"small\"\n label=\"From\"\n value={draft.window.startMinute}\n onChange={(event) =>\n setDraft({ ...draft, window: { ...draft.window, startMinute: Number(event.target.value) } })\n }\n >\n {MINUTE_OPTIONS.slice(0, -1).map((minute) => (\n <MenuItem key={minute} value={minute}>\n {formatMinuteOfDay(minute)}\n </MenuItem>\n ))}\n </TextField>\n <TextField\n select\n size=\"small\"\n label=\"Until\"\n value={draft.window.endMinute}\n onChange={(event) =>\n setDraft({ ...draft, window: { ...draft.window, endMinute: Number(event.target.value) } })\n }\n >\n {MINUTE_OPTIONS.slice(1).map((minute) => (\n <MenuItem key={minute} value={minute}>\n {formatMinuteOfDay(minute)}\n </MenuItem>\n ))}\n </TextField>\n <Autocomplete\n size=\"small\"\n disableClearable\n options={timezoneOptions(mailbox.timezone)}\n value={draft.timezone}\n onChange={(_event, timezone) => setDraft({ ...draft, timezone: timezone ?? draft.timezone })}\n sx={{ minWidth: 240 }}\n renderInput={(params) => <TextField {...params} label=\"Timezone\" />}\n />\n </Stack>\n {'days' in windowCheck ? null : <FormHelperText error>{windowCheck.message}</FormHelperText>}\n </Box>\n {saveError ? <FormHelperText error>{saveError}</FormHelperText> : null}\n <Box>\n <Button variant=\"contained\" disabled={!dirty || invalid || saving} onClick={() => void save()}>\n {MAILBOX_ACTION_LABELS.save}\n </Button>\n </Box>\n </Stack>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`Sends as ${sendAsLabel(stored.sendAs)}, at most ${stored.dailyCap} a day, ` +\n `${stored.window.days.map((day) => OUTREACH_WEEKDAY_LABELS[day]).join(', ')} ` +\n `${formatMinuteOfDay(stored.window.startMinute)}–${formatMinuteOfDay(stored.window.endMinute)} ` +\n `(${stored.timezone}).`}\n </Typography>\n )}\n\n {isMine && !needsReconnect ? (\n <TextField\n label={MAILBOX_ACTION_LABELS.testAddress}\n value={testAddress}\n onChange={(event) => setTestAddress(event.target.value)}\n disabled={busy !== null}\n size=\"small\"\n type=\"email\"\n helperText={\n 'Leave empty to send the test to yourself. A test to an outside mailbox you can ' +\n 'read is the only one whose original source shows the sender authentication results.'\n }\n fullWidth\n />\n ) : null}\n\n {canManage || isMine ? (\n <Stack direction=\"row\" spacing={1} useFlexGap sx={{ flexWrap: 'wrap' }}>\n {canManage && !needsReconnect ? (\n <Button variant=\"outlined\" disabled={busy !== null} onClick={() => void togglePaused()}>\n {mailbox.status === 'paused' ? MAILBOX_ACTION_LABELS.resume : MAILBOX_ACTION_LABELS.pause}\n </Button>\n ) : null}\n {isMine && !needsReconnect ? (\n <Button variant=\"outlined\" disabled={busy !== null} onClick={() => void sendTest()}>\n {testAddress.trim() ? MAILBOX_ACTION_LABELS.testElsewhere : MAILBOX_ACTION_LABELS.test}\n </Button>\n ) : null}\n {canManage ? (\n <Button color=\"error\" disabled={busy !== null} onClick={() => void disconnect()}>\n {MAILBOX_ACTION_LABELS.disconnect}\n </Button>\n ) : null}\n </Stack>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nMailboxCard.displayName = 'MailboxCard'\n\nexport default MailboxCard\n"],"names":["pluginDocsHelp","CardDisplay","useConfirmationContext","useSnackbar","SenderReadinessPanel","Alert","Autocomplete","Box","Button","Chip","FormControlLabel","FormHelperText","MenuItem","Stack","Switch","TextField","ToggleButton","ToggleButtonGroup","Typography","useEffect","useMemo","useRef","useState","formatMinuteOfDay","isValidTimezone","OUTREACH_DEFAULT_WINDOW","OUTREACH_MAILBOX_STATUS_LABELS","OUTREACH_MAX_DAILY_CAP","OUTREACH_RAMP_STEPS","OUTREACH_WEEKDAY_LABELS","outreachEffectiveDailyCap","outreachLocalDay","summarizeMailboxHealth","validateDailyCap","validateDisplayName","validateSendWindow","autoPauseSentence","mailbox","pause","autoPause","day","atMs","Intl","DateTimeFormat","dateStyle","timeZone","timezone","format","until","untilMs","message","MAILBOX_ACTION_LABELS","save","resume","test","testElsewhere","testAddress","warmUp","disconnect","reconnect","recheckReadiness","WEEKDAY_NAMES","STATUS_COLOR","connected","paused","reconnect_required","disconnected","MINUTE_OPTIONS","Array","from","length","_","index","timezoneOptions","current","supported","supportedValuesOf","zones","includes","RAMP_SENTENCE","map","step","week","limit","join","draftOf","sendAs","email","displayName","dailyCap","String","window","rampStartedAtMs","MailboxCard","props","isMine","canManage","api","onReconnect","reconnecting","nowMs","Date","now","enqueueSnackbar","confirm","draft","setDraft","saving","setSaving","saveError","setSaveError","busy","setBusy","setTestAddress","sendAsAddress","readiness","setReadiness","readinessBusy","setReadinessBusy","active","id","then","answer","catch","error","variant","allowDuplicate","stored","storedKey","JSON","stringify","seededFrom","dirty","previous","health","testsToday","Math","max","Number","daily","tests","needsReconnect","status","capCheck","NaN","windowCheck","nameCheck","timezoneValid","invalid","todayLimit","saveSettings","mailboxId","togglePaused","setPaused","sendTest","to","trim","sent","undefined","sentTo","confirmed","title","description","confirmationText","confirmationButtonProps","color","revocation","persist","sendAsLabel","option","sendAsOptions","find","entry","header","subheader","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","size","label","spacing","severity","disabled","onClick","role","hasSent","direction","sx","mt","bounces","replies","value","aria-label","component","slice","lastIndexOf","onSubmit","event","preventDefault","select","onChange","target","helperText","type","slotProps","htmlInput","min","control","checked","gutterBottom","days","_event","sort","a","b","xs","sm","startMinute","minute","endMinute","disableClearable","options","minWidth","renderInput","params","fullWidth","useFlexGap","flexWrap"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/mailbox-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { pluginDocsHelp } from '@aglyn/aglyn'\nimport { CardDisplay, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport SenderReadinessPanel from '@aglyn/tenant-feature-instance/components/sender-readiness-panel'\nimport {\n Alert,\n Autocomplete,\n Box,\n Button,\n Chip,\n FormControlLabel,\n FormHelperText,\n MenuItem,\n Stack,\n Switch,\n TextField,\n ToggleButton,\n ToggleButtonGroup,\n Typography,\n} from '@mui/material'\nimport { useEffect, useMemo, useRef, useState } from 'react'\nimport {\n formatMinuteOfDay,\n isValidTimezone,\n OUTREACH_DEFAULT_WINDOW,\n OUTREACH_MAILBOX_STATUS_LABELS,\n OUTREACH_MAX_DAILY_CAP,\n OUTREACH_RAMP_STEPS,\n OUTREACH_WEEKDAY_LABELS,\n outreachEffectiveDailyCap,\n outreachLocalDay,\n summarizeMailboxHealth,\n validateDailyCap,\n validateDisplayName,\n validateSendWindow,\n} from '../mailboxes/mailbox-settings'\nimport type { OutreachMailboxReadinessResponse } from '../mailboxes/mailbox-api'\nimport type {\n OutreachMailbox,\n OutreachMailboxStatus,\n OutreachSendWindow,\n} from '../model/outreach.types'\nimport { outreachPacingNote } from './next-send'\nimport type { OutreachMailboxApi } from './use-outreach-mailbox-api'\n\nexport interface MailboxCardProps {\n mailbox: OutreachMailbox\n /** The viewer is the member who connected it. */\n isMine: boolean\n /** The viewer may change, pause and disconnect it: its member, or an org owner or admin. */\n canManage: boolean\n api: OutreachMailboxApi\n /** Starts a new Google connect, for a mailbox that needs reconnecting. */\n onReconnect(): void\n /** True while that connect is on its way to Google. */\n reconnecting?: boolean\n /** The clock the health window and today's limit are read on. */\n nowMs?: number\n}\n\n/**\n * What the card says about a mailbox that paused ITSELF (AGL-2981): the\n * engine's own sentence, when it paused, and — after a complaint — the day\n * resuming stops being premature. Nothing sends from it until a member\n * resumes it.\n */\nexport function autoPauseSentence(mailbox: Pick<OutreachMailbox, 'autoPause' | 'timezone'>): string {\n const pause = mailbox.autoPause\n if (!pause) return ''\n const day = (atMs: number) => {\n try {\n return new Intl.DateTimeFormat('en-US', { dateStyle: 'medium', timeZone: mailbox.timezone }).format(atMs)\n } catch {\n return new Intl.DateTimeFormat('en-US', { dateStyle: 'medium', timeZone: 'UTC' }).format(atMs)\n }\n }\n const until = typeof pause.untilMs === 'number' ? ` Wait until ${day(pause.untilMs)} before resuming it.` : ''\n return `Sequences paused this mailbox on ${day(pause.atMs)}. ${pause.message}${until} Nothing sends from it until it is resumed.`\n}\n\n/** Accessible names of the card's actions, spelled once for the specs. */\nexport const MAILBOX_ACTION_LABELS = {\n save: 'Save settings',\n pause: 'Pause',\n resume: 'Resume',\n test: 'Send a test to myself',\n testElsewhere: 'Send a test',\n testAddress: 'Test address',\n warmUp: 'Warm up gradually',\n disconnect: 'Disconnect',\n reconnect: 'Reconnect',\n recheckReadiness: 'Check DNS again',\n} as const\n\nconst WEEKDAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n\nconst STATUS_COLOR: Record<OutreachMailboxStatus, 'success' | 'default' | 'warning' | 'error'> = {\n connected: 'success',\n paused: 'default',\n reconnect_required: 'warning',\n disconnected: 'error',\n}\n\n/** Every half hour of the day, and midnight at its end. */\nconst MINUTE_OPTIONS = Array.from({ length: 49 }, (_, index) => index * 30)\n\n/** The zones this runtime knows, the mailbox's own included whatever it is. */\nfunction timezoneOptions(current: string): string[] {\n const supported = (Intl as { supportedValuesOf?: (key: string) => string[] }).supportedValuesOf?.('timeZone') ?? []\n const zones = supported.length ? supported : ['UTC']\n return zones.includes(current) ? zones : [current, ...zones]\n}\n\n/** The ramp, as the cap's helper text reads it. */\nconst RAMP_SENTENCE = `Warm-up: ${OUTREACH_RAMP_STEPS.map((step, index) =>\n `week ${step.week}${index === OUTREACH_RAMP_STEPS.length - 1 ? '+' : ''}: ${step.limit}`,\n).join(' · ')} a day, never above the cap.`\n\ninterface Draft {\n sendAs: string\n displayName: string\n dailyCap: string\n window: OutreachSendWindow\n timezone: string\n /** Whether the warm-up ramp applies — `rampStartedAtMs` as a switch. */\n warmUp: boolean\n}\n\nconst draftOf = (mailbox: OutreachMailbox): Draft => ({\n sendAs: mailbox.sendAs ?? mailbox.email,\n displayName: mailbox.displayName ?? '',\n dailyCap: String(mailbox.dailyCap ?? ''),\n window: mailbox.window ?? OUTREACH_DEFAULT_WINDOW,\n timezone: mailbox.timezone ?? 'UTC',\n warmUp: typeof mailbox.rampStartedAtMs === 'number',\n})\n\n/**\n * One connected mailbox (AGL-2978): its account and status, its last seven\n * days, its settings, and pause, test and disconnect.\n *\n * Every control is offered only where the route will allow it: the settings\n * form and pause and disconnect to the member who connected the mailbox or an\n * org owner or admin, and the test and the reconnect to its member alone,\n * because both act in that member's own Google account.\n */\nexport function MailboxCard(props: MailboxCardProps) {\n const { mailbox, isMine, canManage, api, onReconnect, reconnecting = false, nowMs = Date.now() } = props\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n const [draft, setDraft] = useState<Draft>(() => draftOf(mailbox))\n const [saving, setSaving] = useState(false)\n const [saveError, setSaveError] = useState<string | null>(null)\n const [busy, setBusy] = useState<'status' | 'test' | 'disconnect' | null>(null)\n // Where a test goes when not to the member themself (AGL-3228): a test to\n // one's own address never leaves Google and shows no authentication result.\n const [testAddress, setTestAddress] = useState('')\n\n /*\n * SPF, DKIM and DMARC for the address this mailbox sends as (AGL-3328),\n * read by the server and remembered there briefly. Re-read when the\n * send-as address changes; a failed read shows nothing rather than a\n * verdict nobody made.\n */\n const sendAsAddress = mailbox.sendAs || mailbox.email\n const [readiness, setReadiness] = useState<OutreachMailboxReadinessResponse | null>(null)\n const [readinessBusy, setReadinessBusy] = useState(false)\n useEffect(() => {\n let active = true\n api\n .readiness(mailbox.id)\n .then((answer) => {\n if (active) setReadiness(answer)\n })\n .catch(() => {\n if (active) setReadiness(null)\n })\n return () => {\n active = false\n }\n }, [api, mailbox.id, sendAsAddress])\n const recheckReadiness = async () => {\n setReadinessBusy(true)\n try {\n setReadiness(await api.readiness(mailbox.id, true))\n } catch (error) {\n enqueueSnackbar((error as Error).message, { variant: 'error', allowDuplicate: true })\n } finally {\n setReadinessBusy(false)\n }\n }\n\n // A save here, a change made from another tab, or a reconnect rewrites the\n // stored settings; the form follows them, unless the member has unsaved\n // edits of their own, which are never thrown away.\n const stored = useMemo(() => draftOf(mailbox), [mailbox])\n const storedKey = JSON.stringify(stored)\n const seededFrom = useRef(storedKey)\n const dirty = JSON.stringify(draft) !== storedKey\n useEffect(() => {\n if (seededFrom.current === storedKey) return\n const previous = seededFrom.current\n seededFrom.current = storedKey\n setDraft((current) => (JSON.stringify(current) === previous ? stored : current))\n }, [storedKey, stored])\n\n const health = summarizeMailboxHealth(mailbox.health, mailbox.timezone, nowMs)\n // Tests of a sequence step sent today (AGL-3325): counted beside the\n // sends, never among them, so the cap and the seven-day sum stay honest.\n const testsToday = Math.max(\n 0,\n Number(mailbox.health?.daily?.[outreachLocalDay(nowMs, mailbox.timezone)]?.tests) || 0,\n )\n const needsReconnect = mailbox.status === 'reconnect_required' || mailbox.status === 'disconnected'\n const capCheck = validateDailyCap(draft.dailyCap === '' ? Number.NaN : Number(draft.dailyCap))\n const windowCheck = validateSendWindow(draft.window)\n const nameCheck = validateDisplayName(draft.displayName)\n const timezoneValid = isValidTimezone(draft.timezone)\n const invalid =\n typeof capCheck !== 'number' || !('days' in windowCheck) || typeof nameCheck !== 'string' || !timezoneValid\n const todayLimit = outreachEffectiveDailyCap(\n {\n dailyCap: typeof capCheck === 'number' ? capCheck : mailbox.dailyCap,\n // A ramp switched on in the draft reads as starting today.\n rampStartedAtMs: draft.warmUp ? (mailbox.rampStartedAtMs ?? nowMs) : null,\n timezone: timezoneValid ? draft.timezone : mailbox.timezone,\n },\n nowMs,\n )\n\n const save = async () => {\n setSaving(true)\n setSaveError(null)\n try {\n await api.saveSettings({\n mailboxId: mailbox.id,\n ...(draft.sendAs !== stored.sendAs ? { sendAs: draft.sendAs } : {}),\n ...(draft.displayName !== stored.displayName ? { displayName: draft.displayName } : {}),\n ...(draft.dailyCap !== stored.dailyCap ? { dailyCap: Number(draft.dailyCap) } : {}),\n ...(JSON.stringify(draft.window) !== JSON.stringify(stored.window) ? { window: draft.window } : {}),\n ...(draft.timezone !== stored.timezone ? { timezone: draft.timezone } : {}),\n ...(draft.warmUp !== stored.warmUp ? { warmUp: draft.warmUp } : {}),\n })\n enqueueSnackbar('Mailbox settings saved', { variant: 'success' })\n } catch (error) {\n setSaveError((error as Error).message)\n } finally {\n setSaving(false)\n }\n }\n\n const togglePaused = async () => {\n const paused = mailbox.status !== 'paused'\n setBusy('status')\n try {\n await api.setPaused(mailbox.id, paused)\n enqueueSnackbar(paused ? 'Mailbox paused' : 'Mailbox resumed', { variant: 'success' })\n } catch (error) {\n enqueueSnackbar((error as Error).message, { variant: 'error', allowDuplicate: true })\n } finally {\n setBusy(null)\n }\n }\n\n const sendTest = async () => {\n setBusy('test')\n try {\n const to = testAddress.trim()\n const sent = await api.sendTest(mailbox.id, to || undefined)\n enqueueSnackbar(\n to\n ? `Test sent to ${sent.sentTo}. Its original source shows whether the sender authenticated.`\n : `Test sent to ${sent.sentTo}. Check your inbox.`,\n { variant: 'success' },\n )\n } catch (error) {\n enqueueSnackbar((error as Error).message, { variant: 'error', allowDuplicate: true })\n } finally {\n setBusy(null)\n }\n }\n\n const disconnect = async () => {\n const confirmed = await confirm({\n title: 'Disconnect this mailbox?',\n description:\n 'Disconnecting deletes the access stored for this mailbox and asks Google to revoke it. ' +\n 'Nothing is sent from it again unless it is connected again.',\n confirmationText: MAILBOX_ACTION_LABELS.disconnect,\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n setBusy('disconnect')\n try {\n const { revocation } = await api.disconnect(mailbox.id)\n if (revocation === 'failed') {\n enqueueSnackbar(\n 'Mailbox disconnected, but Google could not confirm the access was revoked. ' +\n 'Remove it from the Google Account’s third-party connections to be sure.',\n { variant: 'warning', persist: true },\n )\n } else if (revocation === 'kept-for-other-mailbox') {\n enqueueSnackbar(\n 'Mailbox disconnected. Google access stays in place for another connected mailbox on the same account.',\n { variant: 'success' },\n )\n } else {\n enqueueSnackbar('Mailbox disconnected', { variant: 'success' })\n }\n } catch (error) {\n enqueueSnackbar((error as Error).message, { variant: 'error', allowDuplicate: true })\n setBusy(null)\n }\n }\n\n const sendAsLabel = (email: string) => {\n const option = mailbox.sendAsOptions?.find((entry) => entry.email === email)\n return option?.displayName ? `${option.displayName} <${option.email}>` : email\n }\n\n return (\n <CardDisplay\n header={mailbox.email}\n subheader={isMine ? 'Your mailbox' : 'A teammate’s mailbox'}\n help={pluginDocsHelp('sequences', { anchor: '#connect-a-mailbox' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Chip\n size=\"small\"\n label={OUTREACH_MAILBOX_STATUS_LABELS[mailbox.status] ?? mailbox.status}\n color={STATUS_COLOR[mailbox.status] ?? 'default'}\n />\n ),\n }}\n >\n <Stack spacing={2}>\n {needsReconnect ? (\n <Alert\n severity=\"warning\"\n action={\n isMine ? (\n <Button color=\"inherit\" size=\"small\" disabled={reconnecting} onClick={onReconnect}>\n {MAILBOX_ACTION_LABELS.reconnect}\n </Button>\n ) : undefined\n }\n >\n {`${\n mailbox.status === 'disconnected'\n ? 'This mailbox was disconnected.'\n : 'Google stopped accepting this mailbox’s connection.'\n } ${\n isMine\n ? 'Nothing sends from it until you connect it again.'\n : 'Only the member who connected it can reconnect it.'\n }`}\n </Alert>\n ) : null}\n {mailbox.status === 'paused' && mailbox.autoPause ? (\n <Alert severity=\"error\" role=\"alert\">\n {autoPauseSentence(mailbox)}\n </Alert>\n ) : null}\n\n <Box>\n <Typography variant=\"subtitle2\">Last 7 days</Typography>\n {health.hasSent ? null : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n No sends yet\n </Typography>\n )}\n <Stack direction=\"row\" spacing={3} sx={{ mt: 0.5 }}>\n {[\n ['Sent', health.sent],\n ['Bounces', health.bounces],\n ['Replies', health.replies],\n ].map(([label, value]) => (\n <Box key={label as string} aria-label={`${label} in the last 7 days`}>\n <Typography variant=\"h6\" component=\"p\">\n {value}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {label}\n </Typography>\n </Box>\n ))}\n </Stack>\n <Typography variant=\"caption\" color=\"text.secondary\" aria-label=\"Tests today\">\n {`Tests today: ${testsToday}`}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\" component=\"p\" aria-label=\"Pacing\">\n {outreachPacingNote(mailbox, nowMs)}\n </Typography>\n </Box>\n\n {readiness?.readiness ? (\n <Stack spacing={1}>\n <SenderReadinessPanel readiness={readiness.readiness} />\n <Box>\n <Button size=\"small\" disabled={readinessBusy} onClick={() => void recheckReadiness()}>\n {MAILBOX_ACTION_LABELS.recheckReadiness}\n </Button>\n </Box>\n </Stack>\n ) : readiness ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Google authenticates mail from ${readiness.sendAs.slice(readiness.sendAs.lastIndexOf('@') + 1)} ` +\n 'itself, so there are no records to publish for this address.'}\n </Typography>\n ) : null}\n\n {canManage ? (\n <Stack spacing={2} component=\"form\" onSubmit={(event) => event.preventDefault()}>\n <TextField\n select\n size=\"small\"\n label=\"Send as\"\n value={draft.sendAs}\n onChange={(event) => setDraft({ ...draft, sendAs: event.target.value })}\n helperText=\"Addresses Gmail has verified for this account.\"\n >\n {(mailbox.sendAsOptions?.length ? mailbox.sendAsOptions : [{ email: mailbox.sendAs }]).map((option) => (\n <MenuItem key={option.email} value={option.email}>\n {sendAsLabel(option.email)}\n </MenuItem>\n ))}\n </TextField>\n <TextField\n size=\"small\"\n label=\"Display name\"\n value={draft.displayName}\n onChange={(event) => setDraft({ ...draft, displayName: event.target.value })}\n error={typeof nameCheck !== 'string'}\n helperText={typeof nameCheck !== 'string' ? nameCheck.message : 'The name recipients see beside the address.'}\n />\n <TextField\n size=\"small\"\n type=\"number\"\n label=\"Daily cap\"\n value={draft.dailyCap}\n onChange={(event) => setDraft({ ...draft, dailyCap: event.target.value })}\n error={typeof capCheck !== 'number'}\n slotProps={{ htmlInput: { min: 1, max: OUTREACH_MAX_DAILY_CAP, step: 1 } }}\n helperText={\n typeof capCheck !== 'number'\n ? capCheck.message\n : draft.warmUp\n ? `${RAMP_SENTENCE} Today’s limit: ${todayLimit}.`\n : `No warm-up: today’s limit is the cap, ${todayLimit}.`\n }\n />\n <FormControlLabel\n control={\n <Switch\n checked={draft.warmUp}\n onChange={(event) => setDraft({ ...draft, warmUp: event.target.checked })}\n />\n }\n label={MAILBOX_ACTION_LABELS.warmUp}\n />\n <FormHelperText sx={{ mt: -1 }}>\n {'For a mailbox that is new or has not sent much. One that has sent mail for years already has ' +\n 'its reputation and can start at its cap.'}\n </FormHelperText>\n <Box>\n <Typography variant=\"subtitle2\" gutterBottom>\n Sending window\n </Typography>\n <ToggleButtonGroup\n size=\"small\"\n aria-label=\"Sending days\"\n value={draft.window.days}\n onChange={(_event, days: number[]) =>\n setDraft({ ...draft, window: { ...draft.window, days: [...days].sort((a, b) => a - b) } })\n }\n >\n {OUTREACH_WEEKDAY_LABELS.map((label, day) => (\n <ToggleButton key={label} value={day} aria-label={WEEKDAY_NAMES[day]}>\n {label}\n </ToggleButton>\n ))}\n </ToggleButtonGroup>\n <Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 2 }}>\n <TextField\n select\n size=\"small\"\n label=\"From\"\n value={draft.window.startMinute}\n onChange={(event) =>\n setDraft({ ...draft, window: { ...draft.window, startMinute: Number(event.target.value) } })\n }\n >\n {MINUTE_OPTIONS.slice(0, -1).map((minute) => (\n <MenuItem key={minute} value={minute}>\n {formatMinuteOfDay(minute)}\n </MenuItem>\n ))}\n </TextField>\n <TextField\n select\n size=\"small\"\n label=\"Until\"\n value={draft.window.endMinute}\n onChange={(event) =>\n setDraft({ ...draft, window: { ...draft.window, endMinute: Number(event.target.value) } })\n }\n >\n {MINUTE_OPTIONS.slice(1).map((minute) => (\n <MenuItem key={minute} value={minute}>\n {formatMinuteOfDay(minute)}\n </MenuItem>\n ))}\n </TextField>\n <Autocomplete\n size=\"small\"\n disableClearable\n options={timezoneOptions(mailbox.timezone)}\n value={draft.timezone}\n onChange={(_event, timezone) => setDraft({ ...draft, timezone: timezone ?? draft.timezone })}\n sx={{ minWidth: 240 }}\n renderInput={(params) => <TextField {...params} label=\"Timezone\" />}\n />\n </Stack>\n {'days' in windowCheck ? null : <FormHelperText error>{windowCheck.message}</FormHelperText>}\n </Box>\n {saveError ? <FormHelperText error>{saveError}</FormHelperText> : null}\n <Box>\n <Button variant=\"contained\" disabled={!dirty || invalid || saving} onClick={() => void save()}>\n {MAILBOX_ACTION_LABELS.save}\n </Button>\n </Box>\n </Stack>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`Sends as ${sendAsLabel(stored.sendAs)}, at most ${stored.dailyCap} a day, ` +\n `${stored.window.days.map((day) => OUTREACH_WEEKDAY_LABELS[day]).join(', ')} ` +\n `${formatMinuteOfDay(stored.window.startMinute)}–${formatMinuteOfDay(stored.window.endMinute)} ` +\n `(${stored.timezone}).`}\n </Typography>\n )}\n\n {isMine && !needsReconnect ? (\n <TextField\n label={MAILBOX_ACTION_LABELS.testAddress}\n value={testAddress}\n onChange={(event) => setTestAddress(event.target.value)}\n disabled={busy !== null}\n size=\"small\"\n type=\"email\"\n helperText={\n 'Leave empty to send the test to yourself. A test to an outside mailbox you can ' +\n 'read is the only one whose original source shows the sender authentication results.'\n }\n fullWidth\n />\n ) : null}\n\n {canManage || isMine ? (\n <Stack direction=\"row\" spacing={1} useFlexGap sx={{ flexWrap: 'wrap' }}>\n {canManage && !needsReconnect ? (\n <Button variant=\"outlined\" disabled={busy !== null} onClick={() => void togglePaused()}>\n {mailbox.status === 'paused' ? MAILBOX_ACTION_LABELS.resume : MAILBOX_ACTION_LABELS.pause}\n </Button>\n ) : null}\n {isMine && !needsReconnect ? (\n <Button variant=\"outlined\" disabled={busy !== null} onClick={() => void sendTest()}>\n {testAddress.trim() ? MAILBOX_ACTION_LABELS.testElsewhere : MAILBOX_ACTION_LABELS.test}\n </Button>\n ) : null}\n {canManage ? (\n <Button color=\"error\" disabled={busy !== null} onClick={() => void disconnect()}>\n {MAILBOX_ACTION_LABELS.disconnect}\n </Button>\n ) : null}\n </Stack>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nMailboxCard.displayName = 'MailboxCard'\n\nexport default MailboxCard\n"],"names":["pluginDocsHelp","CardDisplay","useConfirmationContext","useSnackbar","SenderReadinessPanel","Alert","Autocomplete","Box","Button","Chip","FormControlLabel","FormHelperText","MenuItem","Stack","Switch","TextField","ToggleButton","ToggleButtonGroup","Typography","useEffect","useMemo","useRef","useState","formatMinuteOfDay","isValidTimezone","OUTREACH_DEFAULT_WINDOW","OUTREACH_MAILBOX_STATUS_LABELS","OUTREACH_MAX_DAILY_CAP","OUTREACH_RAMP_STEPS","OUTREACH_WEEKDAY_LABELS","outreachEffectiveDailyCap","outreachLocalDay","summarizeMailboxHealth","validateDailyCap","validateDisplayName","validateSendWindow","outreachPacingNote","autoPauseSentence","mailbox","pause","autoPause","day","atMs","Intl","DateTimeFormat","dateStyle","timeZone","timezone","format","until","untilMs","message","MAILBOX_ACTION_LABELS","save","resume","test","testElsewhere","testAddress","warmUp","disconnect","reconnect","recheckReadiness","WEEKDAY_NAMES","STATUS_COLOR","connected","paused","reconnect_required","disconnected","MINUTE_OPTIONS","Array","from","length","_","index","timezoneOptions","current","supported","supportedValuesOf","zones","includes","RAMP_SENTENCE","map","step","week","limit","join","draftOf","sendAs","email","displayName","dailyCap","String","window","rampStartedAtMs","MailboxCard","props","isMine","canManage","api","onReconnect","reconnecting","nowMs","Date","now","enqueueSnackbar","confirm","draft","setDraft","saving","setSaving","saveError","setSaveError","busy","setBusy","setTestAddress","sendAsAddress","readiness","setReadiness","readinessBusy","setReadinessBusy","active","id","then","answer","catch","error","variant","allowDuplicate","stored","storedKey","JSON","stringify","seededFrom","dirty","previous","health","testsToday","Math","max","Number","daily","tests","needsReconnect","status","capCheck","NaN","windowCheck","nameCheck","timezoneValid","invalid","todayLimit","saveSettings","mailboxId","togglePaused","setPaused","sendTest","to","trim","sent","undefined","sentTo","confirmed","title","description","confirmationText","confirmationButtonProps","color","revocation","persist","sendAsLabel","option","sendAsOptions","find","entry","header","subheader","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","size","label","spacing","severity","disabled","onClick","role","hasSent","direction","sx","mt","bounces","replies","value","aria-label","component","slice","lastIndexOf","onSubmit","event","preventDefault","select","onChange","target","helperText","type","slotProps","htmlInput","min","control","checked","gutterBottom","days","_event","sort","a","b","xs","sm","startMinute","minute","endMinute","disableClearable","options","minWidth","renderInput","params","fullWidth","useFlexGap","flexWrap"],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { OutreachNextSendState } from '../engine/next-send';
|
|
2
|
+
import type { OutreachMailbox } from '../model/outreach.types';
|
|
3
|
+
/**
|
|
4
|
+
* The clock next sends are judged against, moved on every minute so a row
|
|
5
|
+
* turns from its time to "Queued" while the page stays open.
|
|
6
|
+
*/
|
|
7
|
+
export declare function useOutreachNowMs(everyMs?: number): number;
|
|
8
|
+
/** What the cell, the chip and the export say. */
|
|
9
|
+
export declare function outreachNextSendLabel(state: OutreachNextSendState, timeZone: string | null): string;
|
|
10
|
+
/** The whole explanation of a queued send; `''` for one that is not queued. */
|
|
11
|
+
export declare function outreachNextSendDetail(state: OutreachNextSendState, timeZone: string | null): string;
|
|
12
|
+
/**
|
|
13
|
+
* The Mailboxes card's one line on pacing: what has gone today against the
|
|
14
|
+
* cap, and how much the next run may send — the number a due enrollment's
|
|
15
|
+
* "Queued" is waiting on.
|
|
16
|
+
*/
|
|
17
|
+
export declare function outreachPacingNote(mailbox: Pick<OutreachMailbox, 'status' | 'dailyCap' | 'rampStartedAtMs' | 'timezone' | 'window' | 'health'>, nowMs: number): string;
|
|
18
|
+
/** An enrollment's next send, with the reason on hover when it is queued. */
|
|
19
|
+
export declare function OutreachNextSend(props: {
|
|
20
|
+
state: OutreachNextSendState;
|
|
21
|
+
timeZone: string | null;
|
|
22
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
18
|
+
import { Tooltip, Typography } from "@mui/material";
|
|
19
|
+
import { useEffect, useState } from "react";
|
|
20
|
+
import { isOutreachWindowOpen } from "../engine/schedule.js";
|
|
21
|
+
import { OUTREACH_TICK_MINUTES, outreachTickAllowance } from "../engine/sending-capacity.js";
|
|
22
|
+
import { formatOutreachTime } from "./outreach-ui.js";
|
|
23
|
+
/*
|
|
24
|
+
* The words for an enrollment's next send (AGL-3366): a time while it is
|
|
25
|
+
* still ahead, and once it has come, the queue it is waiting in — short in
|
|
26
|
+
* the cell, whole in the tooltip, which also keeps the exact due time.
|
|
27
|
+
*/ const QUEUED_LABELS = {
|
|
28
|
+
paced: 'Queued · paced',
|
|
29
|
+
next_run: 'Queued · next run',
|
|
30
|
+
window_closed: 'Queued · window closed',
|
|
31
|
+
daily_cap_reached: 'Queued · cap reached',
|
|
32
|
+
mailbox_not_sending: 'Held · mailbox not sending',
|
|
33
|
+
sequence_not_active: 'Held · sequence not active'
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* The clock next sends are judged against, moved on every minute so a row
|
|
37
|
+
* turns from its time to "Queued" while the page stays open.
|
|
38
|
+
*/ export function useOutreachNowMs(everyMs = 60000) {
|
|
39
|
+
const [nowMs, setNowMs] = useState(()=>Date.now());
|
|
40
|
+
useEffect(()=>{
|
|
41
|
+
const timer = setInterval(()=>setNowMs(Date.now()), everyMs);
|
|
42
|
+
return ()=>clearInterval(timer);
|
|
43
|
+
}, [
|
|
44
|
+
everyMs
|
|
45
|
+
]);
|
|
46
|
+
return nowMs;
|
|
47
|
+
}
|
|
48
|
+
/** What the cell, the chip and the export say. */ export function outreachNextSendLabel(state, timeZone) {
|
|
49
|
+
if (state.kind === 'none') return '—';
|
|
50
|
+
if (state.kind === 'scheduled') return formatOutreachTime(state.dueAtMs, timeZone);
|
|
51
|
+
return QUEUED_LABELS[state.reason];
|
|
52
|
+
}
|
|
53
|
+
/** The whole explanation of a queued send; `''` for one that is not queued. */ export function outreachNextSendDetail(state, timeZone) {
|
|
54
|
+
if (state.kind !== 'queued') return '';
|
|
55
|
+
const time = (ms)=>formatOutreachTime(ms, timeZone);
|
|
56
|
+
const due = `Due ${time(state.dueAtMs)}.`;
|
|
57
|
+
const pacing = state.pacing ? ` This mailbox has sent ${state.pacing.sentToday} of ${state.pacing.dailyCap} today` + (state.reason === 'paced' ? ` and may send ${state.pacing.allowance} in this run.` : '.') : '';
|
|
58
|
+
switch(state.reason){
|
|
59
|
+
case 'paced':
|
|
60
|
+
return `${due} Emails go out a few at a time every ${OUTREACH_TICK_MINUTES} minutes, ` + 'follow-ups first, so the day’s cap is spread across the send window rather than ' + `sent at once. This one goes out in a later run.${pacing}`;
|
|
61
|
+
case 'next_run':
|
|
62
|
+
return `${due} The task is created on the next run, within ${OUTREACH_TICK_MINUTES} minutes.`;
|
|
63
|
+
case 'window_closed':
|
|
64
|
+
return `${due} The send window is closed; it goes out once the window opens${state.opensAtMs ? `, ${time(state.opensAtMs)}` : ''}.${pacing}`;
|
|
65
|
+
case 'daily_cap_reached':
|
|
66
|
+
return `${due}${pacing} Sending resumes${state.opensAtMs ? ` ${time(state.opensAtMs)}` : ' in the next window'}.`;
|
|
67
|
+
case 'mailbox_not_sending':
|
|
68
|
+
return `${due} Its mailbox is not sending, so nothing goes out until it is connected and resumed.`;
|
|
69
|
+
case 'sequence_not_active':
|
|
70
|
+
return `${due} The sequence is not active, so nothing goes out until it is activated.`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The Mailboxes card's one line on pacing: what has gone today against the
|
|
75
|
+
* cap, and how much the next run may send — the number a due enrollment's
|
|
76
|
+
* "Queued" is waiting on.
|
|
77
|
+
*/ export function outreachPacingNote(mailbox, nowMs) {
|
|
78
|
+
const tick = outreachTickAllowance({
|
|
79
|
+
mailbox,
|
|
80
|
+
nowMs
|
|
81
|
+
});
|
|
82
|
+
if (tick.hold === 'mailbox_not_connected' || tick.hold === 'invalid_timezone') {
|
|
83
|
+
return 'Sends nothing until it is connected and sending.';
|
|
84
|
+
}
|
|
85
|
+
const today = `Today: ${tick.sentToday} of ${tick.dailyCap} sent`;
|
|
86
|
+
if (tick.hold === 'daily_cap_reached') return `${today} · cap reached, sending resumes in the next day’s window.`;
|
|
87
|
+
if (!isOutreachWindowOpen(nowMs, mailbox.window, mailbox.timezone)) {
|
|
88
|
+
return `${today} · outside its send window.`;
|
|
89
|
+
}
|
|
90
|
+
return `${today} · up to ${tick.allowance} per ${OUTREACH_TICK_MINUTES}-minute run now, ` + 'follow-ups first, so the rest of the day’s cap is spread across the window.';
|
|
91
|
+
}
|
|
92
|
+
/** An enrollment's next send, with the reason on hover when it is queued. */ export function OutreachNextSend(props) {
|
|
93
|
+
const { state, timeZone } = props;
|
|
94
|
+
const label = outreachNextSendLabel(state, timeZone);
|
|
95
|
+
if (state.kind !== 'queued') {
|
|
96
|
+
return /*#__PURE__*/ _jsx(Typography, {
|
|
97
|
+
variant: "body2",
|
|
98
|
+
color: state.kind === 'none' ? 'text.secondary' : undefined,
|
|
99
|
+
children: label
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return /*#__PURE__*/ _jsx(Tooltip, {
|
|
103
|
+
title: outreachNextSendDetail(state, timeZone),
|
|
104
|
+
describeChild: true,
|
|
105
|
+
children: /*#__PURE__*/ _jsx(Typography, {
|
|
106
|
+
variant: "body2",
|
|
107
|
+
color: state.reason === 'mailbox_not_sending' || state.reason === 'sequence_not_active' ? 'warning.main' : 'text.secondary',
|
|
108
|
+
sx: {
|
|
109
|
+
textDecoration: 'underline dotted',
|
|
110
|
+
textUnderlineOffset: 3,
|
|
111
|
+
cursor: 'help'
|
|
112
|
+
},
|
|
113
|
+
children: label
|
|
114
|
+
})
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
//# sourceMappingURL=next-send.js.map
|