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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +3 -3
  2. package/package.json +15 -15
  3. package/src/lib/components/campaign-conversions-card.js +22 -4
  4. package/src/lib/components/campaign-conversions-card.js.map +1 -1
  5. package/src/lib/components/campaign-detail-card.js +3 -4
  6. package/src/lib/components/campaign-detail-card.js.map +1 -1
  7. package/src/lib/components/campaign-report-card.js +3 -3
  8. package/src/lib/components/campaign-report-card.js.map +1 -1
  9. package/src/lib/components/email-compose-card.d.ts +5 -2
  10. package/src/lib/components/email-compose-card.js +2 -3
  11. package/src/lib/components/email-compose-card.js.map +1 -1
  12. package/src/lib/components/email-detail.d.ts +5 -2
  13. package/src/lib/components/email-detail.js +4 -4
  14. package/src/lib/components/email-detail.js.map +1 -1
  15. package/src/lib/components/email-messages-widget.d.ts +17 -2
  16. package/src/lib/components/email-messages-widget.js +33 -2
  17. package/src/lib/components/email-messages-widget.js.map +1 -1
  18. package/src/lib/components/emails-list-card.d.ts +9 -6
  19. package/src/lib/components/emails-list-card.js +9 -9
  20. package/src/lib/components/emails-list-card.js.map +1 -1
  21. package/src/lib/components/host-experiments-card.component.d.ts +2 -0
  22. package/src/lib/components/host-experiments-card.component.js +20 -25
  23. package/src/lib/components/host-experiments-card.component.js.map +1 -1
  24. package/src/lib/components/host-marketing-summary-card.component.d.ts +15 -0
  25. package/src/lib/components/host-marketing-summary-card.component.js +59 -68
  26. package/src/lib/components/host-marketing-summary-card.component.js.map +1 -1
  27. package/src/lib/components/host-overlays-card.component.js +15 -37
  28. package/src/lib/components/host-overlays-card.component.js.map +1 -1
  29. package/src/lib/components/inbox-campaigns-widget.d.ts +23 -0
  30. package/src/lib/components/inbox-campaigns-widget.js +63 -0
  31. package/src/lib/components/inbox-campaigns-widget.js.map +1 -0
  32. package/src/lib/components/marketing-aggregates.d.ts +95 -0
  33. package/src/lib/components/marketing-aggregates.js +114 -0
  34. package/src/lib/components/marketing-aggregates.js.map +1 -0
  35. package/src/lib/components/marketing-console-page.js +32 -27
  36. package/src/lib/components/marketing-console-page.js.map +1 -1
  37. package/src/lib/components/marketing-console-sections.d.ts +24 -13
  38. package/src/lib/components/marketing-console-sections.js +37 -12
  39. package/src/lib/components/marketing-console-sections.js.map +1 -1
  40. package/src/lib/components/org-conversions-section.d.ts +32 -0
  41. package/src/lib/components/org-conversions-section.js +130 -0
  42. package/src/lib/components/org-conversions-section.js.map +1 -0
  43. package/src/lib/components/org-experiments-card.d.ts +32 -0
  44. package/src/lib/components/org-experiments-card.js +407 -0
  45. package/src/lib/components/org-experiments-card.js.map +1 -0
  46. package/src/lib/components/org-marketing-overview-card.d.ts +34 -0
  47. package/src/lib/components/org-marketing-overview-card.js +342 -0
  48. package/src/lib/components/org-marketing-overview-card.js.map +1 -0
  49. package/src/lib/components/org-overlays-card.d.ts +35 -0
  50. package/src/lib/components/org-overlays-card.js +345 -0
  51. package/src/lib/components/org-overlays-card.js.map +1 -0
  52. package/src/lib/components/org-site-picker-button.d.ts +26 -0
  53. package/src/lib/components/org-site-picker-button.js +104 -0
  54. package/src/lib/components/org-site-picker-button.js.map +1 -0
  55. package/src/lib/components/overlay-labels.d.ts +38 -0
  56. package/src/lib/components/overlay-labels.js +55 -0
  57. package/src/lib/components/overlay-labels.js.map +1 -0
  58. package/src/lib/components/use-emails-hub-path.d.ts +17 -8
  59. package/src/lib/components/use-emails-hub-path.js +29 -11
  60. package/src/lib/components/use-emails-hub-path.js.map +1 -1
  61. package/src/lib/components/use-one-shot-reads.d.ts +55 -0
  62. package/src/lib/components/use-one-shot-reads.js +168 -0
  63. package/src/lib/components/use-one-shot-reads.js.map +1 -0
  64. package/src/lib/model/experiments.d.ts +35 -0
  65. package/src/lib/model/experiments.js +34 -0
  66. package/src/lib/model/experiments.js.map +1 -1
  67. package/src/lib/model/overlays.d.ts +13 -2
  68. package/src/lib/model/overlays.js +18 -6
  69. package/src/lib/model/overlays.js.map +1 -1
  70. package/src/lib/plugin.js +13 -8
  71. package/src/lib/plugin.js.map +1 -1
