@aglyn/plugins-outreach 1.0.0-beta.152 → 1.0.0-beta.154
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/README.md +5 -1
- package/package.json +12 -12
- package/src/lib/components/enroll-dialog.js +142 -32
- package/src/lib/components/enroll-dialog.js.map +1 -1
- package/src/lib/components/enrollments-table.js +37 -8
- package/src/lib/components/enrollments-table.js.map +1 -1
- package/src/lib/components/mailbox-card.d.ts +1 -0
- package/src/lib/components/mailbox-card.js +25 -5
- package/src/lib/components/mailbox-card.js.map +1 -1
- package/src/lib/components/use-outreach-crm.d.ts +20 -1
- package/src/lib/components/use-outreach-crm.js +78 -4
- package/src/lib/components/use-outreach-crm.js.map +1 -1
- package/src/lib/declarations.console-server.js +14 -0
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/engine/enrollment-state.d.ts +6 -1
- package/src/lib/engine/enrollment-state.js +3 -1
- package/src/lib/engine/enrollment-state.js.map +1 -1
- package/src/lib/enrollment/enroll-people.d.ts +52 -7
- package/src/lib/enrollment/enroll-people.js +109 -12
- package/src/lib/enrollment/enroll-people.js.map +1 -1
- package/src/lib/enrollment/gate-lookups.d.ts +12 -3
- package/src/lib/enrollment/gate-lookups.js +17 -14
- package/src/lib/enrollment/gate-lookups.js.map +1 -1
- package/src/lib/mailboxes/mailbox-api.d.ts +6 -0
- package/src/lib/mailboxes/mailbox-api.js.map +1 -1
- package/src/lib/mailboxes/mailbox-routes.js +11 -0
- package/src/lib/mailboxes/mailbox-routes.js.map +1 -1
- package/src/lib/model/outreach-api.d.ts +39 -8
- package/src/lib/model/outreach-api.js.map +1 -1
- package/src/lib/model/outreach.types.d.ts +28 -3
- package/src/lib/model/outreach.types.js +9 -0
- package/src/lib/model/outreach.types.js.map +1 -1
- package/src/lib/model/stored-records.js +7 -0
- package/src/lib/model/stored-records.js.map +1 -1
- package/src/lib/routes/enroll-routes.js +112 -39
- package/src/lib/routes/enroll-routes.js.map +1 -1
- package/src/lib/routes/preview-routes.js +18 -5
- package/src/lib/routes/preview-routes.js.map +1 -1
- package/src/lib/runtime/lead-records.d.ts +26 -0
- package/src/lib/runtime/lead-records.js +84 -0
- package/src/lib/runtime/lead-records.js.map +1 -0
- package/src/lib/runtime/send-job.js +30 -12
- package/src/lib/runtime/send-job.js.map +1 -1
- package/src/lib/runtime/sync-job.js +11 -3
- package/src/lib/runtime/sync-job.js.map +1 -1
- package/src/lib/runtime/timeline.d.ts +6 -4
- package/src/lib/runtime/timeline.js +3 -7
- package/src/lib/runtime/timeline.js.map +1 -1
- package/src/lib/storage/outreach-records.d.ts +21 -2
- package/src/lib/storage/outreach-records.js +31 -2
- package/src/lib/storage/outreach-records.js.map +1 -1
|
@@ -20,7 +20,7 @@ import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.
|
|
|
20
20
|
import { ListRowActions, ListTable, listActionsColumn } from "@aglyn/shared-ui-jsx/components/list-table.component";
|
|
21
21
|
import { TABLE_PAGE_SIZE_DEFAULT } from "@aglyn/shared-ui-jsx/const/table-pagination";
|
|
22
22
|
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
23
|
-
import { Button, Card, CardContent, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Stack, TextField, Typography, useMediaQuery, useTheme } from "@mui/material";
|
|
23
|
+
import { Button, Card, CardContent, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Stack, TextField, Typography, useMediaQuery, useTheme, Chip } from "@mui/material";
|
|
24
24
|
import { useState } from "react";
|
|
25
25
|
import { OUTREACH_TASK_KIND_LABELS } from "../model/outreach.types.js";
|
|
26
26
|
import { formatOutreachTime, OUTREACH_STOP_REASON_LABELS, OutreachEnrollmentStatusChip, OutreachLoading, OutreachLoadProblem } from "./outreach-ui.js";
|
|
@@ -86,10 +86,25 @@ const ACTION_LABELS = {
|
|
|
86
86
|
minWidth: 0
|
|
87
87
|
},
|
|
88
88
|
children: [
|
|
89
|
-
/*#__PURE__*/
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
90
|
+
direction: "row",
|
|
91
|
+
spacing: 0.75,
|
|
92
|
+
sx: {
|
|
93
|
+
alignItems: 'center',
|
|
94
|
+
minWidth: 0
|
|
95
|
+
},
|
|
96
|
+
children: [
|
|
97
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
98
|
+
variant: "body2",
|
|
99
|
+
noWrap: true,
|
|
100
|
+
children: row.enrollment.contactName || row.enrollment.email
|
|
101
|
+
}),
|
|
102
|
+
row.enrollment.target === 'lead' ? /*#__PURE__*/ _jsx(Chip, {
|
|
103
|
+
size: "small",
|
|
104
|
+
variant: "outlined",
|
|
105
|
+
label: "Lead"
|
|
106
|
+
}) : null
|
|
107
|
+
]
|
|
93
108
|
}),
|
|
94
109
|
row.enrollment.contactName ? /*#__PURE__*/ _jsx(Typography, {
|
|
95
110
|
variant: "caption",
|
|
@@ -214,9 +229,23 @@ const ACTION_LABELS = {
|
|
|
214
229
|
const person = (enrollment)=>/*#__PURE__*/ _jsxs(Stack, {
|
|
215
230
|
spacing: 0,
|
|
216
231
|
children: [
|
|
217
|
-
/*#__PURE__*/
|
|
218
|
-
|
|
219
|
-
|
|
232
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
233
|
+
direction: "row",
|
|
234
|
+
spacing: 0.75,
|
|
235
|
+
sx: {
|
|
236
|
+
alignItems: 'center'
|
|
237
|
+
},
|
|
238
|
+
children: [
|
|
239
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
240
|
+
variant: "body2",
|
|
241
|
+
children: enrollment.contactName || enrollment.email
|
|
242
|
+
}),
|
|
243
|
+
enrollment.target === 'lead' ? /*#__PURE__*/ _jsx(Chip, {
|
|
244
|
+
size: "small",
|
|
245
|
+
variant: "outlined",
|
|
246
|
+
label: "Lead"
|
|
247
|
+
}) : null
|
|
248
|
+
]
|
|
220
249
|
}),
|
|
221
250
|
enrollment.contactName ? /*#__PURE__*/ _jsx(Typography, {
|
|
222
251
|
variant: "caption",
|
|
@@ -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 { 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 { TABLE_PAGE_SIZE_DEFAULT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Button,\n Card,\n CardContent,\n Dialog,\n DialogActions,\n DialogContent,\n DialogContentText,\n DialogTitle,\n Stack,\n TextField,\n Typography,\n useMediaQuery,\n useTheme,\n} from '@mui/material'\nimport { type ReactNode, useState } from 'react'\nimport type { OutreachEnrollmentAction } from '../model/outreach-api'\nimport {\n OUTREACH_TASK_KIND_LABELS,\n type OutreachEnrollment,\n type OutreachSequenceStep,\n} from '../model/outreach.types'\nimport {\n formatOutreachTime,\n OUTREACH_STOP_REASON_LABELS,\n OutreachEnrollmentStatusChip,\n OutreachLoading,\n OutreachLoadProblem,\n} from './outreach-ui'\nimport type { OutreachApi } from './use-outreach-api'\nimport type { OutreachEnrollmentsLoad } from './use-outreach-data'\n\nexport interface OutreachEnrollmentsTableProps {\n enrollments: OutreachEnrollmentsLoad\n steps: readonly OutreachSequenceStep[]\n /** The mailbox's IANA zone, which next sends are read in; `null` for the reader's own. */\n timeZone: string | null\n api: OutreachApi\n /** Shown in the empty state when the sequence takes people. */\n enrollAction?: ReactNode\n}\n\n/** The step an enrollment is on, as a person reads it. */\nexport function outreachCurrentStepLabel(\n enrollment: Pick<OutreachEnrollment, 'status' | 'stepIndex'>,\n steps: readonly OutreachSequenceStep[],\n): string {\n if (enrollment.status === 'finished' || enrollment.stepIndex >= steps.length)\n return 'All steps done'\n const step = steps[enrollment.stepIndex]\n const kind =\n step?.kind === 'email'\n ? 'Email'\n : step?.kind === 'task'\n ? OUTREACH_TASK_KIND_LABELS[step.taskKind]\n : 'Step'\n return `Step ${enrollment.stepIndex + 1} of ${steps.length} · ${kind}`\n}\n\n/** Why an enrollment stopped, as the table says it; `''` while it runs. */\nexport function outreachStopLabel(\n enrollment: Pick<OutreachEnrollment, 'stopReason' | 'stopDetail'>,\n): string {\n if (!enrollment.stopReason) return ''\n const reason = OUTREACH_STOP_REASON_LABELS[enrollment.stopReason] ?? ''\n return enrollment.stopDetail ? `${reason} — ${enrollment.stopDetail}` : reason\n}\n\n/** The last thing that happened: a send, a stop, or the enrollment itself. */\nfunction lastActivityMs(enrollment: OutreachEnrollment): number {\n return Math.max(\n enrollment.lastSentAtMs ?? 0,\n enrollment.stoppedAtMs ?? 0,\n enrollment.createdAtMs ?? 0,\n )\n}\n\n/** What a member may do to an enrollment in the status it is in. */\nfunction actionsFor(\n enrollment: OutreachEnrollment,\n): OutreachEnrollmentAction[] {\n const actions: OutreachEnrollmentAction[] = []\n if (enrollment.status === 'active') actions.push('pause', 'stop')\n if (enrollment.status === 'paused') actions.push('resume', 'stop')\n if (enrollment.status !== 'opted_out') actions.push('do_not_contact')\n return actions\n}\n\nconst ACTION_LABELS: Record<OutreachEnrollmentAction, string> = {\n pause: 'Pause',\n resume: 'Resume',\n stop: 'Stop',\n do_not_contact: 'Mark do-not-contact',\n}\n\n/** What the snackbar says once an action lands. */\nconst DONE_LABELS: Record<\n Exclude<OutreachEnrollmentAction, 'do_not_contact'>,\n string\n> = {\n pause: 'Paused.',\n resume: 'Resumed.',\n stop: 'Stopped.',\n}\n\n/** The two actions that end something for good ask first. */\nconst CONFIRM: Partial<\n Record<\n OutreachEnrollmentAction,\n { title: string; body: string; label: string }\n >\n> = {\n stop: {\n title: 'Stop this enrollment?',\n body: 'They won’t get any more steps from this sequence, and they can’t be enrolled in it again.',\n label: 'Stop',\n },\n do_not_contact: {\n title: 'Mark do-not-contact?',\n body:\n 'The address goes on your organization’s do-not-contact list, which every sequence checks before ' +\n 'every send, and they are stopped in every sequence they are in.',\n label: 'Mark do-not-contact',\n },\n}\n\n/** The table's columns; the person and the actions are drawn from the row's enrollment. */\nfunction columns(\n rowActions: (enrollment: OutreachEnrollment) => ReactNode,\n): NonNullable<ListTableProps['columns']> {\n return [\n {\n field: 'person',\n headerName: 'Person',\n flex: 1,\n minWidth: 200,\n sortable: false,\n renderCell: ({ row }) => (\n <Stack\n spacing={0}\n sx={{ justifyContent: 'center', height: '100%', minWidth: 0 }}\n >\n <Typography variant=\"body2\" noWrap>\n {row.enrollment.contactName || row.enrollment.email}\n </Typography>\n {row.enrollment.contactName ? (\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {row.enrollment.email}\n </Typography>\n ) : null}\n </Stack>\n ),\n },\n {\n field: 'status',\n headerName: 'Status',\n width: 120,\n renderCell: ({ row }) => (\n <OutreachEnrollmentStatusChip status={row.status} />\n ),\n },\n { field: 'step', headerName: 'Current step', flex: 1, minWidth: 170 },\n { field: 'nextSend', headerName: 'Next send', width: 190, sortable: false },\n {\n field: 'lastActivity',\n headerName: 'Last activity',\n width: 190,\n sortable: false,\n },\n { field: 'stopReason', headerName: 'Stop reason', flex: 1, minWidth: 170 },\n listActionsColumn((row) => rowActions(row.enrollment), { width: 72 }),\n ]\n}\n\n/**\n * A sequence's enrollments (AGL-2980): who is in it, where each person\n * stands, when their next step goes out in the mailbox's timezone, and the\n * four things a member can do about it — pause, resume, stop, and mark\n * do-not-contact. A table on wider screens, a list of cards on a phone.\n */\nexport function OutreachEnrollmentsTable(props: OutreachEnrollmentsTableProps) {\n const { enrollments, steps, timeZone, api } = props\n const theme = useTheme()\n const narrow = useMediaQuery(theme.breakpoints.down('md'))\n const { enqueueSnackbar } = useSnackbar()\n const [confirming, setConfirming] = useState<{\n enrollment: OutreachEnrollment\n action: OutreachEnrollmentAction\n } | null>(null)\n const [detail, setDetail] = useState('')\n const [busy, setBusy] = useState<string | null>(null)\n const [page, setPage] = useState(0)\n const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT)\n\n const act = async (\n enrollment: OutreachEnrollment,\n action: OutreachEnrollmentAction,\n why?: string,\n ) => {\n setBusy(enrollment.id)\n try {\n const answer = await api.actOnEnrollment(enrollment.id, action, why)\n const others = answer.stoppedOthers\n enqueueSnackbar(\n action === 'do_not_contact'\n ? `Marked do-not-contact${others ? `, and stopped in ${others} other ${others === 1 ? 'sequence' : 'sequences'}` : ''}.`\n : answer.changed\n ? DONE_LABELS[action]\n : 'Nothing to change.',\n { variant: 'success' },\n )\n } catch (error) {\n enqueueSnackbar((error as Error).message, {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setBusy(null)\n }\n }\n\n const choose = (\n enrollment: OutreachEnrollment,\n action: OutreachEnrollmentAction,\n ) => {\n if (CONFIRM[action]) {\n setDetail('')\n setConfirming({ enrollment, action })\n } else {\n void act(enrollment, action)\n }\n }\n\n if (enrollments.status === 'loading')\n return <OutreachLoading label=\"Loading enrollments…\" />\n if (enrollments.status === 'error' || enrollments.status === 'refused') {\n return (\n <OutreachLoadProblem status={enrollments.status} what=\"enrollments\" />\n )\n }\n if (!enrollments.data.length) {\n return (\n <Card variant=\"outlined\">\n <EmptyStateComponent\n label=\"Nobody is enrolled yet\"\n description=\"People you enroll show here, with where each of them is in the sequence.\"\n action={props.enrollAction}\n />\n </Card>\n )\n }\n\n const rowActions = (enrollment: OutreachEnrollment) => (\n <ListRowActions\n label={enrollment.contactName || enrollment.email}\n items={actionsFor(enrollment).map((action) => ({\n key: action,\n label: ACTION_LABELS[action],\n onClick: () => choose(enrollment, action),\n destructive: action === 'stop' || action === 'do_not_contact',\n disabled: busy === enrollment.id,\n }))}\n />\n )\n const nextSend = (enrollment: OutreachEnrollment) =>\n enrollment.status === 'active' || enrollment.status === 'paused'\n ? formatOutreachTime(enrollment.nextDueAtMs, timeZone)\n : '—'\n const person = (enrollment: OutreachEnrollment) => (\n <Stack spacing={0}>\n <Typography variant=\"body2\">\n {enrollment.contactName || enrollment.email}\n </Typography>\n {enrollment.contactName ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {enrollment.email}\n </Typography>\n ) : null}\n </Stack>\n )\n\n const loaded = enrollments.data\n const pageRows = loaded.slice(page * pageSize, (page + 1) * pageSize)\n const lastLoadedPage = Math.max(0, Math.ceil(loaded.length / pageSize) - 1)\n /** Past the rows read so far, the next page reads another window first. */\n const turnTo = (next: number) => {\n if ((next + 1) * pageSize > loaded.length && enrollments.hasMore)\n enrollments.showMore()\n setPage(next)\n }\n\n return (\n <Stack spacing={1.5}>\n {narrow ? (\n <Stack\n spacing={1}\n component=\"ul\"\n sx={{ listStyle: 'none', p: 0, m: 0 }}\n aria-label=\"Enrollments\"\n >\n {pageRows.map((enrollment) => (\n <Card key={enrollment.id} variant=\"outlined\" component=\"li\">\n <CardContent>\n <Stack spacing={1}>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'flex-start' }}\n >\n <Stack sx={{ flexGrow: 1, minWidth: 0 }}>\n {person(enrollment)}\n </Stack>\n <OutreachEnrollmentStatusChip status={enrollment.status} />\n {rowActions(enrollment)}\n </Stack>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {outreachCurrentStepLabel(enrollment, steps)}\n </Typography>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`Next send: ${nextSend(enrollment)} · Last activity: ${formatOutreachTime(lastActivityMs(enrollment), timeZone)}`}\n </Typography>\n {outreachStopLabel(enrollment) ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {outreachStopLabel(enrollment)}\n </Typography>\n ) : null}\n </Stack>\n </CardContent>\n </Card>\n ))}\n </Stack>\n ) : (\n <ListTable\n aria-label=\"Enrollments\"\n columns={columns(rowActions)}\n rows={pageRows.map((enrollment) => ({\n $id: enrollment.id,\n enrollment,\n status: enrollment.status,\n step: outreachCurrentStepLabel(enrollment, steps),\n nextSend: nextSend(enrollment),\n lastActivity: formatOutreachTime(\n lastActivityMs(enrollment),\n timeZone,\n ),\n stopReason: outreachStopLabel(enrollment) || '—',\n }))}\n quickFilter={false}\n hideFooter\n />\n )}\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={pageRows.length}\n count={enrollments.hasMore ? undefined : loaded.length}\n hasMore={enrollments.hasMore || page < lastLoadedPage}\n onPageChange={turnTo}\n onPageSizeChange={setPageSize}\n />\n\n <Dialog\n open={Boolean(confirming)}\n onClose={() => setConfirming(null)}\n fullWidth\n maxWidth=\"xs\"\n >\n {confirming ? (\n <>\n <DialogTitle>{CONFIRM[confirming.action]?.title}</DialogTitle>\n <DialogContent>\n <Stack spacing={2}>\n <DialogContentText>\n {CONFIRM[confirming.action]?.body}\n </DialogContentText>\n <TextField\n label=\"Why (optional)\"\n value={detail}\n onChange={(event) => setDetail(event.target.value)}\n fullWidth\n slotProps={{ htmlInput: { maxLength: 200 } }}\n />\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setConfirming(null)}>Cancel</Button>\n <Button\n color=\"error\"\n variant=\"contained\"\n onClick={() => {\n const { enrollment, action } = confirming\n setConfirming(null)\n void act(enrollment, action, detail.trim() || undefined)\n }}\n >\n {CONFIRM[confirming.action]?.label}\n </Button>\n </DialogActions>\n </>\n ) : null}\n </Dialog>\n </Stack>\n )\n}\nOutreachEnrollmentsTable.displayName = 'OutreachEnrollmentsTable'\n\nexport default OutreachEnrollmentsTable\n"],"names":["EmptyStateComponent","ListPagination","ListRowActions","ListTable","listActionsColumn","TABLE_PAGE_SIZE_DEFAULT","useSnackbar","Button","Card","CardContent","Dialog","DialogActions","DialogContent","DialogContentText","DialogTitle","Stack","TextField","Typography","useMediaQuery","useTheme","useState","OUTREACH_TASK_KIND_LABELS","formatOutreachTime","OUTREACH_STOP_REASON_LABELS","OutreachEnrollmentStatusChip","OutreachLoading","OutreachLoadProblem","outreachCurrentStepLabel","enrollment","steps","status","stepIndex","length","step","kind","taskKind","outreachStopLabel","stopReason","reason","stopDetail","lastActivityMs","Math","max","lastSentAtMs","stoppedAtMs","createdAtMs","actionsFor","actions","push","ACTION_LABELS","pause","resume","stop","do_not_contact","DONE_LABELS","CONFIRM","title","body","label","columns","rowActions","field","headerName","flex","minWidth","sortable","renderCell","row","spacing","sx","justifyContent","height","variant","noWrap","contactName","email","color","width","OutreachEnrollmentsTable","props","enrollments","timeZone","api","theme","narrow","breakpoints","down","enqueueSnackbar","confirming","setConfirming","detail","setDetail","busy","setBusy","page","setPage","pageSize","setPageSize","act","action","why","id","answer","actOnEnrollment","others","stoppedOthers","changed","error","message","allowDuplicate","choose","what","data","description","enrollAction","items","map","key","onClick","destructive","disabled","nextSend","nextDueAtMs","person","loaded","pageRows","slice","lastLoadedPage","ceil","turnTo","next","hasMore","showMore","component","listStyle","p","m","aria-label","direction","alignItems","flexGrow","rows","$id","lastActivity","quickFilter","hideFooter","rowCount","count","undefined","onPageChange","onPageSizeChange","open","Boolean","onClose","fullWidth","maxWidth","value","onChange","event","target","slotProps","htmlInput","maxLength","trim","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/enrollments-table.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport EmptyStateComponent from '@aglyn/shared-ui-jsx/components/empty-state.component'\nimport { 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 { TABLE_PAGE_SIZE_DEFAULT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Button,\n Card,\n CardContent,\n Dialog,\n DialogActions,\n DialogContent,\n DialogContentText,\n DialogTitle,\n Stack,\n TextField,\n Typography,\n useMediaQuery,\n useTheme,\n Chip,\n} from '@mui/material'\nimport { type ReactNode, useState } from 'react'\nimport type { OutreachEnrollmentAction } from '../model/outreach-api'\nimport {\n OUTREACH_TASK_KIND_LABELS,\n type OutreachEnrollment,\n type OutreachSequenceStep,\n} from '../model/outreach.types'\nimport {\n formatOutreachTime,\n OUTREACH_STOP_REASON_LABELS,\n OutreachEnrollmentStatusChip,\n OutreachLoading,\n OutreachLoadProblem,\n} from './outreach-ui'\nimport type { OutreachApi } from './use-outreach-api'\nimport type { OutreachEnrollmentsLoad } from './use-outreach-data'\n\nexport interface OutreachEnrollmentsTableProps {\n enrollments: OutreachEnrollmentsLoad\n steps: readonly OutreachSequenceStep[]\n /** The mailbox's IANA zone, which next sends are read in; `null` for the reader's own. */\n timeZone: string | null\n api: OutreachApi\n /** Shown in the empty state when the sequence takes people. */\n enrollAction?: ReactNode\n}\n\n/** The step an enrollment is on, as a person reads it. */\nexport function outreachCurrentStepLabel(\n enrollment: Pick<OutreachEnrollment, 'status' | 'stepIndex'>,\n steps: readonly OutreachSequenceStep[],\n): string {\n if (enrollment.status === 'finished' || enrollment.stepIndex >= steps.length)\n return 'All steps done'\n const step = steps[enrollment.stepIndex]\n const kind =\n step?.kind === 'email'\n ? 'Email'\n : step?.kind === 'task'\n ? OUTREACH_TASK_KIND_LABELS[step.taskKind]\n : 'Step'\n return `Step ${enrollment.stepIndex + 1} of ${steps.length} · ${kind}`\n}\n\n/** Why an enrollment stopped, as the table says it; `''` while it runs. */\nexport function outreachStopLabel(\n enrollment: Pick<OutreachEnrollment, 'stopReason' | 'stopDetail'>,\n): string {\n if (!enrollment.stopReason) return ''\n const reason = OUTREACH_STOP_REASON_LABELS[enrollment.stopReason] ?? ''\n return enrollment.stopDetail ? `${reason} — ${enrollment.stopDetail}` : reason\n}\n\n/** The last thing that happened: a send, a stop, or the enrollment itself. */\nfunction lastActivityMs(enrollment: OutreachEnrollment): number {\n return Math.max(\n enrollment.lastSentAtMs ?? 0,\n enrollment.stoppedAtMs ?? 0,\n enrollment.createdAtMs ?? 0,\n )\n}\n\n/** What a member may do to an enrollment in the status it is in. */\nfunction actionsFor(\n enrollment: OutreachEnrollment,\n): OutreachEnrollmentAction[] {\n const actions: OutreachEnrollmentAction[] = []\n if (enrollment.status === 'active') actions.push('pause', 'stop')\n if (enrollment.status === 'paused') actions.push('resume', 'stop')\n if (enrollment.status !== 'opted_out') actions.push('do_not_contact')\n return actions\n}\n\nconst ACTION_LABELS: Record<OutreachEnrollmentAction, string> = {\n pause: 'Pause',\n resume: 'Resume',\n stop: 'Stop',\n do_not_contact: 'Mark do-not-contact',\n}\n\n/** What the snackbar says once an action lands. */\nconst DONE_LABELS: Record<\n Exclude<OutreachEnrollmentAction, 'do_not_contact'>,\n string\n> = {\n pause: 'Paused.',\n resume: 'Resumed.',\n stop: 'Stopped.',\n}\n\n/** The two actions that end something for good ask first. */\nconst CONFIRM: Partial<\n Record<\n OutreachEnrollmentAction,\n { title: string; body: string; label: string }\n >\n> = {\n stop: {\n title: 'Stop this enrollment?',\n body: 'They won’t get any more steps from this sequence, and they can’t be enrolled in it again.',\n label: 'Stop',\n },\n do_not_contact: {\n title: 'Mark do-not-contact?',\n body:\n 'The address goes on your organization’s do-not-contact list, which every sequence checks before ' +\n 'every send, and they are stopped in every sequence they are in.',\n label: 'Mark do-not-contact',\n },\n}\n\n/** The table's columns; the person and the actions are drawn from the row's enrollment. */\nfunction columns(\n rowActions: (enrollment: OutreachEnrollment) => ReactNode,\n): NonNullable<ListTableProps['columns']> {\n return [\n {\n field: 'person',\n headerName: 'Person',\n flex: 1,\n minWidth: 200,\n sortable: false,\n renderCell: ({ row }) => (\n <Stack\n spacing={0}\n sx={{ justifyContent: 'center', height: '100%', minWidth: 0 }}\n >\n <Stack direction=\"row\" spacing={0.75} sx={{ alignItems: 'center', minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {row.enrollment.contactName || row.enrollment.email}\n </Typography>\n {row.enrollment.target === 'lead' ? (\n <Chip size=\"small\" variant=\"outlined\" label=\"Lead\" />\n ) : null}\n </Stack>\n {row.enrollment.contactName ? (\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {row.enrollment.email}\n </Typography>\n ) : null}\n </Stack>\n ),\n },\n {\n field: 'status',\n headerName: 'Status',\n width: 120,\n renderCell: ({ row }) => (\n <OutreachEnrollmentStatusChip status={row.status} />\n ),\n },\n { field: 'step', headerName: 'Current step', flex: 1, minWidth: 170 },\n { field: 'nextSend', headerName: 'Next send', width: 190, sortable: false },\n {\n field: 'lastActivity',\n headerName: 'Last activity',\n width: 190,\n sortable: false,\n },\n { field: 'stopReason', headerName: 'Stop reason', flex: 1, minWidth: 170 },\n listActionsColumn((row) => rowActions(row.enrollment), { width: 72 }),\n ]\n}\n\n/**\n * A sequence's enrollments (AGL-2980): who is in it, where each person\n * stands, when their next step goes out in the mailbox's timezone, and the\n * four things a member can do about it — pause, resume, stop, and mark\n * do-not-contact. A table on wider screens, a list of cards on a phone.\n */\nexport function OutreachEnrollmentsTable(props: OutreachEnrollmentsTableProps) {\n const { enrollments, steps, timeZone, api } = props\n const theme = useTheme()\n const narrow = useMediaQuery(theme.breakpoints.down('md'))\n const { enqueueSnackbar } = useSnackbar()\n const [confirming, setConfirming] = useState<{\n enrollment: OutreachEnrollment\n action: OutreachEnrollmentAction\n } | null>(null)\n const [detail, setDetail] = useState('')\n const [busy, setBusy] = useState<string | null>(null)\n const [page, setPage] = useState(0)\n const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT)\n\n const act = async (\n enrollment: OutreachEnrollment,\n action: OutreachEnrollmentAction,\n why?: string,\n ) => {\n setBusy(enrollment.id)\n try {\n const answer = await api.actOnEnrollment(enrollment.id, action, why)\n const others = answer.stoppedOthers\n enqueueSnackbar(\n action === 'do_not_contact'\n ? `Marked do-not-contact${others ? `, and stopped in ${others} other ${others === 1 ? 'sequence' : 'sequences'}` : ''}.`\n : answer.changed\n ? DONE_LABELS[action]\n : 'Nothing to change.',\n { variant: 'success' },\n )\n } catch (error) {\n enqueueSnackbar((error as Error).message, {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setBusy(null)\n }\n }\n\n const choose = (\n enrollment: OutreachEnrollment,\n action: OutreachEnrollmentAction,\n ) => {\n if (CONFIRM[action]) {\n setDetail('')\n setConfirming({ enrollment, action })\n } else {\n void act(enrollment, action)\n }\n }\n\n if (enrollments.status === 'loading')\n return <OutreachLoading label=\"Loading enrollments…\" />\n if (enrollments.status === 'error' || enrollments.status === 'refused') {\n return (\n <OutreachLoadProblem status={enrollments.status} what=\"enrollments\" />\n )\n }\n if (!enrollments.data.length) {\n return (\n <Card variant=\"outlined\">\n <EmptyStateComponent\n label=\"Nobody is enrolled yet\"\n description=\"People you enroll show here, with where each of them is in the sequence.\"\n action={props.enrollAction}\n />\n </Card>\n )\n }\n\n const rowActions = (enrollment: OutreachEnrollment) => (\n <ListRowActions\n label={enrollment.contactName || enrollment.email}\n items={actionsFor(enrollment).map((action) => ({\n key: action,\n label: ACTION_LABELS[action],\n onClick: () => choose(enrollment, action),\n destructive: action === 'stop' || action === 'do_not_contact',\n disabled: busy === enrollment.id,\n }))}\n />\n )\n const nextSend = (enrollment: OutreachEnrollment) =>\n enrollment.status === 'active' || enrollment.status === 'paused'\n ? formatOutreachTime(enrollment.nextDueAtMs, timeZone)\n : '—'\n const person = (enrollment: OutreachEnrollment) => (\n <Stack spacing={0}>\n <Stack direction=\"row\" spacing={0.75} sx={{ alignItems: 'center' }}>\n <Typography variant=\"body2\">\n {enrollment.contactName || enrollment.email}\n </Typography>\n {enrollment.target === 'lead' ? (\n <Chip size=\"small\" variant=\"outlined\" label=\"Lead\" />\n ) : null}\n </Stack>\n {enrollment.contactName ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {enrollment.email}\n </Typography>\n ) : null}\n </Stack>\n )\n\n const loaded = enrollments.data\n const pageRows = loaded.slice(page * pageSize, (page + 1) * pageSize)\n const lastLoadedPage = Math.max(0, Math.ceil(loaded.length / pageSize) - 1)\n /** Past the rows read so far, the next page reads another window first. */\n const turnTo = (next: number) => {\n if ((next + 1) * pageSize > loaded.length && enrollments.hasMore)\n enrollments.showMore()\n setPage(next)\n }\n\n return (\n <Stack spacing={1.5}>\n {narrow ? (\n <Stack\n spacing={1}\n component=\"ul\"\n sx={{ listStyle: 'none', p: 0, m: 0 }}\n aria-label=\"Enrollments\"\n >\n {pageRows.map((enrollment) => (\n <Card key={enrollment.id} variant=\"outlined\" component=\"li\">\n <CardContent>\n <Stack spacing={1}>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'flex-start' }}\n >\n <Stack sx={{ flexGrow: 1, minWidth: 0 }}>\n {person(enrollment)}\n </Stack>\n <OutreachEnrollmentStatusChip status={enrollment.status} />\n {rowActions(enrollment)}\n </Stack>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {outreachCurrentStepLabel(enrollment, steps)}\n </Typography>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`Next send: ${nextSend(enrollment)} · Last activity: ${formatOutreachTime(lastActivityMs(enrollment), timeZone)}`}\n </Typography>\n {outreachStopLabel(enrollment) ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {outreachStopLabel(enrollment)}\n </Typography>\n ) : null}\n </Stack>\n </CardContent>\n </Card>\n ))}\n </Stack>\n ) : (\n <ListTable\n aria-label=\"Enrollments\"\n columns={columns(rowActions)}\n rows={pageRows.map((enrollment) => ({\n $id: enrollment.id,\n enrollment,\n status: enrollment.status,\n step: outreachCurrentStepLabel(enrollment, steps),\n nextSend: nextSend(enrollment),\n lastActivity: formatOutreachTime(\n lastActivityMs(enrollment),\n timeZone,\n ),\n stopReason: outreachStopLabel(enrollment) || '—',\n }))}\n quickFilter={false}\n hideFooter\n />\n )}\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={pageRows.length}\n count={enrollments.hasMore ? undefined : loaded.length}\n hasMore={enrollments.hasMore || page < lastLoadedPage}\n onPageChange={turnTo}\n onPageSizeChange={setPageSize}\n />\n\n <Dialog\n open={Boolean(confirming)}\n onClose={() => setConfirming(null)}\n fullWidth\n maxWidth=\"xs\"\n >\n {confirming ? (\n <>\n <DialogTitle>{CONFIRM[confirming.action]?.title}</DialogTitle>\n <DialogContent>\n <Stack spacing={2}>\n <DialogContentText>\n {CONFIRM[confirming.action]?.body}\n </DialogContentText>\n <TextField\n label=\"Why (optional)\"\n value={detail}\n onChange={(event) => setDetail(event.target.value)}\n fullWidth\n slotProps={{ htmlInput: { maxLength: 200 } }}\n />\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setConfirming(null)}>Cancel</Button>\n <Button\n color=\"error\"\n variant=\"contained\"\n onClick={() => {\n const { enrollment, action } = confirming\n setConfirming(null)\n void act(enrollment, action, detail.trim() || undefined)\n }}\n >\n {CONFIRM[confirming.action]?.label}\n </Button>\n </DialogActions>\n </>\n ) : null}\n </Dialog>\n </Stack>\n )\n}\nOutreachEnrollmentsTable.displayName = 'OutreachEnrollmentsTable'\n\nexport default OutreachEnrollmentsTable\n"],"names":["EmptyStateComponent","ListPagination","ListRowActions","ListTable","listActionsColumn","TABLE_PAGE_SIZE_DEFAULT","useSnackbar","Button","Card","CardContent","Dialog","DialogActions","DialogContent","DialogContentText","DialogTitle","Stack","TextField","Typography","useMediaQuery","useTheme","Chip","useState","OUTREACH_TASK_KIND_LABELS","formatOutreachTime","OUTREACH_STOP_REASON_LABELS","OutreachEnrollmentStatusChip","OutreachLoading","OutreachLoadProblem","outreachCurrentStepLabel","enrollment","steps","status","stepIndex","length","step","kind","taskKind","outreachStopLabel","stopReason","reason","stopDetail","lastActivityMs","Math","max","lastSentAtMs","stoppedAtMs","createdAtMs","actionsFor","actions","push","ACTION_LABELS","pause","resume","stop","do_not_contact","DONE_LABELS","CONFIRM","title","body","label","columns","rowActions","field","headerName","flex","minWidth","sortable","renderCell","row","spacing","sx","justifyContent","height","direction","alignItems","variant","noWrap","contactName","email","target","size","color","width","OutreachEnrollmentsTable","props","enrollments","timeZone","api","theme","narrow","breakpoints","down","enqueueSnackbar","confirming","setConfirming","detail","setDetail","busy","setBusy","page","setPage","pageSize","setPageSize","act","action","why","id","answer","actOnEnrollment","others","stoppedOthers","changed","error","message","allowDuplicate","choose","what","data","description","enrollAction","items","map","key","onClick","destructive","disabled","nextSend","nextDueAtMs","person","loaded","pageRows","slice","lastLoadedPage","ceil","turnTo","next","hasMore","showMore","component","listStyle","p","m","aria-label","flexGrow","rows","$id","lastActivity","quickFilter","hideFooter","rowCount","count","undefined","onPageChange","onPageSizeChange","open","Boolean","onClose","fullWidth","maxWidth","value","onChange","event","slotProps","htmlInput","maxLength","trim","displayName"],"mappings":"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"}
|
|
@@ -29,6 +29,7 @@ export declare const MAILBOX_ACTION_LABELS: {
|
|
|
29
29
|
readonly test: "Send a test to myself";
|
|
30
30
|
readonly testElsewhere: "Send a test";
|
|
31
31
|
readonly testAddress: "Test address";
|
|
32
|
+
readonly warmUp: "Warm up gradually";
|
|
32
33
|
readonly disconnect: "Disconnect";
|
|
33
34
|
readonly reconnect: "Reconnect";
|
|
34
35
|
};
|
|
@@ -19,7 +19,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
19
19
|
import { pluginDocsHelp } from "@aglyn/aglyn";
|
|
20
20
|
import { CardDisplay, useConfirmationContext } from "@aglyn/shared-ui-jsx";
|
|
21
21
|
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
22
|
-
import { Alert, Autocomplete, Box, Button, Chip, FormHelperText, MenuItem, Stack, TextField, ToggleButton, ToggleButtonGroup, Typography } from "@mui/material";
|
|
22
|
+
import { Alert, Autocomplete, Box, Button, Chip, FormControlLabel, FormHelperText, MenuItem, Stack, Switch, TextField, ToggleButton, ToggleButtonGroup, Typography } from "@mui/material";
|
|
23
23
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
24
24
|
import { formatMinuteOfDay, isValidTimezone, OUTREACH_DEFAULT_WINDOW, OUTREACH_MAILBOX_STATUS_LABELS, OUTREACH_MAX_DAILY_CAP, OUTREACH_RAMP_STEPS, OUTREACH_WEEKDAY_LABELS, outreachEffectiveDailyCap, summarizeMailboxHealth, validateDailyCap, validateDisplayName, validateSendWindow } from "../mailboxes/mailbox-settings.js";
|
|
25
25
|
/**
|
|
@@ -53,6 +53,7 @@ import { formatMinuteOfDay, isValidTimezone, OUTREACH_DEFAULT_WINDOW, OUTREACH_M
|
|
|
53
53
|
test: 'Send a test to myself',
|
|
54
54
|
testElsewhere: 'Send a test',
|
|
55
55
|
testAddress: 'Test address',
|
|
56
|
+
warmUp: 'Warm up gradually',
|
|
56
57
|
disconnect: 'Disconnect',
|
|
57
58
|
reconnect: 'Reconnect'
|
|
58
59
|
};
|
|
@@ -93,7 +94,8 @@ const draftOf = (mailbox)=>{
|
|
|
93
94
|
displayName: (_mailbox_displayName = mailbox.displayName) != null ? _mailbox_displayName : '',
|
|
94
95
|
dailyCap: String((_mailbox_dailyCap = mailbox.dailyCap) != null ? _mailbox_dailyCap : ''),
|
|
95
96
|
window: (_mailbox_window = mailbox.window) != null ? _mailbox_window : OUTREACH_DEFAULT_WINDOW,
|
|
96
|
-
timezone: (_mailbox_timezone = mailbox.timezone) != null ? _mailbox_timezone : 'UTC'
|
|
97
|
+
timezone: (_mailbox_timezone = mailbox.timezone) != null ? _mailbox_timezone : 'UTC',
|
|
98
|
+
warmUp: typeof mailbox.rampStartedAtMs === 'number'
|
|
97
99
|
};
|
|
98
100
|
};
|
|
99
101
|
/**
|
|
@@ -105,7 +107,7 @@ const draftOf = (mailbox)=>{
|
|
|
105
107
|
* org owner or admin, and the test and the reconnect to its member alone,
|
|
106
108
|
* because both act in that member's own Google account.
|
|
107
109
|
*/ export function MailboxCard(props) {
|
|
108
|
-
var _OUTREACH_MAILBOX_STATUS_LABELS_mailbox_status, _STATUS_COLOR_mailbox_status;
|
|
110
|
+
var _mailbox_rampStartedAtMs, _OUTREACH_MAILBOX_STATUS_LABELS_mailbox_status, _STATUS_COLOR_mailbox_status;
|
|
109
111
|
var _mailbox_sendAsOptions;
|
|
110
112
|
const { mailbox, isMine, canManage, api, onReconnect, reconnecting = false, nowMs = Date.now() } = props;
|
|
111
113
|
const { enqueueSnackbar } = useSnackbar();
|
|
@@ -144,7 +146,8 @@ const draftOf = (mailbox)=>{
|
|
|
144
146
|
const invalid = typeof capCheck !== 'number' || !('days' in windowCheck) || typeof nameCheck !== 'string' || !timezoneValid;
|
|
145
147
|
const todayLimit = outreachEffectiveDailyCap({
|
|
146
148
|
dailyCap: typeof capCheck === 'number' ? capCheck : mailbox.dailyCap,
|
|
147
|
-
|
|
149
|
+
// A ramp switched on in the draft reads as starting today.
|
|
150
|
+
rampStartedAtMs: draft.warmUp ? (_mailbox_rampStartedAtMs = mailbox.rampStartedAtMs) != null ? _mailbox_rampStartedAtMs : nowMs : null,
|
|
148
151
|
timezone: timezoneValid ? draft.timezone : mailbox.timezone
|
|
149
152
|
}, nowMs);
|
|
150
153
|
const save = async ()=>{
|
|
@@ -163,6 +166,8 @@ const draftOf = (mailbox)=>{
|
|
|
163
166
|
window: draft.window
|
|
164
167
|
} : {}, draft.timezone !== stored.timezone ? {
|
|
165
168
|
timezone: draft.timezone
|
|
169
|
+
} : {}, draft.warmUp !== stored.warmUp ? {
|
|
170
|
+
warmUp: draft.warmUp
|
|
166
171
|
} : {}));
|
|
167
172
|
enqueueSnackbar('Mailbox settings saved', {
|
|
168
173
|
variant: 'success'
|
|
@@ -378,7 +383,22 @@ const draftOf = (mailbox)=>{
|
|
|
378
383
|
step: 1
|
|
379
384
|
}
|
|
380
385
|
},
|
|
381
|
-
helperText: typeof capCheck !== 'number' ? capCheck.message : `${RAMP_SENTENCE} Today’s limit: ${todayLimit}.`
|
|
386
|
+
helperText: typeof capCheck !== 'number' ? capCheck.message : draft.warmUp ? `${RAMP_SENTENCE} Today’s limit: ${todayLimit}.` : `No warm-up: today’s limit is the cap, ${todayLimit}.`
|
|
387
|
+
}),
|
|
388
|
+
/*#__PURE__*/ _jsx(FormControlLabel, {
|
|
389
|
+
control: /*#__PURE__*/ _jsx(Switch, {
|
|
390
|
+
checked: draft.warmUp,
|
|
391
|
+
onChange: (event)=>setDraft(_extends({}, draft, {
|
|
392
|
+
warmUp: event.target.checked
|
|
393
|
+
}))
|
|
394
|
+
}),
|
|
395
|
+
label: MAILBOX_ACTION_LABELS.warmUp
|
|
396
|
+
}),
|
|
397
|
+
/*#__PURE__*/ _jsx(FormHelperText, {
|
|
398
|
+
sx: {
|
|
399
|
+
mt: -1
|
|
400
|
+
},
|
|
401
|
+
children: 'For a mailbox that is new or has not sent much. One that has sent mail for years already has ' + 'its reputation and can start at its cap.'
|
|
382
402
|
}),
|
|
383
403
|
/*#__PURE__*/ _jsxs(Box, {
|
|
384
404
|
children: [
|
|
@@ -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 {\n Alert,\n Autocomplete,\n Box,\n Button,\n Chip,\n FormHelperText,\n MenuItem,\n Stack,\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 summarizeMailboxHealth,\n validateDailyCap,\n validateDisplayName,\n validateSendWindow,\n} from '../mailboxes/mailbox-settings'\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 disconnect: 'Disconnect',\n reconnect: 'Reconnect',\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}\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})\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 // 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 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 rampStartedAtMs: mailbox.rampStartedAtMs,\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 })\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 </Box>\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 : `${RAMP_SENTENCE} Today’s limit: ${todayLimit}.`\n }\n />\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","Alert","Autocomplete","Box","Button","Chip","FormHelperText","MenuItem","Stack","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","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","disconnect","reconnect","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","MailboxCard","props","isMine","canManage","api","onReconnect","reconnecting","nowMs","Date","now","enqueueSnackbar","confirm","draft","setDraft","saving","setSaving","saveError","setSaveError","busy","setBusy","setTestAddress","stored","storedKey","JSON","stringify","seededFrom","dirty","previous","health","needsReconnect","status","capCheck","Number","NaN","windowCheck","nameCheck","timezoneValid","invalid","todayLimit","rampStartedAtMs","saveSettings","mailboxId","id","variant","error","togglePaused","setPaused","allowDuplicate","sendTest","to","trim","sent","undefined","sentTo","confirmed","title","description","confirmationText","confirmationButtonProps","color","then","catch","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","onSubmit","event","preventDefault","select","onChange","target","helperText","type","slotProps","htmlInput","min","max","gutterBottom","days","_event","sort","a","b","xs","sm","startMinute","slice","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 {\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 summarizeMailboxHealth,\n validateDailyCap,\n validateDisplayName,\n validateSendWindow,\n} from '../mailboxes/mailbox-settings'\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} 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 // 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 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 </Box>\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","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","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","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","stored","storedKey","JSON","stringify","seededFrom","dirty","previous","health","needsReconnect","status","capCheck","Number","NaN","windowCheck","nameCheck","timezoneValid","invalid","todayLimit","saveSettings","mailboxId","id","variant","error","togglePaused","setPaused","allowDuplicate","sendTest","to","trim","sent","undefined","sentTo","confirmed","title","description","confirmationText","confirmationButtonProps","color","then","catch","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","onSubmit","event","preventDefault","select","onChange","target","helperText","type","slotProps","htmlInput","min","max","control","checked","gutterBottom","days","_event","sort","a","b","xs","sm","startMinute","slice","minute","endMinute","disableClearable","options","minWidth","renderInput","params","fullWidth","useFlexGap","flexWrap"],"mappings":"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"}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { OutreachLoad } from './use-outreach-data';
|
|
2
|
-
/** A saved Contacts view people can be enrolled from. */
|
|
2
|
+
/** A saved Contacts or Leads view people can be enrolled from (AGL-3234). */
|
|
3
3
|
export interface OutreachViewOption {
|
|
4
4
|
id: string;
|
|
5
5
|
name: string;
|
|
6
|
+
/** Which list the view is of; a Leads view selects the sequence's site's leads. */
|
|
7
|
+
section: 'contacts' | 'leads';
|
|
6
8
|
}
|
|
7
9
|
/** The most saved views the picker lists. */
|
|
8
10
|
export declare const OUTREACH_VIEWS_LIMIT = 50;
|
|
@@ -48,3 +50,20 @@ export declare function useOutreachContactSearch(input: {
|
|
|
48
50
|
}): OutreachLoad<OutreachContactOption[]> & {
|
|
49
51
|
idle: boolean;
|
|
50
52
|
};
|
|
53
|
+
/** The most leads the picker reads — the Leads list's own window. */
|
|
54
|
+
export declare const OUTREACH_LEADS_WINDOW = 200;
|
|
55
|
+
/**
|
|
56
|
+
* The open leads at `hostId` a search finds (AGL-3234): the site's most
|
|
57
|
+
* recently seen leads, read once while the tab is open and narrowed in
|
|
58
|
+
* the browser by a word of the name or the address — a lead carries no
|
|
59
|
+
* search tokens, and the window is the Leads list's own. Converted and
|
|
60
|
+
* closed leads are left out: they are a contact's, or a verdict's, to
|
|
61
|
+
* enroll. `idle` until something is typed.
|
|
62
|
+
*/
|
|
63
|
+
export declare function useOutreachLeadSearch(input: {
|
|
64
|
+
hostId: string | null;
|
|
65
|
+
text: string;
|
|
66
|
+
enabled: boolean;
|
|
67
|
+
}): OutreachLoad<OutreachContactOption[]> & {
|
|
68
|
+
idle: boolean;
|
|
69
|
+
};
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/ 'use client';
|
|
17
17
|
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
18
|
import { contactDisplayName, normalizeContactEmail } from "@aglyn/aglyn/app-utils/contacts";
|
|
19
|
-
import { CRM_COLLECTIONS, crmViewIsListed, normalizeCrmViewFilters } from "@aglyn/aglyn/app-utils/crm";
|
|
19
|
+
import { CRM_COLLECTIONS, crmLeadDisplayName, crmViewIsListed, isCrmLeadOpen, normalizeCrmViewFilters } from "@aglyn/aglyn/app-utils/crm";
|
|
20
20
|
import { CRM_EMAIL_TEMPLATES_LIMIT, crmEmailTemplateIsListed, normalizeCrmEmailTemplate } from "@aglyn/aglyn/app-utils/crm-email-templates";
|
|
21
21
|
import { dynamicListDimensionsForCrmView } from "@aglyn/aglyn/app-utils/dynamic-list-rule";
|
|
22
22
|
import { nameSearchToken } from "@aglyn/aglyn/app-utils/name-search";
|
|
@@ -55,7 +55,10 @@ import { useEffect, useState } from "react";
|
|
|
55
55
|
data: []
|
|
56
56
|
});
|
|
57
57
|
if (!orgId) return undefined;
|
|
58
|
-
return onSnapshot(query(collection(firestore, 'orgs', orgId, CRM_COLLECTIONS.views), where('section', '
|
|
58
|
+
return onSnapshot(query(collection(firestore, 'orgs', orgId, CRM_COLLECTIONS.views), where('section', 'in', [
|
|
59
|
+
'contacts',
|
|
60
|
+
'leads'
|
|
61
|
+
]), orderBy('name'), limit(OUTREACH_VIEWS_LIMIT)), (snapshot)=>setResult({
|
|
59
62
|
status: 'ready',
|
|
60
63
|
data: snapshot.docs.map((entry)=>({
|
|
61
64
|
id: entry.id,
|
|
@@ -65,10 +68,13 @@ import { useEffect, useState } from "react";
|
|
|
65
68
|
return typeof data['name'] === 'string' && data['name'] !== '' && crmViewIsListed({
|
|
66
69
|
shared: data['shared'] === true,
|
|
67
70
|
ownerUid: String((_data_ownerUid = data['ownerUid']) != null ? _data_ownerUid : '')
|
|
68
|
-
}, uid) &&
|
|
71
|
+
}, uid) && // A Leads view narrows by status alone, which enrolling can
|
|
72
|
+
// always apply; a Contacts view must be readable as a list.
|
|
73
|
+
(data['section'] === 'leads' || dynamicListDimensionsForCrmView(normalizeCrmViewFilters(data['filters'])).unsupported.length === 0);
|
|
69
74
|
}).map(({ id, data })=>({
|
|
70
75
|
id,
|
|
71
|
-
name: String(data['name'])
|
|
76
|
+
name: String(data['name']),
|
|
77
|
+
section: data['section'] === 'leads' ? 'leads' : 'contacts'
|
|
72
78
|
}))
|
|
73
79
|
}), (error)=>{
|
|
74
80
|
if (!denied(error)) console.error('[outreach] saved views could not be read', error);
|
|
@@ -196,5 +202,73 @@ import { useEffect, useState } from "react";
|
|
|
196
202
|
idle
|
|
197
203
|
});
|
|
198
204
|
}
|
|
205
|
+
/** The most leads the picker reads — the Leads list's own window. */ export const OUTREACH_LEADS_WINDOW = 200;
|
|
206
|
+
/**
|
|
207
|
+
* The open leads at `hostId` a search finds (AGL-3234): the site's most
|
|
208
|
+
* recently seen leads, read once while the tab is open and narrowed in
|
|
209
|
+
* the browser by a word of the name or the address — a lead carries no
|
|
210
|
+
* search tokens, and the window is the Leads list's own. Converted and
|
|
211
|
+
* closed leads are left out: they are a contact's, or a verdict's, to
|
|
212
|
+
* enroll. `idle` until something is typed.
|
|
213
|
+
*/ export function useOutreachLeadSearch(input) {
|
|
214
|
+
const { hostId, text, enabled } = input;
|
|
215
|
+
const firestore = useFirestore();
|
|
216
|
+
const [window, setWindow] = useState({
|
|
217
|
+
status: 'ready',
|
|
218
|
+
data: []
|
|
219
|
+
});
|
|
220
|
+
useEffect(()=>{
|
|
221
|
+
if (!enabled || !hostId) {
|
|
222
|
+
setWindow({
|
|
223
|
+
status: 'ready',
|
|
224
|
+
data: []
|
|
225
|
+
});
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
let current = true;
|
|
229
|
+
setWindow({
|
|
230
|
+
status: 'loading',
|
|
231
|
+
data: []
|
|
232
|
+
});
|
|
233
|
+
getDocs(query(collection(firestore, 'hosts', hostId, 'leads'), orderBy('lastSeenAtMs', 'desc'), limit(OUTREACH_LEADS_WINDOW))).then((snapshot)=>{
|
|
234
|
+
if (!current) return;
|
|
235
|
+
setWindow({
|
|
236
|
+
status: 'ready',
|
|
237
|
+
data: snapshot.docs.map((entry)=>({
|
|
238
|
+
id: entry.id,
|
|
239
|
+
data: entry.data()
|
|
240
|
+
})).filter(({ data })=>isCrmLeadOpen(data) && !data['convertedContactId']).map(({ id, data })=>({
|
|
241
|
+
id,
|
|
242
|
+
name: crmLeadDisplayName(data),
|
|
243
|
+
email: normalizeContactEmail(data['email'])
|
|
244
|
+
}))
|
|
245
|
+
});
|
|
246
|
+
}).catch((error)=>{
|
|
247
|
+
if (!current) return;
|
|
248
|
+
if (!denied(error)) console.error('[outreach] the leads could not be read', error);
|
|
249
|
+
setWindow({
|
|
250
|
+
status: denied(error) ? 'refused' : 'error',
|
|
251
|
+
data: []
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
return ()=>{
|
|
255
|
+
current = false;
|
|
256
|
+
};
|
|
257
|
+
}, [
|
|
258
|
+
firestore,
|
|
259
|
+
hostId,
|
|
260
|
+
enabled
|
|
261
|
+
]);
|
|
262
|
+
const needle = text.trim().toLowerCase();
|
|
263
|
+
const idle = !hostId || !needle;
|
|
264
|
+
const data = idle ? [] : window.data.filter((lead)=>{
|
|
265
|
+
var _lead_email;
|
|
266
|
+
return lead.name.toLowerCase().includes(needle) || ((_lead_email = lead.email) != null ? _lead_email : '').includes(needle);
|
|
267
|
+
}).slice(0, OUTREACH_SEARCH_LIMIT);
|
|
268
|
+
return _extends({}, window, {
|
|
269
|
+
data,
|
|
270
|
+
idle
|
|
271
|
+
});
|
|
272
|
+
}
|
|
199
273
|
|
|
200
274
|
//# sourceMappingURL=use-outreach-crm.js.map
|