@aglyn/plugins-marketing 1.0.0-beta.222 → 1.0.0-beta.223
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +15 -15
- package/src/lib/components/campaign-detail-card.js +17 -3
- package/src/lib/components/campaign-detail-card.js.map +1 -1
- package/src/lib/components/campaign-edit-drawer.d.ts +6 -0
- package/src/lib/components/campaign-edit-drawer.js +13 -2
- package/src/lib/components/campaign-edit-drawer.js.map +1 -1
- package/src/lib/components/campaign-queries.d.ts +5 -0
- package/src/lib/components/campaign-queries.js +7 -0
- package/src/lib/components/campaign-queries.js.map +1 -1
- package/src/lib/components/campaign-reach-sections.d.ts +28 -0
- package/src/lib/components/campaign-reach-sections.js +95 -4
- package/src/lib/components/campaign-reach-sections.js.map +1 -1
- package/src/lib/components/conversion-attribution.component.d.ts +18 -0
- package/src/lib/components/conversion-attribution.component.js +177 -39
- package/src/lib/components/conversion-attribution.component.js.map +1 -1
- package/src/lib/components/record-attribution-widget.d.ts +5 -0
- package/src/lib/components/record-attribution-widget.js +6 -3
- package/src/lib/components/record-attribution-widget.js.map +1 -1
- package/src/lib/components/site-runtime.d.ts +5 -0
- package/src/lib/components/site-runtime.js +101 -2
- package/src/lib/components/site-runtime.js.map +1 -1
- package/src/lib/declarations.server.js +17 -1
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/model/campaign-container.d.ts +28 -0
- package/src/lib/model/campaign-container.js +34 -0
- package/src/lib/model/campaign-container.js.map +1 -1
- package/src/lib/model/campaign-conversions.d.ts +47 -3
- package/src/lib/model/campaign-conversions.js +49 -4
- package/src/lib/model/campaign-conversions.js.map +1 -1
- package/src/lib/model/campaign-visits.d.ts +74 -0
- package/src/lib/model/campaign-visits.js +65 -0
- package/src/lib/model/campaign-visits.js.map +1 -0
- package/src/lib/server/campaign-conversion-attribution.d.ts +34 -3
- package/src/lib/server/campaign-conversion-attribution.js +126 -24
- package/src/lib/server/campaign-conversion-attribution.js.map +1 -1
- package/src/lib/server/campaign-touch-targets.d.ts +61 -0
- package/src/lib/server/campaign-touch-targets.js +162 -0
- package/src/lib/server/campaign-touch-targets.js.map +1 -0
- package/src/lib/server/campaign-visit-beacon.d.ts +44 -0
- package/src/lib/server/campaign-visit-beacon.js +125 -0
- package/src/lib/server/campaign-visit-beacon.js.map +1 -0
- package/src/lib/server/conversion-creditor.js +109 -2
- package/src/lib/server/conversion-creditor.js.map +1 -1
- package/src/lib/server/email-events.js +24 -25
- package/src/lib/server/email-events.js.map +1 -1
- package/src/lib/server/site-page-enricher.js +18 -2
- package/src/lib/server/site-page-enricher.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/components/campaign-edit-drawer.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 { ICON_VARIANT_CLOSE } from '@aglyn/shared-data-enums'\nimport { Container, MdiIcon, SrOnly } from '@aglyn/shared-ui-jsx'\nimport { NavigationDrawerComponent } from '@aglyn/shared-ui-jsx/components/navigation-drawer.component'\nimport {\n Alert,\n Button,\n Chip,\n FormControlLabel,\n IconButton,\n MenuItem,\n Stack,\n Switch,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useEffect, useState } from 'react'\nimport {\n campaignListUnsubscribe,\n campaignSiteIds,\n} from '../model/campaign-container'\n\n/** One option in the list or topic picker. */\nexport interface CampaignEditOption {\n value: string\n label: string\n}\n\n/** What the drawer collects — the container's whole editable surface. */\nexport interface CampaignEditValues {\n name: string\n startAtMs: number | null\n endAtMs: number | null\n listIds: string[]\n topicId: string\n /** Whether its emails carry the mail-client unsubscribe button (AGL-3307). */\n listUnsubscribe: boolean\n /**\n * The sites it is placed on — `null` for every site. Present only when the\n * drawer was handed `sites`, which only the org hub does (a site hub never\n * moves a campaign's placement), and only when the field was changed.\n */\n siteIds?: string[] | null\n}\n\nexport interface CampaignEditDrawerProps {\n open: boolean\n onClose: () => void\n /** The campaign as stored, for the fields to open on. */\n campaign: {\n name?: string\n startAtMs?: number | null\n endAtMs?: number | null\n listIds?: string[]\n topicId?: string\n visibleTo?: string[]\n listUnsubscribe?: boolean\n } | null\n /** The org's email lists, which this campaign may be aimed at. */\n lists: CampaignEditOption[]\n /** The org's active topics — the stream its emails open on. */\n topics: CampaignEditOption[]\n /**\n * The org's sites, which turns on the placement field. Passed by the org\n * hub alone: moving a campaign between sites is an org-wide decision, and\n * the rules refuse it from anyone narrower.\n */\n sites?: CampaignEditOption[]\n busy?: boolean\n /** A refusal from the write, shown where the form still is. */\n error?: string | null\n onSubmit: (values: CampaignEditValues) => void\n}\n\n/**\n * The `YYYY-MM-DD` a `date` input wants, from epoch ms.\n *\n * UTC, matching the writer: the create drawer stores `Date.parse('2026-03-01')`,\n * which is UTC midnight, so reading it back in local time shows the previous\n * day to everyone west of Greenwich — a campaign that changed its own start\n * date by being opened.\n */\nfunction dayInputValue(ms: number | null | undefined): string {\n if (typeof ms !== 'number' || !Number.isFinite(ms)) return ''\n const at = new Date(ms)\n return Number.isNaN(at.getTime()) ? '' : at.toISOString().slice(0, 10)\n}\n\n/** Epoch ms for a `YYYY-MM-DD` a `date` input produced, or null for empty. */\nfunction dayInputMs(value: string): number | null {\n if (!value) return null\n const parsed = Date.parse(value)\n return Number.isFinite(parsed) ? parsed : null\n}\n\n/**\n * EDITING ONE CAMPAIGN, IN A DRAWER ON THE CAMPAIGN'S OWN PAGE.\n *\n * A drawer and not a form above the campaigns table: a record is edited on\n * its own surface in this console, and a list with a form on top of it is\n * the shape the whole surface was routed to stop.\n *\n * ## Everything a container holds is editable, and that is the whole point\n *\n * A campaign is a name, a window, the lists it is aimed at and the stream its\n * emails open on. None of it describes mail that has already been delivered —\n * that lives on the SENDS, which record their own subject, audience and topic\n * at send time and are never rewritten. So unlike an email, where only the\n * console-only display name stays editable after it goes out, a campaign has\n * no field that could come to disagree with somebody's inbox.\n *\n * Changing the topic is the one that reads as though it might. It does not:\n * the topic here is what the composer's picker OPENS ON, and each send\n * records the topic it actually went out under. Re-pointing a campaign\n * changes the default for the next email written in it and nothing about the\n * ones already sent.\n */\nexport function CampaignEditDrawer(props: CampaignEditDrawerProps) {\n const { open, onClose, campaign, lists, topics, sites, busy, error, onSubmit } =\n props\n\n const [name, setName] = useState('')\n const [startAt, setStartAt] = useState('')\n const [endAt, setEndAt] = useState('')\n const [listIds, setListIds] = useState<string[]>([])\n const [topicId, setTopicId] = useState('')\n const [listUnsubscribe, setListUnsubscribe] = useState(true)\n /** The chosen sites; empty is every site, as the create drawer reads it. */\n const [siteIds, setSiteIds] = useState<string[]>([])\n /*\n * Whether the placement was CHANGED here. An untouched field submits\n * nothing, so saving a name cannot rewrite where a campaign is offered —\n * least of all turn a campaign placed on no site (an absent `visibleTo`,\n * which the field has no way to draw) into one on every site.\n */\n const [sitesTouched, setSitesTouched] = useState(false)\n\n /*\n * Re-seeded whenever the drawer opens rather than held from the last time.\n * The campaign arrives asynchronously and can change under a closed drawer,\n * so seeding once at mount would open it on a stale name.\n */\n useEffect(() => {\n if (!open) return\n setName(String(campaign?.name ?? ''))\n setStartAt(dayInputValue(campaign?.startAtMs))\n setEndAt(dayInputValue(campaign?.endAtMs))\n setListIds((campaign?.listIds ?? []).map(String))\n setTopicId(String(campaign?.topicId ?? ''))\n setListUnsubscribe(campaignListUnsubscribe(campaign))\n setSiteIds(campaignSiteIds(campaign) ?? [])\n setSitesTouched(false)\n }, [open, campaign])\n\n const startAtMs = dayInputMs(startAt)\n const endAtMs = dayInputMs(endAt)\n /*\n * Refused in the form rather than stored and rendered, exactly as the\n * create drawer refuses it: a campaign that ends before it starts is a\n * window no state in `campaignWindowState` describes, so it would draw as\n * \"Ended\" from the day it was made.\n */\n const backwards = startAtMs !== null && endAtMs !== null && endAtMs < startAtMs\n const submittable = name.trim().length > 0 && !backwards && !busy\n\n return (\n <NavigationDrawerComponent\n open={open}\n anchor=\"right\"\n variant=\"temporary\"\n onClose={onClose}\n AppBarProps={{ color: 'surface' }}\n appBarLeft={\n <>\n <IconButton\n color=\"inherit\"\n edge=\"start\"\n onClick={onClose}\n sx={{ mr: 2 }}\n >\n <MdiIcon path={ICON_VARIANT_CLOSE.path} />\n <SrOnly>close drawer</SrOnly>\n </IconButton>\n <Typography variant=\"h6\" component=\"div\">\n {'Edit campaign'}\n </Typography>\n </>\n }\n appBarRight={\n <Button variant=\"outlined\" color=\"inherit\" onClick={onClose}>\n {'Cancel'}\n </Button>\n }\n >\n <Container gutterY>\n <Stack spacing={2}>\n <TextField\n label=\"Name\"\n value={name}\n onChange={(event) => setName(event.target.value)}\n helperText=\"What you call this campaign in the console\"\n fullWidth\n />\n <TextField\n type=\"date\"\n label=\"Starts\"\n value={startAt}\n onChange={(event) => setStartAt(event.target.value)}\n helperText=\"When the campaign window opens\"\n slotProps={{ inputLabel: { shrink: true } }}\n fullWidth\n />\n <TextField\n type=\"date\"\n label=\"Ends\"\n value={endAt}\n onChange={(event) => setEndAt(event.target.value)}\n helperText=\"Leave empty for an open-ended campaign\"\n slotProps={{ inputLabel: { shrink: true } }}\n fullWidth\n />\n <TextField\n select\n label=\"Lists\"\n value={listIds}\n onChange={(event) =>\n setListIds(\n // A multiple select hands back an array; the typing on the\n // change event does not know that.\n (event.target.value as unknown as string[]).map(String),\n )\n }\n // A campaign with no list is legitimate: its emails can go to\n // leads, to site members, or to a segment.\n helperText=\"The lists this campaign is aimed at\"\n slotProps={{\n select: {\n multiple: true,\n renderValue: (selected: unknown) => (\n <Stack direction=\"row\" spacing={0.5} sx={{ flexWrap: 'wrap' }}>\n {(selected as string[]).map((id) => (\n <Chip\n key={id}\n size=\"small\"\n label={\n lists.find((list) => list.value === id)?.label ?? id\n }\n />\n ))}\n </Stack>\n ),\n },\n }}\n fullWidth\n >\n {lists.map((list) => (\n <MenuItem key={list.value} value={list.value}>\n {list.label}\n </MenuItem>\n ))}\n </TextField>\n <TextField\n select\n label=\"Topic\"\n value={topicId}\n onChange={(event) => setTopicId(event.target.value)}\n helperText=\"The stream its emails open on — each one can change it\"\n fullWidth\n >\n {/*\n An explicit \"no default\" option rather than only the topics.\n A campaign is allowed to carry none — the send resolves the\n org's default when the composer names nothing — and a select\n with no way back to empty makes the first choice permanent.\n */}\n <MenuItem value=\"\">{'No default — each email chooses'}</MenuItem>\n {topics.map((topic) => (\n <MenuItem key={topic.value} value={topic.value}>\n {topic.label}\n </MenuItem>\n ))}\n </TextField>\n {/*\n The mail-client unsubscribe button (AGL-3307), the setting a\n sequence carries too with the same label. ON for a campaign,\n because campaigns are bulk mail. Off warns, and the send path\n turns it back on for any email that would make the organization\n a bulk sender — the page says so when it does.\n */}\n <Stack spacing={0}>\n <FormControlLabel\n control={\n <Switch\n checked={listUnsubscribe}\n onChange={(event) => setListUnsubscribe(event.target.checked)}\n />\n }\n label=\"Add a mail-client unsubscribe button\"\n />\n <Typography variant=\"caption\" color=\"text.secondary\">\n Adds the List-Unsubscribe header, so the recipient’s mail app\n shows its own Unsubscribe button. It’s the easiest way out for\n them, and Gmail and Yahoo require it from bulk senders. The\n unsubscribe link in the footer is always there, either way.\n </Typography>\n </Stack>\n {!listUnsubscribe ? (\n <Alert severity=\"warning\">\n {'Without it, Gmail and Yahoo can reject or spam-folder these ' +\n 'emails once your organization sends in bulk, and more ' +\n 'people mark you as spam when their mail app offers no ' +\n 'unsubscribe button. It is turned back on automatically for ' +\n 'any email that would make your organization a bulk sender, ' +\n 'and the campaign page says so when it is.'}\n </Alert>\n ) : null}\n {sites ? (\n <TextField\n select\n label=\"Sites\"\n value={siteIds}\n onChange={(event) => {\n setSitesTouched(true)\n setSiteIds(\n (event.target.value as unknown as string[]).map(String),\n )\n }}\n helperText=\"The sites this campaign is offered on. Leave empty for every site.\"\n slotProps={{\n select: {\n multiple: true,\n displayEmpty: true,\n renderValue: (selected: unknown) =>\n (selected as string[]).length ? (\n <Stack\n direction=\"row\"\n spacing={0.5}\n sx={{ flexWrap: 'wrap' }}\n >\n {(selected as string[]).map((id) => (\n <Chip\n key={id}\n size=\"small\"\n label={\n sites.find((site) => site.value === id)?.label ??\n id\n }\n />\n ))}\n </Stack>\n ) : (\n 'Every site'\n ),\n },\n inputLabel: { shrink: true },\n }}\n fullWidth\n >\n {sites.map((site) => (\n <MenuItem key={site.value} value={site.value}>\n {site.label}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n {backwards ? (\n <Alert severity=\"warning\">\n {'The end date is before the start date.'}\n </Alert>\n ) : null}\n {error ? <Alert severity=\"error\">{error}</Alert> : null}\n <Button\n variant=\"contained\"\n disabled={!submittable}\n onClick={() =>\n onSubmit({\n name: name.trim(),\n startAtMs,\n endAtMs,\n listIds,\n topicId,\n listUnsubscribe,\n ...(sites && sitesTouched\n ? { siteIds: siteIds.length ? siteIds : null }\n : {}),\n })\n }\n >\n {busy ? 'Saving…' : 'Save campaign'}\n </Button>\n </Stack>\n </Container>\n </NavigationDrawerComponent>\n )\n}\nCampaignEditDrawer.displayName = 'CampaignEditDrawer'\n\nexport default CampaignEditDrawer\n"],"names":["ICON_VARIANT_CLOSE","Container","MdiIcon","SrOnly","NavigationDrawerComponent","Alert","Button","Chip","FormControlLabel","IconButton","MenuItem","Stack","Switch","TextField","Typography","useEffect","useState","campaignListUnsubscribe","campaignSiteIds","dayInputValue","ms","Number","isFinite","at","Date","isNaN","getTime","toISOString","slice","dayInputMs","value","parsed","parse","CampaignEditDrawer","props","open","onClose","campaign","lists","topics","sites","busy","error","onSubmit","name","setName","startAt","setStartAt","endAt","setEndAt","listIds","setListIds","topicId","setTopicId","listUnsubscribe","setListUnsubscribe","siteIds","setSiteIds","sitesTouched","setSitesTouched","String","startAtMs","endAtMs","map","backwards","submittable","trim","length","anchor","variant","AppBarProps","color","appBarLeft","edge","onClick","sx","mr","path","component","appBarRight","gutterY","spacing","label","onChange","event","target","helperText","fullWidth","type","slotProps","inputLabel","shrink","select","multiple","renderValue","selected","direction","flexWrap","id","size","find","list","topic","control","checked","severity","displayEmpty","site","disabled","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/components/campaign-edit-drawer.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 { ICON_VARIANT_CLOSE } from '@aglyn/shared-data-enums'\nimport { Container, MdiIcon, SrOnly } from '@aglyn/shared-ui-jsx'\nimport { NavigationDrawerComponent } from '@aglyn/shared-ui-jsx/components/navigation-drawer.component'\nimport {\n Alert,\n Button,\n Chip,\n FormControlLabel,\n IconButton,\n MenuItem,\n Stack,\n Switch,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useEffect, useState } from 'react'\nimport {\n campaignListUnsubscribe,\n campaignSiteIds,\n campaignUtmLabels,\n} from '../model/campaign-container'\n\n/** One option in the list or topic picker. */\nexport interface CampaignEditOption {\n value: string\n label: string\n}\n\n/** What the drawer collects — the container's whole editable surface. */\nexport interface CampaignEditValues {\n name: string\n startAtMs: number | null\n endAtMs: number | null\n listIds: string[]\n topicId: string\n /** Whether its emails carry the mail-client unsubscribe button (AGL-3307). */\n listUnsubscribe: boolean\n /**\n * The `utm_campaign` labels that mean this campaign (AGL-3461), as typed —\n * the save normalizes them with `campaignUtmLabels`.\n */\n utmCampaigns: string[]\n /**\n * The sites it is placed on — `null` for every site. Present only when the\n * drawer was handed `sites`, which only the org hub does (a site hub never\n * moves a campaign's placement), and only when the field was changed.\n */\n siteIds?: string[] | null\n}\n\nexport interface CampaignEditDrawerProps {\n open: boolean\n onClose: () => void\n /** The campaign as stored, for the fields to open on. */\n campaign: {\n name?: string\n startAtMs?: number | null\n endAtMs?: number | null\n listIds?: string[]\n topicId?: string\n visibleTo?: string[]\n listUnsubscribe?: boolean\n utmCampaigns?: string[]\n } | null\n /** The org's email lists, which this campaign may be aimed at. */\n lists: CampaignEditOption[]\n /** The org's active topics — the stream its emails open on. */\n topics: CampaignEditOption[]\n /**\n * The org's sites, which turns on the placement field. Passed by the org\n * hub alone: moving a campaign between sites is an org-wide decision, and\n * the rules refuse it from anyone narrower.\n */\n sites?: CampaignEditOption[]\n busy?: boolean\n /** A refusal from the write, shown where the form still is. */\n error?: string | null\n onSubmit: (values: CampaignEditValues) => void\n}\n\n/**\n * The `YYYY-MM-DD` a `date` input wants, from epoch ms.\n *\n * UTC, matching the writer: the create drawer stores `Date.parse('2026-03-01')`,\n * which is UTC midnight, so reading it back in local time shows the previous\n * day to everyone west of Greenwich — a campaign that changed its own start\n * date by being opened.\n */\nfunction dayInputValue(ms: number | null | undefined): string {\n if (typeof ms !== 'number' || !Number.isFinite(ms)) return ''\n const at = new Date(ms)\n return Number.isNaN(at.getTime()) ? '' : at.toISOString().slice(0, 10)\n}\n\n/** Epoch ms for a `YYYY-MM-DD` a `date` input produced, or null for empty. */\nfunction dayInputMs(value: string): number | null {\n if (!value) return null\n const parsed = Date.parse(value)\n return Number.isFinite(parsed) ? parsed : null\n}\n\n/**\n * EDITING ONE CAMPAIGN, IN A DRAWER ON THE CAMPAIGN'S OWN PAGE.\n *\n * A drawer and not a form above the campaigns table: a record is edited on\n * its own surface in this console, and a list with a form on top of it is\n * the shape the whole surface was routed to stop.\n *\n * ## Everything a container holds is editable, and that is the whole point\n *\n * A campaign is a name, a window, the lists it is aimed at and the stream its\n * emails open on. None of it describes mail that has already been delivered —\n * that lives on the SENDS, which record their own subject, audience and topic\n * at send time and are never rewritten. So unlike an email, where only the\n * console-only display name stays editable after it goes out, a campaign has\n * no field that could come to disagree with somebody's inbox.\n *\n * Changing the topic is the one that reads as though it might. It does not:\n * the topic here is what the composer's picker OPENS ON, and each send\n * records the topic it actually went out under. Re-pointing a campaign\n * changes the default for the next email written in it and nothing about the\n * ones already sent.\n */\nexport function CampaignEditDrawer(props: CampaignEditDrawerProps) {\n const { open, onClose, campaign, lists, topics, sites, busy, error, onSubmit } =\n props\n\n const [name, setName] = useState('')\n const [startAt, setStartAt] = useState('')\n const [endAt, setEndAt] = useState('')\n const [listIds, setListIds] = useState<string[]>([])\n const [topicId, setTopicId] = useState('')\n const [listUnsubscribe, setListUnsubscribe] = useState(true)\n /** The utm_campaign labels, as the field holds them: one per line or comma. */\n const [utmLabels, setUtmLabels] = useState('')\n /** The chosen sites; empty is every site, as the create drawer reads it. */\n const [siteIds, setSiteIds] = useState<string[]>([])\n /*\n * Whether the placement was CHANGED here. An untouched field submits\n * nothing, so saving a name cannot rewrite where a campaign is offered —\n * least of all turn a campaign placed on no site (an absent `visibleTo`,\n * which the field has no way to draw) into one on every site.\n */\n const [sitesTouched, setSitesTouched] = useState(false)\n\n /*\n * Re-seeded whenever the drawer opens rather than held from the last time.\n * The campaign arrives asynchronously and can change under a closed drawer,\n * so seeding once at mount would open it on a stale name.\n */\n useEffect(() => {\n if (!open) return\n setName(String(campaign?.name ?? ''))\n setStartAt(dayInputValue(campaign?.startAtMs))\n setEndAt(dayInputValue(campaign?.endAtMs))\n setListIds((campaign?.listIds ?? []).map(String))\n setTopicId(String(campaign?.topicId ?? ''))\n setListUnsubscribe(campaignListUnsubscribe(campaign))\n setUtmLabels(campaignUtmLabels(campaign?.utmCampaigns).join(', '))\n setSiteIds(campaignSiteIds(campaign) ?? [])\n setSitesTouched(false)\n }, [open, campaign])\n\n const startAtMs = dayInputMs(startAt)\n const endAtMs = dayInputMs(endAt)\n /*\n * Refused in the form rather than stored and rendered, exactly as the\n * create drawer refuses it: a campaign that ends before it starts is a\n * window no state in `campaignWindowState` describes, so it would draw as\n * \"Ended\" from the day it was made.\n */\n const backwards = startAtMs !== null && endAtMs !== null && endAtMs < startAtMs\n const submittable = name.trim().length > 0 && !backwards && !busy\n\n return (\n <NavigationDrawerComponent\n open={open}\n anchor=\"right\"\n variant=\"temporary\"\n onClose={onClose}\n AppBarProps={{ color: 'surface' }}\n appBarLeft={\n <>\n <IconButton\n color=\"inherit\"\n edge=\"start\"\n onClick={onClose}\n sx={{ mr: 2 }}\n >\n <MdiIcon path={ICON_VARIANT_CLOSE.path} />\n <SrOnly>close drawer</SrOnly>\n </IconButton>\n <Typography variant=\"h6\" component=\"div\">\n {'Edit campaign'}\n </Typography>\n </>\n }\n appBarRight={\n <Button variant=\"outlined\" color=\"inherit\" onClick={onClose}>\n {'Cancel'}\n </Button>\n }\n >\n <Container gutterY>\n <Stack spacing={2}>\n <TextField\n label=\"Name\"\n value={name}\n onChange={(event) => setName(event.target.value)}\n helperText=\"What you call this campaign in the console\"\n fullWidth\n />\n <TextField\n type=\"date\"\n label=\"Starts\"\n value={startAt}\n onChange={(event) => setStartAt(event.target.value)}\n helperText=\"When the campaign window opens\"\n slotProps={{ inputLabel: { shrink: true } }}\n fullWidth\n />\n <TextField\n type=\"date\"\n label=\"Ends\"\n value={endAt}\n onChange={(event) => setEndAt(event.target.value)}\n helperText=\"Leave empty for an open-ended campaign\"\n slotProps={{ inputLabel: { shrink: true } }}\n fullWidth\n />\n <TextField\n select\n label=\"Lists\"\n value={listIds}\n onChange={(event) =>\n setListIds(\n // A multiple select hands back an array; the typing on the\n // change event does not know that.\n (event.target.value as unknown as string[]).map(String),\n )\n }\n // A campaign with no list is legitimate: its emails can go to\n // leads, to site members, or to a segment.\n helperText=\"The lists this campaign is aimed at\"\n slotProps={{\n select: {\n multiple: true,\n renderValue: (selected: unknown) => (\n <Stack direction=\"row\" spacing={0.5} sx={{ flexWrap: 'wrap' }}>\n {(selected as string[]).map((id) => (\n <Chip\n key={id}\n size=\"small\"\n label={\n lists.find((list) => list.value === id)?.label ?? id\n }\n />\n ))}\n </Stack>\n ),\n },\n }}\n fullWidth\n >\n {lists.map((list) => (\n <MenuItem key={list.value} value={list.value}>\n {list.label}\n </MenuItem>\n ))}\n </TextField>\n <TextField\n select\n label=\"Topic\"\n value={topicId}\n onChange={(event) => setTopicId(event.target.value)}\n helperText=\"The stream its emails open on — each one can change it\"\n fullWidth\n >\n {/*\n An explicit \"no default\" option rather than only the topics.\n A campaign is allowed to carry none — the send resolves the\n org's default when the composer names nothing — and a select\n with no way back to empty makes the first choice permanent.\n */}\n <MenuItem value=\"\">{'No default — each email chooses'}</MenuItem>\n {topics.map((topic) => (\n <MenuItem key={topic.value} value={topic.value}>\n {topic.label}\n </MenuItem>\n ))}\n </TextField>\n {/*\n THE LABELS THAT MEAN THIS CAMPAIGN (AGL-3461). An ad or a social\n post carries `utm_campaign=…`; a label named here credits this\n campaign the way a page filed under it does. Lowercased and\n deduped on save, which is how the lookup matches them.\n */}\n <TextField\n label=\"utm_campaign labels\"\n value={utmLabels}\n onChange={(event) => setUtmLabels(event.target.value)}\n helperText={\n 'Links carrying one of these utm_campaign values credit this ' +\n 'campaign. Separate them with commas. A label two campaigns ' +\n 'both list credits neither.'\n }\n placeholder=\"spring-launch, spring-retargeting\"\n fullWidth\n />\n {/*\n The mail-client unsubscribe button (AGL-3307), the setting a\n sequence carries too with the same label. ON for a campaign,\n because campaigns are bulk mail. Off warns, and the send path\n turns it back on for any email that would make the organization\n a bulk sender — the page says so when it does.\n */}\n <Stack spacing={0}>\n <FormControlLabel\n control={\n <Switch\n checked={listUnsubscribe}\n onChange={(event) => setListUnsubscribe(event.target.checked)}\n />\n }\n label=\"Add a mail-client unsubscribe button\"\n />\n <Typography variant=\"caption\" color=\"text.secondary\">\n Adds the List-Unsubscribe header, so the recipient’s mail app\n shows its own Unsubscribe button. It’s the easiest way out for\n them, and Gmail and Yahoo require it from bulk senders. The\n unsubscribe link in the footer is always there, either way.\n </Typography>\n </Stack>\n {!listUnsubscribe ? (\n <Alert severity=\"warning\">\n {'Without it, Gmail and Yahoo can reject or spam-folder these ' +\n 'emails once your organization sends in bulk, and more ' +\n 'people mark you as spam when their mail app offers no ' +\n 'unsubscribe button. It is turned back on automatically for ' +\n 'any email that would make your organization a bulk sender, ' +\n 'and the campaign page says so when it is.'}\n </Alert>\n ) : null}\n {sites ? (\n <TextField\n select\n label=\"Sites\"\n value={siteIds}\n onChange={(event) => {\n setSitesTouched(true)\n setSiteIds(\n (event.target.value as unknown as string[]).map(String),\n )\n }}\n helperText=\"The sites this campaign is offered on. Leave empty for every site.\"\n slotProps={{\n select: {\n multiple: true,\n displayEmpty: true,\n renderValue: (selected: unknown) =>\n (selected as string[]).length ? (\n <Stack\n direction=\"row\"\n spacing={0.5}\n sx={{ flexWrap: 'wrap' }}\n >\n {(selected as string[]).map((id) => (\n <Chip\n key={id}\n size=\"small\"\n label={\n sites.find((site) => site.value === id)?.label ??\n id\n }\n />\n ))}\n </Stack>\n ) : (\n 'Every site'\n ),\n },\n inputLabel: { shrink: true },\n }}\n fullWidth\n >\n {sites.map((site) => (\n <MenuItem key={site.value} value={site.value}>\n {site.label}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n {backwards ? (\n <Alert severity=\"warning\">\n {'The end date is before the start date.'}\n </Alert>\n ) : null}\n {error ? <Alert severity=\"error\">{error}</Alert> : null}\n <Button\n variant=\"contained\"\n disabled={!submittable}\n onClick={() =>\n onSubmit({\n name: name.trim(),\n startAtMs,\n endAtMs,\n listIds,\n topicId,\n listUnsubscribe,\n utmCampaigns: campaignUtmLabels(utmLabels),\n ...(sites && sitesTouched\n ? { siteIds: siteIds.length ? siteIds : null }\n : {}),\n })\n }\n >\n {busy ? 'Saving…' : 'Save campaign'}\n </Button>\n </Stack>\n </Container>\n </NavigationDrawerComponent>\n )\n}\nCampaignEditDrawer.displayName = 'CampaignEditDrawer'\n\nexport default CampaignEditDrawer\n"],"names":["ICON_VARIANT_CLOSE","Container","MdiIcon","SrOnly","NavigationDrawerComponent","Alert","Button","Chip","FormControlLabel","IconButton","MenuItem","Stack","Switch","TextField","Typography","useEffect","useState","campaignListUnsubscribe","campaignSiteIds","campaignUtmLabels","dayInputValue","ms","Number","isFinite","at","Date","isNaN","getTime","toISOString","slice","dayInputMs","value","parsed","parse","CampaignEditDrawer","props","open","onClose","campaign","lists","topics","sites","busy","error","onSubmit","name","setName","startAt","setStartAt","endAt","setEndAt","listIds","setListIds","topicId","setTopicId","listUnsubscribe","setListUnsubscribe","utmLabels","setUtmLabels","siteIds","setSiteIds","sitesTouched","setSitesTouched","String","startAtMs","endAtMs","map","utmCampaigns","join","backwards","submittable","trim","length","anchor","variant","AppBarProps","color","appBarLeft","edge","onClick","sx","mr","path","component","appBarRight","gutterY","spacing","label","onChange","event","target","helperText","fullWidth","type","slotProps","inputLabel","shrink","select","multiple","renderValue","selected","direction","flexWrap","id","size","find","list","topic","placeholder","control","checked","severity","displayEmpty","site","disabled","displayName"],"mappings":"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"}
|
|
@@ -67,3 +67,8 @@ export declare function campaignSendDoc(firestore: Firestore, orgId: string, sen
|
|
|
67
67
|
export declare function campaignSendReportDoc(firestore: Firestore, orgId: string, sendId: string, report: 'links' | 'revenue' | 'conversions' | 'reached'): DocumentReference;
|
|
68
68
|
/** A container's sequence rollup, `orgs/{orgId}/campaignSequenceReports/{id}`. */
|
|
69
69
|
export declare function campaignSequenceReportDoc(firestore: Firestore, orgId: string, campaignId: string): DocumentReference;
|
|
70
|
+
/**
|
|
71
|
+
* A container's visit rollup (AGL-3461),
|
|
72
|
+
* `orgs/{orgId}/campaignVisitReports/{id}` — first visits and page views.
|
|
73
|
+
*/
|
|
74
|
+
export declare function campaignVisitReportDoc(firestore: Firestore, orgId: string, campaignId: string): DocumentReference;
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/ import { hostScopeToken, scopeTokensForHost } from "@aglyn/aglyn/app-utils/scope-tokens";
|
|
17
17
|
import { orgCampaignSendsPath, orgCampaignSequenceReportsPath, orgEmailCampaignsPath } from "../model/campaign-container.js";
|
|
18
18
|
import { collection, doc, query, where } from "firebase/firestore";
|
|
19
|
+
import { CAMPAIGN_VISIT_REPORTS_COLLECTION } from "../model/campaign-visits.js";
|
|
19
20
|
/**
|
|
20
21
|
* WHERE A CAMPAIGN AND ITS SENDS ARE READ FROM, at either level of the hub.
|
|
21
22
|
*
|
|
@@ -81,5 +82,11 @@ import { collection, doc, query, where } from "firebase/firestore";
|
|
|
81
82
|
/** A container's sequence rollup, `orgs/{orgId}/campaignSequenceReports/{id}`. */ export function campaignSequenceReportDoc(firestore, orgId, campaignId) {
|
|
82
83
|
return doc(firestore, ...orgCampaignSequenceReportsPath(orgId), campaignId);
|
|
83
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* A container's visit rollup (AGL-3461),
|
|
87
|
+
* `orgs/{orgId}/campaignVisitReports/{id}` — first visits and page views.
|
|
88
|
+
*/ export function campaignVisitReportDoc(firestore, orgId, campaignId) {
|
|
89
|
+
return doc(firestore, 'orgs', orgId, CAMPAIGN_VISIT_REPORTS_COLLECTION, campaignId);
|
|
90
|
+
}
|
|
84
91
|
|
|
85
92
|
//# sourceMappingURL=campaign-queries.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/components/campaign-queries.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 { hostScopeToken, scopeTokensForHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport {\n orgCampaignSendsPath,\n orgCampaignSequenceReportsPath,\n orgEmailCampaignsPath,\n} from '../model/campaign-container'\nimport {\n collection,\n doc,\n query,\n where,\n type DocumentReference,\n type Firestore,\n type Query,\n} from 'firebase/firestore'\n\n/**\n * WHERE A CAMPAIGN AND ITS SENDS ARE READ FROM, at either level of the hub.\n *\n * Both collections belong to the organization: containers at\n * `orgs/{orgId}/emailCampaigns`, sends at `orgs/{orgId}/campaigns`, each\n * send's rollups under it at `…/reports/{name}`. Spelled once here so no card\n * rebuilds a path from memory, and so the one rule that makes a site hub's\n * read legal is in one place:\n *\n * **A site hub's LIST reads are filtered by scope.** The rules let an\n * org-wide member read every document and a site collaborator only those\n * whose `visibleTo` names a scope they hold. A query the rules cannot prove\n * document by document is refused whole, so a site hub asks exactly for what\n * that site may see — a container placed on every site (`'org'`) or on this\n * one (`'host:{id}'`), and a send sent AS this site. An org-wide member loses\n * nothing by the filter at a site: those are the campaigns the site shows.\n *\n * The ORG hub is admitted only to org-wide members, so its reads are\n * unfiltered.\n *\n * One `array-contains-any` per query is Firestore's limit, so no caller adds\n * a second array clause to these; per-document reads need no filter at all.\n */\n\n/**\n * `orgs/{orgId}/emailCampaigns`, narrowed to one site's placements when\n * `hostId` names a site; `null` until the org is known.\n */\nexport function campaignContainersQuery(\n firestore: Firestore,\n orgId: string | null,\n hostId: string | null,\n): Query | null {\n if (!orgId) return null\n const containers = collection(firestore, ...orgEmailCampaignsPath(orgId))\n return hostId\n ? query(\n containers,\n where('visibleTo', 'array-contains-any', scopeTokensForHost(hostId)),\n )\n : containers\n}\n\n/**\n * `orgs/{orgId}/campaigns` — the sends — narrowed to the ones sent as one\n * site when `hostId` names a site; `null` until the org is known.\n */\nexport function campaignSendsQuery(\n firestore: Firestore,\n orgId: string | null,\n hostId: string | null,\n): Query | null {\n if (!orgId) return null\n const sends = collection(firestore, ...orgCampaignSendsPath(orgId))\n return hostId\n ? query(\n sends,\n where('visibleTo', 'array-contains-any', [hostScopeToken(hostId)]),\n )\n : sends\n}\n\n/**\n * The two collections UNSCOPED, for a paged list that puts its scope on its\n * query plan (AGL-3321): `campaignSendsScope` / `campaignContainersScope` in\n * `model/campaign-list-query.ts` carry the site's clause there, beside the\n * list's own. A site hub's sends are asked `hostId == {site}` rather than by\n * `visibleTo`, so search keeps the query's one array clause; the rules\n * admit that read for the site's collaborators too.\n */\nexport function campaignSendsCollection(\n firestore: Firestore,\n orgId: string | null,\n): Query | null {\n return orgId ? collection(firestore, ...orgCampaignSendsPath(orgId)) : null\n}\n\n/** `orgs/{orgId}/emailCampaigns`, unscoped — see {@link campaignSendsCollection}. */\nexport function campaignContainersCollection(\n firestore: Firestore,\n orgId: string | null,\n): Query | null {\n return orgId ? collection(firestore, ...orgEmailCampaignsPath(orgId)) : null\n}\n\n/** One container, `orgs/{orgId}/emailCampaigns/{id}`. */\nexport function campaignContainerDoc(\n firestore: Firestore,\n orgId: string,\n campaignId: string,\n): DocumentReference {\n return doc(firestore, ...orgEmailCampaignsPath(orgId), campaignId)\n}\n\n/** One send, `orgs/{orgId}/campaigns/{sendId}`. */\nexport function campaignSendDoc(\n firestore: Firestore,\n orgId: string,\n sendId: string,\n): DocumentReference {\n return doc(firestore, ...orgCampaignSendsPath(orgId), sendId)\n}\n\n/** One of a send's rollups, `orgs/{orgId}/campaigns/{sendId}/reports/{name}`. */\nexport function campaignSendReportDoc(\n firestore: Firestore,\n orgId: string,\n sendId: string,\n report: 'links' | 'revenue' | 'conversions' | 'reached',\n): DocumentReference {\n return doc(\n firestore,\n ...orgCampaignSendsPath(orgId),\n sendId,\n 'reports',\n report,\n )\n}\n\n/** A container's sequence rollup, `orgs/{orgId}/campaignSequenceReports/{id}`. */\nexport function campaignSequenceReportDoc(\n firestore: Firestore,\n orgId: string,\n campaignId: string,\n): DocumentReference {\n return doc(firestore, ...orgCampaignSequenceReportsPath(orgId), campaignId)\n}\n"],"names":["hostScopeToken","scopeTokensForHost","orgCampaignSendsPath","orgCampaignSequenceReportsPath","orgEmailCampaignsPath","collection","doc","query","where","campaignContainersQuery","firestore","orgId","hostId","containers","campaignSendsQuery","sends","campaignSendsCollection","campaignContainersCollection","campaignContainerDoc","campaignId","campaignSendDoc","sendId","campaignSendReportDoc","report","campaignSequenceReportDoc"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,cAAc,EAAEC,kBAAkB,QAAQ,sCAAqC;AACxF,SACEC,oBAAoB,EACpBC,8BAA8B,EAC9BC,qBAAqB,QAChB,iCAA6B;AACpC,SACEC,UAAU,EACVC,GAAG,EACHC,KAAK,EACLC,KAAK,QAIA,qBAAoB;
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/components/campaign-queries.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 { hostScopeToken, scopeTokensForHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport {\n orgCampaignSendsPath,\n orgCampaignSequenceReportsPath,\n orgEmailCampaignsPath,\n} from '../model/campaign-container'\nimport {\n collection,\n doc,\n query,\n where,\n type DocumentReference,\n type Firestore,\n type Query,\n} from 'firebase/firestore'\nimport { CAMPAIGN_VISIT_REPORTS_COLLECTION } from '../model/campaign-visits'\n\n/**\n * WHERE A CAMPAIGN AND ITS SENDS ARE READ FROM, at either level of the hub.\n *\n * Both collections belong to the organization: containers at\n * `orgs/{orgId}/emailCampaigns`, sends at `orgs/{orgId}/campaigns`, each\n * send's rollups under it at `…/reports/{name}`. Spelled once here so no card\n * rebuilds a path from memory, and so the one rule that makes a site hub's\n * read legal is in one place:\n *\n * **A site hub's LIST reads are filtered by scope.** The rules let an\n * org-wide member read every document and a site collaborator only those\n * whose `visibleTo` names a scope they hold. A query the rules cannot prove\n * document by document is refused whole, so a site hub asks exactly for what\n * that site may see — a container placed on every site (`'org'`) or on this\n * one (`'host:{id}'`), and a send sent AS this site. An org-wide member loses\n * nothing by the filter at a site: those are the campaigns the site shows.\n *\n * The ORG hub is admitted only to org-wide members, so its reads are\n * unfiltered.\n *\n * One `array-contains-any` per query is Firestore's limit, so no caller adds\n * a second array clause to these; per-document reads need no filter at all.\n */\n\n/**\n * `orgs/{orgId}/emailCampaigns`, narrowed to one site's placements when\n * `hostId` names a site; `null` until the org is known.\n */\nexport function campaignContainersQuery(\n firestore: Firestore,\n orgId: string | null,\n hostId: string | null,\n): Query | null {\n if (!orgId) return null\n const containers = collection(firestore, ...orgEmailCampaignsPath(orgId))\n return hostId\n ? query(\n containers,\n where('visibleTo', 'array-contains-any', scopeTokensForHost(hostId)),\n )\n : containers\n}\n\n/**\n * `orgs/{orgId}/campaigns` — the sends — narrowed to the ones sent as one\n * site when `hostId` names a site; `null` until the org is known.\n */\nexport function campaignSendsQuery(\n firestore: Firestore,\n orgId: string | null,\n hostId: string | null,\n): Query | null {\n if (!orgId) return null\n const sends = collection(firestore, ...orgCampaignSendsPath(orgId))\n return hostId\n ? query(\n sends,\n where('visibleTo', 'array-contains-any', [hostScopeToken(hostId)]),\n )\n : sends\n}\n\n/**\n * The two collections UNSCOPED, for a paged list that puts its scope on its\n * query plan (AGL-3321): `campaignSendsScope` / `campaignContainersScope` in\n * `model/campaign-list-query.ts` carry the site's clause there, beside the\n * list's own. A site hub's sends are asked `hostId == {site}` rather than by\n * `visibleTo`, so search keeps the query's one array clause; the rules\n * admit that read for the site's collaborators too.\n */\nexport function campaignSendsCollection(\n firestore: Firestore,\n orgId: string | null,\n): Query | null {\n return orgId ? collection(firestore, ...orgCampaignSendsPath(orgId)) : null\n}\n\n/** `orgs/{orgId}/emailCampaigns`, unscoped — see {@link campaignSendsCollection}. */\nexport function campaignContainersCollection(\n firestore: Firestore,\n orgId: string | null,\n): Query | null {\n return orgId ? collection(firestore, ...orgEmailCampaignsPath(orgId)) : null\n}\n\n/** One container, `orgs/{orgId}/emailCampaigns/{id}`. */\nexport function campaignContainerDoc(\n firestore: Firestore,\n orgId: string,\n campaignId: string,\n): DocumentReference {\n return doc(firestore, ...orgEmailCampaignsPath(orgId), campaignId)\n}\n\n/** One send, `orgs/{orgId}/campaigns/{sendId}`. */\nexport function campaignSendDoc(\n firestore: Firestore,\n orgId: string,\n sendId: string,\n): DocumentReference {\n return doc(firestore, ...orgCampaignSendsPath(orgId), sendId)\n}\n\n/** One of a send's rollups, `orgs/{orgId}/campaigns/{sendId}/reports/{name}`. */\nexport function campaignSendReportDoc(\n firestore: Firestore,\n orgId: string,\n sendId: string,\n report: 'links' | 'revenue' | 'conversions' | 'reached',\n): DocumentReference {\n return doc(\n firestore,\n ...orgCampaignSendsPath(orgId),\n sendId,\n 'reports',\n report,\n )\n}\n\n/** A container's sequence rollup, `orgs/{orgId}/campaignSequenceReports/{id}`. */\nexport function campaignSequenceReportDoc(\n firestore: Firestore,\n orgId: string,\n campaignId: string,\n): DocumentReference {\n return doc(firestore, ...orgCampaignSequenceReportsPath(orgId), campaignId)\n}\n\n/**\n * A container's visit rollup (AGL-3461),\n * `orgs/{orgId}/campaignVisitReports/{id}` — first visits and page views.\n */\nexport function campaignVisitReportDoc(\n firestore: Firestore,\n orgId: string,\n campaignId: string,\n): DocumentReference {\n return doc(firestore, 'orgs', orgId, CAMPAIGN_VISIT_REPORTS_COLLECTION, campaignId)\n}\n"],"names":["hostScopeToken","scopeTokensForHost","orgCampaignSendsPath","orgCampaignSequenceReportsPath","orgEmailCampaignsPath","collection","doc","query","where","CAMPAIGN_VISIT_REPORTS_COLLECTION","campaignContainersQuery","firestore","orgId","hostId","containers","campaignSendsQuery","sends","campaignSendsCollection","campaignContainersCollection","campaignContainerDoc","campaignId","campaignSendDoc","sendId","campaignSendReportDoc","report","campaignSequenceReportDoc","campaignVisitReportDoc"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,cAAc,EAAEC,kBAAkB,QAAQ,sCAAqC;AACxF,SACEC,oBAAoB,EACpBC,8BAA8B,EAC9BC,qBAAqB,QAChB,iCAA6B;AACpC,SACEC,UAAU,EACVC,GAAG,EACHC,KAAK,EACLC,KAAK,QAIA,qBAAoB;AAC3B,SAASC,iCAAiC,QAAQ,8BAA0B;AAE5E;;;;;;;;;;;;;;;;;;;;;;CAsBC,GAED;;;CAGC,GACD,OAAO,SAASC,wBACdC,SAAoB,EACpBC,KAAoB,EACpBC,MAAqB;IAErB,IAAI,CAACD,OAAO,OAAO;IACnB,MAAME,aAAaT,WAAWM,cAAcP,sBAAsBQ;IAClE,OAAOC,SACHN,MACEO,YACAN,MAAM,aAAa,sBAAsBP,mBAAmBY,YAE9DC;AACN;AAEA;;;CAGC,GACD,OAAO,SAASC,mBACdJ,SAAoB,EACpBC,KAAoB,EACpBC,MAAqB;IAErB,IAAI,CAACD,OAAO,OAAO;IACnB,MAAMI,QAAQX,WAAWM,cAAcT,qBAAqBU;IAC5D,OAAOC,SACHN,MACES,OACAR,MAAM,aAAa,sBAAsB;QAACR,eAAea;KAAQ,KAEnEG;AACN;AAEA;;;;;;;CAOC,GACD,OAAO,SAASC,wBACdN,SAAoB,EACpBC,KAAoB;IAEpB,OAAOA,QAAQP,WAAWM,cAAcT,qBAAqBU,UAAU;AACzE;AAEA,mFAAmF,GACnF,OAAO,SAASM,6BACdP,SAAoB,EACpBC,KAAoB;IAEpB,OAAOA,QAAQP,WAAWM,cAAcP,sBAAsBQ,UAAU;AAC1E;AAEA,uDAAuD,GACvD,OAAO,SAASO,qBACdR,SAAoB,EACpBC,KAAa,EACbQ,UAAkB;IAElB,OAAOd,IAAIK,cAAcP,sBAAsBQ,QAAQQ;AACzD;AAEA,iDAAiD,GACjD,OAAO,SAASC,gBACdV,SAAoB,EACpBC,KAAa,EACbU,MAAc;IAEd,OAAOhB,IAAIK,cAAcT,qBAAqBU,QAAQU;AACxD;AAEA,+EAA+E,GAC/E,OAAO,SAASC,sBACdZ,SAAoB,EACpBC,KAAa,EACbU,MAAc,EACdE,MAAuD;IAEvD,OAAOlB,IACLK,cACGT,qBAAqBU,QACxBU,QACA,WACAE;AAEJ;AAEA,gFAAgF,GAChF,OAAO,SAASC,0BACdd,SAAoB,EACpBC,KAAa,EACbQ,UAAkB;IAElB,OAAOd,IAAIK,cAAcR,+BAA+BS,QAAQQ;AAClE;AAEA;;;CAGC,GACD,OAAO,SAASM,uBACdf,SAAoB,EACpBC,KAAa,EACbQ,UAAkB;IAElB,OAAOd,IAAIK,WAAW,QAAQC,OAAOH,mCAAmCW;AAC1E"}
|
|
@@ -44,6 +44,14 @@ export declare function CampaignConversionsSection(props: Omit<CampaignReachProp
|
|
|
44
44
|
* one.
|
|
45
45
|
*/
|
|
46
46
|
hostId: string;
|
|
47
|
+
/**
|
|
48
|
+
* The campaign CONTAINER's own id (AGL-3461). A conversion credited to a
|
|
49
|
+
* page filed under the campaign, to a link labeled for it, or to one of
|
|
50
|
+
* its sequences carries the container's id rather than a send's, so it
|
|
51
|
+
* is counted beside the sends' — and a campaign that never sent an email
|
|
52
|
+
* still has figures.
|
|
53
|
+
*/
|
|
54
|
+
campaignId?: string;
|
|
47
55
|
/**
|
|
48
56
|
* That site's marketing hub URL, for its conversions list; `null` hides
|
|
49
57
|
* the link rather than pointing it nowhere.
|
|
@@ -170,3 +178,23 @@ export declare function CampaignSequencesSection(props: {
|
|
|
170
178
|
export declare namespace CampaignSequencesSection {
|
|
171
179
|
var displayName: string;
|
|
172
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* WHO IT REACHED ON THE SITE (AGL-3461) — first visits and views of the pages
|
|
183
|
+
* filed under the campaign.
|
|
184
|
+
*
|
|
185
|
+
* One keyed read of `orgs/{orgId}/campaignVisitReports/{campaignId}`, which the
|
|
186
|
+
* site collector's `campaignVisit` beacon writes: keyed by the campaign's own
|
|
187
|
+
* id, so this costs one document whatever the traffic. Across every site the
|
|
188
|
+
* campaign is placed on, because the visitors are the campaign's whichever of
|
|
189
|
+
* them they read.
|
|
190
|
+
*
|
|
191
|
+
* Absent is not zero, for the sequences section's reason: a campaign whose
|
|
192
|
+
* pages nobody has opened since counting began has no document, and says so.
|
|
193
|
+
*/
|
|
194
|
+
export declare function CampaignVisitsSection(props: {
|
|
195
|
+
orgId: string | null;
|
|
196
|
+
campaignId: string;
|
|
197
|
+
}): import("react").JSX.Element;
|
|
198
|
+
export declare namespace CampaignVisitsSection {
|
|
199
|
+
var displayName: string;
|
|
200
|
+
}
|
|
@@ -29,7 +29,8 @@ import { campaignSequencesReport } from "../model/campaign-report.js";
|
|
|
29
29
|
import { EMAIL_ATTRIBUTION_WINDOW_DAYS } from "@aglyn/shared-util-email";
|
|
30
30
|
import { CAMPAIGN_CONVERSION_KINDS, CAMPAIGN_CONVERSION_KIND_COPY } from "../model/campaign-conversions.js";
|
|
31
31
|
import { campaignRevenueAcrossSends } from "../model/campaign-revenue.js";
|
|
32
|
-
import {
|
|
32
|
+
import { campaignVisitsReport } from "../model/campaign-visits.js";
|
|
33
|
+
import { campaignSendReportDoc, campaignSequenceReportDoc, campaignVisitReportDoc } from "./campaign-queries.js";
|
|
33
34
|
/**
|
|
34
35
|
* A CAMPAIGN BEYOND ITS MAIL — what it caused, what it earned, and where it
|
|
35
36
|
* sent people.
|
|
@@ -108,9 +109,12 @@ import { campaignSendReportDoc, campaignSequenceReportDoc } from "./campaign-que
|
|
|
108
109
|
* three times and would look exactly like a bigger number, so there is no
|
|
109
110
|
* total here and the note under the figures says why.
|
|
110
111
|
*/ export function CampaignConversionsSection(props) {
|
|
111
|
-
const { hostId, sendIds, truncated, basePath } = props;
|
|
112
|
+
const { hostId, sendIds, truncated, basePath, campaignId } = props;
|
|
112
113
|
const firestore = useFirestore();
|
|
113
|
-
const key = idsKey(
|
|
114
|
+
const key = idsKey(campaignId ? [
|
|
115
|
+
campaignId,
|
|
116
|
+
...sendIds.filter((id)=>id !== campaignId)
|
|
117
|
+
] : sendIds);
|
|
114
118
|
const [counts, setCounts] = useState(null);
|
|
115
119
|
const [failed, setFailed] = useState(false);
|
|
116
120
|
useEffect(()=>{
|
|
@@ -190,7 +194,7 @@ import { campaignSendReportDoc, campaignSequenceReportDoc } from "./campaign-que
|
|
|
190
194
|
/*#__PURE__*/ _jsx(Typography, {
|
|
191
195
|
variant: "caption",
|
|
192
196
|
color: "text.secondary",
|
|
193
|
-
children: 'Credited to this campaign
|
|
197
|
+
children: (campaignId ? 'Credited to this campaign — a click on one of its emails or ' + 'sequences, a link carrying a utm_campaign label it declares, ' + 'or a page filed under it, whichever touched the visitor last. ' : 'Credited to this email. ') + 'Somebody who arrived from a utm_ label no campaign declares is ' + 'credited to that label instead, and somebody who arrived ' + 'directly is credited to nobody — both are counted on the ' + 'site’s conversions list.'
|
|
194
198
|
}),
|
|
195
199
|
truncated ? /*#__PURE__*/ _jsx(Typography, {
|
|
196
200
|
variant: "caption",
|
|
@@ -777,5 +781,92 @@ CampaignDestinationsSection.displayName = 'CampaignDestinationsSection';
|
|
|
777
781
|
});
|
|
778
782
|
}
|
|
779
783
|
CampaignSequencesSection.displayName = 'CampaignSequencesSection';
|
|
784
|
+
/**
|
|
785
|
+
* WHO IT REACHED ON THE SITE (AGL-3461) — first visits and views of the pages
|
|
786
|
+
* filed under the campaign.
|
|
787
|
+
*
|
|
788
|
+
* One keyed read of `orgs/{orgId}/campaignVisitReports/{campaignId}`, which the
|
|
789
|
+
* site collector's `campaignVisit` beacon writes: keyed by the campaign's own
|
|
790
|
+
* id, so this costs one document whatever the traffic. Across every site the
|
|
791
|
+
* campaign is placed on, because the visitors are the campaign's whichever of
|
|
792
|
+
* them they read.
|
|
793
|
+
*
|
|
794
|
+
* Absent is not zero, for the sequences section's reason: a campaign whose
|
|
795
|
+
* pages nobody has opened since counting began has no document, and says so.
|
|
796
|
+
*/ export function CampaignVisitsSection(props) {
|
|
797
|
+
const { orgId, campaignId } = props;
|
|
798
|
+
const firestore = useFirestore();
|
|
799
|
+
const [rollup, setRollup] = useState(null);
|
|
800
|
+
const [failed, setFailed] = useState(false);
|
|
801
|
+
useEffect(()=>{
|
|
802
|
+
let active = true;
|
|
803
|
+
setRollup(null);
|
|
804
|
+
setFailed(false);
|
|
805
|
+
if (!orgId) return undefined;
|
|
806
|
+
getDoc(campaignVisitReportDoc(firestore, orgId, campaignId)).then((snapshot)=>{
|
|
807
|
+
var _snapshot_data;
|
|
808
|
+
if (active) setRollup((_snapshot_data = snapshot.data()) != null ? _snapshot_data : undefined);
|
|
809
|
+
}).catch(()=>{
|
|
810
|
+
// Withheld, never zeroed — the conversions section's reason.
|
|
811
|
+
if (active) setFailed(true);
|
|
812
|
+
});
|
|
813
|
+
return ()=>{
|
|
814
|
+
active = false;
|
|
815
|
+
};
|
|
816
|
+
}, [
|
|
817
|
+
firestore,
|
|
818
|
+
orgId,
|
|
819
|
+
campaignId
|
|
820
|
+
]);
|
|
821
|
+
const report = rollup === null ? null : campaignVisitsReport(rollup);
|
|
822
|
+
return /*#__PURE__*/ _jsx(Section, {
|
|
823
|
+
title: "Who it reached",
|
|
824
|
+
children: failed ? /*#__PURE__*/ _jsx(Alert, {
|
|
825
|
+
severity: "warning",
|
|
826
|
+
children: 'The visits to this campaign could not be read.'
|
|
827
|
+
}) : !report ? /*#__PURE__*/ _jsx(Typography, {
|
|
828
|
+
variant: "body2",
|
|
829
|
+
color: "text.secondary",
|
|
830
|
+
children: 'Reading the visits to this campaign…'
|
|
831
|
+
}) : !report.recorded ? /*#__PURE__*/ _jsx(Typography, {
|
|
832
|
+
variant: "body2",
|
|
833
|
+
color: "text.secondary",
|
|
834
|
+
children: 'No visits yet. File a page under this campaign — or give it the ' + 'utm_campaign labels its links carry — and visitors count here ' + 'from their next visit.'
|
|
835
|
+
}) : /*#__PURE__*/ _jsxs(Stack, {
|
|
836
|
+
spacing: 1,
|
|
837
|
+
children: [
|
|
838
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
839
|
+
direction: "row",
|
|
840
|
+
spacing: 3,
|
|
841
|
+
sx: {
|
|
842
|
+
flexWrap: 'wrap'
|
|
843
|
+
},
|
|
844
|
+
children: [
|
|
845
|
+
/*#__PURE__*/ _jsx(Figure, {
|
|
846
|
+
label: "First visits",
|
|
847
|
+
value: report.firstVisits,
|
|
848
|
+
note: `visitors reaching it for the first time in ${EMAIL_ATTRIBUTION_WINDOW_DAYS} days`
|
|
849
|
+
}),
|
|
850
|
+
/*#__PURE__*/ _jsx(Figure, {
|
|
851
|
+
label: "Page views",
|
|
852
|
+
value: report.views,
|
|
853
|
+
note: "views of the pages filed under it"
|
|
854
|
+
})
|
|
855
|
+
]
|
|
856
|
+
}),
|
|
857
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
858
|
+
variant: "caption",
|
|
859
|
+
color: "text.secondary",
|
|
860
|
+
children: 'A first visit is a visitor reaching this campaign — on a page filed ' + 'under it, or from a link labeled for it — who has not reached it ' + `in the last ${EMAIL_ATTRIBUTION_WINDOW_DAYS} days. It is counted ` + 'only where the visitor’s device may remember the visit, so ' + 'visitors who declined analytics storage are in the page views and ' + 'not here. Across every site the campaign is placed on' + (report.since ? `, since ${new Date(`${report.since}-01T00:00:00Z`).toLocaleDateString(undefined, {
|
|
861
|
+
month: 'long',
|
|
862
|
+
year: 'numeric',
|
|
863
|
+
timeZone: 'UTC'
|
|
864
|
+
})}.` : '.')
|
|
865
|
+
})
|
|
866
|
+
]
|
|
867
|
+
})
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
CampaignVisitsSection.displayName = 'CampaignVisitsSection';
|
|
780
871
|
|
|
781
872
|
//# sourceMappingURL=campaign-reach-sections.js.map
|