@@ -0,0 +1,114 @@
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
+ */ import { collection, getAggregateFromServer, getCountFromServer, query, sum, where } from "firebase/firestore";
17
+ /**
18
+ * THE MARKETING FIGURES, as server aggregates.
19
+ *
20
+ * Every figure the Overview sections print is a count or a sum, and a count
21
+ * is not the length of a capped read: three listeners at `limit(50)` summed
22
+ * whatever fifty documents the server happened to return first, so a site
23
+ * past fifty sends reported the totals of an arbitrary fifty and nothing on
24
+ * screen said so. An aggregation is billed per thousand index entries rather
25
+ * than per document, so the exact figure costs about what one row did.
26
+ *
27
+ * ## One aggregation per field
28
+ *
29
+ * Firestore can compute several aggregations in one query, and it then
30
+ * counts only the documents that hold EVERY field the query aggregates. A
31
+ * send the webhook has not yet stamped with `stats.clicks` would drop out of
32
+ * the `stats.sent` total beside it, and a count asked alongside a sum would
33
+ * count only the documents carrying the summed field. So each figure is its
34
+ * own query, and a query of one field is served by that field's automatic
35
+ * index wherever the query carries no filter.
36
+ *
37
+ * ## `null` is not zero
38
+ *
39
+ * A figure the server refused or failed to answer is `null`, and the card
40
+ * draws a dash for it. Zero is a measurement; a dash is the absence of one,
41
+ * and a tile that printed 0 for a denied read would be read as "nothing
42
+ * happened".
43
+ */ /** A server count, or `null` when the server refused or failed it. */ export function countOf(target) {
44
+ return getCountFromServer(target).then((snapshot)=>{
45
+ var _snapshot_data_count;
46
+ return Number((_snapshot_data_count = snapshot.data().count) != null ? _snapshot_data_count : 0);
47
+ }, ()=>null);
48
+ }
49
+ /**
50
+ * A server sum of one numeric field, or `null` when the server refused or
51
+ * failed it. Documents without the field, or with a value that is not a
52
+ * number, add nothing.
53
+ */ export function sumOf(target, field) {
54
+ return getAggregateFromServer(target, {
55
+ total: sum(field)
56
+ }).then((snapshot)=>{
57
+ var _snapshot_data_total;
58
+ return Number((_snapshot_data_total = snapshot.data().total) != null ? _snapshot_data_total : 0);
59
+ }, ()=>null);
60
+ }
61
+ /**
62
+ * The email figures over `sends` — every send of the org, or the ones sent
63
+ * as one site (`campaignSendsQuery` builds both).
64
+ *
65
+ * The site form filters on `visibleTo`, so each of its four figures is
66
+ * served by a composite index that pairs that clause with the summed or
67
+ * compared field; they are declared in `cloud/firebase-firestore.indexes.json`
68
+ * and pinned by `marketing-summary-indexes.spec.ts`. The org form carries no
69
+ * filter and needs nothing but the automatic ones.
70
+ */ export function readSendFigures(sends) {
71
+ return Promise.all([
72
+ sumOf(sends, 'stats.sent'),
73
+ sumOf(sends, 'stats.opens'),
74
+ sumOf(sends, 'stats.clicks'),
75
+ countOf(query(sends, where('status', '==', 'scheduled')))
76
+ ]).then(([sent, opens, clicks, scheduled])=>({
77
+ sent,
78
+ opens,
79
+ clicks,
80
+ scheduled
81
+ }));
82
+ }
83
+ /**
84
+ * The lifetime counters the beacon increments on every overlay of one site
85
+ * (AGL-271), summed across all of them — not across a window of them.
86
+ */ export function readSiteOverlayFigures(firestore, hostId) {
87
+ const overlays = collection(firestore, 'hosts', hostId, 'overlays');
88
+ return Promise.all([
89
+ sumOf(overlays, 'stats.impressions'),
90
+ sumOf(overlays, 'stats.clicks')
91
+ ]).then(([views, clicks])=>({
92
+ views,
93
+ clicks
94
+ }));
95
+ }
96
+ /**
97
+ * One site's A/B tests, counted by state.
98
+ *
99
+ * "Decided" is a test with a winner, which is what `winnerVariantId` records
100
+ * whether a person picked it or the auto-winner did. `!= null` is the query
101
+ * for a field that is PRESENT: Firestore leaves out documents that lack the
102
+ * field, and no writer stores the id as an explicit null.
103
+ */ export function readSiteExperimentFigures(firestore, hostId) {
104
+ const experiments = collection(firestore, 'hosts', hostId, 'experiments');
105
+ return Promise.all([
106
+ countOf(query(experiments, where('status', '==', 'running'))),
107
+ countOf(query(experiments, where('winnerVariantId', '!=', null)))
108
+ ]).then(([running, decided])=>({
109
+ running,
110
+ decided
111
+ }));
112
+ }
113
+
114
+ //# sourceMappingURL=marketing-aggregates.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/components/marketing-aggregates.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 {\n collection,\n getAggregateFromServer,\n getCountFromServer,\n query,\n sum,\n where,\n type Firestore,\n type Query,\n} from 'firebase/firestore'\n\n/**\n * THE MARKETING FIGURES, as server aggregates.\n *\n * Every figure the Overview sections print is a count or a sum, and a count\n * is not the length of a capped read: three listeners at `limit(50)` summed\n * whatever fifty documents the server happened to return first, so a site\n * past fifty sends reported the totals of an arbitrary fifty and nothing on\n * screen said so. An aggregation is billed per thousand index entries rather\n * than per document, so the exact figure costs about what one row did.\n *\n * ## One aggregation per field\n *\n * Firestore can compute several aggregations in one query, and it then\n * counts only the documents that hold EVERY field the query aggregates. A\n * send the webhook has not yet stamped with `stats.clicks` would drop out of\n * the `stats.sent` total beside it, and a count asked alongside a sum would\n * count only the documents carrying the summed field. So each figure is its\n * own query, and a query of one field is served by that field's automatic\n * index wherever the query carries no filter.\n *\n * ## `null` is not zero\n *\n * A figure the server refused or failed to answer is `null`, and the card\n * draws a dash for it. Zero is a measurement; a dash is the absence of one,\n * and a tile that printed 0 for a denied read would be read as \"nothing\n * happened\".\n */\n\n/** A server count, or `null` when the server refused or failed it. */\nexport function countOf(target: Query): Promise<number | null> {\n return getCountFromServer(target).then(\n (snapshot) => Number(snapshot.data().count ?? 0),\n () => null,\n )\n}\n\n/**\n * A server sum of one numeric field, or `null` when the server refused or\n * failed it. Documents without the field, or with a value that is not a\n * number, add nothing.\n */\nexport function sumOf(target: Query, field: string): Promise<number | null> {\n return getAggregateFromServer(target, { total: sum(field) }).then(\n (snapshot) => Number(snapshot.data().total ?? 0),\n () => null,\n )\n}\n\n/** What a set of campaign sends did, summed. */\nexport interface SendFigures {\n sent: number | null\n opens: number | null\n clicks: number | null\n /** Sends still waiting on their scheduled time. */\n scheduled: number | null\n}\n\n/**\n * The email figures over `sends` — every send of the org, or the ones sent\n * as one site (`campaignSendsQuery` builds both).\n *\n * The site form filters on `visibleTo`, so each of its four figures is\n * served by a composite index that pairs that clause with the summed or\n * compared field; they are declared in `cloud/firebase-firestore.indexes.json`\n * and pinned by `marketing-summary-indexes.spec.ts`. The org form carries no\n * filter and needs nothing but the automatic ones.\n */\nexport function readSendFigures(sends: Query): Promise<SendFigures> {\n return Promise.all([\n sumOf(sends, 'stats.sent'),\n sumOf(sends, 'stats.opens'),\n sumOf(sends, 'stats.clicks'),\n countOf(query(sends, where('status', '==', 'scheduled'))),\n ]).then(([sent, opens, clicks, scheduled]) => ({\n sent,\n opens,\n clicks,\n scheduled,\n }))\n}\n\n/** One site's overlays' lifetime engagement, summed. */\nexport interface SiteOverlayFigures {\n views: number | null\n clicks: number | null\n}\n\n/**\n * The lifetime counters the beacon increments on every overlay of one site\n * (AGL-271), summed across all of them — not across a window of them.\n */\nexport function readSiteOverlayFigures(\n firestore: Firestore,\n hostId: string,\n): Promise<SiteOverlayFigures> {\n const overlays = collection(firestore, 'hosts', hostId, 'overlays')\n return Promise.all([\n sumOf(overlays, 'stats.impressions'),\n sumOf(overlays, 'stats.clicks'),\n ]).then(([views, clicks]) => ({ views, clicks }))\n}\n\n/** How many of one site's A/B tests are running, and how many are decided. */\nexport interface SiteExperimentFigures {\n running: number | null\n decided: number | null\n}\n\n/**\n * One site's A/B tests, counted by state.\n *\n * \"Decided\" is a test with a winner, which is what `winnerVariantId` records\n * whether a person picked it or the auto-winner did. `!= null` is the query\n * for a field that is PRESENT: Firestore leaves out documents that lack the\n * field, and no writer stores the id as an explicit null.\n */\nexport function readSiteExperimentFigures(\n firestore: Firestore,\n hostId: string,\n): Promise<SiteExperimentFigures> {\n const experiments = collection(firestore, 'hosts', hostId, 'experiments')\n return Promise.all([\n countOf(query(experiments, where('status', '==', 'running'))),\n countOf(query(experiments, where('winnerVariantId', '!=', null))),\n ]).then(([running, decided]) => ({ running, decided }))\n}\n"],"names":["collection","getAggregateFromServer","getCountFromServer","query","sum","where","countOf","target","then","snapshot","Number","data","count","sumOf","field","total","readSendFigures","sends","Promise","all","sent","opens","clicks","scheduled","readSiteOverlayFigures","firestore","hostId","overlays","views","readSiteExperimentFigures","experiments","running","decided"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,UAAU,EACVC,sBAAsB,EACtBC,kBAAkB,EAClBC,KAAK,EACLC,GAAG,EACHC,KAAK,QAGA,qBAAoB;AAE3B;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BC,GAED,oEAAoE,GACpE,OAAO,SAASC,QAAQC,MAAa;IACnC,OAAOL,mBAAmBK,QAAQC,IAAI,CACpC,CAACC;YAAoBA;eAAPC,QAAOD,uBAAAA,SAASE,IAAI,GAAGC,KAAK,YAArBH,uBAAyB;OAC9C,IAAM;AAEV;AAEA;;;;CAIC,GACD,OAAO,SAASI,MAAMN,MAAa,EAAEO,KAAa;IAChD,OAAOb,uBAAuBM,QAAQ;QAAEQ,OAAOX,IAAIU;IAAO,GAAGN,IAAI,CAC/D,CAACC;YAAoBA;eAAPC,QAAOD,uBAAAA,SAASE,IAAI,GAAGI,KAAK,YAArBN,uBAAyB;OAC9C,IAAM;AAEV;AAWA;;;;;;;;;CASC,GACD,OAAO,SAASO,gBAAgBC,KAAY;IAC1C,OAAOC,QAAQC,GAAG,CAAC;QACjBN,MAAMI,OAAO;QACbJ,MAAMI,OAAO;QACbJ,MAAMI,OAAO;QACbX,QAAQH,MAAMc,OAAOZ,MAAM,UAAU,MAAM;KAC5C,EAAEG,IAAI,CAAC,CAAC,CAACY,MAAMC,OAAOC,QAAQC,UAAU,GAAM,CAAA;YAC7CH;YACAC;YACAC;YACAC;QACF,CAAA;AACF;AAQA;;;CAGC,GACD,OAAO,SAASC,uBACdC,SAAoB,EACpBC,MAAc;IAEd,MAAMC,WAAW3B,WAAWyB,WAAW,SAASC,QAAQ;IACxD,OAAOR,QAAQC,GAAG,CAAC;QACjBN,MAAMc,UAAU;QAChBd,MAAMc,UAAU;KACjB,EAAEnB,IAAI,CAAC,CAAC,CAACoB,OAAON,OAAO,GAAM,CAAA;YAAEM;YAAON;QAAO,CAAA;AAChD;AAQA;;;;;;;CAOC,GACD,OAAO,SAASO,0BACdJ,SAAoB,EACpBC,MAAc;IAEd,MAAMI,cAAc9B,WAAWyB,WAAW,SAASC,QAAQ;IAC3D,OAAOR,QAAQC,GAAG,CAAC;QACjBb,QAAQH,MAAM2B,aAAazB,MAAM,UAAU,MAAM;QACjDC,QAAQH,MAAM2B,aAAazB,MAAM,mBAAmB,MAAM;KAC3D,EAAEG,IAAI,CAAC,CAAC,CAACuB,SAASC,QAAQ,GAAM,CAAA;YAAED;YAASC;QAAQ,CAAA;AACtD"}
@@ -26,10 +26,11 @@ import HostExperimentsCard from "./host-experiments-card.component.js";
26
26
  import HostMarketingSummaryCard from "./host-marketing-summary-card.component.js";
