@aglyn/plugins-marketing 1.0.0-beta.218 → 1.0.0-beta.219

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 (57) hide show
  1. package/README.md +2 -0
  2. package/package.json +15 -15
  3. package/src/lib/analytics-provider.d.ts +177 -0
  4. package/src/lib/analytics-provider.js +370 -0
  5. package/src/lib/analytics-provider.js.map +1 -0
  6. package/src/lib/components/campaign-detail-card.js +16 -16
  7. package/src/lib/components/campaign-detail-card.js.map +1 -1
  8. package/src/lib/components/campaign-members-section.js +11 -11
  9. package/src/lib/components/campaign-members-section.js.map +1 -1
  10. package/src/lib/components/campaign-report-card.js +6 -4
  11. package/src/lib/components/campaign-report-card.js.map +1 -1
  12. package/src/lib/components/campaigns-card.js +1 -1
  13. package/src/lib/components/campaigns-card.js.map +1 -1
  14. package/src/lib/components/conversion-attribution.component.js +1 -1
  15. package/src/lib/components/conversion-attribution.component.js.map +1 -1
  16. package/src/lib/components/email-detail.js +12 -7
  17. package/src/lib/components/email-detail.js.map +1 -1
  18. package/src/lib/components/email-messages-widget.js +1 -1
  19. package/src/lib/components/email-messages-widget.js.map +1 -1
  20. package/src/lib/components/emails-list-card.js +11 -5
  21. package/src/lib/components/emails-list-card.js.map +1 -1
  22. package/src/lib/components/host-experiments-card.component.js +12 -7
  23. package/src/lib/components/host-experiments-card.component.js.map +1 -1
  24. package/src/lib/components/org-experiments-card.js +1 -1
  25. package/src/lib/components/org-experiments-card.js.map +1 -1
  26. package/src/lib/components/record-route-context.d.ts +41 -0
  27. package/src/lib/components/record-route-context.js +63 -0
  28. package/src/lib/components/record-route-context.js.map +1 -0
  29. package/src/lib/components/use-marketing-hub-path.d.ts +18 -5
  30. package/src/lib/components/use-marketing-hub-path.js +36 -6
  31. package/src/lib/components/use-marketing-hub-path.js.map +1 -1
  32. package/src/lib/model/campaign-send-time.d.ts +74 -0
  33. package/src/lib/model/campaign-send-time.js +120 -0
  34. package/src/lib/model/campaign-send-time.js.map +1 -0
  35. package/src/lib/model/experiments.js +1 -1
  36. package/src/lib/model/experiments.js.map +1 -1
  37. package/src/lib/server/campaign-batch-plan.d.ts +73 -0
  38. package/src/lib/server/campaign-batch-plan.js +79 -0
  39. package/src/lib/server/campaign-batch-plan.js.map +1 -0
  40. package/src/lib/server/campaign-send.js +10 -5
  41. package/src/lib/server/campaign-send.js.map +1 -1
  42. package/src/lib/server/email-events.js +7 -6
  43. package/src/lib/server/email-events.js.map +1 -1
  44. package/src/lib/server/list-send-time-facts.d.ts +60 -0
  45. package/src/lib/server/list-send-time-facts.js +108 -0
  46. package/src/lib/server/list-send-time-facts.js.map +1 -0
  47. package/src/lib/server.js +4 -0
  48. package/src/lib/server.js.map +1 -1
  49. package/src/lib/tenant-emails.d.ts +29 -0
  50. package/src/lib/tenant-emails.js +41 -0
  51. package/src/lib/tenant-emails.js.map +1 -0
  52. package/src/lib/testing/stand-in-emails-page-routes.d.ts +27 -0
  53. package/src/lib/testing/stand-in-emails-page-routes.js +42 -0
  54. package/src/lib/testing/stand-in-emails-page-routes.js.map +1 -0
  55. package/src/lib/components/use-emails-hub-path.d.ts +0 -58
  56. package/src/lib/components/use-emails-hub-path.js +0 -109
  57. package/src/lib/components/use-emails-hub-path.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/plugins/marketing/src/lib/analytics-provider.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type {\n AnalyticsEventOptions,\n AnalyticsGrants,\n AnalyticsMountOptions,\n AnalyticsProvider,\n AnalyticsTagMount,\n} from '@aglyn/aglyn/app-utils/analytics-provider'\nimport { analyticsEnvironmentForcesInternal } from '@aglyn/aglyn/app-utils/analytics-environment'\nimport {\n INTERNAL_TRAFFIC_FORCED_SNIPPET,\n INTERNAL_TRAFFIC_GTAG_SNIPPET,\n} from '@aglyn/aglyn/app-utils/internal-traffic'\nimport { PLATFORM_GA_MEASUREMENT_ID } from '@aglyn/aglyn/app-utils/platform-marketing-host'\nimport {\n analyticsConsentSignals,\n consentModeSignals,\n GA_MEASUREMENT_ID_PATTERN,\n resolveGaMeasurementId,\n resolveGtmContainerId,\n type VisitorConsentHost,\n} from '@aglyn/aglyn/app-utils/visitor-consent'\n\n/**\n * The Google tag adapter: Google Analytics 4 and Google Tag Manager on a\n * published site, and the resident Google tag of any document that has one\n * (AGL-3080).\n *\n * Core decides whether a visitor may be measured and names no vendor\n * (`analytics-provider.ts`); this is where Google's shape lives — the inline\n * boot and library each tag needs, Consent Mode, the window flag that\n * silences a resident tag, and how an event reaches `gtag`.\n *\n * Loaded as a chunk of its own, and only where it is used: by a published\n * page whose site configures a measurement id or a container, and by the\n * console, whose own analytics tag the consent controls answer to.\n */\n\n/** The `gtag` a loaded Google tag defines on `window`. */\ntype Gtag = (...args: unknown[]) => void\n\n/** The resident `gtag`, or null when no Google tag is on the page. */\nfunction residentGtag(): Gtag | null {\n if (typeof window === 'undefined') return null\n const gtag = (window as unknown as { gtag?: unknown }).gtag\n return typeof gtag === 'function' ? (gtag as Gtag) : null\n}\n\n/**\n * The `window` flag `gtag.js` consults before every hit: with\n * `window['ga-disable-G-XXXX'] === true` the tag for that property sends\n * nothing and writes nothing. Google's own documented opt-out mechanism, and\n * the one that works on a tag which never received a consent-mode default\n * (AGL-1608) — a case that still exists after AGL-1622, because a tag arriving\n * through GTM or a host's own CMP is not one this adapter declared for.\n */\nexport const GA_DISABLE_FLAG_PREFIX = 'ga-disable-'\n\n/**\n * The consent-mode `default` declaration that goes in front of the tag\n * (AGL-1622), for injection into the same inline block that creates\n * `dataLayer` and calls `gtag('config', …)`.\n *\n * ## What this is, and — load-bearing — what it is NOT\n *\n * It is NOT a licence to load the tag earlier. It is emitted INSIDE the block\n * that only renders when the AGL-1498 gate has already said yes, so it is\n * reached only on a pageview where analytics is granted. On every gated\n * pageview — an EU/UK/EEA visitor with no explicit accept, an unknown region, a\n * declined or opted-out visitor, a GPC browser — nothing here renders, no\n * request goes to `googletagmanager.com`, and this constant is never evaluated\n * into the page. \"The script never LOADS, rather than load-then-suppress\" is a\n * stated AGL-1498 property and it is intact everywhere the gate applies;\n * `consent-mode-default.spec.tsx` fails if that ever stops being true.\n *\n * the decision, 2026-08-14: load-then-restrict is approved for the UNITED\n * STATES, where the implied-consent posture already permits the load and the\n * restriction signals act on a tag that is legitimately resident. EU and UK\n * keep the prior-consent gate exactly as it is, because loading an analytics\n * tag before consent is the specific thing prior-consent law prohibits. So the\n * consent-mode mechanism is strictly ADDITIVE to the gate, never a replacement\n * for it.\n *\n * What declaring it actually buys, given the tag is granted by the time it is\n * read:\n *\n * - The advertising signals are denied FROM THE FIRST HIT. Without a default,\n * a freshly loaded tag runs with `ad_storage` unrestricted and only becomes\n * denied if the visitor happens to withdraw and change their mind again\n * (AGL-1608's `update`). The load-time state now matches the tool's actual\n * scope instead of being wider than it.\n * - A later `update` is a transition from a declared state rather than a tag's\n * first-ever consent signal, which is the shape GA4's consent-mode reporting\n * and any third-party CMP alongside ours both expect.\n *\n * The value is a literal, built from `analyticsConsentSignals` — no\n * interpolated input reaches it, which matters because it lands inside an\n * inline script (the AGL-138 concern).\n */\nexport const GA_CONSENT_DEFAULT_SNIPPET = `gtag('consent', 'default', ${JSON.stringify(\n analyticsConsentSignals(true),\n)});`\n\n/**\n * The same declaration for a visitor who granted ADVERTISING too (AGL-1649).\n *\n * A second CONSTANT rather than a parameterised builder, for the reason the\n * first one is a constant: it lands inside an inline script (the AGL-138\n * concern), and a constant cannot interpolate anything. Both values are\n * literals fixed at module load; the only thing the caller chooses is which\n * of the two to emit.\n *\n * Reached only where the page's `advertising` answer is yes — host opted in,\n * explicit accept, explicit yes to this category — which is evaluated\n * client-side after hydration, like every other part of this gate. The ISR\n * property is unaffected: the cached HTML contains neither snippet, because\n * the whole block is inside the client-side `analyticsAllowed` condition.\n *\n * Declaring the grant as the DEFAULT rather than sending a later `update` is\n * what keeps the tag's first hit correct. An update would arrive after\n * `config`, so the session's first pageview — usually the entire session for\n * a marketing visit — would carry the denied state the visitor did not\n * choose.\n */\nexport const GA_CONSENT_DEFAULT_WITH_ADS_SNIPPET = `gtag('consent', 'default', ${JSON.stringify(\n consentModeSignals({ analytics: true, advertising: true }),\n)});`\n\n/**\n * Consent-mode URL passthrough for the platform's own tag (AGL-2548).\n *\n * The advertising signals in {@link GA_CONSENT_DEFAULT_SNIPPET} are denied, so\n * on `aglyn.com` gtag writes no `_gcl_aw` cookie and the `gclid` an ad click\n * arrives with lives only in that first URL. The conversions it should be\n * credited for fire one hop later, on the console, where the posture grants\n * advertising storage outside the prior-consent regions — and without this\n * declaration the click id never makes that hop. The console reports a\n * conversion with no click behind it, and Google Ads counts nothing.\n *\n * `url_passthrough` is Google's mechanism for exactly this state: while\n * `ad_storage` is denied, gtag appends the click identifiers to links into the\n * cross-domain-linked hosts as URL parameters instead of setting a cookie. The\n * receiving tag reads them under its own consent state. Nothing is stored on\n * the denying surface, so the marketing site's opt-in advertising posture is\n * unchanged; the click id simply survives the link it was always meant to\n * survive.\n *\n * `ads_data_redaction` is the companion Google documents alongside it: with\n * `ad_storage` denied, the ad click identifiers are stripped from the hits this\n * tag sends and its network requests go to a cookieless endpoint. Declared\n * together so a denied visitor is measured with less, not merely differently.\n *\n * Both are `set` calls, so they must precede `gtag('config', …)` in the same\n * inline block — a `set` applies to hits processed after it, and the session's\n * first pageview is the one carrying the click.\n *\n * PLATFORM ID ONLY. A customer's tag is configured with their id and their\n * consent posture; how their ad click ids travel is theirs to decide, and a\n * passthrough declared on their behalf would rewrite links on their site.\n */\nexport const GA_CLICK_ID_PASSTHROUGH_SNIPPET =\n \"gtag('set', 'url_passthrough', true);\" +\n \"gtag('set', 'ads_data_redaction', true);\"\n\n/** The marker `advertising-tags.ts` names in `sharesLibrary` for gtag.js. */\nexport const GTAG_LIBRARY = 'googletagmanager.com/gtag/js'\n\n/** `dataLayer` and the canonical `gtag` shim, the start of every boot. */\nconst GTAG_SHIM =\n 'window.dataLayer=window.dataLayer||[];' +\n 'function gtag(){dataLayer.push(arguments);}'\n\n/**\n * The consent-mode default a boot opens with — none when the host runs its\n * own consent solution (`consentRequired` false): their solution owns the\n * default, and a second one racing it would overwrite their visitor's answer\n * with ours.\n */\nfunction consentDefault(options: AnalyticsMountOptions): string {\n if (!options.consentRequired) return ''\n return options.advertising\n ? GA_CONSENT_DEFAULT_WITH_ADS_SNIPPET\n : GA_CONSENT_DEFAULT_SNIPPET\n}\n\n/**\n * The Google Analytics pair (AGL-138/661): the site's configured measurement\n * id, booted and loaded.\n *\n * Order is load-bearing twice over. The inline block precedes the library, and\n * inside it the consent `default` precedes `config`, so no hit is ever sent\n * before the tag has been told what it may store. Google's canonical snippet\n * shape.\n *\n * ## The platform's own property\n *\n * `aglyn.com` is a site on this platform pointed at the platform's measurement\n * id, and three things are said to THAT property only, because a customer's\n * site configures its own id and their property gets no opinion of ours:\n *\n * - THE INTERNAL-TRAFFIC STAMP (AGL-2064), between the shim and `gtag('js')`.\n * It is a CONSTANT string, evaluated in the browser: these pages are\n * ISR-cached, so a server-side branch on \"is this us\" would bake one\n * browser's answer into the cache for everyone. It runs BEFORE `config`,\n * because the hits that leak are the ones no call site writes —\n * `session_start`, `first_visit`, `user_engagement`, the automatic\n * `page_view` — and a `set` applies only to hits processed after it. Wrongly\n * flagging a real visitor erases them from every report and a GA4 data\n * filter is not retroactive, so the id equality is the guard that keeps the\n * expensive direction unreachable. When the analytics escape hatch\n * deliberately re-enables a non-production build, the stamp is\n * UNCONDITIONAL (AGL-2067): a build that emits because someone asked it to is\n * ours by definition.\n * - THE CLICK-ID PASSTHROUGH (AGL-2548), also a `set` before `config`.\n * - `content_group: 'marketing'` on `config` (AGL-1857): the one-click\n * marketing/docs/console split in GA4 standard reports. The platform's id is\n * the discriminator because same-property IS the definition of \"this is our\n * surface\".\n */\nfunction googleAnalyticsMount(\n measurementId: string,\n options: AnalyticsMountOptions,\n): AnalyticsTagMount {\n const own = measurementId === PLATFORM_GA_MEASUREMENT_ID\n return {\n id: 'ga',\n boot:\n GTAG_SHIM +\n consentDefault(options) +\n (own\n ? analyticsEnvironmentForcesInternal()\n ? INTERNAL_TRAFFIC_FORCED_SNIPPET\n : INTERNAL_TRAFFIC_GTAG_SNIPPET\n : '') +\n (own ? GA_CLICK_ID_PASSTHROUGH_SNIPPET : '') +\n \"gtag('js', new Date());\" +\n (own\n ? `gtag('config', '${measurementId}', {'content_group':'marketing'});`\n : `gtag('config', '${measurementId}');`),\n src: `https://www.googletagmanager.com/gtag/js?id=${measurementId}`,\n library: GTAG_LIBRARY,\n }\n}\n\n/**\n * The Google Tag Manager pair (AGL-2486), under the same gate as GA and never\n * a looser one.\n *\n * A container is not a tag — it is a LOADER, and what it loads is decided in\n * Google's UI by whoever owns it, not here. That is exactly why it cannot have\n * a weaker gate than GA: analytics may run on implied consent outside the\n * EU/EEA/UK while ADVERTISING is opt-in everywhere, and a container is the\n * likeliest thing on a page to carry an advertising tag.\n *\n * CONSENT MODE V2 comes first, in the same script, before `gtm.js` is\n * requested: defaults set after the container has loaded are defaults its\n * tags have already run past. The advertising signals stay denied unless the\n * visitor granted advertising, which is what makes a container holding ad\n * tags safe to load at all.\n *\n * NO `<noscript>` IFRAME, deliberately, and it is the one piece of Google's\n * standard snippet omitted. That iframe fires the container with no\n * JavaScript — so no consent defaults, no gate, nothing to suppress it — and\n * these pages are ISR-cached, so it would sit in shared HTML identical for\n * every visitor and every region. A visitor with JavaScript off gets no\n * container, which is the correct answer.\n */\nfunction tagManagerMount(\n containerId: string,\n options: AnalyticsMountOptions,\n): AnalyticsTagMount {\n return {\n id: 'gtm',\n boot:\n GTAG_SHIM +\n consentDefault(options) +\n \"dataLayer.push({'gtm.start':new Date().getTime(),event:'gtm.js'});\",\n src: `https://www.googletagmanager.com/gtm.js?id=${containerId}`,\n }\n}\n\n/**\n * The measurement ids whose tag is actually RESIDENT in this page.\n *\n * Discovered from the page rather than taken from the host document on\n * purpose: what has to be silenced is whatever gtag.js is currently loaded and\n * configured for, which is not always what the host record configures — a tag\n * can arrive through GTM, the console's is injected by its analytics SDK, and\n * a stale `<script>` from earlier in the pageview is exactly the thing this\n * function exists to find.\n *\n * Ids are format-checked before they are used, because each one becomes a\n * `window` property name.\n */\nexport function residentGaMeasurementIds(): string[] {\n if (typeof document === 'undefined') return []\n const ids = new Set<string>()\n try {\n const loaded = document.querySelectorAll(`script[src*=\"${GTAG_LIBRARY}\"]`)\n for (const element of Array.from(loaded)) {\n const src = String((element as HTMLScriptElement).src ?? '')\n const match = /[?&]id=([^&]*)/.exec(src)\n const id = match ? decodeURIComponent(match[1]) : ''\n if (GA_MEASUREMENT_ID_PATTERN.test(id)) ids.add(id)\n }\n } catch {\n // A hostile or absent DOM: the dataLayer pass below still applies.\n }\n try {\n const layer = (window as unknown as Record<string, unknown>)?.dataLayer\n for (const entry of Array.from((layer ?? []) as ArrayLike<unknown>)) {\n // `arguments` objects, not arrays — gtag pushes its own call sites.\n const args = Array.from((entry ?? []) as ArrayLike<unknown>)\n if (args[0] !== 'config') continue\n const id = String(args[1] ?? '')\n if (GA_MEASUREMENT_ID_PATTERN.test(id)) ids.add(id)\n }\n } catch {\n // No dataLayer, or one that is not iterable: the script pass stands.\n }\n return [...ids]\n}\n\n/**\n * Make every resident Google tag agree with the visitor's answer, and return\n * the measurement ids it acted on.\n *\n * ## Why deleting the cookies is not enough (AGL-1608)\n *\n * The AGL-1498 gate stops the script from LOADING, which is the right\n * enforcement for a fresh pageview and the wrong tool for a visitor who\n * withdraws mid-pageview: `gtag.js` has already executed, and React unmounting\n * the `<script>` element does not unload it. `window.gtag` stays live, GA4\n * enhanced measurement fires on its own (scroll depth, outbound click, file\n * download), and the tag re-writes `_ga_<id>` AFTER the platform's cookie\n * sweep has deleted it. Reproduced on aglyn.com: an opt-out, a hand-run sweep\n * to zero cookies, then one scroll to the footer brought `_ga_YW5PG16YTM`\n * back.\n *\n * Two signals, because they fail differently and neither is guaranteed:\n * the `ga-disable-<id>` window flag silences a tag that never received a\n * consent-mode default, and the `consent`/`update` call reaches a tag that\n * arrived through GTM with its own id this function never saw.\n *\n * ## What this deliberately does NOT do\n *\n * It never touches the gate. Both signals act only on a tag that is already\n * resident — which, by construction, only exists after a grant. The\n * consent-mode DEFAULT ({@link GA_CONSENT_DEFAULT_SNIPPET}) is declared inside\n * the gated block and therefore only on a pageview the gate already allowed.\n *\n * Symmetric on purpose: a visitor who opts out and changes their mind in the\n * same pageview would otherwise stay silently unmeasured until they navigated,\n * because the re-rendered `<Script>` cannot re-execute an already-loaded tag.\n * The re-grant restores exactly what `grants` says, and core clamps\n * advertising to analytics before it arrives, so a withdrawal can never\n * restore advertising alone.\n */\nexport function applyGoogleTagConsent(grants: AnalyticsGrants): string[] {\n if (typeof window === 'undefined') return []\n const granted = grants.analytics === true\n const ids = residentGaMeasurementIds()\n const scope = window as unknown as Record<string, unknown>\n for (const id of ids) {\n scope[`${GA_DISABLE_FLAG_PREFIX}${id}`] = !granted\n }\n try {\n const send = residentGtag()\n if (send) {\n // The same payload builder the load-time `default` is made from\n // (AGL-1622), so the two declarations cannot drift.\n send(\n 'consent',\n 'update',\n consentModeSignals({\n analytics: granted,\n advertising: grants.advertising === true,\n }),\n )\n }\n } catch {\n // A tag that throws on its own consent call is one we cannot silence;\n // the flag above and the cookie sweep still stand.\n }\n return ids\n}\n\n/**\n * Hand one event to the resident tag.\n *\n * ## `measurementOnly`: the measurement properties, not the ads account (AGL-2710)\n *\n * A gtag event with no `send_to` goes to every destination the loader has\n * configured, and on a surface that also runs Google Ads that includes the\n * ads account. Core Web Vitals have nothing to do with advertising: measured\n * on `aglyn.com`, one such event attempted seven requests to\n * `googleads.g.doubleclick.net` and `google.com/{pagead,rmkt,ccm}` — per\n * metric, per pageview, four metrics deep — and Google Ads has no report that\n * reads any of them. Naming the GA4 ids drops all seven and leaves the GA hit\n * exactly as it was.\n *\n * The ids are read at DELIVERY time, from the page: the tag routinely arrives\n * after the first metric, and what has to be addressed is whatever gtag is\n * configured for now — which on a customer site is their own property and not\n * ours.\n *\n * No ids found means no `send_to` and the event goes wherever it went before.\n * That is the fail-open side on purpose: a tag that arrived through GTM with\n * an id this reader never saw would otherwise be handed an empty destination\n * list, and losing the measurement is a worse outcome than an ads account\n * receiving a metric it ignores.\n */\nfunction sendGoogleTagEvent(\n name: string,\n params: Record<string, unknown>,\n options?: AnalyticsEventOptions,\n): void {\n const gtag = residentGtag()\n if (!gtag) return\n try {\n if (!options?.measurementOnly) {\n gtag('event', name, params)\n return\n }\n const destinations = residentGaMeasurementIds()\n gtag(\n 'event',\n name,\n destinations.length ? { ...params, send_to: destinations } : params,\n )\n } catch {\n // Analytics never breaks the page.\n }\n}\n\n/** The adapter core registers from each app's generated manifest. */\nexport const analyticsProvider: AnalyticsProvider = {\n mounts(host: VisitorConsentHost | null | undefined, options) {\n const mounts: AnalyticsTagMount[] = []\n const measurementId = resolveGaMeasurementId(host)\n if (measurementId) mounts.push(googleAnalyticsMount(measurementId, options))\n const containerId = resolveGtmContainerId(host)\n if (containerId) mounts.push(tagManagerMount(containerId, options))\n return mounts\n },\n applyConsent: applyGoogleTagConsent,\n resident: () => residentGtag() !== null,\n sendEvent: sendGoogleTagEvent,\n}\n"],"names":["analyticsEnvironmentForcesInternal","INTERNAL_TRAFFIC_FORCED_SNIPPET","INTERNAL_TRAFFIC_GTAG_SNIPPET","PLATFORM_GA_MEASUREMENT_ID","analyticsConsentSignals","consentModeSignals","GA_MEASUREMENT_ID_PATTERN","resolveGaMeasurementId","resolveGtmContainerId","residentGtag","window","gtag","GA_DISABLE_FLAG_PREFIX","GA_CONSENT_DEFAULT_SNIPPET","JSON","stringify","GA_CONSENT_DEFAULT_WITH_ADS_SNIPPET","analytics","advertising","GA_CLICK_ID_PASSTHROUGH_SNIPPET","GTAG_LIBRARY","GTAG_SHIM","consentDefault","options","consentRequired","googleAnalyticsMount","measurementId","own","id","boot","src","library","tagManagerMount","containerId","residentGaMeasurementIds","document","ids","Set","loaded","querySelectorAll","element","Array","from","String","match","exec","decodeURIComponent","test","add","layer","dataLayer","entry","args","applyGoogleTagConsent","grants","granted","scope","send","sendGoogleTagEvent","name","params","measurementOnly","destinations","length","send_to","analyticsProvider","mounts","host","push","applyConsent","resident","sendEvent"],"mappings":"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"}
