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(