27
27
  import HostOverlaysCard from "./host-overlays-card.component.js";
28
28
  import PopupCard from "./popup-card.component.js";
29
- import EmailComposeCard from "./email-compose-card.js";
30
- import EmailDetail from "./email-detail.js";
31
- import EmailsListCard from "./emails-list-card.js";
32
29
  import { MarketingOrgMountProvider } from "./marketing-org-mount.js";
30
+ import OrgConversionsSection from "./org-conversions-section.js";
31
+ import OrgExperimentsCard from "./org-experiments-card.js";
32
+ import OrgMarketingOverviewCard from "./org-marketing-overview-card.js";
33
+ import OrgOverlaysCard from "./org-overlays-card.js";
33
34
  /**
34
35
  * The body of one marketing section, built only when that section is the one
35
36
  * being read (AGL-2501).
@@ -130,15 +131,22 @@ import { MarketingOrgMountProvider } from "./marketing-org-mount.js";
130
131
  }
131
132
  }
132
133
  /**
133
- * The body of one ORGANIZATION-level section: the campaigns and the emails,
134
- * over every site.
134
+ * The body of one ORGANIZATION-level section: the site rail's sections, over
135
+ * every site.
135
136
  *
136
- * The same cards as the site hub, handed no site. Each reads the org's
137
- * collections unfiltered and takes what stays a site fact — the sender, the
138
- * designs, the conversions — from the site a send is sent as, or asks which
139
- * site when it is about to create one.
140
- */ function orgSectionBody(section, detail, basePath) {
137
+ * Campaigns are the organization's, so the campaign cards are the site hub's
138
+ * own, handed no site: each reads the org's collections unfiltered and takes
139
+ * what stays a site fact — the sender, the designs, the conversions — from
140
+ * the site a send is sent as, or asks which site when it is about to create
141
+ * one. Overlays, A/B tests and conversions are each ONE site's, so their
142
+ * org sections are cards of their own: a list across sites, or the site
143
+ * the reader picks, with every edit made on the site itself.
144
+ */ function orgSectionBody(section, org, detail, basePath) {
141
145
  switch(section){
146
+ case 'overview':
147
+ return /*#__PURE__*/ _jsx(OrgMarketingOverviewCard, {
148
+ org: org
149
+ });
142
150
  case 'campaigns':
143
151
  return detail[0] ? /*#__PURE__*/ _jsx(CampaignDetailCard, {
144
152
  hostId: null,
@@ -148,22 +156,19 @@ import { MarketingOrgMountProvider } from "./marketing-org-mount.js";
148
156
  hostId: null,
149
157
  basePath: basePath
150
158
  });
151
- case 'emails':
152
- /*
153
- * `…/emails` lists them, `…/emails/{id}` reports on one and
154
- * `…/emails/{id}/edit` writes it — the Emails console's Messages
155
- * grammar, under the org hub because there is no site to host it.
156
- */ return detail[0] ? detail[1] === 'edit' ? /*#__PURE__*/ _jsx(EmailComposeCard, {
157
- hostId: null,
158
- emailId: detail[0],
159
- basePath: basePath
160
- }) : /*#__PURE__*/ _jsx(EmailDetail, {
161
- hostId: null,
162
- emailId: detail[0],
163
- basePath: basePath
164
- }) : /*#__PURE__*/ _jsx(EmailsListCard, {
165
- hostId: null,
166
- basePath: basePath
159
+ case 'conversions':
160
+ // `…/conversions/{campaignId}` narrows to one campaign, as on a site.
161
+ return /*#__PURE__*/ _jsx(OrgConversionsSection, {
162
+ basePath: basePath,
163
+ campaignId: detail[0]
164
+ });
165
+ case 'overlays':
166
+ return /*#__PURE__*/ _jsx(OrgOverlaysCard, {
167
+ org: org
168
+ });
169
+ case 'experiments':
170
+ return /*#__PURE__*/ _jsx(OrgExperimentsCard, {
171
+ org: org
167
172
  });
168
173
  default:
169
174
  return null;
@@ -228,7 +233,7 @@ import { MarketingOrgMountProvider } from "./marketing-org-mount.js";
228
233
  value: mount,
229
234
  children: /*#__PURE__*/ _jsx(HubSections, {
230
235
  sections: sections,
231
- children: orgSectionBody(section, detail, basePath)
236
+ children: orgSectionBody(section, org, detail, basePath)
232
237
  })
233
238
  });
234
239
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/components/marketing-console-page.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 type {\n AglynOrgBilling,\n ConsolePluginOrgMount,\n ConsolePluginPageProps,\n} from '@aglyn/aglyn'\nimport { GridItems } from '@aglyn/shared-ui-jsx'\nimport { HubSections } from '@aglyn/shared-ui-next'\nimport { useMemo, type ReactNode } from 'react'\nimport AnnouncementBarCard from './announcement-bar-card.component'\nimport CampaignDetailCard from './campaign-detail-card'\nimport CampaignsCard from './campaigns-card'\nimport CampaignConversionsCard from './campaign-conversions-card'\nimport HostExperimentsCard from './host-experiments-card.component'\nimport HostMarketingSummaryCard from './host-marketing-summary-card.component'\nimport HostOverlaysCard from './host-overlays-card.component'\nimport PopupCard from './popup-card.component'\nimport EmailComposeCard from './email-compose-card'\nimport EmailDetail from './email-detail'\nimport EmailsListCard from './emails-list-card'\nimport type { MarketingConsoleSectionId } from './marketing-console-sections'\nimport {\n MarketingOrgMountProvider,\n type MarketingOrgMount,\n} from './marketing-org-mount'\n\n/**\n * The body of one marketing section, built only when that section is the one\n * being read (AGL-2501).\n *\n * A function rather than a map of nodes on purpose: a `Record<id, ReactNode>`\n * would CONSTRUCT every section every render, and each card opens its\n * Firestore listens on mount — which is the entire cost this page exists to\n * stop paying. Only the returned branch is ever built.\n */\nfunction sectionBody(\n section: MarketingConsoleSectionId,\n hostId: string,\n org: Partial<AglynOrgBilling> | undefined,\n /**\n * The section's OWN segments — `segments[1]` onward, already sliced by the\n * caller. A section that owns deeper routes reads them here; one that does\n * not simply ignores them.\n */\n detail: readonly string[],\n basePath: string,\n): ReactNode {\n switch (section) {\n case 'overview':\n return <HostMarketingSummaryCard hostId={hostId} />\n case 'campaigns':\n /*\n * `/marketing/campaigns/{id}` is one campaign, and it is a ROUTE rather\n * than an expanded row for two reasons: it is linkable, which is what a\n * merchant wants to paste into a message about last week's send; and\n * the composer plus the campaign list are the surface's expensive\n * listens, so a reader who came for one campaign's numbers must not pay\n * for them.\n *\n * The id may name a CAMPAIGN or, for every URL minted before campaigns\n * grouped their emails, a single SEND. `CampaignDetailCard` answers that\n * by reading, and falls through to the send's own report — so no pasted\n * link stops resolving.\n *\n * No registry entry is needed — the shell hands a section every segment\n * beneath it, so a section owns its own subtree. The gate is the\n * section's, which is the same gate the composer is behind.\n */\n return detail[0] ? (\n <CampaignDetailCard\n hostId={hostId}\n campaignId={detail[0]}\n basePath={basePath}\n />\n ) : (\n <CampaignsCard hostId={hostId} basePath={basePath} />\n )\n case 'conversions':\n /*\n * `/marketing/conversions/{campaignId}` narrows the list to one\n * campaign's credited conversions — the drill-down from that campaign's\n * report. A segment rather than a query string, so the section owns its\n * own subtree exactly as `campaigns` does and the URL is linkable.\n */\n return (\n <CampaignConversionsCard\n hostId={hostId}\n basePath={basePath}\n campaignId={detail[0]}\n />\n )\n case 'overlays':\n return (\n <GridItems\n masonry\n spacing={3}\n items={[\n {\n size: { xs: 12 },\n children: <HostOverlaysCard hostId={hostId} org={org} />,\n },\n {\n size: { xs: 12, md: 6 },\n children: <AnnouncementBarCard hostId={hostId} org={org} />,\n },\n {\n size: { xs: 12, md: 6 },\n children: <PopupCard hostId={hostId} org={org} />,\n },\n ]}\n />\n )\n case 'experiments':\n return <HostExperimentsCard hostId={hostId} org={org} />\n default:\n return null\n }\n}\n\n/**\n * The body of one ORGANIZATION-level section: the campaigns and the emails,\n * over every site.\n *\n * The same cards as the site hub, handed no site. Each reads the org's\n * collections unfiltered and takes what stays a site fact — the sender, the\n * designs, the conversions — from the site a send is sent as, or asks which\n * site when it is about to create one.\n */\nfunction orgSectionBody(\n section: MarketingConsoleSectionId,\n detail: readonly string[],\n basePath: string,\n): ReactNode {\n switch (section) {\n case 'campaigns':\n return detail[0] ? (\n <CampaignDetailCard\n hostId={null}\n campaignId={detail[0]}\n basePath={basePath}\n />\n ) : (\n <CampaignsCard hostId={null} basePath={basePath} />\n )\n case 'emails':\n /*\n * `…/emails` lists them, `…/emails/{id}` reports on one and\n * `…/emails/{id}/edit` writes it — the Emails console's Messages\n * grammar, under the org hub because there is no site to host it.\n */\n return detail[0] ? (\n detail[1] === 'edit' ? (\n <EmailComposeCard\n hostId={null}\n emailId={detail[0]}\n basePath={basePath}\n />\n ) : (\n <EmailDetail hostId={null} emailId={detail[0]} basePath={basePath} />\n )\n ) : (\n <EmailsListCard hostId={null} basePath={basePath} />\n )\n default:\n return null\n }\n}\n\n/** The shell's org mount, as the Marketing cards read it through context. */\nfunction marketingOrgMount(\n orgMount: ConsolePluginOrgMount | undefined,\n basePath: string | undefined,\n): MarketingOrgMount | null {\n if (!orgMount || !basePath) return null\n return {\n orgId: orgMount.orgId,\n orgSlug: orgMount.orgSlug,\n hosts: orgMount.hosts,\n hostsReady: orgMount.hostsReady,\n hostsPath: orgMount.hostsPath,\n basePath,\n }\n}\n\n/**\n * Marketing page (AGL-251 → AGL-395): the at-a-glance rollup, the email\n * campaigns, the overlay managers (multi-overlay + announcement bar + popup),\n * and A/B testing — owned by the marketing plugin and rendered by the shell's\n * generic plugin route. Each gated card runs its own entitlement check\n * (overlays vs A/B are distinct plan flags) off the shell's resolved `org`;\n * the popup image picker uses the shell's media browser via `useMediaPicker`.\n *\n * A CAMPAIGN belongs here and not on the Emails console because it is a\n * container with a window of dates, a set of lists, a topic and revenue\n * attribution — a marketing object that reaches people by email. Its server\n * half has always lived in this plugin. The individual MESSAGES, the\n * templates they are built from and the identities they leave on stay on\n * Emails, and each email's page links here for its campaign.\n *\n * Sections are ROUTES (AGL-2501). `HubTabs lazy` already mounted one panel, so\n * this is not a read saving — `marketing-console-read-cost.spec.tsx` was\n * written BEFORE the conversion precisely to hold that line, and reports the\n * same counts after. What routing adds is that the URL names the section: it is\n * linkable, the back button walks sections, the breadcrumb says where you are,\n * and \"mount only what is open\" is structural rather than a `lazy` flag.\n */\nexport function MarketingConsolePage(props: ConsolePluginPageProps) {\n const { hostId, orgMount, org, section, sections, basePath, segments } = props\n /*\n * Mounted with no site: the organization's hub. Memoised because every card\n * below reads it through context, and a fresh object each render would\n * re-render all of them for nothing.\n */\n const mount = useMemo(\n () => (hostId == null ? marketingOrgMount(orgMount, basePath) : null),\n [hostId, orgMount, basePath],\n )\n\n /*\n * Nothing until the URL names a section. The shell redirects a bare hub URL\n * to the landing section and holds a spinner while it does, so this state is\n * transient — and rendering a default section here instead would pay for its\n * listens on a URL that is already being replaced.\n */\n if (!section || !sections?.length || !basePath) return null\n // `segments[0]` IS the section — the shell resolved it into `section`\n // already — so what a section owns is everything after it.\n const detail = (segments ?? []).slice(1)\n\n if (hostId == null) {\n // No site and no org to stand in for it: nothing this page can scope.\n if (!mount) return null\n return (\n <MarketingOrgMountProvider value={mount}>\n <HubSections sections={sections}>\n {orgSectionBody(\n section as MarketingConsoleSectionId,\n detail,\n basePath,\n )}\n </HubSections>\n </MarketingOrgMountProvider>\n )\n }\n\n return (\n <HubSections sections={sections}>\n {sectionBody(\n section as MarketingConsoleSectionId,\n hostId,\n org,\n detail,\n basePath,\n )}\n </HubSections>\n )\n}\nMarketingConsolePage.displayName = 'MarketingConsolePage'\n\nexport default MarketingConsolePage\n"],"names":["GridItems","HubSections","useMemo","AnnouncementBarCard","CampaignDetailCard","CampaignsCard","CampaignConversionsCard","HostExperimentsCard","HostMarketingSummaryCard","HostOverlaysCard","PopupCard","EmailComposeCard","EmailDetail","EmailsListCard","MarketingOrgMountProvider","sectionBody","section","hostId","org","detail","basePath","campaignId","masonry","spacing","items","size","xs","children","md","orgSectionBody","emailId","marketingOrgMount","orgMount","orgId","orgSlug","hosts","hostsReady","hostsPath","MarketingConsolePage","props","sections","segments","mount","length","slice","value","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/components/marketing-console-page.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 type {\n AglynOrgBilling,\n ConsolePluginOrgMount,\n ConsolePluginPageProps,\n} from '@aglyn/aglyn'\nimport { GridItems } from '@aglyn/shared-ui-jsx'\nimport { HubSections } from '@aglyn/shared-ui-next'\nimport { useMemo, type ReactNode } from 'react'\nimport AnnouncementBarCard from './announcement-bar-card.component'\nimport CampaignDetailCard from './campaign-detail-card'\nimport CampaignsCard from './campaigns-card'\nimport CampaignConversionsCard from './campaign-conversions-card'\nimport HostExperimentsCard from './host-experiments-card.component'\nimport HostMarketingSummaryCard from './host-marketing-summary-card.component'\nimport HostOverlaysCard from './host-overlays-card.component'\nimport PopupCard from './popup-card.component'\nimport type { MarketingConsoleSectionId } from './marketing-console-sections'\nimport {\n MarketingOrgMountProvider,\n type MarketingOrgMount,\n} from './marketing-org-mount'\nimport OrgConversionsSection from './org-conversions-section'\nimport OrgExperimentsCard from './org-experiments-card'\nimport OrgMarketingOverviewCard from './org-marketing-overview-card'\nimport OrgOverlaysCard from './org-overlays-card'\n\n/**\n * The body of one marketing section, built only when that section is the one\n * being read (AGL-2501).\n *\n * A function rather than a map of nodes on purpose: a `Record<id, ReactNode>`\n * would CONSTRUCT every section every render, and each card opens its\n * Firestore listens on mount — which is the entire cost this page exists to\n * stop paying. Only the returned branch is ever built.\n */\nfunction sectionBody(\n section: MarketingConsoleSectionId,\n hostId: string,\n org: Partial<AglynOrgBilling> | undefined,\n /**\n * The section's OWN segments — `segments[1]` onward, already sliced by the\n * caller. A section that owns deeper routes reads them here; one that does\n * not simply ignores them.\n */\n detail: readonly string[],\n basePath: string,\n): ReactNode {\n switch (section) {\n case 'overview':\n return <HostMarketingSummaryCard hostId={hostId} />\n case 'campaigns':\n /*\n * `/marketing/campaigns/{id}` is one campaign, and it is a ROUTE rather\n * than an expanded row for two reasons: it is linkable, which is what a\n * merchant wants to paste into a message about last week's send; and\n * the composer plus the campaign list are the surface's expensive\n * listens, so a reader who came for one campaign's numbers must not pay\n * for them.\n *\n * The id may name a CAMPAIGN or, for every URL minted before campaigns\n * grouped their emails, a single SEND. `CampaignDetailCard` answers that\n * by reading, and falls through to the send's own report — so no pasted\n * link stops resolving.\n *\n * No registry entry is needed — the shell hands a section every segment\n * beneath it, so a section owns its own subtree. The gate is the\n * section's, which is the same gate the composer is behind.\n */\n return detail[0] ? (\n <CampaignDetailCard\n hostId={hostId}\n campaignId={detail[0]}\n basePath={basePath}\n />\n ) : (\n <CampaignsCard hostId={hostId} basePath={basePath} />\n )\n case 'conversions':\n /*\n * `/marketing/conversions/{campaignId}` narrows the list to one\n * campaign's credited conversions — the drill-down from that campaign's\n * report. A segment rather than a query string, so the section owns its\n * own subtree exactly as `campaigns` does and the URL is linkable.\n */\n return (\n <CampaignConversionsCard\n hostId={hostId}\n basePath={basePath}\n campaignId={detail[0]}\n />\n )\n case 'overlays':\n return (\n <GridItems\n masonry\n spacing={3}\n items={[\n {\n size: { xs: 12 },\n children: <HostOverlaysCard hostId={hostId} org={org} />,\n },\n {\n size: { xs: 12, md: 6 },\n children: <AnnouncementBarCard hostId={hostId} org={org} />,\n },\n {\n size: { xs: 12, md: 6 },\n children: <PopupCard hostId={hostId} org={org} />,\n },\n ]}\n />\n )\n case 'experiments':\n return <HostExperimentsCard hostId={hostId} org={org} />\n default:\n return null\n }\n}\n\n/**\n * The body of one ORGANIZATION-level section: the site rail's sections, over\n * every site.\n *\n * Campaigns are the organization's, so the campaign cards are the site hub's\n * own, handed no site: each reads the org's collections unfiltered and takes\n * what stays a site fact — the sender, the designs, the conversions — from\n * the site a send is sent as, or asks which site when it is about to create\n * one. Overlays, A/B tests and conversions are each ONE site's, so their\n * org sections are cards of their own: a list across sites, or the site\n * the reader picks, with every edit made on the site itself.\n */\nfunction orgSectionBody(\n section: MarketingConsoleSectionId,\n org: Partial<AglynOrgBilling> | undefined,\n detail: readonly string[],\n basePath: string,\n): ReactNode {\n switch (section) {\n case 'overview':\n return <OrgMarketingOverviewCard org={org} />\n case 'campaigns':\n return detail[0] ? (\n <CampaignDetailCard\n hostId={null}\n campaignId={detail[0]}\n basePath={basePath}\n />\n ) : (\n <CampaignsCard hostId={null} basePath={basePath} />\n )\n case 'conversions':\n // `…/conversions/{campaignId}` narrows to one campaign, as on a site.\n return (\n <OrgConversionsSection basePath={basePath} campaignId={detail[0]} />\n )\n case 'overlays':\n return <OrgOverlaysCard org={org} />\n case 'experiments':\n return <OrgExperimentsCard org={org} />\n default:\n return null\n }\n}\n\n/** The shell's org mount, as the Marketing cards read it through context. */\nfunction marketingOrgMount(\n orgMount: ConsolePluginOrgMount | undefined,\n basePath: string | undefined,\n): MarketingOrgMount | null {\n if (!orgMount || !basePath) return null\n return {\n orgId: orgMount.orgId,\n orgSlug: orgMount.orgSlug,\n hosts: orgMount.hosts,\n hostsReady: orgMount.hostsReady,\n hostsPath: orgMount.hostsPath,\n basePath,\n }\n}\n\n/**\n * Marketing page (AGL-251 → AGL-395): the at-a-glance rollup, the email\n * campaigns, the overlay managers (multi-overlay + announcement bar + popup),\n * and A/B testing — owned by the marketing plugin and rendered by the shell's\n * generic plugin route. Each gated card runs its own entitlement check\n * (overlays vs A/B are distinct plan flags) off the shell's resolved `org`;\n * the popup image picker uses the shell's media browser via `useMediaPicker`.\n *\n * A CAMPAIGN belongs here and not on the Emails console because it is a\n * container with a window of dates, a set of lists, a topic and revenue\n * attribution — a marketing object that reaches people by email. Its server\n * half has always lived in this plugin. The individual MESSAGES, the\n * templates they are built from and the identities they leave on stay on\n * Emails, and each email's page links here for its campaign.\n *\n * Sections are ROUTES (AGL-2501). `HubTabs lazy` already mounted one panel, so\n * this is not a read saving — `marketing-console-read-cost.spec.tsx` was\n * written BEFORE the conversion precisely to hold that line, and reports the\n * same counts after. What routing adds is that the URL names the section: it is\n * linkable, the back button walks sections, the breadcrumb says where you are,\n * and \"mount only what is open\" is structural rather than a `lazy` flag.\n */\nexport function MarketingConsolePage(props: ConsolePluginPageProps) {\n const { hostId, orgMount, org, section, sections, basePath, segments } = props\n /*\n * Mounted with no site: the organization's hub. Memoised because every card\n * below reads it through context, and a fresh object each render would\n * re-render all of them for nothing.\n */\n const mount = useMemo(\n () => (hostId == null ? marketingOrgMount(orgMount, basePath) : null),\n [hostId, orgMount, basePath],\n )\n\n /*\n * Nothing until the URL names a section. The shell redirects a bare hub URL\n * to the landing section and holds a spinner while it does, so this state is\n * transient — and rendering a default section here instead would pay for its\n * listens on a URL that is already being replaced.\n */\n if (!section || !sections?.length || !basePath) return null\n // `segments[0]` IS the section — the shell resolved it into `section`\n // already — so what a section owns is everything after it.\n const detail = (segments ?? []).slice(1)\n\n if (hostId == null) {\n // No site and no org to stand in for it: nothing this page can scope.\n if (!mount) return null\n return (\n <MarketingOrgMountProvider value={mount}>\n <HubSections sections={sections}>\n {orgSectionBody(\n section as MarketingConsoleSectionId,\n org,\n detail,\n basePath,\n )}\n </HubSections>\n </MarketingOrgMountProvider>\n )\n }\n\n return (\n <HubSections sections={sections}>\n {sectionBody(\n section as MarketingConsoleSectionId,\n hostId,\n org,\n detail,\n basePath,\n )}\n </HubSections>\n )\n}\nMarketingConsolePage.displayName = 'MarketingConsolePage'\n\nexport default MarketingConsolePage\n"],"names":["GridItems","HubSections","useMemo","AnnouncementBarCard","CampaignDetailCard","CampaignsCard","CampaignConversionsCard","HostExperimentsCard","HostMarketingSummaryCard","HostOverlaysCard","PopupCard","MarketingOrgMountProvider","OrgConversionsSection","OrgExperimentsCard","OrgMarketingOverviewCard","OrgOverlaysCard","sectionBody","section","hostId","org","detail","basePath","campaignId","masonry","spacing","items","size","xs","children","md","orgSectionBody","marketingOrgMount","orgMount","orgId","orgSlug","hosts","hostsReady","hostsPath","MarketingConsolePage","props","sections","segments","mount","length","slice","value","displayName"],"mappings":"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"}
@@ -15,7 +15,7 @@
15
15
  * limitations under the License.
16
16
  */
17
17
  import type { ConsoleNavSection } from '@aglyn/aglyn';
18
- export type MarketingConsoleSectionId = 'overview' | 'campaigns' | 'conversions' | 'overlays' | 'experiments' | 'emails';
18
+ export type MarketingConsoleSectionId = 'overview' | 'campaigns' | 'conversions' | 'overlays' | 'experiments';
19
19
  /**
20
20
  * The marketing console's sections, in rail order (AGL-2501).
21
21
  *
@@ -33,17 +33,28 @@ export type MarketingConsoleSectionId = 'overview' | 'campaigns' | 'conversions'
33
33
  */
34
34
  export declare const MARKETING_CONSOLE_SECTIONS: readonly ConsoleNavSection[];
35
35
  /**
36
- * The ORGANIZATION-level Marketing hub's sections, at `/[orgSlug]/marketing`.
37
- *
38
- * Campaigns and their emails belong to the organization, so the org hub lists
39
- * every one of them over every site. The rest of the site rail stays on the
40
- * site: overlays and A/B tests are drawn on one site's pages, and the
41
- * conversions list is one site's visitors — each campaign's page links to
42
- * the conversions of the site it asks about.
43
- *
44
- * `emails` is here and not on the site rail because a site's messages
45
- * already have a page — the Emails console's Messages section, which this
46
- * plugin fills through a zone. With no site there is no Emails console, so
47
- * the org hub carries the same list itself.
36
+ * The ORGANIZATION-level Marketing hub's sections, at `/[orgSlug]/marketing`
37
+ * — the site rail's, in the site rail's order, so the two levels of the hub
38
+ * read as one surface and a bare `/[orgSlug]/marketing` lands on Overview.
39
+ *
40
+ * Each section reads what it can at the organization's level and what it
41
+ * must per site. Campaigns and their sends belong to the organization, so
42
+ * the campaigns list and the Overview's email figures are one org
43
+ * collection each. Overlays and A/B tests are drawn on one site's pages, so
44
+ * those two list site by site, a page of sites at a time, and send the
45
+ * reader to the site to edit; conversions are one site's visitors, so that
46
+ * section reads the site the reader picks.
47
+ *
48
+ * Overlays and A/B testing carry their plan flags HERE, unlike the site
49
+ * rail, whose cards run the same checks themselves: at the org level a
50
+ * section is a fan-out over sites, and gating the section in the shell is
51
+ * what keeps an organization without the plan from mounting one read per
52
+ * site to be told it cannot have them.
53
+ *
54
+ * A literal rather than a mapping of the site list: the console's title
55
+ * table and its spec read the ids and labels from this source.
56
+ *
57
+ * No `emails` section: an email is one send of a campaign, and the list of
58
+ * them is the organization's Emails page, as a site's is its own.
48
59
  */
49
60
  export declare const MARKETING_ORG_CONSOLE_SECTIONS: readonly ConsoleNavSection[];
@@ -74,26 +74,51 @@
74
74
  * is deliberately no separate default constant — a second place to say which
75
75
  * section is first is a second place for it to disagree with the rail.
76
76
  */ /**
77
- * The ORGANIZATION-level Marketing hub's sections, at `/[orgSlug]/marketing`.
77
+ * The ORGANIZATION-level Marketing hub's sections, at `/[orgSlug]/marketing`
78
+ * — the site rail's, in the site rail's order, so the two levels of the hub
79
+ * read as one surface and a bare `/[orgSlug]/marketing` lands on Overview.
78
80
  *
79
- * Campaigns and their emails belong to the organization, so the org hub lists
80
- * every one of them over every site. The rest of the site rail stays on the
81
- * site: overlays and A/B tests are drawn on one site's pages, and the
82
- * conversions list is one site's visitors — each campaign's page links to
83
- * the conversions of the site it asks about.
81
+ * Each section reads what it can at the organization's level and what it
82
+ * must per site. Campaigns and their sends belong to the organization, so
83
+ * the campaigns list and the Overview's email figures are one org
84
+ * collection each. Overlays and A/B tests are drawn on one site's pages, so
85
+ * those two list site by site, a page of sites at a time, and send the
86
+ * reader to the site to edit; conversions are one site's visitors, so that
87
+ * section reads the site the reader picks.
84
88
  *
85
- * `emails` is here and not on the site rail because a site's messages
86
- * already have a page — the Emails console's Messages section, which this
87
- * plugin fills through a zone. With no site there is no Emails console, so
88
- * the org hub carries the same list itself.
89
+ * Overlays and A/B testing carry their plan flags HERE, unlike the site
90
+ * rail, whose cards run the same checks themselves: at the org level a
91
+ * section is a fan-out over sites, and gating the section in the shell is
92
+ * what keeps an organization without the plan from mounting one read per
93
+ * site to be told it cannot have them.
94
+ *
95
+ * A literal rather than a mapping of the site list: the console's title
96
+ * table and its spec read the ids and labels from this source.
97
+ *
98
+ * No `emails` section: an email is one send of a campaign, and the list of
99
+ * them is the organization's Emails page, as a site's is its own.
89
100
  */ export const MARKETING_ORG_CONSOLE_SECTIONS = [
101
+ {
102
+ id: 'overview',
103
+ label: 'Overview'
104
+ },
90
105
  {
91
106
  id: 'campaigns',
92
107
  label: 'Campaigns'
93
108
  },
94
109
  {
95
- id: 'emails',
96
- label: 'Emails'
110
+ id: 'conversions',
111
+ label: 'Conversions'
112
+ },
113
+ {
114
+ id: 'overlays',
115
+ label: 'Overlays',
116
+ featureFlag: 'marketingOverlays'
117
+ },
118
+ {
119
+ id: 'experiments',
120
+ label: 'A/B testing',
121
+ featureFlag: 'abTesting'
97
122
  }
98
123
  ];
99
124
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/components/marketing-console-sections.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { ConsoleNavSection } from '@aglyn/aglyn'\n\nexport type MarketingConsoleSectionId =\n | 'overview'\n | 'campaigns'\n | 'conversions'\n | 'overlays'\n | 'experiments'\n | 'emails'\n\n/**\n * The marketing console's sections, in rail order (AGL-2501).\n *\n * One list, read twice and never copied: `plugin.ts` registers it on the nav\n * item so the shell can route and gate each section, and the page switches its\n * body on the id the shell resolves back.\n *\n * Ids are the `?tab=` ids this page already deep-linked by, kept deliberately\n * so a bookmark that named a tab names the same section as a route. `experiments`\n * therefore keeps its id while its LABEL reads \"A/B testing\".\n *\n * No `navTabId` on any of them: they inherit the Marketing nav item's gate.\n * The per-card ENTITLEMENT checks are unchanged and still live in the cards —\n * overlays and A/B are distinct plan flags, and a plan is not a release flag.\n */\nexport const MARKETING_CONSOLE_SECTIONS: readonly ConsoleNavSection[] = [\n { id: 'overview', label: 'Overview' },\n /*\n * A campaign is a CONTAINER with a window of dates, a set of lists, a topic\n * and revenue attribution — a marketing object that reaches people by\n * email, rather than an email object. It sits beside A/B testing because\n * that is what campaign results are attributed against, and beside the\n * overlays because both are ways of putting the same offer in front of the\n * same audience.\n *\n * The individual MESSAGES stay on the Emails console, along with the\n * templates they are built from and the sending identities they leave on.\n * Each email's page links here for the campaign it belongs to.\n */\n { id: 'campaigns', label: 'Campaigns' },\n /*\n * What the campaigns CAUSED, from the conversions' end. A campaign's own\n * report answers this for that campaign; a section is what answers it for\n * the site, and it is the only place two things can be said that a campaign\n * report structurally cannot: the conversions credited to a tagged web link\n * rather than to a campaign document, and the ones credited to nothing at\n * all. A surface listing only the credited ones renders \"we credited nine\n * of these\" as \"nine of these happened\".\n */\n { id: 'conversions', label: 'Conversions' },\n { id: 'overlays', label: 'Overlays' },\n { id: 'experiments', label: 'A/B testing' },\n]\n\n/*\n * Rail ORDER decides where `/marketing` lands: the shell redirects a bare hub\n * URL to the first section in this list the reader may open (AGL-2501). There\n * is deliberately no separate default constant — a second place to say which\n * section is first is a second place for it to disagree with the rail.\n */\n\n/**\n * The ORGANIZATION-level Marketing hub's sections, at `/[orgSlug]/marketing`.\n *\n * Campaigns and their emails belong to the organization, so the org hub lists\n * every one of them over every site. The rest of the site rail stays on the\n * site: overlays and A/B tests are drawn on one site's pages, and the\n * conversions list is one site's visitors — each campaign's page links to\n * the conversions of the site it asks about.\n *\n * `emails` is here and not on the site rail because a site's messages\n * already have a page — the Emails console's Messages section, which this\n * plugin fills through a zone. With no site there is no Emails console, so\n * the org hub carries the same list itself.\n */\nexport const MARKETING_ORG_CONSOLE_SECTIONS: readonly ConsoleNavSection[] = [\n { id: 'campaigns', label: 'Campaigns' },\n { id: 'emails', label: 'Emails' },\n]\n"],"names":["MARKETING_CONSOLE_SECTIONS","id","label","MARKETING_ORG_CONSOLE_SECTIONS"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAYD;;;;;;;;;;;;;;CAcC,GACD,OAAO,MAAMA,6BAA2D;IACtE;QAAEC,IAAI;QAAYC,OAAO;IAAW;IACpC;;;;;;;;;;;GAWC,GACD;QAAED,IAAI;QAAaC,OAAO;IAAY;IACtC;;;;;;;;GAQC,GACD;QAAED,IAAI;QAAeC,OAAO;IAAc;IAC1C;QAAED,IAAI;QAAYC,OAAO;IAAW;IACpC;QAAED,IAAI;QAAeC,OAAO;IAAc;CAC3C,CAAA;AAED;;;;;CAKC,GAED;;;;;;;;;;;;;CAaC,GACD,OAAO,MAAMC,iCAA+D;IAC1E;QAAEF,IAAI;QAAaC,OAAO;IAAY;IACtC;QAAED,IAAI;QAAUC,OAAO;IAAS;CACjC,CAAA"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/components/marketing-console-sections.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { ConsoleNavSection } from '@aglyn/aglyn'\n\nexport type MarketingConsoleSectionId =\n | 'overview'\n | 'campaigns'\n | 'conversions'\n | 'overlays'\n | 'experiments'\n\n/**\n * The marketing console's sections, in rail order (AGL-2501).\n *\n * One list, read twice and never copied: `plugin.ts` registers it on the nav\n * item so the shell can route and gate each section, and the page switches its\n * body on the id the shell resolves back.\n *\n * Ids are the `?tab=` ids this page already deep-linked by, kept deliberately\n * so a bookmark that named a tab names the same section as a route. `experiments`\n * therefore keeps its id while its LABEL reads \"A/B testing\".\n *\n * No `navTabId` on any of them: they inherit the Marketing nav item's gate.\n * The per-card ENTITLEMENT checks are unchanged and still live in the cards —\n * overlays and A/B are distinct plan flags, and a plan is not a release flag.\n */\nexport const MARKETING_CONSOLE_SECTIONS: readonly ConsoleNavSection[] = [\n { id: 'overview', label: 'Overview' },\n /*\n * A campaign is a CONTAINER with a window of dates, a set of lists, a topic\n * and revenue attribution — a marketing object that reaches people by\n * email, rather than an email object. It sits beside A/B testing because\n * that is what campaign results are attributed against, and beside the\n * overlays because both are ways of putting the same offer in front of the\n * same audience.\n *\n * The individual MESSAGES stay on the Emails console, along with the\n * templates they are built from and the sending identities they leave on.\n * Each email's page links here for the campaign it belongs to.\n */\n { id: 'campaigns', label: 'Campaigns' },\n /*\n * What the campaigns CAUSED, from the conversions' end. A campaign's own\n * report answers this for that campaign; a section is what answers it for\n * the site, and it is the only place two things can be said that a campaign\n * report structurally cannot: the conversions credited to a tagged web link\n * rather than to a campaign document, and the ones credited to nothing at\n * all. A surface listing only the credited ones renders \"we credited nine\n * of these\" as \"nine of these happened\".\n */\n { id: 'conversions', label: 'Conversions' },\n { id: 'overlays', label: 'Overlays' },\n { id: 'experiments', label: 'A/B testing' },\n]\n\n/*\n * Rail ORDER decides where `/marketing` lands: the shell redirects a bare hub\n * URL to the first section in this list the reader may open (AGL-2501). There\n * is deliberately no separate default constant — a second place to say which\n * section is first is a second place for it to disagree with the rail.\n */\n\n/**\n * The ORGANIZATION-level Marketing hub's sections, at `/[orgSlug]/marketing`\n * — the site rail's, in the site rail's order, so the two levels of the hub\n * read as one surface and a bare `/[orgSlug]/marketing` lands on Overview.\n *\n * Each section reads what it can at the organization's level and what it\n * must per site. Campaigns and their sends belong to the organization, so\n * the campaigns list and the Overview's email figures are one org\n * collection each. Overlays and A/B tests are drawn on one site's pages, so\n * those two list site by site, a page of sites at a time, and send the\n * reader to the site to edit; conversions are one site's visitors, so that\n * section reads the site the reader picks.\n *\n * Overlays and A/B testing carry their plan flags HERE, unlike the site\n * rail, whose cards run the same checks themselves: at the org level a\n * section is a fan-out over sites, and gating the section in the shell is\n * what keeps an organization without the plan from mounting one read per\n * site to be told it cannot have them.\n *\n * A literal rather than a mapping of the site list: the console's title\n * table and its spec read the ids and labels from this source.\n *\n * No `emails` section: an email is one send of a campaign, and the list of\n * them is the organization's Emails page, as a site's is its own.\n */\nexport const MARKETING_ORG_CONSOLE_SECTIONS: readonly ConsoleNavSection[] = [\n { id: 'overview', label: 'Overview' },\n { id: 'campaigns', label: 'Campaigns' },\n { id: 'conversions', label: 'Conversions' },\n { id: 'overlays', label: 'Overlays', featureFlag: 'marketingOverlays' },\n { id: 'experiments', label: 'A/B testing', featureFlag: 'abTesting' },\n]\n"],"names":["MARKETING_CONSOLE_SECTIONS","id","label","MARKETING_ORG_CONSOLE_SECTIONS","featureFlag"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAWD;;;;;;;;;;;;;;CAcC,GACD,OAAO,MAAMA,6BAA2D;IACtE;QAAEC,IAAI;QAAYC,OAAO;IAAW;IACpC;;;;;;;;;;;GAWC,GACD;QAAED,IAAI;QAAaC,OAAO;IAAY;IACtC;;;;;;;;GAQC,GACD;QAAED,IAAI;QAAeC,OAAO;IAAc;IAC1C;QAAED,IAAI;QAAYC,OAAO;IAAW;IACpC;QAAED,IAAI;QAAeC,OAAO;IAAc;CAC3C,CAAA;AAED;;;;;CAKC,GAED;;;;;;;;;;;;;;;;;;;;;;;;CAwBC,GACD,OAAO,MAAMC,iCAA+D;IAC1E;QAAEF,IAAI;QAAYC,OAAO;IAAW;IACpC;QAAED,IAAI;QAAaC,OAAO;IAAY;IACtC;QAAED,IAAI;QAAeC,OAAO;IAAc;IAC1C;QAAED,IAAI;QAAYC,OAAO;QAAYE,aAAa;IAAoB;IACtE;QAAEH,IAAI;QAAeC,OAAO;QAAeE,aAAa;IAAY;CACrE,CAAA"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The session's memory of the site this section reads, per org. Session
3
+ * storage rather than local, for the reason the CRM's own site pick gives:
4
+ * it is a convenience for one sitting, and a site remembered for weeks
5
+ * would open on figures the reader had forgotten choosing.
6
+ */
7
+ export declare const conversionsSiteStorageKey: (orgId: string) => string;
8
+ export interface OrgConversionsSectionProps {
9
+ /** The org Marketing hub, `/[orgSlug]/marketing` — each credited campaign opens under it. */
10
+ basePath: string;
11
+ /** One campaign's conversions, from `…/conversions/{campaignId}`. */
12
+ campaignId?: string;
13
+ }
14
+ /**
15
+ * The organization's Conversions section: ONE site's conversions at a time.
16
+ *
17
+ * A conversion is recorded by the site the visitor converted on — its
18
+ * attribution records, its submissions, its bookings — and the card that
19
+ * reads them is the site hub's own, unchanged, handed the site picked here.
20
+ * Merging several sites into one list would put two different sites'
21
+ * "not credited" figures under one heading, and there is no org-wide
22
+ * collection of attributions to read instead.
23
+ *
24
+ * The pick defaults to the org's first site and is remembered for the
25
+ * session. A remembered site the org no longer lists is forgotten rather
26
+ * than read.
27
+ */
28
+ export declare function OrgConversionsSection(props: OrgConversionsSectionProps): import("react").JSX.Element;
29
+ export declare namespace OrgConversionsSection {
30
+ var displayName: string;
31
+ }
32
+ export default OrgConversionsSection;
@@ -0,0 +1,130 @@
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 { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
18
+ import { pluginDocsHelp } from "@aglyn/aglyn";
19
+ import { CardDisplay } from "@aglyn/shared-ui-jsx";
20
+ import { MenuItem, Stack, TextField, Typography } from "@mui/material";
21
+ import { useEffect, useState } from "react";
22
+ import CampaignConversionsCard from "./campaign-conversions-card.js";
23
+ import { useMarketingOrgMount } from "./marketing-org-mount.js";
24
+ /**
25
+ * The session's memory of the site this section reads, per org. Session
26
+ * storage rather than local, for the reason the CRM's own site pick gives:
27
+ * it is a convenience for one sitting, and a site remembered for weeks
28
+ * would open on figures the reader had forgotten choosing.
29
+ */ export const conversionsSiteStorageKey = (orgId)=>`aglyn.marketing.conversionsSite.${orgId}`;
30
+ function readRememberedSite(orgId) {
31
+ try {
32
+ return window.sessionStorage.getItem(conversionsSiteStorageKey(orgId));
33
+ } catch (unused) {
34
+ return null;
35
+ }
36
+ }
37
+ function writeRememberedSite(orgId, hostId) {
38
+ try {
39
+ window.sessionStorage.setItem(conversionsSiteStorageKey(orgId), hostId);
40
+ } catch (unused) {
41
+ // A browser that refuses storage still gets the pick for this page.
42
+ }
43
+ }
44
+ /**
45
+ * The organization's Conversions section: ONE site's conversions at a time.
46
+ *
47
+ * A conversion is recorded by the site the visitor converted on — its
48
+ * attribution records, its submissions, its bookings — and the card that
49
+ * reads them is the site hub's own, unchanged, handed the site picked here.
50
+ * Merging several sites into one list would put two different sites'
51
+ * "not credited" figures under one heading, and there is no org-wide
52
+ * collection of attributions to read instead.
53
+ *
54
+ * The pick defaults to the org's first site and is remembered for the
55
+ * session. A remembered site the org no longer lists is forgotten rather
56
+ * than read.
57
+ */ export function OrgConversionsSection(props) {
58
+ var _ref, _ref1;
59
+ var _sites_;
60
+ const { basePath, campaignId } = props;
61
+ const mount = useMarketingOrgMount();
62
+ const orgId = (_ref = mount == null ? void 0 : mount.orgId) != null ? _ref : '';
63
+ /*
64
+ * Resolved after mount rather than in the initializer, so the first client
65
+ * paint agrees with the server's — and the card waits for it, because
66
+ * mounting on the first site and then remounting on the remembered one
67
+ * would pay for the first site's reads on the way.
68
+ */ const [picked, setPicked] = useState(null);
69
+ useEffect(()=>{
70
+ if (orgId) setPicked({
71
+ orgId,
72
+ hostId: readRememberedSite(orgId)
73
+ });
74
+ }, [
75
+ orgId
76
+ ]);
77
+ if (!mount || !mount.hostsReady || (picked == null ? void 0 : picked.orgId) !== orgId) return null;
78
+ const sites = mount.hosts;
79
+ const hostId = picked.hostId && sites.some((site)=>site.id === picked.hostId) ? picked.hostId : (_ref1 = (_sites_ = sites[0]) == null ? void 0 : _sites_.id) != null ? _ref1 : null;
80
+ if (!hostId) {
81
+ return /*#__PURE__*/ _jsx(CardDisplay, {
82
+ header: "Conversions",
83
+ help: pluginDocsHelp('emailCampaigns', {
84
+ anchor: '#the-campaign-report'
85
+ }),
86
+ contentGutterX: true,
87
+ contentGutterY: true,
88
+ children: /*#__PURE__*/ _jsx(Typography, {
89
+ variant: "body2",
90
+ color: "text.secondary",
91
+ children: 'Conversions are recorded by the site a visitor converts on, and ' + 'this organization has no sites yet.'
92
+ })
93
+ });
94
+ }
95
+ return /*#__PURE__*/ _jsxs(Stack, {
96
+ spacing: 2,
97
+ children: [
98
+ sites.length > 1 ? /*#__PURE__*/ _jsx(TextField, {
99
+ select: true,
100
+ size: "small",
101
+ label: "Conversions on",
102
+ value: hostId,
103
+ onChange: (event)=>{
104
+ const next = event.target.value;
105
+ setPicked({
106
+ orgId,
107
+ hostId: next
108
+ });
109
+ writeRememberedSite(orgId, next);
110
+ },
111
+ sx: {
112
+ maxWidth: 320
113
+ },
114
+ children: sites.map((site)=>/*#__PURE__*/ _jsx(MenuItem, {
115
+ value: site.id,
116
+ children: site.name || site.subdomain || site.id
117
+ }, site.id))
118
+ }) : null,
119
+ /*#__PURE__*/ _jsx(CampaignConversionsCard, {
120
+ hostId: hostId,
121
+ basePath: basePath,
122
+ campaignId: campaignId
123
+ }, hostId)
124
+ ]
125
+ });
126
+ }
127
+ OrgConversionsSection.displayName = 'OrgConversionsSection';
128
+ export default OrgConversionsSection;
129
+
130
+ //# sourceMappingURL=org-conversions-section.js.map