@aglyn/plugins-marketing 1.0.0-beta.220 → 1.0.0-beta.222
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/package.json +15 -15
- package/src/lib/components/campaign-conversions-card.js +53 -64
- package/src/lib/components/campaign-conversions-card.js.map +1 -1
- package/src/lib/components/campaign-detail-card.js +2 -1
- package/src/lib/components/campaign-detail-card.js.map +1 -1
- package/src/lib/components/campaign-edit-drawer.js +1 -1
- package/src/lib/components/campaign-edit-drawer.js.map +1 -1
- package/src/lib/components/campaign-queries.js +1 -1
- package/src/lib/components/campaign-queries.js.map +1 -1
- package/src/lib/components/campaign-reach-sections.js +1 -1
- package/src/lib/components/campaign-reach-sections.js.map +1 -1
- package/src/lib/components/campaign-report-card.js +5 -4
- package/src/lib/components/campaign-report-card.js.map +1 -1
- package/src/lib/components/campaigns-card.js +1 -1
- package/src/lib/components/campaigns-card.js.map +1 -1
- package/src/lib/components/email-compose-card.js +1 -1
- package/src/lib/components/email-compose-card.js.map +1 -1
- package/src/lib/components/email-detail.js +5 -4
- package/src/lib/components/email-detail.js.map +1 -1
- package/src/lib/components/email-template-report-card.d.ts +53 -0
- package/src/lib/components/email-template-report-card.js +469 -0
- package/src/lib/components/email-template-report-card.js.map +1 -0
- package/src/lib/components/emails-list-card.js +2 -2
- package/src/lib/components/emails-list-card.js.map +1 -1
- package/src/lib/components/host-experiments-card.component.js +3 -2
- package/src/lib/components/host-experiments-card.component.js.map +1 -1
- package/src/lib/components/site-runtime.js +3 -1
- package/src/lib/components/site-runtime.js.map +1 -1
- package/src/lib/declarations.server.d.ts +9 -0
- package/src/lib/declarations.server.js +62 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/model/campaign-container.d.ts +503 -0
- package/src/lib/model/campaign-container.js +498 -0
- package/src/lib/model/campaign-container.js.map +1 -0
- package/src/lib/model/campaign-conversions.d.ts +8 -1
- package/src/lib/model/campaign-conversions.js +11 -0
- package/src/lib/model/campaign-conversions.js.map +1 -1
- package/src/lib/model/campaign-report.d.ts +158 -0
- package/src/lib/model/campaign-report.js +268 -0
- package/src/lib/model/campaign-report.js.map +1 -0
- package/src/lib/model/campaign-revenue.d.ts +3 -3
- package/src/lib/model/campaign-revenue.js +3 -3
- package/src/lib/model/campaign-revenue.js.map +1 -1
- package/src/lib/model/campaign-send-time.js +2 -2
- package/src/lib/model/campaign-send-time.js.map +1 -1
- package/src/lib/model/compile-client-automations.d.ts +2 -2
- package/src/lib/model/compile-client-automations.js +3 -3
- package/src/lib/model/compile-client-automations.js.map +1 -1
- package/src/lib/model/email-record.d.ts +175 -0
- package/src/lib/model/email-record.js +206 -0
- package/src/lib/model/email-record.js.map +1 -0
- package/src/lib/model/site-contract.d.ts +2 -2
- package/src/lib/model/site-contract.js.map +1 -1
- package/src/lib/model/template-report.d.ts +158 -0
- package/src/lib/model/template-report.js +249 -0
- package/src/lib/model/template-report.js.map +1 -0
- package/src/lib/plugin.js +16 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/preview-props.js.map +1 -1
- package/src/lib/server/campaign-attribution-store.d.ts +55 -0
- package/src/lib/server/campaign-attribution-store.js +98 -0
- package/src/lib/server/campaign-attribution-store.js.map +1 -0
- package/src/lib/server/campaign-conversion-attribution.d.ts +283 -0
- package/src/lib/server/campaign-conversion-attribution.js +447 -0
- package/src/lib/server/campaign-conversion-attribution.js.map +1 -0
- package/src/lib/server/campaign-manage.js +1 -1
- package/src/lib/server/campaign-manage.js.map +1 -1
- package/src/lib/server/campaign-org-refs.js +1 -1
- package/src/lib/server/campaign-org-refs.js.map +1 -1
- package/src/lib/server/campaign-recipients.js +7 -7
- package/src/lib/server/campaign-recipients.js.map +1 -1
- package/src/lib/server/campaign-send.js +25 -19
- package/src/lib/server/campaign-send.js.map +1 -1
- package/src/lib/server/conversion-creditor.d.ts +18 -0
- package/src/lib/server/conversion-creditor.js +131 -0
- package/src/lib/server/conversion-creditor.js.map +1 -0
- package/src/lib/server/email-campaign-touch.d.ts +81 -0
- package/src/lib/server/email-campaign-touch.js +227 -0
- package/src/lib/server/email-campaign-touch.js.map +1 -0
- package/src/lib/server/email-events.js +8 -6
- package/src/lib/server/email-events.js.map +1 -1
- package/src/lib/server/email-revenue-attribution.d.ts +186 -0
- package/src/lib/server/email-revenue-attribution.js +322 -0
- package/src/lib/server/email-revenue-attribution.js.map +1 -0
- package/src/lib/server/email-send-record-index.d.ts +34 -0
- package/src/lib/server/email-send-record-index.js +90 -0
- package/src/lib/server/email-send-record-index.js.map +1 -0
- package/src/lib/server/get-client-automations.js.map +1 -1
- package/src/lib/server/marketing-figures.js +3 -3
- package/src/lib/server/marketing-figures.js.map +1 -1
- package/src/lib/server/send-tally.d.ts +32 -0
- package/src/lib/server/send-tally.js +54 -0
- package/src/lib/server/send-tally.js.map +1 -0
- package/src/lib/testing/stand-in-person-records.d.ts +35 -0
- package/src/lib/testing/stand-in-person-records.js +67 -0
- package/src/lib/testing/stand-in-person-records.js.map +1 -0
- package/src/lib/testing/stand-in-record-counts.d.ts +26 -0
- package/src/lib/testing/stand-in-record-counts.js +70 -0
- package/src/lib/testing/stand-in-record-counts.js.map +1 -0
- package/src/lib/transfer/campaigns-package.d.ts +72 -0
- package/src/lib/transfer/campaigns-package.js +145 -0
- package/src/lib/transfer/campaigns-package.js.map +1 -0
- package/src/lib/transfer/campaigns-package.server.d.ts +33 -0
- package/src/lib/transfer/campaigns-package.server.js +305 -0
- package/src/lib/transfer/campaigns-package.server.js.map +1 -0
|
@@ -0,0 +1,469 @@
|
|
|
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, Fragment as _Fragment } from "react/jsx-runtime";
|
|
19
|
+
import { pluginDocsHelp } from "@aglyn/aglyn";
|
|
20
|
+
import { pluginRecordHref } from "@aglyn/aglyn/plugin-manager/plugin-record-routes";
|
|
21
|
+
import { mdiBullhornOutline, mdiEyeOutline } from "@aglyn/shared-data-mdi";
|
|
22
|
+
import { AppLink, CardDisplay, MdiIcon } from "@aglyn/shared-ui-jsx";
|
|
23
|
+
import { Figure, RateRow, Section } from "@aglyn/shared-ui-jsx/components/measured-figures.component";
|
|
24
|
+
import RowActionsMenu from "@aglyn/shared-ui-jsx/components/row-actions-menu.component";
|
|
25
|
+
import { ScrollTable } from "@aglyn/shared-ui-jsx/components/scroll-table.component";
|
|
26
|
+
import { useConsoleHostRoute, useFirestore, useFirestoreCollection, useOrgDataScope } from "@aglyn/tenant-feature-instance";
|
|
27
|
+
import { Alert, Chip, Divider, Stack, TableBody, TableCell, TableHead, TableRow, Typography } from "@mui/material";
|
|
28
|
+
import { collection, documentId, limit, orderBy, query, where } from "firebase/firestore";
|
|
29
|
+
import { useRouter } from "next/navigation";
|
|
30
|
+
import { useMemo } from "react";
|
|
31
|
+
import { CAMPAIGN_SEND_CONTAINER_FIELD, campaignSendDisplay, campaignSendVisibleTo, orgCampaignSendsPath } from "../model/campaign-container.js";
|
|
32
|
+
import { emailSendTimeMs } from "../model/email-record.js";
|
|
33
|
+
import { templateReport } from "../model/template-report.js";
|
|
34
|
+
/**
|
|
35
|
+
* How many of a template's messages one read covers.
|
|
36
|
+
*
|
|
37
|
+
* A CEILING on the read, not a page size: the report is a single figure per
|
|
38
|
+
* quantity, so there is nothing to page through. Past it every total is a
|
|
39
|
+
* floor and the report says so — the same treatment `audienceSizeTruncated`
|
|
40
|
+
* gets, and for the same reason.
|
|
41
|
+
*/ export const TEMPLATE_CAMPAIGN_CEILING = 100;
|
|
42
|
+
const reportDocsHelp = pluginDocsHelp('designedEmails', {
|
|
43
|
+
anchor: '#send-it',
|
|
44
|
+
excerpt: 'Every message sent from this template, and what those messages did — ' + 'each rate over the population it was measured against.'
|
|
45
|
+
});
|
|
46
|
+
/**
|
|
47
|
+
* WHAT ONE TEMPLATE'S EMAILS DID, SUMMED (AGL-3080).
|
|
48
|
+
*
|
|
49
|
+
* The template is the Email plugin's; the messages sent from it are this
|
|
50
|
+
* plugin's campaign sends. So the template's page hosts a zone and this card
|
|
51
|
+
* fills it: the page names the template, and the sends are read here, where
|
|
52
|
+
* their collection is owned.
|
|
53
|
+
*
|
|
54
|
+
* ## What this card reads
|
|
55
|
+
*
|
|
56
|
+
* The messages naming this template, bounded at
|
|
57
|
+
* {@link TEMPLATE_CAMPAIGN_CEILING}. The recipients table is its own card and
|
|
58
|
+
* its own request, so a reader who came for the totals does not pay for the
|
|
59
|
+
* delivery-log query.
|
|
60
|
+
*/ export function EmailTemplateReportCard(props) {
|
|
61
|
+
var _ref;
|
|
62
|
+
const { hostId, screenId, basePath } = props;
|
|
63
|
+
const { orgSlug, subdomain } = useConsoleHostRoute(hostId);
|
|
64
|
+
/*
|
|
65
|
+
* A campaign's page is published on the record-route seam like every other
|
|
66
|
+
* kind, and read the same way here: one place decides where a `campaign`
|
|
67
|
+
* is read. Null before the console's URL has resolved.
|
|
68
|
+
*/ const campaignHref = (campaignId)=>orgSlug && subdomain ? pluginRecordHref('campaign', {
|
|
69
|
+
orgSlug,
|
|
70
|
+
host: subdomain
|
|
71
|
+
}, campaignId) : null;
|
|
72
|
+
const firestore = useFirestore();
|
|
73
|
+
const router = useRouter();
|
|
74
|
+
/*
|
|
75
|
+
* The messages sent from this template, ordered by DOCUMENT ID.
|
|
76
|
+
*
|
|
77
|
+
* Not by date, and that is forced rather than chosen: a sent message
|
|
78
|
+
* carries `sentAt`, a scheduled one carries `sendAtMs`, and no writer
|
|
79
|
+
* stamps a `createdAt` — so `orderBy` on either would not mis-sort this
|
|
80
|
+
* list, it would DROP every message written by the other branch. Ordering
|
|
81
|
+
* on the document id is the one ordering every message satisfies, and the
|
|
82
|
+
* rows are sorted by date below over a window this component already holds.
|
|
83
|
+
* The index the filters need is `visibleTo` + `templateScreenId`, declared
|
|
84
|
+
* in `cloud/firebase-firestore.indexes.json`.
|
|
85
|
+
*
|
|
86
|
+
* One MORE than the ceiling, so truncation is a fact rather than a guess.
|
|
87
|
+
*
|
|
88
|
+
* The sends are the ORGANIZATION'S, each stamped with the one site it is
|
|
89
|
+
* sent as. The `visibleTo` clause names this site: it keeps a sibling
|
|
90
|
+
* site's sends of a same-id design out, and it is what makes the read
|
|
91
|
+
* provable for a collaborator scoped to this site — the rules evaluate a
|
|
92
|
+
* list per document, and an unfiltered one is refused whole. Nothing is
|
|
93
|
+
* read until the site's org is known.
|
|
94
|
+
*/ const { orgId } = useOrgDataScope({
|
|
95
|
+
hostId
|
|
96
|
+
});
|
|
97
|
+
const sendScope = useMemo(()=>campaignSendVisibleTo(hostId), [
|
|
98
|
+
hostId
|
|
99
|
+
]);
|
|
100
|
+
const { data: messageDocs } = useFirestoreCollection(()=>orgId ? query(collection(firestore, ...orgCampaignSendsPath(orgId)), where('visibleTo', 'array-contains-any', sendScope), where('templateScreenId', '==', screenId), orderBy(documentId()), limit(TEMPLATE_CAMPAIGN_CEILING + 1)) : null, [
|
|
101
|
+
firestore,
|
|
102
|
+
orgId,
|
|
103
|
+
sendScope,
|
|
104
|
+
screenId
|
|
105
|
+
], {
|
|
106
|
+
idField: '$id'
|
|
107
|
+
});
|
|
108
|
+
const truncated = ((_ref = messageDocs == null ? void 0 : messageDocs.length) != null ? _ref : 0) > TEMPLATE_CAMPAIGN_CEILING;
|
|
109
|
+
const messages = useMemo(()=>(messageDocs != null ? messageDocs : []).slice(0, TEMPLATE_CAMPAIGN_CEILING).map((message)=>{
|
|
110
|
+
var _message_status, _message_status1, _message_audience;
|
|
111
|
+
return _extends({
|
|
112
|
+
campaignId: String(message.$id),
|
|
113
|
+
subject: String(message.subject || 'Untitled email'),
|
|
114
|
+
/*
|
|
115
|
+
* A message that has not gone out has NO send time, and that is
|
|
116
|
+
* what `null` means here — not "unknown". The report measures
|
|
117
|
+
* engagement over sent messages only, so a scheduled one
|
|
118
|
+
* contributes to the list below and to no figure above it.
|
|
119
|
+
*/ sentAtMs: String((_message_status = message.status) != null ? _message_status : '') === 'sent' ? emailSendTimeMs(message) || null : null,
|
|
120
|
+
status: String((_message_status1 = message.status) != null ? _message_status1 : 'sent'),
|
|
121
|
+
audience: String((_message_audience = message.audience) != null ? _message_audience : '')
|
|
122
|
+
}, message.listId ? {
|
|
123
|
+
listId: String(message.listId)
|
|
124
|
+
} : {}, message.listName ? {
|
|
125
|
+
listName: String(message.listName)
|
|
126
|
+
} : {}, {
|
|
127
|
+
stats: message.stats,
|
|
128
|
+
scheduledForMs: emailSendTimeMs(message)
|
|
129
|
+
}, message[CAMPAIGN_SEND_CONTAINER_FIELD] ? {
|
|
130
|
+
emailCampaignId: String(message[CAMPAIGN_SEND_CONTAINER_FIELD])
|
|
131
|
+
} : {});
|
|
132
|
+
}), [
|
|
133
|
+
messageDocs
|
|
134
|
+
]);
|
|
135
|
+
const orderedMessages = useMemo(()=>[
|
|
136
|
+
...messages
|
|
137
|
+
].sort((a, b)=>b.scheduledForMs - a.scheduledForMs), [
|
|
138
|
+
messages
|
|
139
|
+
]);
|
|
140
|
+
const report = useMemo(()=>templateReport(messages, truncated), [
|
|
141
|
+
messages,
|
|
142
|
+
truncated
|
|
143
|
+
]);
|
|
144
|
+
const messageHref = (message)=>`${basePath}/messages/${message.campaignId}`;
|
|
145
|
+
/**
|
|
146
|
+
* What one message sent from this template can be opened into.
|
|
147
|
+
*
|
|
148
|
+
* Its own report, and the campaign that grouped it. The template is the page
|
|
149
|
+
* the reader is standing on, so it is not offered a third time. A message
|
|
150
|
+
* sent before campaigns existed belongs to no container, and the item says
|
|
151
|
+
* so rather than vanishing.
|
|
152
|
+
*/ const messageActions = (message)=>[
|
|
153
|
+
{
|
|
154
|
+
key: 'details',
|
|
155
|
+
label: 'Open report',
|
|
156
|
+
icon: /*#__PURE__*/ _jsx(MdiIcon, {
|
|
157
|
+
path: mdiEyeOutline.path,
|
|
158
|
+
size: 0.8
|
|
159
|
+
}),
|
|
160
|
+
href: messageHref(message)
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
key: 'campaign',
|
|
164
|
+
label: 'Open its campaign',
|
|
165
|
+
icon: /*#__PURE__*/ _jsx(MdiIcon, {
|
|
166
|
+
path: mdiBullhornOutline.path,
|
|
167
|
+
size: 0.8
|
|
168
|
+
}),
|
|
169
|
+
// The one href here that is not under the Emails page's base path.
|
|
170
|
+
href: message.emailCampaignId && campaignHref(message.emailCampaignId) || undefined,
|
|
171
|
+
disabled: !message.emailCampaignId || !campaignHref(message.emailCampaignId),
|
|
172
|
+
disabledReason: message.emailCampaignId ? 'This site’s console URL has not resolved yet' : 'Sent before campaigns grouped their emails, so it belongs to none'
|
|
173
|
+
}
|
|
174
|
+
];
|
|
175
|
+
return /*#__PURE__*/ _jsx(CardDisplay, {
|
|
176
|
+
header: 'Sent from this template',
|
|
177
|
+
help: reportDocsHelp,
|
|
178
|
+
contentGutterX: true,
|
|
179
|
+
contentGutterY: true,
|
|
180
|
+
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
181
|
+
spacing: 3,
|
|
182
|
+
children: [
|
|
183
|
+
report.caveats.map((caveat)=>/*#__PURE__*/ _jsx(Alert, {
|
|
184
|
+
severity: "info",
|
|
185
|
+
children: caveat.message
|
|
186
|
+
}, caveat.id)),
|
|
187
|
+
/*#__PURE__*/ _jsx(Section, {
|
|
188
|
+
title: "Delivery",
|
|
189
|
+
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
190
|
+
direction: "row",
|
|
191
|
+
spacing: 4,
|
|
192
|
+
useFlexGap: true,
|
|
193
|
+
sx: {
|
|
194
|
+
flexWrap: 'wrap'
|
|
195
|
+
},
|
|
196
|
+
children: [
|
|
197
|
+
/*#__PURE__*/ _jsx(Figure, {
|
|
198
|
+
label: "Emails",
|
|
199
|
+
value: report.sentCampaigns,
|
|
200
|
+
note: "sent from this template"
|
|
201
|
+
}),
|
|
202
|
+
/*#__PURE__*/ _jsx(Figure, {
|
|
203
|
+
label: "Addressed",
|
|
204
|
+
value: report.recipients,
|
|
205
|
+
note: "after each send's cap"
|
|
206
|
+
}),
|
|
207
|
+
/*#__PURE__*/ _jsx(Figure, {
|
|
208
|
+
label: "Sent",
|
|
209
|
+
value: report.sent,
|
|
210
|
+
note: "accepted by the provider"
|
|
211
|
+
}),
|
|
212
|
+
/*#__PURE__*/ _jsx(Figure, {
|
|
213
|
+
label: "Delivered",
|
|
214
|
+
value: report.delivered,
|
|
215
|
+
note: "accepted by the receiving server"
|
|
216
|
+
}),
|
|
217
|
+
/*#__PURE__*/ _jsx(Figure, {
|
|
218
|
+
label: "Bounced",
|
|
219
|
+
value: report.bounced,
|
|
220
|
+
note: "of sent"
|
|
221
|
+
}),
|
|
222
|
+
/*#__PURE__*/ _jsx(Figure, {
|
|
223
|
+
label: "Marked as spam",
|
|
224
|
+
value: report.complained,
|
|
225
|
+
note: "of delivered"
|
|
226
|
+
})
|
|
227
|
+
]
|
|
228
|
+
})
|
|
229
|
+
}),
|
|
230
|
+
/*#__PURE__*/ _jsx(Divider, {}),
|
|
231
|
+
/*#__PURE__*/ _jsx(Section, {
|
|
232
|
+
title: "Engagement",
|
|
233
|
+
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
234
|
+
direction: "row",
|
|
235
|
+
spacing: 4,
|
|
236
|
+
useFlexGap: true,
|
|
237
|
+
sx: {
|
|
238
|
+
flexWrap: 'wrap'
|
|
239
|
+
},
|
|
240
|
+
children: [
|
|
241
|
+
/*#__PURE__*/ _jsx(Figure, {
|
|
242
|
+
label: "Opens",
|
|
243
|
+
value: report.opens,
|
|
244
|
+
note: "every open, repeats included"
|
|
245
|
+
}),
|
|
246
|
+
/*#__PURE__*/ _jsx(Figure, {
|
|
247
|
+
label: "Readers who opened",
|
|
248
|
+
value: report.uniqueOpens,
|
|
249
|
+
note: "distinct recipients"
|
|
250
|
+
}),
|
|
251
|
+
/*#__PURE__*/ _jsx(Figure, {
|
|
252
|
+
label: "Clicks",
|
|
253
|
+
value: report.clicks,
|
|
254
|
+
note: "every click, repeats included"
|
|
255
|
+
}),
|
|
256
|
+
/*#__PURE__*/ _jsx(Figure, {
|
|
257
|
+
label: "Readers who clicked",
|
|
258
|
+
value: report.uniqueClicks,
|
|
259
|
+
note: "distinct recipients"
|
|
260
|
+
}),
|
|
261
|
+
/*#__PURE__*/ _jsx(Figure, {
|
|
262
|
+
label: "Unsubscribed",
|
|
263
|
+
value: report.unsubscribes,
|
|
264
|
+
note: "through these emails' links"
|
|
265
|
+
})
|
|
266
|
+
]
|
|
267
|
+
})
|
|
268
|
+
}),
|
|
269
|
+
/*#__PURE__*/ _jsx(Divider, {}),
|
|
270
|
+
/*#__PURE__*/ _jsx(Section, {
|
|
271
|
+
title: "Rates",
|
|
272
|
+
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
273
|
+
spacing: 1,
|
|
274
|
+
children: [
|
|
275
|
+
/*#__PURE__*/ _jsx(RateRow, {
|
|
276
|
+
label: "Delivery rate",
|
|
277
|
+
rate: report.rates.delivery
|
|
278
|
+
}),
|
|
279
|
+
/*#__PURE__*/ _jsx(RateRow, {
|
|
280
|
+
label: "Open rate",
|
|
281
|
+
rate: report.rates.open
|
|
282
|
+
}),
|
|
283
|
+
/*#__PURE__*/ _jsx(RateRow, {
|
|
284
|
+
label: "Click rate",
|
|
285
|
+
rate: report.rates.click
|
|
286
|
+
}),
|
|
287
|
+
/*#__PURE__*/ _jsx(RateRow, {
|
|
288
|
+
label: "Click-to-open rate",
|
|
289
|
+
rate: report.rates.clickToOpen
|
|
290
|
+
}),
|
|
291
|
+
/*#__PURE__*/ _jsx(RateRow, {
|
|
292
|
+
label: "Bounce rate",
|
|
293
|
+
rate: report.rates.bounce
|
|
294
|
+
}),
|
|
295
|
+
/*#__PURE__*/ _jsx(RateRow, {
|
|
296
|
+
label: "Complaint rate",
|
|
297
|
+
rate: report.rates.complaint
|
|
298
|
+
}),
|
|
299
|
+
/*#__PURE__*/ _jsx(RateRow, {
|
|
300
|
+
label: "Unsubscribe rate",
|
|
301
|
+
rate: report.rates.unsubscribe
|
|
302
|
+
})
|
|
303
|
+
]
|
|
304
|
+
})
|
|
305
|
+
}),
|
|
306
|
+
/*#__PURE__*/ _jsx(Divider, {}),
|
|
307
|
+
/*#__PURE__*/ _jsx(Section, {
|
|
308
|
+
title: "Who this went to",
|
|
309
|
+
children: report.audiences.length ? /*#__PURE__*/ _jsxs(_Fragment, {
|
|
310
|
+
children: [
|
|
311
|
+
/*#__PURE__*/ _jsxs(ScrollTable, {
|
|
312
|
+
size: "small",
|
|
313
|
+
children: [
|
|
314
|
+
/*#__PURE__*/ _jsx(TableHead, {
|
|
315
|
+
children: /*#__PURE__*/ _jsxs(TableRow, {
|
|
316
|
+
children: [
|
|
317
|
+
/*#__PURE__*/ _jsx(TableCell, {
|
|
318
|
+
children: 'Audience'
|
|
319
|
+
}),
|
|
320
|
+
/*#__PURE__*/ _jsx(TableCell, {
|
|
321
|
+
align: "right",
|
|
322
|
+
children: 'Emails'
|
|
323
|
+
}),
|
|
324
|
+
/*#__PURE__*/ _jsx(TableCell, {
|
|
325
|
+
align: "right",
|
|
326
|
+
children: 'Addressed'
|
|
327
|
+
})
|
|
328
|
+
]
|
|
329
|
+
})
|
|
330
|
+
}),
|
|
331
|
+
/*#__PURE__*/ _jsx(TableBody, {
|
|
332
|
+
children: report.audiences.map((audience)=>/*#__PURE__*/ _jsxs(TableRow, {
|
|
333
|
+
children: [
|
|
334
|
+
/*#__PURE__*/ _jsx(TableCell, {
|
|
335
|
+
children: audience.label
|
|
336
|
+
}),
|
|
337
|
+
/*#__PURE__*/ _jsx(TableCell, {
|
|
338
|
+
align: "right",
|
|
339
|
+
children: audience.campaigns.toLocaleString()
|
|
340
|
+
}),
|
|
341
|
+
/*#__PURE__*/ _jsx(TableCell, {
|
|
342
|
+
align: "right",
|
|
343
|
+
sx: {
|
|
344
|
+
fontWeight: 'bold'
|
|
345
|
+
},
|
|
346
|
+
children: audience.addressed.toLocaleString()
|
|
347
|
+
})
|
|
348
|
+
]
|
|
349
|
+
}, audience.id))
|
|
350
|
+
})
|
|
351
|
+
]
|
|
352
|
+
}),
|
|
353
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
354
|
+
variant: "caption",
|
|
355
|
+
color: "text.secondary",
|
|
356
|
+
children: 'Each list is named as it was when the email was sent, so ' + 'renaming or deleting a list does not rewrite what a past ' + 'send went to.'
|
|
357
|
+
})
|
|
358
|
+
]
|
|
359
|
+
}) : /*#__PURE__*/ _jsx(Typography, {
|
|
360
|
+
variant: "body2",
|
|
361
|
+
color: "text.secondary",
|
|
362
|
+
children: 'No email sent from this template has gone out yet.'
|
|
363
|
+
})
|
|
364
|
+
}),
|
|
365
|
+
orderedMessages.length ? /*#__PURE__*/ _jsxs(_Fragment, {
|
|
366
|
+
children: [
|
|
367
|
+
/*#__PURE__*/ _jsx(Divider, {}),
|
|
368
|
+
/*#__PURE__*/ _jsx(Section, {
|
|
369
|
+
title: "Emails using this template",
|
|
370
|
+
children: /*#__PURE__*/ _jsxs(ScrollTable, {
|
|
371
|
+
size: "small",
|
|
372
|
+
children: [
|
|
373
|
+
/*#__PURE__*/ _jsx(TableHead, {
|
|
374
|
+
children: /*#__PURE__*/ _jsxs(TableRow, {
|
|
375
|
+
children: [
|
|
376
|
+
/*#__PURE__*/ _jsx(TableCell, {
|
|
377
|
+
children: 'Subject'
|
|
378
|
+
}),
|
|
379
|
+
/*#__PURE__*/ _jsx(TableCell, {
|
|
380
|
+
children: 'State'
|
|
381
|
+
}),
|
|
382
|
+
/*#__PURE__*/ _jsx(TableCell, {
|
|
383
|
+
children: 'When'
|
|
384
|
+
}),
|
|
385
|
+
/*#__PURE__*/ _jsx(TableCell, {
|
|
386
|
+
align: "right",
|
|
387
|
+
children: 'Addressed'
|
|
388
|
+
}),
|
|
389
|
+
/*#__PURE__*/ _jsx(TableCell, {
|
|
390
|
+
align: "right",
|
|
391
|
+
children: 'Opens'
|
|
392
|
+
}),
|
|
393
|
+
/*#__PURE__*/ _jsx(TableCell, {
|
|
394
|
+
align: "right",
|
|
395
|
+
children: 'Clicks'
|
|
396
|
+
}),
|
|
397
|
+
/*#__PURE__*/ _jsx(TableCell, {
|
|
398
|
+
align: "right"
|
|
399
|
+
})
|
|
400
|
+
]
|
|
401
|
+
})
|
|
402
|
+
}),
|
|
403
|
+
/*#__PURE__*/ _jsx(TableBody, {
|
|
404
|
+
children: orderedMessages.map((message)=>{
|
|
405
|
+
var _ref, _ref1, _ref2;
|
|
406
|
+
var _message_stats, _message_stats1, _message_stats2;
|
|
407
|
+
return /*#__PURE__*/ _jsxs(TableRow, {
|
|
408
|
+
hover: true,
|
|
409
|
+
onClick: ()=>router.push(messageHref(message)),
|
|
410
|
+
sx: {
|
|
411
|
+
cursor: 'pointer'
|
|
412
|
+
},
|
|
413
|
+
children: [
|
|
414
|
+
/*#__PURE__*/ _jsx(TableCell, {
|
|
415
|
+
children: /*#__PURE__*/ _jsx(AppLink, {
|
|
416
|
+
href: messageHref(message),
|
|
417
|
+
onClick: (event)=>event.stopPropagation(),
|
|
418
|
+
children: message.subject
|
|
419
|
+
})
|
|
420
|
+
}),
|
|
421
|
+
/*#__PURE__*/ _jsx(TableCell, {
|
|
422
|
+
children: /*#__PURE__*/ _jsx(Chip, {
|
|
423
|
+
size: "small",
|
|
424
|
+
label: campaignSendDisplay(message).label
|
|
425
|
+
})
|
|
426
|
+
}),
|
|
427
|
+
/*#__PURE__*/ _jsx(TableCell, {
|
|
428
|
+
children: message.scheduledForMs ? new Date(message.scheduledForMs).toLocaleString() : '—'
|
|
429
|
+
}),
|
|
430
|
+
/*#__PURE__*/ _jsx(TableCell, {
|
|
431
|
+
align: "right",
|
|
432
|
+
children: Number((_ref = (_message_stats = message.stats) == null ? void 0 : _message_stats.recipients) != null ? _ref : 0).toLocaleString()
|
|
433
|
+
}),
|
|
434
|
+
/*#__PURE__*/ _jsx(TableCell, {
|
|
435
|
+
align: "right",
|
|
436
|
+
children: Number((_ref1 = (_message_stats1 = message.stats) == null ? void 0 : _message_stats1.opens) != null ? _ref1 : 0).toLocaleString()
|
|
437
|
+
}),
|
|
438
|
+
/*#__PURE__*/ _jsx(TableCell, {
|
|
439
|
+
align: "right",
|
|
440
|
+
children: Number((_ref2 = (_message_stats2 = message.stats) == null ? void 0 : _message_stats2.clicks) != null ? _ref2 : 0).toLocaleString()
|
|
441
|
+
}),
|
|
442
|
+
/*#__PURE__*/ _jsx(TableCell, {
|
|
443
|
+
align: "right",
|
|
444
|
+
sx: {
|
|
445
|
+
width: 56
|
|
446
|
+
},
|
|
447
|
+
onClick: (event)=>event.stopPropagation(),
|
|
448
|
+
children: /*#__PURE__*/ _jsx(RowActionsMenu, {
|
|
449
|
+
label: String(message.subject),
|
|
450
|
+
items: messageActions(message)
|
|
451
|
+
})
|
|
452
|
+
})
|
|
453
|
+
]
|
|
454
|
+
}, message.campaignId);
|
|
455
|
+
})
|
|
456
|
+
})
|
|
457
|
+
]
|
|
458
|
+
})
|
|
459
|
+
})
|
|
460
|
+
]
|
|
461
|
+
}) : null
|
|
462
|
+
]
|
|
463
|
+
})
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
EmailTemplateReportCard.displayName = 'EmailTemplateReportCard';
|
|
467
|
+
export default EmailTemplateReportCard;
|
|
468
|
+
|
|
469
|
+
//# sourceMappingURL=email-template-report-card.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/components/email-template-report-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use client'\n\nimport { pluginDocsHelp } from '@aglyn/aglyn'\nimport { pluginRecordHref } from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport { mdiBullhornOutline, mdiEyeOutline } from '@aglyn/shared-data-mdi'\nimport { AppLink, CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport {\n Figure,\n RateRow,\n Section,\n} from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport RowActionsMenu, {\n type RowActionsMenuItem,\n} from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport {\n useConsoleHostRoute,\n useFirestore,\n useFirestoreCollection,\n useOrgDataScope,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Chip,\n Divider,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n documentId,\n limit,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useMemo } from 'react'\nimport {\n CAMPAIGN_SEND_CONTAINER_FIELD,\n campaignSendDisplay,\n campaignSendVisibleTo,\n orgCampaignSendsPath,\n} from '../model/campaign-container'\nimport { emailSendTimeMs } from '../model/email-record'\nimport { templateReport, type TemplateCampaign } from '../model/template-report'\n\n/**\n * How many of a template's messages one read covers.\n *\n * A CEILING on the read, not a page size: the report is a single figure per\n * quantity, so there is nothing to page through. Past it every total is a\n * floor and the report says so — the same treatment `audienceSizeTruncated`\n * gets, and for the same reason.\n */\nexport const TEMPLATE_CAMPAIGN_CEILING = 100\n\nconst reportDocsHelp = pluginDocsHelp('designedEmails', {\n anchor: '#send-it',\n excerpt:\n 'Every message sent from this template, and what those messages did — ' +\n 'each rate over the population it was measured against.',\n})\n\n/** What the Email plugin's template page hands its report zone. */\nexport interface EmailTemplateReportCardProps {\n hostId: string\n /** The `kind: 'email'` screen document the template page is about. */\n screenId: string\n /** The Emails page's own path, which every message link hangs beneath. */\n basePath: string\n}\n\n/**\n * A message row on this card: what the report needs, plus what the ROW needs.\n *\n * `TemplateCampaign` is the report's shape and stays that — it is the input to\n * `templateReport`, and a field the figures do not read has no business in it.\n * The send time this table sorts on and the campaign its row menu offers are\n * both properties of the row rather than of the report.\n */\ntype TemplateMessage = TemplateCampaign & {\n scheduledForMs: number\n /** Absent on a message written before campaigns grouped their emails. */\n emailCampaignId?: string\n}\n\n/**\n * WHAT ONE TEMPLATE'S EMAILS DID, SUMMED (AGL-3080).\n *\n * The template is the Email plugin's; the messages sent from it are this\n * plugin's campaign sends. So the template's page hosts a zone and this card\n * fills it: the page names the template, and the sends are read here, where\n * their collection is owned.\n *\n * ## What this card reads\n *\n * The messages naming this template, bounded at\n * {@link TEMPLATE_CAMPAIGN_CEILING}. The recipients table is its own card and\n * its own request, so a reader who came for the totals does not pay for the\n * delivery-log query.\n */\nexport function EmailTemplateReportCard(props: EmailTemplateReportCardProps) {\n const { hostId, screenId, basePath } = props\n const { orgSlug, subdomain } = useConsoleHostRoute(hostId)\n /*\n * A campaign's page is published on the record-route seam like every other\n * kind, and read the same way here: one place decides where a `campaign`\n * is read. Null before the console's URL has resolved.\n */\n const campaignHref = (campaignId: string): string | null =>\n orgSlug && subdomain\n ? pluginRecordHref('campaign', { orgSlug, host: subdomain }, campaignId)\n : null\n const firestore = useFirestore()\n const router = useRouter()\n\n /*\n * The messages sent from this template, ordered by DOCUMENT ID.\n *\n * Not by date, and that is forced rather than chosen: a sent message\n * carries `sentAt`, a scheduled one carries `sendAtMs`, and no writer\n * stamps a `createdAt` — so `orderBy` on either would not mis-sort this\n * list, it would DROP every message written by the other branch. Ordering\n * on the document id is the one ordering every message satisfies, and the\n * rows are sorted by date below over a window this component already holds.\n * The index the filters need is `visibleTo` + `templateScreenId`, declared\n * in `cloud/firebase-firestore.indexes.json`.\n *\n * One MORE than the ceiling, so truncation is a fact rather than a guess.\n *\n * The sends are the ORGANIZATION'S, each stamped with the one site it is\n * sent as. The `visibleTo` clause names this site: it keeps a sibling\n * site's sends of a same-id design out, and it is what makes the read\n * provable for a collaborator scoped to this site — the rules evaluate a\n * list per document, and an unfiltered one is refused whole. Nothing is\n * read until the site's org is known.\n */\n const { orgId } = useOrgDataScope({ hostId })\n const sendScope = useMemo(() => campaignSendVisibleTo(hostId), [hostId])\n const { data: messageDocs } = useFirestoreCollection<any>(\n () =>\n orgId\n ? query(\n collection(firestore, ...orgCampaignSendsPath(orgId)),\n where('visibleTo', 'array-contains-any', sendScope),\n where('templateScreenId', '==', screenId),\n orderBy(documentId()),\n limit(TEMPLATE_CAMPAIGN_CEILING + 1),\n )\n : null,\n [firestore, orgId, sendScope, screenId],\n { idField: '$id' },\n )\n\n const truncated = (messageDocs?.length ?? 0) > TEMPLATE_CAMPAIGN_CEILING\n const messages = useMemo<TemplateMessage[]>(\n () =>\n (messageDocs ?? [])\n .slice(0, TEMPLATE_CAMPAIGN_CEILING)\n .map((message: any) => ({\n campaignId: String(message.$id),\n subject: String(message.subject || 'Untitled email'),\n /*\n * A message that has not gone out has NO send time, and that is\n * what `null` means here — not \"unknown\". The report measures\n * engagement over sent messages only, so a scheduled one\n * contributes to the list below and to no figure above it.\n */\n sentAtMs:\n String(message.status ?? '') === 'sent'\n ? emailSendTimeMs(message) || null\n : null,\n status: String(message.status ?? 'sent'),\n audience: String(message.audience ?? ''),\n ...(message.listId ? { listId: String(message.listId) } : {}),\n ...(message.listName ? { listName: String(message.listName) } : {}),\n stats: message.stats,\n scheduledForMs: emailSendTimeMs(message),\n /*\n * The campaign this message belongs to, carried through so the row\n * can offer it. A message written before campaigns grouped anything\n * names no container, which is why it is optional rather than\n * defaulted to the message's own id — a menu item that navigated to\n * the message you are already looking at would be worse than one\n * that says the message belongs to no campaign.\n */\n ...(message[CAMPAIGN_SEND_CONTAINER_FIELD]\n ? {\n emailCampaignId: String(\n message[CAMPAIGN_SEND_CONTAINER_FIELD],\n ),\n }\n : {}),\n })),\n [messageDocs],\n )\n const orderedMessages = useMemo(\n () => [...messages].sort((a, b) => b.scheduledForMs - a.scheduledForMs),\n [messages],\n )\n const report = useMemo(\n () => templateReport(messages, truncated),\n [messages, truncated],\n )\n\n const messageHref = (message: TemplateMessage) =>\n `${basePath}/messages/${message.campaignId}`\n\n /**\n * What one message sent from this template can be opened into.\n *\n * Its own report, and the campaign that grouped it. The template is the page\n * the reader is standing on, so it is not offered a third time. A message\n * sent before campaigns existed belongs to no container, and the item says\n * so rather than vanishing.\n */\n const messageActions = (message: TemplateMessage): RowActionsMenuItem[] => [\n {\n key: 'details',\n label: 'Open report',\n icon: <MdiIcon path={mdiEyeOutline.path} size={0.8} />,\n href: messageHref(message),\n },\n {\n key: 'campaign',\n label: 'Open its campaign',\n icon: <MdiIcon path={mdiBullhornOutline.path} size={0.8} />,\n // The one href here that is not under the Emails page's base path.\n href:\n (message.emailCampaignId && campaignHref(message.emailCampaignId)) ||\n undefined,\n disabled:\n !message.emailCampaignId || !campaignHref(message.emailCampaignId),\n disabledReason: message.emailCampaignId\n ? 'This site’s console URL has not resolved yet'\n : 'Sent before campaigns grouped their emails, so it belongs to none',\n },\n ]\n\n return (\n <CardDisplay\n header={'Sent from this template'}\n help={reportDocsHelp}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={3}>\n {report.caveats.map((caveat) => (\n <Alert key={caveat.id} severity=\"info\">\n {caveat.message}\n </Alert>\n ))}\n\n {/*==========================================\n * WHAT HAPPENED TO THE MAIL, SUMMED.\n *\n * Counts first and rates second, because a rate is only readable\n * once you know what it was taken over — and because a count here\n * covers every message while a rate covers only the messages that\n * recorded its denominator. The rate labels say which.\n *=========================================*/}\n <Section title=\"Delivery\">\n <Stack\n direction=\"row\"\n spacing={4}\n useFlexGap\n sx={{ flexWrap: 'wrap' }}\n >\n <Figure\n label=\"Emails\"\n value={report.sentCampaigns}\n note=\"sent from this template\"\n />\n <Figure\n label=\"Addressed\"\n value={report.recipients}\n note=\"after each send's cap\"\n />\n <Figure\n label=\"Sent\"\n value={report.sent}\n note=\"accepted by the provider\"\n />\n <Figure\n label=\"Delivered\"\n value={report.delivered}\n note=\"accepted by the receiving server\"\n />\n <Figure label=\"Bounced\" value={report.bounced} note=\"of sent\" />\n <Figure\n label=\"Marked as spam\"\n value={report.complained}\n note=\"of delivered\"\n />\n </Stack>\n </Section>\n\n <Divider />\n\n <Section title=\"Engagement\">\n <Stack\n direction=\"row\"\n spacing={4}\n useFlexGap\n sx={{ flexWrap: 'wrap' }}\n >\n <Figure\n label=\"Opens\"\n value={report.opens}\n note=\"every open, repeats included\"\n />\n <Figure\n label=\"Readers who opened\"\n value={report.uniqueOpens}\n note=\"distinct recipients\"\n />\n <Figure\n label=\"Clicks\"\n value={report.clicks}\n note=\"every click, repeats included\"\n />\n <Figure\n label=\"Readers who clicked\"\n value={report.uniqueClicks}\n note=\"distinct recipients\"\n />\n <Figure\n label=\"Unsubscribed\"\n value={report.unsubscribes}\n note=\"through these emails' links\"\n />\n </Stack>\n </Section>\n\n <Divider />\n\n <Section title=\"Rates\">\n <Stack spacing={1}>\n <RateRow label=\"Delivery rate\" rate={report.rates.delivery} />\n <RateRow label=\"Open rate\" rate={report.rates.open} />\n <RateRow label=\"Click rate\" rate={report.rates.click} />\n <RateRow\n label=\"Click-to-open rate\"\n rate={report.rates.clickToOpen}\n />\n <RateRow label=\"Bounce rate\" rate={report.rates.bounce} />\n <RateRow label=\"Complaint rate\" rate={report.rates.complaint} />\n <RateRow\n label=\"Unsubscribe rate\"\n rate={report.rates.unsubscribe}\n />\n </Stack>\n </Section>\n\n <Divider />\n\n <Section title=\"Who this went to\">\n {report.audiences.length ? (\n <>\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Audience'}</TableCell>\n <TableCell align=\"right\">{'Emails'}</TableCell>\n <TableCell align=\"right\">{'Addressed'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {report.audiences.map((audience) => (\n <TableRow key={audience.id}>\n <TableCell>{audience.label}</TableCell>\n <TableCell align=\"right\">\n {audience.campaigns.toLocaleString()}\n </TableCell>\n <TableCell align=\"right\" sx={{ fontWeight: 'bold' }}>\n {audience.addressed.toLocaleString()}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n {/*\n * The naming rule, stated rather than left to be discovered\n * from a list whose name no longer matches the one in the\n * audiences section.\n */}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Each list is named as it was when the email was sent, so ' +\n 'renaming or deleting a list does not rewrite what a past ' +\n 'send went to.'}\n </Typography>\n </>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No email sent from this template has gone out yet.'}\n </Typography>\n )}\n </Section>\n\n {orderedMessages.length ? (\n <>\n <Divider />\n <Section title=\"Emails using this template\">\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Subject'}</TableCell>\n <TableCell>{'State'}</TableCell>\n <TableCell>{'When'}</TableCell>\n <TableCell align=\"right\">{'Addressed'}</TableCell>\n <TableCell align=\"right\">{'Opens'}</TableCell>\n <TableCell align=\"right\">{'Clicks'}</TableCell>\n <TableCell align=\"right\" />\n </TableRow>\n </TableHead>\n <TableBody>\n {orderedMessages.map((message) => (\n <TableRow\n key={message.campaignId}\n hover\n onClick={() => router.push(messageHref(message))}\n sx={{ cursor: 'pointer' }}\n >\n <TableCell>\n {/*\n The row's own handler would fire too and push the\n same route twice — one history entry per back\n press.\n */}\n <AppLink\n href={messageHref(message)}\n onClick={(event: {\n stopPropagation: () => void\n }) => event.stopPropagation()}\n >\n {message.subject}\n </AppLink>\n </TableCell>\n <TableCell>\n {/*\n What the message is DOING. One delivering an\n audience larger than one batch is written back as\n `scheduled` between runs, so the stored status\n reads \"Scheduled\" about a send already in progress.\n */}\n <Chip\n size=\"small\"\n label={campaignSendDisplay(message as never).label}\n />\n </TableCell>\n <TableCell>\n {message.scheduledForMs\n ? new Date(message.scheduledForMs).toLocaleString()\n : '—'}\n </TableCell>\n <TableCell align=\"right\">\n {Number(\n message.stats?.recipients ?? 0,\n ).toLocaleString()}\n </TableCell>\n <TableCell align=\"right\">\n {Number(message.stats?.opens ?? 0).toLocaleString()}\n </TableCell>\n <TableCell align=\"right\">\n {Number(message.stats?.clicks ?? 0).toLocaleString()}\n </TableCell>\n <TableCell\n align=\"right\"\n sx={{ width: 56 }}\n onClick={(event) => event.stopPropagation()}\n >\n <RowActionsMenu\n label={String(message.subject)}\n items={messageActions(message)}\n />\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n </Section>\n </>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nEmailTemplateReportCard.displayName = 'EmailTemplateReportCard'\n\nexport default EmailTemplateReportCard\n"],"names":["pluginDocsHelp","pluginRecordHref","mdiBullhornOutline","mdiEyeOutline","AppLink","CardDisplay","MdiIcon","Figure","RateRow","Section","RowActionsMenu","ScrollTable","useConsoleHostRoute","useFirestore","useFirestoreCollection","useOrgDataScope","Alert","Chip","Divider","Stack","TableBody","TableCell","TableHead","TableRow","Typography","collection","documentId","limit","orderBy","query","where","useRouter","useMemo","CAMPAIGN_SEND_CONTAINER_FIELD","campaignSendDisplay","campaignSendVisibleTo","orgCampaignSendsPath","emailSendTimeMs","templateReport","TEMPLATE_CAMPAIGN_CEILING","reportDocsHelp","anchor","excerpt","EmailTemplateReportCard","props","hostId","screenId","basePath","orgSlug","subdomain","campaignHref","campaignId","host","firestore","router","orgId","sendScope","data","messageDocs","idField","truncated","length","messages","slice","map","message","String","$id","subject","sentAtMs","status","audience","listId","listName","stats","scheduledForMs","emailCampaignId","orderedMessages","sort","a","b","report","messageHref","messageActions","key","label","icon","path","size","href","undefined","disabled","disabledReason","header","help","contentGutterX","contentGutterY","spacing","caveats","caveat","severity","id","title","direction","useFlexGap","sx","flexWrap","value","sentCampaigns","note","recipients","sent","delivered","bounced","complained","opens","uniqueOpens","clicks","uniqueClicks","unsubscribes","rate","rates","delivery","open","click","clickToOpen","bounce","complaint","unsubscribe","audiences","align","campaigns","toLocaleString","fontWeight","addressed","variant","color","hover","onClick","push","cursor","event","stopPropagation","Date","Number","width","items","displayName"],"mappings":"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"}
|
|
@@ -36,8 +36,8 @@ import { DUPLICATE_MENU_LABEL, useDuplicateResource, useFirestore, useFirestoreC
|
|
|
36
36
|
import { Button, Chip, Stack, Typography } from "@mui/material";
|
|
37
37
|
import { useRouter } from "next/navigation";
|
|
38
38
|
import { useCallback, useMemo, useState } from "react";
|
|
39
|
-
import { campaignPlacedOnHost, campaignSendDisplay, CAMPAIGN_SEND_CONTAINER_FIELD } from "
|
|
40
|
-
import { emailListTimeMs, emailSendTimeMs } from "
|
|
39
|
+
import { campaignPlacedOnHost, campaignSendDisplay, CAMPAIGN_SEND_CONTAINER_FIELD } from "../model/campaign-container.js";
|
|
40
|
+
import { emailListTimeMs, emailSendTimeMs } from "../model/email-record.js";
|
|
41
41
|
import { useCampaignManageApi, useCampaignSendApi } from "./use-campaign-send-api.js";
|
|
42
42
|
import { CAMPAIGN_SEND_STATUS_OPTIONS, SINGLE_SEND_FILTER_VALUE, campaignEmailsListQuery, campaignSendsScope, emailCampaignQueryClauses } from "../model/campaign-list-query.js";
|
|
43
43
|
import { useMarketingHubPath } from "./use-marketing-hub-path.js";
|