@aglyn/plugins-outreach 1.0.0-beta.155 → 1.0.0-beta.157
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 +13 -12
- package/src/lib/components/enrollments-table.d.ts +7 -0
- package/src/lib/components/enrollments-table.js +46 -2
- package/src/lib/components/enrollments-table.js.map +1 -1
- package/src/lib/components/sequence-detail.js +14 -4
- package/src/lib/components/sequence-detail.js.map +1 -1
- package/src/lib/components/sequence-editor.js +22 -0
- package/src/lib/components/sequence-editor.js.map +1 -1
- package/src/lib/components/sequence-report-card.d.ts +23 -0
- package/src/lib/components/sequence-report-card.js +191 -0
- package/src/lib/components/sequence-report-card.js.map +1 -0
- package/src/lib/components/use-outreach-data.d.ts +11 -0
- package/src/lib/components/use-outreach-data.js +24 -1
- package/src/lib/components/use-outreach-data.js.map +1 -1
- package/src/lib/constants/api-routes.d.ts +9 -0
- package/src/lib/constants/api-routes.js +9 -1
- package/src/lib/constants/api-routes.js.map +1 -1
- package/src/lib/engine/click-tracking.d.ts +98 -0
- package/src/lib/engine/click-tracking.js +252 -0
- package/src/lib/engine/click-tracking.js.map +1 -0
- package/src/lib/engine/compose.d.ts +24 -0
- package/src/lib/engine/compose.js +15 -2
- package/src/lib/engine/compose.js.map +1 -1
- package/src/lib/engine/sequence-validation.js +4 -1
- package/src/lib/engine/sequence-validation.js.map +1 -1
- package/src/lib/model/enrollment-engagement.d.ts +31 -0
- package/src/lib/model/enrollment-engagement.js +44 -0
- package/src/lib/model/enrollment-engagement.js.map +1 -0
- package/src/lib/model/outreach.types.d.ts +89 -0
- package/src/lib/model/outreach.types.js +25 -0
- package/src/lib/model/outreach.types.js.map +1 -1
- package/src/lib/model/sequence-draft.js +5 -1
- package/src/lib/model/sequence-draft.js.map +1 -1
- package/src/lib/model/sequence-report.d.ts +68 -0
- package/src/lib/model/sequence-report.js +118 -0
- package/src/lib/model/sequence-report.js.map +1 -0
- package/src/lib/model/stored-records.d.ts +11 -1
- package/src/lib/model/stored-records.js +34 -1
- package/src/lib/model/stored-records.js.map +1 -1
- package/src/lib/routes/sequence-routes.js +4 -1
- package/src/lib/routes/sequence-routes.js.map +1 -1
- package/src/lib/runtime/click-events.d.ts +67 -0
- package/src/lib/runtime/click-events.js +232 -0
- package/src/lib/runtime/click-events.js.map +1 -0
- package/src/lib/runtime/click-link.d.ts +94 -0
- package/src/lib/runtime/click-link.js +158 -0
- package/src/lib/runtime/click-link.js.map +1 -0
- package/src/lib/runtime/click-route.d.ts +19 -0
- package/src/lib/runtime/click-route.js +108 -0
- package/src/lib/runtime/click-route.js.map +1 -0
- package/src/lib/runtime/platform-runtime-deps.d.ts +2 -0
- package/src/lib/runtime/platform-runtime-deps.js +9 -0
- package/src/lib/runtime/platform-runtime-deps.js.map +1 -1
- package/src/lib/runtime/runtime-deps.d.ts +11 -0
- package/src/lib/runtime/runtime-deps.js.map +1 -1
- package/src/lib/runtime/send-job.js +65 -5
- package/src/lib/runtime/send-job.js.map +1 -1
- package/src/lib/server.js +8 -0
- package/src/lib/server.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/sequence-editor.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { pluginDocsHelp, type ConsolePluginOrgMount } from '@aglyn/aglyn'\nimport { mdiPlus } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Autocomplete,\n Box,\n Button,\n Chip,\n FormControlLabel,\n Grid,\n Menu,\n MenuItem,\n Stack,\n Switch,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useEffect, useMemo, useState } from 'react'\nimport {\n isInThreadEmailStep,\n OUTREACH_SEQUENCE_NAME_MAX,\n validateOutreachSequence,\n} from '../engine/sequence-validation'\nimport { outreachCountryLabel } from '../model/compliance-settings'\nimport {\n OUTREACH_MAX_EMAIL_STEPS,\n OUTREACH_MAX_STEPS,\n OUTREACH_TASK_KIND_LABELS,\n OUTREACH_TASK_KINDS,\n type OutreachSequence,\n type OutreachSequenceStep,\n type OutreachTaskKind,\n} from '../model/outreach.types'\nimport {\n emptyOutreachSequenceDraft,\n newOutreachEmailStep,\n newOutreachTaskStep,\n outreachSequenceDraftOf,\n type OutreachSequenceDraft,\n type OutreachSequenceIssue,\n} from '../model/sequence-draft'\nimport { OutreachSendWindowFields } from './send-window-fields'\nimport { OutreachSequenceMailboxPicker } from './sequence-mailbox-picker'\nimport { OutreachSequencePreview } from './sequence-preview'\nimport { OutreachStepCard } from './sequence-step-card'\nimport { OutreachRouteError, useOutreachApi } from './use-outreach-api'\nimport { useOutreachEmailTemplates } from './use-outreach-crm'\nimport type { OutreachMailboxesResult } from './use-outreach-mailboxes'\nimport type { OutreachSettingsLoad } from './use-outreach-settings'\n\nexport interface OutreachSequenceEditorProps {\n orgId: string\n orgMount?: ConsolePluginOrgMount\n /** The stored sequence, or `null` for a new one. */\n sequence: OutreachSequence | null\n /** The organization's compliance settings: its countries, and the preview's footer. */\n settings: OutreachSettingsLoad\n /**\n * The organization's mailboxes, from the Mailboxes section's listener:\n * what the picker offers, and who the preview's emails are from.\n */\n mailboxes: OutreachMailboxesResult\n /** The Mailboxes section, where the picker sends a member to connect one. */\n mailboxesPath: string\n onSaved(sequence: OutreachSequence): void\n onCancel?(): void\n}\n\n/** The step a dotted issue path is about, and the field within it. */\nfunction stepIssueKey(path: string): { index: number; field: string } | null {\n const match = /^steps\\.(\\d+)(?:\\.(.+))?$/.exec(path)\n if (!match) return null\n const field = match[2] ?? 'step'\n return { index: Number(match[1]), field: field === 'id' ? 'step' : field }\n}\n\n/**\n * The sequence editor (AGL-2980): the name, the site and mailbox, the steps,\n * who it sends to and when, beside a live preview of each email.\n *\n * Every issue the engine's validator finds is shown beside its field — the\n * same validator the save route refuses with — errors once a save was\n * attempted, warnings as soon as they appear. What only the server can\n * judge (whose mailbox, which countries the organization allows, what may\n * no longer change once people are enrolled) comes back from the save and\n * is shown in the same places.\n */\nexport function OutreachSequenceEditor(props: OutreachSequenceEditorProps) {\n const { orgId, orgMount, sequence, settings } = props\n const api = useOutreachApi(orgId)\n const { enqueueSnackbar } = useSnackbar()\n const { data: user } = useUser()\n const uid = user?.uid ?? null\n const mountedHosts = orgMount?.hosts\n const hosts = useMemo(() => mountedHosts ?? [], [mountedHosts])\n const orgCountries = settings.settings?.allowedCountries ?? ['US']\n\n const [draft, setDraft] = useState<OutreachSequenceDraft>(() =>\n sequence\n ? outreachSequenceDraftOf(sequence)\n : emptyOutreachSequenceDraft({\n hostId: hosts.length === 1 ? hosts[0].id : '',\n allowedCountries: orgCountries,\n }),\n )\n const [attempted, setAttempted] = useState(false)\n const [serverIssues, setServerIssues] = useState<OutreachSequenceIssue[]>([])\n const [saving, setSaving] = useState(false)\n const [taskAnchor, setTaskAnchor] = useState<HTMLElement | null>(null)\n\n // A stored sequence the listener updated (a save, a status change) is the\n // new starting point.\n useEffect(() => {\n if (sequence) setDraft(outreachSequenceDraftOf(sequence))\n }, [sequence])\n // A new sequence starts on the organization's countries once they are read.\n const settingsCountries = settings.settings?.allowedCountries.join(',')\n useEffect(() => {\n if (!sequence && settingsCountries) {\n setDraft((previous) => ({\n ...previous,\n settings: {\n ...previous.settings,\n allowedCountries: settingsCountries.split(','),\n },\n }))\n }\n }, [sequence, settingsCountries])\n useEffect(() => {\n if (!sequence && !draft.hostId && hosts.length === 1)\n setDraft((previous) => ({ ...previous, hostId: hosts[0].id }))\n }, [sequence, draft.hostId, hosts])\n\n // A new sequence starts on the member's own mailbox when they have exactly\n // one that is sending.\n const ownSending = props.mailboxes.mailboxes.filter(\n (entry) => entry.connectedByUid === uid && entry.status === 'connected',\n )\n const onlyOwnMailbox = ownSending.length === 1 ? ownSending[0].id : ''\n useEffect(() => {\n if (!sequence && !draft.mailboxId && onlyOwnMailbox)\n setDraft((previous) => ({ ...previous, mailboxId: onlyOwnMailbox }))\n }, [sequence, draft.mailboxId, onlyOwnMailbox])\n\n const templates = useOutreachEmailTemplates(orgId, draft.hostId || null, uid)\n const mailbox =\n props.mailboxes.mailboxes.find((entry) => entry.id === draft.mailboxId) ??\n null\n const archived = sequence?.status === 'archived'\n\n const issues = useMemo(() => {\n const judged = [\n ...validateOutreachSequence(draft),\n ...serverIssues,\n ] as OutreachSequenceIssue[]\n return judged.filter(\n (issue) =>\n issue.severity === 'warning' ||\n attempted ||\n serverIssues.includes(issue),\n )\n }, [draft, serverIssues, attempted])\n const issueAt = (path: string) =>\n issues.find((issue) => issue.path === path)?.message\n const stepIssues = (index: number) => {\n const found: Record<string, string> = {}\n for (const issue of issues) {\n const key = stepIssueKey(issue.path)\n if (key?.index === index && !found[key.field])\n found[key.field] = issue.message\n }\n return found\n }\n const listIssue = issueAt('steps')\n const countriesIssue = issues.find((issue) =>\n issue.path.startsWith('settings.allowedCountries'),\n )?.message\n\n const update = (next: Partial<OutreachSequenceDraft>) => {\n setServerIssues([])\n setDraft((previous) => ({ ...previous, ...next }))\n }\n const setStep = (index: number, step: OutreachSequenceStep) =>\n update({\n steps: draft.steps.map((entry, position) =>\n position === index ? step : entry,\n ),\n })\n const moveStep = (index: number, direction: -1 | 1) => {\n const steps = [...draft.steps]\n const [moved] = steps.splice(index, 1)\n steps.splice(index + direction, 0, moved)\n update({ steps })\n }\n const emailCount = draft.steps.filter((step) => step.kind === 'email').length\n const full = draft.steps.length >= OUTREACH_MAX_STEPS\n const threadSubject = (index: number) => {\n for (let position = index - 1; position >= 0; position -= 1) {\n const step = draft.steps[position]\n if (step.kind === 'email' && !isInThreadEmailStep(draft.steps, position))\n return step.subject\n }\n return ''\n }\n\n const save = async () => {\n setAttempted(true)\n if (\n validateOutreachSequence(draft).some(\n (issue) => issue.severity === 'error',\n )\n )\n return\n setSaving(true)\n try {\n const answer = await api.saveSequence(sequence?.id ?? null, draft)\n setServerIssues([])\n setAttempted(false)\n enqueueSnackbar(\n answer.created ? 'Sequence created.' : 'Sequence saved.',\n { variant: 'success' },\n )\n props.onSaved(answer.sequence)\n } catch (error) {\n if (error instanceof OutreachRouteError && error.issues.length) {\n setServerIssues(\n error.issues.filter(\n (issue): issue is OutreachSequenceIssue => 'path' in issue,\n ),\n )\n }\n enqueueSnackbar((error as Error).message, {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setSaving(false)\n }\n }\n\n const siteName = hosts.find((host) => host.id === draft.hostId)?.name ?? ''\n const countryOptions = [\n ...new Set([...orgCountries, ...draft.settings.allowedCountries]),\n ]\n\n return (\n <Grid container spacing={2}>\n <Grid size={{ xs: 12, md: 7 }}>\n <Stack spacing={2}>\n {archived ? (\n <Alert severity=\"info\">\n This sequence is archived, so it can’t be edited.\n </Alert>\n ) : null}\n <CardDisplay\n header=\"Sequence\"\n help={pluginDocsHelp('sequences', { anchor: '#build-a-sequence' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <TextField\n label=\"Name\"\n value={draft.name}\n onChange={(event) => update({ name: event.target.value })}\n disabled={archived}\n error={Boolean(issueAt('name'))}\n helperText={issueAt('name')}\n slotProps={{\n htmlInput: { maxLength: OUTREACH_SEQUENCE_NAME_MAX + 20 },\n }}\n fullWidth\n />\n <TextField\n select\n label=\"Site\"\n value={\n hosts.some((host) => host.id === draft.hostId)\n ? draft.hostId\n : ''\n }\n onChange={(event) => update({ hostId: event.target.value })}\n disabled={archived || !hosts.length}\n error={Boolean(issueAt('hostId'))}\n helperText={\n issueAt('hostId') ??\n 'The site whose contacts this sequence emails.'\n }\n fullWidth\n >\n {hosts.map((host) => (\n <MenuItem key={host.id} value={host.id}>\n {host.name}\n </MenuItem>\n ))}\n </TextField>\n <OutreachSequenceMailboxPicker\n orgId={orgId}\n uid={uid}\n mailboxes={props.mailboxes}\n mailboxesPath={props.mailboxesPath}\n value={draft.mailboxId}\n onChange={(mailboxId) => update({ mailboxId })}\n error={issueAt('mailboxId')}\n disabled={archived}\n />\n </Stack>\n </CardDisplay>\n\n {listIssue ? <Alert severity=\"error\">{listIssue}</Alert> : null}\n {draft.steps.map((step, index) => (\n <OutreachStepCard\n key={step.id || `step-${index}`}\n step={step}\n index={index}\n count={draft.steps.length}\n firstEmail={\n draft.steps.findIndex((entry) => entry.kind === 'email') ===\n index\n }\n inThread={isInThreadEmailStep(draft.steps, index)}\n threadSubject={threadSubject(index)}\n templates={templates.data}\n issues={stepIssues(index)}\n disabled={archived}\n onChange={(next) => setStep(index, next)}\n onMove={(direction) => moveStep(index, direction)}\n onRemove={() =>\n update({\n steps: draft.steps.filter(\n (_entry, position) => position !== index,\n ),\n })\n }\n />\n ))}\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ flexWrap: 'wrap', rowGap: 1 }}\n >\n <Button\n variant=\"outlined\"\n startIcon={<MdiIcon path={mdiPlus.path} fontSize=\"small\" />}\n disabled={\n archived || full || emailCount >= OUTREACH_MAX_EMAIL_STEPS\n }\n onClick={() =>\n update({\n steps: [...draft.steps, newOutreachEmailStep(draft.steps)],\n })\n }\n >\n Add email\n </Button>\n <Button\n variant=\"outlined\"\n startIcon={<MdiIcon path={mdiPlus.path} fontSize=\"small\" />}\n disabled={archived || full}\n onClick={(event) => setTaskAnchor(event.currentTarget)}\n aria-haspopup=\"menu\"\n >\n Add task\n </Button>\n <Menu\n anchorEl={taskAnchor}\n open={Boolean(taskAnchor)}\n onClose={() => setTaskAnchor(null)}\n >\n {OUTREACH_TASK_KINDS.map((kind: OutreachTaskKind) => (\n <MenuItem\n key={kind}\n onClick={() => {\n setTaskAnchor(null)\n update({\n steps: [\n ...draft.steps,\n newOutreachTaskStep(draft.steps, kind),\n ],\n })\n }}\n >\n {OUTREACH_TASK_KIND_LABELS[kind]}\n </MenuItem>\n ))}\n </Menu>\n </Stack>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Up to ${OUTREACH_MAX_STEPS} steps, ${OUTREACH_MAX_EMAIL_STEPS} of them emails.`}\n </Typography>\n\n <CardDisplay\n header=\"Who it sends to, and when\"\n help={pluginDocsHelp('sequences', {\n anchor: '#build-a-sequence',\n excerpt:\n 'A sequence sends only to the countries both it and your organization allow, and in its mailbox’s sending hours unless it has its own.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Autocomplete<string, true>\n multiple\n options={countryOptions}\n value={draft.settings.allowedCountries}\n getOptionLabel={outreachCountryLabel}\n onChange={(_event, next) =>\n update({\n settings: { ...draft.settings, allowedCountries: next },\n })\n }\n disabled={archived}\n renderValue={(value, getItemProps) =>\n value.map((code, index) => {\n const { key, ...item } = getItemProps({ index })\n return (\n <Chip\n key={key}\n size=\"small\"\n label={outreachCountryLabel(code)}\n {...item}\n />\n )\n })\n }\n renderInput={(params) => (\n <TextField\n {...params}\n label=\"Countries\"\n error={Boolean(countriesIssue)}\n helperText={\n countriesIssue ??\n 'Among the countries your organization allows. Cold contacts are emailed only in the United States.'\n }\n />\n )}\n />\n <FormControlLabel\n control={\n <Switch\n checked={draft.settings.allowCustomers}\n onChange={(event) =>\n update({\n settings: {\n ...draft.settings,\n allowCustomers: event.target.checked,\n },\n })\n }\n disabled={archived}\n />\n }\n label=\"Include customers\"\n />\n <OutreachSendWindowFields\n window={draft.settings.window}\n onChange={(window) =>\n update({ settings: { ...draft.settings, window } })\n }\n daysIssue={issueAt('settings.window.days')}\n hoursIssue={issueAt('settings.window')}\n disabled={archived}\n />\n </Stack>\n </CardDisplay>\n\n {archived ? null : (\n <Stack\n direction={{ xs: 'column', sm: 'row' }}\n spacing={1}\n sx={{ justifyContent: 'flex-end' }}\n >\n {props.onCancel ? (\n <Button\n variant=\"text\"\n onClick={props.onCancel}\n disabled={saving}\n >\n Cancel\n </Button>\n ) : null}\n <Button\n variant=\"contained\"\n onClick={() => void save()}\n disabled={saving}\n >\n {saving ? 'Saving…' : sequence ? 'Save' : 'Create sequence'}\n </Button>\n </Stack>\n )}\n </Stack>\n </Grid>\n <Grid size={{ xs: 12, md: 5 }}>\n <Box sx={{ position: { md: 'sticky' }, top: { md: 16 } }}>\n <OutreachSequencePreview\n steps={draft.steps}\n orgSettings={settings.settings}\n orgSettingsStatus={settings.status}\n templates={templates.data}\n sender={\n mailbox\n ? {\n name: mailbox.displayName,\n email: mailbox.sendAs || mailbox.email,\n }\n : null\n }\n siteName={siteName}\n />\n </Box>\n </Grid>\n </Grid>\n )\n}\nOutreachSequenceEditor.displayName = 'OutreachSequenceEditor'\n\nexport default OutreachSequenceEditor\n"],"names":["pluginDocsHelp","mdiPlus","CardDisplay","MdiIcon","useSnackbar","useUser","Alert","Autocomplete","Box","Button","Chip","FormControlLabel","Grid","Menu","MenuItem","Stack","Switch","TextField","Typography","useEffect","useMemo","useState","isInThreadEmailStep","OUTREACH_SEQUENCE_NAME_MAX","validateOutreachSequence","outreachCountryLabel","OUTREACH_MAX_EMAIL_STEPS","OUTREACH_MAX_STEPS","OUTREACH_TASK_KIND_LABELS","OUTREACH_TASK_KINDS","emptyOutreachSequenceDraft","newOutreachEmailStep","newOutreachTaskStep","outreachSequenceDraftOf","OutreachSendWindowFields","OutreachSequenceMailboxPicker","OutreachSequencePreview","OutreachStepCard","OutreachRouteError","useOutreachApi","useOutreachEmailTemplates","stepIssueKey","path","match","exec","field","index","Number","OutreachSequenceEditor","props","issueAt","settings","issues","hosts","orgId","orgMount","sequence","api","enqueueSnackbar","data","user","uid","mountedHosts","orgCountries","allowedCountries","draft","setDraft","hostId","length","id","attempted","setAttempted","serverIssues","setServerIssues","saving","setSaving","taskAnchor","setTaskAnchor","settingsCountries","join","previous","split","ownSending","mailboxes","filter","entry","connectedByUid","status","onlyOwnMailbox","mailboxId","templates","mailbox","find","archived","judged","issue","severity","includes","message","stepIssues","found","key","listIssue","countriesIssue","startsWith","update","next","setStep","step","steps","map","position","moveStep","direction","moved","splice","emailCount","kind","full","threadSubject","subject","save","some","answer","saveSequence","created","variant","onSaved","error","allowDuplicate","siteName","host","name","countryOptions","Set","container","spacing","size","xs","md","header","help","anchor","contentGutterX","contentGutterY","label","value","onChange","event","target","disabled","Boolean","helperText","slotProps","htmlInput","maxLength","fullWidth","select","mailboxesPath","count","firstEmail","findIndex","inThread","onMove","onRemove","_entry","sx","flexWrap","rowGap","startIcon","fontSize","onClick","currentTarget","aria-haspopup","anchorEl","open","onClose","color","excerpt","multiple","options","getOptionLabel","_event","renderValue","getItemProps","code","item","renderInput","params","control","checked","allowCustomers","window","daysIssue","hoursIssue","sm","justifyContent","onCancel","top","orgSettings","orgSettingsStatus","sender","displayName","email","sendAs"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/sequence-editor.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { pluginDocsHelp, type ConsolePluginOrgMount } from '@aglyn/aglyn'\nimport { mdiPlus } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Autocomplete,\n Box,\n Button,\n Chip,\n FormControlLabel,\n Grid,\n Menu,\n MenuItem,\n Stack,\n Switch,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useEffect, useMemo, useState } from 'react'\nimport {\n isInThreadEmailStep,\n OUTREACH_SEQUENCE_NAME_MAX,\n validateOutreachSequence,\n} from '../engine/sequence-validation'\nimport { outreachCountryLabel } from '../model/compliance-settings'\nimport {\n OUTREACH_MAX_EMAIL_STEPS,\n OUTREACH_MAX_STEPS,\n OUTREACH_TASK_KIND_LABELS,\n OUTREACH_TASK_KINDS,\n type OutreachSequence,\n type OutreachSequenceStep,\n type OutreachTaskKind,\n} from '../model/outreach.types'\nimport {\n emptyOutreachSequenceDraft,\n newOutreachEmailStep,\n newOutreachTaskStep,\n outreachSequenceDraftOf,\n type OutreachSequenceDraft,\n type OutreachSequenceIssue,\n} from '../model/sequence-draft'\nimport { OutreachSendWindowFields } from './send-window-fields'\nimport { OutreachSequenceMailboxPicker } from './sequence-mailbox-picker'\nimport { OutreachSequencePreview } from './sequence-preview'\nimport { OutreachStepCard } from './sequence-step-card'\nimport { OutreachRouteError, useOutreachApi } from './use-outreach-api'\nimport { useOutreachEmailTemplates } from './use-outreach-crm'\nimport type { OutreachMailboxesResult } from './use-outreach-mailboxes'\nimport type { OutreachSettingsLoad } from './use-outreach-settings'\n\nexport interface OutreachSequenceEditorProps {\n orgId: string\n orgMount?: ConsolePluginOrgMount\n /** The stored sequence, or `null` for a new one. */\n sequence: OutreachSequence | null\n /** The organization's compliance settings: its countries, and the preview's footer. */\n settings: OutreachSettingsLoad\n /**\n * The organization's mailboxes, from the Mailboxes section's listener:\n * what the picker offers, and who the preview's emails are from.\n */\n mailboxes: OutreachMailboxesResult\n /** The Mailboxes section, where the picker sends a member to connect one. */\n mailboxesPath: string\n onSaved(sequence: OutreachSequence): void\n onCancel?(): void\n}\n\n/** The step a dotted issue path is about, and the field within it. */\nfunction stepIssueKey(path: string): { index: number; field: string } | null {\n const match = /^steps\\.(\\d+)(?:\\.(.+))?$/.exec(path)\n if (!match) return null\n const field = match[2] ?? 'step'\n return { index: Number(match[1]), field: field === 'id' ? 'step' : field }\n}\n\n/**\n * The sequence editor (AGL-2980): the name, the site and mailbox, the steps,\n * who it sends to and when, beside a live preview of each email.\n *\n * Every issue the engine's validator finds is shown beside its field — the\n * same validator the save route refuses with — errors once a save was\n * attempted, warnings as soon as they appear. What only the server can\n * judge (whose mailbox, which countries the organization allows, what may\n * no longer change once people are enrolled) comes back from the save and\n * is shown in the same places.\n */\nexport function OutreachSequenceEditor(props: OutreachSequenceEditorProps) {\n const { orgId, orgMount, sequence, settings } = props\n const api = useOutreachApi(orgId)\n const { enqueueSnackbar } = useSnackbar()\n const { data: user } = useUser()\n const uid = user?.uid ?? null\n const mountedHosts = orgMount?.hosts\n const hosts = useMemo(() => mountedHosts ?? [], [mountedHosts])\n const orgCountries = settings.settings?.allowedCountries ?? ['US']\n\n const [draft, setDraft] = useState<OutreachSequenceDraft>(() =>\n sequence\n ? outreachSequenceDraftOf(sequence)\n : emptyOutreachSequenceDraft({\n hostId: hosts.length === 1 ? hosts[0].id : '',\n allowedCountries: orgCountries,\n }),\n )\n const [attempted, setAttempted] = useState(false)\n const [serverIssues, setServerIssues] = useState<OutreachSequenceIssue[]>([])\n const [saving, setSaving] = useState(false)\n const [taskAnchor, setTaskAnchor] = useState<HTMLElement | null>(null)\n\n // A stored sequence the listener updated (a save, a status change) is the\n // new starting point.\n useEffect(() => {\n if (sequence) setDraft(outreachSequenceDraftOf(sequence))\n }, [sequence])\n // A new sequence starts on the organization's countries once they are read.\n const settingsCountries = settings.settings?.allowedCountries.join(',')\n useEffect(() => {\n if (!sequence && settingsCountries) {\n setDraft((previous) => ({\n ...previous,\n settings: {\n ...previous.settings,\n allowedCountries: settingsCountries.split(','),\n },\n }))\n }\n }, [sequence, settingsCountries])\n useEffect(() => {\n if (!sequence && !draft.hostId && hosts.length === 1)\n setDraft((previous) => ({ ...previous, hostId: hosts[0].id }))\n }, [sequence, draft.hostId, hosts])\n\n // A new sequence starts on the member's own mailbox when they have exactly\n // one that is sending.\n const ownSending = props.mailboxes.mailboxes.filter(\n (entry) => entry.connectedByUid === uid && entry.status === 'connected',\n )\n const onlyOwnMailbox = ownSending.length === 1 ? ownSending[0].id : ''\n useEffect(() => {\n if (!sequence && !draft.mailboxId && onlyOwnMailbox)\n setDraft((previous) => ({ ...previous, mailboxId: onlyOwnMailbox }))\n }, [sequence, draft.mailboxId, onlyOwnMailbox])\n\n const templates = useOutreachEmailTemplates(orgId, draft.hostId || null, uid)\n const mailbox =\n props.mailboxes.mailboxes.find((entry) => entry.id === draft.mailboxId) ??\n null\n const archived = sequence?.status === 'archived'\n\n const issues = useMemo(() => {\n const judged = [\n ...validateOutreachSequence(draft),\n ...serverIssues,\n ] as OutreachSequenceIssue[]\n return judged.filter(\n (issue) =>\n issue.severity === 'warning' ||\n attempted ||\n serverIssues.includes(issue),\n )\n }, [draft, serverIssues, attempted])\n const issueAt = (path: string) =>\n issues.find((issue) => issue.path === path)?.message\n const stepIssues = (index: number) => {\n const found: Record<string, string> = {}\n for (const issue of issues) {\n const key = stepIssueKey(issue.path)\n if (key?.index === index && !found[key.field])\n found[key.field] = issue.message\n }\n return found\n }\n const listIssue = issueAt('steps')\n const countriesIssue = issues.find((issue) =>\n issue.path.startsWith('settings.allowedCountries'),\n )?.message\n\n const update = (next: Partial<OutreachSequenceDraft>) => {\n setServerIssues([])\n setDraft((previous) => ({ ...previous, ...next }))\n }\n const setStep = (index: number, step: OutreachSequenceStep) =>\n update({\n steps: draft.steps.map((entry, position) =>\n position === index ? step : entry,\n ),\n })\n const moveStep = (index: number, direction: -1 | 1) => {\n const steps = [...draft.steps]\n const [moved] = steps.splice(index, 1)\n steps.splice(index + direction, 0, moved)\n update({ steps })\n }\n const emailCount = draft.steps.filter((step) => step.kind === 'email').length\n const full = draft.steps.length >= OUTREACH_MAX_STEPS\n const threadSubject = (index: number) => {\n for (let position = index - 1; position >= 0; position -= 1) {\n const step = draft.steps[position]\n if (step.kind === 'email' && !isInThreadEmailStep(draft.steps, position))\n return step.subject\n }\n return ''\n }\n\n const save = async () => {\n setAttempted(true)\n if (\n validateOutreachSequence(draft).some(\n (issue) => issue.severity === 'error',\n )\n )\n return\n setSaving(true)\n try {\n const answer = await api.saveSequence(sequence?.id ?? null, draft)\n setServerIssues([])\n setAttempted(false)\n enqueueSnackbar(\n answer.created ? 'Sequence created.' : 'Sequence saved.',\n { variant: 'success' },\n )\n props.onSaved(answer.sequence)\n } catch (error) {\n if (error instanceof OutreachRouteError && error.issues.length) {\n setServerIssues(\n error.issues.filter(\n (issue): issue is OutreachSequenceIssue => 'path' in issue,\n ),\n )\n }\n enqueueSnackbar((error as Error).message, {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setSaving(false)\n }\n }\n\n const siteName = hosts.find((host) => host.id === draft.hostId)?.name ?? ''\n const countryOptions = [\n ...new Set([...orgCountries, ...draft.settings.allowedCountries]),\n ]\n\n return (\n <Grid container spacing={2}>\n <Grid size={{ xs: 12, md: 7 }}>\n <Stack spacing={2}>\n {archived ? (\n <Alert severity=\"info\">\n This sequence is archived, so it can’t be edited.\n </Alert>\n ) : null}\n <CardDisplay\n header=\"Sequence\"\n help={pluginDocsHelp('sequences', { anchor: '#build-a-sequence' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <TextField\n label=\"Name\"\n value={draft.name}\n onChange={(event) => update({ name: event.target.value })}\n disabled={archived}\n error={Boolean(issueAt('name'))}\n helperText={issueAt('name')}\n slotProps={{\n htmlInput: { maxLength: OUTREACH_SEQUENCE_NAME_MAX + 20 },\n }}\n fullWidth\n />\n <TextField\n select\n label=\"Site\"\n value={\n hosts.some((host) => host.id === draft.hostId)\n ? draft.hostId\n : ''\n }\n onChange={(event) => update({ hostId: event.target.value })}\n disabled={archived || !hosts.length}\n error={Boolean(issueAt('hostId'))}\n helperText={\n issueAt('hostId') ??\n 'The site whose contacts this sequence emails.'\n }\n fullWidth\n >\n {hosts.map((host) => (\n <MenuItem key={host.id} value={host.id}>\n {host.name}\n </MenuItem>\n ))}\n </TextField>\n <OutreachSequenceMailboxPicker\n orgId={orgId}\n uid={uid}\n mailboxes={props.mailboxes}\n mailboxesPath={props.mailboxesPath}\n value={draft.mailboxId}\n onChange={(mailboxId) => update({ mailboxId })}\n error={issueAt('mailboxId')}\n disabled={archived}\n />\n </Stack>\n </CardDisplay>\n\n {listIssue ? <Alert severity=\"error\">{listIssue}</Alert> : null}\n {draft.steps.map((step, index) => (\n <OutreachStepCard\n key={step.id || `step-${index}`}\n step={step}\n index={index}\n count={draft.steps.length}\n firstEmail={\n draft.steps.findIndex((entry) => entry.kind === 'email') ===\n index\n }\n inThread={isInThreadEmailStep(draft.steps, index)}\n threadSubject={threadSubject(index)}\n templates={templates.data}\n issues={stepIssues(index)}\n disabled={archived}\n onChange={(next) => setStep(index, next)}\n onMove={(direction) => moveStep(index, direction)}\n onRemove={() =>\n update({\n steps: draft.steps.filter(\n (_entry, position) => position !== index,\n ),\n })\n }\n />\n ))}\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ flexWrap: 'wrap', rowGap: 1 }}\n >\n <Button\n variant=\"outlined\"\n startIcon={<MdiIcon path={mdiPlus.path} fontSize=\"small\" />}\n disabled={\n archived || full || emailCount >= OUTREACH_MAX_EMAIL_STEPS\n }\n onClick={() =>\n update({\n steps: [...draft.steps, newOutreachEmailStep(draft.steps)],\n })\n }\n >\n Add email\n </Button>\n <Button\n variant=\"outlined\"\n startIcon={<MdiIcon path={mdiPlus.path} fontSize=\"small\" />}\n disabled={archived || full}\n onClick={(event) => setTaskAnchor(event.currentTarget)}\n aria-haspopup=\"menu\"\n >\n Add task\n </Button>\n <Menu\n anchorEl={taskAnchor}\n open={Boolean(taskAnchor)}\n onClose={() => setTaskAnchor(null)}\n >\n {OUTREACH_TASK_KINDS.map((kind: OutreachTaskKind) => (\n <MenuItem\n key={kind}\n onClick={() => {\n setTaskAnchor(null)\n update({\n steps: [\n ...draft.steps,\n newOutreachTaskStep(draft.steps, kind),\n ],\n })\n }}\n >\n {OUTREACH_TASK_KIND_LABELS[kind]}\n </MenuItem>\n ))}\n </Menu>\n </Stack>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Up to ${OUTREACH_MAX_STEPS} steps, ${OUTREACH_MAX_EMAIL_STEPS} of them emails.`}\n </Typography>\n\n <CardDisplay\n header=\"Who it sends to, and when\"\n help={pluginDocsHelp('sequences', {\n anchor: '#build-a-sequence',\n excerpt:\n 'A sequence sends only to the countries both it and your organization allow, and in its mailbox’s sending hours unless it has its own.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Autocomplete<string, true>\n multiple\n options={countryOptions}\n value={draft.settings.allowedCountries}\n getOptionLabel={outreachCountryLabel}\n onChange={(_event, next) =>\n update({\n settings: { ...draft.settings, allowedCountries: next },\n })\n }\n disabled={archived}\n renderValue={(value, getItemProps) =>\n value.map((code, index) => {\n const { key, ...item } = getItemProps({ index })\n return (\n <Chip\n key={key}\n size=\"small\"\n label={outreachCountryLabel(code)}\n {...item}\n />\n )\n })\n }\n renderInput={(params) => (\n <TextField\n {...params}\n label=\"Countries\"\n error={Boolean(countriesIssue)}\n helperText={\n countriesIssue ??\n 'Among the countries your organization allows. Cold contacts are emailed only in the United States.'\n }\n />\n )}\n />\n <FormControlLabel\n control={\n <Switch\n checked={draft.settings.allowCustomers}\n onChange={(event) =>\n update({\n settings: {\n ...draft.settings,\n allowCustomers: event.target.checked,\n },\n })\n }\n disabled={archived}\n />\n }\n label=\"Include customers\"\n />\n {/*\n * Click tracking (AGL-3239), with its cost stated where it is\n * chosen rather than in a document nobody opens. Off by\n * default: it changes the link a cold recipient sees, and the\n * emails stay plain text either way — there is no open\n * tracking to turn on, because a tracking image needs an HTML\n * email and these are not.\n */}\n <Stack spacing={0}>\n <FormControlLabel\n control={\n <Switch\n checked={draft.settings.trackClicks}\n onChange={(event) =>\n update({\n settings: {\n ...draft.settings,\n trackClicks: event.target.checked,\n },\n })\n }\n disabled={archived}\n />\n }\n label=\"Count link clicks\"\n />\n <Typography variant=\"caption\" color=\"text.secondary\">\n Links in these emails are replaced with links of ours that\n forward to the same page, so clicks can be counted. The\n recipient sees the replacement, not your address. Opens\n aren’t counted either way: these emails are plain text, and\n counting an open needs a tracking image.\n </Typography>\n </Stack>\n <OutreachSendWindowFields\n window={draft.settings.window}\n onChange={(window) =>\n update({ settings: { ...draft.settings, window } })\n }\n daysIssue={issueAt('settings.window.days')}\n hoursIssue={issueAt('settings.window')}\n disabled={archived}\n />\n </Stack>\n </CardDisplay>\n\n {archived ? null : (\n <Stack\n direction={{ xs: 'column', sm: 'row' }}\n spacing={1}\n sx={{ justifyContent: 'flex-end' }}\n >\n {props.onCancel ? (\n <Button\n variant=\"text\"\n onClick={props.onCancel}\n disabled={saving}\n >\n Cancel\n </Button>\n ) : null}\n <Button\n variant=\"contained\"\n onClick={() => void save()}\n disabled={saving}\n >\n {saving ? 'Saving…' : sequence ? 'Save' : 'Create sequence'}\n </Button>\n </Stack>\n )}\n </Stack>\n </Grid>\n <Grid size={{ xs: 12, md: 5 }}>\n <Box sx={{ position: { md: 'sticky' }, top: { md: 16 } }}>\n <OutreachSequencePreview\n steps={draft.steps}\n orgSettings={settings.settings}\n orgSettingsStatus={settings.status}\n templates={templates.data}\n sender={\n mailbox\n ? {\n name: mailbox.displayName,\n email: mailbox.sendAs || mailbox.email,\n }\n : null\n }\n siteName={siteName}\n />\n </Box>\n </Grid>\n </Grid>\n )\n}\nOutreachSequenceEditor.displayName = 'OutreachSequenceEditor'\n\nexport default OutreachSequenceEditor\n"],"names":["pluginDocsHelp","mdiPlus","CardDisplay","MdiIcon","useSnackbar","useUser","Alert","Autocomplete","Box","Button","Chip","FormControlLabel","Grid","Menu","MenuItem","Stack","Switch","TextField","Typography","useEffect","useMemo","useState","isInThreadEmailStep","OUTREACH_SEQUENCE_NAME_MAX","validateOutreachSequence","outreachCountryLabel","OUTREACH_MAX_EMAIL_STEPS","OUTREACH_MAX_STEPS","OUTREACH_TASK_KIND_LABELS","OUTREACH_TASK_KINDS","emptyOutreachSequenceDraft","newOutreachEmailStep","newOutreachTaskStep","outreachSequenceDraftOf","OutreachSendWindowFields","OutreachSequenceMailboxPicker","OutreachSequencePreview","OutreachStepCard","OutreachRouteError","useOutreachApi","useOutreachEmailTemplates","stepIssueKey","path","match","exec","field","index","Number","OutreachSequenceEditor","props","issueAt","settings","issues","hosts","orgId","orgMount","sequence","api","enqueueSnackbar","data","user","uid","mountedHosts","orgCountries","allowedCountries","draft","setDraft","hostId","length","id","attempted","setAttempted","serverIssues","setServerIssues","saving","setSaving","taskAnchor","setTaskAnchor","settingsCountries","join","previous","split","ownSending","mailboxes","filter","entry","connectedByUid","status","onlyOwnMailbox","mailboxId","templates","mailbox","find","archived","judged","issue","severity","includes","message","stepIssues","found","key","listIssue","countriesIssue","startsWith","update","next","setStep","step","steps","map","position","moveStep","direction","moved","splice","emailCount","kind","full","threadSubject","subject","save","some","answer","saveSequence","created","variant","onSaved","error","allowDuplicate","siteName","host","name","countryOptions","Set","container","spacing","size","xs","md","header","help","anchor","contentGutterX","contentGutterY","label","value","onChange","event","target","disabled","Boolean","helperText","slotProps","htmlInput","maxLength","fullWidth","select","mailboxesPath","count","firstEmail","findIndex","inThread","onMove","onRemove","_entry","sx","flexWrap","rowGap","startIcon","fontSize","onClick","currentTarget","aria-haspopup","anchorEl","open","onClose","color","excerpt","multiple","options","getOptionLabel","_event","renderValue","getItemProps","code","item","renderInput","params","control","checked","allowCustomers","trackClicks","window","daysIssue","hoursIssue","sm","justifyContent","onCancel","top","orgSettings","orgSettingsStatus","sender","displayName","email","sendAs"],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { OutreachSequence } from '../model/outreach.types';
|
|
2
|
+
import type { OutreachSequenceLinksLoad } from './use-outreach-data';
|
|
3
|
+
/**
|
|
4
|
+
* WHAT A SEQUENCE MEASURED (AGL-3239).
|
|
5
|
+
*
|
|
6
|
+
* Four figures and one rate, and the sentences saying what each of them is
|
|
7
|
+
* not. The decisions are all in `model/sequence-report.ts`; this renders
|
|
8
|
+
* them and adds no arithmetic of its own — in particular it never divides,
|
|
9
|
+
* so there is no second place a percentage can be computed differently from
|
|
10
|
+
* the way a campaign's report computes it.
|
|
11
|
+
*
|
|
12
|
+
* The caveats are rendered as prominently as the numbers rather than as
|
|
13
|
+
* small print. "Opens aren't measured" is the thing a reader coming from
|
|
14
|
+
* the campaign report most needs told, and a blank where an open rate would
|
|
15
|
+
* be says the opposite of what is true.
|
|
16
|
+
*/
|
|
17
|
+
export interface OutreachSequenceReportCardProps {
|
|
18
|
+
sequence: Pick<OutreachSequence, 'stats' | 'settings'>;
|
|
19
|
+
links: OutreachSequenceLinksLoad;
|
|
20
|
+
/** The mailbox's zone, which times are read in; `null` for the reader's own. */
|
|
21
|
+
timeZone: string | null;
|
|
22
|
+
}
|
|
23
|
+
export declare function OutreachSequenceReportCard(props: OutreachSequenceReportCardProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,191 @@
|
|
|
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 { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
19
|
+
import { Alert, Card, CardContent, Divider, Stack, TableBody, TableCell, TableHead, TableRow, Typography } from "@mui/material";
|
|
20
|
+
// Not a raw MUI `Table`: a destination is a long string, and a raw table is
|
|
21
|
+
// cut off by the card around it with nothing able to scroll to what is past
|
|
22
|
+
// the edge (AGL-3045).
|
|
23
|
+
import { ScrollTable } from "@aglyn/shared-ui-jsx/components/scroll-table.component";
|
|
24
|
+
import { outreachSequenceLinkReport, outreachSequenceReport } from "../model/sequence-report.js";
|
|
25
|
+
import { formatOutreachTime, OutreachLoadProblem } from "./outreach-ui.js";
|
|
26
|
+
/** One figure, with what it counts under it. */ function Figure(props) {
|
|
27
|
+
return /*#__PURE__*/ _jsxs(Stack, {
|
|
28
|
+
spacing: 0,
|
|
29
|
+
sx: {
|
|
30
|
+
minWidth: 110
|
|
31
|
+
},
|
|
32
|
+
children: [
|
|
33
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
34
|
+
variant: "h5",
|
|
35
|
+
component: "p",
|
|
36
|
+
children: props.value
|
|
37
|
+
}),
|
|
38
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
39
|
+
variant: "body2",
|
|
40
|
+
children: props.label
|
|
41
|
+
}),
|
|
42
|
+
props.hint ? /*#__PURE__*/ _jsx(Typography, {
|
|
43
|
+
variant: "caption",
|
|
44
|
+
color: "text.secondary",
|
|
45
|
+
children: props.hint
|
|
46
|
+
}) : null
|
|
47
|
+
]
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/** A whole number as the card prints one. */ const count = (value)=>value.toLocaleString();
|
|
51
|
+
/**
|
|
52
|
+
* A rate and the denominator it was taken over, or the em dash that stands
|
|
53
|
+
* for a rate the model refused to take. The denominator is printed beside
|
|
54
|
+
* the percentage, never dropped: "12% of people emailed" and "12% of people
|
|
55
|
+
* who opened" are different claims and a bare "12%" is neither.
|
|
56
|
+
*/ function rateText(rate) {
|
|
57
|
+
if (!rate) return {
|
|
58
|
+
value: '—'
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
value: `${(rate.value * 100).toFixed(1)}%`,
|
|
62
|
+
hint: `${count(rate.numerator)} of ${count(rate.denominator)} ${rate.denominatorLabel}`
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export function OutreachSequenceReportCard(props) {
|
|
66
|
+
var _props_links_data;
|
|
67
|
+
const report = outreachSequenceReport(props.sequence.stats, props.sequence.settings.trackClicks);
|
|
68
|
+
const clickRate = rateText(report.rates.click);
|
|
69
|
+
const links = outreachSequenceLinkReport(props.links.status === 'ready' ? (_props_links_data = props.links.data) != null ? _props_links_data : undefined : undefined);
|
|
70
|
+
return /*#__PURE__*/ _jsx(Card, {
|
|
71
|
+
variant: "outlined",
|
|
72
|
+
children: /*#__PURE__*/ _jsx(CardContent, {
|
|
73
|
+
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
74
|
+
spacing: 2,
|
|
75
|
+
children: [
|
|
76
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
77
|
+
variant: "h6",
|
|
78
|
+
component: "h2",
|
|
79
|
+
children: "Results"
|
|
80
|
+
}),
|
|
81
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
82
|
+
direction: "row",
|
|
83
|
+
spacing: 3,
|
|
84
|
+
sx: {
|
|
85
|
+
flexWrap: 'wrap',
|
|
86
|
+
rowGap: 2
|
|
87
|
+
},
|
|
88
|
+
children: [
|
|
89
|
+
/*#__PURE__*/ _jsx(Figure, {
|
|
90
|
+
label: "Emails sent",
|
|
91
|
+
value: count(report.sent)
|
|
92
|
+
}),
|
|
93
|
+
/*#__PURE__*/ _jsx(Figure, _extends({
|
|
94
|
+
label: "People emailed",
|
|
95
|
+
value: report.people === null ? '—' : count(report.people)
|
|
96
|
+
}, report.people === null ? {
|
|
97
|
+
hint: 'Not counted for this sequence'
|
|
98
|
+
} : {})),
|
|
99
|
+
/*#__PURE__*/ _jsx(Figure, {
|
|
100
|
+
label: "Clicked",
|
|
101
|
+
value: count(report.uniqueClicks),
|
|
102
|
+
hint: report.clicks === report.uniqueClicks ? undefined : `${count(report.clicks)} clicks in all`
|
|
103
|
+
}),
|
|
104
|
+
/*#__PURE__*/ _jsx(Figure, {
|
|
105
|
+
label: "Click rate",
|
|
106
|
+
value: clickRate.value,
|
|
107
|
+
hint: clickRate.hint
|
|
108
|
+
}),
|
|
109
|
+
report.machineClicks > 0 ? /*#__PURE__*/ _jsx(Figure, {
|
|
110
|
+
label: "Scanner clicks",
|
|
111
|
+
value: count(report.machineClicks),
|
|
112
|
+
hint: "Not in the rate"
|
|
113
|
+
}) : null
|
|
114
|
+
]
|
|
115
|
+
}),
|
|
116
|
+
report.lastClickAtMs === null ? null : /*#__PURE__*/ _jsxs(Typography, {
|
|
117
|
+
variant: "body2",
|
|
118
|
+
color: "text.secondary",
|
|
119
|
+
children: [
|
|
120
|
+
"Last click ",
|
|
121
|
+
formatOutreachTime(report.lastClickAtMs, props.timeZone)
|
|
122
|
+
]
|
|
123
|
+
}),
|
|
124
|
+
report.caveats.map((entry)=>/*#__PURE__*/ _jsx(Alert, {
|
|
125
|
+
severity: "info",
|
|
126
|
+
variant: "outlined",
|
|
127
|
+
children: entry.message
|
|
128
|
+
}, entry.id)),
|
|
129
|
+
props.links.status === 'error' || props.links.status === 'refused' ? /*#__PURE__*/ _jsx(OutreachLoadProblem, {
|
|
130
|
+
status: props.links.status,
|
|
131
|
+
what: "the link report"
|
|
132
|
+
}) : links.rows.length ? /*#__PURE__*/ _jsxs(Stack, {
|
|
133
|
+
spacing: 1,
|
|
134
|
+
children: [
|
|
135
|
+
/*#__PURE__*/ _jsx(Divider, {}),
|
|
136
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
137
|
+
variant: "subtitle2",
|
|
138
|
+
component: "h3",
|
|
139
|
+
children: "Links followed"
|
|
140
|
+
}),
|
|
141
|
+
/*#__PURE__*/ _jsxs(ScrollTable, {
|
|
142
|
+
size: "small",
|
|
143
|
+
children: [
|
|
144
|
+
/*#__PURE__*/ _jsx(TableHead, {
|
|
145
|
+
children: /*#__PURE__*/ _jsxs(TableRow, {
|
|
146
|
+
children: [
|
|
147
|
+
/*#__PURE__*/ _jsx(TableCell, {
|
|
148
|
+
children: "Destination"
|
|
149
|
+
}),
|
|
150
|
+
/*#__PURE__*/ _jsx(TableCell, {
|
|
151
|
+
align: "right",
|
|
152
|
+
children: "Clicks"
|
|
153
|
+
})
|
|
154
|
+
]
|
|
155
|
+
})
|
|
156
|
+
}),
|
|
157
|
+
/*#__PURE__*/ _jsx(TableBody, {
|
|
158
|
+
children: links.rows.map((row)=>/*#__PURE__*/ _jsxs(TableRow, {
|
|
159
|
+
children: [
|
|
160
|
+
/*#__PURE__*/ _jsx(TableCell, {
|
|
161
|
+
sx: {
|
|
162
|
+
wordBreak: 'break-all'
|
|
163
|
+
},
|
|
164
|
+
children: row.url
|
|
165
|
+
}),
|
|
166
|
+
/*#__PURE__*/ _jsx(TableCell, {
|
|
167
|
+
align: "right",
|
|
168
|
+
children: count(row.clicks)
|
|
169
|
+
})
|
|
170
|
+
]
|
|
171
|
+
}, row.url))
|
|
172
|
+
})
|
|
173
|
+
]
|
|
174
|
+
}),
|
|
175
|
+
/*#__PURE__*/ _jsxs(Typography, {
|
|
176
|
+
variant: "caption",
|
|
177
|
+
color: "text.secondary",
|
|
178
|
+
children: [
|
|
179
|
+
"Two links to the same page are counted as one destination, whatever tracking parameters follow the address.",
|
|
180
|
+
links.truncated ? ` ${count(links.overflowClicks)} more clicks went to destinations past the ones listed.` : ''
|
|
181
|
+
]
|
|
182
|
+
})
|
|
183
|
+
]
|
|
184
|
+
}) : null
|
|
185
|
+
]
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
//# sourceMappingURL=sequence-report-card.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/sequence-report-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport type { CampaignRate } from '@aglyn/shared-ui-email-campaigns/model'\nimport {\n Alert,\n Card,\n CardContent,\n Divider,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\n// Not a raw MUI `Table`: a destination is a long string, and a raw table is\n// cut off by the card around it with nothing able to scroll to what is past\n// the edge (AGL-3045).\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport type { OutreachSequence } from '../model/outreach.types'\nimport {\n outreachSequenceLinkReport,\n outreachSequenceReport,\n} from '../model/sequence-report'\nimport { formatOutreachTime, OutreachLoadProblem } from './outreach-ui'\nimport type { OutreachSequenceLinksLoad } from './use-outreach-data'\n\n/**\n * WHAT A SEQUENCE MEASURED (AGL-3239).\n *\n * Four figures and one rate, and the sentences saying what each of them is\n * not. The decisions are all in `model/sequence-report.ts`; this renders\n * them and adds no arithmetic of its own — in particular it never divides,\n * so there is no second place a percentage can be computed differently from\n * the way a campaign's report computes it.\n *\n * The caveats are rendered as prominently as the numbers rather than as\n * small print. \"Opens aren't measured\" is the thing a reader coming from\n * the campaign report most needs told, and a blank where an open rate would\n * be says the opposite of what is true.\n */\n\nexport interface OutreachSequenceReportCardProps {\n sequence: Pick<OutreachSequence, 'stats' | 'settings'>\n links: OutreachSequenceLinksLoad\n /** The mailbox's zone, which times are read in; `null` for the reader's own. */\n timeZone: string | null\n}\n\n/** One figure, with what it counts under it. */\nfunction Figure(props: { label: string; value: string; hint?: string }) {\n return (\n <Stack spacing={0} sx={{ minWidth: 110 }}>\n <Typography variant=\"h5\" component=\"p\">\n {props.value}\n </Typography>\n <Typography variant=\"body2\">{props.label}</Typography>\n {props.hint ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {props.hint}\n </Typography>\n ) : null}\n </Stack>\n )\n}\n\n/** A whole number as the card prints one. */\nconst count = (value: number): string => value.toLocaleString()\n\n/**\n * A rate and the denominator it was taken over, or the em dash that stands\n * for a rate the model refused to take. The denominator is printed beside\n * the percentage, never dropped: \"12% of people emailed\" and \"12% of people\n * who opened\" are different claims and a bare \"12%\" is neither.\n */\nfunction rateText(rate: CampaignRate | null): { value: string; hint?: string } {\n if (!rate) return { value: '—' }\n return {\n value: `${(rate.value * 100).toFixed(1)}%`,\n hint: `${count(rate.numerator)} of ${count(rate.denominator)} ${rate.denominatorLabel}`,\n }\n}\n\nexport function OutreachSequenceReportCard(props: OutreachSequenceReportCardProps) {\n const report = outreachSequenceReport(\n props.sequence.stats,\n props.sequence.settings.trackClicks,\n )\n const clickRate = rateText(report.rates.click)\n const links = outreachSequenceLinkReport(\n props.links.status === 'ready' ? (props.links.data ?? undefined) : undefined,\n )\n\n return (\n <Card variant=\"outlined\">\n <CardContent>\n <Stack spacing={2}>\n <Typography variant=\"h6\" component=\"h2\">\n Results\n </Typography>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap', rowGap: 2 }}>\n <Figure label=\"Emails sent\" value={count(report.sent)} />\n <Figure\n label=\"People emailed\"\n value={report.people === null ? '—' : count(report.people)}\n {...(report.people === null ? { hint: 'Not counted for this sequence' } : {})}\n />\n <Figure\n label=\"Clicked\"\n value={count(report.uniqueClicks)}\n hint={\n report.clicks === report.uniqueClicks\n ? undefined\n : `${count(report.clicks)} clicks in all`\n }\n />\n <Figure label=\"Click rate\" value={clickRate.value} hint={clickRate.hint} />\n {report.machineClicks > 0 ? (\n <Figure\n label=\"Scanner clicks\"\n value={count(report.machineClicks)}\n hint=\"Not in the rate\"\n />\n ) : null}\n </Stack>\n {report.lastClickAtMs === null ? null : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n Last click {formatOutreachTime(report.lastClickAtMs, props.timeZone)}\n </Typography>\n )}\n\n {report.caveats.map((entry) => (\n <Alert key={entry.id} severity=\"info\" variant=\"outlined\">\n {entry.message}\n </Alert>\n ))}\n\n {props.links.status === 'error' || props.links.status === 'refused' ? (\n <OutreachLoadProblem status={props.links.status} what=\"the link report\" />\n ) : links.rows.length ? (\n <Stack spacing={1}>\n <Divider />\n <Typography variant=\"subtitle2\" component=\"h3\">\n Links followed\n </Typography>\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>Destination</TableCell>\n <TableCell align=\"right\">Clicks</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {links.rows.map((row) => (\n <TableRow key={row.url}>\n <TableCell sx={{ wordBreak: 'break-all' }}>{row.url}</TableCell>\n <TableCell align=\"right\">{count(row.clicks)}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n <Typography variant=\"caption\" color=\"text.secondary\">\n Two links to the same page are counted as one destination, whatever\n tracking parameters follow the address.\n {links.truncated\n ? ` ${count(links.overflowClicks)} more clicks went to destinations past the ones listed.`\n : ''}\n </Typography>\n </Stack>\n ) : null}\n </Stack>\n </CardContent>\n </Card>\n )\n}\n"],"names":["Alert","Card","CardContent","Divider","Stack","TableBody","TableCell","TableHead","TableRow","Typography","ScrollTable","outreachSequenceLinkReport","outreachSequenceReport","formatOutreachTime","OutreachLoadProblem","Figure","props","spacing","sx","minWidth","variant","component","value","label","hint","color","count","toLocaleString","rateText","rate","toFixed","numerator","denominator","denominatorLabel","OutreachSequenceReportCard","report","sequence","stats","settings","trackClicks","clickRate","rates","click","links","status","data","undefined","direction","flexWrap","rowGap","sent","people","uniqueClicks","clicks","machineClicks","lastClickAtMs","timeZone","caveats","map","entry","severity","message","id","what","rows","length","size","align","row","wordBreak","url","truncated","overflowClicks"],"mappings":"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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CampaignLinkRollup } from '@aglyn/shared-ui-email-campaigns/model';
|
|
1
2
|
import { type OutreachEnrollment, type OutreachSequence } from '../model/outreach.types';
|
|
2
3
|
/**
|
|
3
4
|
* The Outreach documents the console reads LIVE (AGL-2980): sequences,
|
|
@@ -23,6 +24,16 @@ export declare const OUTREACH_SEQUENCES_LIMIT = 200;
|
|
|
23
24
|
export declare function useOutreachSequences(orgId: string | null): OutreachLoad<OutreachSequence[]>;
|
|
24
25
|
/** One sequence, or `null` once it is known not to exist. */
|
|
25
26
|
export declare function useOutreachSequence(orgId: string | null, sequenceId: string | null): OutreachLoad<OutreachSequence | null>;
|
|
27
|
+
/**
|
|
28
|
+
* A sequence's per-destination click rollup (AGL-3239), or `null` until a
|
|
29
|
+
* click has been counted.
|
|
30
|
+
*
|
|
31
|
+
* One document, whatever the sequence's size: the aggregate exists so the
|
|
32
|
+
* table is not a read of every enrollment. A sequence that tracks nothing
|
|
33
|
+
* never has one, and an absent document is not an error.
|
|
34
|
+
*/
|
|
35
|
+
export type OutreachSequenceLinksLoad = OutreachLoad<CampaignLinkRollup | null>;
|
|
36
|
+
export declare function useOutreachSequenceLinks(orgId: string | null, sequenceId: string | null): OutreachSequenceLinksLoad;
|
|
26
37
|
/** How many enrollments one page of the table reads; "Show more" reads another. */
|
|
27
38
|
export declare const OUTREACH_ENROLLMENTS_PAGE = 200;
|
|
28
39
|
export interface OutreachEnrollmentsLoad extends OutreachLoad<OutreachEnrollment[]> {
|
|
@@ -18,7 +18,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
18
18
|
import { useFirestore } from "@aglyn/tenant-feature-instance";
|
|
19
19
|
import { collection, doc, documentId, getCountFromServer, limit, onSnapshot, orderBy, query, where } from "firebase/firestore";
|
|
20
20
|
import { useEffect, useMemo, useState } from "react";
|
|
21
|
-
import { OUTREACH_COLLECTIONS } from "../model/outreach.types.js";
|
|
21
|
+
import { OUTREACH_COLLECTIONS, OUTREACH_LINK_ROLLUP_PATH } from "../model/outreach.types.js";
|
|
22
22
|
import { readStoredOutreachEnrollment, readStoredOutreachSequence } from "../model/stored-records.js";
|
|
23
23
|
const denied = (error)=>(error == null ? void 0 : error.code) === 'permission-denied';
|
|
24
24
|
function failed(data, error, what) {
|
|
@@ -83,6 +83,29 @@ function failed(data, error, what) {
|
|
|
83
83
|
]);
|
|
84
84
|
return result;
|
|
85
85
|
}
|
|
86
|
+
export function useOutreachSequenceLinks(orgId, sequenceId) {
|
|
87
|
+
const firestore = useFirestore();
|
|
88
|
+
const [result, setResult] = useState({
|
|
89
|
+
status: 'loading',
|
|
90
|
+
data: null
|
|
91
|
+
});
|
|
92
|
+
useEffect(()=>{
|
|
93
|
+
setResult({
|
|
94
|
+
status: 'loading',
|
|
95
|
+
data: null
|
|
96
|
+
});
|
|
97
|
+
if (!orgId || !sequenceId) return undefined;
|
|
98
|
+
return onSnapshot(doc(firestore, 'orgs', orgId, OUTREACH_COLLECTIONS.sequences, sequenceId, OUTREACH_LINK_ROLLUP_PATH[0], OUTREACH_LINK_ROLLUP_PATH[1]), (snapshot)=>setResult({
|
|
99
|
+
status: 'ready',
|
|
100
|
+
data: snapshot.exists() ? snapshot.data() : null
|
|
101
|
+
}), (error)=>setResult(failed(null, error, 'the link report')));
|
|
102
|
+
}, [
|
|
103
|
+
firestore,
|
|
104
|
+
orgId,
|
|
105
|
+
sequenceId
|
|
106
|
+
]);
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
86
109
|
/** How many enrollments one page of the table reads; "Show more" reads another. */ export const OUTREACH_ENROLLMENTS_PAGE = 200;
|
|
87
110
|
/**
|
|
88
111
|
* A sequence's enrollments, the most recently enrolled first.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/use-outreach-data.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport {\n collection,\n doc,\n documentId,\n getCountFromServer,\n limit,\n onSnapshot,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useEffect, useMemo, useState } from 'react'\nimport {\n OUTREACH_COLLECTIONS,\n type OutreachEnrollment,\n type OutreachEnrollmentStatus,\n type OutreachSequence,\n} from '../model/outreach.types'\nimport {\n readStoredOutreachEnrollment,\n readStoredOutreachSequence,\n} from '../model/stored-records'\n\n/**\n * The Outreach documents the console reads LIVE (AGL-2980): sequences,\n * enrollments, and the mailboxes they send from.\n *\n * Read straight from Firestore — the rules let an org-wide member holding\n * `outreach.use` on an entitled organization read them, and every write goes\n * through a route — so what a page shows is what the routes and the sending\n * runtime wrote, the moment it lands.\n *\n * Every hook answers one of four states, and a page renders each: still\n * loading, ready, failed, or REFUSED — the rules said no, which is a reader\n * who may not see Outreach here rather than something that went wrong.\n */\n\nexport type OutreachLoadStatus = 'loading' | 'ready' | 'error' | 'refused'\n\nexport interface OutreachLoad<T> {\n status: OutreachLoadStatus\n data: T\n}\n\nconst denied = (error: unknown) =>\n (error as { code?: unknown } | null)?.code === 'permission-denied'\n\nfunction failed<T>(data: T, error: unknown, what: string): OutreachLoad<T> {\n if (denied(error)) return { status: 'refused', data }\n console.error(`[outreach] ${what} could not be read`, error)\n return { status: 'error', data }\n}\n\n/** The most sequences the list reads. */\nexport const OUTREACH_SEQUENCES_LIMIT = 200\n\n/** The organization's sequences, newest first. */\nexport function useOutreachSequences(\n orgId: string | null,\n): OutreachLoad<OutreachSequence[]> {\n const firestore = useFirestore()\n const [result, setResult] = useState<OutreachLoad<OutreachSequence[]>>({\n status: 'loading',\n data: [],\n })\n useEffect(() => {\n setResult({ status: 'loading', data: [] })\n if (!orgId) return undefined\n return onSnapshot(\n query(\n collection(firestore, 'orgs', orgId, OUTREACH_COLLECTIONS.sequences),\n // Ordered, so the cap keeps the newest rather than an arbitrary set;\n // every sequence carries `createdAtMs`, stamped by the save route.\n orderBy('createdAtMs', 'desc'),\n limit(OUTREACH_SEQUENCES_LIMIT),\n ),\n (snapshot) => {\n const sequences = snapshot.docs\n .map((entry) => readStoredOutreachSequence(entry.id, entry.data()))\n .filter((sequence): sequence is OutreachSequence => sequence !== null)\n .sort((a, b) => b.createdAtMs - a.createdAtMs)\n setResult({ status: 'ready', data: sequences })\n },\n (error) => setResult(failed([], error, 'sequences')),\n )\n }, [firestore, orgId])\n return result\n}\n\n/** One sequence, or `null` once it is known not to exist. */\nexport function useOutreachSequence(\n orgId: string | null,\n sequenceId: string | null,\n): OutreachLoad<OutreachSequence | null> {\n const firestore = useFirestore()\n const [result, setResult] = useState<OutreachLoad<OutreachSequence | null>>({\n status: 'loading',\n data: null,\n })\n useEffect(() => {\n setResult({ status: 'loading', data: null })\n if (!orgId || !sequenceId) return undefined\n return onSnapshot(\n doc(firestore, 'orgs', orgId, OUTREACH_COLLECTIONS.sequences, sequenceId),\n (snapshot) =>\n setResult({\n status: 'ready',\n data: readStoredOutreachSequence(\n snapshot.id,\n snapshot.exists() ? snapshot.data() : undefined,\n ),\n }),\n (error) => setResult(failed(null, error, 'the sequence')),\n )\n }, [firestore, orgId, sequenceId])\n return result\n}\n\n/** How many enrollments one page of the table reads; \"Show more\" reads another. */\nexport const OUTREACH_ENROLLMENTS_PAGE = 200\n\nexport interface OutreachEnrollmentsLoad extends OutreachLoad<\n OutreachEnrollment[]\n> {\n /** Whether a further page may exist. */\n hasMore: boolean\n showMore(): void\n}\n\n/**\n * A sequence's enrollments, the most recently enrolled first.\n *\n * Paged by document id — an equality on `sequenceId` in the id's own order\n * needs no composite index — and sorted by enrollment time once read, so a\n * table of up to a page reads in the order people were enrolled.\n */\nexport function useOutreachEnrollments(\n orgId: string | null,\n sequenceId: string | null,\n): OutreachEnrollmentsLoad {\n const firestore = useFirestore()\n const [pages, setPages] = useState(1)\n const [result, setResult] = useState<\n OutreachLoad<OutreachEnrollment[]> & { size: number }\n >({\n status: 'loading',\n data: [],\n size: 0,\n })\n useEffect(() => setPages(1), [orgId, sequenceId])\n useEffect(() => {\n if (!orgId || !sequenceId) {\n setResult({ status: 'loading', data: [], size: 0 })\n return undefined\n }\n return onSnapshot(\n query(\n collection(firestore, 'orgs', orgId, OUTREACH_COLLECTIONS.enrollments),\n where('sequenceId', '==', sequenceId),\n orderBy(documentId()),\n limit(pages * OUTREACH_ENROLLMENTS_PAGE),\n ),\n (snapshot) => {\n const enrollments = snapshot.docs\n .map((entry) => readStoredOutreachEnrollment(entry.id, entry.data()))\n .filter(\n (enrollment): enrollment is OutreachEnrollment =>\n enrollment !== null,\n )\n .sort((a, b) => b.createdAtMs - a.createdAtMs)\n setResult({ status: 'ready', data: enrollments, size: snapshot.size })\n },\n (error) =>\n setResult({ ...failed([], error, 'the enrollments'), size: 0 }),\n )\n }, [firestore, orgId, sequenceId, pages])\n return useMemo(\n () => ({\n status: result.status,\n data: result.data,\n hasMore: result.size >= pages * OUTREACH_ENROLLMENTS_PAGE,\n showMore: () => setPages((count) => count + 1),\n }),\n [result, pages],\n )\n}\n\n/** The counts the sequence list shows beside each sequence. */\nexport interface OutreachSequenceCounts {\n enrolled: number\n active: number\n replied: number\n bounced: number\n optedOut: number\n}\n\nconst COUNTED: ReadonlyArray<\n [keyof OutreachSequenceCounts, OutreachEnrollmentStatus | null]\n> = [\n ['enrolled', null],\n ['active', 'active'],\n ['replied', 'replied'],\n ['bounced', 'bounced'],\n ['optedOut', 'opted_out'],\n]\n\n/**\n * Each sequence's enrollment counts, by server-side aggregation — one count\n * per figure, never a read of the enrollments themselves. Recounted when the\n * sequences asked about change — the list asks about the page on screen —\n * and absent while counting, or if a count failed.\n */\nexport function useOutreachSequenceCounts(\n orgId: string | null,\n sequenceIds: readonly string[],\n): Record<string, OutreachSequenceCounts> {\n const firestore = useFirestore()\n const [counts, setCounts] = useState<Record<string, OutreachSequenceCounts>>(\n {},\n )\n const key = sequenceIds.join(',')\n useEffect(() => {\n if (!orgId || !key) return undefined\n let current = true\n const enrollments = collection(\n firestore,\n 'orgs',\n orgId,\n OUTREACH_COLLECTIONS.enrollments,\n )\n Promise.all(\n key.split(',').map(async (sequenceId) => {\n const figures = await Promise.all(\n COUNTED.map(async ([, status]) => {\n const counted = await getCountFromServer(\n status\n ? query(\n enrollments,\n where('sequenceId', '==', sequenceId),\n where('status', '==', status),\n )\n : query(enrollments, where('sequenceId', '==', sequenceId)),\n )\n return counted.data().count\n }),\n )\n return [\n sequenceId,\n Object.fromEntries(\n COUNTED.map(([name], index) => [name, figures[index]]),\n ) as unknown as OutreachSequenceCounts,\n ] as const\n }),\n )\n .then((entries) => current && setCounts(Object.fromEntries(entries)))\n .catch((error: unknown) =>\n console.error('[outreach] enrollment counts could not be read', error),\n )\n return () => {\n current = false\n }\n }, [firestore, orgId, key])\n return counts\n}\n"],"names":["useFirestore","collection","doc","documentId","getCountFromServer","limit","onSnapshot","orderBy","query","where","useEffect","useMemo","useState","OUTREACH_COLLECTIONS","readStoredOutreachEnrollment","readStoredOutreachSequence","denied","error","code","failed","data","what","status","console","OUTREACH_SEQUENCES_LIMIT","useOutreachSequences","orgId","firestore","result","setResult","undefined","sequences","snapshot","docs","map","entry","id","filter","sequence","sort","a","b","createdAtMs","useOutreachSequence","sequenceId","exists","OUTREACH_ENROLLMENTS_PAGE","useOutreachEnrollments","pages","setPages","size","enrollments","enrollment","hasMore","showMore","count","COUNTED","useOutreachSequenceCounts","sequenceIds","counts","setCounts","key","join","current","Promise","all","split","figures","counted","Object","fromEntries","name","index","then","entries","catch"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/use-outreach-data.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport type { CampaignLinkRollup } from '@aglyn/shared-ui-email-campaigns/model'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport {\n collection,\n doc,\n documentId,\n getCountFromServer,\n limit,\n onSnapshot,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useEffect, useMemo, useState } from 'react'\nimport {\n OUTREACH_COLLECTIONS,\n OUTREACH_LINK_ROLLUP_PATH,\n type OutreachEnrollment,\n type OutreachEnrollmentStatus,\n type OutreachSequence,\n} from '../model/outreach.types'\nimport {\n readStoredOutreachEnrollment,\n readStoredOutreachSequence,\n} from '../model/stored-records'\n\n/**\n * The Outreach documents the console reads LIVE (AGL-2980): sequences,\n * enrollments, and the mailboxes they send from.\n *\n * Read straight from Firestore — the rules let an org-wide member holding\n * `outreach.use` on an entitled organization read them, and every write goes\n * through a route — so what a page shows is what the routes and the sending\n * runtime wrote, the moment it lands.\n *\n * Every hook answers one of four states, and a page renders each: still\n * loading, ready, failed, or REFUSED — the rules said no, which is a reader\n * who may not see Outreach here rather than something that went wrong.\n */\n\nexport type OutreachLoadStatus = 'loading' | 'ready' | 'error' | 'refused'\n\nexport interface OutreachLoad<T> {\n status: OutreachLoadStatus\n data: T\n}\n\nconst denied = (error: unknown) =>\n (error as { code?: unknown } | null)?.code === 'permission-denied'\n\nfunction failed<T>(data: T, error: unknown, what: string): OutreachLoad<T> {\n if (denied(error)) return { status: 'refused', data }\n console.error(`[outreach] ${what} could not be read`, error)\n return { status: 'error', data }\n}\n\n/** The most sequences the list reads. */\nexport const OUTREACH_SEQUENCES_LIMIT = 200\n\n/** The organization's sequences, newest first. */\nexport function useOutreachSequences(\n orgId: string | null,\n): OutreachLoad<OutreachSequence[]> {\n const firestore = useFirestore()\n const [result, setResult] = useState<OutreachLoad<OutreachSequence[]>>({\n status: 'loading',\n data: [],\n })\n useEffect(() => {\n setResult({ status: 'loading', data: [] })\n if (!orgId) return undefined\n return onSnapshot(\n query(\n collection(firestore, 'orgs', orgId, OUTREACH_COLLECTIONS.sequences),\n // Ordered, so the cap keeps the newest rather than an arbitrary set;\n // every sequence carries `createdAtMs`, stamped by the save route.\n orderBy('createdAtMs', 'desc'),\n limit(OUTREACH_SEQUENCES_LIMIT),\n ),\n (snapshot) => {\n const sequences = snapshot.docs\n .map((entry) => readStoredOutreachSequence(entry.id, entry.data()))\n .filter((sequence): sequence is OutreachSequence => sequence !== null)\n .sort((a, b) => b.createdAtMs - a.createdAtMs)\n setResult({ status: 'ready', data: sequences })\n },\n (error) => setResult(failed([], error, 'sequences')),\n )\n }, [firestore, orgId])\n return result\n}\n\n/** One sequence, or `null` once it is known not to exist. */\nexport function useOutreachSequence(\n orgId: string | null,\n sequenceId: string | null,\n): OutreachLoad<OutreachSequence | null> {\n const firestore = useFirestore()\n const [result, setResult] = useState<OutreachLoad<OutreachSequence | null>>({\n status: 'loading',\n data: null,\n })\n useEffect(() => {\n setResult({ status: 'loading', data: null })\n if (!orgId || !sequenceId) return undefined\n return onSnapshot(\n doc(firestore, 'orgs', orgId, OUTREACH_COLLECTIONS.sequences, sequenceId),\n (snapshot) =>\n setResult({\n status: 'ready',\n data: readStoredOutreachSequence(\n snapshot.id,\n snapshot.exists() ? snapshot.data() : undefined,\n ),\n }),\n (error) => setResult(failed(null, error, 'the sequence')),\n )\n }, [firestore, orgId, sequenceId])\n return result\n}\n\n/**\n * A sequence's per-destination click rollup (AGL-3239), or `null` until a\n * click has been counted.\n *\n * One document, whatever the sequence's size: the aggregate exists so the\n * table is not a read of every enrollment. A sequence that tracks nothing\n * never has one, and an absent document is not an error.\n */\nexport type OutreachSequenceLinksLoad = OutreachLoad<CampaignLinkRollup | null>\n\nexport function useOutreachSequenceLinks(\n orgId: string | null,\n sequenceId: string | null,\n): OutreachSequenceLinksLoad {\n const firestore = useFirestore()\n const [result, setResult] = useState<OutreachSequenceLinksLoad>({\n status: 'loading',\n data: null,\n })\n useEffect(() => {\n setResult({ status: 'loading', data: null })\n if (!orgId || !sequenceId) return undefined\n return onSnapshot(\n doc(\n firestore,\n 'orgs',\n orgId,\n OUTREACH_COLLECTIONS.sequences,\n sequenceId,\n OUTREACH_LINK_ROLLUP_PATH[0],\n OUTREACH_LINK_ROLLUP_PATH[1],\n ),\n (snapshot) =>\n setResult({\n status: 'ready',\n data: snapshot.exists() ? (snapshot.data() as CampaignLinkRollup) : null,\n }),\n (error) => setResult(failed(null, error, 'the link report')),\n )\n }, [firestore, orgId, sequenceId])\n return result\n}\n\n/** How many enrollments one page of the table reads; \"Show more\" reads another. */\nexport const OUTREACH_ENROLLMENTS_PAGE = 200\n\nexport interface OutreachEnrollmentsLoad extends OutreachLoad<\n OutreachEnrollment[]\n> {\n /** Whether a further page may exist. */\n hasMore: boolean\n showMore(): void\n}\n\n/**\n * A sequence's enrollments, the most recently enrolled first.\n *\n * Paged by document id — an equality on `sequenceId` in the id's own order\n * needs no composite index — and sorted by enrollment time once read, so a\n * table of up to a page reads in the order people were enrolled.\n */\nexport function useOutreachEnrollments(\n orgId: string | null,\n sequenceId: string | null,\n): OutreachEnrollmentsLoad {\n const firestore = useFirestore()\n const [pages, setPages] = useState(1)\n const [result, setResult] = useState<\n OutreachLoad<OutreachEnrollment[]> & { size: number }\n >({\n status: 'loading',\n data: [],\n size: 0,\n })\n useEffect(() => setPages(1), [orgId, sequenceId])\n useEffect(() => {\n if (!orgId || !sequenceId) {\n setResult({ status: 'loading', data: [], size: 0 })\n return undefined\n }\n return onSnapshot(\n query(\n collection(firestore, 'orgs', orgId, OUTREACH_COLLECTIONS.enrollments),\n where('sequenceId', '==', sequenceId),\n orderBy(documentId()),\n limit(pages * OUTREACH_ENROLLMENTS_PAGE),\n ),\n (snapshot) => {\n const enrollments = snapshot.docs\n .map((entry) => readStoredOutreachEnrollment(entry.id, entry.data()))\n .filter(\n (enrollment): enrollment is OutreachEnrollment =>\n enrollment !== null,\n )\n .sort((a, b) => b.createdAtMs - a.createdAtMs)\n setResult({ status: 'ready', data: enrollments, size: snapshot.size })\n },\n (error) =>\n setResult({ ...failed([], error, 'the enrollments'), size: 0 }),\n )\n }, [firestore, orgId, sequenceId, pages])\n return useMemo(\n () => ({\n status: result.status,\n data: result.data,\n hasMore: result.size >= pages * OUTREACH_ENROLLMENTS_PAGE,\n showMore: () => setPages((count) => count + 1),\n }),\n [result, pages],\n )\n}\n\n/** The counts the sequence list shows beside each sequence. */\nexport interface OutreachSequenceCounts {\n enrolled: number\n active: number\n replied: number\n bounced: number\n optedOut: number\n}\n\nconst COUNTED: ReadonlyArray<\n [keyof OutreachSequenceCounts, OutreachEnrollmentStatus | null]\n> = [\n ['enrolled', null],\n ['active', 'active'],\n ['replied', 'replied'],\n ['bounced', 'bounced'],\n ['optedOut', 'opted_out'],\n]\n\n/**\n * Each sequence's enrollment counts, by server-side aggregation — one count\n * per figure, never a read of the enrollments themselves. Recounted when the\n * sequences asked about change — the list asks about the page on screen —\n * and absent while counting, or if a count failed.\n */\nexport function useOutreachSequenceCounts(\n orgId: string | null,\n sequenceIds: readonly string[],\n): Record<string, OutreachSequenceCounts> {\n const firestore = useFirestore()\n const [counts, setCounts] = useState<Record<string, OutreachSequenceCounts>>(\n {},\n )\n const key = sequenceIds.join(',')\n useEffect(() => {\n if (!orgId || !key) return undefined\n let current = true\n const enrollments = collection(\n firestore,\n 'orgs',\n orgId,\n OUTREACH_COLLECTIONS.enrollments,\n )\n Promise.all(\n key.split(',').map(async (sequenceId) => {\n const figures = await Promise.all(\n COUNTED.map(async ([, status]) => {\n const counted = await getCountFromServer(\n status\n ? query(\n enrollments,\n where('sequenceId', '==', sequenceId),\n where('status', '==', status),\n )\n : query(enrollments, where('sequenceId', '==', sequenceId)),\n )\n return counted.data().count\n }),\n )\n return [\n sequenceId,\n Object.fromEntries(\n COUNTED.map(([name], index) => [name, figures[index]]),\n ) as unknown as OutreachSequenceCounts,\n ] as const\n }),\n )\n .then((entries) => current && setCounts(Object.fromEntries(entries)))\n .catch((error: unknown) =>\n console.error('[outreach] enrollment counts could not be read', error),\n )\n return () => {\n current = false\n }\n }, [firestore, orgId, key])\n return counts\n}\n"],"names":["useFirestore","collection","doc","documentId","getCountFromServer","limit","onSnapshot","orderBy","query","where","useEffect","useMemo","useState","OUTREACH_COLLECTIONS","OUTREACH_LINK_ROLLUP_PATH","readStoredOutreachEnrollment","readStoredOutreachSequence","denied","error","code","failed","data","what","status","console","OUTREACH_SEQUENCES_LIMIT","useOutreachSequences","orgId","firestore","result","setResult","undefined","sequences","snapshot","docs","map","entry","id","filter","sequence","sort","a","b","createdAtMs","useOutreachSequence","sequenceId","exists","useOutreachSequenceLinks","OUTREACH_ENROLLMENTS_PAGE","useOutreachEnrollments","pages","setPages","size","enrollments","enrollment","hasMore","showMore","count","COUNTED","useOutreachSequenceCounts","sequenceIds","counts","setCounts","key","join","current","Promise","all","split","figures","counted","Object","fromEntries","name","index","then","entries","catch"],"mappings":"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"}
|
|
@@ -65,4 +65,13 @@ export declare const OUTREACH_API_ROUTES: {
|
|
|
65
65
|
readonly preview: "outreach/preview";
|
|
66
66
|
/** `GET` and the RFC 8058 one-click `POST` — `?t=<signed token>`. */
|
|
67
67
|
readonly unsubscribe: "outreach/unsubscribe";
|
|
68
|
+
/**
|
|
69
|
+
* `GET` — a link in a tracked sequence email (AGL-3239). `?t=<signed
|
|
70
|
+
* token>`; answers a redirect to the destination the token names, and
|
|
71
|
+
* records the click afterwards. A recipient link like the unsubscribe
|
|
72
|
+
* one: no session, and it answers whether or not Outreach is released to
|
|
73
|
+
* the organization now, because a link in an email that already left must
|
|
74
|
+
* not stop working when a rollout is paused.
|
|
75
|
+
*/
|
|
76
|
+
readonly click: "outreach/click";
|
|
68
77
|
};
|
|
@@ -51,7 +51,15 @@
|
|
|
51
51
|
// The one route a RECIPIENT reaches (AGL-2981): no session, a signed
|
|
52
52
|
// token instead, and registered as a recipient link so it answers whether
|
|
53
53
|
// or not Outreach is released for the organization now.
|
|
54
|
-
/** `GET` and the RFC 8058 one-click `POST` — `?t=<signed token>`. */ unsubscribe: 'outreach/unsubscribe'
|
|
54
|
+
/** `GET` and the RFC 8058 one-click `POST` — `?t=<signed token>`. */ unsubscribe: 'outreach/unsubscribe',
|
|
55
|
+
/**
|
|
56
|
+
* `GET` — a link in a tracked sequence email (AGL-3239). `?t=<signed
|
|
57
|
+
* token>`; answers a redirect to the destination the token names, and
|
|
58
|
+
* records the click afterwards. A recipient link like the unsubscribe
|
|
59
|
+
* one: no session, and it answers whether or not Outreach is released to
|
|
60
|
+
* the organization now, because a link in an email that already left must
|
|
61
|
+
* not stop working when a rollout is paused.
|
|
62
|
+
*/ click: 'outreach/click'
|
|
55
63
|
};
|
|
56
64
|
|
|
57
65
|
//# sourceMappingURL=api-routes.js.map
|