@@ -16,6 +16,7 @@
16
16
  */ 'use client';
17
17
  import { _ as _extends } from "@swc/helpers/_/_extends";
18
18
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
19
+ import { pluginRecordHref } from "@aglyn/aglyn/plugin-manager/plugin-record-routes";
19
20
  import { mdiDeleteOutline, mdiEyeOutline, mdiPaletteOutline, mdiPencilOutline } from "@aglyn/shared-data-mdi";
20
21
  import { AppLink, CardDisplay, MdiIcon, useConfirmationContext } from "@aglyn/shared-ui-jsx";
21
22
  import { Figure, RateRow, Section } from "@aglyn/shared-ui-jsx/components/measured-figures.component";
@@ -47,7 +48,7 @@ import CampaignReportCard from "./campaign-report-card.js";
47
48
  import { campaignContainerDoc, campaignSendsCollection, campaignSendsQuery } from "./campaign-queries.js";
48
49
  import { orgSiteHubPath, orgSiteName, orgSiteOptions, topicCatalogHostId, useMarketingOrgId, useMarketingOrgMount } from "./marketing-org-mount.js";
49
50
  import { useCampaignManageApi } from "./use-campaign-send-api.js";
50
- import { useEmailsHubPath } from "./use-emails-hub-path.js";
51
+ import { siteRecordRouteContext, useRecordRouteContext } from "./record-route-context.js";
51
52
  import { useCampaignTopicOptions } from "./use-campaign-topic-options.js";
