b1575fa321
Dialogs (with a refresh option) appear whenever the SignalR client disconnects or encounters an error. Nonsensical error messages replaced. Page refresh technique changed to allow for urls containing fragment hashes.
451 lines
18 KiB
Plaintext
451 lines
18 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 for Users</title>
|
|
@Html.BundleRenderDeferred()
|
|
</head>
|
|
<body class="theme-@(Model.DefaultTheme) status-connecting">
|
|
<div id="page">
|
|
<header id="header">
|
|
<div id="heading">Held Devices for Users</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: UserIdFriendly + ' - ' + UserDisplayName"></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.updateHeldDeviceForUser = updateHeldDevice;
|
|
hub.client.setTheme = setTheme;
|
|
|
|
$.connection.hub.qs = { Noticeboard: '@(Disco.Services.Jobs.Noticeboards.HeldDevicesForUsers.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.UserHeldDevices.UserHeldDevices()))', 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 (UserId, heldDeviceItem) {
|
|
// Remove Existing
|
|
removeItem(UserId);
|
|
|
|
// Add Item
|
|
addItem(heldDeviceItem);
|
|
});
|
|
}
|
|
}
|
|
|
|
function removeItem(UserId) {
|
|
removeItemFromArray(viewModel.inProcess, UserId);
|
|
removeItemFromArray(viewModel.readyForReturn, UserId);
|
|
removeItemFromArray(viewModel.waitingForUserAction, UserId);
|
|
}
|
|
|
|
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 '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, UserId) {
|
|
var items = koArray();
|
|
for (var i = 0; i < items.length; i++) {
|
|
if (items[i].UserId == UserId) {
|
|
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.UserIdFriendly.toLowerCase() == r.UserIdFriendly.toLowerCase() ? 0 : (l.UserIdFriendly.toLowerCase() < r.UserIdFriendly.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>
|