@aglyn/plugins-marketing 1.0.0-beta.146 → 1.0.0-beta.147

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/components/email-detail.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 { PageHeaderRecord, pluginDocsHelp } from '@aglyn/aglyn'\nimport {\n mdiCalendarClockOutline,\n mdiCloseCircleOutline,\n mdiDeleteOutline,\n mdiPencilOutline,\n} from '@aglyn/shared-data-mdi'\nimport { AppLink, CardDisplay, MdiIcon, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport RowActionsMenu, {\n type RowActionsMenuItem,\n} from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useFirestore, useFirestoreDoc } from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n Chip,\n Divider,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\nimport { doc } from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useMemo, useState } from 'react'\nimport {\n campaignLinkReport,\n campaignReport,\n type CampaignLinkRollup,\n type CampaignStats,\n} from '@aglyn/shared-ui-email-campaigns/model/campaign-report'\nimport {\n campaignSendDisplay,\n CAMPAIGN_SEND_CONTAINER_FIELD,\n} from '@aglyn/shared-ui-email-campaigns/model/campaign-container'\nimport {\n emailAudienceLabel,\n emailIsUnsent,\n emailSendTimeMs,\n emailSentAs,\n} from '@aglyn/shared-ui-email-campaigns/model/email-record'\nimport { emailPlainTextState } from '@aglyn/aglyn/app-utils/recipient-email-render'\nimport { useMarketingHubPath } from './use-marketing-hub-path'\nimport { CampaignDesignPreview as EmailDesignPreview } from './campaign-email-zones'\nimport EmailEditDrawer from './email-edit-drawer'\nimport EmailRecipientsCard from './email-recipients-card'\nimport {\n Figure,\n percent,\n RateRow,\n Section,\n} from '@aglyn/shared-ui-email-campaigns/components/report-figures'\nimport {\n useCampaignManageApi,\n useCampaignSendApi,\n} from './use-campaign-send-api'\n\nconst previewDocsHelp = pluginDocsHelp('emailCampaigns', {\n anchor: '#the-campaign-report',\n excerpt:\n 'The email as an inbox receives it, drawn by the same renderer the send ' +\n 'path uses. Merge tokens are left standing — a real send fills them from ' +\n 'each recipient.',\n})\n\nconst emailDocsHelp = pluginDocsHelp('emailCampaigns', {\n anchor: '#the-campaign-report',\n excerpt:\n 'One email: what it looked like, who it went to, what was delivered, ' +\n 'and which links were followed — each rate over the population it is ' +\n 'measured against.',\n})\n\nexport interface EmailDetailProps {\n hostId: string\n /** The message document under `hosts/{hostId}/campaigns`. */\n emailId: string\n /** The emails hub URL, for the way back and for sibling links. */\n basePath: string\n}\n\n/**\n * ONE MESSAGE: what it looked like, where it went, and what it did.\n *\n * ## The template it was built from, drawn as it stands NOW\n *\n * The preview renders the template's CURRENT version, because the HTML that\n * was actually mailed is not stored — it is rendered per recipient at send\n * time, with that recipient's merge values in it, and keeping a copy per\n * message would be a copy of the whole email per address. So a message sent\n * before its template was last edited previews as the template is today, and\n * the frame says so rather than letting a reader take it for a record of what\n * went out.\n *\n * ## Every rate names its denominator\n *\n * The arithmetic is `campaign-report.ts` — the same pure module the campaign\n * report reads — so open rate over `delivered` and click rate over `delivered`\n * are computed once, carry their own denominator labels, and come back `null`\n * rather than 0% when they cannot honestly be taken. Nothing on this screen\n * divides anything.\n *\n * ## What this page reads\n *\n * Four documents: the message, its link rollup, the template screen and the\n * template's version. None of them grows with the size of the send. The\n * recipient list is the one read that does, and it is its own card with its\n * own request.\n */\nexport function EmailDetail(props: EmailDetailProps) {\n const { hostId, emailId, basePath } = props\n // The sibling hub: a campaign's page belongs to the Marketing console.\n const marketingHub = useMarketingHubPath()\n const firestore = useFirestore()\n\n const { data: email, status } = useFirestoreDoc<\n Record<string, any> & { stats?: CampaignStats }\n >(\n () => doc(firestore, 'hosts', hostId, 'campaigns', emailId),\n [firestore, hostId, emailId],\n )\n const notFound = status !== 'loading' && !email\n\n /*\n * The link rollup, its own document rather than a field on the message.\n *\n * A map of destinations grows with the content, and the message document is\n * read by the list, the glance widget and the send path; putting an\n * unbounded map on it would make every one of those reads larger.\n */\n const { data: links } = useFirestoreDoc<CampaignLinkRollup>(\n () =>\n doc(firestore, 'hosts', hostId, 'campaigns', emailId, 'reports', 'links'),\n [firestore, hostId, emailId],\n )\n\n const templateScreenId: string | undefined = email?.templateScreenId\n const { data: template } = useFirestoreDoc<any>(\n () =>\n templateScreenId\n ? doc(firestore, 'hosts', hostId, 'screens', templateScreenId)\n : null,\n [firestore, hostId, templateScreenId],\n )\n const templateVersionId: string | undefined = template?.versionId\n const { data: templateVersion } = useFirestoreDoc<any>(\n () =>\n templateScreenId && templateVersionId\n ? doc(\n firestore,\n 'hosts',\n hostId,\n 'screens',\n templateScreenId,\n 'versions',\n templateVersionId,\n )\n : null,\n [firestore, hostId, templateScreenId, templateVersionId],\n )\n\n /**\n * A hand-written plain-text version that no longer describes the design.\n *\n * Said HERE and not only in the composer, because an email can be scheduled\n * and then have its design edited — after which nobody opens the composer\n * again, and the send goes out with a styled half and a text half that\n * disagree. This page is where somebody looks at a scheduled email, so it is\n * where the fact has to be readable.\n *\n * Only while the email is unsent. On a sent one the text part that went out\n * is history; the design moving afterwards is expected and is what the\n * preview's own note already says.\n */\n const plainTextState = emailPlainTextState(\n {\n plainText: String(email?.plainText ?? ''),\n plainTextVersionId: String(email?.plainTextVersionId ?? ''),\n },\n template?.versionId,\n )\n\n const report = useMemo(() => campaignReport(email?.stats), [email])\n const linkReport = useMemo(() => campaignLinkReport(links), [links])\n const subject = String(email?.subject || 'Untitled email')\n /*\n * The composed body, kept on the send document. A message written without\n * a template still has a rendered HTML part in the inbox, so this is what\n * the preview draws for one.\n */\n const composedBody = String(email?.body ?? '')\n const sendTimeMs = email ? emailSendTimeMs(email) : 0\n const state = String(email?.status ?? '')\n /** What this email is doing, which the stored status alone cannot say. */\n const display = campaignSendDisplay(email as never)\n /**\n * Part way through an audience larger than one batch.\n *\n * Stored as `scheduled` — the state the processor claims to resume it — so\n * every control below that keyed on `scheduled` alone was offering an\n * action about an email that is already going out.\n */\n const midFlight = display.state === 'sending'\n /**\n * This email has not gone to anybody yet.\n *\n * Everything below the state table is a REPORT, and an unsent email has\n * nothing to report — no `stats` at all. Drawing the figures anyway would\n * fill the page with zeros and a delivery rate of 0%, which is the reading\n * \"this reached nobody\" rather than \"this has not been sent\", and those are\n * different facts about an email.\n */\n const unsent = emailIsUnsent(email)\n /** The merchant's own name for this email, where one was given. */\n const displayName = String(email?.displayName ?? '')\n /**\n * How many times this email has been sent, and when the last one was.\n *\n * A message written before an email could be sent twice carries neither, and\n * one send is what an absent count means — not zero.\n */\n const sendCount = Number(email?.sendCount ?? 1) || 1\n const lastSentMs = email?.lastSentAt\n ? emailSendTimeMs({ sentAt: email.lastSentAt })\n : 0\n /**\n * The sender this message actually left with, as the SEND recorded it.\n *\n * Read, never composed. The site's sending identity is a setting: a\n * merchant who verifies a new domain in November has not changed what went\n * out in March, and resolving the identity here would answer \"what would\n * this send as today\" on a page whose whole subject is a message that\n * already went. Exactly the rule the list name beside it follows.\n */\n const sentAs = emailSentAs(email)\n\n /*\n * The campaign this message belongs to.\n *\n * `emailCampaignId` — {@link CAMPAIGN_SEND_CONTAINER_FIELD} — is the one\n * linkage, and it is deliberately not spelled `campaignId`: on a message\n * document that name already means the message's OWN id, which is what the\n * report route addresses and what every delivered unsubscribe footer\n * carries as `cid=`.\n *\n * The fallback is the migration. A message written before campaigns grouped\n * anything names no container, and its own id IS the campaign the URL\n * resolves — the campaign detail route answers an id it does not recognize\n * as a container with that message's own report.\n */\n const campaignId = String(email?.[CAMPAIGN_SEND_CONTAINER_FIELD] ?? emailId)\n\n /*==========================================\n * SENDING THIS EMAIL TO MORE PEOPLE.\n *\n * The whole control is a confirmation and one POST. Every decision it looks\n * like it is making — who is left, who is suppressed, whether there is\n * allowance and hourly room — is made by the send path, which is also the\n * path the original send took; asking any of it here would be a second set\n * of rules to disagree with the first.\n *\n * Two requests rather than one, and the first is a READ. `dryRun` runs the\n * whole resolution and writes nothing, so the confirmation can say how many\n * people this would reach before the merchant agrees to it. A send is the\n * one action on this page that cannot be taken back, and \"Send to more\n * recipients?\" with no number in it is a button nobody can answer honestly.\n *=========================================*/\n const campaignSendApi = useCampaignSendApi(hostId)\n const { confirm } = useConfirmationContext()\n const { enqueueSnackbar } = useSnackbar()\n const [sendingMore, setSendingMore] = useState(false)\n\n const handleSendToMore = useCallback(async () => {\n if (sendingMore) return\n setSendingMore(true)\n try {\n const counted = await campaignSendApi({\n action: 'followUp',\n campaignId: emailId,\n dryRun: true,\n })\n if (!counted.response.ok) {\n return void enqueueSnackbar(\n counted.payload?.error ?? 'This email cannot be sent again',\n { variant: 'warning', allowDuplicate: true },\n )\n }\n const reaching = Number(counted.payload?.sendable ?? 0)\n const already = Number(counted.payload?.alreadyReached ?? 0)\n if (!reaching) {\n return void enqueueSnackbar(\n 'Everyone in this audience already has this email',\n { variant: 'info', persist: false },\n )\n }\n const agreed = await confirm({\n title: 'Send this email to more people?',\n description:\n `This sends the same email to ${reaching.toLocaleString()} more ` +\n `${reaching === 1 ? 'person' : 'people'} in the same audience. ` +\n `The ${already.toLocaleString()} who already received it are not ` +\n 'sent it again, and its report adds the new figures to the ones ' +\n 'it already holds.',\n confirmationText: 'Send',\n })\n .then(() => true)\n .catch(() => false)\n if (!agreed) return\n const result = await campaignSendApi({\n action: 'followUp',\n campaignId: emailId,\n })\n if (!result.response.ok) {\n return void enqueueSnackbar(result.payload?.error ?? 'Send failed', {\n variant: 'warning',\n allowDuplicate: true,\n })\n }\n enqueueSnackbar(\n `Sent to ${Number(result.payload?.sent ?? 0).toLocaleString()} more ` +\n 'recipients',\n { variant: 'success', persist: false },\n )\n } catch (error) {\n console.error(error)\n enqueueSnackbar('Send failed', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setSendingMore(false)\n }\n }, [campaignSendApi, confirm, emailId, enqueueSnackbar, sendingMore])\n\n /*==========================================\n * THE LIFECYCLE ACTIONS.\n *\n * Every one of them is one POST to the same route the composer and the\n * scheduled processor use. None of them decides anything: whether an email\n * may be sent now, rescheduled or canceled is decided by the route against\n * the record's stored `status`, so the rules live in one place and the\n * header's job is only to offer the ones that apply.\n *\n * The copy is deliberately NOT sent with any of them. `sendNow` reads the\n * whole message off the record — a request that could also carry a subject\n * and a body would be a way to put arbitrary copy on an existing send id\n * and mail it under that id's unsubscribe scope.\n *=========================================*/\n const [busy, setBusy] = useState('')\n const [editing, setEditing] = useState<'details' | 'schedule' | null>(null)\n\n /** One POST, one snackbar, one busy flag — the shape all four share. */\n const runAction = useCallback(\n async (\n key: string,\n request: Record<string, unknown>,\n success: (payload: any) => string,\n failure: string,\n ) => {\n if (busy) return false\n setBusy(key)\n try {\n const { response, payload } = await campaignSendApi({\n campaignId: emailId,\n ...request,\n })\n if (!response.ok) {\n enqueueSnackbar(payload?.error ?? failure, {\n variant: 'warning',\n allowDuplicate: true,\n })\n return false\n }\n enqueueSnackbar(success(payload), {\n variant: 'success',\n persist: false,\n })\n return true\n } catch (error) {\n console.error(error)\n enqueueSnackbar(failure, { variant: 'error', allowDuplicate: true })\n return false\n } finally {\n setBusy('')\n }\n },\n [busy, campaignSendApi, emailId, enqueueSnackbar],\n )\n\n const handleSendNow = useCallback(async () => {\n /*\n * Counted before it is offered, the same two-request shape the follow-up\n * uses: `dryRun` runs the whole resolution and writes nothing, so the\n * confirmation can name how many people this reaches. \"Send this now?\"\n * with no number in it is a question nobody can answer honestly, and this\n * is the action on the page that cannot be taken back.\n */\n if (busy) return\n setBusy('sendNow')\n /*\n * `null` for \"the count did not happen\", which is not the same answer as\n * zero — zero is a real reach that the confirmation would go on to\n * describe, and the failure branches below return rather than reaching it.\n */\n let reaching: number | null = null\n try {\n const counted = await campaignSendApi({\n action: 'sendNow',\n campaignId: emailId,\n dryRun: true,\n })\n if (counted.response.ok) {\n reaching = Number(\n counted.payload?.sendable ?? counted.payload?.sent ?? 0,\n )\n } else {\n enqueueSnackbar(counted.payload?.error ?? 'This email cannot be sent', {\n variant: 'warning',\n allowDuplicate: true,\n })\n }\n } catch (error) {\n console.error(error)\n enqueueSnackbar('Send failed', { variant: 'error', allowDuplicate: true })\n }\n setBusy('')\n if (reaching === null) return\n const agreed = await confirm({\n title: 'Send this email now?',\n description:\n `This sends it to ${reaching.toLocaleString()} ` +\n `${reaching === 1 ? 'person' : 'people'} straight away` +\n (state === 'scheduled'\n ? ', instead of at the time it is scheduled for. '\n : '. ') +\n 'It cannot be taken back once it goes.',\n confirmationText: 'Send now',\n })\n .then(() => true)\n .catch(() => false)\n if (!agreed) return\n await runAction(\n 'sendNow',\n { action: 'sendNow' },\n (payload) =>\n `Sent to ${Number(payload?.sent ?? 0).toLocaleString()} recipients`,\n 'Send failed',\n )\n }, [\n busy,\n campaignSendApi,\n confirm,\n emailId,\n enqueueSnackbar,\n runAction,\n state,\n ])\n\n /*==========================================\n * STOPPING A SEND, WHICH NOW MEANS TWO DIFFERENT THINGS.\n *\n * `cancel` acts on `scheduled`, and an email delivering an audience larger\n * than one batch is stored as `scheduled` between runs — so the control\n * that withdraws a campaign before it goes also stops one that is half\n * delivered, with no change to the route. That is a real capability, and a\n * merchant watching a send go wrong needs to be told which of the two they\n * are about to do: nothing has been mailed, or two thousand people already\n * have it and are keeping it.\n *=========================================*/\n const handleCancel = useCallback(async () => {\n const reached = display.progress.reached\n const left = display.progress.remaining\n const agreed = await confirm({\n title: midFlight ? 'Stop sending this email?' : 'Cancel this scheduled email?',\n description: midFlight\n ? `It has reached ${reached.toLocaleString()} ` +\n `${reached === 1 ? 'person' : 'people'} so far, and stopping it ` +\n `leaves ${left.toLocaleString()} unaddressed. What has already ` +\n 'gone out cannot be taken back — those messages stay in inboxes ' +\n 'and keep their unsubscribe links. The email and its report are ' +\n 'kept, but a stopped send cannot be resumed; reaching the rest ' +\n 'means composing a new email.'\n : 'It will not be sent at the time it is scheduled for. The email ' +\n 'and everything written on it are kept, but a canceled email ' +\n 'cannot be put back on the schedule — you would compose a new one.',\n confirmationText: midFlight ? 'Stop sending' : 'Cancel send',\n })\n .then(() => true)\n .catch(() => false)\n if (!agreed) return\n await runAction(\n 'cancel',\n { action: 'cancel' },\n () =>\n midFlight\n ? 'This email has stopped sending'\n : 'This email will not be sent',\n 'This email could not be canceled',\n )\n }, [confirm, display.progress, midFlight, runAction])\n\n const handleReschedule = useCallback(\n async (values: { sendAtMs?: number }) => {\n const done = await runAction(\n 'schedule',\n { action: 'schedule', sendAtMs: values.sendAtMs },\n () =>\n `Scheduled for ${new Date(\n Number(values.sendAtMs ?? 0),\n ).toLocaleString()}`,\n 'This email could not be scheduled',\n )\n if (done) setEditing(null)\n },\n [runAction],\n )\n\n /*==========================================\n * DISCARDING A DRAFT, WHICH IS THE ONE REMOVAL THIS PAGE HAS.\n *\n * Only ever offered on a `draft`, and refused again by the route inside the\n * transaction that deletes — the state on screen is a snapshot, and\n * `sendNow` claims a draft by moving it to `sending` in a transaction of\n * its own, so a check made only here could remove a record the send path\n * was mailing from.\n *\n * A sent email is never discardable from anywhere. Its report is what a\n * merchant answers a complaint with, and its id is inside the HMAC of every\n * unsubscribe link it delivered; a scheduled one is withdrawn with Cancel,\n * which keeps the record and takes it off the clock.\n *\n * The reader is sent back to the list afterwards rather than left on the\n * page of a record that no longer exists — which would render the \"could\n * not be loaded\" branch and read as a failure.\n *=========================================*/\n const manageApi = useCampaignManageApi(hostId)\n const router = useRouter()\n\n const handleDiscard = useCallback(async () => {\n const agreed = await confirm({\n title: 'Discard this draft?',\n description:\n 'This email has not been sent to anybody, and discarding it removes ' +\n 'it for good — the subject, the message and everything else written ' +\n 'on it. There is no undo.',\n confirmationText: 'Discard',\n })\n .then(() => true)\n .catch(() => false)\n if (!agreed) return\n if (busy) return\n setBusy('discard')\n try {\n const { response, payload } = await manageApi({\n action: 'discardEmail',\n campaignId: emailId,\n })\n if (!response.ok) {\n return void enqueueSnackbar(\n payload?.error ?? 'This draft could not be discarded',\n { variant: 'warning', allowDuplicate: true },\n )\n }\n enqueueSnackbar('Draft discarded', { variant: 'success', persist: false })\n router.push(`${basePath}/messages`)\n } catch (error) {\n console.error(error)\n enqueueSnackbar('This draft could not be discarded', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setBusy('')\n }\n }, [basePath, busy, confirm, emailId, enqueueSnackbar, manageApi, router])\n\n const handleRename = useCallback(\n async (values: { displayName?: string }) => {\n const done = await runAction(\n 'update',\n { action: 'update', displayName: values.displayName },\n () => 'Name updated',\n 'The name could not be updated',\n )\n if (done) setEditing(null)\n },\n [runAction],\n )\n\n /*==========================================\n * THE HEADER, IN THREE REGISTERS.\n *\n * Navigation reads as navigation — a naked link button, because that is\n * what it is and a reader should be able to tell without clicking. The\n * PRIMARY action of the state is the one contained button, so there is\n * exactly one on the page and it is the thing a merchant came to do.\n * Everything else goes in the overflow, and the two irreversible entries in\n * there are marked `destructive` so they carry the error color rather than\n * sitting in the list looking like \"Rename\".\n *\n * `RowActionsMenu` is named for table rows and its rendering is not: it is\n * a kebab `IconButton` and a `Menu` whose items support `onClick`,\n * `destructive`, `disabled` and `disabledReason` — exactly what a card\n * header's overflow needs. Reusing it is what keeps the menu on this page\n * behaving like every other overflow menu in the console.\n *=========================================*/\n const scheduled = state === 'scheduled'\n const draft = state === 'draft'\n const sending = state === 'sending'\n\n const overflowItems: RowActionsMenuItem[] = [\n {\n key: 'rename',\n label: 'Edit details',\n icon: <MdiIcon path={mdiPencilOutline.path} size={0.8} />,\n onClick: () => setEditing('details'),\n },\n /*\n Rescheduling an email that is ALREADY GOING OUT is not a thing to\n offer: its remaining batches are due when the sender said, and moving\n `sendAtMs` under the processor mid-campaign changes when the rest of a\n delivery happens rather than when it starts.\n */\n ...((draft || scheduled) && !midFlight\n ? [\n {\n key: 'schedule',\n label: scheduled ? 'Reschedule' : 'Schedule',\n icon: <MdiIcon path={mdiCalendarClockOutline.path} size={0.8} />,\n onClick: () => setEditing('schedule'),\n } as RowActionsMenuItem,\n ]\n : []),\n ...(scheduled && !midFlight\n ? [\n {\n key: 'cancel',\n label: 'Cancel send',\n icon: <MdiIcon path={mdiCloseCircleOutline.path} size={0.8} />,\n destructive: true,\n disabled: Boolean(busy),\n disabledReason: 'Another action on this email is still running',\n onClick: () => void handleCancel(),\n } as RowActionsMenuItem,\n ]\n : []),\n /*\n Discard is offered ONLY on a draft, and it is hidden rather than\n disabled everywhere else — the opposite of how this menu treats\n `Reschedule`, and deliberately.\n\n A disabled control tells a reader that the action exists for this\n record and is momentarily unavailable. There is no state in which a\n sent email becomes discardable, so showing the entry greyed out on one\n would be an offer this product will never honor, sitting under the\n report it is promising to destroy.\n */\n ...(draft\n ? [\n {\n key: 'discard',\n label: 'Discard draft',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n disabled: Boolean(busy),\n disabledReason: 'Another action on this email is still running',\n onClick: () => void handleDiscard(),\n } as RowActionsMenuItem,\n ]\n : []),\n ]\n\n /*\n * The one contained button, and what it is per state.\n *\n * `draft` and `scheduled` share it: the email has not gone out, so the act\n * is to make it go. A `sent` email's is the follow-up. A `canceled` one has\n * no primary act at all — it was withdrawn deliberately, and offering a way\n * to un-withdraw it would be a resurrect path this model does not have —\n * and neither does one that is mid-send.\n */\n const primaryAction =\n (draft || scheduled) && !midFlight ? (\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled={Boolean(busy)}\n onClick={() => void handleSendNow()}\n >\n {busy === 'sendNow' ? 'Checking…' : 'Send now'}\n </Button>\n ) : midFlight ? (\n /*\n A CAMPAIGN THAT IS ALREADY GOING OUT HAS ONE ACT: STOPPING IT.\n\n \"Send now\" is withheld rather than disabled, and the reason is not\n cosmetic — `sendNow` re-resolves the WHOLE audience and mails it, with\n no subtraction of anyone already reached, so pressing it on an email\n between batches sends a second copy to every person who has had the\n first. Withholding it leaves exactly one primary action, and it is the\n one a merchant watching a send go wrong actually wants.\n */\n <Button\n size=\"small\"\n variant=\"contained\"\n color=\"error\"\n disabled={Boolean(busy)}\n onClick={() => void handleCancel()}\n >\n {'Stop sending'}\n </Button>\n ) : state === 'sent' ? (\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled={sendingMore}\n onClick={() => void handleSendToMore()}\n >\n {sendingMore ? 'Checking…' : 'Send to more recipients'}\n </Button>\n ) : null\n\n /**\n * WHERE THIS EMAIL IS WRITTEN, which is no longer this page.\n *\n * A page cannot both be \"what this email did\" and \"write this email\" — the\n * first is a report the reader scrolls, the second is a form with one\n * irreversible button — so the composer is its own route and this is the\n * link to it. Naked, because navigation should read as navigation; the one\n * CONTAINED button on this page stays the primary act of the state.\n *\n * Offered only while the copy can still be changed. An email part way\n * through a send is stored as `scheduled`, so `midFlight` is what keeps the\n * link off a message that is already reaching inboxes — the same distinction\n * \"Send now\" is withheld on.\n */\n const editHref = `${basePath}/messages/${emailId}/edit`\n\n const headerActions = (\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={`${basePath}/messages`}\n size=\"small\"\n color=\"primary\"\n >\n {'All messages'}\n </Button>\n {(draft || scheduled) && !midFlight ? (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={editHref}\n size=\"small\"\n color=\"primary\"\n >\n {'Write this email'}\n </Button>\n ) : null}\n {templateScreenId ? (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={`${basePath}/templates/${templateScreenId}`}\n size=\"small\"\n color=\"primary\"\n >\n {'Open template'}\n </Button>\n ) : null}\n {primaryAction}\n <RowActionsMenu label={subject} items={overflowItems} />\n </Stack>\n )\n\n if (notFound) {\n return (\n <CardDisplay\n header={'Email'}\n help={emailDocsHelp}\n contentGutterX\n contentGutterY\n HeaderProps={{ action: headerActions }}\n >\n {/*\n * Not \"no data\". An email that cannot be read is a different\n * situation from one with no engagement, and rendering an empty\n * report for the first is how somebody comes to believe a message\n * they sent reached nobody.\n */}\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This email could not be loaded. It may have been deleted.'}\n </Typography>\n </CardDisplay>\n )\n }\n\n return (\n <Stack spacing={3}>\n {/* The page heading and the trail name the message; this card is\n then free to say what it holds rather than repeating the title. */}\n <PageHeaderRecord title={email ? subject : undefined} />\n <CardDisplay\n header={'Email'}\n help={emailDocsHelp}\n contentGutterX\n contentGutterY\n HeaderProps={{ action: headerActions }}\n >\n <Stack spacing={3}>\n {report.caveats.map((caveat) => (\n <Alert key={caveat.id} severity=\"info\">\n {caveat.message}\n </Alert>\n ))}\n {unsent && plainTextState.stale ? (\n <Alert severity=\"warning\">\n {'The design has been edited since this email’s plain-text ' +\n 'version was written, so the two halves may no longer say the ' +\n 'same thing. Nothing has overwritten what was written — open ' +\n 'this email to read it, or take the design’s text instead.'}\n </Alert>\n ) : null}\n\n <Divider />\n\n <Section title=\"Where this went\">\n <ScrollTable size=\"small\">\n <TableBody>\n {/*\n WHAT THIS EMAIL IS DOING, not the field it stores.\n\n An email delivering an audience larger than one batch is\n written back as `scheduled` between runs — the state the\n processor claims to resume it — so the stored status read\n \"Scheduled\" on a page reporting five hundred deliveries.\n */}\n <TableRow>\n <TableCell>{'State'}</TableCell>\n <TableCell align=\"right\">\n <Chip\n size=\"small\"\n color={\n display.state === 'sending'\n ? 'info'\n : display.state === 'stopped'\n ? 'warning'\n : undefined\n }\n label={display.label}\n />\n </TableCell>\n </TableRow>\n {midFlight ? (\n <TableRow>\n <TableCell>{'Next batch'}</TableCell>\n <TableCell align=\"right\">\n {`${display.progress.remaining.toLocaleString()} still ` +\n 'to reach, ' +\n (display.progress.nextAtMs\n ? `next run ${new Date(\n display.progress.nextAtMs,\n ).toLocaleString()}`\n : 'next run due')}\n </TableCell>\n </TableRow>\n ) : null}\n <TableRow>\n <TableCell>\n {state === 'sent' ? 'Sent' : 'Scheduled for'}\n </TableCell>\n <TableCell align=\"right\">\n {sendTimeMs\n ? new Date(sendTimeMs).toLocaleString()\n : 'not recorded'}\n </TableCell>\n </TableRow>\n {/*\n An email that has been sent more than once, said out loud.\n Every figure below covers all of them, and a reader who took\n the single `Sent` date above for the whole story would read\n the delivery numbers as one mailing's.\n */}\n {sendCount > 1 ? (\n <TableRow>\n <TableCell>{'Sends'}</TableCell>\n <TableCell align=\"right\">\n {`${sendCount.toLocaleString()}, most recently ` +\n (lastSentMs\n ? new Date(lastSentMs).toLocaleString()\n : 'not recorded')}\n </TableCell>\n </TableRow>\n ) : null}\n {displayName ? (\n <TableRow>\n <TableCell>{'Name'}</TableCell>\n <TableCell align=\"right\">{displayName}</TableCell>\n </TableRow>\n ) : null}\n {/*\n THE ADDRESS THIS MESSAGE ACTUALLY LEFT AS.\n\n Always a row. A site's sending identity can move — a domain\n verifies, a mailbox is renamed, a sender changes — so the\n question \"what did my recipients see\" has an answer only if\n the send wrote one down, and a page that omitted the row for\n the sends that did not would make an unanswerable question\n look like one nobody asked.\n\n Three states, and they are three different facts. A\n recorded address is what went out. An unsent email has no\n address yet, which is not the same as having lost one. And a\n message sent before the send began stamping its sender says\n so plainly rather than being handed today's identity, which\n would be this page inventing history.\n */}\n <TableRow>\n <TableCell>{'Sent as'}</TableCell>\n <TableCell align=\"right\">\n {sentAs.recorded ? (\n <Typography variant=\"body2\" sx={{ fontFamily: 'monospace' }}>\n {sentAs.from}\n </Typography>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {unsent ? 'not sent yet' : 'not recorded'}\n </Typography>\n )}\n </TableCell>\n </TableRow>\n {/*\n The two rows that only exist once there is a sender to\n describe. Both name what an ABSENT value means rather than\n leaving a blank: a message with no display name showed the\n address on its own, and one with no reply address takes\n replies where it was sent from. Neither is missing\n information — each is a fact the record states by omission.\n */}\n {sentAs.recorded ? (\n <TableRow>\n <TableCell>{'From name'}</TableCell>\n <TableCell align=\"right\">\n {sentAs.fromName ?? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'The address on its own'}\n </Typography>\n )}\n </TableCell>\n </TableRow>\n ) : null}\n {sentAs.recorded ? (\n <TableRow>\n <TableCell>{'Reply-to'}</TableCell>\n <TableCell align=\"right\">\n {sentAs.replyTo ?? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'The sending address'}\n </Typography>\n )}\n </TableCell>\n </TableRow>\n ) : null}\n <TableRow>\n <TableCell>{'Campaign'}</TableCell>\n <TableCell align=\"right\">\n {/*\n The campaign's page belongs to the Marketing console, so\n this href is built from the sibling hub rather than this\n surface's own. Plain text until that hub resolves: a\n link with no destination is worse than none.\n */}\n {marketingHub ? (\n <AppLink href={`${marketingHub}/campaigns/${campaignId}`}>\n {'Open the campaign'}\n </AppLink>\n ) : (\n 'Open the campaign'\n )}\n </TableCell>\n </TableRow>\n <TableRow>\n <TableCell>{'List'}</TableCell>\n <TableCell align=\"right\">\n {emailAudienceLabel(email)}\n </TableCell>\n </TableRow>\n {/*\n Always a row, never a hidden one. A template this email did\n not use and a template row that was not rendered look\n identical to a reader, and the second sends them looking for\n a link that was never going to be there.\n */}\n <TableRow>\n <TableCell>{'Template'}</TableCell>\n <TableCell align=\"right\">\n {templateScreenId ? (\n <AppLink\n href={`${basePath}/templates/${templateScreenId}`}\n >\n {template?.displayName ?? 'Untitled template'}\n </AppLink>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Written as plain text in the composer'}\n </Typography>\n )}\n </TableCell>\n </TableRow>\n </TableBody>\n </ScrollTable>\n {/*\n * The list is named as the SEND recorded it, and saying so is\n * what stops a renamed or deleted list quietly rewriting the\n * history of a message that went out months ago.\n */}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'The list and the sender are recorded as they were when this ' +\n 'email was sent, not as this site is configured now.'}\n </Typography>\n </Section>\n\n <Divider />\n\n {/*==========================================\n * AN EMAIL THAT HAS NOT BEEN SENT HAS NO REPORT.\n *\n * Not an empty one — none. Every figure below divides or counts\n * something that only exists once mail has gone out, and an unsent\n * email carries no `stats` at all, so rendering the sections would\n * publish a column of zeros and a delivery rate of 0%. That reads\n * as \"this reached nobody\", which is a claim about a send that\n * happened; the truth is that no send has happened.\n *\n * The same reasoning the rate rows already follow, one level up: a\n * rate whose denominator is unrecorded renders absent rather than\n * as 0%, and a report whose whole subject is unrecorded renders\n * absent rather than as zeros.\n *=========================================*/}\n {unsent ? (\n <Section title=\"Delivery\">\n <Typography variant=\"body2\" color=\"text.secondary\">\n {sending\n ? 'This email is being sent right now. Its figures appear ' +\n 'here once the send finishes.'\n : draft\n ? 'This email has not been sent, so there is nothing to ' +\n 'report yet. Write this email — the link is in the ' +\n 'header — then send it or put it on the schedule.'\n : 'This email has not been sent yet. Its figures appear ' +\n 'here once it goes out.'}\n </Typography>\n </Section>\n ) : (\n <>\n <Section title=\"Delivery\">\n <Stack\n direction=\"row\"\n spacing={4}\n useFlexGap\n sx={{ flexWrap: 'wrap' }}\n >\n <Figure\n label=\"Addressed\"\n value={report.recipients}\n note=\"after the per-send cap\"\n />\n <Figure\n label=\"Sent\"\n value={report.sent}\n note=\"accepted by the provider\"\n />\n <Figure\n label=\"Delivered\"\n value={report.delivered}\n note=\"accepted by the receiving server\"\n />\n <Figure label=\"Bounced\" value={report.bounced} note=\"of sent\" />\n <Figure\n label=\"Marked as spam\"\n value={report.complained}\n note=\"of delivered\"\n />\n </Stack>\n </Section>\n\n <Divider />\n\n <Section title=\"Engagement\">\n <Stack\n direction=\"row\"\n spacing={4}\n useFlexGap\n sx={{ flexWrap: 'wrap' }}\n >\n <Figure\n label=\"Opens\"\n value={report.opens}\n note=\"every open, repeats included\"\n />\n <Figure\n label=\"Readers who opened\"\n value={report.uniqueOpens}\n note=\"distinct recipients\"\n />\n <Figure\n label=\"Clicks\"\n value={report.clicks}\n note=\"every click, repeats included\"\n />\n <Figure\n label=\"Readers who clicked\"\n value={report.uniqueClicks}\n note=\"distinct recipients\"\n />\n <Figure\n label=\"Unsubscribed\"\n value={report.unsubscribes}\n note=\"through this email's link\"\n />\n </Stack>\n </Section>\n\n <Divider />\n\n <Section title=\"Rates\">\n <Stack spacing={1}>\n <RateRow label=\"Delivery rate\" rate={report.rates.delivery} />\n <RateRow label=\"Open rate\" rate={report.rates.open} />\n <RateRow label=\"Click rate\" rate={report.rates.click} />\n <RateRow\n label=\"Click-to-open rate\"\n rate={report.rates.clickToOpen}\n />\n <RateRow label=\"Bounce rate\" rate={report.rates.bounce} />\n <RateRow label=\"Complaint rate\" rate={report.rates.complaint} />\n <RateRow\n label=\"Unsubscribe rate\"\n rate={report.rates.unsubscribe}\n />\n </Stack>\n </Section>\n\n {report.populations.length ? (\n <>\n <Divider />\n <Section title=\"Who this was allowed to reach\">\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Measured when this email was sent, and stored as it was ' +\n 'then. These figures describe the send, not the audience ' +\n 'as it stands today.'}\n </Typography>\n <ScrollTable size=\"small\">\n <TableBody>\n {report.populations.map((population) => (\n <TableRow key={population.id}>\n <TableCell>{population.label}</TableCell>\n <TableCell align=\"right\" sx={{ fontWeight: 'bold' }}>\n {population.count.toLocaleString()}\n </TableCell>\n <TableCell align=\"right\">\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`of ${population.of.toLocaleString()} ${population.ofLabel}`}\n </Typography>\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n </Section>\n </>\n ) : null}\n\n <Divider />\n\n <Section title=\"Links\">\n {linkReport.rows.length ? (\n <>\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Destination'}</TableCell>\n <TableCell align=\"right\">{'Clicks'}</TableCell>\n <TableCell align=\"right\">{'Share'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {linkReport.rows.map((row) => (\n <TableRow key={row.url}>\n <TableCell sx={{ wordBreak: 'break-all' }}>\n {row.url}\n </TableCell>\n <TableCell align=\"right\" sx={{ fontWeight: 'bold' }}>\n {row.clicks.toLocaleString()}\n </TableCell>\n <TableCell align=\"right\">\n {row.share\n ? `${percent(row.share.value)} of ${row.share.denominator.toLocaleString()} ${row.share.denominatorLabel}`\n : '—'}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Counted by address and path — query strings are dropped, ' +\n 'so two links to the same page with different tracking ' +\n 'parameters count as one row.'}\n </Typography>\n {linkReport.unattributedClicks ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`${linkReport.unattributedClicks.toLocaleString()} clicks ` +\n 'arrived without a destination and are not in this table.'}\n </Typography>\n ) : null}\n {linkReport.overflowClicks ? (\n <Alert severity=\"info\">\n {'This email has more distinct destinations than the ' +\n `rollup keeps. ${linkReport.overflowClicks.toLocaleString()} ` +\n 'clicks landed on links past that limit and are counted ' +\n 'in the click total above but not in this table.'}\n </Alert>\n ) : null}\n </>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {report.clicks\n ? 'Clicks were recorded for this email, but none of them ' +\n 'carried a destination, so there is nothing to break down ' +\n 'by link.'\n : 'No link clicks have been recorded for this email.'}\n </Typography>\n )}\n </Section>\n </>\n )}\n </Stack>\n </CardDisplay>\n\n <EmailRecipientsCard hostId={hostId} emailId={emailId} />\n\n {/*\n * Last, and its own card. The numbers are what a reader came for and\n * the preview is the tallest thing on the page — above them it pushes\n * every figure below the fold.\n *\n * `header` rather than `title`: `CardDisplay` has no `title` prop, so\n * one spreads through to the MUI `Card` root and lands on the DOM as a\n * hover tooltip, leaving the card with no heading at all. The gutters\n * are named for the same reason — without them the 640px frame sits\n * flush against the card's edge.\n */}\n <CardDisplay\n header={'Preview'}\n help={previewDocsHelp}\n contentGutterX\n contentGutterY\n >\n {templateScreenId ? (\n <EmailDesignPreview\n hostId={hostId}\n nodes={templateVersion?.nodes}\n loading={template === undefined || templateVersion === undefined}\n subject={subject}\n preheader={String(template?.emailPreheader ?? '')}\n emptyMessage={\n 'The template this email was built from is empty or has ' +\n 'been deleted, so there is nothing to draw.'\n }\n note={\n 'The template as it stands today. The mail itself is ' +\n 'rendered per recipient at send time and not kept, so a ' +\n 'template edited since this went out previews as it is now.'\n }\n />\n ) : (\n <EmailDesignPreview\n hostId={hostId}\n nodes={undefined}\n text={composedBody}\n loading={email === undefined}\n subject={subject}\n emptyMessage={\n 'This email carries no body, so there is nothing to draw.'\n }\n note={\n 'Written as plain text in the composer. Merge tokens are ' +\n 'left standing here — the mail itself resolves them per ' +\n 'recipient at send time and is not kept.'\n }\n />\n )}\n </CardDisplay>\n\n {/*\n * Editing in a DRAWER, never a form above the content. The name is the\n * one detail a sent email still owns — see the drawer's own header for\n * why the subject, body, audience and topic are not on offer once mail\n * has been delivered.\n */}\n <EmailEditDrawer\n open={editing !== null}\n onClose={() => setEditing(null)}\n field={editing === 'schedule' ? 'schedule' : 'details'}\n title={\n editing === 'schedule'\n ? scheduled\n ? 'Reschedule this email'\n : 'Schedule this email'\n : 'Edit details'\n }\n submitLabel={\n editing === 'schedule'\n ? scheduled\n ? 'Reschedule'\n : 'Schedule'\n : 'Save'\n }\n displayName={displayName}\n sendAtMs={scheduled ? sendTimeMs : 0}\n busy={Boolean(busy)}\n note={\n editing === 'schedule'\n ? 'The email goes out at this time. You can send it sooner, or ' +\n 'cancel it, from this page.'\n : unsent\n ? 'The name is for finding this email in your own lists. The ' +\n 'subject and the message are written on this email’s own ' +\n 'compose page.'\n : 'This email has been sent, so its subject, message and ' +\n 'audience describe mail that is already in inboxes and can ' +\n 'no longer be changed. Its name is yours and stays editable.'\n }\n onSubmit={(values) =>\n void (editing === 'schedule'\n ? handleReschedule(values)\n : handleRename(values))\n }\n />\n </Stack>\n )\n}\nEmailDetail.displayName = 'EmailDetail'\n\nexport default EmailDetail\n"],"names":["PageHeaderRecord","pluginDocsHelp","mdiCalendarClockOutline","mdiCloseCircleOutline","mdiDeleteOutline","mdiPencilOutline","AppLink","CardDisplay","MdiIcon","useConfirmationContext","RowActionsMenu","ScrollTable","useSnackbar","useFirestore","useFirestoreDoc","Alert","Button","Chip","Divider","Stack","TableBody","TableCell","TableHead","TableRow","Typography","doc","useRouter","useCallback","useMemo","useState","campaignLinkReport","campaignReport","campaignSendDisplay","CAMPAIGN_SEND_CONTAINER_FIELD","emailAudienceLabel","emailIsUnsent","emailSendTimeMs","emailSentAs","emailPlainTextState","useMarketingHubPath","CampaignDesignPreview","EmailDesignPreview","EmailEditDrawer","EmailRecipientsCard","Figure","percent","RateRow","Section","useCampaignManageApi","useCampaignSendApi","previewDocsHelp","anchor","excerpt","emailDocsHelp","EmailDetail","props","sentAs","hostId","emailId","basePath","marketingHub","firestore","data","email","status","notFound","links","templateScreenId","template","templateVersionId","versionId","templateVersion","plainTextState","plainText","String","plainTextVersionId","report","stats","linkReport","subject","composedBody","body","sendTimeMs","state","display","midFlight","unsent","displayName","sendCount","Number","lastSentMs","lastSentAt","sentAt","campaignId","campaignSendApi","confirm","enqueueSnackbar","sendingMore","setSendingMore","handleSendToMore","counted","result","action","dryRun","response","ok","payload","error","variant","allowDuplicate","reaching","sendable","already","alreadyReached","persist","agreed","title","description","toLocaleString","confirmationText","then","catch","sent","console","busy","setBusy","editing","setEditing","runAction","key","request","success","failure","handleSendNow","handleCancel","reached","progress","left","remaining","handleReschedule","values","done","sendAtMs","Date","manageApi","router","handleDiscard","push","handleRename","scheduled","draft","sending","overflowItems","label","icon","path","size","onClick","destructive","disabled","Boolean","disabledReason","primaryAction","color","editHref","headerActions","direction","spacing","sx","alignItems","component","componentVariant","nativeButton","href","items","header","help","contentGutterX","contentGutterY","HeaderProps","undefined","caveats","map","caveat","severity","message","id","stale","align","nextAtMs","recorded","fontFamily","from","fromName","replyTo","useFlexGap","flexWrap","value","recipients","note","delivered","bounced","complained","opens","uniqueOpens","clicks","uniqueClicks","unsubscribes","rate","rates","delivery","open","click","clickToOpen","bounce","complaint","unsubscribe","populations","length","population","fontWeight","count","of","ofLabel","rows","row","wordBreak","url","share","denominator","denominatorLabel","unattributedClicks","overflowClicks","nodes","loading","preheader","emailPreheader","emptyMessage","text","onClose","field","submitLabel","onSubmit"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,SAASA,gBAAgB,EAAEC,cAAc,QAAQ,eAAc;AAC/D,SACEC,uBAAuB,EACvBC,qBAAqB,EACrBC,gBAAgB,EAChBC,gBAAgB,QACX,yBAAwB;AAC/B,SAASC,OAAO,EAAEC,WAAW,EAAEC,OAAO,EAAEC,sBAAsB,QAAQ,uBAAsB;AAC5F,OAAOC,oBAEA,6DAA4D;AACnE,SAASC,WAAW,QAAQ,yDAAwD;AACpF,SAASC,WAAW,QAAQ,8BAA6B;AACzD,SAASC,YAAY,EAAEC,eAAe,QAAQ,iCAAgC;AAC9E,SACEC,KAAK,EACLC,MAAM,EACNC,IAAI,EACJC,OAAO,EACPC,KAAK,EACLC,SAAS,EACTC,SAAS,EACTC,SAAS,EACTC,QAAQ,EACRC,UAAU,QACL,gBAAe;AACtB,SAASC,GAAG,QAAQ,qBAAoB;AACxC,SAASC,SAAS,QAAQ,kBAAiB;AAC3C,SAASC,WAAW,EAAEC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AACtD,SACEC,kBAAkB,EAClBC,cAAc,QAGT,yDAAwD;AAC/D,SACEC,mBAAmB,EACnBC,6BAA6B,QACxB,4DAA2D;AAClE,SACEC,kBAAkB,EAClBC,aAAa,EACbC,eAAe,EACfC,WAAW,QACN,sDAAqD;AAC5D,SAASC,mBAAmB,QAAQ,gDAA+C;AACnF,SAASC,mBAAmB,QAAQ,8BAA0B;AAC9D,SAASC,yBAAyBC,kBAAkB,QAAQ,4BAAwB;AACpF,OAAOC,qBAAqB,yBAAqB;AACjD,OAAOC,yBAAyB,6BAAyB;AACzD,SACEC,MAAM,EACNC,OAAO,EACPC,OAAO,EACPC,OAAO,QACF,6DAA4D;AACnE,SACEC,oBAAoB,EACpBC,kBAAkB,QACb,6BAAyB;AAEhC,MAAMC,kBAAkBjD,eAAe,kBAAkB;IACvDkD,QAAQ;IACRC,SACE,4EACA,6EACA;AACJ;AAEA,MAAMC,gBAAgBpD,eAAe,kBAAkB;IACrDkD,QAAQ;IACRC,SACE,yEACA,yEACA;AACJ;AAUA;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BC,GACD,OAAO,SAASE,YAAYC,KAAuB;wDAm0B5BC,kBAYAA;IA90BrB,MAAM,EAAEC,MAAM,EAAEC,OAAO,EAAEC,QAAQ,EAAE,GAAGJ;IACtC,uEAAuE;IACvE,MAAMK,eAAerB;IACrB,MAAMsB,YAAYhD;IAElB,MAAM,EAAEiD,MAAMC,KAAK,EAAEC,MAAM,EAAE,GAAGlD,gBAG9B,IAAMW,IAAIoC,WAAW,SAASJ,QAAQ,aAAaC,UACnD;QAACG;QAAWJ;QAAQC;KAAQ;IAE9B,MAAMO,WAAWD,WAAW,aAAa,CAACD;IAE1C;;;;;;GAMC,GACD,MAAM,EAAED,MAAMI,KAAK,EAAE,GAAGpD,gBACtB,IACEW,IAAIoC,WAAW,SAASJ,QAAQ,aAAaC,SAAS,WAAW,UACnE;QAACG;QAAWJ;QAAQC;KAAQ;IAG9B,MAAMS,mBAAuCJ,yBAAAA,MAAOI,gBAAgB;IACpE,MAAM,EAAEL,MAAMM,QAAQ,EAAE,GAAGtD,gBACzB,IACEqD,mBACI1C,IAAIoC,WAAW,SAASJ,QAAQ,WAAWU,oBAC3C,MACN;QAACN;QAAWJ;QAAQU;KAAiB;IAEvC,MAAME,oBAAwCD,4BAAAA,SAAUE,SAAS;IACjE,MAAM,EAAER,MAAMS,eAAe,EAAE,GAAGzD,gBAChC,IACEqD,oBAAoBE,oBAChB5C,IACEoC,WACA,SACAJ,QACA,WACAU,kBACA,YACAE,qBAEF,MACN;QAACR;QAAWJ;QAAQU;QAAkBE;KAAkB;IAG1D;;;;;;;;;;;;GAYC,GACD,MAAMG,iBAAiBlC,oBACrB;QACEmC,WAAWC,eAAOX,yBAAAA,MAAOU,SAAS,mBAAI;QACtCE,oBAAoBD,gBAAOX,yBAAAA,MAAOY,kBAAkB,oBAAI;IAC1D,GACAP,4BAAAA,SAAUE,SAAS;IAGrB,MAAMM,SAAShD,QAAQ,IAAMG,eAAegC,yBAAAA,MAAOc,KAAK,GAAG;QAACd;KAAM;IAClE,MAAMe,aAAalD,QAAQ,IAAME,mBAAmBoC,QAAQ;QAACA;KAAM;IACnE,MAAMa,UAAUL,OAAOX,CAAAA,yBAAAA,MAAOgB,OAAO,KAAI;IACzC;;;;GAIC,GACD,MAAMC,eAAeN,gBAAOX,yBAAAA,MAAOkB,IAAI,oBAAI;IAC3C,MAAMC,aAAanB,QAAQ3B,gBAAgB2B,SAAS;IACpD,MAAMoB,QAAQT,gBAAOX,yBAAAA,MAAOC,MAAM,oBAAI;IACtC,wEAAwE,GACxE,MAAMoB,UAAUpD,oBAAoB+B;IACpC;;;;;;GAMC,GACD,MAAMsB,YAAYD,QAAQD,KAAK,KAAK;IACpC;;;;;;;;GAQC,GACD,MAAMG,SAASnD,cAAc4B;IAC7B,iEAAiE,GACjE,MAAMwB,cAAcb,gBAAOX,yBAAAA,MAAOwB,WAAW,oBAAI;IACjD;;;;;GAKC,GACD,MAAMC,YAAYC,gBAAO1B,yBAAAA,MAAOyB,SAAS,oBAAI,MAAM;IACnD,MAAME,aAAa3B,CAAAA,yBAAAA,MAAO4B,UAAU,IAChCvD,gBAAgB;QAAEwD,QAAQ7B,MAAM4B,UAAU;IAAC,KAC3C;IACJ;;;;;;;;GAQC,GACD,MAAMnC,SAASnB,YAAY0B;IAE3B;;;;;;;;;;;;;GAaC,GACD,MAAM8B,aAAanB,gBAAOX,yBAAAA,KAAO,CAAC9B,8BAA8B,oBAAIyB;IAEpE;;;;;;;;;;;;;;6CAc2C,GAC3C,MAAMoC,kBAAkB7C,mBAAmBQ;IAC3C,MAAM,EAAEsC,OAAO,EAAE,GAAGtF;IACpB,MAAM,EAAEuF,eAAe,EAAE,GAAGpF;IAC5B,MAAM,CAACqF,aAAaC,eAAe,GAAGrE,SAAS;IAE/C,MAAMsE,mBAAmBxE,YAAY;QACnC,IAAIsE,aAAa;QACjBC,eAAe;QACf,IAAI;;gBAYsBE,kBACDA,mBA+BHC;YA3CpB,MAAMD,UAAU,MAAMN,gBAAgB;gBACpCQ,QAAQ;gBACRT,YAAYnC;gBACZ6C,QAAQ;YACV;YACA,IAAI,CAACH,QAAQI,QAAQ,CAACC,EAAE,EAAE;;oBAEtBL;gBADF,OAAO,KAAKJ,0BACVI,oBAAAA,QAAQM,OAAO,qBAAfN,kBAAiBO,KAAK,oBAAI,mCAC1B;oBAAEC,SAAS;oBAAWC,gBAAgB;gBAAK;YAE/C;YACA,MAAMC,WAAWrB,gBAAOW,mBAAAA,QAAQM,OAAO,qBAAfN,iBAAiBW,QAAQ,mBAAI;YACrD,MAAMC,UAAUvB,iBAAOW,oBAAAA,QAAQM,OAAO,qBAAfN,kBAAiBa,cAAc,oBAAI;YAC1D,IAAI,CAACH,UAAU;gBACb,OAAO,KAAKd,gBACV,oDACA;oBAAEY,SAAS;oBAAQM,SAAS;gBAAM;YAEtC;YACA,MAAMC,SAAS,MAAMpB,QAAQ;gBAC3BqB,OAAO;gBACPC,aACE,CAAC,6BAA6B,EAAEP,SAASQ,cAAc,GAAG,MAAM,CAAC,GACjE,GAAGR,aAAa,IAAI,WAAW,SAAS,uBAAuB,CAAC,GAChE,CAAC,IAAI,EAAEE,QAAQM,cAAc,GAAG,iCAAiC,CAAC,GAClE,oEACA;gBACFC,kBAAkB;YACpB,GACGC,IAAI,CAAC,IAAM,MACXC,KAAK,CAAC,IAAM;YACf,IAAI,CAACN,QAAQ;YACb,MAAMd,SAAS,MAAMP,gBAAgB;gBACnCQ,QAAQ;gBACRT,YAAYnC;YACd;YACA,IAAI,CAAC2C,OAAOG,QAAQ,CAACC,EAAE,EAAE;;oBACKJ;gBAA5B,OAAO,KAAKL,0BAAgBK,mBAAAA,OAAOK,OAAO,qBAAdL,iBAAgBM,KAAK,oBAAI,eAAe;oBAClEC,SAAS;oBACTC,gBAAgB;gBAClB;YACF;YACAb,gBACE,CAAC,QAAQ,EAAEP,iBAAOY,kBAAAA,OAAOK,OAAO,qBAAdL,gBAAgBqB,IAAI,oBAAI,GAAGJ,cAAc,GAAG,MAAM,CAAC,GACnE,cACF;gBAAEV,SAAS;gBAAWM,SAAS;YAAM;QAEzC,EAAE,OAAOP,OAAO;YACdgB,QAAQhB,KAAK,CAACA;YACdX,gBAAgB,eAAe;gBAC7BY,SAAS;gBACTC,gBAAgB;YAClB;QACF,SAAU;YACRX,eAAe;QACjB;IACF,GAAG;QAACJ;QAAiBC;QAASrC;QAASsC;QAAiBC;KAAY;IAEpE;;;;;;;;;;;;;6CAa2C,GAC3C,MAAM,CAAC2B,MAAMC,QAAQ,GAAGhG,SAAS;IACjC,MAAM,CAACiG,SAASC,WAAW,GAAGlG,SAAwC;IAEtE,sEAAsE,GACtE,MAAMmG,YAAYrG,YAChB,OACEsG,KACAC,SACAC,SACAC;QAEA,IAAIR,MAAM,OAAO;QACjBC,QAAQI;QACR,IAAI;YACF,MAAM,EAAEzB,QAAQ,EAAEE,OAAO,EAAE,GAAG,MAAMZ,gBAAgB;gBAClDD,YAAYnC;eACTwE;YAEL,IAAI,CAAC1B,SAASC,EAAE,EAAE;;gBAChBT,wBAAgBU,2BAAAA,QAASC,KAAK,mBAAIyB,SAAS;oBACzCxB,SAAS;oBACTC,gBAAgB;gBAClB;gBACA,OAAO;YACT;YACAb,gBAAgBmC,QAAQzB,UAAU;gBAChCE,SAAS;gBACTM,SAAS;YACX;YACA,OAAO;QACT,EAAE,OAAOP,OAAO;YACdgB,QAAQhB,KAAK,CAACA;YACdX,gBAAgBoC,SAAS;gBAAExB,SAAS;gBAASC,gBAAgB;YAAK;YAClE,OAAO;QACT,SAAU;YACRgB,QAAQ;QACV;IACF,GACA;QAACD;QAAM9B;QAAiBpC;QAASsC;KAAgB;IAGnD,MAAMqC,gBAAgB1G,YAAY;QAChC;;;;;;KAMC,GACD,IAAIiG,MAAM;QACVC,QAAQ;QACR;;;;KAIC,GACD,IAAIf,WAA0B;QAC9B,IAAI;YACF,MAAMV,UAAU,MAAMN,gBAAgB;gBACpCQ,QAAQ;gBACRT,YAAYnC;gBACZ6C,QAAQ;YACV;YACA,IAAIH,QAAQI,QAAQ,CAACC,EAAE,EAAE;oBAErBL;oBAAAA,kBAA6BA;gBAD/BU,WAAWrB,QACTW,iBAAAA,mBAAAA,QAAQM,OAAO,qBAAfN,iBAAiBW,QAAQ,qBAAIX,oBAAAA,QAAQM,OAAO,qBAAfN,kBAAiBsB,IAAI,YAAlDtB,OAAsD;YAE1D,OAAO;;oBACWA;gBAAhBJ,0BAAgBI,oBAAAA,QAAQM,OAAO,qBAAfN,kBAAiBO,KAAK,oBAAI,6BAA6B;oBACrEC,SAAS;oBACTC,gBAAgB;gBAClB;YACF;QACF,EAAE,OAAOF,OAAO;YACdgB,QAAQhB,KAAK,CAACA;YACdX,gBAAgB,eAAe;gBAAEY,SAAS;gBAASC,gBAAgB;YAAK;QAC1E;QACAgB,QAAQ;QACR,IAAIf,aAAa,MAAM;QACvB,MAAMK,SAAS,MAAMpB,QAAQ;YAC3BqB,OAAO;YACPC,aACE,CAAC,iBAAiB,EAAEP,SAASQ,cAAc,GAAG,CAAC,CAAC,GAChD,GAAGR,aAAa,IAAI,WAAW,SAAS,cAAc,CAAC,GACtD3B,CAAAA,UAAU,cACP,mDACA,IAAG,IACP;YACFoC,kBAAkB;QACpB,GACGC,IAAI,CAAC,IAAM,MACXC,KAAK,CAAC,IAAM;QACf,IAAI,CAACN,QAAQ;QACb,MAAMa,UACJ,WACA;YAAE1B,QAAQ;QAAU,GACpB,CAACI;;mBACC,CAAC,QAAQ,EAAEjB,eAAOiB,2BAAAA,QAASgB,IAAI,mBAAI,GAAGJ,cAAc,GAAG,WAAW,CAAC;WACrE;IAEJ,GAAG;QACDM;QACA9B;QACAC;QACArC;QACAsC;QACAgC;QACA7C;KACD;IAED;;;;;;;;;;6CAU2C,GAC3C,MAAMmD,eAAe3G,YAAY;QAC/B,MAAM4G,UAAUnD,QAAQoD,QAAQ,CAACD,OAAO;QACxC,MAAME,OAAOrD,QAAQoD,QAAQ,CAACE,SAAS;QACvC,MAAMvB,SAAS,MAAMpB,QAAQ;YAC3BqB,OAAO/B,YAAY,6BAA6B;YAChDgC,aAAahC,YACT,CAAC,eAAe,EAAEkD,QAAQjB,cAAc,GAAG,CAAC,CAAC,GAC7C,GAAGiB,YAAY,IAAI,WAAW,SAAS,yBAAyB,CAAC,GACjE,CAAC,OAAO,EAAEE,KAAKnB,cAAc,GAAG,+BAA+B,CAAC,GAChE,oEACA,oEACA,mEACA,iCACA,oEACA,iEACA;YACJC,kBAAkBlC,YAAY,iBAAiB;QACjD,GACGmC,IAAI,CAAC,IAAM,MACXC,KAAK,CAAC,IAAM;QACf,IAAI,CAACN,QAAQ;QACb,MAAMa,UACJ,UACA;YAAE1B,QAAQ;QAAS,GACnB,IACEjB,YACI,mCACA,+BACN;IAEJ,GAAG;QAACU;QAASX,QAAQoD,QAAQ;QAAEnD;QAAW2C;KAAU;IAEpD,MAAMW,mBAAmBhH,YACvB,OAAOiH;QACL,MAAMC,OAAO,MAAMb,UACjB,YACA;YAAE1B,QAAQ;YAAYwC,UAAUF,OAAOE,QAAQ;QAAC,GAChD;gBAEWF;mBADT,CAAC,cAAc,EAAE,IAAIG,KACnBtD,QAAOmD,mBAAAA,OAAOE,QAAQ,YAAfF,mBAAmB,IAC1BtB,cAAc,IAAI;WACtB;QAEF,IAAIuB,MAAMd,WAAW;IACvB,GACA;QAACC;KAAU;IAGb;;;;;;;;;;;;;;;;;6CAiB2C,GAC3C,MAAMgB,YAAYhG,qBAAqBS;IACvC,MAAMwF,SAASvH;IAEf,MAAMwH,gBAAgBvH,YAAY;QAChC,MAAMwF,SAAS,MAAMpB,QAAQ;YAC3BqB,OAAO;YACPC,aACE,wEACA,wEACA;YACFE,kBAAkB;QACpB,GACGC,IAAI,CAAC,IAAM,MACXC,KAAK,CAAC,IAAM;QACf,IAAI,CAACN,QAAQ;QACb,IAAIS,MAAM;QACVC,QAAQ;QACR,IAAI;YACF,MAAM,EAAErB,QAAQ,EAAEE,OAAO,EAAE,GAAG,MAAMsC,UAAU;gBAC5C1C,QAAQ;gBACRT,YAAYnC;YACd;YACA,IAAI,CAAC8C,SAASC,EAAE,EAAE;;gBAChB,OAAO,KAAKT,wBACVU,2BAAAA,QAASC,KAAK,mBAAI,qCAClB;oBAAEC,SAAS;oBAAWC,gBAAgB;gBAAK;YAE/C;YACAb,gBAAgB,mBAAmB;gBAAEY,SAAS;gBAAWM,SAAS;YAAM;YACxE+B,OAAOE,IAAI,CAAC,GAAGxF,SAAS,SAAS,CAAC;QACpC,EAAE,OAAOgD,OAAO;YACdgB,QAAQhB,KAAK,CAACA;YACdX,gBAAgB,qCAAqC;gBACnDY,SAAS;gBACTC,gBAAgB;YAClB;QACF,SAAU;YACRgB,QAAQ;QACV;IACF,GAAG;QAAClE;QAAUiE;QAAM7B;QAASrC;QAASsC;QAAiBgD;QAAWC;KAAO;IAEzE,MAAMG,eAAezH,YACnB,OAAOiH;QACL,MAAMC,OAAO,MAAMb,UACjB,UACA;YAAE1B,QAAQ;YAAUf,aAAaqD,OAAOrD,WAAW;QAAC,GACpD,IAAM,gBACN;QAEF,IAAIsD,MAAMd,WAAW;IACvB,GACA;QAACC;KAAU;IAGb;;;;;;;;;;;;;;;;6CAgB2C,GAC3C,MAAMqB,YAAYlE,UAAU;IAC5B,MAAMmE,QAAQnE,UAAU;IACxB,MAAMoE,UAAUpE,UAAU;IAE1B,MAAMqE,gBAAsC;QAC1C;YACEvB,KAAK;YACLwB,OAAO;YACPC,oBAAM,KAAClJ;gBAAQmJ,MAAMtJ,iBAAiBsJ,IAAI;gBAAEC,MAAM;;YAClDC,SAAS,IAAM9B,WAAW;QAC5B;QACA;;;;;KAKC,MACG,AAACuB,CAAAA,SAASD,SAAQ,KAAM,CAAChE,YACzB;YACE;gBACE4C,KAAK;gBACLwB,OAAOJ,YAAY,eAAe;gBAClCK,oBAAM,KAAClJ;oBAAQmJ,MAAMzJ,wBAAwByJ,IAAI;oBAAEC,MAAM;;gBACzDC,SAAS,IAAM9B,WAAW;YAC5B;SACD,GACD,EAAE;WACFsB,aAAa,CAAChE,YACd;YACE;gBACE4C,KAAK;gBACLwB,OAAO;gBACPC,oBAAM,KAAClJ;oBAAQmJ,MAAMxJ,sBAAsBwJ,IAAI;oBAAEC,MAAM;;gBACvDE,aAAa;gBACbC,UAAUC,QAAQpC;gBAClBqC,gBAAgB;gBAChBJ,SAAS,IAAM,KAAKvB;YACtB;SACD,GACD,EAAE;QACN;;;;;;;;;;KAUC,MACGgB,QACA;YACE;gBACErB,KAAK;gBACLwB,OAAO;gBACPC,oBAAM,KAAClJ;oBAAQmJ,MAAMvJ,iBAAiBuJ,IAAI;oBAAEC,MAAM;;gBAClDE,aAAa;gBACbC,UAAUC,QAAQpC;gBAClBqC,gBAAgB;gBAChBJ,SAAS,IAAM,KAAKX;YACtB;SACD,GACD,EAAE;KACP;IAED;;;;;;;;GAQC,GACD,MAAMgB,gBACJ,AAACZ,CAAAA,SAASD,SAAQ,KAAM,CAAChE,0BACvB,KAACrE;QACC4I,MAAK;QACLhD,SAAQ;QACRmD,UAAUC,QAAQpC;QAClBiC,SAAS,IAAM,KAAKxB;kBAEnBT,SAAS,YAAY,cAAc;SAEpCvC,YACF;;;;;;;;;OASC,iBACD,KAACrE;QACC4I,MAAK;QACLhD,SAAQ;QACRuD,OAAM;QACNJ,UAAUC,QAAQpC;QAClBiC,SAAS,IAAM,KAAKvB;kBAEnB;SAEDnD,UAAU,uBACZ,KAACnE;QACC4I,MAAK;QACLhD,SAAQ;QACRmD,UAAU9D;QACV4D,SAAS,IAAM,KAAK1D;kBAEnBF,cAAc,cAAc;SAE7B;IAEN;;;;;;;;;;;;;GAaC,GACD,MAAMmE,WAAW,GAAGzG,SAAS,UAAU,EAAED,QAAQ,KAAK,CAAC;IAEvD,MAAM2G,8BACJ,MAAClJ;QAAMmJ,WAAU;QAAMC,SAAS;QAAGC,IAAI;YAAEC,YAAY;QAAS;;0BAC5D,KAACzJ;gBACC0J,WAAWpK;eACN;gBAAEqK,kBAAkB;gBAASC,cAAc;YAAM;gBACtDC,MAAM,GAAGlH,SAAS,SAAS,CAAC;gBAC5BiG,MAAK;gBACLO,OAAM;0BAEL;;YAEDb,CAAAA,SAASD,SAAQ,KAAM,CAAChE,0BACxB,KAACrE;gBACC0J,WAAWpK;eACN;gBAAEqK,kBAAkB;gBAASC,cAAc;YAAM;gBACtDC,MAAMT;gBACNR,MAAK;gBACLO,OAAM;0BAEL;kBAED;YACHhG,iCACC,KAACnD;gBACC0J,WAAWpK;eACN;gBAAEqK,kBAAkB;gBAASC,cAAc;YAAM;gBACtDC,MAAM,GAAGlH,SAAS,WAAW,EAAEQ,kBAAkB;gBACjDyF,MAAK;gBACLO,OAAM;0BAEL;kBAED;YACHD;0BACD,KAACxJ;gBAAe+I,OAAO1E;gBAAS+F,OAAOtB;;;;IAI3C,IAAIvF,UAAU;QACZ,qBACE,KAAC1D;YACCwK,QAAQ;YACRC,MAAM3H;YACN4H,cAAc;YACdC,cAAc;YACdC,aAAa;gBAAE7E,QAAQ+D;YAAc;sBAQrC,cAAA,KAAC7I;gBAAWoF,SAAQ;gBAAQuD,OAAM;0BAC/B;;;IAIT;IAEA,qBACE,MAAChJ;QAAMoJ,SAAS;;0BAGd,KAACvK;gBAAiBoH,OAAOrD,QAAQgB,UAAUqG;;0BAC3C,KAAC7K;gBACCwK,QAAQ;gBACRC,MAAM3H;gBACN4H,cAAc;gBACdC,cAAc;gBACdC,aAAa;oBAAE7E,QAAQ+D;gBAAc;0BAErC,cAAA,MAAClJ;oBAAMoJ,SAAS;;wBACb3F,OAAOyG,OAAO,CAACC,GAAG,CAAC,CAACC,uBACnB,KAACxK;gCAAsByK,UAAS;0CAC7BD,OAAOE,OAAO;+BADLF,OAAOG,EAAE;wBAItBpG,UAAUd,eAAemH,KAAK,iBAC7B,KAAC5K;4BAAMyK,UAAS;sCACb,8DACC,kEACA,iEACA;6BAEF;sCAEJ,KAACtK;sCAED,MAAC6B;4BAAQqE,OAAM;;8CACb,KAACzG;oCAAYiJ,MAAK;8CAChB,cAAA,MAACxI;;0DASC,MAACG;;kEACC,KAACF;kEAAW;;kEACZ,KAACA;wDAAUuK,OAAM;kEACf,cAAA,KAAC3K;4DACC2I,MAAK;4DACLO,OACE/E,QAAQD,KAAK,KAAK,YACd,SACAC,QAAQD,KAAK,KAAK,YAChB,YACAiG;4DAER3B,OAAOrE,QAAQqE,KAAK;;;;;4CAIzBpE,0BACC,MAAC9D;;kEACC,KAACF;kEAAW;;kEACZ,KAACA;wDAAUuK,OAAM;kEACd,GAAGxG,QAAQoD,QAAQ,CAACE,SAAS,CAACpB,cAAc,GAAG,OAAO,CAAC,GACtD,eACClC,CAAAA,QAAQoD,QAAQ,CAACqD,QAAQ,GACtB,CAAC,SAAS,EAAE,IAAI9C,KACd3D,QAAQoD,QAAQ,CAACqD,QAAQ,EACzBvE,cAAc,IAAI,GACpB,cAAa;;;iDAGrB;0DACJ,MAAC/F;;kEACC,KAACF;kEACE8D,UAAU,SAAS,SAAS;;kEAE/B,KAAC9D;wDAAUuK,OAAM;kEACd1G,aACG,IAAI6D,KAAK7D,YAAYoC,cAAc,KACnC;;;;4CASP9B,YAAY,kBACX,MAACjE;;kEACC,KAACF;kEAAW;;kEACZ,KAACA;wDAAUuK,OAAM;kEACd,GAAGpG,UAAU8B,cAAc,GAAG,gBAAgB,CAAC,GAC7C5B,CAAAA,aACG,IAAIqD,KAAKrD,YAAY4B,cAAc,KACnC,cAAa;;;iDAGrB;4CACH/B,4BACC,MAAChE;;kEACC,KAACF;kEAAW;;kEACZ,KAACA;wDAAUuK,OAAM;kEAASrG;;;iDAE1B;0DAkBJ,MAAChE;;kEACC,KAACF;kEAAW;;kEACZ,KAACA;wDAAUuK,OAAM;kEACdpI,OAAOsI,QAAQ,iBACd,KAACtK;4DAAWoF,SAAQ;4DAAQ4D,IAAI;gEAAEuB,YAAY;4DAAY;sEACvDvI,OAAOwI,IAAI;2EAGd,KAACxK;4DAAWoF,SAAQ;4DAAQuD,OAAM;sEAC/B7E,SAAS,iBAAiB;;;;;4CAalC9B,OAAOsI,QAAQ,iBACd,MAACvK;;kEACC,KAACF;kEAAW;;kEACZ,KAACA;wDAAUuK,OAAM;mEACdpI,mBAAAA,OAAOyI,QAAQ,YAAfzI,iCACC,KAAChC;4DAAWoF,SAAQ;4DAAQuD,OAAM;sEAC/B;;;;iDAKP;4CACH3G,OAAOsI,QAAQ,iBACd,MAACvK;;kEACC,KAACF;kEAAW;;kEACZ,KAACA;wDAAUuK,OAAM;mEACdpI,kBAAAA,OAAO0I,OAAO,YAAd1I,gCACC,KAAChC;4DAAWoF,SAAQ;4DAAQuD,OAAM;sEAC/B;;;;iDAKP;0DACJ,MAAC5I;;kEACC,KAACF;kEAAW;;kEACZ,KAACA;wDAAUuK,OAAM;kEAOdhI,6BACC,KAACtD;4DAAQuK,MAAM,GAAGjH,aAAa,WAAW,EAAEiC,YAAY;sEACrD;6DAGH;;;;0DAIN,MAACtE;;kEACC,KAACF;kEAAW;;kEACZ,KAACA;wDAAUuK,OAAM;kEACd1J,mBAAmB6B;;;;0DASxB,MAACxC;;kEACC,KAACF;kEAAW;;kEACZ,KAACA;wDAAUuK,OAAM;kEACdzH,iCACC,KAAC7D;4DACCuK,MAAM,GAAGlH,SAAS,WAAW,EAAEQ,kBAAkB;+EAEhDC,4BAAAA,SAAUmB,WAAW,oBAAI;2EAG5B,KAAC/D;4DAAWoF,SAAQ;4DAAQuD,OAAM;sEAC/B;;;;;;;;8CAYb,KAAC3I;oCAAWoF,SAAQ;oCAAUuD,OAAM;8CACjC,iEACC;;;;sCAIN,KAACjJ;wBAiBAoE,uBACC,KAACvC;4BAAQqE,OAAM;sCACb,cAAA,KAAC5F;gCAAWoF,SAAQ;gCAAQuD,OAAM;0CAC/BZ,UACG,4DACA,iCACAD,QACE,0DACA,uDACA,qDACA,0DACA;;2CAIV;;8CACF,KAACvG;oCAAQqE,OAAM;8CACb,cAAA,MAACjG;wCACCmJ,WAAU;wCACVC,SAAS;wCACT4B,UAAU;wCACV3B,IAAI;4CAAE4B,UAAU;wCAAO;;0DAEvB,KAACxJ;gDACC6G,OAAM;gDACN4C,OAAOzH,OAAO0H,UAAU;gDACxBC,MAAK;;0DAEP,KAAC3J;gDACC6G,OAAM;gDACN4C,OAAOzH,OAAO8C,IAAI;gDAClB6E,MAAK;;0DAEP,KAAC3J;gDACC6G,OAAM;gDACN4C,OAAOzH,OAAO4H,SAAS;gDACvBD,MAAK;;0DAEP,KAAC3J;gDAAO6G,OAAM;gDAAU4C,OAAOzH,OAAO6H,OAAO;gDAAEF,MAAK;;0DACpD,KAAC3J;gDACC6G,OAAM;gDACN4C,OAAOzH,OAAO8H,UAAU;gDACxBH,MAAK;;;;;8CAKX,KAACrL;8CAED,KAAC6B;oCAAQqE,OAAM;8CACb,cAAA,MAACjG;wCACCmJ,WAAU;wCACVC,SAAS;wCACT4B,UAAU;wCACV3B,IAAI;4CAAE4B,UAAU;wCAAO;;0DAEvB,KAACxJ;gDACC6G,OAAM;gDACN4C,OAAOzH,OAAO+H,KAAK;gDACnBJ,MAAK;;0DAEP,KAAC3J;gDACC6G,OAAM;gDACN4C,OAAOzH,OAAOgI,WAAW;gDACzBL,MAAK;;0DAEP,KAAC3J;gDACC6G,OAAM;gDACN4C,OAAOzH,OAAOiI,MAAM;gDACpBN,MAAK;;0DAEP,KAAC3J;gDACC6G,OAAM;gDACN4C,OAAOzH,OAAOkI,YAAY;gDAC1BP,MAAK;;0DAEP,KAAC3J;gDACC6G,OAAM;gDACN4C,OAAOzH,OAAOmI,YAAY;gDAC1BR,MAAK;;;;;8CAKX,KAACrL;8CAED,KAAC6B;oCAAQqE,OAAM;8CACb,cAAA,MAACjG;wCAAMoJ,SAAS;;0DACd,KAACzH;gDAAQ2G,OAAM;gDAAgBuD,MAAMpI,OAAOqI,KAAK,CAACC,QAAQ;;0DAC1D,KAACpK;gDAAQ2G,OAAM;gDAAYuD,MAAMpI,OAAOqI,KAAK,CAACE,IAAI;;0DAClD,KAACrK;gDAAQ2G,OAAM;gDAAauD,MAAMpI,OAAOqI,KAAK,CAACG,KAAK;;0DACpD,KAACtK;gDACC2G,OAAM;gDACNuD,MAAMpI,OAAOqI,KAAK,CAACI,WAAW;;0DAEhC,KAACvK;gDAAQ2G,OAAM;gDAAcuD,MAAMpI,OAAOqI,KAAK,CAACK,MAAM;;0DACtD,KAACxK;gDAAQ2G,OAAM;gDAAiBuD,MAAMpI,OAAOqI,KAAK,CAACM,SAAS;;0DAC5D,KAACzK;gDACC2G,OAAM;gDACNuD,MAAMpI,OAAOqI,KAAK,CAACO,WAAW;;;;;gCAKnC5I,OAAO6I,WAAW,CAACC,MAAM,iBACxB;;sDACE,KAACxM;sDACD,MAAC6B;4CAAQqE,OAAM;;8DACb,KAAC5F;oDAAWoF,SAAQ;oDAAQuD,OAAM;8DAC/B,6DACC,6DACA;;8DAEJ,KAACxJ;oDAAYiJ,MAAK;8DAChB,cAAA,KAACxI;kEACEwD,OAAO6I,WAAW,CAACnC,GAAG,CAAC,CAACqC,2BACvB,MAACpM;;kFACC,KAACF;kFAAWsM,WAAWlE,KAAK;;kFAC5B,KAACpI;wEAAUuK,OAAM;wEAAQpB,IAAI;4EAAEoD,YAAY;wEAAO;kFAC/CD,WAAWE,KAAK,CAACvG,cAAc;;kFAElC,KAACjG;wEAAUuK,OAAM;kFACf,cAAA,KAACpK;4EAAWoF,SAAQ;4EAAUuD,OAAM;sFACjC,CAAC,GAAG,EAAEwD,WAAWG,EAAE,CAACxG,cAAc,GAAG,CAAC,EAAEqG,WAAWI,OAAO,EAAE;;;;+DAPpDJ,WAAWjC,EAAE;;;;;;qCAgBpC;8CAEJ,KAACxK;8CAED,KAAC6B;oCAAQqE,OAAM;8CACZtC,WAAWkJ,IAAI,CAACN,MAAM,iBACrB;;0DACE,MAAC/M;gDAAYiJ,MAAK;;kEAChB,KAACtI;kEACC,cAAA,MAACC;;8EACC,KAACF;8EAAW;;8EACZ,KAACA;oEAAUuK,OAAM;8EAAS;;8EAC1B,KAACvK;oEAAUuK,OAAM;8EAAS;;;;;kEAG9B,KAACxK;kEACE0D,WAAWkJ,IAAI,CAAC1C,GAAG,CAAC,CAAC2C,oBACpB,MAAC1M;;kFACC,KAACF;wEAAUmJ,IAAI;4EAAE0D,WAAW;wEAAY;kFACrCD,IAAIE,GAAG;;kFAEV,KAAC9M;wEAAUuK,OAAM;wEAAQpB,IAAI;4EAAEoD,YAAY;wEAAO;kFAC/CK,IAAIpB,MAAM,CAACvF,cAAc;;kFAE5B,KAACjG;wEAAUuK,OAAM;kFACdqC,IAAIG,KAAK,GACN,GAAGvL,QAAQoL,IAAIG,KAAK,CAAC/B,KAAK,EAAE,IAAI,EAAE4B,IAAIG,KAAK,CAACC,WAAW,CAAC/G,cAAc,GAAG,CAAC,EAAE2G,IAAIG,KAAK,CAACE,gBAAgB,EAAE,GACxG;;;+DAVOL,IAAIE,GAAG;;;;0DAgB5B,KAAC3M;gDAAWoF,SAAQ;gDAAUuD,OAAM;0DACjC,8DACC,2DACA;;4CAEHrF,WAAWyJ,kBAAkB,iBAC5B,KAAC/M;gDAAWoF,SAAQ;gDAAUuD,OAAM;0DACjC,GAAGrF,WAAWyJ,kBAAkB,CAACjH,cAAc,GAAG,QAAQ,CAAC,GAC1D;iDAEF;4CACHxC,WAAW0J,cAAc,iBACxB,KAACzN;gDAAMyK,UAAS;0DACb,wDACC,CAAC,cAAc,EAAE1G,WAAW0J,cAAc,CAAClH,cAAc,GAAG,CAAC,CAAC,GAC9D,4DACA;iDAEF;;uDAGN,KAAC9F;wCAAWoF,SAAQ;wCAAQuD,OAAM;kDAC/BvF,OAAOiI,MAAM,GACV,2DACA,8DACA,aACA;;;;;;;;0BASd,KAAClK;gBAAoBc,QAAQA;gBAAQC,SAASA;;0BAa9C,KAACnD;gBACCwK,QAAQ;gBACRC,MAAM9H;gBACN+H,cAAc;gBACdC,cAAc;0BAEb/G,iCACC,KAAC1B;oBACCgB,QAAQA;oBACRgL,KAAK,EAAElK,mCAAAA,gBAAiBkK,KAAK;oBAC7BC,SAAStK,aAAagH,aAAa7G,oBAAoB6G;oBACvDrG,SAASA;oBACT4J,WAAWjK,gBAAON,4BAAAA,SAAUwK,cAAc,oBAAI;oBAC9CC,cACE,4DACA;oBAEFtC,MACE,yDACA,4DACA;mCAIJ,KAAC9J;oBACCgB,QAAQA;oBACRgL,OAAOrD;oBACP0D,MAAM9J;oBACN0J,SAAS3K,UAAUqH;oBACnBrG,SAASA;oBACT8J,cACE;oBAEFtC,MACE,6DACA,4DACA;;;0BAYR,KAAC7J;gBACCyK,MAAMrF,YAAY;gBAClBiH,SAAS,IAAMhH,WAAW;gBAC1BiH,OAAOlH,YAAY,aAAa,aAAa;gBAC7CV,OACEU,YAAY,aACRuB,YACE,0BACA,wBACF;gBAEN4F,aACEnH,YAAY,aACRuB,YACE,eACA,aACF;gBAEN9D,aAAaA;gBACbuD,UAAUO,YAAYnE,aAAa;gBACnC0C,MAAMoC,QAAQpC;gBACd2E,MACEzE,YAAY,aACR,iEACA,+BACAxC,SACE,+DACA,6DACA,kBACA,2DACA,+DACA;gBAER4J,UAAU,CAACtG,SACT,KAAMd,CAAAA,YAAY,aACda,iBAAiBC,UACjBQ,aAAaR,OAAM;;;;AAKjC;AACAtF,YAAYiC,WAAW,GAAG;AAE1B,eAAejC,YAAW"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/components/email-detail.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 { PageHeaderRecord, pluginDocsHelp } from '@aglyn/aglyn'\nimport {\n mdiCalendarClockOutline,\n mdiCloseCircleOutline,\n mdiDeleteOutline,\n mdiPencilOutline,\n} from '@aglyn/shared-data-mdi'\nimport { AppLink, CardDisplay, MdiIcon, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport RowActionsMenu, {\n type RowActionsMenuItem,\n} from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useFirestore, useFirestoreDoc } from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n Chip,\n Divider,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\nimport { doc } from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useMemo, useState } from 'react'\nimport {\n campaignLinkReport,\n campaignReport,\n type CampaignLinkRollup,\n type CampaignStats,\n} from '@aglyn/shared-ui-email-campaigns/model/campaign-report'\nimport {\n campaignSendDisplay,\n CAMPAIGN_SEND_CONTAINER_FIELD,\n} from '@aglyn/shared-ui-email-campaigns/model/campaign-container'\nimport {\n emailAudienceLabel,\n emailIsUnsent,\n emailSendTimeMs,\n emailSentAs,\n} from '@aglyn/shared-ui-email-campaigns/model/email-record'\nimport { emailPlainTextState } from '@aglyn/aglyn/app-utils/recipient-email-render'\nimport { useMarketingHubPath } from './use-marketing-hub-path'\nimport { CampaignDesignPreview as EmailDesignPreview } from './campaign-email-zones'\nimport EmailEditDrawer from './email-edit-drawer'\nimport EmailRecipientsCard from './email-recipients-card'\nimport {\n Figure,\n percent,\n RateRow,\n Section,\n} from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport {\n useCampaignManageApi,\n useCampaignSendApi,\n} from './use-campaign-send-api'\n\nconst previewDocsHelp = pluginDocsHelp('emailCampaigns', {\n anchor: '#the-campaign-report',\n excerpt:\n 'The email as an inbox receives it, drawn by the same renderer the send ' +\n 'path uses. Merge tokens are left standing — a real send fills them from ' +\n 'each recipient.',\n})\n\nconst emailDocsHelp = pluginDocsHelp('emailCampaigns', {\n anchor: '#the-campaign-report',\n excerpt:\n 'One email: what it looked like, who it went to, what was delivered, ' +\n 'and which links were followed — each rate over the population it is ' +\n 'measured against.',\n})\n\nexport interface EmailDetailProps {\n hostId: string\n /** The message document under `hosts/{hostId}/campaigns`. */\n emailId: string\n /** The emails hub URL, for the way back and for sibling links. */\n basePath: string\n}\n\n/**\n * ONE MESSAGE: what it looked like, where it went, and what it did.\n *\n * ## The template it was built from, drawn as it stands NOW\n *\n * The preview renders the template's CURRENT version, because the HTML that\n * was actually mailed is not stored — it is rendered per recipient at send\n * time, with that recipient's merge values in it, and keeping a copy per\n * message would be a copy of the whole email per address. So a message sent\n * before its template was last edited previews as the template is today, and\n * the frame says so rather than letting a reader take it for a record of what\n * went out.\n *\n * ## Every rate names its denominator\n *\n * The arithmetic is `campaign-report.ts` — the same pure module the campaign\n * report reads — so open rate over `delivered` and click rate over `delivered`\n * are computed once, carry their own denominator labels, and come back `null`\n * rather than 0% when they cannot honestly be taken. Nothing on this screen\n * divides anything.\n *\n * ## What this page reads\n *\n * Four documents: the message, its link rollup, the template screen and the\n * template's version. None of them grows with the size of the send. The\n * recipient list is the one read that does, and it is its own card with its\n * own request.\n */\nexport function EmailDetail(props: EmailDetailProps) {\n const { hostId, emailId, basePath } = props\n // The sibling hub: a campaign's page belongs to the Marketing console.\n const marketingHub = useMarketingHubPath()\n const firestore = useFirestore()\n\n const { data: email, status } = useFirestoreDoc<\n Record<string, any> & { stats?: CampaignStats }\n >(\n () => doc(firestore, 'hosts', hostId, 'campaigns', emailId),\n [firestore, hostId, emailId],\n )\n const notFound = status !== 'loading' && !email\n\n /*\n * The link rollup, its own document rather than a field on the message.\n *\n * A map of destinations grows with the content, and the message document is\n * read by the list, the glance widget and the send path; putting an\n * unbounded map on it would make every one of those reads larger.\n */\n const { data: links } = useFirestoreDoc<CampaignLinkRollup>(\n () =>\n doc(firestore, 'hosts', hostId, 'campaigns', emailId, 'reports', 'links'),\n [firestore, hostId, emailId],\n )\n\n const templateScreenId: string | undefined = email?.templateScreenId\n const { data: template } = useFirestoreDoc<any>(\n () =>\n templateScreenId\n ? doc(firestore, 'hosts', hostId, 'screens', templateScreenId)\n : null,\n [firestore, hostId, templateScreenId],\n )\n const templateVersionId: string | undefined = template?.versionId\n const { data: templateVersion } = useFirestoreDoc<any>(\n () =>\n templateScreenId && templateVersionId\n ? doc(\n firestore,\n 'hosts',\n hostId,\n 'screens',\n templateScreenId,\n 'versions',\n templateVersionId,\n )\n : null,\n [firestore, hostId, templateScreenId, templateVersionId],\n )\n\n /**\n * A hand-written plain-text version that no longer describes the design.\n *\n * Said HERE and not only in the composer, because an email can be scheduled\n * and then have its design edited — after which nobody opens the composer\n * again, and the send goes out with a styled half and a text half that\n * disagree. This page is where somebody looks at a scheduled email, so it is\n * where the fact has to be readable.\n *\n * Only while the email is unsent. On a sent one the text part that went out\n * is history; the design moving afterwards is expected and is what the\n * preview's own note already says.\n */\n const plainTextState = emailPlainTextState(\n {\n plainText: String(email?.plainText ?? ''),\n plainTextVersionId: String(email?.plainTextVersionId ?? ''),\n },\n template?.versionId,\n )\n\n const report = useMemo(() => campaignReport(email?.stats), [email])\n const linkReport = useMemo(() => campaignLinkReport(links), [links])\n const subject = String(email?.subject || 'Untitled email')\n /*\n * The composed body, kept on the send document. A message written without\n * a template still has a rendered HTML part in the inbox, so this is what\n * the preview draws for one.\n */\n const composedBody = String(email?.body ?? '')\n const sendTimeMs = email ? emailSendTimeMs(email) : 0\n const state = String(email?.status ?? '')\n /** What this email is doing, which the stored status alone cannot say. */\n const display = campaignSendDisplay(email as never)\n /**\n * Part way through an audience larger than one batch.\n *\n * Stored as `scheduled` — the state the processor claims to resume it — so\n * every control below that keyed on `scheduled` alone was offering an\n * action about an email that is already going out.\n */\n const midFlight = display.state === 'sending'\n /**\n * This email has not gone to anybody yet.\n *\n * Everything below the state table is a REPORT, and an unsent email has\n * nothing to report — no `stats` at all. Drawing the figures anyway would\n * fill the page with zeros and a delivery rate of 0%, which is the reading\n * \"this reached nobody\" rather than \"this has not been sent\", and those are\n * different facts about an email.\n */\n const unsent = emailIsUnsent(email)\n /** The merchant's own name for this email, where one was given. */\n const displayName = String(email?.displayName ?? '')\n /**\n * How many times this email has been sent, and when the last one was.\n *\n * A message written before an email could be sent twice carries neither, and\n * one send is what an absent count means — not zero.\n */\n const sendCount = Number(email?.sendCount ?? 1) || 1\n const lastSentMs = email?.lastSentAt\n ? emailSendTimeMs({ sentAt: email.lastSentAt })\n : 0\n /**\n * The sender this message actually left with, as the SEND recorded it.\n *\n * Read, never composed. The site's sending identity is a setting: a\n * merchant who verifies a new domain in November has not changed what went\n * out in March, and resolving the identity here would answer \"what would\n * this send as today\" on a page whose whole subject is a message that\n * already went. Exactly the rule the list name beside it follows.\n */\n const sentAs = emailSentAs(email)\n\n /*\n * The campaign this message belongs to.\n *\n * `emailCampaignId` — {@link CAMPAIGN_SEND_CONTAINER_FIELD} — is the one\n * linkage, and it is deliberately not spelled `campaignId`: on a message\n * document that name already means the message's OWN id, which is what the\n * report route addresses and what every delivered unsubscribe footer\n * carries as `cid=`.\n *\n * The fallback is the migration. A message written before campaigns grouped\n * anything names no container, and its own id IS the campaign the URL\n * resolves — the campaign detail route answers an id it does not recognize\n * as a container with that message's own report.\n */\n const campaignId = String(email?.[CAMPAIGN_SEND_CONTAINER_FIELD] ?? emailId)\n\n /*==========================================\n * SENDING THIS EMAIL TO MORE PEOPLE.\n *\n * The whole control is a confirmation and one POST. Every decision it looks\n * like it is making — who is left, who is suppressed, whether there is\n * allowance and hourly room — is made by the send path, which is also the\n * path the original send took; asking any of it here would be a second set\n * of rules to disagree with the first.\n *\n * Two requests rather than one, and the first is a READ. `dryRun` runs the\n * whole resolution and writes nothing, so the confirmation can say how many\n * people this would reach before the merchant agrees to it. A send is the\n * one action on this page that cannot be taken back, and \"Send to more\n * recipients?\" with no number in it is a button nobody can answer honestly.\n *=========================================*/\n const campaignSendApi = useCampaignSendApi(hostId)\n const { confirm } = useConfirmationContext()\n const { enqueueSnackbar } = useSnackbar()\n const [sendingMore, setSendingMore] = useState(false)\n\n const handleSendToMore = useCallback(async () => {\n if (sendingMore) return\n setSendingMore(true)\n try {\n const counted = await campaignSendApi({\n action: 'followUp',\n campaignId: emailId,\n dryRun: true,\n })\n if (!counted.response.ok) {\n return void enqueueSnackbar(\n counted.payload?.error ?? 'This email cannot be sent again',\n { variant: 'warning', allowDuplicate: true },\n )\n }\n const reaching = Number(counted.payload?.sendable ?? 0)\n const already = Number(counted.payload?.alreadyReached ?? 0)\n if (!reaching) {\n return void enqueueSnackbar(\n 'Everyone in this audience already has this email',\n { variant: 'info', persist: false },\n )\n }\n const agreed = await confirm({\n title: 'Send this email to more people?',\n description:\n `This sends the same email to ${reaching.toLocaleString()} more ` +\n `${reaching === 1 ? 'person' : 'people'} in the same audience. ` +\n `The ${already.toLocaleString()} who already received it are not ` +\n 'sent it again, and its report adds the new figures to the ones ' +\n 'it already holds.',\n confirmationText: 'Send',\n })\n .then(() => true)\n .catch(() => false)\n if (!agreed) return\n const result = await campaignSendApi({\n action: 'followUp',\n campaignId: emailId,\n })\n if (!result.response.ok) {\n return void enqueueSnackbar(result.payload?.error ?? 'Send failed', {\n variant: 'warning',\n allowDuplicate: true,\n })\n }\n enqueueSnackbar(\n `Sent to ${Number(result.payload?.sent ?? 0).toLocaleString()} more ` +\n 'recipients',\n { variant: 'success', persist: false },\n )\n } catch (error) {\n console.error(error)\n enqueueSnackbar('Send failed', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setSendingMore(false)\n }\n }, [campaignSendApi, confirm, emailId, enqueueSnackbar, sendingMore])\n\n /*==========================================\n * THE LIFECYCLE ACTIONS.\n *\n * Every one of them is one POST to the same route the composer and the\n * scheduled processor use. None of them decides anything: whether an email\n * may be sent now, rescheduled or canceled is decided by the route against\n * the record's stored `status`, so the rules live in one place and the\n * header's job is only to offer the ones that apply.\n *\n * The copy is deliberately NOT sent with any of them. `sendNow` reads the\n * whole message off the record — a request that could also carry a subject\n * and a body would be a way to put arbitrary copy on an existing send id\n * and mail it under that id's unsubscribe scope.\n *=========================================*/\n const [busy, setBusy] = useState('')\n const [editing, setEditing] = useState<'details' | 'schedule' | null>(null)\n\n /** One POST, one snackbar, one busy flag — the shape all four share. */\n const runAction = useCallback(\n async (\n key: string,\n request: Record<string, unknown>,\n success: (payload: any) => string,\n failure: string,\n ) => {\n if (busy) return false\n setBusy(key)\n try {\n const { response, payload } = await campaignSendApi({\n campaignId: emailId,\n ...request,\n })\n if (!response.ok) {\n enqueueSnackbar(payload?.error ?? failure, {\n variant: 'warning',\n allowDuplicate: true,\n })\n return false\n }\n enqueueSnackbar(success(payload), {\n variant: 'success',\n persist: false,\n })\n return true\n } catch (error) {\n console.error(error)\n enqueueSnackbar(failure, { variant: 'error', allowDuplicate: true })\n return false\n } finally {\n setBusy('')\n }\n },\n [busy, campaignSendApi, emailId, enqueueSnackbar],\n )\n\n const handleSendNow = useCallback(async () => {\n /*\n * Counted before it is offered, the same two-request shape the follow-up\n * uses: `dryRun` runs the whole resolution and writes nothing, so the\n * confirmation can name how many people this reaches. \"Send this now?\"\n * with no number in it is a question nobody can answer honestly, and this\n * is the action on the page that cannot be taken back.\n */\n if (busy) return\n setBusy('sendNow')\n /*\n * `null` for \"the count did not happen\", which is not the same answer as\n * zero — zero is a real reach that the confirmation would go on to\n * describe, and the failure branches below return rather than reaching it.\n */\n let reaching: number | null = null\n try {\n const counted = await campaignSendApi({\n action: 'sendNow',\n campaignId: emailId,\n dryRun: true,\n })\n if (counted.response.ok) {\n reaching = Number(\n counted.payload?.sendable ?? counted.payload?.sent ?? 0,\n )\n } else {\n enqueueSnackbar(counted.payload?.error ?? 'This email cannot be sent', {\n variant: 'warning',\n allowDuplicate: true,\n })\n }\n } catch (error) {\n console.error(error)\n enqueueSnackbar('Send failed', { variant: 'error', allowDuplicate: true })\n }\n setBusy('')\n if (reaching === null) return\n const agreed = await confirm({\n title: 'Send this email now?',\n description:\n `This sends it to ${reaching.toLocaleString()} ` +\n `${reaching === 1 ? 'person' : 'people'} straight away` +\n (state === 'scheduled'\n ? ', instead of at the time it is scheduled for. '\n : '. ') +\n 'It cannot be taken back once it goes.',\n confirmationText: 'Send now',\n })\n .then(() => true)\n .catch(() => false)\n if (!agreed) return\n await runAction(\n 'sendNow',\n { action: 'sendNow' },\n (payload) =>\n `Sent to ${Number(payload?.sent ?? 0).toLocaleString()} recipients`,\n 'Send failed',\n )\n }, [\n busy,\n campaignSendApi,\n confirm,\n emailId,\n enqueueSnackbar,\n runAction,\n state,\n ])\n\n /*==========================================\n * STOPPING A SEND, WHICH NOW MEANS TWO DIFFERENT THINGS.\n *\n * `cancel` acts on `scheduled`, and an email delivering an audience larger\n * than one batch is stored as `scheduled` between runs — so the control\n * that withdraws a campaign before it goes also stops one that is half\n * delivered, with no change to the route. That is a real capability, and a\n * merchant watching a send go wrong needs to be told which of the two they\n * are about to do: nothing has been mailed, or two thousand people already\n * have it and are keeping it.\n *=========================================*/\n const handleCancel = useCallback(async () => {\n const reached = display.progress.reached\n const left = display.progress.remaining\n const agreed = await confirm({\n title: midFlight ? 'Stop sending this email?' : 'Cancel this scheduled email?',\n description: midFlight\n ? `It has reached ${reached.toLocaleString()} ` +\n `${reached === 1 ? 'person' : 'people'} so far, and stopping it ` +\n `leaves ${left.toLocaleString()} unaddressed. What has already ` +\n 'gone out cannot be taken back — those messages stay in inboxes ' +\n 'and keep their unsubscribe links. The email and its report are ' +\n 'kept, but a stopped send cannot be resumed; reaching the rest ' +\n 'means composing a new email.'\n : 'It will not be sent at the time it is scheduled for. The email ' +\n 'and everything written on it are kept, but a canceled email ' +\n 'cannot be put back on the schedule — you would compose a new one.',\n confirmationText: midFlight ? 'Stop sending' : 'Cancel send',\n })\n .then(() => true)\n .catch(() => false)\n if (!agreed) return\n await runAction(\n 'cancel',\n { action: 'cancel' },\n () =>\n midFlight\n ? 'This email has stopped sending'\n : 'This email will not be sent',\n 'This email could not be canceled',\n )\n }, [confirm, display.progress, midFlight, runAction])\n\n const handleReschedule = useCallback(\n async (values: { sendAtMs?: number }) => {\n const done = await runAction(\n 'schedule',\n { action: 'schedule', sendAtMs: values.sendAtMs },\n () =>\n `Scheduled for ${new Date(\n Number(values.sendAtMs ?? 0),\n ).toLocaleString()}`,\n 'This email could not be scheduled',\n )\n if (done) setEditing(null)\n },\n [runAction],\n )\n\n /*==========================================\n * DISCARDING A DRAFT, WHICH IS THE ONE REMOVAL THIS PAGE HAS.\n *\n * Only ever offered on a `draft`, and refused again by the route inside the\n * transaction that deletes — the state on screen is a snapshot, and\n * `sendNow` claims a draft by moving it to `sending` in a transaction of\n * its own, so a check made only here could remove a record the send path\n * was mailing from.\n *\n * A sent email is never discardable from anywhere. Its report is what a\n * merchant answers a complaint with, and its id is inside the HMAC of every\n * unsubscribe link it delivered; a scheduled one is withdrawn with Cancel,\n * which keeps the record and takes it off the clock.\n *\n * The reader is sent back to the list afterwards rather than left on the\n * page of a record that no longer exists — which would render the \"could\n * not be loaded\" branch and read as a failure.\n *=========================================*/\n const manageApi = useCampaignManageApi(hostId)\n const router = useRouter()\n\n const handleDiscard = useCallback(async () => {\n const agreed = await confirm({\n title: 'Discard this draft?',\n description:\n 'This email has not been sent to anybody, and discarding it removes ' +\n 'it for good — the subject, the message and everything else written ' +\n 'on it. There is no undo.',\n confirmationText: 'Discard',\n })\n .then(() => true)\n .catch(() => false)\n if (!agreed) return\n if (busy) return\n setBusy('discard')\n try {\n const { response, payload } = await manageApi({\n action: 'discardEmail',\n campaignId: emailId,\n })\n if (!response.ok) {\n return void enqueueSnackbar(\n payload?.error ?? 'This draft could not be discarded',\n { variant: 'warning', allowDuplicate: true },\n )\n }\n enqueueSnackbar('Draft discarded', { variant: 'success', persist: false })\n router.push(`${basePath}/messages`)\n } catch (error) {\n console.error(error)\n enqueueSnackbar('This draft could not be discarded', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setBusy('')\n }\n }, [basePath, busy, confirm, emailId, enqueueSnackbar, manageApi, router])\n\n const handleRename = useCallback(\n async (values: { displayName?: string }) => {\n const done = await runAction(\n 'update',\n { action: 'update', displayName: values.displayName },\n () => 'Name updated',\n 'The name could not be updated',\n )\n if (done) setEditing(null)\n },\n [runAction],\n )\n\n /*==========================================\n * THE HEADER, IN THREE REGISTERS.\n *\n * Navigation reads as navigation — a naked link button, because that is\n * what it is and a reader should be able to tell without clicking. The\n * PRIMARY action of the state is the one contained button, so there is\n * exactly one on the page and it is the thing a merchant came to do.\n * Everything else goes in the overflow, and the two irreversible entries in\n * there are marked `destructive` so they carry the error color rather than\n * sitting in the list looking like \"Rename\".\n *\n * `RowActionsMenu` is named for table rows and its rendering is not: it is\n * a kebab `IconButton` and a `Menu` whose items support `onClick`,\n * `destructive`, `disabled` and `disabledReason` — exactly what a card\n * header's overflow needs. Reusing it is what keeps the menu on this page\n * behaving like every other overflow menu in the console.\n *=========================================*/\n const scheduled = state === 'scheduled'\n const draft = state === 'draft'\n const sending = state === 'sending'\n\n const overflowItems: RowActionsMenuItem[] = [\n {\n key: 'rename',\n label: 'Edit details',\n icon: <MdiIcon path={mdiPencilOutline.path} size={0.8} />,\n onClick: () => setEditing('details'),\n },\n /*\n Rescheduling an email that is ALREADY GOING OUT is not a thing to\n offer: its remaining batches are due when the sender said, and moving\n `sendAtMs` under the processor mid-campaign changes when the rest of a\n delivery happens rather than when it starts.\n */\n ...((draft || scheduled) && !midFlight\n ? [\n {\n key: 'schedule',\n label: scheduled ? 'Reschedule' : 'Schedule',\n icon: <MdiIcon path={mdiCalendarClockOutline.path} size={0.8} />,\n onClick: () => setEditing('schedule'),\n } as RowActionsMenuItem,\n ]\n : []),\n ...(scheduled && !midFlight\n ? [\n {\n key: 'cancel',\n label: 'Cancel send',\n icon: <MdiIcon path={mdiCloseCircleOutline.path} size={0.8} />,\n destructive: true,\n disabled: Boolean(busy),\n disabledReason: 'Another action on this email is still running',\n onClick: () => void handleCancel(),\n } as RowActionsMenuItem,\n ]\n : []),\n /*\n Discard is offered ONLY on a draft, and it is hidden rather than\n disabled everywhere else — the opposite of how this menu treats\n `Reschedule`, and deliberately.\n\n A disabled control tells a reader that the action exists for this\n record and is momentarily unavailable. There is no state in which a\n sent email becomes discardable, so showing the entry greyed out on one\n would be an offer this product will never honor, sitting under the\n report it is promising to destroy.\n */\n ...(draft\n ? [\n {\n key: 'discard',\n label: 'Discard draft',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n disabled: Boolean(busy),\n disabledReason: 'Another action on this email is still running',\n onClick: () => void handleDiscard(),\n } as RowActionsMenuItem,\n ]\n : []),\n ]\n\n /*\n * The one contained button, and what it is per state.\n *\n * `draft` and `scheduled` share it: the email has not gone out, so the act\n * is to make it go. A `sent` email's is the follow-up. A `canceled` one has\n * no primary act at all — it was withdrawn deliberately, and offering a way\n * to un-withdraw it would be a resurrect path this model does not have —\n * and neither does one that is mid-send.\n */\n const primaryAction =\n (draft || scheduled) && !midFlight ? (\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled={Boolean(busy)}\n onClick={() => void handleSendNow()}\n >\n {busy === 'sendNow' ? 'Checking…' : 'Send now'}\n </Button>\n ) : midFlight ? (\n /*\n A CAMPAIGN THAT IS ALREADY GOING OUT HAS ONE ACT: STOPPING IT.\n\n \"Send now\" is withheld rather than disabled, and the reason is not\n cosmetic — `sendNow` re-resolves the WHOLE audience and mails it, with\n no subtraction of anyone already reached, so pressing it on an email\n between batches sends a second copy to every person who has had the\n first. Withholding it leaves exactly one primary action, and it is the\n one a merchant watching a send go wrong actually wants.\n */\n <Button\n size=\"small\"\n variant=\"contained\"\n color=\"error\"\n disabled={Boolean(busy)}\n onClick={() => void handleCancel()}\n >\n {'Stop sending'}\n </Button>\n ) : state === 'sent' ? (\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled={sendingMore}\n onClick={() => void handleSendToMore()}\n >\n {sendingMore ? 'Checking…' : 'Send to more recipients'}\n </Button>\n ) : null\n\n /**\n * WHERE THIS EMAIL IS WRITTEN, which is no longer this page.\n *\n * A page cannot both be \"what this email did\" and \"write this email\" — the\n * first is a report the reader scrolls, the second is a form with one\n * irreversible button — so the composer is its own route and this is the\n * link to it. Naked, because navigation should read as navigation; the one\n * CONTAINED button on this page stays the primary act of the state.\n *\n * Offered only while the copy can still be changed. An email part way\n * through a send is stored as `scheduled`, so `midFlight` is what keeps the\n * link off a message that is already reaching inboxes — the same distinction\n * \"Send now\" is withheld on.\n */\n const editHref = `${basePath}/messages/${emailId}/edit`\n\n const headerActions = (\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={`${basePath}/messages`}\n size=\"small\"\n color=\"primary\"\n >\n {'All messages'}\n </Button>\n {(draft || scheduled) && !midFlight ? (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={editHref}\n size=\"small\"\n color=\"primary\"\n >\n {'Write this email'}\n </Button>\n ) : null}\n {templateScreenId ? (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={`${basePath}/templates/${templateScreenId}`}\n size=\"small\"\n color=\"primary\"\n >\n {'Open template'}\n </Button>\n ) : null}\n {primaryAction}\n <RowActionsMenu label={subject} items={overflowItems} />\n </Stack>\n )\n\n if (notFound) {\n return (\n <CardDisplay\n header={'Email'}\n help={emailDocsHelp}\n contentGutterX\n contentGutterY\n HeaderProps={{ action: headerActions }}\n >\n {/*\n * Not \"no data\". An email that cannot be read is a different\n * situation from one with no engagement, and rendering an empty\n * report for the first is how somebody comes to believe a message\n * they sent reached nobody.\n */}\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This email could not be loaded. It may have been deleted.'}\n </Typography>\n </CardDisplay>\n )\n }\n\n return (\n <Stack spacing={3}>\n {/* The page heading and the trail name the message; this card is\n then free to say what it holds rather than repeating the title. */}\n <PageHeaderRecord title={email ? subject : undefined} />\n <CardDisplay\n header={'Email'}\n help={emailDocsHelp}\n contentGutterX\n contentGutterY\n HeaderProps={{ action: headerActions }}\n >\n <Stack spacing={3}>\n {report.caveats.map((caveat) => (\n <Alert key={caveat.id} severity=\"info\">\n {caveat.message}\n </Alert>\n ))}\n {unsent && plainTextState.stale ? (\n <Alert severity=\"warning\">\n {'The design has been edited since this email’s plain-text ' +\n 'version was written, so the two halves may no longer say the ' +\n 'same thing. Nothing has overwritten what was written — open ' +\n 'this email to read it, or take the design’s text instead.'}\n </Alert>\n ) : null}\n\n <Divider />\n\n <Section title=\"Where this went\">\n <ScrollTable size=\"small\">\n <TableBody>\n {/*\n WHAT THIS EMAIL IS DOING, not the field it stores.\n\n An email delivering an audience larger than one batch is\n written back as `scheduled` between runs — the state the\n processor claims to resume it — so the stored status read\n \"Scheduled\" on a page reporting five hundred deliveries.\n */}\n <TableRow>\n <TableCell>{'State'}</TableCell>\n <TableCell align=\"right\">\n <Chip\n size=\"small\"\n color={\n display.state === 'sending'\n ? 'info'\n : display.state === 'stopped'\n ? 'warning'\n : undefined\n }\n label={display.label}\n />\n </TableCell>\n </TableRow>\n {midFlight ? (\n <TableRow>\n <TableCell>{'Next batch'}</TableCell>\n <TableCell align=\"right\">\n {`${display.progress.remaining.toLocaleString()} still ` +\n 'to reach, ' +\n (display.progress.nextAtMs\n ? `next run ${new Date(\n display.progress.nextAtMs,\n ).toLocaleString()}`\n : 'next run due')}\n </TableCell>\n </TableRow>\n ) : null}\n <TableRow>\n <TableCell>\n {state === 'sent' ? 'Sent' : 'Scheduled for'}\n </TableCell>\n <TableCell align=\"right\">\n {sendTimeMs\n ? new Date(sendTimeMs).toLocaleString()\n : 'not recorded'}\n </TableCell>\n </TableRow>\n {/*\n An email that has been sent more than once, said out loud.\n Every figure below covers all of them, and a reader who took\n the single `Sent` date above for the whole story would read\n the delivery numbers as one mailing's.\n */}\n {sendCount > 1 ? (\n <TableRow>\n <TableCell>{'Sends'}</TableCell>\n <TableCell align=\"right\">\n {`${sendCount.toLocaleString()}, most recently ` +\n (lastSentMs\n ? new Date(lastSentMs).toLocaleString()\n : 'not recorded')}\n </TableCell>\n </TableRow>\n ) : null}\n {displayName ? (\n <TableRow>\n <TableCell>{'Name'}</TableCell>\n <TableCell align=\"right\">{displayName}</TableCell>\n </TableRow>\n ) : null}\n {/*\n THE ADDRESS THIS MESSAGE ACTUALLY LEFT AS.\n\n Always a row. A site's sending identity can move — a domain\n verifies, a mailbox is renamed, a sender changes — so the\n question \"what did my recipients see\" has an answer only if\n the send wrote one down, and a page that omitted the row for\n the sends that did not would make an unanswerable question\n look like one nobody asked.\n\n Three states, and they are three different facts. A\n recorded address is what went out. An unsent email has no\n address yet, which is not the same as having lost one. And a\n message sent before the send began stamping its sender says\n so plainly rather than being handed today's identity, which\n would be this page inventing history.\n */}\n <TableRow>\n <TableCell>{'Sent as'}</TableCell>\n <TableCell align=\"right\">\n {sentAs.recorded ? (\n <Typography variant=\"body2\" sx={{ fontFamily: 'monospace' }}>\n {sentAs.from}\n </Typography>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {unsent ? 'not sent yet' : 'not recorded'}\n </Typography>\n )}\n </TableCell>\n </TableRow>\n {/*\n The two rows that only exist once there is a sender to\n describe. Both name what an ABSENT value means rather than\n leaving a blank: a message with no display name showed the\n address on its own, and one with no reply address takes\n replies where it was sent from. Neither is missing\n information — each is a fact the record states by omission.\n */}\n {sentAs.recorded ? (\n <TableRow>\n <TableCell>{'From name'}</TableCell>\n <TableCell align=\"right\">\n {sentAs.fromName ?? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'The address on its own'}\n </Typography>\n )}\n </TableCell>\n </TableRow>\n ) : null}\n {sentAs.recorded ? (\n <TableRow>\n <TableCell>{'Reply-to'}</TableCell>\n <TableCell align=\"right\">\n {sentAs.replyTo ?? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'The sending address'}\n </Typography>\n )}\n </TableCell>\n </TableRow>\n ) : null}\n <TableRow>\n <TableCell>{'Campaign'}</TableCell>\n <TableCell align=\"right\">\n {/*\n The campaign's page belongs to the Marketing console, so\n this href is built from the sibling hub rather than this\n surface's own. Plain text until that hub resolves: a\n link with no destination is worse than none.\n */}\n {marketingHub ? (\n <AppLink href={`${marketingHub}/campaigns/${campaignId}`}>\n {'Open the campaign'}\n </AppLink>\n ) : (\n 'Open the campaign'\n )}\n </TableCell>\n </TableRow>\n <TableRow>\n <TableCell>{'List'}</TableCell>\n <TableCell align=\"right\">\n {emailAudienceLabel(email)}\n </TableCell>\n </TableRow>\n {/*\n Always a row, never a hidden one. A template this email did\n not use and a template row that was not rendered look\n identical to a reader, and the second sends them looking for\n a link that was never going to be there.\n */}\n <TableRow>\n <TableCell>{'Template'}</TableCell>\n <TableCell align=\"right\">\n {templateScreenId ? (\n <AppLink\n href={`${basePath}/templates/${templateScreenId}`}\n >\n {template?.displayName ?? 'Untitled template'}\n </AppLink>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Written as plain text in the composer'}\n </Typography>\n )}\n </TableCell>\n </TableRow>\n </TableBody>\n </ScrollTable>\n {/*\n * The list is named as the SEND recorded it, and saying so is\n * what stops a renamed or deleted list quietly rewriting the\n * history of a message that went out months ago.\n */}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'The list and the sender are recorded as they were when this ' +\n 'email was sent, not as this site is configured now.'}\n </Typography>\n </Section>\n\n <Divider />\n\n {/*==========================================\n * AN EMAIL THAT HAS NOT BEEN SENT HAS NO REPORT.\n *\n * Not an empty one — none. Every figure below divides or counts\n * something that only exists once mail has gone out, and an unsent\n * email carries no `stats` at all, so rendering the sections would\n * publish a column of zeros and a delivery rate of 0%. That reads\n * as \"this reached nobody\", which is a claim about a send that\n * happened; the truth is that no send has happened.\n *\n * The same reasoning the rate rows already follow, one level up: a\n * rate whose denominator is unrecorded renders absent rather than\n * as 0%, and a report whose whole subject is unrecorded renders\n * absent rather than as zeros.\n *=========================================*/}\n {unsent ? (\n <Section title=\"Delivery\">\n <Typography variant=\"body2\" color=\"text.secondary\">\n {sending\n ? 'This email is being sent right now. Its figures appear ' +\n 'here once the send finishes.'\n : draft\n ? 'This email has not been sent, so there is nothing to ' +\n 'report yet. Write this email — the link is in the ' +\n 'header — then send it or put it on the schedule.'\n : 'This email has not been sent yet. Its figures appear ' +\n 'here once it goes out.'}\n </Typography>\n </Section>\n ) : (\n <>\n <Section title=\"Delivery\">\n <Stack\n direction=\"row\"\n spacing={4}\n useFlexGap\n sx={{ flexWrap: 'wrap' }}\n >\n <Figure\n label=\"Addressed\"\n value={report.recipients}\n note=\"after the per-send cap\"\n />\n <Figure\n label=\"Sent\"\n value={report.sent}\n note=\"accepted by the provider\"\n />\n <Figure\n label=\"Delivered\"\n value={report.delivered}\n note=\"accepted by the receiving server\"\n />\n <Figure label=\"Bounced\" value={report.bounced} note=\"of sent\" />\n <Figure\n label=\"Marked as spam\"\n value={report.complained}\n note=\"of delivered\"\n />\n </Stack>\n </Section>\n\n <Divider />\n\n <Section title=\"Engagement\">\n <Stack\n direction=\"row\"\n spacing={4}\n useFlexGap\n sx={{ flexWrap: 'wrap' }}\n >\n <Figure\n label=\"Opens\"\n value={report.opens}\n note=\"every open, repeats included\"\n />\n <Figure\n label=\"Readers who opened\"\n value={report.uniqueOpens}\n note=\"distinct recipients\"\n />\n <Figure\n label=\"Clicks\"\n value={report.clicks}\n note=\"every click, repeats included\"\n />\n <Figure\n label=\"Readers who clicked\"\n value={report.uniqueClicks}\n note=\"distinct recipients\"\n />\n <Figure\n label=\"Unsubscribed\"\n value={report.unsubscribes}\n note=\"through this email's link\"\n />\n </Stack>\n </Section>\n\n <Divider />\n\n <Section title=\"Rates\">\n <Stack spacing={1}>\n <RateRow label=\"Delivery rate\" rate={report.rates.delivery} />\n <RateRow label=\"Open rate\" rate={report.rates.open} />\n <RateRow label=\"Click rate\" rate={report.rates.click} />\n <RateRow\n label=\"Click-to-open rate\"\n rate={report.rates.clickToOpen}\n />\n <RateRow label=\"Bounce rate\" rate={report.rates.bounce} />\n <RateRow label=\"Complaint rate\" rate={report.rates.complaint} />\n <RateRow\n label=\"Unsubscribe rate\"\n rate={report.rates.unsubscribe}\n />\n </Stack>\n </Section>\n\n {report.populations.length ? (\n <>\n <Divider />\n <Section title=\"Who this was allowed to reach\">\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Measured when this email was sent, and stored as it was ' +\n 'then. These figures describe the send, not the audience ' +\n 'as it stands today.'}\n </Typography>\n <ScrollTable size=\"small\">\n <TableBody>\n {report.populations.map((population) => (\n <TableRow key={population.id}>\n <TableCell>{population.label}</TableCell>\n <TableCell align=\"right\" sx={{ fontWeight: 'bold' }}>\n {population.count.toLocaleString()}\n </TableCell>\n <TableCell align=\"right\">\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`of ${population.of.toLocaleString()} ${population.ofLabel}`}\n </Typography>\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n </Section>\n </>\n ) : null}\n\n <Divider />\n\n <Section title=\"Links\">\n {linkReport.rows.length ? (\n <>\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Destination'}</TableCell>\n <TableCell align=\"right\">{'Clicks'}</TableCell>\n <TableCell align=\"right\">{'Share'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {linkReport.rows.map((row) => (\n <TableRow key={row.url}>\n <TableCell sx={{ wordBreak: 'break-all' }}>\n {row.url}\n </TableCell>\n <TableCell align=\"right\" sx={{ fontWeight: 'bold' }}>\n {row.clicks.toLocaleString()}\n </TableCell>\n <TableCell align=\"right\">\n {row.share\n ? `${percent(row.share.value)} of ${row.share.denominator.toLocaleString()} ${row.share.denominatorLabel}`\n : '—'}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Counted by address and path — query strings are dropped, ' +\n 'so two links to the same page with different tracking ' +\n 'parameters count as one row.'}\n </Typography>\n {linkReport.unattributedClicks ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`${linkReport.unattributedClicks.toLocaleString()} clicks ` +\n 'arrived without a destination and are not in this table.'}\n </Typography>\n ) : null}\n {linkReport.overflowClicks ? (\n <Alert severity=\"info\">\n {'This email has more distinct destinations than the ' +\n `rollup keeps. ${linkReport.overflowClicks.toLocaleString()} ` +\n 'clicks landed on links past that limit and are counted ' +\n 'in the click total above but not in this table.'}\n </Alert>\n ) : null}\n </>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {report.clicks\n ? 'Clicks were recorded for this email, but none of them ' +\n 'carried a destination, so there is nothing to break down ' +\n 'by link.'\n : 'No link clicks have been recorded for this email.'}\n </Typography>\n )}\n </Section>\n </>\n )}\n </Stack>\n </CardDisplay>\n\n <EmailRecipientsCard hostId={hostId} emailId={emailId} />\n\n {/*\n * Last, and its own card. The numbers are what a reader came for and\n * the preview is the tallest thing on the page — above them it pushes\n * every figure below the fold.\n *\n * `header` rather than `title`: `CardDisplay` has no `title` prop, so\n * one spreads through to the MUI `Card` root and lands on the DOM as a\n * hover tooltip, leaving the card with no heading at all. The gutters\n * are named for the same reason — without them the 640px frame sits\n * flush against the card's edge.\n */}\n <CardDisplay\n header={'Preview'}\n help={previewDocsHelp}\n contentGutterX\n contentGutterY\n >\n {templateScreenId ? (\n <EmailDesignPreview\n hostId={hostId}\n nodes={templateVersion?.nodes}\n loading={template === undefined || templateVersion === undefined}\n subject={subject}\n preheader={String(template?.emailPreheader ?? '')}\n emptyMessage={\n 'The template this email was built from is empty or has ' +\n 'been deleted, so there is nothing to draw.'\n }\n note={\n 'The template as it stands today. The mail itself is ' +\n 'rendered per recipient at send time and not kept, so a ' +\n 'template edited since this went out previews as it is now.'\n }\n />\n ) : (\n <EmailDesignPreview\n hostId={hostId}\n nodes={undefined}\n text={composedBody}\n loading={email === undefined}\n subject={subject}\n emptyMessage={\n 'This email carries no body, so there is nothing to draw.'\n }\n note={\n 'Written as plain text in the composer. Merge tokens are ' +\n 'left standing here — the mail itself resolves them per ' +\n 'recipient at send time and is not kept.'\n }\n />\n )}\n </CardDisplay>\n\n {/*\n * Editing in a DRAWER, never a form above the content. The name is the\n * one detail a sent email still owns — see the drawer's own header for\n * why the subject, body, audience and topic are not on offer once mail\n * has been delivered.\n */}\n <EmailEditDrawer\n open={editing !== null}\n onClose={() => setEditing(null)}\n field={editing === 'schedule' ? 'schedule' : 'details'}\n title={\n editing === 'schedule'\n ? scheduled\n ? 'Reschedule this email'\n : 'Schedule this email'\n : 'Edit details'\n }\n submitLabel={\n editing === 'schedule'\n ? scheduled\n ? 'Reschedule'\n : 'Schedule'\n : 'Save'\n }\n displayName={displayName}\n sendAtMs={scheduled ? sendTimeMs : 0}\n busy={Boolean(busy)}\n note={\n editing === 'schedule'\n ? 'The email goes out at this time. You can send it sooner, or ' +\n 'cancel it, from this page.'\n : unsent\n ? 'The name is for finding this email in your own lists. The ' +\n 'subject and the message are written on this email’s own ' +\n 'compose page.'\n : 'This email has been sent, so its subject, message and ' +\n 'audience describe mail that is already in inboxes and can ' +\n 'no longer be changed. Its name is yours and stays editable.'\n }\n onSubmit={(values) =>\n void (editing === 'schedule'\n ? handleReschedule(values)\n : handleRename(values))\n }\n />\n </Stack>\n )\n}\nEmailDetail.displayName = 'EmailDetail'\n\nexport default EmailDetail\n"],"names":["PageHeaderRecord","pluginDocsHelp","mdiCalendarClockOutline","mdiCloseCircleOutline","mdiDeleteOutline","mdiPencilOutline","AppLink","CardDisplay","MdiIcon","useConfirmationContext","RowActionsMenu","ScrollTable","useSnackbar","useFirestore","useFirestoreDoc","Alert","Button","Chip","Divider","Stack","TableBody","TableCell","TableHead","TableRow","Typography","doc","useRouter","useCallback","useMemo","useState","campaignLinkReport","campaignReport","campaignSendDisplay","CAMPAIGN_SEND_CONTAINER_FIELD","emailAudienceLabel","emailIsUnsent","emailSendTimeMs","emailSentAs","emailPlainTextState","useMarketingHubPath","CampaignDesignPreview","EmailDesignPreview","EmailEditDrawer","EmailRecipientsCard","Figure","percent","RateRow","Section","useCampaignManageApi","useCampaignSendApi","previewDocsHelp","anchor","excerpt","emailDocsHelp","EmailDetail","props","sentAs","hostId","emailId","basePath","marketingHub","firestore","data","email","status","notFound","links","templateScreenId","template","templateVersionId","versionId","templateVersion","plainTextState","plainText","String","plainTextVersionId","report","stats","linkReport","subject","composedBody","body","sendTimeMs","state","display","midFlight","unsent","displayName","sendCount","Number","lastSentMs","lastSentAt","sentAt","campaignId","campaignSendApi","confirm","enqueueSnackbar","sendingMore","setSendingMore","handleSendToMore","counted","result","action","dryRun","response","ok","payload","error","variant","allowDuplicate","reaching","sendable","already","alreadyReached","persist","agreed","title","description","toLocaleString","confirmationText","then","catch","sent","console","busy","setBusy","editing","setEditing","runAction","key","request","success","failure","handleSendNow","handleCancel","reached","progress","left","remaining","handleReschedule","values","done","sendAtMs","Date","manageApi","router","handleDiscard","push","handleRename","scheduled","draft","sending","overflowItems","label","icon","path","size","onClick","destructive","disabled","Boolean","disabledReason","primaryAction","color","editHref","headerActions","direction","spacing","sx","alignItems","component","componentVariant","nativeButton","href","items","header","help","contentGutterX","contentGutterY","HeaderProps","undefined","caveats","map","caveat","severity","message","id","stale","align","nextAtMs","recorded","fontFamily","from","fromName","replyTo","useFlexGap","flexWrap","value","recipients","note","delivered","bounced","complained","opens","uniqueOpens","clicks","uniqueClicks","unsubscribes","rate","rates","delivery","open","click","clickToOpen","bounce","complaint","unsubscribe","populations","length","population","fontWeight","count","of","ofLabel","rows","row","wordBreak","url","share","denominator","denominatorLabel","unattributedClicks","overflowClicks","nodes","loading","preheader","emailPreheader","emptyMessage","text","onClose","field","submitLabel","onSubmit"],"mappings":"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"}