52
53
  /** How many of a campaign's emails the detail page enumerates. */ const CAMPAIGN_EMAIL_CEILING = 50;
53
54
  const detailDocsHelp = pluginDocsHelp('emailCampaigns', {
@@ -137,9 +138,9 @@ const SEND_FILTER_HEADERS = {
137
138
  var _placedSites_;
138
139
  const { hostId, campaignId, basePath } = props;
139
140
  const firestore = useFirestore();
140
- // The sibling hub, for the two records this page links to but does not own:
141
- // each message's report and the template it was built from.
142
- const emailsHub = useEmailsHubPath();
141
+ // The scope this page asks the owner of two records it links to but does
142
+ // not own: each message's report and the template it was built from.
143
+ const routeContext = useRecordRouteContext();
143
144
  const router = useRouter();
144
145
  const orgMount = useMarketingOrgMount();
145
146
  const { orgId } = useMarketingOrgId(hostId);
@@ -366,7 +367,7 @@ const SEND_FILTER_HEADERS = {
366
367
  */ const unfinished = rollup.scheduled + rollup.sending;
367
368
  const agreed = await confirm({
368
369
  title: 'Delete this campaign?',
369
- description: (sends.length ? `The ${sends.length.toLocaleString()} ` + `${sends.length === 1 ? 'email' : 'emails'} in it are kept — ` + 'each keeps its own report and its unsubscribe links, and they ' + 'appear in the campaigns list as single sends. ' : 'It holds no emails. ') + (unfinished ? `${unfinished.toLocaleString()} of them ${unfinished === 1 ? 'is' : 'are'} still going out or still due, and deleting the campaign does ` + 'not stop that — the sender picks each one up from its own ' + 'record. Stop a send from its own page. ' : '') + 'Any screens, forms and contacts assigned to it come out of it and ' + 'are otherwise untouched. Only the campaign itself goes.',
370
+ description: (sends.length ? `The ${sends.length.toLocaleString()} ` + `${sends.length === 1 ? 'email' : 'emails'} in it are kept — ` + 'each keeps its own report and its unsubscribe links, and they ' + 'appear in the campaigns list as single sends. ' : 'It holds no emails. ') + (unfinished ? `${unfinished.toLocaleString()} of them ${unfinished === 1 ? 'is' : 'are'} still going out or still due, and deleting the campaign does ` + 'not stop that — the sender picks each one up from its own ' + 'record. Stop a send from its own page. ' : '') + 'Any pages, forms and contacts assigned to it come out of it and ' + 'are otherwise untouched. Only the campaign itself goes.',
370
371
  confirmationText: 'Delete campaign'
371
372
  }).then(()=>true).catch(()=>false);
372
373
  if (!agreed) return;
@@ -453,19 +454,18 @@ const SEND_FILTER_HEADERS = {
453
454
  *=========================================*/ /*
454
455
  * A message opens on the Emails page: the site's under a site, and over the
455
456
  * org the organization's own, which lists every site's messages.
456
- */ const messagesPath = emailsHub ? `${emailsHub}/messages` : null;
457
- const sendHref = (send)=>messagesPath ? `${messagesPath}/${send.$id}` : undefined;
458
- /** A template is a site's design, so it opens on the site the send used. */ const templatesPath = (send)=>{
459
- const hub = orgMount ? orgSiteHubPath(orgMount, send.hostId, 'emails') : emailsHub;
460
- return hub ? `${hub}/templates` : null;
457
+ */ const sendHref = (send)=>routeContext && pluginRecordHref('emailMessage', routeContext, send.$id) || undefined;
458
+ /** A template is a site's design, so it opens on the site the send used. */ const templateHref = (send, templateScreenId)=>{
459
+ const siteContext = siteRecordRouteContext(routeContext, orgMount, send.hostId);
460
+ return siteContext ? pluginRecordHref('emailTemplate', siteContext, templateScreenId) : null;
461
461
  };
462
462
  /**
463
463
  * What one of this campaign's emails can be opened into.
464
464
  *
465
465
  * The same two the Emails hub offers, less the campaign — this page IS the
466
- * campaign. Both destinations are records the Emails console owns, so they
467
- * are built from {@link useEmailsHubPath} rather than from this surface's
468
- * own `basePath`.
466
+ * campaign. Both destinations are records the Emails page owns, so they
467
+ * are asked of its record routes ({@link useRecordRouteContext}) rather than
468
+ * built from this surface's own `basePath`.
469
469
  *
470
470
  * A message composed inline was built from no template, so that entry is
471
471
  * shown DISABLED with the reason rather than hidden: an absent control and
@@ -473,7 +473,7 @@ const SEND_FILTER_HEADERS = {
473
473
  */ const sendActions = (send)=>{
474
474
  var _send_templateScreenId;
475
475
  const templateScreenId = String((_send_templateScreenId = send.templateScreenId) != null ? _send_templateScreenId : '');
476
- const templates = templatesPath(send);
476
+ const templates = templateScreenId ? templateHref(send, templateScreenId) : null;
477
477
  return [
478
478
  {
479
479
  key: 'details',
@@ -493,8 +493,8 @@ const SEND_FILTER_HEADERS = {
493
493
  path: mdiPaletteOutline.path,
494
494
  size: 0.8
495
495
  }),
496
- href: templateScreenId && templates ? `${templates}/${templateScreenId}` : undefined,
497
- disabled: !templateScreenId || !templates,
496
+ href: templates != null ? templates : undefined,
497
+ disabled: !templates,
498
498
  disabledReason: templateScreenId ? 'This site’s console URL has not resolved yet' : 'This message was not built from a template'
499
499
  }
500
500
  ];
@@ -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 ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport ListQueryNotices, {\n listQueryRefusals,\n} from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport {\n ListRowActions,\n ListTable,\n listActionsColumn,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport { hiddenFilterVisibility } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport { listFilterGridColumns } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\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 { useListQuery } from '@aglyn/tenant-feature-instance/hooks/use-list-query'\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 campaignForcedListUnsubscribeSends,\n campaignListUnsubscribe,\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 {\n CAMPAIGN_EMAILS_QUERY,\n CAMPAIGN_SEND_STATUS_OPTIONS,\n campaignContainerSearchFields,\n campaignSendsScope,\n} from '../model/campaign-list-query'\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 {\n campaignContainerDoc,\n campaignSendsCollection,\n campaignSendsQuery,\n} 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\n/*\n * What the campaign's emails table filters by, each clause and the search on\n * the table's own Firestore query (AGL-3321, `CAMPAIGN_EMAILS_QUERY`). The\n * State column is derived from an email's counters and cannot be asked of a\n * query, so the panel offers the Status it stores.\n */\nconst SEND_FILTER_FIELDS = CAMPAIGN_EMAILS_QUERY.fields\nconst SEND_FILTER_OPTIONS = { status: CAMPAIGN_SEND_STATUS_OPTIONS }\nconst SEND_FILTER_HEADERS: Readonly<Record<string, string>> = {\n subject: 'Subject',\n status: 'Status',\n createdAtMs: 'Created',\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 /*\n * THE EMAILS TABLE IS ITS OWN QUERY (AGL-3321).\n *\n * The figures above are a sum over the window read above; the table under\n * them is a list a reader filters and searches, so each clause and the\n * search word go onto a query of their own — this campaign's emails\n * (`emailCampaignId == id`, and under a site the ones sent as it), newest\n * first, paged by the query. A match on the fifth page is found; nothing\n * is matched over the rows a read happened to hold. Gated on the container\n * like the window: a single send's report reads none of it.\n */\n const gridFilter = useListGridFilter({ selectFields: ['status'] })\n const sendPage = useListQuery<CampaignSend>({\n collection: campaign ? campaignSendsCollection(firestore, orgId) : null,\n declaration: CAMPAIGN_EMAILS_QUERY,\n request: {\n clauses: gridFilter.clauses,\n search: gridFilter.searchWords,\n base: [\n { path: 'emailCampaignId', op: '==', value: campaignId },\n ...campaignSendsScope(hostId),\n ],\n },\n deps: [firestore, orgId, hostId, campaignId, Boolean(campaign)],\n idField: '$id',\n })\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 const placement =\n !hostId && values.siteIds !== undefined\n ? { visibleTo: campaignVisibleTo(values.siteIds) }\n : {}\n await updateDoc(campaignContainerDoc(firestore, orgId, campaignId), {\n name: values.name,\n // The list's search keys (AGL-3321), from the name written here.\n ...campaignContainerSearchFields(values.name),\n startAtMs: values.startAtMs,\n endAtMs: values.endAtMs,\n listIds: values.listIds,\n topicId: values.topicId ? values.topicId : deleteField(),\n listUnsubscribe: values.listUnsubscribe,\n ...placement,\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 * A page of the table's own query, paged by that query.\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'\n ? 'info'\n : state === 'stopped' || state === 'held'\n ? 'warning'\n : 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 /*\n * The mail-client unsubscribe button (AGL-3307): the campaign's setting,\n * and the emails whose header the bulk guard turned back on while it was\n * off — read off the sends this page already holds, each of which records\n * what it did and why.\n */\n const listUnsubscribeOn = campaignListUnsubscribe(campaign)\n const forcedSends = campaignForcedListUnsubscribeSends(sends)\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 <Typography variant=\"body2\" color=\"text.secondary\">\n {listUnsubscribeOn\n ? 'Mail-client unsubscribe button: on'\n : 'Mail-client unsubscribe button: off — the footer link is ' +\n 'the way out, and the button comes back for any email that ' +\n 'would make your organization a bulk sender'}\n </Typography>\n {forcedSends.length ? (\n <Alert severity=\"info\">\n {`The mail-client unsubscribe button was turned back on for ` +\n `${forcedSends.length === 1 ? 'one email' : `${forcedSends.length.toLocaleString()} emails`} ` +\n 'in this campaign, though the campaign has it off. '}\n {forcedSends\n .map(\n (send) =>\n `${send.subject || send.$id}: ` +\n (send.listUnsubscribe?.detail ||\n 'turned back on by the bulk-sender guard.'),\n )\n .join(' ')}\n </Alert>\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 <ListFilterChips\n fields={SEND_FILTER_FIELDS}\n headers={SEND_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={SEND_FILTER_OPTIONS}\n />\n <ListQueryNotices\n refused={listQueryRefusals(sendPage.plan.refused, {\n fields: SEND_FILTER_FIELDS,\n headers: SEND_FILTER_HEADERS,\n options: SEND_FILTER_OPTIONS,\n })}\n notices={sendPage.plan.notices}\n />\n <ListTable\n aria-label=\"The campaign's emails\"\n rows={sendPage.rows}\n columns={listFilterGridColumns(\n sendColumns as GridColDef[],\n SEND_FILTER_FIELDS,\n SEND_FILTER_OPTIONS,\n SEND_FILTER_HEADERS,\n )}\n rowHeight={TABLE_ROW_HEIGHT}\n onOpen={(_id, send) => {\n const href = sendHref(send)\n if (href) router.push(href)\n }}\n loading={sendPage.status === 'loading'}\n /*\n * The panel and the search are the grid's; every clause and the\n * search word are on the table's query (AGL-3321), and the grid\n * neither filters nor sorts the page it is handed.\n */\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n disableColumnSorting\n hideFooter\n initialState={{\n columns: {\n columnVisibilityModel: hiddenFilterVisibility(\n SEND_FILTER_FIELDS,\n sendColumns.map((column) => String(column.field)),\n ),\n },\n }}\n noRowsLabel=\"No emails match these filters\"\n />\n <ListPagination\n page={sendPage.page}\n pageSize={sendPage.pageSize}\n rowCount={sendPage.rows.length}\n hasMore={sendPage.hasMore}\n onPageChange={sendPage.setPage}\n onPageSizeChange={sendPage.setPageSize}\n />\n {sendsTruncated ? (\n <Alert severity=\"info\">\n {`The figures above cover ${CAMPAIGN_EMAIL_CEILING} of this ` +\n 'campaign’s emails. It has sent more; the table lists ' +\n 'every one of them.'}\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","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListRowActions","ListTable","listActionsColumn","hiddenFilterVisibility","listFilterGridColumns","useListGridFilter","RowActionsMenu","TABLE_ROW_HEIGHT","ceilingedWindow","useListQuery","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","campaignForcedListUnsubscribeSends","campaignListUnsubscribe","campaignRollup","campaignSendDisplay","campaignSiteIds","campaignVisibleTo","campaignWindowState","emailListTimeMs","CAMPAIGN_EMAILS_QUERY","CAMPAIGN_SEND_STATUS_OPTIONS","campaignContainerSearchFields","campaignSendsScope","CampaignComposer","CampaignEditDrawer","CampaignConversionsSection","CampaignDestinationsSection","CampaignRevenueSection","CampaignSequencesSection","CampaignMembersSection","CampaignReportCard","campaignContainerDoc","campaignSendsCollection","campaignSendsQuery","orgSiteHubPath","orgSiteName","orgSiteOptions","topicCatalogHostId","useMarketingOrgId","useMarketingOrgMount","useCampaignManageApi","useEmailsHubPath","useCampaignTopicOptions","CAMPAIGN_EMAIL_CEILING","detailDocsHelp","anchor","excerpt","rolled","value","note","recorded","sends","SEND_FILTER_FIELDS","fields","SEND_FILTER_OPTIONS","status","SEND_FILTER_HEADERS","subject","createdAtMs","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","sendDocs","Boolean","idField","listDocs","topics","source","topicSource","enabled","topicOptions","map","topic","id","label","name","rows","readSends","truncated","sendsTruncated","sort","a","b","rollup","gridFilter","selectFields","sendPage","declaration","request","clauses","search","searchWords","base","path","op","deps","sendIds","send","String","$id","filter","handleSave","values","placement","siteIds","undefined","visibleTo","startAtMs","endAtMs","listIds","topicId","listUnsubscribe","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","size","href","disabled","disabledReason","sendColumns","field","headerName","flex","minWidth","valueGetter","_value","renderCell","row","onClick","event","stopPropagation","width","display","state","sendAtMs","Date","color","type","align","headerAlign","Number","stats","sent","recipients","opens","valueFormatter","clicks","items","windowState","now","listUnsubscribeOn","forcedSends","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","severity","detail","select","onChange","target","maxWidth","subdomain","siteName","flexWrap","addressed","delivered","unsubscribes","rate","openRate","clickRate","unsubscribeRate","justifyContent","open","headers","setClauses","options","refused","plan","notices","aria-label","columns","rowHeight","onOpen","_id","loading","filterMode","filterModel","onFilterModelChange","quickFilter","disableColumnSorting","hideFooter","initialState","columnVisibilityModel","column","noRowsLabel","page","pageSize","rowCount","hasMore","onPageChange","setPage","onPageSizeChange","setPageSize","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 { pluginRecordHref } from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\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 ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport ListQueryNotices, {\n listQueryRefusals,\n} from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport {\n ListRowActions,\n ListTable,\n listActionsColumn,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport { hiddenFilterVisibility } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport { listFilterGridColumns } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\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 { useListQuery } from '@aglyn/tenant-feature-instance/hooks/use-list-query'\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 campaignForcedListUnsubscribeSends,\n campaignListUnsubscribe,\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 {\n CAMPAIGN_EMAILS_QUERY,\n CAMPAIGN_SEND_STATUS_OPTIONS,\n campaignContainerSearchFields,\n campaignSendsScope,\n} from '../model/campaign-list-query'\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 {\n campaignContainerDoc,\n campaignSendsCollection,\n campaignSendsQuery,\n} 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 { siteRecordRouteContext, useRecordRouteContext } from './record-route-context'\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\n/*\n * What the campaign's emails table filters by, each clause and the search on\n * the table's own Firestore query (AGL-3321, `CAMPAIGN_EMAILS_QUERY`). The\n * State column is derived from an email's counters and cannot be asked of a\n * query, so the panel offers the Status it stores.\n */\nconst SEND_FILTER_FIELDS = CAMPAIGN_EMAILS_QUERY.fields\nconst SEND_FILTER_OPTIONS = { status: CAMPAIGN_SEND_STATUS_OPTIONS }\nconst SEND_FILTER_HEADERS: Readonly<Record<string, string>> = {\n subject: 'Subject',\n status: 'Status',\n createdAtMs: 'Created',\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 scope this page asks the owner of two records it links to but does\n // not own: each message's report and the template it was built from.\n const routeContext = useRecordRouteContext()\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 /*\n * THE EMAILS TABLE IS ITS OWN QUERY (AGL-3321).\n *\n * The figures above are a sum over the window read above; the table under\n * them is a list a reader filters and searches, so each clause and the\n * search word go onto a query of their own — this campaign's emails\n * (`emailCampaignId == id`, and under a site the ones sent as it), newest\n * first, paged by the query. A match on the fifth page is found; nothing\n * is matched over the rows a read happened to hold. Gated on the container\n * like the window: a single send's report reads none of it.\n */\n const gridFilter = useListGridFilter({ selectFields: ['status'] })\n const sendPage = useListQuery<CampaignSend>({\n collection: campaign ? campaignSendsCollection(firestore, orgId) : null,\n declaration: CAMPAIGN_EMAILS_QUERY,\n request: {\n clauses: gridFilter.clauses,\n search: gridFilter.searchWords,\n base: [\n { path: 'emailCampaignId', op: '==', value: campaignId },\n ...campaignSendsScope(hostId),\n ],\n },\n deps: [firestore, orgId, hostId, campaignId, Boolean(campaign)],\n idField: '$id',\n })\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 const placement =\n !hostId && values.siteIds !== undefined\n ? { visibleTo: campaignVisibleTo(values.siteIds) }\n : {}\n await updateDoc(campaignContainerDoc(firestore, orgId, campaignId), {\n name: values.name,\n // The list's search keys (AGL-3321), from the name written here.\n ...campaignContainerSearchFields(values.name),\n startAtMs: values.startAtMs,\n endAtMs: values.endAtMs,\n listIds: values.listIds,\n topicId: values.topicId ? values.topicId : deleteField(),\n listUnsubscribe: values.listUnsubscribe,\n ...placement,\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 pages, 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 sendHref = (send: CampaignSend) =>\n (routeContext && pluginRecordHref('emailMessage', routeContext, send.$id)) || undefined\n /** A template is a site's design, so it opens on the site the send used. */\n const templateHref = (send: CampaignSend, templateScreenId: string): string | null => {\n const siteContext = siteRecordRouteContext(routeContext, orgMount, send.hostId)\n return siteContext ? pluginRecordHref('emailTemplate', siteContext, templateScreenId) : 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 page owns, so they\n * are asked of its record routes ({@link useRecordRouteContext}) rather than\n * built from this surface's 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 = templateScreenId ? templateHref(send, templateScreenId) : null\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: templates ?? undefined,\n disabled: !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 * A page of the table's own query, paged by that query.\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'\n ? 'info'\n : state === 'stopped' || state === 'held'\n ? 'warning'\n : 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 /*\n * The mail-client unsubscribe button (AGL-3307): the campaign's setting,\n * and the emails whose header the bulk guard turned back on while it was\n * off — read off the sends this page already holds, each of which records\n * what it did and why.\n */\n const listUnsubscribeOn = campaignListUnsubscribe(campaign)\n const forcedSends = campaignForcedListUnsubscribeSends(sends)\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 <Typography variant=\"body2\" color=\"text.secondary\">\n {listUnsubscribeOn\n ? 'Mail-client unsubscribe button: on'\n : 'Mail-client unsubscribe button: off — the footer link is ' +\n 'the way out, and the button comes back for any email that ' +\n 'would make your organization a bulk sender'}\n </Typography>\n {forcedSends.length ? (\n <Alert severity=\"info\">\n {`The mail-client unsubscribe button was turned back on for ` +\n `${forcedSends.length === 1 ? 'one email' : `${forcedSends.length.toLocaleString()} emails`} ` +\n 'in this campaign, though the campaign has it off. '}\n {forcedSends\n .map(\n (send) =>\n `${send.subject || send.$id}: ` +\n (send.listUnsubscribe?.detail ||\n 'turned back on by the bulk-sender guard.'),\n )\n .join(' ')}\n </Alert>\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 <ListFilterChips\n fields={SEND_FILTER_FIELDS}\n headers={SEND_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={SEND_FILTER_OPTIONS}\n />\n <ListQueryNotices\n refused={listQueryRefusals(sendPage.plan.refused, {\n fields: SEND_FILTER_FIELDS,\n headers: SEND_FILTER_HEADERS,\n options: SEND_FILTER_OPTIONS,\n })}\n notices={sendPage.plan.notices}\n />\n <ListTable\n aria-label=\"The campaign's emails\"\n rows={sendPage.rows}\n columns={listFilterGridColumns(\n sendColumns as GridColDef[],\n SEND_FILTER_FIELDS,\n SEND_FILTER_OPTIONS,\n SEND_FILTER_HEADERS,\n )}\n rowHeight={TABLE_ROW_HEIGHT}\n onOpen={(_id, send) => {\n const href = sendHref(send)\n if (href) router.push(href)\n }}\n loading={sendPage.status === 'loading'}\n /*\n * The panel and the search are the grid's; every clause and the\n * search word are on the table's query (AGL-3321), and the grid\n * neither filters nor sorts the page it is handed.\n */\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n disableColumnSorting\n hideFooter\n initialState={{\n columns: {\n columnVisibilityModel: hiddenFilterVisibility(\n SEND_FILTER_FIELDS,\n sendColumns.map((column) => String(column.field)),\n ),\n },\n }}\n noRowsLabel=\"No emails match these filters\"\n />\n <ListPagination\n page={sendPage.page}\n pageSize={sendPage.pageSize}\n rowCount={sendPage.rows.length}\n hasMore={sendPage.hasMore}\n onPageChange={sendPage.setPage}\n onPageSizeChange={sendPage.setPageSize}\n />\n {sendsTruncated ? (\n <Alert severity=\"info\">\n {`The figures above cover ${CAMPAIGN_EMAIL_CEILING} of this ` +\n 'campaign’s emails. It has sent more; the table lists ' +\n 'every one of them.'}\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":["pluginRecordHref","mdiDeleteOutline","mdiEyeOutline","mdiPaletteOutline","mdiPencilOutline","AppLink","CardDisplay","MdiIcon","useConfirmationContext","Figure","RateRow","Section","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListRowActions","ListTable","listActionsColumn","hiddenFilterVisibility","listFilterGridColumns","useListGridFilter","RowActionsMenu","TABLE_ROW_HEIGHT","ceilingedWindow","useListQuery","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","campaignForcedListUnsubscribeSends","campaignListUnsubscribe","campaignRollup","campaignSendDisplay","campaignSiteIds","campaignVisibleTo","campaignWindowState","emailListTimeMs","CAMPAIGN_EMAILS_QUERY","CAMPAIGN_SEND_STATUS_OPTIONS","campaignContainerSearchFields","campaignSendsScope","CampaignComposer","CampaignEditDrawer","CampaignConversionsSection","CampaignDestinationsSection","CampaignRevenueSection","CampaignSequencesSection","CampaignMembersSection","CampaignReportCard","campaignContainerDoc","campaignSendsCollection","campaignSendsQuery","orgSiteHubPath","orgSiteName","orgSiteOptions","topicCatalogHostId","useMarketingOrgId","useMarketingOrgMount","useCampaignManageApi","siteRecordRouteContext","useRecordRouteContext","useCampaignTopicOptions","CAMPAIGN_EMAIL_CEILING","detailDocsHelp","anchor","excerpt","rolled","value","note","recorded","sends","SEND_FILTER_FIELDS","fields","SEND_FILTER_OPTIONS","status","SEND_FILTER_HEADERS","subject","createdAtMs","CampaignDetailCard","props","campaign","placedSites","hostId","campaignId","basePath","firestore","routeContext","router","orgMount","orgId","confirm","enqueueSnackbar","manageApi","composeHostId","setComposeHostId","conversionsHostId","setConversionsHostId","composing","setComposing","editing","setEditing","saving","setSaving","saveError","setSaveError","deleting","setDeleting","data","sendDocs","Boolean","idField","listDocs","topics","source","topicSource","enabled","topicOptions","map","topic","id","label","name","rows","readSends","truncated","sendsTruncated","sort","a","b","rollup","gridFilter","selectFields","sendPage","declaration","request","clauses","search","searchWords","base","path","op","deps","sendIds","send","String","$id","filter","handleSave","values","placement","siteIds","undefined","visibleTo","startAtMs","endAtMs","listIds","topicId","listUnsubscribe","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","sendHref","templateHref","templateScreenId","siteContext","sendActions","templates","key","icon","size","href","disabled","disabledReason","sendColumns","field","headerName","flex","minWidth","valueGetter","_value","renderCell","row","onClick","event","stopPropagation","width","display","state","sendAtMs","Date","color","type","align","headerAlign","Number","stats","sent","recipients","opens","valueFormatter","clicks","items","windowState","now","listUnsubscribeOn","forcedSends","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","severity","detail","select","onChange","target","maxWidth","subdomain","siteName","flexWrap","addressed","delivered","unsubscribes","rate","openRate","clickRate","unsubscribeRate","justifyContent","open","headers","setClauses","options","refused","plan","notices","aria-label","columns","rowHeight","onOpen","_id","loading","filterMode","filterModel","onFilterModelChange","quickFilter","disableColumnSorting","hideFooter","initialState","columnVisibilityModel","column","noRowsLabel","page","pageSize","rowCount","hasMore","onPageChange","setPage","onPageSizeChange","setPageSize","mb","helperText","fullWidth","emailCampaignId","campaignListIds","campaignTopicId","onSent","onClose","lists","sites","busy","onSubmit","displayName"],"mappings":"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"}
@@ -15,6 +15,7 @@
15
15
  * limitations under the License.
16
16
  */ 'use client';
17
17
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
18
+ import { pluginRecordHref, pluginRecordListHref } from "@aglyn/aglyn/plugin-manager/plugin-record-routes";
18
19
  import { AppLink } from "@aglyn/shared-ui-jsx";
19
20
  import { Figure, Section } from "@aglyn/shared-ui-jsx/components/measured-figures.component";
20
21
  import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
@@ -170,7 +171,7 @@ import { CAMPAIGN_MEMBERS_QUERY, CAMPAIGN_MEMBERS_SEARCH_NOTICE, campaignMembers
170
171
  screenId: id,
171
172
  versionId
172
173
  }) : null,
173
- hrefReason: versionId ? 'This site’s console URL has not resolved yet' : 'This screen has no saved version yet',
174
+ hrefReason: versionId ? 'This site’s console URL has not resolved yet' : 'This page has no saved version yet',
174
175
  campaigns: readCampaignIds(row).length
175
176
  };
176
177
  }, [
@@ -186,11 +187,10 @@ import { CAMPAIGN_MEMBERS_QUERY, CAMPAIGN_MEMBERS_SEARCH_NOTICE, campaignMembers
186
187
  return {
187
188
  id,
188
189
  name: String((_row_displayName = row['displayName']) != null ? _row_displayName : id),
189
- href: orgSlug && host ? buildRoute(Route.FORM_DETAILS, {
190
+ href: orgSlug && host ? pluginRecordHref('form', {
190
191
  orgSlug,
191
- host,
192
- formId: id
193
- }) : null,
192
+ host
193
+ }, id) : null,
194
194
  hrefReason: 'This site’s console URL has not resolved yet',
195
195
  totals: campaignFormTotals(row['stats'], range),
196
196
  campaigns: readCampaignIds(row).length
@@ -218,7 +218,7 @@ import { CAMPAIGN_MEMBERS_QUERY, CAMPAIGN_MEMBERS_SEARCH_NOTICE, campaignMembers
218
218
  orgSlug,
219
219
  host
220
220
  }) : null;
221
- const contactsHref = orgSlug && host ? buildRoute(Route.HOST_CONTACTS, {
221
+ const contactsHref = orgSlug && host ? pluginRecordListHref('contact', {
222
222
  orgSlug,
223
223
  host
224
224
  }) : null;
@@ -233,8 +233,8 @@ import { CAMPAIGN_MEMBERS_QUERY, CAMPAIGN_MEMBERS_SEARCH_NOTICE, campaignMembers
233
233
  children: 'The pages and forms somebody put in this campaign. Assignment is ' + 'a grouping — what the campaign was credited with is measured ' + 'above, from the links its emails carried.'
234
234
  }),
235
235
  /*#__PURE__*/ _jsx(MemberTable, {
236
- heading: "Screens",
237
- noun: "screen",
236
+ heading: "Pages",
237
+ noun: "page",
238
238
  collectionName: "screens",
239
239
  hostId: hostId,
240
240
  campaignId: campaignId,
@@ -244,11 +244,11 @@ import { CAMPAIGN_MEMBERS_QUERY, CAMPAIGN_MEMBERS_SEARCH_NOTICE, campaignMembers
244
244
  variant: "caption",
245
245
  color: "text.secondary",
246
246
  children: [
247
- 'Page views are not shown here. A screen keeps no running total ' + 'on its own record — traffic is stored a day at a time — so ' + 'counting it would be a fresh read of the site’s history every ' + 'time this page opens. ',
247
+ 'Page views are not shown here. A page keeps no running total ' + 'on its own record — traffic is stored a day at a time — so ' + 'counting it would be a fresh read of the site’s history every ' + 'time this view opens. ',
248
248
  analyticsHref ? /*#__PURE__*/ _jsx(AppLink, {
249
249
  href: analyticsHref,
250
- children: 'The site’s analytics measures it by screen.'
251
- }) : 'The site’s analytics page measures it by screen.'
250
+ children: 'The site’s analytics measures it by page.'
251
+ }) : 'The site’s analytics view measures it by page.'
252
252
  ]
253
253
  }),
254
254
  /*#__PURE__*/ _jsx(MemberTable, {