@aglyn/plugins-marketing 1.0.0-beta.179 → 1.0.0-beta.181

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +3 -3
  2. package/package.json +15 -15
  3. package/src/lib/components/campaign-conversions-card.js +22 -4
  4. package/src/lib/components/campaign-conversions-card.js.map +1 -1
  5. package/src/lib/components/campaign-detail-card.js +3 -4
  6. package/src/lib/components/campaign-detail-card.js.map +1 -1
  7. package/src/lib/components/campaign-report-card.js +3 -3
  8. package/src/lib/components/campaign-report-card.js.map +1 -1
  9. package/src/lib/components/email-compose-card.d.ts +5 -2
  10. package/src/lib/components/email-compose-card.js +2 -3
  11. package/src/lib/components/email-compose-card.js.map +1 -1
  12. package/src/lib/components/email-detail.d.ts +5 -2
  13. package/src/lib/components/email-detail.js +4 -4
  14. package/src/lib/components/email-detail.js.map +1 -1
  15. package/src/lib/components/email-messages-widget.d.ts +17 -2
  16. package/src/lib/components/email-messages-widget.js +33 -2
  17. package/src/lib/components/email-messages-widget.js.map +1 -1
  18. package/src/lib/components/emails-list-card.d.ts +9 -6
  19. package/src/lib/components/emails-list-card.js +9 -9
  20. package/src/lib/components/emails-list-card.js.map +1 -1
  21. package/src/lib/components/host-experiments-card.component.d.ts +2 -0
  22. package/src/lib/components/host-experiments-card.component.js +20 -25
  23. package/src/lib/components/host-experiments-card.component.js.map +1 -1
  24. package/src/lib/components/host-marketing-summary-card.component.d.ts +15 -0
  25. package/src/lib/components/host-marketing-summary-card.component.js +59 -68
  26. package/src/lib/components/host-marketing-summary-card.component.js.map +1 -1
  27. package/src/lib/components/host-overlays-card.component.js +15 -37
  28. package/src/lib/components/host-overlays-card.component.js.map +1 -1
  29. package/src/lib/components/inbox-campaigns-widget.d.ts +23 -0
  30. package/src/lib/components/inbox-campaigns-widget.js +63 -0
  31. package/src/lib/components/inbox-campaigns-widget.js.map +1 -0
  32. package/src/lib/components/marketing-aggregates.d.ts +95 -0
  33. package/src/lib/components/marketing-aggregates.js +114 -0
  34. package/src/lib/components/marketing-aggregates.js.map +1 -0
  35. package/src/lib/components/marketing-console-page.js +32 -27
  36. package/src/lib/components/marketing-console-page.js.map +1 -1
  37. package/src/lib/components/marketing-console-sections.d.ts +24 -13
  38. package/src/lib/components/marketing-console-sections.js +37 -12
  39. package/src/lib/components/marketing-console-sections.js.map +1 -1
  40. package/src/lib/components/org-conversions-section.d.ts +32 -0
  41. package/src/lib/components/org-conversions-section.js +130 -0
  42. package/src/lib/components/org-conversions-section.js.map +1 -0
  43. package/src/lib/components/org-experiments-card.d.ts +32 -0
  44. package/src/lib/components/org-experiments-card.js +407 -0
  45. package/src/lib/components/org-experiments-card.js.map +1 -0
  46. package/src/lib/components/org-marketing-overview-card.d.ts +34 -0
  47. package/src/lib/components/org-marketing-overview-card.js +342 -0
  48. package/src/lib/components/org-marketing-overview-card.js.map +1 -0
  49. package/src/lib/components/org-overlays-card.d.ts +35 -0
  50. package/src/lib/components/org-overlays-card.js +345 -0
  51. package/src/lib/components/org-overlays-card.js.map +1 -0
  52. package/src/lib/components/org-site-picker-button.d.ts +26 -0
  53. package/src/lib/components/org-site-picker-button.js +104 -0
  54. package/src/lib/components/org-site-picker-button.js.map +1 -0
  55. package/src/lib/components/overlay-labels.d.ts +38 -0
  56. package/src/lib/components/overlay-labels.js +55 -0
  57. package/src/lib/components/overlay-labels.js.map +1 -0
  58. package/src/lib/components/use-emails-hub-path.d.ts +17 -8
  59. package/src/lib/components/use-emails-hub-path.js +29 -11
  60. package/src/lib/components/use-emails-hub-path.js.map +1 -1
  61. package/src/lib/components/use-one-shot-reads.d.ts +55 -0
  62. package/src/lib/components/use-one-shot-reads.js +168 -0
  63. package/src/lib/components/use-one-shot-reads.js.map +1 -0
  64. package/src/lib/model/experiments.d.ts +35 -0
  65. package/src/lib/model/experiments.js +34 -0
  66. package/src/lib/model/experiments.js.map +1 -1
  67. package/src/lib/model/overlays.d.ts +13 -2
  68. package/src/lib/model/overlays.js +18 -6
  69. package/src/lib/model/overlays.js.map +1 -1
  70. package/src/lib/plugin.js +13 -8
  71. package/src/lib/plugin.js.map +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/components/org-conversions-section.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { pluginDocsHelp } from '@aglyn/aglyn'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { MenuItem, Stack, TextField, Typography } from '@mui/material'\nimport { useEffect, useState } from 'react'\nimport CampaignConversionsCard from './campaign-conversions-card'\nimport { useMarketingOrgMount } from './marketing-org-mount'\n\n/**\n * The session's memory of the site this section reads, per org. Session\n * storage rather than local, for the reason the CRM's own site pick gives:\n * it is a convenience for one sitting, and a site remembered for weeks\n * would open on figures the reader had forgotten choosing.\n */\nexport const conversionsSiteStorageKey = (orgId: string) =>\n `aglyn.marketing.conversionsSite.${orgId}`\n\nfunction readRememberedSite(orgId: string): string | null {\n try {\n return window.sessionStorage.getItem(conversionsSiteStorageKey(orgId))\n } catch {\n return null\n }\n}\n\nfunction writeRememberedSite(orgId: string, hostId: string): void {\n try {\n window.sessionStorage.setItem(conversionsSiteStorageKey(orgId), hostId)\n } catch {\n // A browser that refuses storage still gets the pick for this page.\n }\n}\n\nexport interface OrgConversionsSectionProps {\n /** The org Marketing hub, `/[orgSlug]/marketing` — each credited campaign opens under it. */\n basePath: string\n /** One campaign's conversions, from `…/conversions/{campaignId}`. */\n campaignId?: string\n}\n\n/**\n * The organization's Conversions section: ONE site's conversions at a time.\n *\n * A conversion is recorded by the site the visitor converted on — its\n * attribution records, its submissions, its bookings — and the card that\n * reads them is the site hub's own, unchanged, handed the site picked here.\n * Merging several sites into one list would put two different sites'\n * \"not credited\" figures under one heading, and there is no org-wide\n * collection of attributions to read instead.\n *\n * The pick defaults to the org's first site and is remembered for the\n * session. A remembered site the org no longer lists is forgotten rather\n * than read.\n */\nexport function OrgConversionsSection(props: OrgConversionsSectionProps) {\n const { basePath, campaignId } = props\n const mount = useMarketingOrgMount()\n const orgId = mount?.orgId ?? ''\n /*\n * Resolved after mount rather than in the initializer, so the first client\n * paint agrees with the server's — and the card waits for it, because\n * mounting on the first site and then remounting on the remembered one\n * would pay for the first site's reads on the way.\n */\n const [picked, setPicked] = useState<{\n orgId: string\n hostId: string | null\n } | null>(null)\n useEffect(() => {\n if (orgId) setPicked({ orgId, hostId: readRememberedSite(orgId) })\n }, [orgId])\n\n if (!mount || !mount.hostsReady || picked?.orgId !== orgId) return null\n\n const sites = mount.hosts\n const hostId =\n picked.hostId && sites.some((site) => site.id === picked.hostId)\n ? picked.hostId\n : (sites[0]?.id ?? null)\n\n if (!hostId) {\n return (\n <CardDisplay\n header=\"Conversions\"\n help={pluginDocsHelp('emailCampaigns', { anchor: '#the-campaign-report' })}\n contentGutterX\n contentGutterY\n >\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Conversions are recorded by the site a visitor converts on, and ' +\n 'this organization has no sites yet.'}\n </Typography>\n </CardDisplay>\n )\n }\n\n return (\n <Stack spacing={2}>\n {sites.length > 1 ? (\n <TextField\n select\n size=\"small\"\n label=\"Conversions on\"\n value={hostId}\n onChange={(event) => {\n const next = event.target.value\n setPicked({ orgId, hostId: next })\n writeRememberedSite(orgId, next)\n }}\n sx={{ maxWidth: 320 }}\n >\n {sites.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 {/* Keyed by site, so nothing one site's card was holding survives into another's. */}\n <CampaignConversionsCard\n key={hostId}\n hostId={hostId}\n basePath={basePath}\n campaignId={campaignId}\n />\n </Stack>\n )\n}\nOrgConversionsSection.displayName = 'OrgConversionsSection'\n\nexport default OrgConversionsSection\n"],"names":["pluginDocsHelp","CardDisplay","MenuItem","Stack","TextField","Typography","useEffect","useState","CampaignConversionsCard","useMarketingOrgMount","conversionsSiteStorageKey","orgId","readRememberedSite","window","sessionStorage","getItem","writeRememberedSite","hostId","setItem","OrgConversionsSection","props","sites","basePath","campaignId","mount","picked","setPicked","hostsReady","hosts","some","site","id","header","help","anchor","contentGutterX","contentGutterY","variant","color","spacing","length","select","size","label","value","onChange","event","next","target","sx","maxWidth","map","name","subdomain","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAASA,cAAc,QAAQ,eAAc;AAC7C,SAASC,WAAW,QAAQ,uBAAsB;AAClD,SAASC,QAAQ,EAAEC,KAAK,EAAEC,SAAS,EAAEC,UAAU,QAAQ,gBAAe;AACtE,SAASC,SAAS,EAAEC,QAAQ,QAAQ,QAAO;AAC3C,OAAOC,6BAA6B,iCAA6B;AACjE,SAASC,oBAAoB,QAAQ,2BAAuB;AAE5D;;;;;CAKC,GACD,OAAO,MAAMC,4BAA4B,CAACC,QACxC,CAAC,gCAAgC,EAAEA,OAAO,CAAA;AAE5C,SAASC,mBAAmBD,KAAa;IACvC,IAAI;QACF,OAAOE,OAAOC,cAAc,CAACC,OAAO,CAACL,0BAA0BC;IACjE,EAAE,eAAM;QACN,OAAO;IACT;AACF;AAEA,SAASK,oBAAoBL,KAAa,EAAEM,MAAc;IACxD,IAAI;QACFJ,OAAOC,cAAc,CAACI,OAAO,CAACR,0BAA0BC,QAAQM;IAClE,EAAE,eAAM;IACN,oEAAoE;IACtE;AACF;AASA;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASE,sBAAsBC,KAAiC;;QAwB9DC;IAvBP,MAAM,EAAEC,QAAQ,EAAEC,UAAU,EAAE,GAAGH;IACjC,MAAMI,QAAQf;IACd,MAAME,gBAAQa,yBAAAA,MAAOb,KAAK,mBAAI;IAC9B;;;;;GAKC,GACD,MAAM,CAACc,QAAQC,UAAU,GAAGnB,SAGlB;IACVD,UAAU;QACR,IAAIK,OAAOe,UAAU;YAAEf;YAAOM,QAAQL,mBAAmBD;QAAO;IAClE,GAAG;QAACA;KAAM;IAEV,IAAI,CAACa,SAAS,CAACA,MAAMG,UAAU,IAAIF,CAAAA,0BAAAA,OAAQd,KAAK,MAAKA,OAAO,OAAO;IAEnE,MAAMU,QAAQG,MAAMI,KAAK;IACzB,MAAMX,SACJQ,OAAOR,MAAM,IAAII,MAAMQ,IAAI,CAAC,CAACC,OAASA,KAAKC,EAAE,KAAKN,OAAOR,MAAM,IAC3DQ,OAAOR,MAAM,aACZI,UAAAA,KAAK,CAAC,EAAE,qBAARA,QAAUU,EAAE,oBAAI;IAEvB,IAAI,CAACd,QAAQ;QACX,qBACE,KAAChB;YACC+B,QAAO;YACPC,MAAMjC,eAAe,kBAAkB;gBAAEkC,QAAQ;YAAuB;YACxEC,cAAc;YACdC,cAAc;sBAEd,cAAA,KAAC/B;gBAAWgC,SAAQ;gBAAQC,OAAM;0BAC/B,qEACC;;;IAIV;IAEA,qBACE,MAACnC;QAAMoC,SAAS;;YACblB,MAAMmB,MAAM,GAAG,kBACd,KAACpC;gBACCqC,MAAM;gBACNC,MAAK;gBACLC,OAAM;gBACNC,OAAO3B;gBACP4B,UAAU,CAACC;oBACT,MAAMC,OAAOD,MAAME,MAAM,CAACJ,KAAK;oBAC/BlB,UAAU;wBAAEf;wBAAOM,QAAQ8B;oBAAK;oBAChC/B,oBAAoBL,OAAOoC;gBAC7B;gBACAE,IAAI;oBAAEC,UAAU;gBAAI;0BAEnB7B,MAAM8B,GAAG,CAAC,CAACrB,qBACV,KAAC5B;wBAAuB0C,OAAOd,KAAKC,EAAE;kCACnCD,KAAKsB,IAAI,IAAItB,KAAKuB,SAAS,IAAIvB,KAAKC,EAAE;uBAD1BD,KAAKC,EAAE;iBAKxB;0BAEJ,KAACvB;gBAECS,QAAQA;gBACRK,UAAUA;gBACVC,YAAYA;eAHPN;;;AAOb;AACAE,sBAAsBmC,WAAW,GAAG;AAEpC,eAAenC,sBAAqB"}
@@ -0,0 +1,32 @@
1
+ import { type AglynOrgBilling } from '@aglyn/aglyn';
2
+ /**
3
+ * How many A/B tests are read for each site.
4
+ *
5
+ * The same bargain as the org overlay list: a site runs a few tests at a
6
+ * time, a page here holds a page of sites' worth of them, and a site with
7
+ * more says so on its row and links to its own list, which pages through
8
+ * every one.
9
+ */
10
+ export declare const ORG_EXPERIMENTS_PER_SITE = 10;
11
+ export interface OrgExperimentsCardProps {
12
+ /** The org's plan, for the `abTesting` entitlement. */
13
+ org?: Partial<AglynOrgBilling>;
14
+ }
15
+ /**
16
+ * Every site's A/B tests, on the organization's Marketing hub.
17
+ *
18
+ * A test splits ONE site's traffic between versions of that site's screens,
19
+ * sections or emails, so it is created, started and decided on the site:
20
+ * the New button and each row's Open go to the site's own A/B testing
21
+ * section. What the org hub adds is the view across sites, and each test's
22
+ * results — read when somebody asks for them, one document per variant,
23
+ * rather than for every test on every visit.
24
+ *
25
+ * Grouped by site and paged by site, like the org overlay list, so turning
26
+ * the page reads the next sites and nothing else.
27
+ */
28
+ export declare function OrgExperimentsCard(props: OrgExperimentsCardProps): import("react").JSX.Element;
29
+ export declare namespace OrgExperimentsCard {
30
+ var displayName: string;
31
+ }
32
+ export default OrgExperimentsCard;
@@ -0,0 +1,407 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ 'use client';
17
+ import { _ as _extends } from "@swc/helpers/_/_extends";
18
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
19
+ import { checkEntitlement, pluginDocsHelp } from "@aglyn/aglyn";
20
+ import { describeVariantComparison, experimentResultRows } from "../model/index.js";
21
+ import { AppLink, CardDisplay } from "@aglyn/shared-ui-jsx";
22
+ import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
23
+ import { ScrollTable } from "@aglyn/shared-ui-jsx/components/scroll-table.component";
24
+ import { useSnackbar } from "@aglyn/shared-ui-snackstack";
25
+ import { Alert, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, Stack, TableBody, TableCell, TableHead, TableRow, Typography } from "@mui/material";
26
+ import { collection, getDocs, limit, orderBy, query } from "firebase/firestore";
27
+ import { Fragment, useState } from "react";
28
+ import { useFirestore } from "@aglyn/tenant-feature-instance";
29
+ import { ceilingedWindow } from "@aglyn/tenant-feature-instance/hooks/host-collection-queries";
30
+ import { ExperimentResultZone } from "./experiment-zones.js";
31
+ import { EXPERIMENT_STATUS_COLORS } from "./host-experiments-card.component.js";
32
+ import { useMarketingOrgMount } from "./marketing-org-mount.js";
33
+ import { OrgSitePickerButton, orgSiteSectionHref } from "./org-site-picker-button.js";
34
+ import { ORG_SITES_PER_PAGE, useOrgSiteReads } from "./use-one-shot-reads.js";
35
+ /**
36
+ * How many A/B tests are read for each site.
37
+ *
38
+ * The same bargain as the org overlay list: a site runs a few tests at a
39
+ * time, a page here holds a page of sites' worth of them, and a site with
40
+ * more says so on its row and links to its own list, which pages through
41
+ * every one.
42
+ */ export const ORG_EXPERIMENTS_PER_SITE = 10;
43
+ /**
44
+ * One site's tests, ordered by name as the site's own list pages them —
45
+ * `name` is on every experiment, since the one editor that creates them
46
+ * refuses a save without it.
47
+ */ async function readSiteExperiments(firestore, hostId) {
48
+ const snapshot = await getDocs(query(collection(firestore, 'hosts', hostId, 'experiments'), orderBy('name'), limit(ORG_EXPERIMENTS_PER_SITE + 1)));
49
+ const { rows, truncated } = ceilingedWindow(snapshot.docs.map((entry)=>_extends({}, entry.data(), {
50
+ $id: entry.id
51
+ })), ORG_EXPERIMENTS_PER_SITE);
52
+ return {
53
+ rows: rows.filter((row)=>!row.deletedAt),
54
+ truncated
55
+ };
56
+ }
57
+ const TARGET_LABEL = {
58
+ screen: 'Screen',
59
+ section: 'Section',
60
+ email: 'Email'
61
+ };
62
+ /** Columns in a row, so a site's own line can span all of them. */ const COLUMNS = 5;
63
+ /**
64
+ * Every site's A/B tests, on the organization's Marketing hub.
65
+ *
66
+ * A test splits ONE site's traffic between versions of that site's screens,
67
+ * sections or emails, so it is created, started and decided on the site:
68
+ * the New button and each row's Open go to the site's own A/B testing
69
+ * section. What the org hub adds is the view across sites, and each test's
70
+ * results — read when somebody asks for them, one document per variant,
71
+ * rather than for every test on every visit.
72
+ *
73
+ * Grouped by site and paged by site, like the org overlay list, so turning
74
+ * the page reads the next sites and nothing else.
75
+ */ export function OrgExperimentsCard(props) {
76
+ var _ref, _ref1;
77
+ var _results_experiment_name;
78
+ const { org } = props;
79
+ const mount = useMarketingOrgMount();
80
+ const firestore = useFirestore();
81
+ const { enqueueSnackbar } = useSnackbar();
82
+ const entitled = checkEntitlement(org, 'abTesting');
83
+ const sites = (_ref = mount == null ? void 0 : mount.hosts) != null ? _ref : [];
84
+ const [page, setPage] = useState(0);
85
+ const pageSites = sites.slice(page * ORG_SITES_PER_PAGE, (page + 1) * ORG_SITES_PER_PAGE);
86
+ const { reads } = useOrgSiteReads(entitled ? pageSites.map((site)=>site.id) : [], (hostId)=>readSiteExperiments(firestore, hostId), [
87
+ firestore,
88
+ entitled
89
+ ]);
90
+ const [results, setResults] = useState(null);
91
+ const [opening, setOpening] = useState(null);
92
+ /** The per-variant counters of one test, `experiments/{id}/stats`. */ const openResults = async (hostId, siteName, experiment)=>{
93
+ setOpening(experiment.$id);
94
+ try {
95
+ const snapshot = await getDocs(collection(firestore, 'hosts', hostId, 'experiments', experiment.$id, 'stats'));
96
+ const stats = {};
97
+ snapshot.forEach((entry)=>{
98
+ stats[entry.id] = entry.data();
99
+ });
100
+ setResults({
101
+ hostId,
102
+ siteName,
103
+ experiment,
104
+ stats
105
+ });
106
+ } catch (error) {
107
+ console.error(error);
108
+ enqueueSnackbar('The results could not be read', {
109
+ variant: 'error'
110
+ });
111
+ } finally{
112
+ setOpening(null);
113
+ }
114
+ };
115
+ const resultsHref = results && mount ? orgSiteSectionHref(mount, results.hostId, 'experiments') : null;
116
+ return /*#__PURE__*/ _jsxs(CardDisplay, {
117
+ header: "Experiments",
118
+ subheader: "Every site in this organization",
119
+ help: pluginDocsHelp('emailCampaigns', {
120
+ anchor: '#experiments-across-sites'
121
+ }),
122
+ contentGutterX: true,
123
+ contentGutterY: true,
124
+ contentBordered: "all",
125
+ children: [
126
+ !entitled ? /*#__PURE__*/ _jsx(Alert, {
127
+ severity: "info",
128
+ children: 'A/B experiments are included in the Business plan — see ' + 'Billing to upgrade.'
129
+ }) : !mount || !mount.hostsReady ? null : !sites.length ? /*#__PURE__*/ _jsx(Typography, {
130
+ variant: "body2",
131
+ color: "text.secondary",
132
+ children: 'This organization has no sites yet. A test splits one site’s ' + 'traffic, so it is created on a site.'
133
+ }) : /*#__PURE__*/ _jsxs(Stack, {
134
+ spacing: 2,
135
+ children: [
136
+ /*#__PURE__*/ _jsxs(Stack, {
137
+ direction: "row",
138
+ spacing: 2,
139
+ sx: {
140
+ alignItems: 'center',
141
+ justifyContent: 'space-between'
142
+ },
143
+ children: [
144
+ /*#__PURE__*/ _jsx(Typography, {
145
+ variant: "body2",
146
+ color: "text.secondary",
147
+ children: 'Each test splits one site’s traffic. Read any test’s results ' + 'here; create, start and decide them on the site.'
148
+ }),
149
+ /*#__PURE__*/ _jsx(OrgSitePickerButton, {
150
+ mount: mount,
151
+ section: "experiments",
152
+ label: "New experiment"
153
+ })
154
+ ]
155
+ }),
156
+ /*#__PURE__*/ _jsxs(ScrollTable, {
157
+ size: "small",
158
+ "aria-label": "Experiments by site",
159
+ children: [
160
+ /*#__PURE__*/ _jsx(TableHead, {
161
+ children: /*#__PURE__*/ _jsxs(TableRow, {
162
+ children: [
163
+ /*#__PURE__*/ _jsx(TableCell, {
164
+ children: 'Experiment'
165
+ }),
166
+ /*#__PURE__*/ _jsx(TableCell, {
167
+ children: 'Tests'
168
+ }),
169
+ /*#__PURE__*/ _jsx(TableCell, {
170
+ children: 'Variants'
171
+ }),
172
+ /*#__PURE__*/ _jsx(TableCell, {
173
+ children: 'Status'
174
+ }),
175
+ /*#__PURE__*/ _jsx(TableCell, {
176
+ align: "right",
177
+ children: 'Actions'
178
+ })
179
+ ]
180
+ })
181
+ }),
182
+ /*#__PURE__*/ _jsx(TableBody, {
183
+ children: pageSites.map((site)=>{
184
+ var _ref, _ref1;
185
+ const read = reads.get(site.id);
186
+ const answer = (_ref = read == null ? void 0 : read.value) != null ? _ref : null;
187
+ const href = orgSiteSectionHref(mount, site.id, 'experiments');
188
+ const name = site.name || site.subdomain || site.id;
189
+ return /*#__PURE__*/ _jsxs(Fragment, {
190
+ children: [
191
+ /*#__PURE__*/ _jsx(TableRow, {
192
+ children: /*#__PURE__*/ _jsx(TableCell, {
193
+ colSpan: COLUMNS,
194
+ children: /*#__PURE__*/ _jsxs(Stack, {
195
+ direction: "row",
196
+ spacing: 2,
197
+ sx: {
198
+ alignItems: 'baseline',
199
+ flexWrap: 'wrap'
200
+ },
201
+ children: [
202
+ /*#__PURE__*/ _jsx(Typography, {
203
+ variant: "subtitle2",
204
+ children: href ? /*#__PURE__*/ _jsx(AppLink, {
205
+ href: href,
206
+ children: name
207
+ }) : name
208
+ }),
209
+ (answer == null ? void 0 : answer.truncated) ? /*#__PURE__*/ _jsx(Typography, {
210
+ variant: "caption",
211
+ color: "text.secondary",
212
+ children: `More than ${ORG_EXPERIMENTS_PER_SITE} on this ` + 'site — these are the first by name; the ' + 'site’s own list has every one.'
213
+ }) : null
214
+ ]
215
+ })
216
+ })
217
+ }),
218
+ (read == null ? void 0 : read.status) === 'error' ? /*#__PURE__*/ _jsx(TableRow, {
219
+ children: /*#__PURE__*/ _jsx(TableCell, {
220
+ colSpan: COLUMNS,
221
+ children: /*#__PURE__*/ _jsx(Typography, {
222
+ variant: "body2",
223
+ color: "text.secondary",
224
+ children: 'This site’s experiments could not be read.'
225
+ })
226
+ })
227
+ }) : answer && !answer.rows.length ? /*#__PURE__*/ _jsx(TableRow, {
228
+ children: /*#__PURE__*/ _jsx(TableCell, {
229
+ colSpan: COLUMNS,
230
+ children: /*#__PURE__*/ _jsx(Typography, {
231
+ variant: "body2",
232
+ color: "text.secondary",
233
+ children: 'No experiments on this site.'
234
+ })
235
+ })
236
+ }) : ((_ref1 = answer == null ? void 0 : answer.rows) != null ? _ref1 : []).map((experiment)=>{
237
+ var _TARGET_LABEL_experiment_target, _experiment_variants, _EXPERIMENT_STATUS_COLORS_experiment_status;
238
+ return /*#__PURE__*/ _jsxs(TableRow, {
239
+ children: [
240
+ /*#__PURE__*/ _jsx(TableCell, {
241
+ children: experiment.name || experiment.$id
242
+ }),
243
+ /*#__PURE__*/ _jsx(TableCell, {
244
+ children: (_TARGET_LABEL_experiment_target = TARGET_LABEL[experiment.target]) != null ? _TARGET_LABEL_experiment_target : experiment.target
245
+ }),
246
+ /*#__PURE__*/ _jsx(TableCell, {
247
+ children: ((_experiment_variants = experiment.variants) != null ? _experiment_variants : []).length.toLocaleString()
248
+ }),
249
+ /*#__PURE__*/ _jsx(TableCell, {
250
+ children: /*#__PURE__*/ _jsx(Chip, {
251
+ size: "small",
252
+ color: (_EXPERIMENT_STATUS_COLORS_experiment_status = EXPERIMENT_STATUS_COLORS[experiment.status]) != null ? _EXPERIMENT_STATUS_COLORS_experiment_status : 'default',
253
+ label: experiment.status
254
+ })
255
+ }),
256
+ /*#__PURE__*/ _jsxs(TableCell, {
257
+ align: "right",
258
+ children: [
259
+ /*#__PURE__*/ _jsx(Button, {
260
+ size: "small",
261
+ color: "primary",
262
+ disabled: opening === experiment.$id,
263
+ "aria-label": `Results for ${experiment.name || experiment.$id} on ${name}`,
264
+ onClick: ()=>void openResults(site.id, name, experiment),
265
+ children: 'Results'
266
+ }),
267
+ href ? /*#__PURE__*/ _jsx(AppLink, {
268
+ componentVariant: "button",
269
+ href: href,
270
+ size: "small",
271
+ color: "primary",
272
+ "aria-label": `Open ${experiment.name || experiment.$id} on ${name}`,
273
+ children: 'Open'
274
+ }) : null
275
+ ]
276
+ })
277
+ ]
278
+ }, experiment.$id);
279
+ })
280
+ ]
281
+ }, site.id);
282
+ })
283
+ })
284
+ ]
285
+ }),
286
+ /*#__PURE__*/ _jsx(ListPagination, {
287
+ page: page,
288
+ pageSize: ORG_SITES_PER_PAGE,
289
+ rowCount: pageSites.length,
290
+ count: sites.length,
291
+ onPageChange: setPage,
292
+ labelDisplayedRows: ({ from, to, count })=>`Sites ${from}–${to} of ${count}`
293
+ })
294
+ ]
295
+ }),
296
+ /*#__PURE__*/ _jsxs(Dialog, {
297
+ open: Boolean(results),
298
+ onClose: ()=>setResults(null),
299
+ maxWidth: "xs",
300
+ fullWidth: true,
301
+ children: [
302
+ /*#__PURE__*/ _jsxs(DialogTitle, {
303
+ children: [
304
+ `Results — ${(_ref1 = results == null ? void 0 : results.experiment.name) != null ? _ref1 : ''}`,
305
+ (results == null ? void 0 : results.experiment.autoCompleted) ? /*#__PURE__*/ _jsx(Chip, {
306
+ size: "small",
307
+ color: "success",
308
+ label: "Auto-completed",
309
+ sx: {
310
+ ml: 1
311
+ }
312
+ }) : null
313
+ ]
314
+ }),
315
+ /*#__PURE__*/ _jsxs(DialogContent, {
316
+ children: [
317
+ /*#__PURE__*/ _jsx(Typography, {
318
+ variant: "caption",
319
+ color: "text.secondary",
320
+ children: results ? `On ${results.siteName}` : ''
321
+ }),
322
+ /*#__PURE__*/ _jsxs(ScrollTable, {
323
+ size: "small",
324
+ children: [
325
+ /*#__PURE__*/ _jsx(TableHead, {
326
+ children: /*#__PURE__*/ _jsxs(TableRow, {
327
+ children: [
328
+ /*#__PURE__*/ _jsx(TableCell, {
329
+ children: 'Variant'
330
+ }),
331
+ /*#__PURE__*/ _jsx(TableCell, {
332
+ children: 'Exposures'
333
+ }),
334
+ /*#__PURE__*/ _jsx(TableCell, {
335
+ children: 'Conversions'
336
+ }),
337
+ /*#__PURE__*/ _jsx(TableCell, {
338
+ children: 'Rate'
339
+ }),
340
+ /*#__PURE__*/ _jsx(TableCell, {
341
+ children: 'vs control'
342
+ })
343
+ ]
344
+ })
345
+ }),
346
+ /*#__PURE__*/ _jsx(TableBody, {
347
+ children: (results ? experimentResultRows(results.experiment, results.stats) : []).map(({ variant, summary, comparison, leader, winner })=>{
348
+ var _variant_name;
349
+ return /*#__PURE__*/ _jsxs(TableRow, {
350
+ selected: leader,
351
+ children: [
352
+ /*#__PURE__*/ _jsxs(TableCell, {
353
+ children: [
354
+ (_variant_name = variant.name) != null ? _variant_name : variant.id.toUpperCase(),
355
+ winner ? ' 🏆' : leader ? ' ▲' : ''
356
+ ]
357
+ }),
358
+ /*#__PURE__*/ _jsx(TableCell, {
359
+ children: summary.exposures
360
+ }),
361
+ /*#__PURE__*/ _jsx(TableCell, {
362
+ children: summary.conversions
363
+ }),
364
+ /*#__PURE__*/ _jsx(TableCell, {
365
+ children: `${(summary.rate * 100).toFixed(1)}%`
366
+ }),
367
+ /*#__PURE__*/ _jsx(TableCell, {
368
+ children: describeVariantComparison(comparison)
369
+ })
370
+ ]
371
+ }, variant.id);
372
+ })
373
+ })
374
+ ]
375
+ }),
376
+ results ? /*#__PURE__*/ _jsx(ExperimentResultZone, {
377
+ hostId: results.hostId,
378
+ experimentId: results.experiment.$id,
379
+ test: ((_results_experiment_name = results.experiment.name) == null ? void 0 : _results_experiment_name.trim()) || results.experiment.$id
380
+ }) : null
381
+ ]
382
+ }),
383
+ /*#__PURE__*/ _jsxs(DialogActions, {
384
+ children: [
385
+ resultsHref ? /*#__PURE__*/ _jsx(AppLink, {
386
+ componentVariant: "button",
387
+ href: resultsHref,
388
+ size: "small",
389
+ color: "primary",
390
+ children: 'Open on the site'
391
+ }) : null,
392
+ /*#__PURE__*/ _jsx(Button, {
393
+ color: "inherit",
394
+ onClick: ()=>setResults(null),
395
+ children: 'Close'
396
+ })
397
+ ]
398
+ })
399
+ ]
400
+ })
401
+ ]
402
+ });
403
+ }
404
+ OrgExperimentsCard.displayName = 'OrgExperimentsCard';
405
+ export default OrgExperimentsCard;
406
+
407
+ //# sourceMappingURL=org-experiments-card.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/components/org-experiments-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 type AglynOrgBilling,\n checkEntitlement,\n pluginDocsHelp,\n} from '@aglyn/aglyn'\nimport {\n describeVariantComparison,\n experimentResultRows,\n type ExperimentStatsByVariant,\n type HostExperiment,\n} from '../model'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Alert,\n Button,\n Chip,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n getDocs,\n limit,\n orderBy,\n query,\n type Firestore,\n} from 'firebase/firestore'\nimport { Fragment, useState } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { ceilingedWindow } from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport { ExperimentResultZone } from './experiment-zones'\nimport { EXPERIMENT_STATUS_COLORS } from './host-experiments-card.component'\nimport { useMarketingOrgMount } from './marketing-org-mount'\nimport {\n OrgSitePickerButton,\n orgSiteSectionHref,\n} from './org-site-picker-button'\nimport { ORG_SITES_PER_PAGE, useOrgSiteReads } from './use-one-shot-reads'\n\n/**\n * How many A/B tests are read for each site.\n *\n * The same bargain as the org overlay list: a site runs a few tests at a\n * time, a page here holds a page of sites' worth of them, and a site with\n * more says so on its row and links to its own list, which pages through\n * every one.\n */\nexport const ORG_EXPERIMENTS_PER_SITE = 10\n\ntype ExperimentRow = HostExperiment & { $id: string; deletedAt?: unknown }\n\ninterface SiteExperiments {\n rows: ExperimentRow[]\n truncated: boolean\n}\n\n/**\n * One site's tests, ordered by name as the site's own list pages them —\n * `name` is on every experiment, since the one editor that creates them\n * refuses a save without it.\n */\nasync function readSiteExperiments(\n firestore: Firestore,\n hostId: string,\n): Promise<SiteExperiments> {\n const snapshot = await getDocs(\n query(\n collection(firestore, 'hosts', hostId, 'experiments'),\n orderBy('name'),\n limit(ORG_EXPERIMENTS_PER_SITE + 1),\n ),\n )\n const { rows, truncated } = ceilingedWindow(\n snapshot.docs.map(\n (entry) => ({ ...(entry.data() as HostExperiment), $id: entry.id }) as ExperimentRow,\n ),\n ORG_EXPERIMENTS_PER_SITE,\n )\n return { rows: rows.filter((row) => !row.deletedAt), truncated }\n}\n\nconst TARGET_LABEL: Record<string, string> = {\n screen: 'Screen',\n section: 'Section',\n email: 'Email',\n}\n\n/** Columns in a row, so a site's own line can span all of them. */\nconst COLUMNS = 5\n\nexport interface OrgExperimentsCardProps {\n /** The org's plan, for the `abTesting` entitlement. */\n org?: Partial<AglynOrgBilling>\n}\n\n/**\n * Every site's A/B tests, on the organization's Marketing hub.\n *\n * A test splits ONE site's traffic between versions of that site's screens,\n * sections or emails, so it is created, started and decided on the site:\n * the New button and each row's Open go to the site's own A/B testing\n * section. What the org hub adds is the view across sites, and each test's\n * results — read when somebody asks for them, one document per variant,\n * rather than for every test on every visit.\n *\n * Grouped by site and paged by site, like the org overlay list, so turning\n * the page reads the next sites and nothing else.\n */\nexport function OrgExperimentsCard(props: OrgExperimentsCardProps) {\n const { org } = props\n const mount = useMarketingOrgMount()\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const entitled = checkEntitlement(org, 'abTesting')\n\n const sites = mount?.hosts ?? []\n const [page, setPage] = useState(0)\n const pageSites = sites.slice(\n page * ORG_SITES_PER_PAGE,\n (page + 1) * ORG_SITES_PER_PAGE,\n )\n const { reads } = useOrgSiteReads<SiteExperiments>(\n entitled ? pageSites.map((site) => site.id) : [],\n (hostId) => readSiteExperiments(firestore, hostId),\n [firestore, entitled],\n )\n\n const [results, setResults] = useState<{\n hostId: string\n siteName: string\n experiment: ExperimentRow\n stats: ExperimentStatsByVariant\n } | null>(null)\n const [opening, setOpening] = useState<string | null>(null)\n\n /** The per-variant counters of one test, `experiments/{id}/stats`. */\n const openResults = async (\n hostId: string,\n siteName: string,\n experiment: ExperimentRow,\n ) => {\n setOpening(experiment.$id)\n try {\n const snapshot = await getDocs(\n collection(\n firestore,\n 'hosts',\n hostId,\n 'experiments',\n experiment.$id,\n 'stats',\n ),\n )\n const stats: ExperimentStatsByVariant = {}\n snapshot.forEach((entry) => {\n stats[entry.id] = entry.data()\n })\n setResults({ hostId, siteName, experiment, stats })\n } catch (error) {\n console.error(error)\n enqueueSnackbar('The results could not be read', { variant: 'error' })\n } finally {\n setOpening(null)\n }\n }\n\n const resultsHref =\n results && mount\n ? orgSiteSectionHref(mount, results.hostId, 'experiments')\n : null\n\n return (\n <CardDisplay\n header=\"Experiments\"\n subheader=\"Every site in this organization\"\n help={pluginDocsHelp('emailCampaigns', {\n anchor: '#experiments-across-sites',\n })}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n >\n {!entitled ? (\n <Alert severity=\"info\">\n {'A/B experiments are included in the Business plan — see ' +\n 'Billing to upgrade.'}\n </Alert>\n ) : !mount || !mount.hostsReady ? null : !sites.length ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This organization has no sites yet. A test splits one site’s ' +\n 'traffic, so it is created on a site.'}\n </Typography>\n ) : (\n <Stack spacing={2}>\n <Stack\n direction=\"row\"\n spacing={2}\n sx={{ alignItems: 'center', justifyContent: 'space-between' }}\n >\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Each test splits one site’s traffic. Read any test’s results ' +\n 'here; create, start and decide them on the site.'}\n </Typography>\n <OrgSitePickerButton\n mount={mount}\n section=\"experiments\"\n label=\"New experiment\"\n />\n </Stack>\n <ScrollTable size=\"small\" aria-label=\"Experiments by site\">\n <TableHead>\n <TableRow>\n <TableCell>{'Experiment'}</TableCell>\n <TableCell>{'Tests'}</TableCell>\n <TableCell>{'Variants'}</TableCell>\n <TableCell>{'Status'}</TableCell>\n <TableCell align=\"right\">{'Actions'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {pageSites.map((site) => {\n const read = reads.get(site.id)\n const answer = read?.value ?? null\n const href = orgSiteSectionHref(mount, site.id, 'experiments')\n const name = site.name || site.subdomain || site.id\n return (\n <Fragment key={site.id}>\n {/* The site's own line: its name, and what was not read. */}\n <TableRow>\n <TableCell colSpan={COLUMNS}>\n <Stack\n direction=\"row\"\n spacing={2}\n sx={{ alignItems: 'baseline', flexWrap: 'wrap' }}\n >\n <Typography variant=\"subtitle2\">\n {href ? <AppLink href={href}>{name}</AppLink> : name}\n </Typography>\n {answer?.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`More than ${ORG_EXPERIMENTS_PER_SITE} on this ` +\n 'site — these are the first by name; the ' +\n 'site’s own list has every one.'}\n </Typography>\n ) : null}\n </Stack>\n </TableCell>\n </TableRow>\n {read?.status === 'error' ? (\n <TableRow>\n <TableCell colSpan={COLUMNS}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This site’s experiments could not be read.'}\n </Typography>\n </TableCell>\n </TableRow>\n ) : answer && !answer.rows.length ? (\n <TableRow>\n <TableCell colSpan={COLUMNS}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No experiments on this site.'}\n </Typography>\n </TableCell>\n </TableRow>\n ) : (\n (answer?.rows ?? []).map((experiment) => (\n <TableRow key={experiment.$id}>\n <TableCell>{experiment.name || experiment.$id}</TableCell>\n <TableCell>\n {TARGET_LABEL[experiment.target] ?? experiment.target}\n </TableCell>\n <TableCell>\n {(experiment.variants ?? []).length.toLocaleString()}\n </TableCell>\n <TableCell>\n <Chip\n size=\"small\"\n color={\n EXPERIMENT_STATUS_COLORS[experiment.status] ??\n 'default'\n }\n label={experiment.status}\n />\n </TableCell>\n <TableCell align=\"right\">\n <Button\n size=\"small\"\n color=\"primary\"\n disabled={opening === experiment.$id}\n aria-label={`Results for ${experiment.name || experiment.$id} on ${name}`}\n onClick={() =>\n void openResults(site.id, name, experiment)\n }\n >\n {'Results'}\n </Button>\n {/* A site the shell could not address has no section to open. */}\n {href ? (\n <AppLink\n componentVariant=\"button\"\n href={href}\n size=\"small\"\n color=\"primary\"\n aria-label={`Open ${experiment.name || experiment.$id} on ${name}`}\n >\n {'Open'}\n </AppLink>\n ) : null}\n </TableCell>\n </TableRow>\n ))\n )}\n </Fragment>\n )\n })}\n </TableBody>\n </ScrollTable>\n <ListPagination\n page={page}\n pageSize={ORG_SITES_PER_PAGE}\n rowCount={pageSites.length}\n count={sites.length}\n onPageChange={setPage}\n labelDisplayedRows={({ from, to, count }) =>\n `Sites ${from}–${to} of ${count}`\n }\n />\n </Stack>\n )}\n\n {/*\n The site card's results, read-only: picking a winner changes what a\n site serves, so it is done on the site, one link away.\n */}\n <Dialog\n open={Boolean(results)}\n onClose={() => setResults(null)}\n maxWidth=\"xs\"\n fullWidth\n >\n <DialogTitle>\n {`Results — ${results?.experiment.name ?? ''}`}\n {results?.experiment.autoCompleted ? (\n <Chip\n size=\"small\"\n color=\"success\"\n label=\"Auto-completed\"\n sx={{ ml: 1 }}\n />\n ) : null}\n </DialogTitle>\n <DialogContent>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {results ? `On ${results.siteName}` : ''}\n </Typography>\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Variant'}</TableCell>\n <TableCell>{'Exposures'}</TableCell>\n <TableCell>{'Conversions'}</TableCell>\n <TableCell>{'Rate'}</TableCell>\n <TableCell>{'vs control'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {(results\n ? experimentResultRows(results.experiment, results.stats)\n : []\n ).map(({ variant, summary, comparison, leader, winner }) => (\n <TableRow key={variant.id} selected={leader}>\n <TableCell>\n {variant.name ?? variant.id.toUpperCase()}\n {winner ? ' 🏆' : leader ? ' ▲' : ''}\n </TableCell>\n <TableCell>{summary.exposures}</TableCell>\n <TableCell>{summary.conversions}</TableCell>\n <TableCell>{`${(summary.rate * 100).toFixed(1)}%`}</TableCell>\n <TableCell>{describeVariantComparison(comparison)}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n {results ? (\n <ExperimentResultZone\n hostId={results.hostId}\n experimentId={results.experiment.$id}\n test={results.experiment.name?.trim() || results.experiment.$id}\n />\n ) : null}\n </DialogContent>\n <DialogActions>\n {resultsHref ? (\n <AppLink\n componentVariant=\"button\"\n href={resultsHref}\n size=\"small\"\n color=\"primary\"\n >\n {'Open on the site'}\n </AppLink>\n ) : null}\n <Button color=\"inherit\" onClick={() => setResults(null)}>\n {'Close'}\n </Button>\n </DialogActions>\n </Dialog>\n </CardDisplay>\n )\n}\nOrgExperimentsCard.displayName = 'OrgExperimentsCard'\n\nexport default OrgExperimentsCard\n"],"names":["checkEntitlement","pluginDocsHelp","describeVariantComparison","experimentResultRows","AppLink","CardDisplay","ListPagination","ScrollTable","useSnackbar","Alert","Button","Chip","Dialog","DialogActions","DialogContent","DialogTitle","Stack","TableBody","TableCell","TableHead","TableRow","Typography","collection","getDocs","limit","orderBy","query","Fragment","useState","useFirestore","ceilingedWindow","ExperimentResultZone","EXPERIMENT_STATUS_COLORS","useMarketingOrgMount","OrgSitePickerButton","orgSiteSectionHref","ORG_SITES_PER_PAGE","useOrgSiteReads","ORG_EXPERIMENTS_PER_SITE","readSiteExperiments","firestore","hostId","snapshot","rows","truncated","docs","map","entry","data","$id","id","filter","row","deletedAt","TARGET_LABEL","screen","section","email","COLUMNS","OrgExperimentsCard","props","results","org","mount","enqueueSnackbar","entitled","sites","hosts","page","setPage","pageSites","slice","reads","site","setResults","opening","setOpening","openResults","siteName","experiment","stats","forEach","error","console","variant","resultsHref","header","subheader","help","anchor","contentGutterX","contentGutterY","contentBordered","severity","hostsReady","length","color","spacing","direction","sx","alignItems","justifyContent","label","size","aria-label","align","read","get","answer","value","href","name","subdomain","colSpan","flexWrap","status","target","variants","toLocaleString","disabled","onClick","componentVariant","pageSize","rowCount","count","onPageChange","labelDisplayedRows","from","to","open","Boolean","onClose","maxWidth","fullWidth","autoCompleted","ml","summary","comparison","leader","winner","selected","toUpperCase","exposures","conversions","rate","toFixed","experimentId","test","trim","displayName"],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ import { type AglynOrgBilling } from '@aglyn/aglyn';
2
+ /**
3
+ * How many sites the Overview reads figures for.
4
+ *
5
+ * The one read on this card that grows with the organization: four
6
+ * aggregations a site. Capped, the landing section of the org hub costs four
7
+ * org-wide aggregations and four for each site up to this number, however
8
+ * many sites there are; the sites past it are counted in a note rather than
9
+ * read.
10
+ */
11
+ export declare const ORG_OVERVIEW_SITE_CAP = 25;
12
+ export interface OrgMarketingOverviewCardProps {
13
+ /** The org's plan, for which channels it carries. */
14
+ org?: Partial<AglynOrgBilling>;
15
+ }
16
+ /**
17
+ * Marketing at a glance, over every site — the landing section of the
18
+ * organization's Marketing hub.
19
+ *
20
+ * The email figures come from ONE collection: campaign sends belong to the
21
+ * organization (`orgs/{orgId}/campaigns`), so four aggregations over it
22
+ * answer for every site at once. Overlays and A/B tests are a SITE's —
23
+ * drawn on its pages, splitting its traffic — so their figures are read per
24
+ * site, each site one row, and each row opens that site's own Overview.
25
+ *
26
+ * A channel the plan does not carry is left out rather than read: an
27
+ * organization without overlays or A/B testing pays for no per-site reads
28
+ * at all, and its landing is the email figures alone.
29
+ */
30
+ export declare function OrgMarketingOverviewCard(props: OrgMarketingOverviewCardProps): import("react").JSX.Element;
31
+ export declare namespace OrgMarketingOverviewCard {
32
+ var displayName: string;
33
+ }
34
+ export default OrgMarketingOverviewCard;