Panels with campaign statistics and some fixes in computation of clicks.
This commit is contained in:
parent
ba996d845d
commit
d103a2cc79
18 changed files with 811 additions and 96 deletions
|
|
@ -16,8 +16,12 @@ import {
|
|||
import axios
|
||||
from "../lib/axios";
|
||||
import {getUrl} from "../lib/urls";
|
||||
import {AlignedRow} from "../lib/form";
|
||||
import {Icon} from "../lib/bootstrap-components";
|
||||
|
||||
import Chart from 'react-google-charts';
|
||||
import styles
|
||||
from "./styles.scss";
|
||||
import {Link} from "react-router-dom";
|
||||
|
||||
@withTranslation()
|
||||
@withPageHelpers
|
||||
|
|
@ -30,7 +34,8 @@ export default class Statistics extends Component {
|
|||
const t = props.t;
|
||||
|
||||
this.state = {
|
||||
entity: props.entity
|
||||
entity: props.entity,
|
||||
statisticsOverview: props.statisticsOverview
|
||||
};
|
||||
|
||||
this.refreshTimeoutHandler = ::this.periodicRefreshTask;
|
||||
|
|
@ -38,7 +43,8 @@ export default class Statistics extends Component {
|
|||
}
|
||||
|
||||
static propTypes = {
|
||||
entity: PropTypes.object
|
||||
entity: PropTypes.object,
|
||||
statisticsOverview: PropTypes.object
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
|
|
@ -48,8 +54,12 @@ export default class Statistics extends Component {
|
|||
resp = await axios.get(getUrl(`rest/campaigns-stats/${this.props.entity.id}`));
|
||||
const entity = resp.data;
|
||||
|
||||
resp = await axios.get(getUrl(`rest/campaign-statistics/${this.props.entity.id}/overview`));
|
||||
const statisticsOverview = resp.data;
|
||||
|
||||
this.setState({
|
||||
entity
|
||||
entity,
|
||||
statisticsOverview
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -57,7 +67,7 @@ export default class Statistics extends Component {
|
|||
// The periodic task runs all the time, so that we don't have to worry about starting/stopping it as a reaction to the buttons.
|
||||
await this.refreshEntity();
|
||||
if (this.refreshTimeoutHandler) { // For some reason the task gets rescheduled if server is restarted while the page is shown. That why we have this check here.
|
||||
this.refreshTimeoutId = setTimeout(this.refreshTimeoutHandler, 10000);
|
||||
this.refreshTimeoutId = setTimeout(this.refreshTimeoutHandler, 60000);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -76,49 +86,56 @@ export default class Statistics extends Component {
|
|||
const t = this.props.t;
|
||||
const entity = this.state.entity;
|
||||
|
||||
const stats = this.state.statisticsOverview;
|
||||
|
||||
const renderMetrics = (key, label, showZoomIn = true) => {
|
||||
const val = stats[key]
|
||||
|
||||
return (
|
||||
<AlignedRow label={label}><span className={styles.statsMetrics}>{val}</span>{showZoomIn && <span className={styles.zoomIn}><Link to={`/campaigns/${entity.id}/statistics/${key}`}><Icon icon="zoom-in"/></Link></span>}</AlignedRow>
|
||||
);
|
||||
}
|
||||
|
||||
const renderMetricsWithProgress = (key, label, progressBarClass, showZoomIn = true) => {
|
||||
const val = stats[key]
|
||||
|
||||
if (!stats.total) {
|
||||
return renderMetrics(key, label);
|
||||
}
|
||||
|
||||
const rate = Math.round(val / stats.total * 100);
|
||||
|
||||
return (
|
||||
<AlignedRow label={label}>
|
||||
{showZoomIn && <span className={styles.statsProgressBarZoomIn}><Link to={`/campaigns/${entity.id}/statistics/${key}`}><Icon icon="zoom-in"/></Link></span>}
|
||||
<div className={`progress ${styles.statsProgressBar}`}>
|
||||
<div
|
||||
className={`progress-bar progress-bar-${progressBarClass}`}
|
||||
role="progressbar"
|
||||
aria-valuenow={stats.bounced}
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
style={{minWidth: '6em', width: rate + '%'}}>
|
||||
{val} ({rate}%)
|
||||
</div>
|
||||
</div>
|
||||
</AlignedRow>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title>{t('campaignStatistics')}</Title>
|
||||
|
||||
<Chart
|
||||
width="100%"
|
||||
height="500px"
|
||||
chartType="PieChart"
|
||||
loader={<div>Loading Chart</div>}
|
||||
data={[
|
||||
['Task', 'Hours per Day'],
|
||||
['Work', 11],
|
||||
['Eat', 2],
|
||||
['Commute', 2],
|
||||
['Watch TV', 2],
|
||||
['Sleep', 7],
|
||||
]}
|
||||
options={{
|
||||
title: 'My Daily Activities',
|
||||
}}
|
||||
rootProps={{ 'data-testid': '1' }}
|
||||
/>
|
||||
|
||||
<Chart
|
||||
width="100%"
|
||||
height="500px"
|
||||
chartType="GeoChart"
|
||||
data={[
|
||||
['Country', 'Popularity'],
|
||||
['Germany', 200],
|
||||
['United States', 300],
|
||||
['Brazil', 400],
|
||||
['Canada', 500],
|
||||
['France', 600],
|
||||
['RU', 700],
|
||||
]}
|
||||
// Note: you will need to get a mapsApiKey for your project.
|
||||
// See: https://developers.google.com/chart/interactive/docs/basic_load_libs#load-settings
|
||||
mapsApiKey="YOUR_KEY_HERE"
|
||||
rootProps={{ 'data-testid': '1' }}
|
||||
/>
|
||||
</div>
|
||||
{renderMetrics('total', t('Total'), false)}
|
||||
{renderMetrics('delivered', t('Delivered'))}
|
||||
{renderMetrics('blacklisted', t('Blacklisted'), false)}
|
||||
{renderMetricsWithProgress('bounced', t('Bounced'), 'info')}
|
||||
{renderMetricsWithProgress('complained', t('Complaints'), 'danger')}
|
||||
{renderMetricsWithProgress('unsubscribed', t('Unsubscribed'), 'warning')}
|
||||
{!entity.open_tracking_disabled && renderMetricsWithProgress('opened', t('Opened'), 'success')}
|
||||
{!entity.click_tracking_disabled && renderMetricsWithProgress('clicks', t('Clicked'), 'success')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
52
client/src/campaigns/StatisticsLinkClicks.js
Normal file
52
client/src/campaigns/StatisticsLinkClicks.js
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
'use strict';
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes
|
||||
from 'prop-types';
|
||||
import {withTranslation} from '../lib/i18n';
|
||||
import {
|
||||
requiresAuthenticatedUser,
|
||||
Title,
|
||||
withPageHelpers
|
||||
} from '../lib/page';
|
||||
import {withErrorHandling} from '../lib/error-handling';
|
||||
import {Table} from "../lib/table";
|
||||
|
||||
@withTranslation()
|
||||
@withPageHelpers
|
||||
@withErrorHandling
|
||||
@requiresAuthenticatedUser
|
||||
export default class StatisticsLinkClicks extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const t = props.t;
|
||||
|
||||
this.state = {
|
||||
};
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
entity: PropTypes.object,
|
||||
title: PropTypes.string
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
const linksColumns = [
|
||||
{ data: 0, title: t('URL'), render: data => <code>{data}</code> },
|
||||
{ data: 1, title: t('Unique visitors') },
|
||||
{ data: 2, title: t('Total clicks') }
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title>{t('Campaign links')}</Title>
|
||||
|
||||
<Table ref={node => this.table = node} withHeader dataUrl={`rest/campaigns-link-clicks-table/${this.props.entity.id}`} columns={linksColumns} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
231
client/src/campaigns/StatisticsOpened.js
Normal file
231
client/src/campaigns/StatisticsOpened.js
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
'use strict';
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes
|
||||
from 'prop-types';
|
||||
import {withTranslation} from '../lib/i18n';
|
||||
import {
|
||||
requiresAuthenticatedUser,
|
||||
Title,
|
||||
withPageHelpers
|
||||
} from '../lib/page';
|
||||
import {
|
||||
withAsyncErrorHandler,
|
||||
withErrorHandling
|
||||
} from '../lib/error-handling';
|
||||
import axios
|
||||
from "../lib/axios";
|
||||
import {getUrl} from "../lib/urls";
|
||||
|
||||
import Chart from 'react-google-charts';
|
||||
import {AlignedRow} from "../lib/form";
|
||||
import {
|
||||
ActionLink,
|
||||
Icon
|
||||
} from "../lib/bootstrap-components";
|
||||
import {SubscriptionStatus} from "../../../shared/lists";
|
||||
|
||||
import styles from "./styles.scss";
|
||||
import {Table} from "../lib/table";
|
||||
import {Link} from "react-router-dom";
|
||||
|
||||
import mailtrainConfig from "mailtrainConfig";
|
||||
|
||||
@withTranslation()
|
||||
@withPageHelpers
|
||||
@withErrorHandling
|
||||
@requiresAuthenticatedUser
|
||||
export default class StatisticsOpened extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const t = props.t;
|
||||
|
||||
this.state = {
|
||||
entity: props.entity,
|
||||
statisticsOpened: props.statisticsOpened
|
||||
};
|
||||
|
||||
this.refreshTimeoutHandler = ::this.periodicRefreshTask;
|
||||
this.refreshTimeoutId = 0;
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
entity: PropTypes.object,
|
||||
statisticsOpened: PropTypes.object,
|
||||
agg: PropTypes.string
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async refreshEntity() {
|
||||
let resp;
|
||||
|
||||
resp = await axios.get(getUrl(`rest/campaigns-stats/${this.props.entity.id}`));
|
||||
const entity = resp.data;
|
||||
|
||||
resp = await axios.get(getUrl(`rest/campaign-statistics/${this.props.entity.id}/opened`));
|
||||
const statisticsOpened = resp.data;
|
||||
|
||||
this.setState({
|
||||
entity,
|
||||
statisticsOpened
|
||||
});
|
||||
}
|
||||
|
||||
async periodicRefreshTask() {
|
||||
// The periodic task runs all the time, so that we don't have to worry about starting/stopping it as a reaction to the buttons.
|
||||
await this.refreshEntity();
|
||||
if (this.refreshTimeoutHandler) { // For some reason the task gets rescheduled if server is restarted while the page is shown. That why we have this check here.
|
||||
this.refreshTimeoutId = setTimeout(this.refreshTimeoutHandler, 60000);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
this.periodicRefreshTask();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearTimeout(this.refreshTimeoutId);
|
||||
this.refreshTimeoutHandler = null;
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const entity = this.state.entity;
|
||||
const agg = this.props.agg;
|
||||
|
||||
const stats = this.state.statisticsOpened;
|
||||
|
||||
const subscribersColumns = [
|
||||
{ data: 0, title: t('Email') },
|
||||
{ data: 1, title: t('subscriptionId'), render: data => <code>{data}</code> },
|
||||
{ data: 2, title: t('listId'), render: data => <code>{data}</code> },
|
||||
{ data: 3, title: t('list') },
|
||||
{ data: 4, title: t('listNamespace') },
|
||||
{ data: 5, title: t('Opens count') }
|
||||
];
|
||||
|
||||
console.log(this.state.statisticsOpened);
|
||||
|
||||
const renderNavPill = (key, label) => (
|
||||
<li role="presentation" className={agg === key ? 'active' : ''}>
|
||||
<Link to={`/campaigns/${entity.id}/statistics/opened/${key}`}>{label}</Link>
|
||||
</li>
|
||||
);
|
||||
|
||||
const navPills = (
|
||||
<ul className={`nav nav-pills ${styles.navPills}`}>
|
||||
{renderNavPill('countries', t('Countries'))}
|
||||
{renderNavPill('devices', t('Devices'))}
|
||||
</ul>
|
||||
);
|
||||
|
||||
|
||||
let charts = null;
|
||||
|
||||
if (agg === 'devices') {
|
||||
charts = (
|
||||
<div className={styles.charts}>
|
||||
{navPills}
|
||||
<h4 className={styles.chartTitle}>{t('Distribution by device type')}</h4>
|
||||
<Chart
|
||||
width="100%"
|
||||
height="300px"
|
||||
chartType="PieChart"
|
||||
loader={<div>{t('Loading chart')}</div>}
|
||||
data={[
|
||||
[t('Device type'), t('Count')],
|
||||
...stats.devices.map(entry => [entry.key, entry.count])
|
||||
]}
|
||||
options={{
|
||||
chartArea: {
|
||||
left: "25%",
|
||||
top: 15,
|
||||
width: "100%",
|
||||
height: 270
|
||||
},
|
||||
tooltip: {
|
||||
showColorCode: true
|
||||
},
|
||||
legend: {
|
||||
position: "right",
|
||||
alignment: "start",
|
||||
textStyle: {
|
||||
fontSize: 14
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else if (agg === 'countries') {
|
||||
charts = (
|
||||
<div className={styles.charts}>
|
||||
{navPills}
|
||||
<h4 className={styles.sectionTitle}>{t('Distribution by country')}</h4>
|
||||
<div className="row">
|
||||
<div className={`col-md-6 ${styles.chart}`}>
|
||||
<Chart
|
||||
width="100%"
|
||||
height="300px"
|
||||
chartType="PieChart"
|
||||
loader={<div>{t('Loading chart')}</div>}
|
||||
data={[
|
||||
[t('Country'), t('Count')],
|
||||
...stats.countries.map(entry => [entry.key || t('Unknown'), entry.count])
|
||||
]}
|
||||
options={{
|
||||
chartArea: {
|
||||
left: "25%",
|
||||
top: 15,
|
||||
width: "100%",
|
||||
height: 270
|
||||
},
|
||||
tooltip: {
|
||||
showColorCode: true
|
||||
},
|
||||
legend: {
|
||||
position: "right",
|
||||
alignment: "start",
|
||||
textStyle: {
|
||||
fontSize: 14
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={`col-md-6 ${styles.chart}`}>
|
||||
<Chart
|
||||
width="100%"
|
||||
height="300px"
|
||||
chartType="GeoChart"
|
||||
data={[
|
||||
['Country', 'Count'],
|
||||
...stats.countries.map(entry => [entry.key || t('Unknown'), entry.count])
|
||||
]}
|
||||
mapsApiKey={mailtrainConfig.mapsApiKey}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
console.log(mailtrainConfig);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title>{t('Detailed Statistics')}</Title>
|
||||
|
||||
{charts}
|
||||
|
||||
<hr/>
|
||||
|
||||
<h4 className={styles.sectionTitle}>{t('List of subscribers that opened the campaign')}</h4>
|
||||
<Table ref={node => this.table = node} withHeader dataUrl={`rest/campaigns-opens-table/${entity.id}`} columns={subscribersColumns} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
55
client/src/campaigns/StatisticsSubsList.js
Normal file
55
client/src/campaigns/StatisticsSubsList.js
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
'use strict';
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes
|
||||
from 'prop-types';
|
||||
import {withTranslation} from '../lib/i18n';
|
||||
import {
|
||||
requiresAuthenticatedUser,
|
||||
Title,
|
||||
withPageHelpers
|
||||
} from '../lib/page';
|
||||
import {withErrorHandling} from '../lib/error-handling';
|
||||
import {Table} from "../lib/table";
|
||||
|
||||
@withTranslation()
|
||||
@withPageHelpers
|
||||
@withErrorHandling
|
||||
@requiresAuthenticatedUser
|
||||
export default class StatisticsSubsList extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const t = props.t;
|
||||
|
||||
this.state = {
|
||||
};
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
entity: PropTypes.object,
|
||||
status: PropTypes.number,
|
||||
title: PropTypes.string
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
const subscribersColumns = [
|
||||
{ data: 0, title: t('Email') },
|
||||
{ data: 1, title: t('subscriptionId'), render: data => <code>{data}</code> },
|
||||
{ data: 2, title: t('listId'), render: data => <code>{data}</code> },
|
||||
{ data: 3, title: t('list') },
|
||||
{ data: 4, title: t('listNamespace') }
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title>{this.props.title}</Title>
|
||||
|
||||
<Table ref={node => this.table = node} withHeader dataUrl={`rest/campaigns-subscribers-by-status-table/${this.props.entity.id}/${this.props.status}`} columns={subscribersColumns} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -27,7 +27,8 @@ import {getCampaignLabels} from './helpers';
|
|||
import {Table} from "../lib/table";
|
||||
import {
|
||||
Button,
|
||||
Icon
|
||||
Icon,
|
||||
ModalDialog
|
||||
} from "../lib/bootstrap-components";
|
||||
import axios from "../lib/axios";
|
||||
import {getUrl, getPublicUrl} from "../lib/urls";
|
||||
|
|
@ -214,8 +215,15 @@ class SendControls extends Component {
|
|||
}
|
||||
|
||||
async resetAsync() {
|
||||
await this.postAndMaskStateError(`rest/campaign-reset/${this.props.entity.id}`);
|
||||
await this.refreshEntity();
|
||||
const t = this.props.t;
|
||||
this.actionDialog(
|
||||
t('Confirm reset'),
|
||||
t('Do you want to reset the campaign? All statistics and the track of delivered messages will be lost.'),
|
||||
async () => {
|
||||
await this.postAndMaskStateError(`rest/campaign-reset/${this.props.entity.id}`);
|
||||
await this.refreshEntity();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async enableAsync() {
|
||||
|
|
@ -228,16 +236,47 @@ class SendControls extends Component {
|
|||
await this.refreshEntity();
|
||||
}
|
||||
|
||||
actionDialog(title, message, callback) {
|
||||
this.setState({
|
||||
modalTitle: title,
|
||||
modalMessage: message,
|
||||
modalCallback: callback,
|
||||
modalVisible: true
|
||||
});
|
||||
}
|
||||
|
||||
modalAction(isYes) {
|
||||
if (isYes && this.state.modalCallback) {
|
||||
this.state.modalCallback();
|
||||
}
|
||||
|
||||
this.setState({
|
||||
modalTitle: '',
|
||||
modalMessage: '',
|
||||
modalCallback: null,
|
||||
modalVisible: false
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const entity = this.props.entity;
|
||||
|
||||
const yesNoDialog = (
|
||||
<ModalDialog hidden={!this.state.modalVisible} title={this.state.modalTitle} onCloseAsync={() => this.modalAction(false)} buttons={[
|
||||
{ label: t('no'), className: 'btn-primary', onClickAsync: () => this.modalAction(false) },
|
||||
{ label: t('yes'), className: 'btn-danger', onClickAsync: () => this.modalAction(true) }
|
||||
]}>
|
||||
{this.state.modalMessage}
|
||||
</ModalDialog>
|
||||
);
|
||||
|
||||
if (entity.status === CampaignStatus.IDLE || entity.status === CampaignStatus.PAUSED || (entity.status === CampaignStatus.SCHEDULED && entity.scheduled)) {
|
||||
|
||||
const subscrInfo = entity.subscriptionsTotal === undefined ? '' : ` (${entity.subscriptionsToSend} ${t('subscribers-1')})`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>{yesNoDialog}
|
||||
<AlignedRow label={t('sendStatus')}>
|
||||
{entity.scheduled ? t('campaignIsScheduledForDelivery') : t('campaignIsReadyToBeSentOut')}
|
||||
</AlignedRow>
|
||||
|
|
@ -265,7 +304,7 @@ class SendControls extends Component {
|
|||
|
||||
} else if (entity.status === CampaignStatus.SENDING || (entity.status === CampaignStatus.SCHEDULED && !entity.scheduled)) {
|
||||
return (
|
||||
<div>
|
||||
<div>{yesNoDialog}
|
||||
<AlignedRow label={t('sendStatus')}>
|
||||
{t('campaignIsBeingSentOut')}
|
||||
</AlignedRow>
|
||||
|
|
@ -280,7 +319,7 @@ class SendControls extends Component {
|
|||
const subscrInfo = entity.subscriptionsTotal === undefined ? '' : ` (${entity.subscriptionsToSend} ${t('subscribers-1')})`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>{yesNoDialog}
|
||||
<AlignedRow label={t('sendStatus')}>
|
||||
{t('allMessagesSent!HitContinueIfYouYouWant')}
|
||||
</AlignedRow>
|
||||
|
|
@ -294,7 +333,7 @@ class SendControls extends Component {
|
|||
|
||||
} else if (entity.status === CampaignStatus.INACTIVE) {
|
||||
return (
|
||||
<div>
|
||||
<div>{yesNoDialog}
|
||||
<AlignedRow label={t('sendStatus')}>
|
||||
{t('yourCampaignIsCurrentlyDisabledClick')}
|
||||
</AlignedRow>
|
||||
|
|
@ -306,7 +345,7 @@ class SendControls extends Component {
|
|||
|
||||
} else if (entity.status === CampaignStatus.ACTIVE) {
|
||||
return (
|
||||
<div>
|
||||
<div>{yesNoDialog}
|
||||
<AlignedRow label={t('sendStatus')}>
|
||||
{t('yourCampaignIsEnabledAndSendingMessages')}
|
||||
</AlignedRow>
|
||||
|
|
|
|||
|
|
@ -1,24 +1,46 @@
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import React
|
||||
from 'react';
|
||||
|
||||
import Status from './Status';
|
||||
import Statistics from './Statistics';
|
||||
import CampaignsCUD from './CUD';
|
||||
import Content from './Content';
|
||||
import CampaignsList from './List';
|
||||
import Share from '../shares/Share';
|
||||
import Files from "../lib/files";
|
||||
import Status
|
||||
from './Status';
|
||||
import Statistics
|
||||
from './Statistics';
|
||||
import CampaignsCUD
|
||||
from './CUD';
|
||||
import Content
|
||||
from './Content';
|
||||
import CampaignsList
|
||||
from './List';
|
||||
import Share
|
||||
from '../shares/Share';
|
||||
import Files
|
||||
from "../lib/files";
|
||||
import {
|
||||
CampaignSource,
|
||||
CampaignStatus,
|
||||
CampaignType
|
||||
} from "../../../shared/campaigns";
|
||||
import TriggersCUD from './triggers/CUD';
|
||||
import TriggersList from './triggers/List';
|
||||
import TriggersCUD
|
||||
from './triggers/CUD';
|
||||
import TriggersList
|
||||
from './triggers/List';
|
||||
import StatisticsSubsList
|
||||
from "./StatisticsSubsList";
|
||||
import {SubscriptionStatus} from "../../../shared/lists";
|
||||
import StatisticsOpened
|
||||
from "./StatisticsOpened";
|
||||
import StatisticsLinkClicks
|
||||
from "./StatisticsLinkClicks";
|
||||
|
||||
|
||||
function getMenus(t) {
|
||||
const aggLabels = {
|
||||
'countries': t('Countries'),
|
||||
'devices': t('Devices')
|
||||
};
|
||||
|
||||
return {
|
||||
'campaigns': {
|
||||
title: t('campaigns'),
|
||||
|
|
@ -30,7 +52,7 @@ function getMenus(t) {
|
|||
resolve: {
|
||||
campaign: params => `rest/campaigns-settings/${params.campaignId}`
|
||||
},
|
||||
link: params => `/campaigns/${params.campaignId}/edit`,
|
||||
link: params => `/campaigns/${params.campaignId}/status`,
|
||||
navs: {
|
||||
status: {
|
||||
title: t('status'),
|
||||
|
|
@ -40,9 +62,53 @@ function getMenus(t) {
|
|||
},
|
||||
statistics: {
|
||||
title: t('statistics'),
|
||||
resolve: {
|
||||
statisticsOverview: params => `rest/campaign-statistics/${params.campaignId}/overview`
|
||||
},
|
||||
link: params => `/campaigns/${params.campaignId}/statistics`,
|
||||
visible: resolved => resolved.campaign.permissions.includes('viewStats') && (resolved.campaign.status === CampaignStatus.SENDING || resolved.campaign.status === CampaignStatus.PAUSED || resolved.campaign.status === CampaignStatus.FINISHED),
|
||||
panelRender: props => <Statistics entity={props.resolved.campaign} />
|
||||
panelRender: props => <Statistics entity={props.resolved.campaign} statisticsOverview={props.resolved.statisticsOverview} />,
|
||||
children: {
|
||||
delivered: {
|
||||
title: t('Delivered'),
|
||||
link: params => `/campaigns/${params.campaignId}/statistics/delivered`,
|
||||
panelRender: props => <StatisticsSubsList entity={props.resolved.campaign} title={t('Delivered Emails')} status={SubscriptionStatus.SUBSCRIBED} />
|
||||
},
|
||||
complained: {
|
||||
title: t('Complained'),
|
||||
link: params => `/campaigns/${params.campaignId}/statistics/complained`,
|
||||
panelRender: props => <StatisticsSubsList entity={props.resolved.campaign} title={t('Subscribers that Complained')} status={SubscriptionStatus.COMPLAINED} />
|
||||
},
|
||||
bounced: {
|
||||
title: t('Bounced'),
|
||||
link: params => `/campaigns/${params.campaignId}/statistics/bounced`,
|
||||
panelRender: props => <StatisticsSubsList entity={props.resolved.campaign} title={t('Emails that Bounced')} status={SubscriptionStatus.BOUNCED} />
|
||||
},
|
||||
unsubscribed: {
|
||||
title: t('Unsubscribed'),
|
||||
link: params => `/campaigns/${params.campaignId}/statistics/unsubscribed`,
|
||||
panelRender: props => <StatisticsSubsList entity={props.resolved.campaign} title={t('Subscribers that Unsubscribed')} status={SubscriptionStatus.UNSUBSCRIBED} />
|
||||
},
|
||||
'opened': {
|
||||
title: t('Opened'),
|
||||
resolve: {
|
||||
statisticsOpened: params => `rest/campaign-statistics/${params.campaignId}/opened`
|
||||
},
|
||||
link: params => `/campaigns/${params.campaignId}/statistics/opened/countries`,
|
||||
children: {
|
||||
':agg(countries|devices)': {
|
||||
title: (resolved, params) => aggLabels[params.agg],
|
||||
link: params => `/campaigns/${params.campaignId}/statistics/opened/${params.agg}`,
|
||||
panelRender: props => <StatisticsOpened entity={props.resolved.campaign} statisticsOpened={props.resolved.statisticsOpened} agg={props.match.params.agg} />
|
||||
}
|
||||
}
|
||||
},
|
||||
'clicks': {
|
||||
title: t('Clicks'),
|
||||
link: params => `/campaigns/${params.campaignId}/statistics/clicks`,
|
||||
panelRender: props => <StatisticsLinkClicks entity={props.resolved.campaign} />
|
||||
}
|
||||
}
|
||||
},
|
||||
':action(edit|delete)': {
|
||||
title: t('edit'),
|
||||
|
|
|
|||
|
|
@ -39,4 +39,55 @@
|
|||
|
||||
.sendButtonRow {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.statsMetrics {
|
||||
width: 10ex;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.statsProgressBar {
|
||||
margin-right: 30px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.statsProgressBarZoomIn {
|
||||
float: right;
|
||||
width: 30px;
|
||||
text-align: right;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.zoomIn {
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.navPills {
|
||||
margin-top: -3px;
|
||||
margin-bottom: 5px;
|
||||
float: right;
|
||||
|
||||
& > li {
|
||||
display: inline-block;
|
||||
float: none;
|
||||
|
||||
& > a {
|
||||
padding: 3px 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.charts {
|
||||
margin-bottom: 30px;
|
||||
|
||||
.chart {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-weight: bold;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue