dee54bb6d7
also restyle noticeboard using flex
461 lines
19 KiB
Plaintext
461 lines
19 KiB
Plaintext
@model Disco.Web.Areas.Public.Models.UserHeldDevices.NoticeboardModel
|
|
@{
|
|
Layout = null;
|
|
Html.BundleDeferred("~/ClientScripts/Modules/Knockout");
|
|
Html.BundleDeferred("~/ClientScripts/Modules/jQuery-SignalR");
|
|
Html.BundleDeferred("~/ClientScripts/Core");
|
|
Html.BundleDeferred("~/Style/Public/HeldDevicesNoticeboard");
|
|
}
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
<title>Disco ICT - Held Devices</title>
|
|
@Html.BundleRenderDeferred()
|
|
</head>
|
|
<body class="theme-@(Model.DefaultTheme) status-connecting">
|
|
<div id="page">
|
|
<header id="header">
|
|
<div id="heading">Held Devices</div>
|
|
<div id="statusConnecting"><i class="fa fa-cog fa-spin"></i><span>connecting...</span></div>
|
|
<div id="statusError"><i class="fa fa-cog fa-spin"></i><span>disconnected, reconnecting...</span></div>
|
|
<div id="credits">
|
|
powered by Disco ICT <i title="Disco ICT - Jobs"></i>
|
|
</div>
|
|
</header>
|
|
<section id="mainSection">
|
|
<div id="inProcess" class="list">
|
|
<h3>In Process (<span data-bind="text: inProcess().length"></span>)
|
|
</h3>
|
|
<div class="content">
|
|
<!-- ko if: inProcess().length == 0 -->
|
|
<div class="noContent"><None></div>
|
|
<!-- /ko -->
|
|
<ul data-bind="template: { name: 'item-template', foreach: inProcess, afterRender: onAdd, beforeRemove: onRemove }"></ul>
|
|
</div>
|
|
</div>
|
|
<div id="readyForReturn" class="list">
|
|
<h3>Ready for Return (<span data-bind="text: readyForReturn().length"></span>)
|
|
</h3>
|
|
<div class="content">
|
|
<!-- ko if: readyForReturn().length == 0 -->
|
|
<div class="noContent"><None></div>
|
|
<!-- /ko -->
|
|
<ul data-bind="template: { name: 'item-template', foreach: readyForReturn, afterRender: onAdd, beforeRemove: onRemove }"></ul>
|
|
</div>
|
|
</div>
|
|
<div id="waitingForUserAction" class="list">
|
|
<h3>Waiting for User Action (<span data-bind="text: waitingForUserAction().length"></span>)
|
|
</h3>
|
|
<div class="content">
|
|
<!-- ko if: waitingForUserAction().length == 0 -->
|
|
<div class="noContent"><None></div>
|
|
<!-- /ko -->
|
|
<ul data-bind="template: { name: 'item-template', foreach: waitingForUserAction, afterAdd: onAdd, beforeRemove: onRemove }"></ul>
|
|
</div>
|
|
</div>
|
|
<footer id="footer">
|
|
</footer>
|
|
</section>
|
|
</div>
|
|
<script type="text/html" id="item-template">
|
|
<li data-bind="css: { alert: IsAlert }">
|
|
<span data-bind="text: DeviceDescription"></span>
|
|
<!-- ko if: !ReadyForReturn && EstimatedReturnTimeUnixEpoc -->
|
|
<span class="small">(Expected <span data-bind="livestamp: EstimatedReturnTimeUnixEpoc"></span>)</span>
|
|
<!-- /ko -->
|
|
<!-- ko if: WaitingForUserAction -->
|
|
<span class="small">(Since <span data-bind="livestamp: WaitingForUserActionSinceUnixEpoc"></span>)</span>
|
|
<!-- /ko -->
|
|
<!-- ko if: ReadyForReturn && !WaitingForUserAction -->
|
|
<span class="small">(Ready <span data-bind="livestamp: ReadyForReturnSinceUnixEpoc"></span>)</span>
|
|
<!-- /ko -->
|
|
</li>
|
|
</script>
|
|
<script>
|
|
ko.bindingHandlers.livestamp = {
|
|
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
|
|
var value = valueAccessor();
|
|
var valueUnwrapped = ko.unwrap(value);
|
|
|
|
if (valueUnwrapped)
|
|
$(element).livestamp(valueUnwrapped);
|
|
else
|
|
$(element).livestamp('destroy');
|
|
}
|
|
};
|
|
</script>
|
|
<script>
|
|
$(function () {
|
|
var hub;
|
|
var viewModel;
|
|
|
|
var rotateSpeed = 3000;
|
|
var itemFilters;
|
|
var fixedTheme = null;
|
|
|
|
var $inProcessList = $('#inProcess').find('ul');
|
|
var $readyForReturnList = $('#readyForReturn').find('ul');
|
|
var $waitingForUserActionList = $('#waitingForUserAction').find('ul');
|
|
|
|
function noticeboardViewModel(inProcess, readyForReturn, waitingForUserAction) {
|
|
var self = this;
|
|
|
|
self.initialized = false;
|
|
|
|
self.inProcess = ko.observableArray(inProcess);
|
|
self.readyForReturn = ko.observableArray(readyForReturn);
|
|
self.waitingForUserAction = ko.observableArray(waitingForUserAction);
|
|
|
|
self.onRemove = function (element, index, data) {
|
|
$(element).slideUp(400, function () {
|
|
$(this).remove();
|
|
});
|
|
}
|
|
self.onAdd = function (element, index, data) {
|
|
if (self.initialized)
|
|
$(element).hide().slideDown(400);
|
|
}
|
|
}
|
|
|
|
function init() {
|
|
monitorMouseMove();
|
|
applyQueryString();
|
|
|
|
// Connect to Hub
|
|
hub = $.connection.noticeboardUpdates;
|
|
|
|
// Map Functions
|
|
hub.client.updateHeldDevice = updateHeldDevice;
|
|
hub.client.setTheme = setTheme;
|
|
|
|
$.connection.hub.qs = { Noticeboard: '@(Disco.Services.Jobs.Noticeboards.HeldDevices.Name)' };
|
|
$.connection.hub.error(connectionError);
|
|
$.connection.hub.disconnected(connectionError);
|
|
$.connection.hub.reconnected(connectionError);
|
|
|
|
// Start Connection
|
|
$.connection.hub.start().fail(connectionError).done(loadData);
|
|
}
|
|
|
|
// Called after SignalR is connected
|
|
function loadData() {
|
|
$.getJSON('@(Url.Action(MVC.Public.HeldDevices.HeldDevices()))', null, function (data) {
|
|
|
|
var inProcess = [];
|
|
var readyForReturn = [];
|
|
var waitingForUserAction = [];
|
|
|
|
data.filter(function (heldDeviceItem) {
|
|
return includeItem(heldDeviceItem)
|
|
}).forEach(function (heldDeviceItem) {
|
|
if (isWaitingForUserAction(heldDeviceItem))
|
|
waitingForUserAction.push(heldDeviceItem);
|
|
else if (isReadyForReturn(heldDeviceItem))
|
|
readyForReturn.push(heldDeviceItem);
|
|
else if (isInProcess(heldDeviceItem))
|
|
inProcess.push(heldDeviceItem);
|
|
});
|
|
|
|
inProcess.sort(sortFunction);
|
|
readyForReturn.sort(sortFunction);
|
|
waitingForUserAction.sort(sortFunction);
|
|
|
|
viewModel = new noticeboardViewModel(inProcess, readyForReturn, waitingForUserAction);
|
|
|
|
ko.applyBindings(viewModel);
|
|
viewModel.initialized = true;
|
|
|
|
$('body').removeClass('status-connecting');
|
|
|
|
window.setTimeout(scheduleRotation, rotateSpeed);
|
|
});
|
|
}
|
|
|
|
// Called by SignalR
|
|
function updateHeldDevice(updates) {
|
|
if (viewModel) {
|
|
|
|
$.each(updates, function (deviceSerialNumber, heldDeviceItem) {
|
|
// Remove Existing
|
|
removeItem(deviceSerialNumber);
|
|
|
|
// Add Item
|
|
addItem(heldDeviceItem);
|
|
});
|
|
}
|
|
}
|
|
|
|
function removeItem(deviceSerialNumber) {
|
|
removeItemFromArray(viewModel.inProcess, deviceSerialNumber);
|
|
removeItemFromArray(viewModel.readyForReturn, deviceSerialNumber);
|
|
removeItemFromArray(viewModel.waitingForUserAction, deviceSerialNumber);
|
|
}
|
|
|
|
function addItem(heldDeviceItem) {
|
|
if (heldDeviceItem !== null &&
|
|
heldDeviceItem !== undefined &&
|
|
includeItem(heldDeviceItem)) {
|
|
|
|
var array;
|
|
|
|
if (isWaitingForUserAction(heldDeviceItem))
|
|
array = viewModel.waitingForUserAction;
|
|
else if (isReadyForReturn(heldDeviceItem))
|
|
array = viewModel.readyForReturn;
|
|
else if (isInProcess(heldDeviceItem))
|
|
array = viewModel.inProcess;
|
|
|
|
if (array().length === 0) {
|
|
array.push(heldDeviceItem);
|
|
} else {
|
|
var index = findSortedInsertIndex(array, heldDeviceItem);
|
|
if (index === -1)
|
|
array.push(heldDeviceItem);
|
|
else
|
|
array.splice(index, 0, heldDeviceItem);
|
|
}
|
|
}
|
|
}
|
|
|
|
function rotateArrays() {
|
|
rotateArray(viewModel.inProcess, $inProcessList);
|
|
rotateArray(viewModel.readyForReturn, $readyForReturnList);
|
|
rotateArray(viewModel.waitingForUserAction, $waitingForUserActionList);
|
|
}
|
|
|
|
function scheduleRotation() {
|
|
rotateArrays();
|
|
|
|
window.setTimeout(scheduleRotation, rotateSpeed);
|
|
}
|
|
|
|
function includeItem(heldDeviceItem) {
|
|
if (itemFilters == null || itemFilters.length == 0)
|
|
return true;
|
|
|
|
return itemFilters.reduce(function (previousValue, currentValue, index, array) {
|
|
if (previousValue === false)
|
|
return false;
|
|
return currentValue(heldDeviceItem);
|
|
}, true);
|
|
}
|
|
|
|
function setTheme(theme) {
|
|
if (!!fixedTheme)
|
|
return;
|
|
|
|
var $body = $(document.body);
|
|
|
|
// Existing classes
|
|
var c = $body.attr('class').split(' ');
|
|
// Remove existing theme
|
|
c = $.grep(c, function (i) { return (i.indexOf('theme-') !== 0) });
|
|
|
|
c.push('theme-' + theme);
|
|
|
|
$body.attr('class', c.join(' '));
|
|
}
|
|
|
|
function monitorMouseMove() {
|
|
var token = null,
|
|
$body = $(document.body);
|
|
|
|
$body.mousemove(function () {
|
|
if (!!token)
|
|
window.clearTimeout(token);
|
|
else if ($body.css('cursor') == 'none')
|
|
$body.css('cursor', 'auto');
|
|
|
|
token = window.setTimeout(function () {
|
|
$body.css('cursor', 'none');
|
|
token = null;
|
|
}, 3500);
|
|
});
|
|
|
|
}
|
|
|
|
function applyQueryString() {
|
|
var queryStringParameters = getQueryStringParameters();
|
|
|
|
if (queryStringParameters !== null) {
|
|
var filters = [];
|
|
|
|
$.each(queryStringParameters, function (key, value) {
|
|
switch (key.toLowerCase()) {
|
|
case 'components':
|
|
const showComponents = value.split(",");
|
|
if (showComponents.length > 0) {
|
|
const components = ['inProcess', 'readyForReturn', 'waitingForUserAction'];
|
|
components.forEach(function (component) {
|
|
if (!showComponents.includes(component)) {
|
|
$('body').addClass('hide-' + component);
|
|
}
|
|
});
|
|
}
|
|
break;
|
|
case 'theme': // THEME
|
|
setTheme(value);
|
|
fixedTheme = value;
|
|
break;
|
|
case 'deviceaddressinclude': // FILTER: Device Address Include
|
|
var deviceAddresses = value.split(",").map(function (v) { return v.toLowerCase(); });
|
|
if (deviceAddresses.length > 0) {
|
|
filters.push(function (heldDeviceItem) {
|
|
// false if DeviceAddressShortName is null
|
|
if (!heldDeviceItem.DeviceAddressShortName)
|
|
return false;
|
|
|
|
// true if DeviceAddressShortName is included
|
|
return $.inArray(heldDeviceItem.DeviceAddressShortName.toLowerCase(), deviceAddresses) >= 0;
|
|
});
|
|
}
|
|
break;
|
|
case 'deviceaddressexclude': // FILTER: Device Address Exclude
|
|
var deviceAddresses = value.split(",").map(function (v) { return v.toLowerCase(); });
|
|
if (deviceAddresses.length > 0) {
|
|
filters.push(function (heldDeviceItem) {
|
|
// true if DeviceAddressShortName is null
|
|
if (!heldDeviceItem.DeviceAddressShortName)
|
|
return true;
|
|
|
|
// true if DeviceAddressShortName is excluded
|
|
return $.inArray(heldDeviceItem.DeviceAddressShortName.toLowerCase(), deviceAddresses) < 0;
|
|
});
|
|
}
|
|
break;
|
|
case 'deviceprofileinclude': // FILTER: Device Profile Include
|
|
var deviceProfiles = value.split(",").map(function (v) { return parseInt(v); });
|
|
if (deviceProfiles.length > 0) {
|
|
filters.push(function (heldDeviceItem) {
|
|
// true if DeviceProfileId is included
|
|
return $.inArray(heldDeviceItem.DeviceProfileId, deviceProfiles) >= 0;
|
|
});
|
|
}
|
|
break;
|
|
case 'deviceprofileexclude': // FILTER: Device Profile Exclude
|
|
var deviceProfiles = value.split(",").map(function (v) { return parseInt(v); });
|
|
if (deviceProfiles.length > 0) {
|
|
filters.push(function (heldDeviceItem) {
|
|
// true if DeviceProfileId is excluded
|
|
return $.inArray(heldDeviceItem.DeviceProfileId, deviceProfiles) < 0;
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
});
|
|
|
|
if (filters.length > 0)
|
|
itemFilters = filters;
|
|
else
|
|
itemFilters = null;
|
|
}
|
|
}
|
|
|
|
function connectionError() {
|
|
try {
|
|
$('body').addClass('status-error');
|
|
$.connection.hub.stop();
|
|
} catch (e) {
|
|
// Ignore
|
|
}
|
|
|
|
window.setTimeout(function () {
|
|
window.location.reload(true);
|
|
}, 10000);
|
|
}
|
|
|
|
// Helpers
|
|
function rotateArray(koArray, element) {
|
|
var items = koArray();
|
|
|
|
if (items.length <= 1)
|
|
return 0;
|
|
|
|
if (element.height() < (element.parent().height() - 30)) {
|
|
|
|
if (findUnsortedArrayTopIndex(items) !== 0)
|
|
koArray.sort(sortFunction);
|
|
|
|
// Don't rotate if small & sorted correctly
|
|
return;
|
|
}
|
|
|
|
// Move Last Item to Top
|
|
var item = koArray.pop();
|
|
koArray.unshift(item);
|
|
}
|
|
function removeItemFromArray(koArray, deviceSerialNumber) {
|
|
var items = koArray();
|
|
for (var i = 0; i < items.length; i++) {
|
|
if (items[i].DeviceSerialNumber == deviceSerialNumber) {
|
|
koArray.splice(i, 1);
|
|
items = koArray();
|
|
i--;
|
|
}
|
|
}
|
|
}
|
|
function findUnsortedArrayTopIndex(items) {
|
|
// Only one Item
|
|
if (items.length <= 1)
|
|
return 0;
|
|
|
|
for (var i = 1; i < items.length; i++) {
|
|
var s = sortFunction(items[i - 1], items[i]);
|
|
if (s > 0)
|
|
return i;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
function findSortedInsertIndex(koArray, heldDeviceItem) {
|
|
var items = koArray();
|
|
var startIndex = findUnsortedArrayTopIndex(items);
|
|
for (var i = startIndex; i < items.length; i++) {
|
|
var s = sortFunction(heldDeviceItem, items[i]);
|
|
if (s <= 0)
|
|
return i;
|
|
}
|
|
if (startIndex !== 0) {
|
|
for (var i = 0; i < startIndex; i++) {
|
|
var s = sortFunction(heldDeviceItem, items[i]);
|
|
if (s <= 0)
|
|
return i;
|
|
}
|
|
return startIndex;
|
|
} else {
|
|
return -1;
|
|
}
|
|
}
|
|
function sortFunction(l, r) {
|
|
return l.DeviceDescription.toLowerCase() == r.DeviceDescription.toLowerCase() ? 0 : (l.DeviceDescription.toLowerCase() < r.DeviceDescription.toLowerCase() ? -1 : 1)
|
|
}
|
|
function isInProcess(i) {
|
|
return !i.ReadyForReturn && !i.WaitingForUserAction;
|
|
}
|
|
function isReadyForReturn(i) {
|
|
return i.ReadyForReturn && !i.WaitingForUserAction;
|
|
}
|
|
function isWaitingForUserAction(i) {
|
|
return i.WaitingForUserAction;
|
|
}
|
|
function getQueryStringParameters() {
|
|
|
|
if (window.location.search.length === 0)
|
|
return null;
|
|
|
|
var params = {};
|
|
window.location.search.substr(1).split("&").forEach(function (pair) {
|
|
if (pair === "") return;
|
|
var parts = pair.split("=");
|
|
params[parts[0]] = parts[1] && decodeURIComponent(parts[1].replace(/\+/g, " "));
|
|
});
|
|
return params;
|
|
}
|
|
|
|
init();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |