Files
Disco/Disco.Web/Areas/Public/Views/HeldDevices/Noticeboard.cshtml
T
Gary Sharp b1575fa321 Unified SignalR disconnected/error dialogs
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.
2014-09-11 17:21:39 +10:00

450 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</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">&lt;None&gt;</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">&lt;None&gt;</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">&lt;None&gt;</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 '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>