559 lines
22 KiB
Twig
559 lines
22 KiB
Twig
{#
|
|
/**
|
|
* Copyright (C) 2022 Xibo Signage Ltd
|
|
*
|
|
* Xibo - Digital Signage - http://www.xibo.org.uk
|
|
*
|
|
* This file is part of Xibo.
|
|
*
|
|
* Xibo is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* any later version.
|
|
*
|
|
* Xibo is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
#}
|
|
{% import "inline.twig" as inline %}
|
|
{% set gridId = random() %}
|
|
<script type="text/javascript" nonce="{{ cspNonce }}">
|
|
$(function() {
|
|
$('#connectors').on('connectors.loaded', function() {
|
|
var $sspConnector = $('#connectors').find('div[data-connector-class-name-last="XiboSspConnector"]');
|
|
var $button = $('<button class="btn btn-info" role="button">{% trans "Activity Report" %}</button>');
|
|
$button.on('click', function() {
|
|
sspActivityDialogOpen($sspConnector);
|
|
});
|
|
$sspConnector.find('.card-footer').append($button);
|
|
});
|
|
});
|
|
|
|
window.sspFormOpen = function(dialog) {
|
|
// CMS Url
|
|
var $cmsUrl = dialog.find('input[name="cmsUrl"]');
|
|
if ($cmsUrl.val() === '') {
|
|
$cmsUrl.val(window.location.origin || null);
|
|
}
|
|
|
|
// Share of voice
|
|
var $shareOfVoice = dialog.find('.share-of-voice-control');
|
|
$shareOfVoice.on('change paste keyup', function() {
|
|
dialog.find($(this).data('partner')).val(((100 * $(this).val()) / 3600).toFixed(2));
|
|
}).trigger('change');
|
|
|
|
$shareOfVoice.each(function(index, element) {
|
|
dialog.find($(element).data('partner')).on('change paste keyup', function() {
|
|
$(element).val(Math.round((3600 * $(this).val()) / 100));
|
|
});
|
|
});
|
|
};
|
|
|
|
window.sspActivityDialogOpen = function($sspConnector) {
|
|
bootbox.hideAll();
|
|
// Put the template into a modal.
|
|
var template = Handlebars.compile($('#connector-ssp-activity').html());
|
|
var dialog = bootbox.dialog({
|
|
message: template({}),
|
|
title: '{{ "Activity Report"|trans }}',
|
|
animate: false,
|
|
size: 'xl',
|
|
onShown: function(e) {
|
|
$.ajax({
|
|
url: $sspConnector.data('proxyUrl').replace(':method', 'getAvailablePartnersFilter'),
|
|
success: function(result) {
|
|
if (result) {
|
|
const $form = $('#activity-log-filters');
|
|
const $partnerId = $form.find('select[name=partnerId]');
|
|
|
|
$.each(result, function(partnerKey, available) {
|
|
$partnerId.append($('<option>', {
|
|
id: partnerKey,
|
|
text: available.name,
|
|
value : partnerKey
|
|
}));
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
XiboInitialise('#{{ gridId }}');
|
|
|
|
dialog.closest('.modal').addClass('modal-big');
|
|
|
|
// Initialize summary filter
|
|
let filter;
|
|
|
|
// Make a datatable
|
|
var table;
|
|
var chart;
|
|
|
|
// Disable the Apply button on initialization
|
|
let applyBtn = dialog.find('a[data-apply-button="true"]').addClass('disabled');
|
|
let displaySelect = dialog.find('select[name="displayId"]');
|
|
let formError = $('.form-error').hide();
|
|
|
|
// Enable the applyBtn when there's a display selected
|
|
displaySelect.on('change', function () {
|
|
let value = $(this).val();
|
|
|
|
applyBtn.toggleClass('disabled', !value);
|
|
|
|
$('.alert-danger').toggle(!value);
|
|
});
|
|
|
|
// Find the apply button
|
|
applyBtn.on('click', function(e) {
|
|
if ($(this).hasClass('disabled')) {
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
|
|
$(this).addClass('disabled').append('<span class="saving fa fa-cog fa-spin p-1"></span>');
|
|
|
|
if (!table) {
|
|
initTable();
|
|
} else {
|
|
table.ajax.reload(function(json) {
|
|
let filteredData = filterData(json.data, filter);
|
|
|
|
drawSummaryTable(filteredData, filter);
|
|
}, true);
|
|
}
|
|
});
|
|
|
|
// Watch for filter option changes in Summary tab
|
|
dialog.find('select[name="campaignFilter"]').on('change', function(e) {
|
|
if (displaySelect.val()) {
|
|
table.ajax.reload(function(json) {
|
|
filter = $(e.target).val();
|
|
let filteredData = filterData(json.data, filter);
|
|
|
|
drawSummaryTable(filteredData, filter);
|
|
}, true);
|
|
}
|
|
});
|
|
|
|
// Parse activity log summary according to filter selected
|
|
function filterData(summaryData, filter='hour') {
|
|
let filterOption = '';
|
|
|
|
// Group the data according the filter selected
|
|
let groups = summaryData.reduce((group, item) => {
|
|
let hourKey = `${moment(item.scheduledAt).format('YYYY-MM-DD HH')}:00`;
|
|
let errorKey = item.errorCode || '';
|
|
|
|
if (filter === 'hour') {
|
|
filterOption = hourKey;
|
|
} else if (filter === 'errorCode') {
|
|
filterOption = errorKey;
|
|
} else {
|
|
filterOption = `${hourKey} - ${errorKey}`;
|
|
}
|
|
|
|
if (!group[filterOption]) {
|
|
group[filterOption] = [];
|
|
}
|
|
|
|
group[filterOption].push({key: filterOption, ...item});
|
|
|
|
return group;
|
|
}, {});
|
|
|
|
const groupKeys = Object.keys(groups);
|
|
|
|
// Aggregate the data
|
|
let data = groupKeys.map(key => {
|
|
return groups[key].reduce((acc, {key, campaignId, scheduledAt, displayId, errors, isPlayed, isErrored, impressions, impressionActual, errorCode}) => {
|
|
acc['key'] = String(key);
|
|
acc['errorCount'] = errors + (acc['errorCount'] || 0);
|
|
acc['playCount'] = (isPlayed ? 1 : 0) + (acc['playCount'] || 0);
|
|
acc['missesCount'] = (!isPlayed && !isErrored ? 1 : 0) + (acc['missesCount'] || 0);
|
|
acc['impressions'] = impressions + (acc['impressions'] || 0);
|
|
acc['impressionActual'] = impressionActual + (acc['impressionActual'] || 0);
|
|
acc['campaignId'] = campaignId;
|
|
acc['date'] = moment(scheduledAt).format('MM-DD-YYYY');
|
|
acc['time'] = `${moment(scheduledAt).format('HH')}:00`;
|
|
acc['errorCode'] = errorCode;
|
|
|
|
return acc;
|
|
}, {});
|
|
})
|
|
|
|
// Get new data stats
|
|
let stats = {
|
|
totalErrorCount: 0,
|
|
totalPlayCount: 0,
|
|
totalMissCount: 0,
|
|
totalImpressions: 0,
|
|
impressionActual: 0
|
|
}
|
|
|
|
$.each(data, function(index, el) {
|
|
stats.totalErrorCount += el.errorCount;
|
|
stats.totalPlayCount += el.playCount;
|
|
stats.totalMissCount += el.missesCount;
|
|
stats.totalImpressions += el.impressions;
|
|
stats.impressionActual += el.impressionActual;
|
|
});
|
|
|
|
return {
|
|
data: data.sort((a,b) => a.key.replace(/-|\s/g,"") - b.key.replace(/-|\s/g,"")),
|
|
stats: stats
|
|
}
|
|
}
|
|
|
|
function initTable() {
|
|
table = $('#ssp-activity').DataTable({
|
|
language: dataTablesLanguage,
|
|
dom: dataTablesTemplate,
|
|
serverSide: false,
|
|
stateSave: false,
|
|
responsive: true,
|
|
filter: false,
|
|
searchDelay: 3000,
|
|
order: [[ 0, 'asc']],
|
|
ajax: {
|
|
url: $sspConnector.data('proxyUrl').replace(':method', 'activity'),
|
|
data: function (d) {
|
|
$.extend(d, $('#ssp-activity').closest('.XiboGrid').find('.FilterDiv form').serializeObject());
|
|
},
|
|
dataSrc: function (json) {
|
|
if (json.success === false) {
|
|
formError.show().text(json.message);
|
|
|
|
return [];
|
|
}
|
|
|
|
formError.hide().text('');
|
|
return json.data || [];
|
|
}
|
|
},
|
|
columns: [
|
|
{
|
|
data: 'scheduledAt',
|
|
responsivePriority: 1,
|
|
render: function(data, type) {
|
|
if (type !== 'display' && type !== 'export' || data == null) {
|
|
return data;
|
|
}
|
|
return moment(data).format(jsDateFormat);
|
|
},
|
|
},
|
|
{ data: 'campaignId', responsivePriority: 1 },
|
|
{ data: 'displayId', responsivePriority: 1 },
|
|
{ data: 'isPlayed', responsivePriority: 1 },
|
|
{ data: 'isErrored', responsivePriority: 1 },
|
|
{ data: 'impressions', responsivePriority: 2 },
|
|
{
|
|
data: 'impressionDate',
|
|
responsivePriority: 10,
|
|
render: function(data, type) {
|
|
if (type !== 'display' && type !== 'export' || data == null) {
|
|
return data;
|
|
}
|
|
return moment(data).format(jsDateFormat);
|
|
},
|
|
},
|
|
{ data: 'impressionActual', responsivePriority: 10 },
|
|
{ data: 'errors', responsivePriority: 10 },
|
|
{
|
|
data: 'errorDate',
|
|
responsivePriority: 10,
|
|
render: function(data, type) {
|
|
if (type !== 'display' && type !== 'export' || data == null) {
|
|
return data;
|
|
}
|
|
return moment(data).format(jsDateFormat);
|
|
},
|
|
},
|
|
{ data: 'errorCode', responsivePriority: 10 },
|
|
],
|
|
|
|
initComplete: function(settings, json) {
|
|
if (json && json.data) {
|
|
let filteredData = filterData(json.data, filter);
|
|
drawSummaryTable(filteredData, filter);
|
|
}
|
|
},
|
|
|
|
footerCallback: function(row, data, start, end, display) {
|
|
var json = this.api().ajax.json();
|
|
if (json && json.stats) {
|
|
$(this.api().column(0).footer()).html(json.stats.scheduled || 0);
|
|
$(this.api().column(3).footer()).html(json.stats.played || 0);
|
|
$(this.api().column(4).footer()).html(json.stats.errored || 0);
|
|
$(this.api().column(5).footer()).html(json.stats.actualImpressions || 0);
|
|
}
|
|
},
|
|
|
|
drawCallback: function(settings) {
|
|
setTimeout(function() {
|
|
dialog.find('a[data-apply-button="true"]')
|
|
.removeClass('disabled')
|
|
.find('.saving').remove();
|
|
}, 300);
|
|
},
|
|
});
|
|
|
|
table.on('draw', dataTableDraw);
|
|
table.on('processing.dt', dataTableProcessing);
|
|
dataTableAddButtons(table, $('#ssp-activity_wrapper').find('.dataTables_buttons'));
|
|
}
|
|
|
|
function drawSummaryTable(filteredData, filter='hour') {
|
|
let summaryTable = $('#ssp-activity-summary').dataTable({
|
|
"bDestroy": true,
|
|
data: filteredData.data,
|
|
columns: [
|
|
{ data: 'date', responsivePriority: 1 },
|
|
{ data: 'time', responsivePriority: 1 },
|
|
{ data: 'campaignId', responsivePriority: 1 },
|
|
{ data: 'playCount', responsivePriority: 1 },
|
|
{ data: 'errorCount', responsivePriority: 1 },
|
|
{ data: 'missesCount', responsivePriority: 1 },
|
|
{ data: 'impressions', responsivePriority: 2 },
|
|
{ data: 'impressionActual', responsivePriority: 10 },
|
|
{ data: 'errorCode', responsivePriority: 1 },
|
|
],
|
|
|
|
initComplete: function () {
|
|
if (filter === 'hour' ) {
|
|
// Hide Error Code column
|
|
$(this.api().column(8).visible(false));
|
|
} else if (filter === 'errorCode') {
|
|
// Hide date and hour
|
|
$(this.api().column(0).visible(false));
|
|
$(this.api().column(1).visible(false));
|
|
}
|
|
|
|
drawSummaryChart(filteredData.stats);
|
|
},
|
|
|
|
footerCallback: function(row, data, start, end, display) {
|
|
var json = filteredData.stats;
|
|
if (filteredData) {
|
|
$(this.api().column(3).footer()).html(json.totalPlayCount || 0);
|
|
$(this.api().column(4).footer()).html(json.totalErrorCount || 0);
|
|
$(this.api().column(5).footer()).html(json.totalMissCount || 0);
|
|
$(this.api().column(6).footer()).html(json.totalImpressions || 0);
|
|
$(this.api().column(7).footer()).html(json.impressionActual || 0);
|
|
}
|
|
},
|
|
});
|
|
|
|
// Render the summary table
|
|
summaryTable.on('draw', dataTableDraw);
|
|
summaryTable.on('processing.dt', dataTableProcessing);
|
|
}
|
|
|
|
// Renders the chart data
|
|
function drawSummaryChart(data) {
|
|
if (chart !== undefined && chart !== null) {
|
|
chart.destroy();
|
|
}
|
|
|
|
// Organise the rows into datasets for the chart
|
|
let totalSize = data.totalErrorCount + data.totalPlayCount + data.totalMissCount;
|
|
let chartData = {
|
|
userData: [getPercentage(data.totalErrorCount), getPercentage(data.totalPlayCount), getPercentage(data.totalMissCount)],
|
|
userLabels: ['Errors', 'Plays', 'Misses'],
|
|
colours: ['red', 'green', 'blue']
|
|
}
|
|
|
|
function getPercentage(data) {
|
|
return ((data/totalSize)*100).toFixed(2);
|
|
}
|
|
|
|
// Create the pie chart
|
|
chart = new Chart($("#canvas"), {
|
|
type: 'pie',
|
|
data: {
|
|
datasets: [{
|
|
data: chartData.userData,
|
|
backgroundColor: chartData.colours
|
|
}],
|
|
labels: chartData.userLabels
|
|
},
|
|
options: {
|
|
maintainAspectRatio: true
|
|
}
|
|
});
|
|
}
|
|
};
|
|
</script>
|
|
<script type="text/x-handlebars-template" id="connector-ssp-activity">
|
|
<div class="XiboGrid" id="{{ gridId }}" data-grid-name="connector-ssp-activity-log">
|
|
<div class="XiboFilterCustom card bg-light mb-3">
|
|
<div class="FilterDiv card-body" id="connector-ssp-activity-log">
|
|
<form id="activity-log-filters" class="form-inline">
|
|
{% set title %}{% trans "From Date" %}{% endset %}
|
|
{{ inline.dateTime("activityFromDt", title, 'now'|date_modify('today')|date("Y-m-d H:i:s"), "", "activity-from-dt", "", "") }}
|
|
|
|
{% set title %}{% trans "To Date" %}{% endset %}
|
|
{{ inline.dateTime("activityToDt", title, 'tomorrow'|date_modify('-1 minute')|date("Y-m-d H:i:s"), "", "activity-to-dt", "", "") }}
|
|
|
|
{% set title %}{% trans "Display" %}<span class="text-danger">*</span>{% endset %}
|
|
{% set attributes = [
|
|
{ name: "data-width", value: "200px" },
|
|
{ name: "data-allow-clear", value: "true" },
|
|
{ name: "data-placeholder--id", value: null },
|
|
{ name: "data-placeholder--value", value: "" },
|
|
{ name: "data-search-url", value: url_for("display.search") },
|
|
{ name: "data-search-term", value: "display" },
|
|
{ name: "data-search-term-tags", value: "tags" },
|
|
{ name: "data-id-property", value: "displayId" },
|
|
{ name: "data-text-property", value: "display" }
|
|
] %}
|
|
{{ inline.dropdown("displayId", "single", title, "", null, "displayId", "display", "", "pagedSelect", "", "", "", attributes) }}
|
|
|
|
{% set title %}{% trans "Partner" %}{% endset %}
|
|
{% set options = [{id: '', value: ''}] %}
|
|
|
|
{{ inline.dropdown("partnerId", "single", title, "", options, "id", "value") }}
|
|
|
|
<div class="w-100">
|
|
<a data-apply-button="true" class="btn btn-success">
|
|
<span>{% trans "Apply" %}</span>
|
|
</a>
|
|
</div>
|
|
|
|
<div class="alert alert-danger my-1">Please select a display</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<!-- Card Header -->
|
|
<div class="card-header">
|
|
<ul class="nav nav-tabs card-header-tabs" role="tablist">
|
|
<li class="nav-item">
|
|
<a class="nav-link active" id="chart-tab" data-toggle="tab" href="#chartTab" role="tab"
|
|
aria-controls="chartTab" aria-selected="true">Summary</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" id="tabular-tab" data-toggle="tab" href="#tabularTab" role="tab"
|
|
aria-controls="tabularTab" aria-selected="false">Detailed</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<!-- Card Body -->
|
|
<div class="card-body">
|
|
<div class="tab-content">
|
|
|
|
<!-- SUMMARY TAB-->
|
|
<div class="tab-pane active" id="chartTab" role="tabpanel" aria-labelledby="chart-tab">
|
|
<div class="XiboFilterCustom card bg-light pb-0 mb-0">
|
|
<div class="FilterDiv card-body" id="connector-ssp-activity-logs">
|
|
<form id="campaign-filter">
|
|
{% set title %}{% trans "Filter Options" %}{% endset %}
|
|
{% set options = [
|
|
{ optionid: "hour", option: "Hour" },
|
|
{ optionid: "errorCode", option: "Error Code" },
|
|
{ optionid: "hourerrorcode", option: "Hour and Error Code" },
|
|
] %}
|
|
{{ inline.dropdown("campaignFilter", "single", title, "", options, "optionid", "option", helpText) }}
|
|
{{ forms.hidden("isDynamic", 1) }}
|
|
</form>
|
|
|
|
<!-- SUMMARY DATATABLE -->
|
|
<div class="XiboData card pt-3 my-3">
|
|
<table id="ssp-activity-summary"
|
|
class="table table-striped table-full-width"
|
|
style="width: 100%"
|
|
data-state-preference-name="connector-ssp-activity-log-summary">
|
|
<thead>
|
|
<tr>
|
|
<th>{% trans "Date" %}</th>
|
|
<th>{% trans "Hour" %}</th>
|
|
<th>{% trans "Campaign" %}</th>
|
|
<th>{% trans "Play Count" %}</th>
|
|
<th>{% trans "Error Count" %}</th>
|
|
<th>{% trans "Misses Count" %}</th>
|
|
<th>{% trans "Impressions" %}</th>
|
|
<th>{% trans "Impression Actual" %}</th>
|
|
<th>{% trans "Error Code" %}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
</tbody>
|
|
<tfoot>
|
|
<tr>
|
|
<th></th>
|
|
<th></th>
|
|
<th></th>
|
|
<th></th>
|
|
<th></th>
|
|
<th></th>
|
|
<th></th>
|
|
<th></th>
|
|
<th></th>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="widget mt-2 pb-2">
|
|
<div class="widget-title">
|
|
<i class="fa fa-tasks"></i>
|
|
{% trans "Summary Chart" %}
|
|
<div class="clearfix"></div>
|
|
</div>
|
|
<canvas id="canvas" style="clear:both; margin-top:25px;" height="100%"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- TABULAR TAB-->
|
|
<div class="tab-pane show" id="tabularTab" role="tabpanel" aria-labelledby="tabular-tab">
|
|
<div class="XiboData card pt-3">
|
|
<table id="ssp-activity"
|
|
class="table table-striped table-full-width"
|
|
style="width: 100%"
|
|
data-state-preference-name="connector-ssp-activity-log" >
|
|
<thead>
|
|
<tr>
|
|
<th>{% trans "Scheduled At" %}</th>
|
|
<th>{% trans "Campaign" %}</th>
|
|
<th>{% trans "Display ID" %}</th>
|
|
<th>{% trans "Played?" %}</th>
|
|
<th>{% trans "Errored?" %}</th>
|
|
<th>{% trans "Impressions" %}</th>
|
|
<th>{% trans "Impression Date" %}</th>
|
|
<th>{% trans "Impression Actual" %}</th>
|
|
<th>{% trans "Errors" %}</th>
|
|
<th>{% trans "Error Date" %}</th>
|
|
<th>{% trans "Error Code" %}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
</tbody>
|
|
<tfoot>
|
|
<tr>
|
|
<th></th>
|
|
<th></th>
|
|
<th></th>
|
|
<th></th>
|
|
<th></th>
|
|
<th></th>
|
|
<th></th>
|
|
<th></th>
|
|
<th></th>
|
|
<th></th>
|
|
<th></th>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card bg-light p-3 text-danger col-sm-12 text-center form-error"></div>
|
|
</div>
|
|
</script>
|