Initial Upload
This commit is contained in:
558
views/xibo-ssp-connector-form-javascript.twig
Normal file
558
views/xibo-ssp-connector-form-javascript.twig
Normal file
@@ -0,0 +1,558 @@
|
||||
{#
|
||||
/**
|
||||
* 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>
|
||||
Reference in New Issue
Block a user