diff --git a/mailpoet/assets/css/src/components-automation-analytics/colors.scss b/mailpoet/assets/css/src/components-automation-analytics/colors.scss
index 36cf8f5c1c..e471637bc4 100644
--- a/mailpoet/assets/css/src/components-automation-analytics/colors.scss
+++ b/mailpoet/assets/css/src/components-automation-analytics/colors.scss
@@ -1,4 +1,11 @@
$color-grey: #ddd;
+$color-gutenberg-grey-600: #949494;
$color-white: #fff;
$color-black: #1e1e1e;
$color-primary: #007cba;
+$color-gutenberg-alert-green: #4ab866;
+$color-wp-green-0: #edfaef;
+$color-wp-green-60: #007017;
+$color-wp-yellow-0: #fcf9e8;
+$color-wp-yellow-50: #996800;
+$color-wp-yellow-60: #755100;
diff --git a/mailpoet/assets/css/src/components-automation-analytics/tabs/email.scss b/mailpoet/assets/css/src/components-automation-analytics/tabs/email.scss
new file mode 100644
index 0000000000..5082855320
--- /dev/null
+++ b/mailpoet/assets/css/src/components-automation-analytics/tabs/email.scss
@@ -0,0 +1,52 @@
+@import '../colors';
+
+.mailpoet-analytics-main-value {
+ font-size: 13px;
+ line-height: 18px;
+ margin: 0 0 4px;
+ font-weight: 400;
+}
+.mailpoet-analytics-badge {
+ font-weight: 400;
+}
+
+.mailpoet-automation-analytics-email-name {
+ max-width: 300px;
+}
+
+.mailpoet-automation-analytics-email-clicked {
+ font-weight: 600;
+}
+
+.mailpoet-analytics-badge {
+ background: $color-grey;
+ border-radius: 2px;
+ font-size: 11px;
+ line-height: 16px;
+ margin-right: 4px;
+ padding: 4px 8px;
+}
+
+.mailpoet-analytics-badge-success {
+ color: $color-gutenberg-alert-green;
+
+ .mailpoet-analytics-badge {
+ background: $color-wp-green-0;
+ color: $color-wp-green-60;
+ }
+}
+
+.mailpoet-analytics-badge-warning {
+
+ color: $color-wp-yellow-50;
+ .mailpoet-analytics-badge {
+ background: $color-wp-yellow-0;
+ color: $color-wp-yellow-60;
+ }
+}
+
+.mailpoet-automation-analytics-table-subvalue {
+ color: $color-gutenberg-grey-600;
+ font-size: 12px;
+ margin: 0;
+}
diff --git a/mailpoet/assets/css/src/components-automation-analytics/tabs/general.scss b/mailpoet/assets/css/src/components-automation-analytics/tabs/general.scss
new file mode 100644
index 0000000000..a153097c95
--- /dev/null
+++ b/mailpoet/assets/css/src/components-automation-analytics/tabs/general.scss
@@ -0,0 +1,32 @@
+@import '../colors';
+
+.mailpoet-analytics-tabs {
+ background: $color-white;
+ border: 1px solid $color-grey;
+
+ .components-tab-panel__tabs-item.is-active {
+ box-shadow: inset 0 -4px 0 0 var(--wp-admin-theme-color);
+ }
+
+ .components-tab-panel__tabs {
+ border-bottom: 1px solid $color-grey;
+ }
+
+ .components-tab-panel__tab-content {
+ padding: 0;
+ }
+
+ .woocommerce-table {
+ box-shadow: none;
+ margin-bottom: 0;
+
+ /** Remove table header */
+ .components-card-header {
+ display: none;
+ }
+ }
+}
+
+.woocommerce-summary__item {
+ box-sizing: border-box;
+}
diff --git a/mailpoet/assets/css/src/components-automation-analytics/tabs/index.scss b/mailpoet/assets/css/src/components-automation-analytics/tabs/index.scss
new file mode 100644
index 0000000000..8f3fad3ee7
--- /dev/null
+++ b/mailpoet/assets/css/src/components-automation-analytics/tabs/index.scss
@@ -0,0 +1,2 @@
+@import 'general';
+@import 'email';
diff --git a/mailpoet/assets/js/src/automation/integrations/mailpoet/analytics/components/overview/index.tsx b/mailpoet/assets/js/src/automation/integrations/mailpoet/analytics/components/overview/index.tsx
index 1648fb2ed4..b0a562b251 100644
--- a/mailpoet/assets/js/src/automation/integrations/mailpoet/analytics/components/overview/index.tsx
+++ b/mailpoet/assets/js/src/automation/integrations/mailpoet/analytics/components/overview/index.tsx
@@ -19,12 +19,12 @@ function getEmailPercentage(
}
const data = overview.data[type] ?? null;
- const total = overview.data?.total ?? null;
- if (!data || !total || !data[period] || !total[period]) {
+ const sent = overview.data?.sent ?? null;
+ if (!data || !sent || !data[period] || !sent[period]) {
return 0;
}
- const percentage = (data[period] * 100) / total[period] / 100;
+ const percentage = (data[period] * 100) / sent[period] / 100;
return percentage;
}
diff --git a/mailpoet/assets/js/src/automation/integrations/mailpoet/analytics/components/tabs/emails/actions.tsx b/mailpoet/assets/js/src/automation/integrations/mailpoet/analytics/components/tabs/emails/actions.tsx
new file mode 100644
index 0000000000..14bb3aeeac
--- /dev/null
+++ b/mailpoet/assets/js/src/automation/integrations/mailpoet/analytics/components/tabs/emails/actions.tsx
@@ -0,0 +1,5 @@
+
+
+export function Actions({id}): JSX.Element {
+ return
Actions for {id}
+}
diff --git a/mailpoet/assets/js/src/automation/integrations/mailpoet/analytics/components/tabs/emails/cell.tsx b/mailpoet/assets/js/src/automation/integrations/mailpoet/analytics/components/tabs/emails/cell.tsx
new file mode 100644
index 0000000000..e465e5e1c6
--- /dev/null
+++ b/mailpoet/assets/js/src/automation/integrations/mailpoet/analytics/components/tabs/emails/cell.tsx
@@ -0,0 +1,29 @@
+type CellProps = {
+ value: number | string;
+ subValue?: number | string;
+ link?: string;
+ badge?: string;
+ badgeType?: string;
+ className?: string;
+}
+export function Cell({value, subValue, link, badge, badgeType, className}: CellProps): JSX.Element {
+
+ const badgeElement = badge ? {badge} : null
+ const mainElement = link === undefined ?
+
+ {badgeElement}
+ {value}
+
:
+
+ {badgeElement}
+ {value}
+
+
+
+ return (
+
+ {mainElement}
+
{subValue}
+
+ )
+}
diff --git a/mailpoet/assets/js/src/automation/integrations/mailpoet/analytics/components/tabs/emails/index.tsx b/mailpoet/assets/js/src/automation/integrations/mailpoet/analytics/components/tabs/emails/index.tsx
index c554d8a378..e739913b9a 100644
--- a/mailpoet/assets/js/src/automation/integrations/mailpoet/analytics/components/tabs/emails/index.tsx
+++ b/mailpoet/assets/js/src/automation/integrations/mailpoet/analytics/components/tabs/emails/index.tsx
@@ -1,3 +1,98 @@
+import {TableCard} from "@woocommerce/components/build";
+import {useSelect} from "@wordpress/data";
+import {EmailStats, OverviewSection, storeName} from "../../../store";
+import {__} from "@wordpress/i18n";
+import {useEffect, useState} from "react";
+import {calculateSummary} from "./summary";
+import {transformEmailsToRows} from "./rows";
+
+const headers = [
+ {
+ key: 'email',
+ label: __('Email', 'mailpoet'),
+ },
+ {
+ key: 'sent',
+ label: __('Sent', 'mailpoet'),
+ isLeftAligned:false,
+ isNumeric: true
+ },
+ {
+ key: 'opened',
+ label: __('Opened', 'mailpoet'),
+ isLeftAligned:false,
+ isNumeric: true
+ },
+ {
+ key: 'clicked',
+ label: __('Clicked', 'mailpoet'),
+ isLeftAligned:false,
+ isNumeric: true
+ },
+ {
+ key: 'orders',
+ label: __('Orders', 'mailpoet'),
+ isLeftAligned:false,
+ isNumeric: true
+ },
+ {
+ key: 'revenue',
+ label: __('Revenue', 'mailpoet'),
+ isLeftAligned:false,
+ isNumeric: true
+ },
+ {
+ key: 'unsubscribed',
+ label: __('Unsubscribed', 'mailpoet'),
+ isLeftAligned:false,
+ isNumeric: true
+ },
+ {
+ key: 'actions',
+ label: ''
+ },
+];
+
export function Emails(): JSX.Element {
- return Emails
;
+ const { overview } = useSelect((s) => ({
+ overview: s(storeName).getSection('overview'),
+ })) as { overview: OverviewSection };
+
+ const [visibleEmails, setVisibleEmails] = useState(undefined);
+ const [currentPage, setCurrentPage] = useState(1);
+ const [rowsPerPage, setRowsPerPage] = useState(5);
+ //const [rowsPerPage, setRowsPerPage] = useState(25);
+ useEffect(
+ () => {
+ setVisibleEmails(overview.data !== undefined ? Object.values(overview.data.emails).splice((currentPage-1)*rowsPerPage, rowsPerPage): undefined)
+ }, [overview.data]
+ )
+
+ const rows = visibleEmails !== undefined ? transformEmailsToRows(visibleEmails) : [];
+
+ const summary = calculateSummary(visibleEmails??[]);
+ return (param) => {
+ if (type === 'paged') {
+ setCurrentPage(param);
+ setVisibleEmails(overview.data !== undefined ? Object.values(overview.data.emails).splice((param-1)*rowsPerPage, rowsPerPage): undefined)
+ } else if(type==='per_page') {
+ setCurrentPage(1);
+ setRowsPerPage(param);
+ setVisibleEmails(overview.data !== undefined ? Object.values(overview.data.emails).splice(0, param): undefined)
+ }
+ }
+ }
+ query={ {paged: currentPage, sort: {key: 'email', direction: 'asc'}} }
+ rows={ rows }
+ headers={ headers }
+ showMenu={ false }
+ rowsPerPage={ rowsPerPage }
+ onRowClick={ () => {} }
+ totalRows={ overview.data !== undefined ? Object.values(overview.data.emails).length : 0 }
+ summary={ summary }
+ isLoading={ overview.data === undefined }
+ />
}
diff --git a/mailpoet/assets/js/src/automation/integrations/mailpoet/analytics/components/tabs/emails/rows.tsx b/mailpoet/assets/js/src/automation/integrations/mailpoet/analytics/components/tabs/emails/rows.tsx
new file mode 100644
index 0000000000..b52fd53933
--- /dev/null
+++ b/mailpoet/assets/js/src/automation/integrations/mailpoet/analytics/components/tabs/emails/rows.tsx
@@ -0,0 +1,97 @@
+import {EmailStats} from "../../../store";
+import {__, sprintf} from "@wordpress/i18n";
+import {Actions} from "./actions";
+import {locale} from "../../../../../../config";
+import {Cell} from "./cell";
+
+const percentageFormatter = Intl.NumberFormat(locale.toString(), { style: 'percent', maximumFractionDigits: 2 });
+
+function calculatePercentage(value: number, base: number, canBeNegative: boolean = false) : number {
+ if (base === 0) {
+ return 0;
+ }
+ const percentage = (value * 100) / base;
+ return (canBeNegative) ? percentage - 100 : percentage;
+}
+
+function percentageBadgeCalculation(percentage:number) : {badge: string, badgeType: string} {
+ if (percentage > 3) {
+ return {badge: __('Excellent', 'mailpoet'), badgeType: 'mailpoet-analytics-badge-success'}
+ } else if (percentage > 1) {
+ return {badge: __('Good', 'mailpoet'), badgeType: 'mailpoet-analytics-badge-success'}
+ }
+ return {badge: __('Average', 'mailpoet'), badgeType: 'mailpoet-analytics-badge-warning'}
+}
+
+export function transformEmailsToRows(emails: EmailStats[]) {
+ return emails.map((email) => {
+
+ // Shows the percentage of clicked emails compared to the number of sent emails
+ const clickedPercentage = calculatePercentage(email.clicked.current, email.sent.current);
+ const clickedBadge = percentageBadgeCalculation(clickedPercentage);
+
+ return [
+ {
+ display: | ,
+ value: email.name
+ },
+ {
+ display: | ,
+ value: email.sent.current
+ },
+ {
+ display: | ,
+ value: email.opened.current
+ },
+ {
+ display: 0 ? 'mailpoet-automation-analytics-email-clicked' : '' }
+ subValue={percentageFormatter.format(clickedPercentage/100)}
+ badge={email.sent.current > 0 ? clickedBadge.badge : undefined}
+ badgeType={email.sent.current > 0 ? clickedBadge.badgeType : undefined}
+ />,
+ value: email.clicked.current
+ },
+ {
+ display: | ,
+ value: email.orders.current
+ },
+ {
+ display: | ,
+ value: email.revenue.current
+ },
+ {
+ display: | ,
+ value: email.unsubscribed.current
+ },
+ {
+ display: ,
+ value: null
+ },
+ ]
+ })
+}
diff --git a/mailpoet/assets/js/src/automation/integrations/mailpoet/analytics/components/tabs/emails/summary.tsx b/mailpoet/assets/js/src/automation/integrations/mailpoet/analytics/components/tabs/emails/summary.tsx
new file mode 100644
index 0000000000..86be4f3393
--- /dev/null
+++ b/mailpoet/assets/js/src/automation/integrations/mailpoet/analytics/components/tabs/emails/summary.tsx
@@ -0,0 +1,37 @@
+import {__} from "@wordpress/i18n";
+import {locale} from "../../../../../../config";
+import {EmailStats} from "../../../store";
+import {formattedPrice} from "../../../formatter";
+
+export function calculateSummary(rows:EmailStats[]) {
+ if (rows.length === 0) {
+ return [];
+ }
+ const data = rows.reduce((acc, row) => {
+ acc.sent += row.sent.current;
+ acc.opened += row.opened.current;
+ acc.clicked += row.clicked.current;
+ acc.orders += row.orders.current;
+ acc.unsubscribed += row.unsubscribed.current;
+ acc.revenue += row.revenue.current;
+ return acc;
+ }, {
+ sent: 0,
+ opened: 0,
+ clicked: 0,
+ orders: 0,
+ unsubscribed: 0,
+ revenue: 0,
+ });
+
+ const summary = [
+ { label: __('sent', 'mailpoet'), value: Intl.NumberFormat(locale.toString(), { notation: 'compact' }).format(data.sent) },
+ { label: __('opened', 'mailpoet'), value: Intl.NumberFormat(locale.toString(), { notation: 'compact' }).format(data.opened) },
+ { label: __('clicked', 'mailpoet'), value: Intl.NumberFormat(locale.toString(), { notation: 'compact' }).format(data.clicked) },
+ { label: __('orders', 'mailpoet'), value: Intl.NumberFormat(locale.toString(), { notation: 'compact' }).format(data.orders) },
+ { label: __('revenue', 'mailpoet'), value: formattedPrice(data.revenue) },
+ { label: __('unsubscribed', 'mailpoet'), value: Intl.NumberFormat(locale.toString(), { notation: 'compact' }).format(data.unsubscribed) },
+ ];
+
+ return summary;
+}
diff --git a/mailpoet/assets/js/src/automation/integrations/mailpoet/analytics/store/types.ts b/mailpoet/assets/js/src/automation/integrations/mailpoet/analytics/store/types.ts
index ec8d97b82c..53a1ad5832 100644
--- a/mailpoet/assets/js/src/automation/integrations/mailpoet/analytics/store/types.ts
+++ b/mailpoet/assets/js/src/automation/integrations/mailpoet/analytics/store/types.ts
@@ -8,18 +8,31 @@ type Automation = {
steps: Record;
};
-type CurrentAndPrevious = {
+export type CurrentAndPrevious = {
current: number;
previous: number;
};
+export type EmailStats = {
+ id: number;
+ name: string;
+ sent: CurrentAndPrevious;
+ opened: CurrentAndPrevious;
+ clicked: CurrentAndPrevious;
+ orders: CurrentAndPrevious;
+ revenue: CurrentAndPrevious;
+ unsubscribed: CurrentAndPrevious;
+}
+
type OverviewSectionData = SectionData & {
opened: CurrentAndPrevious;
clicked: CurrentAndPrevious;
orders: CurrentAndPrevious;
+ unsubscribed: CurrentAndPrevious;
revenue: CurrentAndPrevious;
revenue_formatted: CurrentAndPrevious;
- total: CurrentAndPrevious;
+ sent: CurrentAndPrevious;
+ emails: Record
};
export type SectionData = Record;
diff --git a/mailpoet/lib/Automation/Integrations/MailPoet/Analytics/Controller/OverviewStatisticsController.php b/mailpoet/lib/Automation/Integrations/MailPoet/Analytics/Controller/OverviewStatisticsController.php
index 23d1d006a2..ed5ba37a59 100644
--- a/mailpoet/lib/Automation/Integrations/MailPoet/Analytics/Controller/OverviewStatisticsController.php
+++ b/mailpoet/lib/Automation/Integrations/MailPoet/Analytics/Controller/OverviewStatisticsController.php
@@ -42,15 +42,17 @@ class OverviewStatisticsController {
]
);
$data = [
- 'total' => ['current' => 0, 'previous' => 0],
+ 'sent' => ['current' => 0, 'previous' => 0],
'opened' => ['current' => 0, 'previous' => 0],
'clicked' => ['current' => 0, 'previous' => 0],
'orders' => ['current' => 0, 'previous' => 0],
+ 'unsubscribed' => ['current' => 0, 'previous' => 0],
'revenue' => ['current' => 0, 'previous' => 0],
'revenue_formatted' => [
'current' => $formattedEmptyRevenue,
'previous' => $formattedEmptyRevenue,
],
+ 'emails' => [],
];
if (!$emails) {
return $data;
@@ -69,12 +71,22 @@ class OverviewStatisticsController {
$query->getPrimaryBefore(),
$requiredData
);
- foreach ($currentStatistics as $statistic) {
- $data['total']['current'] += $statistic->getTotalSentCount();
+ foreach ($currentStatistics as $newsletterId => $statistic) {
+ $data['sent']['current'] += $statistic->getTotalSentCount();
$data['opened']['current'] += $statistic->getOpenCount();
$data['clicked']['current'] += $statistic->getClickCount();
+ $data['unsubscribed']['current'] += $statistic->getUnsubscribeCount();
$data['orders']['current'] += $statistic->getWooCommerceRevenue() ? $statistic->getWooCommerceRevenue()->getOrdersCount() : 0;
$data['revenue']['current'] += $statistic->getWooCommerceRevenue() ? $statistic->getWooCommerceRevenue()->getValue() : 0;
+ $newsletter = $this->newslettersRepository->findOneById($newsletterId);
+ $data['emails'][$newsletterId]['id'] = $newsletterId;
+ $data['emails'][$newsletterId]['name'] = $newsletter ? $newsletter->getSubject() : '';
+ $data['emails'][$newsletterId]['sent']['current'] = $statistic->getTotalSentCount();
+ $data['emails'][$newsletterId]['opened']['current'] = $statistic->getOpenCount();
+ $data['emails'][$newsletterId]['clicked']['current'] = $statistic->getClickCount();
+ $data['emails'][$newsletterId]['unsubscribed']['current'] = $statistic->getUnsubscribeCount();
+ $data['emails'][$newsletterId]['orders']['current'] = $statistic->getWooCommerceRevenue() ? $statistic->getWooCommerceRevenue()->getOrdersCount() : 0;
+ $data['emails'][$newsletterId]['revenue']['current'] = $statistic->getWooCommerceRevenue() ? $statistic->getWooCommerceRevenue()->getValue() : 0;
}
$previousStatistics = $this->newsletterStatisticsRepository->getBatchStatistics(
@@ -84,12 +96,19 @@ class OverviewStatisticsController {
$requiredData
);
- foreach ($previousStatistics as $statistic) {
- $data['total']['previous'] += $statistic->getTotalSentCount();
+ foreach ($previousStatistics as $newsletterId => $statistic) {
+ $data['sent']['previous'] += $statistic->getTotalSentCount();
$data['opened']['previous'] += $statistic->getOpenCount();
$data['clicked']['previous'] += $statistic->getClickCount();
+ $data['unsubscribed']['previous'] += $statistic->getUnsubscribeCount();
$data['orders']['previous'] += $statistic->getWooCommerceRevenue() ? $statistic->getWooCommerceRevenue()->getOrdersCount() : 0;
$data['revenue']['previous'] += $statistic->getWooCommerceRevenue() ? $statistic->getWooCommerceRevenue()->getValue() : 0;
+ $data['emails'][$newsletterId]['sent']['previous'] = $statistic->getTotalSentCount();
+ $data['emails'][$newsletterId]['opened']['previous'] = $statistic->getOpenCount();
+ $data['emails'][$newsletterId]['clicked']['previous'] = $statistic->getClickCount();
+ $data['emails'][$newsletterId]['unsubscribed']['previous'] = $statistic->getUnsubscribeCount();
+ $data['emails'][$newsletterId]['orders']['previous'] = $statistic->getWooCommerceRevenue() ? $statistic->getWooCommerceRevenue()->getOrdersCount() : 0;
+ $data['emails'][$newsletterId]['revenue']['previous'] = $statistic->getWooCommerceRevenue() ? $statistic->getWooCommerceRevenue()->getValue() : 0;
}
$data['revenue_formatted']['current'] = $this->wooCommerceHelper->getRawPrice(
|