@aglyn/plugins-marketing 1.0.0-beta.178 → 1.0.0-beta.180

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +3 -3
  2. package/package.json +15 -15
  3. package/src/lib/components/campaign-conversions-card.js +22 -4
  4. package/src/lib/components/campaign-conversions-card.js.map +1 -1
  5. package/src/lib/components/campaign-detail-card.js +3 -4
  6. package/src/lib/components/campaign-detail-card.js.map +1 -1
  7. package/src/lib/components/campaign-report-card.js +3 -3
  8. package/src/lib/components/campaign-report-card.js.map +1 -1
  9. package/src/lib/components/email-compose-card.d.ts +5 -2
  10. package/src/lib/components/email-compose-card.js +2 -3
  11. package/src/lib/components/email-compose-card.js.map +1 -1
  12. package/src/lib/components/email-detail.d.ts +5 -2
  13. package/src/lib/components/email-detail.js +4 -4
  14. package/src/lib/components/email-detail.js.map +1 -1
  15. package/src/lib/components/email-messages-widget.d.ts +17 -2
  16. package/src/lib/components/email-messages-widget.js +33 -2
  17. package/src/lib/components/email-messages-widget.js.map +1 -1
  18. package/src/lib/components/emails-list-card.d.ts +9 -6
  19. package/src/lib/components/emails-list-card.js +9 -9
  20. package/src/lib/components/emails-list-card.js.map +1 -1
  21. package/src/lib/components/host-experiments-card.component.d.ts +2 -0
  22. package/src/lib/components/host-experiments-card.component.js +20 -25
  23. package/src/lib/components/host-experiments-card.component.js.map +1 -1
  24. package/src/lib/components/host-marketing-summary-card.component.d.ts +15 -0
  25. package/src/lib/components/host-marketing-summary-card.component.js +59 -68
  26. package/src/lib/components/host-marketing-summary-card.component.js.map +1 -1
  27. package/src/lib/components/host-overlays-card.component.js +15 -37
  28. package/src/lib/components/host-overlays-card.component.js.map +1 -1
  29. package/src/lib/components/inbox-campaigns-widget.d.ts +23 -0
  30. package/src/lib/components/inbox-campaigns-widget.js +63 -0
  31. package/src/lib/components/inbox-campaigns-widget.js.map +1 -0
  32. package/src/lib/components/marketing-aggregates.d.ts +95 -0
  33. package/src/lib/components/marketing-aggregates.js +114 -0
  34. package/src/lib/components/marketing-aggregates.js.map +1 -0
  35. package/src/lib/components/marketing-console-page.js +32 -27
  36. package/src/lib/components/marketing-console-page.js.map +1 -1
  37. package/src/lib/components/marketing-console-sections.d.ts +24 -13
  38. package/src/lib/components/marketing-console-sections.js +37 -12
  39. package/src/lib/components/marketing-console-sections.js.map +1 -1
  40. package/src/lib/components/org-conversions-section.d.ts +32 -0
  41. package/src/lib/components/org-conversions-section.js +130 -0
  42. package/src/lib/components/org-conversions-section.js.map +1 -0
  43. package/src/lib/components/org-experiments-card.d.ts +32 -0
  44. package/src/lib/components/org-experiments-card.js +407 -0
  45. package/src/lib/components/org-experiments-card.js.map +1 -0
  46. package/src/lib/components/org-marketing-overview-card.d.ts +34 -0
  47. package/src/lib/components/org-marketing-overview-card.js +342 -0
  48. package/src/lib/components/org-marketing-overview-card.js.map +1 -0
  49. package/src/lib/components/org-overlays-card.d.ts +35 -0
  50. package/src/lib/components/org-overlays-card.js +345 -0
  51. package/src/lib/components/org-overlays-card.js.map +1 -0
  52. package/src/lib/components/org-site-picker-button.d.ts +26 -0
  53. package/src/lib/components/org-site-picker-button.js +104 -0
  54. package/src/lib/components/org-site-picker-button.js.map +1 -0
  55. package/src/lib/components/overlay-labels.d.ts +38 -0
  56. package/src/lib/components/overlay-labels.js +55 -0
  57. package/src/lib/components/overlay-labels.js.map +1 -0
  58. package/src/lib/components/use-emails-hub-path.d.ts +17 -8
  59. package/src/lib/components/use-emails-hub-path.js +29 -11
  60. package/src/lib/components/use-emails-hub-path.js.map +1 -1
  61. package/src/lib/components/use-one-shot-reads.d.ts +55 -0
  62. package/src/lib/components/use-one-shot-reads.js +168 -0
  63. package/src/lib/components/use-one-shot-reads.js.map +1 -0
  64. package/src/lib/model/experiments.d.ts +35 -0
  65. package/src/lib/model/experiments.js +34 -0
  66. package/src/lib/model/experiments.js.map +1 -1
  67. package/src/lib/model/overlays.d.ts +13 -2
  68. package/src/lib/model/overlays.js +18 -6
  69. package/src/lib/model/overlays.js.map +1 -1
  70. package/src/lib/plugin.js +13 -8
  71. package/src/lib/plugin.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/components/campaign-detail-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n mdiDeleteOutline,\n mdiEyeOutline,\n mdiPaletteOutline,\n mdiPencilOutline,\n} from '@aglyn/shared-data-mdi'\nimport {\n AppLink,\n CardDisplay,\n MdiIcon,\n useConfirmationContext,\n} from '@aglyn/shared-ui-jsx'\nimport {\n Figure,\n RateRow,\n Section,\n} from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport {\n ListRowActions,\n ListTable,\n listActionsColumn,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport RowActionsMenu, {\n type RowActionsMenuItem,\n} from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { TABLE_ROW_HEIGHT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { ceilingedWindow } from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport { PageHeaderRecord, pluginDocsHelp } from '@aglyn/aglyn'\nimport {\n Alert,\n Box,\n Button,\n Chip,\n Divider,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport {\n collection,\n deleteField,\n documentId,\n limit,\n orderBy,\n query,\n updateDoc,\n where,\n} from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport { useRouter } from 'next/navigation'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useFirestoreCollection,\n useFirestoreDoc,\n} from '@aglyn/tenant-feature-instance'\nimport {\n campaignRollup,\n campaignSendDisplay,\n campaignSiteIds,\n campaignVisibleTo,\n campaignWindowState,\n emailListTimeMs,\n type CampaignAggregate,\n type CampaignSend,\n type EmailCampaign,\n} from '@aglyn/shared-ui-email-campaigns/model'\nimport CampaignComposer from './campaign-composer'\nimport CampaignEditDrawer, {\n type CampaignEditValues,\n} from './campaign-edit-drawer'\nimport {\n CampaignConversionsSection,\n CampaignDestinationsSection,\n CampaignRevenueSection,\n CampaignSequencesSection,\n} from './campaign-reach-sections'\nimport CampaignMembersSection from './campaign-members-section'\nimport CampaignReportCard from './campaign-report-card'\nimport { campaignContainerDoc, campaignSendsQuery } from './campaign-queries'\nimport {\n orgSiteHubPath,\n orgSiteName,\n orgSiteOptions,\n topicCatalogHostId,\n useMarketingOrgId,\n useMarketingOrgMount,\n} from './marketing-org-mount'\nimport { useCampaignManageApi } from './use-campaign-send-api'\nimport { useEmailsHubPath } from './use-emails-hub-path'\nimport { useCampaignTopicOptions } from './use-campaign-topic-options'\n\n/** How many of a campaign's emails the detail page enumerates. */\nconst CAMPAIGN_EMAIL_CEILING = 50\n\nconst detailDocsHelp = pluginDocsHelp('emailCampaigns', {\n anchor: '#the-campaign-report',\n excerpt:\n 'A campaign groups the emails sent to its lists. The figures here are ' +\n 'the sum across those emails, and each one keeps its own report.',\n})\n\n/**\n * A rolled-up figure, in the shared `Figure`'s shape.\n *\n * The note is where an aggregate differs from a single send's count: a total\n * measured over fewer emails than the campaign holds says which part of the\n * campaign it describes, rather than presenting a partial sum as a complete\n * one. `null` still renders as an em dash and says \"not recorded\", which is\n * the property the shared component already owns.\n */\nconst rolled = (\n value: CampaignAggregate,\n): { value: number | null; note: string } => ({\n value: value.value,\n note:\n value.value !== null && value.recorded < value.sends\n ? `across ${value.recorded} of ${value.sends} emails`\n : `across ${value.sends} email${value.sends === 1 ? '' : 's'}`,\n})\n\nexport interface CampaignDetailCardProps {\n /** The site, or `null` on the org Marketing hub. */\n hostId: string | null\n /** A campaign container id, or a send id from before containers existed. */\n campaignId: string\n /** The marketing hub URL, for the way back to the campaigns list. */\n basePath: string\n}\n\n/**\n * ONE CAMPAIGN: its lists, what it caused, what it earned, where it sent\n * people, and the emails it did all of that with.\n *\n * ## Why the emails are a section rather than the page\n *\n * A campaign reaches people by email, which is not the same as a campaign\n * BEING a list of emails. It runs over a window, against a set of audiences,\n * and the pages its links land on, the money credited to it and the\n * conversions credited to it are facts about the campaign that no single\n * message in it holds. So the body is sectioned — what it caused, what it\n * earned, where it sent people, then the mail's own figures, then its emails\n * — and each section names the population it describes.\n *\n * The ORDER is the part that says which of those the page is about. Delivery,\n * engagement and rates are mechanics of a message; a page that met the reader\n * with them and reached the outcomes only after scrolling was a mail report\n * whatever its headings claimed. `campaign-report-card.tsx` — the page a\n * campaign of one email gets — carries the same two outcome headings first,\n * for the same reason and in the same words.\n *\n * The three middle sections live in `campaign-reach-sections.tsx`, which is\n * also where the reason a campaign cannot simply DECLARE the screens and\n * forms it runs across is written down: no such edge is stored, attribution\n * is the mechanism, and all three sections join on this campaign's send ids.\n *\n * ## Why this resolves two kinds of id\n *\n * The id in `campaigns/{id}` may name a campaign CONTAINER or a single SEND,\n * and which it is is answered by reading: a container at `emailCampaigns/{id}`\n * renders this page, and anything else falls through to that send's own\n * report.\n *\n * Not every send belongs to a container. One written before campaigns grouped\n * their emails names none, its own id is the only id the URL can carry, and\n * that id is also inside the HMAC of every unsubscribe footer already\n * delivered — so a send id has to go on addressing a page rather than a 404.\n * The fall-through is what makes the container additive: no send document is\n * rewritten and no id is reassigned.\n *\n * The extra read is one document, and it buys the guarantee that a pasted\n * report link resolves whichever kind of id it carries.\n *\n * ## Under a site, and over the organization\n *\n * The container and its sends are the org's, so the same page renders on the\n * org hub with `hostId` null. What changes there is everything that is a SITE\n * fact: the emails listed are every site's rather than the ones sent as this\n * site, the forms and screens filed under the campaign are gathered from each\n * site it is placed on, the conversions are one chosen site's (they are\n * recorded per site), and writing an email first asks which site it is sent\n * as — its sender, its designs and its consent all belong to that site.\n */\nexport function CampaignDetailCard(props: CampaignDetailCardProps) {\n const { hostId, campaignId, basePath } = props\n const firestore = useFirestore()\n // The sibling hub, for the two records this page links to but does not own:\n // each message's report and the template it was built from.\n const emailsHub = useEmailsHubPath()\n const router = useRouter()\n const orgMount = useMarketingOrgMount()\n const { orgId } = useMarketingOrgId(hostId)\n const { confirm } = useConfirmationContext()\n const { enqueueSnackbar } = useSnackbar()\n const manageApi = useCampaignManageApi(hostId, orgId)\n /*\n * The site an email written here is sent AS. Under a site it is that site\n * and never asked; on the org hub it is chosen before the composer opens.\n * Empty means nobody has chosen yet.\n */\n const [composeHostId, setComposeHostId] = useState('')\n /** The site whose conversions the org hub is showing; empty until chosen. */\n const [conversionsHostId, setConversionsHostId] = useState('')\n const [composing, setComposing] = useState(false)\n const [editing, setEditing] = useState(false)\n const [saving, setSaving] = useState(false)\n const [saveError, setSaveError] = useState<string | null>(null)\n const [deleting, setDeleting] = useState(false)\n\n const { data: campaign, status } = useFirestoreDoc<EmailCampaign>(\n () => (orgId ? campaignContainerDoc(firestore, orgId, campaignId) : null),\n [firestore, orgId, campaignId],\n )\n\n /*\n * ONLY WHEN THE ID NAMES A CAMPAIGN.\n *\n * Both queries below are gated on the container having been found, because\n * the other half of this component is the single-send report — and that\n * screen's whole design is that it reads two documents whatever the size of\n * the audience. Listening unconditionally would add two collection reads to\n * every legacy report URL, which is the cost the report route was split out\n * to avoid. Not listening is what a null builder means.\n *\n * The campaign's emails come back by the field each send carries: equality\n * on one field ordered by DOCUMENT NAME. On the org hub that is all the\n * query says, and the automatic single-field index serves it. Under a site\n * the sends are also narrowed to the ones sent as that site — the scope\n * filter that makes the read provable for a site collaborator — and that\n * pair is the one composite index this page needs. Ordering on a date\n * would need that index AND would drop every send missing that particular\n * date — a sent send carries `sentAt`, a scheduled one `sendAtMs`, and\n * neither is on both. Sorted by date below, over a window this page holds\n * whole.\n *\n * One document past the ceiling, so \"this campaign has more emails than are\n * listed\" is a fact rather than a guess.\n */\n const { data: sendDocs } = useFirestoreCollection<any>(\n () => {\n const sends = campaign\n ? campaignSendsQuery(firestore, orgId, hostId)\n : null\n return sends\n ? query(\n sends,\n where('emailCampaignId', '==', campaignId),\n orderBy(documentId()),\n limit(CAMPAIGN_EMAIL_CEILING + 1),\n )\n : null\n },\n [firestore, orgId, hostId, campaignId, Boolean(campaign)],\n { idField: '$id' },\n )\n\n const { data: listDocs } = useFirestoreCollection<any>(\n () =>\n campaign && orgId\n ? query(collection(firestore, 'orgs', orgId, 'lists'), limit(50))\n : null,\n [firestore, orgId, Boolean(campaign)],\n { idField: '$id' },\n )\n\n /*\n * The org's topic catalog, read only while the EDIT DRAWER is open.\n *\n * It fills one picker in that drawer and is drawn nowhere else on this\n * page, so reading it on mount would charge every reader who came for the\n * campaign's numbers for a field they are not looking at. The composer\n * carries its own read of the same catalog, behind its own button, for the\n * same reason.\n */\n const { topics, source: topicSource } = useCampaignTopicOptions(\n topicCatalogHostId(hostId, orgMount),\n { enabled: editing },\n )\n const topicOptions = useMemo(\n () =>\n topics.map((topic) => ({\n value: topic.id,\n label: topic.name,\n })),\n [topics],\n )\n\n const { rows: readSends, truncated: sendsTruncated } = ceilingedWindow<any>(\n sendDocs,\n CAMPAIGN_EMAIL_CEILING,\n )\n /*\n * Newest first on the time each email SITS at — its send time where it has\n * one, its creation where it does not. A draft has neither `sentAt` nor\n * `sendAtMs`, so ordering on the send time alone gave every draft the key 0\n * and filed the email a merchant is in the middle of writing below mail\n * sent years ago.\n */\n const sends = useMemo(\n () =>\n [...(readSends as CampaignSend[])].sort(\n (a, b) => emailListTimeMs(b) - emailListTimeMs(a),\n ),\n [readSends],\n )\n const rollup = useMemo(() => campaignRollup(sends), [sends])\n // The ids the two sections beneath the figures join on. Derived from the\n // window this card already holds, so neither of them reads the send list\n // again to find out which emails the campaign has.\n const sendIds = useMemo(\n () => sends.map((send) => String(send.$id)).filter(Boolean),\n [sends],\n )\n\n /*==========================================\n * SAVING THE CONTAINER, WITH THE CLIENT SDK.\n *\n * The same door the create drawer already uses. `emailCampaigns` is\n * deliberately outside the security rules' server-only exclusion list — a\n * container holds no counter, no consent record and no entitlement input,\n * so an editor who writes one cannot buy themselves a send — and giving the\n * edit a route of its own would be a second writer to keep in step with the\n * create.\n *\n * `updateDoc` rather than a merge-set, because this is an edit of a\n * document that exists: a set would CREATE a campaign at this id for\n * somebody who deleted it in another tab, which is the shape the delivery\n * webhook is careful about on the send collection for the same reason.\n *\n * A cleared date is written as `null` — the absence the model already\n * spells — and a cleared topic REMOVES the field, because the model has no\n * null topic and `campaignTopicId` is handed to the composer as one.\n *\n * `visibleTo` is written only from the org hub, and only when its drawer\n * says the placement changed. A site hub never touches it: the rules let a\n * site collaborator edit a campaign only while its scope is unchanged, and\n * a site hub has no field for it anyway.\n *=========================================*/\n const handleSave = useCallback(\n async (values: CampaignEditValues) => {\n if (saving || !orgId) return\n setSaving(true)\n setSaveError(null)\n try {\n await updateDoc(campaignContainerDoc(firestore, orgId, campaignId), {\n name: values.name,\n startAtMs: values.startAtMs,\n endAtMs: values.endAtMs,\n listIds: values.listIds,\n topicId: values.topicId ? values.topicId : deleteField(),\n ...(!hostId && values.siteIds !== undefined\n ? { visibleTo: campaignVisibleTo(values.siteIds) }\n : {}),\n })\n setEditing(false)\n enqueueSnackbar('Campaign updated', {\n variant: 'success',\n persist: false,\n })\n } catch (error) {\n console.error(error)\n setSaveError('The campaign could not be saved')\n } finally {\n setSaving(false)\n }\n },\n [campaignId, enqueueSnackbar, firestore, hostId, orgId, saving],\n )\n\n /*==========================================\n * DELETING THE CONTAINER, WHICH IS NOT DELETING ITS MAIL.\n *\n * The route detaches every email first and then removes the campaign, so\n * what a merchant is agreeing to is the grouping going away — the emails\n * stay, keep their ids, keep their reports, and read afterwards as single\n * sends. Their unsubscribe links carry those ids inside an HMAC and go on\n * working, which is why the container can never take them with it.\n *\n * The confirmation says the count and says what a delete does NOT do. A\n * scheduled email inside the campaign still goes out at its time — stopping\n * somebody's mail is `cancel`, on that email's own page — and a merchant\n * who reads \"delete campaign\" as \"stop the campaign\" has to be told\n * otherwise before they press it, not after.\n *=========================================*/\n const handleDelete = useCallback(async () => {\n if (deleting) return\n /*\n * Emails with mail still to go: the ones waiting for their time AND the\n * ones part way through an audience larger than one batch. Both are what\n * a merchant needs warning about, and counting only `scheduled` would\n * miss the case that needs it most — a campaign that is delivering right\n * now is stored as `scheduled` too, but `rollup.scheduled` is the\n * narrower reading that excludes it.\n */\n const unfinished = rollup.scheduled + rollup.sending\n const agreed = await confirm({\n title: 'Delete this campaign?',\n description:\n (sends.length\n ? `The ${sends.length.toLocaleString()} ` +\n `${sends.length === 1 ? 'email' : 'emails'} in it are kept — ` +\n 'each keeps its own report and its unsubscribe links, and they ' +\n 'appear in the campaigns list as single sends. '\n : 'It holds no emails. ') +\n (unfinished\n ? `${unfinished.toLocaleString()} of them ${\n unfinished === 1 ? 'is' : 'are'\n } still going out or still due, and deleting the campaign does ` +\n 'not stop that — the sender picks each one up from its own ' +\n 'record. Stop a send from its own page. '\n : '') +\n 'Any screens, forms and contacts assigned to it come out of it and ' +\n 'are otherwise untouched. Only the campaign itself goes.',\n confirmationText: 'Delete campaign',\n })\n .then(() => true)\n .catch(() => false)\n if (!agreed) return\n setDeleting(true)\n try {\n const { response, payload } = await manageApi({\n action: 'deleteCampaign',\n campaignId,\n })\n if (!response.ok) {\n return void enqueueSnackbar(\n payload?.error ?? 'The campaign could not be deleted',\n { variant: 'warning', allowDuplicate: true },\n )\n }\n enqueueSnackbar('Campaign deleted', { variant: 'success', persist: false })\n router.push(`${basePath}/campaigns`)\n } catch (error) {\n console.error(error)\n enqueueSnackbar('The campaign could not be deleted', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setDeleting(false)\n }\n }, [\n basePath,\n campaignId,\n confirm,\n deleting,\n enqueueSnackbar,\n manageApi,\n rollup.scheduled,\n rollup.sending,\n router,\n sends.length,\n ])\n\n // Still settling. Falling through to the send report here would flash \"this\n // campaign could not be loaded\" on every open of a campaign that exists —\n // and under a site nothing is read at all until the site's org is known.\n if (!campaign && (status === 'loading' || !orgId)) return null\n\n /*\n * NOT A CONTAINER — so it is a send id, and the send's own report is what\n * this URL has always meant.\n */\n if (!campaign) {\n return (\n <CampaignReportCard\n hostId={hostId}\n campaignId={campaignId}\n basePath={basePath}\n />\n )\n }\n\n const listIds = campaign.listIds ?? []\n const names = new Map<string, string>(\n (listDocs ?? []).map((list: any) => [\n String(list.$id),\n String(list.name ?? list.$id),\n ]),\n )\n /*==========================================\n * ONE EMAIL'S PAGE — the same one every other route to that record opens.\n *\n * The Emails hub's `emails/{sendId}`, which is where the messages list and\n * a template's messages table also send a reader. One record has one page:\n * a link that resolved somewhere else depending on which list it was\n * clicked in would give the same message two pages, and the reader would\n * have no way to tell which one they were on.\n *\n * `campaigns/{sendId}` on THIS hub goes on resolving as well, through the\n * fall-through at the top of this file. That is not a convenience — every\n * unsubscribe footer already delivered carries `cid={sendId}`, those\n * messages sit in inboxes forever, and merchants paste report URLs into\n * their own mail. Which link the console GENERATES and which URLs ANSWER\n * are two separate questions, and only the first is decided here.\n *\n * Nor is a redirect the answer to the second. A redirect would be another\n * thing to be wrong about an id that is inside an HMAC; a page that keeps\n * working has nothing to get wrong, and it costs nothing to leave standing.\n *=========================================*/\n /*\n * On the org hub there is no Emails console to send a reader to, so a\n * message opens on the org hub's own Emails section — the same page, over\n * every site.\n */\n const messagesPath = orgMount\n ? `${orgMount.basePath}/emails`\n : emailsHub\n ? `${emailsHub}/messages`\n : null\n const sendHref = (send: CampaignSend) =>\n messagesPath ? `${messagesPath}/${send.$id}` : undefined\n /** A template is a site's design, so it opens on the site the send used. */\n const templatesPath = (send: CampaignSend): string | null => {\n const hub = orgMount ? orgSiteHubPath(orgMount, send.hostId, 'emails') : emailsHub\n return hub ? `${hub}/templates` : null\n }\n\n /**\n * What one of this campaign's emails can be opened into.\n *\n * The same two the Emails hub offers, less the campaign — this page IS the\n * campaign. Both destinations are records the Emails console owns, so they\n * are built from {@link useEmailsHubPath} rather than from this surface's\n * own `basePath`.\n *\n * A message composed inline was built from no template, so that entry is\n * shown DISABLED with the reason rather than hidden: an absent control and\n * an inapplicable one look identical, and only one is honest.\n */\n const sendActions = (send: CampaignSend): RowActionsMenuItem[] => {\n const templateScreenId = String((send as any).templateScreenId ?? '')\n const templates = templatesPath(send)\n return [\n {\n key: 'details',\n label: 'Open report',\n icon: <MdiIcon path={mdiEyeOutline.path} size={0.8} />,\n href: sendHref(send),\n disabled: !sendHref(send),\n disabledReason: 'This site’s console URL has not resolved yet',\n },\n {\n key: 'template',\n label: 'Open its template',\n icon: <MdiIcon path={mdiPaletteOutline.path} size={0.8} />,\n href:\n templateScreenId && templates\n ? `${templates}/${templateScreenId}`\n : undefined,\n disabled: !templateScreenId || !templates,\n disabledReason: templateScreenId\n ? 'This site’s console URL has not resolved yet'\n : 'This message was not built from a template',\n },\n ]\n }\n\n /*\n * The campaign's emails, on the surface's own row grammar: the row opens\n * the email's report, its subject is also a real link so it can be\n * middle-clicked and copied, and the trailing cluster holds the actions.\n * The window is one the card already holds, so the grid pages it.\n */\n const sendColumns: GridColDef<CampaignSend>[] = [\n {\n field: 'subject',\n headerName: 'Subject',\n flex: 1,\n minWidth: 220,\n valueGetter: (_value, send) => send.subject || send.$id,\n /*\n Plain text until the Emails hub resolves: a link with no destination\n is worse than none, and this cell is the message's subject either way.\n */\n renderCell: ({ row: send, value }) => {\n const href = sendHref(send)\n return href ? (\n <AppLink\n href={href}\n // The row's own handler would fire too and push the same route\n // twice — one history entry per back press.\n onClick={(event: { stopPropagation: () => void }) =>\n event.stopPropagation()\n }\n >\n {value}\n </AppLink>\n ) : (\n value\n )\n },\n },\n {\n /*\n WHAT THE EMAIL IS DOING, not the field it stores.\n\n This branched on the status, which reads \"Scheduled\" about an email\n that has delivered five hundred messages and is between batches — the\n stored state the processor claims to resume it. The derivation reads\n the counters beside the status; the due date is appended only where\n there is genuinely nothing delivered yet, which is the one case a time\n answers.\n */\n field: 'state',\n headerName: 'State',\n width: 220,\n valueGetter: (_value, send) => {\n const display = campaignSendDisplay(send)\n return display.state === 'pending' && send.sendAtMs\n ? `${display.label} · ${new Date(send.sendAtMs).toLocaleString()}`\n : display.label\n },\n renderCell: ({ row: send, value }) => {\n const state = campaignSendDisplay(send).state\n return (\n <Chip\n size=\"small\"\n color={\n state === 'sending' ? 'info' : state === 'stopped' ? 'warning' : undefined\n }\n label={value}\n />\n )\n },\n },\n {\n /*\n Sent over addressed, on one line: the two figures only mean anything\n beside each other, and the gap between them is the suppression list\n doing its work.\n */\n field: 'sent',\n headerName: 'Sent',\n type: 'number',\n align: 'right',\n headerAlign: 'right',\n width: 110,\n valueGetter: (_value, send) => Number(send.stats?.sent ?? 0),\n renderCell: ({ row: send }) =>\n `${send.stats?.sent ?? 0}/${send.stats?.recipients ?? 0}`,\n },\n {\n field: 'opens',\n headerName: 'Opens',\n type: 'number',\n align: 'right',\n headerAlign: 'right',\n width: 100,\n valueGetter: (_value, send) => Number(send.stats?.opens ?? 0),\n valueFormatter: (value: number) => value.toLocaleString(),\n },\n {\n field: 'clicks',\n headerName: 'Clicks',\n type: 'number',\n align: 'right',\n headerAlign: 'right',\n width: 100,\n valueGetter: (_value, send) => Number(send.stats?.clicks ?? 0),\n valueFormatter: (value: number) => value.toLocaleString(),\n },\n listActionsColumn(\n (send: CampaignSend) => (\n <ListRowActions\n label={String(send.subject || send.$id)}\n items={sendActions(send)}\n />\n ),\n { width: 72 },\n ),\n ]\n\n const windowState = campaignWindowState(campaign, Date.now())\n const start = campaign.startAtMs\n ? new Date(campaign.startAtMs).toLocaleDateString()\n : ''\n const end = campaign.endAtMs\n ? new Date(campaign.endAtMs).toLocaleDateString()\n : ''\n\n /*\n * THE SITES IT IS PLACED ON, which the org hub needs three times over: the\n * forms and screens filed under it are gathered from each of them, its\n * conversions are read for one of them, and an email written here is sent\n * as one of them. Every site when it is placed everywhere. Under a site the\n * answer is that site, and none of this is read.\n */\n const placedIds = campaignSiteIds(campaign)\n const placedSites = orgMount\n ? orgMount.hosts.filter(\n (site) => placedIds === null || placedIds.includes(site.id),\n )\n : []\n const conversionsHost =\n hostId ?? (conversionsHostId || placedSites[0]?.id || '')\n const conversionsBasePath = hostId\n ? basePath\n : orgSiteHubPath(orgMount, conversionsHost, 'marketing')\n /** Asked only when there is a choice to make; one site answers itself. */\n const writeAsHostId =\n hostId ??\n (placedSites.length === 1 ? placedSites[0].id : composeHostId)\n\n /* The card, named so the page chrome above it is a plain list of\n what this surface publishes upward. */\n const card = (\n <CardDisplay\n header={'Campaign'}\n subheader={\n start || end\n ? `${start || 'Open'} – ${end || 'open-ended'}`\n : 'No campaign dates'\n }\n help={detailDocsHelp}\n HeaderProps={{\n /*\n NAVIGATION, THEN THE OVERFLOW.\n\n Editing and deleting a campaign live here rather than on the\n campaigns table, because a record is edited on its own page in this\n console. `RowActionsMenu` is named for table rows and its rendering\n is not — a kebab and a menu whose items carry `destructive` and\n `disabled` — so reusing it is what keeps this overflow behaving like\n every other one on the surface, including the email page's.\n */\n action: (\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}/campaigns`}\n size=\"small\"\n color=\"primary\"\n >\n {'All campaigns'}\n </Button>\n <RowActionsMenu\n label={campaign.name || 'Campaign'}\n items={[\n {\n key: 'edit',\n label: 'Edit campaign',\n icon: <MdiIcon path={mdiPencilOutline.path} size={0.8} />,\n onClick: () => {\n setSaveError(null)\n setEditing(true)\n },\n },\n {\n key: 'delete',\n label: 'Delete campaign',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n disabled: deleting,\n disabledReason: 'This campaign is being deleted',\n onClick: () => void handleDelete(),\n },\n ]}\n />\n </Stack>\n ),\n }}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Chip size=\"small\" label={windowState} />\n {listIds.length ? (\n listIds.map((id) => (\n <Chip\n key={id}\n size=\"small\"\n variant=\"outlined\"\n label={names.get(id) ?? id}\n />\n ))\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'No lists assigned — its emails pick their own audience'}\n </Typography>\n )}\n </Stack>\n {orgMount ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {placedIds === null\n ? 'Offered on every site'\n : placedIds.length\n ? `Offered on ${placedIds\n .map((id) => orgSiteName(orgMount, id))\n .join(', ')}`\n : 'Not offered on any site yet — edit the campaign to place it'}\n </Typography>\n ) : null}\n\n <Divider />\n {/*\n A CAMPAIGN IS NOT ONLY ITS MAIL, and it leads with the part that\n is not.\n\n What the campaign CAUSED and where it SENT people are facts about the\n campaign; delivery, engagement and rates are mechanics of the\n messages it used, and the list of those messages is one section among\n several rather than the page. Ordering the mail first said the\n opposite in the only way a page can — by what a reader meets before\n they scroll — and a campaign of ONE email says it loudest, which is\n why `campaign-report-card.tsx` carries these two headings in this\n order too.\n\n All three sections join on the campaign's own send ids, which is the\n only handle the attribution, revenue and click-report collections\n offer — see `campaign-reach-sections.tsx` for why no other join\n exists, and why the web channel is named there rather than quietly\n omitted.\n */}\n {/*\n Conversions are recorded per SITE — they are that site's visitors —\n so the org hub reads them for one site at a time, defaulting to the\n first site the campaign is placed on.\n */}\n {orgMount && placedSites.length > 1 ? (\n <TextField\n select\n size=\"small\"\n label=\"Conversions on\"\n value={conversionsHost}\n onChange={(event) => setConversionsHostId(event.target.value)}\n sx={{ maxWidth: 320 }}\n >\n {placedSites.map((site) => (\n <MenuItem key={site.id} value={site.id}>\n {site.name || site.subdomain || site.id}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n {conversionsHost ? (\n <CampaignConversionsSection\n key={conversionsHost}\n hostId={conversionsHost}\n sendIds={sendIds}\n truncated={sendsTruncated}\n basePath={conversionsBasePath}\n />\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Conversions are counted per site, and this campaign is not ' +\n 'placed on one yet.'}\n </Typography>\n )}\n\n <Divider />\n {/*\n WHAT ITS SEQUENCES PRODUCED (AGL-3254), beside what its mail caused:\n a sequence in this campaign is a rep's one-to-one outreach, and its\n enrollments, sends, replies, meetings and conversions are the\n campaign's numbers too. Joined on the container's own id, since a\n sequence's emails are not the campaign's sends.\n */}\n <CampaignSequencesSection orgId={orgId} campaignId={campaignId} />\n\n <Divider />\n {/*\n WHAT IT EARNED, between what it caused and where it sent people —\n the order `campaign-report-card.tsx` puts these three in, so a\n campaign of one email and a campaign of ten answer the merchant's\n questions in the same sequence.\n\n How many messages a container holds is not a fact about whether its\n revenue is knowable, so the money question is answered here whatever\n the count. Reading it costs one record per email, so it asks first —\n see `campaign-reach-sections.tsx` for that bargain, and for why the\n currencies are reported apart and never totalled.\n */}\n <CampaignRevenueSection\n orgId={orgId}\n sendIds={sendIds}\n truncated={sendsTruncated}\n />\n\n <Divider />\n <CampaignDestinationsSection\n orgId={orgId}\n sendIds={sendIds}\n truncated={sendsTruncated}\n />\n\n <Divider />\n {/*\n WHAT THE MERCHANT SAID BELONGS HERE, under what the visitors did.\n\n Everything above this line is measured — the conversions, the money\n and the pages the mail's own links pointed at, all joined on this\n campaign's send ids. This section is the only one on the page that\n reports a DECLARATION: a screen or a form carrying this campaign's\n id in its own document because somebody put it there.\n\n It sits after the evidence deliberately. A reader who met the\n assigned list first would take it for the campaign's reach, and the\n two are not the same list — a landing page can be assigned to a\n campaign no email ever linked to, and a campaign's mail can drive\n traffic to a page assigned to nothing.\n\n The dates go with it. A form's counters are lifetime, so a campaign\n with a window has to be able to confine them to its own months; a\n campaign without one is told so, and says its figures are lifetime\n rather than implying they are its own.\n */}\n {hostId ? (\n <CampaignMembersSection\n hostId={hostId}\n campaignId={campaignId}\n startAtMs={campaign.startAtMs}\n endAtMs={campaign.endAtMs}\n />\n ) : (\n /*\n * Forms and screens are a site's records, so on the org hub each\n * site the campaign is placed on answers for its own. One section\n * per site keeps each one's figures and ceilings its own rather\n * than merging lists that were bounded separately.\n */\n placedSites.map((site) => (\n <CampaignMembersSection\n key={site.id}\n hostId={site.id}\n siteName={site.name || site.subdomain || site.id}\n campaignId={campaignId}\n startAtMs={campaign.startAtMs}\n endAtMs={campaign.endAtMs}\n />\n ))\n )}\n\n <Divider />\n {/*\n THE MAIL'S OWN FIGURES, under the outcomes rather than over them.\n\n The sum over the campaign's emails, not a second set of counters.\n Nothing is stored per campaign: a rollup document would have to be\n kept true against every delivery event of every email in it, and the\n numbers it duplicates are already on the sends this page reads.\n\n Drawn by the shared figures so a campaign's rate and a single\n message's read the same way — denominator named on the line, and an\n em dash rather than a zero where nothing was recorded.\n */}\n <Section title=\"The mail, across this campaign\">\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <Figure label=\"Addressed\" {...rolled(rollup.addressed)} />\n <Figure label=\"Sent\" {...rolled(rollup.sent)} />\n <Figure label=\"Delivered\" {...rolled(rollup.delivered)} />\n <Figure label=\"Opens\" {...rolled(rollup.opens)} />\n <Figure label=\"Clicks\" {...rolled(rollup.clicks)} />\n <Figure label=\"Unsubscribed\" {...rolled(rollup.unsubscribes)} />\n </Stack>\n </Section>\n <Stack spacing={0.5}>\n <RateRow label=\"Open rate\" rate={rollup.openRate} />\n <RateRow label=\"Click rate\" rate={rollup.clickRate} />\n <RateRow label=\"Unsubscribe rate\" rate={rollup.unsubscribeRate} />\n </Stack>\n\n <Divider />\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', justifyContent: 'space-between' }}\n >\n {/*\n The section rail's own heading style, so this reads as one section\n among the others rather than as the page's subject.\n */}\n <Typography variant=\"overline\" color=\"text.secondary\">\n {`Emails (${sends.length})`}\n </Typography>\n <Button\n size=\"small\"\n variant={composing ? 'text' : 'contained'}\n onClick={() => setComposing((open) => !open)}\n >\n {composing ? 'Close composer' : 'Write an email'}\n </Button>\n </Stack>\n {sends.length ? (\n <Stack spacing={0.5}>\n <ListTable\n aria-label=\"The campaign's emails\"\n rows={sends}\n columns={sendColumns}\n rowHeight={TABLE_ROW_HEIGHT}\n onOpen={(_id, send) => {\n const href = sendHref(send)\n if (href) router.push(href)\n }}\n />\n {sendsTruncated ? (\n <Alert severity=\"info\">\n {`Showing ${CAMPAIGN_EMAIL_CEILING} of this campaign's ` +\n 'emails. It has sent more — the ones listed are not ' +\n 'necessarily the most recent, because a send carries no ' +\n 'date field that every writer stamps, and the figures above ' +\n 'cover the emails listed.'}\n </Alert>\n ) : null}\n </Stack>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No emails in this campaign yet.'}\n </Typography>\n )}\n\n {/*\n THE COMPOSER, ON DEMAND.\n\n It opens listens of its own — the site's email designs, the org's\n lists and segments, the running experiments — and this page is also\n where somebody arrives to read numbers. Mounting it unasked would\n make every reader pay for a composer they did not open, which is the\n same cost the campaign report was split onto its own route to avoid.\n */}\n {composing ? (\n <Box>\n <Divider sx={{ mb: 2 }} />\n {/*\n WHICH SITE IT IS SENT AS, asked first on the org hub. The sender,\n the designs, the consent a recipient gave and the unsubscribe\n signature are all one site's, so the composer cannot open\n without one — and the choice is limited to the sites this\n campaign is placed on.\n */}\n {!hostId && placedSites.length > 1 ? (\n <TextField\n select\n size=\"small\"\n label=\"Send as\"\n value={composeHostId}\n onChange={(event) => setComposeHostId(event.target.value)}\n helperText=\"The site this email is sent from — its sender, designs and unsubscribe page\"\n sx={{ mb: 2, maxWidth: 420 }}\n fullWidth\n >\n {placedSites.map((site) => (\n <MenuItem key={site.id} value={site.id}>\n {site.name || site.subdomain || site.id}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n {writeAsHostId ? (\n <CampaignComposer\n key={writeAsHostId}\n hostId={writeAsHostId}\n emailCampaignId={campaignId}\n campaignListIds={listIds}\n campaignTopicId={campaign.topicId}\n onSent={() => setComposing(false)}\n />\n ) : !placedSites.length && !hostId ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Place this campaign on a site before writing an email in ' +\n 'it — every email is sent as one site.'}\n </Typography>\n ) : null}\n </Box>\n ) : null}\n </Stack>\n {/*\n THE EDIT DRAWER, on the campaign's own page.\n\n Its list picker reads the same `listDocs` the chips above already\n cost, so opening it adds one read — the topic catalog — and only\n while it is open.\n */}\n {/* Draws nothing: the zone the topic catalog's owner answers through. */}\n {topicSource}\n <CampaignEditDrawer\n open={editing}\n onClose={() => setEditing(false)}\n campaign={campaign}\n lists={(listDocs ?? []).map((list: any) => ({\n value: String(list.$id),\n label: String(list.name ?? list.$id),\n }))}\n topics={topicOptions}\n sites={orgMount ? orgSiteOptions(orgMount) : undefined}\n busy={saving}\n error={saveError}\n onSubmit={(values) => void handleSave(values)}\n />\n </CardDisplay>\n )\n\n return (\n <>\n {/* The page heading and the trail name the campaign; this card is\n then free to say what it holds rather than repeating the title. */}\n <PageHeaderRecord title={campaign.name || campaignId} />\n {card}\n </>\n )\n}\nCampaignDetailCard.displayName = 'CampaignDetailCard'\n\nexport default CampaignDetailCard\n"],"names":["mdiDeleteOutline","mdiEyeOutline","mdiPaletteOutline","mdiPencilOutline","AppLink","CardDisplay","MdiIcon","useConfirmationContext","Figure","RateRow","Section","ListRowActions","ListTable","listActionsColumn","RowActionsMenu","TABLE_ROW_HEIGHT","ceilingedWindow","PageHeaderRecord","pluginDocsHelp","Alert","Box","Button","Chip","Divider","MenuItem","Stack","TextField","Typography","collection","deleteField","documentId","limit","orderBy","query","updateDoc","where","useCallback","useMemo","useState","useRouter","useSnackbar","useFirestore","useFirestoreCollection","useFirestoreDoc","campaignRollup","campaignSendDisplay","campaignSiteIds","campaignVisibleTo","campaignWindowState","emailListTimeMs","CampaignComposer","CampaignEditDrawer","CampaignConversionsSection","CampaignDestinationsSection","CampaignRevenueSection","CampaignSequencesSection","CampaignMembersSection","CampaignReportCard","campaignContainerDoc","campaignSendsQuery","orgSiteHubPath","orgSiteName","orgSiteOptions","topicCatalogHostId","useMarketingOrgId","useMarketingOrgMount","useCampaignManageApi","useEmailsHubPath","useCampaignTopicOptions","CAMPAIGN_EMAIL_CEILING","detailDocsHelp","anchor","excerpt","rolled","value","note","recorded","sends","CampaignDetailCard","props","campaign","placedSites","hostId","campaignId","basePath","firestore","emailsHub","router","orgMount","orgId","confirm","enqueueSnackbar","manageApi","composeHostId","setComposeHostId","conversionsHostId","setConversionsHostId","composing","setComposing","editing","setEditing","saving","setSaving","saveError","setSaveError","deleting","setDeleting","data","status","sendDocs","Boolean","idField","listDocs","topics","source","topicSource","enabled","topicOptions","map","topic","id","label","name","rows","readSends","truncated","sendsTruncated","sort","a","b","rollup","sendIds","send","String","$id","filter","handleSave","values","startAtMs","endAtMs","listIds","topicId","siteIds","undefined","visibleTo","variant","persist","error","console","handleDelete","unfinished","scheduled","sending","agreed","title","description","length","toLocaleString","confirmationText","then","catch","response","payload","action","ok","allowDuplicate","push","names","Map","list","messagesPath","sendHref","templatesPath","hub","sendActions","templateScreenId","templates","key","icon","path","size","href","disabled","disabledReason","sendColumns","field","headerName","flex","minWidth","valueGetter","_value","subject","renderCell","row","onClick","event","stopPropagation","width","display","state","sendAtMs","Date","color","type","align","headerAlign","Number","stats","sent","recipients","opens","valueFormatter","clicks","items","windowState","now","start","toLocaleDateString","end","placedIds","hosts","site","includes","conversionsHost","conversionsBasePath","writeAsHostId","card","header","subheader","help","HeaderProps","direction","spacing","sx","alignItems","component","componentVariant","nativeButton","destructive","contentGutterX","contentGutterY","get","join","select","onChange","target","maxWidth","subdomain","siteName","flexWrap","addressed","delivered","unsubscribes","rate","openRate","clickRate","unsubscribeRate","justifyContent","open","aria-label","columns","rowHeight","onOpen","_id","severity","mb","helperText","fullWidth","emailCampaignId","campaignListIds","campaignTopicId","onSent","onClose","lists","sites","busy","onSubmit","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/components/campaign-detail-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n mdiDeleteOutline,\n mdiEyeOutline,\n mdiPaletteOutline,\n mdiPencilOutline,\n} from '@aglyn/shared-data-mdi'\nimport {\n AppLink,\n CardDisplay,\n MdiIcon,\n useConfirmationContext,\n} from '@aglyn/shared-ui-jsx'\nimport {\n Figure,\n RateRow,\n Section,\n} from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport {\n ListRowActions,\n ListTable,\n listActionsColumn,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport RowActionsMenu, {\n type RowActionsMenuItem,\n} from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { TABLE_ROW_HEIGHT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { ceilingedWindow } from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport { PageHeaderRecord, pluginDocsHelp } from '@aglyn/aglyn'\nimport {\n Alert,\n Box,\n Button,\n Chip,\n Divider,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport {\n collection,\n deleteField,\n documentId,\n limit,\n orderBy,\n query,\n updateDoc,\n where,\n} from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport { useRouter } from 'next/navigation'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useFirestoreCollection,\n useFirestoreDoc,\n} from '@aglyn/tenant-feature-instance'\nimport {\n campaignRollup,\n campaignSendDisplay,\n campaignSiteIds,\n campaignVisibleTo,\n campaignWindowState,\n emailListTimeMs,\n type CampaignAggregate,\n type CampaignSend,\n type EmailCampaign,\n} from '@aglyn/shared-ui-email-campaigns/model'\nimport CampaignComposer from './campaign-composer'\nimport CampaignEditDrawer, {\n type CampaignEditValues,\n} from './campaign-edit-drawer'\nimport {\n CampaignConversionsSection,\n CampaignDestinationsSection,\n CampaignRevenueSection,\n CampaignSequencesSection,\n} from './campaign-reach-sections'\nimport CampaignMembersSection from './campaign-members-section'\nimport CampaignReportCard from './campaign-report-card'\nimport { campaignContainerDoc, campaignSendsQuery } from './campaign-queries'\nimport {\n orgSiteHubPath,\n orgSiteName,\n orgSiteOptions,\n topicCatalogHostId,\n useMarketingOrgId,\n useMarketingOrgMount,\n} from './marketing-org-mount'\nimport { useCampaignManageApi } from './use-campaign-send-api'\nimport { useEmailsHubPath } from './use-emails-hub-path'\nimport { useCampaignTopicOptions } from './use-campaign-topic-options'\n\n/** How many of a campaign's emails the detail page enumerates. */\nconst CAMPAIGN_EMAIL_CEILING = 50\n\nconst detailDocsHelp = pluginDocsHelp('emailCampaigns', {\n anchor: '#the-campaign-report',\n excerpt:\n 'A campaign groups the emails sent to its lists. The figures here are ' +\n 'the sum across those emails, and each one keeps its own report.',\n})\n\n/**\n * A rolled-up figure, in the shared `Figure`'s shape.\n *\n * The note is where an aggregate differs from a single send's count: a total\n * measured over fewer emails than the campaign holds says which part of the\n * campaign it describes, rather than presenting a partial sum as a complete\n * one. `null` still renders as an em dash and says \"not recorded\", which is\n * the property the shared component already owns.\n */\nconst rolled = (\n value: CampaignAggregate,\n): { value: number | null; note: string } => ({\n value: value.value,\n note:\n value.value !== null && value.recorded < value.sends\n ? `across ${value.recorded} of ${value.sends} emails`\n : `across ${value.sends} email${value.sends === 1 ? '' : 's'}`,\n})\n\nexport interface CampaignDetailCardProps {\n /** The site, or `null` on the org Marketing hub. */\n hostId: string | null\n /** A campaign container id, or a send id from before containers existed. */\n campaignId: string\n /** The marketing hub URL, for the way back to the campaigns list. */\n basePath: string\n}\n\n/**\n * ONE CAMPAIGN: its lists, what it caused, what it earned, where it sent\n * people, and the emails it did all of that with.\n *\n * ## Why the emails are a section rather than the page\n *\n * A campaign reaches people by email, which is not the same as a campaign\n * BEING a list of emails. It runs over a window, against a set of audiences,\n * and the pages its links land on, the money credited to it and the\n * conversions credited to it are facts about the campaign that no single\n * message in it holds. So the body is sectioned — what it caused, what it\n * earned, where it sent people, then the mail's own figures, then its emails\n * — and each section names the population it describes.\n *\n * The ORDER is the part that says which of those the page is about. Delivery,\n * engagement and rates are mechanics of a message; a page that met the reader\n * with them and reached the outcomes only after scrolling was a mail report\n * whatever its headings claimed. `campaign-report-card.tsx` — the page a\n * campaign of one email gets — carries the same two outcome headings first,\n * for the same reason and in the same words.\n *\n * The three middle sections live in `campaign-reach-sections.tsx`, which is\n * also where the reason a campaign cannot simply DECLARE the screens and\n * forms it runs across is written down: no such edge is stored, attribution\n * is the mechanism, and all three sections join on this campaign's send ids.\n *\n * ## Why this resolves two kinds of id\n *\n * The id in `campaigns/{id}` may name a campaign CONTAINER or a single SEND,\n * and which it is is answered by reading: a container at `emailCampaigns/{id}`\n * renders this page, and anything else falls through to that send's own\n * report.\n *\n * Not every send belongs to a container. One written before campaigns grouped\n * their emails names none, its own id is the only id the URL can carry, and\n * that id is also inside the HMAC of every unsubscribe footer already\n * delivered — so a send id has to go on addressing a page rather than a 404.\n * The fall-through is what makes the container additive: no send document is\n * rewritten and no id is reassigned.\n *\n * The extra read is one document, and it buys the guarantee that a pasted\n * report link resolves whichever kind of id it carries.\n *\n * ## Under a site, and over the organization\n *\n * The container and its sends are the org's, so the same page renders on the\n * org hub with `hostId` null. What changes there is everything that is a SITE\n * fact: the emails listed are every site's rather than the ones sent as this\n * site, the forms and screens filed under the campaign are gathered from each\n * site it is placed on, the conversions are one chosen site's (they are\n * recorded per site), and writing an email first asks which site it is sent\n * as — its sender, its designs and its consent all belong to that site.\n */\nexport function CampaignDetailCard(props: CampaignDetailCardProps) {\n const { hostId, campaignId, basePath } = props\n const firestore = useFirestore()\n // The sibling hub, for the two records this page links to but does not own:\n // each message's report and the template it was built from.\n const emailsHub = useEmailsHubPath()\n const router = useRouter()\n const orgMount = useMarketingOrgMount()\n const { orgId } = useMarketingOrgId(hostId)\n const { confirm } = useConfirmationContext()\n const { enqueueSnackbar } = useSnackbar()\n const manageApi = useCampaignManageApi(hostId, orgId)\n /*\n * The site an email written here is sent AS. Under a site it is that site\n * and never asked; on the org hub it is chosen before the composer opens.\n * Empty means nobody has chosen yet.\n */\n const [composeHostId, setComposeHostId] = useState('')\n /** The site whose conversions the org hub is showing; empty until chosen. */\n const [conversionsHostId, setConversionsHostId] = useState('')\n const [composing, setComposing] = useState(false)\n const [editing, setEditing] = useState(false)\n const [saving, setSaving] = useState(false)\n const [saveError, setSaveError] = useState<string | null>(null)\n const [deleting, setDeleting] = useState(false)\n\n const { data: campaign, status } = useFirestoreDoc<EmailCampaign>(\n () => (orgId ? campaignContainerDoc(firestore, orgId, campaignId) : null),\n [firestore, orgId, campaignId],\n )\n\n /*\n * ONLY WHEN THE ID NAMES A CAMPAIGN.\n *\n * Both queries below are gated on the container having been found, because\n * the other half of this component is the single-send report — and that\n * screen's whole design is that it reads two documents whatever the size of\n * the audience. Listening unconditionally would add two collection reads to\n * every legacy report URL, which is the cost the report route was split out\n * to avoid. Not listening is what a null builder means.\n *\n * The campaign's emails come back by the field each send carries: equality\n * on one field ordered by DOCUMENT NAME. On the org hub that is all the\n * query says, and the automatic single-field index serves it. Under a site\n * the sends are also narrowed to the ones sent as that site — the scope\n * filter that makes the read provable for a site collaborator — and that\n * pair is the one composite index this page needs. Ordering on a date\n * would need that index AND would drop every send missing that particular\n * date — a sent send carries `sentAt`, a scheduled one `sendAtMs`, and\n * neither is on both. Sorted by date below, over a window this page holds\n * whole.\n *\n * One document past the ceiling, so \"this campaign has more emails than are\n * listed\" is a fact rather than a guess.\n */\n const { data: sendDocs } = useFirestoreCollection<any>(\n () => {\n const sends = campaign\n ? campaignSendsQuery(firestore, orgId, hostId)\n : null\n return sends\n ? query(\n sends,\n where('emailCampaignId', '==', campaignId),\n orderBy(documentId()),\n limit(CAMPAIGN_EMAIL_CEILING + 1),\n )\n : null\n },\n [firestore, orgId, hostId, campaignId, Boolean(campaign)],\n { idField: '$id' },\n )\n\n const { data: listDocs } = useFirestoreCollection<any>(\n () =>\n campaign && orgId\n ? query(collection(firestore, 'orgs', orgId, 'lists'), limit(50))\n : null,\n [firestore, orgId, Boolean(campaign)],\n { idField: '$id' },\n )\n\n /*\n * The org's topic catalog, read only while the EDIT DRAWER is open.\n *\n * It fills one picker in that drawer and is drawn nowhere else on this\n * page, so reading it on mount would charge every reader who came for the\n * campaign's numbers for a field they are not looking at. The composer\n * carries its own read of the same catalog, behind its own button, for the\n * same reason.\n */\n const { topics, source: topicSource } = useCampaignTopicOptions(\n topicCatalogHostId(hostId, orgMount),\n { enabled: editing },\n )\n const topicOptions = useMemo(\n () =>\n topics.map((topic) => ({\n value: topic.id,\n label: topic.name,\n })),\n [topics],\n )\n\n const { rows: readSends, truncated: sendsTruncated } = ceilingedWindow<any>(\n sendDocs,\n CAMPAIGN_EMAIL_CEILING,\n )\n /*\n * Newest first on the time each email SITS at — its send time where it has\n * one, its creation where it does not. A draft has neither `sentAt` nor\n * `sendAtMs`, so ordering on the send time alone gave every draft the key 0\n * and filed the email a merchant is in the middle of writing below mail\n * sent years ago.\n */\n const sends = useMemo(\n () =>\n [...(readSends as CampaignSend[])].sort(\n (a, b) => emailListTimeMs(b) - emailListTimeMs(a),\n ),\n [readSends],\n )\n const rollup = useMemo(() => campaignRollup(sends), [sends])\n // The ids the two sections beneath the figures join on. Derived from the\n // window this card already holds, so neither of them reads the send list\n // again to find out which emails the campaign has.\n const sendIds = useMemo(\n () => sends.map((send) => String(send.$id)).filter(Boolean),\n [sends],\n )\n\n /*==========================================\n * SAVING THE CONTAINER, WITH THE CLIENT SDK.\n *\n * The same door the create drawer already uses. `emailCampaigns` is\n * deliberately outside the security rules' server-only exclusion list — a\n * container holds no counter, no consent record and no entitlement input,\n * so an editor who writes one cannot buy themselves a send — and giving the\n * edit a route of its own would be a second writer to keep in step with the\n * create.\n *\n * `updateDoc` rather than a merge-set, because this is an edit of a\n * document that exists: a set would CREATE a campaign at this id for\n * somebody who deleted it in another tab, which is the shape the delivery\n * webhook is careful about on the send collection for the same reason.\n *\n * A cleared date is written as `null` — the absence the model already\n * spells — and a cleared topic REMOVES the field, because the model has no\n * null topic and `campaignTopicId` is handed to the composer as one.\n *\n * `visibleTo` is written only from the org hub, and only when its drawer\n * says the placement changed. A site hub never touches it: the rules let a\n * site collaborator edit a campaign only while its scope is unchanged, and\n * a site hub has no field for it anyway.\n *=========================================*/\n const handleSave = useCallback(\n async (values: CampaignEditValues) => {\n if (saving || !orgId) return\n setSaving(true)\n setSaveError(null)\n try {\n await updateDoc(campaignContainerDoc(firestore, orgId, campaignId), {\n name: values.name,\n startAtMs: values.startAtMs,\n endAtMs: values.endAtMs,\n listIds: values.listIds,\n topicId: values.topicId ? values.topicId : deleteField(),\n ...(!hostId && values.siteIds !== undefined\n ? { visibleTo: campaignVisibleTo(values.siteIds) }\n : {}),\n })\n setEditing(false)\n enqueueSnackbar('Campaign updated', {\n variant: 'success',\n persist: false,\n })\n } catch (error) {\n console.error(error)\n setSaveError('The campaign could not be saved')\n } finally {\n setSaving(false)\n }\n },\n [campaignId, enqueueSnackbar, firestore, hostId, orgId, saving],\n )\n\n /*==========================================\n * DELETING THE CONTAINER, WHICH IS NOT DELETING ITS MAIL.\n *\n * The route detaches every email first and then removes the campaign, so\n * what a merchant is agreeing to is the grouping going away — the emails\n * stay, keep their ids, keep their reports, and read afterwards as single\n * sends. Their unsubscribe links carry those ids inside an HMAC and go on\n * working, which is why the container can never take them with it.\n *\n * The confirmation says the count and says what a delete does NOT do. A\n * scheduled email inside the campaign still goes out at its time — stopping\n * somebody's mail is `cancel`, on that email's own page — and a merchant\n * who reads \"delete campaign\" as \"stop the campaign\" has to be told\n * otherwise before they press it, not after.\n *=========================================*/\n const handleDelete = useCallback(async () => {\n if (deleting) return\n /*\n * Emails with mail still to go: the ones waiting for their time AND the\n * ones part way through an audience larger than one batch. Both are what\n * a merchant needs warning about, and counting only `scheduled` would\n * miss the case that needs it most — a campaign that is delivering right\n * now is stored as `scheduled` too, but `rollup.scheduled` is the\n * narrower reading that excludes it.\n */\n const unfinished = rollup.scheduled + rollup.sending\n const agreed = await confirm({\n title: 'Delete this campaign?',\n description:\n (sends.length\n ? `The ${sends.length.toLocaleString()} ` +\n `${sends.length === 1 ? 'email' : 'emails'} in it are kept — ` +\n 'each keeps its own report and its unsubscribe links, and they ' +\n 'appear in the campaigns list as single sends. '\n : 'It holds no emails. ') +\n (unfinished\n ? `${unfinished.toLocaleString()} of them ${\n unfinished === 1 ? 'is' : 'are'\n } still going out or still due, and deleting the campaign does ` +\n 'not stop that — the sender picks each one up from its own ' +\n 'record. Stop a send from its own page. '\n : '') +\n 'Any screens, forms and contacts assigned to it come out of it and ' +\n 'are otherwise untouched. Only the campaign itself goes.',\n confirmationText: 'Delete campaign',\n })\n .then(() => true)\n .catch(() => false)\n if (!agreed) return\n setDeleting(true)\n try {\n const { response, payload } = await manageApi({\n action: 'deleteCampaign',\n campaignId,\n })\n if (!response.ok) {\n return void enqueueSnackbar(\n payload?.error ?? 'The campaign could not be deleted',\n { variant: 'warning', allowDuplicate: true },\n )\n }\n enqueueSnackbar('Campaign deleted', { variant: 'success', persist: false })\n router.push(`${basePath}/campaigns`)\n } catch (error) {\n console.error(error)\n enqueueSnackbar('The campaign could not be deleted', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setDeleting(false)\n }\n }, [\n basePath,\n campaignId,\n confirm,\n deleting,\n enqueueSnackbar,\n manageApi,\n rollup.scheduled,\n rollup.sending,\n router,\n sends.length,\n ])\n\n // Still settling. Falling through to the send report here would flash \"this\n // campaign could not be loaded\" on every open of a campaign that exists —\n // and under a site nothing is read at all until the site's org is known.\n if (!campaign && (status === 'loading' || !orgId)) return null\n\n /*\n * NOT A CONTAINER — so it is a send id, and the send's own report is what\n * this URL has always meant.\n */\n if (!campaign) {\n return (\n <CampaignReportCard\n hostId={hostId}\n campaignId={campaignId}\n basePath={basePath}\n />\n )\n }\n\n const listIds = campaign.listIds ?? []\n const names = new Map<string, string>(\n (listDocs ?? []).map((list: any) => [\n String(list.$id),\n String(list.name ?? list.$id),\n ]),\n )\n /*==========================================\n * ONE EMAIL'S PAGE — the same one every other route to that record opens.\n *\n * The Emails hub's `emails/{sendId}`, which is where the messages list and\n * a template's messages table also send a reader. One record has one page:\n * a link that resolved somewhere else depending on which list it was\n * clicked in would give the same message two pages, and the reader would\n * have no way to tell which one they were on.\n *\n * `campaigns/{sendId}` on THIS hub goes on resolving as well, through the\n * fall-through at the top of this file. That is not a convenience — every\n * unsubscribe footer already delivered carries `cid={sendId}`, those\n * messages sit in inboxes forever, and merchants paste report URLs into\n * their own mail. Which link the console GENERATES and which URLs ANSWER\n * are two separate questions, and only the first is decided here.\n *\n * Nor is a redirect the answer to the second. A redirect would be another\n * thing to be wrong about an id that is inside an HMAC; a page that keeps\n * working has nothing to get wrong, and it costs nothing to leave standing.\n *=========================================*/\n /*\n * A message opens on the Emails page: the site's under a site, and over the\n * org the organization's own, which lists every site's messages.\n */\n const messagesPath = emailsHub ? `${emailsHub}/messages` : null\n const sendHref = (send: CampaignSend) =>\n messagesPath ? `${messagesPath}/${send.$id}` : undefined\n /** A template is a site's design, so it opens on the site the send used. */\n const templatesPath = (send: CampaignSend): string | null => {\n const hub = orgMount ? orgSiteHubPath(orgMount, send.hostId, 'emails') : emailsHub\n return hub ? `${hub}/templates` : null\n }\n\n /**\n * What one of this campaign's emails can be opened into.\n *\n * The same two the Emails hub offers, less the campaign — this page IS the\n * campaign. Both destinations are records the Emails console owns, so they\n * are built from {@link useEmailsHubPath} rather than from this surface's\n * own `basePath`.\n *\n * A message composed inline was built from no template, so that entry is\n * shown DISABLED with the reason rather than hidden: an absent control and\n * an inapplicable one look identical, and only one is honest.\n */\n const sendActions = (send: CampaignSend): RowActionsMenuItem[] => {\n const templateScreenId = String((send as any).templateScreenId ?? '')\n const templates = templatesPath(send)\n return [\n {\n key: 'details',\n label: 'Open report',\n icon: <MdiIcon path={mdiEyeOutline.path} size={0.8} />,\n href: sendHref(send),\n disabled: !sendHref(send),\n disabledReason: 'This site’s console URL has not resolved yet',\n },\n {\n key: 'template',\n label: 'Open its template',\n icon: <MdiIcon path={mdiPaletteOutline.path} size={0.8} />,\n href:\n templateScreenId && templates\n ? `${templates}/${templateScreenId}`\n : undefined,\n disabled: !templateScreenId || !templates,\n disabledReason: templateScreenId\n ? 'This site’s console URL has not resolved yet'\n : 'This message was not built from a template',\n },\n ]\n }\n\n /*\n * The campaign's emails, on the surface's own row grammar: the row opens\n * the email's report, its subject is also a real link so it can be\n * middle-clicked and copied, and the trailing cluster holds the actions.\n * The window is one the card already holds, so the grid pages it.\n */\n const sendColumns: GridColDef<CampaignSend>[] = [\n {\n field: 'subject',\n headerName: 'Subject',\n flex: 1,\n minWidth: 220,\n valueGetter: (_value, send) => send.subject || send.$id,\n /*\n Plain text until the Emails hub resolves: a link with no destination\n is worse than none, and this cell is the message's subject either way.\n */\n renderCell: ({ row: send, value }) => {\n const href = sendHref(send)\n return href ? (\n <AppLink\n href={href}\n // The row's own handler would fire too and push the same route\n // twice — one history entry per back press.\n onClick={(event: { stopPropagation: () => void }) =>\n event.stopPropagation()\n }\n >\n {value}\n </AppLink>\n ) : (\n value\n )\n },\n },\n {\n /*\n WHAT THE EMAIL IS DOING, not the field it stores.\n\n This branched on the status, which reads \"Scheduled\" about an email\n that has delivered five hundred messages and is between batches — the\n stored state the processor claims to resume it. The derivation reads\n the counters beside the status; the due date is appended only where\n there is genuinely nothing delivered yet, which is the one case a time\n answers.\n */\n field: 'state',\n headerName: 'State',\n width: 220,\n valueGetter: (_value, send) => {\n const display = campaignSendDisplay(send)\n return display.state === 'pending' && send.sendAtMs\n ? `${display.label} · ${new Date(send.sendAtMs).toLocaleString()}`\n : display.label\n },\n renderCell: ({ row: send, value }) => {\n const state = campaignSendDisplay(send).state\n return (\n <Chip\n size=\"small\"\n color={\n state === 'sending' ? 'info' : state === 'stopped' ? 'warning' : undefined\n }\n label={value}\n />\n )\n },\n },\n {\n /*\n Sent over addressed, on one line: the two figures only mean anything\n beside each other, and the gap between them is the suppression list\n doing its work.\n */\n field: 'sent',\n headerName: 'Sent',\n type: 'number',\n align: 'right',\n headerAlign: 'right',\n width: 110,\n valueGetter: (_value, send) => Number(send.stats?.sent ?? 0),\n renderCell: ({ row: send }) =>\n `${send.stats?.sent ?? 0}/${send.stats?.recipients ?? 0}`,\n },\n {\n field: 'opens',\n headerName: 'Opens',\n type: 'number',\n align: 'right',\n headerAlign: 'right',\n width: 100,\n valueGetter: (_value, send) => Number(send.stats?.opens ?? 0),\n valueFormatter: (value: number) => value.toLocaleString(),\n },\n {\n field: 'clicks',\n headerName: 'Clicks',\n type: 'number',\n align: 'right',\n headerAlign: 'right',\n width: 100,\n valueGetter: (_value, send) => Number(send.stats?.clicks ?? 0),\n valueFormatter: (value: number) => value.toLocaleString(),\n },\n listActionsColumn(\n (send: CampaignSend) => (\n <ListRowActions\n label={String(send.subject || send.$id)}\n items={sendActions(send)}\n />\n ),\n { width: 72 },\n ),\n ]\n\n const windowState = campaignWindowState(campaign, Date.now())\n const start = campaign.startAtMs\n ? new Date(campaign.startAtMs).toLocaleDateString()\n : ''\n const end = campaign.endAtMs\n ? new Date(campaign.endAtMs).toLocaleDateString()\n : ''\n\n /*\n * THE SITES IT IS PLACED ON, which the org hub needs three times over: the\n * forms and screens filed under it are gathered from each of them, its\n * conversions are read for one of them, and an email written here is sent\n * as one of them. Every site when it is placed everywhere. Under a site the\n * answer is that site, and none of this is read.\n */\n const placedIds = campaignSiteIds(campaign)\n const placedSites = orgMount\n ? orgMount.hosts.filter(\n (site) => placedIds === null || placedIds.includes(site.id),\n )\n : []\n const conversionsHost =\n hostId ?? (conversionsHostId || placedSites[0]?.id || '')\n const conversionsBasePath = hostId\n ? basePath\n : orgSiteHubPath(orgMount, conversionsHost, 'marketing')\n /** Asked only when there is a choice to make; one site answers itself. */\n const writeAsHostId =\n hostId ??\n (placedSites.length === 1 ? placedSites[0].id : composeHostId)\n\n /* The card, named so the page chrome above it is a plain list of\n what this surface publishes upward. */\n const card = (\n <CardDisplay\n header={'Campaign'}\n subheader={\n start || end\n ? `${start || 'Open'} – ${end || 'open-ended'}`\n : 'No campaign dates'\n }\n help={detailDocsHelp}\n HeaderProps={{\n /*\n NAVIGATION, THEN THE OVERFLOW.\n\n Editing and deleting a campaign live here rather than on the\n campaigns table, because a record is edited on its own page in this\n console. `RowActionsMenu` is named for table rows and its rendering\n is not — a kebab and a menu whose items carry `destructive` and\n `disabled` — so reusing it is what keeps this overflow behaving like\n every other one on the surface, including the email page's.\n */\n action: (\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}/campaigns`}\n size=\"small\"\n color=\"primary\"\n >\n {'All campaigns'}\n </Button>\n <RowActionsMenu\n label={campaign.name || 'Campaign'}\n items={[\n {\n key: 'edit',\n label: 'Edit campaign',\n icon: <MdiIcon path={mdiPencilOutline.path} size={0.8} />,\n onClick: () => {\n setSaveError(null)\n setEditing(true)\n },\n },\n {\n key: 'delete',\n label: 'Delete campaign',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n disabled: deleting,\n disabledReason: 'This campaign is being deleted',\n onClick: () => void handleDelete(),\n },\n ]}\n />\n </Stack>\n ),\n }}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Chip size=\"small\" label={windowState} />\n {listIds.length ? (\n listIds.map((id) => (\n <Chip\n key={id}\n size=\"small\"\n variant=\"outlined\"\n label={names.get(id) ?? id}\n />\n ))\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'No lists assigned — its emails pick their own audience'}\n </Typography>\n )}\n </Stack>\n {orgMount ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {placedIds === null\n ? 'Offered on every site'\n : placedIds.length\n ? `Offered on ${placedIds\n .map((id) => orgSiteName(orgMount, id))\n .join(', ')}`\n : 'Not offered on any site yet — edit the campaign to place it'}\n </Typography>\n ) : null}\n\n <Divider />\n {/*\n A CAMPAIGN IS NOT ONLY ITS MAIL, and it leads with the part that\n is not.\n\n What the campaign CAUSED and where it SENT people are facts about the\n campaign; delivery, engagement and rates are mechanics of the\n messages it used, and the list of those messages is one section among\n several rather than the page. Ordering the mail first said the\n opposite in the only way a page can — by what a reader meets before\n they scroll — and a campaign of ONE email says it loudest, which is\n why `campaign-report-card.tsx` carries these two headings in this\n order too.\n\n All three sections join on the campaign's own send ids, which is the\n only handle the attribution, revenue and click-report collections\n offer — see `campaign-reach-sections.tsx` for why no other join\n exists, and why the web channel is named there rather than quietly\n omitted.\n */}\n {/*\n Conversions are recorded per SITE — they are that site's visitors —\n so the org hub reads them for one site at a time, defaulting to the\n first site the campaign is placed on.\n */}\n {orgMount && placedSites.length > 1 ? (\n <TextField\n select\n size=\"small\"\n label=\"Conversions on\"\n value={conversionsHost}\n onChange={(event) => setConversionsHostId(event.target.value)}\n sx={{ maxWidth: 320 }}\n >\n {placedSites.map((site) => (\n <MenuItem key={site.id} value={site.id}>\n {site.name || site.subdomain || site.id}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n {conversionsHost ? (\n <CampaignConversionsSection\n key={conversionsHost}\n hostId={conversionsHost}\n sendIds={sendIds}\n truncated={sendsTruncated}\n basePath={conversionsBasePath}\n />\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Conversions are counted per site, and this campaign is not ' +\n 'placed on one yet.'}\n </Typography>\n )}\n\n <Divider />\n {/*\n WHAT ITS SEQUENCES PRODUCED (AGL-3254), beside what its mail caused:\n a sequence in this campaign is a rep's one-to-one outreach, and its\n enrollments, sends, replies, meetings and conversions are the\n campaign's numbers too. Joined on the container's own id, since a\n sequence's emails are not the campaign's sends.\n */}\n <CampaignSequencesSection orgId={orgId} campaignId={campaignId} />\n\n <Divider />\n {/*\n WHAT IT EARNED, between what it caused and where it sent people —\n the order `campaign-report-card.tsx` puts these three in, so a\n campaign of one email and a campaign of ten answer the merchant's\n questions in the same sequence.\n\n How many messages a container holds is not a fact about whether its\n revenue is knowable, so the money question is answered here whatever\n the count. Reading it costs one record per email, so it asks first —\n see `campaign-reach-sections.tsx` for that bargain, and for why the\n currencies are reported apart and never totalled.\n */}\n <CampaignRevenueSection\n orgId={orgId}\n sendIds={sendIds}\n truncated={sendsTruncated}\n />\n\n <Divider />\n <CampaignDestinationsSection\n orgId={orgId}\n sendIds={sendIds}\n truncated={sendsTruncated}\n />\n\n <Divider />\n {/*\n WHAT THE MERCHANT SAID BELONGS HERE, under what the visitors did.\n\n Everything above this line is measured — the conversions, the money\n and the pages the mail's own links pointed at, all joined on this\n campaign's send ids. This section is the only one on the page that\n reports a DECLARATION: a screen or a form carrying this campaign's\n id in its own document because somebody put it there.\n\n It sits after the evidence deliberately. A reader who met the\n assigned list first would take it for the campaign's reach, and the\n two are not the same list — a landing page can be assigned to a\n campaign no email ever linked to, and a campaign's mail can drive\n traffic to a page assigned to nothing.\n\n The dates go with it. A form's counters are lifetime, so a campaign\n with a window has to be able to confine them to its own months; a\n campaign without one is told so, and says its figures are lifetime\n rather than implying they are its own.\n */}\n {hostId ? (\n <CampaignMembersSection\n hostId={hostId}\n campaignId={campaignId}\n startAtMs={campaign.startAtMs}\n endAtMs={campaign.endAtMs}\n />\n ) : (\n /*\n * Forms and screens are a site's records, so on the org hub each\n * site the campaign is placed on answers for its own. One section\n * per site keeps each one's figures and ceilings its own rather\n * than merging lists that were bounded separately.\n */\n placedSites.map((site) => (\n <CampaignMembersSection\n key={site.id}\n hostId={site.id}\n siteName={site.name || site.subdomain || site.id}\n campaignId={campaignId}\n startAtMs={campaign.startAtMs}\n endAtMs={campaign.endAtMs}\n />\n ))\n )}\n\n <Divider />\n {/*\n THE MAIL'S OWN FIGURES, under the outcomes rather than over them.\n\n The sum over the campaign's emails, not a second set of counters.\n Nothing is stored per campaign: a rollup document would have to be\n kept true against every delivery event of every email in it, and the\n numbers it duplicates are already on the sends this page reads.\n\n Drawn by the shared figures so a campaign's rate and a single\n message's read the same way — denominator named on the line, and an\n em dash rather than a zero where nothing was recorded.\n */}\n <Section title=\"The mail, across this campaign\">\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <Figure label=\"Addressed\" {...rolled(rollup.addressed)} />\n <Figure label=\"Sent\" {...rolled(rollup.sent)} />\n <Figure label=\"Delivered\" {...rolled(rollup.delivered)} />\n <Figure label=\"Opens\" {...rolled(rollup.opens)} />\n <Figure label=\"Clicks\" {...rolled(rollup.clicks)} />\n <Figure label=\"Unsubscribed\" {...rolled(rollup.unsubscribes)} />\n </Stack>\n </Section>\n <Stack spacing={0.5}>\n <RateRow label=\"Open rate\" rate={rollup.openRate} />\n <RateRow label=\"Click rate\" rate={rollup.clickRate} />\n <RateRow label=\"Unsubscribe rate\" rate={rollup.unsubscribeRate} />\n </Stack>\n\n <Divider />\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', justifyContent: 'space-between' }}\n >\n {/*\n The section rail's own heading style, so this reads as one section\n among the others rather than as the page's subject.\n */}\n <Typography variant=\"overline\" color=\"text.secondary\">\n {`Emails (${sends.length})`}\n </Typography>\n <Button\n size=\"small\"\n variant={composing ? 'text' : 'contained'}\n onClick={() => setComposing((open) => !open)}\n >\n {composing ? 'Close composer' : 'Write an email'}\n </Button>\n </Stack>\n {sends.length ? (\n <Stack spacing={0.5}>\n <ListTable\n aria-label=\"The campaign's emails\"\n rows={sends}\n columns={sendColumns}\n rowHeight={TABLE_ROW_HEIGHT}\n onOpen={(_id, send) => {\n const href = sendHref(send)\n if (href) router.push(href)\n }}\n />\n {sendsTruncated ? (\n <Alert severity=\"info\">\n {`Showing ${CAMPAIGN_EMAIL_CEILING} of this campaign's ` +\n 'emails. It has sent more — the ones listed are not ' +\n 'necessarily the most recent, because a send carries no ' +\n 'date field that every writer stamps, and the figures above ' +\n 'cover the emails listed.'}\n </Alert>\n ) : null}\n </Stack>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No emails in this campaign yet.'}\n </Typography>\n )}\n\n {/*\n THE COMPOSER, ON DEMAND.\n\n It opens listens of its own — the site's email designs, the org's\n lists and segments, the running experiments — and this page is also\n where somebody arrives to read numbers. Mounting it unasked would\n make every reader pay for a composer they did not open, which is the\n same cost the campaign report was split onto its own route to avoid.\n */}\n {composing ? (\n <Box>\n <Divider sx={{ mb: 2 }} />\n {/*\n WHICH SITE IT IS SENT AS, asked first on the org hub. The sender,\n the designs, the consent a recipient gave and the unsubscribe\n signature are all one site's, so the composer cannot open\n without one — and the choice is limited to the sites this\n campaign is placed on.\n */}\n {!hostId && placedSites.length > 1 ? (\n <TextField\n select\n size=\"small\"\n label=\"Send as\"\n value={composeHostId}\n onChange={(event) => setComposeHostId(event.target.value)}\n helperText=\"The site this email is sent from — its sender, designs and unsubscribe page\"\n sx={{ mb: 2, maxWidth: 420 }}\n fullWidth\n >\n {placedSites.map((site) => (\n <MenuItem key={site.id} value={site.id}>\n {site.name || site.subdomain || site.id}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n {writeAsHostId ? (\n <CampaignComposer\n key={writeAsHostId}\n hostId={writeAsHostId}\n emailCampaignId={campaignId}\n campaignListIds={listIds}\n campaignTopicId={campaign.topicId}\n onSent={() => setComposing(false)}\n />\n ) : !placedSites.length && !hostId ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Place this campaign on a site before writing an email in ' +\n 'it — every email is sent as one site.'}\n </Typography>\n ) : null}\n </Box>\n ) : null}\n </Stack>\n {/*\n THE EDIT DRAWER, on the campaign's own page.\n\n Its list picker reads the same `listDocs` the chips above already\n cost, so opening it adds one read — the topic catalog — and only\n while it is open.\n */}\n {/* Draws nothing: the zone the topic catalog's owner answers through. */}\n {topicSource}\n <CampaignEditDrawer\n open={editing}\n onClose={() => setEditing(false)}\n campaign={campaign}\n lists={(listDocs ?? []).map((list: any) => ({\n value: String(list.$id),\n label: String(list.name ?? list.$id),\n }))}\n topics={topicOptions}\n sites={orgMount ? orgSiteOptions(orgMount) : undefined}\n busy={saving}\n error={saveError}\n onSubmit={(values) => void handleSave(values)}\n />\n </CardDisplay>\n )\n\n return (\n <>\n {/* The page heading and the trail name the campaign; this card is\n then free to say what it holds rather than repeating the title. */}\n <PageHeaderRecord title={campaign.name || campaignId} />\n {card}\n </>\n )\n}\nCampaignDetailCard.displayName = 'CampaignDetailCard'\n\nexport default CampaignDetailCard\n"],"names":["mdiDeleteOutline","mdiEyeOutline","mdiPaletteOutline","mdiPencilOutline","AppLink","CardDisplay","MdiIcon","useConfirmationContext","Figure","RateRow","Section","ListRowActions","ListTable","listActionsColumn","RowActionsMenu","TABLE_ROW_HEIGHT","ceilingedWindow","PageHeaderRecord","pluginDocsHelp","Alert","Box","Button","Chip","Divider","MenuItem","Stack","TextField","Typography","collection","deleteField","documentId","limit","orderBy","query","updateDoc","where","useCallback","useMemo","useState","useRouter","useSnackbar","useFirestore","useFirestoreCollection","useFirestoreDoc","campaignRollup","campaignSendDisplay","campaignSiteIds","campaignVisibleTo","campaignWindowState","emailListTimeMs","CampaignComposer","CampaignEditDrawer","CampaignConversionsSection","CampaignDestinationsSection","CampaignRevenueSection","CampaignSequencesSection","CampaignMembersSection","CampaignReportCard","campaignContainerDoc","campaignSendsQuery","orgSiteHubPath","orgSiteName","orgSiteOptions","topicCatalogHostId","useMarketingOrgId","useMarketingOrgMount","useCampaignManageApi","useEmailsHubPath","useCampaignTopicOptions","CAMPAIGN_EMAIL_CEILING","detailDocsHelp","anchor","excerpt","rolled","value","note","recorded","sends","CampaignDetailCard","props","campaign","placedSites","hostId","campaignId","basePath","firestore","emailsHub","router","orgMount","orgId","confirm","enqueueSnackbar","manageApi","composeHostId","setComposeHostId","conversionsHostId","setConversionsHostId","composing","setComposing","editing","setEditing","saving","setSaving","saveError","setSaveError","deleting","setDeleting","data","status","sendDocs","Boolean","idField","listDocs","topics","source","topicSource","enabled","topicOptions","map","topic","id","label","name","rows","readSends","truncated","sendsTruncated","sort","a","b","rollup","sendIds","send","String","$id","filter","handleSave","values","startAtMs","endAtMs","listIds","topicId","siteIds","undefined","visibleTo","variant","persist","error","console","handleDelete","unfinished","scheduled","sending","agreed","title","description","length","toLocaleString","confirmationText","then","catch","response","payload","action","ok","allowDuplicate","push","names","Map","list","messagesPath","sendHref","templatesPath","hub","sendActions","templateScreenId","templates","key","icon","path","size","href","disabled","disabledReason","sendColumns","field","headerName","flex","minWidth","valueGetter","_value","subject","renderCell","row","onClick","event","stopPropagation","width","display","state","sendAtMs","Date","color","type","align","headerAlign","Number","stats","sent","recipients","opens","valueFormatter","clicks","items","windowState","now","start","toLocaleDateString","end","placedIds","hosts","site","includes","conversionsHost","conversionsBasePath","writeAsHostId","card","header","subheader","help","HeaderProps","direction","spacing","sx","alignItems","component","componentVariant","nativeButton","destructive","contentGutterX","contentGutterY","get","join","select","onChange","target","maxWidth","subdomain","siteName","flexWrap","addressed","delivered","unsubscribes","rate","openRate","clickRate","unsubscribeRate","justifyContent","open","aria-label","columns","rowHeight","onOpen","_id","severity","mb","helperText","fullWidth","emailCampaignId","campaignListIds","campaignTopicId","onSent","onClose","lists","sites","busy","onSubmit","displayName"],"mappings":"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"}
@@ -185,10 +185,10 @@ import { useEmailsHubPath } from "./use-emails-hub-path.js";
185
185
  campaignId
186
186
  ]);
187
187
  /*
188
- * Where this send's links lead. The message's page is the Emails console's
189
- * under a site and the org hub's own Emails section over the org; the
188
+ * Where this send's links lead. The message's page is on the Emails page —
189
+ * the site's under a site, the organization's over the org; the
190
190
  * conversions list is always the sending site's.
191
- */ const messageHref = orgMount ? `${orgMount.basePath}/emails/${campaignId}` : emailsHub ? `${emailsHub}/messages/${campaignId}` : null;
191
+ */ const messageHref = emailsHub ? `${emailsHub}/messages/${campaignId}` : null;
192
192
  const conversionsHub = orgMount ? orgSiteHubPath(orgMount, String((_ref = campaign == null ? void 0 : campaign.hostId) != null ? _ref : ''), 'marketing') : basePath;
193
193
  const report = campaignReport(campaign == null ? void 0 : campaign.stats);
194
194
  const linkReport = campaignLinkReport(links);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/components/campaign-report-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { PageHeaderRecord, pluginDocsHelp } from '@aglyn/aglyn'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport {\n Alert,\n Box,\n Button,\n Chip,\n Divider,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\nimport { useFirestore, useFirestoreDoc } from '@aglyn/tenant-feature-instance'\nimport {\n campaignLinkReport,\n campaignReport,\n type CampaignLinkRollup,\n type CampaignStats,\n} from '@aglyn/shared-ui-email-campaigns/model/campaign-report'\nimport {\n campaignRevenueReport,\n type CampaignRevenueRollup,\n} from '@aglyn/shared-ui-email-campaigns/model/campaign-revenue'\nimport {\n campaignConversionsReport,\n type CampaignConversionsRollup,\n} from '@aglyn/shared-ui-email-campaigns/model/campaign-conversions'\n/*\n * The three renderers every email report shares. Imported rather than kept\n * here, so \"a rate prints its denominator\" is one implementation and not a\n * convention each new card has to remember.\n */\nimport {\n Figure,\n MoneyFigure,\n MoneyPerUnitRow,\n percent,\n RateRow,\n Section,\n} from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport {\n emailSendTimeMs,\n} from '@aglyn/shared-ui-email-campaigns/model/email-record'\nimport {\n campaignSendDisplay,\n campaignSendProgress,\n} from '@aglyn/shared-ui-email-campaigns/model/campaign-container'\nimport { campaignSendDoc, campaignSendReportDoc } from './campaign-queries'\nimport {\n orgSiteHubPath,\n useMarketingOrgId,\n useMarketingOrgMount,\n} from './marketing-org-mount'\nimport { useEmailsHubPath } from './use-emails-hub-path'\n\n/**\n * The help affordance, hoisted so BOTH headers carry it.\n *\n * The unreadable-campaign branch below renders its own `CardDisplay`, and a\n * header without help is a header a reader has no way out of — which is\n * exactly the state that branch describes.\n */\nconst reportDocsHelp = pluginDocsHelp('emailCampaigns', {\n anchor: '#the-campaign-report',\n excerpt:\n 'What one campaign did: delivery, opens, clicks, bounces, complaints, ' +\n 'unsubscribes and the revenue it was credited with, each with the ' +\n 'population it is measured against.',\n})\n\nexport interface CampaignReportCardProps {\n /** The site, or `null` on the org Marketing hub. */\n hostId: string | null\n campaignId: string\n /** The marketing hub URL, for the way back to the campaigns list. */\n basePath: string\n}\n\n/**\n * ONE CAMPAIGN, MEASURED — and every denominator on screen.\n *\n * ## What was missing\n *\n * Opens and clicks have been recorded on the campaign document since AGL-268\n * and displayed as two words in a history row: `12 opens · 3 clicks`. Nothing\n * anywhere showed a rate, a delivery count, a bounce, a complaint or an\n * unsubscribe, and the populations the send itself measured — how large the\n * audience was, how many the consent rule refused, how many were already\n * suppressed — were computed on every send and written nowhere.\n *\n * ## The rule this screen is built around\n *\n * **Every rate names its denominator, in the same breath as the number.**\n *\n * That is not decoration. An open rate over `sent` and an open rate over\n * `delivered` are different numbers that share a label, and the difference\n * between them is exactly the mail that bounced — so a report that showed one\n * while the reader assumed the other would be wrong by however bad the\n * address list is, which is the case where being wrong matters most. Worse,\n * \"click rate\" is used in the wild for two unrelated quantities — clicks over\n * delivered, and clicks over openers — and they differ by a factor of three\n * or four on ordinary mail.\n *\n * So the arithmetic is not done here at all. `campaign-report.ts` returns\n * each rate as a triple carrying `denominatorLabel`, this file renders that\n * label, and a rate whose denominator is zero or unrecorded comes back `null`\n * and is drawn as \"—\" with a caveat naming the reason. There is no path\n * through this component that prints a percentage without saying what it is a\n * percentage of.\n *\n * ## What the page is about comes before how the mail did\n *\n * A campaign of one email is the ordinary shape, and this card is the page it\n * gets: the URL names a send, so there is no container above it and no list of\n * emails to draw. That collapse is honest, but it decided what the page was\n * ABOUT — delivery, engagement and rates are mechanics of a message, and a\n * page that led with them at length read as a mail report wearing the word\n * campaign.\n *\n * So the outcomes lead: what it caused, what it earned, where it sent people.\n * Two of those headings are the ones `campaign-reach-sections.tsx` puts at the\n * top of a campaign holding SEVERAL emails, and they are the same words here\n * deliberately — a merchant who opens a campaign of one and a campaign of six\n * is asking the same question and should not have to learn two vocabularies to\n * find the answer. The email's own figures follow, under a row that names the\n * message they belong to.\n *\n * Nothing about the reads changed with the order. The conversions, revenue and\n * destination figures were already on this page and already came from the four\n * documents below; they were simply underneath the mail.\n *\n * ## Three documents, whatever the campaign's size\n *\n * The whole screen is one campaign document plus one link-rollup document\n * plus one revenue-rollup document. All three are single-document listens, so\n * the cost does not move with the audience — a report over a 50,000-recipient\n * send reads the same three documents as one over ten. Aggregating the\n * per-recipient delivery log on mount would have been the obvious\n * implementation and would have read one document per recipient, every time\n * anybody opened the page; joining orders to campaigns at read time would\n * have been the same mistake one collection along, which is why the join is\n * done once at the sale and stored.\n *\n * ## No per-recipient view here, deliberately\n *\n * Every number on this screen is an aggregate that carries no address, and\n * that is what makes it three single-document reads. A per-recipient list is a\n * different read and a different question, so it lives where it is asked:\n * the design report's recipients table, which resolves the campaigns from the\n * design on a server that has already checked the reader's site role, and\n * returns only rows tagged with that site.\n *\n * ## The same four documents at the org level\n *\n * A send and its rollups are the org's, so the org hub reads the same four\n * documents. The one link that leads to a SITE fact — the conversions list,\n * which is that site's visitors — goes to the site the send was sent as.\n *\n * The other per-recipient view — the staff delivery log on the user detail\n * page — spans every site on the install and is behind a staff claim that\n * records who looked. Neither reaches this card: adding a recipient list here\n * would put an unbounded read behind a widget whose whole cost model is that\n * it has none.\n */\nexport function CampaignReportCard(props: CampaignReportCardProps) {\n const { hostId, campaignId, basePath } = props\n // The sibling hub: the message's own page belongs to the Emails console.\n const emailsHub = useEmailsHubPath()\n const orgMount = useMarketingOrgMount()\n const { orgId } = useMarketingOrgId(hostId)\n const firestore = useFirestore()\n\n const { data: campaign, status } = useFirestoreDoc<\n Record<string, unknown> & { stats?: CampaignStats }\n >(\n () => (orgId ? campaignSendDoc(firestore, orgId, campaignId) : null),\n [firestore, orgId, campaignId],\n )\n /*\n * The link rollup, its own document rather than a field on the campaign.\n *\n * A map of destinations grows with the campaign's content and a campaign\n * document is read by the history list, the glance widget and the send\n * path; putting an unbounded map on it would make every one of those reads\n * larger. Split, the rollup is read by exactly the screen that renders it.\n */\n const { data: links } = useFirestoreDoc<CampaignLinkRollup>(\n () =>\n orgId ? campaignSendReportDoc(firestore, orgId, campaignId, 'links') : null,\n [firestore, orgId, campaignId],\n )\n /*\n * The revenue rollup, a third single-document listen.\n *\n * Its own document beside the link rollup rather than a field on the\n * campaign, for the reason the link rollup is: the campaign document is\n * read by the history list, the glance widget and the send path, and a map\n * that grows with the campaign's sales would enlarge every one of those\n * reads. The cost model in this file's header is unchanged in the way that\n * matters — the page still reads a fixed number of documents whatever the\n * audience, and no per-order or per-recipient read exists anywhere on it.\n */\n const { data: revenue } = useFirestoreDoc<CampaignRevenueRollup>(\n () =>\n orgId\n ? campaignSendReportDoc(firestore, orgId, campaignId, 'revenue')\n : null,\n [firestore, orgId, campaignId],\n )\n\n /*\n * The conversions rollup, a fourth single-document listen.\n *\n * Beside `reports/revenue` and read the same way, because it is the same\n * join answering the other half of the question: revenue is what a campaign\n * EARNED from people who named themselves at checkout, and this is what it\n * CAUSED among people who were anonymous until the moment they submitted,\n * signed up or booked. The cost model in this file's header is unchanged in\n * the way that matters — a fixed number of documents whatever the audience,\n * and no per-conversion read anywhere on the page.\n */\n const { data: conversions } = useFirestoreDoc<CampaignConversionsRollup>(\n () =>\n orgId\n ? campaignSendReportDoc(firestore, orgId, campaignId, 'conversions')\n : null,\n [firestore, orgId, campaignId],\n )\n /*\n * Where this send's links lead. The message's page is the Emails console's\n * under a site and the org hub's own Emails section over the org; the\n * conversions list is always the sending site's.\n */\n const messageHref = orgMount\n ? `${orgMount.basePath}/emails/${campaignId}`\n : emailsHub\n ? `${emailsHub}/messages/${campaignId}`\n : null\n const conversionsHub = orgMount\n ? orgSiteHubPath(orgMount, String(campaign?.hostId ?? ''), 'marketing')\n : basePath\n\n const report = campaignReport(campaign?.stats)\n const linkReport = campaignLinkReport(links)\n const revenueReport = campaignRevenueReport({\n rollup: revenue,\n /*\n * The DENOMINATOR, handed over from the same document read at the same\n * instant rather than re-read here. `report.delivered` is `null` when no\n * delivery event was ever recorded, and it is passed as `null` so the\n * revenue figures over it are withheld for exactly the reason every other\n * rate over it is.\n */\n delivered: report.delivered,\n midFlight: campaign\n ? campaignSendProgress(campaign as never).state === 'sending'\n : false,\n })\n /*\n * No denominator is handed in, unlike the revenue report. A conversion RATE\n * over delivered messages would repeat the defect the revenue section\n * refuses for orders — one visitor can submit two forms, so the quotient\n * passes 100% without anything being wrong — and counting distinct people\n * instead needs a document per person per campaign, which is the\n * per-recipient read this screen's cost model exists to refuse.\n */\n const conversionsReport = campaignConversionsReport({ rollup: conversions })\n const subject = String(campaign?.subject ?? 'Campaign')\n const sendTimeMs = campaign ? emailSendTimeMs(campaign) : 0\n\n const backButton = (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={`${basePath}/campaigns`}\n size=\"small\"\n color=\"primary\"\n >\n {'All campaigns'}\n </Button>\n )\n\n // Nothing to say until the read has had its chance to answer.\n if (!campaign && (status === 'loading' || !orgId)) return null\n\n if (!campaign) {\n return (\n <CardDisplay\n header={'Campaign report'}\n help={reportDocsHelp}\n contentGutterX\n contentGutterY\n HeaderProps={{ action: backButton }}\n >\n {/*\n * Not \"no data\". A campaign 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 campaign\n * they sent reached nobody.\n */}\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This campaign could not be loaded. It may have been deleted.'}\n </Typography>\n </CardDisplay>\n )\n }\n\n /* The card, named so the page chrome above it is a plain list of\n what this surface publishes upward. */\n const card = (\n <CardDisplay\n header={'Campaign report'}\n help={reportDocsHelp}\n contentGutterX\n contentGutterY\n HeaderProps={{ action: backButton }}\n >\n <Stack spacing={3}>\n {report.caveats.map((caveat) => (\n <Alert key={caveat.id} severity=\"info\">\n {caveat.message}\n </Alert>\n ))}\n\n {/*==========================================\n * WHAT THIS CAMPAIGN CAUSED — the page's first section, and the\n * heading a campaign of several emails leads with too.\n *\n * The same join as the revenue below, credited under the same rule,\n * answering it for people who were anonymous until the moment being\n * counted: they arrived from a campaign link, browsed, and only\n * became somebody when they submitted a form, signed up or booked.\n *\n * THE FOUR FIGURES ARE NEVER ADDED. One person filling in one form\n * writes a submission, a contact and a lead, so a total would count\n * that visit three times — and it would look like a bigger version\n * of a real number, which is why the model carries no total for this\n * JSX to reach for and the caveat says so in words above them.\n *\n * They are laid out in a row of independent `Figure`s rather than in\n * a table with a footer, because a footer row is where a reader\n * expects the sum to be.\n *=========================================*/}\n <Section title=\"What it caused\">\n {conversionsReport.caveats.map((caveat) => (\n <Alert key={caveat.id} severity=\"info\">\n {caveat.message}\n </Alert>\n ))}\n {conversionsReport.any ? (\n <Stack spacing={1}>\n <Stack\n direction=\"row\"\n spacing={4}\n useFlexGap\n sx={{ flexWrap: 'wrap' }}\n >\n {conversionsReport.kinds.map((entry) => (\n <Figure\n key={entry.kind}\n label={entry.label}\n value={entry.value}\n note={entry.note}\n />\n ))}\n </Stack>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Credited ${conversionsReport.model === 'last-click' ? 'to the last campaign whose link the visitor clicked' : `under the ${conversionsReport.model} model`}, ` +\n `within ${conversionsReport.windowDays} days of that click. ` +\n 'Somebody who converted without ever following a campaign ' +\n 'link is credited to no campaign at all — nothing is ' +\n 'inferred from a referrer — so these are a floor rather ' +\n 'than everything this campaign influenced. The ones ' +\n 'credited to nobody are counted under Conversions in the ' +\n 'marketing console.'}\n </Typography>\n {/*\n THE RECORDS BEHIND THE FIGURES. The rollup says how many; the\n list says which, and it is the only place the uncredited half\n is counted. A link rather than a table here — the records are\n a paged read and this page's whole cost model is a fixed\n number of documents whatever the audience.\n */}\n {conversionsHub ? (\n <Box>\n <Button\n component={AppLink as any}\n {...({\n componentVariant: 'naked',\n nativeButton: false,\n } as any)}\n href={`${conversionsHub}/conversions/${campaignId}`}\n size=\"small\"\n color=\"primary\"\n >\n {'See these conversions'}\n </Button>\n </Box>\n ) : null}\n </Stack>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {conversionsReport.recorded\n ? 'Nothing has been credited to this campaign yet.'\n : 'No conversions have been attributed to this campaign. A ' +\n 'form submission, lead, contact or booking is credited to ' +\n 'the last campaign whose link the visitor clicked, within ' +\n `${conversionsReport.windowDays} days — a campaign sent ` +\n 'before that was recorded will never show a figure here.'}\n </Typography>\n )}\n </Section>\n\n <Divider />\n\n {/*==========================================\n * WHAT THIS CAMPAIGN EARNED.\n *\n * The merchant's second question, and the one every compared\n * product answers by reconstructing revenue probabilistically\n * because it does not own the order. Here it is a join: the click\n * and the order are rows in one database, so there is no tracking\n * script, no catalog sync and nothing estimated except the model.\n *\n * THE MODEL IS ON SCREEN, in a sentence, above the money. A revenue\n * figure whose rule the reader cannot state is worse than no\n * figure, because they will use it anyway — and the two things they\n * have to know to use it are which campaign gets the credit when\n * several touched the buyer, and how long a click stays creditable.\n *\n * NET LEADS. Gross and refunded are both shown beside it, because\n * \"this campaign made $4,000\" and \"this campaign made $4,000 of\n * which $3,100 came back\" are different facts about whether to send\n * another one.\n *=========================================*/}\n <Section title=\"Revenue\">\n {revenueReport.caveats.map((caveat) => (\n <Alert key={caveat.id} severity=\"info\">\n {caveat.message}\n </Alert>\n ))}\n {revenueReport.currencies.length ? (\n <Stack spacing={3}>\n {revenueReport.currencies.map((entry) => (\n <Stack key={entry.currency} spacing={1}>\n {/*\n * The currency is a HEADING when there is more than one,\n * and absent when there is one. A per-currency label on a\n * report that only ever shows dollars is noise; the moment\n * there are two, the reader has to be able to see at a\n * glance that the blocks are not addable — which is also\n * what the caveat above says in words.\n */}\n {revenueReport.multiCurrency ? (\n <Typography variant=\"subtitle2\">\n {entry.currency.toUpperCase()}\n </Typography>\n ) : null}\n <Stack\n direction=\"row\"\n spacing={4}\n useFlexGap\n sx={{ flexWrap: 'wrap' }}\n >\n <MoneyFigure\n label=\"Net revenue\"\n cents={entry.netCents}\n currency={entry.currency}\n note=\"after refunds\"\n />\n <MoneyFigure\n label=\"Gross revenue\"\n cents={entry.grossCents}\n currency={entry.currency}\n note=\"as charged\"\n />\n <MoneyFigure\n label=\"Refunded\"\n cents={entry.refundedCents}\n currency={entry.currency}\n note=\"handed back\"\n />\n <Figure\n label=\"Orders\"\n value={entry.orders}\n note=\"credited to this campaign\"\n />\n <Figure\n label=\"Fully refunded\"\n value={entry.refundedOrders}\n note=\"of those orders\"\n />\n </Stack>\n <MoneyPerUnitRow\n label=\"Net revenue per delivered message\"\n figure={entry.netPerDelivered}\n />\n </Stack>\n ))}\n {/*\n * NO CONVERSION RATE, and this is where a reader would look\n * for one. Orders over delivered is not a rate: one buyer can\n * place two orders, so the quotient passes 100% without\n * anything being wrong — the same defect that keeps `opens`\n * out of the open-rate numerator two sections above. Counting\n * distinct buyers instead would need a document per person per\n * campaign, which is the per-recipient read this screen's cost\n * model exists to refuse. The order count is shown as a count.\n */}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Credited ${revenueReport.model === 'last-click' ? 'to the last campaign whose link the buyer clicked' : `under the ${revenueReport.model} model`}, ` +\n `within ${revenueReport.windowDays} days of that click. ` +\n 'Clicks only — an open is not treated as evidence that ' +\n 'anybody read the email. An order placed by somebody who ' +\n 'never clicked, or who checked out without giving an ' +\n 'address, is credited to no campaign, so this is a floor ' +\n 'rather than every sale this campaign influenced.'}\n </Typography>\n </Stack>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {revenueReport.recorded\n ? 'No orders have been credited to this campaign.'\n : 'No revenue has been attributed to this campaign. Orders ' +\n 'are credited to the last campaign whose link the buyer ' +\n `clicked, within ${revenueReport.windowDays} days — a ` +\n 'campaign sent before that was recorded, or one on a site ' +\n 'with no store, will never show a figure here.'}\n </Typography>\n )}\n </Section>\n\n <Divider />\n\n <Section title=\"Where it sent people\">\n {/*\n The heading a campaign of several emails carries over the same\n question, so the two shapes of campaign page read alike. A\n campaign owns no page — this is where its mail was actually\n followed to, which is the only account of it the data holds.\n */}\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'The pages this campaign’s links were followed to.'}\n </Typography>\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 {/*\n * The normalisation, stated rather than left for somebody to\n * discover from a total that does not add up. Two links to the\n * same page distinguished only by their UTM tags are one row\n * here, and a merchant comparing this table with the campaign\n * they built has to be told that before they conclude a link\n * is missing.\n */}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Counted by address and path — query strings are dropped, so ' +\n '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 campaign 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 campaign, 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 campaign.'}\n </Typography>\n )}\n </Section>\n\n <Divider />\n\n {/*==========================================\n * THE EMAIL THE FIGURES BELOW CAME FROM — and the hinge between the\n * two halves of this page.\n *\n * This screen is reached at `/marketing/campaigns/{sendId}`, which\n * is a CAMPAIGN url resolving to a send — the fall-through that keeps\n * every unsubscribe footer and every pasted report link working. So\n * a reader arrives from the campaigns table, where the row carries a\n * \"Single send\" chip, and everything under this row is a fact about\n * a message rather than about the campaign.\n *\n * Figures with no visible source read as a campaign holding no\n * emails while reporting real numbers, which is a contradiction\n * rather than a shortage of detail. Naming the one email — and\n * saying it IS the campaign, which is what the chip already claims\n * and what `campaignListRows` already does at read time — is what\n * makes the page answer the question it raises.\n *\n * One row, from the document this card already read: no query, no\n * second listen, and the two-document cost the header describes is\n * unchanged.\n *=========================================*/}\n <Section title=\"The email in this campaign\">\n <Stack spacing={1}>\n <Stack\n direction=\"row\"\n spacing={1}\n useFlexGap\n sx={{ alignItems: 'center', flexWrap: 'wrap' }}\n >\n {/*\n The MESSAGE's own page, which the Emails console owns — hence\n the sibling hub rather than this surface's `basePath`. Plain\n text until that hub resolves: a link with no destination is\n worse than none, and the subject is what the reader came for\n either way.\n */}\n {messageHref ? (\n <AppLink href={messageHref}>{subject}</AppLink>\n ) : (\n <Typography variant=\"body2\">{subject}</Typography>\n )}\n {/*\n What the email is DOING, not the field it stores. One\n delivering an audience larger than one batch is written back\n as `scheduled` between runs, so a chip rendering the status\n said \"Scheduled\" at the head of a report of five hundred\n deliveries.\n */}\n <Chip\n size=\"small\"\n label={campaignSendDisplay(campaign as never).label}\n />\n <Chip\n size=\"small\"\n variant=\"outlined\"\n label=\"Single send\"\n title={\n 'One email, sent on its own rather than as part of a ' +\n 'campaign of several. Its report and its unsubscribe links ' +\n 'are unchanged.'\n }\n />\n <Typography variant=\"caption\" color=\"text.secondary\">\n {sendTimeMs\n ? new Date(sendTimeMs).toLocaleString()\n : 'not recorded'}\n </Typography>\n </Stack>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'This campaign is one email, so the sections above are what ' +\n 'that one email caused. What follows is the mail itself — ' +\n 'how it was delivered and how it was read. Open the message ' +\n 'for its body and the people it reached.'}\n </Typography>\n </Stack>\n </Section>\n\n <Divider />\n\n {/*==========================================\n * WHAT HAPPENED TO THE MAIL.\n *\n * Counts first and rates second, in that order and not the other\n * way round, because a rate is only readable once you know what it\n * was taken over — and because the counts are the part that is\n * always true. Every rate below can be absent; none of these can.\n *=========================================*/}\n <Section title=\"Delivery\">\n <Stack direction=\"row\" spacing={4} useFlexGap sx={{ flexWrap: 'wrap' }}>\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 {/*\n * BOTH the event count and the distinct count, side by side.\n *\n * `Opens` is every open event — one reader opening four times is\n * four — and it is the number this product has always shown. It\n * cannot be a rate numerator: divided by anything it exceeds 100%\n * the moment somebody reads an email twice. `Readers who opened`\n * is the distinct count, and it is what the rate below divides.\n * Showing only one of them would either hide activity or invite a\n * rate nobody can defend, so both are here with different names.\n */}\n <Stack direction=\"row\" spacing={4} useFlexGap sx={{ flexWrap: 'wrap' }}>\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 campaign'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 went to\">\n {/*\n * The populations the SEND measured, recorded at send time.\n *\n * Not recomputed here, and the note under the heading says so:\n * consent records change and addresses get suppressed, so\n * asking the list today produces a number that is true of the\n * list and false of the campaign.\n */}\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Measured when this campaign 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 </Stack>\n </CardDisplay>\n )\n\n return (\n <>\n {/* The page heading and the trail name the send; this card is\n then free to say what it holds rather than repeating the title. */}\n <PageHeaderRecord\n title={\n campaign ? String(campaign.subject || '') || campaignId : undefined\n }\n />\n {card}\n </>\n )\n}\nCampaignReportCard.displayName = 'CampaignReportCard'\n\nexport default CampaignReportCard\n"],"names":["PageHeaderRecord","pluginDocsHelp","AppLink","CardDisplay","ScrollTable","Alert","Box","Button","Chip","Divider","Stack","TableBody","TableCell","TableHead","TableRow","Typography","useFirestore","useFirestoreDoc","campaignLinkReport","campaignReport","campaignRevenueReport","campaignConversionsReport","Figure","MoneyFigure","MoneyPerUnitRow","percent","RateRow","Section","emailSendTimeMs","campaignSendDisplay","campaignSendProgress","campaignSendDoc","campaignSendReportDoc","orgSiteHubPath","useMarketingOrgId","useMarketingOrgMount","useEmailsHubPath","reportDocsHelp","anchor","excerpt","CampaignReportCard","props","hostId","campaignId","basePath","emailsHub","orgMount","orgId","firestore","data","campaign","status","links","revenue","conversions","messageHref","conversionsHub","String","report","stats","linkReport","revenueReport","rollup","delivered","midFlight","state","conversionsReport","subject","sendTimeMs","backButton","component","componentVariant","nativeButton","href","size","color","header","help","contentGutterX","contentGutterY","HeaderProps","action","variant","card","spacing","caveats","map","caveat","severity","message","id","title","any","direction","useFlexGap","sx","flexWrap","kinds","entry","label","value","note","kind","model","windowDays","recorded","currencies","length","multiCurrency","currency","toUpperCase","cents","netCents","grossCents","refundedCents","orders","refundedOrders","figure","netPerDelivered","rows","align","row","wordBreak","url","fontWeight","clicks","toLocaleString","share","denominator","denominatorLabel","unattributedClicks","overflowClicks","alignItems","Date","recipients","sent","bounced","complained","opens","uniqueOpens","uniqueClicks","unsubscribes","rate","rates","delivery","open","click","clickToOpen","bounce","complaint","unsubscribe","populations","population","count","of","ofLabel","undefined","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/components/campaign-report-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { PageHeaderRecord, pluginDocsHelp } from '@aglyn/aglyn'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport {\n Alert,\n Box,\n Button,\n Chip,\n Divider,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\nimport { useFirestore, useFirestoreDoc } from '@aglyn/tenant-feature-instance'\nimport {\n campaignLinkReport,\n campaignReport,\n type CampaignLinkRollup,\n type CampaignStats,\n} from '@aglyn/shared-ui-email-campaigns/model/campaign-report'\nimport {\n campaignRevenueReport,\n type CampaignRevenueRollup,\n} from '@aglyn/shared-ui-email-campaigns/model/campaign-revenue'\nimport {\n campaignConversionsReport,\n type CampaignConversionsRollup,\n} from '@aglyn/shared-ui-email-campaigns/model/campaign-conversions'\n/*\n * The three renderers every email report shares. Imported rather than kept\n * here, so \"a rate prints its denominator\" is one implementation and not a\n * convention each new card has to remember.\n */\nimport {\n Figure,\n MoneyFigure,\n MoneyPerUnitRow,\n percent,\n RateRow,\n Section,\n} from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport {\n emailSendTimeMs,\n} from '@aglyn/shared-ui-email-campaigns/model/email-record'\nimport {\n campaignSendDisplay,\n campaignSendProgress,\n} from '@aglyn/shared-ui-email-campaigns/model/campaign-container'\nimport { campaignSendDoc, campaignSendReportDoc } from './campaign-queries'\nimport {\n orgSiteHubPath,\n useMarketingOrgId,\n useMarketingOrgMount,\n} from './marketing-org-mount'\nimport { useEmailsHubPath } from './use-emails-hub-path'\n\n/**\n * The help affordance, hoisted so BOTH headers carry it.\n *\n * The unreadable-campaign branch below renders its own `CardDisplay`, and a\n * header without help is a header a reader has no way out of — which is\n * exactly the state that branch describes.\n */\nconst reportDocsHelp = pluginDocsHelp('emailCampaigns', {\n anchor: '#the-campaign-report',\n excerpt:\n 'What one campaign did: delivery, opens, clicks, bounces, complaints, ' +\n 'unsubscribes and the revenue it was credited with, each with the ' +\n 'population it is measured against.',\n})\n\nexport interface CampaignReportCardProps {\n /** The site, or `null` on the org Marketing hub. */\n hostId: string | null\n campaignId: string\n /** The marketing hub URL, for the way back to the campaigns list. */\n basePath: string\n}\n\n/**\n * ONE CAMPAIGN, MEASURED — and every denominator on screen.\n *\n * ## What was missing\n *\n * Opens and clicks have been recorded on the campaign document since AGL-268\n * and displayed as two words in a history row: `12 opens · 3 clicks`. Nothing\n * anywhere showed a rate, a delivery count, a bounce, a complaint or an\n * unsubscribe, and the populations the send itself measured — how large the\n * audience was, how many the consent rule refused, how many were already\n * suppressed — were computed on every send and written nowhere.\n *\n * ## The rule this screen is built around\n *\n * **Every rate names its denominator, in the same breath as the number.**\n *\n * That is not decoration. An open rate over `sent` and an open rate over\n * `delivered` are different numbers that share a label, and the difference\n * between them is exactly the mail that bounced — so a report that showed one\n * while the reader assumed the other would be wrong by however bad the\n * address list is, which is the case where being wrong matters most. Worse,\n * \"click rate\" is used in the wild for two unrelated quantities — clicks over\n * delivered, and clicks over openers — and they differ by a factor of three\n * or four on ordinary mail.\n *\n * So the arithmetic is not done here at all. `campaign-report.ts` returns\n * each rate as a triple carrying `denominatorLabel`, this file renders that\n * label, and a rate whose denominator is zero or unrecorded comes back `null`\n * and is drawn as \"—\" with a caveat naming the reason. There is no path\n * through this component that prints a percentage without saying what it is a\n * percentage of.\n *\n * ## What the page is about comes before how the mail did\n *\n * A campaign of one email is the ordinary shape, and this card is the page it\n * gets: the URL names a send, so there is no container above it and no list of\n * emails to draw. That collapse is honest, but it decided what the page was\n * ABOUT — delivery, engagement and rates are mechanics of a message, and a\n * page that led with them at length read as a mail report wearing the word\n * campaign.\n *\n * So the outcomes lead: what it caused, what it earned, where it sent people.\n * Two of those headings are the ones `campaign-reach-sections.tsx` puts at the\n * top of a campaign holding SEVERAL emails, and they are the same words here\n * deliberately — a merchant who opens a campaign of one and a campaign of six\n * is asking the same question and should not have to learn two vocabularies to\n * find the answer. The email's own figures follow, under a row that names the\n * message they belong to.\n *\n * Nothing about the reads changed with the order. The conversions, revenue and\n * destination figures were already on this page and already came from the four\n * documents below; they were simply underneath the mail.\n *\n * ## Three documents, whatever the campaign's size\n *\n * The whole screen is one campaign document plus one link-rollup document\n * plus one revenue-rollup document. All three are single-document listens, so\n * the cost does not move with the audience — a report over a 50,000-recipient\n * send reads the same three documents as one over ten. Aggregating the\n * per-recipient delivery log on mount would have been the obvious\n * implementation and would have read one document per recipient, every time\n * anybody opened the page; joining orders to campaigns at read time would\n * have been the same mistake one collection along, which is why the join is\n * done once at the sale and stored.\n *\n * ## No per-recipient view here, deliberately\n *\n * Every number on this screen is an aggregate that carries no address, and\n * that is what makes it three single-document reads. A per-recipient list is a\n * different read and a different question, so it lives where it is asked:\n * the design report's recipients table, which resolves the campaigns from the\n * design on a server that has already checked the reader's site role, and\n * returns only rows tagged with that site.\n *\n * ## The same four documents at the org level\n *\n * A send and its rollups are the org's, so the org hub reads the same four\n * documents. The one link that leads to a SITE fact — the conversions list,\n * which is that site's visitors — goes to the site the send was sent as.\n *\n * The other per-recipient view — the staff delivery log on the user detail\n * page — spans every site on the install and is behind a staff claim that\n * records who looked. Neither reaches this card: adding a recipient list here\n * would put an unbounded read behind a widget whose whole cost model is that\n * it has none.\n */\nexport function CampaignReportCard(props: CampaignReportCardProps) {\n const { hostId, campaignId, basePath } = props\n // The sibling hub: the message's own page belongs to the Emails console.\n const emailsHub = useEmailsHubPath()\n const orgMount = useMarketingOrgMount()\n const { orgId } = useMarketingOrgId(hostId)\n const firestore = useFirestore()\n\n const { data: campaign, status } = useFirestoreDoc<\n Record<string, unknown> & { stats?: CampaignStats }\n >(\n () => (orgId ? campaignSendDoc(firestore, orgId, campaignId) : null),\n [firestore, orgId, campaignId],\n )\n /*\n * The link rollup, its own document rather than a field on the campaign.\n *\n * A map of destinations grows with the campaign's content and a campaign\n * document is read by the history list, the glance widget and the send\n * path; putting an unbounded map on it would make every one of those reads\n * larger. Split, the rollup is read by exactly the screen that renders it.\n */\n const { data: links } = useFirestoreDoc<CampaignLinkRollup>(\n () =>\n orgId ? campaignSendReportDoc(firestore, orgId, campaignId, 'links') : null,\n [firestore, orgId, campaignId],\n )\n /*\n * The revenue rollup, a third single-document listen.\n *\n * Its own document beside the link rollup rather than a field on the\n * campaign, for the reason the link rollup is: the campaign document is\n * read by the history list, the glance widget and the send path, and a map\n * that grows with the campaign's sales would enlarge every one of those\n * reads. The cost model in this file's header is unchanged in the way that\n * matters — the page still reads a fixed number of documents whatever the\n * audience, and no per-order or per-recipient read exists anywhere on it.\n */\n const { data: revenue } = useFirestoreDoc<CampaignRevenueRollup>(\n () =>\n orgId\n ? campaignSendReportDoc(firestore, orgId, campaignId, 'revenue')\n : null,\n [firestore, orgId, campaignId],\n )\n\n /*\n * The conversions rollup, a fourth single-document listen.\n *\n * Beside `reports/revenue` and read the same way, because it is the same\n * join answering the other half of the question: revenue is what a campaign\n * EARNED from people who named themselves at checkout, and this is what it\n * CAUSED among people who were anonymous until the moment they submitted,\n * signed up or booked. The cost model in this file's header is unchanged in\n * the way that matters — a fixed number of documents whatever the audience,\n * and no per-conversion read anywhere on the page.\n */\n const { data: conversions } = useFirestoreDoc<CampaignConversionsRollup>(\n () =>\n orgId\n ? campaignSendReportDoc(firestore, orgId, campaignId, 'conversions')\n : null,\n [firestore, orgId, campaignId],\n )\n /*\n * Where this send's links lead. The message's page is on the Emails page —\n * the site's under a site, the organization's over the org; the\n * conversions list is always the sending site's.\n */\n const messageHref = emailsHub ? `${emailsHub}/messages/${campaignId}` : null\n const conversionsHub = orgMount\n ? orgSiteHubPath(orgMount, String(campaign?.hostId ?? ''), 'marketing')\n : basePath\n\n const report = campaignReport(campaign?.stats)\n const linkReport = campaignLinkReport(links)\n const revenueReport = campaignRevenueReport({\n rollup: revenue,\n /*\n * The DENOMINATOR, handed over from the same document read at the same\n * instant rather than re-read here. `report.delivered` is `null` when no\n * delivery event was ever recorded, and it is passed as `null` so the\n * revenue figures over it are withheld for exactly the reason every other\n * rate over it is.\n */\n delivered: report.delivered,\n midFlight: campaign\n ? campaignSendProgress(campaign as never).state === 'sending'\n : false,\n })\n /*\n * No denominator is handed in, unlike the revenue report. A conversion RATE\n * over delivered messages would repeat the defect the revenue section\n * refuses for orders — one visitor can submit two forms, so the quotient\n * passes 100% without anything being wrong — and counting distinct people\n * instead needs a document per person per campaign, which is the\n * per-recipient read this screen's cost model exists to refuse.\n */\n const conversionsReport = campaignConversionsReport({ rollup: conversions })\n const subject = String(campaign?.subject ?? 'Campaign')\n const sendTimeMs = campaign ? emailSendTimeMs(campaign) : 0\n\n const backButton = (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={`${basePath}/campaigns`}\n size=\"small\"\n color=\"primary\"\n >\n {'All campaigns'}\n </Button>\n )\n\n // Nothing to say until the read has had its chance to answer.\n if (!campaign && (status === 'loading' || !orgId)) return null\n\n if (!campaign) {\n return (\n <CardDisplay\n header={'Campaign report'}\n help={reportDocsHelp}\n contentGutterX\n contentGutterY\n HeaderProps={{ action: backButton }}\n >\n {/*\n * Not \"no data\". A campaign 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 campaign\n * they sent reached nobody.\n */}\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This campaign could not be loaded. It may have been deleted.'}\n </Typography>\n </CardDisplay>\n )\n }\n\n /* The card, named so the page chrome above it is a plain list of\n what this surface publishes upward. */\n const card = (\n <CardDisplay\n header={'Campaign report'}\n help={reportDocsHelp}\n contentGutterX\n contentGutterY\n HeaderProps={{ action: backButton }}\n >\n <Stack spacing={3}>\n {report.caveats.map((caveat) => (\n <Alert key={caveat.id} severity=\"info\">\n {caveat.message}\n </Alert>\n ))}\n\n {/*==========================================\n * WHAT THIS CAMPAIGN CAUSED — the page's first section, and the\n * heading a campaign of several emails leads with too.\n *\n * The same join as the revenue below, credited under the same rule,\n * answering it for people who were anonymous until the moment being\n * counted: they arrived from a campaign link, browsed, and only\n * became somebody when they submitted a form, signed up or booked.\n *\n * THE FOUR FIGURES ARE NEVER ADDED. One person filling in one form\n * writes a submission, a contact and a lead, so a total would count\n * that visit three times — and it would look like a bigger version\n * of a real number, which is why the model carries no total for this\n * JSX to reach for and the caveat says so in words above them.\n *\n * They are laid out in a row of independent `Figure`s rather than in\n * a table with a footer, because a footer row is where a reader\n * expects the sum to be.\n *=========================================*/}\n <Section title=\"What it caused\">\n {conversionsReport.caveats.map((caveat) => (\n <Alert key={caveat.id} severity=\"info\">\n {caveat.message}\n </Alert>\n ))}\n {conversionsReport.any ? (\n <Stack spacing={1}>\n <Stack\n direction=\"row\"\n spacing={4}\n useFlexGap\n sx={{ flexWrap: 'wrap' }}\n >\n {conversionsReport.kinds.map((entry) => (\n <Figure\n key={entry.kind}\n label={entry.label}\n value={entry.value}\n note={entry.note}\n />\n ))}\n </Stack>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Credited ${conversionsReport.model === 'last-click' ? 'to the last campaign whose link the visitor clicked' : `under the ${conversionsReport.model} model`}, ` +\n `within ${conversionsReport.windowDays} days of that click. ` +\n 'Somebody who converted without ever following a campaign ' +\n 'link is credited to no campaign at all — nothing is ' +\n 'inferred from a referrer — so these are a floor rather ' +\n 'than everything this campaign influenced. The ones ' +\n 'credited to nobody are counted under Conversions in the ' +\n 'marketing console.'}\n </Typography>\n {/*\n THE RECORDS BEHIND THE FIGURES. The rollup says how many; the\n list says which, and it is the only place the uncredited half\n is counted. A link rather than a table here — the records are\n a paged read and this page's whole cost model is a fixed\n number of documents whatever the audience.\n */}\n {conversionsHub ? (\n <Box>\n <Button\n component={AppLink as any}\n {...({\n componentVariant: 'naked',\n nativeButton: false,\n } as any)}\n href={`${conversionsHub}/conversions/${campaignId}`}\n size=\"small\"\n color=\"primary\"\n >\n {'See these conversions'}\n </Button>\n </Box>\n ) : null}\n </Stack>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {conversionsReport.recorded\n ? 'Nothing has been credited to this campaign yet.'\n : 'No conversions have been attributed to this campaign. A ' +\n 'form submission, lead, contact or booking is credited to ' +\n 'the last campaign whose link the visitor clicked, within ' +\n `${conversionsReport.windowDays} days — a campaign sent ` +\n 'before that was recorded will never show a figure here.'}\n </Typography>\n )}\n </Section>\n\n <Divider />\n\n {/*==========================================\n * WHAT THIS CAMPAIGN EARNED.\n *\n * The merchant's second question, and the one every compared\n * product answers by reconstructing revenue probabilistically\n * because it does not own the order. Here it is a join: the click\n * and the order are rows in one database, so there is no tracking\n * script, no catalog sync and nothing estimated except the model.\n *\n * THE MODEL IS ON SCREEN, in a sentence, above the money. A revenue\n * figure whose rule the reader cannot state is worse than no\n * figure, because they will use it anyway — and the two things they\n * have to know to use it are which campaign gets the credit when\n * several touched the buyer, and how long a click stays creditable.\n *\n * NET LEADS. Gross and refunded are both shown beside it, because\n * \"this campaign made $4,000\" and \"this campaign made $4,000 of\n * which $3,100 came back\" are different facts about whether to send\n * another one.\n *=========================================*/}\n <Section title=\"Revenue\">\n {revenueReport.caveats.map((caveat) => (\n <Alert key={caveat.id} severity=\"info\">\n {caveat.message}\n </Alert>\n ))}\n {revenueReport.currencies.length ? (\n <Stack spacing={3}>\n {revenueReport.currencies.map((entry) => (\n <Stack key={entry.currency} spacing={1}>\n {/*\n * The currency is a HEADING when there is more than one,\n * and absent when there is one. A per-currency label on a\n * report that only ever shows dollars is noise; the moment\n * there are two, the reader has to be able to see at a\n * glance that the blocks are not addable — which is also\n * what the caveat above says in words.\n */}\n {revenueReport.multiCurrency ? (\n <Typography variant=\"subtitle2\">\n {entry.currency.toUpperCase()}\n </Typography>\n ) : null}\n <Stack\n direction=\"row\"\n spacing={4}\n useFlexGap\n sx={{ flexWrap: 'wrap' }}\n >\n <MoneyFigure\n label=\"Net revenue\"\n cents={entry.netCents}\n currency={entry.currency}\n note=\"after refunds\"\n />\n <MoneyFigure\n label=\"Gross revenue\"\n cents={entry.grossCents}\n currency={entry.currency}\n note=\"as charged\"\n />\n <MoneyFigure\n label=\"Refunded\"\n cents={entry.refundedCents}\n currency={entry.currency}\n note=\"handed back\"\n />\n <Figure\n label=\"Orders\"\n value={entry.orders}\n note=\"credited to this campaign\"\n />\n <Figure\n label=\"Fully refunded\"\n value={entry.refundedOrders}\n note=\"of those orders\"\n />\n </Stack>\n <MoneyPerUnitRow\n label=\"Net revenue per delivered message\"\n figure={entry.netPerDelivered}\n />\n </Stack>\n ))}\n {/*\n * NO CONVERSION RATE, and this is where a reader would look\n * for one. Orders over delivered is not a rate: one buyer can\n * place two orders, so the quotient passes 100% without\n * anything being wrong — the same defect that keeps `opens`\n * out of the open-rate numerator two sections above. Counting\n * distinct buyers instead would need a document per person per\n * campaign, which is the per-recipient read this screen's cost\n * model exists to refuse. The order count is shown as a count.\n */}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Credited ${revenueReport.model === 'last-click' ? 'to the last campaign whose link the buyer clicked' : `under the ${revenueReport.model} model`}, ` +\n `within ${revenueReport.windowDays} days of that click. ` +\n 'Clicks only — an open is not treated as evidence that ' +\n 'anybody read the email. An order placed by somebody who ' +\n 'never clicked, or who checked out without giving an ' +\n 'address, is credited to no campaign, so this is a floor ' +\n 'rather than every sale this campaign influenced.'}\n </Typography>\n </Stack>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {revenueReport.recorded\n ? 'No orders have been credited to this campaign.'\n : 'No revenue has been attributed to this campaign. Orders ' +\n 'are credited to the last campaign whose link the buyer ' +\n `clicked, within ${revenueReport.windowDays} days — a ` +\n 'campaign sent before that was recorded, or one on a site ' +\n 'with no store, will never show a figure here.'}\n </Typography>\n )}\n </Section>\n\n <Divider />\n\n <Section title=\"Where it sent people\">\n {/*\n The heading a campaign of several emails carries over the same\n question, so the two shapes of campaign page read alike. A\n campaign owns no page — this is where its mail was actually\n followed to, which is the only account of it the data holds.\n */}\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'The pages this campaign’s links were followed to.'}\n </Typography>\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 {/*\n * The normalisation, stated rather than left for somebody to\n * discover from a total that does not add up. Two links to the\n * same page distinguished only by their UTM tags are one row\n * here, and a merchant comparing this table with the campaign\n * they built has to be told that before they conclude a link\n * is missing.\n */}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Counted by address and path — query strings are dropped, so ' +\n '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 campaign 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 campaign, 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 campaign.'}\n </Typography>\n )}\n </Section>\n\n <Divider />\n\n {/*==========================================\n * THE EMAIL THE FIGURES BELOW CAME FROM — and the hinge between the\n * two halves of this page.\n *\n * This screen is reached at `/marketing/campaigns/{sendId}`, which\n * is a CAMPAIGN url resolving to a send — the fall-through that keeps\n * every unsubscribe footer and every pasted report link working. So\n * a reader arrives from the campaigns table, where the row carries a\n * \"Single send\" chip, and everything under this row is a fact about\n * a message rather than about the campaign.\n *\n * Figures with no visible source read as a campaign holding no\n * emails while reporting real numbers, which is a contradiction\n * rather than a shortage of detail. Naming the one email — and\n * saying it IS the campaign, which is what the chip already claims\n * and what `campaignListRows` already does at read time — is what\n * makes the page answer the question it raises.\n *\n * One row, from the document this card already read: no query, no\n * second listen, and the two-document cost the header describes is\n * unchanged.\n *=========================================*/}\n <Section title=\"The email in this campaign\">\n <Stack spacing={1}>\n <Stack\n direction=\"row\"\n spacing={1}\n useFlexGap\n sx={{ alignItems: 'center', flexWrap: 'wrap' }}\n >\n {/*\n The MESSAGE's own page, which the Emails console owns — hence\n the sibling hub rather than this surface's `basePath`. Plain\n text until that hub resolves: a link with no destination is\n worse than none, and the subject is what the reader came for\n either way.\n */}\n {messageHref ? (\n <AppLink href={messageHref}>{subject}</AppLink>\n ) : (\n <Typography variant=\"body2\">{subject}</Typography>\n )}\n {/*\n What the email is DOING, not the field it stores. One\n delivering an audience larger than one batch is written back\n as `scheduled` between runs, so a chip rendering the status\n said \"Scheduled\" at the head of a report of five hundred\n deliveries.\n */}\n <Chip\n size=\"small\"\n label={campaignSendDisplay(campaign as never).label}\n />\n <Chip\n size=\"small\"\n variant=\"outlined\"\n label=\"Single send\"\n title={\n 'One email, sent on its own rather than as part of a ' +\n 'campaign of several. Its report and its unsubscribe links ' +\n 'are unchanged.'\n }\n />\n <Typography variant=\"caption\" color=\"text.secondary\">\n {sendTimeMs\n ? new Date(sendTimeMs).toLocaleString()\n : 'not recorded'}\n </Typography>\n </Stack>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'This campaign is one email, so the sections above are what ' +\n 'that one email caused. What follows is the mail itself — ' +\n 'how it was delivered and how it was read. Open the message ' +\n 'for its body and the people it reached.'}\n </Typography>\n </Stack>\n </Section>\n\n <Divider />\n\n {/*==========================================\n * WHAT HAPPENED TO THE MAIL.\n *\n * Counts first and rates second, in that order and not the other\n * way round, because a rate is only readable once you know what it\n * was taken over — and because the counts are the part that is\n * always true. Every rate below can be absent; none of these can.\n *=========================================*/}\n <Section title=\"Delivery\">\n <Stack direction=\"row\" spacing={4} useFlexGap sx={{ flexWrap: 'wrap' }}>\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 {/*\n * BOTH the event count and the distinct count, side by side.\n *\n * `Opens` is every open event — one reader opening four times is\n * four — and it is the number this product has always shown. It\n * cannot be a rate numerator: divided by anything it exceeds 100%\n * the moment somebody reads an email twice. `Readers who opened`\n * is the distinct count, and it is what the rate below divides.\n * Showing only one of them would either hide activity or invite a\n * rate nobody can defend, so both are here with different names.\n */}\n <Stack direction=\"row\" spacing={4} useFlexGap sx={{ flexWrap: 'wrap' }}>\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 campaign'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 went to\">\n {/*\n * The populations the SEND measured, recorded at send time.\n *\n * Not recomputed here, and the note under the heading says so:\n * consent records change and addresses get suppressed, so\n * asking the list today produces a number that is true of the\n * list and false of the campaign.\n */}\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Measured when this campaign 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 </Stack>\n </CardDisplay>\n )\n\n return (\n <>\n {/* The page heading and the trail name the send; this card is\n then free to say what it holds rather than repeating the title. */}\n <PageHeaderRecord\n title={\n campaign ? String(campaign.subject || '') || campaignId : undefined\n }\n />\n {card}\n </>\n )\n}\nCampaignReportCard.displayName = 'CampaignReportCard'\n\nexport default CampaignReportCard\n"],"names":["PageHeaderRecord","pluginDocsHelp","AppLink","CardDisplay","ScrollTable","Alert","Box","Button","Chip","Divider","Stack","TableBody","TableCell","TableHead","TableRow","Typography","useFirestore","useFirestoreDoc","campaignLinkReport","campaignReport","campaignRevenueReport","campaignConversionsReport","Figure","MoneyFigure","MoneyPerUnitRow","percent","RateRow","Section","emailSendTimeMs","campaignSendDisplay","campaignSendProgress","campaignSendDoc","campaignSendReportDoc","orgSiteHubPath","useMarketingOrgId","useMarketingOrgMount","useEmailsHubPath","reportDocsHelp","anchor","excerpt","CampaignReportCard","props","hostId","campaignId","basePath","emailsHub","orgMount","orgId","firestore","data","campaign","status","links","revenue","conversions","messageHref","conversionsHub","String","report","stats","linkReport","revenueReport","rollup","delivered","midFlight","state","conversionsReport","subject","sendTimeMs","backButton","component","componentVariant","nativeButton","href","size","color","header","help","contentGutterX","contentGutterY","HeaderProps","action","variant","card","spacing","caveats","map","caveat","severity","message","id","title","any","direction","useFlexGap","sx","flexWrap","kinds","entry","label","value","note","kind","model","windowDays","recorded","currencies","length","multiCurrency","currency","toUpperCase","cents","netCents","grossCents","refundedCents","orders","refundedOrders","figure","netPerDelivered","rows","align","row","wordBreak","url","fontWeight","clicks","toLocaleString","share","denominator","denominatorLabel","unattributedClicks","overflowClicks","alignItems","Date","recipients","sent","bounced","complained","opens","uniqueOpens","uniqueClicks","unsubscribes","rate","rates","delivery","open","click","clickToOpen","bounce","complaint","unsubscribe","populations","population","count","of","ofLabel","undefined","displayName"],"mappings":"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"}