@aglyn/plugins-marketing 1.0.0-beta.143
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/LICENSE +201 -0
- package/README.md +7 -0
- package/package.json +56 -0
- package/src/index.d.ts +19 -0
- package/src/index.js +20 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/announcement-bar-card.component.d.ts +20 -0
- package/src/lib/components/announcement-bar-card.component.js +292 -0
- package/src/lib/components/announcement-bar-card.component.js.map +1 -0
- package/src/lib/components/campaign-composer.d.ts +84 -0
- package/src/lib/components/campaign-composer.js +1643 -0
- package/src/lib/components/campaign-composer.js.map +1 -0
- package/src/lib/components/campaign-conversions-card.d.ts +22 -0
- package/src/lib/components/campaign-conversions-card.js +642 -0
- package/src/lib/components/campaign-conversions-card.js.map +1 -0
- package/src/lib/components/campaign-detail-card.d.ts +55 -0
- package/src/lib/components/campaign-detail-card.js +771 -0
- package/src/lib/components/campaign-detail-card.js.map +1 -0
- package/src/lib/components/campaign-edit-drawer.d.ts +60 -0
- package/src/lib/components/campaign-edit-drawer.js +252 -0
- package/src/lib/components/campaign-edit-drawer.js.map +1 -0
- package/src/lib/components/campaign-email-zones.d.ts +111 -0
- package/src/lib/components/campaign-email-zones.js +54 -0
- package/src/lib/components/campaign-email-zones.js.map +1 -0
- package/src/lib/components/campaign-glance-card.component.d.ts +18 -0
- package/src/lib/components/campaign-glance-card.component.js +151 -0
- package/src/lib/components/campaign-glance-card.component.js.map +1 -0
- package/src/lib/components/campaign-members-section.d.ts +87 -0
- package/src/lib/components/campaign-members-section.js +499 -0
- package/src/lib/components/campaign-members-section.js.map +1 -0
- package/src/lib/components/campaign-reach-sections.d.ts +137 -0
- package/src/lib/components/campaign-reach-sections.js +694 -0
- package/src/lib/components/campaign-reach-sections.js.map +1 -0
- package/src/lib/components/campaign-report-card.d.ts +91 -0
- package/src/lib/components/campaign-report-card.js +667 -0
- package/src/lib/components/campaign-report-card.js.map +1 -0
- package/src/lib/components/campaign-test-send-drawer.d.ts +45 -0
- package/src/lib/components/campaign-test-send-drawer.js +238 -0
- package/src/lib/components/campaign-test-send-drawer.js.map +1 -0
- package/src/lib/components/campaigns-card.d.ts +31 -0
- package/src/lib/components/campaigns-card.js +667 -0
- package/src/lib/components/campaigns-card.js.map +1 -0
- package/src/lib/components/conversion-attribution.component.d.ts +62 -0
- package/src/lib/components/conversion-attribution.component.js +127 -0
- package/src/lib/components/conversion-attribution.component.js.map +1 -0
- package/src/lib/components/email-compose-card.d.ts +12 -0
- package/src/lib/components/email-compose-card.js +157 -0
- package/src/lib/components/email-compose-card.js.map +1 -0
- package/src/lib/components/email-detail.d.ts +40 -0
- package/src/lib/components/email-detail.js +1180 -0
- package/src/lib/components/email-detail.js.map +1 -0
- package/src/lib/components/email-edit-drawer.d.ts +54 -0
- package/src/lib/components/email-edit-drawer.js +172 -0
- package/src/lib/components/email-edit-drawer.js.map +1 -0
- package/src/lib/components/email-messages-widget.d.ts +22 -0
- package/src/lib/components/email-messages-widget.js +49 -0
- package/src/lib/components/email-messages-widget.js.map +1 -0
- package/src/lib/components/email-recipients-card.d.ts +50 -0
- package/src/lib/components/email-recipients-card.js +338 -0
- package/src/lib/components/email-recipients-card.js.map +1 -0
- package/src/lib/components/emails-list-card.d.ts +36 -0
- package/src/lib/components/emails-list-card.js +544 -0
- package/src/lib/components/emails-list-card.js.map +1 -0
- package/src/lib/components/experiment-zones.d.ts +78 -0
- package/src/lib/components/experiment-zones.js +65 -0
- package/src/lib/components/experiment-zones.js.map +1 -0
- package/src/lib/components/host-experiments-card.component.d.ts +17 -0
- package/src/lib/components/host-experiments-card.component.js +887 -0
- package/src/lib/components/host-experiments-card.component.js.map +1 -0
- package/src/lib/components/host-marketing-summary-card.component.d.ts +15 -0
- package/src/lib/components/host-marketing-summary-card.component.js +161 -0
- package/src/lib/components/host-marketing-summary-card.component.js.map +1 -0
- package/src/lib/components/host-overlays-card.component.d.ts +19 -0
- package/src/lib/components/host-overlays-card.component.js +783 -0
- package/src/lib/components/host-overlays-card.component.js.map +1 -0
- package/src/lib/components/marketing-console-page.d.ts +28 -0
- package/src/lib/components/marketing-console-page.js +166 -0
- package/src/lib/components/marketing-console-page.js.map +1 -0
- package/src/lib/components/marketing-console-sections.d.ts +34 -0
- package/src/lib/components/marketing-console-sections.js +77 -0
- package/src/lib/components/marketing-console-sections.js.map +1 -0
- package/src/lib/components/overlay-stats-row.component.d.ts +15 -0
- package/src/lib/components/overlay-stats-row.component.js +78 -0
- package/src/lib/components/overlay-stats-row.component.js.map +1 -0
- package/src/lib/components/popup-card.component.d.ts +18 -0
- package/src/lib/components/popup-card.component.js +462 -0
- package/src/lib/components/popup-card.component.js.map +1 -0
- package/src/lib/components/record-attribution-widget.d.ts +35 -0
- package/src/lib/components/record-attribution-widget.js +33 -0
- package/src/lib/components/record-attribution-widget.js.map +1 -0
- package/src/lib/components/site-runtime.d.ts +9 -0
- package/src/lib/components/site-runtime.js +1081 -0
- package/src/lib/components/site-runtime.js.map +1 -0
- package/src/lib/components/use-campaign-send-api.d.ts +35 -0
- package/src/lib/components/use-campaign-send-api.js +99 -0
- package/src/lib/components/use-campaign-send-api.js.map +1 -0
- package/src/lib/components/use-campaign-topic-options.d.ts +20 -0
- package/src/lib/components/use-campaign-topic-options.js +44 -0
- package/src/lib/components/use-campaign-topic-options.js.map +1 -0
- package/src/lib/components/use-emails-hub-path.d.ts +39 -0
- package/src/lib/components/use-emails-hub-path.js +70 -0
- package/src/lib/components/use-emails-hub-path.js.map +1 -0
- package/src/lib/components/use-marketing-hub-path.d.ts +21 -0
- package/src/lib/components/use-marketing-hub-path.js +50 -0
- package/src/lib/components/use-marketing-hub-path.js.map +1 -0
- package/src/lib/components/use-overlay-copy-editor.d.ts +79 -0
- package/src/lib/components/use-overlay-copy-editor.js +110 -0
- package/src/lib/components/use-overlay-copy-editor.js.map +1 -0
- package/src/lib/constants/bundle-common.d.ts +8 -0
- package/src/lib/constants/bundle-common.js +9 -0
- package/src/lib/constants/bundle-common.js.map +1 -0
- package/src/lib/model/campaign-membership-figures.d.ts +109 -0
- package/src/lib/model/campaign-membership-figures.js +113 -0
- package/src/lib/model/campaign-membership-figures.js.map +1 -0
- package/src/lib/model/compile-client-automations.d.ts +65 -0
- package/src/lib/model/compile-client-automations.js +95 -0
- package/src/lib/model/compile-client-automations.js.map +1 -0
- package/src/lib/model/experiments.d.ts +130 -0
- package/src/lib/model/experiments.js +186 -0
- package/src/lib/model/experiments.js.map +1 -0
- package/src/lib/model/index.d.ts +26 -0
- package/src/lib/model/index.js +26 -0
- package/src/lib/model/index.js.map +1 -0
- package/src/lib/model/marketing-record-routes.d.ts +6 -0
- package/src/lib/model/marketing-record-routes.js +46 -0
- package/src/lib/model/marketing-record-routes.js.map +1 -0
- package/src/lib/model/overlay-copy.d.ts +62 -0
- package/src/lib/model/overlay-copy.js +78 -0
- package/src/lib/model/overlay-copy.js.map +1 -0
- package/src/lib/model/overlays.d.ts +113 -0
- package/src/lib/model/overlays.js +93 -0
- package/src/lib/model/overlays.js.map +1 -0
- package/src/lib/model/site-contract.d.ts +100 -0
- package/src/lib/model/site-contract.js +23 -0
- package/src/lib/model/site-contract.js.map +1 -0
- package/src/lib/plugin.d.ts +27 -0
- package/src/lib/plugin.js +168 -0
- package/src/lib/plugin.js.map +1 -0
- package/src/lib/preview-props.d.ts +31 -0
- package/src/lib/preview-props.js +54 -0
- package/src/lib/preview-props.js.map +1 -0
- package/src/lib/server/campaign-manage.d.ts +62 -0
- package/src/lib/server/campaign-manage.js +829 -0
- package/src/lib/server/campaign-manage.js.map +1 -0
- package/src/lib/server/campaign-process-scheduled.d.ts +26 -0
- package/src/lib/server/campaign-process-scheduled.js +198 -0
- package/src/lib/server/campaign-process-scheduled.js.map +1 -0
- package/src/lib/server/campaign-recipients.d.ts +55 -0
- package/src/lib/server/campaign-recipients.js +219 -0
- package/src/lib/server/campaign-recipients.js.map +1 -0
- package/src/lib/server/campaign-send.d.ts +487 -0
- package/src/lib/server/campaign-send.js +3157 -0
- package/src/lib/server/campaign-send.js.map +1 -0
- package/src/lib/server/email-campaign-reach.d.ts +174 -0
- package/src/lib/server/email-campaign-reach.js +254 -0
- package/src/lib/server/email-campaign-reach.js.map +1 -0
- package/src/lib/server/email-events.d.ts +27 -0
- package/src/lib/server/email-events.js +714 -0
- package/src/lib/server/email-events.js.map +1 -0
- package/src/lib/server/get-client-automations.d.ts +60 -0
- package/src/lib/server/get-client-automations.js +87 -0
- package/src/lib/server/get-client-automations.js.map +1 -0
- package/src/lib/server/get-overlays.d.ts +28 -0
- package/src/lib/server/get-overlays.js +65 -0
- package/src/lib/server/get-overlays.js.map +1 -0
- package/src/lib/server/get-screen-experiments.d.ts +42 -0
- package/src/lib/server/get-screen-experiments.js +82 -0
- package/src/lib/server/get-screen-experiments.js.map +1 -0
- package/src/lib/server/lists-materialize.d.ts +18 -0
- package/src/lib/server/lists-materialize.js +130 -0
- package/src/lib/server/lists-materialize.js.map +1 -0
- package/src/lib/server/marketing-figures.d.ts +37 -0
- package/src/lib/server/marketing-figures.js +248 -0
- package/src/lib/server/marketing-figures.js.map +1 -0
- package/src/lib/server/site-page-enricher.d.ts +26 -0
- package/src/lib/server/site-page-enricher.js +210 -0
- package/src/lib/server/site-page-enricher.js.map +1 -0
- package/src/lib/server.d.ts +20 -0
- package/src/lib/server.js +152 -0
- package/src/lib/server.js.map +1 -0
- package/src/lib/site.d.ts +22 -0
- package/src/lib/site.js +48 -0
- package/src/lib/site.js.map +1 -0
|
@@ -0,0 +1,771 @@
|
|
|
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 { mdiDeleteOutline, mdiEyeOutline, mdiPaletteOutline, mdiPencilOutline } from "@aglyn/shared-data-mdi";
|
|
20
|
+
import { AppLink, CardDisplay, MdiIcon, useConfirmationContext } from "@aglyn/shared-ui-jsx";
|
|
21
|
+
import { Figure, RateRow, Section } from "@aglyn/shared-ui-email-campaigns/components/report-figures";
|
|
22
|
+
import { ListRowActions, ListTable, listActionsColumn } from "@aglyn/shared-ui-jsx/components/list-table.component";
|
|
23
|
+
import RowActionsMenu from "@aglyn/shared-ui-jsx/components/row-actions-menu.component";
|
|
24
|
+
import { TABLE_ROW_HEIGHT } from "@aglyn/shared-ui-jsx/const/table-pagination";
|
|
25
|
+
import { ceilingedWindow } from "@aglyn/tenant-feature-instance/hooks/host-collection-queries";
|
|
26
|
+
import { PageHeaderRecord, pluginDocsHelp } from "@aglyn/aglyn";
|
|
27
|
+
import { Alert, Box, Button, Chip, Divider, Stack, Typography } from "@mui/material";
|
|
28
|
+
import { collection, deleteField, doc, documentId, limit, orderBy, query, updateDoc, where } from "firebase/firestore";
|
|
29
|
+
import { useCallback, useMemo, useState } from "react";
|
|
30
|
+
import { useRouter } from "next/navigation";
|
|
31
|
+
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
32
|
+
import { useFirestore, useFirestoreCollection, useFirestoreDoc, useOrgDataScope } from "@aglyn/tenant-feature-instance";
|
|
33
|
+
import { campaignRollup, campaignSendDisplay, campaignWindowState, emailListTimeMs } from "@aglyn/shared-ui-email-campaigns/model";
|
|
34
|
+
import CampaignComposer from "./campaign-composer.js";
|
|
35
|
+
import CampaignEditDrawer from "./campaign-edit-drawer.js";
|
|
36
|
+
import { CampaignConversionsSection, CampaignDestinationsSection, CampaignRevenueSection } from "./campaign-reach-sections.js";
|
|
37
|
+
import CampaignMembersSection from "./campaign-members-section.js";
|
|
38
|
+
import CampaignReportCard from "./campaign-report-card.js";
|
|
39
|
+
import { useCampaignManageApi } from "./use-campaign-send-api.js";
|
|
40
|
+
import { useEmailsHubPath } from "./use-emails-hub-path.js";
|
|
41
|
+
import { useCampaignTopicOptions } from "./use-campaign-topic-options.js";
|
|
42
|
+
/** How many of a campaign's emails the detail page enumerates. */ const CAMPAIGN_EMAIL_CEILING = 50;
|
|
43
|
+
const detailDocsHelp = pluginDocsHelp('emailCampaigns', {
|
|
44
|
+
anchor: '#the-campaign-report',
|
|
45
|
+
excerpt: 'A campaign groups the emails sent to its lists. The figures here are ' + 'the sum across those emails, and each one keeps its own report.'
|
|
46
|
+
});
|
|
47
|
+
/**
|
|
48
|
+
* A rolled-up figure, in the shared `Figure`'s shape.
|
|
49
|
+
*
|
|
50
|
+
* The note is where an aggregate differs from a single send's count: a total
|
|
51
|
+
* measured over fewer emails than the campaign holds says which part of the
|
|
52
|
+
* campaign it describes, rather than presenting a partial sum as a complete
|
|
53
|
+
* one. `null` still renders as an em dash and says "not recorded", which is
|
|
54
|
+
* the property the shared component already owns.
|
|
55
|
+
*/ const rolled = (value)=>({
|
|
56
|
+
value: value.value,
|
|
57
|
+
note: value.value !== null && value.recorded < value.sends ? `across ${value.recorded} of ${value.sends} emails` : `across ${value.sends} email${value.sends === 1 ? '' : 's'}`
|
|
58
|
+
});
|
|
59
|
+
/**
|
|
60
|
+
* ONE CAMPAIGN: its lists, what it caused, what it earned, where it sent
|
|
61
|
+
* people, and the emails it did all of that with.
|
|
62
|
+
*
|
|
63
|
+
* ## Why the emails are a section rather than the page
|
|
64
|
+
*
|
|
65
|
+
* A campaign reaches people by email, which is not the same as a campaign
|
|
66
|
+
* BEING a list of emails. It runs over a window, against a set of audiences,
|
|
67
|
+
* and the pages its links land on, the money credited to it and the
|
|
68
|
+
* conversions credited to it are facts about the campaign that no single
|
|
69
|
+
* message in it holds. So the body is sectioned — what it caused, what it
|
|
70
|
+
* earned, where it sent people, then the mail's own figures, then its emails
|
|
71
|
+
* — and each section names the population it describes.
|
|
72
|
+
*
|
|
73
|
+
* The ORDER is the part that says which of those the page is about. Delivery,
|
|
74
|
+
* engagement and rates are mechanics of a message; a page that met the reader
|
|
75
|
+
* with them and reached the outcomes only after scrolling was a mail report
|
|
76
|
+
* whatever its headings claimed. `campaign-report-card.tsx` — the page a
|
|
77
|
+
* campaign of one email gets — carries the same two outcome headings first,
|
|
78
|
+
* for the same reason and in the same words.
|
|
79
|
+
*
|
|
80
|
+
* The three middle sections live in `campaign-reach-sections.tsx`, which is
|
|
81
|
+
* also where the reason a campaign cannot simply DECLARE the screens and
|
|
82
|
+
* forms it runs across is written down: no such edge is stored, attribution
|
|
83
|
+
* is the mechanism, and all three sections join on this campaign's send ids.
|
|
84
|
+
*
|
|
85
|
+
* ## Why this resolves two kinds of id
|
|
86
|
+
*
|
|
87
|
+
* The id in `campaigns/{id}` may name a campaign CONTAINER or a single SEND,
|
|
88
|
+
* and which it is is answered by reading: a container at `emailCampaigns/{id}`
|
|
89
|
+
* renders this page, and anything else falls through to that send's own
|
|
90
|
+
* report.
|
|
91
|
+
*
|
|
92
|
+
* Not every send belongs to a container. One written before campaigns grouped
|
|
93
|
+
* their emails names none, its own id is the only id the URL can carry, and
|
|
94
|
+
* that id is also inside the HMAC of every unsubscribe footer already
|
|
95
|
+
* delivered — so a send id has to go on addressing a page rather than a 404.
|
|
96
|
+
* The fall-through is what makes the container additive: no send document is
|
|
97
|
+
* rewritten and no id is reassigned.
|
|
98
|
+
*
|
|
99
|
+
* The extra read is one document, and it buys the guarantee that a pasted
|
|
100
|
+
* report link resolves whichever kind of id it carries.
|
|
101
|
+
*/ export function CampaignDetailCard(props) {
|
|
102
|
+
var _campaign_listIds;
|
|
103
|
+
const { hostId, campaignId, basePath } = props;
|
|
104
|
+
const firestore = useFirestore();
|
|
105
|
+
// The sibling hub, for the two records this page links to but does not own:
|
|
106
|
+
// each message's report and the template it was built from.
|
|
107
|
+
const emailsHub = useEmailsHubPath();
|
|
108
|
+
const router = useRouter();
|
|
109
|
+
const { scope: dataScope } = useOrgDataScope({
|
|
110
|
+
hostId
|
|
111
|
+
});
|
|
112
|
+
const { confirm } = useConfirmationContext();
|
|
113
|
+
const { enqueueSnackbar } = useSnackbar();
|
|
114
|
+
const manageApi = useCampaignManageApi(hostId);
|
|
115
|
+
const [composing, setComposing] = useState(false);
|
|
116
|
+
const [editing, setEditing] = useState(false);
|
|
117
|
+
const [saving, setSaving] = useState(false);
|
|
118
|
+
const [saveError, setSaveError] = useState(null);
|
|
119
|
+
const [deleting, setDeleting] = useState(false);
|
|
120
|
+
const { data: campaign, status } = useFirestoreDoc(()=>doc(firestore, 'hosts', hostId, 'emailCampaigns', campaignId), [
|
|
121
|
+
firestore,
|
|
122
|
+
hostId,
|
|
123
|
+
campaignId
|
|
124
|
+
]);
|
|
125
|
+
/*
|
|
126
|
+
* ONLY WHEN THE ID NAMES A CAMPAIGN.
|
|
127
|
+
*
|
|
128
|
+
* Both queries below are gated on the container having been found, because
|
|
129
|
+
* the other half of this component is the single-send report — and that
|
|
130
|
+
* screen's whole design is that it reads two documents whatever the size of
|
|
131
|
+
* the audience. Listening unconditionally would add two collection reads to
|
|
132
|
+
* every legacy report URL, which is the cost the report route was split out
|
|
133
|
+
* to avoid. Not listening is what a null builder means.
|
|
134
|
+
*
|
|
135
|
+
* The campaign's emails come back by the field each send carries: equality
|
|
136
|
+
* on one field ordered by DOCUMENT NAME, which Firestore's automatic
|
|
137
|
+
* single-field index serves without a composite one. Ordering on a date
|
|
138
|
+
* would need that index AND would drop every send missing that particular
|
|
139
|
+
* date — a sent send carries `sentAt`, a scheduled one `sendAtMs`, and
|
|
140
|
+
* neither is on both. Sorted by date below, over a window this page holds
|
|
141
|
+
* whole.
|
|
142
|
+
*
|
|
143
|
+
* One document past the ceiling, so "this campaign has more emails than are
|
|
144
|
+
* listed" is a fact rather than a guess.
|
|
145
|
+
*/ const { data: sendDocs } = useFirestoreCollection(()=>campaign ? query(collection(firestore, 'hosts', hostId, 'campaigns'), where('emailCampaignId', '==', campaignId), orderBy(documentId()), limit(CAMPAIGN_EMAIL_CEILING + 1)) : null, [
|
|
146
|
+
firestore,
|
|
147
|
+
hostId,
|
|
148
|
+
campaignId,
|
|
149
|
+
Boolean(campaign)
|
|
150
|
+
], {
|
|
151
|
+
idField: '$id'
|
|
152
|
+
});
|
|
153
|
+
const { data: listDocs } = useFirestoreCollection(()=>campaign && dataScope ? query(collection(firestore, dataScope[0], dataScope[1], 'lists'), limit(50)) : null, [
|
|
154
|
+
firestore,
|
|
155
|
+
dataScope,
|
|
156
|
+
Boolean(campaign)
|
|
157
|
+
], {
|
|
158
|
+
idField: '$id'
|
|
159
|
+
});
|
|
160
|
+
/*
|
|
161
|
+
* The org's topic catalog, read only while the EDIT DRAWER is open.
|
|
162
|
+
*
|
|
163
|
+
* It fills one picker in that drawer and is drawn nowhere else on this
|
|
164
|
+
* page, so reading it on mount would charge every reader who came for the
|
|
165
|
+
* campaign's numbers for a field they are not looking at. The composer
|
|
166
|
+
* carries its own read of the same catalog, behind its own button, for the
|
|
167
|
+
* same reason.
|
|
168
|
+
*/ const { topics, source: topicSource } = useCampaignTopicOptions(hostId, {
|
|
169
|
+
enabled: editing
|
|
170
|
+
});
|
|
171
|
+
const topicOptions = useMemo(()=>topics.map((topic)=>({
|
|
172
|
+
value: topic.id,
|
|
173
|
+
label: topic.name
|
|
174
|
+
})), [
|
|
175
|
+
topics
|
|
176
|
+
]);
|
|
177
|
+
const { rows: readSends, truncated: sendsTruncated } = ceilingedWindow(sendDocs, CAMPAIGN_EMAIL_CEILING);
|
|
178
|
+
/*
|
|
179
|
+
* Newest first on the time each email SITS at — its send time where it has
|
|
180
|
+
* one, its creation where it does not. A draft has neither `sentAt` nor
|
|
181
|
+
* `sendAtMs`, so ordering on the send time alone gave every draft the key 0
|
|
182
|
+
* and filed the email a merchant is in the middle of writing below mail
|
|
183
|
+
* sent years ago.
|
|
184
|
+
*/ const sends = useMemo(()=>[
|
|
185
|
+
...readSends
|
|
186
|
+
].sort((a, b)=>emailListTimeMs(b) - emailListTimeMs(a)), [
|
|
187
|
+
readSends
|
|
188
|
+
]);
|
|
189
|
+
const rollup = useMemo(()=>campaignRollup(sends), [
|
|
190
|
+
sends
|
|
191
|
+
]);
|
|
192
|
+
// The ids the two sections beneath the figures join on. Derived from the
|
|
193
|
+
// window this card already holds, so neither of them reads the send list
|
|
194
|
+
// again to find out which emails the campaign has.
|
|
195
|
+
const sendIds = useMemo(()=>sends.map((send)=>String(send.$id)).filter(Boolean), [
|
|
196
|
+
sends
|
|
197
|
+
]);
|
|
198
|
+
/*==========================================
|
|
199
|
+
* SAVING THE CONTAINER, WITH THE CLIENT SDK.
|
|
200
|
+
*
|
|
201
|
+
* The same door the create drawer already uses. `emailCampaigns` is
|
|
202
|
+
* deliberately outside the security rules' server-only exclusion list — a
|
|
203
|
+
* container holds no counter, no consent record and no entitlement input,
|
|
204
|
+
* so an editor who writes one cannot buy themselves a send — and giving the
|
|
205
|
+
* edit a route of its own would be a second writer to keep in step with the
|
|
206
|
+
* create.
|
|
207
|
+
*
|
|
208
|
+
* `updateDoc` rather than a merge-set, because this is an edit of a
|
|
209
|
+
* document that exists: a set would CREATE a campaign at this id for
|
|
210
|
+
* somebody who deleted it in another tab, which is the shape the delivery
|
|
211
|
+
* webhook is careful about on the send collection for the same reason.
|
|
212
|
+
*
|
|
213
|
+
* A cleared date is written as `null` — the absence the model already
|
|
214
|
+
* spells — and a cleared topic REMOVES the field, because the model has no
|
|
215
|
+
* null topic and `campaignTopicId` is handed to the composer as one.
|
|
216
|
+
*=========================================*/ const handleSave = useCallback(async (values)=>{
|
|
217
|
+
if (saving) return;
|
|
218
|
+
setSaving(true);
|
|
219
|
+
setSaveError(null);
|
|
220
|
+
try {
|
|
221
|
+
await updateDoc(doc(firestore, 'hosts', hostId, 'emailCampaigns', campaignId), {
|
|
222
|
+
name: values.name,
|
|
223
|
+
startAtMs: values.startAtMs,
|
|
224
|
+
endAtMs: values.endAtMs,
|
|
225
|
+
listIds: values.listIds,
|
|
226
|
+
topicId: values.topicId ? values.topicId : deleteField()
|
|
227
|
+
});
|
|
228
|
+
setEditing(false);
|
|
229
|
+
enqueueSnackbar('Campaign updated', {
|
|
230
|
+
variant: 'success',
|
|
231
|
+
persist: false
|
|
232
|
+
});
|
|
233
|
+
} catch (error) {
|
|
234
|
+
console.error(error);
|
|
235
|
+
setSaveError('The campaign could not be saved');
|
|
236
|
+
} finally{
|
|
237
|
+
setSaving(false);
|
|
238
|
+
}
|
|
239
|
+
}, [
|
|
240
|
+
campaignId,
|
|
241
|
+
enqueueSnackbar,
|
|
242
|
+
firestore,
|
|
243
|
+
hostId,
|
|
244
|
+
saving
|
|
245
|
+
]);
|
|
246
|
+
/*==========================================
|
|
247
|
+
* DELETING THE CONTAINER, WHICH IS NOT DELETING ITS MAIL.
|
|
248
|
+
*
|
|
249
|
+
* The route detaches every email first and then removes the campaign, so
|
|
250
|
+
* what a merchant is agreeing to is the grouping going away — the emails
|
|
251
|
+
* stay, keep their ids, keep their reports, and read afterwards as single
|
|
252
|
+
* sends. Their unsubscribe links carry those ids inside an HMAC and go on
|
|
253
|
+
* working, which is why the container can never take them with it.
|
|
254
|
+
*
|
|
255
|
+
* The confirmation says the count and says what a delete does NOT do. A
|
|
256
|
+
* scheduled email inside the campaign still goes out at its time — stopping
|
|
257
|
+
* somebody's mail is `cancel`, on that email's own page — and a merchant
|
|
258
|
+
* who reads "delete campaign" as "stop the campaign" has to be told
|
|
259
|
+
* otherwise before they press it, not after.
|
|
260
|
+
*=========================================*/ const handleDelete = useCallback(async ()=>{
|
|
261
|
+
if (deleting) return;
|
|
262
|
+
/*
|
|
263
|
+
* Emails with mail still to go: the ones waiting for their time AND the
|
|
264
|
+
* ones part way through an audience larger than one batch. Both are what
|
|
265
|
+
* a merchant needs warning about, and counting only `scheduled` would
|
|
266
|
+
* miss the case that needs it most — a campaign that is delivering right
|
|
267
|
+
* now is stored as `scheduled` too, but `rollup.scheduled` is the
|
|
268
|
+
* narrower reading that excludes it.
|
|
269
|
+
*/ const unfinished = rollup.scheduled + rollup.sending;
|
|
270
|
+
const agreed = await confirm({
|
|
271
|
+
title: 'Delete this campaign?',
|
|
272
|
+
description: (sends.length ? `The ${sends.length.toLocaleString()} ` + `${sends.length === 1 ? 'email' : 'emails'} in it are kept — ` + 'each keeps its own report and its unsubscribe links, and they ' + 'appear in the campaigns list as single sends. ' : 'It holds no emails. ') + (unfinished ? `${unfinished.toLocaleString()} of them ${unfinished === 1 ? 'is' : 'are'} still going out or still due, and deleting the campaign does ` + 'not stop that — the sender picks each one up from its own ' + 'record. Stop a send from its own page. ' : '') + 'Any screens, forms and contacts assigned to it come out of it and ' + 'are otherwise untouched. Only the campaign itself goes.',
|
|
273
|
+
confirmationText: 'Delete campaign'
|
|
274
|
+
}).then(()=>true).catch(()=>false);
|
|
275
|
+
if (!agreed) return;
|
|
276
|
+
setDeleting(true);
|
|
277
|
+
try {
|
|
278
|
+
const { response, payload } = await manageApi({
|
|
279
|
+
action: 'deleteCampaign',
|
|
280
|
+
campaignId
|
|
281
|
+
});
|
|
282
|
+
if (!response.ok) {
|
|
283
|
+
var _ref;
|
|
284
|
+
return void enqueueSnackbar((_ref = payload == null ? void 0 : payload.error) != null ? _ref : 'The campaign could not be deleted', {
|
|
285
|
+
variant: 'warning',
|
|
286
|
+
allowDuplicate: true
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
enqueueSnackbar('Campaign deleted', {
|
|
290
|
+
variant: 'success',
|
|
291
|
+
persist: false
|
|
292
|
+
});
|
|
293
|
+
router.push(`${basePath}/campaigns`);
|
|
294
|
+
} catch (error) {
|
|
295
|
+
console.error(error);
|
|
296
|
+
enqueueSnackbar('The campaign could not be deleted', {
|
|
297
|
+
variant: 'error',
|
|
298
|
+
allowDuplicate: true
|
|
299
|
+
});
|
|
300
|
+
} finally{
|
|
301
|
+
setDeleting(false);
|
|
302
|
+
}
|
|
303
|
+
}, [
|
|
304
|
+
basePath,
|
|
305
|
+
campaignId,
|
|
306
|
+
confirm,
|
|
307
|
+
deleting,
|
|
308
|
+
enqueueSnackbar,
|
|
309
|
+
manageApi,
|
|
310
|
+
rollup.scheduled,
|
|
311
|
+
rollup.sending,
|
|
312
|
+
router,
|
|
313
|
+
sends.length
|
|
314
|
+
]);
|
|
315
|
+
// Still settling. Falling through to the send report here would flash "this
|
|
316
|
+
// campaign could not be loaded" on every open of a campaign that exists.
|
|
317
|
+
if (!campaign && status === 'loading') return null;
|
|
318
|
+
/*
|
|
319
|
+
* NOT A CONTAINER — so it is a send id, and the send's own report is what
|
|
320
|
+
* this URL has always meant.
|
|
321
|
+
*/ if (!campaign) {
|
|
322
|
+
return /*#__PURE__*/ _jsx(CampaignReportCard, {
|
|
323
|
+
hostId: hostId,
|
|
324
|
+
campaignId: campaignId,
|
|
325
|
+
basePath: basePath
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
const listIds = (_campaign_listIds = campaign.listIds) != null ? _campaign_listIds : [];
|
|
329
|
+
const names = new Map((listDocs != null ? listDocs : []).map((list)=>{
|
|
330
|
+
var _list_name;
|
|
331
|
+
return [
|
|
332
|
+
String(list.$id),
|
|
333
|
+
String((_list_name = list.name) != null ? _list_name : list.$id)
|
|
334
|
+
];
|
|
335
|
+
}));
|
|
336
|
+
/*==========================================
|
|
337
|
+
* ONE EMAIL'S PAGE — the same one every other route to that record opens.
|
|
338
|
+
*
|
|
339
|
+
* The Emails hub's `emails/{sendId}`, which is where the messages list and
|
|
340
|
+
* a template's messages table also send a reader. One record has one page:
|
|
341
|
+
* a link that resolved somewhere else depending on which list it was
|
|
342
|
+
* clicked in would give the same message two pages, and the reader would
|
|
343
|
+
* have no way to tell which one they were on.
|
|
344
|
+
*
|
|
345
|
+
* `campaigns/{sendId}` on THIS hub goes on resolving as well, through the
|
|
346
|
+
* fall-through at the top of this file. That is not a convenience — every
|
|
347
|
+
* unsubscribe footer already delivered carries `cid={sendId}`, those
|
|
348
|
+
* messages sit in inboxes forever, and merchants paste report URLs into
|
|
349
|
+
* their own mail. Which link the console GENERATES and which URLs ANSWER
|
|
350
|
+
* are two separate questions, and only the first is decided here.
|
|
351
|
+
*
|
|
352
|
+
* Nor is a redirect the answer to the second. A redirect would be another
|
|
353
|
+
* thing to be wrong about an id that is inside an HMAC; a page that keeps
|
|
354
|
+
* working has nothing to get wrong, and it costs nothing to leave standing.
|
|
355
|
+
*=========================================*/ const sendHref = (send)=>emailsHub ? `${emailsHub}/messages/${send.$id}` : undefined;
|
|
356
|
+
/**
|
|
357
|
+
* What one of this campaign's emails can be opened into.
|
|
358
|
+
*
|
|
359
|
+
* The same two the Emails hub offers, less the campaign — this page IS the
|
|
360
|
+
* campaign. Both destinations are records the Emails console owns, so they
|
|
361
|
+
* are built from {@link useEmailsHubPath} rather than from this surface's
|
|
362
|
+
* own `basePath`.
|
|
363
|
+
*
|
|
364
|
+
* A message composed inline was built from no template, so that entry is
|
|
365
|
+
* shown DISABLED with the reason rather than hidden: an absent control and
|
|
366
|
+
* an inapplicable one look identical, and only one is honest.
|
|
367
|
+
*/ const sendActions = (send)=>{
|
|
368
|
+
var _send_templateScreenId;
|
|
369
|
+
const templateScreenId = String((_send_templateScreenId = send.templateScreenId) != null ? _send_templateScreenId : '');
|
|
370
|
+
return [
|
|
371
|
+
{
|
|
372
|
+
key: 'details',
|
|
373
|
+
label: 'Open report',
|
|
374
|
+
icon: /*#__PURE__*/ _jsx(MdiIcon, {
|
|
375
|
+
path: mdiEyeOutline.path,
|
|
376
|
+
size: 0.8
|
|
377
|
+
}),
|
|
378
|
+
href: sendHref(send),
|
|
379
|
+
disabled: !sendHref(send),
|
|
380
|
+
disabledReason: 'This site’s console URL has not resolved yet'
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
key: 'template',
|
|
384
|
+
label: 'Open its template',
|
|
385
|
+
icon: /*#__PURE__*/ _jsx(MdiIcon, {
|
|
386
|
+
path: mdiPaletteOutline.path,
|
|
387
|
+
size: 0.8
|
|
388
|
+
}),
|
|
389
|
+
href: templateScreenId && emailsHub ? `${emailsHub}/templates/${templateScreenId}` : undefined,
|
|
390
|
+
disabled: !templateScreenId || !emailsHub,
|
|
391
|
+
disabledReason: templateScreenId ? 'This site’s console URL has not resolved yet' : 'This message was not built from a template'
|
|
392
|
+
}
|
|
393
|
+
];
|
|
394
|
+
};
|
|
395
|
+
/*
|
|
396
|
+
* The campaign's emails, on the surface's own row grammar: the row opens
|
|
397
|
+
* the email's report, its subject is also a real link so it can be
|
|
398
|
+
* middle-clicked and copied, and the trailing cluster holds the actions.
|
|
399
|
+
* The window is one the card already holds, so the grid pages it.
|
|
400
|
+
*/ const sendColumns = [
|
|
401
|
+
{
|
|
402
|
+
field: 'subject',
|
|
403
|
+
headerName: 'Subject',
|
|
404
|
+
flex: 1,
|
|
405
|
+
minWidth: 220,
|
|
406
|
+
valueGetter: (_value, send)=>send.subject || send.$id,
|
|
407
|
+
/*
|
|
408
|
+
Plain text until the Emails hub resolves: a link with no destination
|
|
409
|
+
is worse than none, and this cell is the message's subject either way.
|
|
410
|
+
*/ renderCell: ({ row: send, value })=>{
|
|
411
|
+
const href = sendHref(send);
|
|
412
|
+
return href ? /*#__PURE__*/ _jsx(AppLink, {
|
|
413
|
+
href: href,
|
|
414
|
+
// The row's own handler would fire too and push the same route
|
|
415
|
+
// twice — one history entry per back press.
|
|
416
|
+
onClick: (event)=>event.stopPropagation(),
|
|
417
|
+
children: value
|
|
418
|
+
}) : value;
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
/*
|
|
423
|
+
WHAT THE EMAIL IS DOING, not the field it stores.
|
|
424
|
+
|
|
425
|
+
This branched on the status, which reads "Scheduled" about an email
|
|
426
|
+
that has delivered five hundred messages and is between batches — the
|
|
427
|
+
stored state the processor claims to resume it. The derivation reads
|
|
428
|
+
the counters beside the status; the due date is appended only where
|
|
429
|
+
there is genuinely nothing delivered yet, which is the one case a time
|
|
430
|
+
answers.
|
|
431
|
+
*/ field: 'state',
|
|
432
|
+
headerName: 'State',
|
|
433
|
+
width: 220,
|
|
434
|
+
valueGetter: (_value, send)=>{
|
|
435
|
+
const display = campaignSendDisplay(send);
|
|
436
|
+
return display.state === 'pending' && send.sendAtMs ? `${display.label} · ${new Date(send.sendAtMs).toLocaleString()}` : display.label;
|
|
437
|
+
},
|
|
438
|
+
renderCell: ({ row: send, value })=>{
|
|
439
|
+
const state = campaignSendDisplay(send).state;
|
|
440
|
+
return /*#__PURE__*/ _jsx(Chip, {
|
|
441
|
+
size: "small",
|
|
442
|
+
color: state === 'sending' ? 'info' : state === 'stopped' ? 'warning' : undefined,
|
|
443
|
+
label: value
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
/*
|
|
449
|
+
Sent over addressed, on one line: the two figures only mean anything
|
|
450
|
+
beside each other, and the gap between them is the suppression list
|
|
451
|
+
doing its work.
|
|
452
|
+
*/ field: 'sent',
|
|
453
|
+
headerName: 'Sent',
|
|
454
|
+
type: 'number',
|
|
455
|
+
align: 'right',
|
|
456
|
+
headerAlign: 'right',
|
|
457
|
+
width: 110,
|
|
458
|
+
valueGetter: (_value, send)=>{
|
|
459
|
+
var _ref;
|
|
460
|
+
var _send_stats;
|
|
461
|
+
return Number((_ref = (_send_stats = send.stats) == null ? void 0 : _send_stats.sent) != null ? _ref : 0);
|
|
462
|
+
},
|
|
463
|
+
renderCell: ({ row: send })=>{
|
|
464
|
+
var _ref, _ref1;
|
|
465
|
+
var _send_stats, _send_stats1;
|
|
466
|
+
return `${(_ref = (_send_stats = send.stats) == null ? void 0 : _send_stats.sent) != null ? _ref : 0}/${(_ref1 = (_send_stats1 = send.stats) == null ? void 0 : _send_stats1.recipients) != null ? _ref1 : 0}`;
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
field: 'opens',
|
|
471
|
+
headerName: 'Opens',
|
|
472
|
+
type: 'number',
|
|
473
|
+
align: 'right',
|
|
474
|
+
headerAlign: 'right',
|
|
475
|
+
width: 100,
|
|
476
|
+
valueGetter: (_value, send)=>{
|
|
477
|
+
var _ref;
|
|
478
|
+
var _send_stats;
|
|
479
|
+
return Number((_ref = (_send_stats = send.stats) == null ? void 0 : _send_stats.opens) != null ? _ref : 0);
|
|
480
|
+
},
|
|
481
|
+
valueFormatter: (value)=>value.toLocaleString()
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
field: 'clicks',
|
|
485
|
+
headerName: 'Clicks',
|
|
486
|
+
type: 'number',
|
|
487
|
+
align: 'right',
|
|
488
|
+
headerAlign: 'right',
|
|
489
|
+
width: 100,
|
|
490
|
+
valueGetter: (_value, send)=>{
|
|
491
|
+
var _ref;
|
|
492
|
+
var _send_stats;
|
|
493
|
+
return Number((_ref = (_send_stats = send.stats) == null ? void 0 : _send_stats.clicks) != null ? _ref : 0);
|
|
494
|
+
},
|
|
495
|
+
valueFormatter: (value)=>value.toLocaleString()
|
|
496
|
+
},
|
|
497
|
+
listActionsColumn((send)=>/*#__PURE__*/ _jsx(ListRowActions, {
|
|
498
|
+
label: String(send.subject || send.$id),
|
|
499
|
+
items: sendActions(send)
|
|
500
|
+
}), {
|
|
501
|
+
width: 72
|
|
502
|
+
})
|
|
503
|
+
];
|
|
504
|
+
const windowState = campaignWindowState(campaign, Date.now());
|
|
505
|
+
const start = campaign.startAtMs ? new Date(campaign.startAtMs).toLocaleDateString() : '';
|
|
506
|
+
const end = campaign.endAtMs ? new Date(campaign.endAtMs).toLocaleDateString() : '';
|
|
507
|
+
/* The card, named so the page chrome above it is a plain list of
|
|
508
|
+
what this surface publishes upward. */ const card = /*#__PURE__*/ _jsxs(CardDisplay, {
|
|
509
|
+
header: 'Campaign',
|
|
510
|
+
subheader: start || end ? `${start || 'Open'} – ${end || 'open-ended'}` : 'No campaign dates',
|
|
511
|
+
help: detailDocsHelp,
|
|
512
|
+
HeaderProps: {
|
|
513
|
+
/*
|
|
514
|
+
NAVIGATION, THEN THE OVERFLOW.
|
|
515
|
+
|
|
516
|
+
Editing and deleting a campaign live here rather than on the
|
|
517
|
+
campaigns table, because a record is edited on its own page in this
|
|
518
|
+
console. `RowActionsMenu` is named for table rows and its rendering
|
|
519
|
+
is not — a kebab and a menu whose items carry `destructive` and
|
|
520
|
+
`disabled` — so reusing it is what keeps this overflow behaving like
|
|
521
|
+
every other one on the surface, including the email page's.
|
|
522
|
+
*/ action: /*#__PURE__*/ _jsxs(Stack, {
|
|
523
|
+
direction: "row",
|
|
524
|
+
spacing: 1,
|
|
525
|
+
sx: {
|
|
526
|
+
alignItems: 'center'
|
|
527
|
+
},
|
|
528
|
+
children: [
|
|
529
|
+
/*#__PURE__*/ _jsx(Button, _extends({
|
|
530
|
+
component: AppLink
|
|
531
|
+
}, {
|
|
532
|
+
componentVariant: 'naked',
|
|
533
|
+
nativeButton: false
|
|
534
|
+
}, {
|
|
535
|
+
href: `${basePath}/campaigns`,
|
|
536
|
+
size: "small",
|
|
537
|
+
color: "primary",
|
|
538
|
+
children: 'All campaigns'
|
|
539
|
+
})),
|
|
540
|
+
/*#__PURE__*/ _jsx(RowActionsMenu, {
|
|
541
|
+
label: campaign.name || 'Campaign',
|
|
542
|
+
items: [
|
|
543
|
+
{
|
|
544
|
+
key: 'edit',
|
|
545
|
+
label: 'Edit campaign',
|
|
546
|
+
icon: /*#__PURE__*/ _jsx(MdiIcon, {
|
|
547
|
+
path: mdiPencilOutline.path,
|
|
548
|
+
size: 0.8
|
|
549
|
+
}),
|
|
550
|
+
onClick: ()=>{
|
|
551
|
+
setSaveError(null);
|
|
552
|
+
setEditing(true);
|
|
553
|
+
}
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
key: 'delete',
|
|
557
|
+
label: 'Delete campaign',
|
|
558
|
+
icon: /*#__PURE__*/ _jsx(MdiIcon, {
|
|
559
|
+
path: mdiDeleteOutline.path,
|
|
560
|
+
size: 0.8
|
|
561
|
+
}),
|
|
562
|
+
destructive: true,
|
|
563
|
+
disabled: deleting,
|
|
564
|
+
disabledReason: 'This campaign is being deleted',
|
|
565
|
+
onClick: ()=>void handleDelete()
|
|
566
|
+
}
|
|
567
|
+
]
|
|
568
|
+
})
|
|
569
|
+
]
|
|
570
|
+
})
|
|
571
|
+
},
|
|
572
|
+
contentGutterX: true,
|
|
573
|
+
contentGutterY: true,
|
|
574
|
+
children: [
|
|
575
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
576
|
+
spacing: 2,
|
|
577
|
+
children: [
|
|
578
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
579
|
+
direction: "row",
|
|
580
|
+
spacing: 1,
|
|
581
|
+
sx: {
|
|
582
|
+
alignItems: 'center'
|
|
583
|
+
},
|
|
584
|
+
children: [
|
|
585
|
+
/*#__PURE__*/ _jsx(Chip, {
|
|
586
|
+
size: "small",
|
|
587
|
+
label: windowState
|
|
588
|
+
}),
|
|
589
|
+
listIds.length ? listIds.map((id)=>{
|
|
590
|
+
var _names_get;
|
|
591
|
+
return /*#__PURE__*/ _jsx(Chip, {
|
|
592
|
+
size: "small",
|
|
593
|
+
variant: "outlined",
|
|
594
|
+
label: (_names_get = names.get(id)) != null ? _names_get : id
|
|
595
|
+
}, id);
|
|
596
|
+
}) : /*#__PURE__*/ _jsx(Typography, {
|
|
597
|
+
variant: "caption",
|
|
598
|
+
color: "text.secondary",
|
|
599
|
+
children: 'No lists assigned — its emails pick their own audience'
|
|
600
|
+
})
|
|
601
|
+
]
|
|
602
|
+
}),
|
|
603
|
+
/*#__PURE__*/ _jsx(Divider, {}),
|
|
604
|
+
/*#__PURE__*/ _jsx(CampaignConversionsSection, {
|
|
605
|
+
hostId: hostId,
|
|
606
|
+
sendIds: sendIds,
|
|
607
|
+
truncated: sendsTruncated,
|
|
608
|
+
basePath: basePath
|
|
609
|
+
}),
|
|
610
|
+
/*#__PURE__*/ _jsx(Divider, {}),
|
|
611
|
+
/*#__PURE__*/ _jsx(CampaignRevenueSection, {
|
|
612
|
+
hostId: hostId,
|
|
613
|
+
sendIds: sendIds,
|
|
614
|
+
truncated: sendsTruncated
|
|
615
|
+
}),
|
|
616
|
+
/*#__PURE__*/ _jsx(Divider, {}),
|
|
617
|
+
/*#__PURE__*/ _jsx(CampaignDestinationsSection, {
|
|
618
|
+
hostId: hostId,
|
|
619
|
+
sendIds: sendIds,
|
|
620
|
+
truncated: sendsTruncated
|
|
621
|
+
}),
|
|
622
|
+
/*#__PURE__*/ _jsx(Divider, {}),
|
|
623
|
+
/*#__PURE__*/ _jsx(CampaignMembersSection, {
|
|
624
|
+
hostId: hostId,
|
|
625
|
+
campaignId: campaignId,
|
|
626
|
+
startAtMs: campaign.startAtMs,
|
|
627
|
+
endAtMs: campaign.endAtMs
|
|
628
|
+
}),
|
|
629
|
+
/*#__PURE__*/ _jsx(Divider, {}),
|
|
630
|
+
/*#__PURE__*/ _jsx(Section, {
|
|
631
|
+
title: "The mail, across this campaign",
|
|
632
|
+
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
633
|
+
direction: "row",
|
|
634
|
+
spacing: 3,
|
|
635
|
+
sx: {
|
|
636
|
+
flexWrap: 'wrap'
|
|
637
|
+
},
|
|
638
|
+
children: [
|
|
639
|
+
/*#__PURE__*/ _jsx(Figure, _extends({
|
|
640
|
+
label: "Addressed"
|
|
641
|
+
}, rolled(rollup.addressed))),
|
|
642
|
+
/*#__PURE__*/ _jsx(Figure, _extends({
|
|
643
|
+
label: "Sent"
|
|
644
|
+
}, rolled(rollup.sent))),
|
|
645
|
+
/*#__PURE__*/ _jsx(Figure, _extends({
|
|
646
|
+
label: "Delivered"
|
|
647
|
+
}, rolled(rollup.delivered))),
|
|
648
|
+
/*#__PURE__*/ _jsx(Figure, _extends({
|
|
649
|
+
label: "Opens"
|
|
650
|
+
}, rolled(rollup.opens))),
|
|
651
|
+
/*#__PURE__*/ _jsx(Figure, _extends({
|
|
652
|
+
label: "Clicks"
|
|
653
|
+
}, rolled(rollup.clicks))),
|
|
654
|
+
/*#__PURE__*/ _jsx(Figure, _extends({
|
|
655
|
+
label: "Unsubscribed"
|
|
656
|
+
}, rolled(rollup.unsubscribes)))
|
|
657
|
+
]
|
|
658
|
+
})
|
|
659
|
+
}),
|
|
660
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
661
|
+
spacing: 0.5,
|
|
662
|
+
children: [
|
|
663
|
+
/*#__PURE__*/ _jsx(RateRow, {
|
|
664
|
+
label: "Open rate",
|
|
665
|
+
rate: rollup.openRate
|
|
666
|
+
}),
|
|
667
|
+
/*#__PURE__*/ _jsx(RateRow, {
|
|
668
|
+
label: "Click rate",
|
|
669
|
+
rate: rollup.clickRate
|
|
670
|
+
}),
|
|
671
|
+
/*#__PURE__*/ _jsx(RateRow, {
|
|
672
|
+
label: "Unsubscribe rate",
|
|
673
|
+
rate: rollup.unsubscribeRate
|
|
674
|
+
})
|
|
675
|
+
]
|
|
676
|
+
}),
|
|
677
|
+
/*#__PURE__*/ _jsx(Divider, {}),
|
|
678
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
679
|
+
direction: "row",
|
|
680
|
+
spacing: 1,
|
|
681
|
+
sx: {
|
|
682
|
+
alignItems: 'center',
|
|
683
|
+
justifyContent: 'space-between'
|
|
684
|
+
},
|
|
685
|
+
children: [
|
|
686
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
687
|
+
variant: "overline",
|
|
688
|
+
color: "text.secondary",
|
|
689
|
+
children: `Emails (${sends.length})`
|
|
690
|
+
}),
|
|
691
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
692
|
+
size: "small",
|
|
693
|
+
variant: composing ? 'text' : 'contained',
|
|
694
|
+
onClick: ()=>setComposing((open)=>!open),
|
|
695
|
+
children: composing ? 'Close composer' : 'Write an email'
|
|
696
|
+
})
|
|
697
|
+
]
|
|
698
|
+
}),
|
|
699
|
+
sends.length ? /*#__PURE__*/ _jsxs(Stack, {
|
|
700
|
+
spacing: 0.5,
|
|
701
|
+
children: [
|
|
702
|
+
/*#__PURE__*/ _jsx(ListTable, {
|
|
703
|
+
"aria-label": "The campaign's emails",
|
|
704
|
+
rows: sends,
|
|
705
|
+
columns: sendColumns,
|
|
706
|
+
rowHeight: TABLE_ROW_HEIGHT,
|
|
707
|
+
onOpen: (_id, send)=>{
|
|
708
|
+
const href = sendHref(send);
|
|
709
|
+
if (href) router.push(href);
|
|
710
|
+
}
|
|
711
|
+
}),
|
|
712
|
+
sendsTruncated ? /*#__PURE__*/ _jsx(Alert, {
|
|
713
|
+
severity: "info",
|
|
714
|
+
children: `Showing ${CAMPAIGN_EMAIL_CEILING} of this campaign's ` + 'emails. It has sent more — the ones listed are not ' + 'necessarily the most recent, because a send carries no ' + 'date field that every writer stamps, and the figures above ' + 'cover the emails listed.'
|
|
715
|
+
}) : null
|
|
716
|
+
]
|
|
717
|
+
}) : /*#__PURE__*/ _jsx(Typography, {
|
|
718
|
+
variant: "body2",
|
|
719
|
+
color: "text.secondary",
|
|
720
|
+
children: 'No emails in this campaign yet.'
|
|
721
|
+
}),
|
|
722
|
+
composing ? /*#__PURE__*/ _jsxs(Box, {
|
|
723
|
+
children: [
|
|
724
|
+
/*#__PURE__*/ _jsx(Divider, {
|
|
725
|
+
sx: {
|
|
726
|
+
mb: 2
|
|
727
|
+
}
|
|
728
|
+
}),
|
|
729
|
+
/*#__PURE__*/ _jsx(CampaignComposer, {
|
|
730
|
+
hostId: hostId,
|
|
731
|
+
emailCampaignId: campaignId,
|
|
732
|
+
campaignListIds: listIds,
|
|
733
|
+
campaignTopicId: campaign.topicId,
|
|
734
|
+
onSent: ()=>setComposing(false)
|
|
735
|
+
})
|
|
736
|
+
]
|
|
737
|
+
}) : null
|
|
738
|
+
]
|
|
739
|
+
}),
|
|
740
|
+
topicSource,
|
|
741
|
+
/*#__PURE__*/ _jsx(CampaignEditDrawer, {
|
|
742
|
+
open: editing,
|
|
743
|
+
onClose: ()=>setEditing(false),
|
|
744
|
+
campaign: campaign,
|
|
745
|
+
lists: (listDocs != null ? listDocs : []).map((list)=>{
|
|
746
|
+
var _list_name;
|
|
747
|
+
return {
|
|
748
|
+
value: String(list.$id),
|
|
749
|
+
label: String((_list_name = list.name) != null ? _list_name : list.$id)
|
|
750
|
+
};
|
|
751
|
+
}),
|
|
752
|
+
topics: topicOptions,
|
|
753
|
+
busy: saving,
|
|
754
|
+
error: saveError,
|
|
755
|
+
onSubmit: (values)=>void handleSave(values)
|
|
756
|
+
})
|
|
757
|
+
]
|
|
758
|
+
});
|
|
759
|
+
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
760
|
+
children: [
|
|
761
|
+
/*#__PURE__*/ _jsx(PageHeaderRecord, {
|
|
762
|
+
title: campaign.name || campaignId
|
|
763
|
+
}),
|
|
764
|
+
card
|
|
765
|
+
]
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
CampaignDetailCard.displayName = 'CampaignDetailCard';
|
|
769
|
+
export default CampaignDetailCard;
|
|
770
|
+
|
|
771
|
+
//# sourceMappingURL=campaign-detail-card.js.map
|