PageSourceSearch

https://my.isy.io/webportal.js

js isy.io collected 2026-10-04 17:20:35 UTC 648,958 bytes, 19,500 lines download raw bytes

1// "use strict"
2/*
3* webportal.js
4* Author: Benoit Mercier
5* Copyright 2015-2022, Universal Devices inc.
6*/
7
8// Set temporarily to true when portal should not be accessed
9var maintenanceMode=false;
10
11var registeredUser=null; // Mirror of udiportal session storage
12var currentISY=null; // Mirror of udidevice session storage
13
14// Handles to objects returned by the form initializations
15var domainDialog,
16	userDialog,
17	loginDialog,
18	myProfileDialog,
19	migrateDialog,
20	eisyuiSettingsDialog,
21	isyClaimDialog,
22	isyNoteDialog,
23	isyContactDialog,
24	// isyFirmwareDialog,
25	eisyProvisioningDialog,
26	isyInfoDialog,
27	isyImportDialog,
28	isyAccessLicenseDialog,
29	isyAccessLicenseRenewDialog,
30	nsPurchaseDialog,
31	isyOpenadrRegUploadDialog,
32	isyOpenadrRegDialog,
33	liveMessageDialog,
34	pinDialog,
35	passwordChangeDialog,
36	selfRegistrationDialog,
37	lastConnectedDialog,
38	echoSpokenNodesDialog,
39	echoSpokenNodesEditDialog,
40	echoSpokenNodesAVEditDialog,
41	echoSpokenRoomEditDialog,
42	ghSpokenNodesDialog,
43	ghSpokenNodesEditDialog,
44	ghSpokenRoomEditDialog,
45	iftttInNodesDialog,
46	iftttInNodesEditDialog,
47	iftttInNodesKeyDialog,
48	nsListDialog,
49	hintEditorDialog,
50	activityLogDialog,
51	ipalRenewalOptionEditDialog,
52	ipalNotificationOptionEditDialog,
53	ratePlanEditDialog,
54	downloadUsersDialog,
55	downloadUsageDialog,
56	downloadStatsDialog,
57	nsConfigDialog,
58	nsOccupancyDialog,
59	nsOccupancyEditDialog,
60	nsOccupancyLocationEditDialog,
61	nsOccupancyUrlDialog,
62	nsOccupancy2Dialog,
63	nsOccupancy2EditDialog,
64	nsOccupancy2GeofenceEditDialog,
65	nsOccupancy2DeviceEditDialog,
66	nsOccupancy2GeofenceUrlDialog,
67	nsOccupancy2DevicePinDialog,
68	udCommsEditDialog,
69	udCommsSurveyDialog,
70	udCommsDeliveryDialog;
71
72var	allDialogs = [];
73var	tempDialogs = []; // Information and confirmation messages
74
75// PASSWORD REGEX
76// Must contain uppercase and/or lowercase letters
77// Must contain digits and/or special characters
78// Must be 8-20 in length
79
80// Beta functions allowed (this.isPilots will be true in renderTools)
81var pilots = ['$superadmin',
82	'56cca3962e48ea151a015852', // Javier
83	'5b98260f136f942ff6c578ac', // Bob
84	'63486a0a867e3029deda5817' // JR dev
85	// '583703092dc9c25a649e2ebb' // UUID 00:21:b9:02:5f:aa, trying to decommission manually
86];
87
88var rememberMeInfo
89
90var passwordRegex = /^(?=.*[a-zA-Z])(?=.*\d|.*[!?&@#$-\/:-@\[-_])(\w|\d|[!?&@#$-_]){8,20}$/;
91//var passwordRegex = /^(?=[a-zA-Z]).(?=.*\d|.*[!?&@#$-_])(\w|\d|[!?&@#$-_]){7,20}$/  //This one required to start with chars
92var emailRegex = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
93var emailRegexWithSpace = /^\s*[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\s*$/;
94var uuidRegex = /^(?:uuid:)?(?=00:*21:*b9|00:*0d:*b9)([a-f\d]{2}):*([a-f\d]{2}):*([a-f\d]{2}):*([a-f\d]{2}):*([a-f\d]{2}):*([a-f\d]{2})$/i;
95var nodeNameRegex = /^[\w .\-\+,=_|]+$/i;
96var roomNameRegex = /^([A-Za-z\s])+$/i;
97var spokenRegex = /^([a-zA-Zñáíóúàâéèêëïîôçùûüÿ'\-]+\s)*[a-zA-Zñáíóúàâéèêëïîôçùûüÿ'\-]*$/i; // /^([a-zA-Z]+\s)*[a-zA-Z]*$/i
98
99
100var accessLicenseExpirationSoonDays = 30;
101var nodeServerMinimumFirmware='5.0.9';
102
103// These are flags to stop giving warnings when adding/editing/deleting Alexa spokens
104var skillNotLinkedWarningDelete = true
105var skillNotLinkedWarningAddUpdate = true
106var incorrectUnitWarningDelete = true
107var incorrectUnitWarningAddUpdate = true
108
109var tinymceLoaded= false
110
111// Called when logging in with Google. Function has to be global.
112function gsi_login(args) {
113	loginDialog.gsi_login(args)
114}
115
116function initTinymceTextarea() {
117	// Push notifications editor
118	$('textarea#ud-comms-content-push, textarea#ud-comms-content-appLink-text-header, textarea#ud-comms-content-appLink-text-footer, textarea#ipal_notification_option_pushContent').tinymce({
119		selector: 'textarea#ud-comms-content-push',
120		plugins: 'lists link',
121		toolbar: 'undo redo | styles | bold italic | bullist numlist | alignleft aligncenter alignright al
121ignjustify | outdent indent | link',
122		// convert_newlines_to_brs : true,
123		// forced_root_block : false
124		// toolbar: 'bullist numlist',
125		// inline: true
126		// plugins: 'a11ychecker advcode casechange export formatpainter image editimage linkchecker autolink lists checklist media mediaembed pageembed permanentpen powerpaste table advtable tableofcontents tinycomments tinymcespellchecker',
127		// toolbar: 'a11ycheck addcomment showcomments casechange checklist code export formatpainter image editimage pageembed permanentpen table tableofcontents',
128		// toolbar_mode: 'floating',
129		// tinycomments_mode: 'embedded',
130		// tinycomments_author: 'Author name',
131	})
132
133	// Editor for emails
134	$('textarea#ipal_notification_option_emailBody').tinymce({
135		selector: 'textarea#ud-comms-content-push',
136		plugins: 'lists',
137		toolbar: 'undo redo | styles | bold italic | bullist numlist | alignleft aligncenter alignright alignjustify | outdent indent | link',
138		plugins: 'link',
139		// br_in_pre: false
140		convert_newlines_to_brs : true,
141		// forced_root_block : false
142		// toolbar: 'bullist numlist',
143		// inline: true
144		// plugins: 'a11ychecker advcode casechange export formatpainter image editimage linkchecker autolink lists checklist media mediaembed pageembed permanentpen powerpaste table advtable tableofcontents tinycomments tinymcespellchecker',
145		// toolbar: 'a11ycheck addcomment showcomments casechange checklist code export formatpainter image editimage pageembed permanentpen table tableofcontents',
146		// toolbar_mode: 'floating',
147		// tinycomments_mode: 'embedded',
148		// tinycomments_author: 'Author name',
149	})
150
151	$('#ee-admin-tabs-config textarea.tinymce').tinymce({
152		selector: 'textarea#comms-content-push',
153		plugins: 'lists',
154		toolbar: 'undo redo | styles | bold italic | bullist numlist | alignleft aligncenter alignright al
154ignjustify | outdent indent',
155		// convert_newlines_to_brs : true,
156		// forced_root_block : false
157		// toolbar: 'bullist numlist',
158		// inline: true
159		// plugins: 'a11ychecker advcode casechange export formatpainter image editimage linkchecker autolink lists checklist media mediaembed pageembed permanentpen powerpaste table advtable tableofcontents tinycomments tinymcespellchecker',
160		// toolbar: 'a11ycheck addcomment showcomments casechange checklist code export formatpainter image editimage pageembed permanentpen table tableofcontents',
161		// toolbar_mode: 'floating',
162		// tinycomments_mode: 'embedded',
163		// tinycomments_author: 'Author name',
164	});
165
166	$('textarea#comms-content-push, textarea#comms-content-appLink-text-header, textarea#comms-content-appLink-text-footer').tinymce({
167		selector: 'textarea#comms-content-push',
168		plugins: 'lists',
169		toolbar: 'undo redo | styles | bold italic | bullist numlist | alignleft aligncenter alignright alignjustify | outdent indent',
170		// convert_newlines_to_brs : true,
171		// forced_root_block : false
172		// toolbar: 'bullist numlist',
173		// inline: true
174		// plugins: 'a11ychecker advcode casechange export formatpainter image editimage linkchecker autolink lists checklist media mediaembed pageembed permanentpen powerpaste table advtable tableofcontents tinycomments tinymcespellchecker',
175		// toolbar: 'a11ycheck addcomment showcomments casechange checklist code export formatpainter image editimage pageembed permanentpen table tableofcontents',
176		// toolbar_mode: 'floating',
177		// tinycomments_mode: 'embedded',
178		// tinycomments_author: 'Author name',
179	});
180}
181
182function loadTinymce() {
183	if (!tinymceLoaded) {
184		$.getScript('https://cdn.tiny.cloud/1/b2rxvxrnov0nv5qt9zbh2u69kc7obg7p64aeaz9e2yxts1sz/tinymce/6/tinymce.min.js')
185		.then(function () {
186			return $.getScript('https://cdn.jsdelivr.net/npm/@tinymce/tinymce-jquery@1/dist/tinymce-jquery.min.js')
187		})
188		.then(function () {
189			tinymceLoaded = true
190			initTinymceTextarea()
191		})
192	}
193}
194
195// Used by MyProfile & user dialog
196// var preferredIsyChangeWarning =
197// 'You changed your preferred ISY, but it appears that you have an Alexa skill enabled. ' +
198// 'When changing the preferred ISY, you also need to disable and re-enable the skill.<br/><br/>Continue?';
199
200
201// localStorage:
202// udidata: { lastLoginAction: 'isy' }
203
204// sessionStorage:
205// udidevice: { uuid: '11:22:33:44:55:66', alias: 'production', isyKey: '??' }
206// udiportal: { registeredUser: registeredUser }
207
208// RATES Project
209// var ratesInit=null; // Function defined in webrates.js
210// var ratesMenuOptions=null; // Function defined in webrates.js
211// var ratesInitialized=false;
212//'560895243a29b8641009c6be'
213// var ratesAccounts = ['58fe94cbd61ce57076697989']; // '$superadmin',
214
215// SCE Pilot project
216// var sceInit=null; // Function defined in websce.js
217// var sceMenuOptions=null; // Function defined in websce.js
218// var sceInitialized=false;
219// var sceAccounts = [/*'$superadmin',*/ '5d94eb333888c5344881c589', '56a6dc41ae6343360e2f2813'];
220// 5d94eb333888c5344881c589 SCE UD
221// 56a6dc41ae6343360e2f2813 P.Chan
222// SCE Pilot project
223// var sceInit=null; // Function defined in websce.js
224// var sceMenuOptions=null; // Function defined in websce.js
225// var sceInitialized=false;
226// var sceAccounts = [/*'$superadmin',*/ '5d94eb333888c5344881c589', '56a6dc41ae6343360e2f2813'];
227// 5d94eb333888c5344881c589 SCE UD
228// 56a6dc41ae6343360e2f2813 P.Chan
229
230// Valid location hash / parameters
231// #passwordreset
232// #registration
233// #udajax
234// #myprofile
235// #license
236//		?uuid
237//    ?key
238//    ?redirect (Optional)
239// #tab-domains
240// #tab-ee
241// #tab-ee-admin
242// #ns-purchase
243//    ?pid
244//    ?nsid
245//    ?redirect
246//    ?beta
247//    ?test
248// OTHER
249//	 ?emailverification
250//   ?client_id  // If we have a client_id, it's a redirect from the authorization page.
251//   ?refresh=domain (will refresh domainInfo after logging in from session storage data)
252//   ?accessToken=<token> Bypasses login. Meant for temporary usage. Token must have decent life left
253
254function testUI() {
255/*
256	var	roomsPromise,
257		lookupDataPromise,
258		lookupDataPromisePgm,
259		lookupDataPromiseStateVar,
260		lookupDataPromiseNetworkRsc,
261		uuid = '00:21:b9:01:fd:26',
262		domain = '$superadmin';
263
264	function loadRooms(uuid) {
265		roomsPromise = ajaxJsonGet('/api/voice/amazon/spoken/rooms', $.param({ uuid: uuid, domain: domain }));
266
267		roomsPromise
268		.fail(function(jqXHR, textStatus, errorThrown) {
269			if (jqXHR.status==503) {
270				errorMessageDialog('ISY is not online')
271			}
272			else if (jqXHR.status==504) {
273				errorMessageDialog('Timeout occured. Please try again.')
274			}
275			else if (jqXHR.status==550) {  // Custom error message
276				var data = JSON.parse(jqXHR.responseText);
277
278				if (data.message) {
279					errorMessageDialog(data.message)
280				}
281				else {
282					errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
283				}
284			}
285			else {
286				errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
287			}
288		})
289		.done();
290
291		return roomsPromise;
292	}
293
294	lookupDataPromise = ajaxJsonGet('/api/nodes/lookup', $.param({ uuid: uuid, domain: domain }))
295	.done(function(content, status, jqXHR){
296		content.data.devices.map(function (item) {
297			item.data = item.address;
298			item.value = item.label = item.name + ' (' + item.address.replace(/ /g, '.') + ')';
299			item.option = item; // Self reference;
300		})
301
302		content.data.devices.sort(function(a, b) {
303			return a.name.localeCompare(b.name) }
304		);
305
306		content.data.scenes.map(function (item) {
307			item.data = item.address;
308			item.value = item.label = item.name + ' (' + item.address.replace(/ /g, '.') + ')';
309			item.option = item; // Self reference;
310		})
311
312		content.data.scenes.sort(function(a, b) {
313			return a.name.localeCompare(b.name) }
314		);
315	})
316	.fail(function (jqXHR, textStatus, errorThrown) {
317		jqXHR.requested = 'Devices'; // Track which request it is in the jqXHR object
318	})
319
320	lookupDataPromisePgm = ajaxJsonGet('/api/programs/lookup', $.param({uuid: uuid, domain: domain}))
321	.done(function(content, status, jqXHR){
322		content.data.map(function (item) {
323			item.data = item.address;
324			item.value = item.label = item.name + ' (' + item.address.replace(/ /g, '.') + ')';
325			item.option = item; // Self reference;
326		})
327
328		content.data.sort(function(a, b) {
329			return a.name.localeCompare(b.name) }
330		);
331	})
332	.fail(function (jqXHR, textStatus, errorThrown) {
333		jqXHR.requested = 'Programs'; // Track which request it is in the jqXHR object
334	})
335
336	lookupDataPromiseStateVar = ajaxJsonGet('/api/statevars/lookup', $.param({uuid: uuid, domain: domain}))
337	.done(function(content, status, jqXHR){
338		content.data.map(function (item) {
339			item.data = item.address;
340			item.value = item.label = item.name + ' (' + item.address.replace(/ /g, '.') + ')';
341			item.option = item; // Self reference;
342		})
343
344		content.data.sort(function(a, b) {
345			return a.name.localeCompare(b.name) }
346		);
347	})
348	.fail(function (jqXHR, textStatus, errorThrown) {
349		jqXHR.requested = 'State variables'; // Track which request it is in the jqXHR object
350		jqXHR.skipErrorMessage = true;
351	})
352
353	lookupDataPromiseNetworkRsc = ajaxJsonGet('/api/networkrsc/lookup', $.param({uuid: uuid, domain: domain}))
354	.done(function(content, status, jqXHR){
355		content.data.map(function (item) {
356			item.data = item.address;
357			item.value = item.label = item.name + ' (' + item.address.replace(/ /g, '.') + ')';
358			item.option = item; // Self reference;
359		})
360
361		content.data.sort(function(a, b) {
362			return a.name.localeCompare(b.name) }
363		);
364	})
365	.fail(function (jqXHR, textStatus, errorThrown) {
366		jqXHR.requested = 'Network resources'; // Track which request it is in the jqXHR object
367		jqXHR.skipErrorMessage = true;
368	})
369
370	$.when(lookupDataPromise, lookupDataPromiseStateVar, lookupDataPromiseNetworkRsc)
371	.fail(function(jqXHR, textStatus, errorThrown) {
372		if (jqXHR.status==503) {
373			errorMessageDialog('ISY is not online')
374		}
375		else if (jqXHR.status==404) {
376			// No need to warn the user about state variables not loading. It may be normal.
377			if (!jqXHR.skipErrorMessage) {
378				errorMessageDialog(jqXHR.requested + ' are not available')
379			}
380		}
381		else {
382			errorMessageDialog(jqXHR.requested + ' are not available<br>' + 'HTTP error ' + jqXHR.status, 'Error', 400)
383		}
384	})
385
386	loadRooms(uuid)
387
388//	var roomsPromise = echoSpokenNodesDialog.loadRooms(uuid);
389
390	$.when(lookupDataPromise, lookupDataPromiseStateVar, lookupDataPromiseNetworkRsc, lookupDataPromisePgm, roomsPromise)
391	.then(function(nodes, vars, net, pgm, rooms) {
392//			var content=contentNodes[0];
393//			var rooms=roomsAjax[0];
394//		console.log('nodes', nodes)
395//		console.log('vars', vars)
396//		console.log('net', net)
397//		console.log('rooms', rooms)
398
399		echoSpokenNodesAVEditDialog.setFields({ category: 'av', uuid: uuid, domain: domain, lookupNodes: nodes[0], lookupPgms: pgm[0], lookupVars: vars[0], lookupNet: net[0], rooms: rooms[0].data, editMode:false });
400		echoSpokenNodesAVEditDialog.modal.dialog('open');
401	})
402	.fail(function(jqXHR, textStatus, errorThrown) {
403		errorMessageDialog("A/V data not available", 'Error', 350)
404	})
405	.done();
406*/
407}
408
409
410// Used in the Select tools menu && tableIsys
411// function isRatesAccount(account) {
412// 	if (typeof account == 'string') {
413// 		return ratesAccounts.includes(account);
414// 	}
415// 	else {
416// 		console.log('account', account)
417// 		return ratesAccounts.includes(account._id) || ratesAccounts.includes(account.parent);
418// 	}
419// }
420
421// True if the logged in user is in the RATES root account
422// function isRatesRootAccount(domain) {
423// 	return ratesAccounts.includes(domain)
424// }
425
426// Used in the Select tools menu && tableIsys
427// function isSceAccount(account) {
428//   if (typeof account == 'string') {
429//     return sceAccounts.includes(account);
430//   } else if (account) {
431//     return sceAccounts.includes(account._id) || sceAccounts.includes(account.parent);
432//   } else {
433//   	return false
434// 	}
435// }
436
437// True if the logged in user is in the SCE root account
438// function isSceRootAccount(domain) {
439//   return sceAccounts.includes(domain)
440// }
441
442function recaptchaRender() {
443	grecaptcha.render('recaptcha_selfr', {
444		'sitekey' : serverConfig.recaptchaKeySite
445	});
446}
447
448
449function rememberMeActive() {
450	return getLocalStorage('udi_remember_me');
451}
452
453function rememberMe() {
454	var cookieInfo=null;
455
456	if (rememberMeActive()) {
457		if (rememberMeInfo) { // rememberMeInfo comes from rememberme.js
458			try {
459				cookieInfo = JSON.parse(rememberMeInfo);
460			} catch (e) {}
461		}
462	}
463
464	return cookieInfo;
465}
466
467function rememberMeSet(username, password) {
468	if (rememberMeActive() && typeof rememberMeInfo !== 'undefined') {
469		rememberMeInfo = JSON.stringify({
470			username: username,
471			password: password
472		})
473	}
474}
475
476// Page load & login sequence:
477// init
478//	If available udiportal from Session storage,
479//		Set registeredUser
480//		run afterLogin
481//	If not, login
482//		run afterLogin
483//
484//	afterLogin
485//		If we have an email verification token, process it
486//		If email is not verified, prompt user
487//		run afterLoginEmailVerified
488//
489//	afterLoginEmailVerified
490//		Update history
491//		Reset query string
492//		Run afterLoginGoToISY or afterLoginPortalUI
493//
494//	afterLoginPortalUI
495//
496
497// Initialize Paypal script, then continueInit()
498function initMain() {
499	var isProduction = serverConfig.ppEnv==='production'
500
501	if (!isProduction) {
502		console.log('Using Paypal Sandbox')
503	}
504
505	var ppClientId = isProduction ? serverConfig.ppClientIdProduction : serverConfig.ppClientIdSandbox;
506	var result = $.ajax({
507		url: 'https://www.paypal.com/sdk/js?client-id=' + ppClientId + '&currency=USD',
508		dataType: 'script',
509		timeout: 1000, // Need this in case of scipt load failure.
510		cache: true
511	})
512	.fail(function(data, status, jqXHR) {
513		console.log('Error loading Paypal script', status);
514	})
515	.always(function(data, status, jqXHR) {
516		continueInit();
517	})
518}
519
520function continueInit() {
521	var data;
522
523	// Initialises the tab UI, and set events for when tab are switched
524	$('#tabs').tabs({ create: tabCreate, activate: tabNavigate })
525	$('#admin-tabs').tabs({ activate: tabNavigateAdmin })
526	$('#body').show() // Body is hidden by default, until tabs are initialized
527
528	polyfill();
529
530	initCustomCombobox();
531
532	if (maintenanceMode) {
533		errorMessageDialog('ISY Portal is currently under maintenance.<br> Please come back later.', 'Warning')
534	} else {
535		loginDialog = loginDialogInit();
536		selfRegistrationDialog = selfRegistrationDialogInit();
537
538		if ($.gup('error')) {
539			// Error message to display? Possible case after a failed Green button authorization
540			errorMessageDialog(decodeURIComponent($.gup('error')), 'Error', 500)
541		}
542
543		// NOTE: No matter what the path is below, we need to make sure to call continueInit2() only once.
544		// To do this, we set a deferred promise which starts resolved, so we can chain promises
545
546		// Start a chain of promises
547		$.Deferred().resolve().promise()
548		.then(function () {
549			// If we have an accessToken, we will bypass login
550			if (!registeredUser && $.gup('accessToken')) {
551				var accessToken = $.gup('accessToken')
552
553				return $.ajax({
554					type: 'GET',
555					url: '/api/profile',
556					headers: {
557						Authorization: 'Bearer ' + accessToken
558					},
559				})
560				.then(function (profile) {
561					if (profile && profile.domainId) {
562						return $.ajax({
563							type: 'GET',
564							url: '/api/domain',
565							data: {id: profile.domainId},
566							// API requires no auth
567							// headers: {
568							// 	Authorization: 'Bearer ' + accessToken
569							// }
570						})
571						.then(function (domain) {
572							// console.log('profile', profile)
573							// console.log('domain', domain)
574							setRegisteredUserFromProfileAndAccessToken(profile, accessToken, domain.data)
575							// console.log('registeredUser', registeredUser)
576							// continueInit2()
577							// afterLogin({user: registeredUser});
578						})
579					}
580				})
581				.fail(function (jqXHR, textStatus, errorThrown) {
582					if (jqXHR.status === 403 || jqXHR.status === 401) {
583						console.log('Provided accessToken is not valid. Will show login dialog.')
584					} else {
585						console.log('Request to /api/profile failed with provided accessToken with HTTP %s', jqXHR.status)
586					}
587				})
588			} else if (!registeredUser) {
589				// See if we have a session cookie set (is this just a page refresh?)
590				data = getSessionStorage('udiportal');
591
592				if (data && data.registeredUser) {
593					registeredUser = data.registeredUser;
594					setLastConnected(data.registeredUser.lastConnected);
595
596					if (data.registeredUser.jwt) {
597						var refreshTokenExpiry = getJwtExpiry(registeredUser.jwt.refreshToken)
598
599						// If refreshToken is already expired, we need to go to login instead
600						if (refreshTokenExpiry.getTime() < Date.now()) {
601							setRegisteredUser(null);
602						}
603					}
604				}
605
606				if (registeredUser) {
607					if ($.gup('refresh') === 'domain') {
608						return ajaxJsonGet('/api/domain', $.param({id: registeredUser.domain}))
609						.then(function (data) {
610							setRegisteredUserDomainInfo(data.data);
611							// continueInit2()
612							// afterLogin({user: registeredUser});
613						})
614						.fail(function (jqXHR, textStatus, errorThrown) {
615							errorMessageDialog('Error refreshing account information. Please logout and login again', 'Error', 350)
616						})
617					}
618				}
619			}
620		})
621		.always(function () {
622			continueInit2()
623		})
624	}
625}
626
627function continueInit2() {
628	// If we did not have a 'udiportal' in session storage, and no accessToken provided, prompt for a login
629	if (!registeredUser) {
630		var data = getLocalStorage('udidata');
631		var loginAction='portal';
632		var cookieInfo=null;
633
634		if (data && data.lastLoginAction) {
635			loginAction = data.lastLoginAction;
636		}
637
638		loginDialog.resetForm();
639
640		if (location.hash && location.hash==='#passwordreset') {
641			loginDialog.setFields({ rememberMeCheckbox: rememberMeActive(), lastLoginAction: loginAction, mode:2 }); // mode: 0=login, 2=forgot your password 4=password change
642			loginDialog.modal.dialog('open');
643		} else if (location.hash && location.hash==='#registration') {
644			loginDialog.setFields({ lastLoginAction: loginAction }) // If user clicks cancel on self-registration
645			selfRegistrationDialog.setFields({ email: '', cb: afterLogin });
646			selfRegistrationDialog.modal.dialog('open');
647		} else {
648			loginDialog.setFields({ cb: afterLogin, lastLoginAction: loginAction, rememberMeCheckbox: rememberMeActive(), cookieInfo: rememberMe() })
649			loginDialog.modal.dialog('open');
650		}
651	} else {
652		afterLogin({user: registeredUser});
653	}
654
655	domainDialog = domainDialogInit();
656	userDialog = userDialogInit();
657	isyClaimDialog = isyClaimDialogInit();
658	isyNoteDialog = isyNoteDialogInit();
659	isyContactDialog = isyContactDialogInit();
660	// isyFirmwareDialog = isyFirmwareDialogInit();
661	eisyProvisioningDialog = eisyProvisioningDialogInit()
662	isyInfoDialog = isyInfoDialogInit();
663	isyImportDialog = isyImportDialogInit();
664	isyAccessLicenseDialog = isyAccessLicenseDialogInit();
665	isyAccessLicenseRenewDialog = isyAccessLicenseRenewDialogInit();
666	nsPurchaseDialog = nsPurchaseDialogInit();
667	isyOpenadrRegUploadDialog = isyOpenadrRegUploadDialogInit();
668	isyOpenadrRegDialog = isyOpenadrRegDialogInit();
669	liveMessageDialog = liveMessageDialogInit();
670	lastConnectedDialog = lastConnectedDialogInit();
671	myProfileDialog = myProfileDialogInit();
672	migrateDialog = migrateDialogInit();
673	eisyuiSettingsDialog = eisyuiSettingsDialogInit();
674	pinDialog = pinDialogInit();
675	passwordChangeDialog = passwordChangeDialogInit();
676	echoSpokenNodesDialog = echoSpokenNodesDialogInit();
677	echoSpokenNodesEditDialog = echoSpokenNodesEditDialogInit();
678	echoSpokenNodesAVEditDialog = echoSpokenNodesAVEditDialogInit();
679	echoSpokenRoomEditDialog = echoSpokenRoomEditDialogInit();
680	ghSpokenNodesDialog = ghSpokenNodesDialogInit();
681	ghSpokenNodesEditDialog = ghSpokenNodesEditDialogInit();
682	ghSpokenRoomEditDialog = ghSpokenRoomEditDialogInit();
683	iftttInNodesDialog = iftttInNodesDialogInit();
684	iftttInNodesEditDialog = iftttInNodesEditDialogInit();
685	iftttInNodesKeyDialog = iftttInNodesKeyDialogInit();
686	nsListDialog = nsListDialogInit();
687	hintEditorDialog = hintEditorDialogInit();
688	activityLogDialog = activityLogDialogInit();
689	ipalRenewalOptionEditDialog = ipalRenewalOptionEditDialogInit();
690	ipalNotificationOptionEditDialog = ipalNotificationOptionEditDialogInit();
691	ratePlanEditDialog = ratePlanEditDialogInit();
692	downloadUsersDialog = downloadUsersDialogInit();
693	downloadUsageDialog = downloadUsageDialogInit();
694	downloadStatsDialog = downloadStatsDialogInit();
695	nsConfigDialog = nsConfigDialogInit();
696	nsOccupancyDialog = nsOccupancyDialogInit();
697	nsOccupancyEditDialog = nsOccupancyEditDialogInit();
698	nsOccupancyLocationEditDialog = nsOccupancyLocationEditDialogInit();
699	nsOccupancyUrlDialog = nsOccupancyUrlDialogInit();
700	nsOccupancy2Dialog = nsOccupancy2DialogInit();
701	nsOccupancy2EditDialog = nsOccupancy2EditDialogInit();
702	nsOccupancy2GeofenceEditDialog = nsOccupancy2GeofenceEditDialogInit();
703	nsOccupancy2DeviceEditDialog = nsOccupancy2DeviceEditDialogInit();
704	nsOccupancy2GeofenceUrlDialog = nsOccupancy2GeofenceUrlDialogInit();
705	nsOccupancy2DevicePinDialog = nsOccupancy2DevicePinDialogInit();
706	udCommsEditDialog = udCommsEditDialogInit();
707	udCommsSurveyDialog = udCommsSurveyDialogInit();
708	udCommsDeliveryDialog = udCommsDeliveryDialogInit();
709
710	// Used in logout to close all dialogs
711	allDialogs = [
712		domainDialog.modal,
713		userDialog.modal,
714		loginDialog.modal,
715		myProfileDialog.modal,
716		migrateDialog.modal,
717		eisyuiSettingsDialog.modal,
718		isyClaimDialog.modal,
719		isyNoteDialog.modal,
720		isyContactDialog.modal,
721		// isyFirmwareDialog.modal,
722		eisyProvisioningDialog.modal,
723		isyInfoDialog.modal,
724		isyImportDialog.modal,
725		isyAccessLicenseDialog.modal,
726		isyAccessLicenseRenewDialog.modal,
727		nsPurchaseDialog.modal,
728		isyOpenadrRegUploadDialog.modal,
729		isyOpenadrRegDialog.modal,
730		liveMessageDialog.modal,
731		pinDialog.modal,
732		passwordChangeDialog.modal,
733		selfRegistrationDialog.modal,
734		lastConnectedDialog.modal,
735		echoSpokenNodesDialog.modal,
736		echoSpokenNodesEditDialog.modal,
737		echoSpokenNodesAVEditDialog.modal,
738		echoSpokenRoomEditDialog.modal,
739		ghSpokenNodesDialog.modal,
740		ghSpokenNodesEditDialog.modal,
741		ghSpokenRoomEditDialog.modal,
742		iftttInNodesDialog.modal,
743		iftttInNodesEditDialog.modal,
744		iftttInNodesKeyDialog.modal,
745		nsListDialog.modal,
746		hintEditorDialog.modal,
747		activityLogDialog.modal,
748		ipalRenewalOptionEditDialog.modal,
749		ipalNotificationOptionEditDialog.modal,
750		ratePlanEditDialog.modal,
751		downloadUsersDialog.modal,
752		downloadUsageDialog.modal,
753		downloadStatsDialog.modal,
754		nsConfigDialog.modal,
755		nsOccupancyDialog.modal,
756		nsOccupancyEditDialog.modal,
757		nsOccupancyLocationEditDialog.modal,
758		nsOccupancyUrlDialog.modal,
759		nsOccupancy2Dialog.modal,
760		nsOccupancy2EditDialog.modal,
761		nsOccupancy2GeofenceEditDialog.modal,
762		nsOccupancy2DeviceEditDialog.modal,
763		nsOccupancy2GeofenceUrlDialog.modal,
764		nsOccupancy2DevicePinDialog.modal,
765		udCommsEditDialog.modal,
766		udCommsSurveyDialog.modal,
767		udCommsDeliveryDialog.modal
768	];
769
770	initUI() // Initialize buttons
771
772	// Extends the Date object
773	initDateLog()
774
775	initEeUI()
776
777	// Test code only
778	// testUI();
779}
780
781// This is used when launched with ?accessToken=<accessToken>
782// profile is the output from /api/profile
783// accessToken will only provide temporary access to APIs
784function setRegisteredUserFromProfileAndAccessToken(profile, accessToken, domainInfo) {
785	registeredUser = {
786		basicAuth: null,
787		password: null,
788		jwt: null,
789		accessToken: accessToken,
790		domain: profile.domainId,
791		domainInfo: domainInfo,
792		domainadmin: profile.domainAdmin || (profile.domain === '$superadmin'), // superadmin is implicitly considered a domainadmin as well
793		emailVerified: profile.emailVerified,
794		isDeveloper: profile.isDeveloper,
795		isEEAdmin: profile.isEEAdmin,
796		isReviewer: profile.isReviewer,
797		isRootAccountUser: (!domainInfo.parent || domainInfo.parent===''),
798		isy: profile.preferredIsy,
799		lasConnected: [], // We don't have it in /api/profile. This will hide the last connected time from UI
800		notificationmaint: profile.notificationMaintenance,
801		notificationnews: profile.notificationNews,
802		passwordchange: false, // We won't request for a new password
803		superadmin: (profile.domain === '$superadmin'),
804		username: profile.username,
805		_id: profile.id
806	}
807
808	// Don't store it! If user refreshes the screen, too bad, he will have to re-login
809	deleteSessionStorage('udiportal');
810
811	// We don't care about the button label - We will not get there when we have an accessToken provided
812	// $('#domainadd').button('option', 'label', registeredUser.superadmin ? 'Add Account' : 'Add Sub-Account');
813}
814
815function credsToRegisteredUser(credentials) {
816	if (credentials.password) {
817		registeredUser.password = credentials.password
818		registeredUser.basicAuth = btoa(registeredUser.username + ':' + credentials.password)
819		registeredUser.jwt = null
820		registeredUser.accessToken = null
821	} else 	if (credentials.jwt) {
822		registeredUser.jwt = credentials.jwt
823		registeredUser.password = null
824		registeredUser.basicAuth = null
825		registeredUser.accessToken = null
826	} else {
827		// This should never happen
828		registeredUser.jwt = null
829		registeredUser.password = null
830		registeredUser.basicAuth = null
831		registeredUser.accessToken = null
832		console.log('No credentials are available')
833	}
834}
835
836// User is the user profile object, password is the actual password we got at login
837function setRegisteredUser(user, credentials, domainInfo) {
838	if (user) {
839		registeredUser = user;
840
841		if (credentials)
842			credsToRegisteredUser(credentials)
843
844		registeredUser.superadmin = (registeredUser.domain === '$superadmin');
845		registeredUser.domainadmin |= registeredUser.superadmin; // superadmin is implicitly considered a domainadmin as well
846		registeredUser.isRootAccountUser = (!domainInfo.parent || domainInfo.parent==='');
847		registeredUser.domainInfo = domainInfo; // Data obtain from /api/login
848		setSessionStorage('udiportal', { registeredUser: registeredUser });
849
850		// Change the Add account button label
851		// UDI admins can create both accounts and sub-accounts. Regular users, just sub-
851accounts
852		$('#domainadd').button('option', 'label', registeredUser.superadmin ? 'Add Account' : 'Add Sub-Account');
853	}
854	else {
855		registeredUser = null;
856		deleteSessionStorage('udiportal');
857	}
858}
859
860// Used after an update to the user profile
861function setRegisteredUserProfile(user) {
862	if (user) {
863		// Keep a copy as we don't know the creds.
864		var password = registeredUser.password;
865		var basicAuth = registeredUser.basicAuth;
866		var jwt = registeredUser.jwt;
867		var superadmin = registeredUser.superadmin;
868		var domainInfo = registeredUser.domainInfo;
869
870		registeredUser = user;
871		registeredUser.password = password;
872		registeredUser.basicAuth = basicAuth;
873		registeredUser.jwt = jwt;
874		registeredUser.superadmin = superadmin;
875		registeredUser.domainadmin |= superadmin; // superadmin is implicitly considered a domainadmin as well
876		registeredUser.isRootAccountUser = (!domainInfo.parent || domainInfo.parent==='');
877		registeredUser.domainInfo = domainInfo;
878
879		setSessionStorage('udiportal', { registeredUser: registeredUser });
880	}
881}
882
883// Used after an update to the domainInfo
884function setRegisteredUserDomainInfo(domainInfo) {
885	// Make sure we are attaching the right domain
886	if (domainInfo && domainInfo._id === registeredUser.domain) {
887		registeredUser.domainInfo = domainInfo;
888		setSessionStorage('udiportal', { registeredUser: registeredUser });
889	}
890}
891
892// Used only when changing password
893function setRegisteredUserCredentials(credentials) {
894	credsToRegisteredUser(credentials)
895	setSessionStorage('udiportal', { registeredUser: registeredUser });
896}
897
898// After adding a children sub-account, add it to our children
899function setRegisteredUserAddChildrenDomain(newDomainRecord) {
900	if (registeredUser.domain === newDomainRecord.parent) {
901		registeredUser.domainInfo.children.push(newDomainRecord._id)
902	}
903
904	setSessionStorage('udiportal', { registeredUser: registeredUser });
905}
906
907function getJwtExpiry(jwt) {
908	try {
909		const payloadBase64 = jwt.split('.')[1];
910		const payloadJson = atob(payloadBase64);
911		const payload = JSON.parse(payloadJson);
912		return payload.exp ? new Date(payload.exp * 1000) : null;
913	} catch (error) {
914		console.error('Invalid JWT format:', error);
915		return null;
916	}
917}
918
919var currentRefreshPromise = null
920
921function getJwtAccessToken() {
922	return $.when()
923	.then(function() {
924		if (registeredUser && registeredUser.jwt) {
925			if (!registeredUser.jwt.accessTokenExpiry) {
926				registeredUser.jwt.accessTokenExpiry = getJwtExpiry(registeredUser.jwt.accessToken)
927			}
928
929			// Expiring in less than a minute?
930			if (registeredUser.jwt.accessTokenExpiry.getTime() < Date.now() + (60*1000)) {
931				// If we already have a refresh underway, return the result of that refresh instead
932				if (currentRefreshPromise) {
933					console.log('JWT refresh already in progress')
934					return currentRefreshPromise
935				}
936
937				var refreshTokenExpiry = getJwtExpiry(registeredUser.jwt.refreshToken)
938
939				// If refresh token is not expired
940				if (refreshTokenExpiry.getTime() > Date.now()) {
941					console.log('Refreshing JWT tokens')
942
943					currentRefreshPromise = $.ajax({
944						url: '/api/jwt/refresh',
945						type: 'POST',
946						contentType: 'application/x-www-form-urlencoded',
947						data: new URLSearchParams({
948							refreshToken: registeredUser.jwt.refreshToken
949						}).toString()
950					})
951					.then(function (data) {
952						if (data.data.accessToken && data.data.refreshToken) {
953							setRegisteredUserCredentials({
954								jwt: {
955									accessToken: data.data.accessToken,
956									refreshToken: data.data.refreshToken
957								}
958							})
959
960							return data.data.accessToken
961						}
962					})
963					.fail(function (jqXHR, textStatus, errorThrown) {
964						console.log('Error renewing JWT tokens')
965						logout({ sessionError: true })
966					})
967					.done(() => {
968						currentRefreshPromise = null
969					})
970
971					return currentRefreshPromise
972				} else {
973					console.log('Refresh token is expired')
974					logout({ sessionExpired: true })
975				}
976			} else {
977				// console.log('Using existing JWT access token')
978				return registeredUser.jwt.accessToken
979			}
980		}
981	})
982}
983
984// Functions in parent window called from child windows
985function getRegisteredUser() {
986	return JSON.stringify(registeredUser);
987}
988
989function getCurrentISY() {
990	return JSON.stringify(currentISY);
991}
992
993// Functions called from child windows
994function initRegisteredUser() {
995	var user = getSessionStorage('udiportal');
996
997	if (user) {  // Use sessionStorage data if available
998		registeredUser = user.registeredUser;
999	}
1000	else if (window.opener) { // Otherwise, request to the parent (IE/Edge will use this on first launch, then use sessionStorage)
1001		var registeredUserStr = window.opener.getRegisteredUser();
1002
1003		if (registeredUserStr) {
1004			registeredUser = JSON.parse(registeredUserStr);
1005
1006			// Keep copy in this session storage, in case parent window gets closed.
1007			setSessionStorage('udiportal', { registeredUser: registeredUser });
1008		}
1009	}
1010	else {
1011		console.log('Initializing RegisteredUser: Neither session storage nor parent window are available')
1012	}
1013}
1014
1015function initCurrentISY() {
1016	var data = getSessionStorage('udidevice');
1017
1018	if (data) {  // Use sessionStorage data if available
1019		currentISY = data;
1020	}
1021	else if (window.opener) { // Otherwise, request to the parent (IE/Edge will use this on first launch, then use sessionStorage)
1022		var currentISYStr = window.opener.getCurrentISY();
1023
1024		if (currentISYStr) {
1025			currentISY = JSON.parse(currentISYStr);
1026
1027			// Keep copy in this session storage, in case parent window gets closed.
1028			setSessionStorage('udidevice', currentISY);
1029		}
1030	}
1031	else {
1032		console.log('Initializing currentISY: Neither session storage nor parent window are available')
1033	}
1034}
1035
1036
1037// Process this after user login
1038// data contains data returned by /login
1039// loginAction is 'portal' or 'isy'
1040var afterLogin = function (data, loginAction) {
1041	// if (registeredUser.domainInfo && isRatesAccount(registeredUser.domainInfo) && !ratesInitialized) {
1042	// 	$.getScript('webrates.js')
1043	// 	.done(function() {
1044	// 		ratesInitialized=true;
1045	// 		if (ratesInit) {
1046	// 			ratesInit();
1047	// 		}
1048	// 	})
1049	// 	.fail(function() {
1050	// 		console.log('ratesInit() was not loaded')
1051	// 	});
1052	// }
1053
1054	// if (registeredUser.domainInfo && isSceAccount(registeredUser.domainInfo) && !sceInitialized) {
1055	// 	$.getScript('MonthPicker.js')
1056	// 	.fail(function() {
1057	// 		console.log('MonthPicker.js was not loaded')
1058	// 	});
1059	//
1060	// 	// Load MonthPicker CSS
1061	// 	$('<link>').appendTo('head').attr({
1062	// 		type: 'text/css',
1063	// 		rel: 'stylesheet',
1064	// 		href: 'MonthPicker.css'
1065	// 	});
1066	//
1067	// 	$.getScript('websce.js')
1068	// 	.done(function() {
1069	// 		sceInitialized=true;
1070	// 		if (sceInit) {
1071	// 			sceInit();
1072	// 		}
1073	// 	})
1074	// 	.fail(function() {
1075	// 		console.log('sceInit() was not loaded')
1076	// 	});
1077	// }
1078
1079	// If page parameter has email verification token, process it (see emailverification.js)
1080	var emailVerificationToken = $.gup('emailverification');
1081
1082	if (emailVerificationToken) {
1083		if (data.user.emailVerified) {
1084			errorMessageDialog('Your email address is already verified.', 'Warning');
1085			afterLoginEmailVerified(data, loginAction);
1086		}
1087		else {
1088			ajaxJsonPost('/api/emailverification/code/' + emailVerificationToken, $.param( { email: data.user.username } ))
1089			.then(function(data, status, jqXHR) {
1090
1091				if (data.o2) {
1092					informationMessageDialog('Your email address verification is successful.<br/><br/>' +
1093						'Please click Ok to finalize the authorization process.')
1094					.then(function () {
1095						// We make the same call as we do from the authozization page, using the captured query string we had
1096						var originalLocationSearch = atob(data.o2)
1097						window.location.replace('/o2/authorize' + originalLocationSearch + '&'  +
1098							$.param({ email: registeredUser.username, password: registeredUser.password, new:true }));
1099							// the new:true above indicates that we went through the new registration process
1100					})
1101					.done();
1102				} else {
1103					informationMessageDialog('Your email address verification is successful.')
1104					.then(function () {
1105						setRegisteredUserProfile(data.data); // We need to update the profile with the emailVerified flag now set to true
1106						afterLoginEmailVerified({ user: data.data, account: registeredUser.domainInfo }, loginAction);
1107					})
1108					.done();
1109				}
1110			})
1111			.fail(function(jqXHR, textStatus, errorThrown) {
1112				if (jqXHR.status==404) {
1113					errorMessageDialog('email address verification failed');
1114				}
1115				else {
1116					errorMessageDialog('API error; HTTP error ' + jqXHR.status)
1117				}
1118			})
1119			.done();
1120		}
1121	}
1122	else if (!data.user.emailVerified) {
1123		errorMessageDialog('Your email address is not verified.<br><br>Click Ok to send a verification email to ' + data.user.username)
1124		.then(function() {
1125			ajaxJsonPost('/api/emailverification/request', $.param( { email: data.user.username } ))
1126			.then(function(data, status, jqXHR) {
1127				informationMessageDialog('In order to verify your email address, please check your inbox and follow the instructions.')
1128			})
1129			.fail(function(jqXHR, textStatus, errorThrown) {
1130				errorMessageDialog('API error; HTTP error ' + jqXHR.status)
1131			})
1132			.done();
1133		})
1134		.done();
1135	} else {
1136		afterLoginEmailVerified(data, loginAction)
1137	}
1138};
1139
1140function resetLocation() {
1141	// Reset the query string & hash without reloading page
1142	if (history.pushState) {
1143		var newurl = window.location.protocol + "//" + window.location.host + window.location.pathname;
1144		window.history.pushState({path:newurl},'',newurl);
1145	}
1146	else {
1147		location.search=''; //Reset the query & hash string, the old way, with page reload
1148	}
1149}
1150
1151function afterLoginEmailVerified(data, loginAction) {
1152	// Save the login action as selected in the login dialog
1153	setLocalStorage('udidata', { lastLoginAction: loginAction });
1154
1155	// #udajax allows to load udajax directly, without changing the preference
1156	if (location.hash === '#udajax') {
1157		loginAction = 'isy';
1158	}
1159
1160	if (loginAction === 'isy') {
1161		resetLocation();
1162		afterLoginGoToISY(data);
1163	}
1164	else {
1165		//resetLocation() // We do it later.
1166		afterLoginPortalUI(data);
1167	}
1168}
1169
1170function afterLoginGoToISY(data) {
1171	if (!data.user.isy || !data.user.isy.length) {
1172		errorMessageDialog('Preferred ISY not set on user profile', 'Preferred ISY not set')
1173		.then(function () {
1174			afterLoginPortalUI(data);
1175		})
1176		.done();
1177	}
1178	else {
1179		var preferredIsy = data.user.isy;
1180
1181		ajaxJsonGet('/api/isy', $.param({ uuid: data.user.isy }))
1182		.done(function (data) {
1183			if (!data.data.online) {
1184				errorMessageDialog('Your ISY is not online', 'ISY not online')
1185				.then(function () {
1186					afterLoginPortalUI(data);
1187				})
1188				.done();
1189			}
1190			else {
1191				currentISY = { uuid: preferredIsy, alias: data.data.domain[0].alias, isyKey: data.data.isyKey };
1192				setSessionStorage('udidevice', currentISY);
1193				launchWebAccess('_self');
1194			}
1195		})
1196		.fail(function(jqXHR, textStatus, errorThrown) {
1197			errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
1198		})
1199		.done();
1200	}
1201}
1202
1203
1204function afterLoginPortalUI(data) {
1205	var domainType;
1206
1207	$('#tabs').show();
1208	$('#footer').show();
1209
1210	if (data.domain && data.domain.type) {
1211		domainType = data.domain.type; // 0=Utility, 1=Corp
1212	}
1213
1214	switchLoginState(true); // Adjusts the top-right tab
1215
1216	// default active tab is ISY's list (in case we just re-logged in with less rights)
1217	$('#tabs').tabs('option', 'active', 0);
1218	$('#tabs').tabs('refresh');
1219
1220	// Remove logged out message
1221	$('#loggedoutmsg').hide();
1222
1223	updateDomainSelectors(registeredUser.domainInfo)
1224
1225	// Will display or hide buttons based on domain options
1226	var userType = getUserType(registeredUser)
1227	var domainOptions = getDomainOptions(registeredUser.domainInfo)
1228
1229	if (registeredUser.superadmin) {
1230		switchRole(userType, null); // Superadmin tabs & buttons
1231		switchDomainOptions(userType, domainOptions);
1232		$('#tab-domains').text('Accounts'); // Lets personalize the tab name
1233	} else {
1234		$('#tab-domains').text('My account'); // Lets personalize the tab name
1235
1236		if (registeredUser.domainadmin) {
1237			switchRole(userType, domainType); // Show domain admin tabs & buttons
1238			switchDomainOptions(userType, domainOptions);
1239		}
1240		else {
1241			switchRole(userType, domainType); // Show only user tabs & buttons
1242			switchDomainOptions(userType, domainOptions);
1243		}
1244	}
1245
1246	// The sub-account roller is shown only if we are a root account user (domain admin or domain user)
1247	if (registeredUser.isRootAccountUser) {
1248		$('#subDomainSelectorIsys, #subDomainSelectorUsers').show();
1249	} else {
1250		$('#subDomainSelectorIsys, #subDomainSelectorUsers').hide();
1251		// The domains tab is for domain admins, but also root accounts only!
1252		$('#tab-domains').hide();
1253	}
1254
1255	// When we recreate the rollers in updateDomainSelectors, we will start from the defaults
1256	$('.domainSelect, .subDomainSelect').val('');
1257
1258	// Indirectly triggers updateDomainSelectors - Domain rollers get feeded
1259//	tableDomains.setDomain(registeredUser.domain, true); // True = force reload.
1260
1261	// Show all toolbars
1262	$('.toolbar').show();
1263
1264	// Energy Expert Specific UI code
1265	afterLoginEeUI(data)
1266
1267
1268	if (location.hash) {
1269		var hashEnd = location.hash.indexOf('?');
1270		var hash = hashEnd>=0?location.hash.slice(0, hashEnd):location.hash;
1271		// Switch with hash without the parameters, if any
1272		switch(hash) {
1273			case '#myprofile':
1274				openMyProfile();
1275				break;
1276
1277			case '#license':
1278				openLicense(
1279					$.gup('uuid') && decodeURIComponent($.gup('uuid')),
1280					$.gup('key') && decodeURIComponent($.gup('key')), // isyKey
1281					$.gup('redirect') && decodeURIComponent($.gup('redirect')))
1282				break;
1283
1284			case '#ns-purchase':
1285				nodeserverPurchase(
1286					$.gup('pid') ? decodeURIComponent($.gup('pid')) : null,
1287					$.gup('nsid') ? decodeURIComponent($.gup('nsid')) : null,
1288					$.gup('redirect') ? decodeURIComponent($.gup('redirect')) : null,
1289					$.gup('beta') ? $.gup('beta') === 'true' : false,
1290					// If not specified, will take the paypal env from the server config
1291					($.gup('test') ? $.gup('test') === 'true' : undefined));
1292				break;
1293
1294			case '#tab-domains':
1295			case '#tab-ee':
1296			case '#tab-ee-admin':
1297				// If the ee tab is hidden, then we might be on our first try at linking, display the domains tab instead
1298				if ($(hash).is(':hidden')) {
1299					hash = '#tab-domains'
1300				}
1301
1302				// Search the hash in the ids in the tab's a ids
1303				setMainTab(hash)
1304				break;
1305		}
1306	}
1307
1308	resetLocation();
1309
1310	// Start logout timers & detect if there is activity
1311	autoLogout.start(function() {
1312		logout({ inactivity: true });
1313	});
1314}
1315
1316function setMainTab(tabId) {
1317	var index = $('#tabs ul a').index($(tabId));
1318	if (index !== -1) {
1319		$('#tabs').tabs('option', 'active', index);
1320		$('#tabs').tabs('refresh');
1321	}
1322}
1323
1324function getMainTab() {
1325	var activeTabIdx = $('#tabs').tabs('option','active');
1326	return $('#tabs > ul > li').eq(activeTabIdx).attr('aria-controls')
1327}
1328
1329function getDomainSelected() {
1330	return $('.domainSelect:first').val();
1331}
1332
1333// Low level function
1334function setDomainSelector(domainId) {
1335	return $('.domainSelect').val();
1336}
1337
1338// Triggered from tableDomains (onDataLoad), and at startup (/api/domains/lookup)
1339function updateDomainSelectors(userDomain) {
1340
1341	// currentSelectedDomain will be either:
1342	//	- The previously selected domain
1343	//  - The domain of the user, if it is a root domain
1344	//  - The parent of the domain of the user, if it is a child domain.
1345
1346	// Keep note of the domain before overwriting the selector
1347	var currentSelectedDomain = $('.domainSelect').first().val();
1348
1349	// currentSelectedSubDomain will be either:
1350	//	- The previously selected sub-domain
1351	//  - The domain of the user, if it is a sub-domain
1352	//  - $all ([All]) the domain of the user is a root domain
1353
1354	// Keep note of the sub-domain before overwriting the selector
1355	var currentSelectedSubDomain = $('.subDomainSelect').first().val();
1356
1357	// Initialize selectors based on user
1358	if (userDomain === '$superadmin') {
1359		// Keep existing selectors if they have something
1360		if (!currentSelectedDomain || !currentSelectedSubDomain) {
1361			currentSelectedDomain = userDomain;
1362			currentSelectedSubDomain = '$root';
1363		}
1364	} else {
1365		if (!userDomain.parent || userDomain.parent==='') {
1366			currentSelectedDomain = userDomain._id;
1367			currentSelectedSubDomain = '$root';
1368		} else {
1369			currentSelectedDomain = userDomain.parent;
1370			currentSelectedSubDomain = userDomain._id;
1371		}
1372	}
1373
1374	createDomainSelect(currentSelectedDomain);
1375
1376	ajaxJsonGet('/api/domains/lookup/sub', $.param({ domain: currentSelectedDomain }))
1377	.done(function (data) {
1378		createSubDomainSelect(data.data, currentSelectedDomain, currentSelectedSubDomain);
1379
1380		var activeTabID = getMainTab()
1381
1382		// Refresh the table (if the current table is tab-panel-isys or tab-panel-users - useful for startup only)
1383		var subDomainSelected = $('#tab-panel-isys .subDomainSelect').val();
1384
1385		updateTable(activeTabID, subDomainSelected);
1386	})
1387	.fail(function(jqXHR, textStatus, errorThrown) {
1388		errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
1389	})
1390	.done();
1391}
1392
1393function createDomainSelect(currentSelectedDomain) {
1394	var selector = $('.domainSelect')
1395	selector.domainCombobox({
1396		inputClass: 'role sadmin', // widget is visible to superadmin only
1397		// Process the input field
1398		focusout: function(event, ui) {
1399			// $(event.target).blur();
1400			var newDomainShortname = $(event.target).val();
1401
1402			// console.log('domainSelector focusout shortname', newDomainShortname)
1403			if (newDomainShortname) {
1404				ajaxJsonGet('/api/domains/lookup/shortname', $.param({ shortname: newDomainShortname }))
1405				.then((content) => {
1406
1407					// Could we locate the shortname?
1408					if (content.data) {
1409						var idDomain = content.data._id
1410
1411						// Will display or hide buttons based on domain options
1412						switchDomainOptions(getUserType(registeredUser), getDomainOptions(content.data));
1413
1414						// Rebuild the sub-Domain selector
1415						ajaxJsonGet('/api/domains/lookup/sub', $.param({ domain: idDomain }))
1416						.then((content) => {
1417							var subDomains = content.data
1418							// Rebuild the sub-Domain selector
1419							createSubDomainSelect(subDomains, idDomain, '$all');
1420
1421							// Makes all selectors set to same domain
1422							$('.domainSelect').domainCombobox('option', 'data', idDomain)
1423							if (idDomain==='$superadmin') {
1424								$('.subDomainSelect').val('$root');
1425							}
1426							else {
1427								$('.subDomainSelect').val('$all');
1428							}
1429							$('.subDomainSelect').selectmenu('refresh'); // Needs a refresh to show
1430
1431							var idTab = getMainTab()
1432
1433							if (idTab==='tab-panel-users') {
1434								tableUsers.setDomain(idDomain, true); // sub-account == $all, so show all from sub-accounts as well
1435							} else if (idTab==='tab-panel-isys') {
1436								tableIsys.setDomain(idDomain, true); // sub-account == $all, so show all from sub-accounts as well
1437							}
1438
1439							$('#isys_table_container, #users_table_container').show();
1440						})
1441					} else {
1442						// shortname entered was not valid
1443						// Rebuild the sub-Domain selector
1444						createSubDomainSelect([], '', '$all');
1445
1446						// Makes all selectors set to same domain
1447						$('.domainSelect').domainCombobox('option', 'data', '')
1448						if (idDomain==='$superadmin') {
1449							$('.subDomainSelect').val('$root');
1450						}
1451						else {
1452							$('.subDomainSelect').val('$all');
1453						}
1454						$('.subDomainSelect').selectmenu('refresh'); // Needs a refresh to show
1455
1456						$('#isys_table_container, #users_table_container').hide();
1457					}
1458				})
1459			} else {
1460				// shortname is empty
1461				$('#isys_table_container, #users_table_container').hide();
1462			}
1463		},
1464	})
1465
1466	selector.domainCombobox('option', 'source', function (request, response) {
1467		ajaxJsonGet('/api/domains/lookup/root', $.param(request))
1468		.done(function(content, status, jqXHR){
1469			response($.map(content.data, function (el) {
1470				return {
1471					label: el.shortname,
1472					value: el.shortname,
1473					data: el._id
1474				};
1475			}));
1476		})
1477	})
1478	.domainCombobox('option', 'data', currentSelectedDomain)
1479	.toggle(registeredUser.superadmin)
1480}
1481
1482
1483function createSubDomainSelect(domains, selectedDomain, selectedSubDomain) {
1484	// ---- Create the sub-domain selector SELECT
1485	var $subSelect = $('<select class="subDomainSelect"></select>');
1486	var selectedDomainIsValid=false;
1487
1488	// [UDI] has no sub accounts.
1489	if (selectedDomain!=='$superadmin') {
1490		$subSelect.append('<option value="$all">[All]</option>');
1491	}
1492
1493	$subSelect.append('<option value="$root">[Main Account]</option>');
1494
1495	domains.forEach(function(domain) {
1496		$subSelect.append('<option value="' + domain._id + '">' + domain.shortname + '</option>');
1497
1498		if (selectedSubDomain===domain._id) {
1499			selectedDomainIsValid=true;
1500		}
1501	});
1502
1503	// ---- Write the sub-domain SELECT
1504	$('#subDomainSelectorIsys').empty().append($subSelect.clone());
1505	$('#subDomainSelectorUsers').empty().append($subSelect.clone());
1506
1507	if (selectedDomainIsValid || selectedSubDomain==='$root') {
1508		$('.subDomainSelect').val(selectedSubDomain);
1509	}
1510
1511	// Sub-Domain selector
1512	$('.subDomainSelect').selectmenu({
1513		width:'14em',
1514
1515		// Adjust menu heigth based on window heigth
1516		open: function(event) {
1517			// Adjusts menu max height to the available height when it opens
1518			var topMenu = event.currentTarget.getBoundingClientRect().bottom;
1519			var windowHeigth = window.innerHeight;
1520			var ul = $('#' + $(event.target).attr('id') + '-menu');
1521			ul.attr('style', ul.attr('style') + 'max-height:' + (windowHeigth-topMenu-15) + 'px;')
1522		},
1523
1524		change: function(event, ui) {
1525			var idSubDomain = ui.item.value;
1526
1527			// Makes all selector set to same domain
1528			$('.subDomainSelect').val(idSubDomain);
1529			$('.subDomainSelect').selectmenu('refresh'); // Needs a refresh to show
1530
1531			// Find out which tab we are in
1532			var idTab = $(ui.item.element).closest('.ui-tabs-panel').attr('id');
1533
1534			// Update the table in that tab
1535			updateTable(idTab, idSubDomain);
1536		}
1537	});
1538}
1539
1540function updateTable(idTab, idSubDomain) {
1541	var domainSelected = getDomainSelected();
1542	// console.log('updateTable for', domainSelected)
1543
1544	switch(idSubDomain) {
1545		case '$all':
1546			if (idTab==='tab-panel-users') {
1547				tableUsers.setDomain(domainSelected, true);
1548			}
1549			else if (idTab==='tab-panel-isys') {
1550				tableIsys.setDomain(domainSelected, true);
1551			}
1552			break;
1553
1554		case '$root':
1555			if (idTab==='tab-panel-users') {
1556				tableUsers.setDomain(domainSelected, false);
1557			}
1558			else if (idTab==='tab-panel-isys') {
1559				tableIsys.setDomain(domainSelected, false);
1560			}
1561			break;
1562
1563		default:
1564			if (idTab==='tab-panel-users') {
1565				tableUsers.setDomain(idSubDomain, false);
1566			}
1567			else if (idTab==='tab-panel-isys') {
1568				tableIsys.setDomain(idSubDomain, false);
1569			}
1570			break;
1571	}
1572}
1573
1574// Process this on logout
1575function logout(cause) {
1576	if (!cause)
1577		cause = {}
1578
1579	if (rememberMeActive() && registeredUser) {
1580		rememberMeSet(registeredUser.username, registeredUser.password)
1581	}
1582
1583	// Close any tooltip by triggering a mouseout event.
1584//	$(document).trigger('mouseout');
1585
1586	setRegisteredUser(null);
1587	switchLoginState(false);  // adjusts the top-right tab
1588	setLastConnected(null); // Removed the connection history link
1589
1590	allDialogs.forEach(function (item) {
1591		item.dialog('close');
1592	});
1593
1594	tempDialogs.forEach(function (item) {
1595		item.dialog('close');
1596	});
1597
1598	// Disable autoLogout
1599	autoLogout.stop();
1600
1601	$('#tabs>ul>li>a:not(.duser)').hide(); // Only show user tabs.
1602	// Isys tab becomes the active tab
1603	$('#tabs').tabs('option', 'active', 0);
1604	$('#tabs').tabs('refresh');
1605
1606	$('.toolbar').hide();
1607	tableIsys.clear();
1608	tableUsers.clear();
1609
1610	$('#loggedoutmsg').text(
1611		cause.inactivity ? 'You have been logged out due to inactivity.' :
1612		cause.sessionError ? 'Error renewing your session token. Please login again.' :
1613		cause.sessionExpired ? 'Your session has expired.' :
1614			'You are logged out.');
1615	$('#loggedoutmsg').show();
1616}
1617
1618
1619// -------------- UI utilities --------------
1620
1621function getDomainOptions(domainInfo) {
1622	// Will display or hide buttons with class .domainOption.allowIsyImport
1623	var domainOptions = {}
1624	domainOptions.allowIsyImport = domainInfo.allowIsyImport || false
1625	domainOptions.allowMassMaintenance = domainInfo.allowMassMaintenance || false
1626
1627	return domainOptions
1628}
1629
1630function getUserType(user) {
1631	return user.superadmin ? 'sadmin' : user.domainadmin ? 'dadmin' : 'duser'
1632}
1633
1634
1635// Show/Hide tabs & buttons based on user role
1636// userType: sadmin, dadmin, duser
1637// domainType: '0'=Utility, '1'=corporate
1638function switchRole(userType, domainType) {
1639	// html that needs to be hidden/shown based on role needs to have the following syntax:
1640	// A class 'role' AND a class of userType or userType+domainType
1641	var roleSelector = '.role.' + userType;
1642
1643	if (domainType && domainType.length) {
1644		roleSelector += ', .role.' + userType + domainType;
1645	}
1646
1647	$('#tabs').find('.role').not(roleSelector).hide();
1648	$('#tabs').find(roleSelector).show();
1649}
1650
1651
1652// Show/Hide tabs & buttons based on user role
1653// userType: sadmin, dadmin, duser
1654// domainType: '0'=Utility, '1'=corporate
1655function switchDomainOptions(userType, domainOptions) {
1656	// html that needs to be hidden/shown based on the domain selected
1657	var domainSelector
1658
1659	if (typeof domainOptions === 'object') {
1660		domainSelector = Object.entries(domainOptions)
1661		.reduce(function (selector, optionPair) {
1662			var option = optionPair[0]
1663			var optionValue = optionPair[1]
1664			if (optionValue) {
1665				return selector += (selector.length?', ':'') + '.domainOption.' + option + '.' + userType
1666			} else {
1667				return selector
1668			}
1669		}, '')
1670	} else {
1671		domainSelector = ''
1672	}
1673
1674	$('#tabs').find('.domainOption').not(domainSelector).hide();
1675	$('#tabs').find(domainSelector).show();
1676}
1677
1678// Enables/Disables top right floating tabs
1679function switchLoginState(loggedIn) {
1680	if (loggedIn) {
1681		$('#loginbutton').hide();
1682		$('#logoutbutton').show();
1683		$('#myprofilebutton').show();
1684		$('#openpolyglot').show();
1685	}
1686	else {
1687		$('#loginbutton').show();
1688		$('#logoutbutton').hide();
1689		$('#myprofilebutton').hide();
1690		$('#openpolyglot').show(); // Shown all the time
1691	}
1692}
1693
1694// Build a dynamic jQuery selectmenu
1695function buildSelect(options, target, width, changeFunction) {
1696	var $select = $('<select></select>');
1697
1698	options.forEach(function(item) {
1699		$select.append('<option value="' + item.value + '">' + item.description + '</option>');
1700	});
1701
1702	$(target).empty().append($select.clone());
1703
1704	var opts = {};
1705
1706	if (width) {
1707		opts.width = width;
1708	}
1709
1710	if (changeFunction) {
1711		opts.change = changeFunction;
1712	}
1713
1714	$(target + ' select').selectmenu(opts);
1715}
1716
1717// Compare 2 selects.
1718function selectHasIdenticalOptions($select1, $select2) {
1719	var newOptions = $.map($select1.find('option'), function(option) { return $(option).attr('value'); });
1720	var oldOptions = $.map($select2.find('option'), function(option) { return $(option).attr('value'); });
1721
1722	var identical = true;
1723
1724	if (newOptions.length === oldOptions.length) {
1725		newOptions.forEach(function(option, index) {
1726			if (option !== oldOptions[index]) {
1727				identical=false;
1728			}
1729		})
1730	}
1731	else {
1732		identical=false;
1733	}
1734
1735	return identical;
1736}
1737
1738function deviceHasColorNode(address, stdLookup) {
1739	var found = false;
1740	var addressSplit = address.split('_');
1741	var addressSearch;
1742	var isMain = (addressSplit.length===2 && addressSplit[1].slice(0,3)==='1');
1743
1744	if (isMain) {
1745		addressSearch = addressSplit[0]+'_186';
1746
1747		// See if the individual device is a lock or thermostat, and use that device category instead ('lock' or 'thermostat')
1748		found = stdLookup.find(function(item) {
1749			return item.address === addressSearch;
1750		})
1751	}
1752
1753	return found
1754}
1755
1756
1757function zpad(n) {
1758	return n < 10 ? '0' + n.toString(10) : n.toString(10);
1759}
1760
1761function dtStr(dt) {
1762	return zpad(dt.getMonth()+1) + '/' + zpad(dt.getDate()) + '/' + + zpad(dt.getFullYear()) + ' ' +
1763		zpad(dt.getHours()) + ':' + zpad(dt.getMinutes())
1764}
1765
1766function dtDateStr(dt) {
1767	return zpad(dt.getMonth()+1) + '/' + zpad(dt.getDate()) + '/' + + zpad(dt.getFullYear())
1768}
1769
1770function createTabs(tabsId, contentId, tabsDef) {
1771	$(tabsId).empty().append(
1772		'<ul>' +
1773		tabsDef.map(function(name, index) {
1774			return '<li><a href="' + contentId + index + '">' + name + '</a></li>'
1775		}) +
1776		'</ul>' +
1777		tabsDef.map(function(name, index) {
1778			return '<div id="' + contentId.slice(1) + index + '"></div>'
1779		}))
1780
1781	$(tabsId).tabs()
1782}
1783
1784function addTabs(tabsId, contentId, tabsDef) {
1785	var existingTabs = $(tabsId + ' ul li')
1786	var n = existingTabs.length;
1787
1788	existingTabs.last().after(
1789		tabsDef.map(function(name, index) {
1790			return '<li><a href="' + contentId + (n+index) + '">' + name + '</a></li>'
1791		}))
1792
1793	existingTabs = $(tabsId).children('div:last').after(
1794		tabsDef.map(function(name, index) {
1795			return '<div id="' + contentId.slice(1) + (n+index) + '"></div>'
1796		}))
1797
1798	$(tabsId).tabs('refresh')
1799}
1800
1801function createTabCanvas(tabsContentId, tabContentIndex, title) {
1802	$(tabsContentId+tabContentIndex).append('<label>' + title + '</label><canvas class="spacer-top-10px" width="600" height="600">')
1803}
1804
1805function createChart(tabsContentId, tabContentIndex, data) {
1806	new Chart($(tabsContentId + tabContentIndex + ' canvas'), data);
1807}
1808
1809function makeTabsVertical(selector) {
1810	$(selector).addClass('ui-tabs-vertical ui-helper-clearfix' )
1811	.find('li').removeClass( 'ui-corner-top' ).addClass( 'ui-corner-left' );
1812}
1813
1814
1815// Inits buttons
1816function initUI() {
1817	// -------------- Domains page----------------
1818
1819	$('#domainrefresh').button({ icons: {primary: 'ui-icon-custom-refresh'}, text: false}).tooltip().click(function(event) {
1820		$(this).blur(); // Remove focus from button
1821		tableDomains.reload();
1822	});
1823
1824	$('#domainadd').button().click(function(event) {
1825		$(this).blur(); // Remove focus from button
1826
1827		if (registeredUser.superadmin) {
1828			ajaxJsonGet('/api/domains/root')
1829			.then(function(data) {
1830				data.data.map(function (item) {
1831					item.label = item.value = item.shortname;
1832					item.data = item._id; // Data used in the real input field sent to the server
1833					item.option = item;
1834				});
1835
1836				data.data.sort(function(a, b) {
1837					return a.shortname.localeCompare(b.shortname) }
1838				);
1839
1840				domainDialog.setFields({ editMode: false, isUDI: registeredUser.superadmin, sub:false, parentList: data.data });
1841				domainDialog.modal.dialog('open');
1842			})
1843		}
1844		else {
1845			domainDialog.setFields({ editMode: false, isUDI: registeredUser.superadmin, sub:true });
1846			domainDialog.modal.dialog('open');
1847		}
1848	});
1849
1850	// -------------- Users page----------------
1851	$('#useradd').button().click(function(event) {
1852		$(this).blur(); // Remove focus from button
1853		userDialog.resetForm();
1854
1855		// Get the current sub account
1856		var subDomainSelected = $(this).closest('.toolbar').find('.subDomainSelect').val();
1857
1858		if (subDomainSelected==='$all') {
1859			// If all we have is $all and $root, automatically select $root!
1860			if ($(this).closest('.toolbar').find('.subDomainSelect option').length===2) {
1861				subDomainSelected='$root';
1862				$('.subDomainSelect').val(subDomainSelected);
1863				$('.subDomainSelect').selectmenu('refresh'); // Needs a refresh to show
1864			}
1865			else {
1866				errorMessageDialog('Please select a specific sub-account first', 'Select sub-account')
1867			}
1868		}
1869
1870		switch (subDomainSelected) {
1871			case '$all':
1872				break; // Add requires a sub-account to be specified
1873
1874			case '$root':
1875				// When creating a user, put it in the root account, if $root is selected
1876				var domainSelected = $(this).closest('.toolbar').find('.domainSelect').val();
1877				userDialog.setDomain(domainSelected);
1878				userDialog.modal.dialog('open');
1879				break;
1880
1881			default:
1882				// Put the user in the selected sub account
1883				userDialog.setDomain(subDomainSelected);
1884				userDialog.modal.dialog('open');
1885				break;
1886		}
1887	});
1888
1889	$('#userrefresh').button({ icons: {primary: 'ui-icon-custom-refresh'}, text: false}).tooltip().click(function(event) {
1890		$(this).blur(); // Remove focus from button
1891		tableUsers.reload();
1892	});
1893
1894	// -------------- ISYs page----------------
1895	$('#isyadd').button().click(function(event) {
1896		var domainSelected = $(this).closest('.toolbar').find('.domainSelect').val();
1897		var subDomainSelected = $(this).closest('.toolbar').find('.subDomainSelect').val();
1898
1899		$(this).blur(); // Remove focus from button
1900
1901		isyClaimDialog.resetForm();
1902
1903		if (subDomainSelected==='$all') {
1904			// If all we have is $all and $root, automatically select $root!
1905			if ($(this).closest('.toolbar').find('.subDomainSelect option').length===2) {
1906				subDomainSelected='$root';
1907				$('.subDomainSelect').val(subDomainSelected);
1908				$('.subDomainSelect').selectmenu('refresh'); // Needs a refresh to show
1909			}
1910			else {
1911				errorMessageDialog('Please select a specific sub-account first', 'Select sub-
1911account')
1912			}
1913		}
1914
1915		switch (subDomainSelected) {
1916			case '$all':
1917				break; // Add requires a sub-account to be specified
1918
1919			case '$root':
1920				isyClaimDialog.setDomain(domainSelected);
1921				isyClaimDialog.modal.dialog('open');
1922				break;
1923
1924			default:
1925				isyClaimDialog.setDomain(subDomainSelected);
1926				isyClaimDialog.modal.dialog('open');
1927				break;
1928		}
1929	});
1930
1931	$('#isyimport').button().click(function(event) {
1932		var domainSelected = $(this).closest('.toolbar').find('.domainSelect').val();
1933		var subDomainSelected = $(this).closest('.toolbar').find('.subDomainSelect').val();
1934
1935		$(this).blur(); // Remove focus from button
1936
1937		isyClaimDialog.resetForm();
1938
1939		if (subDomainSelected==='$all') {
1940			// If all we have is $all and $root, automatically select $root!
1941			if ($(this).closest('.toolbar').find('.subDomainSelect option').length===2) {
1942				subDomainSelected='$root';
1943				$('.subDomainSelect').val(subDomainSelected);
1944				$('.subDomainSelect').selectmenu('refresh'); // Needs a refresh to show
1945			} else {
1946				errorMessageDialog('Please select a specific sub-account first', 'Select sub-account')
1947			}
1948		} else {
1949			switch (subDomainSelected) {
1950				case '$root':
1951					isyImportDialog.setFields({ domain: domainSelected });
1952					isyImportDialog.modal.dialog('open');
1953					break;
1954
1955				default:
1956					isyImportDialog.setFields({ domain: subDomainSelected });
1957					isyImportDialog.modal.dialog('open');
1958					break;
1959			}
1960		}
1961	});
1962
1963	$('#isyupgradeall').button().click(function(event) {
1964		var domainSelected = $(this).closest('.toolbar').find('.domainSelect').val();
1965		var subDomainSelected = $(this).closest('.toolbar').find('.subDomainSelect').val();
1966
1967		$(this).blur(); // Remove focus from button
1968
1969		if (subDomainSelected==='$all') {
1970			// If all we have is $all and $root, automatically select $root!
1971			if ($(this).closest('.toolbar').find('.subDomainSelect option').length===2) {
1972				subDomainSelected='$root';
1973				$('.subDomainSelect').val(subDomainSelected);
1974				$('.subDomainSelect').selectmenu('refresh'); // Needs a refresh to show
1975			} else {
1976				errorMessageDialog('Please select a specific sub-account first', 'Select sub-account')
1977			}
1978		} else {
1979			confirmationDialog('All ISYs in account will be upgraded.<br/>Are you sure?', 'Upgrade all ISYs?')
1980			.then(function () {
1981				var domain = subDomainSelected === '$root' ? domainSelected : subDomainSelected
1982
1983				// Works only for eisys
1984				ajaxJsonGet('/api/eisy/upgradeall', $.param({ domain: domain }))
1985				.done(function () {
1986					tableIsys.reload();
1987				})
1988				.fail(function(jqXHR, textStatus, errorThrown) {
1989					errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350);
1990				})
1991			})
1992			.done();
1993		}
1994	});
1995
1996	$('#isyrebootall').button().click(function(event) {
1997		var domainSelected = $(this).closest('.toolbar').find('.domainSelect').val();
1998		var subDomainSelected = $(this).closest('.toolbar').find('.subDomainSelect').val();
1999
2000		$(this).blur(); // Remove focus from button
2001
2002		if (subDomainSelected==='$all') {
2003			// If all we have is $all and $root, automatically select $root!
2004			if ($(this).closest('.toolbar').find('.subDomainSelect option').length===2) {
2005				subDomainSelected='$root';
2006				$('.subDomainSelect').val(subDomainSelected);
2007				$('.subDomainSelect').selectmenu('refresh'); // Needs a refresh to show
2008			}
2009			else {
2010				errorMessageDialog('Please select a specific sub-account first', 'Select sub-account')
2011			}
2012		} else {
2013			confirmationDialog('All ISYs in account will be rebooted.<br/>Are you sure?', 'Reboot all ISYs?')
2014			.then(function () {
2015				var domain = subDomainSelected === '$root' ? domainSelected : subDomainSelected
2016
2017				ajaxJsonGet('/api/isy/rebootall', $.param({ domain: domain }))
2018				.done(function () {
2019					tableIsys.reload();
2020				})
2021				.fail(function(jqXHR, textStatus, errorThrown) {
2022					errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350);
2023				})
2024			})
2025			.done();
2026		}
2027	});
2028
2029	$('#isyrefresh').button({ icons: {primary: 'ui-icon-custom-refresh'}, text: false}).tooltip().click(function(event) {
2030		$(this).blur(); // Remove focus from button
2031		tableIsys.reload();
2032	});
2033
2034	$('#scereports').button();
2035
2036	// -------------- Admin / All ISYs page----------------
2037	$('#isyconnectedrefresh').button({ icons: {primary: 'ui-icon-custom-refresh'}, text: false}).tooltip().click(function(event) {
2038		$(this).blur(); // Remove focus from button
2039		tableConnectedIsys.reload();
2040	});
2041
2042	// -------------- Admin / IPAL Renewals options ----------------
2043	$('#ipal_renewal_option_add').button().tooltip().click(function(event) {
2044		$(this).blur(); // Remove focus from button
2045		ipalRenewalOptionEditDialog.setFields();
2046		ipalRenewalOptionEditDialog.modal.dialog('open');
2047	});
2048
2049	$('#ipal_renewal_option_refresh').button({ icons: {primary: 'ui-icon-custom-refresh'}, text: false}).tooltip().click(function(event) {
2050		$(this).blur(); // Remove focus from button
2051		tableIpalRenewalOptions.reload();
2052	});
2053
2054	// -------------- Admin / IPAL Notifications options ----------------
2055	$('#ipal_notification_option_add').button().tooltip().click(function(event) {
2056		$(this).blur(); // Remove focus from button
2057		ipalNotificationOptionEditDialog.setFields();
2058		ipalNotificationOptionEditDialog.modal.dialog('open');
2059	});
2060
2061	$('#ipal_notification_option_download').button().tooltip().click(function(event) {
2062		$(this).blur(); // Remove focus from button
2063
2064		var message = 'This will find the ISYs which still have an active license that will be expired within 30 days. ' +
2065			'For those ISYs, the relevant users will be identified based on the account billing notification settings.<br/><br/>' +
2066			'Users that are not domain admins, or did not have their email verified are excluded.<br/><br/>' +
2067			'Press Ok to download.';
2068
2069		informationMessageDialog(message, 'Export users', 800)
2070		.then(function () {
2071			ajaxJsonPostTimeout3M('/api/ipal/notification/download/expired')
2072			.then(function(data, status, jqXHR) {
2073				try {
2074					var isFileSaverSupported = !!new Blob;
2075				} catch (e) {}
2076
2077				if (isFileSaverSupported) {
2078					var BOM = '\uFEFF';
2079					saveAs(new Blob([BOM + data.users], {type: "application/csv;charset=utf-8"}), 'users.csv');
2080					// saveAs(new Blob([data.orphaned], {type: "application/csv;charset=utf-8"}), 'orphaned.csv');
2081				}
2082				else {
2083					errorMessageDialog('Feature not supported on this browser', 'Not supported');
2084				}
2085			})
2086			.fail(function(jqXHR, textStatus, errorThrown) {
2087				errorMessageDialog('API error - HTTP error ' + jqXHR.status)
2088			})
2089			.done();
2090
2091//			location.href = '/api/ipal/notification/download/expired';
2092//			window.open('/api/ipal/notification/download/expired'); // , '_blank'
2093		})
2094		.done();
2095	});
2096
2097	$('#ipal_notification_option_refresh').button({ icons: {primary: 'ui-icon-custom-refresh'}, text: false}).tooltip().click(function(event) {
2098		$(this).blur(); // Remove focus from button
2099		tableIpalNotificationOptions.reload();
2100	});
2101
2102	// -------------- Admin / UD Communications ----------------
2103	$('#ud-comms-option-add').button().tooltip().click(function(event) {
2104		$(this).blur(); // Remove focus from button
2105		udCommsEditDialog.setFields({});
2106		udCommsEditDialog.modal.dialog('open');
2107	});
2108
2109	$('#ud-comms-option-refresh').button({ icons: {primary: 'ui-icon-custom-refresh'}, text: false}).tooltip().click(function(event) {
2110		$(this).blur(); // Remove focus from button
2111		tableUdComms.reload();
2112	});
2113
2114	// -------------- Admin / Rate plan options ----------------
2115	$('#rateplans_add').button().tooltip().click(function(event) {
2116		$(this).blur(); // Remove focus from button
2117		ratePlanEditDialog.setFields({refreshFunction: tableRatePlanOptions.refresh});
2118		ratePlanEditDialog.modal.dialog('open');
2119	});
2120
2121	$('#rateplans_refresh').button({ icons: {primary: 'ui-icon-custom-refresh'}, text: false}).tooltip().click(function(event) {
2122		$(this).blur(); // Remove focus from button
2123		tableRatePlanOptions.refresh();
2124	});
2125
2126	$('#downloadUsers').button().tooltip().click(function(event) {
2127		$(this).blur(); // Remove focus from button
2128		downloadUsersDialog.modal.dialog('open');
2129	});
2130	$('#downloadUsage').button().tooltip().click(function(event) {
2131		$(this).blur(); // Remove focus from button
2132		downloadUsageDialog.modal.dialog('open');
2133	});
2134	$('#downloadStats').button().tooltip().click(function(event) {
2135		$(this).blur(); // Remove focus from button
2136		downloadStatsDialog.modal.dialog('open');
2137	});
2138}
2139
2140// The tabActivate event is not run on initial tab creation, so we trap the creation here and allow tabActivated to run.
2141function tabCreate(event, ui) {
2142	ui.newPanel = ui.panel;  // The activate event works with newPanel, not panel
2143	ui.oldPanel = null;
2144	tabNavigate(event, ui);
2145}
2146
2147// This runs when we switch tabs
2148function tabNavigate(event, ui) {
2149	var activeTab = ui.newPanel.attr('id');
2150	var oldTab;
2151	var domain;
2152	var idSubDomain;
2153
2154	if (ui.oldPanel!=null) {
2155		oldTab = ui.oldPanel.attr('id');
2156	}
2157
2158	if (activeTab === 'tab-panel-admin') {
2159//		tableConnectedIsys.load();
2160		tableIpalRenewalOptions.load(); // This is the first tab in the admin tab. We need to load it here.
2161	} else if (activeTab === 'tab-panel-domains') {
2162		tableDomains.setDomain(registeredUser.domainInfo); // Refresh if domain has changed since last time.
2163	} else if (activeTab === 'tab-panel-users') {
2164		idSubDomain = $('#tab-panel-users .subDomainSelect').val();
2165		updateTable('tab-panel-users', idSubDomain)
2166	} else if (activeTab === 'tab-panel-isys') {
2167		if (registeredUser) {
2168			idSubDomain = $('#tab-panel-isys .subDomainSelect').val();
2169			updateTable('tab-panel-isys', idSubDomain)
2170		}
2171	} else if (activeTab === 'tab-panel-ee') {
2172		tabEE()
2173	} else if (activeTab === 'tab-panel-ee-admin') {
2174		tabEEAdmin()
2175	}
2176}
2177
2178// This runs when we switch tabs
2179function tabNavigateAdmin(event, ui) {
2180	var activeTab = ui.newPanel.attr('id');
2181
2182	if (activeTab === 'tab-panel-admin-ipal-renewal') {
2183		tableIpalRenewalOptions.load(); // We don't automatically refresh! Just load once. (load instead of reload)
2184	} else if (activeTab === 'tab-panel-admin-ipal-notifications') {
2185		tableIpalNotificationOptions.load(); // We don't automatically refresh! Just load once. (load instead of reload)
2186	} else if (activeTab === 'tab-panel-admin-ud-comms') {
2187		tableUdComms.load(); // We don't automatically refresh! Just load once. (load instead of reload)
2188	} else if (activeTab === 'tab-panel-admin-rateplans') {
2189		tableRatePlanOptions.load(); // We don't automatically refresh! Just load once. (load instead of reload)
2190	} else if (activeTab === 'tab-panel-admin-connected') {
2191		tableConnectedIsys.load(); // We don't automatically refresh! Just load once. (load instead of reload)
2192	}
2193}
2194
2195/*
2196// The tabActivateAdmin event is not run on initial tab creation, so we trap the creation here and allow tabActivated to run.
2197function tabCreateAdmin(event, ui) {
2198	ui.newPanel = ui.panel;  // The activate event works with newPanel, not panel
2199	ui.oldPanel = null;
2200	tabNavigateAdmin(event, ui);
2201}
2202*/
2203
2204// Extends the Date object to provide a function to get a date string as we want it in the log
2205function initDateLog() {
2206	Date.prototype.dateLog = function() {
2207		var yyyy = this.getFullYear().toString();
2208		var MM = (this.getMonth()+1).toString(); // getMonth() is zero-based
2209		var dd  = this.getDate().toString();
2210		var hh  = this.getHours().toString();
2211		var mm  = this.getMinutes().toString();
2212		var ss  = this.getSeconds().toString();
2213		return yyyy + '/' + (MM[1]?MM:"0"+MM[0]) + '/' + (dd[1]?dd:"0"+dd[0]) + ' ' + (hh[1]?hh:"0"+hh[0]) + ':' + (mm[1]?mm:"0"+mm[0])+ ':' + (ss[1]?ss:"0"+ss[0]);
2214	};
2215/*
2216	Date.prototype.isoDate = function() {
2217		console.log('Date before', this)
2218		var yyyy = this.getFullYear().toString();
2219		var MM = (this.getMonth()+1).toString(); // getMonth() is zero-based
2220		var dd  = this.getDate().toString();
2221		console.log('Date After', yyyy + '-' + (MM[1]?MM:"0"+MM[0]) + '-' + (dd[1]?dd:"0"+dd[0]))
2222
2223		return yyyy + '-' + (MM[1]?MM:"0"+MM[0]) + '-' + (dd[1]?dd:"0"+dd[0]);
2224	};
2225*/
2226}
2227
2228// If date is not valid, returns [Invalid Date] instead of throwing
2229function dateFormatNoError(dt, mask) {
2230	var formatted;
2231
2232	try {
2233		formatted = dateFormat(dt, mask);
2234	}
2235	catch (e) {
2236		formatted = '[Invalid Date]'
2237	}
2238
2239	return formatted;
2240}
2241
2242function polyfill() {
2243	// Creates Array.prototype.find
2244
2245	if (!Array.prototype.find) {
2246		Array.prototype.find = function(predicate) {
2247			if (this == null) {
2248			  throw new TypeError('Array.prototype.find called on null or undefined');
2249			}
2250
2251			if (typeof predicate !== 'function') {
2252			  throw new TypeError('predicate must be a function');
2253			}
2254
2255			var list = Object(this);
2256			var length = list.length >>> 0;
2257			var thisArg = arguments[1];
2258			var value;
2259
2260			for (var i = 0; i < length; i++) {
2261				value = list[i];
2262				if (predicate.call(thisArg, value, i, list)) {
2263					return value;
2264				}
2265			}
2266			return undefined;
2267		};
2268	}
2269
2270	if (![].includes) {
2271		Array.prototype.includes = function(searchElement /*, fromIndex*/ ) {
2272			var O = Object(this);
2273			var len = parseInt(O.length) || 0;
2274
2275			if (len === 0) {
2276				return false;
2277			}
2278
2279			var n = parseInt(arguments[1]) || 0;
2280			var k;
2281
2282			if (n >= 0) {
2283				k = n;
2284			}
2285			else {
2286				k = len + n;
2287				if (k < 0) {k = 0;}
2288			}
2289
2290			var currentElement;
2291
2292			while (k < len) {
2293				currentElement = O[k];
2294
2295				if (searchElement === currentElement || (searchElement !== searchElement && currentElement !== currentElement)) {
2296					return true;
2297				}
2298
2299				k++;
2300			}
2301
2302			return false;
2303		};
2304	}
2305
2306	if (!Object.assign) {
2307		Object.defineProperty(Object, 'assign', {
2308			enumerable: false,
2309			configurable: true,
2310			writable: true,
2311			value: function(target) {
2312				'use strict';
2313				if (target === undefined || target === null) {
2314					throw new TypeError('Cannot convert first argument to object');
2315				}
2316
2317				var to = Object(target);
2318				for (var i = 1; i < arguments.length; i++) {
2319					var nextSource = arguments[i];
2320					if (nextSource === undefined || nextSource === null) {
2321						continue;
2322					}
2323					nextSource = Object(nextSource);
2324
2325					var keysArray = Object.keys(Object(nextSource));
2326					for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) {
2327						var nextKey = keysArray[nextIndex];
2328						var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey);
2329						if (desc !== undefined && desc.enumerable) {
2330							to[nextKey] = nextSource[nextKey];
2331						}
2332					}
2333				}
2334
2335				return to;
2336			}
2337		});
2338	}
2339
2340	jQuery.extend({
2341		gup: function(param){//Retrieves get information form URL
2342			var param2 = param.replace(/[[]/,"[").replace(/[]]/,"]");
2343			var regexS = "[?&]"+param2+"=([^&#]*)";
2344			var regex = new RegExp(regexS);
2345			var results = regex.exec(window.location.href);
2346			if(results) {
2347				return results[1];
2348			}
2349		}
2350	})
2351}
2352
2353function initCustomCombobox() {
2354	// autocompleteMaxHeight works like autocomplete, but menu will scroll if it falls outside of the viewport
2355	$.widget( "custom.autocompleteMaxHeight", $.ui.autocomplete, {
2356
2357		_resizeMenu: function() {
2358			var topMenu = this.element[0].getBoundingClientRect().bottom;
2359			var windowHeigth = window.innerHeight;
2360			var maxHeight = windowHeigth-topMenu-15;
2361
2362			this.menu.element.css({height:''});
2363
2364			if (this.menu.element.outerHeight() > maxHeight) {
2365				this.menu.element.outerHeight(maxHeight);
2366			}
2367
2368			return this._super();
2369		},
2370
2371		_renderItem: function(ul, item) {
2372			if (item.class) {
2373				return $("<li>").attr("class", item.class).append(item.label).appendTo(ul);
2374			}
2375			else {
2376				return $("<li>").text(item.label).appendTo(ul); // Standard autocomplete _renderItem
2377			}
2378		}
2379	});
2380
2381	// Create combobox, which uses custom.autocompleteMaxHeight & a button to show the items
2382	$.widget("custom.combobox", {
2383		options: {
2384			inputClass: 'ui-state-default',
2385			focusout: null,
2386			source: []  // source is an array of {value: x, label, x, data: x, option: <self reference> }
2387		},
2388			// label: shown in the dropdown list
2389			// value: Sets the input field to this value when selecting from the dropdown (this.input)
2390			// data: real data assigned to the input field (this.element)
2391
2392		// Creates the custom combobox
2393		_create: function() {
2394			this.disab = false;
2394 // disabled is a reserved method for a widget
2395			this.wrapper = $("<span>")
2396			.addClass("custom-combobox")
2397			.insertAfter(this.element);
2398
2399			this.element.hide();
2400			this._createAutocomplete();
2401			this._createShowAllButton();
2402		},
2403
2404		// Autocomplete popups up a menu when we type something
2405		_createAutocomplete: function() {
2406			this.inputWrapper1 = $("<div>")
2407			.addClass("custom-combobox-inputwrapper1")
2408			.appendTo(this.wrapper);
2409
2410			this.inputWrapper2 = $("<div>")
2411			.addClass("custom-combobox-inputwrapper2")
2412			.appendTo(this.inputWrapper1);
2413
2414			this.input = $("<input>")
2415			.appendTo(this.inputWrapper2)
2416			.val(this._dataToValue(this.element.val()))
2417			.attr("title", "")
2418			.addClass("custom-combobox-input ui-widget ui-widget-content ui-corner-left")
2419			.addClass(this.options.inputClass)
2420			.autocompleteMaxHeight({
2421				delay: 100,
2422				minLength: 0,
2423				source: this.option('source')
2424			})
2425			.tooltip({
2426				tooltipClass: "ui-state-highlight"
2427			});
2428
2429			this._on(this.input, {
2430				autocompleteselect: function(event, ui) {
2431					this._trigger("select", event, {
2432						item: ui.item.option
2433					});
2434				},
2435				focusout: function(event, ui) {
2436					if (this.options.focusout) {
2437						this.options.focusout(event, ui)
2438					}
2439				}
2440			});
2441
2442		},
2443
2444		// Set custom combobox options after initialization
2445		_setOption: function(key, value) {
2446			if (key==='data') {
2447				this.input.val(this._dataToValue(value)); // This widget's value
2448				this.element.val(value); // Real input field
2449			}
2450			else if (key==='inputClass') {
2451				this.options.inputClass = value;
2452				this.input.addClass(value)
2453			}
2454			else if (key==='focusout') {
2455				this.options.focusout = value;
2456			}
2457			else if (key==='source') {
2458				this.options.source = value; // We keep a local reference
2459				this.input.autocompleteMaxHeight("option", key, value);
2460			}
2461			else if (key==='getValue') {
2462				return this.getValue();
2463			}
2464			else if (key==='getOption') {
2465				return this.getOption();
2466			}
2467			else if (key==='disabled') {
2468				this.disabled(value);
2469			}
2470			else {
2471				this.input.autocompleteMaxHeight("option", key, value);
2472			}
2473		},
2474
2475		// Creates the drop down button
2476		_createShowAllButton: function() {
2477			var input = this.input,
2478				thisWidget = this,
2479				wasOpen = false;
2480
2481			this.buttonWrapper = $("<a>")
2482			.attr("tabIndex", -1)
2483			.appendTo(this.wrapper)
2484			.button({
2485				icons: { primary: "ui-icon-triangle-1-s" },
2486				text: false
2487			})
2488			.removeClass("ui-corner-all")
2489			.addClass("custom-combobox-toggle ui-corner-right")
2490			.mousedown(function() {
2491				wasOpen = input.autocompleteMaxHeight("widget").is(":visible");
2492			})
2493			.click(function(event) {
2494				if (!thisWidget.disab) {
2495					input.focus();
2496
2497					// Close if already visible
2498					if (wasOpen) {
2499						return;
2500					}
2501
2502					// Pass empty string as value to search for, displaying all results
2503					input.autocompleteMaxHeight("search", "");
2504				}
2505			});
2506		},
2507
2508		_valueToItem: function (value) {
2509			var valueLowerCase = value ? value.toLowerCase() : null,
2510				result = null;
2511
2512			this.options.source.forEach(function(item) {
2513				if (item.value.toLowerCase() === valueLowerCase ) {
2514					result = item;
2515					return false;
2516				}
2517			});
2518
2519			return result;
2520		},
2521
2522		_valueToData: function (value) {
2523			// var valueLowerCase = value.toLowerCase(),
2524			// 	data = null;
2525			//
2526			// this.options.source.forEach(function(item) {
2527			// 	if (item.value.toLowerCase() == valueLowerCase ) {
2528			// 		data = item.data;
2529			// 		return false;
2530			// 	}
2531			// });
2532			// return data;
2533
2534			var item = this._valueToItem(value);
2535			return item ? item.data : null;
2536		},
2537
2538		_dataToValue: function (data) {
2539			var dataLowerCase = data?data.toLowerCase():null,
2540				value=null;
2541
2542			if (!this.options.source) {
2543				console.log('combobox source is empty');
2544			}
2545
2546			this.options.source.forEach(function(item) {
2547				if (item.data.toLowerCase() === dataLowerCase ) {
2548				value = item.value;
2549				return false;
2550			}
2551		});
2552
2553			return value;
2554		},
2555
2556		refresh: function() {
2557			var data = this._valueToData(this.input.val());
2558
2559			if (!data) {
2560				data='';
2561			}
2562			this.element.val(data);
2563		},
2564
2565		disabled: function(value) {
2566			this.disab = value;
2567			this.buttonWrapper.button('option', 'disabled', value);
2568			if (value) {
2569				this.input.attr('readonly', value);
2570			}
2571			else {
2572				this.input.removeAttr('readonly')
2573			}
2574		},
2575
2576		validate: function() {
2577			this.refresh();
2578
2579			// Search for a match (case-insensitive)
2580			var valueLowerCase = this.input.val().toLowerCase(),
2581				valid = false;
2582
2583			this.options.source.forEach(function(item) {
2584				if (item.label.toLowerCase() === valueLowerCase || item.value.toLowerCase() === valueLowerCase) {
2585					valid = true;
2586					return false;
2587				}
2588			});
2589
2590			return valid;
2591		},
2592
2593		getValue: function() {
2594			//	this.refresh();
2595			// Gets the text value from the input box
2596			return this.input.val();
2597		},
2598
2599		getItem: function() {
2600			var value = this.getValue();
2601			return this._valueToItem(value)
2602		},
2603
2604		_destroy: function() {
2605			this.wrapper.remove();
2606			this.element.show();
2607		}
2608	});
2609
2610	// Create combobox, which uses custom.autocompleteMaxHeight & a button to show the items
2611	$.widget("custom.domainCombobox", {
2612		options: {
2613			inputClass: 'ui-state-default',
2614			focusout: null,
2615			submit: null,
2616			source: []  // source will be a function
2617		},
2618		// label: shown in the dropdown list
2619		// value: Sets the input field to this value when selecting from the dropdown (this.input)
2620		// data: real data assigned to the input field (this.element)
2621
2622		// Creates the custom combobox
2623		_create: function() {
2624			this.disab = false;
2624 // disabled is a reserved method for a widget
2625			this.wrapper = $("<span>")
2626			.addClass("custom-domainCombobox")
2627			.insertAfter(this.element);
2628
2629			this.element.hide();
2630			this._createAutocomplete();
2631			// this._createShowAllButton();
2632		},
2633
2634		// Autocomplete popups up a menu when we type something
2635		_createAutocomplete: function() {
2636			this.inputWrapper1 = $("<div>")
2637			.addClass("custom-combobox-inputwrapper1")
2638			.appendTo(this.wrapper);
2639
2640			this.inputWrapper2 = $("<div>")
2641			.addClass("custom-combobox-inputwrapper2")
2642			.appendTo(this.inputWrapper1);
2643
2644			this.input = $("<input>")
2645			.appendTo(this.inputWrapper2)
2646			// .val(this._dataToValue(this.element.val()))
2647			.attr("title", "")
2648			.addClass("custom-combobox-input ui-widget ui-widget-content ui-corner-left")
2649			.addClass(this.options.inputClass)
2650			.autocompleteMaxHeight({
2651				delay: 400,
2652				minLength: 1,
2653				source: this.option('source')
2654			})
2655			.tooltip({
2656				tooltipClass: "ui-state-highlight"
2657			});
2658
2659			this._on(this.input, {
2660				autocompleteselect: function(event, ui) {
2661					this._trigger("select", event, {
2662						item: ui.item.option
2663					});
2664					if (this.options.select) {
2665						this.options.select(event)
2666					}
2667				},
2668				focusout: function(event, ui) {
2669					if (this.options.focusout) {
2670						this.options.focusout(event)
2671					}
2672				},
2673				select: function(event, ui) {
2674					if (this.options.select) {
2675						this.options.select(event)
2676					}
2677				}
2678			});
2679
2680		},
2681
2682		// Set custom combobox options after initialization
2683		_setOption: function(key, value) {
2684			var self = this;
2685			if (key==='data.shortname') {
2686				if (value) {
2687					ajaxJsonGet('/api/domains/lookup/shortname', $.param({shortname: value}))
2688					.done(function (data) {
2689						if (data.data && data.data._id && data.data.shortname) {
2690							self.input.val(data.data.shortname); // This widget's value
2691							self.element.val(data.data._id); // Real input field
2692						}
2693					})
2694					.fail(function (jqXHR, textStatus, errorThrown) {
2695						errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
2696					})
2697					.done();
2698				} else {
2699					self.input.val(''); // This widget's value
2700					self.element.val(''); // Real input field
2701				}
2702			}
2703			else if (key==='data') {
2704				if (value) {
2705					ajaxJsonGet('/api/domains/lookup/id', $.param({ id: value }))
2706					.done(function (data) {
2707
2708						if (data.data && data.data._id && data.data.shortname) {
2709							self.input.val(data.data.shortname); // This widget's value
2710							self.element.val(data.data._id); // Real input field
2711						}
2712					})
2713					.fail(function(jqXHR, textStatus, errorThrown) {
2714						errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
2715					})
2716					.done();
2717				} else {
2718					self.input.val(''); // This widget's value
2719					self.element.val(''); // Real input field
2720				}
2721			}
2722			else if (key==='inputClass') {
2723				this.options.inputClass = value;
2724				this.input.addClass(value)
2725			}
2726			else if (key==='focusout') {
2727				this.options.focusout = value;
2728			}
2729			else if (key==='source') {
2730				this.options.source = value; // We keep a local reference
2731				this.input.autocompleteMaxHeight("option", key, value);
2732			}
2733			else if (key==='getValue') {
2734				return this.getValue();
2735			}
2736			// else if (key==='getOption') {
2737			// 	return this.getOption();
2738			// }
2739			else if (key==='disabled') {
2740				this.disabled(value);
2741			}
2742			// else if (key==='show') {
2743			// 	this.disabled(value);
2744			// }
2745			else {
2746				this.input.autocompleteMaxHeight("option", key, value);
2747			}
2748		},
2749
2750		// // Creates the drop down button
2751		// _createShowAllButton: function() {
2752		// 	var input = this.input,
2753		// 		thisWidget = this,
2754		// 		wasOpen = false;
2755		//
2756		// 	this.buttonWrapper = $("<a>")
2757		// 	.attr("tabIndex", -1)
2758		// 	.appendTo(this.wrapper)
2759		// 	.button({
2760		// 		icons: { primary: "ui-icon-triangle-1-s" },
2761		// 		text: false
2762		// 	})
2763		// 	.removeClass('ui-corner-all')
2764		// 	.addClass('custom-combobox-toggle ui-corner-right')
2765		// 	.mousedown(function() {
2766		// 		wasOpen = input.autocompleteMaxHeight('widget').is(':visible');
2767		// 	})
2768		// 	.click(function(event) {
2769		// 		if (!thisWidget.disab) {
2770		// 			console.log('click')
2771		// 			input.focus();
2772		//
2773		// 			// Close if already visible
2774		// 			if (wasOpen) {
2775		// 				console.log('close!')
2776		// 				return;
2777		// 			}
2778		//
2779		// 			// Pass empty string as value to search for, displaying all results
2780		// 			input.autocompleteMaxHeight('search', '');
2781		// 		}
2782		// 	});
2783		// },
2784
2785		// _valueToItem: function (value) {
2786		// 	var valueLowerCase = value ? value.toLowerCase() : null,
2787		// 		result = null;
2788		//
2789		// 	this.options.source.forEach(function(item) {
2790		// 		if (item.value.toLowerCase() == valueLowerCase ) {
2791		// 			result = item;
2792		// 			return false;
2793		// 		}
2794		// 	});
2795		//
2796		// 	return result;
2797		// },
2798		//
2799		// _valueToData: function (value) {
2800		// 	// var valueLowerCase = value.toLowerCase(),
2801		// 	// 	data = null;
2802		// 	//
2803		// 	// this.options.source.forEach(function(item) {
2804		// 	// 	if (item.value.toLowerCase() == valueLowerCase ) {
2805		// 	// 		data = item.data;
2806		// 	// 		return false;
2807		// 	// 	}
2808		// 	// });
2809		// 	// return data;
2810		//
2811		// 	var item = this._valueToItem(value);
2812		// 	return item ? item.data : null;
2813		// },
2814		//
2815		// _dataToValue: function (data) {
2816		// 	var dataLowerCase = data?data.toLowerCase():null,
2817		// 		value=null;
2818		//
2819		// 	if (!this.options.source) {
2820		// 		console.log('combobox source is empty');
2821		// 	}
2822		//
2823		// 	this.options.source.forEach(function(item) {
2824		// 		if (item.data.toLowerCase() === dataLowerCase ) {
2825		// 			value = item.value;
2826		// 			return false;
2827		// 		}
2828		// 	});
2829		//
2830		// 	return value;
2831		// },
2832
2833		refresh: function() {
2834			var data = this._valueToData(this.input.val());
2835
2836			if (!data) {
2837				data='';
2838			}
2839			this.element.val(data);
2840		},
2841
2842		disabled: function(value) {
2843			this.disab = value;
2844			this.buttonWrapper.button('option', 'disabled', value);
2845			if (value) {
2846				this.input.attr('readonly', value);
2847			}
2848			else {
2849				this.input.removeAttr('readonly')
2850			}
2851		},
2852
2853		validate: function() {
2854			this.refresh();
2855
2856			// Search for a match (case-insensitive)
2857			var valueLowerCase = this.input.val().toLowerCase(),
2858				valid = false;
2859
2860			this.options.source.forEach(function(item) {
2861				if (item.label.toLowerCase() === valueLowerCase || item.value.toLowerCase() === valueLowerCase) {
2862					valid = true;
2863					return false;
2864				}
2865			});
2866
2867			return valid;
2868		},
2869
2870		getValue: function() {
2871			//	this.refresh();
2872			// Gets the text value from the input box
2873			return this.input.val();
2874		},
2875
2876		// getItem: function() {
2877		// 	var value = this.getValue();
2878		// 	return this._valueToItem(value)
2879		// },
2880
2881		_destroy: function() {
2882			this.wrapper.remove();
2883			this.element.show();
2884		}
2885	});
2886}
2887
2888// Returns string 'time since...'
2889function since(min) {
2890	if (Math.round(min) <= 1) {
2891		return '1 minute ago';
2892	}
2893	else if (min < 90) {  // < 90 minutes
2894		return Math.round(min) + ' minutes ago';
2895	}
2896	else if (min < 48*60) {  // < 48 hr
2897		return Math.round(min/60) + ' hours ago';
2898	}
2899	else {
2900		return Math.round(min/1440) + ' days ago';
2901	}
2902}
2903
2904
2905var autoLogout = {
2906	// Set timeout variables.
2907
2908	timeoutWarningDelay: 840000, // Display warning in 14 Mins.
2909	timeoutGraceDelay: 60000, // Warning has been shown, give the user 1 minute to interact
2910//	timeoutWarningDelay: 10000, // Display warning in 14 Mins.
2911//	timeoutGraceDelay: 4000, // Warning has been shown, give the user 1 minute to interact
2912
2913	warningTimer: null,
2914	timeoutTimer: null,
2915	warningDialog: null,
2916	logout: null,
2917
2918	// Start timer & detect activity
2919	start: function (logoutFunct) {
2920		// Restart timer on any click
2921		this.resetTimer();
2922		this.logout = logoutFunct;
2923		document.body.addEventListener('mouseup', this.resetTimer.bind(this));
2924	},
2925
2926	// Stop timeout & stop activity detection
2927	stop: function () {
2928		clearTimeout(this.warningTimer);
2929		clearTimeout(this.timeoutTimer);
2930		document.body.removeEventListener('mouseup', this.resetTimer.bind(this));
2931	},
2932
2933	// Restart inactivity timeout
2934	resetTimer: function () {
2935		clearTimeout(this.warningTimer);
2936		clearTimeout(this.timeoutTimer);
2937		this.warningTimer = setTimeout(this.idleWarning.bind(this), this.timeoutWarningDelay);
2938
2939		if (this.warningDialog) {
2940			this.warningDialog.dialog('close');
2941		}
2942	},
2943
2944	// Show idle timeout warning dialog.
2945	idleWarning: function () {
2946		var _this = this;
2947
2948		clearTimeout(this.warningTimer);
2949		this.timeoutTimer = setTimeout(this.idleTimeout.bind(this), this.timeoutGraceDelay);
2950
2951		var dialogOptions = {
2952			autoOpen: true,
2953			buttons: {
2954				'Yes': function() {
2955					$(this).dialog('close');
2956					_this.resetTimer();
2957				}
2958			},
2959			close: function() {
2960				this.warningDialog=null;
2961			}
2962		};
2963
2964		var html = '<div title="Inactivity timeout"><p autofocus>' +
2965			'<span class="ui-icon ui-icon-alert" style="float:left; margin:0 7px 20px 0;"></span><span>Stay logged in?</span></p></div>';
2966
2967		this.warningDialog = $(html).dialog(dialogOptions);
2968	},
2969
2970	// Logout the user.
2971	idleTimeout: function () {
2972		if (this.warningDialog) {
2973			this.warningDialog.dialog('close');
2974		}
2975
2976		if (this.logout) {
2977			this.logout();
2978		}
2979	}
2980};
2981
2982function launchEisyui(uuid, isyKey, version) {
2983	// Strip build suffix (e.g. 1.2.2-1 -> 1.2.2)
2984	version = version.replace(/-\d+$/, '');
2985	// User must have logged in normally (not with a JWT)
2986	if (!registeredUser.password)
2987		errorMessageDialog('Error launching eisy-ui: Please logout/login to portal');
2988
2989	ajaxJsonPost('/api/jwt/create', $.param({
2990		email: registeredUser.username,
2991		password: registeredUser.password,
2992		uuid: uuid
2993	}))
2994	.then(function(data, status, jqXHR) {
2995		if (!data.successful) {
2996			console.log('Error creating JWT: ' + data.errorMessage);
2997			return $.Deferred().reject(data.errorMessage).promise();
2998		}
2999
3000		setLocalStorage('eisy-sso', {
3001			accessToken: data.data.accessToken,
3002			refreshToken: data.data.refreshToken,
3003			isyKey: isyKey
3004		})
3005
3006		window.open('/eisy/' + version + '/', '_blank', 'menubar=1,location=1,resizable=1,scrollbars=1,status=1,toolbar=1');
3007	})
3008	.fail(function(jqXHR, textStatus, errorThrown) {
3009		errorMessageDialog('Error launching eisy-ui');
3010	})
3011	.done();
3012}
3013
3014
3015function launchWebAccess(windowOption, specs) {
3016	var redirectUrl=["udajax.htm","had.htm"];  // UDAjax / HAD
3017	var redirectOption = 0; // Default Home page (0=UDAjax, 1 = HAD)
3018	var s = localStorage.getItem("redirectOption");
3019
3020	if (s) {
3021		redirectOption = parseInt(JSON.parse(s));
3022	}
3023
3024	if (redirectOption >
3024= redirectUrl.length) { // That would be an invalid option
3025		redirectOption = 0
3026	}
3027
3028	window.open('udajax/' + redirectUrl[redirectOption] + '?mode=corp', windowOption, specs);
3029}
3030
3031
3032function setLastConnected(lastConnected) {
3033	if (lastConnected && lastConnected[0]) {
3034		var minutes = (new Date - new Date(lastConnected[0].time))/60000; // returns diff minutes
3035
3036		$('#footer-lastconnection-label').show();
3037		$('#footer-lastconnection-data').text(since(minutes));
3038/* Not useful: The IP shown is from the load balancer!
3039		$('#footer-lastconnection-data').click(function () {
3040			lastConnectedDialog.setFields(registeredUser.lastConnected);
3041			lastConnectedDialog.modal.dialog('open');
3042		})
3043*/
3044	}	else {
3045		$('#footer-lastconnection-label').hide();
3046		$('#footer-lastconnection-data').text('');
3047	}
3048}
3049
3050function findInArray(arr, property, value) {
3051	return arr.find(function(item) { return item[property] === value })
3052}
3053
3054// Zero-pad a number to a certain length
3055function pad(num, size) {
3056	var s = num+"";
3057	while (s.length < size) s = "0" + s;
3058	return s;
3059}
3060
3061function getIsPolisyOrEisy(productId) {
3062	if (!productId) {
3063		return false
3064	}
3065
3066	// ISY 994: '1110'
3067	// 52xx is an eisy
3068	return [ /^52\d\d$/ ].findIndex((regex) => regex.test(productId)) !== -1
3069}
3070
3071
3072// Returns firmware database, but will use cache if another request is within cacheDelay
3073// var firmwareDB = {
3074// 	cacheDelay: 3600000, // No need to refresh more often than 1 hour;
3075// 	data: null,
3076// 	lastRefresh: null,
3077//
3078// 	refresh: function () {
3079// 		var vars = this;
3080//
3081// 		return ajaxJsonGet('/api/firmwaredb')
3082// 		.then(function (xml) {
3083// 			vars.data = xml;
3084// 			vars.lastRefresh = new Date();
3085// 			return vars.data;
3086// 		})
3087// 	},
3088// 	getXML: function () {
3089// 		if ((new Date() - this.lastRefresh) > this.cacheDelay ) {
3090// 			return this.refresh();
3091// 		}
3092// 		else {
3093// 			return $.Deferred().resolve(this.data).promise();
3094// 		}
3095//     },
3096// 	getVersions: function (appName, platform) {  // Get available versions for appName/platform
3097// 	    var deferred = $.Deferred();
3098//
3099// 		this.getXML()
3100// 		.then(function (xml) {
3101// 			var latestVersion;
3102// 			var availableVersions = [];
3103//
3104// 			// Finds the right app
3105// 			$(xml).find("product>name:contains('"+ appName +"')").first().parent().each(function() {
3106// 				// Finds the current version
3107// 				latestVersion = $(this).children('currentVersion').text();
3108//
3109// 				$(this).find("version").each( function() {
3110// 					var versionObj = {};
3111// 					versionObj.version = $(this).children('number').text();
3112// 					versionObj.description = $(this).children('description').text();
3113//
3114// 					// Make sure the platform is found on this version before adding it to the list
3115// 					$(this).find("platform>name:contains('"+ platform +"')").first().each( function() {
3116// 						availableVersions.push(versionObj);
3117// 					});
3118// 				});
3119// 			});
3120//
3121// 			availableVersions.sort(function (a, b) {
3122// 				if (a.version===latestVersion) { // Latest version will always be first.
3123// 					return -1;
3124// 				}
3125// 				else {
3126// 					return versionCompare(b.version, a.version)
3127// 				}
3128// 			});
3129//
3130// 			deferred.resolve(availableVersions);
3131// 		});
3132//
3133// 		return 	deferred.promise();
3134// 	}
3135// };
3136
3137
3138
3139// --------------- Ajax functions -------------------------
3140
3141function ajaxJsonGet(url, query, headers) {
3142	return ajaxCall('GET', url, query, headers)
3143}
3144
3145function ajaxDownloadGet(url, query, headers) {
3146	var headers = headers || {}
3147
3148	if (registeredUser && registeredUser.basicAuth) {
3149		// It's faster to send the Authorization header from the get go
3150		headers['Authorization'] = 'Basic ' + registeredUser.basicAuth
3151	} else if (registeredUser && registeredUser.accessToken) {
3152		headers['Authorization'] = 'Bearer ' + registeredUser.accessToken
3153	} else if (registeredUser && registeredUser.jwt) {
3154		return getJwtAccessToken()
3155		.then(function (accessToken) {
3156			if (!accessToken)
3157				throw new Error('Session has expired')
3158
3159			headers['Authorization'] = 'Bearer ' + accessToken
3160		})
3161	}
3162
3163	return new Promise((resolve, reject) => {
3164		// Construct URL with query string
3165		let fullUrl = url;
3166		if (query) {
3167			fullUrl = `${url}${url.includes('?') ? '&' : '?'}${query}`;
3168		}
3169
3170		const xhr = new XMLHttpRequest();
3171		xhr.open('GET', fullUrl, true);
3172		xhr.responseType = 'blob';
3173
3174		// Copy headers
3175		if (headers) {
3176			Object.keys(headers).forEach(key => {
3177				xhr.setRequestHeader(key, headers[key]);
3178			});
3179		}
3180
3181		xhr.onload = function() {
3182			if (xhr.status === 200) {
3183				const blob = xhr.response;
3184
3185				// Get filename from Content-Disposition header
3186				let filename = 'data.dta'; // default filename
3187				const disposition = xhr.getResponseHeader('Content-Disposition');
3188				if (disposition && disposition.indexOf('attachment') !== -1) {
3189					const matches = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(disposition);
3190					if (matches != null && matches[1]) {
3191						filename = matches[1].replace(/['"]/g, '');
3192					}
3193				}
3194
3195				const url = window.URL.createObjectURL(blob);
3196				const a = document.createElement('a');
3197				a.style.display = 'none';
3198				a.href = url;
3199				a.download = filename;
3200				document.body.appendChild(a);
3201				a.click();
3202				window.URL.revokeObjectURL(url);
3203				document.body.removeChild(a);
3204				resolve();
3205			} else {
3206				reject(new Error('Download failed with status: ' + xhr.status));
3207			}
3208		};
3209
3210		xhr.onerror = function() {
3211			reject(new Error('Download failed'));
3212		};
3213
3214		xhr.send();
3215	});
3216}
3217
3218
3219
3220function ajaxJsonDelete(url, query, headers) {
3221	return ajaxCall('DELETE', url, query, headers)
3222}
3223
3224function ajaxJsonPost(url, query, headers) {
3225	return ajaxCall('POST', url, query, headers)
3226}
3227
3228function ajaxJsonPostTimeout2M(url, query, headers) {
3229	return ajaxCall('POST', url, query, headers, {
3230		timeout:120000
3231	})
3232}
3233
3234function ajaxJsonPostTimeout3M(url, query, headers) {
3235	return ajaxCall('POST', url, query, headers, {
3236		timeout:180000
3237	})
3238}
3239
3240function ajaxJsonPostTimeout4M(url, query, headers) {
3241	return ajaxCall('POST', url, query, headers, {
3242		timeout:240000
3243	})
3244}
3245
3246function ajaxJsonPut(url, query, headers) {
3247	return ajaxCall('PUT', url, query, headers)
3248}
3249
3250function ajaxIsyRestGet(url, isyKey) {
3251	return ajaxCall('GET', '/' + encodeURIComponent(isyKey) + url)
3252}
3253
3254function ajaxCall(method, url, query, customHeaders, customAjaxOptions) {
3255	var ajaxOptions = {
3256		type: method,
3257		url: url,
3258		headers: {},
3259  };
3260
3261	if (customAjaxOptions) {
3262		Object.assign(ajaxOptions, customAjaxOptions);
3263	}
3264
3265	if (query) {
3266		ajaxOptions.data = query;
3267	}
3268
3269	if (customHeaders) {
3270		for (var header in customHeaders) {
3271			if (Object.prototype.hasOwnProperty.call(customHeaders, header) ) {
3272				ajaxOptions.headers[header] = customHeaders[header];
3273			}
3274		}
3275	}
3276
3277	return $.when()
3278	.then(function () {
3279		if (registeredUser && registeredUser.basicAuth) {
3280			// It's faster to send the Authorization header from the get go
3281			ajaxOptions.headers['Authorization'] = 'Basic ' + registeredUser.basicAuth
3282		} else if (registeredUser && registeredUser.accessToken) {
3283			ajaxOptions.headers['Authorization'] = 'Bearer ' + registeredUser.accessToken
3284		} else if (registeredUser && registeredUser.jwt) {
3285			return getJwtAccessToken()
3286			.then(function (accessToken) {
3287				if (!accessToken)
3288					throw new Error('Session has expired')
3289
3290				ajaxOptions.headers['Authorization'] = 'Bearer ' + accessToken
3291			})
3292		}
3293	})
3294	.then(function () {
3295		if (registeredUser)
3296			feedbackRequestingURL(method, url);
3297
3298		return $.ajax(ajaxOptions)
3299		.done(function(data, status, jqXHR) {
3300			setTimeout('feedback()', 800);
3301		})
3302		.fail(function(jqXHR, textStatus, errorThrown) {
3303			feedbackErrURL(method, url);
3304			setTimeout('feedback()', 2000);
3305		});
3306	})
3307}
3308
3309function msgprefix(method) {
3310	var prefix = '';
3311
3312	switch(method) {
3313		case 'GET':
3314			prefix = 'Retrieving';
3315			break;
3316		case 'POST':
3317			prefix = 'Updating';
3318			break;
3319		case 'PUT':
3320			prefix = 'Inserting';
3321			break;
3322    case 'DELETE':
3323      prefix = 'Removing';
3324      break;
3325    default:
3326      prefix = method;
3327	}
3328	return prefix;
3329}
3330
3331
3332// ------------ Message feedback functions ----------------------
3333function feedbackRequestingURL(method, url) {
3334	feedback(msgprefix(method) + ': ' + url);
3335}
3336
3337function feedbackErrURL(method, url, errmsg) {
3338	feedback(msgprefix(method) + ': ' + url + '  (' + (errmsg?errmsg:'error returned') + ')');
3339}
3340
3341function feedback(text, selector) { // Displays a status at the top of page
3342	if (!selector) 	{
3343		selector = '.feedback'; // Default feedback placeholder: any tag with the class feedback.
3344	}
3345
3346	if (!text || text === '') {
3347		text = '<br>';
3348	}
3349
3350	$(selector).html(text);
3351}
3352
3353
3354// -------------- Data persistence across pages ------------
3355
3356function setSessionStorage(key, content) {
3357	// Mobile browser with incognito mode will raise an error
3358	try {
3359		sessionStorage.setItem(key, JSON.stringify(content));
3360	}
3361	catch(err) {
3362		console.log(err)
3363	}
3364}
3365
3366function deleteSessionStorage(key) {
3367	sessionStorage.removeItem(key);
3368}
3369
3370function getSessionStorage(key) {
3371	return JSON.parse(sessionStorage.getItem(key))
3372}
3373
3374// -------------- Data persistence with no expiration ------------
3375
3376function setLocalStorage(key, content) {
3377	// Mobile browser with incognito mode will raise an error
3378	try {
3379		localStorage.setItem(key, JSON.stringify(content));
3380	}
3381	catch(err) {
3382		console.log(err)
3383	}
3384}
3385
3386function getLocalStorage(key) {
3387	return JSON.parse(localStorage.getItem(key))
3388}
3389
3390function deleteLocalStorage(key) {
3391	localStorage.removeItem(key);
3392}
3393
3394
3395// returns UUID with No Colons
3396function uuidNC(uuid) {
3397	return uuid.toLowerCase().replace(uuidRegex, '$1$2$3$4$5$6');
3398}
3399
3400// returns UUID with With Colons
3401function uuidWC(uuid) {
3402	return uuid.toLowerCase().replace(uuidRegex, '$1:$2:$3:$4:$5:$6');
3403}
3404
3405// Compares firmware versions
3406// v2>v1 -> -1
3407// v2<v1 -> 1
3408// v2==v1 -> 0
3409function versionCompare(v1, v2) {
3410	v1 = /^([\d.]+)/.exec(v1)[0]; // Skips last portion of version that are not digits (Example: 5.0.8T2 -> 5.0.8)
3411	v2 = /^([\d.]+)/.exec(v2)[0];
3412
3413	var v1parts = v1.split('.');
3414	var v2parts = v2.split('.');
3415
3416	function isValidPart(x) {
3417		return /^\d+$/.test(x);
3418	}
3419
3420	if (!v1parts.every(isValidPart) || !v2parts.every(isValidPart)) {
3421		return NaN;
3422	}
3423
3424	while (v1parts.length < v2parts.length) v1parts.push("0");
3425	while (v2parts.length < v1parts.length) v2parts.push("0");
3426
3427	v1parts = v1parts.map(Number);
3428	v2parts = v2parts.map(Number);
3429
3430	for (var i = 0; i < v1parts.length; ++i) {
3431		if (v2parts.length === i) {
3432			return 1;
3433		}
3434
3435		if (v1parts[i] === v2parts[i]) {
3436			continue;
3437		}
3438		else if (v1parts[i] > v2parts[i]) {
3439			return 1;
3440		}
3441		else {
3442			return -1;
3443		}
3444	}
3445
3446	if (v1parts.length !== v2parts.length) {
3447		return -1;
3448	}
3449
3450	return 0;
3451}
3452
3453function loadOpenadrReg(uuid, alias, pk, domain) {
3454	isyOpenadrRegDialog.setFields({ pk: pk, uuid: uuid, alias: alias });
3455	isyOpenadrRegDialog.modal.dialog('open');
3456
3457	ajaxJsonGet('/api/isy/oadr/retrieve', $.param({id: pk, domain: domain }))
3458	.done(function (data) {
3459		isyOpenadrRegDialog.setFields({ pk: pk, domain: domain, uuid: uuid, alias: alias, xml: data });
3460	})
3461	.fail(function(jqXHR, textStatus, errorThrown) {
3462		if (jqXHR.status==503) {
3463			errorMessageDialog('ISY is not online')
3464		} else if (jqXHR.status==423) {
3465			errorMessageDialog('Requested ISY not found')
3466		}	else if (jqXHR.status==404) {
3467			errorMessageDialog('OpenADR configuration file not found')
3468		}	else {
3469			errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
3470		}
3471	})
3472	.done();
3473}
3474
3475// Read a page's GET URL variables and return them as an associative array.
3476function getUrlVars() {
3477	var vars = [], hash;
3478	var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&');
3479	for(var i = 0; i < hashes.length; i++)
3480	{
3481		hash = hashes[i].split('=');
3482		vars.push(hash[0]);
3483		vars[hash[0]] = hash[1];
3484	}
3485	return vars;
3486}
3487
3488function minimumVersionCheck(currentVersion, requiredVersion) {
3489	var successful = (versionCompare(currentVersion, requiredVersion) >= 0);
3490
3491	if (!successful) {
3492		errorMessageDialog('ISY Firmware version must be ' + nodeServerMinimumFirmware + ' or newer.', 'Error', 500);
3493	}
3494
3495	return successful
3496}
3497
3498
3499/*
3500function startAdminConsole(uuid, firmware) {
3501	try {
3502		var isFileSaverSupported = !!new Blob;
3503	} catch (e) {}
3504
3505	if (isFileSaverSupported) {
3506		ajaxJsonGet('/rsc/admin.tpl')
3507		.then(function(str) {
3508			str = str.replace(/\$CODEBASE/, 'http://isy.universal-devices.com/994i/'+firmware)
3509			str = str.replace(/\$UUID/, uuid)
3510			str = str.replace(/\$PORTALURL/, 'https://' + location.hostname + '/isy/' +uuidNC(uuid) +'/desc')
3511
3512			var blob = new Blob([str], {type: "text/xml;charset=utf-8"}); // application/x-java-jnlp-file
3513			saveAs(blob, "admin" + uuidNC(uuid) + ".jnlp");
3514		})
3515		.fail(function(jqXHR, textStatus, errorThrown) {
3516			errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
3517		})
3518		.done();
3519	}
3520	else {
3521		errorMessageDialog('Feature not supported on this browser', 'Not supported')
3522		.then(function() {
3523			$(this).dialog('close');
3524		})
3525	}
3526}
3527*/
3528
3529
3530// **********************************************************************
3531// ***************************** Tables ********************************
3532// **********************************************************************
3533
3534$.widget("custom.toolMenu", $.ui.menu, {
3535//	delay:2000,
3536
3537	// This is a fix to jQuery UI menu. When hovering main item, then going to a submenu item may not highlight the submenu item.
3538	// There is an extra event being fired with currentTarget tied to the parent menu item. It has to be skipped.
3539
3540	_create: function() {
3541		this._on({
3542			"mouseenter .ui-menu-item": function( event ) {
3543				if (event.target.id > event.currentTarget.id) {
3544					event.stopImmediatePropagation()
3545				}
3546			}
3547		});
3548
3549		this._super(); // The _create from the original ui-menu
3550	},
3551
3552	_closeOnDocumentClick: function(event) {
3553		// If we click outside the menu,
3554		if (!($(event.target).hasClass('ui-menu-item') && $(event.target).parent().hasClass('toolMenu'))) {
3555			// Closes the menu (Not good for mobile devices)
3556			$(this.element)
3557			.hide()
3558		}
3559
3560		return false;
3561	},
3562
3563
3564	// Like collapseAll, but with no delay. We use it to start menu with all collapsed.
3565	collapseAllNow: function(event, all) {
3566		// If we were passed an event, look for the submenu that contains the event
3567		var currentMenu = all ? this.element :
3568			$( event && event.target ).closest( this.element.find( ".ui-menu" ) );
3569
3570		// If we found no valid submenu ancestor, use the main menu to close all sub menus anyway
3571		if (!currentMenu.length) {
3572			currentMenu = this.element;
3573		}
3574
3575		this._close(currentMenu);
3576
3577		this.blur(event);
3578		this.activeMenu = currentMenu;
3579	}
3580
3581});
3582
3583
3584var gracePeriodEndDate = new Date ('2018-06-30T00:00:00Z');
3585
3586function isExpired(ipal) {
3587	var nowValue = new Date().valueOf();
3588	var moduleNotSet = ipal.expiry === undefined;
3589	var isPerpetual = ipal.expiry === null;
3590	var isPastExpiry = nowValue >= new Date(ipal.expiry).valueOf();
3591	var isAfterGracePeriod = nowValue > gracePeriodEndDate.valueOf();
3592
3593	return (!ipal || !ipal.active || moduleNotSet || (!isPerpetual && isPastExpiry && isAfterGracePeriod))
3594}
3595
3596function checkIfExpired(isydata) {
3597	var expired = (isydata && isExpired(isydata.ipal));
3598	var message='';
3599
3600	if (expired && serverConfig.ipalEnabled) {
3601		if (isydata.ipal) {
3602			if (!isydata.ipal.active) {
3603				message = 'Access license is not active'
3604			}
3605			else {
3606				message = 'Access license is expired'
3607			}
3608		}
3609		else {
3610			message = 'Access license not found'
3611		}
3612
3613		errorMessageDialog(message, 'Error', 350);
3614		return false;
3615	}
3616	else {
3617		return true;
3618	}
3619}
3620
3621
3622function checkIfOnline(isydata) {
3623	var online = (isydata && isydata.online);
3624
3625	if (!online) {
3626		errorMessageDialog('ISY is not online.', 'Error', 350);
3627		return false;
3628	}
3629	else {
3630		return checkIfAllowed(isydata);
3631	}
3632}
3633
3634function checkIfAllowed(isydata) {
3635	var registered=null;
3636
3637	if (isydata && isydata.domain && isydata.domain[0]) {
3638		registered = isydata.domain[0].registered
3639	}
3640
3641	if (registered==0) {
3642		errorMessageDialog('Access denied.<br>Pending approval by owner.', 'Access denied', 350);
3643		return false;
3644	}
3645	else if (registered==-1) {
3646		errorMessageDialog('Access has been revoked by owner.', 'Access denied', 350);
3647		return false;
3648	}
3649	else {
3650		return true;
3651	}
3652}
3653
3654function isLicenseActive(webStore) {
3655	var licenseActive;
3656
3657	if (!webStore.module) {
3658		licenseActive = false;
3659	}
3660	else {
3661		// Perpetual license is installed
3662		if (webStore.expiry==null) {
3663			licenseActive = true;
3664		}
3665		// Regular license is installed
3666		else if (webStore.expiry) {
3667			var expiryDate = new Date(webStore.expiry);
3668			licenseActive = webStore.module.active && expiryDate.valueOf() > new Date().valueOf();
3669		}
3670	}
3671
3672	return licenseActive;
3673}
3674
3675function isLicenseInstalled(webStore) {
3676	return webStore.module?!!webStore.module.installed:false;
3677}
3678
3679// Open the license window (if fromNewIsy==false)
3680function ipalDialog(uuid, alias, pk, isyDomain, fromNewIsy, online, registered, firmware, redirectUrl) {
3681
3682	if (serverConfig.ipalEnabled) {
3683		ajaxJsonGet('/api/isy/ipal', $.param({id: pk, domain: isyDomain }))
3684		.done(function (info) {
3685			var licenseActive = isLicenseActive(info.webStore);
3686			var licenseInstalled = isLicenseInstalled(info.webStore);
3687			// Open the license Window if
3688			if (!licenseActive || !fromNewIsy || redirectUrl) {
3689				isyAccessLicenseDialog.init()
3690				isyAccessLicenseDialog.setFields({ uuid: uuid, alias: alias, pk: pk, domain: isyDomain, info: info, online:online, registered:registered, redirectUrl:redirectUrl });
3691
3692				if (redirectUrl) {
3693					isyAccessLicenseDialog.setRedirectUrl(redirectUrl)
3694				}
3695
3696				isyAccessLicenseDialog.modal.dialog('open');
3697			} else {
3698				var silentSuccess=true; // No popup if everything is good.
3699				var freeTrialAvailable = (info.webStore.module == null);
3700
3701				accessTroubleshootingMessage({
3702					freeTrialAvailable: freeTrialAvailable,
3703					licenseActive: licenseActive,
3704					licenseInstalled: licenseInstalled,
3705					online: online,
3706					registered: registered,
3707					firmware: firmware
3708				}, silentSuccess)
3709			}
3710		})
3711		.fail(function(jqXHR, textStatus, errorThrown) {
3712			var errorMessage = 'HTTP error ' + jqXHR.status
3713			if (redirectUrl) {
3714				redirectWithError(redirectUrl, errorMessage)
3715			} else {
3716				errorMessageDialog(errorMessage, 'Error', 350)
3717			}
3718		})
3719		.done();
3720	} else {
3721		// Should only be here after having added an ISY
3722		accessTroubleshootingMessage({
3723//			freeTrialAvailable: null, // Not used when licenseActive==true;
3724			licenseActive: true, // Will not show a license warning
3725			licenseInstalled: true, // Will not show a license warning
3726			online: online,
3727			registered: registered,
3728			firmware: firmware
3729		}, silentSuccess)
3730	}
3731}
3732
3733function accessTroubleshooting(uuid, alias, pk, isyDomain, online, registered, firmware) {
3734	var silentSuccess=false;
3735
3736	if (serverConfig.ipalEnabled) {
3737		ajaxJsonGet('/api/isy/ipal', $.param({id: pk, domain: isyDomain }))
3738		.done(function (info) {
3739			var licenseActive = isLicenseActive(info.webStore);
3740			var licenseInstalled = isLicenseInstalled(info.webStore);
3741			var freeTrialAvailable = (info.webStore.module == null);
3742
3743			accessTroubleshootingMessage({
3744				freeTrialAvailable: freeTrialAvailable,
3745				licenseActive: licenseActive,
3746				licenseInstalled: licenseInstalled,
3747				online: online,
3748				registered: registered,
3749				firmware: firmware
3750			}, silentSuccess)
3751		})
3752		.fail(function(jqXHR, textStatus, errorThrown) {
3753			errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
3754		})
3755		.done();
3756	}
3757	else {
3758		accessTroubleshootingMessage({
3759//			freeTrialAvailable: null, // Not used when licenseActive==true;
3760			licenseActive: true, // Will not show a license warning
3761			licenseInstalled: true, // Will not show a license warning
3762			online: online,
3763			registered: registered,
3764			firmware: firmware
3765		}, silentSuccess)
3766	}
3767}
3768
3769function accessTroubleshootingMessage(conn, silentSuccess) {
3770	var message;
3771
3772	var is600AndUp = (conn.firmware && versionCompare(conn.firmware, '6.0.0') >= 0);
3773
3774	var troubleshooting='In order to use your ISY on ISY portal, please make sure to:<br><br><ol>';
3775	var actionFound=false;
3776
3777	if (!conn.licenseActive) {
3778		troubleshooting += '<li>In ISY Portal, click on Select Tool | Information | ISY Portal Access License and buy a license' + (conn.freeTrialAvailable?' or choose Free Trial':'') + '</li>';
3779		actionFound=true;
3780	}
3781
3782	// if (!conn.licenseActive || !conn.licenseInstalled) {
3783	// 	troubleshooting += '<li>In the Admin Console, click on Help | Manage modules to install the license. This will reboot ISY</li>';
3784	// 	actionFound=true;
3785	// }
3786
3787	if (!conn.online || !conn.licenseActive || !conn.licenseInstalled) {
3788		troubleshooting += '<li>Wait for the ISY to come online on ISY portal. In the ISY list, you can click refresh to update the online status</li>'
3789	}
3790
3791	if (conn.registered==-1) {
3792		troubleshooting += is600AndUp ?
3793			'<li>Open eisy-ui at <a href="http://eisy.local/settings/portal">http://eisy.local/settings/portal</a>, and re-approve prove the access request</li>' :
3794			'<li>Access from this account is currently revoked. You will have to remove the ISY from this account and re-add it. Your license will be kept, but your other configuration items such as Alexa spokens will be lost</li>';
3795		actionFound=true;
3796	}
3797
3798	if (conn.registered==0) {
3799		troubleshooting += is600AndUp ?
3800			'<li>Open eisy-ui at <a href="http://eisy.local/settings/portal">http://eisy.local/settings/portal</a>, and approve the access request</li>' :
3801			'<li>In the Admin Console, click on Configuration | Portal tab, and approve the access request for this account</li>';
3802		actionFound=true;
3803	}
3804
3805	troubleshooting += '<li>You are done!</li></ol>';
3806
3807	// If we have errors to handle (other than just not online)
3808	if (actionFound) {
3809		message = troubleshooting;
3810	}
3811	else if (!silentSuccess) {
3812		if (!conn.online) {
3813			message = 'Everything appears to be configured correctly, but your ISY does not appear to be online. ' +
3814				'If it does not come online after 2-3 minutes, you may try the following:<ul>' +
3815				'<li>Reboot your ISY.</li>' +
3816				'<li>Reboot your router.</li>' +
3817				'<li>In ISY Portal, on the ISY tab, click refresh a few times.</li></ul>' +
3818				'If it still does not connect, please open a ticket at:<br> <a href="https://www.universal-devices.com/contact-support/" target="_blank" class="editable_link">https://www.universal-devices.com/contact-support/</a>'
3819		}
3820		else {
3821			message = 'Everything appears to be configured correctly!';
3822		}
3823	}
3824
3825	if (message) {
3826		informationMessageDialog(message, 'Troubleshooting tips', 800);
3827	}
3828}
3829
3830
3831var tableIsys = {
3832	api: null,
3833	currentDomain: null, // Selected Domain
3834	// isRatesAccount: false, // Set at the same time of currentDomain
3835	// isSceAccount: false, // Set at the same time of currentDomain
3836	// isSceAccountFlagEnabled: false, // Set at the same time of currentDomain
3837	showAll: null, // Show sub-accounts as well?
3838	domainIsys: null, // Domain data returned by /isys
3839	isSameDomain: false,
3840	isDomainAdmin: false,
3841	isSuperAdmin: false,
3842	isCorporate: false,
3843	isUtility: false,
3844	isPilot: false,
3845	now: null, // onDataLoad initializes it once per refresh
3846	soon: null, // onDataLoad initializes it once per refresh
3847
3848	// Triggered when json data has loaded (before table draw)
3849	onDataLoad: function (data) {
3850//		console.log('data', data.data.map(function(rec) {
3851//			return rec.uuid
3852//		}).join('\r\n'))
3853		this.domainIsys = data.domain;
3854		this.userDomainChildren = registeredUser.domainInfo.children?registeredUser.domainInfo.children:[];
3855		this.isSuperAdmin = registeredUser.superadmin;
3856		this.isDomainAdmin = (this.isSuperAdmin || registeredUser.domainadmin);
3857		this.isUtility = (this.isSuperAdmin || this.domainIsys.type=='0');
3858		this.isCorporate = (this.isSuperAdmin || this.domainIsys.type=='1');
3859		this.isPilot = pilots.includes(registeredUser.domain);
3860		this.now = new Date().valueOf();
3861		this.soon = new Date();
3862		this.soon.setDate(this.soon.getDate() + accessLicenseExpirationSoonDays); // 30 days from now
3863		this.soon = this.soon.valueOf();
3864	},
3865
3866	completeUI : function() {
3867		var table = this;
3868		table.api.columns.adjust();
3869
3870		$('#isys_table_container').show();
3871		$('#isys_table_container').tooltip();
3872
3873		// Setup the tool button
3874		$('#isystable .toolMenuButton').button({
3875			width:'12em',
3876			label: 'Select tool...',
3877			icons: { secondary: "ui-icon-triangle-1-s" }
3878		})
3879		.unbind('click')
3880		.click(function (event) {
3881			$('#isystable .toolMenu').not($(this).next()).hide(); // Close all other menus
3882			$(this).next()
3883			.toolMenu("collapseAllNow", null, true ) // Start with collapsed menus
3884			.toggle(); // Toggle the active one
3885			event.stopPropagation(); // Otherwise, it won't open
3886		});
3887
3888		// Setup the tool menu underneath the button
3889		$('#isystable .toolMenu').toolMenu({
3890			width:'14em',
3891	//		position: { my: "left top", at: "left+10 bottom" }, // Position of submenu relative to parent
3892			select: function(event, ui) {
3893				var tool = ui.item.attr('value');
3894
3895				var pk = table.api.cell($(this).closest('tr'), 'pk:name').data();
3896				var uuid = table.api.cell($(this).closest('tr'), 'uuid:name').data();
3897				var isyKey = table.api.cell($(this).closest('tr'), 'isyKey:name').data();
3898				var alias = table.api.cell($(this).closest('tr'), 'alias:name').data();
3899				var isyDomain = table.api.cell($(this).closest('tr'), 'isyDomain:name').data();
3900				var registered = table.api.cell($(this).closest('tr'), 'registered:name').data();
3901				var isydata = table.api.row($(this).closest('tr')).data();
3902				var firmware = table.api.cell($(this).closest('tr'), 'firmware:name').data();
3903				var isWs = table.api.cell($(this).closest('tr'), 'isWs:name').data();
3904
3905				currentISY = { alias: alias, uuid: uuid, isyKey: isyKey, domain: isyDomain };
3906				setSessionStorage('udidevice', currentISY);
3907
3908				function checkAmazonEchoLink() {
3909					var noAmazonLink = !registeredUser.amz_email || !registeredUser.amz_email.length; // Careful: At link time, amz_user_id is not set unless we relogin!
3910					var noAmazonLinkMsg =
3911						'Your portal user profile is not linked to an Amazon account. ' +
3912						'This will prevent you from using the Alexa Smart Home skill. To link your account, please go to My profile.';
3913
3914					if (noAmazonLink) {
3915						errorMessageDialog(noAmazonLinkMsg, 'Warning', 600);
3916					}
3917				}
3918
3919				switch(tool) {
3920
3921				case 'openadr':
3922					if (checkIfOnline(isydata) && checkIfExpired(isydata)) {
3923						window.open('openadr.htm', '_blank');
3924					}
3925					break;
3926
3927				case 'udajax':
3928					$(this).hide(); // For some reason, we need this, otherwise the menu item stays open.
3929					if (checkIfOnline(isydata) && checkIfExpired(isydata)) {
3930						launchWebAccess('_blank', 'menubar=1,location=1,resizable=1,scrollbars=1,status=1,toolbar=1');
3931					}
3932					break;
3933
3934				case 'eisyui':
3935					$(this).hide();
3936					if (!isWs) {
3937						errorMessageDialog("Remote eisy-ui require eisy-ui version 1.0.0 or higher.", 'Error', 575)
3938					} else if (checkIfOnline(isydata) && checkIfExpired(isydata)) {
3939						launchEisyui(uuid, isyKey, isydata.eisyui.version);
3940					}
3941					break;
3942				/*
3943                  case 'adminconsole':
3944                    if (checkIfOnline(isydata) && isydata) {
3945                      var firmware = table.api.cell($(this).closest('tr'), 'firmware:name').data();
3946                      startAdminConsole(uuid, firmware);
3947                    }
3948                    break;
3949          */
3950				case 'eventlog':
3951					if (checkIfOnline(isydata) && checkIfExpired(isydata)) {
3952						window.open('eventlog.htm', '_blank');
3953					}
3954					break;
3955
3956				case 'errorlog':
3957					if (checkIfOnline(isydata) && checkIfExpired(isydata)) {
3958						var user = getSessionStorage('udiportal');
3959						var data = getSessionStorage('udidevice');
3960
3961						window.open('errorlog.htm', '_blank');
3962					}
3963					break;
3964
3965				case 'activitylog':
3966					activityLogDialog.setFields({ uuid: uuid, domain: isyDomain, alias: alias });
3967					activityLogDialog.modal.dialog('open');
3968					break;
3969
3970				case 'contact':
3971					var fields = {};
3972					var domainContact;
3973
3974					if (isydata) {
3975						fields.pk = pk;
3976						fields.uuid = uuid;
3977						fields.alias = alias;
3978
3979						if (isydata.domain && isydata.domain[0]) {
3980							if (!isydata.domain[0].contact) {
3981								isydata.domain[0].contact = {}; // If no contact info entered so far, start from blank
3982							}
3983
3984							domainContact = isydata.domain[0].contact;
3985
3986							fields.name = domainContact.name;
3987							fields.phone = domainContact.phone;
3988							fields.email = domainContact.email;
3989							fields.address = domainContact.address;
3990							fields.handle = domainContact;
3991
3992							isyContactDialog.setFields(fields);
3993							isyContactDialog.setDomain(isyDomain); // It will be passed to the API
3994							isyContactDialog.modal.dialog('open');
3995						}
3996					}
3997
3998					break;
3999
4000				case 'note':
4001					if (isydata && isydata.domain && isydata.domain[0]) {
4002						 if (!isydata.domain[0].note) {
4003							isydata.domain[0].note=''
4004						 }
4005
4006						isyNoteDialog.setFields({ pk: pk, note: isydata.domain[0].note, uuid: uuid, alias: alias, handle: isydata });
4007						isyNoteDialog.setDomain(isyDomain); // It will be passed to the API
4008						isyNoteDialog.modal.dialog('open');
4009					}
4010
4011					break;
4012
4013				// case 'firmware':
4014				// 	if (checkIfOnline(isydata) && checkIfExpired(isydata)) {
4015				// 		if (isydata && isydata.isy && isydata.isy.app && isydata.isy.platform) {
4016				// 			firmwareDB.getVersions(isydata.isy.app, isydata.isy.platform)
4017				// 			.then(function (availableVersions) {
4018				// 				// availableVersions.splice(1, 0, { version: "Beta version", description: 'A beta version can be uploaded.<br>Please select a firmware zip file' });
4019				// 				isyFirmwareDialog.setFields({ pk: pk, uuid: uuid, alias: alias, currentVersion: isydata.isy.firmware, availableVersions: availableVersions });
4020				// 				isyFirmwareDialog.setDomain(isyDomain); // It will be passed to the API
4021				// 				isyFirmwareDialog.modal.dialog('open');
4022				// 			})
4023				// 			.done();
4024				// 		}
4025				// 		else {
4026				// 			errorMessageDialog('Database error.<br/>ISY registration data not found.');
4027				// 		}
4028				// 	}
4029				// 	break;
4030
4031				case 'eisy':
4032					if (checkIfOnline(isydata)) {
4033						eisyProvisioningDialog.setFields({
4034							pk: pk,
4035							uuid: uuid,
4036							alias: alias,
4037							domain: isyDomain,
4038							iotProvisioned: isydata.isy.iot.provisioned,
4039							iotProvisioningAvailable: isydata.isy.iot.provisioningAvailable,
4040							iotOnline: isydata.iotOnline,
4041							iotOnlineLastModified: isydata.iotOnlineLastModified
4042						});
4043						eisyProvisioningDialog.modal.dialog('open');
4044				}
4045				break;
4046
4047				case 'reboot':
4048				if (checkIfOnline(isydata)) {
4049					confirmationDialog('ISY will be rebooted.<br/>Are you sure?', 'Reboot ISY?')
4050					.then(function () {
4051						ajaxJsonGet('/api/isy/reboot', $.param({id: pk, domain: isyDomain }))
4052						.done(function () {
4053							tableIsys.reload();
4054						})
4055						.fail(function(jqXHR, textStatus, errorThrown) {
4056							errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350);
4057						})
4058					})
4059					.done();
4060				}
4061				break;
4062
4063				case 'migrate':
4064					ajaxJsonGet('/api/isys', $.param({domain: isyDomain }))
4065					.then(function (result) {
4066						var uuidList = result.data
4067						.filter(function (item) {
4068							return item.uuid !== uuid;
4069						})
4070						.map(function (item) {
4071							return {
4072								label: item.uuid, // Shown in the dropdown list
4073								value: item.uuid, // Sets the input field to this value when selecting from the dropdown
4074								data: item.uuid,
4075								firmware: item.isy && item.isy.firmware,
4076								option: item
4077							}
4078						});
4079
4080						migrateDialog.setFields({pk: pk, uuid: uuid, uuidList: uuidList, domain: isyDomain });
4081						migrateDialog.modal.dialog('open');
4082					})
4083					.fail(function(jqXHR, textStatus, errorThrown) {
4084						errorMessageDialog("Can't retrieve ISY list<br>HTTP error " + jqXHR.status, 'Error', 350)
4085					})
4086					break;
4087
4088				case 'upgrade_packages':
4089					if (checkIfOnline(isydata)) {
4090						confirmationDialog('This will upgrade all of your packages.<br/><br/>Depending on the size of the upgrade, this process may take up to 30 minutes.<br/><br/>Are you sure?', 'Upgrade packages?', 800)
4091						.then(function () {
4092							ajaxJsonGet('/api/eisy/upgrade_packages', $.param({ uuid: uuid }))
4093							.done(function (content, status, jqXHR) {
4094								informationMessageDialog('System upgrade in progress.<br/><br/>please do not power off.')
4095							})
4096							.fail(function(jqXHR, textStatus, errorThrown) {
4097								errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
4098							})
4099							.done();
4100						})
4101						.done();
4102					}
4103					break;
4104
4105
4106					case 'eisyui_settings':
4107						if (checkIfOnline(isydata)) {
4108							ajaxJsonGet('/api/eisy/settings', $.param({ uuid: uuid, domain: isyDomain }))
4109							.then(function (result) {
4110								eisyuiSettingsDialog.setFields({ pk: pk, uuid: uuid, domain: isyDomain, settings: result.data });
4111								eisyuiSettingsDialog.modal.dialog('open');
4112							})
4113						}
4114						break;
4115
4116
4117					case 'isyinfo':
4118					if (checkIfOnline(isydata) && checkIfExpired(isydata)) {
4119						isyInfoDialog.setFields({ uuid: uuid, alias: alias });
4120						isyInfoDialog.modal.dialog('open');
4121
4122						ajaxJsonGet('/api/isy/info', $.param({id: pk, domain: isyDomain }))
4123						.done(function (data) {
4124							isyInfoDialog.setFields({ uuid: uuid, alias: alias, info: data });
4125						})
4126						.fail(function(jqXHR, textStatus, errorThrown) {
4127							if (jqXHR.status==503) {
4128								errorMessageDialog('ISY is not online')
4129							}
4130							else {
4131								errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
4132							}
4133						})
4134						.done();
4135					}
4136					break;
4137
4138				case 'ipal':
4139					ipalDialog(uuid, alias, pk, isyDomain, false, isydata.online, registered, firmware, null);
4140					break;
4141
4142				case 'troubleshoot':
4143					accessTroubleshooting(uuid, alias, pk, isyDomain, isydata.online, registered, firmware);
4144					break;
4145
4146				case 'openadrReg':
4147					if (checkIfOnline(isydata) && checkIfExpired(isydata)) {
4148						loadOpenadrReg(uuid, alias, pk, isyDomain);
4149					}
4150					break;
4151
4152					case 'echo_spoken_nodes':
4153					if (checkIfOnline(isydata) && checkIfExpired(isydata)) {
4154						echoSpokenNodesDialog.setFields({ uuid: uuid, domain: isyDomain, alias: alias }); // , subscriptionEnabled:data.data.state, skillEnabled:data.data.skillEnabled
4155						echoSpokenNodesDialog.modal.dialog('open');
4156					}
4157					break;
4158
4159				case 'gh_spoken_nodes':
4160					if (checkIfOnline(isydata) && checkIfExpired(isydata)) {
4161						ghSpokenNodesDialog.setFields({ uuid: uuid, domain: isyDomain, alias: alias });
4162						ghSpokenNodesDialog.modal.dialog('open');
4163					}
4164					break;
4165
4166				case 'ifttt_in_nodes':
4167					if (checkIfOnline(isydata) && checkIfExpired(isydata)) {
4168						iftttInNodesDialog.setFields({ uuid: uuid, domain: isyDomain, alias: alias });
4169						iftttInNodesDialog.modal.dialog('open');
4170					}
4171					break;
4172
4173					case 'ns_list':
4174						nsListDialog.setFields({ uuid: uuid, domain: isyDomain, alias: alias });
4175						nsListDialog.modal.dialog('open');
4176						break;
4177
4178					case 'ns_reinstall':
4179						if (isydata.iotOnline) {
4180							confirmationDialog('This will update all of your Plugins in PG3x to their latest version.<br/><br/>You can expect the process to take a few minutes.<br/><br/>Are you sure?', 'Reinstall Plugins?', 800)
4181							.then(function () {
4182								ajaxJsonGet('/api/eisy/nodeserver/reinstall', $.param({ uuid: uuid }))
4183								.done(function (data) {
4184									informationMessageDialog('Plugins upgrade in progress.')
4185								})
4186								.fail(function(jqXHR, textStatus, errorThrown) {
4187									errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
4188								})
4189								.done();
4190							})
4191							.done();
4192						} else {
4193							errorMessageDialog('PG3 remote connection is not active')
4194						}
4195						break;
4196
4197					case 'hint_edit':
4198					if (checkIfOnline(isydata) && checkIfExpired(isydata)) {
4199						ajaxJsonGet('/api/nodes/lookup', $.param({ uuid: uuid, domain: isyDomain }))
4200						.done(function(content, status, jqXHR){
4201							content.data.devices.map(function (item) {
4202								var path = item.path.map(function(p) { return p.name })
4203								.concat([ item.name ])
4204								.join(' / ');
4205
4206								item.data = item.address;
4207								item.value = item.label = path + ' (' + item.address.replace(/ /g, '.') + ')';
4208								item.option = item; // Self reference;
4209							});
4210
4211							content.data.devices.sort(function(a, b) {
4212								return a.value.localeCompare(b.value) }
4213							);
4214
4215							hintEditorDialog.setFields({ pk: pk, uuid: uuid, domain: isyDomain, lookup: content.data.devices });
4216							hintEditorDialog.modal.dialog('open');
4217						})
4218						.fail(function(jqXHR, textStatus, errorThrown) {
4219							errorMessageDialog("Devices are not available", 'Error', 350)
4220						})
4221						.done();
4222					}
4223					break;
4224
4225				case 'nsconfig':
4226					if (checkIfOnline(isydata) && checkIfExpired(isydata)) {
4227						if (minimumVersionCheck(isydata.isy.firmware, nodeServerMinimumFirmware)) {
4228							nsConfigDialog.setFields({ uuid: uuid, alias: alias, pk: pk, isyDomain: isyDomain });
4229							nsConfigDialog.modal.dialog('open');
4230							nsConfigDialog.refresh();
4231						}
4232					}
4233					break;
4234
4235				case 'nsoccupancy':
4236					if (checkIfOnline(isydata) && checkIfExpired(isydata)) {
4237						if (minimumVersionCheck(isydata.isy.firmware, nodeServerMinimumFirmware)) {
4238							nsOccupancyDialog.setFields({ uuid: uuid, domain: isyDomain, alias: alias });
4239							nsOccupancyDialog.modal.dialog('open');
4240						}
4241					}
4242					break;
4243
4244					case 'nsoccupancy2':
4245					if (checkIfOnline(isydata) && checkIfExpired(isydata)) {
4246                        if (minimumVersionCheck(isydata.isy.firmware, nodeServerMinimumFirmware)) {
4247                            nsOccupancy2Dialog.setFields({ uuid: uuid, domain: isyDomain, alias: alias });
4248                            nsOccupancy2Dialog.modal.dialog('open');
4249						}
4250					}
4251					break;
4252
4253				case 'nssolaredge':
4254					if (checkIfOnline(isydata) && checkIfExpired(isydata)) {
4255						if (minimumVersionCheck(isydata.isy.firmware, nodeServerMinimumFirmware)) {
4256							nsSolaredgeDialog.setFields({ uuid: uuid, domain: isyDomain, alias: alias });
4257							nsSolaredgeDialog.modal.dialog('open');
4258						}
4259					}
4260					break;
4261
4262				default:
4263					// RATES
4264					// if (ratesMenuOptions) {
4265					// 	ratesMenuOptions({
4266					// 			tool: tool,
4267					// 			isydata: isydata,
4268					// 			uuid: uuid,
4269					// 			alias: alias,
4270					// 			pk: pk,
4271					// 			isyDomain: isyDomain
4272					// 		})
4273					// }
4274
4275					// SCE Pilot project
4276					// if (sceMenuOptions) {
4277					// 	sceMenuOptions({
4278					// 		tool: tool,
4279					// 		isydata: isydata,
4280					// 		uuid: uuid,
4281					// 		alias: alias,
4282					// 		pk: pk,
4283					// 		isyDomain: isyDomain
4284					// 	})
4285					// }
4286				}
4287			}
4288		})
4289		.hide()
4290		.addClass('ui-front');
4291
4292		// Only a domain admin or superadmin can delete ISY's
4293		if (this.isDomainAdmin) {
4294			$('#isys_table_container .delbutton').unbind('click').click(function (e) {
4295				var pk = table.api.cell($(this).closest('tr'), 'pk:name').data();
4296				var isyDomain = table.api.cell($(this).closest('tr'), 'isyDomain:name').data();
4297
4298				confirmationDialog('ISY will be removed from account.<br/>Are you sure?', 'Remove ISY?')
4299				.then(function () {
4300					ajaxJsonDelete('/api/isy/domain', $.param({id: pk, domain: isyDomain }))
4301					.done(function () {
4302						tableIsys.reload();
4303					})
4304				})
4305				.done();
4306			});
4307		}
4308
4309		// Edit existing ISY
4310		$('#isys_table_container .alias').click(function () {
4311			var pk = table.api.cell($(this).closest('tr'), 'pk:name').data();
4312			var uuid = table.api.cell($(this).closest('tr'), 'uuid:name').data();
4313			var alias = table.api.cell($(this).closest('tr'), 'alias:name').data();
4314			var isyDomain = table.api.cell($(this).closest('tr'), 'isyDomain:name').data();
4315
4316			isyClaimDialog.setFields({ pk: pk, uuid: uuid, alias: alias });
4317			isyClaimDialog.setDomain(isyDomain);	// This will allow to refresh the list with the same domain as now
4318			isyClaimDialog.modal.dialog('open');
4319		});
4320
4321		// Shortcut to open the license window
4322		$('#isys_table_container .expired, #isys_table_container .warning').click(function () {
4323			var pk = table.api.cell($(this).closest('tr'), 'pk:name').data();
4324			var uuid = table.api.cell($(this).closest('tr'), 'uuid:name').data();
4325			var alias = table.api.cell($(this).closest('tr'), 'alias:name').data();
4326			var isyDomain = table.api.cell($(this).closest('tr'), 'isyDomain:name').data();
4327			var registered = table.api.cell($(this).closest('tr'), 'registered:name').data();
4328			var isydata = table.api.row($(this).closest('tr')).data();
4329			var firmware = table.api.cell($(this).closest('tr'), 'firmware:name').data();
4330
4331			ipalDialog(uuid, alias, pk, isyDomain, false, isydata.online, registered, firmware, null)
4332		});
4333	},
4334
4335	// Hidden data used for sorting the online indicator
4336	dataOnline: function (row, type, set, meta) {
4337		return (typeof row.online !== 'undefined')?(row.online?1:0):-1;
4338	},
4339
4340	renderUUIDwithDelete: function (data, type, row, meta) {
4341		var table = tableIsys;
4342		var licenseIndicator = '';
4343
4344		if (serverConfig.ipalEnabled) {
4345			if (row.ipal) {
4346				if (!row.ipal.active) {
4347					licenseIndicator = '<span class="expired" title="Access license is not active"><span class="ui-icon ui-icon-alert" style="display:inline-block;width:16px"></span></span>'
4348				}
4349				else if (row.ipal.expiry) {
4350					var expDt = new Date(row.ipal.expiry).valueOf();
4351
4352					if (expDt < table.now) {
4353						licenseIndicator = '<span class="expired" title="Access license is expired"><span class="ui-icon ui-icon-alert" style="display:inline-block;width:16px"></span></span>'
4354					}
4355					else if (expDt < table.soon) {
4356						licenseIndicator = '<span class="warning" title="Access license is expiring soon"><span class="ui-icon ui-icon-alert" style="display:inline-block;width:16px"></span></span>'
4357					}
4358				}
4359			}
4360			else {
4361				licenseIndicator = '<span class="expired" title="Access license not found"><span class="ui-icon ui-icon-alert" style="display:inline-block;width:16px"></span></span>'
4362			}
4363		}
4364
4365		return 	(registeredUser && registeredUser.domainadmin?'<span class="delbutton"><span class="ui-icon ui-icon-closethick" style="display:inline-block;width:16px"></span></span>':'') +
4366			'<span>' + data +
4367			// (tableIsys.isRatesAccount && row.facilityId?' / ' + row.facilityId:'') +
4368			// (tableIsys.isSceAccountFlagEnabled && !row.sceIsActivated?'<span title="SCE Pilot is not activated" class="ui-icon ui-icon-flag" style="display:inline-block;width:16px"></span>':'') +
4369			licenseIndicator +
4370			'</span>'
4371	},
4372
4373	renderOnlineIndicator: function (data, type, row) {
4374		if (row.online) {
4375			if (row.domain[0].registered==-1) {
4376				return '<div class="isystatus revoked" title="Access revoked by Owner.\nOnline since ' + dateFormatNoError(row.lastModified, "dddd, mmmm dS, h:MM:ss TT") +
4377				'"><span style="display: none;">' + data + '</span></div>';
4378			}
4379			else if (row.domain[0].registered==0) {
4380				return '<div class="isystatus pending" title="Pending approval by owner.\nOnline since ' + dateFormatNoError(row.lastModified, "dddd, mmmm dS, h:MM:ss TT") +
4381				'"><span style="display: none;">' + data + '</span></div>';
4382			}
4383			else {
4384				return '<div class="isystatus online" title="Online since ' + dateFormatNoError(row.lastModified, "dddd, mmmm dS, h:MM:ss TT") +
4385				'"><span style="display: none;">' + data + '</span></div>';
4386			}
4387		}
4388		else {
4389			return '<div class="isystatus offline" title="' + (row.lastModified?('Offline since ') + dateFormatNoError(row.lastModified, "dddd, mmmm dS, h:MM:ss TT"):'ISY never connected') +
4390			'"><span style="display: none;">' + data + '</span></div>';
4391		}
4392	},
4393
4394	renderAlias: function (data, type, row) {
4395		// row.domain[0].id + '/'+  (To display domain)
4396		return '<span class="alias editable_link">' + (data && data.length?data:"\[Empty\]") + '</span>';
4397	},
4398
4399	renderTools: function (data, type, row, table) {
4400		//	if ISY domain == user domain
4401		var isSameDomain = (registeredUser && (row.domain[0].id === registeredUser.domain));
4402		//	if ISY domain == user domain OR ISY domain is part of the children of the user domain
4403		var isSameOrParentDomain = (isSameDomain || this.userDomainChildren.includes(row.domain[0].id));
4404		var isIotAvailable = row.isy && row.isy.iot && row.isy.iot.provisioningAvailable
4405		var isPolisyOrEisy = getIsPolisyOrEisy(row.isy && row.isy.product && row.isy.product.id)
4406		var isEisyUi = !!(row.isy && row.isy.multiConnectionSupport)
4407		var isWs = row.isWs
4408
4409//		var isRates = isRatesAccount(row.domain[0].id)
4410// 		var isRates = registeredUser && isRatesAccount(registeredUser.domainInfo); // We check the domain of the user, not the domain of the ISY!! We we also check that the ISY is from the same domain of the user.
4411// 		var isSce = registeredUser && isSceAccount(registeredUser.domainInfo); // We check the domain of the user, not the domain of the ISY!! We we also check that the ISY is from the same domain of the user.
4412
4413//		console.log('Rights', isSameOrParentDomain, this.isDomainAdmin, this.isRates)
4414		return	'<div><button class="toolMenuButton"></button>' +
4415			'<ul class="toolMenu">' +
4416			(isSameDomain && this.isCorporate?'<li value="udajax">ISY Web Access</li>':'') +
4417			(isSameDomain && this.isCorporate && isWs ?'<li value="eisyui">eisy-ui</li>':'') +
4418			(((isSameOrParentDomain || this.isSuperAdmin) && this.isDomainAdmin && this.isCorporate)?
4419			'<li>Connectivity<ul>' +
4420				'<li value="echo_spoken_nodes">Amazon Echo</li>' +
4421				'<li value="gh_spoken_nodes">Google Home</li>' +
4422				'<li value="ifttt_in_nodes">IFTTT / Webhooks</li>' +
4423				'<li value="hint_edit">Device Hint Editor</li>' +
4424				'</ul></li>':'') +
4425			(((isSameOrParentDomain || this.isSuperAdmin) && this.isDomainAdmin && this.isCorporate)?
4426				'<li>PG3<ul>' +
4427				'<li value="ns_list">Purchased Plugins</li>' +
4428				(isIotAvailable ? '<li value="eisy">Remote Connection</li>' : '') +
4429				(isIotAvailable ? '<li value="ns_reinstall">Reinstall all Plugins</li>' : '') +
4430				'</ul></li>':'') +
4431			// RATES
4432			// ((isSameOrParentDomain && this.isDomainAdmin && isRates)?
4433			// '<li>RATES<ul>' +
4434			// 	'<li value="ratesTemixAccountLinking">Temix Account Link</li>' +
4435			// 	'<li value="ratesConfiguration">Facility Configuration</li>' +
4436			// 	'<li value="ratesDevices">Devices Configuration</li>' +
4437			// 	'<li value="ratesDataQuery">Reports</li>' +
4438			// 	'</ul></li>':'') +
4439			// SCE
4440			// ((isSameOrParentDomain && this.isDomainAdmin && isSce)?
4441			// 	'<li>Energy Expert<ul>' +
4442			// 	'<li value="sceAccountActivation">Energy Expert Activation</li>' +
4443			// 	'<li value="sceConfiguration">Facility Configuration</li>' +
4444			// 	'<li value="sceDevices">Devices Configuration</li>' +
4445			// 	// '<li value="sceDataQuery">Reports</li>' +
4446			// 	'</ul></li>':'') +
4447			((isSameOrParentDomain && this.isDomainAdmin && this.isCorporate)?  //  && this.isPilot
4448			'<li>Occupancy Node Server<ul>' +
4449				'<li value="nsconfig">Configuration</li>' +
4450				'<li value="nsoccupancy">Occupancy</li>' +
4451				'<li value="nsoccupancy2">Occupancy v2.0 (with UDI Mobile)</li>' +
4452//					'<li value="nssolaredge">Solaredge</li>'
4453				'</ul></li>':'') +
4454			(isSameOrParentDomain && this.isUtility && row.isy && row.isy.features && row.isy.features.oadr?
4455			'<li>OpenADR<ul>' +
4456				(isSameDomain?'<li value="openadr">OpenADR Events</li>':'') +
4457				'<li value="openadrReg">OpenADR Registration</li>' +
4458				'</ul></li>':'') +
4459
4460			((isSameOrParentDomain || this.isSuperAdmin)?
4461			'<li>Information<ul>' +
4462				'<li value="isyinfo">ISY Information</li>' +
4463				(this.isDomainAdmin?
4464					(serverConfig.ipalEnabled?'<li value="ipal">ISY Portal Access License</li>':'') +
4465					'<li value="troubleshoot">ISY Portal Access Troubleshooting</li>':'') +
4466				'<li value="contact">Contact Information</li>' +
4467				'<li value="note">Notes</li></ul></li>':'') +
4468
4469			(((isSameOrParentDomain && this.isDomainAdmin) || this.isSuperAdmin)?
4470			'<li>Logs<ul>' +
4471				'<li value="errorlog">Error Log</li>' +
4472				(isSameDomain?'<li value="eventlog">Event Log</li>':'') +
4473				// (isRates?'<li value="activitylog">
4473Activity Log</li>':'') +
4474				'</ul>':'') +
4475
4476			(this.isDomainAdmin?
4477			'<li>Maintenance<ul>' +
4478/*				'<li value="firmware">Firmware Update</li>' +*/
4479				'<li value="reboot">Reboot</li>' +
4480				'<li value="migrate">Migrate to</li>' +
4481				((isSameDomain && isPolisyOrEisy) ? '<li value="upgrade_packages">Upgrade Packages</li>' : '') +
4482				((isEisyUi && this.isSuperAdmin) ? '<li value="eisyui_settings">eisy-ui settings</li>' : '') +
4483			'</ul></li>':'') +
4484			'</div>'
4485	},
4486
4487	renderIfPresent: function (data) {
4488		return data?data:'';
4489	},
4490
4491	load: function () {
4492		var table = this;
4493
4494		if (!this.api) {
4495			this.api = $('#isystable').DataTable({
4496				"language": { "emptyTable": "Press Add ISY to start" },
4497				"ajax": function (data, callback, settings) {
4498					// For RATES account, we need the facilityId as well, so we call a different API specific to RATES on the RATES server
4499					var url;
4500					// if (table.isRatesAccount) {
4501					// 	url = '/api/rates/isys'
4502					// } else
4503					// if (table.isSceAccount) {
4504					// 	url = '/api/sce1/isys'
4505					// }	else {
4506						url = '/api/isys';
4507					// }
4508					ajaxJsonGet(url, $.param({domain: table.currentDomain, subaccounts: (table.showAll?'true':'false')}))
4509					.done(function(data, status, jqXHR){
4510						table.onDataLoad(data);
4511						callback(data);
4512					});
4513				},
4514				"columns": [
4515					{ data: "uuid", render: this.renderUUIDwithDelete, className: "x_uuid", name: "uuid" },
4516					{ data: this.dataOnline, render: this.renderOnlineIndicator, searchable: false, className: "isystatusholder" },
4517					{ data: "domain[0].alias", render: this.renderAlias, name: "alias", defaultContent: '' },
4518					{ data: "isy.firmware", defaultContent: '', name: 'firmware' },
4519					{ render: function (d, t, r) { return table.renderTools(d, t, r, table) }, orderable: false, searchable: false },
4520					{ data: "_id", visible: false, searchable: false, name: 'pk' },
4521					{ data: "domain[0].registered", visible: false, searchable: false, name: 'registered', defaultContent: '0' },
4522					{ data: "domain[0].id", visible: false, searchable: false, name: 'isyDomain', defaultContent: {} },
4523					{ data: "isyKey", visible: false, searchable: false, name: "isyKey", defaultContent: '' },
4524					{ data: "ipal", visible: false, searchable: false, defaultContent: {} },
4525					{ data: "isy.iot", visible: false, searchable: false, defaultContent: {}, name: "iot" },
4526					{ data: "isWs", visible: false, searchable: false, defaultContent: false, name: "isWs" },
4527				],
4528				"drawCallback": function (settings, json) {
4529					// API might not be initialized yet, page may display 'Loading...'
4530					if (table.api) {
4531						table.completeUI();
4532					}
4533				}
4534			});
4535		}
4536	},
4537
4538	reload: function () {
4539		var table = this;
4540
4541		if (!table.api) {
4542			table.load();
4543		}
4544		else {
4545			table.api.page('first');
4546			table.api.ajax.reload(null, false);
4547		}
4548	},
4549
4550	setDomain: function (domain, showAll) {
4551		// this.isRatesAccount = isRatesAccount(domain);
4552		// this.isSceAccount = isSceAccount(domain);
4553
4554		// The account SCE enabled flag is enabled only for SCE account, except if we are in prod && admin account
4555		// this.isSceAccountFlagEnabled = this.isSceAccount && !(location.hostname === 'my.isy.io' && domain === '$superadmin');
4556
4557		if (this.currentDomain!=domain || this.showAll!=showAll) {
4558			this.currentDomain = domain;
4559			this.showAll = showAll;
4560			this.reload();
4561		}
4562
4563		// if (this.isSceAccount) {
4564		// 	$('#scereports').show()
4565		// } else {
4566		// 	$('#scereports').hide()
4567		// }
4568	},
4569
4570	clear: function () {
4571		this.currentDomain = '';
4572		$('#isys_table_container').hide();
4573	}
4574};
4575
4576var tableUsers = {
4577	api: null,
4578	currentDomain: null,
4579	showAll: null, // Show sub-accounts as well?
4580
4581	completeUI : function() {
4582		var table = this;
4583		table.api.columns.adjust();
4584
4585		$('#users_table_container').show();
4586		$('#users_table_container').tooltip();
4587
4588		switch (this.currentDomain) {
4589			case '$superadmin' :
4590				this.api.column("domainadmin:name").visible(false);
4591				this.api.column("passwordchange:name").visible(true);
4592				this.api.column("uuid:name").visible(false);
4593				break;
4594
4595			default :
4596				this.api.column("domainadmin:name").visible(true);
4597				this.api.column("passwordchange:name").visible(true);
4598				this.api.column("uuid:name").visible(false);
4599				break;
4600		}
4601
4602		// Delete existing user
4603		$('#userstable .delbutton').unbind('click').click(function () {
4604			var thisbutton = this;
4605			var pk = table.api.cell($(this).closest('tr'), 'username:name').data();
4606
4607			// We are removing ourself
4608			if (pk === registeredUser.username) {
4609				confirmationDialog('YOU ARE REMOVING YOUR OWN USER PROFILE. ARE YOU SURE?', 'Remove YOURSELF?')
4610				.then(function () {
4611					ajaxJsonDelete('/api/user', $.param({username: pk, domain: table.currentDomain}))
4612					.done(function () {
4613						table.reload();
4614					})
4615				})
4616				.done();
4617			}
4618			else {
4619				confirmationDialog('User will be removed.<br>Are you sure?', 'Remove user?')
4620				.then(function () {
4621					ajaxJsonDelete('/api/user', $.param({username: pk, domain: table.currentDomain}))
4622					.done(function () {
4623						table.reload();
4624					})
4625				})
4626				.done();
4627			}
4628		});
4629
4630		// Edit existing user
4631		$('#userstable .username').click(function () {
4632			var username = table.api.cell($(this).closest('tr'), 'username:name').data();
4633			var pk = username;
4634			var password = '$unchanged';
4635			var domainadmin = table.api.cell($(this).closest('tr'), 'domainadmin:name').data();
4636			var userDomain = table.api.cell($(this).closest('tr'), 'domain:name').data();
4637			var passwordchange = table.api.cell($(this).closest('tr'), 'passwordchange:name').data();
4638			var isDeveloper = table.api.cell($(this).closest('tr'), 'isDeveloper:name').data();
4639			var isReviewer = table.api.cell($(this).closest('tr'), 'isReviewer:name').data();
4640			var isEEAdmin = table.api.cell($(this).closest('tr'), 'isEEAdmin:name').data();
4641			var notificationmaint = table.api.cell($(this).closest('tr'), 'notificationmaint:name').data();
4642			var notificationnews = table.api.cell($(this).closest('tr'), 'notificationnews:name').data();
4643			var isy = table.api.cell($(this).closest('tr'), 'isy:name').data();
4644
4645			var data = { pk: pk, username: username, password:password, domainadmin:domainadmin, passwordchange:passwordchange, isDeveloper: isDeveloper, isReviewer: isReviewer, isEEAdmin: isEEAdmin, notificationmaint:notificationmaint, notificationnews:notificationnews, isy:isy };
4646
4647			userDialog.setFields(data);
4648			userDialog.setDomain(userDomain);
4649			userDialog.modal.dialog('open');
4650		});
4651	},
4652
4653	renderUserName: function (data, type, row, meta) {
4654		return 	'<span class="delbutton"><span class="ui-icon ui-icon-closethick"></span></span>' +
4655                '<span class="username editable_link">' + data + '</span>' +
4656				(row.emailVerified?'':' <span class="warning" title="email is not verified"><span class="ui-icon ui-icon-alert"></span></span>');
4657	},
4658
4659	renderDomainAdmin: function (data, type, row, meta) {
4660		return (row.domainadmin)?'Yes':'';
4661	},
4662
4663	renderPasswordChange: function (data, type, row, meta) {
4664		return (row.passwordchange)?'Yes':'';
4665	},
4666
4667	renderIfPresent: function (data) {
4668		return data?data:'';
4669	},
4670
4671	load: function () {
4672		var table = this;
4673
4674		if (!this.api) {
4675			this.api = $('#userstable').DataTable({
4676				"ajax": function (data, callback, settings) {
4677					ajaxJsonGet('/api/users', $.param({domain: table.currentDomain, subaccounts: (table.showAll?'true':'false') }))
4678					.done(function(data, status, jqXHR){
4679						callback(data);
4680					});
4681				},
4682				"columns": [
4683					{ data: "username", render: this.renderUserName, name: "username" },
4684					{ data: "domainadmin", defaultContent: false, render: this.renderDomainAdmin, name: "domainadmin" },
4685					{ data: "passwordchange", defaultContent: false, render: this.renderPasswordChange, name: "passwordchange" },
4686					{ data: "isy", defaultContent:"", visible: true, name: "isy" },
4687					{ data: "emailVerified", defaultContent: false, visible:false, name: "emailVerified" },
4688					{ data: "domain", defaultContent:"", visible: false, name: "domain" },
4689					{ data: "isDeveloper", defaultContent: false, visible:false, name: "isDeveloper" },
4690					{ data: "isReviewer", defaultContent: false, visible:false, name: "isReviewer" },
4691					{ data: "isEEAdmin", defaultContent: false, visible:false, name: "isEEAdmin" },
4692					{ data: "notificationmaint", defaultContent: true, visible:false, name: "notificationmaint" },
4693					{ data: "notificationnews", defaultContent: true, visible:false, name: "notificationnews" }
4694				],
4695				"drawCallback": function (settings, json) {
4696					// API might not be initialized yet, page may display 'Loading...'
4697					if (table.api) {
4698						table.completeUI();
4699					}
4700				}
4701			});
4702		}
4703	},
4704
4705	reload: function () {
4706		var table = this;
4707
4708		if (!table.api) {
4709			table.load();
4710		}
4711		else {
4712			this.api.page('first');
4713			table.api.ajax.reload(null, false);
4714		}
4715	},
4716
4717	setDomain: function (domain, showAll) {
4718		if (this.currentDomain!==domain || this.showAll!==showAll) {
4719			this.currentDomain = domain;
4720			this.showAll = showAll;
4721			this.reload();
4722		}
4723	},
4724
4725	clear: function () {
4726		this.currentDomain = '';
4727		$('#users_table_container').hide();
4728	}
4729};
4730
4731var tableDomains = {
4732	api: null,
4733	currentDomain: null, // Contains the registeredUser's domain info (registeredUser.domainInfo)
4734	isUDI: false,
4735
4736	// Triggered when json data has loaded (before table draw). We do this to update the selectors if domains have changed.
4737	onDataLoad: function (data) {
4738		updateDomainSelectors(this.currentDomain)
4739	},
4740
4741	completeUI : function() {
4742		var table = this;
4743		table.api.columns.adjust();
4744
4745		// Delete existing domain
4746		$('#domainstable .delbutton').unbind('click').click(function () {
4747			var pk = table.api.cell($(this).closest('tr'), 'pk:name').data();
4748
4749			confirmationDialog('Account will be removed.<br>Are you sure?', 'Remove account?')
4750			.then(function () {
4751				ajaxJsonDelete('/api/domain', '_id='+pk)
4752				.done(function () {
4753					table.reload();
4754				})
4755				.fail(function(jqXHR, textStatus, errorThrown) {
4756					if (jqXHR.status==425) { // Validation error message
4757						errorMessageDialog(jqXHR.responseText, 'Error', 350);
4758					}
4759					else {
4760						errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350);
4761					}
4762				})
4763				.done();
4764			})
4765			.done();
4766		});
4767
4768		// Edit existing domain
4769		$('#domainstable .shortname').click(function () {
4770			var pk = table.api.cell($(this).closest('tr'), 'pk:name').data();
4771			var shortname = table.api.cell($(this).closest('tr'), 'shortname:name').data();
4772			var description = table.api.cell($(this).closest('tr'), 'desc:name').data();
4773			var type = table.api.cell($(this).closest('tr'), 'type:name').data(); // Raw type: '0' or '1'
4774			var website = table.api.cell($(this).closest('tr'), 'website:name').data();
4775			var address = table.api.cell($(this).closest('tr'), 'address:name').data();
4776			var contactname = table.api.cell($(this).closest('tr'), 'contactname:name').data();
4777			var contactphone = table.api.cell($(this).closest('tr'), 'contactphone:name').data();
4778//			var contactemail = table.api.cell($(this).closest('tr'), 'contactemail:name').data();
4779			var parent = table.api.cell($(this).closest('tr'), 'parent:name').data();
4780			var parentname = table.api.cell($(this).closest('tr'), 'parentname:name').data();
4781			var billingroot = table.api.cell($(this).closest('tr'), 'billingroot:name').data();
4782			var billingsub = table.api.cell($(this).closest('tr'), 'billingsub:name').data();
4783			var consent = table.api.cell($(this).closest('tr'), 'consent:name').data();
4784
4785			//parentList
4786			//parent
4787			domainDialog.setFields({ editMode: true, isUDI: table.isUDI, pk: pk, sub: (parent!=''),
4788								   parent: parent, parentname: parentname, shortname: shortname, description:description, type: type, address: address, website:website,
4789								   contactname: contactname, contactphone: contactphone, billingroot: billingroot, billingsub: billingsub, consent: consent/*, contactemail: contactemail*/ });
4790			domainDialog.modal.dialog('open');
4791		});
4792	},
4793
4794	dataDomainType: function (row, type, set, meta) {
4795		if (row.parent && row.parent!=='') {
4796			return 'Sub-Account';
4797		}
4798		else {
4799			return row.type=="0"?"Utility":row.type=="1"?"Corporate":"Other";
4800		}
4801	},
4802
4803	renderDomainName: function (data, type, row, meta) {
4804		var domainDisplayed;
4805
4806		// 'this' is set to the global namespace ??? Need to refer to tableDomains...
4807		if (tableDomains.isUDI) {
4808			domainDisplayed = (row.parentname?row.parentname+'/':'') + data;
4809		}
4810		else {
4811			// This is the main account
4812			if (!row.parent || row.parent.length===0) {
4813				domainDisplayed = '[Main account]';
4814			}
4815			else {
4816				domainDisplayed = data;
4817			}
4818		}
4819
4820		// If UDI admin, Delete button is deplayed on all accounts/subaccount
4821		// If not UDI admin, Delete button is deplayed on sub accounts only
4822		return ((tableDomains.isUDI||(row.parent && row.parent.length>0))?'<span class="delbutton"><span class="ui-icon ui-icon-closethick"></span></span>' : '<span class="nodelbutton"></span>') +
4823				'<span class="shortname editable_link">' + domainDisplayed + '</span>';
4824	},
4825	load: function () {
4826		var table = this;
4827
4828		if (!this.api) {
4829			this.api = $('#domainstable').DataTable({
4830				"ajax": function (data, callback, settings) {
4831					ajaxJsonGet('/api/domains')
4832					.done(function(data, status, jqXHR){
4833						table.onDataLoad(data);
4834						callback(data);
4835					});
4836				},
4837				"columns": [
4838					{ data: "shortname", render: this.renderDomainName, name: 'shortname' },
4839					{ data: "description", name: 'desc', defaultContent: "" },
4840					{ data: this.dataDomainType, defaultContent: "" },
4841					{ data: "_id", visible: false, name: 'pk' },
4842					{ data: "type", visible: false, name: 'type', defaultContent: "" },
4843					{ data: "website", visible: false, name: 'website', defaultContent: "" },
4844					{ data: "address", visible: false, name: 'address', defaultContent: "" },
4845					{ data: "contactname", visible: false, name: 'contactname', defaultContent: "" },
4846					{ data: "contactphone", visible: false, name: 'contactphone', defaultContent: "" },
4847//					{ data: "contactemail", visible: false, name: 'contactemail', defaultContent: "" },
4848					{ data: "parent", visible: false, name: 'parent', defaultContent: "" },
4849					{ data: "parentname", visible: false, name: 'parentname', defaultContent: "" },
4850					{ data: "billingroot", visible: false, name: 'billingroot', defaultContent: true },
4851					{ data: "billingsub", visible: false, name: 'billingsub', defaultContent: false },
4852					{ data: "consent", visible: false, name: 'consent', defaultContent: false },
4853				],
4854				"drawCallback": function (settings, json) {
4855					// API might not be initialized yet, page may display 'Loading...'
4856					if (table.api) {
4857						table.completeUI();
4858					}
4859				}
4860			});
4861		}
4862	},
4863
4864	reload: function (forceReload) {
4865		var table = this;
4866
4867		if (!table.api) {
4868			table.load();
4869		}
4870		else {
4871			table.api.ajax.reload(null, false);
4872		}
4873	},
4874
4875	setDomain: function (domainInfo, forceReload) {
4876		if (!this.currentDomain || this.currentDomain._id !== domainInfo._id) {
4877			this.currentDomain = domainInfo;
4878
4879			this.isUDI = (this.currentDomain._id === '$superadmin');
4880
4881			this.reload();
4882
4883			// Type column is only visible if UDI admin
4884			this.api.column(2).visible(this.isUDI);
4885		}
4886		else if (forceReload) {
4887			this.reload(forceReload);
4888		}
4889	},
4890};
4891
4892var tableConnectedIsys = {
4893	api: null,
4894
4895	// Hidden data used for sorting the online indicator
4896	dataOnline: function (row, type, set, meta) {
4897		return (typeof row.online !== 'undefined')?(row.online?1:0):-1;
4898	},
4899
4900	renderOnlineIndicator: function (data, type, row) {
4901		return '<div class="isystatus ' + (row.online?'online':'offline') + '" title="' + (row.lastModified?(row.online?'Online since ':'Offline since ') + dateFormatNoError(row.lastModified, "dddd, mmmm dS, yyyy, h:MM:ss TT"):'ISY never connected') + '"><span style="display: none;">' + data + '</span></div>';
4902	},
4903
4904	renderIfOnline: function (data, type, row, meta) {
4905		return (row.online && data)?data:'';
4906	},
4907
4908	// Data will contain the first domain, a separator, then then all the list
4909	dataDomain: function (row, type, set, meta) {
4910		var str='';
4911
4912		if (row.domain && row.domain.length>=1) {
4913			str = row.domain[0].shortname;
4914
4915			if (row.domain.length>1) {
4916				str += '|' + row.domain[0].shortname;
4917
4918				for (var i = 1; i < row.domain.length; i++) {
4919					str += ', ' + row.domain[i].shortname;
4920				}
4921			}
4922		}
4923
4924		return str;
4925	},
4926
4927	renderDomain: function (data, type, row, meta) {
4928		var renderStr='';
4929		var dd=data.split('|');
4930
4931		if (dd.length===1) {
4932			renderStr = dd[0];
4933		}
4934		else if (dd.length===2) {
4935			renderStr = dd[0] + ', <span title="' + dd[1] + '">[More...]</span>'
4936		}
4937
4938		return renderStr;
4939	},
4940
4941	completeUI: function () {
4942		$('#isysconnectedtable').tooltip();
4943	},
4944
4945	load: function () {
4946		var table = this;
4947		if (!this.api) {
4948			this.api = $('#isysconnectedtable').DataTable({
4949				"ajax": function (data, callback, settings) {
4950					ajaxJsonGet('/api/isys/all')
4951					.done(function(data, status, jqXHR){
4952						callback(data);
4953					});
4954				},
4955				"columns": [
4956					{ data: "uuid", defaultContent: "" },
4957					{ data: this.dataOnline, render: this.renderOnlineIndicator, searchable: false, className: "isystatusholder" },
4958					{ data: "ip", render: this.renderIfOnline },
4959					// { data: "cipher", render: this.renderIfOnline  },
4960					{ data: "isy.firmware", defaultContent: "" },
4961					{ data: this.dataDomain, render: this.renderDomain, searchable:false },
4962					{ data: this.dataDomain, visible:false }
4963				],
4964				"drawCallback": function (settings, json) {
4965					// API might not be initialized yet, page may display 'Loading...'
4966					if (table.api) {
4967						table.completeUI();
4968					}
4969				}
4970			});
4971		}
4972	},
4973
4974	reload: function () {
4975		var table = this;
4976
4977		if (!table.api) {
4978			table.load();
4979		}
4980		else {
4981			table.api.ajax.reload(null, false);
4982		}
4983	}
4984};
4985
4986// Admin / ISY Portal Access License Renewal Options table
4987var tableIpalRenewalOptions = {
4988	api: null,
4989
4990	completeUI : function() {
4991		var table = this;
4992		table.api.columns.adjust();
4993
4994		// Delete existing renewal options
4995		$('#ipal_renewals_table .delbutton').unbind('click').click(function () {
4996			var pk = table.api.cell($(this).closest('tr'), 'pk:name').data();
4997
4998			confirmationDialog('Renewal option will be removed.<br>Are you sure?', 'Remove ISY Portal Renewal Option?')
4999			.then(function () {
5000				ajaxJsonDelete('/api/ipal/renewal/option', 'id='+pk)
5001				.done(function () {
5002					table.reload();
5003				})
5004				.fail(function(jqXHR, textStatus, errorThrown) {
5005					if (jqXHR.status==425) { // Validation error message
5006						errorMessageDialog(jqXHR.responseText, 'Error', 350);
5007					}
5008					else {
5009						errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350);
5010					}
5011				})
5012				.done();
5013			})
5014			.done();
5015		});
5016
5017		// Edit existing option
5018		$('#ipal_renewals_table .description').click(function () {
5019			var pk = table.api.cell($(this).closest('tr'), 'pk:name').data();
5020			var part = table.api.cell($(this).closest('tr'), 'part:name').data();
5021			var isRenewal = table.api.cell($(this).closest('tr'), 'isrenewal:name').data();
5022			var isLegacy = table.api.cell($(this).closest('tr'), 'islegacy:name').data();
5023			var description = table.api.cell($(this).closest('tr'), 'description:name').data();
5024			var legacyText = table.api.cell($(this).closest('tr'), 'legacytext:name').data();
5025			var duration = table.api.cell($(this).closest('tr'), 'duration:name').data();
5026			var price = table.api.cell($(this).closest('tr'), 'price:name').data();
5027
5028			ipalRenewalOptionEditDialog.setFields({ editMode: true, pk:pk, isRenewal: isRenewal, isLegacy: isLegacy, part:part, description:description, legacyText: legacyText, duration:duration, price:price });
5029			ipalRenewalOptionEditDialog.modal.dialog('open');
5030		});
5031	},
5032
5033	renderPart: function (data, type, row, meta) {
5034		return	'<span class="delbutton"><span class="ui-icon ui-icon-closethick"></span></span>' +
5035				'<span>' + data + '</span>';
5036	},
5037
5038	renderDescription: function (data, type, row, meta) {
5039		return	'<span class="description editable_link">' + data + '</span>';
5040	},
5041
5042	renderRenewal: function (data, type, row, meta) {
5043		return 	data?'Renewal':'Initial';
5044	},
5045
5046	renderLegacy: function (data, type, row, meta) {
5047		return 	data?'Yes':'';
5048	},
5049
5050	renderYears: function (data, type, row, meta) {
5051		return 	data?data + ' Year' + (data>1?'s':''):'Perpetual';
5052	},
5053
5054	renderPrice: function (data, type, row, meta) {
5055		return 	data.toFixed(2);
5056	},
5057/*
5058	renderDiscount: function (data, type, row, meta) {
5059		return 	data + '%';
5060	},
5061*/
5062	load: function () {
5063		var table = this;
5064
5065		if (!this.api) {
5066			this.api = $('#ipal_renewals_table').DataTable({
5067				'ajax': function (data, callback, settings) {
5068					ajaxJsonGet('/api/ipal/renewal/options')
5069					.done(function(data, status, jqXHR){
5070						callback(data);
5071					});
5072				},
5073				'columns': [
5074					{ data: 'part', render: this.renderPart, defaultContent:'', name: 'part' },
5075					{ data: 'isrenewal', render: this.renderRenewal, defaultContent:'', name: 'isrenewal' },
5076					{ data: 'islegacy', render: this.renderLegacy, defaultContent:'', name: 'islegacy' },
5077					{ data: 'description', render: this.renderDescription, name: 'description' },
5078					{ data: 'duration', render: this.renderYears, name: 'duration' },
5079					{ data: 'price', render: this.renderPrice, name: 'price' },
5080					{ data: 'legacytext', visible: false, defaultContent:'', name: 'legacytext' },
5081					{ data: '_id', visible: false, name: 'pk' }
5082				],
5083				'order': [[ 1, 'asc' ], [ 3, 'asc' ]],
5084				'drawCallback': function (settings, json) {
5085					// API might not be initialized yet, page may display 'Loading...'
5086					if (table.api) {
5087						table.completeUI();
5088					}
5089				}
5090			});
5091		}
5092	},
5093
5094	reload: function (forceReload) {
5095		var table = this;
5096
5097		if (!table.api) {
5098			table.load();
5099		}
5100		else {
5101			table.api.ajax.reload(null, false);
5102		}
5103	}
5104};
5105
5106// Admin / ISY Portal Access License Renewal Options table
5107var tableIpalNotificationOptions = {
5108	api: null,
5109	types: [ 'Trial', 'License' ],
5110
5111	completeUI : function() {
5112		var table = this;
5113		table.api.columns.adjust();
5114
5115		// Delete existing renewal options
5116		$('#ipal_notifications_table .delbutton').unbind('click').click(function () {
5117			var pk = table.api.cell($(this).closest('tr'), 'pk:name').data();
5118
5119			confirmationDialog('Notification option will be removed.<br>Are you sure?', 'Remove ISY Portal Notification Option?')
5120			.then(function () {
5121				ajaxJsonDelete('/api/ipal/notification/option', 'id='+pk)
5122				.done(function () {
5123					table.reload();
5124				})
5125				.fail(function(jqXHR, textStatus, errorThrown) {
5126					if (jqXHR.status==425) { // Validation error message
5127						errorMessageDialog(jqXHR.responseText, 'Error', 350);
5128					}
5129					else {
5130						errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350);
5131					}
5132				})
5133				.done();
5134			})
5135			.done();
5136		});
5137
5138		// Edit existing option
5139		$('#ipal_notifications_table .description').click(function () {
5140			var pk = table.api.cell($(this).closest('tr'), 'pk:name').data();
5141			var name = table.api.cell($(this).closest('tr'), 'name:name').data();
5142			var days = table.api.cell($(this).closest('tr'), 'days:name').data();
5143			var skip = table.api.cell($(this).closest('tr'), 'skip:name').data();
5144			var notification = table.api.cell($(this).closest('tr'), 'notification:name').data();
5145			var pushContent = table.api.cell($(this).closest('tr'), 'pushContent:name').data();
5146			var emailTitle = table.api.cell($(this).closest('tr'), 'emailTitle:name').data();
5147			var emailBody = table.api.cell($(this).closest('tr'), 'emailBody:name').data();
5148			var type = table.api.cell($(this).closest('tr'), 'type:name').data();
5149
5150			ipalNotificationOptionEditDialog.setFields({ editMode: true, pk:pk, name:name, days:days, skip:skip, notification:notification, pushContent:pushContent, emailTitle:emailTitle, emailBody:emailBody, type:type });
5151
5152			ipalNotificationOptionEditDialog.modal.dialog('open');
5153		});
5154	},
5155
5156	renderName: function (data, type, row, meta) {
5157		return 	'<span class="delbutton"><span class="ui-icon ui-icon-closethick"></span></span>' +
5158                '<span class="description editable_link">' + data + '</span>';
5159	},
5160
5161	renderDays: function (data, type, row, meta) {
5162		return 	data?data<0?(data*-1)+' days before expiry':data+' days after expiry':'On expiry';
5163	},
5164
5165	renderType: function (data, type, row, meta) {
5166		return 	tableIpalNotificationOptions.types[data];
5167	},
5168
5169	load: function () {
5170		var table = this;
5171
5172		if (!this.api) {
5173			this.api = $('#ipal_notifications_table').DataTable({
5174				'ajax': function (data, callback, settings) {
5175					ajaxJsonGet('/api/ipal/notification/options')
5176					.done(function(data, status, jqXHR){
5177						callback(data);
5178					});
5179				},
5180				'columns': [
5181					{ data: 'name', render: this.renderName, name: 'name' },
5182					{ data: 'type', render: this.renderType, defaultContent:'', name: 'type', orderData: [ 5 ] },
5183					{ data: 'days', render: this.renderDays, name: 'days', orderData: [ 6 ] },
5184					{ data: 'notification', visible: false, defaultContent:'', name: 'notification' },
5185					{ data: 'pushContent', visible: false, defaultContent:'', name: 'pushContent' },
5186					{ data: 'emailTitle', visible: false, defaultContent:'', name: 'emailTitle' },
5187					{ data: 'emailBody', visible: false, defaultContent:'', name: 'emailBody' },
5188					{ data: 'type', visible: false }, // For ordering
5189					{ data: 'days', visible: false }, // For ordering
5190					{ data: 'skip', visible: false, defaultContent: false, name: 'skip' },
5191					{ data: '_id', visible: false, name: 'pk' }
5192				],
5193				'order': [[ 7, 'asc' ],[ 8, 'asc' ]], // Trial first, then days
5194				'drawCallback': function (settings, json) {
5195					// API might not be initialized yet, page may display 'Loading...'
5196					if (table.api) {
5197						table.completeUI();
5198					}
5199				}
5200			});
5201		}
5202	},
5203
5204	reload: function (forceReload) {
5205		var table = this;
5206
5207		if (!table.api) {
5208			table.load();
5209		}
5210		else {
5211			table.api.ajax.reload(null, false);
5212		}
5213	}
5214};
5215
5216
5217// Admin / Rate Plans Options table
5218// this is used for the main Admin tab only (scope=all providers)
5219var tableRatePlanOptions = {
5220	api: null,
5221
5222	refresh: function () {
5223		tableRatePlanOptions.reload();
5224	},
5225
5226	completeUI : function() {
5227		var table = this;
5228		table.api.columns.adjust();
5229
5230		// Delete existing renewal options
5231		$('#rateplans_table .delbutton').unbind('click').click(function () {
5232			var pk = table.api.cell($(this).closest('tr'), 'pk:name').data();
5233
5234			confirmationDialog('Rate plan option will be removed.<br>Are you sure?', 'Remove Rate Plan?')
5235			.then(function () {
5236				ajaxJsonDelete('/api/rateplan', 'id='+pk)
5237				.done(function () {
5238					table.reload();
5239				})
5240				.fail(function(jqXHR, textStatus, errorThrown) {
5241					if (jqXHR.status==425) { // Validation error message
5242						errorMessageDialog(jqXHR.responseText, 'Error', 350);
5243					}
5244					else {
5245						errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350);
5246					}
5247				})
5248				.done();
5249			})
5250			.done();
5251		});
5252
5253		// Edit existing option
5254		$('#rateplans_table .rateplan').click(function () {
5255			var pk = table.api.cell($(this).closest('tr'), 'pk:name').data();
5256			var provider = table.api.cell($(this).closest('tr'), 'provider:name').data();
5257			var ratePlan = table.api.cell($(this).closest('tr'), 'ratePlan:name').data();
5258			var name = table.api.cell($(this).closest('tr'), 'name:name').data();
5259			var altCodes = table.api.cell($(this).closest('tr'), 'altCodes:name').data();
5260			var gridx = table.api.cell($(this).closest('tr'), 'gridx:name').data();
5261			var active = table.api.cell($(this).closest('tr'), 'active:name').data();
5262			var isTiered = table.api.cell($(this).closest('tr'), 'isTiered:name').data();
5263			var sort = table.api.cell($(this).closest('tr'), 'sort:name').data();
5264			var description = table.api.cell($(this).closest('tr'), 'description:name').data();
5265			var details = table.api.cell($(this).closest('tr'), 'details:name').data();
5266			var programs = table.api.cell($(this).closest('tr'), 'programs:name').data();
5267
5268			ratePlanEditDialog.setFields({ editMode: true, pk:pk, provider:provider,
5269				ratePlan:ratePlan, name:name, altCodes: altCodes, gridx: gridx,
5270				active:active, isTiered:isTiered, sort:sort, description:description,
5271				details:details, programs: programs,
5272				refreshFunction: table.refresh
5273			});
5274			ratePlanEditDialog.modal.dialog('open');
5275		});
5276	},
5277
5278	renderProvider: function (data, type, row, meta) {
5279		return 	'<span class="delbutton"><span class="ui-icon ui-icon-closethick"></span></span>' + data;
5280	},
5281
5282	renderRatePlan: function (data, type, row, meta) {
5283		return '<span class="rateplan editable_link">' + (data && data.length? data : '(Not found error msg)') + '</span>';
5284	},
5285
5286	renderName: function (data, type, row, meta) {
5287		return '<span class="rateplan editable_link">' + data + '</span>';
5288	},
5289
5290	renderActive: function (data, type, row, meta) {
5291		return 	data ? 'Yes' : '';
5292	},
5293
5294	load: function () {
5295		var table = this;
5296
5297		if (!this.api) {
5298			this.api = $('#rateplans_table').DataTable({
5299				'ajax': function (data, callback, settings) {
5300					ajaxJsonGet('/api/rateplans')
5301					.done(function(data, status, jqXHR){
5302						callback(data);
5303					});
5304				},
5305				'columns': [
5306					{ data: 'provider', render: this.renderProvider, name: 'provider' },
5307					{ data: 'ratePlan', render: this.renderRatePlan, name: 'ratePlan' },
5308					{ data: 'name', name: 'name' },
5309					{ data: 'active', render: this.renderActive, defaultContent:'', name: 'active' }, // , orderData: [ 5 ]
5310					{ data: 'sort', defaultContent: '', name: 'sort' }, // , orderData: [ 6 ]
5311					{ data: 'altCodes', defaultContent:'', visible: false, name: 'altCodes' },
5312					{ data: 'gridx', defaultContent:'', visible: false, name: 'gridx' },
5313					{ data: 'description', defaultContent:'', visible: false, name: 'description' },
5314					{ data: 'isTiered', defaultContent:false, visible:false, name: 'isTiered' },
5315					{ data: 'details', defaultContent:'', visible: false, name: 'details' },
5316					{ data: 'programs', defaultContent:{}, visible: false, name: 'programs' },
5317					// { data: 'type', visible: false }, // For ordering
5318					// { data: 'days', visible: false }, // For ordering
5319					{ data: '_id', visible: false, name: 'pk' }
5320				],
5321				'order': [[ 0, 'asc' ],[ 3, 'desc' ],[ 4, 'asc' ],[ 1, 'asc' ]], // Provider, Active yes/no, sort order...
5322				'drawCallback': function (settings, json) {
5323					// API might not be initialized yet, page may display 'Loading...'
5324					if (table.api) {
5325						table.completeUI();
5326					}
5327				}
5328			});
5329		}
5330	},
5331
5332	reload: function (forceReload) {
5333		var table = this;
5334
5335		if (!table.api) {
5336			table.load();
5337		}
5338		else {
5339			table.api.ajax.reload(null, false);
5340		}
5341	}
5342};
5343
5344
5345
5346var tableEchoSpokenNodes = {
5347	data: null,
5348	api: null,
5349	currentUuid: null,
5350	currentDomain: null,
5351	lookupDataPromise: null,
5352	lookupDataPromisePgm: null,
5353	lookupDataPromiseStateVar: null,
5354	lookupDataPromiseNetworkRsc: null,
5355	lookupDataPromiseElk: null,
5356	roomsPromise:null,
5357	userCats: {
5358		'scene': 'Scene',
5359		'light': 'Device\\Light',
5360		'switch': 'Device\\Switch',
5361		'outlet': 'Device\\Outlet',
5362		'fan': 'Device\\Fan',
5363		'openClose': 'Device\\Open-Close',
5364		'lowerRaise': 'Device\\Set-Lower-Raise',
5365		'motion': 'Device\\Motion Sensor',
5366		'contact': 'Device\\Contact Sensor',
5367		'lock': 'Device\\Lock',
5368		'thermostat': 'Device\\Thermostat',
5369	},
5370
5371	// Triggered when json data has loaded (before table draw)
5372	onDataLoad: function (spokens) {
5373		var table = this;
5374
5375		table.data = spokens; // Keep a reference to the original data to facilitate export.
5376
5377		table.lookupDataPromise
5378		.then(function(lookups) {
5379			lookups.data.devices.forEach(function (item) {
5380				item.class='';
5381
5382				spokens.data.forEach(function (spoken) {
5383					if (spoken.category === item.category && spoken.address === item.address) {
5384						item.class='dup';
5385					}
5386				})
5387			});
5388
5389			lookups.data.scenes.forEach(function (item) {
5390				item.class='';
5391
5392				spokens.data.forEach(function (spoken) {
5393					if (spoken.category === 'scene' && spoken.address === item.address) {
5394						item.class='dup';
5395					}
5396				})
5397			})
5398		});
5399
5400		table.lookupDataPromisePgm
5401		.then(function(lookups) {
5402			lookups.data.forEach(function (item) {
5403				item.class='';
5404
5405				spokens.data.forEach(function (spoken) {
5406					if (spoken.category === 'program' && spoken.address === item.address) {
5407						item.class='dup';
5408					}
5409				})
5410			})
5411		});
5412
5413		table.lookupDataPromiseElk
5414		.then(function(lookups) {
5415			lookups.data.zones.forEach(function (item) {
5416				item.class='';
5417
5418				spokens.data.forEach(function (spoken) {
5419					if (spoken.category === 'elk' && spoken.address === item.id) {
5420						item.class='dup';
5421					}
5422				})
5423			})
5424		});
5425
5426		// We want to delay screen draw till after we have processed rooms
5427		return table.roomsPromise
5428		.then(function(rooms) {
5429			spokens.data.forEach(function (spoken) {
5430				rooms.data.forEach(function (room) {
5431					if (spoken.room === room._id) {
5432						spoken.roomName=room.name;
5433					}
5434				})
5435			})
5436		})
5437		// NOTE: For variables, we allow duplicates so that we can have several spokens for the same variable with different turn on/turn off values
5438	},
5439
5440	completeUI: function () {
5441		var table = this;
5442		table.api.columns.adjust();
5443
5444		$('#echo_spokennodestable').tooltip();
5445
5446		// Delete spoken
5447		$('#echo_spokennodestable .delbutton').unbind('click').click(function (e) {
5448			var pk = table.api.cell($(this).closest('tr'), 'pk:name').data();
5449
5450			confirmationDialog('Are you sure?', 'Remove spoken?')
5451			.then(function () {
5452				ajaxJsonDelete('/api/voice/amazon/spoken/node', $.param({id: pk, uuid: table.currentUuid, domain: table.currentDomain }))
5453				.fail(function(jqXHR, textStatus, errorThrown) {
5454					if (jqXHR.status===412) {
5455						if (skillNotLinkedWarningDelete) {
5456							var skipButton = 'Skip Warning'
5457							var okButton = 'Ok'
5458							var buttons = [ skipButton, okButton ]
5459							// Delete successful, but skill is not linked
5460							optionsDialog('The Alexa skill is not linked.<br/><br/>To remove it from Alexa, please use the Alexa app and click on Devices.', 'Skill not linked', buttons)
5461							.then(function (button) {
5462								if (button === skipButton) {
5463									// On next delete, there will be no warning
5464									skillNotLinkedWarningDelete = false
5465								}
5466							})
5467							.always(function () {
5468								tableEchoSpokenNodes.reload();
5469							})
5470							.done();
5471						} else {
5472							tableEchoSpokenNodes.reload();
5473						}
5474					}
5475					else if (jqXHR.status===428) {
5476						if (incorrectUnitWarningDelete) {
5477							var skipButton = 'Skip Warning'
5478							var okButton = 'Ok'
5479							var buttons = [ skipButton, okButton ]
5480							// Save successful, but skill is not linked
5481							optionsDialog('This user profile ' + registeredUser.username + ' cannot manage this ISY.<br/><br/>' +
5482								'If Alexa is linked with another user profile in this account, then you may have to remove this device manually from the Alexa app.<br/><br/>' +
5483								'If on the other hand Alexa is linked with this user profile and you want it to control this ISY, then click on My Profile to change the preferred ISY to this ISY.<br/><br/>', 'Updating incorrect ISY', buttons, 800)
5484							.then(function (button) {
5485								if (button === skipButton) {
5486									// On next edit, there will be no warning
5487									incorrectUnitWarningDelete = false
5488								}
5489							})
5490							.always(function () {
5491								tableEchoSpokenNodes.reload();
5492							})
5493							.done();
5494						} else {
5495							tableEchoSpokenNodes.reload();
5496						}
5497					}
5498					else {
5499						errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
5500						.always(function () {
5501							tableEchoSpokenNodes.reload();
5502						})
5503						.done();
5504					}
5505				})
5506				.done(function () {
5507					tableEchoSpokenNodes.reload();
5508				})
5509			})
5510			.done();
5511		});
5512
5513//		$('#echo_spokennodestable .spoken').unbind('click').click(function (e) { (???)
5514		// Edit existing spoken
5515		$('#echo_spokennodestable .spoken').click(function () {
5516			var pk = table.api.cell($(this).closest('tr'), 'pk:name').data();
5517			var address = table.api.cell($(this).closest('tr'), 'address:name').data();
5518			var spokens = table.api.cell($(this).closest('tr'), 'spokens:name').data();
5519			var category = table.api.cell($(this).closest('tr'), 'category:name').data();
5520			var room = table.api.cell($(this).closest('tr'), 'room:name').data();
5521			var colorMfr = table.api.cell($(this).closest('tr'), 'colorMfr:name').data();
5522			var userCat = table.api.cell($(this).closest('tr'), 'userCat:name').data();
5523			var avData = table.api.cell($(this).closest('tr'), 'av:name').data();
5524			var spoken_link = this;
5525
5526			table.roomsPromise
5527			.done(function(rooms) {
5528				if (category==='statevar') {
5529					var turnon_value = table.api.cell($(spoken_link).closest('tr'), 'turnon_value:name').data();
5530					var turnoff_value = table.api.cell($(spoken_link).closest('tr'), 'turnoff_value:name').data();
5531					var motion_value = table.api.cell($(spoken_link).closest('tr'), 'motion_value:name').data(); // Can be motion or contact
5532
5533					table.lookupDataPromiseStateVar
5534					.then(function(content) {
5535						if (!content) {
5536							throw new Error('Variables not available');
5537						}
5538						echoSpokenNodesEditDialog.setFields({ category: category, uuid: table.currentUuid, domain: table.currentDomain, lookup: content.data, address: address?address:'', spokens: spokens, turnon_value: turnon_value, turnoff_value: turnoff_value, motion_value: motion_value, pk: pk, editMode: true, room: (room && room.length?room:'$noroom'), rooms: rooms.data, userCat: userCat });
5539						echoSpokenNodesEditDialog.modal.dialog('open');
5540					})
5541					.fail(function(jqXHR, textStatus, errorThrown) {
5542						errorMessageDialog("Variables are not available", 'Error', 350)
5543					})
5544					.done();
5545				}
5546				else if (category==='program') {
5547					table.lookupDataPromisePgm
5548					.then(function(content) {
5549						if (!content) {
5550							throw new Error('Programs not available');
5551						}
5552						echoSpokenNodesEditDialog.setFields({ category: category, uuid: table.currentUuid, domain: table.currentDomain, lookup: content.data, address: address?address:'', spokens: spokens, pk: pk, editMode: true, room: (room && room.length?room:'$noroom'), rooms: rooms.data, userCat: userCat });
5553						echoSpokenNodesEditDialog.modal.dialog('open');
5554					})
5555					.fail(function(jqXHR, textStatus, errorThrown) {
5556						errorMessageDialog("Programs are not available", 'Error', 350)
5557					})
5558					.done();
5559				}
5560				else if (category==='scene') {
5561					table.lookupDataPromise
5562					.then(function(content) {
5563						if (!content) {
5564							throw new Error('Scenes not available');
5565						}
5566						echoSpokenNodesEditDialog.setFields({ category: category, uuid: table.currentUuid, domain: table.currentDomain, lookup: content.data.scenes, address: address?address:'', spokens: spokens, pk: pk, editMode: true, room: (room && room.length?room:'$noroom'), rooms: rooms.data, userCat: userCat });
5567						echoSpokenNodesEditDialog.modal.dialog('open');
5568					})
5569					.fail(function(jqXHR, textStatus, errorThrown) {
5570						errorMessageDialog("Scenes are not available", 'Error', 350)
5571					})
5572					.done();
5573				}
5574				else if (category==='elk') {
5575					table.lookupDataPromiseElk
5576					.then(function(content) {
5577						if (!content) {
5578							throw new Error('ELK zones not available');
5579						}
5580						echoSpokenNodesEditDialog.setFields({ category: category, uuid: table.currentUuid, domain: table.currentDomain, lookup: content.data.zones, address: address?address:'', spokens: spokens, pk: pk, editMode: true, room: (room && room.length?room:'$noroom'), rooms: rooms.data, userCat: userCat });
5581						echoSpokenNodesEditDialog.modal.dialog('open');
5582					})
5583					.fail(function(jqXHR, textStatus, errorThrown) {
5584						errorMessageDialog("ELK zones are not available", 'Error', 350)
5585					})
5586					.done();
5587				}
5588				else if (category==='av') {
5589					$.when(table.lookupDataPromise, table.lookupDataPromiseStateVar, table.lookupDataPromisePgm, table.lookupDataPromiseNetworkRsc)
5590					.then(function(nodes, vars, pgms, net) {
5591
5592						if (!nodes) {
5593							throw new Error('Nodes not available');
5594						}
5595
5596						if (!vars) {
5597							throw new Error('State variables not available');
5598						}
5599
5600						if (!pgms) {
5601							throw new Error('Programs not available');
5602						}
5603
5604						if (!net) {
5605							throw new Error('Network resources not available');
5606						}
5607/*
5608						console.log('nodes', nodes)
5609						console.log('vars', vars)
5610						console.log('net', net)
5611						console.log('rooms', rooms)
5612						console.log('room', room)
5613*/
5614						echoSpokenNodesAVEditDialog.setFields({ uuid: table.currentUuid, domain: table.currentDomain, lookupNodes: nodes[0], lookupVars: vars[0], lookupPgms: pgms[0], lookupNet: net[0], address: address?address:'', spokens: spokens, pk: pk, editMode: true, room: (room && room.length?room:'$noroom'), rooms: rooms.data, avData: avData });
5615						echoSpokenNodesAVEditDialog.modal.dialog('open');
5616					})
5617					.fail(function(jqXHR, textStatus, errorThrown) {
5618						errorMessageDialog("AV resources are not available", 'Error', 350)
5619					})
5620					.done();
5621				}
5622				else { // Regular devices (std), thermostats & locks
5623					table.lookupDataPromise
5624					.then(function(content) {
5625						if (!content) {
5626							throw new Error('Devices not available');
5627						}
5628
5629						echoSpokenNodesEditDialog.setFields({ category: 'std', uuid: table.currentUuid, domain: table.currentDomain, lookup: content.data.devices, address: address?address:'', spokens: spokens, pk: pk, editMode: true, room: (room && room.length?room:'$noroom'), rooms: rooms.data, colorMfr:colorMfr, userCat: userCat });
5630						echoSpokenNodesEditDialog.modal.dialog('open');
5631					})
5632					.fail(function(jqXHR, textStatus, errorThrown) {
5633						errorMessageDialog("Devices are not available", 'Error', 350)
5634					})
5635					.done();
5636				}
5637			})
5638		});
5639	},
5640
5641	renderNameWithDelete: function (data, type, row, meta) {
5642		return 	'<span class="delbutton"><span class="ui-icon ui-icon-closethick"></span></span><span>' + (!data?row.spoken:data) + '</span>';
5643	},
5644
5645	renderAddress: function (data, type, row) {
5646//		return row.category!='av'?(data.replace(/ /g,".") + (row.category=='statevar'?' (Off:' + row.turnoff_value + ' On:' + row.turnon_value + ')':'')) :'N/A'
5647		return row.category!=='av'?
5648		(data.replace(/ /g,".") + (row.category==='statevar'?
5649			(row.userCat==='motion'?' (Detect:' + row.motion_value + ')':row.userCat==='contact'?' (Close:' + row.motion_value + ')':' (Off:' + row.turnoff_value + ' On:' + row.turnon_value + ')')
5650			:''))
5651		:'N/A'
5652	},
5653
5654	renderCategory: function (data, type, row) {
5655		return echoCategoryTxt[data];
5656	},
5657
5658	renderUserCat: function (data, type, row) {
5659		var userCat = tableEchoSpokenNodes.userCats[data];
5660
5661		if (['statevar', 'program', 'scene'].includes(row.category)) {
5662			return userCat?userCat:tableEchoSpokenNodes.userCats['scene'];
5663		}
5664		else if (row.category==='lock') {
5665			return userCat?userCat:tableEchoSpokenNodes.userCats['lock'];
5666		}
5667		else if (row.category==='thermostat') {
5668			return userCat?userCat:tableEchoSpokenNodes.userCats['thermostat'];
5669		}
5670		else {
5671			return userCat?userCat:tableEchoSpokenNodes.userCats['light'];
5672		}
5673	},
5674
5675	renderSpoken: function (data, type, row) {
5676		var spoken;
5677		if (data.length===1) {
5678			spoken = data[0];
5679		}
5680		else if (data.length>1) {
5681			spoken = data[0] + ', [More]';
5682		}
5683		else {
5684			spoken = "\[Empty\]";
5685		}
5686
5687		return '<span class="spoken editable_link">' + spoken + '</span>';
5688	},
5689
5690	load: function () {
5691		var table = this;
5692		if (!this.api) {
5693			this.api = $('#echo_spokennodestable').DataTable({
5694				"width": "100%",
5695				"ajax": function (data, callback, settings) {
5696					ajaxJsonGet('/api/voice/amazon/spoken/nodes', $.param({ uuid: table.currentUuid, domain: table.currentDomain }))
5697					.done(function(data, status, jqXHR){
5698						table.onDataLoad(data)
5699						.done(function() {
5700							callback(data);
5701						})
5702						.done();
5703					});
5704				},
5705				"columns": [
5706					{ data: "name", defaultContent: "", render: this.renderNameWithDelete },
5707					{ data: "category", render: this.renderCategory, name: "category" },
5708					{ data: "userCat", defaultContent: "", render: this.renderUserCat, name: "userCat" },
5709					{ data: "address", render: this.renderAddress, name: "address" },
5710					{ data: "roomName", defaultContent: "" },
5711					{ data: "spoken", render: this.renderSpoken, name: "spokens" },
5712					{ data: "room", defaultContent: "", name: "room", visible:false },
5713					{ data: "spoken[;]", visible: false, searchable:true },
5714					{ data: "_id", visible: false, searchable: false, name: 'pk' },
5715					{ data: "turnon_value", visible: false, defaultContent: "", name: "turnon_value" },
5716					{ data: "turnoff_value", visible: false, defaultContent: "", name: "turnoff_value" },
5717					{ data: "motion_value", visible: false, defaultContent: "", name: "motion_value" },
5718					{ data: "colorMfr", visible: false, defaultContent: "", name: "colorMfr" },
5719					{ data: "av", visible: false, defaultContent: "", name: "av" }
5720				],
5721				"drawCallback": function (settings, json) {
5722					// API might not be initialized yet, page may display 'Loading...'
5723					if (table.api) {
5724						table.completeUI();
5725					}
5726				}
5727			});
5728		}
5729	},
5730
5731	reload: function () {
5732		var table = this;
5733
5734		if (!table.api) {
5735			table.load();
5736		}
5737		else {
5738			table.api.ajax.reload(null, false);
5739		}
5740	},
5741
5742	setUuid: function (uuid, domain, forceReload) {
5743		if (this.currentUuid!==uuid || forceReload) {
5744			this.currentUuid = uuid;
5745			this.currentDomain = domain;
5746			this.reload();
5747		}
5748	},
5749
5750	setLookupDataPromise: function (lookupDataPromise, lookupDataPromisePgm, lookupDataPromiseStateVar, lookupDataPromiseNetworkRsc, lookupDataPromise
5750Elk) {
5751		this.lookupDataPromise = lookupDataPromise; // Reference to the lookup promise used to validate if we have lookup data prior to editing spoken
5752		this.lookupDataPromisePgm = lookupDataPromisePgm;
5753		this.lookupDataPromiseStateVar = lookupDataPromiseStateVar;
5754		this.lookupDataPromiseNetworkRsc = lookupDataPromiseNetworkRsc;
5755		this.lookupDataPromiseElk = lookupDataPromiseElk;
5756	},
5757
5758	setRooms: function (roomsPromise) {
5759		this.roomsPromise = roomsPromise;
5760	}
5761};
5762
5763var tableGHSpokenNodes = {
5764	data: null,
5765	api: null,
5766	currentUuid: null,
5767	currentDomain: null,
5768	lookupDataPromise: null,
5769	lookupDataPromisePgm: null,
5770	lookupDataPromiseStateVar: null,
5771	roomsPromise:null,
5772	userCats: {
5773		'scene': 'Scene',
5774		'light': 'Device\\Light',
5775		'switch': 'Device\\Switch',
5776		'outlet': 'Device\\Outlet',
5777		'fan': 'Device\\Fan',
5778		'lock': 'Device\\Lock',
5779		'thermostat': 'Device\\Thermostat',
5780		'openClose': 'Device\\Open Close',
5781	},
5782
5783	// Triggered when json data has loaded (before table draw)
5784	onDataLoad: function (spokens) {
5785		var table = this;
5786
5787		table.data = spokens; // Keep a reference to the original data to facilitate export.
5788
5789		table.lookupDataPromise
5790		.then(function(lookups) {
5791			lookups.data.devices.forEach(function (item) {
5792				item.class='';
5793
5794				spokens.data.forEach(function (spoken) {
5795					if (spoken.category === item.category && spoken.address === item.address) {
5796						item.class='dup';
5797					}
5798				})
5799			});
5800
5801			lookups.data.scenes.forEach(function (item) {
5802				item.class='';
5803
5804				spokens.data.forEach(function (spoken) {
5805					if (spoken.category === 'scene' && spoken.address === item.address) {
5806						item.class='dup';
5807					}
5808				})
5809			})
5810		});
5811
5812		table.lookupDataPromisePgm
5813		.then(function(lookups) {
5814			lookups.data.forEach(function (item) {
5815				item.class='';
5816
5817				spokens.data.forEach(function (spoken) {
5818					if (spoken.category === 'program' && spoken.address === item.address) {
5819						item.class='dup';
5820					}
5821				})
5822			})
5823		});
5824
5825		// We want to delay screen draw till after we have processed rooms
5826		return table.roomsPromise
5827		.then(function(rooms) {
5828			spokens.data.forEach(function (spoken) {
5829				rooms.data.forEach(function (room) {
5830					if (spoken.room === room._id) {
5831						spoken.roomName=room.name;
5832					}
5833				})
5834			})
5835		})
5836		// NOTE: For variables, we allow duplicates so that we can have several spokens for the same variable with different turn on/turn off values
5837	},
5838
5839	completeUI: function () {
5840		var table = this;
5841		table.api.columns.adjust();
5842
5843		$('#gh_spokennodestable').tooltip();
5844
5845		// Delete spoken
5846		$('#gh_spokennodestable .delbutton').unbind('click').click(function (e) {
5847			var pk = table.api.cell($(this).closest('tr'), 'pk:name').data();
5848
5849			confirmationDialog('Are you sure?', 'Remove spoken?')
5850			.then(function () {
5851				ajaxJsonDelete('/api/voice/google/spoken/node', $.param({id: pk, uuid: table.currentUuid, domain: table.currentDomain }))
5852				.done(function () {
5853					tableGHSpokenNodes.reload();
5854				})
5855			})
5856			.done();
5857		});
5858
5859//		$('#gh_spokennodestable .spoken').unbind('click').click(function (e) { (???)
5860		// Edit existing spoken
5861		$('#gh_spokennodestable .spoken').click(function () {
5862			var pk = table.api.cell($(this).closest('tr'), 'pk:name').data();
5863			var address = table.api.cell($(this).closest('tr'), 'address:name').data();
5864			var spokens = table.api.cell($(this).closest('tr'), 'spokens:name').data();
5865			var category = table.api.cell($(this).closest('tr'), 'category:name').data();
5866			var room = table.api.cell($(this).closest('tr'), 'room:name').data();
5867			var colorMfr = table.api.cell($(this).closest('tr'), 'colorMfr:name').data();
5868			var userCat = table.api.cell($(this).closest('tr'), 'userCat:name').data();
5869			var spoken_link = this;
5870
5871			table.roomsPromise
5872			.done(function(rooms) {
5873				if (category==='statevar') {
5874					var turnon_value = table.api.cell($(spoken_link).closest('tr'), 'turnon_value:name').data();
5875					var turnoff_value = table.api.cell($(spoken_link).closest('tr'), 'turnoff_value:name').data();
5876
5877					table.lookupDataPromiseStateVar
5878					.then(function(content) {
5879						if (!content) {
5880							throw new Error('Variables not available');
5881						}
5882						ghSpokenNodesEditDialog.setFields({ category: category, uuid: table.currentUuid, domain: table.currentDomain, lookup: content.data, address: address?address:'', spokens: spokens, turnon_value: turnon_value, turnoff_value: turnoff_value, pk: pk, editMode: true, room: (room && room.length?room:'$noroom'), rooms: rooms.data, userCat: userCat });
5883						ghSpokenNodesEditDialog.modal.dialog('open');
5884					})
5885					.fail(function(jqXHR, textStatus, errorThrown) {
5886						errorMessageDialog("Variables are not available", 'Error', 350)
5887					})
5888					.done();
5889				}
5890				else if (category==='program') {
5891					table.lookupDataPromisePgm
5892					.then(function(content) {
5893						if (!content) {
5894							throw new Error('Programs not available');
5895						}
5896						ghSpokenNodesEditDialog.setFields({ category: category, uuid: table.currentUuid, domain: table.currentDomain, lookup: content.data, address: address?address:'', spokens: spokens, pk: pk, editMode: true, room: (room && room.length?room:'$noroom'), rooms: rooms.data, userCat: userCat });
5897						ghSpokenNodesEditDialog.modal.dialog('open');
5898					})
5899					.fail(function(jqXHR, textStatus, errorThrown) {
5900						errorMessageDialog("Programs are not available", 'Error', 350)
5901					})
5902					.done();
5903				}
5904				else if (category==='scene') {
5905					table.lookupDataPromise
5906					.then(function(content) {
5907						if (!content) {
5908							throw new Error('Scenes not available');
5909						}
5910						ghSpokenNodesEditDialog.setFields({ category: category, uuid: table.currentUuid, domain: table.currentDomain, lookup: content.data.scenes, address: address?address:'', spokens: spokens, pk: pk, editMode: true, room: (room && room.length?room:'$noroom'), rooms: rooms.data, userCat: userCat });
5911						ghSpokenNodesEditDialog.modal.dialog('open');
5912					})
5913					.fail(function(jqXHR, textStatus, errorThrown) {
5914						errorMessageDialog("Scenes are not available", 'Error', 350)
5915					})
5916					.done();
5917				}
5918				else { // Regular devices (std), thermostats & locks
5919					table.lookupDataPromise
5920					.then(function(content) {
5921						if (!content) {
5922							throw new Error('Devices not available');
5923						}
5924						ghSpokenNodesEditDialog.setFields({ category: 'std', uuid: table.currentUuid, domain: table.currentDomain, lookup: content.data.devices, address: address?address:'', spokens: spokens, pk: pk, editMode: true, room: (room && room.length?room:'$noroom'), rooms: rooms.data, colorMfr:colorMfr, userCat: userCat });
5925						ghSpokenNodesEditDialog.modal.dialog('open');
5926					})
5927					.fail(function(jqXHR, textStatus, errorThrown) {
5928						errorMessageDialog("Devices are not available", 'Error', 350)
5929					})
5930					.done();
5931				}
5932			})
5933		});
5934	},
5935
5936	renderNameWithDelete: function (data, type, row, meta) {
5937		return 	'<span class="delbutton"><span class="ui-icon ui-icon-closethick"></span></span><span>' + (!data?row.spoken:data) + '</span>';
5938	},
5939
5940	renderAddress: function (data, type, row) {
5941		return data.replace(/ /g,".") + (row.category==='statevar'?' (Off:' + row.turnoff_value + ' On:' 
5941+ row.turnon_value + ')':'');
5942	},
5943
5944	renderCategory: function (data, type, row) {
5945		return echoCategoryTxt[data];
5946	},
5947
5948	renderUserCat: function (data, type, row) {
5949		var userCat = tableGHSpokenNodes.userCats[data];
5950
5951		if (['statevar', 'program', 'scene'].includes(row.category)) {
5952			return userCat?userCat:tableGHSpokenNodes.userCats['switch']; // Default GH cat for an ISY scene is a switch
5953		}
5954		else if (row.category==='lock') {
5955			return userCat?userCat:tableGHSpokenNodes.userCats['lock'];
5956		}
5957		else if (row.category==='thermostat') {
5958			return userCat?userCat:tableGHSpokenNodes.userCats['thermostat'];
5959		}
5960		else {
5961			return userCat?userCat:tableGHSpokenNodes.userCats['light'];
5962		}
5963	},
5964
5965	renderSpoken: function (data, type, row) {
5966		var spoken;
5967		if (data.length===1) {
5968			spoken = data[0];
5969		}
5970		else if (data.length>1) {
5971			spoken = data[0] + ', [More]';
5972		}
5973		else {
5974			spoken = "\[Empty\]";
5975		}
5976
5977		return '<span class="spoken editable_link">' + spoken + '</span>';
5978	},
5979
5980	load: function () {
5981		var table = this;
5982		if (!this.api) {
5983			this.api = $('#gh_spokennodestable').DataTable({
5984				"ajax": function (data, callback, settings) {
5985					ajaxJsonGet('/api/voice/google/spoken/nodes', $.param({ uuid: table.currentUuid, domain: table.currentDomain }))
5986					.done(function(data, status, jqXHR){
5987						table.onDataLoad(data)
5988						.done(function() {
5989							callback(data);
5990						})
5991						.done();
5992					});
5993				},
5994				"columns": [
5995					{ data: "name", defaultContent: "", render: this.renderNameWithDelete },
5996					{ data: "category", render: this.renderCategory, name: "category" },
5997					{ data: "userCat", defaultContent: "", render: this.renderUserCat, name: "userCat" },
5998					{ data: "address", render: this.renderAddress, name: "address" },
5999					{ data: "roomName", defaultContent: "" },
6000					{ data: "spoken", render: this.renderSpoken, name: "spokens" },
6001					{ data: "room", defaultContent: "", name: "room", visible:false },
6002					{ data: "spoken[;]", visible: false, searchable:true },
6003					{ data: "_id", visible: false, searchable: false, name: 'pk' },
6004					{ data: "turnon_value", visible: false, defaultContent: "", name: "turnon_value" },
6005					{ data: "turnoff_value", visible: false, defaultContent: "", name: "turnoff_value" },
6006					{ data: "colorMfr", visible: false, defaultContent: "", name: "colorMfr" }
6007				],
6008				"drawCallback": function (settings, json) {
6009					// API might not be initialized yet, page may display 'Loading...'
6010					if (table.api) {
6011						table.completeUI();
6012					}
6013				}
6014			});
6015		}
6016	},
6017
6018	reload: function () {
6019		var table = this;
6020
6021		if (!table.api) {
6022			table.load();
6023		}
6024		else {
6025			table.api.ajax.reload(null, false);
6026		}
6027	},
6028
6029	setUuid: function (uuid, domain, forceReload) {
6030		if (this.currentUuid!==uuid || forceReload) {
6031			this.currentUuid = uuid;
6032			this.currentDomain = domain;
6033			this.reload();
6034		}
6035	},
6036
6037	setLookupDataPromise: function (lookupDataPromise, lookupDataPromisePgm, lookupDataPromiseStateVar) {
6038		this.lookupDataPromise = lookupDataPromise; // Reference to the lookup promise used to validate if we have lookup data prior to editing spoken
6039		this.lookupDataPromisePgm = lookupDataPromisePgm; // Reference to the lookup promise used to validate if we have lookup data prior to editing spoken
6040		this.lookupDataPromiseStateVar = lookupDataPromiseStateVar; // Reference to the lookup promise used to validate if we have lookup data prior to editing spoken
6041	},
6042
6043	setRooms: function (roomsPromise) {
6044		this.roomsPromise = roomsPromise;
6045	}
6046};
6047
6048var tableNsList = {
6049	data: null,
6050	api: null,
6051	currentUuid: null,
6052	currentDomain: null,
6053
6054	completeUI: function () {
6055		var table = this;
6056		table.api.columns.adjust();
6057
6058		$('#ns_list_table').tooltip();
6059	},
6060
6061	renderExpiry: function (data, type, row) {
6062		var text;
6063		if (data === null) {
6064			text = 'Perpetual'
6065		} else {
6066			return tableNsList.renderDate(data, type, row)
6067		}
6068
6069		return text;
6070	},
6071
6072	renderDate: function (data, type, row) {
6073		var text;
6074
6075		try {
6076			var dt = new Date (data)
6077			text = pad(dt.getFullYear(), 4) + '-' +
6078				pad(dt.getMonth()+1, 2) + '-' +
6079				pad(dt.getDate(), 2)
6080
6081		} catch (e) {
6082			text = '[Invalid date]'
6083		}
6084
6085		return text;
6086	},
6087
6088	load: function () {
6089		var table = this;
6090		if (!this.api) {
6091			this.api = $('#ns_list_table').DataTable({
6092				"ajax": function (data, callback, settings) {
6093					ajaxJsonGet('/api/nodeserver/list/'+table.currentUuid, $.param({ name: 'true', domain: table.currentDomain }))
6094					.done(function(data, status, jqXHR){
6095						callback(data);
6096					});
6097				},
6098				"columns": [
6099					{ data: "name", defaultContent: "" },
6100					{ data: "edition", defaultContent: "" },
6101					{ data: "purchase_date", render: this.renderDate },
6102					{ data: "expiry", render: this.renderExpiry }
6103				],
6104				"drawCallback": function (settings, json) {
6105					// API might not be initialized yet, page may display 'Loading...'
6106					if (table.api) {
6107						table.completeUI();
6108					}
6109				}
6110			});
6111		}
6112	},
6113
6114	reload: function () {
6115		var table = this;
6116
6117		if (!table.api) {
6118			table.load();
6119		}
6120		else {
6121			table.api.ajax.reload(null, false);
6122		}
6123	},
6124
6125	setUuid: function (uuid, domain, forceReload) {
6126		if (this.currentUuid!==uuid || forceReload) {
6127			this.currentUuid = uuid;
6128			this.currentDomain = domain;
6129			this.reload();
6130		}
6131	}
6132};
6133
6134
6135var tableIftttInNodes = {
6136	api: null,
6137	currentUuid: null,
6138	currentDomain: null,
6139	currentIftttInKey: null,
6140	lookupDataPromise : null,
6141	lookupDataPromisePgm : null,
6142	lookupDataPromiseStateVar: null,
6143
6144	// Triggered when json data has loaded (before table draw)
6145	onDataLoad: function (data) {
6146		this.currentIftttInKey = data.iftttInKey;
6147	},
6148
6149	completeUI: function () {
6150		var table = this;
6151		table.api.columns.adjust();
6152		$('#iftttinnodestable').tooltip();
6153
6154		// Delete IFTTT rule
6155		$('#iftttinnodestable .delbutton').unbind('click').click(function (e) {
6156			var pk = table.api.cell($(this).closest('tr'), 'pk:name').data();
6157
6158			confirmationDialog('Are you sure?', 'Remove IFTTT / Webhook?')
6159			.then(function () {
6160				ajaxJsonDelete('/api/ifttt/in/node', $.param({id: pk, uuid: table.currentUuid, domain: table.currentDomain }))
6161				.done(function () {
6162					tableIftttInNodes.reload();
6163				})
6164			})
6165			.done();
6166		});
6167
6168		// Show IFTTT Maker channel URL to use
6169		$('#iftttinnodestable .linkbutton').unbind('click').click(function (e) {
6170			var event = table.api.cell($(this).closest('tr'), 'event:name').data();
6171            var message;
6172
6173			if (table.currentIftttInKey && table.currentIftttInKey.length) {
6174				var url = 'https://' + location.hostname + '/api/ifttt/' + encodeURIComponent(event) + '/key/' + table.currentIftttInKey;
6175				message = 'This event URL is:<br><p class="url80">'+url+'</p>';
6176				informationMessageDialog(message, 'Event URL', 800);
6177			}
6178			else {
6179				message = 'First, you have to set an IFTTT / Webhook key.<br>Please click on "Set Key".';
6180				informationMessageDialog(message, 'Error');
6181			}
6182
6183		});
6184
6185//		$('#iftttinnodestable .spoken').unbind('click').click(function (e) { (???)
6186		// Edit existing spoken
6187		$('#iftttinnodestable .event').click(function () {
6188			var pk = table.api.cell($(this).closest('tr'), 'pk:name').data();
6189			var address = table.api.cell($(this).closest('tr'), 'address:name').data();
6190			var event = table.api.cell($(this).closest('tr'), 'event:name').data();
6191			var category = table.api.cell($(this).closest('tr'), 'category:name').data();
6192			var control = table.api.cell($(this).closest('tr'), 'control:name').data();
6193			var value = table.api.cell($(this).closest('tr'), 'value:name').data();
6194
6195			if (category==='statevar') {
6196				table.lookupDataPromiseStateVar
6197				.then(function(content) {
6198					if (!content) {
6199						throw new Error('Variables not available');
6200					}
6201					iftttInNodesEditDialog.setFields({ category: category, uuid: table.currentUuid, domain: table.currentDomain, lookup: content.data, address: address?address:'', event: event, control: control, value: value, pk: pk, editMode: true });
6202					iftttInNodesEditDialog.modal.dialog('open');
6203				})
6204				.fail(function(jqXHR, textStatus, errorThrown) {
6205					errorMessageDialog("Variables are not available", 'Error', 350)
6206				})
6207				.done();
6208			}
6209			else if (category==='program') {
6210				table.lookupDataPromisePgm
6211				.then(function(content) {
6212					if (!content) {
6213						throw new Error('Programs not available');
6214					}
6215					iftttInNodesEditDialog.setFields({ category: category, uuid: table.currentUuid, domain: table.currentDomain, lookup: content.data, address: address?address:'', event: event, control: control, pk: pk, editMode: true });
6216					iftttInNodesEditDialog.modal.dialog('open');
6217				})
6218				.fail(function(jqXHR, textStatus, errorThrown) {
6219					errorMessageDialog("Programs are not available", 'Error', 350)
6220				})
6221				.done();
6222			}
6223			else if (category==='scene') {
6224				table.lookupDataPromise
6225				.then(function(content) {
6226					if (!content) {
6227						throw new Error('Scenes not available');
6228					}
6229					iftttInNodesEditDialog.setFields({ category: category, uuid: table.currentUuid, domain: table.currentDomain, lookup: content.data.scenes, address: address?address:'', event: event, control: control, pk: pk, editMode: true });
6230					iftttInNodesEditDialog.modal.dialog('open');
6231				})
6232				.fail(function(jqXHR, textStatus, errorThrown) {
6233					errorMessageDialog("Scenes are not available", 'Error', 350)
6234				})
6235				.done();
6236			}
6237			else { // Regular devices (std), thermostats & locks
6238				table.lookupDataPromise
6239				.then(function(content) {
6240					if (!content) {
6241						throw new Error('Devices not available');
6242					}
6243					iftttInNodesEditDialog.setFields({ category: 'std', uuid: table.currentUuid, domain: table.currentDomain, lookup: content.data.devices, address: address?address:'', event: event, control: control, value: value, pk: pk, editMode: true });
6244					iftttInNodesEditDialog.modal.dialog('open');
6245				})
6246				.fail(function(jqXHR, textStatus, errorThrown) {
6247					errorMessageDialog("Devices are not available", 'Error', 350)
6248				})
6249				.done();
6250			}
6251		});
6252	},
6253
6254	renderEventWithDelete: function (data, type, row, meta) {
6255		return 	'<span class="delbutton"><span class="ui-icon ui-icon-closethick"></span></span><span class="linkbutton"><span class="ui-icon ui-icon-extlink"></span></span><span class="event editable_link">' + (data && data.length?data:"\[Empty\]") + '</span>';
6256	},
6257
6258	renderAddress: function (data, type, row) {
6259		return data.replace(/ /g,".");
6260	},
6261
6262	renderCategory: function (data, type, row) {
6263		return iftttCategoryTxt[data];
6264	},
6265
6266	renderControl: function (data, type, row) {
6267		return iftttControlsLabels[data] + (data==='value'?(' to ' + row.value):'');
6268	},
6269
6270	load: function () {
6271		var table = this;
6272		if (!this.api) {
6273			this.api = $('#iftttinnodestable').DataTable({
6274				"ajax": function (data, callback, settings) {
6275					ajaxJsonGet('/api/ifttt/in/nodes', $.param({ uuid: table.currentUuid, domain: table.currentDomain }))
6276					.done(function(data, status, jqXHR){
6277						table.onDataLoad(data);
6278						callback(data);
6279					});
6280				},
6281				"columns": [
6282					{ data: "event", render: this.renderEventWithDelete, name: "event" },
6283					{ data: "category", render: this.renderCategory, name: "category" },
6284					{ data: "name", defaultContent: ""  }, //render: this.renderNameWithDelete
6285					{ data: "address", render: this.renderAddress, name: "address" },
6286					{ data: "control", render: this.renderControl, name: "control" },
6287					{ data: "value", defaultContent: '', visible: false, searchable: true, name: 'value' },
6288					{ data: "_id", visible: false, searchable: false, name: 'pk' }
6289				],
6290				"drawCallback": function (settings, json) {
6291					// API might not be initialized yet, page may display 'Loading...'
6292					if (table.api) {
6293						table.completeUI();
6294					}
6295				}
6296			});
6297		}
6298	},
6299
6300	reload: function () {
6301		var table = this;
6302
6303		if (!table.api) {
6304			table.load();
6305		}
6306		else {
6307			table.api.ajax.reload(null, false);
6308		}
6309	},
6310
6311	setUuid: function (uuid, domain, forceReload) {
6312		if (this.currentUuid!==uuid || forceReload) {
6313			this.currentUuid = uuid;
6314			this.currentDomain = domain;
6315			this.reload();
6316		}
6317	},
6318
6319	setLookupDataPromise: function (lookupDataPromise, lookupDataPromisePgm, lookupDataPromiseStateVar) {
6320		this.lookupDataPromise = lookupDataPromise; // Reference to the lookup promise used to validate if we have lookup data prior to editing ifttt event
6321		this.lookupDataPromisePgm = lookupDataPromisePgm;
6322		this.lookupDataPromiseStateVar = lookupDataPromiseStateVar;
6323	}
6324};
6325
6326var activityLevels = {
6327	0: 'Error',
6328	1: 'Warn',
6329	2: 'Info'
6330};
6331
6332var activityCategories = {
6333	'rates': 'RATES'
6334};
6335
6336var tableActivityLog = {
6337	api: null,
6338	currentUuid: null,
6339	currentDomain: null,
6340
6341	completeUI: function () {
6342	},
6343
6344	renderCat: function (data, type, row, meta) {
6345		var cat = activityCategories[data];
6346		return 	cat?cat:data;
6347	},
6348
6349	renderLevel: function (data, type, row, meta) {
6350		return 	activityLevels[data];
6351	},
6352
6353	renderTime: function (data, type, row, meta) {
6354		return 	new Date(data).dateLog();
6355	},
6356
6357	load: function () {
6358		var table = this;
6359		if (!this.api) {
6360			this.api = $('#activitylogtable').DataTable({
6361				'ajax': function (data, callback, settings) {
6362					ajaxJsonGet('/api/isy/activity', $.param({ uuid: table.currentUuid, domain: table.currentDomain }))
6363					.done(function(data, status, jqXHR){
6364						callback(data);
6365					});
6366				},
6367				'columns': [
6368					{ data: 'time', render: this.renderTime },
6369					{ data: 'cat', render: this.renderCat },
6370					{ data: 'level', render: this.renderLevel },
6371					{ data: 'message' }
6372				],
6373				'order': [[ 0, 'desc' ]], //  Last activity first in the table
6374				'drawCallback': function (settings, json) {
6375					// API might not be initialized yet, page may display 'Loading...'
6376					if (table.api) {
6377						table.completeUI();
6378					}
6379				}
6380			});
6381		}
6382	},
6383
6384	reload: function () {
6385		var table = this;
6386
6387		if (!table.api) {
6388			table.load();
6389		}
6390		else {
6391			table.api.ajax.reload(null, false);
6392		}
6393	},
6394	setUuid: function (uuid, domain, forceReload) {
6395		if (this.currentUuid!==uuid || forceReload) {
6396			this.currentUuid = uuid;
6397			this.currentDomain = domain;
6398			this.reload();
6399		}
6400	}
6401};
6402
6403
6404var tableUdComms = {
6405	api: null,
6406	provider: null,
6407
6408	refresh: function () {
6409		this.load();
6410	},
6411
6412	completeUI : function() {
6413		var table = this;
6414		table.api.columns.adjust();
6415
6416		// Delete existing message
6417		$('#ud-comms-table .delbutton').unbind('click').click(function () {
6418			var code = table.api.cell($(this).closest('tr'), 'code:name').data();
6419
6420			confirmationDialog('Message will be removed. If notifications were sent, users will no longer be able to read the content.<br><br>Are you sure?', 'Remove Message?')
6421			.then(function () {
6422				ajaxJsonDelete('/api/ud/communication', $.param( { code:code } ))
6423				.done(function () {
6424					table.refresh();
6425				})
6426				.fail(function(jqXHR, textStatus, errorThrown) {
6427					if (jqXHR.status===425) { // Validation error message
6428						errorMessageDialog(jqXHR.responseText, 'Error', 350);
6429					}
6430					else {
6431						errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350);
6432					}
6433				})
6434				.done();
6435			})
6436			.done();
6437		});
6438
6439		// Edit existing option
6440		$('#ud-comms-table .code').click(function () {
6441			var code = table.api.cell($(this).closest('tr'), 'code:name').data();
6442			var message = table.api.cell($(this).closest('tr'), 'message:name').data();
6443			var type = table.api.cell($(this).closest('tr'), 'type:name').data();
6444			var contentPush = table.api.cell($(this).closest('tr'), 'contentPush:name').data();
6445			var contentSurvey = table.api.cell($(this).closest('tr'), 'contentSurvey:name').data();
6446			var contentAppLink = table.api.cell($(this).closest('tr'), 'contentAppLink:name').data();
6447			var sentCount = table.api.cell($(this).closest('tr'), 'sentCount:name').data();
6448			var lastSentTime = table.api.cell($(this).closest('tr'), 'lastSentTime:name').data();
6449			var filterLicense = table.api.cell($(this).closest('tr'), 'filterLicense:name').data();
6450			var filterLicenseExpiryDays = table.api.cell($(this).closest('tr'), 'filterLicenseExpiryDays:name').data();
6451			var filterModel = table.api.cell($(this).closest('tr'), 'filterModel:name').data();
6452			var filterVersion = table.api.cell($(this).closest('tr'), 'filterVersion:name').data();
6453			var filterOnline = table.api.cell($(this).closest('tr'), 'filterOnline:name').data();
6454			var filterOnlineHours = table.api.cell($(this).closest('tr'), 'filterOnlineHours:name').data();
6455			var separateUuid = table.api.cell($(this).closest('tr'), 'separateUuid:name').data();
6456			var accountWithIsy = table.api.cell($(this).closest('tr'), 'accountWithIsy:name').data();
6457
6458			udCommsEditDialog.setFields({ editMode: true, code:code,
6459				message: message, type: type, contentPush: contentPush, contentSurvey: contentSurvey, contentAppLink: contentAppLink,
6460				filterLicense: filterLicense, filterLicenseExpiryDays: filterLicenseExpiryDays, filterModel: filterModel, filterVersion: filterVersion,
6461				filterOnline: filterOnline, filterOnlineHours: filterOnlineHours, separateUuid: separateUuid, accountWithIsy: accountWithIsy,
6462				sentCount: sentCount, lastSentTime: lastSentTime
6463			});
6464			udCommsEditDialog.modal.dialog('open');
6465		});
6466
6467		// Setup the tool button
6468		$('#ud-comms-table .toolMenuButton').button({
6469			width:'12em',
6470			label: 'Select tool...',
6471			icons: { secondary: "ui-icon-triangle-1-s" }
6472		})
6473		.unbind('click')
6474		.click(function (event) {
6475			$('#ud-comms-table .toolMenu').not($(this).next()).hide(); // Close all other menus
6476			$(this).next()
6477			.toolMenu('collapseAllNow', null, true ) // Start with collapsed menus
6478			.toggle(); // Toggle the active one
6479			event.stopPropagation(); // Otherwise, it won't open
6480		});
6481
6482		// Setup the tool menu underneath the button
6483		$('#ud-comms-table .toolMenu').toolMenu({
6484			width:'14em',
6485			//		position: { my: "left top", at: "left+10 bottom" }, // Position of submenu relative to parent
6486			select: function(event, ui) {
6487				var tool = ui.item.attr('value');
6488				var code = table.api.cell($(this).closest('tr'), 'code:name').data();
6489				var contentSurvey = table.api.cell($(this).closest('tr'), 'contentSurvey:name').data();
6490
6491				switch(tool) {
6492					case 'chart':
6493						$(this).hide(); // For some reason, we need this, otherwise the menu item stays open.
6494						udCommsDeliveryDialog.setFields({ code: code });
6495						udCommsDeliveryDialog.modal.dialog('open');
6496						break;
6497
6498					case 'surveyChart':
6499						$(this).hide(); // For some reason, we need this, otherwise the menu item stays open.
6500						udCommsSurveyDialog.setFields({ code: code, contentSurvey: contentSurvey });
6501						udCommsSurveyDialog.modal.dialog('open');
6502						break;
6503
6504					case 'surveyDownload':
6505						$(this).hide(); // For some reason, we need this, otherwise the menu item stays open.
6506						ajaxJsonGet('/api/ud/survey/download', $.param({ code: code }))
6507						.then(function(data, status, jqXHR) {
6508							try {
6509								var isFileSaverSupported = !!new Blob;
6510							} catch (e) {}
6511
6512							if (isFileSaverSupported) {
6513								var BOM = '\uFEFF';
6514								saveAs(new Blob([BOM + data.data], {type: "application/csv;charset=utf-8"}), 'survey ' + code + '.csv');
6515							}
6516							else {
6517								errorMessageDialog('Feature not supported on this browser', 'Not supported');
6518							}
6519						})
6520						.fail(function(jqXHR, textStatus, errorThrown) {
6521							updateTips(tips, 'HTTP error ' + jqXHR.status);
6522						})
6523						.done();
6524						break;
6525
6526					default:
6527				}
6528			}
6529		})
6530		.hide()
6531		.addClass('ui-front');
6532	},
6533
6534	renderCode: function (data, type, row, meta) {
6535		return '<span class="delbutton"><span class="ui-icon ui-icon-closethick"></span></span><span class="code editable_link">' + (data && data.length? data : '(Not found error msg)') + '</span>';
6536	},
6537
6538	renderMessage: function (data, type, row, meta) {
6539		var keepLen = 40
6540		return '<span class="code editable_link">' + data.substring(0, keepLen) + (data.length > keepLen ? '...':'') + '</span>';
6541	},
6542
6543	renderType: function (t) {
6544		var types = {
6545			push: 'Push',
6546			survey: 'Survey',
6547			appLink: 'App Link'
6548		}
6549		return types[t] || t
6550	},
6551
6552	renderTime: function (t) {
6553		return dtStr(new Date(t))
6554	},
6555
6556	renderSent: function (t) {
6557		if (!t) {
6558			return 'Not sent'
6559		}
6560
6561		return tableUdComms.renderTime(t)
6562	},
6563
6564	renderTools: function (data, type, row, table) {
6565		var isSurvey = (row.type === 'survey');
6566
6567		return	'<div><button class="toolMenuButton"></button>' +
6568			'<ul class="toolMenu">' +
6569			'<li value="chart">View delivery</li>' +
6570			(isSurvey ? ('<li>Survey<ul>' +
6571				'<li value="surveyChart">View results</li>' +
6572				'<li value="surveyDownload">Download results</li>' +
6573				'</ul></li>'):'') +
6574			'</ul>'
6575	},
6576
6577	load: function () {
6578		var table = this;
6579
6580		if (!this.api) {
6581			this.api = $('#ud-comms-table').DataTable({
6582				'ajax': function (data, callback, settings) {
6583					ajaxJsonGet('/api/ud/communication')
6584					.done(function(data, status, jqXHR){
6585						callback(data);
6586					});
6587				},
6588				'columns': [
6589					{ data: 'code', render: this.renderCode, name: 'code' },
6590					{ data: 'message', render: this.renderMessage, name: 'message' },
6591					{ data: 'type', render: this.renderType, name: 'type' },
6592					{ data: 'createdTime', render: this.renderTime, name: 'createdTime' },
6593					{ data: 'lastSentTime', render: this.renderSent, name: 'lastSentTime', defaultContent: '' },
6594					{ render: function (d, t, r) { return table.renderTools(d, t, r, table) }, orderable: false, searchable: false },
6595					{ data: 'contentPush', visible:false, name: 'contentPush' },
6596					{ data: 'contentSurvey', visible:false, name: 'contentSurvey' },
6597					{ data: 'contentAppLink', visible:false, name: 'contentAppLink' },
6598					{ data: 'sentCount', visible:false, name: 'sentCount', defaultContent: 0 },
6599					{ data: 'filterLicense', visible:false, name: 'filterLicense', defaultContent: '' },
6600					{ data: 'filterLicenseExpiryDays', visible:false, name: 'filterLicenseExpiryDays' },
6601					{ data: 'filterModel', visible:false, name: 'filterModel', defaultContent: '' },
6602					{ data: 'filterVersion', visible:false, name: 'filterVersion', defaultContent: '' },
6603					{ data: 'filterOnline', visible:false, name: 'filterOnline', defaultContent:'' },
6604					{ data: 'filterOnlineHours', visible:false, name: 'filterOnlineHours' },
6605					{ data: 'separateUuid', visible:false, name: 'separateUuid', defaultContent: false },
6606					{ data: 'accountWithIsy', visible:false, name: 'accountWithIsy', defaultContent: true }
6607				],
6608				'order': [[ 3, 'desc' ]], // Default sort by created time, recent on top
6609				'drawCallback': function (settings, json) {
6610					// API might not be initialized yet, page may display 'Loading...'
6611					if (table.api) {
6612						table.completeUI();
6613					}
6614				}
6615			});
6616		} else {
6617			table.api.ajax.reload(null, false);
6618		}
6619	},
6620	reload: function () {
6621		var table = this;
6622
6623		if (!table.api) {
6624			table.load();
6625		}
6626		else {
6627			table.api.ajax.reload(null, false);
6628		}
6629	}
6630};
6631
6632
6633
6634var tableNsOccupancy = {
6635	api: null,
6636	currentUuid: null,
6637	currentDomain: null,
6638	locationsPromise: null,
6639
6640	addressRight: function (address) {
6641		return address.substring(address.indexOf('_')+1)
6642	},
6643
6644	// Triggered when json data has loaded (before table draw)
6645	onDataLoad:	function (nodes) {
6646		var locations = [];
6647
6648		// Build list of locations. Used in dropdown to select location.
6649		nodes.data.forEach(function(node) {
6650			if (node.isLocation) {
6651				locations.push(node)
6652			}
6653		});
6654
6655		// Set the name of the location, used here to display the name along with the location
6656		nodes.data.forEach(function(node) {
6657			if (!node.isLocation && node.primary!==node.address) {
6658				locations.forEach(function(location) {
6659					if (node.primary === location.address) {
6660						node.primaryName = location.name;
6661					}
6662				});
6663
6664				if (!node.primaryName) {
6665					node.primaryName = '[Not Found]';
6666				}
6667			}
6668		})
6669	},
6670
6671	completeUI: function () {
6672		var table = this;
6673		table.api.columns.adjust();
6674
6675//		$('#nsoccupancytable').tooltip();
6676
6677		// Delete geofence
6678		$('#nsoccupancytable .delbutton').unbind('click').click(function (e) {
6679			var pk = table.api.cell($(this).closest('tr'), 'pk:name').data();
6680			var isLocation = table.api.cell($(this).closest('tr'), 'isLocation:name').data();
6681
6682			if (isLocation) {
6683				confirmationDialog('Are you sure?', 'Remove location?')
6684				.then(function () {
6685					ajaxJsonDelete('/api/nsmodule/' + table.currentUuid + '/occupancy/location', $.param({id: pk, uuid: table.currentUuid, domain: table.currentDomain }))
6686					.fail(function(jqXHR, textStatus, errorThrown) {
6687						if (jqXHR.status==503) {
6688							errorMessageDialog('ISY is not online')
6689						}
6690						else if (jqXHR.status==400) {
6691							errorMessageDialog('This location has children. Please remove them first.')
6692						}
6693						else {
6694							errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
6695						}
6696					})
6697					.done(function () {
6698						tableNsOccupancy.reload();
6699						table.locationsPromise = nsOccupancyDialog.loadLocations(table.currentUuid)
6700					})
6701				})
6702				.done();
6703			}
6704			else {
6705				confirmationDialog('Are you sure?', 'Remove Geofence?')
6706				.then(function () {
6707					ajaxJsonDelete('/api/nsmodule/' + table.currentUuid + '/occupancy/node', $.param({id: pk, uuid: table.currentUuid, domain: table.currentDomain }))
6708					.fail(function(jqXHR, textStatus, errorThrown) {
6709						if (jqXHR.status==503) {
6710							errorMessageDialog('ISY is not online')
6711						}
6712						else {
6713							errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
6714						}
6715					})
6716					.done(function () {
6717						tableNsOccupancy.reload();
6718
6719						table.locationsPromise = nsOccupancyDialog.loadLocations(table.currentUuid)
6720					})
6721				})
6722				.done();
6723			}
6724		});
6725
6726		// Show Locative URL to use
6727		$('#nsoccupancytable .linkbutton').unbind('click').click(function (e) {
6728			var key = table.api.cell($(this).closest('tr'), 'key:name').data();
6729			nsOccupancyUrlDialog.setFields(location.hostname, key);
6730			nsOccupancyUrlDialog.modal.dialog('open');
6731
6732		});
6733
6734		// Edit existing device name
6735		$('#nsoccupancytable .occname').click(function () {
6736
6737			var pk = table.api.cell($(this).closest('tr'), 'pk:name').data();
6738			var name = table.api.cell($(this).closest('tr'), 'name:name').data();
6739			var location = table.api.cell($(this).closest('tr'), 'primary:name').data();
6740			var address = table.api.cell($(this).closest('tr'), 'address:name').data();
6741
6742			table.locationsPromise
6743			.done(function(locations) {
6744				nsOccupancyEditDialog.setFields({ uuid: table.currentUuid, name: name, location: (location!==address?location:'$nolocation'), pk: pk, editMode: true, locations: locations.data });
6745				nsOccupancyEditDialog.modal.dialog('open');
6746			})
6747			.fail(function(jqXHR, textStatus, errorThrown) {
6748				errorMessageDialog('Error loading locations', 'Error', 350)
6749			})
6750		});
6751
6752		// Edit existing location name
6753		$('#nsoccupancytable .locname').click(function () {
6754
6755			var pk = table.api.cell($(this).closest('tr'), 'pk:name').data();
6756			var name = table.api.cell($(this).closest('tr'), 'name:name').data();
6757
6758			nsOccupancyLocationEditDialog.setFields({ uuid: table.currentUuid, name: name, pk: pk, editMode: true });
6759			nsOccupancyLocationEditDialog.modal.dialog('open');
6760		});
6761	},
6762
6763	renderNameWithDelete: function (data, type, row, meta) {
6764		if (row.isLocation) {
6765			return 	'<span class="delbutton"><span class="ui-icon ui-icon-closethick"></span></span><span class="linkbuttonspacer"></span><span class="locname editable_link">' + data + '</span>';
6766		}
6767		else {
6768			if (row.primaryName) {
6769				return 	'<span class="delbutton"><span class="ui-icon ui-icon-closethick"></span></span><span class="linkbutton"><span class="ui-icon ui-icon-extlink"></span></span>' + row.primaryName + '/<span class="occname editable_link">' + data + '</span>';
6770			}
6771			else {
6772				return 	'<span class="delbutton"><span class="ui-icon ui-icon-closethick"></span></span><span class="linkbutton"><span class="ui-icon ui-icon-extlink"></span></span><span class="occname editable_link">' + data + '</span>';
6773			}
6774		}
6775	},
6776
6777	renderValue: function (data, type, row, meta) {
6778		return 	typeof data == 'boolean'?(data?'In':'Out'):data; // Data can be a string set to "Unknown", if variable not present.
6779	},
6780
6781	load: function () {
6782		var table = this;
6783		if (!this.api) {
6784			this.api = $('#nsoccupancytable').DataTable({
6785				'ajax': function (data, callback, settings) {
6786					ajaxJsonGet('/api/nsmodule/' + table.currentUuid + '/occupancy/nodes', $.param({ domain: table.currentDomain }))
6787					.done(function(data, status, jqXHR){
6788						table.onDataLoad(data);
6789						callback(data);
6790					});
6791				},
6792				'columns': [
6793					{ data: 'name', defaultContent: '', render: this.renderNameWithDelete, name: 'name' },
6794					{ data: 'key', defaultContent: '', name: 'key' },
6795					{ data: 'address', defaultContent: '', name: 'address' },
6796					{ data: 'props.ST.value', defaultContent: 'Unknown', render: this.renderValue },
6797					{ data: 'primary', defaultContent: '', name: 'primary', visible: false },
6798					{ data: 'isLocation', defaultContent: false, name: 'isLocation', visible: false },
6799					{ data: '_id', visible: false, searchable: false, name: 'pk' }
6800				],
6801				'drawCallback': function (settings, json) {
6802					// API might not be initialized yet, page may display 'Loading...'
6803					if (table.api) {
6804						table.completeUI();
6805					}
6806				}
6807			});
6808		}
6809	},
6810
6811	reload: function () {
6812		var table = this;
6813
6814		if (!table.api) {
6815			table.load();
6816		}
6817		else {
6818			table.api.ajax.reload(null, false);
6819		}
6820	},
6821
6822	setUuid: function (uuid, domain, forceReload) {
6823		if (this.currentUuid!==uuid || forceReload) {
6824			this.currentUuid = uuid;
6825			this.currentDomain = domain;
6826			this.reload();
6827		}
6828	},
6829	setLocations: function (locationsPromise) {
6830		this.locationsPromise = locationsPromise;
6831	},
6832};
6833
6834var tableNsOccupancy2Geofence = {
6835	api: null,
6836	currentUuid: null,
6837	currentDomain: null,
6838    geofencesPromise: null,
6839    devicesPromise: null,
6840
6841	addressRight: function (address) {
6842		return address.substring(address.indexOf('_')+1)
6843	},
6844
6845	// Triggered when json data has loaded (before table draw)
6846	onDataLoad:	function (nodes) {
6847		var geofences = [];
6848
6849		// Build list of geofences. Used in dropdown to select geofence.
6850		nodes.data.forEach(function(node) {
6851			if (node.isGeofence) {
6852				geofences.push(node)
6853			}
6854		});
6855
6856		// Set the name of the geofence, used here to display the name along with the geofence
6857		nodes.data.forEach(function(node) {
6858			if (!node.isGeofence && node.primary!==node.address) {
6859				geofences.forEach(function(geofence) {
6860					if (node.primary === geofence.address) {
6861						node.primaryName = geofence.name;
6862					}
6863				});
6864
6865				if (!node.primaryName) {
6866					node.primaryName = '[Not Found]';
6867				}
6868			}
6869		})
6870	},
6871
6872	completeUI: function () {
6873		var table = this;
6874		table.api.columns.adjust();
6875
6876		// Delete geofence
6877		$('#nsoccupancy2geofencetable .delbutton').unbind('click').click(function (e) {
6878			var pk = table.api.cell($(this).closest('tr'), 'pk:name').data();
6879			var isGeofence = table.api.cell($(this).closest('tr'), 'isGeofence:name').data();
6880
6881			if (isGeofence) {
6882				confirmationDialog('Are you sure?', 'Remove Geofence?')
6883				.then(function () {
6884					ajaxJsonDelete('/api/nsmodule/' + table.currentUuid + '/occupancy2/geofence', $.param({id: pk, uuid: table.currentUuid, domain: table.currentDomain }))
6885					.fail(function(jqXHR, textStatus, errorThrown) {
6886						if (jqXHR.status==503) {
6887							errorMessageDialog('ISY is not online')
6888						}	else if (jqXHR.status==400) {
6889							errorMessageDialog('This geofence has assigned mobile devices. Please unassign them first.')
6890						} else {
6891							errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
6892						}
6893					})
6894					.done(function () {
6895						tableNsOccupancy2Geofence.reload();
6896						table.geofencesPromise = nsOccupancy2Dialog.loadGeofences(table.currentUuid)
6897					})
6898				})
6899				.done();
6900			} else {
6901				confirmationDialog('Are you sure?', 'Unassign Mobile Device?')
6902				.then(function () {
6903					ajaxJsonDelete('/api/nsmodule/' + table.currentUuid + '/occupancy2/node', $.param({id: pk, uuid: table.currentUuid, domain: table.currentDomain }))
6904					.fail(function(jqXHR, textStatus, errorThrown) {
6905						if (jqXHR.status==503) {
6906							errorMessageDialog('ISY is not online')
6907						}	else if (jqXHR.status==403) {
6908							errorMessageDialog('There is no Mobile Device assigned on ISY, please try "Synchronize" option below before unassigning.<br><br>In the case you want to start from scratch, please use  "Remove" option bellow.')
6909						} else {
6910							errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
6911						}
6912					})
6913					.done(function () {
6914						tableNsOccupancy2Geofence.reload();
6915						table.geofencesPromise = nsOccupancy2Dialog.loadGeofences(table.currentUuid)
6916					})
6917				})
6918				.done();
6919			}
6920		});
6921
6922		// Show Locative URL to use
6923		$('#nsoccupancy2geofencetable .linkbutton').unbind('click').click(function (e) {
6924			var key = table.api.cell($(this).closest('tr'), 'key:name').data();
6925			nsOccupancy2GeofenceUrlDialog.setFields(location.hostname, key);
6926			nsOccupancy2GeofenceUrlDialog.modal.dialog('open');
6927		});
6928
6929		// Edit existing device name
6930		$('#nsoccupancy2geofencetable .occname').click(function () {
6931			var pk = table.api.cell($(this).closest('tr'), 'pk:name').data();
6932			var geofence = table.api.cell($(this).closest('tr'), 'primary:name').data();
6933			var address = table.api.cell($(this).closest('tr'), 'address:name').data();
6934			var device = table.api.cell($(this).closest('tr'), 'deviceAddress:name').data();
6935
6936			table.geofencesPromise
6937			.done(function(geofences) {
6938				table.devicesPromise
6939				.done(function(devices) {
6940					nsOccupancy2EditDialog.setFields({ uuid: table.currentUuid, device: device, geofence: (geofence!==address?geofence:'$nogeofence'), pk: pk, editMode: true, geofences: geofences.data, devices: devices.data });
6941					nsOccupancy2EditDialog.modal.dialog('open');
6942				})
6943				.fail(function(jqXHR, textStatus, errorThrown) {
6944					errorMessageDialog('Error loading geofences', 'Error', 350)
6945				})
6946			})
6947			.fail(function(jqXHR, textStatus, errorThrown) {
6948				errorMessageDialog('Error loading geofences', 'Error', 350)
6949			})
6950		});
6951
6952		// Edit existing geofence name
6953		$('#nsoccupancy2geofencetable .locname').click(function () {
6954			var pk = table.api.cell($(this).closest('tr'), 'pk:name').data();
6955			var name = table.api.cell($(this).closest('tr'), 'name:name').data();
6956			nsOccupancy2GeofenceEditDialog.setFields({ uuid: table.currentUuid, name: name, pk: pk, editMode: true });
6957			nsOccupancy2GeofenceEditDialog.modal.dialog('open');
6958		});
6959	},
6960
6961	renderNameWithDelete: function (data, type, row, meta) {
6962		if (row.isGeofence) {
6963			return 	'<span class="delbutton"><span class="ui-icon ui-icon-closethick"></span></span><span class="linkbuttonspacer"></span><span class="locname editable_link">' + data + '</span>';
6964		} else {
6965			if (row.primaryName) {
6966				return 	'<span class="delbutton"><span class="ui-icon ui-icon-closethick"></span></span><span class="linkbutton"><span class="ui-icon ui-icon-extlink"></span></span>' + row.primaryName + '/<span class="occname editable_link">' + data + '</span>';
6967			}
6968			else {
6969				return 	'<span class="delbutton"><span class="ui-icon ui-icon-closethick"></span></span><span class="linkbutton"><span class="ui-icon ui-icon-extlink"></span></span><span class="occname editable_link">' + data + '</span>';
6970			}
6971		}
6972	},
6973
6974	renderValue: function (data, type, row, meta) {
6975		return 	typeof data == 'boolean'?(data?'In':'Out'):data; // Data can be a string set to "Unknown", if variable not present.
6976	},
6977
6978	load: function () {
6979		var table = this;
6980		if (!this.api) {
6981			this.api = $('#nsoccupancy2geofencetable').DataTable({
6982				'ajax': function (data, callback, settings) {
6983										ajaxJsonGet('/api/nsmodule/' + table.currentUuid + '/occupancy2/nodes', $.param({ domain: table.currentDomain }))
6984					.done(function(data, status, jqXHR){
6985						table.onDataLoad(data);
6986												table.devicesPromise = nsOccupancy2Dialog.loadDevices(table.currentUuid);
6987						callback(data);
6988					});
6989				},
6990				'columns': [
6991					{ data: 'name', defaultContent: '', render: this.renderNameWithDelete, name: 'name' },
6992					{ data: 'key', defaultContent: '', name: 'key' },
6993					{ data: 'address', defaultContent: '', name: 'address' },
6994					{ data: 'props.ST.value', defaultContent: 'Unknown', render: this.renderValue },
6995					{ data: 'primary', defaultContent: '', name: 'primary', visible: false },
6996										{ data: 'isGeofence', defaultContent: false, name: 'isGeofence', visible: false },
6997										{ data: 'deviceAddress', defaultContent: '', name: 'deviceAddress', visible: false },
6998					{ data: '_id', visible: false, searchable: false, name: 'pk' }
6999				],
7000				'drawCallback': function (settings, json) {
7001					// API might not be initialized yet, page may display 'Loading...'
7002					if (table.api) {
7003						table.completeUI();
7004					}
7005				}
7006			})
7007		}
7008	},
7009
7010	reload: function () {
7011		var table = this;
7012		if (!table.api) {
7013			table.load();
7014		} else {
7015			table.api.ajax.reload(null, false);
7016		}
7017	},
7018
7019	setUuid: function (uuid, domain, forceReload) {
7020		if (this.currentUuid!==uuid || forceReload) {
7021			this.currentUuid = uuid;
7022			this.currentDomain = domain;
7023			this.reload();
7024		}
7025	},
7026
7027	setGeofences: function (geofencesPromise) {
7028		this.geofencesPromise = geofencesPromise;
7029	},
7030
7031	setDevices: function (devicesPromise) {
7032		this.devicesPromise = devicesPromise;
7033	},
7034};
7035
7036var tableNsOccupancy2Device = {
7037    api: null,
7038    currentUuid: null,
7039    currentDomain: null,
7040    devicesPromise: null,
7041
7042    addressRight: function (address) {
7043        return address.substring(address.indexOf('_')+1)
7044    },
7045
7046    // Triggered when json data has loaded (before table draw)
7047    onDataLoad:	function (devices) {
7048    },
7049
7050    completeUI: function () {
7051        var table = this;
7052        table.api.columns.adjust();
7053
7054        // Delete Mobile Device
7055        $('#nsoccupancy2devicetable .delbutton').unbind('click').click(function (e) {
7056            var pk = table.api.cell($(this).closest('tr'), 'pk:name').data();
7057            confirmationDialog('Are you sure?', 'Remove Mobile Device?')
7058            .then(function () {
7059                ajaxJsonDelete('/api/nsmodule/' + table.currentUuid + '/occupancy2/device', $.param({id: pk, uuid: table.currentUuid, domain: table.currentDomain }))
7060                .fail(function(jqXHR, textStatus, errorThrown) {
7061                    if (jqXHR.status==503) {
7062                        errorMessageDialog('ISY is not online')
7063                    }
7064                    else if (jqXHR.status==400) {
7065                        errorMessageDialog('This mobile device has assigned geofences. Please unassign them first.')
7066                    }
7067                    else {
7068                        errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
7069                    }
7070                })
7071                .done(function () {
7072                    tableNsOccupancy2Device.reload();
7073                    table.devicesPromise = nsOccupancy2Dialog.loadDevices(table.currentUuid)
7074                })
7075            })
7076            .done();
7077
7078        });
7079
7080        // Link Mobile Device with UDI Mobile
7081        $('#nsoccupancy2devicetable .linkbutton').unbind('click').click(function (e) {
7082            var pk = table.api.cell($(this).closest('tr'), 'pk:name').data();
7083            var pinCode = table.api.cell($(this).closest('tr'), 'pinCode:name').data();
7084            var pinValid = table.api.cell($(this).closest('tr'), 'pinValid:name').data();
7085            var name = table.api.cell($(this).closest('tr'), 'name:name').data();
7086            if(!pinCode){
7087                pinCode = '';
7088                pinValid = '';
7089            }
7090
7091            nsOccupancy2DevicePinDialog.setFields({uuid: table.currentUuid, pk: pk, name: name, pinCode: pinCode, pinValid: pinValid});
7092            nsOccupancy2DevicePinDialog.modal.dialog('open');
7093        });
7094
7095        // Unlink Mobile Device with UDI Mobile
7096        $('#nsoccupancy2devicetable .unlinkbutton').unbind('click').click(function (e) {
7097            var key = table.api.cell($(this).closest('tr'), 'key:name').data();
7098            var token = table.api.cell($(this).closest('tr'), 'token:name').data();
7099            confirmationDialog('Please confirm if you want to Unlink this Mobile Device with UDI Mobile.<br><br>WARNING: UDI Mobile would not report any geofence status for this Mobile Device after this process.', 'Unlink Mobile Device')
7100            .then(function () {
7101                ajaxJsonDelete('/api/nsmodule/' + table.currentUuid + '/occupancy2/device/' + key + '/link','',{apitoken: token})
7102                .fail(function(jqXHR, textStatus, errorThrown) {
7103                    if (jqXHR.status==503) {
7104                        errorMessageDialog('ISY is not online')
7105                    }
7106                    else if (jqXHR.status==400) {
7107                        errorMessageDialog(jqXHR.responseText)
7108                    }
7109                    else {
7110                        errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
7111                    }
7112                })
7113                .done(function () {
7114                    tableNsOccupancy2Device.reload();
7115                    table.devicesPromise = nsOccupancy2Dialog.loadDevices(table.currentUuid)
7116                })
7117            })
7118            .done()
7119        });
7120
7121        // Edit existing Mobile Device name
7122        $('#nsoccupancy2devicetable .devname').click(function () {
7123
7124            var pk = table.api.cell($(this).closest('tr'), 'pk:name').data();
7125            var name = table.api.cell($(this).closest('tr'), 'name:name').data();
7126            var profile = table.api.cell($(this).closest('tr'), 'profile:name').data();
7127
7128            nsOccupancy2DeviceEditDialog.setFields({ uuid: table.currentUuid, name: name, profile: profile, pk: pk, editMode: true });
7129            nsOccupancy2DeviceEditDialog.modal.dialog('open');
7130        });
7131	},
7132
7133    renderNameWithDelete: function (data, type, row, meta) {
7134        return 	'<span class="delbutton"><span class="ui-icon ui-icon-closethick"></span></span><span class="devname editable_link">' + data + '</span>';
7135	},
7136
7137    renderValue: function (data, type, row, meta) {
7138		return 	typeof data == 'boolean'?(data?'Yes':'No'):data; // Data can be a string set to "Unknown", if variable not present.
7139	},
7140
7141    renderToken: function (data, type, row, meta) {
7142        var linked = (data!='');
7143        return 	(linked?'<span class="unlinkbutton"><span class="ui-icon ui-icon-closethick"></span></span>Yes':'<span class="linkbutton"><span class="ui-icon ui-icon-extlink"></span></span><span class="linkbutton editable_link">No</span>');
7144	},
7145
7146    load: function () {
7147        var table = this;
7148        if (!this.api) {
7149            this.api = $('#nsoccupancy2devicetable').DataTable({
7150                'ajax': function (data, callback, settings) {
7151                    ajaxJsonGet('/api/nsmodule/' + table.currentUuid + '/occupancy2/devices', $.param({ domain: table.currentDomain }))
7152                    .done(function(data, status, jqXHR){
7153                        table.onDataLoad(data);
7154                        callback(data);
7155                    });
7156	},
7157                'columns': [
7158                    { data: 'name', defaultContent: '', render: this.renderNameWithDelete, name: 'name' },
7159                    { data: 'key', defaultContent: '', name: 'key' },
7160                    { data: 'address', defaultContent: '', name: 'address' },
7161                    { data: 'token', defaultContent: '', render: this.renderToken, name: 'token'  },
7162                    { data: 'profile', defaultContent: '', render: this.renderValue, name: 'profile' },
7163                    { data: 'pinCode', defaultContent: '', visible: false, name: 'pinCode' },
7164                    { data: 'pinValid', defaultContent: '', visible: false, name: 'pinValid' },
7165					{ data: '_id', visible: false, searchable: false, name: 'pk' }
7166                ],
7167                'drawCallback': function (settings, json) {
7168                    // API might not be initialized yet, page may display 'Loading...'
7169                    if (table.api) {
7170                        table.completeUI();
7171}
7172}
7173            });
7174}
7175    },
7176
7177    reload: function () {
7178        var table = this;
7179        if (!table.api) {
7180            table.load();
7181        }
7182        else {
7183            table.api.ajax.reload(null, false);
7184        }
7185    },
7186
7187    setUuid: function (uuid, domain, forceReload) {
7188        if (this.currentUuid!=uuid || forceReload) {
7189            this.currentUuid = uuid;
7190            this.currentDomain = domain;
7191            this.reload();
7192        }
7193    },
7194    setDevices: function (devicesPromise) {
7195        this.devicesPromise = devicesPromise;
7196    },
7197};
7198
7199// **********************************************************************
7200// ***************************** Dialogs ********************************
7201// **********************************************************************
7202
7203// ----------------- Generic dialogs -------------------------
7204
7205// We store temporary dialog handles so we can close them if we autoLogout
7206function addTempDialog(dialog) {
7207	tempDialogs.push(dialog);
7208}
7209
7210function removeTempDialog(dialog) {
7211	tempDialogs = tempDialogs.filter(function(dialogItem) {
7212		return dialogItem !== dialog;
7213	})
7214}
7215
7216
7217// Generic error message dialog
7218function errorMessageDialog(message, title, width) {
7219	var defer = $.Deferred();
7220	var dialog = null;
7221
7222	var dialogOptions = {
7223		autoOpen: true,
7224		modal: true,
7225		width: width?width:450,
7226		buttons: {
7227			'Ok': function() {
7228				defer.resolve();
7229				$(this).dialog('close');
7230			}
7231		},
7232		close: function() {
7233			removeTempDialog(dialog);
7234			defer.reject();
7235		}
7236	};
7237
7238	var html = '<div title="' + (title?title:"Error") + '"><p autofocus>' +
7239		'<table><tr><td style="vertical-align:top"><span class="ui-icon ui-icon-alert" style="margin-right: 7px;"></span></td><td>
7239<span">' + message + '</span></td></tr></table></p></div>';
7240
7241	// Initializes the dialog
7242	dialog = $(html).dialog(dialogOptions);
7243	addTempDialog(dialog);
7244
7245	return defer.promise();
7246}
7247
7248// Generic information message dialog
7249function informationMessageDialog(message, title, width) {
7250	var defer = $.Deferred();
7251	var dialog = null;
7252
7253	var dialogOptions = {
7254		autoOpen: true,
7255		modal: true,
7256		width: width?width:450,
7257		buttons: {
7258			'Ok': function() {
7259				defer.resolve();
7260				$(this).dialog('close');
7261			}
7262		},
7263		close: function() {
7264			removeTempDialog(dialog);
7265			defer.reject();
7266		}
7267	};
7268
7269	var html = '<div title="' + (title?title:'Information') + '"><p autofocus><span>' + message + '</span></p></div>';
7270
7271	dialog = $(html).dialog(dialogOptions);
7272	addTempDialog(dialog);
7273
7274	return defer.promise();
7275}
7276
7277// Generic error message dialog
7278function confirmationDialog(message, title, width) {
7279	var defer = $.Deferred();
7280	var dialog = null;
7281
7282	var dialogOptions = {
7283		autoOpen: true,
7284		modal: true,
7285		width: width?width:450,
7286		buttons: {
7287			'Yes': function() {
7288				defer.resolve();
7289				$(this).dialog('close');
7290			},
7291			Cancel: function() {
7292				defer.reject();
7293				$(this).dialog('close');
7294			}
7295		},
7296		close: function() {
7297			removeTempDialog(dialog);
7298			defer.reject();
7299		}
7300	};
7301
7302	var html = '<div title="' + (title?title:'Please confirm') + '"><p autofocus>' +
7303		'<table><tr><td style="vertical-align:top"><span class="ui-icon ui-icon-alert" style="margin-right: 7px;"></span></td><td><span">' + message + '</span></td></tr></table></p></div>';
7304
7305
7306	dialog = $(html).dialog(dialogOptions);
7307	addTempDialog(dialog);
7308
7309	return defer.promise();
7310}
7311
7312// Dialog which has multiple buttons at the bottom. Each of them resolves the promise with the button text used
7313// buttons is an array of string
7314function optionsDialog(message, title, buttons, width) {
7315	var defer = $.Deferred();
7316
7317	var dialogOptions = {
7318		autoOpen: true,
7319		modal: true,
7320		width: width?width:450,
7321		buttons: Object.fromEntries(buttons.map(function (txt) {
7322			return [
7323				txt,
7324				function() {
7325					defer.resolve(txt);
7326					$(this).dialog('close');
7327				}
7328			]
7329		})),
7330		close: function() {
7331			removeTempDialog(dialog);
7332			defer.reject();
7333		}
7334	};
7335
7336	var html = '<div title="' + (title?title:'Please select an option') + '"><p autofocus>' +
7337		'<table><tr><td style="vertical-align:top"><span class="ui-icon ui-icon-alert" style="margin-right: 7px;"></span></td><td><span">' + message + '</span></td></tr></table></p></div>';
7338
7339	var dialog = $(html).dialog(dialogOptions);
7340	addTempDialog(dialog);
7341
7342	return defer.promise();
7343}
7344
7345// Live message update dialog
7346function liveMessageDialogInit() {
7347	var defer,
7348		dialog;
7349
7350	function setMessage(msg) {
7351		$('#live_message_text').html(msg);
7352	}
7353
7354	function setTitle(title) {
7355		$('#live_message_dialog').attr('title', title);
7356	}
7357
7358	function restartDefer(title) {
7359		defer = $.Deferred();
7360		return defer.promise();
7361	}
7362
7363	var dialogOptions = {
7364		autoOpen: false,
7365		modal: true,
7366		//width: width?width:450,
7367		buttons: {
7368			Cancel: function() {
7369				$(this).dialog('close');
7370			}
7371		},
7372		close: function() {
7373			if (defer) {
7374				defer.reject();
7375			}
7376		}
7377	};
7378
7379	// Initializes the dialog
7380	dialog = $('#live_message_dialog').dialog(dialogOptions);
7381
7382	return { modal: dialog, setTitle: setTitle, setMessage: setMessage, restartDefer: restartDefer };
7383}
7384
7385// Generic pin request dialog
7386function pinDialog(message, length, title, width) {
7387	var defer = $.Deferred();
7388	var dialog = null;
7389	var regx;
7390
7391	function closeDialog() {
7392		// A close will leave the html in the dom
7393		dialog.dialog('destroy').remove();
7394	}
7395
7396	function isValid(pin) {
7397		return regx && regx.test(pin);
7398	}
7399
7400	function submit() {
7401		var pin = $('#dialog_pin_code').val();
7402
7403		if (isValid(pin)) {
7404			// console.log('pin is valid', pin)
7405			defer.resolve(pin);
7406		} else {
7407			// console.log('pin not valid', pin)
7408			defer.reject(pin);
7409		}
7410
7411		closeDialog();
7412	}
7413
7414	var dialogOptions = {
7415		autoOpen: true,
7416		modal: true,
7417		width: width?width:450,
7418		buttons: {
7419			'Submit': function() {
7420				submit();
7421			},
7422			Cancel: function() {
7423				defer.reject();
7424				closeDialog();
7425			}
7426		},
7427		close: function() {
7428			removeTempDialog(dialog);
7429			defer.reject();
7430			closeDialog();
7431		}
7432	};
7433
7434	if (length) {
7435		regx = new RegExp(Array(length).fill('\\d').join(''));
7436	}
7437
7438	var html = '<div class="modal ui-widget-content" title="' + (title?title:'PIN Required') + '"><p>' +
7439		'<table><tr><td style="vertical-align:top"><span class="ui-icon ui-icon-alert" style="margin-right: 7px;"></span></td><td><span">' +
7440		message + '</span></td></tr></table></p>' +
7441		'<form>' +
7442		'<input type="text" ' + (length ? 'maxLength="' + length + '" ' : ' ') + ' name="pin" id="dialog_pin_code" value="" class="text ui-widget-content ui-corner-all">' +
7443		'<input type="submit" class="submit">' +
7444		'</form>' +
7445		'</div>';
7446
7447	dialog = $(html).dialog(dialogOptions);
7448	addTempDialog(dialog);
7449
7450	// Lastpass fix
7451	dialog.find('form input').on('keydown', function(e) {
7452		if (e.keyCode===13) {
7453			event.stopPropagation();
7454			event.preventDefault();
7455			submit();
7456		}
7457	});
7458
7459	// Handles keyboad enter (submit event)
7460	// dialog.find('form').submit(function(e) {
7461	// 	e.preventDefault();
7462	// 	submit();
7463	// });
7464
7465	return defer.promise();
7466}
7467
7468/*
7469<!-- Generic PIN dialog -->
7470<div id="pin_dialog" class="modal ui-widget-content" title="PIN Required" style="display:none;">
7471	<div class="validateTipsContainer"><p class="validateTips"></p></div>
7472	<div>
7473		<form>
7474			<fieldset>
7475				<label for="pin" id="pin_label"></label>
7476				<input type="number" name="pin" id="pin" value="" class="text serialize ui-widget-content ui-corner-all">
7477			</fieldset>
7478		</form>
7479	</div>
7480</div>
7481
7482*/
7483
7484// ------------------- Custom Dialog utilities ----------------------------
7485
7486// Displays error message in 'tips' placeholder
7487function updateTips(tips, txt) {
7488	tips.text(txt);
7489	tips.hide().show('fade');
7490}
7491
7492// Validates if there is a value selected
7493function checkSelection(tips, field, name, text) {
7494	if (field.val() === name ) {
7495		field.addClass('fieldfocus');
7496		field.focus();
7497		updateTips(tips, 'Please select or create a new '+text+'.');
7498		return false;
7499	}
7500	else {
7501		return true;
7502	}
7503}
7504
7505// Validates field length
7506function checkLength(tips, field, name, min, max) {
7507	if (field.val().length > max || field.val().length < min ) {
7508		field.addClass('fieldfocus');
7509		field.focus();
7510		updateTips(tips, name + ' must be between ' + min + ' and ' + max + ' characters.' );
7511		return false;
7512	}
7513	else {
7514		return true;
7515	}
7516}
7517
7518// Max value 2147483647 (int32)
7519
7520
7521function checkNumber(tips, field, name, min, max) {
7522	var value = field.val();
7523
7524	if (!/^-?\d+$/.test(value)) {
7525		field.addClass('fieldfocus');
7526		field.focus();
7527		updateTips(tips, name + ' must be a number.' );
7528		return false;
7529	}
7530	else if (max && (parseInt(value)<min || parseInt(value)>max)) { // (Dont validate min, usually will be 0)
7531		field.addClass('fieldfocus');
7532		field.focus();
7533		updateTips(tips, name + ' must be between ' + min + ' and ' + max + '.');
7534		return false;
7535	}
7536	else {
7537		return true;
7538	}
7539}
7540
7541function checkNumberOrDelta(tips, field, name) {
7542	var value = field.val();
7543
7544	// Allowed: Number, optionnally preceded by + or -
7545	if (!/^[\+\-]*\d+$/.test(value)) {
7546		field.addClass('fieldfocus');
7547		field.focus();
7548		updateTips(tips, name + ' must be a number.' );
7549		return false;
7550	}
7551	else {
7552		return true;
7553	}
7554}
7555
7556function checkFloat(tips, field, name, min, max) {
7557	var value = field.val();
7558
7559	if (!/^(\d|\.)+$/.test(value)) {
7560		field.addClass('fieldfocus');
7561		field.focus();
7562		updateTips(tips, name + ' must be a number.' );
7563		return false;
7564	}
7565	else if (max && (parseFloat(value)<min || parseFloat(value)>max)) { // (Dont validate min, usually will be 0)
7566		field.addClass('fieldfocus');
7567		field.focus();
7568		updateTips(tips, name + ' must be between ' + min + ' and ' + max + '.');
7569		return false;
7570	}
7571	else {
7572		return true;
7573	}
7574}
7575
7576function checkNotEmpty(tips, field, name) {
7577	if (!field.val().length) {
7578		field.addClass('fieldfocus');
7579		field.focus();
7580		updateTips(tips, name + ' is required.');
7581		return false;
7582	}
7583	else {
7584		return true;
7585	}
7586}
7587
7588// Validates against regexp
7589function checkRegexp(tips, field, regexp, txt) {
7590	if (!(regexp.test(field.val()))) {
7591		field.addClass('fieldfocus');
7592		field.focus();
7593		updateTips(tips, txt);
7594		return false;
7595	}
7596	else {
7597		return true;
7598	}
7599}
7600
7601function checkRegexpUuid(tips, o, n) {
7602	var uuid = o.val();
7603
7604	if (!uuidRegex.test(uuid)) {
7605		o.addClass('fieldfocus');
7606		o.focus();
7607		updateTips(tips, n);
7608		return false;
7609	}
7610	else { // Rewrite the regex
7611		o.val(uuid.toLowerCase().replace(uuidRegex, '$1:$2:$3:$4:$5:$6'));
7612		return true;
7613	}
7614}
7615
7616function checkPwd(tips, password) {
7617	var valid=true;
7618
7619	if (password.val()!=='$unchanged') {
7620		valid = valid && checkLength(tips, password, 'password', 8, 20);
7621		valid = valid && checkRegexp(tips, password, passwordRegex, 'Password must have letters and have at least 1 digit or special character');
7622	}
7623
7624	return valid;
7625}
7626
7627//Validates password and passwordconf are equal
7628function checkPwdEqual(tips, p1, p2, txt) {
7629	if ((p1.val().localeCompare(p2.val()))) { // returns 0 if strings equal
7630		p2.addClass('fieldfocus');
7631		p2.focus();
7632		updateTips(tips, txt);
7633		return false;
7634	}
7635	else {
7636		return true;
7637	}
7638}
7639
7640//Validates Radiobutton is selected
7641function checkRadio(tips, selector, txt) {
7642	if ($(selector).length === 0) {
7643		updateTips(tips, txt);
7644		return false;
7645	}
7646	else {
7647		return true;
7648	}
7649}
7650
7651function serializeFormWithCheckboxes($find) {
7652	var data = $find.filter(':not([type="checkbox"])').serialize();
7653
7654	var checkboxes = $find.filter('[type="checkbox"]').map(function() {
7655		return this.name + "=" + this.checked;
7656	})
7657	.get()
7658	.join("&");
7659
7660	if (checkboxes!=="" && data!=="") {
7661		data += "&" + checkboxes;
7662	}
7663	else {
7664		data += checkboxes;
7665	}
7666
7667	return data;
7668}
7669
7670function openMyProfile() {
7671	var isyLookupPromise = ajaxJsonGet('/api/isys/lookup', $.param({domain: registeredUser.domain}))
7672	.fail(function(jqXHR, textStatus, errorThrown) {
7673		errorMessageDialog("Can't retrieve ISY list<br>HTTP error " + jqXHR.status, 'Error', 350)
7674	});
7675
7676	var userProfilePromise = ajaxJsonGet('/api/profile', $.param({domain: registeredUser.domain}))
7677	.fail(function(jqXHR, textStatus, errorThrown) {
7678		errorMessageDialog("Can't retrieve user profile<br>HTTP error " + jqXHR.status, 'Error', 350)
7679	});
7680
7681	$.when(isyLookupPromise, userProfilePromise)
7682	.then(function(isyLookupXHR, userProfileXHR) {
7683		// Alter the data to be able to use it in a selectmenu
7684		var userProfile = userProfileXHR[0]; // The response is the first item in the array
7685		var uuidList = isyLookupXHR[0].data.map(function (item) {
7686			return {
7687				label: item.uuid, // Shown in the dropdown list
7688				value: item.uuid, // Sets the input field to this value when selecting from the dropdown
7689				data: item.uuid,
7690				option: item
7691			}
7692		});
7693
7694		myProfileDialog.setFields({
7695			username: userProfile.username,
7696//			amz_email: registeredUser.amz_email,
7697			uuid: userProfile.preferredIsy,
7698			uuidList: uuidList,
7699			voiceSelector: userProfile.voiceSelector,
7700			notificationmaint: userProfile.notificationMaintenance,
7701			notificationnews: userProfile.notificationNews
7702		});
7703
7704		myProfileDialog.modal.dialog('open');
7705	})
7706	.fail(function() {
7707		console.log('Could not open My Profile')
7708	})
7709	.done()
7710
7711}
7712
7713// Works with uuid, key, or none.
7714function openLicense(uuid, isyKey, redirectUrl) {
7715  var errorMessage;
7716
7717	ajaxJsonGet('/api/isys', $.param({domain: registeredUser.domain, subaccounts:true }))
7718	.done(function(result, status, jqXHR) {
7719		var isysFound;
7720
7721		if (uuid) {
7722			isysFound = result.data.filter(function(isyRecord) {
7723				return isyRecord.uuid === uuid;
7724			})
7725		} else if (isyKey) {
7726			isysFound = result.data.filter(function(isyRecord) {
7727				return isyRecord.isyKey === isyKey;
7728			})
7729		} else {
7730			if (!result.data.length) {
7731				errorMessage = 'You have no ISYs in your account.'
7732
7733				if (redirectUrl) {
7734					redirectWithError(redirectUrl, errorMessage)
7735				} else {
7736					errorMessageDialog(errorMessage)
7737				}
7738			} else if (result.data.length>1) {
7739				errorMessage = 'You have more than one ISY in your account.'
7740
7741				if (redirectUrl) {
7742					redirectWithError(redirectUrl, errorMessage)
7743				} else {
7744					errorMessageDialog(errorMessage + '<br/><br/>In order to see the license information for an ISY, please click on:<br/> Select tool | Information | ISY Portal Access License.', 'Choose ISY', 800)
7745				}
7746			}
7747			else {
7748				isysFound = result.data;
7749			}
7750		}
7751
7752		if (isysFound && !isysFound.length) {
7753			errorMessage = 'Requested ISY not found.';
7754
7755			if (redirectUrl) {
7756				redirectWithError(redirectUrl, errorMessage)
7757			} else {
7758				errorMessageDialog(errorMessage + '<br/><br/>In order to see the license information for an ISY, please click on:<br/>Select tool | Information | ISY Portal Access License.', 'Choose ISY', 800)
7759			}
7760		}	else {
7761			var pk = isysFound[0]._id.toString();
7762			var uuidFound = isysFound[0].uuid;
7763			var online = isysFound[0].online;
7764			var isyDomain = isysFound[0].domain[0].id;
7765			var alias = isysFound[0].domain[0].alias;
7766			var registered = isysFound[0].domain[0].registered;
7767			var firmware = isysFound[0].isy && isysFound[0].isy.firmware
7768
7769			ipalDialog(uuidFound, alias, pk, isyDomain, false, online, registered, firmware, redirectUrl)
7770		}
7771	})
7772	.fail(function(jqXHR, textStatus, errorThrown) {
7773		var errorMessage = "Can't retrieve ISY list<br>HTTP error " + jqXHR.status
7774		if (redirectUrl) {
7775			redirectWithError(redirectUrl, errorMessage)
7776		} else {
7777			errorMessageDialog(errorMessage, 'Error', 350)
7778		}
7779	})
7780	.done();
7781
7782/*
7783	if (registeredUser) {
7784		var uuid="uuid"
7785		var alias="alias"
7786		var pk="54bdadc5a64ada4ece8bd0d5"
7787		var isyDomain="$superadmin"
7788
7789		isyAccessLicenseDialog.setFields({ uuid: uuid, alias: alias, pk: pk, domain: isyDomain });
7790		isyAccessLicenseDialog.modal.dialog('open');
7791
7792		ajaxJsonGet('/api/isy/ipal', $.param({id: pk, domain: isyDomain }))
7793		.done(function (data) {
7794			isyAccessLicenseDialog.setFields({ uuid: uuid, alias: alias, info: data, pk: pk, domain: isyDomain });
7795		})
7796		.fail(function(jqXHR, textStatus, errorThrown) {
7797			if (jqXHR.status==503) {
7798				errorMessageDialog('ISY is not online')
7799			}
7800			else {
7801				errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
7802			}
7803		})
7804		.done();
7805	}
7806*/
7807}
7808
7809
7810// Plugin module purchase process
7811// Called from Polisy
7812function nodeserverPurchase(pid, nsid, redirectUrl, requestBeta, isTest) {
7813
7814	if (redirectUrl) {
7815		if (!pid) {
7816			redirectWithError(redirectUrl, 'System error: No pid provided')
7817		} else if (!uuidRegex.test(pid)) {
7818			redirectWithError(redirectUrl, 'System error: pid is not valid')
7819		} else if (!nsid) {
7820			redirectWithError(redirectUrl, 'System error: No nsid provided')
7821		}
7822		ajaxJsonGet('/api/nodeserver/purchase/' + pid + '/' + nsid, $.param({ beta: requestBeta }))
7823		.then(function(result, status, jqXHR) {
7824			if (result.data) {
7825				var nodeserverInfo = result.data.ns
7826				var licenseInfo = result.data.license
7827
7828				ajaxJsonGet('/api/isys', $.param({domain: registeredUser.superadmin ? '$superadmin' : registeredUser.domain, subaccounts:true }))
7829				.then(function(result, status, jqXHR) {
7830					var isysFound = result.data.filter(function(isyRecord) {
7831						return isyRecord.uuid === pid;
7832					})
7833
7834					if (isysFound.length === 1) {
7835						var isyRecord = isysFound[0];
7836						var alias = isyRecord.domain.length && isyRecord.domain[0].alias || ''
7837						nsPurchaseDialogStart(pid, alias, nsid, nodeserverInfo, licenseInfo, redirectUrl, requestBeta, isTest)
7838					} else {
7839						confirmationDialog('Device ' + pid + ' not found in your account.<br/><br/>Do you want to add it to your account?', 'Add to your account?')
7840						.then(function () {
7841							var alias = 'Added to account during plugin purchase'
7842							ajaxJsonPut('/api/isy/domain', $.param({
7843								uuid:pid,
7844								alias: alias,
7845								domain: registeredUser.domain
7846							}))
7847							.then(function(data, status, jqXHR) {
7848								tableIsys.reload();
7849								nsPurchaseDialogStart(pid, alias, nsid, nodeserverInfo, redirectUrl, requestBeta, isTest)
7850							})
7851							.fail(function(jqXHR, textStatus, errorThrown) {
7852								redirectWithError(redirectUrl, 'pid could not be added to ISY Portal account.')
7853							})
7854							.done();
7855						})
7856						.fail(function(jqXHR, textStatus, errorThrown) {
7857							redirectWithError(redirectUrl, 'pid is not in ISY Portal account.')
7858						})
7859						.done();
7860					}
7861				})
7862				.fail(function(jqXHR, textStatus, errorThrown) {
7863					redirectWithError(redirectUrl, "Can't retrieve ISY list. HTTP error " + jqXHR.status)
7864				})
7865			} else {
7866				redirectWithError(redirectUrl, 'nsid is not valid.')
7867			}
7868		})
7869		.fail(function(jqXHR, textStatus, errorThrown) {
7870			redirectWithError(redirectUrl, 'Plugin info is not valid.')
7871		})
7872		.done();
7873	}	else {
7874		// We can't redirect, so we just leave a message here
7875		errorMessageDialog('System error: redirect url is missing');
7876	}
7877}
7878
7879// Open the nodeserver purchase dialog
7880// Can be from the purchase process, or eventual menu option
7881function nsPurchaseDialogStart(pid, alias, nsid, nodeserverInfo, licenseInfo, redirectUrl, requestBeta, isTest) {
7882	nsPurchaseDialog.setFields({ pid: pid, alias: alias, nsid: nsid, nodeserverInfo: nodeserverInfo, installed:licenseInfo, redirectUrl: redirectUrl, requestBeta: requestBeta, isTest: isTest});
7883	nsPurchaseDialog.modal.dialog('open');
7884}
7885
7886
7887function redirectWithError(redirectUrl, err) {
7888	var redirect = redirectUrl + '?' +
7889		$.param({
7890			successful: false,
7891			error: err instanceof Error ? err.message : err
7892		}).replace(/\+/g, '%20');
7893
7894	window.location.replace(redirect);
7895}
7896
7897
7898// opt = {tag:, message:, content:, uuid:, subTag: }
7899function sendLog(opt) {
7900	if (typeof opt.content!='string') {
7901		if (opt.content instanceof Error) {
7902			if (opt.content.stack && false) {
7903				opt.content = opt.content.stack;
7904			}
7905			else {
7906				opt.content = opt.content.toString();
7907			}
7908		}
7909		else {
7910			opt.content = JSON.stringify(opt.content);
7911		}
7912	}
7913
7914	return ajaxJsonPost('/api/log', $.param( {
7915		tag: opt.tag,
7916		message: opt.message,
7917		content: opt.content,
7918		uuid: opt.uuid,
7919		subTag: opt.subTag }
7920	))
7921}
7922
7923// ------------------- Dialogs -------------------------
7924
7925// Domain Dialog initialization
7926var domainDialogInit = function () {
7927	var dialog, form, pk,
7928		sub = $('#domainsub'),
7929		parent = $('#domainparent'),
7930		parentreadonly = $('#domainparent-readonly'),
7931		type = $('#domaintype'),
7932		shortname = $('#domainshortname'),
7933		description = $('#domaindescription'),
7934		address = $('#domainaddress'),
7935		website = $('#domainwebsite'),
7936		contactname = $('#domaincontactname'),
7937		contactphone = $('#domaincontactphone'),
7938		billingroot = $('#domain_billing_root'),
7939		billingsub = $('#domain_billing_sub'),
7940		consent = $('#domain_consent'),
7941		isRootAccount,
7942		isUDI,
7943		editMode,
7944		shortNameEditable;
7945//		contactemail = $('#domaincontactemail'),
7946
7947		// Used only to remove focus
7948		var allFields = $([]).add(sub).add(parent).add(parentreadonly).add(type).add(shortname).add(description).add(address).add(website).add(contactname).add(contactphone).add(billingroot).add(billingsub).add(consent),//.add(contactemail),
7949		tips = $('#domain_dialog .validateTips');
7950
7951	function resetForm() {
7952		tips.text('Please enter account information.');
7953		allFields.removeClass('fieldfocus');
7954		form[0].reset();
7955		pk=null;
7956	}
7957
7958	function setFields(obj) {
7959		resetForm();
7960
7961		isRootAccount = !obj.sub;
7962		isUDI = obj.isUDI;
7963		editMode = obj.editMode;
7964		shortNameReadonly = !obj.isUDI && !obj.sub && obj.editMode
7965
7966		sub.prop('checked', obj.sub);
7967		sub.prop('disabled', obj.editMode);
7968
7969		shortname.prop('readonly', shortNameReadonly);
7970
7971		// Adjusts field visibility based on sub-account checkbox
7972		subChangeEvent(obj.sub, obj.isUDI);
7973
7974		if (obj.editMode) {
7975			parentreadonly.show();
7976			parent.next().hide(); // next is the actual combobox
7977
7978			if (obj.parentname) {
7979				parentreadonly.val(obj.parentname);
7980			}
7981
7982			billingroot.prop('checked', obj.billingroot);
7983			billingsub.prop('checked', obj.billingsub);
7984			consent.prop('checked', obj.consent);
7985		}
7986		else {
7987			parent.next().show(); // next is the actual combobox
7988			parentreadonly.hide();
7989
7990			// Enable the combobox with the parent id and the combobox source
7991			if (obj.parentList) {
7992				parent.combobox("option", "source", obj.parentList);
7993			}
7994
7995			if (obj.parent) {
7996				parent.combobox("option", "data", obj.parent);
7997			}
7998
7999			// Default values
8000			billingroot.prop('checked', true);
8001			billingsub.prop('checked', false);
8002			consent.prop('checked', false);
8003		}
8004
8005		if (obj.shortname) {
8006			shortname.val(obj.shortname);
8007		}
8008
8009		if (obj.description) {
8010			description.val(obj.description);
8011		}
8012
8013		if (obj.address) {
8014			address.val(obj.address);
8015		}
8016
8017		if (obj.website) {
8018			website.val(obj.website);
8019		}
8020
8021		if (obj.contactname) {
8022			contactname.val(obj.contactname);
8023		}
8024
8025		if (obj.contactphone) {
8026			contactphone.val(obj.contactphone);
8027		}
8028/*
8029		if (obj.contactemail) {
8030			contactemail.val(obj.contactemail);
8031		}
8032*/
8033		if (obj.type) {
8034			$('#domaintype input[name=type]').val([obj.type]);
8035		}
8036
8037		if (obj.pk) {
8038			pk=obj.pk
8039		}
8040	}
8041
8042	function subChangeEvent(isSub, isUDI) {
8043		if (isUDI) {
8044			$('#domain_dialog .udionly').show();
8045
8046			if (isSub) {
8047				$('#domain_dialog .subtrue').show();
8048				$('#domain_dialog .subfalse').hide();
8049			}
8050			else {
8051				$('#domain_dialog .subtrue').hide();
8052				$('#domain_dialog .subfalse').show();
8053			}
8054		}
8055		else {
8056			if (isSub) {
8057				$('#domain_dialog .subtrue:not(.udionly)').show();
8058				$('#domain_dialog .subfalse').hide();
8059			}
8060			else {
8061				$('#domain_dialog .subtrue').hide();
8062				$('#domain_dialog .subfalse:not(.udionly)').show();
8063			}
8064
8065			$('#domain_dialog .udionly').hide();
8066		}
8067	}
8068
8069	function domainSubmit() {
8070		var valid = true;
8071		allFields.removeClass('fieldfocus');
8072		parent.combobox("refresh"); // Makes sure the real input field is up to date
8073		valid = valid && (sub.prop('checked') || checkRadio(tips, 'input[name="type"]:checked', "Domain type must be selected"));
8074		valid = valid && (!sub.prop('checked') || checkNotEmpty(tips, parent, 'Parent') || isRootAccount || editMode || !isUDI);
8075		valid = valid && checkLength(tips, description, 'Description', 1, 50);
8076
8077		if (!shortNameReadonly) {
8078			valid = valid && checkLength(tips, shortname, 'Short name', 1, 20);
8079		}
8080
8081    //		valid = valid && (contactemail.val()=='' || checkRegexp(tips, contactemail, emailRegexWithSpace, 'email address is not valid.' ));
8082
8083		if (valid) {
8084//			contactemail.val(contactemail.val().trim());
8085			var data = serializeFormWithCheckboxes(form.find('input, textarea'));
8086
8087			// if pk is there this will be an insert instead of an update, we need pk (this is the primary key)
8088			if (pk) {
8089				data += '&_id='+pk;
8090			}
8091
8092			if (!sub.prop('checked') && !billingroot.prop('checked') && !billingsub.prop('checked')) {
8093				confirmationDialog('Access License renewal notifications will not be sent.<br/>Are you sure?', 'No License Notifications?')
8094				.then(function () {
8095					save(data)
8096				})
8097				.done();
8098			}
8099			else {
8100				save(data)
8101			}
8102		}
8103	}
8104
8105	function save(data) {
8106		// If inserting, call ajaxJsonPut, else ajaxJsonPost
8107		(pk?ajaxJsonPost('/api/domain', data):ajaxJsonPut('/api/domain', data))
8108		.then(function(domainRecord, status, jqXHR) {
8109			// If we added a children to our own domain, then add it to our children
8110			// This is a fix to the Select Tools not showing the options after adding a domain.
8111			setRegisteredUserAddChildrenDomain(domainRecord);
8112
8113			dialog.dialog('close');
8114			tableDomains.reload();
8115		})
8116		.fail(function(jqXHR, textStatus, errorThrown) {
8117			if (jqXHR.status==420) {
8118				updateTips(tips, 'Domain already exists');
8119			}
8120			else {
8121				updateTips(tips, 'HTTP error ' + jqXHR.status);
8122			}
8123		})
8124		.done();
8125	}
8126
8127	var isyOptions = {
8128		autoOpen: false,
8129		//height: 400,
8130		width: 750,
8131		modal: true,
8132		buttons: {
8133			'Save': domainSubmit,
8134			Cancel: function() {
8135				resetForm();
8136				$(this).dialog('close');
8137			}
8138		},
8139		close: function() {
8140			resetForm();
8141		},
8142		open: function(event, ui) {
8143			dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
8144		}
8145	};
8146
8147	// Initializes the dialog
8148	dialog = $('#domain_dialog').dialog(isyOptions);
8149
8150	parent.combobox({inputClass: ''});
8151
8152	sub.change(function() {
8153		isRootAccount = !this.checked;
8154		subChangeEvent(this.checked, isUDI);
8155	});
8156
8157	$('#consentHelp').click(function() {
8158		informationMessageDialog('Help us improve!<br/><br/> By enabling this option, you allow us to collect anonymized system configuration to enhance your experience. We do not collect personal or sensitive information. You can change this setting anytime.', 'Help', 800)
8159	});
8160
8161	// Handles keyboad enter (submit event)
8162	form = dialog.find('form').submit(function(event) {
8163		event.preventDefault();
8164		domainSubmit();
8165	});
8166
8167	return { modal: dialog, setFields: setFields, resetForm: resetForm };
8168};
8169
8170// User Dialog initialization
8171var userDialogInit = function () {
8172	var dialog, form, pk,
8173		username = $('#user_name'),
8174		password = $('#user_password'),
8175		isy = $('#user_isy'),
8176		isDeveloper = $('#user_isDeveloper'),
8177		isReviewer = $('#user_isReviewer'),
8178		isEEAdmin = $('#user_isEEAdmin'),
8179		notificationmaint = $('#user_notificationmaint'),
8180		notificationnews = $('#user_notificationnews'),
8181		passwordconf = $('#user_passwordconf'),
8182		domainadmin = $('#user_domainadmin'),
8183		passwordchange = $('#user_passwordchange'),
8184		allFields = $([]).add(username).add(password).add(passwordconf).add(domainadmin).add(passwordchange).add(isDeveloper).add(isReviewer).add(isEEAdmin).add(notificationmaint).add(notificationnews),
8185		tips = $('#user_dialog .validateTips'),
8186		activeDomain,
8187		domainAdminBeforeChange;
8188
8189	function resetForm() {
8190		tips.text('Please enter user information.');
8191		allFields.removeClass('fieldfocus');
8192		form[0].reset();
8193		pk=null;
8194		activeDomain=null;
8195
8196		username.prop('readonly', false); // If we set fields, we are in edit mode, email is not editable
8197		username.removeProp('tabindex');
8198
8199		$('#user_email_invitation_span').show();
8200	}
8201
8202	function setFields(obj) {
8203		resetForm();
8204		username.val(obj.username);
8205		password.val(obj.password);
8206		passwordconf.val(obj.password);
8207		isy.val(obj.isy);
8208
8209		domainadmin.prop('checked', obj.domainadmin);
8210		passwordchange.prop('checked', obj.passwordchange);
8211		isDeveloper.prop('checked', obj.isDeveloper);
8212		isReviewer.prop('checked', obj.isReviewer);
8213		isEEAdmin.prop('checked', obj.isEEAdmin);
8214		notificationmaint.prop('checked', obj.notificationmaint);
8215		notificationnews.prop('checked', obj.notificationnews);
8216		domainAdminBeforeChange = obj.domainadmin;
8217
8218		isDeveloper.closest('.checkbox').toggle(registeredUser.superadmin);
8219		isReviewer.closest('.checkbox').toggle(registeredUser.superadmin);
8220		isEEAdmin.closest('.checkbox').toggle(registeredUser.superadmin);
8221
8222		if (obj.pk) {
8223			pk=obj.pk;
8224			username.prop('readonly', true); // If we set fields, we are in edit mode, email is not editable
8225			username.prop('tabindex', -1);
8226			$('#user_email_invitation_span').hide();
8227		}
8228		else {
8229			pk=null;
8230		}
8231	}
8232
8233	function setDomain(id) {
8234		activeDomain = id;
8235
8236		if (id==='$superadmin') {
8237			// All users in $superadmin are implicitly domain admins
8238			$('#user_domainadmin_span').hide();
8239			domainadmin.prop('checked', true);
8240		}
8241		else {
8242			$('#user_domainadmin_span').show();
8243		}
8244	}
8245
8246	function userSubmit() {
8247		var valid = true;
8248
8249		allFields.removeClass('fieldfocus');
8250		valid = valid && checkLength(tips, username, 'email', 6, 80);
8251		valid = valid && checkRegexp(tips, username, emailRegexWithSpace, 'email address is not valid.' );
8252		valid = valid && checkPwd(tips, password);
8253		valid = valid && checkPwdEqual(tips, password, passwordconf, 'Password confirmation does not match.' );
8254		valid = valid && (!isy.val().length || checkRegexpUuid(tips, isy, 'ISY must be in the format 00:21:b9:99:99:99'));
8255
8256		if (valid) {
8257			username.val(username.val().trim());
8258
8259			var currentDomainAdmin = domainadmin.prop('checked');
8260
8261			var data = serializeFormWithCheckboxes(form.find('input'));
8262
8263			if (activeDomain) {
8264				data += '&' + $.param({domain: activeDomain})
8265			}
8266
8267			// We are removing domain admin rights to our own user profile
8268			if (pk == registeredUser.username && domainAdminBeforeChange && !currentDomainAdmin) {
8269				confirmationDialog('You are removing your own account admin rights. Are you sure?', 'Are you sure?')
8270				.then(function () {
8271					userSubmitProcess(data);
8272				})
8273			}
8274			else {
8275				userSubmitProcess(data);
8276			}
8277		}
8278	}
8279
8280	function userSubmitProcess(data, isRetry) {
8281		// If we are a UDI admin, the activeDomain comes from the Domain roller (if adding) or the actual user record (if editing).
8282
8283		// if pk is there this will be an insert instead of an update (pk is only a flag in this case)
8284		(pk?ajaxJsonPost('/api/user', data):ajaxJsonPut('/api/user', data))
8285		.then(function(data, status, jqXHR) {
8286
8287			// We are changing our own profile...
8288			if (pk == registeredUser.username) {
8289				if (password.val()=='$unchanged') {
8290					// If we did not change the password, we don't know it, so just update the profile with same creds
8291					setRegisteredUserProfile(data);
8292				}
8293				else {
8294					// Profile is updated along with creds
8295					// We do it in 2 steps, so we can keep domainInfo on registeredUser
8296					setRegisteredUserProfile(data);
8297					setRegisteredUserCredentials({ password: password.val() });
8298				}
8299			}
8300
8301			tableUsers.reload();
8302			dialog.dialog('close');
8303		})
8304		.fail(function(jqXHR, textStatus, errorThrown) {
8305			if (jqXHR.status==420) {
8306				updateTips(tips, 'User already exists (Possibly in another account)');
8307			}
8308			else if (jqXHR.status==424) {
8309				updateTips(tips, 'Password is too easy. Please choose a more complex one.');
8310			}
8311			else if (jqXHR.status==425) { // Custom server error message (Typically for uuid index error)
8312				updateTips(tips, jqXHR.responseText);
8313			}
8314			// NOTE: THIS IS NO LONGER RELEVANT. Push info is on the user profile
8315			// else if (jqXHR.status==426) {  // Preferred ISY change warning
8316			// 	if (!isRetry) {
8317			// 		confirmationDialog(preferredIsyChangeWarning, 'Preferred ISY Change', 800)
8318			// 		.then(function () {
8319			// 			// Retry the request, bypass the preferred ISY change validation
8320			// 			userSubmitProcess(data+'&confirmed=true', true)
8321			// 		})
8322			// 		.fail(function() {
8323			// 			// Clicked cancel
8324			// 		})
8325			// 		.done();
8326			// 	}
8327			// 	else {
8328			// 		errorMessageDialog('Error updating the preferred ISY', 'Error', 350)
8329			// 	}
8330			// }
8331			else {
8332				updateTips(tips, 'HTTP error ' + jqXHR.status);
8333			}
8334		})
8335		.done();
8336	}
8337
8338	var userOptions = {
8339		autoOpen: false,
8340		//height: 600,
8341		width: 400,
8342		modal: true,
8343		buttons: {
8344			'Save': userSubmit,
8345			Cancel: function() {
8346				resetForm();
8347				$(this).dialog('close');
8348			}
8349		},
8350		close: function() {
8351			resetForm();
8352		},
8353		open: function(event, ui) {
8354			dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
8355		}
8356	};
8357
8358	// Initializes the dialog
8359	dialog = $('#user_dialog').dialog(userOptions);
8360
8361	// Handles keyboad enter (submit event)
8362	form = dialog.find('form').submit(function(event) {
8363		event.preventDefault();
8364		userSubmit();
8365	});
8366
8367	return { modal: dialog, setFields: setFields, setDomain: setDomain, resetForm: resetForm };
8368};
8369
8370// ISY Dialog for uuid & alias
8371var isyClaimDialogInit = function () {
8372	var dialog, form, pk,
8373		uuid = $('#isy_claim_uuid'),
8374		alias = $('#isy_claim_alias'),
8375		allFields = $([]).add(uuid).add(alias),
8376		tips = $('#isy_claim_dialog .validateTips'),
8377		activeDomain;
8378
8379	function resetForm() {
8380		tips.text('Please enter ISY UUID and alias.');
8381		allFields.removeClass('fieldfocus');
8382		form[0].reset();
8383		pk=null;
8384		activeDomain=null;
8385
8386		uuid.prop('readonly', false);
8387		uuid.removeProp('tabindex');
8388
8389	}
8390
8391	function setFields(obj) {
8392		resetForm();
8393		uuid.val(obj.uuid);
8394		alias.val(obj.alias);
8395
8396		if (obj.pk) {
8397			pk=obj.pk;
8398			uuid.prop('readonly', true); // If we set fields, we are in edit mode, uuid is not editable
8399			uuid.prop('tabindex', -1);
8400		}
8401		else {
8402			pk=null;
8403		}
8404	}
8405
8406	function setDomain(id) {
8407		activeDomain = id;
8408	}
8409
8410	function isySubmit() {
8411		var valid = true;
8412
8413		allFields.removeClass('fieldfocus');
8414		valid = valid && checkRegexpUuid(tips, uuid, 'UUID is not valid');
8415
8416		if (valid) {
8417			var data = form.find('input').serialize();
8418
8419			data += '&' + $.param({id: pk}); // The API required the isy id (not the uuid)
8420
8421			// If we are a UDI admin, the activeDomain comes from the Domain roller.
8422			if (activeDomain) {
8423				data += '&' + $.param({domain: activeDomain})
8424			}
8425
8426			// if pk is there this will be an update instead of an upsert (pk is only a flag in this case)
8427			(pk?ajaxJsonPost('/api/isy/domain', data):ajaxJsonPut('/api/isy/domain', data))
8428			.then(function(data, status, jqXHR) {
8429				var firmware = data.isy && data.isy.firmware
8430
8431				tableIsys.reload();
8432
8433				// If we are adding the ISY in the account (Not updating the alias)
8434				if (!pk) {
8435					// Automatically open the license dialog, if license is not active.
8436					ipalDialog(uuid.val(), alias.val(), data._id, activeDomain, true, data.online, 0, firmware, null) // registered status is necessarily 0.
8437				}
8438
8439				dialog.dialog('close');	// Needs to be done after, otherwise activeDomain is null!
8440			})
8441			.fail(function(jqXHR, textStatus, errorThrown) {
8442				if (jqXHR.status==420) {
8443					updateTips(tips, 'ISY already entered');
8444				}
8445				else {
8446					updateTips(tips, 'HTTP error ' + jqXHR.status);
8447				}
8448			})
8449			.done();
8450		}
8451	}
8452
8453	var isyOptions = {
8454		autoOpen: false,
8455		width: 400,
8456		modal: true,
8457		buttons: {
8458			'Save': isySubmit,
8459			Cancel: function() {
8460				resetForm();
8461				$(this).dialog('close');
8462			}
8463		},
8464		close: function() {
8465			resetForm();
8466		},
8467		open: function(event, ui) {
8468			dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
8469		}
8470	};
8471
8472	// Initializes the dialog
8473	dialog = $('#isy_claim_dialog').dialog(isyOptions);
8474
8475	// Handles keyboad enter (submit event)
8476	form = dialog.find('form').submit(function(event) {
8477		event.preventDefault();
8478		isySubmit();
8479	});
8480
8481	return { modal: dialog, setFields: setFields, setDomain: setDomain, resetForm: resetForm };
8482};
8483
8484// ISY Dialog for Notes
8485var isyNoteDialogInit = function () {
8486	var dialog, form, pk,
8487		note = $('#isy_note'),
8488		allFields = $([]).add(note),
8489		tips = $('#isy_note_dialog .validateTips'),
8490		isyObject, // Points to the ISY object obtained from get
8491		activeDomain;
8492
8493	function resetForm() {
8494		tips.text('Please enter a note.');
8495		allFields.removeClass('fieldfocus');
8496		form[0].reset();
8497		pk=null;
8498		activeDomain=null;
8499		isyObject = null;
8500
8501	}
8502
8503	function setFields(obj) {
8504		resetForm();
8505		$('#isy_note_uuid').text(obj.uuid);
8506		$('#isy_note_alias').text(obj.alias);
8507		isyObject = obj.handle;
8508		note.val(obj.note);
8509		pk=obj.pk
8510	}
8511
8512	function setDomain(id) {
8513		activeDomain = id;
8514	}
8515
8516	function isySubmit() {
8517		var valid = true;
8518
8519		allFields.removeClass('fieldfocus');
8520
8521		if (valid) {
8522			var data = form.find('input, textarea, select').serialize();
8523
8524			data += '&' + $.param({id: pk}); // The API requires the isy id (not the uuid)
8525
8526			// If we are a UDI admin, the activeDomain comes from the Domain roller.
8527			if (activeDomain) {
8528				data += '&' + $.param({domain: activeDomain})
8529			}
8530
8531			ajaxJsonPost('/api/isy/domain', data)
8532			.then(function(data, status, jqXHR) {
8533
8534				// We need this because we don't refresh the isy table
8535				isyObject.domain[0].note = form.find('textarea').val(); // Update the copy we have in memory (coming from get /rest...)
8536				dialog.dialog('close');
8537			})
8538			.fail(function(jqXHR, textStatus, errorThrown) {
8539				updateTips(tips, 'HTTP error ' + jqXHR.status);
8540			})
8541			.done();
8542		}
8543	}
8544
8545	var isyOptions = {
8546		autoOpen: false,
8547		width: 700,
8548		modal: true,
8549		buttons: {
8550			'Save': isySubmit,
8551			Cancel: function() {
8552				resetForm();
8553				$(this).dialog('close');
8554			}
8555		},
8556		close: function() {
8557			resetForm();
8558		},
8559		open: function(event, ui) {
8560			dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
8561		}
8562	};
8563
8564	// Initializes the dialog
8565	dialog = $('#isy_note_dialog').dialog(isyOptions);
8566
8567	// Handles keyboad enter (submit event)
8568	form = dialog.find('form').submit(function(event) {
8569		event.preventDefault();
8570		isySubmit();
8571	});
8572
8573	return { modal: dialog, setFields: setFields, setDomain: setDomain, resetForm: resetForm };
8574};
8575
8576// ISY Dialog for Firmware upgrades
8577// var isyFirmwareDialogInit = function () {
8578// 	var dialog, pk,
8579// 		activeDomain,
8580// 		enableUpgradeMonitor,
8581// 		targetVersion;
8582//
8583// 	function updateDescription(description) {
8584// 		var urlRegex = /(https?:\/\/[\da-z\.-\?\-]+)/;
8585// 		var newdesc = description.replace(urlRegex, ' <a href="$1" target="_blank" class="editable_link">$1</a> ');
8586// 		$('#isy_fw_description').html(newdesc);
8587// 	}
8588//
8589// 	function setButtons(currentVersion, targetVersion) {
8590// 		switch (versionCompare(currentVersion, targetVersion)) {
8591// 		case -1: dialog.dialog('option', 'buttons', {
8592// 					'Upgrade': function() {
8593// 						startUpgrade(targetVersion);
8594// 					},
8595// 					Cancel: function() {
8596// 						$(this).dialog('close');
8597// 					}
8598// 				});
8599// 				break;
8600//
8601// 		case 1: dialog.dialog('option', 'buttons', {
8602// 					'Downgrade': function() {
8603// 						$(this).dialog('close');
8604// 						confirmationDialog('Are you sure you want to DOWNGRADE?', 'Are you sure?')
8605// 						.then(function() {
8606// 							startUpgrade(targetVersion);
8607// 						})
8608// 					},
8609// 					Cancel: function() {
8610// 						$(this).dialog('close');
8611// 					}
8612// 				});
8613// 				break;
8614//
8615// 		case 0: dialog.dialog('option', 'buttons', {
8616// 					Cancel: function() {
8617// 						$(this).dialog('close');
8618// 					}
8619// 				});
8620// 				break;
8621// 		}
8622// 	}
8623//
8624// 	function setFields(obj) {
8625// 		$('#isy_fw_uuid').text(obj.uuid);
8626// 		$('#isy_fw_alias').text(obj.alias);
8627// 		$('#isy_fw_current').text(obj.currentVersion);
8628// 		$('#firmwareupload_row').hide();
8629//
8630// 		// Build the firmware select
8631// 		$('#isy_fw_select')
8632// 		.setTemplateElement('firmwareSelectorTemplate')
8633// 		.processTemplate(obj.availableVersions);
8634//
8635// 		// Firmware selector
8636// 		$('.firmwareSelect').selectmenu({
8637// 			width:'10em',
8638//
8639// 			change: function(event, ui) {
8640// 				var index = $('#isy_fw_select .firmwareSelect')[0].selectedIndex;
8641// 				targetVersion = ui.item.value;
8642// 				updateDescription(obj.availableVersions[index].description);
8643//
8644// 				if (targetVersion == 'Beta version') {
8645// 					setButtons(obj.currentVersion, '999'); // e.g. 'Upgrade' button.
8646// 					$('#firmwareupload_row').show()
8647// 				}
8648// 				else {
8649// 					setButtons(obj.currentVersion, targetVersion);
8650// 					$('#firmwareupload_row').hide()
8651// 				}
8652// 			}
8653// 		});
8654//
8655// 		// Set Initial version, description and buttons
8656// 		if (obj.availableVersions.length) {
8657// 			targetVersion = obj.availableVersions[0].version;
8658// 			updateDescription(obj.availableVersions[0].description);
8659// 			setButtons(obj.currentVersion, targetVersion)
8660// 		}
8661//
8662// 		pk=obj.pk
8663// 	}
8664//
8665// 	function setDomain(id) {
8666// 		activeDomain = id;
8667// 	}
8668//
8669// 	function startUpgrade(targetVersion) {
8670// 		var data = $.param({id: pk}); // The API requires the isy id (not the uuid)
8671//
8672// 		// If we are a UDI admin, the activeDomain comes from the Domain roller.
8673// 		if (activeDomain) {
8674// 			data += '&' + $.param({domain: activeDomain, version: targetVersion})
8675// 		}
8676//
8677// 		dialog.dialog('close'); // Close main firmware upgrade dialog
8678// 		enableUpgradeMonitor=true;
8679// 		liveMessageDialog.setTitle('Firmware update');
8680// 		liveMessageDialog.setMessage('Firmware upload in progress...');
8681// 		liveMessageDialog.restartDefer()
8682// 		.fail(function () {
8683// 			enableUpgradeMonitor=false; // A cancel on the popup will stop the monitor
8684// 		})
8685// 		.done();
8686//
8687// 		liveMessageDialog.modal.dialog('open');
8688//
8689// 		ajaxJsonPost('/api/isy/firmwareupgrade/', data)
8690// 		.then(function(data, status, jqXHR) {
8691// 			if (enableUpgradeMonitor) {
8692// 				liveMessageDialog.setMessage('ISY reboot in progress...');
8693// 				monitorReboot();
8694// 			}
8695// 		})
8696// 		.fail(function(jqXHR, textStatus, errorThrown) {
8697// 			var promise;
8698//
8699// 			liveMessageDialog.modal.dialog('close');
8700//
8701// 			if (jqXHR.status==425) { // Normally due to zip not found
8702// 				promise = errorMessageDialog(jqXHR.responseText, 'Error', 350);
8703// 			}
8704// 			else {
8705// 				promise = errorMessageDialog("Firmware upgrade FAILED!<br/>HTTP error " + jqXHR.status + "<br/>" + jqXHR.responseText, "Failure")
8706// 			}
8707// 			promise.then(function () {
8708// 				tableIsys.reload();
8709// 			})
8710// 		})
8711// 		.done();
8712// 	}
8713//
8714// 	function monitorReboot() {
8715// 		var data = $.param({id: pk}); // The API requires the isy id (not the uuid)
8716//
8717// 		// If we are a UDI admin, the activeDomain comes from the Domain roller.
8718// 		if (activeDomain) {
8719// 			data += '&' + $.param({domain: activeDomain})
8720// 		}
8721//
8722// 		ajaxJsonPost('/api/isy/firmwareupgrade/status', data)
8723// 		.then(function (data) {
8724// 			switch (data.state) {
8725// 			case 'rebooting' :
8726// 				if (enableUpgradeMonitor) {
8727// 					setTimeout(monitorReboot, 3000);
8728// 				}
8729// 				break;
8730//
8731// 			case 'successful' :
8732// 				liveMessageDialog.modal.dialog('close');
8733//
8734// 				informationMessageDialog("ISY firmware update Successful!", "Success")
8735// 				.then(function () {
8736// 					tableIsys.reload();
8737// 				});
8738// 				break;
8739//
8740// 			case 'timeout' :
8741// 				liveMessageDialog.modal.dialog('close');
8742//
8743// 				errorMessageDialog("ISY firmware update timeout", "Failure")
8744// 				.then(function () {
8745// 					tableIsys.reload();
8746// 				});
8747// 				break;
8748//
8749// 			}
8750// 		})
8751// 		.fail(function(jqXHR, textStatus, errorThrown) {
8752// 			liveMessageDialog.modal.dialog('close');
8753//
8754// 			errorMessageDialog("Failure while checking firmware update status<br/>HTTP error " + jqXHR.status + "<br/>" + jqXHR.responseText, "Failure")
8755// 			.then(function () {
8756// 				tableIsys.reload();
8757// 			})
8758// 		})
8759// 		.done();
8760// 	}
8761//
8762// 	var isyOptions = {
8763// 		autoOpen: false,
8764// 		width: 700,
8765// 		modal: true,
8766// /*	Buttons are set during setFields
8767// 		buttons: {
8768// 			'Upgrade': isySubmit,
8769// 			Cancel: function() {
8770// 				$(this).dialog('close');
8771// 			}
8772// 		},
8773// */
8774// 		open: function(event, ui) {
8775// 			dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
8776// 		}
8777// 	};
8778//
8779// 	// Initializes the dialog
8780// 	dialog = $('#isy_firmware_dialog').dialog(isyOptions);
8781//
8782// 	$('#isy_fw_select_label').tooltip();
8783//
8784// 	return { modal: dialog, setFields: setFields, setDomain: setDomain };
8785// };
8786
8787// ISY Dialog for Contact
8788var isyContactDialogInit = function () {
8789	var dialog, form, pk,
8790		name = $('#contact_name'),
8791		phone = $('#contact_phone'),
8792		email = $('#contact_email'),
8793		address = $('#contact_address'),
8794		allFields = $([]).add(name).add(phone).add(email).add(address),
8795		tips = $('#isy_contact_dialog .validateTips'),
8796		isyObject, // Points to the ISY object obtained from get
8797		activeDomain;
8798
8799	function resetForm() {
8800		tips.text('Please enter ISY contact information.');
8801		allFields.removeClass('fieldfocus');
8802		form[0].reset();
8803		pk=null;
8804		activeDomain=null;
8805		isyObject = null;
8806	}
8807
8808	function setFields(obj) {
8809		resetForm();
8810		$('#isy_contact_uuid').text(obj.uuid);
8811		$('#isy_contact_alias').text(obj.alias);
8812		isyObject = obj.handle;
8813		name.val(obj.name);
8814		phone.val(obj.phone);
8815		email.val(obj.email);
8816		address.val(obj.address);
8817		pk=obj.pk
8818	}
8819
8820	function setDomain(id) {
8821		activeDomain = id;
8822	}
8823
8824	function isySubmit() {
8825		var valid = true;
8826
8827		allFields.removeClass('fieldfocus');
8828		valid = valid && (email.val().length==0 || checkRegexp(tips, email, emailRegexWithSpace, 'email address is not valid.'));
8829
8830		if (valid) {
8831			email.val(email.val().trim());
8832
8833			var data = form.find('input, textarea, select').serialize();
8834
8835			data += '&' + $.param({id: pk}); // The API requires the isy id (not the uuid)
8836
8837			// If we are a UDI admin, the activeDomain comes from the Domain roller.
8838			if (activeDomain) {
8839				data += '&' + $.param({domain: activeDomain})
8840			}
8841
8842			ajaxJsonPost('/api/isy/domain', data)
8843			.then(function(data, status, jqXHR) {
8844				// We need this because we don't refresh
8845				isyObject.name = name.val(); // Update the copy we have in memory (coming from get /rest...)
8846				isyObject.phone = phone.val();
8847				isyObject.email = email.val();
8848				isyObject.address = address.val();
8849				dialog.dialog('close');
8850			})
8851			.fail(function(jqXHR, textStatus, errorThrown) {
8852				updateTips(tips, 'HTTP error ' + jqXHR.status);
8853			})
8854			.done();
8855		}
8856	}
8857
8858	var isyOptions = {
8859		autoOpen: false,
8860		width: 550,
8861		modal: true,
8862		buttons: {
8863			'Save': isySubmit,
8864			Cancel: function() {
8865				resetForm();
8866				$(this).dialog('close');
8867			}
8868		},
8869		close: function() {
8870			resetForm();
8871		},
8872		open: function(event, ui) {
8873			dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
8874		}
8875	};
8876
8877	// Initializes the dialog
8878	dialog = $('#isy_contact_dialog').dialog(isyOptions);
8879
8880	// Handles keyboad enter (submit event)
8881	form = dialog.find('form').submit(function(event) {
8882		event.preventDefault();
8883		isySubmit();
8884	});
8885
8886	return { modal: dialog, setFields: setFields, setDomain: setDomain, resetForm: resetForm };
8887};
8888
8889// ISY Information
8890var isyInfoDialogInit = function () {
8891	var dialog;
8892
8893	function setFields(obj) {
8894		$('#isy_info_uuid').text(obj.uuid);
8895		$('#isy_info_alias').text(obj.alias);
8896
8897		if (obj.info) {
8898			var isyBaseUrl = location.origin + '/isy/' + encodeURIComponent(obj.info.isyKey)
8899
8900			$('#isy_info_product').text(obj.info.product && (obj.info.product.id + ' ' + obj.info.product.desc));
8901			$('#isy_info_ipexternal').text(obj.info.ipExternal);
8902			$('#isy_info_ipinternal').text(obj.info.ipInternal);
8903			// $('#isy_info_cipher').text(obj.info.cipher);
8904			$('#isy_info_multi').text(
8905				obj.info.multiConnectionSupport ?
8906					'eisy-ui service (' + (obj.info.isWs ? 'Multi Connections' : obj.info.multiConnectionCount + ' connection') + ')' :
8907					'ISY');
8908			$('#isy_info_admin_console').text(isyBaseUrl+'/desc')
8909			$('#isy_info_base_url').text(isyBaseUrl);
8910
8911			// Build the firmware select
8912			$('#isyfeatures_placeholder')
8913			.setTemplateElement('isyFeaturesTemplate')
8914			.processTemplate(obj.info.features);
8915		}
8916		else {
8917			$('#isy_info_product').text();
8918			$('#isy_info_ipexternal').text('');
8919			$('#isy_info_ipinternal').text('');
8920			// $('#isy_info_cipher').text('');
8921			$('#isyfeatures_placeholder').text('');
8922		}
8923	}
8924
8925	$('#multiConnectionsHelp').click(function() {
8926		informationMessageDialog('Multi-Connections is an upcoming feature of eISY allowing multiple connections to portal which will increase communication speed.', 'Help', 500)
8927	});
8928
8929
8930	var isyOptions = {
8931		autoOpen: false,
8932		width: 1060,
8933		modal: true,
8934		buttons: {
8935			'Close': function() {
8936				$(this).dialog('close');
8937			}
8938		},
8939		open: function(event, ui) {
8940			dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
8941		}
8942	};
8943
8944	// Initializes the dialog
8945	dialog = $('#isy_info_dialog').dialog(isyOptions);
8946	// Tooltip removed - Causing issues as the scope is too wide
8947	//$('[title]').tooltip()
8948
8949	if (!Clipboard.isSupported()) {
8950		$('#isy_info_dialog .copy_clip').hide();
8951	}
8952
8953	var clipboard = new Clipboard('#isy_info_dialog .copy_clip');
8954
8955	clipboard.on('success', function(e) {
8956		var target = $(e.trigger).attr('data-clipboard-target');
8957		if($(target).tooltip('option', 'disabled')) {
8958			$(target).tooltip('option', {disabled: false, content: 'Copied'}).tooltip('open')
8959		}
8960		setTimeout(function(){
8961			e.clearSelection();
8962			$(target).tooltip('option', {disabled: true}).tooltip('close')
8963		}, 800);
8964	});
8965
8966	clipboard.on('error', function(e) {
8967		var target = $(e.trigger).attr('data-clipboard-target');
8968		if ($(target).tooltip('option', 'disabled')) {
8969			$(target).tooltip('option', { disabled: false, content: 'COPY FAILED' }).tooltip('open')
8970		}
8971		setTimeout(function () {
8972			e.clearSelection();
8973			$(target).tooltip('option', { disabled: true }).tooltip('close')
8974		}, 2000);
8975	})
8976
8977	// $('#isy_info_dialog .copy_clip').button() // { icons: {primary: 'ui-icon-copy'}, text: false}
8978
8979	return { modal: dialog, setFields: setFields };
8980};
8981
8982
8983// ISY Portal Access License main dialog
8984var isyAccessLicenseDialogInit = function () {
8985	var dialog,
8986		pk,
8987		isyDomain,
8988		uuid,
8989		alias,
8990		lookupRenewOptions,
8991		licenseActive,
8992		licenseInstalled,
8993		online,
8994		registered, // Registered status of the device. (0=pending approval, 1=approved, -1=revoked)
8995		info, // Setfield obj.info
8996		redirectUrl,
8997		firmware,
8998		purchaseWasSuccessful
8999
9000	function init() {
9001		redirectUrl = null;
9002		purchaseWasSuccessful = null; // When we start, we don't have a purchase tried yet
9003	}
9004
9005	function setFields(obj) {
9006		firmware = obj.firmware;
9007		$('#isy_license_uuid').text(obj.uuid);
9008		$('#isy_license_alias').text(obj.alias);
9009
9010		if (obj.info) {
9011			var licenseExpiryHtml='';
9012			var licenseActiveHtml='';
9013			var buttons;
9014
9015			// Keep a copy for when we go to the renew dialog
9016			uuid = obj.uuid;
9017			alias = obj.alias;
9018			info = obj.info;
9019			pk = obj.pk;
9020			online = obj.online;
9021			registered = obj.registered;
9022			isyDomain = obj.domain;
9023
9024//			console.log('domain %s pk %s alias %s uuid %s registered %s', isyDomain, pk, alias, uuid, registered)
9025			if (!obj.info.webStore.module || !obj.info.webStore.expiry) {
9026				// There is no license yet
9027				if (!obj.info.webStore.module) {
9028					dialog.dialog('option', 'buttons', [trialButton, buyButton, closeButton]); // Set the dialog buttons
9029				} else {
9030					dialog.dialog('option', 'buttons', [buyButton, closeButton]); // Set the dialog buttons
9031				}
9032
9033				licenseActiveHtml = '<span title="ISY Portal License has to be purchased.">No</span>';
9034				licenseExpiryHtml = '<span title="ISY Portal License has to be purchased.">No active license</span>';
9035				licenseActive = false;
9036				licenseInstalled = false;
9037			}	else {
9038				// Renewals are eligibles only when we have an initial or renewal already in place, and the module is still active
9039				var enableRenew = obj.info.webStore.type>=1 && obj.info.webStore.module && obj.info.webStore.module.active;
9040
9041				buttons = [enableRenew?renewButton:buyButton, closeButton];
9042
9043				if (registeredUser.superadmin) {
9044					buttons.unshift(revokeButton)
9045				}
9046
9047				dialog.dialog('option', 'buttons', buttons); // Set the dialog buttons
9048
9049				var expiryDate = new Date(obj.info.webStore.expiry);
9050
9051				// Set access license expired flag
9052				var expired = expiryDate.valueOf() < new Date().valueOf();
9053
9054				// Set access license will be expired soon flag
9055				var soon = new Date();
9056				soon.setDate(soon.getDate() + accessLicenseExpirationSoonDays);
9057				var soonExpired = !expired && expiryDate.valueOf() < soon.valueOf();
9058
9059				licenseExpiryHtml = '<span>' + dateFormatNoError(expiryDate, "yyyy-mm-dd") +'</span>' +
9060				(expired?'<span class="red expired" title="ISY Portal License is expired.">expired</span>':'') +
9061				(soonExpired?'<span class="warning expired" title="ISY Portal License is expiring soon."><span class="ui-icon ui-icon-alert"></span></span>':'');
9062
9063				licenseActive = obj.info.webStore.module.active && !expired;
9064				licenseInstalled = !!obj.info.webStore.module.installed;
9065
9066				licenseActiveHtml = '<span>' + (licenseActive?'Yes':'No') + '</span>' +
9067					(licenseActive && !licenseInstalled?'<span class="red expired" title="Please open the admin console and click on Help | Manage Modules to install the license.">(License is not installed)</span>':'')
9068			}
9069
9070			// if (!obj.info.webStore.uuidValid) {
9071			// 	$('#isy_license_uuid').html(obj.uuid + '<span class="warning" title="This UUID does not appear to be valid."><span class="ui-icon ui-icon-alert"></span></span>').tooltip();
9072			// }
9073
9074			$('#isy_license_active').html(licenseActiveHtml).tooltip();
9075			$('#isy_license_expiry').html(licenseExpiryHtml).tooltip();
9076
9077			var history = [];
9078
9079			if (obj.info.webStore.module) {
9080				history.push(obj.info.webStore.module);
9081				history = history.concat(obj.info.webStore.renewals)
9082			}
9083
9084			history = history.map(function(entry) {
9085				if (!entry.start_data && entry.install_date) {
9086					entry.start_date = entry.dpurch;
9087				}
9088
9089				return {
9090					// We convert all ISO string to a date back then to a local date
9091					dpurch: entry.txid === 'trial' ? 'Free trial' : dateFormatNoError(new Date(entry.dpurch), "yyyy-mm-dd"),
9092					start_date: dateFormatNoError(new Date(entry.start_date), "yyyy-mm-dd"),
9093					expiry: entry.expiry==null?'License inactive':dateFormatNoError(new Date(entry.expiry), "yyyy-mm-dd"),
9094				}
9095			});
9096
9097			if (history.length) {
9098				$('#isy_license_history').show();
9099
9100				// Build the History
9101				$('#isy_license_history_placeholder')
9102				.setTemplateElement('ipalHistoryTemplate')
9103				.processTemplate(history);
9104			}
9105			else {
9106				$('#isy_license_history').hide();
9107			}
9108		}
9109		else {
9110			$('#isy_license_expiry').text('');
9111			$('#isy_license_active').text('');
9112			$('#isy_license_history_placeholder').empty();
9113			$('#isy_license_history').hide();
9114			dialog.dialog('option', 'buttons', [closeButton]); // Set the dialog buttons
9115		}
9116
9117		// Pre-fetch list of options
9118		lookupRenewOptions = ajaxJsonGet('/api/ipal/renewal/options')
9119		.fail(function(jqXHR, textStatus, errorThrown) {
9120			errorMessageDialog('Error retrieving renewal options: HTTP error ' + jqXHR.status, 'Error', 350)
9121		})
9122	}
9123
9124	// We set it outside of setfields because we also use setfields from the children renew dialog, and we don't know there if we have a redirectUrl in this dialog
9125	function setRedirectUrl(u) {
9126		redirectUrl = u
9127	}
9128
9129	// This is set by the purchasing dialog (isyAccessLicenseRenewDialog)
9130	// We use it to know if we return success or failure on closing
9131	function setPurchaseSuccess(result) {
9132		purchaseWasSuccessful = result
9133	}
9134	// function uuidNotValidError() {
9135	// 	var message = 'The UUID ' + uuid + ' does not appear to be valid.<br><br>' +
9136	// 		'Please contact [email protected].';
9137	//
9138	// 	errorMessageDialog(message, 'Error', 800);
9139	// }
9140
9141	var mobilincWarningMessage = 'Please note that the installation of the ISY Portal module will deactivate the MobiLinc module. ' +
9142		'You will be able to configure the MobiLinc app to connect to your ISY through ISY Portal by following these instructions:<br><br> ' +
9143		'<a href="https://wiki.universal-devices.com/index.php?title=ISY_Portal_MobiLinc_Configuration" class="url80 editable_link" target="_blank">https://wiki.universal-devices.com/index.php?title=ISY_Portal_MobiLinc_Configuration</a><br><br>' +
9144		'Continue?';
9145
9146	function adminRevoke() {
9147		var message = 'This will revoke the last transaction. This will set the last transaction expiry to today, or completely remove the transaction if not started. ' +
9148			'If after removing the last transaction the license is expired, the module active flag will be set to false, the module will be removed from ISY, and it will be rebooted.<br><br>' +
9149			'Continue?';
9150
9151		confirmationDialog(message, 'Revoke last transaction', 800)
9152		.then(function () {
9153			ajaxJsonDelete('/api/isy/ipal/payment/revoke', $.param({
9154				id: pk, // ISY ID
9155				domain: isyDomain,
9156			}))
9157			.then(function(data, status, jqXHR) {
9158				var message = 'ISY Portal Access License has been revoked successfully.';
9159
9160				informationMessageDialog(message)
9161				.done(function() {
9162					// Refresh main access licence dialog with the new license info
9163					isyAccessLicenseDialog.setFields({ uuid: uuid, alias: alias, info: data, pk: pk, domain: isyDomain });
9164				})
9165			})
9166			.fail(function(jqXHR, textStatus, errorThrown) {
9167				var message = 'Error trying to revoke the last transaction.<br><br>' +
9168					(jqXHR.status==425?'Error: ' + jqXHR.responseText + '<br><br>':'');
9169
9170				errorMessageDialog(message, 'Error', 800);
9171			})
9172			.done();
9173		})
9174		.done();
9175	}
9176
9177	function redirectSuccess() {
9178		if (redirectUrl) {
9179			window.location.replace(redirectUrl + '?successful=true');
9180		}
9181	}
9182
9183	function redirectError(errMessage) {
9184		if (redirectUrl) {
9185			var redirect = redirectUrl + '?successful=false' + (errMessage && '&error=' + errMessage)
9186			window.location.replace(redirect);
9187		}
9188	}
9189
9190	function trial() {
9191
9192		if (!('productId' in info) && registered==='0') {
9193			errorMessageDialog('This unit is not registeded.<br/>Please use the Admin Console, go to the configuration tab and click on "Approve" to allow access from this account.', null, 750)
9194		} else {
9195			// if (info.webStore.uuidValid || true) {
9196			var message = 'Do you want to enable the ISY Portal free trial?';
9197
9198			// Give warning if the Mobilinc app will be deactivated
9199			if (info.webStore.mobilincModule && (info.webStore.mobilincModule.active || info.webStore.mobilincModule.installed)) {
9200				message += '<br><br>' + mobilincWarningMessage
9201			}
9202
9203			confirmationDialog(message, 'Enable trial?', 800)
9204			.then(function () {
9205				ajaxJsonPost('/api/isy/ipal/trial', $.param({
9206					id: pk, // ISY ID
9207					domain: isyDomain
9208				}))
9209				.then(function(data, status, jqXHR) {
9210					var message = 'ISY Portal free trial access license has been added successfully.' +
9211						(data.webStore.adminConsoleWarning?'<br><br>Please open the admin console and click on Help | Manage Modules to install the license.':'');
9212
9213					informationMessageDialog(message)
9214					.done(function() {
9215						// When we close the dialog, we will return success (if redirect is used)
9216						setPurchaseSuccess(true)
9217
9218						// Refresh main access licence dialog with the new license info
9219						isyAccessLicenseDialog.setFields({ uuid: uuid, alias: alias, info: data, pk: pk, domain: isyDomain });
9220
9221						//Refresh the table behind
9222						tableIsys.reload();
9223					})
9224				})
9225				.fail(function(jqXHR, textStatus, errorThrown) {
9226					var message = 'There was an error processing the free trial.<br><br>' +
9227						(jqXHR.status==425?'Error: ' + jqXHR.responseText + '<br><br>':'');
9228
9229					errorMessageDialog(message, 'Error', 800);
9230				})
9231				.done();
9232			})
9233			.done();
9234			// }
9235			// else {
9236			// 	uuidNotValidError()
9237			// }
9238		}
9239	}
9240
9241
9242	function buy() {
9243		if (info.webStore.module || getIsPolisyOrEisy(info.productId)) {
9244			// Give warning if the Mobilinc app will be deactivated
9245			if (info.webStore.mobilincModule && info.webStore.mobilincModule.active) {
9246				confirmationDialog(mobilincWarningMessage, 'MobiLinc warning', 800)
9247				.then(function () {
9248					openRenewalWindow();
9249				})
9250				.done();
9251			}
9252			else {
9253				openRenewalWindow();
9254			}
9255		} else if (!('productId' in info) && registered==='0') {
9256			errorMessageDialog('This unit is not registeded.<br/>Please use the Admin Console, go to the configuration tab and click on "Approve" to allow access from this account.', null, 750)
9257		} else {
9258			errorMessageDialog('This unit is no longer supported.<br/>A license can\'t be added.')
9259		}
9260	}
9261
9262	function openRenewalWindow() {
9263		var isLegacy = !getIsPolisyOrEisy(info.productId);
9264
9265		// Renewals are eligibles only when we have an initial or renewal already in place, and the module is still active
9266		var showRenew = info.webStore.type>=1 && info.webStore.module && info.webStore.module.active;
9267
9268		lookupRenewOptions
9269		.then(function(renewOptions) {
9270			renewOptions = renewOptions.data
9271			.filter(function (opt) { return (!!opt.islegacy) === isLegacy }) // renewOption.islegacy can be undefined, which means false
9272			.filter(function (opt) {
9273				return (showRenew && opt.isrenewal) || (!showRenew && !opt.isrenewal) // If we have an initial term or renewal which is active, we show renewals
9274			})
9275
9276			if (renewOptions.length) {
9277				isyAccessLicenseRenewDialog.setFields({
9278					uuid: uuid,
9279					alias: alias,
9280					pk: pk,
9281					domain: isyDomain,
9282					info: info,
9283					renewOptions: renewOptions,
9284					isLegacy: isLegacy
9285				});
9286				isyAccessLicenseRenewDialog.modal.dialog('open');
9287			} else {
9288				errorMessageDialog('There are no purchase options available for your unit as it is discontinued.', 'Error', 550)
9289			}
9290		})
9291		.fail(function(jqXHR, textStatus, errorThrown) {
9292			errorMessageDialog('Error retrieving renewal options: HTTP error ' + jqXHR.status, 'Error', 350)
9293		})
9294	}
9295
9296	var revokeButton = {
9297		text: 'Revoke last transaction',
9298		click: adminRevoke
9299	};
9300
9301	var trialButton = {
9302		text: 'Free trial',
9303		click: trial
9304	};
9305
9306	var buyButton = {
9307		text: 'Buy License',
9308		click: buy
9309	};
9310
9311	var renewButton = {
9312		text: 'Renew License',
9313		click: buy // Even if it's a "renewal", we check if the mobilinc module is active. ISY Portal module might exist but be deactivated while mobilinc is active!
9314	};
9315
9316	var closeButton = {
9317		text: 'Close',
9318		click: function() {
9319			$(this).dialog('close'); // Will trigger the close event, and redirect if required
9320		}
9321	};
9322
9323	var isyOptions = {
9324		autoOpen: false,
9325		width: 700,
9326		modal: true,
9327		// buttons defined dynamically
9328		open: function(event, ui) {
9329			dialog.dialog('option', 'position', { my: 'center', at: 'center', of: window, collision: 'fit' });
9330		},
9331		close: function() {
9332			var silentSuccess=true;
9333			var freeTrialAvailable = (info.webStore.module == null);
9334
9335			if (redirectUrl) {
9336				if (purchaseWasSuccessful) {
9337					redirectSuccess()
9338				} else {
9339					redirectError('Cancelled');
9340				}
9341			} else if (licenseActive) {
9342				// Don't show instructions if we don't even have a license. It was obvious in the popup.
9343
9344				accessTroubleshootingMessage({
9345					freeTrialAvailable: freeTrialAvailable,
9346					licenseActive: licenseActive,
9347					licenseInstalled: licenseInstalled,
9348					online: online,
9349					registered: registered,
9350					firmware: firmware
9351				}, silentSuccess)
9352			}
9353		}
9354	};
9355
9356	// Initializes the dialog
9357	dialog = $('#isy_license_dialog').dialog(isyOptions);
9358
9359	return {
9360		modal: dialog,
9361		init: init,
9362		setFields: setFields,
9363		setRedirectUrl: setRedirectUrl,
9364		setPurchaseSuccess: setPurchaseSuccess
9365	};
9366};
9367
9368// ISY Portal Access License main dialog
9369var isyAccessLicenseRenewDialogInit = function () {
9370	var paypalButtonTarget = '#isy_license_paypal_button',
9371		dialog,
9372		pk,
9373		uuid, // Kept to refresh isyAccessLicenseDialog
9374		alias, // Kept to refresh isyAccessLicenseDialog
9375		isyDomain,
9376		renewOptions,
9377		apiIsyIpalResult,
9378		expiryDate,
9379		renewalStartDate,
9380		ppCurrentEnv,
9381		isLegacy;
9382
9383	function setFields(obj) {
9384		pk = obj.pk;
9385		uuid = obj.uuid;
9386		alias = obj.alias;
9387		isyDomain = obj.domain;
9388		isLegacy = obj.isLegacy;
9389		renewOptions = obj.renewOptions;
9390		apiIsyIpalResult = obj.info;
9391		ppCurrentEnv = serverConfig.ppEnv; // Reset to default env specified on server
9392
9393		$('#isy_license_renew_uuid').text(obj.uuid);
9394		$('#isy_license_renew_alias').text(obj.alias);
9395
9396		var licenseExpiryHtml='';
9397		var licenseActiveHtml='';
9398
9399		if (!obj.info.webStore.module) {
9400			licenseExpiryHtml = '<span title="ISY Portal License has to be purchased.">No active license</span>';
9401			licenseActiveHtml='<span title="ISY Portal License has to be purchased.">No</span>'
9402		}
9403		else {
9404			if (obj.info.webStore.expiry==null) {
9405				licenseExpiryHtml = '<span>No expiration</span>'
9406			}
9407			else if (obj.info.webStore.expiry) {
9408				var expiryDate = new Date(obj.info.webStore.expiry);
9409
9410				// Set access license expired flag
9411				var expired = expiryDate.valueOf() < new Date().valueOf();
9412
9413				// Set access license will be expired soon flag
9414				var soon = new Date();
9415				soon.setDate(soon.getDate() + accessLicenseExpirationSoonDays);
9416				var soonExpired = !expired && expiryDate.valueOf() < soon.valueOf();
9417
9418				licenseExpiryHtml = '<span>' + dateFormatNoError(expiryDate, "yyyy-mm-dd") +'</span>' +
9419				(expired?'<span class="red expired" title="ISY Portal License is expired.">expired</span>':'') +
9420				(soonExpired?'<span class="warning expired" title="ISY Portal License is expiring soon."><span class="ui-icon ui-icon-alert"></span></span>':'')
9421			}
9422
9423			var licenseActive = obj.info.webStore.module.active && !expired;
9424			var licenseInstalled = obj.info.webStore.module.installed;
9425
9426			licenseActiveHtml = '<span>' + (licenseActive?'Yes':'No') + '</span>' +
9427				(licenseActive && !licenseInstalled?'<span class="red expired" title="Please open the admin console and click on Help | Manage Modules to install the license.">(License is not installed)</span>':'')
9428		}
9429
9430		// if (!obj.info.webStore.uuidValid) {
9431		// 	$('#isy_license_renew_uuid').html(obj.uuid + '<span class="warning" title="This UUID does not appear to be valid."><span class="ui-icon ui-icon-alert"></span></span>').tooltip();
9432		// }
9433
9434		$('#isy_license_renew_active').html(licenseActiveHtml).tooltip();
9435		$('#isy_license_renew_expiry').html(licenseExpiryHtml).tooltip();
9436
9437		renewalStartDate = new Date(apiIsyIpalResult.renewalStartDate);
9438
9439		var len = renewOptions.reduce(function(summary, element) {
9440			return Math.max(summary, element.description.length)
9441		}, 0);
9442
9443		renewOptions.sort(function(a, b) {
9444			// if (!a.duration) { return 1; }
9445			// if (!b.duration) { return -1; }
9446			return a.duration > b.duration;
9447		});
9448
9449		var selectOptions = renewOptions.map(function(option) {
9450			return {
9451				value: option._id,
9452				description: option.description
9453			}
9454		});
9455
9456		var em = (len*0.55)+5; // Formula found by trial and error
9457
9458		buildSelect(selectOptions, '#isy_license_renew_selector', ''+em+'em', function(event, ui) {
9459			displayPaymentOptions(ui.item.value)
9460		});
9461
9462		if (selectOptions.length>=1) {
9463			displayPaymentOptions(selectOptions[0].value)
9464		} else {
9465			displayPaymentOptions(null);
9466		}
9467	}
9468
9469	// Called when the renewal option changes.
9470	function displayPaymentOptions(id) {
9471		var options = renewOptions.filter(function(option) {
9472			return option._id===id;
9473		});
9474
9475		var option = options.length?options[0]:null;
9476
9477		if (option) {
9478			var newExpiry;
9479			var newExpiryStr;
9480
9481			if (option.duration) {
9482				newExpiry = new Date(renewalStartDate);
9483				newExpiry.setFullYear(newExpiry.getFullYear() + option.duration);
9484				newExpiryStr = dateFormatNoError(newExpiry, "yyyy-mm-dd")
9485			} else {
9486				newExpiryStr = '';
9487			}
9488
9489			var startDate = dateFormatNoError(renewalStartDate, "yyyy-mm-dd");
9490			$('#isy_license_renew_start').html(startDate);
9491			$('#isy_license_renew_end').html(newExpiryStr); //.tooltip();
9492			$('#isy_license_renew_price').text(option.price.toFixed(2)+'$');
9493
9494			var displayLegacyText = isLegacy && option.legacytext && option.legacytext.length>0
9495			$('#isy_license_renew_legacytext_display').toggle(displayLegacyText)
9496			$('#isy_license_renew_legacytext').html(option.legacytext)
9497
9498			var globalDescription = 'ISY Portal License' + (option.isrenewal?' Renewal':'') + ' for uuid:' + uuid + ' from ' + startDate + ' to ' + newExpiryStr;
9499			updatePaypalButton(option, globalDescription);
9500		}
9501		else {
9502			$('#isy_license_renew_start').empty();
9503			$('#isy_license_renew_end').empty();
9504			$('#isy_license_renew_price').empty();
9505		}
9506	}
9507
9508	// We pass the selector option
9509	function updatePaypalButton(option, globalDescription) {
9510		$('#isy_license_paypal_sandbox_indicator').text(ppCurrentEnv==='sandbox'?'[SANDBOX ENVIRONMENT]':'');
9511
9512		$(paypalButtonTarget).empty(); // Each time we change the option, we rebuild the button
9513
9514		paypal.Buttons({
9515			// LEGACY PAYPAL WEB UI
9516			// createOrder: function(data, actions) {
9517			//
9518			// 	// option example:
9519			// 	// description: "1 Year renewal"
9520			// 	// duration: 1
9521			// 	// isrenewal: true
9522			// 	// part: "LIC21075-1Y-RNW"
9523			// 	// price: 0.01
9524			//
9525			// 	sendLog({
9526			// 		tag: 'ipal',
9527			// 		subTag: 'ppcreate',
9528			// 		uuid: uuid,
9529			// 		message: 'Paypal Create',
9530			// 		content: {
9531			// 			option: option,
9532			// 			sku: option.part
9533			// 		}
9534			// 	});
9535			//
9536			// 	var orderRequest = {
9537			// 		intent: 'CAPTURE',
9538			// 		purchase_units: [{
9539			// 			// custom_id: option.part,
9540			// 			description: option.part, // Looks like this is not used
9541			// 			amount: {
9542			// 				currency_code: 'USD',
9543			// 				value: option.price,
9544			// 				breakdown: {
9545			// 					item_total: {
9546			// 						currency_code: 'USD',
9547			// 						value: option.price
9548			// 					}
9549			// 				}
9550			// 			},
9551			// 			items: [{
9552			// 				sku: option.part, // Second line on the receipt: "numéro de l'object <sku>"
9553			// 				name: 'ISY Portal License - ' + uuid,  // First line on the receipt
9554			// 				description: (option.description + ' uuid: ' + uuid).substring(0,127), // Does not show on the receipt
9555			// 				unit_amount: {
9556			// 					currency_code: 'USD',
9557			// 					value: option.price
9558			// 				},
9559			// 				quantity: 1
9560			// 			}]
9561			// 		}],
9562			// 		application_context: {
9563			// 			shipping_preference: 'NO_SHIPPING'
9564			// 		}
9565			// 	}
9566			//
9567			// 	// console.log('order', orderRequest)
9568			//
9569			// 	return actions.order.create(orderRequest)
9570			// 	.then(function (orderId) {
9571			// 		return sendLog({
9572			// 			tag: 'ipal',
9573			// 			subTag: 'ppcreated',
9574			// 			uuid: uuid,
9575			// 			message: 'Paypal orderId created',
9576			// 			content: {
9577			// 				option: option,
9578			// 				sku: option.part,
9579			// 				orderId: orderId
9580			// 			}
9581			// 		})
9582			// 		.then(function () {
9583			// 			return orderId
9584			// 		})
9585			// 	})
9586			// },
9587      //
9588			// // Finalize the transaction after payer approval
9589			// onApprove: function(data, actions) {
9590			// 	return actions.order.capture().then(function(orderData) {
9591			// 		// Successful capture!
9592			// 		// console.log('Capture result', orderData, JSON.stringify(orderData, null, 2));
9593			// 		var orderID = orderData.id
9594			// 		// var payerID = orderData.payer.payer_id;
9595			// 		var transaction = orderData.purchase_units[0].payments.captures[0];
9596			// 		var paymentID = transaction.id; // Need this in case of error
9597			//
9598			// 		if (registeredUser) {
9599			// 			sendLog({
9600			// 				tag: 'ipal',
9601			// 				subTag: 'ppapprove',
9602			// 				uuid: uuid,
9603			// 				message: 'Paypal Approve',
9604			// 				content: {
9605			// 					data: orderData,
9606			// 				}
9607			// 			})
9608			// 			.then(function () {
9609			// 				return ajaxJsonPost('/api/isy/ipal/payment/execute', $.param({
9610			// 					id: pk, // ISY ID
9611			// 					domain: isyDomain,
9612			// 					optionID: option._id, // We use it to verify the transaction amount has not been tampered with
9613			// 					orderID: orderID,
9614			// 					paymentID: paymentID,
9615			// 					env: ppCurrentEnv
9616			// 				}))
9617			// 			})
9618			// 			.then(function(data, status, jqXHR) {
9619			// 				var message = 'ISY Portal Access License purchase has been completed successfully.' +
9620			// 					(data.webStore.adminConsoleWarning?'<br><br>Please open the admin console and click on Help | Manage Modules to install the license.':'');
9621			//
9622			// 				informationMessageDialog(message)
9623			// 				.done(function() {
9624			// 					// When we close isyAccessLicenseDialog, we will return success (if redirect is used)
9625			// 					isyAccessLicenseDialog.setPurchaseSuccess(true)
9626			//
9627			// 					// Refresh main access licence dialog with the new license info
9628			// 					isyAccessLicenseDialog.setFields({ uuid: uuid, alias: alias, info: data, pk: pk, domain: isyDomain });
9629			//
9630			// 					//Refresh the table behind
9631			// 					tableIsys.reload();
9632			//
9633			// 					// And close this dialog
9634			// 					dialog.dialog('close');
9635			// 				})
9636			// 			})
9637			// 			.fail(function(jqXHR, textStatus, errorThrown) {
9638			// 				sendLog({
9639			// 					tag: 'ipal',
9640			// 					subTag: 'ppapproveFailure',
9641			// 					uuid: uuid,
9642			// 					message: 'Paypal Approve Failure',
9643			// 					content: {
9644			// 						textStatus: textStatus,
9645			// 						status: jqXHR && jqXHR.status,
9646			// 						responseText: jqXHR && jqXHR.responseText,
9647			// 						error: errorThrown
9648			// 					}
9649			// 				});
9650			//
9651			// 				var message = 'Paypal transaction completed, but we could not update your access license.<br><br>' +
9652			// 					(jqXHR.status==425?'Error: ' + jqXHR.responseText + '<br><br>':'') +
9653			// 					'Please contact [email protected] and provide your transaction ID:<br>' + paymentID;
9654			//
9655			// 				errorMessageDialog(message, 'Error', 800);
9656			// 			})
9657			// 			.done();
9658			// 		}	else {
9659			// 			errorMessageDialog('You are logged out of ISY Portal.<br>Transaction is cancelled.')
9660			// 			.always(function() {
9661			// 				redirectError('Transaction is cancelled');
9662			// 			})
9663			// 		}
9664			// 	});
9665			// }
9666
9667			// New server side order create
9668			// Ref: https://developer.paypal.com/sdk/js/reference/#createorder
9669			createOrder: function(data) {
9670				console.log('Creating order. Payment source selected: ' + data.paymentSource)
9671
9672				// New server side createOrder
9673				return ajaxJsonPost('/api/isy/ipal/order/create', $.param({
9674					id: pk, // ISY ID
9675					domain: isyDomain,
9676					sku: option.part,
9677					paymentSource: data.paymentSource,
9678					env: ppCurrentEnv
9679				}))
9680				.then(function(data, status, jqXHR) {
9681					return data.id
9682				})
9683				.fail(function(jqXHR, textStatus, errorThrown) {
9684					console.log('Order creation failed: ' + jqXHR.responseText)
9685				})
9686			},
9687
9688			// New server side order capture
9689			onApprove: function(data) {
9690				var orderID = data.orderID
9691				var paymentID = data.paymentID
9692				// var payerID = data.payerID
9693				var paymentSource = data.paymentSource
9694
9695				if (registeredUser) {
9696					// New server side createOrder
9697					return ajaxJsonPost('/api/isy/ipal/order/capture', $.param({
9698						id: pk, // ISY ID
9699						domain: isyDomain,
9700						env: ppCurrentEnv,
9701						orderID,
9702						// payerID,
9703						paymentID,
9704						paymentSource
9705					}))
9706					.then(function(data, status, jqXHR) {
9707						var message = 'ISY Portal Access License purchase has been completed successfully.' +
9708							(data.webStore.adminConsoleWarning?'<br><br>Please open the admin console and click on Help | Manage Modules to install the license.':'');
9709
9710						informationMessageDialog(message)
9711						.done(function() {
9712							// When we close isyAccessLicenseDialog, we will return success (if redirect is used)
9713							isyAccessLicenseDialog.setPurchaseSuccess(true)
9714
9715							// Refresh main access licence dialog with the new license info
9716							isyAccessLicenseDialog.setFields({ uuid: uuid, alias: alias, info: data, pk: pk, domain: isyDomain });
9717
9718							//Refresh the table behind
9719							tableIsys.reload();
9720
9721							// And close this dialog
9722							dialog.dialog('close');
9723						})
9724					})
9725					.fail(function(jqXHR, textStatus, errorThrown) {
9726						var message = 'Transaction failed.<br><br>' +
9727							(jqXHR.status==425?'Error: ' + jqXHR.responseText + '<br><br>':'')
9728
9729						errorMessageDialog(message, 'Error', 800);
9730					})
9731				} else {
9732					errorMessageDialog('You are logged out.<br>Transaction is cancelled.')
9733				}
9734			},
9735
9736			onError: function(err) {
9737				console.log('Order error', err)
9738
9739				sendLog({
9740					tag: 'ipal',
9741					subTag: 'orderError',
9742					uuid: uuid,
9743					message: 'Order error: ' + (err && err.message),
9744					content: {}
9745				});
9746			},
9747
9748			onCancel: function(data) {
9749				sendLog({
9750					tag: 'ipal',
9751					subTag: 'orderCancelled',
9752					uuid: uuid,
9753					message: 'Order cancelled: ' + data.orderID,
9754					content: {
9755						orderID: data.orderID
9756					}
9757				});
9758
9759				console.log('Order cancelled')
9760			}
9761		}).render(paypalButtonTarget);
9762	}
9763
9764	// Keypressed, active only when this dialog is open.
9765	// function keyEvent(e) {
9766	// 	if( e.which === 90 && e.ctrlKey && !e.shiftKey ){
9767	// 		var refreshPaypalButton=false;
9768	//
9769	// 		if (ppCurrentEnv=='sandbox') {
9770	// 			if (serverConfig.ppClientIdProduction) {
9771	// 				ppCurrentEnv='production';
9772	// 				refreshPaypalButton=true;
9773	// 			}
9774	// 			else {
9775	// 				errorMessageDialog('Production environment is not configured');
9776	// 			}
9777	// 		}
9778	// 		else if (ppCurrentEnv=='production') {
9779	// 			if (serverConfig.ppClientIdSandbox) {
9780	// 				ppCurrentEnv='sandbox';
9781	// 				refreshPaypalButton=true;
9782	// 			}
9783	// 			else {
9784	// 				errorMessageDialog('Sandbox environment is not configured');
9785	// 			}
9786	// 		}
9787	//
9788	// 		if (refreshPaypalButton) {
9789	// 			displayPaymentOptions($('#isy_license_renew_selector select').val())
9790	// 		}
9791	// 	}
9792	// }
9793
9794	var isyOptions = {
9795		autoOpen: false,
9796		width: 700,
9797		modal: true,
9798		buttons: {
9799			'Close': function() {
9800				$(this).dialog('close');
9801			}
9802		},
9803		open: function(event, ui) {
9804			dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
9805			// $(document).on('keydown.pp', keyEvent);
9806		},
9807		close: function( event, ui ) {
9808			// $(document).off('keydown.pp');
9809		}
9810	};
9811
9812	// Initializes the dialog
9813	dialog = $('#isy_license_renew_dialog').dialog(isyOptions);
9814
9815	return { modal: dialog, setFields: setFields };
9816};
9817
9818
9819// OpenADR Registration
9820var isyOpenadrRegDialogInit = function () {
9821	var dialog,
9822		xmlData,
9823		uuid,
9824		alias,
9825		pk,
9826		domain;
9827
9828
9829	function setFields(obj) {
9830		$('#isy_openadrReg_uuid').text(obj.uuid);
9831		$('#isy_openadrReg_alias').text(obj.alias);
9832
9833		uuid = obj.uuid;
9834		pk = obj.pk;
9835		alias = obj.alias;
9836		domain = obj.domain;
9837
9838		if (obj.xml) {
9839			xmlData = obj.xml;
9840			var $xml = $(obj.xml);
9841
9842			$('#isy_openadrReg_vtnid').text($xml.find('VtnID').text());
9843			$('#isy_openadrReg_venid').text($xml.find('VenID').text());
9844			$('#isy_openadrReg_partyid').text($xml.find('PartyID').text());
9845			$('#isy_openadrReg_resourceid').text($xml.find('ResourceID').text());
9846			$('#isy_openadrReg_groupid').text($xml.find('GroupID').text());
9847			$('#isy_openadrReg_marketcontext').text($xml.find('MarketContext').text());
9848			$('#isy_openadrReg_registrationid').text($xml.find('RegistrationID').text());
9849		}
9850		else {
9851			xmlData=null;
9852			uuid=null;
9853			$('#isy_openadrReg_vtnid').text('');
9854			$('#isy_openadrReg_venid').text('');
9855			$('#isy_openadrReg_partyid').text('');
9856			$('#isy_openadrReg_resourceid').text('');
9857			$('#isy_openadrReg_groupid').text('');
9858			$('#isy_openadrReg_marketcontext').text('');
9859			$('#isy_openadrReg_registrationid').text('');
9860		}
9861	}
9862
9863	function saveToDisk() {
9864		try {
9865			var isFileSaverSupported = !!new Blob;
9866		} catch (e) {}
9867
9868		if (isFileSaverSupported) {
9869			var str = new XMLSerializer().serializeToString(xmlData);
9870			var blob = new Blob([str], {type: "text/xml;charset=utf-8"});
9871			saveAs(blob, "profile " + uuidNC(uuid) + ".xml");
9872			$(this).dialog('close');
9873		}
9874		else {
9875			errorMessageDialog('Feature not supported on this browser', 'Not supported')
9876			.then(function() {
9877				$(this).dialog('close');
9878			})
9879		}
9880	}
9881
9882	var isyOptions = {
9883		autoOpen: false,
9884		width: 450,
9885		modal: true,
9886		buttons: {
9887			'Save to disk': saveToDisk,
9888			'Upload new profile': function() {
9889				$(this).dialog('close');
9890				isyOpenadrRegUploadDialog.setFields({ pk: pk, uuid: uuid, domain: domain, alias: alias });
9891				isyOpenadrRegUploadDialog.modal.dialog('open');
9892			}
9893		},
9894		open: function(event, ui) {
9895			dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
9896		}
9897	};
9898
9899	// Initializes the dialog
9900	dialog = $('#isy_openadrReg_dialog').dialog(isyOptions);
9901
9902	return { modal: dialog, setFields: setFields };
9903};
9904
9905// OpenADR Registration Profile upload
9906var isyOpenadrRegUploadDialogInit = function () {
9907	var dialog,
9908		uploader,
9909		pk,
9910		uuid,
9911		alias,
9912		domain,
9913		$uploadButton;
9914
9915	function resetUI() {
9916		$('#openadr_profile_upload_filename').text(' ');
9917		$uploadButton.hide();
9918	}
9919
9920	function setFields(obj) {
9921		pk = obj.pk;
9922
9923		domain = obj.domain;
9924		uuid = obj.uuid;
9925		alias = obj.alias;
9926		uploader.setData({ id: pk });
9927		uploader.setOptions({ customHeaders: { Authorization: 'Basic ' + registeredUser.basicAuth }});
9928	}
9929
9930	var isyOptions = {
9931		autoOpen: false,
9932		width: 450,
9933		modal: true,
9934		buttons: {
9935			'Choose a file': function () {},
9936			'Upload new profile': function () {
9937				$uploadButton.blur();
9938				uploader.submit();
9939			}
9940		},
9941		open: function(event, ui) {
9942			resetUI();
9943			dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
9944		}
9945	};
9946
9947	// Initializes the dialog
9948	dialog = $('#isy_openadrRegUpload_dialog').dialog(isyOptions);
9949
9950	// Set id on first button to allow uploader to hook to it
9951	var $buttons = $('#isy_openadrRegUpload_dialog').parent().find('.ui-dialog-buttonset button');
9952
9953	var buttonId = 'profile_upload'
9954	$buttons.first().attr('id', buttonId);
9955
9956	$uploadButton = $buttons.eq(1); // Second button
9957
9958	uploader = new ss.SimpleUpload({
9959		button: buttonId, // file upload button
9960		url: '/api/isy/oadr/save', // server side handler
9961		name: 'profile', // upload parameter name
9962		autoSubmit: false,
9963		maxSize: 3072, // Standard file is < 1k
9964		multipart: true,
9965//		responseType: 'json',
9966		dragClass: 'ui-state-hover',
9967		hoverClass: 'ui-state-hover',
9968		focusClass: 'ui-state-focus',
9969		disabledClass: 'ui-state-disabled',
9970		allowedExtensions: ['xml'],
9971		dropzone: 'openadr_profile_upload_filename',
9972		onChange: function(filename, extension) {
9973			$('#openadr_profile_upload_filename').text(filename);
9974			$uploadButton.show();
9975		},
9976		onExtError: function(filename, extension) {
9977			errorMessageDialog('File must be an XML with extension .xml', 'Error')
9978		},
9979		onSizeError: function(filename, size) {
9980			errorMessageDialog('File is too large', 'Error')
9981		},
9982		onError: function(filename, errorType, status, statusText, response, uploadBtn) {
9983			errorMessageDialog('Error uploading file<br>HTTP code ' + status, 'Error')
9984			.then(function () {
9985				resetUI();
9986			})
9987		},
9988		onComplete: function(filename, response) {
9989			informationMessageDialog('Profile uploaded successfully', 'Profile uploaded')
9990			.then(function () {
9991				dialog.dialog('close');
9992				loadOpenadrReg(uuid, alias, pk, domain);
9993			})
9994		}
9995	});
9996
9997	return { modal: dialog, setFields: setFields };
9998};
9999
10000// Last Connected Form
10001var lastConnectedDialogInit = function () {
10002	var dialog;
10003
10004	function setFields(data) {
10005		// Build the table
10006		$('#lastconnected_placeholder')
10007		.setTemplateElement('lastConnectedTemplate')
10008		.processTemplate(data);
10009	}
10010
10011	var isyOptions = {
10012		autoOpen: false,
10013		width: 700,
10014		modal: true,
10015		buttons: {
10016			'Close': function() {
10017				$(this).dialog('close');
10018			}
10019		},
10020		open: function(event, ui) {
10021			dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
10022		}
10023	};
10024
10025	// Initializes the dialog
10026	dialog = $('#lastconnected_dialog').dialog(isyOptions);
10027
10028	return { modal: dialog, setFields: setFields };
10029};
10030
10031function loginDialogInit() {
10032	var dialog, form, currentMode=0, cbAfterLogin,
10033		lastLoginAction,
10034		cookieInfo,
10035		email = $('#email'),
10036		password = $('#password'),
10037		rememberMeCheckbox = $('#login_rememberme'),
10038		allFields = $([]).add(email).add(password).add(rememberMeCheckbox),
10039		tips = $('#logindialog .validateTips');
10040
10041	function resetForm() {
10042		tips.text('Please enter your email and password.');
10043		allFields.removeClass('fieldfocus');
10044		form[0].reset();
10045	}
10046
10047	// Initializes the callback (afterLogin)
10048	function setFields(obj) {
10049		lastLoginAction = obj.lastLoginAction?obj.lastLoginAction:'portal';
10050		rewriteButton();
10051
10052		rememberMeCheckbox.prop('checked', obj.rememberMeCheckbox);
10053
10054		cbAfterLogin=obj.cb;
10055		cookieInfo = obj.cookieInfo;
10056
10057		if (cookieInfo) {
10058			email.val(cookieInfo.username);
10059			password.val(cookieInfo.password)
10060		}
10061
10062		if (obj.mode) {
10063			switchMode(obj.mode); // 0=login, 2=forgot your password 4=password change
10064		}
10065	}
10066
10067	function login() {
10068		var valid = true;
10069		allFields.removeClass('fieldfocus');
10070		valid = valid && checkLength(tips, email, 'email', 6, 80);
10071		valid = valid && checkRegexp(tips, email, emailRegexWithSpace, 'email address is not valid.' );
10072		valid = valid && checkNotEmpty(tips, password, 'password');
10073
10074		if (valid) {
10075			email.val(email.val().trim());
10076			var password_value = password.val();
10077
10078			ajaxJsonPost('/api/login', form.find('.login').serialize())
10079			.then(function(data, status, jqXHR) {
10080				if (data) {
10081					setLastConnected(data.user.lastConnected);
10082
10083					if (rememberMeCheckbox.prop('checked')) {
10084						setLocalStorage('udi_remember_me', true);
10085
10086						if (data.rememberMeToken) {
10087							Cookies.set('udi_user', data.rememberMeToken, { expires: 30 });
10088						}
10089					}	else {
10090						deleteLocalStorage('udi_remember_me');
10091						Cookies.remove('udi_user');
10092					}
10093
10094					if (data.user.passwordchange) {
10095						var afterPwChange = function (new_password_value) {
10096							// Sets the registeredUser with the new password only if the change was successful
10097							setRegisteredUser(data.user, { password: new_password_value?new_password_value:password_value }, data.account);
10098
10099							if (cbAfterLogin) {
10100								cbAfterLogin(data, lastLoginAction);
10101							}
10102						};
10103
10104						passwordChangeDialog.setFields({ pk: data.user._id, cb: afterPwChange });
10105						passwordChangeDialog.modal.dialog('open');
10106					}	else {
10107						setRegisteredUser(data.user, { password: password_value }, data.account);
10108
10109						if (cbAfterLogin) {
10110							cbAfterLogin(data, lastLoginAction);
10111						}
10112					}
10113				}
10114
10115				dialog.dialog('close');
10116			})
10117			.fail(function(jqXHR, textStatus, errorThrown) {
10118				if (jqXHR.status==403) {
10119					updateTips(tips, 'Access denied');
10120				}
10121				else {
10122					updateTips(tips, 'HTTP error ' + jqXHR.status);
10123				}
10124			})
10125			.done();
10126		}
10127	}
10128
10129	function gsi_login(loginParams) {
10130		ajaxJsonPost('/api/gsi/login', new URLSearchParams(loginParams).toString())
10131		.then(function(data, status, jqXHR) {
10132			if (data) {
10133				setLastConnected(data.user.lastConnected);
10134
10135				// This is only for password pased login
10136				deleteLocalStorage('udi_remember_me');
10137				Cookies.remove('udi_user');
10138
10139				setRegisteredUser(data.user, { jwt: data.jwtCredentials }, data.account);
10140
10141				if (cb) {
10142					cb(data, lastLoginAction);
10143				}
10144			}
10145
10146			dialog.dialog('close');
10147		})
10148		.fail(function(jqXHR, textStatus, errorThrown) {
10149			if (jqXHR.status==403) {
10150				updateTips(tips, 'Access denied');
10151			}
10152			else {
10153				updateTips(tips, 'HTTP error ' + jqXHR.status);
10154			}
10155		})
10156		.done();
10157	}
10158
10159	function forgotYourPassword() {
10160		var valid = true;
10161		var o2Location = null
10162
10163		allFields.removeClass('fieldfocus');
10164		valid = valid && checkLength(tips, email, 'email', 6, 80);
10165		valid = valid && checkRegexp(tips, email, emailRegexWithSpace, 'email address is not valid.' );
10166
10167		if (valid) {
10168			var username = email.val().trim()
10169			var data = 'username='+username;
10170
10171			// If we have a client_id, it's a redirect from the authorization page.
10172			var isFromAuthPage = $.gup('client_id')
10173
10174			// We have to capture the query string to use it after the user is verified.
10175			if (isFromAuthPage) {
10176				o2Location = window.location.search
10177			}
10178
10179			ajaxJsonPost('/api/pin/request', data)
10180			.done(function () {
10181				dialog.dialog('close');
10182
10183				// Callback: Runs after pin successfully entered (when forget your password is from the login page)
10184				var afterPin = function (verifyResponse, pin) {
10185					// verifyResponse is the result of /api/pin/verify. It's the same as /login
10186
10187					console.log('Pin verification is successful')
10188
10189					var afterPwChange = function (new_password_value) {
10190						// Sets the registeredUser with the new password only if the change was successful
10191						setRegisteredUser(verifyResponse.user, { password: new_password_value }, verifyResponse.account);
10192
10193						if (isFromAuthPage) {
10194							window.location.replace('/o2/authorize' + o2Location + '&'  +
10195								$.param({ email: registeredUser.username, password: registeredUser.password, new:true }));
10196						} if (cbAfterLogin) {
10197							cbAfterLogin(verifyResponse, 'portal');
10198						}
10199					};
10200
10201					passwordChangeDialog.setFields({ pk: verifyResponse.user._id, pin: pin, cb: afterPwChange });
10202					passwordChangeDialog.modal.dialog('open');
10203				}
10204
10205				pinDialog.setFields({ username: username, cb: afterPin })
10206				pinDialog.modal.dialog('open')
10207			})
10208			.fail(function(jqXHR, textStatus, errorThrown) {
10209				// Should only happen in case of system error (server not responding or username not passed to api)
10210				informationMessageDialog("Password reset error.<br>HTTP error code " + jqXHR.status, "Error")
10211			})
10212			.done();
10213		}
10214	}
10215
10216	var buttonLabels = {
10217		'portal' : 'Portal Login',
10218		'isy' : 'Go to my ISY'
10219	};
10220
10221	function rewriteButton() {
10222		$('#rerun').button('option', 'label', buttonLabels[lastLoginAction])
10223	}
10224
10225	function loginSplitButton() {
10226		// Rewrite the button pane as we need it
10227		var $buttonset = $("#logindialog").parent().find('.ui-dialog-buttonset')
10228		.addClass('ui-dialog-buttonset-fixedheigth')  // Prevents the menu from extending dialog heigth
10229		.html('<div class="modalbuttonset"><button id="rerun">' +
10230			  buttonLabels[lastLoginAction] +
10231			  '</button><button id="select"></button></div>' +
10232			  '<ul class="ui-front"><li id="splitMenuPortal">Portal Login</li><li id="splitMenuISY">Go to my ISY</li></ul>');
10233
10234		$('#rerun').button().click(function() {
10235			login();
10236		});
10237
10238		$('#select').button({
10239			text: false,
10240			icons: {
10241				primary: "ui-icon-triangle-1-s"
10242		}})
10243		.click(function() {
10244			  var menu = $(this).parent().next().toggle().position({
10245				my: "left top",
10246				at: "left bottom",
10247				of: this
10248			});
10249
10250			$(document).one("click", function() {
10251				menu.hide();
10252			});
10253
10254			return false;
10255		})
10256		.parent().buttonset().next().hide().menu();
10257
10258		$('#splitMenuPortal').click(function() {
10259			lastLoginAction='portal';
10260			rewriteButton();
10261			login();
10262		});
10263
10264		$('#splitMenuISY').click(function() {
10265			lastLoginAction='isy';
10266			rewriteButton();
10267			login();
10268		})
10269	}
10270
10271	function switchMode(mode) { // 0=login, 2=forgot your password 4=password change
10272		allFields.removeClass('fieldfocus');
10273		currentMode=mode;
10274
10275		switch (mode) {
10276			case 0:	dialog.dialog('option', loginOptions);
10277					// Login
10278					loginSplitButton();
10279					$('[aria-describedby="logindialog"] .ui-dialog-title').text('Login');
10280					tips.text('Please enter your email and password.');
10281					$('#logindialog fieldset :not(.login)').hide();
10282					$('#logindialog fieldset .login').show();
10283					$('#loginforgot').show();
10284					$('#logincreate').show();
10285					$('.g_id_signin').show();
10286					$('#email').prop('readonly', false);
10287					$('#email').focus();
10288					$('#email').removeProp('tabindex');
10289					break;
10290
10291			// Forgot your password
10292			case 2:	dialog.dialog('option', fypOptions);
10293					$('[aria-describedby="logindialog"] .ui-dialog-title').text('Forgot your password?');
10294					tips.text('Enter your email address to receive a password reset pin.');
10295					$('#logindialog fieldset :not(.fyp)').hide();
10296					$('#logindialog fieldset .fyp').show();
10297					$('#loginforgot').hide();
10298					$('#logincreate').hide();
10299					$('#logindeleteprofile').hide();
10300					$('.g_id_signin').hide();
10301
10302
10303					$('#email').prop('readonly', false);
10304					$('#email').focus();
10305					$('#email').removeProp('tabindex');
10306					break;
10307		}
10308
10309		$('#logindialog .submit').show(); // In all cases, it has to be visible for IE! (CSS hides it well)
10310	}
10311
10312	var loginOptions = {
10313		autoOpen: false,
10314		width: 450,
10315		modal: true,
10316		buttons: [{}], // Button pane created with dummy empty button, will be rewritten by loginSplitButton
10317		close: function() {
10318			form[0].reset();
10319			allFields.removeClass('fieldfocus');
10320		},
10321		open: function(event, ui) {
10322			dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
10323		}
10324	};
10325
10326	var fypOptions = {
10327		autoOpen: false,
10328		width: 350,
10329		modal: true,
10330		buttons: {
10331			'Send': forgotYourPassword,
10332			Cancel: function() {
10333				switchMode(0);
10334			}
10335		},
10336		close: function() {
10337			form[0].reset();
10338			allFields.removeClass('fieldfocus');
10339		},
10340		open: function(event, ui) {
10341			dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
10342		}
10343	};
10344
10345	// Initializes the dialog
10346	dialog = $('#logindialog').dialog(loginOptions);
10347	loginSplitButton();
10348
10349	// Forgot Your password link
10350	dialog.find('#loginforgot').click(function(event) {
10351		switchMode(2);
10352	});
10353
10354	// Create new account link
10355	dialog.find('#logincreate').click(function(event) {
10356		selfRegistrationDialog.setFields({ email: email.val(), cb: afterLogin });
10357		dialog.dialog('close');
10358		selfRegistrationDialog.modal.dialog('open');
10359	});
10360
10361	// Handles keyboad enter (submit event)
10362	form = dialog.find('form').submit(function(event) {
10363		event.preventDefault();
10364
10365		switch (currentMode) {
10366			case 0:	login();
10367					break;
10368
10369			case 2:	forgotYourPassword();
10370					break;
10371		}
10372	});
10373
10374	// Enables mouseover on top right tabs
10375	$('#loginreg .loginnav').hover(
10376		function() {
10377			$(this).addClass('ui-state-hover');
10378		},
10379		function() {
10380			$(this).removeClass('ui-state-hover');
10381		}
10382	);
10383
10384	// Setup onclick for top-right tabs
10385	$('#loginbutton').click(function() {
10386		switchMode(0);
10387		dialog.dialog('close'); // Required, in case it is already open. Otherwise, focus will not be set properly.
10388
10389		loginDialog.resetForm();
10390		loginDialog.setFields({ cb: afterLogin, lastLoginAction: 'portal', rememberMeCheckbox: rememberMeActive(), cookieInfo: rememberMe() });
10391		loginDialog.modal.dialog('open');
10392	});
10393
10394	$('#logoutbutton').click(function() {
10395		logout();
10396	});
10397
10398	// Setup onclick for top-right tabs
10399	$('#myprofilebutton').click(function() {
10400		openMyProfile();
10401	});
10402
10403	// Setup onclick for top-right tabs
10404	// $('#openpolyglot').click(function() {
10405	// 	window.open('https://polyglot.isy.io', '_blank', 'menubar=1,location=1,resizable=1,scrollbars=1,status=1,toolbar=1');
10406	// });
10407
10408	// Show main top-right toolbar
10409	$('#loginreg').show();
10410
10411	return { modal: dialog, resetForm: resetForm, setFields: setFields, gsi_login: gsi_login };
10412}
10413
10414// Password change dialog
10415function passwordChangeDialogInit() {
10416	var dialog, form, pk, cb=null, pin,
10417		oldpassword = $('#change_oldpassword'),
10418		password = $('#change_password'),
10419		passwordconf = $('#change_passwordconf'),
10420		allFields = $([]).add(oldpassword).add(password).add(passwordconf),
10421		tips = $('.validateTips'),
10422		new_password; // Contains the new password if submit successful.
10423
10424	// We change the type here instead the html to avoid password tools issues
10425	oldpassword.attr('type', 'password');
10426	password.attr('type', 'password');
10427	passwordconf.attr('type', 'password');
10428
10429	function resetForm() {
10430		allFields.removeClass('fieldfocus');
10431		form[0].reset();
10432		pk=null;
10433		cb=null;
10434		pin=null;
10435		new_password=null;
10436	}
10437
10438	function setFields(obj) {
10439		resetForm();
10440		cb=obj.cb
10441		pin=obj.pin
10442		pk=obj.pk
10443
10444		if (pin)
10445			tips.text('Please enter a new password.');
10446		else
10447			tips.text('Please enter old and new password.');
10448
10449		// If we authenticate with a pin, hide the old password field
10450		oldpassword.toggle(!pin)
10451	}
10452
10453	function submit() {
10454		var valid = true;
10455		allFields.removeClass('fieldfocus');
10456		valid = valid && checkPwd(tips, password);
10457		valid = valid && checkPwdEqual(tips, password, passwordconf, 'Passwords confirmation is not identical');
10458
10459		if (valid) {
10460			var data = form.find('input').serialize();
10461
10462			if (pk) {
10463				data += '&id='+pk;
10464			}
10465
10466			if (pin) {
10467				data += '&pin='+pin;
10468			}
10469
10470			ajaxJsonPost('/api/password-change', data)
10471			.then(function(data, status, jqXHR) {
10472				// Closing the window also triggers the callback (afterLogin). See dialog options.
10473				new_password = password.val();
10474				dialog.dialog('close');
10475				informationMessageDialog("Password change successful", "Success")
10476			})
10477			.fail(function(jqXHR, textStatus, errorThrown) {
10478				if (jqXHR.status==404) {
10479					updateTips(tips, 'User code not found');
10480				}
10481				else if (jqXHR.status==403) {
10482					updateTips(tips, 'Old password does not match');
10483				}
10484				else if (jqXHR.status==424) {
10485					updateTips(tips, 'New password is too easy. Please choose a more complex one.');
10486				}
10487				else {
10488					updateTips(tips, 'HTTP error ' + jqXHR.status);
10489				}
10490			})
10491			.done();
10492		}
10493	}
10494
10495	var dialogOptions = {
10496		autoOpen: false,
10497		modal: true,
10498		width: 350,
10499		closeOnEscape: false,
10500		dialogClass: "no-close",
10501		buttons: {
10502			'Save': function() {
10503				submit();
10504			},
10505			Cancel: function() {
10506				$(this).dialog('close');
10507			}
10508		},
10509		close: function() {
10510			if (cb) {
10511				cb(new_password);
10511 // Contains new password only if password change was successful
10512			}
10513		},
10514		open: function(event, ui) {
10515			dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
10516		}
10517	};
10518
10519	// Initializes the dialog
10520	dialog = $('#passwordchangedialog').dialog(dialogOptions);
10521
10522	password.focus();
10523
10524	// Handles keyboad enter (submit event)
10525	form = dialog.find('form').submit(function(event) {
10526		event.preventDefault();
10527		submit();
10528	});
10529
10530	// Setup onclick for My Profile Dialog
10531	$('#myprofile_changepassword').click(function() {
10532		myProfileDialog.modal.dialog('close');
10533		var afterPwChange = function (new_password_value) {
10534			// Sets the registeredUser with the new password only if the change was successful
10535			if (new_password_value) {
10536				setRegisteredUserCredentials({ password: new_password_value });
10537			}
10538		};
10539
10540		resetForm();
10541		setFields({ pk: registeredUser._id, cb: afterPwChange });
10542		dialog.dialog('open');
10543	});
10544
10545	return { modal: dialog, setFields: setFields, resetForm: resetForm };
10546}
10547
10548// Pin dialog - used for password reset
10549function pinDialogInit() {
10550	var dialog, form, cb, username
10551	var form = document.getElementById('pin-form');
10552	var inputs = form.querySelectorAll('input');
10553
10554	// Handle input behavior
10555	inputs.forEach((input, index) => {
10556		input.addEventListener('keyup', (e) => {
10557			if (e.key >= 0 && e.key <= 9) {
10558				if (index < inputs.length - 1) {
10559					inputs[index + 1].focus();
10560				} else {
10561					// When entering the last digit, submit!
10562					submit()
10563				}
10564			} else if (e.key === 'Backspace') {
10565				if (index > 0) {
10566					inputs[index - 1].value = '';
10567					inputs[index - 1].focus();
10568				}
10569			}
10570		});
10571
10572		// Prevent non-numeric inputs
10573		input.addEventListener('input', (e) => {
10574			e.target.value = e.target.value.replace(/[^0-9]/g, '');
10575		});
10576	});
10577
10578	function resetForm() {
10579		inputs.forEach(input => input.value = '');
10580
10581		// When user had an incorrect pin, we need the setTimeout so the information dialog
10582		// is closed. Otherwise the input field is hidden and focus doesn't work.
10583		setTimeout(function () {
10584			inputs[0].focus();
10585		}, 0);
10586	}
10587
10588	function setFields(obj) {
10589		resetForm()
10590
10591		if (obj.username) {
10592			username = obj.username
10593		}
10594
10595		if (obj.cb) {
10596			cb = obj.cb;
10597		}
10598	}
10599
10600	function submit() {
10601		var pin = Array.from(inputs)
10602			.map(input => input.value)
10603			.join('');
10604
10605		// Should never happen as we submit on the last digit entered
10606		if (pin.length !== 6) {
10607			informationMessageDialog('Please enter the 6-digits PIN', 'Error')
10608			.then(function () {
10609				resetForm()
10610			})
10611
10612			return
10613		}
10614
10615		var data = 'pin=' + pin + '&username=' + username
10616
10617		ajaxJsonPost('/api/pin/verify', data)
10618		.then(function(verifyData, status, jqXHR) {
10619			dialog.dialog('close');
10620
10621			if (cb) {
10622				cb(verifyData, pin);
10623			}
10624		})
10625		.fail(function(jqXHR, textStatus, errorThrown) {
10626			informationMessageDialog('Pin verification failed.', 'Error')
10627			.then(function () {
10628				resetForm()
10629			})
10630		})
10631		.done();
10632	}
10633
10634	var dialogOptions = {
10635		autoOpen: false,
10636		modal: true,
10637		width: 400,
10638		// closeOnEscape: false,
10639		// dialogClass: 'no-close',
10640		buttons: {
10641			Cancel: function() {
10642				$(this).dialog('close');
10643				loginDialog.modal.dialog('open');
10644			}
10645		},
10646		// close: function() {},
10647		open: function(event, ui) {
10648			dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
10649		}
10650	};
10651
10652	// Initializes the dialog
10653	dialog = $('#pin-dialog').dialog(dialogOptions);
10654
10655	// Handles keyboad enter (submit event)
10656	form = dialog.find('form').submit(function(event) {
10657		event.preventDefault();
10658		save();
10659	});
10660
10661	return { modal: dialog, setFields: setFields };
10662}
10663
10664
10665
10666// This needs to be global for recaptcha to work
10667function selfRegistrationRecaptchaSubmit(token) {
10668	selfRegistrationDialog.submit(token)
10669}
10670
10671// Self-Registration dialog
10672function selfRegistrationDialogInit() {
10673	var dialog, form, cb,
10674		fname = $('#selfr_fname'),
10675		lname = $('#selfr_lname'),
10676		company = $('#selfr_company'),
10677		email = $('#selfr_email'),
10678		password = $('#selfr_password'),
10679		passwordconf = $('#selfr_passwordconf'),
10680		allFields = $([]).add(fname).add(lname).add(company).add(email).add(password).add(passwordconf),
10681		tips = $('#selfregistrationdialog .validateTips');
10682
10683	// We change the type here instead of the html to avoid password tools issues
10684	password.attr('type', 'password');
10685	passwordconf.attr('type', 'password');
10686
10687	function resetForm() {
10688		tips.text('Please enter new account information.');
10689		allFields.removeClass('fieldfocus');
10690		form[0].reset();
10691	}
10692
10693	function setFields(obj) {
10694		resetForm();
10695
10696		if (obj.email) {
10697			email.val(obj.email);
10698		}
10699
10700		if (obj.cb) {
10701			cb = obj.cb;
10702		}
10703	}
10704
10705	function save() {
10706		var valid = true;
10707		allFields.removeClass('fieldfocus');
10708		valid = valid && checkLength(tips, fname, 'First name', 2, 50);
10709		valid = valid && checkLength(tips, lname, 'Last name', 2, 50);
10710		valid = valid && checkLength(tips, email, 'email', 6, 80);
10711		valid = valid && checkRegexp(tips, email, emailRegexWithSpace, 'email address is not valid.' );
10712		valid = valid && checkPwd(tips, password);
10713		valid = valid && checkPwdEqual(tips, password, passwordconf, 'Passwords confirmation is not identical');
10714
10715		if (valid) {
10716			grecaptcha.execute();
10717		}
10718	}
10719
10720	function submit(token) {
10721		email.val(email.val().trim());
10722
10723		var data = form.find('input').serialize();
10724
10725		data += "&token=" + token;
10726
10727		// If we have a client_id, it's a redirect from the authorization page.
10728		// We have to capture the query string to use it after the user is verified.
10729		if ($.gup('client_id')) {
10730			data += "&o2=" + btoa(window.location.search)
10731		}
10732
10733		var emailVerificationToken = $.gup('emailverification');
10734
10735		ajaxJsonPut('/api/account/self_register', data)
10736		.then(function(data, status, jqXHR) {
10737			dialog.dialog('close');
10738
10739			setLastConnected([]);
10740			setRegisteredUser(data.user, { password: password.val() }, data.account);
10741
10742			informationMessageDialog("Account created successfully.<br>In order to verify your email address, please check your inbox and follow the instructions.", "Success")
10743			.then(function () {
10744//					if (cb) {
10745//						cb(data, 'portal');
10746//					}
10747			})
10748		})
10749		.fail(function(jqXHR, textStatus, errorThrown) {
10750			if (jqXHR.status==426) {
10751				updateTips(tips, 'reCAPTCHA validation failed.');
10752			}
10753			else {
10754				if (jqXHR.status==420) {
10755					updateTips(tips, 'Email address is already in use.');
10756				}
10757				else if (jqXHR.status==424) {
10758					updateTips(tips, 'New password is too easy. Please choose a more complex one.');
10759				}
10760				else if (jqXHR.status==425) {
10761					updateTips(tips, jqXHR.responseText);
10762				}
10763				else {
10764					updateTips(tips, 'HTTP error ' + jqXHR.status);
10765				}
10766
10767				// Otherwise user would not be able to submit if let's say email address was already in use
10768				grecaptcha.reset()
10769			}
10770
10771		})
10772		.done();
10773	}
10774
10775	var dialogOptions = {
10776		autoOpen: false,
10777		modal: true,
10778		width: 400,
10779		closeOnEscape: false,
10780		dialogClass: "no-close",
10781		buttons: {
10782			'Save': function() {
10783				save();
10784			},
10785			Cancel: function() {
10786				$(this).dialog('close');
10787				loginDialog.modal.dialog('open');
10788			}
10789		},
10790		close: function() {
10791		},
10792		open: function(event, ui) {
10793			dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
10794		}
10795	};
10796
10797	// Initializes the dialog
10798	dialog = $('#selfregistrationdialog').dialog(dialogOptions);
10799
10800	// Handles keyboad enter (submit event)
10801	form = dialog.find('form').submit(function(event) {
10802		event.preventDefault();
10803		save();
10804	});
10805
10806	return { modal: dialog, setFields: setFields, resetForm: resetForm, submit:submit };
10807}
10808
10809// My Profile
10810var myProfileDialogInit = function () {
10811	var dialog,
10812		form,
10813		isy = $('#preferred_isy'),
10814		username = $('#myprofile_user_name'),
10815//		amz_email = $('#myprofile_amz_email'),
10816		notificationmaint = $('#myprofile_notificationmaint'),
10817		notificationnews = $('#myprofile_notificationnews'),
10818		allFields = $([]).add(isy).add(username).add(notificationmaint).add(notificationnews),
10819		tips = $('#myprofile_dialog .validateTips'),
10820		voiceSelector = '#myprofile_voice_isy_selector',
10821		voiceSelectorSelect = '#myprofile_voice_isy_selector' + ' select';
10822
10823
10824	function resetForm() {
10825		tips.text('You can change your profile');
10826		allFields.removeClass('fieldfocus');
10827		form[0].reset();
10828	}
10829/*
10830	function setAmzField(value) {
10831		amz_email.val(value);
10832
10833		if (value && value.length) {
10834			$('#linkAmazonWrapper').hide();
10835			$('#unlinkAmazon').show();
10836		}
10837		else {
10838			$('#linkAmazonWrapper').show();
10839			$('#unlinkAmazon').hide();
10840		}
10841	}
10842*/
10843	function setFields(obj) {
10844		resetForm();
10845		username.val(obj.username);
10846		username.prop('readonly', true);
10847//		setAmzField(obj.amz_email);
10848		notificationmaint.prop('checked', obj.notificationmaint);
10849		notificationnews.prop('checked', obj.notificationnews);
10850		isy.combobox("option", "source", obj.uuidList);
10851		isy.combobox("option", "data", obj.uuid);
10852
10853        if (obj.voiceSelector) {
10854            $(voiceSelectorSelect).val(obj.voiceSelector); // Set user voice selector
10855        }
10856        // resetForm sets it ot the first option.
10857        // else {
10858        //     // Selects the first option by default
10859        //     $(voiceSelectorSelect).val($(select + ' option:first').val());
10860        // }
10861
10862        $(voiceSelectorSelect).selectmenu("refresh");
10863	}
10864
10865	function submit() {
10866		var valid = true;
10867
10868		allFields.removeClass('fieldfocus');
10869
10870		isy.combobox("refresh"); // Makes sure the real input field is up to date
10871
10872		if (isy.val().length && !isy.combobox("validate")) {
10873			updateTips(tips, 'UUID is not allowed')
10874		}
10875		else {
10876//			var data = form.find('input').serialize();
10877			var data = serializeFormWithCheckboxes(form.find('input'));
10878			data += '&voiceSelector=' + $(voiceSelectorSelect).val();
10879
10880			submitProcess(data, false)
10881		}
10882	}
10883
10884	function submitProcess(data, isRetry) {
10885		ajaxJsonPost('/api/user', data)
10886		.then(function(data, status, jqXHR) {
10887			setRegisteredUserProfile(data);
10888			dialog.dialog('close');
10889
10890			// If table is initialized, make it refresh
10891			if (tableUsers.api) {
10892				tableUsers.reload();
10893			}
10894		})
10895		.fail(function(jqXHR, textStatus, errorThrown) {
10896			// Preferred ISY changed
10897			// if (jqXHR.status==426) {
10898			// 	if (!isRetry) {
10899			// 		confirmationDialog(preferredIsyChangeWarning, 'Preferred ISY Change', 800)
10900			// 		.then(function () {
10901			// 			// Retry the request, bypass the preferred ISY change validation
10902			// 			submitProcess(data+'&confirmed=true', true)
10903			// 		})
10904			// 		.fail(function() {
10905			//
10906			// 		})
10907			// 		.done();
10908			// 	}
10909			// 	else {
10910			// 		errorMessageDialog('Error updating the preferred ISY', 'Error', 350)
10911			// 	}
10912			// }
10913			// else {
10914				errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
10915			// }
10916		})
10917		.done();
10918	}
10919
10920	var isyOptions = {
10921		autoOpen: false,
10922		width: 450,
10923		height: 500,
10924		modal: true,
10925		buttons: {
10926			'Save': function() {
10927				submit();
10928			},
10929			Cancel: function() {
10930				$(this).dialog('close');
10931			}
10932		},
10933		open: function(event, ui) {
10934			dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
10935			$('.custom-combobox-input').focus();
10936		}
10937	};
10938
10939	// Initializes the dialog
10940	dialog = $('#myprofile_dialog').dialog(isyOptions);
10941
10942	isy.combobox({inputClass: ''});
10943
10944	// Handles keyboad enter (submit event)
10945	form = dialog.find('form').submit(function(event) {
10946		event.preventDefault();
10947		submit();
10948	});
10949/*
10950	$('#linkAmazon').click(function() {
10951		amazon.Login.authorize({ scope : 'profile' }, 'https://' + location.hostname + '/amazon_link.htm');
10952	});
10953
10954	$('#unlinkAmazon').click(function() {
10955		ajaxJsonPost('/api/voice/amazon/unlink')
10956		.then(function(data, status, jqXHR) {
10957			registeredUser.amz_email='';
10958			registeredUser.amz_user_id='';
10959			setSessionStorage('udiportal', { registeredUser: registeredUser });
10960			setAmzField('');
10961		})
10962		.fail(function(jqXHR, textStatus, errorThrown) {
10963			errorMessageDialog("Error unlinking Amazon user profile<br/>HTTP error " + jqXHR.status, "Failure")
10964		})
10965		.done();
10966	});
10967*/
10968	$('#preferredIsyHelp').click(function() {
10969		informationMessageDialog('The preferred ISY is used during login when you select "Go to my ISY". It is also used by Amazon Echo and Google Home to direct request to your preferred ISY.', 'Help', 500)
10970	});
10971
10972	var voiceOptions = [
10973        {
10974            value: '$preferred',
10975            description: 'Use preferred ISY'
10976        },
10977        {
10978            value: '$all',
10979            description: 'Use all ISYs in account'
10980        }
10981    ]
10982
10983    buildSelect(voiceOptions, voiceSelector, '350px', function(event, ui) {});
10984
10985    /*
10986        $('#linkAmazonHelp').click(function() {
10987            //If you want to use your ISY via the connected-home feature of the Amazon echo
10988            informationMessageDialog("With the ISY Smart Home skill V3, you no longer need to link here. This is now deprecated.", 'Help', 500)
10989        });
10990    */
10991	return { modal: dialog, setFields: setFields };
10992};
10993
10994
10995// Migrate ISY
10996var migrateDialogInit = function () {
10997	var dialog,
10998		form,
10999		domain,
11000		isyList,
11001		migrate_from  = $('#migrate_from'),
11002		isy = $('#migrate_to'),
11003		migrate_alexa = $('#migrate_alexa'),
11004		migrate_gh = $('#migrate_gh'),
11005		migrate_ifttt = $('#migrate_ifttt'),
11006		migrate_lic = $('#migrate_lic'),
11007		migrate_NS = $('#migrate_NS'),
11008		update_preferred = $('#update_preferred'),
11009		allFields = $([]).add(isy),
11010		tips = $('#migrate_dialog .validateTips');
11011
11012	function resetForm() {
11013		tips.text('Please select which ISY you want to migrate to');
11014		allFields.removeClass('fieldfocus');
11015		migrate_alexa.prop('checked', true); // We migrate the license by default
11016		migrate_gh.prop('checked', true); // We migrate the license by default
11017		migrate_ifttt.prop('checked', true); // We migrate the license by default
11018		migrate_lic.prop('checked', true); // We migrate the license by default
11019		migrate_NS.prop('checked', true); // We migrate the nodeservers by default
11020		update_preferred.prop('checked', true); // We migrate the license by default
11021		form[0].reset();
11022	}
11023
11024	function setFields(obj) {
11025		resetForm();
11026		domain = obj.domain
11027		migrate_from.val(obj.uuid)
11028		isyList = obj.uuidList
11029		isy.combobox("option", "source", obj.uuidList);
11030		isy.combobox("option", "data", "");
11031	}
11032
11033	function isUuidDuplicate(uuid) {
11034		// This range of uuid was issued twice. It got corrected by an update.
11035		// We need to prevent migration to these uuid if they are below 6.0.4
11036		var DUP_RANGE_START_HEX=0x0021b90271fc
11037		var DUP_RANGE_END_HEX=0x0021b90272f5
11038		var uuidHex = parseInt(uuid.replace(/:/g,''), 16)
11039		return uuidHex >= DUP_RANGE_START_HEX && uuidHex <= DUP_RANGE_END_HEX
11040	}
11041
11042	function isFirmwareOld(firmware) {
11043		var is604AndUp = (firmware && versionCompare(firmware, '6.0.4') >= 0);
11044		return !is604AndUp
11045	}
11046
11047
11048
11049	function submit() {
11050		var valid = true;
11051
11052		allFields.removeClass('fieldfocus');
11053
11054		isy.combobox('refresh'); // Makes sure the real input field is up to date
11055
11056		var migrateToItem = isyList
11057			.find(function (item) { return item.data === isy.val() })
11058		var migrateToFirmware = migrateToItem && migrateToItem.firmware
11059
11060		if (!isy.val().length || !isy.combobox("validate")) {
11061			updateTips(tips, 'Please select an ISY to migrate to.')
11062		} else if (isUuidDuplicate(isy.val()) && isFirmwareOld(migrateToFirmware)) {
11063			errorMessageDialog('Before migrating, the unit you are migrating to needs to be upgraded. Please use "Upgrade packages" in the Admin Console, eisy-ui or portal.', 'Error', 750)
11064		} else if (!(migrate_alexa.prop('checked') || migrate_gh.prop('checked') || migrate_ifttt.prop('checked') || migrate_lic.prop('checked') || migrate_NS.prop('checked') || update_preferred.prop('checked'))) {
11065			updateTips(tips, 'Please select one or more migration options.')
11066		} else {
11067			var data = serializeFormWithCheckboxes(form.find('input'));
11068			data += '&migrate_from=' + $(migrate_from).val();
11069			data += '&domain=' + domain;
11070
11071			submitProcess(data, false)
11072		}
11073	}
11074
11075	function migrationResultString(result) {
11076		var migrated = result.total - result.dup - result.failed;
11077		var failed = result.dup + result.failed;
11078		var resultStr = '' + migrated;
11079
11080		// Applicable to IFTTT only (null===no key to migrate, false=key not migrated, true=migrated successfully)
11081		var keyNotMigratedStr = result.keyNotMigrated ? 'Key not migrated':''
11082
11083		if (failed) {
11084			// If failures are only duplicates
11085			if (result.dup && !result.failed) {
11086				resultStr += ' migrated (' + result.dup + ' duplicate' + (keyNotMigratedStr?' - '+keyNotMigratedStr:'') + ')';
11087			} else {
11088				resultStr += ' migrated (' + (result.dup + result.failed) + ' failed' + (keyNotMigratedStr?' - '+keyNotMigratedStr:'') + ')';
11089			}
11090		} else {
11091			resultStr += ' migrated' + (keyNotMigratedStr?' ('+keyNotMigratedStr+')':'');
11092		}
11093
11094		return resultStr
11095	}
11096
11097	function submitProcess(data, isConfirmed) {
11098		ajaxJsonPost('/api/isy/migrate', data + (isConfirmed ? '&merge_confirmation=true' : ''))
11099		.then(function(data, status, jqXHR) {
11100			var msg = '<table>' +
11101				(migrate_alexa.prop('checked') ? '<tr><td>Amazon Echo spokens:</td><td> ' + migrationResultString(data.spokenAlexa) + '</td></tr>' : '') +
11102				(migrate_gh.prop('checked') ? '<tr><td>Google Home spokens:</td><td> ' + migrationResultString(data.spokenGH) + '</td></tr>' : '') +
11103				(migrate_ifttt.prop('checked') ? '<tr><td>IFTTT / Webhooks:</td><td> ' + migrationResultString(data.iftttIn) + '</td></tr>' : '') +
11104				(migrate_NS.prop('checked') ? '<tr><td>Nodeserver Licences:</td><td> ' + migrationResultString(data.ns) + '</td></tr>' : '') +
11105				(migrate_lic.prop('checked') ? '<tr><td>Access Licences:</td><td>
11105 ' + (data.license.migrated?
11106							('Migrated successfully' + (data.license.trialActivated ? ' </td></tr><tr><td/><td>(Free trial was also automatically applied)</td></tr>':'')):'Not migrated') + '</td></tr>' :
11107						''
11108				) +
11109				(update_preferred.prop('checked') ? '<tr><td>Preferred ISY:</td><td> ' + (data.preferredISY.updated?'Updated':'Not updated') + '</td></tr>' : '') +
11110				'</table>'
11111
11112			// msg += '<br/>Please don\'t forget to migrate your Node Servers.'
11113
11114			informationMessageDialog(msg, 'Success', 750)
11115			.then(function() {
11116				dialog.dialog('close');
11117			})
11118			.done()
11119		})
11120		.fail(function(jqXHR, textStatus, errorThrown) {
11121			if (jqXHR.status===425) { // Passthrough message
11122				errorMessageDialog(jqXHR.responseText)
11123			}
11124			// Warning destination has records
11125			else if (jqXHR.status===424) {
11126				var response = JSON.parse(jqXHR.responseText)
11127				var fromExpired = response.fromExpired
11128				var licType = response.destinationLicenseType
11129				var licExpiry = response.destinationLicenseExpiry && new Date(response.destinationLicenseExpiry)
11130						// null: "no license",
11131						// 	0: 'a trial',
11132						// 	1: 'a license',
11133						// 	2: 'renewals'
11134
11135				// Start a chain of promises
11136				$.Deferred().resolve().promise()
11137				.then(function () {
11138					// We display only one of the 2 warnings below
11139					if (fromExpired) {
11140						return errorMessageDialog('WARNING: The ISY you are migrating from has no ISY Portal license to migrate.</br></br>Continue with the other items?', 'Error', 600)
11141					}
11142
11143					// If destination ISY has a non-expired license, we are essentially merging licences.
11144					if ((licType === 1 || licType === 2) && licExpiry && licExpiry.getTime() > Date.now()) {
11145						return errorMessageDialog('WARNING: The ISY Portal license will be merged with your existing portal license permanently. You will not be able to migrate it back as is.</br></br>Continue and merge licenses?', 'Error', 600)
11146					}
11147				})
11148				.then(function () {
11149					if (response.merge.length) {
11150						var merges = response.merge
11151						.map(function (item) {
11152							return item.count + ' ' + item.name
11153						})
11154
11155						var last = merges.pop();
11156						var mergeMessage = 'Destination ISY already has ' +
11157							merges.join(', ') + (merges.length ?' and ' : ' ') + last +
11158							'.'
11159
11160						return confirmationDialog('WARNING: ' + mergeMessage + '</br></br>Continue and merge?', 'Warning', 600)
11161					}
11162				})
11163				.then(function () {
11164					submitProcess(data, true)
11165				})
11166				.done();
11167			}
11168			else {
11169				errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
11170			}
11171		})
11172		.done();
11173	}
11174
11175	var isyOptions = {
11176		autoOpen: false,
11177		width: 450,
11178		height: 500,
11179		modal: true,
11180		buttons: {
11181			'Migrate': function() {
11182				submit();
11183			},
11184			Cancel: function() {
11185				$(this).dialog('close');
11186			}
11187		},
11188		open: function(event, ui) {
11189			dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
11190			$('.custom-combobox-input').focus();
11191		}
11192	};
11193
11194	// Initializes the dialog
11195	dialog = $('#migrate_dialog').dialog(isyOptions);
11196
11197	isy.combobox({inputClass: ''});
11198
11199	// Handles keyboad enter (submit event)
11200	form = dialog.find('form').submit(function(event) {
11201		event.preventDefault();
11202		submit();
11203	});
11204
11205	$('#migrateHelp').click(function() {
11206		informationMessageDialog('ISY Migration allows to move ISY Portal configurations & access licence to a new ISY.<br/><br/>' +
11207			'Please keep in mind that a license move is permanent. It can\'t be migrated back to the original ISY.<br/><br/>' +
11208			'For more informations, please refer to the <a href="https://wiki.universal-devices.com/index.php?title=ISY_Portal_Migrate_configurations_to_a_new_ISY" target="_blank">Wiki instructions</a>', 'Migration Help', 600)
11209	});
11210
11211	return { modal: dialog, setFields: setFields };
11212};
11213
11214
11215
11216// eisy-ui settings
11217var eisyuiSettingsDialogInit = function () {
11218	var dialog,
11219		form,
11220		uuid,
11221		domain,
11222		remoteLogging  = $('#remoteLogging')
11223
11224	function setFields(obj) {
11225		domain = obj.domain
11226		uuid = obj.uuid
11227		remoteLogging.prop('checked', obj.settings.remoteLogging);
11228	}
11229
11230	function submit() {
11231		var valid = true;
11232
11233		var data = {
11234			uuid: uuid,
11235			domain: domain
11236		}
11237
11238		form.find('input').filter('[type="checkbox"]').each(function() {
11239			data[this.name] = this.checked;
11240		})
11241
11242  	ajaxJsonPost('/api/eisy/settings', data)
11243		.then(function(data, status, jqXHR) {
11244			informationMessageDialog('Settings saved', 'Success')
11245			.then(function() {
11246				dialog.dialog('close');
11247			})
11248			.done()
11249		})
11250		.fail(function(jqXHR, textStatus, errorThrown) {
11251			errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
11252		})
11253		.done();
11254	}
11255
11256	var isyOptions = {
11257		autoOpen: false,
11258		width: 450,
11259		height: 200,
11260		modal: true,
11261		buttons: {
11262			'Save': function() {
11263				submit();
11264			},
11265			Cancel: function() {
11266				$(this).dialog('close');
11267			}
11268		},
11269		open: function(event, ui) {
11270			dialog.dialog( 'option', 'position', { my: 'center', at: 'center', of: window, collision: 'fit' });
11271		}
11272	};
11273
11274	// Initializes the dialog
11275	dialog = $('#eisyui_settings_dialog').dialog(isyOptions);
11276
11277	// Handles keyboad enter (submit event)
11278	form = dialog.find('form').submit(function(event) {
11279		event.preventDefault();
11280		submit();
11281	});
11282
11283	return { modal: dialog, setFields: setFields };
11284};
11285
11286
11287
11288// AMAZON ECHO Main Spoken Devices & Scenes mapping
11289var echoSpokenNodesDialogInit = function () {
11290	var dialog,
11291		uuid,
11292		domain,
11293		roomsPromise,
11294		lookupDataPromise,
11295		lookupDataPromisePgm,
11296		lookupDataPromiseStateVar,
11297		lookupDataPromiseNetworkRsc,
11298		lookupDataPromiseElk;
11299
11300	// var echoSubscriptionCheckbox = $('#echo_spoken_nodes_subscription');
11301
11302	function loadRooms(uuid) {
11303		roomsPromise = ajaxJsonGet('/api/voice/amazon/spoken/rooms', $.param({ uuid: uuid, domain: domain }));
11304
11305		roomsPromise
11306		.fail(function(jqXHR, textStatus, errorThrown) {
11307			if (jqXHR.status==503) {
11308				errorMessageDialog('ISY is not online')
11309			}
11310			else if (jqXHR.status==504) {
11311				errorMessageDialog('Timeout occured. Please try again.')
11312			}
11313			else if (jqXHR.status==550) {  // Custom error message
11314				var data = JSON.parse(jqXHR.responseText);
11315
11316				if (data.message) {
11317					errorMessageDialog(data.message)
11318				}
11319				else {
11320					errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
11321				}
11322			}
11323			else {
11324				errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
11325			}
11326		})
11327		.done();
11328
11329		return roomsPromise;
11330	}
11331
11332	function setFields(obj) {
11333		$('#echo_spoken_nodes_uuid').text(obj.uuid);
11334		$('#echo_spoken_nodes_alias').text(obj.alias);
11335
11336		// echoSubscriptionCheckbox.prop('checked', obj.subscriptionEnabled);
11337		// echoSubscriptionCheckbox.button('refresh');
11338		// setSubscriptionButton();
11339
11340		uuid = obj.uuid;
11341		domain = obj.domain;
11342
11343		lookupDataPromise = ajaxJsonGet('/api/nodes/lookup', $.param({ uuid: uuid, domain: domain }))
11344		.done(function(content, status, jqXHR){
11345			content.data.devices.map(function (item) {
11346			var path = item.path.map(function(p) { return p.name })
11347				.concat([ item.name ])
11348				.join(' / ');
11349
11350				item.data = item.address;
11351				item.value = item.label = path + ' (' + item.address.replace(/ /g, '.') + ')';
11352				item.option = item; // Self reference;
11353			});
11354
11355			content.data.devices.sort(function(a, b) {
11356				return a.value.localeCompare(b.value) }
11357			);
11358
11359			content.data.scenes.map(function (item) {
11360				item.data = item.address;
11361				item.value = item.label = item.name + ' (' + item.address.replace(/ /g, '.') + ')';
11362				item.option = item; // Self reference;
11363			});
11364
11365			content.data.scenes.sort(function(a, b) {
11366				return a.name.localeCompare(b.name) }
11367			);
11368		})
11369		.fail(function (jqXHR, textStatus, errorThrown) {
11370			jqXHR.requested = 'Devices'; // Track which request it is in the jqXHR object
11371		});
11372
11373		lookupDataPromisePgm = ajaxJsonGet('/api/programs/lookup', $.param({uuid: uuid, domain: domain}))
11374		.done(function(content, status, jqXHR){
11375			content.data.map(function (item) {
11376				item.data = item.address;
11377				item.value = item.label = item.name + ' (' + item.address.replace(/ /g, '.') + ')';
11378				item.option = item; // Self reference;
11379			});
11380
11381			content.data.sort(function(a, b) {
11382				return a.name.localeCompare(b.name) }
11383			);
11384		})
11385		.fail(function (jqXHR, textStatus, errorThrown) {
11386			jqXHR.requested = 'Programs'; // Track which request it is in the jqXHR object
11387		});
11388
11389		lookupDataPromiseStateVar = ajaxJsonGet('/api/statevars/lookup', $.param({uuid: uuid, domain: domain}))
11390		.done(function(content, status, jqXHR){
11391			content.data.map(function (item) {
11392				item.data = item.address;
11393				item.value = item.label = item.name + ' (' + item.address.replace(/ /g, '.') + ')';
11394				item.option = item; // Self reference;
11395			});
11396
11397			content.data.sort(function(a, b) {
11398				return a.name.localeCompare(b.name) }
11399			);
11400		})
11401		.fail(function (jqXHR, textStatus, errorThrown) {
11402			jqXHR.requested = 'State variables'; // Track which request it is in the jqXHR object
11403			jqXHR.skipErrorMessage = true;
11404		});
11405
11406		lookupDataPromiseNetworkRsc = ajaxJsonGet('/api/networkrsc/lookup', $.param({uuid: uuid, domain: domain}))
11407		.done(function(content, status, jqXHR){
11408			content.data.map(function (item) {
11409				item.data = item.address;
11410				item.value = item.label = item.name + ' (' + item.address.replace(/ /g, '.') + ')';
11411				item.option = item; // Self reference;
11412			});
11413
11414			content.data.sort(function(a, b) {
11415				return a.name.localeCompare(b.name) }
11416			);
11417		})
11418		.fail(function (jqXHR, textStatus, errorThrown) {
11419			jqXHR.requested = 'Network resources'; // Track which request it is in the jqXHR object
11420			jqXHR.skipErrorMessage = true;
11421		});
11422
11423		lookupDataPromiseElk = ajaxJsonGet('/api/elk/lookup', $.param({uuid: uuid, domain: domain}))
11424		.done(function(content, status, jqXHR){
11425			content.data.zones.map(function (item) {
11426				item.data = item.id;
11427				item.value = item.label = 'Zone ' + item.id + ' ' + item.name;
11428				item.option = item; // Self reference;
11429			})
11430
11431// No need to sort, it's always by the zone number, which is good.
11432//			content.data.sort(function(a, b) {
11433//				return a.name.localeCompare(b.name) }
11434//			);
11435		})
11436		.fail(function (jqXHR, textStatus, errorThrown) {
11437			jqXHR.requested = 'ELK topology'; // Track which request it is in the jqXHR object
11438			jqXHR.skipErrorMessage = true;
11439		});
11440
11441		$.when(lookupDataPromise, lookupDataPromisePgm, lookupDataPromiseStateVar, lookupDataPromiseNetworkRsc, lookupDataPromiseElk)
11442		.done(function() {
11443			// if (!obj.skillEnabled) {
11444			// 	errorMessageDialog('The Alexa smart home skill appears to be inactive. To enable it, go to the Alexa app and enable the skill named "ISY Optimized for Smart Home V3".', 'Alexa skill is not enabled', 650)
11445			// }
11446		})
11447		.fail(function(jqXHR, textStatus, errorThrown) {
11448			if (jqXHR.status==503) {
11449				errorMessageDialog('ISY is not online')
11450			}
11451			else if (jqXHR.status==404) {
11452				// No need to warn the user about state variables not loading. It may be normal.
11453				if (!jqXHR.skipErrorMessage) {
11454					errorMessageDialog(jqXHR.requested + ' are not available')
11455				}
11456			}
11457			else {
11458				errorMessageDialog(jqXHR.requested + ' are not available<br>' + 'HTTP error ' + jqXHR.status, 'Error', 400)
11459			}
11460		});
11461
11462		loadRooms(uuid);
11463		tableEchoSpokenNodes.setRooms(roomsPromise);
11464		tableEchoSpokenNodes.setLookupDataPromise(lookupDataPromise, lookupDataPromisePgm, lookupDataPromiseStateVar, lookupDataPromiseNetworkRsc, lookupDataPromise
11464Elk); // Pass a reference to the promises, so that we can check it when we try to edit a spoken.
11465
11466		// setRooms && setLookupDataPromise must have been done first
11467		tableEchoSpokenNodes.setUuid(uuid, domain, true);
11468	}
11469
11470	// function setSubscriptionButton() {
11471	// 	if (echoSubscriptionCheckbox.is(":checked")) {
11472	// 		echoSubscriptionCheckbox.button('option', 'icons', { primary: "ui-icon-check" } );
11473	// 	}
11474	// 	else {
11475	// 		echoSubscriptionCheckbox.button('option', 'icons', { primary: "ui-icon-closethick" } );
11476	// 	}
11477	// }
11478
11479	var isyOptions = {
11480		autoOpen: false,
11481		width: 900,
11482		modal: true,
11483		buttons: {
11484			'Close': function() {
11485				$(this).dialog('close');
11486			}
11487		},
11488		open: function(event, ui) {
11489			dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
11490		}
11491	};
11492
11493	// Initializes the dialog
11494	dialog = $('#echo_spoken_nodes_dialog').dialog(isyOptions);
11495
11496	$("#echo_spoken_nodes_add").buttonset({items: 'button'});
11497
11498	$('#echo_spoken_nodes_adddevice').click(function () {
11499		$(this).blur(); // Remove focus from button
11500
11501		$.when(lookupDataPromise, roomsPromise)
11502		.then(function(contentAjax, roomsAjax) {
11503			var content=contentAjax[0];
11504			var rooms=roomsAjax[0];
11505
11506			if (!content) {
11507				throw new Error('Devices not available');
11508			}
11509
11510			echoSpokenNodesEditDialog.setFields({ category: 'std', uuid: uuid, domain: domain, lookup: content.data.devices, address: '', editMode:false, rooms: rooms.data });
11511			echoSpokenNodesEditDialog.modal.dialog('open');
11512		})
11513		.fail(function(jqXHR, textStatus, errorThrown) {
11514			errorMessageDialog("Devices are not available", 'Error', 350)
11515		})
11516		.done();
11517	});
11518
11519	$('#echo_spoken_nodes_addscene').click(function () {
11520		$(this).blur(); // Remove focus from button
11521
11522		$.when(lookupDataPromise, roomsPromise)
11523		.then(function(contentAjax, roomsAjax) {
11524			var content=contentAjax[0];
11525			var rooms=roomsAjax[0];
11526
11527			if (!content) {
11528				throw new Error('Devices not available');
11529			}
11530			echoSpokenNodesEditDialog.setFields({ category: 'scene', uuid: uuid, domain: domain, lookup: content.data.scenes, address: '', editMode:false, rooms: rooms.data });
11531			echoSpokenNodesEditDialog.modal.dialog('open');
11532		})
11533		.fail(function(jqXHR, textStatus, errorThrown) {
11534			errorMessageDialog("Devices are not available", 'Error', 350)
11535		})
11536		.done();
11537	});
11538
11539	$('#echo_spoken_nodes_addprogram').click(function () {
11540		$(this).blur(); // Remove focus from button
11541
11542		$.when(lookupDataPromisePgm, roomsPromise)
11543		.then(function(contentAjax, roomsAjax) {
11544			var content=contentAjax[0];
11545			var rooms=roomsAjax[0];
11546
11547			if (!content) {
11548				throw new Error('Programs not available');
11549			}
11550			echoSpokenNodesEditDialog.setFields({ category: 'program', uuid: uuid, domain: domain, lookup: content.data, address: '', editMode:false, rooms: rooms.data });
11551			echoSpokenNodesEditDialog.modal.dialog('open');
11552		})
11553		.fail(function(jqXHR, textStatus, errorThrown) {
11554			errorMessageDialog("Programs are not available", 'Error', 350)
11555		})
11556		.done();
11557	});
11558
11559	$('#echo_spoken_nodes_addstatevar').click(function () {
11560		$(this).blur(); // Remove focus from button
11561
11562		$.when(lookupDataPromiseStateVar, roomsPromise)
11563		.then(function(contentAjax, roomsAjax) {
11564			var content=contentAjax[0];
11565			var rooms=roomsAjax[0];
11566
11567			if (!content) {
11568				throw new Error('State Variables not available');
11569			}
11570
11571			echoSpokenNodesEditDialog.setFields({ category: 'statevar', uuid: uuid, domain: domain, lookup: content.data, address: '', editMode:false, rooms: rooms.data });
11572			echoSpokenNodesEditDialog.modal.dialog('open');
11573		})
11574		.fail(function(jqXHR, textStatus, errorThrown) {
11575			errorMessageDialog("State variables are not available", 'Error', 350)
11576		})
11577		.done();
11578	});
11579
11580	$('#echo_spoken_nodes_addelk').click(function () {
11581		$(this).blur(); // Remove focus from button
11582
11583		$.when(lookupDataPromiseElk, roomsPromise)
11584		.then(function(contentAjax, roomsAjax) {
11585			var content=contentAjax[0];
11586			var rooms=roomsAjax[0];
11587
11588//			if (!content.zones) {
11589//				throw new Error('ELK Zones not available');
11590//			}
11591
11592			echoSpokenNodesEditDialog.setFields({ category: 'elk', uuid: uuid, domain: domain, lookup: content.data.zones, address: '', editMode:false, rooms: rooms.data });
11593			echoSpokenNodesEditDialog.modal.dialog('open');
11594		})
11595		.fail(function(jqXHR, textStatus, errorThrown) {
11596			errorMessageDialog("ELK zones are not available", 'Error', 350)
11597		})
11598		.done();
11599	});
11600
11601	$('#echo_spoken_nodes_add_av').click(function () {
11602		$(this).blur(); // Remove focus from button
11603
11604		$.when(lookupDataPromiseStateVar, lookupDataPromisePgm, lookupDataPromiseNetworkRsc, roomsPromise) // lookupDataPromise,
11605		.then(function(vars, pgms, net, rooms) {
11606//			var content=contentNodes[0];
11607//			var rooms=roomsAjax[0];
11608
11609			echoSpokenNodesAVEditDialog.setFields({ category: 'av', uuid: uuid, domain: domain, lookupVars: vars[0], lookupPgms: pgms[0], lookupNet: net[0], rooms: rooms[0].data, editMode:false }); // lookupNodes: nodes[0],
11610			echoSpokenNodesAVEditDialog.modal.dialog('open');
11611		})
11612		.fail(function(jqXHR, textStatus, errorThrown) {
11613
11614			lookupDataPromiseStateVar
11615			.fail(function() {
11616				errorMessageDialog("State variables are not available", 'Error', 350)
11617			})
11618			.done();
11619
11620			lookupDataPromisePgm
11621			.fail(function() {
11622				errorMessageDialog("Programs are not available", 'Error', 350)
11623			})
11624			.done();
11625
11626			lookupDataPromiseNetworkRsc
11627			.fail(function() {
11628				errorMessageDialog("Network resources are not available", 'Error', 350)
11629			})
11630			.done();
11631		})
11632		.done();
11633	});
11634
11635	$('#echo_spoken_nodes_deleteall').button({ icons: {primary: 'ui-icon-custom-delete'}, text: false}).click(function () {
11636		$(this).blur(); // Remove focus from button
11637
11638		confirmationDialog('All of your Echo spokens for this ISY will be deleted. Are you sure?', 'Delete spokens?')
11639		.then(function () {
11640			ajaxJsonDelete('/api/voice/amazon/spoken/nodes/deleteall', $.param({uuid: uuid, domain: domain }))
11641			.done(function () {
11642				tableEchoSpokenNodes.reload();
11643			})
11644			.fail(function(jqXHR, textStatus, errorThrown) {
11645				errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350);
11646			})
11647		})
11648		.done();
11649	});
11650
11651	$('#echo_spoken_nodes_download').button({ icons: {primary: 'ui-icon-custom-download'}, text: false}).click(function () {
11652		$(this).blur(); // Remove focus from button
11653
11654		informationMessageDialog('Press Ok to download to a CSV file.', 'Export spokens')
11655		.then(function () {
11656			try {
11657				var isFileSaverSupported = !!new Blob;
11658			} catch (e) {}
11659
11660			if (isFileSaverSupported) {
11661				var str = 'Device name,Address,Category,Room,Spoken,Spoken-2,Spoken-3,Spoken-4,Spoken-5\r\n';
11662
11663				tableEchoSpokenNodes.data.data.forEach(function(spoken) {
11664					str += [spoken.name, spoken.address, spoken.category==='std'?'device':spoken.category, spoken.roomName].concat(spoken.spoken).join(',') + '\r\n'
11665				});
11666
11667				var BOM = '\uFEFF';
11668				var blob = new Blob([BOM + str], {type: "application/csv;charset=utf-8"});
11669				saveAs(blob, "Spokens_" + uuidNC(uuid) + ".csv");
11670			}
11671			else {
11672				errorMessageDialog('Feature not supported on this browser', 'Not supported');
11673			}
11674		})
11675		.done();
11676	});
11677
11678	$('#echo_spoken_nodes_refresh').button({ icons: {primary: 'ui-icon-custom-refresh'}, text: false}).click(function(event) {
11679		$(this).blur(); // Remove focus from button
11680		tableEchoSpokenNodes.reload();
11681	});
11682
11683	// echoSubscriptionCheckbox.button({ label: "Live update" }).click(function(event) {
11684	// 	setSubscriptionButton();
11685	// 	ajaxJsonPost('/api/voice/amazon/spoken/subscription/state', $.param({
11686	// 		uuid: uuid,
11687	// 		domain: domain,
11688	// 		state: echoSubscriptionCheckbox.is(":checked")
11689	// 	}))
11690	// 	.fail(function(jqXHR, textStatus, errorThrown) {
11691	// 		if (jqXHR.status==404) {
11692	// 			echoSubscriptionCheckbox.prop('checked', false);
11693	// 			echoSubscriptionCheckbox.button('refresh');
11694	// 			setSubscriptionButton();
11695	// 			errorMessageDialog('The Alexa smart home skill appears to be inactive. To enable it, go to the Alexa app and enable the skill named "ISY Optimized for Smart Home V3".<br/><br/>Once the skill is enabled and devices has been discovered by Alexa, you can enable Live update.', 'Live update error', 650)
11696	// 		}
11697	// 		else {
11698	// 			errorMessageDialog('API error; HTTP error ' + jqXHR.status)
11699	// 		}
11700	// 	})
11701	// 	.done();
11702	// 	$(this).blur(); // Remove focus from button
11703	// });
11704
11705	// $('#echo_spoken_nodes_subscription_help').click(function() {
11706	// 	informationMessageDialog('When live update is enabled, your devices status will be sent in real-time to your echo app. This is required to get motion and contact sensors to trigger Alexa routines.', 'Live update', 650)
11707	// });
11708
11709	return { modal: dialog, setFields: setFields, loadRooms: loadRooms };
11710};
11711
11712// AMAZON ECHO Spoken Devices & Scenes mapping
11713var echoSpokenNodesEditDialogInit = function () {
11714	var dialog,
11715		form,
11716		pk,
11717		uuid,
11718		domain,
11719		currentRooms,
11720		category,
11721		stdLookup,
11722		editMode,
11723		userCat, // Follows the user cat selector
11724		device = $('#echo_spoken_address'),
11725		room = $('#echo_spoken_room'),
11726		turnon_value = $('#echo_spoken_turnon_value'),
11727		turnoff_value = $('#echo_spoken_turnoff_value'),
11728		motion_value = $('#echo_spoken_motion_value'),
11729		colorManufacturer = $('#echo_spoken_color_input_selector'),
11730		spokens = [],
11731		allFields,
11732		tips = $('#echo_spoken_nodes_edit_dialog .validateTips'),
11733		isColor,
11734		colorMfr, // Manufacturer - selected option
11735		colorMfrOptions = [
11736			{ value: '$select', description: 'Select one...' },
11737			{ value: 'AEOTEC', description: 'Aeotec' },
11738			{ value: 'FIBARO', description: 'Fibaro' },
11739			{ value: 'ZWGEN', description: 'Z-Wave Generic' }
11740		];
11741
11742	spokens[0] = $('#echo_spoken_spoken')
11743	spokens[1] = $('#echo_spoken_spoken2')
11744	spokens[2] = $('#echo_spoken_spoken3')
11745	spokens[3] = $('#echo_spoken_spoken4')
11746	spokens[4] = $('#echo_spoken_spoken5')
11747
11748	allFields = $([]).add(device).add(room).add(turnon_value).add(turnoff_value).add(motion_value).add(colorManufacturer).add(spokens[0]).add(spokens[1]).add(spokens[2]).add(spokens[3]).add(spokens[4]).add(spokens[5]);
11749
11750	var categoryLabel = {
11751		'std' : 'device',
11752		'scene' : 'scene',
11753		'program' : 'program',
11754		'statevar' : 'state variable',
11755		'elk' : 'ELK zone'
11756	};
11757
11758	// Updates the room selector
11759	function updateRoomSelector(rooms, choice) {
11760		// label: shown in the selections
11761		// value: displayed in the combobox input field (this.input)
11762		// data: real data assigned to the input field (this.element)
11763
11764		rooms = rooms.map(function(item) {
11765			return {label: item.name, value: item.name, data: item._id}
11766		});
11767
11768		rooms.sort(function(a, b) {
11769			return a.label.localeCompare(b.label)
11770		});
11771
11772		rooms.unshift({ label: '(No room)', value: '', data: '$noroom'});
11773
11774		room.combobox("option", "source", rooms);
11775		room.combobox("option", "data", choice);
11776
11777		currentRooms = rooms;
11778	}
11779
11780	// Set isColor based on the address and Show/Hide the colorMfr selectmenu
11781	function setIsColor(address) {
11782		if (address) {
11783			var addressSplit = address.split('_');
11784			isColor = (addressSplit.length===2 && addressSplit[1].slice(0,3)==='186')
11785		}
11786		else {
11787			isColor=false;
11788		}
11789
11790		if (isColor) {
11791			$('#echo_spoken_color_input').show();
11792		}
11793		else {
11794			$('#echo_spoken_color_input').hide();
11795		}
11796	}
11797
11798	function displayValueFields(userCat) {
11799		if (userCat==='motion' || userCat==='contact') {
11800			$('#echo_spoken_statevar_input_onoff').hide();
11801			$('#echo_spoken_statevar_input_motion').show();
11802			$('#echo_spoken_motion_label').text(userCat==='motion'?'Motion detected value':'Contact closed value')
11803		}
11804		else {
11805			$('#echo_spoken_statevar_input_onoff').show();
11806			$('#echo_spoken_statevar_input_motion').hide();
11807		}
11808	}
11809
11810	// Initialize form with new values
11811	function setFields(obj) {
11812		var label;
11813
11814		uuid = obj.uuid;
11815		domain = obj.domain;
11816		pk = obj.pk;
11817		category = obj.category; // 4 possibilities: 'statevar', 'program', 'scene', 'std'
11818		stdLookup = obj.lookup;
11819
11820		form[0].reset();
11821		device.combobox("option", "source", obj.lookup);
11822		device.combobox("option", "data", obj.address);
11823		turnon_value.val(obj.turnon_value);
11824		turnoff_value.val(obj.turnoff_value);
11825		motion_value.val(obj.motion_value);
11826
11827		for (var i=0; i<5; i++) {
11828			if (obj.spokens && i<=obj.spokens.length) {
11829				spokens[i].val(obj.spokens[i]);
11830			}
11831			else {
11832				spokens[i].val('');
11833			}
11834		}
11835
11836		editMode = obj.editMode;
11837
11838		// Initially, the userCat selectmenu is not available for standard devices.
11839		$('#echo_spoken_user_cat_input').show();
11840
11841		// statevars, programs and scenes used to be a scene by default.
11842		// If userCat is not available, and we are editing, set the userCat default to 'scene'
11843		if (['statevar', 'program', 'scene'].includes(category) && !obj.userCat) {
11844			obj.userCat = 'scene';
11845		}
11846
11847		buildUserCatSelect(categoryOverride(category, stdLookup, obj.address, isColor), obj.userCat);
11848
11849		if (editMode) {
11850			tips.text('Please choose a spoken for this ' + categoryLabel[category] + '.');
11851			spokens[0].focus();
11852
11853			setIsColor(obj.address);
11854			if (isColor) {
11855				colorMfr=obj.colorMfr?obj.colorMfr:'$select';
11856			}
11857		}
11858		else {
11859			tips.text('Please choose a ' + categoryLabel[category] +' and enter a corresponding spoken.');
11860			setIsColor(false);
11861			colorMfr='$select';
11862		}
11863
11864		// Takes the one loaded, or the one by default if new
11865		// userCat = $('#echo_spoken_user_cat_selector select').val(); // Take the one from the initialize
11866
11867		if (category==='statevar') {
11868			displayValueFields(userCat); // If user cat is not set, we will display the turn on/off fields, not motion
11869			$('#echo_spoken_statevar_input').show();
11870		}
11871		else {
11872			$('#echo_spoken_statevar_input').hide();
11873		}
11874
11875		$('#echo_spoken_color_input_selector select').val(colorMfr);
11876		$('#echo_spoken_color_input_selector select').selectmenu('refresh');
11877
11878		// Disable field if editing
11879		device.combobox("disabled", editMode);
11880
11881		$('#echo_spoken_lookup_label').text('My ' + categoryLabel[category]);
11882		allFields.removeClass('fieldfocus');
11883
11884		if (!editMode) {
11885			obj.room = '$noroom';
11886		}
11887
11888		updateRoomSelector(obj.rooms, obj.room);
11889	}
11890
11891	// Restricts the userCat select to valid categories. (If the device is a thermostat, lock or color bulb)
11892	function categoryOverride(category, stdLookup, stdAddress, isColor) {
11893		if (category==='std') {
11894
11895			// See if the individual device is a lock or thermostat, and use that device category instead ('lock' or 'thermostat')
11896			var option = stdLookup.find(function(item) {
11897				return item.address === stdAddress;
11898			});
11899
11900			if (option) {
11901				category = option.category;
11902			}
11903
11904			// Check if current device is a color bulb
11905			if (isColor) {
11906				category = 'color'
11907			}
11908		}
11909
11910		return category;
11911	}
11912
11913	function buildUserCatSelect(category, setUserCatTo) {
11914		// The default option is the first one.
11915		var userCats = {
11916			'std': [ { option:'light', desc: 'Light' }, { option:'switch', desc: 'Switch' }, { option:'outlet', desc: 'Outlet' }, { option:'fan', desc: 'Fan' }, { option:'openClose', desc: 'Device with Open/Close syntax' }, { option:'lowerRaise', desc: 'Device with Set/Lower/Raise syntax' }, { option:'motion', desc: 'Motion Sensor' }, { option:'contact', desc: 'Contact Sensor' } ],
11917			'color': [ { option:'light', desc: 'Light' } ],
11918			'lock': [ { option:'lock', desc: 'Lock' } ],
11919			'thermostat': [ { option:'thermostat', desc: 'Thermostat' } ],
11920			'scene': [ { option:'scene', desc: 'Scene' }, { option:'light', desc: 'Light' }, { option:'switch', desc: 'Switch' }, { option:'outlet', desc: 'Outlet' }, { option:'openClose', desc: 'Device with Open/Close syntax' } ],
11921			'program': [ { option:'scene', desc: 'Scene' }, { option:'light', desc: 'Light' }, { option:'switch', desc: 'Switch' }, { option:'outlet', desc: 'Outlet' }, { option:'openClose', desc: 'Device with Open/Close syntax' } ],
11922			'statevar': [ { option:'scene', desc: 'Scene' }, { option:'light', desc: 'Light' }, { option:'switch', desc: 'Switch' }, { option:'outlet', desc: 'Outlet' }, { option:'openClose', desc: 'Device with Open/Close syntax' }, { option:'lowerRaise', desc: 'Device with Lower/Raise syntax' }, { option:'motion', desc: 'Motion Sensor' }, { option:'contact', desc: 'Contact Sensor' } ],
11923			'elk': [ { option:'motion', desc: 'Motion Sensor' }, { option:'contact', desc: 'Contact Sensor' } ]
11924		};
11925
11926		if (userCats[category]) {
11927			var $select = $('<select></select>');
11928
11929			userCats[category].forEach(function(item) {
11930				$select.append('<option value="' + item.option + '">' + item.desc + '</option>');
11931			});
11932
11933			// is the userCat selector changing?
11934			if (!selectHasIdenticalOptions($select, $('#echo_spoken_user_cat_selector'))) {
11935				// Default option is the first
11936				$('#echo_spoken_user_cat_selector').empty().append($select.clone());
11937				$('#echo_spoken_user_cat_selector select').selectmenu({
11938					width:'22em',
11939					change: function (event, ui) {
11940						userCat = ui.item.value;
11941						displayValueFields(userCat)
11942					}
11943				});
11944
11945				if (setUserCatTo) {
11946					setUserCatSelect(setUserCatTo)
11947				}
11948			}
11949			else {
11950				if (setUserCatTo) {
11951					setUserCatSelect(setUserCatTo)
11952				} else {
11953					// Default to first option
11954					setUserCatSelect($('#echo_spoken_user_cat_selector option').eq(0).attr('value'))
11955				}
11956			}
11957		}
11958		else {
11959			$('#echo_spoken_user_cat_input').hide();
11960			$('#echo_spoken_user_cat_selector').empty(); // Destroy the selector.
11961		}
11962	}
11963
11964	function setUserCatSelect(option) {
11965		var select = '#echo_spoken_user_cat_selector select';
11966
11967		if (option && option!=='') {
11968			$(select).val(option); // Set user category select
11969			$(select).selectmenu("refresh");
11970
11971			// If option is not valid
11972			if (!$(select).val()) {
11973				// Selects the first option
11974				$(select).val($(select + ' option:first').val());
11975			}
11976
11977		}
11978		else {
11979			// Selects the first option by default
11980			$(select).val($(select + ' option:first').val());
11981		}
11982
11983		$(select).selectmenu('refresh');
11984		userCat = $(select).val();
11985	}
11986
11987	// Validation function
11988	function checkColorMfr(tips, field) {
11989		if (isColor && colorMfr==='$select') {
11990			field.focus();
11991			updateTips(tips, 'Device manufacturer is required.');
11992			return false;
11993		}
11994		else {
11995			return true;
11996		}
11997	}
11998
11999	// Validate and save!
12000	function submit() {
12001		var valid = true;
12002
12003		device.combobox("refresh"); // Makes sure the real input field is up to date
12004		if (!device.combobox("validate")) {
12005			updateTips(tips, 'Device is not allowed');
12006			valid=false;
12007		}
12008
12009		room.combobox("refresh"); // Makes sure the real input field is up to date
12010		if (!room.combobox("validate")) {
12011
12012			var name = room.combobox("getValue");
12013
12014			confirmationDialog('Room ' + name + ' does not exists.<br/><br/>Create it?', 'Create room?')
12015			.then(function () {
12016				ajaxJsonPut('/api/voice/amazon/spoken/room', $.param({domain: domain, uuid: uuid, name: name }))
12017				.done(function (record) {
12018					var roomsPromise = echoSpokenNodesDialog.loadRooms(uuid);
12019
12020					roomsPromise
12021					.then(function(data) {
12022						updateRoomSelector(data.data, record.data._id);
12023					});
12024
12025					tableEchoSpokenNodes.setRooms(roomsPromise);
12026				})
12027				.fail(function(jqXHR, textStatus, errorThrown) {
12028					errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350);
12029				})
12030			})
12031			.done();
12032
12033
12034			valid=false;
12035		}
12036
12037		allFields.removeClass('fieldfocus');
12038
12039		if (category==='statevar') {
12040			if (userCat==='motion') {
12041				valid = valid && checkNotEmpty(tips, motion_value, 'Motion detected value');
12042				valid = valid && checkNumber(tips, motion_value, 'Motion detected value', 0, 2147483647);
12043			}
12044			else if (userCat==='contact') {
12045				valid = valid && checkNotEmpty(tips, motion_value, 'Contact closed value');
12046				valid = valid && checkNumber(tips, motion_value, 'Contact closed value', 0, 2147483647);
12047			}
12048			else {
12049				valid = valid && checkNotEmpty(tips, turnoff_value, 'Turn off value');
12050				valid = valid && checkNumber(tips, turnoff_value, 'Turn off value', 0, 2147483647);
12051				valid = valid && checkNotEmpty(tips, turnon_value, 'Turn on value');
12052				valid = valid && checkNumber(tips, turnon_value, 'Turn on value', 0, 2147483647);
12053			}
12054		}
12055
12056		valid = valid && checkColorMfr(tips, colorManufacturer);
12057		valid = valid && checkLength(tips, spokens[0], 'Spoken', 2, 40);
12058		valid = valid && checkRegexp(tips, spokens[0], spokenRegex, 'Spoken must contain only letters with spaces');
12059		valid = valid && checkLength(tips, spokens[1], 'Alternate spoken 1', 0, 40);
12060		valid = valid && checkRegexp(tips, spokens[1], spokenRegex, 'Spoken must contain only letters with spaces');
12061		valid = valid && checkLength(tips, spokens[2], 'Alternate spoken 2', 0, 40);
12062		valid = valid && checkRegexp(tips, spokens[2], spokenRegex, 'Spoken must contain only letters with spaces');
12063		valid = valid && checkLength(tips, spokens[3], 'Alternate spoken 3', 0, 40);
12064		valid = valid && checkRegexp(tips, spokens[3], spokenRegex, 'Spoken must contain only letters with spaces');
12065		valid = valid && checkLength(tips, spokens[4], 'Alternate spoken 4', 0, 40);
12066		valid = valid && checkRegexp(tips, spokens[4], spokenRegex, 'Spoken must contain only letters with spaces');
12067
12068		if (valid) {
12069			var data = form.find('input').serialize();
12070			data += '&uuid=' + uuid + '&domain=' + domain + (isColor?'&colorMfr='+colorMfr:'');
12071
12072			if (editMode) {
12073				data += '&id=' + pk;
12074			}
12075
12076			data += '&category=' + category; // valid values: program, scene, std
12077
12078			if (userCat) {
12079				data += '&userCat=' + userCat;
12080			}
12081
12082			(editMode?ajaxJsonPost('/api/voice/amazon/spoken/node', data):ajaxJsonPut('/api/voice/amazon/spoken/node', data))
12083			.then(function(data, status, jqXHR) {
12084				dialog.dialog('close');
12085				tableEchoSpokenNodes.reload();
12086			})
12087			.fail(function(jqXHR, textStatus, errorThrown) {
12088				if (jqXHR.status==503) {
12089					updateTips(tips, 'ISY is not online');
12090				}
12091				else if (jqXHR.status==420) { // Dupkey error with Custom server error message
12092					var data = JSON.parse(jqXHR.responseText);
12093
12094					if (data.message) {
12095						updateTips(tips, data.message);
12096					}
12097					else {
12098						updateTips(tips, 'HTTP error ' + jqXHR.status);
12099					}
12100				}
12101				else if (jqXHR.status==400) {
12102					updateTips(tips, 'HTTP error ' + jqXHR.status + ' : ' + jqXHR.responseText);
12103				}
12104				else if (jqXHR.status===412) {
12105					if (skillNotLinkedWarningAddUpdate) {
12106						var skipButton = 'Skip Warning'
12107						var okButton = 'Ok'
12108						var buttons = [ skipButton, okButton ]
12109						// Save successful, but skill is not linked
12110						optionsDialog('The Alexa skill is not linked.<br/><br/>You can continue to add/edit your spoken mapping. When you are ready to link the skill, your devices configured here will be synchronized to Alexa.', 'Skill not linked', buttons)
12111						.then(function (button) {
12112							if (button === skipButton) {
12113								// On next edit, there will be no warning
12114								skillNotLinkedWarningAddUpdate = false
12115							}
12116						})
12117						.always(function () {
12118							// Note: If dialog was closed, promise is rejected.
12119							dialog.dialog('close');
12120							tableEchoSpokenNodes.reload();
12121						})
12122						.done();
12123					} else {
12124						dialog.dialog('close');
12125						tableEchoSpokenNodes.reload();
12126					}
12127				}
12128				else if (jqXHR.status===428) {
12129					if (incorrectUnitWarningAddUpdate) {
12130						var skipButton = 'Skip Warning'
12131						var okButton = 'Ok'
12132						var buttons = [ skipButton, okButton ]
12133						// Save successful, but skill is not linked
12134						optionsDialog('This user profile ' + registeredUser.username + ' cannot manage this ISY.<br/><br/>' +
12135							'If Alexa is linked with another user profile in this account, then you can ask Alexa "Discover my devices" to reflect the changes you made in Alexa.<br/><br/>' +
12136							'If on the other hand Alexa is linked with this user profile and you want it to control this ISY, then click on My Profile to change the preferred ISY to this ISY.<br/><br/>', 'Updating incorrect ISY', buttons, 800)
12137						.then(function (button) {
12138							if (button === skipButton) {
12139								// On next edit, there will be no warning
12140								incorrectUnitWarningAddUpdate = false
12141							}
12142						})
12143						.always(function () {
12144							// Note: If dialog was closed, promise is rejected.
12145							dialog.dialog('close');
12146							tableEchoSpokenNodes.reload();
12147						})
12148						.done();
12149					} else {
12150						dialog.dialog('close');
12151						tableEchoSpokenNodes.reload();
12152					}
12153				}
12154				else {
12155					updateTips(tips, 'HTTP error ' + jqXHR.status);
12156				}
12157			})
12158			.done();
12159		}
12160	}
12161
12162	var isyOptions = {
12163		autoOpen: false,
12164		width: 950,
12165		modal: true,
12166		buttons: {
12167			'Save': function() {
12168				submit();
12169			}
12170		},
12171		open: function(event, ui) {
12172			dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
12173		}
12174	};
12175
12176	// Initializes the dialog
12177	dialog = $('#echo_spoken_nodes_edit_dialog').dialog(isyOptions);
12178
12179	// Handles keyboad enter (submit event)
12180	form = dialog.find('form').submit(function(event) {
12181		event.preventDefault();
12182		submit();
12183	});
12184
12185	$('#echo_spoken_room_add').button({ icons: {primary: 'ui-icon-plus'}, text: false}).tooltip().click(function(event) {
12186		$(this).blur(); // Remove focus from button
12187		echoSpokenRoomEditDialog.setFields({ uuid: uuid, editMode: false, domain: domain });
12188		echoSpokenRoomEditDialog.modal.dialog('open');
12189	});
12190
12191	$('#echo_spoken_room_edit').button({ icons: {primary: 'ui-icon-pencil'}, text: false}).tooltip().click(function(event) {
12192		$(this).blur(); // Remove focus from button
12193
12194		room.combobox("refresh"); // Makes sure the real input field is up to date
12195		var pk = room.val();
12196
12197		var item = currentRooms.find(function(item) {
12198			return item.data === pk;
12199		});
12200
12201		if (pk!=='$noroom' && item) {
12202			echoSpokenRoomEditDialog.setFields({ uuid: uuid, editMode: true, domain: domain, pk: pk, name: item.label });
12203			echoSpokenRoomEditDialog.modal.dialog('open');
12204		}
12205		else {
12206			informationMessageDialog("This can't be edited.")
12207		}
12208	});
12209
12210	$('#echo_spoken_room_delete').button({ icons: {primary: 'ui-icon-trash'}, text: false}).tooltip().click(function(event) {
12211		$(this).blur(); // Remove focus from button
12212
12213		room.combobox("refresh"); // Makes sure the real input field is up to date
12214		var pk = room.val();
12215
12216		var item = currentRooms.find(function(item) {
12217			return item.data === pk;
12218		});
12219
12220		if (pk!=='$noroom' && item) {
12221			confirmationDialog('Delete room ' + item.label + '.<br/><br/>Are you sure?', 'Delete room?')
12222			.then(function () {
12223				ajaxJsonDelete('/api/voice/amazon/spoken/room', $.param({id: pk, domain: domain, uuid: uuid }))
12224				.done(function () {
12225					var roomsPromise = echoSpokenNodesDialog.loadRooms(uuid);
12226
12227					roomsPromise
12228					.then(function(data) {
12229						echoSpokenNodesEditDialog.updateRoomSelector(data.data, '$noroom');
12230						echoSpokenNodesAVEditDialog.updateRoomSelector(data.data, '$noroom');
12231					});
12232
12233					tableEchoSpokenNodes.setRooms(roomsPromise);
12234					tableEchoSpokenNodes.reload();
12235				})
12236				.fail(function(jqXHR, textStatus, errorThrown) {
12237					errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350);
12238				})
12239			})
12240			.done();
12241		}
12242		else {
12243			informationMessageDialog("This can't be removed.")
12244		}
12245	});
12246
12247	$('#echo_spokenStatevarHelp').click(function() {
12248		informationMessageDialog('When you turn on or turn off this "device", the state variable will be set to the one of these values. However, if you turn on to X percent, the variable will be set to the percent value.', 'Help', 550)
12249	});
12250
12251	$('#echo_spokenStatevarMotionHelp').click(function() {
12252		var message = userCat=='motion'?'When the state variable is set to the motion detected value, the Alexa motion detector will be triggered.':
12253		'When the state variable is set to the Contact closed value, the Alexa contact sensor will be closed. Any other value means the contact is open.';
12254
12255		informationMessageDialog(message, 'Help', 550)
12256	});
12257
12258	device.combobox({
12259		inputClass: '',
12260		focusout: function(event) {
12261			device.combobox('refresh');
12262
12263			if (category==='std') {
12264				if (deviceHasColorNode(device.val(), stdLookup)) {
12265					var colorAddress = device.val().split('_')[0]+'_186';
12266
12267					informationMessageDialog('This device supports color. However, to enable color support, you need to choose the device whose address ends with _186. Do you want to use the device ' + colorAddress + ' instead?', 'Color support?', 600)
12268					.then(function () {
12269						device.combobox("option", "data", colorAddress);
12270						device.combobox("refresh"); // Makes sure the real input field is up to date
12271						setIsColor(colorAddress);
12272					})
12273					.done();
12274				}
12275				else {
12276					setIsColor(device.val());
12277				}
12278
12279				// If the device has change, see if this is a lock or thermostat, and if so, enforce the user cat to be lock or thermostat.
12280				if (device.combobox("validate")) {
12281					buildUserCatSelect(categoryOverride(category, stdLookup, $('#echo_spoken_address').val(), isColor), userCat);
12282				}
12283			}
12284		}
12285	});
12286	room.combobox({inputClass: ''});
12287
12288	buildSelect(colorMfrOptions, '#echo_spoken_color_input_selector', '15em', function(event, ui) {
12289		colorMfr = ui.item.value;
12290	});
12291
12292	return { modal: dialog, setFields: setFields, updateRoomSelector: updateRoomSelector };
12293};
12294
12295// AMAZON ECHO Spoken room edit dialog
12296var echoSpokenRoomEditDialogInit = function () {
12297	var dialog,
12298		form,
12299		pk,
12300		uuid,
12301		domain,
12302		editMode,
12303		name = $('#echo_spoken_room_name'),
12304		allFields = $([]).add(name),
12305		tips = $('#echo_spoken_room_edit_dialog .validateTips');
12306
12307	function setFields(obj) {
12308		var label;
12309
12310		uuid = obj.uuid;
12311		domain = obj.domain;
12312		pk = obj.pk;
12313
12314		form[0].reset();
12315		name.val(obj.name);
12316
12317		editMode = obj.editMode;
12318
12319		tips.text('Please enter a room name');
12320
12321		allFields.removeClass('fieldfocus');
12322	}
12323
12324	function submit() {
12325		var valid = true;
12326
12327		allFields.removeClass('fieldfocus');
12328
12329		valid = valid && checkLength(tips, name, 'Name', 2, 40);
12330		valid = valid && checkRegexp(tips, name, roomNameRegex, 'room name is not valid.' );
12331
12332		if (valid) {
12333			var data = form.find('input').serialize();
12334			data += '&domain=' + domain + '&uuid=' + uuid;
12335
12336			if (editMode) {
12337				data += '&id=' + pk;
12338			}
12339
12340			var url = '/api/voice/amazon/spoken/room';
12341
12342			(editMode?ajaxJsonPost(url, data):ajaxJsonPut(url, data))
12343			.then(function(roomRecord, status, jqXHR) {
12344
12345				tableEchoSpokenNodes.reload();
12346
12347				var roomsPromise = echoSpokenNodesDialog.loadRooms(uuid);
12348				tableEchoSpokenNodes.setRooms(roomsPromise); // Will be available when we edit!
12349
12350				roomsPromise.done(function(data) {
12351					var newRoom=null;
12352
12353					if (roomRecord.data && roomRecord.data._id) {
12354						newRoom = roomRecord.data._id;
12355					}
12356
12357					echoSpokenNodesEditDialog.updateRoomSelector(data.data, newRoom);
12358					echoSpokenNodesAVEditDialog.updateRoomSelector(data.data, newRoom);
12359					dialog.dialog('close');
12360				})
12361			})
12362			.fail(function(jqXHR, textStatus, errorThrown) {
12363				if (jqXHR.status==503) {
12364					updateTips(tips, 'ISY is not online');
12365				}
12366				else if (jqXHR.status==420) { // Dupkey error with Custom server error message
12367					var data = JSON.parse(jqXHR.responseText);
12368
12369					if (data.message) {
12370						updateTips(tips, data.message);
12371					}
12372					else {
12373						updateTips(tips, 'HTTP error ' + jqXHR.status);
12374					}
12375				}
12376				else {
12377					updateTips(tips, 'HTTP error ' + jqXHR.status);
12378				}
12379			})
12380			.done();
12381		}
12382	}
12383
12384	var isyOptions = {
12385		autoOpen: false,
12386		width: 400,
12387		modal: true,
12388		buttons: {
12389			'Save': function() {
12390				submit();
12391			}
12392		},
12393		open: function(event, ui) {
12394			dialog.dialog( "option", "position", { my: "center", at: "center+10", of: window });
12395		}
12396	};
12397
12398	// Initializes the dialog
12399	dialog = $('#echo_spoken_room_edit_dialog').dialog(isyOptions);
12400
12401	// Handles keyboad enter (submit event)
12402	form = dialog.find('form').submit(function(event) {
12403		event.preventDefault();
12404		submit();
12405	});
12406
12407	return { modal: dialog, setFields: setFields }; //, updateRoomSelector: updateRoomSelector
12408};
12409
12410
12411// GOOGLE HOME Main Spoken Devices & Scenes mapping
12412var ghSpokenNodesDialogInit = function () {
12413	var dialog,
12414		uuid,
12415		domain,
12416		roomsPromise,
12417		lookupDataPromise,
12418		lookupDataPromisePgm,
12419		lookupDataPromiseStateVar;
12420
12421	function loadRooms(uuid) {
12422		roomsPromise = ajaxJsonGet('/api/voice/google/spoken/rooms', $.param({ uuid: uuid, domain: domain }));
12423
12424		roomsPromise
12425		.fail(function(jqXHR, textStatus, errorThrown) {
12426			if (jqXHR.status==503) {
12427				errorMessageDialog('ISY is not online')
12428			}
12429			else if (jqXHR.status==504) {
12430				errorMessageDialog('Timeout occured. Please try again.')
12431			}
12432			else if (jqXHR.status==550) {  // Custom error message
12433				var data = JSON.parse(jqXHR.responseText);
12434
12435				if (data.message) {
12436					errorMessageDialog(data.message)
12437				}
12438				else {
12439					errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
12440				}
12441			}
12442			else {
12443				errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
12444			}
12445		})
12446		.done();
12447
12448		return roomsPromise;
12449	}
12450
12451	function setFields(obj) {
12452		$('#gh_spoken_nodes_uuid').text(obj.uuid);
12453		$('#gh_spoken_nodes_alias').text(obj.alias);
12454
12455		uuid = obj.uuid;
12456		domain = obj.domain;
12457
12458		lookupDataPromise = ajaxJsonGet('/api/nodes/lookup', $.param({ uuid: uuid, domain: domain }))
12459		.done(function(content, status, jqXHR){
12460			content.data.devices.map(function (item) {
12461				var path = item.path.map(function(p) { return p.name })
12462  				.concat([ item.name ])
12463  				.join(' / ');
12464
12465				item.data = item.address;
12466				item.value = item.label = path + ' (' + item.address.replace(/ /g, '.') + ')';
12467				item.option = item; // Self reference;
12468			});
12469/*
12470			content.data.devices = content.data.devices.filter(function(item) {
12471				var addressSplit = item.address.split('_');
12472				if (addressSplit.length==2) { // This is a ZWave device
12473					 if (!['1'].includes(addressSplit[1])) {
12474						return false // We exclude subdevices like color bulb (186)
12475					 }
12476				}
12477				return true;
12478			})
12479*/
12480			content.data.devices.sort(function(a, b) {
12481				return a.value.localeCompare(b.value) }
12482			);
12483
12484			content.data.scenes.map(function (item) {
12485				item.data = item.address;
12486				item.value = item.label = item.name + ' (' + item.address.replace(/ /g, '.') + ')';
12487				item.option = item; // Self reference;
12488			});
12489
12490			content.data.scenes.sort(function(a, b) {
12491				return a.name.localeCompare(b.name) }
12492			);
12493		})
12494		.fail(function (jqXHR, textStatus, errorThrown) {
12495			jqXHR.requested = 'Devices'; // Track which request it is in the jqXHR object
12496		});
12497
12498		lookupDataPromisePgm = ajaxJsonGet('/api/programs/lookup', $.param({uuid: uuid, domain: domain}))
12499		.done(function(content, status, jqXHR){
12500			content.data.map(function (item) {
12501				item.data = item.address;
12502				item.value = item.label = item.name + ' (' + item.address.replace(/ /g, '.') + ')';
12503				item.option = item; // Self reference;
12504			});
12505
12506			content.data.sort(function(a, b) {
12507				return a.name.localeCompare(b.name) }
12508			);
12509		})
12510		.fail(function (jqXHR, textStatus, errorThrown) {
12511			jqXHR.requested = 'Programs'; // Track which request it is in the jqXHR object
12512		});
12513
12514		lookupDataPromiseStateVar = ajaxJsonGet('/api/statevars/lookup', $.param({uuid: uuid, domain: domain}))
12515		.done(function(content, status, jqXHR){
12516			content.data.map(function (item) {
12517				item.data = item.address;
12518				item.value = item.label = item.name + ' (' + item.address.replace(/ /g, '.') + ')';
12519				item.option = item; // Self reference;
12520			});
12521
12522			content.data.sort(function(a, b) {
12523				return a.name.localeCompare(b.name) }
12524			);
12525		})
12526		.fail(function (jqXHR, textStatus, errorThrown) {
12527			jqXHR.requested = 'State variables'; // Track which request it is in the jqXHR object
12528			jqXHR.skipErrorMessage = true;
12529		});
12530
12531		$.when(lookupDataPromise, lookupDataPromisePgm, lookupDataPromiseStateVar)
12532		.fail(function(jqXHR, textStatus, errorThrown) {
12533			if (jqXHR.status==503) {
12534				errorMessageDialog('ISY is not online')
12535			}
12536			else if (jqXHR.status==404) {
12537				// No need to warn the user about state variables not loading. It may be normal.
12538				if (!jqXHR.skipErrorMessage) {
12539					errorMessageDialog(jqXHR.requested + ' are not available')
12540				}
12541			}
12542			else {
12543				errorMessageDialog(jqXHR.requested + ' are not available<br>' + 'HTTP error ' + jqXHR.status, 'Error', 400)
12544			}
12545		});
12546
12547		loadRooms(uuid);
12548		tableGHSpokenNodes.setRooms(roomsPromise);
12549		tableGHSpokenNodes.setLookupDataPromise(lookupDataPromise, lookupDataPromisePgm, lookupDataPromiseStateVar); // Pass a reference to the promises, so that we can check it when we try to edit a spoken.
12550
12551		// setRooms && setLookupDataPromise must have been done first
12552		tableGHSpokenNodes.setUuid(uuid, domain, true);
12553	}
12554
12555	var isyOptions = {
12556		autoOpen: false,
12557		width: 900,
12558		modal: true,
12559		buttons: {
12560			'Close': function() {
12561				$(this).dialog('close');
12562			}
12563		},
12564		open: function(event, ui) {
12565			dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
12566		}
12567	};
12568
12569	// Initializes the dialog
12570	dialog = $('#gh_spoken_nodes_dialog').dialog(isyOptions);
12571
12572	$("#gh_spoken_nodes_add").buttonset({items: 'button'});
12573
12574	$('#gh_spoken_nodes_adddevice').click(function () {
12575		$(this).blur(); // Remove focus from button
12576
12577		$.when(lookupDataPromise, roomsPromise)
12578		.then(function(contentAjax, roomsAjax) {
12579			var content=contentAjax[0];
12580			var rooms=roomsAjax[0];
12581
12582			if (!content) {
12583				throw new Error('Devices not available');
12584			}
12585
12586			ghSpokenNodesEditDialog.setFields({ category: 'std', uuid: uuid, domain: domain, lookup: content.data.devices, address: '', editMode:false, rooms: rooms.data });
12587			ghSpokenNodesEditDialog.modal.dialog('open');
12588		})
12589		.fail(function(jqXHR, textStatus, errorThrown) {
12590			errorMessageDialog("Devices are not available", 'Error', 350)
12591		})
12592		.done();
12593	});
12594
12595	$('#gh_spoken_nodes_addscene').click(function () {
12596		$(this).blur(); // Remove focus from button
12597
12598		$.when(lookupDataPromise, roomsPromise)
12599		.then(function(contentAjax, roomsAjax) {
12600			var content=contentAjax[0];
12601			var rooms=roomsAjax[0];
12602
12603			if (!content) {
12604				throw new Error('Devices not available');
12605			}
12606			ghSpokenNodesEditDialog.setFields({ category: 'scene', uuid: uuid, domain: domain, lookup: content.data.scenes, address: '', editMode:false, rooms: rooms.data });
12607			ghSpokenNodesEditDialog.modal.dialog('open');
12608		})
12609		.fail(function(jqXHR, textStatus, errorThrown) {
12610			errorMessageDialog("Devices are not available", 'Error', 350)
12611		})
12612		.done();
12613	});
12614
12615	$('#gh_spoken_nodes_addprogram').click(function () {
12616		$(this).blur(); // Remove focus from button
12617
12618		$.when(lookupDataPromisePgm, roomsPromise)
12619		.then(function(contentAjax, roomsAjax) {
12620			var content=contentAjax[0];
12621			var rooms=roomsAjax[0];
12622
12623			if (!content) {
12624				throw new Error('Programs not available');
12625			}
12626			ghSpokenNodesEditDialog.setFields({ category: 'program', uuid: uuid, domain: domain, lookup: content.data, address: '', editMode:false, rooms: rooms.data });
12627			ghSpokenNodesEditDialog.modal.dialog('open');
12628		})
12629		.fail(function(jqXHR, textStatus, errorThrown) {
12630			errorMessageDialog("Programs are not available", 'Error', 350)
12631		})
12632		.done();
12633	});
12634
12635	$('#gh_spoken_nodes_addstatevar').click(function () {
12636		$(this).blur(); // Remove focus from button
12637
12638		$.when(lookupDataPromiseStateVar, roomsPromise)
12639		.then(function(contentAjax, roomsAjax) {
12640			var content=contentAjax[0];
12641			var rooms=roomsAjax[0];
12642
12643			if (!content) {
12644				throw new Error('State Variables not available');
12645			}
12646
12647			ghSpokenNodesEditDialog.setFields({ category: 'statevar', uuid: uuid, domain: domain, lookup: content.data, address: '', editMode:false, rooms: rooms.data });
12648			ghSpokenNodesEditDialog.modal.dialog('open');
12649		})
12650		.fail(function(jqXHR, textStatus, errorThrown) {
12651			errorMessageDialog("State variables are not available", 'Error', 350)
12652		})
12653		.done();
12654	});
12655
12656	$('#gh_spoken_nodes_deleteall').button({ icons: {primary: 'ui-icon-custom-delete'}, text: false}).click(function () {
12657		$(this).blur(); // Remove focus from button
12658
12659		confirmationDialog('All of your Google Home spokens for this ISY will be deleted. Are you sure?', 'Delete spokens?')
12660		.then(function () {
12661			ajaxJsonDelete('/api/voice/google/spoken/nodes/deleteall', $.param({uuid: uuid, domain: domain }))
12662			.done(function () {
12663				tableGHSpokenNodes.reload();
12664			})
12665			.fail(function(jqXHR, textStatus, errorThrown) {
12666				errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350);
12667			})
12668		})
12669		.done();
12670	});
12671
12672	$('#gh_spoken_nodes_download').button({ icons: {primary: 'ui-icon-custom-download'}, text: false}).click(function () {
12673		$(this).blur(); // Remove focus from button
12674
12675		informationMessageDialog('Press Ok to download to a CSV file.', 'Export spokens')
12676		.then(function () {
12677			try {
12678				var isFileSaverSupported = !!new Blob;
12679			} catch (e) {}
12680
12681			if (isFileSaverSupported) {
12682				var str = 'Device name,Address,ISY Cat.,GH Cat.,Room,Spoken,Spoken-2,Spoken-3,Spoken-4,Spoken-5\r\n';
12683
12684				tableGHSpokenNodes.data.data.forEach(function(spoken) {
12685					str += [spoken.name, spoken.address, spoken.category==='std'?'device':spoken.category, spoken.userCat, spoken.roomName].concat(spoken.spoken).join(',') + '\r\n'
12686				});
12687
12688				var BOM = '\uFEFF';
12689				var blob = new Blob([BOM + str], {type: "application/csv;charset=utf-8"});
12690				saveAs(blob, "Spokens_" + uuidNC(uuid) + ".csv");
12691			}
12692			else {
12693				errorMessageDialog('Feature not supported on this browser', 'Not supported');
12694			}
12695		})
12696		.done();
12697	});
12698
12699	$('#gh_spoken_nodes_refresh').button({ icons: {primary: 'ui-icon-custom-refresh'}, text: false}).click(function(event) {
12700		$(this).blur(); // Remove focus from button
12701		tableGHSpokenNodes.reload();
12702	});
12703
12704	$('#gh_spoken_nodes_sync').button({ icons: {primary: 'ui-icon-custom-talk'}, text: false}).click(function () {
12705		$(this).blur(); // Remove focus from button
12706
12707		informationMessageDialog('Press Ok to send your spokens to Google Home.', 'Send spokens?')
12708		.then(function () {
12709			ajaxJsonPost('/api/voice/google/spoken/nodes/sync', $.param({uuid: uuid, domain: domain }))
12710			.then(function() {
12711				informationMessageDialog('Spokens successfully sent to Google Home.', 'Success', 350)
12712			})
12713			.fail(function(jqXHR, textStatus, errorThrown) {
12714				if (jqXHR.status==425) {
12715					errorMessageDialog(jqXHR.responseText, 'Error', 650);
12716				}
12717				else {
12718					errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
12719				}
12720			})
12721		})
12722		.done();
12723	});
12724
12725
12726	return { modal: dialog, setFields: setFields, loadRooms: loadRooms };
12727};
12728
12729// GOOGLE HOME Spoken Devices & Scenes mapping
12730var ghSpokenNodesEditDialogInit = function () {
12731	var dialog,
12732		form,
12733		pk,
12734		uuid,
12735		domain,
12736		currentRooms,
12737		category,
12738		stdLookup,
12739		editMode,
12740		device = $('#gh_spoken_address'),
12741		room = $('#gh_spoken_room'),
12742		turnon_value = $('#gh_spoken_turnon_value'),
12743		turnoff_value = $('#gh_spoken_turnoff_value'),
12744		colorManufacturer = $('#gh_spoken_color_input_selector'),
12745		spokens = [],
12746		allFields,
12747		tips = $('#gh_spoken_nodes_edit_dialog .validateTips'),
12748		isColor,
12749		colorMfr, // Manufacturer - selected option
12750		colorMfrOptions = [
12751			{ value: '$select', description: 'Select one...' },
12752			{ value: 'AEOTEC', description: 'Aeotec' },
12753			{ value: 'FIBARO', description: 'Fibaro' },
12754			{ value: 'ZWGEN', description: 'Z-Wave Generic' }
12755		];
12756
12757	spokens[0] = $('#gh_spoken_spoken');
12758	spokens[1] = $('#gh_spoken_spoken2');
12759	spokens[2] = $('#gh_spoken_spoken3');
12760	spokens[3] = $('#gh_spoken_spoken4');
12761	spokens[4] = $('#gh_spoken_spoken5');
12762
12763	allFields = $([]).add(device).add(room).add(turnon_value).add(turnoff_value).add(colorManufacturer).add(spokens[0]).add(spokens[1]).add(spokens[2]).add(spokens[3]).add(spokens[4]).add(spokens[5]);
12764
12765	var categoryLabel = {
12766		'std' : 'device',
12767		'scene' : 'scene',
12768		'program' : 'program',
12769		'statevar' : 'state variable'
12770	};
12771
12772	function updateRoomSelector(rooms, choice) {
12773		// label: shown in the selections
12774		// value: displayed in the combobox input field (this.input)
12775		// data: real data assigned to the input field (this.element)
12776
12777		rooms = rooms.map(function(item) {
12778			return {label: item.name, value: item.name, data: item._id}
12779		});
12780
12781		rooms.sort(function(a, b) {
12782			return a.label.localeCompare(b.label)
12783		});
12784
12785		rooms.unshift({ label: '(No room)', value: '', data: '$noroom'});
12786
12787		room.combobox("option", "source", rooms);
12788		room.combobox("option", "data", choice);
12789
12790		currentRooms = rooms;
12791	}
12792
12793	// Set isColor based on the address and Show/Hide the colorMfr selectmenu
12794	function setIsColor(address) {
12795		if (address) {
12796			var addressSplit = address.split('_');
12797			isColor = (addressSplit.length===2 && addressSplit[1].slice(0,3)==='186')
12798		}
12799		else {
12800			isColor=false;
12801		}
12802
12803		if (isColor) {
12804			$('#gh_spoken_color_input').show();
12805		}
12806		else {
12807			$('#gh_spoken_color_input').hide();
12808		}
12809	}
12810
12811	function setFields(obj) {
12812		var label;
12813
12814		uuid = obj.uuid;
12815		domain = obj.domain;
12816		pk = obj.pk;
12817		category = obj.category; // 4 possibilities: 'statevar', 'program', 'scene', 'std'
12818		stdLookup = obj.lookup;
12819
12820		form[0].reset();
12821		device.combobox("option", "source", obj.lookup);
12822		device.combobox("option", "data", obj.address);
12823		turnon_value.val(obj.turnon_value);
12824		turnoff_value.val(obj.turnoff_value);
12825
12826		for (var i=0; i<5; i++) {
12827			if (obj.spokens && i<=obj.spokens.length) {
12828				spokens[i].val(obj.spokens[i]);
12829			}
12830			else {
12831				spokens[i].val('');
12832			}
12833		}
12834
12835		if (category==='statevar') {
12836			$('#gh_spoken_statevar_input').show();
12837		}
12838		else {
12839			$('#gh_spoken_statevar_input').hide();
12840		}
12841
12842		// Initially, the userCat selectmenu is not available for standard devices.
12843		$('#gh_spoken_user_cat_input').show();
12844
12845		editMode = obj.editMode;
12846
12847		buildUserCatSelect(categoryOverride(category, stdLookup, obj.address, isColor), true);
12848
12849		if (editMode) {
12850			tips.text('Please choose a spoken for this ' + categoryLabel[category] + '.');
12851			spokens[0].focus();
12852
12853			setIsColor(obj.address);
12854			if (isColor) {
12855				colorMfr=obj.colorMfr?obj.colorMfr:'$select';
12856			}
12857
12858			// statevars, programs and scenes used to be a light by default.
12859			// If userCat is not available, and we are editing, set the userCat default to 'light'
12860			if (['statevar', 'program', 'scene'].includes(category) && !obj.userCat) {
12861				obj.userCat = 'switch'
12862			}
12863
12864			if (obj.userCat) {
12865				setUserCatSelect(obj.userCat)
12866			}
12867		}
12868		else {
12869			tips.text('Please choose a ' + categoryLabel[category] +' and enter a corresponding spoken.');
12870			setIsColor(false);
12871			colorMfr='$select';
12872		}
12873
12874		$('#gh_spoken_color_input_selector select').val(colorMfr);
12875		$('#gh_spoken_color_input_selector select').selectmenu('refresh');
12876
12877		// Disable field if editing
12878		device.combobox("disabled", editMode);
12879
12880		$('#gh_spoken_lookup_label').text('My ' + categoryLabel[category]);
12881		allFields.removeClass('fieldfocus');
12882
12883		if (!editMode) {
12884			obj.room = '$noroom';
12885		}
12886
12887		updateRoomSelector(obj.rooms, obj.room);
12888	}
12889
12890	// Restricts the userCat select to valid categories. (If the device is a thermostat, lock or color bulb)
12891	function categoryOverride(category, stdLookup, stdAddress, isColor) {
12892		if (category==='std') {
12893
12894			// See if the individual device is a lock or thermostat, and use that device category instead ('lock' or 'thermostat')
12895			var option = stdLookup.find(function(item) {
12896				return item.address === stdAddress;
12897			});
12898
12899			if (option) {
12900				category = option.category;
12901			}
12902
12903			// Check if current device is a color bulb
12904			if (isColor) {
12905				category = 'color'
12906			}
12907		}
12908
12909		return category;
12910	}
12911
12912	function buildUserCatSelect(category, defaultToFirstOption) {
12913		// The default option is the first one.
12914		var userCats = {
12915			'std': [ { option:'light', desc: 'Light' }, { option:'switch', desc: 'Switch' }, { option:'outlet', desc: 'Outlet' }, { option:'fan', desc: 'Fan' }, { option:'openClose', desc: 'Device with open/close syntax' } ],
12916			'color': [ { option:'light', desc: 'Light' } ],
12917			'lock': [ { option:'lock', desc: 'Lock' } ],
12918			'thermostat': [ { option:'thermostat', desc: 'Thermostat' } ],
12919			'scene': [ { option:'scene', desc: 'Scene' }, { option:'light', desc: 'Light' }, { option:'switch', desc: 'Switch' }, { option:'outlet', desc: 'Outlet' }, { option:'openClose', desc: 'Device with open/close syntax' } ],
12920			'program': [ { option:'scene', desc: 'Scene' }, { option:'light', desc: 'Light' }, { option:'switch', desc: 'Switch' }, { option:'outlet', desc: 'Outlet' }, { option:'openClose', desc: 'Device with open/close syntax' } ],
12921			'statevar': [ { option:'scene', desc: 'Scene' }, { option:'light', desc: 'Light' }, { option:'switch', desc: 'Switch' }, { option:'outlet', desc: 'Outlet' }, { option:'openClose', desc: 'Device with open/close syntax' } ]
12922		};
12923
12924		if (userCats[category]) {
12925			var $select = $('<select></select>');
12926
12927			userCats[category].forEach(function(item) {
12928				$select.append('<option value="' + item.option + '">' + item.desc + '</option>');
12929			});
12930
12931			if (!selectHasIdenticalOptions($select, $('#gh_spoken_user_cat_selector'))) {
12932				// Default option is the first
12933				$('#gh_spoken_user_cat_selector').empty().append($select.clone());
12934				$('#gh_spoken_user_cat_selector select').selectmenu({ width:'20em' });
12935			}
12936			else {
12937				// If we don't rewrite, set the default option to the first one when we setField.
12938				if (defaultToFirstOption) {
12939					setUserCatSelect($('#gh_spoken_user_cat_selector option').eq(0).attr('value'))
12940				}
12941			}
12942		}
12943		else {
12944			$('#gh_spoken_user_cat_input').hide();
12945			$('#gh_spoken_user_cat_selector').empty(); // Destroy the selector.
12946		}
12947	}
12948
12949	function setUserCatSelect(option) {
12950		$('#gh_spoken_user_cat_selector select').val(option); // Set user category select
12951		$('#gh_spoken_user_cat_selector select').selectmenu("refresh");
12952	}
12953
12954	// Validation function
12955	function checkColorMfr(tips, field) {
12956		if (isColor && colorMfr==='$select') {
12957			field.focus();
12958			updateTips(tips, 'Device manufacturer is required.');
12959			return false;
12960		}
12961		else {
12962			return true;
12963		}
12964	}
12965
12966	function submit() {
12967		var valid = true;
12968
12969		device.combobox("refresh"); // Makes sure the real input field is up to date
12970		if (!device.combobox("validate")) {
12971			updateTips(tips, 'Device is not allowed');
12972			valid=false;
12973		}
12974
12975		room.combobox("refresh"); // Makes sure the real input field is up to date
12976		if (!room.combobox("validate")) {
12977
12978			var name = room.combobox("getValue");
12979
12980			confirmationDialog('Room ' + name + ' does not exists.<br/><br/>Create it?', 'Create room?')
12981			.then(function () {
12982				ajaxJsonPut('/api/voice/google/spoken/room', $.param({domain: domain, uuid: uuid, name: name }))
12983				.done(function (record) {
12984					var roomsPromise = ghSpokenNodesDialog.loadRooms(uuid);
12985
12986					roomsPromise
12987					.then(function(data) {
12988						updateRoomSelector(data.data, record.data._id);
12989					});
12990
12991					tableGHSpokenNodes.setRooms(roomsPromise);
12992				})
12993				.fail(function(jqXHR, textStatus, errorThrown) {
12994					errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350);
12995				})
12996			})
12997			.done();
12998
12999
13000			valid=false;
13001		}
13002
13003		allFields.removeClass('fieldfocus');
13004
13005		if (category==='statevar') {
13006			valid = valid && checkNotEmpty(tips, turnoff_value, 'Turn off value');
13007			valid = valid && checkNumber(tips, turnoff_value, 'Turn off value', 0, 2147483647);
13008			valid = valid && checkNotEmpty(tips, turnon_value, 'Turn on value');
13009			valid = valid && checkNumber(tips, turnon_value, 'Turn on value', 0, 2147483647);
13010		}
13011
13012		valid = valid && checkColorMfr(tips, colorManufacturer);
13013		valid = valid && checkLength(tips, spokens[0], 'Spoken', 2, 40);
13014		valid = valid && checkRegexp(tips, spokens[0], spokenRegex, 'Spoken must contain only letters with spaces');
13015		valid = valid && checkLength(tips, spokens[1], 'Alternate spoken 1', 0, 40);
13016		valid = valid && checkRegexp(tips, spokens[1], spokenRegex, 'Spoken must contain only letters with spaces');
13017		valid = valid && checkLength(tips, spokens[2], 'Alternate spoken 2', 0, 40);
13018		valid = valid && checkRegexp(tips, spokens[2], spokenRegex, 'Spoken must contain only letters with spaces');
13019		valid = valid && checkLength(tips, spokens[3], 'Alternate spoken 3', 0, 40);
13020		valid = valid && checkRegexp(tips, spokens[3], spokenRegex, 'Spoken must contain only letters with spaces');
13021		valid = valid && checkLength(tips, spokens[4], 'Alternate spoken 4', 0, 40);
13022		valid = valid && checkRegexp(tips, spokens[4], spokenRegex, 'Spoken must contain only letters with spaces');
13023
13024		if (valid) {
13025			var data = form.find('input').serialize();
13026			data += '&uuid=' + uuid + '&domain=' + domain + (isColor?'&colorMfr='+colorMfr:'');
13027
13028			if (editMode) {
13029				data += '&id=' + pk;
13030			}
13031
13032			data += '&category=' + category; // valid values: program, scene, std
13033
13034			var userCat = $('#gh_spoken_user_cat_selector select').val();
13035			if (userCat) {
13036				data += '&userCat=' + userCat;
13037			}
13038
13039			(editMode?ajaxJsonPost('/api/voice/google/spoken/node', data):ajaxJsonPut('/api/voice/google/spoken/node', data))
13040			.then(function(data, status, jqXHR) {
13041				dialog.dialog('close');
13042				tableGHSpokenNodes.reload();
13043			})
13044			.fail(function(jqXHR, textStatus, errorThrown) {
13045				if (jqXHR.status==503) {
13046					updateTips(tips, 'ISY is not online');
13047				}
13048				else if (jqXHR.status==420) { // Dupkey error with Custom server error message
13049					var data = JSON.parse(jqXHR.responseText);
13050
13051					if (data.message) {
13052						updateTips(tips, data.message);
13053					}
13054					else {
13055						updateTips(tips, 'HTTP error ' + jqXHR.status);
13056					}
13057				}
13058				else if (jqXHR.status==400) {
13059					updateTips(tips, 'HTTP error ' + jqXHR.status + ' : ' + jqXHR.responseText);
13060				}
13061				else {
13062					updateTips(tips, 'HTTP error ' + jqXHR.status);
13063				}
13064			})
13065			.done();
13066		}
13067	}
13068
13069	var isyOptions = {
13070		autoOpen: false,
13071		width: 950,
13072		modal: true,
13073		buttons: {
13074			'Save': function() {
13075				submit();
13076			}
13077		},
13078		open: function(event, ui) {
13079			dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
13080		}
13081	};
13082
13083	// Initializes the dialog
13084	dialog = $('#gh_spoken_nodes_edit_dialog').dialog(isyOptions);
13085
13086	// Handles keyboad enter (submit event)
13087	form = dialog.find('form').submit(function(event) {
13088		event.preventDefault();
13089		submit();
13090	});
13091
13092	$('#gh_spoken_room_add').button({ icons: {primary: 'ui-icon-plus'}, text: false}).tooltip().click(function(event) {
13093		$(this).blur(); // Remove focus from button
13094		ghSpokenRoomEditDialog.setFields({ uuid: uuid, editMode: false, domain: domain });
13095		ghSpokenRoomEditDialog.modal.dialog('open');
13096	});
13097
13098	$('#gh_spoken_room_edit').button({ icons: {primary: 'ui-icon-pencil'}, text: false}).tooltip().click(function(event) {
13099		$(this).blur(); // Remove focus from button
13100
13101		room.combobox("refresh"); // Makes sure the real input field is up to date
13102		var pk = room.val();
13103
13104		var item = currentRooms.find(function(item) {
13105			return item.data === pk;
13106		});
13107
13108		if (pk!=='$noroom' && item) {
13109			ghSpokenRoomEditDialog.setFields({ uuid: uuid, editMode: true, domain: domain, pk: pk, name: item.label });
13110			ghSpokenRoomEditDialog.modal.dialog('open');
13111		}
13112		else {
13113			informationMessageDialog("This can't be edited.")
13114		}
13115	});
13116
13117	$('#gh_spoken_room_delete').button({ icons: {primary: 'ui-icon-trash'}, text: false}).tooltip().click(function(event) {
13118		$(this).blur(); // Remove focus from button
13119
13120		room.combobox("refresh"); // Makes sure the real input field is up to date
13121		var pk = room.val();
13122
13123		var item = currentRooms.find(function(item) {
13124			return item.data === pk;
13125		});
13126
13127		if (pk!=='$noroom' && item) {
13128			confirmationDialog('Delete room ' + item.label + '.<br/><br/>Are you sure?', 'Delete room?')
13129			.then(function () {
13130				ajaxJsonDelete('/api/voice/google/spoken/room', $.param({id: pk, domain: domain, uuid: uuid }))
13131				.done(function () {
13132					var roomsPromise = ghSpokenNodesDialog.loadRooms(uuid);
13133
13134					roomsPromise
13135					.then(function(data) {
13136						ghSpokenNodesEditDialog.updateRoomSelector(data.data, '$noroom');
13137					});
13138
13139					tableGHSpokenNodes.setRooms(roomsPromise);
13140					tableGHSpokenNodes.reload();
13141				})
13142				.fail(function(jqXHR, textStatus, errorThrown) {
13143					errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350);
13144				})
13145			})
13146			.done();
13147		}
13148		else {
13149			informationMessageDialog("This can't be removed.")
13150		}
13151	});
13152
13153	$('#gh_spokenStatevarHelp').click(function() {
13154		informationMessageDialog('When you turn on or turn off this "device", the state variable will be set to the one of these values. However, if you turn on to X percent, the variable will be set to the percent value.', 'Help', 550)
13155	});
13156
13157	device.combobox({
13158		inputClass: '',
13159		focusout: function(event) {
13160			device.combobox('refresh');
13161
13162			if (category==='std') {
13163				if (deviceHasColorNode(device.val(), stdLookup)) {
13164					var colorAddress = device.val().split('_')[0]+'_186';
13165
13166					informationMessageDialog('This device supports color. However, to enable color support, you need to choose the device whose address ends with _186. Do you want to use the device ' + colorAddress + ' instead?', 'Color support?', 600)
13167					.then(function () {
13168						device.combobox("option", "data", colorAddress);
13169						device.combobox("refresh"); // Makes sure the real input field is up to date
13170						setIsColor(colorAddress);
13171					})
13172					.done();
13173				}
13174				else {
13175					setIsColor(device.val());
13176				}
13177
13178				// If the device has change, see if this is a lock or thermostat, and if so, enforce the user cat to be lock or thermostat.
13179				if (device.combobox("validate")) {
13180					var catOverride = categoryOverride(category, stdLookup, $('#gh_spoken_address').val(), isColor);
13181					buildUserCatSelect(catOverride, false);
13182
13183					// if (catOverride=='lock') {
13184					// 	errorMessageDialog('Locks are not yet supported by Google Home. They can be saved, but they are not sent to Google.', 'Error', 450)
13185					// }
13186				}
13187			}
13188		}
13189	});
13190	room.combobox({inputClass: ''});
13191
13192	buildSelect(colorMfrOptions, '#gh_spoken_color_input_selector', '15em', function(event, ui) {
13193		colorMfr = ui.item.value;
13194	});
13195
13196	return { modal: dialog, setFields: setFields, updateRoomSelector: updateRoomSelector };
13197};
13198
13199// GOOGLE HOME Spoken room edit dialog
13200var ghSpokenRoomEditDialogInit = function () {
13201	var dialog,
13202		form,
13203		pk,
13204		uuid,
13205		domain,
13206		editMode,
13207		name = $('#gh_spoken_room_name'),
13208		allFields = $([]).add(name),
13209		tips = $('#gh_spoken_room_edit_dialog .validateTips');
13210
13211	function setFields(obj) {
13212		var label;
13213
13214		uuid = obj.uuid;
13215		domain = obj.domain;
13216		pk = obj.pk;
13217
13218		form[0].reset();
13219		name.val(obj.name);
13220
13221		editMode = obj.editMode;
13222
13223		tips.text('Please enter a room name');
13224
13225		allFields.removeClass('fieldfocus');
13226	}
13227
13228	function submit() {
13229		var valid = true;
13230
13231		allFields.removeClass('fieldfocus');
13232
13233		valid = valid && checkLength(tips, name, 'Name', 2, 40);
13234		valid = valid && checkRegexp(tips, name, roomNameRegex, 'room name is not valid.' );
13235
13236		if (valid) {
13237			var data = form.find('input').serialize();
13238			data += '&domain=' + domain + '&uuid=' + uuid;
13239
13240			if (editMode) {
13241				data += '&id=' + pk;
13242			}
13243
13244			var url = '/api/voice/google/spoken/room';
13245
13246			(editMode?ajaxJsonPost(url, data):ajaxJsonPut(url, data))
13247			.then(function(roomRecord, status, jqXHR) {
13248
13249				tableGHSpokenNodes.reload();
13250
13251				var roomsPromise = ghSpokenNodesDialog.loadRooms(uuid);
13252				tableGHSpokenNodes.setRooms(roomsPromise); // Will be available when we edit!
13253
13254				roomsPromise.done(function(data) {
13255					var newRoom=null;
13256
13257					if (roomRecord.data && roomRecord.data._id) {
13258						newRoom = roomRecord.data._id;
13259					}
13260
13261					ghSpokenNodesEditDialog.updateRoomSelector(data.data, newRoom);
13262					dialog.dialog('close');
13263				})
13264			})
13265			.fail(function(jqXHR, textStatus, errorThrown) {
13266				if (jqXHR.status==503) {
13267					updateTips(tips, 'ISY is not online');
13268				}
13269				else if (jqXHR.status==420) { // Dupkey error with Custom server error message
13270					var data = JSON.parse(jqXHR.responseText);
13271
13272					if (data.message) {
13273						updateTips(tips, data.message);
13274					}
13275					else {
13276						updateTips(tips, 'HTTP error ' + jqXHR.status);
13277					}
13278				}
13279				else {
13280					updateTips(tips, 'HTTP error ' + jqXHR.status);
13281				}
13282			})
13283			.done();
13284		}
13285	}
13286
13287	var isyOptions = {
13288		autoOpen: false,
13289		width: 400,
13290		modal: true,
13291		buttons: {
13292			'Save': function() {
13293				submit();
13294			}
13295		},
13296		open: function(event, ui) {
13297			dialog.dialog( "option", "position", { my: "center", at: "center+10", of: window });
13298		}
13299	};
13300
13301	// Initializes the dialog
13302	dialog = $('#gh_spoken_room_edit_dialog').dialog(isyOptions);
13303
13304	// Handles keyboad enter (submit event)
13305	form = dialog.find('form').submit(function(event) {
13306		event.preventDefault();
13307		submit();
13308	});
13309
13310	return { modal: dialog, setFields: setFields }; //, updateRoomSelector: updateRoomSelector
13311};
13312
13313// Node Server List main dialog
13314var nsListDialogInit = function () {
13315	var dialog,
13316		uuid,
13317		domain
13318
13319	function setFields(obj) {
13320		$('#ns_list_uuid').text(obj.uuid);
13321		$('#ns_list_alias').text(obj.alias);
13322
13323		uuid = obj.uuid;
13324		domain = obj.domain;
13325
13326		// setRooms && setLookupDataPromise must have been done first
13327		tableNsList.setUuid(uuid, domain, true);
13328	}
13329
13330	var isyOptions = {
13331		autoOpen: false,
13332		width: 900,
13333		modal: true,
13334		buttons: {
13335			'Close': function() {
13336				$(this).dialog('close');
13337			}
13338		},
13339		open: function(event, ui) {
13340			dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
13341		}
13342	};
13343
13344	// Initializes the dialog
13345	dialog = $('#ns_list_dialog').dialog(isyOptions);
13346
13347	$('#ns_list_refresh').button({ icons: {primary: 'ui-icon-custom-refresh'}, text: false}).click(function(event) {
13348		$(this).blur(); // Remove focus from button
13349		tableNsList.reload();
13350	});
13351
13352	return { modal: dialog, setFields: setFields };
13353};
13354
13355
13356// IFTTT Inbound events
13357var iftttInNodesDialogInit = function () {
13358	var dialog,
13359		uuid,
13360		domain,
13361		lookupDataPromise,
13362		lookupDataPromisePgm,
13363		lookupDataPromiseStateVar;
13364
13365	function setFields(obj) {
13366		$('#ifttt_in_nodes_uuid').text(obj.uuid);
13367		$('#ifttt_in_nodes_alias').text(obj.alias);
13368
13369		uuid = obj.uuid;
13370		domain = obj.domain;
13371
13372		tableIftttInNodes.setUuid(uuid, domain, true);
13373
13374		lookupDataPromise = ajaxJsonGet('/api/nodes/lookup', $.param({ uuid: uuid, domain: domain }))
13375		.done(function(content, status, jqXHR){
13376
13377			content.data.devices = content.data.devices.filter(function (element) {
13378				return element.category === 'std'; // Removes thermostats and locks, keep only std
13379			});
13380
13381			content.data.devices.map(function (item) {
13382				var path = item.path.map(function(p) { return p.name })
13383  				.concat([ item.name ])
13384  				.join(' / ');
13385
13386				item.data = item.address;
13387				item.value = item.label = path + ' (' + item.address.replace(/ /g, '.') + ')';
13388				item.option = item; // Self reference;
13389			});
13390
13391			content.data.devices.sort(function(a, b) {
13392				return a.value.localeCompare(b.value) }
13393			);
13394
13395			content.data.scenes.map(function (item) {
13396				item.data = item.address;
13397				item.value = item.label = item.name + ' (' + item.address.replace(/ /g, '.') + ')';
13398				item.option = item; // Self reference;
13399			});
13400
13401			content.data.scenes.sort(function(a, b) {
13402				return a.name.localeCompare(b.name) }
13403			);
13404		})
13405		.fail(function (jqXHR, textStatus, errorThrown) {
13406			jqXHR.requested = 'Devices'; // Track which request it is in the jqXHR object
13407		});
13408
13409		lookupDataPromiseStateVar = ajaxJsonGet('/api/statevars/lookup', $.param({uuid: uuid, domain: domain}))
13410		.done(function(content, status, jqXHR){
13411			content.data.map(function (item) {
13412				item.data = item.address;
13413				item.value = item.label = item.name + ' (' + item.address.replace(/ /g, '.') + ')';
13414				item.option = item; // Self reference;
13415			});
13416
13417			content.data.sort(function(a, b) {
13418				return a.name.localeCompare(b.name) }
13419			);
13420		})
13421		.fail(function (jqXHR, textStatus, errorThrown) {
13422			jqXHR.requested = 'State variables'; // Track which request it is in the jqXHR object
13423			jqXHR.skipErrorMessage = true;
13424		});
13425
13426		lookupDataPromisePgm = ajaxJsonGet('/api/programs/lookup', $.param({uuid: uuid, domain: domain}))
13427		.done(function(content, status, jqXHR){
13428			content.data.map(function (item) {
13429				item.data = item.address;
13430				item.value = item.label = item.name + ' (' + item.address.replace(/ /g, '.') + ')';
13431				item.option = item; // Self reference;
13432			});
13433
13434			content.data.sort(function(a, b) {
13435				return a.name.localeCompare(b.name) }
13436			);
13437		})
13438		.fail(function (jqXHR, textStatus, errorThrown) {
13439			jqXHR.requested = 'Programs'; // Track which request it is in the jqXHR object
13440		});
13441
13442		$.when(lookupDataPromise, lookupDataPromiseStateVar, lookupDataPromisePgm)
13443		.fail(function(jqXHR, textStatus, errorThrown) {
13444			if (jqXHR.status==503) {
13445				errorMessageDialog('ISY is not online')
13446			}
13447			else if (jqXHR.status==404) {
13448				// No need to warn the user in the case of state vars. It may be normal
13449				if (!jqXHR.skipErrorMessage) {
13450					errorMessageDialog(jqXHR.requested + ' are not available')
13451				}
13452			}
13453			else {
13454				errorMessageDialog(jqXHR.requested + ' are not available<br>' + 'HTTP error ' + jqXHR.status, 'Error', 400)
13455			}
13456		});
13457
13458		tableIftttInNodes.setLookupDataPromise(lookupDataPromise, lookupDataPromisePgm, lookupDataPromiseStateVar); // Pass a reference to the promises, so that we can check it when we try to edit an event.
13459	}
13460
13461	function onlineHelp() {
13462		informationMessageDialog(
13463			'<div class="online-help"><p>The IFTTT / Webhook integration allows you to trigger ISY events from the cloud using services such as IFTTT. ' +
13464			'This is possible using the <a href="http://ifttt.com/maker" target="_blank">IFTTT Maker Channel</a> in the THAT portion of your IFTTT recipe.</p>' +
13465			'<p>First, you need to click on "Set Key" to create a key that will be used to authenticate your IFTTT / Webhook requests. ' +
13466			'Then, you need to create the events that the IFTTT Maker Channel or other cloud service will use.</p>' +
13467			'<p>To configure your Maker channel, first click <span class="ui-icon ui-icon-extlink linkbutton"></span> beside your event configured here to retrieve the URL to use. ' +
13468			'In the maker channel, use the url with the "POST" method, and you are ready to go.</p>' +
13469			'<p>In addition, you can pass values from IFTTT events to a state variable or a dimmable device. To do so, set your event with the action "IFTTT Value". ' +
13470			'Then in the maker channel, you can pass the value in the "Body". Just make sure to set the Content Type to "text/plain".</p>' +
13471			'<p>Values can be passed as an absolute, or relative value. To send a relative value, just precede the value with a + or -. Example: +10</p></div>',
13472			'IFTTT / Webhook Online Help', 820)
13473	}
13474
13475	var isyOptions = {
13476		autoOpen: false,
13477		width: 900,
13478		modal: true,
13479		buttons: {
13480			'Close': function() {
13481				$(this).dialog('close');
13482			}
13483		},
13484		open: function(event, ui) {
13485			dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
13486		}
13487	};
13488
13489	// Initializes the dialog
13490	dialog = $('#ifttt_in_nodes_dialog').dialog(isyOptions);
13491	// Tooltip removed - Causing issues as the scope is too wide
13492//	$('[title]').tooltip();
13493
13494	$("#ifttt_in_nodes_add").buttonset({items: 'button'});
13495
13496	$('#ifttt_in_nodes_adddevice').click(function () {
13497		$(this).blur(); // Remove focus from button
13498
13499		lookupDataPromise
13500		.then(function(content) {
13501			if (!content) {
13502				throw new Error('Devices not available');
13503			}
13504			iftttInNodesEditDialog.setFields({ category: 'std', uuid: uuid, domain: domain, lookup: content.data.devices, address: '', editMode:false });
13505			iftttInNodesEditDialog.modal.dialog('open');
13506		})
13507		.fail(function(jqXHR, textStatus, errorThrown) {
13508			errorMessageDialog("Devices are not available", 'Error', 350)
13509		})
13510		.done();
13511	});
13512
13513	$('#ifttt_in_nodes_addscene').click(function () {
13514		$(this).blur(); // Remove focus from button
13515
13516		lookupDataPromise
13517		.then(function(content) {
13518			if (!content) {
13519				throw new Error('Scenes not available');
13520			}
13521			iftttInNodesEditDialog.setFields({ category: 'scene', uuid: uuid, domain: domain, lookup: content.data.scenes, address: '', editMode:false });
13522			iftttInNodesEditDialog.modal.dialog('open');
13523		})
13524		.fail(function(jqXHR, textStatus, errorThrown) {
13525			errorMessageDialog("Scenes are not available", 'Error', 350)
13526		})
13527		.done();
13528	});
13529
13530	$('#ifttt_in_nodes_addprogram').click(function () {
13531		$(this).blur(); // Remove focus from button
13532
13533		lookupDataPromisePgm
13534		.then(function(content) {
13535			if (!content) {
13536				throw new Error('Programs not available');
13537			}
13538			iftttInNodesEditDialog.setFields({ category: 'program', uuid: uuid, domain: domain, lookup: content.data, address: '', editMode:false });
13539			iftttInNodesEditDialog.modal.dialog('open');
13540		})
13541		.fail(function(jqXHR, textStatus, errorThrown) {
13542			errorMessageDialog("Programs are not available", 'Error', 350)
13543		})
13544		.done();
13545	});
13546
13547	$('#ifttt_in_nodes_addstatevar').click(function () {
13548		$(this).blur(); // Remove focus from button
13549
13550		lookupDataPromiseStateVar
13551		.then(function(content) {
13552			if (!content) {
13553				throw new Error('State Variables not available');
13554			}
13555			iftttInNodesEditDialog.setFields({ category: 'statevar', uuid: uuid, domain: domain, lookup: content.data, address: '', editMode:false });
13556			iftttInNodesEditDialog.modal.dialog('open');
13557		})
13558		.fail(function(jqXHR, textStatus, errorThrown) {
13559			errorMessageDialog("State variables are not available", 'Error', 350)
13560		})
13561		.done();
13562	});
13563
13564	// Set IFTTT key button
13565	$('#ifttt_in_nodes_key').button().click(function(event) {
13566		$(this).blur(); // Remove focus from button
13567		ajaxJsonGet('/api/ifttt/in/key', $.param({ uuid: uuid, domain: domain}))
13568		.then(function (data) {
13569			iftttInNodesKeyDialog.setFields({ uuid: uuid, domain: domain, key: data.data.key});
13570			iftttInNodesKeyDialog.modal.dialog('open');
13571		})
13572	});
13573
13574	$('#ifttt_in_nodes_help').button().click(function(event) {
13575		$(this).blur(); // Remove focus from button
13576		onlineHelp();
13577	});
13578
13579	$('#ifttt_in_nodes_refresh').button({ icons: {primary: 'ui-icon-custom-refresh'}, text: false}).click(function(event) {
13580		$(this).blur(); // Remove focus from button
13581		tableIftttInNodes.reload();
13582	});
13583
13584	return { modal: dialog, setFields: setFields };
13585};
13586
13587var echoCategoryTxt = {
13588	'std' : 'Device',
13589	'scene' : 'Scene',
13590	'program' : 'Program',
13591	'statevar' : 'Variable',
13592	'lock' : 'Lock',
13593	'thermostat' : 'Thermostat',
13594	'av' : 'A/V Device' ,
13595	'elk' : 'ELK Zone'
13596};
13597
13598var iftttCategoryTxt = {
13599	'std' : 'Device',
13600	'scene' : 'Scene',
13601	'program' : 'Program',
13602	'statevar' : 'Variable'
13603//	'lock' : 'Lock', // Only valid for spokens, not IFTTT!
13604//	'thermostat' : 'Thermostat' // Only valid for spokens, not IFTTT!
13605};
13606
13607var iftttCategoryLabel = {
13608	'std' : 'My device',
13609	'scene' : 'My scene',
13610	'program' : 'My program',
13611	'statevar' : 'My state variable',
13612};
13613
13614var iftttControls = {
13615	'std': ['on', 'off', 'brighten', 'dim', 'value', 'parm' ],
13616	'scene': ['on', 'off', 'brighten', 'dim' ],
13617	'program': ['if', 'then', 'else', 'stop'],
13618	'statevar': ['value', 'parm' ],
13619};
13620
13621var iftttControlsLabels = {
13622	'on': 'On',
13623	'off': 'Off',
13624	'brighten': 'Brighten',
13625	'dim': 'Dim',
13626	'value': 'Set value',
13627	'parm': 'IFTTT value',
13628	'if':'Run If',
13629	'then':'Run Then',
13630	'else': 'Run else',
13631	'stop': 'Stop program'
13632};
13633
13634// Spoken Devices & Scenes mapping
13635var iftttInNodesEditDialogInit = function () {
13636	var dialog,
13637		form,
13638		pk,
13639		uuid,
13640		domain,
13641		category,
13642		editMode,
13643		lookup,
13644		device = $('#ifttt_in_address'),
13645		event = $('#ifttt_in_event'),
13646		value = $('#ifttt_in_value'),
13647		allFields = $([]).add(device).add(event).add(value),
13648		tips = $('#ifttt_in_nodes_edit_dialog .validateTips');
13649
13650	function displayValueIf(condition) {
13651		if (condition) {
13652			$('#ifttt_in_value_container').show();
13653		}
13654		else {
13655			$('#ifttt_in_value_container').hide();
13656		}
13657	}
13658
13659	function buildControlSelect(control) {
13660		var $select = $('<select></select>');
13661
13662		iftttControls[control].forEach(function(item) {
13663			$select.append('<option value="' + item + '">' + iftttControlsLabels[item] + '</option>');
13664		});
13665
13666		$('#ifttt_in_control_selector').empty().append($select.clone());
13667
13668		$('#ifttt_in_control_selector select').selectmenu({
13669			width:'13em',
13670
13671			change: function(event, ui) {
13672				var id = ui.item.value;
13673
13674				displayValueIf(id==='value');
13675
13676				if (id!=='value') {
13677					value.val('');
13678				}
13679			}
13680		});
13681	}
13682
13683	function setFields(obj) {
13684		var label;
13685
13686		uuid = obj.uuid;
13687		domain = obj.domain;
13688		pk = obj.pk;
13689		category = obj.category;
13690		lookup = obj.lookup;
13691		editMode = obj.editMode;
13692
13693		$('#ifttt_in_lookup_label').text(iftttCategoryLabel[category]);
13694
13695		// Set event name
13696		event.val(obj.event?obj.event:'');
13697
13698		// set combobox
13699		device.combobox("option", "source", lookup);
13700		device.combobox("option", "data", obj.address);
13701
13702		buildControlSelect(category);
13703
13704		if (editMode) {
13705			tips.text('Please update event conditions.');
13706
13707			$('#ifttt_in_control_selector select').val(obj.control); // Set control select
13708			$('#ifttt_in_control_selector select').selectmenu("refresh");
13709
13710			if (obj.control==='value') {
13711				value.val(obj.value!=null?obj.value.toString():'')
13712			}
13713		}
13714		else {
13715			tips.text('Please enter an IFTTT / Webhook event.');
13716			value.val('');
13717		}
13718
13719		displayValueIf($('#ifttt_in_control_selector select').val()==='value');
13720
13721		event.focus();
13722		allFields.removeClass('fieldfocus');
13723	}
13724
13725	function submit() {
13726		var control = $('#ifttt_in_control_selector select').val();
13727		var valid = true;
13728
13729		valid = valid && checkLength(tips, event, 'Event name', 1, 40);
13730		valid = valid && checkRegexp(tips, event, /^[A-Za-z0-9_.~\-]*$/, 'Event name contains invalid characters.');
13731
13732		device.combobox("refresh"); // Makes sure the real input field is up to date
13733		if (valid && (!device.val().length || !device.combobox("validate"))) {
13734			updateTips(tips, iftttCategoryTxt[category] + ' is not valid');
13735			valid=false;
13736		}
13737
13738		switch(category) {
13739			case 'std':
13740
13741				var devData = findInArray(lookup, 'address', device.val());
13742				if (valid && !devData) {
13743					updateTips(tips, 'Device is not valid'); // Should not happen
13744					valid=false;
13745				}
13746
13747				if (valid && !devData.dimscale && !['on', 'off'].includes(control)) {
13748					updateTips(tips, 'This device can only be set to On or Off');
13749					valid=false;
13750				}
13751
13752				if (control==='value') {
13753					valid = valid && checkRegexp(tips, value, /^(\+|-)?([1-9][0-9]?|100)$/, 'Value must be from 1 to 100. It may be preceded with + or -'); // /^[1-9][0-9]?$|^100$/
13754				}
13755
13756				break;
13757
13758			case 'scene':
13759				break;
13760			case 'program':
13761				break;
13762			case 'statevar':
13763				if (control==='value') {
13764					valid = valid && checkRegexp(tips, value, /^(\+|-)?[0-9]{1,9}$/, 'Value must be from 0 to 999999999. It may be preceded with + or -');
13765				}
13766				break;
13767		}
13768
13769		allFields.removeClass('fieldfocus');
13770
13771		if (valid) {
13772			var data = form.find('input').serialize();
13773			data += '&uuid=' + uuid + '&domain=' + domain;
13774
13775			if (editMode) {
13776				data += '&id=' + pk;
13777			}
13778
13779			data += '&category=' + category; // valid values: program, scene, std, statevar
13780			data += '&control=' + control; // valid values depends on category
13781
13782			(editMode?ajaxJsonPost('/api/ifttt/in/node', data):ajaxJsonPut('/api/ifttt/in/node', data))
13783			.then(function(data, status, jqXHR) {
13784				dialog.dialog('close');
13785				tableIftttInNodes.reload();
13786			})
13787			.fail(function(jqXHR, textStatus, errorThrown) {
13788				if (jqXHR.status===503) {
13789					updateTips(tips, 'ISY is not online');
13790				}
13791				else if (jqXHR.status===420) { // Dupkey error with Custom server error message
13792					var data = JSON.parse(jqXHR.responseText);
13793
13794					if (data.message) {
13795						updateTips(tips, data.message);
13796					}
13797					else {
13798						updateTips(tips, 'HTTP error ' + jqXHR.status);
13799					}
13800				}
13801				else if (jqXHR.status==400) {
13802					updateTips(tips, 'HTTP error ' + jqXHR.status + ' : ' + jqXHR.responseText);
13803				}
13804				else {
13805					updateTips(tips, 'HTTP error ' + jqXHR.status);
13806				}
13807			})
13808			.done();
13809		}
13810	}
13811
13812	var isyOptions = {
13813		autoOpen: false,
13814		width: 950,
13815		modal: true,
13816		buttons: {
13817			'Save': function() {
13818				submit();
13819			}
13820		},
13821		open: function(event, ui) {
13822			dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
13823		}
13824	};
13825
13826	// Initializes the dialog
13827	dialog = $('#ifttt_in_nodes_edit_dialog').dialog(isyOptions);
13828// Tooltip removed - Causing issues as the scope is too wide
13829//	$('[title]').tooltip();
13830
13831	// Handles keyboad enter (submit event)
13832	form = dialog.find('form').submit(function(event) {
13833		event.preventDefault();
13834		submit();
13835	});
13836
13837	device.combobox({inputClass: ''});
13838
13839	return { modal: dialog, setFields: setFields };
13840};
13841
13842// IFTTT Key edit/change
13843var iftttInNodesKeyDialogInit = function () {
13844	var dialog,
13845		form,
13846		uuid,
13847		domain,
13848		key = $('#ifttt_in_key'),
13849		tips = $('#ifttt_in_nodes_key_dialog .validateTips');
13850
13851	function setFields(obj) {
13852		tips.text('Click Create key to generate a new key.');
13853
13854		uuid = obj.uuid;
13855		domain = obj.domain;
13856		key.val(obj.key)
13857	}
13858
13859	function createKey() {
13860		ajaxJsonPost('/api/ifttt/in/key', $.param( { uuid: uuid, domain: domain } ))
13861		.then(function(data, status, jqXHR) {
13862			if (data.data && data.data.key) {
13863				key.val(data.data.key)
13864			}
13865			tableIftttInNodes.reload(); // The table needs to get the new key and refresh
13866			informationMessageDialog('New IFTTT / Webhook key created successfully.', 'Successful')
13867		})
13868		.fail(function(jqXHR, textStatus, errorThrown) {
13869			updateTips(tips, 'HTTP error ' + jqXHR.status);
13870		})
13871		.done();
13872	}
13873
13874	var isyOptions = {
13875		autoOpen: false,
13876		width: 500,
13877		modal: true,
13878		buttons: {
13879			'Create key': function() {
13880				if (key.val().length) {
13881					confirmationDialog('Creating new IFTTT / Webhook key.<br/>Are you sure?', 'Create new IFTTT / Webhook key?')
13882					.then(createKey)
13883				}
13884				else {
13885					createKey();
13886				}
13887			},
13888			'Close': function() {
13889				$(this).dialog('close');
13890			}
13891		},
13892		open: function(event, ui) {
13893			dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
13894		}
13895	};
13896
13897	// Initializes the dialog
13898	dialog = $('#ifttt_in_nodes_key_dialog').dialog(isyOptions);
13899
13900	form = dialog.find('form');
13901
13902	return { modal: dialog, setFields: setFields };
13903};
13904
13905// Hint edit
13906var hintEditorDialogInit = function () {
13907	var dialog,
13908		form,
13909		pk,
13910		uuid,
13911		domain,
13912		lookup,
13913		device = $('#hint_address'),
13914		hint = $('#hint_value'),
13915		allFields = $([]).add(device).add(hint),
13916		tips = $('#hint_edit_dialog .validateTips');
13917
13918	function isNsNode(address) {
13919		return /^n\d\d\d_/.test(address);
13920	}
13921
13922	function setFields(obj) {
13923		pk = obj.pk;
13924		uuid = obj.uuid;
13925		domain = obj.domain;
13926		lookup = obj.lookup;
13927
13928		// Initial hint value will come from the device selection
13929		hint.val('');
13930
13931		// set combobox
13932		device.combobox("option", "source", lookup);
13933		device.combobox("option", "data", ''); // Empty the box
13934
13935		tips.text('Please choose a device to set his hint.');
13936
13937		// device.focus();
13938		allFields.removeClass('fieldfocus');
13939	}
13940
13941	function submit() {
13942		var valid = true;
13943
13944		device.combobox("refresh"); // Makes sure the real input field is up to date
13945		if (valid && (!device.val().length || !device.combobox("validate"))) {
13946			updateTips(tips, 'Device is not valid');
13947			valid=false;
13948		}
13949
13950		// Hint can be in the n.n.n.n or 0x00000000 formats
13951		if (valid && !/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hint.val()) && !/^0x[0-9a-f]{8}$/.test(hint.val())) {
13952			updateTips(tips, 'Hint format is not valid.');
13953			valid=false;
13954		}
13955
13956		var devData = findInArray(lookup, 'address', device.val());
13957		if (valid && !devData) {
13958			updateTips(tips, 'Device is not valid'); // Should not happen
13959			valid=false;
13960		}
13961
13962		// node server node's type & hint are the same. But for those, we prefer to use the type for backward compatibility
13963		var currentHint = isNsNode(device.val()) ? devData.type : (devData.hint || '0.0.0.0')
13964
13965		// Compare the form hint field with the current hint
13966		if (valid && currentHint === hint.val()) {
13967			updateTips(tips, 'This device hint is already set to ' + hint.val());
13968			valid=false;
13969		}
13970
13971		allFields.removeClass('fieldfocus');
13972
13973		if (valid) {
13974			confirmationDialog('Your device hint will be changed from ' + currentHint + ' to ' + hint.val() +
13975				'.<br/><br/>Please take note of your existing hint in case you need to change it back to its original value.<br/><br/>Are you sure you want to proceed?', 'Change Hint?', 700)
13976			.then(function () {
13977				var data = form.find('input').serialize();
13978				data += '&id=' + pk + '&domain=' + domain + '&oldhint=' + currentHint;  // oldhint is only used for logging
13979
13980				ajaxJsonPost('/api/isy/hint/set', data)
13981				.then(function(data, status, jqXHR) {
13982					confirmationDialog('Your device hint was changed from ' + currentHint + ' to ' + hint.val() + ' successfully.', 'Hint updated', 800)
13983					.then(function() {
13984						dialog.dialog('close');
13985					})
13986					.done();
13987				})
13988				.fail(function(jqXHR, textStatus, errorThrown) {
13989					if (jqXHR.status==503) {
13990						updateTips(tips, 'ISY is not online');
13991					} else if (jqXHR.status==425) { // Validation error message
13992						updateTips(tips, jqXHR.responseText);
13993					}	else {
13994						updateTips(tips, 'HTTP error ' + jqXHR.status + ' ' + jqXHR.responseText);
13995					}
13996				})
13997				.done();
13998			})
13999			.done();
14000
14001		}
14002	}
14003
14004	function help() {
14005		informationMessageDialog(
14006			'<div class="online-help"><p>The Hint Editor allows you to update the "hint" of a nodeserver, ' +
14007			'which helps ISY Portal and other REST API based softwares identify the features of a node. ' +
14008			'This is for example what helps ISY Portal know if a device is a lock, a thermostat, a regular light, etc.</p>' +
14009			'<p>Some Nodeservers sets the hint correctly, whereas others might leave the hint to a default value of 0.0.0.0. ' +
14010			'This is where the Hint Editor comes into play.</p>' +
14011			'<p>You can specify the hint using the decimal format (0.0.0.0) or the hex format (0x00000000).</p>' +
14012			'<p>Here\'s a reference to the <a href="https://github.com/UniversalDevicesInc/hints" target="_blank">Hints Specifications</a>.</p>' +
14013			'<p>Please be aware that if the node is deleted and re-created, the node will lose its hint. ' +
14014			'Whenever possible, it\'s better to have the nodeserver developer update the nodeserver code to automatically set the hint when the node is created.</p>',
14015			'Hint Editor Help', 820)
14016	}
14017
14018	var isyOptions = {
14019		autoOpen: false,
14020		width: 750,
14021		modal: true,
14022		buttons: {
14023			'Help': function() {
14024				help();
14025			},
14026			'Change Hint': function() {
14027				submit();
14028			},
14029			Cancel: function() {
14030				$(this).dialog('close');
14031			}
14032		},
14033		open: function(event, ui) {
14034			dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
14035		}
14036	};
14037
14038	// Initializes the dialog
14039	dialog = $('#hint_edit_dialog').dialog(isyOptions);
14040// Tooltip removed - Causing issues as the scope is too wide
14041//	$('[title]').tooltip();
14042
14043	// Handles keyboad enter (submit event)
14044	form = dialog.find('form').submit(function(event) {
14045		event.preventDefault();
14046		submit();
14047	});
14048	device.combobox({
14049		inputClass: '',
14050	});
14051
14052	device.combobox({
14053		// When pressing tab to go to the hint field
14054		focusout: function(event) {
14055			device.combobox('refresh');
14056
14057			// Set the hint field to the existing hint
14058			var devData = findInArray(lookup, 'address', device.val());
14059			if (devData) {
14060				// If this is a node server node, use the type field as default. We do this for backward compatibility (hint field exists only since 5.7.
140601)
14061				hint.val(isNsNode(devData.address) ? devData.type : (devData.hint || '0.0.0.0'));
14062			}
14063		},
14064		inputClass: ''
14065	});
14066
14067	return { modal: dialog, setFields: setFields };
14068};
14069
14070
14071// ISY Portal Activity log dialog
14072var activityLogDialogInit = function () {
14073	var dialog,
14074		uuid,
14075		domain;
14076
14077	function setFields(obj) {
14078		$('#activitylog_uuid').text(obj.uuid);
14079		$('#activitylog_alias').text(obj.alias);
14080
14081		uuid = obj.uuid;
14082		domain = obj.domain;
14083		tableActivityLog.setUuid(uuid, domain, true);
14084	}
14085
14086	var isyOptions = {
14087		autoOpen: false,
14088		width: 1200,
14089		modal: true,
14090		buttons: {
14091			'Close': function() {
14092				$(this).dialog('close');
14093			}
14094		},
14095		open: function(event, ui) {
14096			dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
14097		}
14098	};
14099
14100	// Initializes the dialog
14101	dialog = $('#activitylog_dialog').dialog(isyOptions);
14102
14103	$('#activitylog_refresh').button({ icons: {primary: 'ui-icon-custom-refresh'}, text: false}).click(function(event) {
14104		$(this).blur(); // Remove focus from button
14105		tableActivityLog.reload();
14106	});
14107
14108	return { modal: dialog, setFields: setFields };
14109};
14110
14111// ISY Portal Access License Renewal Option edit dialog
14112var ipalRenewalOptionEditDialogInit = function () {
14113	var dialog,
14114		form,
14115		pk,
14116		editMode,
14117		part = $('#ipal_renewal_option_part'),
14118		isRenewal = $('#ipal_renewal_option_isrenewal'),
14119		isLegacy = $('#ipal_renewal_option_islegacy'), // For 994s
14120		description = $('#ipal_renewal_option_description'),
14121		legacyText = $('#ipal_renewal_option_legacytext'),
14122		duration = $('#ipal_renewal_option_duration'),
14123		price = $('#ipal_renewal_option_price'),
14124		allFields = $([]).add(isRenewal).add(isLegacy).add(description).add(legacyText).add(duration).add(price),
14125		tips = $('#ipal_renewal_option_edit_dialog .validateTips');
14126
14127	function setFields(obj) {
14128		var label;
14129
14130		form[0].reset();
14131		editMode = obj && obj.editMode;
14132
14133		if (editMode) {
14134			pk = obj.pk;
14135			isRenewal.prop('checked', obj.isRenewal).prop('disabled', true);
14136			isLegacy.prop('checked', obj.isLegacy).prop('disabled', true);
14137			part.val(obj.part).prop('readonly', true);
14138			duration.val(obj.duration).prop('readonly', true);
14139			price.val(obj.price).prop('readonly', false);
14140			description.val(obj.description);
14141			legacyText.val(obj.legacyText);
14142		}
14143		else {
14144			pk=null;
14145			isRenewal.prop('disabled', false);
14146			isLegacy.prop('disabled', false);
14147			part.prop('readonly', false);
14148			duration.prop('readonly', false);
14149			price.prop('readonly', false);
14150		}
14151
14152		tips.text('Please enter the renewal option');
14153
14154		allFields.removeClass('fieldfocus');
14155	}
14156
14157	function submit() {
14158		var valid = true;
14159
14160		allFields.removeClass('fieldfocus');
14161
14162		valid = valid && checkLength(tips, description, 'Description', 2, 60);
14163		valid = valid && checkNumber(tips, duration, 'Duration', 0, 100);
14164		valid = valid && checkFloat(tips, price, 'Price', 0, 1000000);
14165
14166		if (valid) {
14167			var data = serializeFormWithCheckboxes(form.find('input, textarea'));
14168
14169			if (editMode) {
14170				data += '&id=' + pk;
14171			}
14172
14173			var url = '/api/ipal/renewal/option';
14174
14175			(editMode?ajaxJsonPost(url, data):ajaxJsonPut(url, data))
14176			.then(function(ipalOptionRecord, status, jqXHR) {
14177				tableIpalRenewalOptions.reload();
14178				dialog.dialog('close');
14179			})
14180			.fail(function(jqXHR, textStatus, errorThrown) {
14181				if (jqXHR.status==420) {
14182					updateTips(tips, 'Part number already exists.');
14183				}
14184				else {
14185					updateTips(tips, 'HTTP error ' + jqXHR.status);
14186				}
14187			})
14188			.done();
14189		}
14190	}
14191
14192	var isyOptions = {
14193		autoOpen: false,
14194		width: 600,
14195		modal: true,
14196		buttons: {
14197			'Save': function() {
14198				submit();
14199			}
14200		},
14201		open: function(event, ui) {
14202			dialog.dialog( "option", "position", { my: "center", at: "center+10", of: window });
14203		}
14204	};
14205
14206	// Initializes the dialog
14207	dialog = $('#ipal_renewal_option_edit_dialog').dialog(isyOptions);
14208// Tooltip removed - Causing issues as the scope is too wide
14209//	$('[title]').tooltip();
14210
14211	// Handles keyboad enter (submit event)
14212	form = dialog.find('form').submit(function(event) {
14213		event.preventDefault();
14214		submit();
14215	});
14216
14217	return { modal: dialog, setFields: setFields };
14218};
14219
14220// ISY Portal Access License Notification Option edit dialog
14221var ipalNotificationOptionEditDialogInit = function () {
14222	var dialog,
14223		form,
14224		pk,
14225		editMode,
14226		name = $('#ipal_notification_option_name'),
14227		days = $('#ipal_notification_option_days'),
14228		skip = $('#ipal_notification_option_skip'),
14229		notification = $('#ipal_notification_option_notification'),
14230		pushContent = $('#ipal_notification_option_pushContent'),
14231		emailTitle = $('#ipal_notification_option_emailTitle'),
14232		emailBody = $('#ipal_notification_option_emailBody'),
14233		// sendTest = $('#ipal_notification_option_sendtest'),
14234		allFields = $([]).add(name).add(days).add(skip).add(notification).add(pushContent),
14235		tips = $('#ipal_notification_option_edit_dialog .validateTips');
14236
14237	function setFields(obj) {
14238		var label
14239		var typeSelect = $('#ipal_notification_option_type select')
14240
14241		loadTinymce()
14242
14243		form[0].reset();
14244		// sendTest.prop('checked', false);
14245		editMode = obj && obj.editMode;
14246
14247		$('#ipal_notification_option_tabs').tabs('option', 'active', 0);
14248
14249		if (editMode) {
14250			pk = obj.pk;
14251			name.val(obj.name);
14252			days.val(obj.days);
14253			skip.prop('checked', obj.skip);
14254			notification.val(obj.notification);
14255			pushContent.val(obj.pushContent);
14256			emailTitle.val(obj.emailTitle);
14257			emailBody.val(obj.emailBody);
14258			typeSelect.val(obj.type);
14259
14260			displayNotificationAndPushContent(!obj.skip)
14261		}
14262		else {
14263			pk=null;
14264			typeSelect.val(0);
14265			displayNotificationAndPushContent(true)
14266		}
14267
14268		typeSelect.selectmenu('refresh');
14269
14270		tips.text('Please enter the notification option');
14271
14272		allFields.removeClass('fieldfocus');
14273	}
14274
14275	function displayNotificationAndPushContent(display) {
14276		// Disable the online hours field if filterOnline is set to no filter
14277		$('#ipal_notification_option_tabs').toggle(display)
14278	}
14279
14280
14281	function submit(sendTestType) {
14282		var valid = true;
14283
14284		allFields.removeClass('fieldfocus');
14285
14286		valid = valid && checkLength(tips, name, 'Name', 1, 40);
14287		valid = valid && checkNumberOrDelta(tips, days, 'Days', -90, 90);
14288
14289		if (!skip.is(":checked")) {
14290			valid = valid && checkLength(tips, notification, 'Push notification', 1, 100);
14291			valid = valid && checkLength(tips, pushContent, 'Notification content', 1, 4096);
14292			valid = valid && checkLength(tips, emailTitle, 'Email title', 1, 100);
14293			valid = valid && checkLength(tips, emailBody, 'Email body', 1, 4096);
14294		}
14295
14296		if (valid) {
14297			var data = serializeFormWithCheckboxes(form.find('input, textarea'));
14298
14299			data += '&type=' + $('#ipal_notification_option_type select').val();
14300
14301			if (editMode) {
14302				data += '&id=' + pk;
14303			}
14304
14305			var url = '/api/ipal/notification/option';
14306
14307			(editMode?ajaxJsonPost(url, data):ajaxJsonPut(url, data))
14308			.then(function(ipalOptionRecord, status, jqXHR) {
14309				tableIpalNotificationOptions.reload();
14310
14311				dialog.dialog('close');
14312
14313				if (sendTestType) {
14314					var url = '/api/ipal/notification/option/test/' + sendTestType
14315
14316					if (!skip.is(":checked")) {
14317						ajaxJsonGet(url, $.param({id: pk}))
14318					} else {
14319						errorMessageDialog('Test can\'t be sent for this notification option.')
14320					}
14321				}
14322			})
14323			.fail(function(jqXHR, textStatus, errorThrown) {
14324				if (jqXHR.status==420) { // Dupkey error with Custom server error message
14325					var data = JSON.parse(jqXHR.responseText);
14326
14327					if (data.message) {
14328						updateTips(tips, data.message);
14329					}
14330					else {
14331						updateTips(tips, 'HTTP error ' + jqXHR.status);
14332					}
14333				}
14334				else {
14335					updateTips(tips, 'HTTP error ' + jqXHR.status);
14336				}
14337			})
14338			.done();
14339		}
14340	}
14341
14342	function onlineHelp() {
14343		informationMessageDialog(
14344			'<div class="online-help"><p>Tips for creating notification message:<br/>' +
14345			'You can embed the following variables:<br/>' +
14346			'&nbsp;&nbsp;&nbsp;&#9679;&nbsp;{expiryDate} The expiry date in the format YYYY-MM-DD.<br/>' +
14347			'&nbsp;&nbsp;&nbsp;&#9679;&nbsp;{renewalUrl} The link to renew. Example use: &lt;a href="{renewal-url}"&gtClick here&lt;/a&gt.<br/>' +
14348			'&nbsp;&nbsp;&nbsp;&#9679;&nbsp;{days} The number of days before or after (absolute value).<br/>' +
14349			'&nbsp;&nbsp;&nbsp;&#9679;&nbsp;{uuid} The UUID of the ISY.<br/>' +
14350			'&nbsp;&nbsp;&nbsp;&#9679;&nbsp;{contact} The ISY contact, or account contact. (If empty, defaults to "Customer").',
14351			'Notification Online Help', 850)
14352	}
14353
14354	var isyOptions = {
14355		autoOpen: false,
14356		width: 1000,
14357		modal: true,
14358		buttons: {
14359			'Help': function() {
14360				onlineHelp();
14361			},
14362			'Send test push': function() {
14363				submit('push');
14364			},
14365			'Send test email': function() {
14366				submit('email');
14367			},
14368			'Save': function() {
14369				submit();
14370			}
14371		},
14372		open: function(event, ui) {
14373			dialog.dialog( "option", "position", { my: "center", at: "center+10", of: window });
14374		}
14375	};
14376
14377	// Initializes the dialog
14378	dialog = $('#ipal_notification_option_edit_dialog').dialog(isyOptions);
14379// Tooltip removed - Causing issues as the scope is too wide
14380//	$('#ipal_notification_option_istrial_tip').tooltip();
14381
14382	var selectOptions = [
14383		{ value: 0, description: 'Trial' },
14384		{ value: 1, description: 'Regular license' }
14385		// { value: 2, description: 'Renewal' }
14386	];
14387
14388	buildSelect(selectOptions, '#ipal_notification_option_type', '15em', function(event, ui) {
14389//		displayPaymentOptions(ui.item.value)
14390	});
14391
14392	$('#ipal_notification_option_tabs').tabs()
14393
14394	skip.change(function(){
14395		displayNotificationAndPushContent(!$(this).is(':checked'))
14396	})
14397
14398	// Handles keyboad enter (submit event)
14399	form = dialog.find('form').submit(function(event) {
14400		event.preventDefault();
14401		submit();
14402	});
14403
14404	return { modal: dialog, setFields: setFields };
14405};
14406
14407
14408// OpenADR Rate Plan edit dialog
14409var ratePlanEditDialogInit = function () {
14410	var dialog,
14411		form,
14412		pk,
14413		editMode,
14414		ratePlanTabForm = '#tabs-rateplan-rates-form',
14415		code = $('#rateplan_code'),
14416		name = $('#rateplan_name'),
14417		altCodes = $('#rateplan_altcodes'),
14418		gridx = $('#rateplan_gridx'), // PG&E Gridx code
14419		description = $('#rateplan_description'),
14420		details = $('#rateplan_details'),
14421		active = $('#rateplan_active'),
14422		isTiered = $('#rateplan_isTiered'),
14423		sort = $('#rateplan_sort'),
14424		allFields = $([]).add(code).add(name).add(sort).add(description).add(details),
14425		tableRefreshFunction = null,
14426		tips = $('#rateplan_edit_dialog .validateTips');
14427
14428	// Used for the selectmenu
14429	var providerOptions = [
14430		{ value: 'SCE', description: 'SCE' },
14431		// { value: 'HECO', description: 'HECO' },
14432		// { value: 'LADWP', description: 'LADWP' },
14433		// { value: 'NVE', description: 'NVE' },
14434		// { value: 'ONCOR', description: 'ONCOR' },
14435		{ value: 'PGE', description: 'PG&E' },
14436		// { value: 'PJM', description: 'PJM' },
14437		// { value: 'SDGE', description: 'SDGE' }
14438	];
14439
14440	var ratesDef = {
14441		PGE: {
14442			program_season_1: {
14443				month: { label: 'Summer Start date - Month', type: 'month' },
14444				day: { label: 'Day', type: 'day' },
14445			},
14446			program_season_1_wd_1: {
14447				time: { label: 'Summer weekday start time', type: 'time' },
14448				rate: { label: 'Rate',	type: 'rate' },
14449				text: { label: 'Comment', type: 'text' },
14450				// highPrice: { label: 'Smart day high price', type: 'checkbox', class:'smartday' },
14451			},
14452			program_season_1_wd_2: {
14453				time: { label: 'Summer weekday start time', type: 'time' },
14454				rate: { label: 'Rate',	type: 'rate' },
14455				text: { label: 'Comment', type: 'text' },
14456				// highPrice: { label: 'Smart day high price', type: 'checkbox', class:'smartday' },
14457			},
14458			program_season_1_wd_3: {
14459				time: { label: 'Summer weekday start time', type: 'time' },
14460				rate: { label: 'Rate',	type: 'rate' },
14461				text: { label: 'Comment', type: 'text' },
14462				// highPrice: { label: 'Smart day high price', type: 'checkbox', class:'smartday' },
14463			},
14464			program_season_1_wd_4: {
14465				time: { label: 'Summer weekday start time', type: 'time' },
14466				rate: { label: 'Rate',	type: 'rate' },
14467				text: { label: 'Comment', type: 'text' },
14468				// highPrice: { label: 'Smart day high price', type: 'checkbox', class:'smartday' },
14469			},
14470			program_season_1_wd_5: {
14471				time: { label: 'Summer weekday start time', type: 'time' },
14472				rate: { label: 'Rate',	type: 'rate' },
14473				text: { label: 'Comment', type: 'text' },
14474				// highPrice: { label: 'Smart day high price', type: 'checkbox', class:'smartday' },
14475			},
14476			program_season_1_we_1: {
14477				time: { label: 'Summer week end start time', type: 'time' },
14478				rate: { label: 'Rate',	type: 'rate' },
14479				text: { label: 'Comment', type: 'text' },
14480				// highPrice: { label: 'Smart day high price', type: 'checkbox', class:'smartday' },
14481			},
14482			program_season_1_we_2: {
14483				time: { label: 'Summer week end start time', type: 'time' },
14484				rate: { label: 'Rate',	type: 'rate' },
14485				text: { label: 'Comment', type: 'text' },
14486				// highPrice: { label: 'Smart day high price', type: 'checkbox', class:'smartday' },
14487			},
14488			program_season_1_we_3: {
14489				time: { label: 'Summer week end start time', type: 'time' },
14490				rate: { label: 'Rate',	type: 'rate' },
14491				text: { label: 'Comment', type: 'text' },
14492				// highPrice: { label: 'Smart day high price', type: 'checkbox', class:'smartday' },
14493			},
14494			program_season_1_we_4: {
14495				time: { label: 'Summer week end start time', type: 'time' },
14496				rate: { label: 'Rate',	type: 'rate' },
14497				text: { label: 'Comment', type: 'text' },
14498				// highPrice: { label: 'Smart day high price', type: 'checkbox', class:'smartday' },
14499			},
14500			program_season_1_holiday: {
14501				checkbox: { label: 'On Holidays use this rate', type: 'checkbox' },
14502				rate: { label: 'Rate',	type: 'rate' },
14503				text: { label: 'Comment', type: 'text' },
14504			},
14505
14506			program_season_2: {
14507				month: { label: 'Winter Start date - Month', type: 'month' },
14508				day: { label: 'Day', type: 'day' },
14509			},
14510			program_season_2_wd_1: {
14511				time: { label: 'Winter weekday start time', type: 'time' },
14512				rate: { label: 'Rate',	type: 'rate' },
14513				text: { label: 'Comment', type: 'text' },
14514			},
14515			program_season_2_wd_2: {
14516				time: { label: 'Winter weekday start time', type: 'time' },
14517				rate: { label: 'Rate',	type: 'rate' },
14518				text: { label: 'Comment', type: 'text' },
14519			},
14520			program_season_2_wd_3: {
14521				time: { label: 'Winter weekday start time', type: 'time' },
14522				rate: { label: 'Rate',	type: 'rate' },
14523				text: { label: 'Comment', type: 'text' },
14524			},
14525			program_season_2_wd_4: {
14526				time: { label: 'Winter weekday start time', type: 'time' },
14527				rate: { label: 'Rate',	type: 'rate' },
14528				text: { label: 'Comment', type: 'text' },
14529			},
14530			program_season_2_wd_5: {
14531				time: { label: 'Winter weekday start time', type: 'time' },
14532				rate: { label: 'Rate',	type: 'rate' },
14533				text: { label: 'Comment', type: 'text' },
14534			},
14535			program_season_2_we_1: {
14536				time: { label: 'Winter week end start time', type: 'time' },
14537				rate: { label: 'Rate',	type: 'rate' },
14538				text: { label: 'Comment', type: 'text' },
14539			},
14540			program_season_2_we_2: {
14541				time: { label: 'Winter week end start time', type: 'time' },
14542				rate: { label: 'Rate',	type: 'rate' },
14543				text: { label: 'Comment', type: 'text' },
14544			},
14545			program_season_2_we_3: {
14546				time: { label: 'Winter week end start time', type: 'time' },
14547				rate: { label: 'Rate',	type: 'rate' },
14548				text: { label: 'Comment', type: 'text' },
14549			},
14550			program_season_2_we_4: {
14551				time: { label: 'Winter week end start time', type: 'time' },
14552				rate: { label: 'Rate',	type: 'rate' },
14553				text: { label: 'Comment', type: 'text' },
14554			},
14555			program_season_2_holiday: {
14556				checkbox: { label: 'On Holidays use this rate', type: 'checkbox' },
14557				rate: { label: 'Rate',	type: 'rate' },
14558				text: { label: 'Comment', type: 'text' },
14559			},
14560			program_holidaywe: {
14561				checkbox: { label: 'Treat Holiday as weed-end', type: 'checkbox' },
14562			},
14563		},
14564		SCE: {
14565			program_season_1: {
14566				month: { label: 'Summer Start date - Month', type: 'month' },
14567				day: { label: 'Day', type: 'day' },
14568			},
14569			program_season_1_wd_1: {
14570				time: { label: 'Summer weekday start time', type: 'time' },
14571				rate: { label: 'Rate',	type: 'rate' },
14572				text: { label: 'Comment', type: 'text' },
14573				// highPrice: { label: 'Smart day high price', type: 'checkbox', class:'smartday' },
14574			},
14575			program_season_1_wd_2: {
14576				time: { label: 'Summer weekday start time', type: 'time' },
14577				rate: { label: 'Rate',	type: 'rate' },
14578				text: { label: 'Comment', type: 'text' },
14579				// highPrice: { label: 'Smart day high price', type: 'checkbox', class:'smartday' },
14580			},
14581			program_season_1_wd_3: {
14582				time: { label: 'Summer weekday start time', type: 'time' },
14583				rate: { label: 'Rate',	type: 'rate' },
14584				text: { label: 'Comment', type: 'text' },
14585				// highPrice: { label: 'Smart day high price', type: 'checkbox', class:'smartday' },
14586			},
14587			program_season_1_wd_4: {
14588				time: { label: 'Summer weekday start time', type: 'time' },
14589				rate: { label: 'Rate',	type: 'rate' },
14590				text: { label: 'Comment', type: 'text' },
14591				// highPrice: { label: 'Smart day high price', type: 'checkbox', class:'smartday' },
14592			},
14593			program_season_1_wd_5: {
14594				time: { label: 'Summer weekday start time', type: 'time' },
14595				rate: { label: 'Rate',	type: 'rate' },
14596				text: { label: 'Comment', type: 'text' },
14597				// highPrice: { label: 'Smart day high price', type: 'checkbox', class:'smartday' },
14598			},
14599			program_season_1_we_1: {
14600				time: { label: 'Summer week end start time', type: 'time' },
14601				rate: { label: 'Rate',	type: 'rate' },
14602				text: { label: 'Comment', type: 'text' },
14603				// highPrice: { label: 'Smart day high price', type: 'checkbox', class:'smartday' },
14604			},
14605			program_season_1_we_2: {
14606				time: { label: 'Summer week end start time', type: 'time' },
14607				rate: { label: 'Rate',	type: 'rate' },
14608				text: { label: 'Comment', type: 'text' },
14609				// highPrice: { label: 'Smart day high price', type: 'checkbox', class:'smartday' },
14610			},
14611			program_season_1_we_3: {
14612				time: { label: 'Summer week end start time', type: 'time' },
14613				rate: { label: 'Rate',	type: 'rate' },
14614				text: { label: 'Comment', type: 'text' },
14615				// highPrice: { label: 'Smart day high price', type: 'checkbox', class:'smartday' },
14616			},
14617			program_season_1_we_4: {
14618				time: { label: 'Summer week end start time', type: 'time' },
14619				rate: { label: 'Rate',	type: 'rate' },
14620				text: { label: 'Comment', type: 'text' },
14621				// highPrice: { label: 'Smart day high price', type: 'checkbox', class:'smartday' },
14622			},
14623			program_season_1_holiday: {
14624				checkbox: { label: 'On Holidays use this rate', type: 'checkbox' },
14625				rate: { label: 'Rate',	type: 'rate' },
14626				text: { label: 'Comment', type: 'text' },
14627			},
14628
14629			program_season_2: {
14630				month: { label: 'Winter Start date - Month', type: 'month' },
14631				day: { label: 'Day', type: 'day' },
14632			},
14633			program_season_2_wd_1: {
14634				time: { label: 'Winter weekday start time', type: 'time' },
14635				rate: { label: 'Rate',	type: 'rate' },
14636				text: { label: 'Comment', type: 'text' },
14637			},
14638			program_season_2_wd_2: {
14639				time: { label: 'Winter weekday start time', type: 'time' },
14640				rate: { label: 'Rate',	type: 'rate' },
14641				text: { label: 'Comment', type: 'text' },
14642			},
14643			program_season_2_wd_3: {
14644				time: { label: 'Winter weekday start time', type: 'time' },
14645				rate: { label: 'Rate',	type: 'rate' },
14646				text: { label: 'Comment', type: 'text' },
14647			},
14648			program_season_2_wd_4: {
14649				time: { label: 'Winter weekday start time', type: 'time' },
14650				rate: { label: 'Rate',	type: 'rate' },
14651				text: { label: 'Comment', type: 'text' },
14652			},
14653			program_season_2_wd_5: {
14654				time: { label: 'Winter weekday start time', type: 'time' },
14655				rate: { label: 'Rate',	type: 'rate' },
14656				text: { label: 'Comment', type: 'text' },
14657			},
14658			program_season_2_we_1: {
14659				time: { label: 'Winter week end start time', type: 'time' },
14660				rate: { label: 'Rate',	type: 'rate' },
14661				text: { label: 'Comment', type: 'text' },
14662			},
14663			program_season_2_we_2: {
14664				time: { label: 'Winter week end start time', type: 'time' },
14665				rate: { label: 'Rate',	type: 'rate' },
14666				text: { label: 'Comment', type: 'text' },
14667			},
14668			program_season_2_we_3: {
14669				time: { label: 'Winter week end start time', type: 'time' },
14670				rate: { label: 'Rate',	type: 'rate' },
14671				text: { label: 'Comment', type: 'text' },
14672			},
14673			program_season_2_we_4: {
14674				time: { label: 'Winter week end start time', type: 'time' },
14675				rate: { label: 'Rate',	type: 'rate' },
14676				text: { label: 'Comment', type: 'text' },
14677			},
14678			program_season_2_holiday: {
14679				checkbox: { label: 'On Holidays use this rate', type: 'checkbox' },
14680				rate: { label: 'Rate',	type: 'rate' },
14681				text: { label: 'Comment', type: 'text' },
14682			},
14683			program_holidaywe: {
14684				checkbox: { label: 'Treat Holiday as weed-end', type: 'checkbox' },
14685			},
14686		},
14687	}
14688
14689	// function showHideSmartDayCheckboxes() {
14690	// 	// If Rate plan ends with _SD, enable the smart day high price checkboxes
14691	// 	var name = $('#rateplan_code').val()
14692	// 	$('#rateplan_edit_dialog .smartday').toggle(/.*_SD/.test(name))
14693	// }
14694
14695	function setFields(obj) {
14696		var label;
14697
14698		form[0].reset();
14699		editMode = obj && obj.editMode;
14700
14701		tableRefreshFunction = obj.refreshFunction || null;
14702
14703		$('#rateplan_provider_show').toggle(!obj.hideProvider)
14704
14705		if (editMode) {
14706			pk = obj.pk;
14707			code.val(obj.ratePlan);
14708			name.val(obj.name);
14709			altCodes.val(obj.altCodes);
14710			gridx.val(obj.gridx);
14711			description.val(obj.description);
14712			details.val(obj.details);
14713			sort.val(obj.sort);
14714			active.prop('checked', obj.active);
14715			isTiered.prop('checked', obj.isTiered);
14716			buildRatesForm(obj.provider)
14717
14718			$('#rateplan_provider select').val(obj.provider);
14719			$('#rateplan_provider select').selectmenu('refresh');
14720
14721			// showHideSmartDayCheckboxes()
14722
14723			if (obj.programs) {
14724				Object.keys(obj.programs).forEach(function(field) {
14725					if (['checkbox', 'highPrice'].includes(field.split('_').pop())) {
14726						$('#rate_' + field).prop('checked', obj.programs[field]);
14727					} else {
14728						$('#rate_' + field).val(obj.programs[field]);
14729					}
14730				})
14731			}
14732		}
14733		else {
14734			pk=null
14735
14736			// provider may be specified when adding
14737			var provider = obj.provider ? obj.provider : providerOptions[0].value
14738			$('#rateplan_provider select').val(provider);
14739			$('#rateplan_provider select').selectmenu('refresh');
14740
14741			active.prop('checked', false);
14742			isTiered.prop('checked', false);
14743			buildRatesForm(provider)
14744
14745			// When adding, always start with the first tab
14746			$('#rateplan-tabs').tabs('option', 'active', 0);
14747		}
14748
14749		// $('#rateplan_provider select').selectmenu('refresh'); // Needs a refresh to show
14750
14751		tips.text('Please enter a rate plan');
14752
14753		allFields.removeClass('fieldfocus');
14754	}
14755
14756	function genField(field, fieldOpt) {
14757		var html;
14758		switch (fieldOpt.type) {
14759			case 'text':
14760				html = '<td class="col1"><label htmlFor="rate_' + field +'">' + fieldOpt.label + ':</label></td>' +
14761					'<td><input type="text" name="' + field + '" id="rate_' + field + '" value="" class="text-thin ui-widget-content ui-corner-all"  style="width:130px"></td>'
14762				break;
14763
14764			case 'rate':
14765				html = '<td class="col1"><label htmlFor="rate_' + field +'">' + fieldOpt.label + ':</label></td>' +
14766				'<td><input type="number" step="0.01" name="' + field + '" id="rate_' + field + '" value="" class="text-thin ui-widget-content ui-corner-all"  style="width:130px"></td>'
14767				break;
14768
14769			case 'month':
14770				html = '<td class="col1"><label htmlFor="rate_' + field +'">' + fieldOpt.label + ':</label></td>' +
14771					'<td><input type="number" min="1" max="12" step="1" name="' + field + '" id="rate_' + field + '" value="" class="text-thin ui-widget-content ui-corner-all" style="width:50px"></td>'
14772				break;
14773
14774			case 'day':
14775				html = '<td class="col1"><label htmlFor="rate_' + field +'">' + fieldOpt.label + ':</label></td>' +
14776					'<td><input type="number" min="1" max="31" step="1" name="' + field + '" id="rate_' + field + '" value="" class="text-thin ui-widget-content ui-corner-all" style="width:50px"></td>'
14777				break;
14778
14779			case 'time':
14780				html = '<td class="col1"><label htmlFor="rate_' + field +'">' + fieldOpt.label + ':</label></td>' +
14781					'<td><input type="text" name="' + field + '" id="rate_' + field + '" value="" class="text-thin time-field ui-widget-content ui-corner-all"></td>'
14782				break;
14783
14784			case 'checkbox':
14785				html = '<td' + (fieldOpt.class ? ' class="' + fieldOpt.class+ '"': '') + '><div><input type="checkbox" name="' + field + '" value="true"  id="rate_' + field + '"><label>' + fieldOpt.label + '</label></div></td><td/>'
14786				break;
14787
14788			default:
14789				html = ''
14790			}
14791
14792		return html
14793	}
14794
14795	// Build a dynamic jQuery Rate Plan Rates form
14796	function buildRatesForm(provider) {
14797		// Rates first tab - Show only sections specific to provider
14798		$('#tabs-rateplan-main .provider-filter').hide()
14799		$('#tabs-rateplan-main .provider-filter.' + provider).show();
14800
14801		// Rates details
14802		var $ratesTable = $('<table></table>');
14803
14804		var options = ratesDef[provider]
14805		if (options) {
14806			Object.keys(options).forEach(function(field) {
14807				var row = '<tr>'
14808				var fieldOpt = options[field];
14809
14810				// This is a standard field
14811				if (fieldOpt.label && fieldOpt.type) {
14812					row += genField(field, fieldOpt)
14813				} else {
14814					// this is for multiple columns
14815					Object.keys(fieldOpt).forEach(function(subField) {
14816						row += genField(field + '_' + subField, options[field][subField])
14817					})
14818				}
14819
14820				row += '</tr>'
14821				$ratesTable.append(row);
14822			});
14823		}
14824
14825		$(ratePlanTabForm).empty().append($ratesTable.clone());
14826
14827		$(ratePlanTabForm + ' .time-field').timepicker({
14828			timeFormat: 'H:i',  // :s
14829			step: 60,
14830			forceRoundTime: true,
14831			roundingFunction: function(sec) {
14832				return (sec/60)*60;
14833			},
14834			noneOption: [{
14835				label: 'None',
14836				value: ''
14837			}]
14838		})
14839	}
14840
14841
14842	function submit() {
14843		var valid = true;
14844
14845		allFields.removeClass('fieldfocus');
14846
14847		valid = valid && checkLength(tips, code, 'Rate plan', 0, 256); // 0 length = error message!
14848		valid = valid && checkLength(tips, name, 'Name', 1, 60);
14849
14850		if (active.prop('checked')) {
14851			valid = valid && checkNumber(tips, sort, 'Sort order', 0, 100);
14852		}
14853
14854		// If not an error message
14855		if (code.val().length) {
14856			valid = valid && checkLength(tips, description, 'Description', 1, 10000);
14857			valid = valid && checkLength(tips, details, 'Details', 1, 10000);
14858		}
14859
14860		if (valid) {
14861			var data = serializeFormWithCheckboxes(form.find('input, textarea'));
14862
14863			data += '&provider=' + $('#rateplan_provider select').val();
14864
14865			if (editMode) {
14866				data += '&id=' + pk;
14867			}
14868
14869			var url = '/api/rateplan';
14870
14871			(editMode?ajaxJsonPost(url, data):ajaxJsonPut(url, data))
14872			.then(function(ratePlanRecord, status, jqXHR) {
14873				// Must be supplied by setfield, whether we are editing or adding a rate plan
14874				if (tableRefreshFunction) {
14875					tableRefreshFunction()
14876				}
14877				dialog.dialog('close');
14878			})
14879			.fail(function(jqXHR, textStatus, errorThrown) {
14880				if (jqXHR.status==420) { // Dupkey error with Custom server error message
14881					updateTips(tips, 'Record already exists');
14882					// var data = JSON.parse(jqXHR.responseText);
14883					//
14884					// if (data.message) {
14885					// 	updateTips(tips, data.message);
14886					// }
14887					// else {
14888					// 	updateTips(tips, 'HTTP error ' + jqXHR.status);
14889					// }
14890				}
14891				else {
14892					updateTips(tips, 'HTTP error ' + jqXHR.status);
14893				}
14894			})
14895			.done();
14896		}
14897	}
14898
14899	var isyOptions = {
14900		autoOpen: false,
14901		width: 1000,
14902		modal: true,
14903		buttons: {
14904			'Save': function() {
14905				submit();
14906			}
14907		},
14908		open: function(event, ui) {
14909			dialog.dialog( "option", "position", { my: "center", at: "center+10", of: window });
14910		}
14911	};
14912
14913	// Initializes the dialog
14914	dialog = $('#rateplan_edit_dialog').dialog(isyOptions);
14915// Tooltip removed - Causing issues as the scope is too wide
14916//	$('#ipal_notification_option_istrial_tip').tooltip();
14917
14918	buildSelect(providerOptions, '#rateplan_provider', '10em', function(event, ui) {
14919		buildRatesForm(ui.item.value)
14920//		displayPaymentOptions(ui.item.value)
14921	});
14922
14923	$('#rateplan-tabs').tabs()
14924
14925	// Add/remove checkboxes when name ends with _SD or not
14926	// code.focusout(function () {
14927	// 	showHideSmartDayCheckboxes()
14928	// })
14929
14930	// Handles keyboad enter (submit event)
14931	form = dialog.find('form').submit(function(event) {
14932		event.preventDefault();
14933		submit();
14934	});
14935
14936	return { modal: dialog, setFields: setFields };
14937};
14938
14939
14940
14941
14942// Node Server Configuration Dialog
14943var nsConfigDialogInit = function () {
14944	var dialog;
14945	var minimumVersion = '5.0.8';
14946	var uuid;
14947	var profileNumber;
14948	var profileVersion;
14949	var pk;
14950	var isyDomain;
14951
14952	function setFields(obj) {
14953		// Not there during a refresh
14954		if (obj.uuid) {
14955			uuid = obj.uuid;
14956			$('#nsconfig_uuid').text(obj.uuid);
14957		}
14958
14959		// Not there during a refresh
14960		if (obj.alias) {
14961			$('#nsconfig_alias').text(obj.alias);
14962		}
14963
14964		if (obj.pk) {
14965			pk = obj.pk;
14966		}
14967
14968		if (obj.isyDomain) {
14969			isyDomain = obj.isyDomain;
14970		}
14971
14972		// For refresh only
14973		if (obj.info) {
14974			profileNumber = obj.info.profileNumber;
14975			profileVersion = obj.info.profileVersion;
14976			$('#nsconfig_configured').html(obj.info.profileNumber?'Configured using profile ID #'+obj.info.profileNumber + '<br>User profile is ' + obj.info.user:'Not configured');
14977			$('#nsconfig_installed').text(obj.info.profileVersion?'Version installed is '+obj.info.profileVersion:'Not installed');
14978		}
14979		else {
14980			$('#nsconfig_configured').text('');
14981			$('#nsconfig_installed').text('');
14982		}
14983	}
14984
14985	function refresh(reboot) {
14986		ajaxJsonGet('/api/nsmanage/' + uuid + '/config/status')
14987		.done(function (data) {
14988			setFields({ info: data.data });
14989			// Used after we have uploaded files. We refresh, then reboot.
14990			if (reboot) {
14991				ajaxJsonGet('/api/isy/reboot', $.param({id: pk, domain: isyDomain }))
14992			}
14993		})
14994		.fail(function(jqXHR, textStatus, errorThrown) {
14995			if (jqXHR.status==503) {
14996				errorMessageDialog('ISY is not online')
14997			}
14998			else if (jqXHR.status==550) {  // Custom error message
14999				var data = JSON.parse(jqXHR.responseText);
15000
15001				if (data.message) {
15002					errorMessageDialog(data.message)
15003				}
15004				else {
15005					errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
15006				}
15007			}
15008			else {
15009				errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
15010			}
15011		})
15012		.done();
15013	}
15014
15015	function onlineHelp() {
15016		informationMessageDialog(
15017			'<div class="online-help"><p>In order to configure an ISY Portal Nodes server for your ISY, you need to:<br>' +
15018			'1 - Configure a connection on your ISY.<br>2 - Upload the nodes definitions.<br><br> ' +
15019			'Once this is done you can use nodes server modules like Occupancy.<br><br>' +
15020			'Whenever you change your ISY Portal password, you will need to change it for the node server connection too. ' +
15021			'You can simply click on configure connection again, or manually update it in the admin console.<br><br>' +
15022			'When new ISY Portal Nodes server modules will be released, you will need to refresh the nodes definitions by re-uploading the latest version. ' +
15023			'Please note that the upload of the nodes definitions requires a reboot of the ISY.<br><br>' +
15024			'If you wish to remove all Nodes server configurations from your ISY, including the connection, the nodes definitions, and this nodes server devices, click on Remove.',
15025			'Nodes Server Configuration Online Help', 850)
15026	}
15027
15028	var isyOptions = {
15029		autoOpen: false,
15030		width: 825,
15031		modal: true,
15032
15033		buttons: {
15034			'Configure connection': function() {
15035				$buttonConfigure.blur(); // Remove focus from button
15036				confirmationDialog('This will ' + (profileNumber?'update your existing':'create a new') + ' ISY Node server connection.<br/><br/>Are you sure?', 'Configure node server connection?')
15037				.then(function () {
15038//					ajaxJsonGet('/api/nsmanage/' + uuid + '/configure', $.param({ user: registeredUser.username, password: registeredUser.password }))
15039					ajaxJsonGet('/api/nsmanage/' + uuid + '/configure') // , $.param({ user: registeredUser.username, password: registeredUser.password })
15040					.done(function () {
15041						refresh();
15042						informationMessageDialog('Configuration completed successfully.')
15043						.done();
15044					})
15045					.fail(function(jqXHR, textStatus, errorThrown) {
15046						if (jqXHR.status==503) {
15047							errorMessageDialog('ISY is not online')
15048						}
15049						else if (jqXHR.status==550) {  // Custom error message
15050							var data = JSON.parse(jqXHR.responseText);
15051
15052							if (data.message) {
15053								errorMessageDialog(data.message)
15054							}
15055							else {
15056								errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
15057							}
15058						}
15059						else {
15060							errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
15061						}
15062					})
15063					.done();
15064				})
15065				.done();
15066			},
15067			'Upload nodes definitions': function() {
15068				var self = this;
15069				$buttonInstall.blur(); // Remove focus from button
15070				confirmationDialog('This will upload the latest ISY portal nodes definitions to your ISY. When the upload is completed, your ISY will be rebooted.<br/><br/>Are you sure?', 'Upload nodes definitions?')
15071				.then(function () {
15072					ajaxJsonGet('/api/nsmanage/' + uuid + '/install')
15073					.done(function () {
15074						refresh(true); // Reboot after refresh
15075						informationMessageDialog('Upload completed successfully. ISY is currently rebooting.')
15076						.done(function () {
15077							$(self).dialog('close');
15078							tableIsys.reload();
15079						})
15080						.done();
15081					})
15082					.fail(function(jqXHR, textStatus, errorThrown) {
15083						if (jqXHR.status==503) {
15084							errorMessageDialog('ISY is not online')
15085						}
15086						else if (jqXHR.status==504) {
15087							errorMessageDialog('Timeout occured. Please try again.')
15088						}
15089						else if (jqXHR.status==550) {  // Custom error message
15090							var data = JSON.parse(jqXHR.responseText);
15091
15092							if (data.message) {
15093								errorMessageDialog(data.message)
15094							}
15095							else {
15096								errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
15097							}
15098						}
15099						else {
15100							errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
15101						}
15102					})
15103					.done();
15104				})
15105				.done();
15106			},
15107			'Remove': function() {
15108				$buttonRemove.blur(); // Remove focus from button
15109				confirmationDialog('WARNING: This will remove the connection, the nodes definitions, and also all of your ISY Portal node server nodes.<br/><br/>Are you sure?', 'Remove Node server configuration?')
15110				.then(function () {
15111					ajaxJsonGet('/api/nsmanage/' + uuid + '/removeall')
15112					.done(function () {
15113						refresh(false); // NO Reboot after refresh
15114						informationMessageDialog('Configuration removal completed successfully.')
15115						.done(function () {
15116							tableIsys.reload();
15117						})
15118						.done();
15119					})
15120					.fail(function(jqXHR, textStatus, errorThrown) {
15121						if (jqXHR.status==503) {
15122							errorMessageDialog('ISY is not online')
15123						}
15124						else if (jqXHR.status==504) {
15125							errorMessageDialog('Timeout occured. Please try again.')
15126						}
15127						else if (jqXHR.status==550) {  // Custom error message
15128							var data = JSON.parse(jqXHR.responseText);
15129
15130							if (data.message) {
15131								errorMessageDialog(data.message)
15132							}
15133							else {
15134								errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
15135							}
15136						}
15137						else {
15138							errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
15139						}
15140					})
15141					.done();
15142				})
15143				.done();
15144			},
15145			'Help' : function() {
15146				$buttonHelp.blur(); // Remove focus from button
15147				onlineHelp();
15148			},
15149			'Close': function() {
15150				$(this).dialog('close');
15151			}
15152		},
15153
15154		open: function(event, ui) {
15155			dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
15156		}
15157	};
15158
15159	// Initializes the dialog
15160	dialog = $('#nsconfig_dialog').dialog(isyOptions);
15161	// Tooltip removed - Causing issues as the scope is too wide
15162//	$('[title]').tooltip();
15163
15164	var $buttons = $('#nsconfig_dialog').parent().find('.ui-dialog-buttonset button');
15165	var $buttonConfigure = $buttons.first();
15166	var $buttonInstall = $buttons.eq(1); // Second button
15167	var $buttonRemove = $buttons.eq(2); // Third button
15168	var $buttonHelp = $buttons.eq(3);
15169
15170	return { modal: dialog, setFields: setFields, refresh: refresh };
15171};
15172
15173// Main Occupancu dialog (with the device table)
15174var nsOccupancyDialogInit = function () {
15175	var dialog,
15176		uuid,
15177		domain,
15178		validationPromise,
15179		locationsPromise;
15180
15181	function loadLocations(uuid) {
15182		locationsPromise = ajaxJsonGet('/api/nsmodule/' + uuid + '/occupancy/locations');
15183
15184		// Only display locationsPromise error if validationPromise passed.
15185		validationPromise
15186		.done(function() {
15187
15188			locationsPromise
15189			.fail(function(jqXHR, textStatus, errorThrown) {
15190				if (jqXHR.status==503) {
15191					errorMessageDialog('ISY is not online')
15192				}
15193				else if (jqXHR.status==504) {
15194					errorMessageDialog('Timeout occured. Please try again.')
15195				}
15196				else if (jqXHR.status==550) {  // Custom error message
15197					var data = JSON.parse(jqXHR.responseText);
15198
15199					if (data.message) {
15200						errorMessageDialog(data.message)
15201					}
15202					else {
15203						errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
15204					}
15205				}
15206				else {
15207					errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
15208				}
15209			})
15210			.done();
15211		});
15212
15213		return locationsPromise;
15214	}
15215
15216	function setFields(obj) {
15217		$('#nsoccupancy_uuid').text(obj.uuid);
15218		$('#nsoccupancy_alias').text(obj.alias);
15219
15220		uuid = obj.uuid;
15221		domain = obj.domain;
15222
15223		// Detect if Node server is properly configured, connection and profile files
15224		validationPromise = ajaxJsonGet('/api/nsmanage/' + uuid + '/config/status');
15225
15226		validationPromise
15227		.fail(function(jqXHR, textStatus, errorThrown) {
15228			if (jqXHR.status==503) {
15229				errorMessageDialog('ISY is not online')
15230			}
15231			else if (jqXHR.status==504) {
15232				errorMessageDialog('Timeout occured. Please try again.')
15233			}
15234			else if (jqXHR.status==550) {  // Custom error message
15235				var data = JSON.parse(jqXHR.responseText);
15236
15237				if (data.message) {
15238					errorMessageDialog(data.message)
15239				}
15240				else {
15241					errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
15242				}
15243			}
15244			else {
15245				errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
15246			}
15247		})
15248		.done();
15249
15250		tableNsOccupancy.setUuid(uuid, domain, true);
15251
15252		loadLocations(uuid);
15253		tableNsOccupancy.setLocations(locationsPromise);
15254	}
15255
15256	function onlineHelp() {
15257		informationMessageDialog(
15258			'<div class="online-help"><p>The occupancy module allows your ISY programs to detect if your home is Occupied, and adjust the temperature or any other devices accordingly. This works using geofencing with the help of a mobile app on your household members' + "'" + ' mobile devices.<br><br>' +
15259			'The recommended application is "Locative" and is available on iOS and Android. But any app that can trigger a URL based on location will work.<br><br>' +
15260			'First, you need to "Add a geofence" for each of your mobile devices. This will create a corresponding node on your ISY for each one, along with an optional location node. ' +
15261			'The location node will be true (in) if any of the geofence nodes created in the location is true (in). ' +
15262			'A typical location would be "Home". So, this allows your ISY to have a global home occupancy flag telling you if anyone is home, or not.<br><br>' +
15263			'In order to configure your mobile app, choose a node in the list and click on <span class="linkbutton"><span class="ui-icon ui-icon-extlink"/></span>. This will give you the URL to use to report in or out status for each of your geofences. ' +
15264			'In your mobile app, configure your geofence with this URL, using the GET or POST method.<br><br>' +
15265			'If you accidentally delete a node in the admin console, you can recreate it using the Synchronize button. You can also click Remove to delete all occupancy related nodes.</p>',
15266			'Nodes Server Occupancy module Online Help', 825)
15267	}
15268
15269	var isyOptions = {
15270		autoOpen: false,
15271		width: 800,
15272		modal: true,
15273		buttons: {
15274			'Synchronize': function() {
15275				$buttonSynchronize.blur(); // Remove focus from button
15276
15277				validationPromise.then(function(data) {
15278					if (data.data.profileNumber && data.data.profileVersion) {
15279						confirmationDialog('This will resynchronize your geofences with ISY.<br/><br/>Are you sure?', 'Synchronize?')
15280						.then(function () {
15281							ajaxJsonGet('/api/nsmanage/' + uuid + '/synchronize/occupancy')
15282							.done(function () {
15283								informationMessageDialog('Synchronize completed successfully.')
15284								.done(function () {
15285									tableNsOccupancy.reload();
15286								})
15287								.done();
15288							})
15289							.fail(function(jqXHR, textStatus, errorThrown) {
15290								if (jqXHR.status==503) {
15291									errorMessageDialog('ISY is not online')
15292								}
15293								else if (jqXHR.status==504) {
15294									errorMessageDialog('Timeout occured. Please try again.')
15295								}
15296								else if (jqXHR.status==550) {  // Custom error message
15297									var data = JSON.parse(jqXHR.responseText);
15298
15299									if (data.message) {
15300										errorMessageDialog(data.message)
15301									}
15302									else {
15303										errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
15304									}
15305								}
15306								else {
15307									errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
15308								}
15309							})
15310							.done();
15311						})
15312						.done();
15313					}
15314					else {
15315						errorMessageDialog('Node Server is not configured.', 'Error')
15316					}
15317				})
15318			},
15319			'Remove': function() {
15320				$buttonRemove.blur(); // Remove focus from button
15321
15322				validationPromise.then(function(data) {
15323					if (data.data.profileNumber && data.data.profileVersion) {
15324						confirmationDialog('This will remove all of your geofences from your configuration and ISY.<br/><br/>Are you sure?', 'Remove geofences?')
15325						.then(function () {
15326							ajaxJsonGet('/api/nsmanage/' + uuid + '/remove/occupancy')
15327							.done(function () {
15328								informationMessageDialog('Removal completed successfully.')
15329								.done(function () {
15330									tableNsOccupancy.reload();
15331									loadLocations(uuid); // No need update the tableNsOccupancy promise, there is nothing more to edit
15332								})
15333								.done();
15334							})
15335							.fail(function(jqXHR, textStatus, errorThrown) {
15336								if (jqXHR.status==503) {
15337									errorMessageDialog('ISY is not online')
15338								}
15339								else if (jqXHR.status==504) {
15340									errorMessageDialog('Timeout occured. Please try again.')
15341								}
15342								else if (jqXHR.status==550) {  // Custom error message
15343									var data = JSON.parse(jqXHR.responseText);
15344
15345									if (data.message) {
15346										errorMessageDialog(data.message)
15347									}
15348									else {
15349										errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
15350									}
15351								}
15352								else {
15353									errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
15354								}
15355							})
15356							.done();
15357						})
15358						.done();
15359					}
15360					else {
15361						errorMessageDialog('Node Server is not configured.', 'Error')
15362					}
15363				})
15364			},
15365			'Help' : function() {
15366				$buttonHelp.blur(); // Remove focus from button
15367				onlineHelp();
15368			},
15369			'Close': function() {
15370				$(this).dialog('close');
15371			}
15372		},
15373		open: function(event, ui) {
15374			dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
15375		}
15376	};
15377
15378	// Initializes the dialog
15379	dialog = $('#nsoccupancy_dialog').dialog(isyOptions);
15380	// Tooltip removed - Causing issues as the scope is too wide
15381//	$('[title]').tooltip();
15382
15383	var $buttons = $('#nsoccupancy_dialog').parent().find('.ui-dialog-buttonset button');
15384	var $buttonSynchronize = $buttons.first(); // First button
15385	var $buttonRemove = $buttons.eq(1); // Second button
15386	var $buttonHelp = $buttons.eq(2);
15387
15388	$('#nsoccupancy_add').button().click(function () {
15389		$(this).blur(); // Remove focus from button
15390
15391		validationPromise.then(function(data) {
15392			if (data.data.profileNumber && data.data.profileVersion) {
15393				locationsPromise
15394				.done(function(locations) {
15395					nsOccupancyEditDialog.setFields({ uuid: uuid, domain: domain, editMode:false, locations: locations.data });
15396					nsOccupancyEditDialog.modal.dialog('open');
15397				})
15398				.fail(function(jqXHR, textStatus, errorThrown) {
15399					errorMessageDialog('Error loading locations', 'Error', 350)
15400				})
15401			}
15402			else {
15403				errorMessageDialog('Node Server is not configured.', 'Error')
15404			}
15405		})
15406	});
15407
15408	$('#nsoccupancy_refresh').button({ icons: {primary: 'ui-icon-custom-refresh'}, text: false}).click(function(event) {
15409		$(this).blur(); // Remove focus from button
15410		tableNsOccupancy.reload();
15411	});
15412
15413	return { modal: dialog, setFields: setFields, loadLocations: loadLocations };
15414};
15415
15416// Mobile devices edit dialog
15417var nsOccupancyEditDialogInit = function () {
15418	var dialog,
15419		form,
15420		pk,
15421		uuid,
15422		domain,
15423		editMode,
15424		locations,
15425		name = $('#nsoccupancy_name'),
15426		allFields = $([]).add(name),
15427		tips = $('#nsoccupancy_edit_dialog .validateTips');
15428
15429	function updateSelector(locations, location) {
15430		var $select = $('<select><option value="$nolocation">(No location)</option></select>');
15431
15432		locations.sort(function(a, b) {
15433			return a.name.localeCompare(b.name) }
15434		);
15435
15436		locations.forEach(function(location) {
15437			$select.append('<option value="' + location.address + '">' + location.name + ' (' + location.address + ')</option>');
15438		});
15439
15440		if (!location) {
15441			location = $('#nsoccupancy_locations_selector select').val();
15442		}
15443
15444		$('#nsoccupancy_locations_selector').empty().append($select.clone());
15445
15446		if (location) { // If no location is specified, and selector did not exist, it would be set to null
15447			$('#nsoccupancy_locations_selector select').val(location);
15448		}
15449
15450		$('#nsoccupancy_locations_selector select').selectmenu({
15451			width: '80%'
15452		});
15453	}
15454
15455	function setFields(obj) {
15456		var label;
15457
15458		uuid = obj.uuid;
15459		domain = obj.domain;
15460		pk = obj.pk;
15461
15462		form[0].reset();
15463		name.val(obj.name);
15464
15465		editMode = obj.editMode;
15466
15467		tips.text('Please enter a name for this geofence');
15468
15469		allFields.removeClass('fieldfocus');
15470
15471		if (!editMode) {
15472			obj.location = '$nolocation';
15473		}
15474
15475		updateSelector(obj.locations, obj.location);
15476	}
15477
15478	function submit() {
15479		var valid = true;
15480
15481		allFields.removeClass('fieldfocus');
15482
15483		valid = valid && checkLength(tips, name, 'Name', 2, 40);
15484		valid = valid && checkRegexp(tips, name, nodeNameRegex, 'node name is not valid.' );
15485
15486		if (valid) {
15487			var data = form.find('input').serialize();
15488			data += '&location=' + $('#nsoccupancy_locations_selector select').val();
15489			data += '&domain=' + domain;
15490
15491			if (editMode) {
15492				data += '&id=' + pk;
15493			}
15494
15495			var url = '/api/nsmodule/' + uuid + '/occupancy/node';
15496
15497			(editMode?ajaxJsonPost(url, data):ajaxJsonPut(url, data))
15498			.then(function(data, status, jqXHR) {
15499				dialog.dialog('close');
15500				tableNsOccupancy.reload();
15501			})
15502			.fail(function(jqXHR, textStatus, errorThrown) {
15503				if (jqXHR.status==503) {
15504					updateTips(tips, 'ISY is not online');
15505				}
15506				else if (jqXHR.status==404) { // Can happen if device was just deleted and recreated shortly with same address
15507					updateTips(tips, 'Could not create device on ISY (HTTP 404 returned). Please try again later.');
15508				}
15509				else if (jqXHR.status==420) { // Dupkey error with Custom server error message
15510					var data = JSON.parse(jqXHR.responseText);
15511
15512					if (data.message) {
15513						updateTips(tips, data.message);
15514					}
15515					else {
15516						updateTips(tips, 'HTTP error ' + jqXHR.status);
15517					}
15518				}
15519				else {
15520					updateTips(tips, 'HTTP error ' + jqXHR.status);
15521				}
15522			})
15523			.done();
15524		}
15525	}
15526
15527	var isyOptions = {
15528		autoOpen: false,
15529		width: 500,
15530		modal: true,
15531		buttons: {
15532			'Save': function() {
15533				submit();
15534			}
15535		},
15536		open: function(event, ui) {
15537			dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
15538		}
15539	};
15540
15541	// Initializes the dialog
15542	dialog = $('#nsoccupancy_edit_dialog').dialog(isyOptions);
15543	// Tooltip removed - Causing issues as the scope is too wide
15544//	$('[title]').tooltip();
15545
15546	$('#nsoccupancy_locationadd').button({ icons: {primary: 'ui-icon-plus'}, text: false}).tooltip().click(function(event) {
15547		$(this).blur(); // Remove focus from button
15548			nsOccupancyLocationEditDialog.setFields({ uuid: uuid, editMode: false });
15549			nsOccupancyLocationEditDialog.modal.dialog('open');
15550	});
15551
15552	// Handles keyboad enter (submit event)
15553	form = dialog.find('form').submit(function(event) {
15554		event.preventDefault();
15555		submit();
15556	});
15557
15558	return { modal: dialog, setFields: setFields, updateSelector: updateSelector };
15559};
15560
15561// Occupancy location edit dialog
15562var nsOccupancyLocationEditDialogInit = function () {
15563	var dialog,
15564		form,
15565		pk,
15566		uuid,
15567		domain,
15568		editMode,
15569		name = $('#nsoccupancy_location_name'),
15570		allFields = $([]).add(name),
15571		tips = $('#nsoccupancy_location_edit_dialog .validateTips');
15572
15573	function setFields(obj) {
15574		var label;
15575
15576		uuid = obj.uuid;
15577		domain = obj.domain;
15578		pk = obj.pk;
15579
15580		form[0].reset();
15581		name.val(obj.name);
15582
15583		editMode = obj.editMode;
15584
15585		tips.text('Please enter a location name');
15586
15587		allFields.removeClass('fieldfocus');
15588	}
15589
15590	function submit() {
15591		var valid = true;
15592
15593		allFields.removeClass('fieldfocus');
15594
15595		valid = valid && checkLength(tips, name, 'Name', 2, 40);
15596		valid = valid && checkRegexp(tips, name, nodeNameRegex, 'location name is not valid.' );
15597
15598		if (valid) {
15599			var data = form.find('input').serialize();
15600			data += '&domain=' + domain;
15601
15602			if (editMode) {
15603				data += '&id=' + pk;
15604			}
15605
15606			var url = '/api/nsmodule/' + uuid + '/occupancy/location';
15607
15608			(editMode?ajaxJsonPost(url, data):ajaxJsonPut(url, data))
15609			.then(function(locationRecord, status, jqXHR) {
15610				tableNsOccupancy.reload();
15611
15612				var locationsPromise = nsOccupancyDialog.loadLocations(uuid);
15613				tableNsOccupancy.setLocations(locationsPromise); // Will be available when we edit!
15614
15615				locationsPromise.done(function(data) {
15616					var newLocation=null;
15617
15618					if (locationRecord.data && locationRecord.data.address) {
15619						newLocation = locationRecord.data.address;
15620					}
15621
15622					nsOccupancyEditDialog.updateSelector(data.data, newLocation);
15623					dialog.dialog('close');
15624				})
15625			})
15626			.fail(function(jqXHR, textStatus, errorThrown) {
15627				if (jqXHR.status==503) {
15628					updateTips(tips, 'ISY is not online');
15629				}
15630				else if (jqXHR.status==404) { // Can happen if device was just deleted and recreated shortly with same address
15631					updateTips(tips, 'Could not create location on ISY (HTTP 404 returned). Please try again later.');
15632				}
15633				else if (jqXHR.status==420) { // Dupkey error with Custom server error message
15634					var data = JSON.parse(jqXHR.responseText);
15635
15636					if (data.message) {
15637						updateTips(tips, data.message);
15638					}
15639					else {
15640						updateTips(tips, 'HTTP error ' + jqXHR.status);
15641					}
15642				}
15643				else {
15644					updateTips(tips, 'HTTP error ' + jqXHR.status);
15645				}
15646			})
15647			.done();
15648		}
15649	}
15650
15651	var isyOptions = {
15652		autoOpen: false,
15653		width: 400,
15654		modal: true,
15655		buttons: {
15656			'Save': function() {
15657				submit();
15658			}
15659		},
15660		open: function(event, ui) {
15661			dialog.dialog( "option", "position", { my: "center", at: "center+10", of: window });
15662		}
15663	};
15664
15665	// Initializes the dialog
15666	dialog = $('#nsoccupancy_location_edit_dialog').dialog(isyOptions);
15667	// Tooltip removed - Causing issues as the scope is too wide
15668//	$('[title]').tooltip();
15669
15670	// Handles keyboad enter (submit event)
15671	form = dialog.find('form').submit(function(event) {
15672		event.preventDefault();
15673		submit();
15674	});
15675
15676	return { modal: dialog, setFields: setFields };
15677};
15678
15679var nsOccupancyUrlDialogInit = function() {
15680	var dialog;
15681
15682	function setFields(hostname, key) {
15683		var url = 'https://' + hostname + '/api/location/' + key + '/report/';
15684		$('#report_url_in').text(url+'in');
15685		$('#report_url_out').text(url+'out');
15686	}
15687
15688	var isyOptions = {
15689		autoOpen: false,
15690		modal: true,
15691		width: 875,
15692		buttons: {
15693			'Ok': function() {
15694				$(this).dialog('close');
15695			}
15696		},
15697		close: function() {
15698		}
15699	};
15700	dialog = $('#occupancy_url_dialog').dialog(isyOptions);
15701
15702	$('#occupancy_url_dialog .copy_clip').button().click(function() {
15703		$(this).blur();
15704	});
15705
15706	if (!Clipboard.isSupported()) {
15707		$('#occupancy_url_dialog .copy_clip').hide();
15708	}
15709
15710	var clipboard = new Clipboard('#occupancy_url_dialog .copy_clip');
15711
15712	clipboard.on('success', function(e) {
15713		var target = $(e.trigger).attr('data-clipboard-target');
15714		if($(target).tooltip('option', 'disabled')) {
15715			$(target).tooltip('option', {disabled: false, content: 'Copied'}).tooltip('open')
15716		}
15717		setTimeout(function(){
15718			e.clearSelection();
15719			$(target).tooltip('option', {disabled: true}).tooltip('close')
15720		}, 800);
15721	});
15722
15723	clipboard.on('error', function(e) {
15724		var target = $(e.trigger).attr('data-clipboard-target');
15725		if($(target).tooltip('option', 'disabled')) {
15726			$(target).tooltip('option', {disabled: false, content: 'COPY FAILED'}).tooltip('open')
15727		}
15728		setTimeout(function(){
15729			e.clearSelection();
15730			$(target).tooltip('option', {disabled: true}).tooltip('close')
15731		}, 2000);
15732	});
15733
15734	$(".copied").tooltip({
15735		disabled: true,
15736		position: { my: "center", at: "center", collision: "flipfit" }
15737	})
15738	.hover(function(){
15739		$(this).tooltip('option', {disabled: true}).tooltip('close');
15740	});
15741
15742	return { modal: dialog, setFields: setFields };
15743};
15744
15745// Occupancy2 Main dialog (with Geofences table and Mobile Devices table)
15746var nsOccupancy2DialogInit = function () {
15747    var dialog,
15748        uuid,
15749        domain,
15750        validationPromise,
15751        geofencesPromise,
15752        devicesPromise;
15753
15754    function loadGeofences(uuid) {
15755        geofencesPromise = ajaxJsonGet('/api/nsmodule/' + uuid + '/occupancy2/geofences');
15756
15757        // Only display geofencesPromise error if validationPromise passed.
15758        validationPromise
15759        .done(function() {
15760
15761            geofencesPromise
15762            .fail(function(jqXHR, textStatus, errorThrown) {
15763                if (jqXHR.status==503) {
15764                    errorMessageDialog('ISY is not online')
15765                }
15766                else if (jqXHR.status==504) {
15767                    errorMessageDialog('Timeout occured. Please try again.')
15768                }
15769                else if (jqXHR.status==550) {  // Custom error message
15770                    var data = JSON.parse(jqXHR.responseText);
15771
15772                    if (data.message) {
15773                        errorMessageDialog(data.message)
15774                    }
15775                    else {
15776                        errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
15777                    }
15778                }
15779                else {
15780                    errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
15781                }
15782            })
15783            .done();
15784        });
15785
15786        return geofencesPromise;
15787    }
15788
15789    function loadDevices(uuid) {
15790        devicesPromise = ajaxJsonGet('/api/nsmodule/' + uuid + '/occupancy2/devices');
15791
15792        // Only display devicesPromise error if validationPromise passed.
15793        validationPromise
15794        .done(function() {
15795
15796            devicesPromise
15797            .fail(function(jqXHR, textStatus, errorThrown) {
15798                if (jqXHR.status==503) {
15799                    errorMessageDialog('ISY is not online')
15800                }
15801                else if (jqXHR.status==504) {
15802                    errorMessageDialog('Timeout occured. Please try again.')
15803                }
15804                else if (jqXHR.status==550) {  // Custom error message
15805                    var data = JSON.parse(jqXHR.responseText);
15806
15807                    if (data.message) {
15808                        errorMessageDialog(data.message)
15809                    }
15810                    else {
15811                        errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
15812                    }
15813                }
15814                else {
15815                    errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
15816                }
15817            })
15818            .done();
15819        });
15820
15821        return devicesPromise;
15822    }
15823
15824    function setFields(obj) {
15825        $('#nsoccupancy2_uuid').text(obj.uuid);
15826        $('#nsoccupancy2_alias').text(obj.alias);
15827
15828        uuid = obj.uuid;
15829        domain = obj.domain;
15830
15831        // Detect if Node server is properly configured, connection and profile files
15832        validationPromise = ajaxJsonGet('/api/nsmanage/' + uuid + '/config/status');
15833
15834        validationPromise
15835        .fail(function(jqXHR, textStatus, errorThrown) {
15836            if (jqXHR.status==503) {
15837                errorMessageDialog('ISY is not online')
15838            }
15839            else if (jqXHR.status==504) {
15840                errorMessageDialog('Timeout occured. Please try again.')
15841            }
15842            else if (jqXHR.status==550) {  // Custom error message
15843                var data = JSON.parse(jqXHR.responseText);
15844
15845                if (data.message) {
15846                    errorMessageDialog(data.message)
15847                }
15848                else {
15849                    errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
15850                }
15851            }
15852            else {
15853                errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
15854            }
15855        })
15856        .done();
15857
15858        tableNsOccupancy2Geofence.setUuid(uuid, domain, true);
15859        tableNsOccupancy2Device.setUuid(uuid, domain, true);
15860
15861        loadGeofences(uuid);
15862        loadDevices(uuid);
15863        tableNsOccupancy2Geofence.setGeofences(geofencesPromise);
15864        tableNsOccupancy2Geofence.setDevices(devicesPromise);
15865        tableNsOccupancy2Device.setDevices(devicesPromise);
15866    }
15867
15868    function onlineHelp() {
15869        informationMessageDialog(
15870            '<div class="online-help"><p>The Occupancy module v2.0 allows your ISY programs to detect if your "Geofence" is Occupied, and adjust thermostat or any other device accordingly. This works using Geofencing with the help of a Mobile App on your household members mobile devices.<br><br>' +
15871            'The recommended application is "UDI Mobile" and is available on iOS and Android. But any app that can trigger a URL based on Geofence will work.<br><br>' +
15872            'First, you need to "Add Geofence" for each of your locations you want to monitor Occupancy (ie. "Home"). This will create a corresponding node on your ISY, allowing to have a global "Home" occupancy flag telling you if anyone is at this location, or not.<br><br>' +
15873            'Next, you need to "Add a Mobile Device" for each of your household members mobile devices (ie. "Dad’s iPhone"). Additionally, for "UDI Mobile" you can choose a profile for each mobile device: User or Editor (ie. Dad can edit geofence data but Kids can’t).<br><br>' +
15874            'And then, you need to "Assign Mobile Device" to each "Geofence" in order to monitor Occupancy (ie. "Dad’s iPhone" is at "Home"). This will create a corresponding node on your ISY for each one. The node will be true (in) if the mobile device enters the assigned geofence and will be false (out) if leaves.<br><br>' +
15875            'In order to configure "UDI Mobile" in your mobile device, choose a "Mobile Device" in ISY Portal list and click on <span class="linkbutton"><span class="ui-icon ui-icon-extlink"/></span> in "Linked" column. This will give you a 6-digit Pin that you have to enter the first time you open "UDI Mobile" in order to Link your mobile device with ISY Portal, then the app will obtain all geofences assigned for this mobile device and (if allowed) let you edit geofence data. From this point, the app will report to ISY portal each time mobile device enters o leaves each geofence.<br><br>' +
15876            'In order to configure any other mobile app, choose a node in the list and click on <span class="linkbutton"><span class="ui-icon ui-icon-extlink"/></span>. This will give you the URL to use to report in or out status for each of your geofences. In your mobile app, configure your geofence with this URL, using the GET or POST method.<br><br>' +
15877            'If you accidentally delete a node in the admin console, you can recreate it using the Synchronize button. You can also click Remove to delete all occupancy related nodes (Geofences) and Mobile Devices.</p>',
15878            'Nodes Server Occupancy module v2.0 Online Help', 825)
15879    }
15880
15881    var isyOptions = {
15882        autoOpen: false,
15883        width: 800,
15884        modal: true,
15885        buttons: {
15886            'Synchronize': function() {
15887                $buttonSynchronize.blur(); // Remove focus from button
15888
15889                validationPromise.then(function(data) {
15890                    if (data.data.profileNumber && data.data.profileVersion) {
15891                        confirmationDialog('This will resynchronize your geofences with ISY.<br/><br/>Are you sure?', 'Synchronize?')
15892                        .then(function () {
15893                            ajaxJsonGet('/api/nsmanage/' + uuid + '/synchronize/occ2_geofence')
15894                            .done(function () {
15895                                ajaxJsonGet('/api/nsmanage/' + uuid + '/synchronize/occ2_device')
15896                                .done(function () {
15897                                    informationMessageDialog('Synchronize completed successfully.')
15898                                    .done(function () {
15899                                        tableNsOccupancy2Geofence.reload();
15900                                        tableNsOccupancy2Device.reload();
15901                                    })
15902                                    .done();
15903                                })
15904                                .fail(function(jqXHR, textStatus, errorThrown) {
15905                                    if (jqXHR.status==503) {
15906                                        errorMessageDialog('ISY is not online')
15907                                    }
15908                                    else if (jqXHR.status==504) {
15909                                        errorMessageDialog('Timeout occured. Please try again.')
15910                                    }
15911                                    else if (jqXHR.status==550) {  // Custom error message
15912                                        var data = JSON.parse(jqXHR.responseText);
15913
15914                                        if (data.message) {
15915                                            errorMessageDialog(data.message)
15916                                        }
15917                                        else {
15918                                            errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
15919                                        }
15920                                    }
15921                                    else {
15922                                        errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
15923                                    }
15924                                })
15925                                .done();
15926                            })
15927                            .fail(function(jqXHR, textStatus, errorThrown) {
15928                                if (jqXHR.status==503) {
15929                                    errorMessageDialog('ISY is not online')
15930                                }
15931                                else if (jqXHR.status==504) {
15932                                    errorMessageDialog('Timeout occured. Please try again.')
15933                                }
15934                                else if (jqXHR.status==550) {  // Custom error message
15935                                    var data = JSON.parse(jqXHR.responseText);
15936
15937                                    if (data.message) {
15938                                        errorMessageDialog(data.message)
15939                                    }
15940                                    else {
15941                                        errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
15942                                    }
15943                                }
15944                                else {
15945                                    errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
15946                                }
15947                            })
15948                            .done();
15949                        })
15950                        .done();
15951                    }
15952                    else {
15953                        errorMessageDialog('Node Server is not configured.', 'Error')
15954                    }
15955                })
15956            },
15957            'Remove': function() {
15958                $buttonRemove.blur(); // Remove focus from button
15959
15960                validationPromise.then(function(data) {
15961                    if (data.data.profileNumber && data.data.profileVersion) {
15962                        confirmationDialog('This will remove all of your geofences from your configuration and ISY.<br/><br/>Are you sure?', 'Remove geofences?')
15963                        .then(function () {
15964                            ajaxJsonGet('/api/nsmanage/' + uuid + '/remove/occ2_device')
15965                            .done(function () {
15966                                ajaxJsonGet('/api/nsmanage/' + uuid + '/remove/occ2_geofence')
15967                                .done(function () {
15968                                    ajaxJsonDelete('/api/nsmodule/' + uuid + '/occupancy2/devices', $.param({uuid: uuid }))
15969                                    .fail(function(jqXHR, textStatus, errorThrown) {
15970                                        if (jqXHR.status==503) {
15971                                            errorMessageDialog('ISY is not online')
15972                                        }
15973                                        else if (jqXHR.status==400) {
15974                                            errorMessageDialog('Mobile devices has assigned geofences. Please unassign them first.')
15975                                        }
15976                                        else {
15977                                            errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
15978                                        }
15979                                    })
15980                                    .done(function () {
15981                                        informationMessageDialog('Removal completed successfully.')
15982                                        .done(function () {
15983                                            tableNsOccupancy2Geofence.reload();
15984                                            tableNsOccupancy2Device.reload();
15985                                            loadGeofences(uuid); // No need update the tableNsOccupancy2Geofence promise, there is nothing more to edit
15986                                            loadDevices(uuid); // No need update the tableNsOccupancy2Device promise, there is nothing more to edit
15987                                        })
15988                                        .done();
15989                                    })
15990                                })
15991                                .fail(function(jqXHR, textStatus, errorThrown) {
15992                                    if (jqXHR.status==503) {
15993                                        errorMessageDialog('ISY is not online')
15994                                    }
15995                                    else if (jqXHR.status==504) {
15996                                        errorMessageDialog('Timeout occured. Please try again.')
15997                                    }
15998                                    else if (jqXHR.status==550) {  // Custom error message
15999                                        var data = JSON.parse(jqXHR.responseText);
16000
16001                                        if (data.message) {
16002                                            errorMessageDialog(data.message)
16003                                        }
16004                                        else {
16005                                            errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
16006                                        }
16007                                    }
16008                                    else {
16009                                        errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
16010                                    }
16011                                })
16012                                .done()
16013                            })
16014                            .fail(function(jqXHR, textStatus, errorThrown) {
16015                                if (jqXHR.status==503) {
16016                                    errorMessageDialog('ISY is not online')
16017                                }
16018                                else if (jqXHR.status==504) {
16019                                    errorMessageDialog('Timeout occured. Please try again.')
16020                                }
16021                                else if (jqXHR.status==550) {  // Custom error message
16022                                    var data = JSON.parse(jqXHR.responseText);
16023
16024                                    if (data.message) {
16025                                        errorMessageDialog(data.message)
16026                                    }
16027                                    else {
16028                                        errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
16029                                    }
16030                                }
16031                                else {
16032                                    errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
16033                                }
16034                            })
16035                            .done();
16036                        })
16037                        .done();
16038                    }
16039                    else {
16040                        errorMessageDialog('Node Server is not configured.', 'Error')
16041                    }
16042                })
16043            },
16044            'Help' : function() {
16045                $buttonHelp.blur(); // Remove focus from button
16046                onlineHelp();
16047            },
16048            'Close': function() {
16049                $(this).dialog('close');
16050            }
16051        },
16052        open: function(event, ui) {
16053            dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
16054        }
16055    };
16056
16057    // Initializes the dialog
16058    dialog = $('#nsoccupancy2_dialog').dialog(isyOptions);
16059// Tooltip removed - Causing issues as the scope is too wide
16060//    $('[title]').tooltip();
16061    $("#nsoccupancy2_tabs").tabs();
16062
16063    var $buttons = $('#nsoccupancy2_dialog').parent().find('.ui-dialog-buttonset button');
16064    var $buttonSynchronize = $buttons.first(); // First button
16065    var $buttonRemove = $buttons.eq(1); // Second button
16066    var $buttonHelp = $buttons.eq(2);
16067
16068    $('#nsoccupancy2_geofenceadd').button().click(function () {
16069		$(this).blur(); // Remove focus from button
16070		validationPromise.then(function(data) {
16071            if (data.data.profileNumber && data.data.profileVersion) {
16072				nsOccupancy2GeofenceEditDialog.setFields({ uuid: uuid, editMode: false });
16073				nsOccupancy2GeofenceEditDialog.modal.dialog('open');
16074			}
16075			else {
16076				errorMessageDialog('Node Server is not configured.', 'Error')
16077			}
16078		})
16079    });
16080
16081    $('#nsoccupancy2_deviceassign').button().click(function () {
16082        $(this).blur(); // Remove focus from button
16083
16084        validationPromise.then(function(data) {
16085            if (data.data.profileNumber && data.data.profileVersion) {
16086                geofencesPromise
16087                .done(function(geofences) {
16088                    devicesPromise
16089                    .done(function(devices) {
16090                        nsOccupancy2EditDialog.setFields({ uuid: uuid, domain: domain, editMode:false, geofences: geofences.data, devices: devices.data });
16091                        nsOccupancy2EditDialog.modal.dialog('open');
16092                    })
16093                    .fail(function(jqXHR, textStatus, errorThrown) {
16094                        errorMessageDialog('Error loading devices', 'Error', 350)
16095                    })
16096                })
16097                .fail(function(jqXHR, textStatus, errorThrown) {
16098                    errorMessageDialog('Error loading geofences', 'Error', 350)
16099                })
16100            }
16101            else {
16102                errorMessageDialog('Node Server is not configured.', 'Error')
16103            }
16104        })
16105    });
16106
16107    $('#nsoccupancy2_refresh').button({ icons: {primary: 'ui-icon-custom-refresh'}, text: false}).click(function(event) {
16108        $(this).blur(); // Remove focus from button
16109        tableNsOccupancy2Geofence.reload();
16110        tableNsOccupancy2Device.reload();
16111    });
16112
16113    $('#nsoccupancy2_deviceadd').button().click(function () {
16114		$(this).blur(); // Remove focus from button
16115		validationPromise.then(function(data) {
16116			if (data.data.profileNumber && data.data.profileVersion) {
16117				nsOccupancy2DeviceEditDialog.setFields({ uuid: uuid, editMode: false });
16118				nsOccupancy2DeviceEditDialog.modal.dialog('open');
16119			}
16120			else {
16121				errorMessageDialog('Node Server is not configured.', 'Error')
16122			}
16123		})
16124    });
16125
16126    $('#nsoccupancy2_refresh2').button({ icons: {primary: 'ui-icon-custom-refresh'}, text: false}).click(function(event) {
16127        $(this).blur(); // Remove focus from button
16128        tableNsOccupancy2Geofence.reload();
16129        tableNsOccupancy2Device.reload();
16130    });
16131
16132    return { modal: dialog, setFields: setFields, loadGeofences: loadGeofences, loadDevices: loadDevices };
16133};
16134
16135// Occupancy2 Edit dialog - Assign Mobile Device to a Geofence
16136var nsOccupancy2EditDialogInit = function () {
16137    var dialog,
16138        form,
16139        pk,
16140        uuid,
16141        domain,
16142        editMode,
16143        geofences,
16144        devices,
16145        allFields = $([]),
16146        tips = $('#nsoccupancy2_edit_dialog .validateTips');
16147
16148    function updateGeofenceSelector(geofences, geofence) {
16149        var $select = $('<select><option value="$nogeofence">(Select)</option></select>');
16150
16151        geofences.sort(function(a, b) {
16152            return a.name.localeCompare(b.name) }
16153        );
16154
16155        geofences.forEach(function(geofence) {
16156            $select.append('<option value="' + geofence.address + '">' + geofence.name + ' (' + geofence.address + ')</option>');
16157        });
16158
16159        if (!geofence) {
16160            geofence = $('#nsoccupancy2_geofences_selector select').val();
16161        }
16162
16163        $('#nsoccupancy2_geofences_selector').empty().append($select.clone());
16164
16165        if (geofence) { // If no geofence is specified, and selector did not exist, it would be set to null
16166            $('#nsoccupancy2_geofences_selector select').val(geofence);
16167        }
16168
16169        $('#nsoccupancy2_geofences_selector select').selectmenu({
16170            width: '80%'
16171        });
16172    }
16173
16174    function updateDeviceSelector(devices, device) {
16175
16176        if (!devices) devices = [];
16177
16178        var $select = $('<select><option value="$nodevice">(Select)</option></select>');
16179
16180        devices.sort(function(a, b) {
16181            return a.name.localeCompare(b.name) }
16182        );
16183
16184        devices.forEach(function(device) {
16185            $select.append('<option value="' + device.address + '">' + device.name + ' (' + device.address + ')</option>');
16186        });
16187
16188        if (!device) {
16189            device = $('#nsoccupancy2_devices_selector select').val();
16190        }
16191
16192        $('#nsoccupancy2_devices_selector').empty().append($select.clone());
16193
16194        if (device) { // If no mobile device is specified, and selector did not exist, it would be set to null
16195            $('#nsoccupancy2_devices_selector select').val(device);
16196        }
16197
16198        $('#nsoccupancy2_devices_selector select').selectmenu({
16199            width: '80%'
16200        });
16201    }
16202
16203    function setFields(obj) {
16204        var label;
16205
16206        uuid = obj.uuid;
16207        domain = obj.domain;
16208        pk = obj.pk;
16209
16210        form[0].reset();
16211
16212        editMode = obj.editMode;
16213
16214        tips.text('Please select a Geofence and then assign a Mobile Device');
16215
16216        allFields.removeClass('fieldfocus');
16217
16218        if (!editMode) {
16219            obj.geofence = '$nogeofence';
16220            obj.device = '$nodevice';
16221        }
16222
16223        updateGeofenceSelector(obj.geofences, obj.geofence);
16224        updateDeviceSelector(obj.devices, obj.device);
16225    }
16226
16227    function submit() {
16228        var valid = true;
16229
16230        allFields.removeClass('fieldfocus');
16231
16232        valid = valid && checkSelection(tips, $('#nsoccupancy2_geofences_selector select'), '$nogeofence', 'Geofence' );
16233        valid = valid && checkSelection(tips, $('#nsoccupancy2_devices_selector select'), '$nodevice', 'Mobile Device' );
16234
16235        if (valid) {
16236            var data = 'geofence=' + $('#nsoccupancy2_geofences_selector select').val();
16237            data += '&deviceAddress=' + $('#nsoccupancy2_devices_selector select').val();
16238            data += '&domain=' + domain;
16239
16240            if (editMode) {
16241                data += '&id=' + pk;
16242            }
16243
16244            var url = '/api/nsmodule/' + uuid + '/occupancy2/node';
16245
16246            (editMode?ajaxJsonPost(url, data):ajaxJsonPut(url, data))
16247            .then(function(data, status, jqXHR) {
16248                dialog.dialog('close');
16249                tableNsOccupancy2Geofence.reload();
16250                tableNsOccupancy2Device.reload();
16251            })
16252            .fail(function(jqXHR, textStatus, errorThrown) {
16253                if (jqXHR.status==503) {
16254                    updateTips(tips, 'ISY is not online');
16255                }
16256                else if (jqXHR.status==400) { // Mobile Device already assigned to this Geofence
16257                    updateTips(tips, jqXHR.responseText);
16258                }
16259                else if (jqXHR.status==404) { // Can happen if device was just deleted and recreated shortly with same address
16260                    updateTips(tips, 'Could not create device on ISY (HTTP 404 returned). Please try again later.');
16261                }
16262                else if (jqXHR.status==420) { // Dupkey error with Custom server error message
16263                    var data = JSON.parse(jqXHR.responseText);
16264
16265                    if (data.message) {
16266                        updateTips(tips, data.message);
16267                    }
16268                    else {
16269                        updateTips(tips, 'HTTP error ' + jqXHR.status);
16270                    }
16271                }
16272                else {
16273                    updateTips(tips, 'HTTP error ' + jqXHR.status);
16274                }
16275            })
16276            .done();
16277        }
16278    }
16279
16280    var isyOptions = {
16281        autoOpen: false,
16282        width: 500,
16283        modal: true,
16284        buttons: {
16285            'Save': function() {
16286                submit();
16287            }
16288        },
16289        open: function(event, ui) {
16290            dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
16291        }
16292    };
16293
16294    // Initializes the dialog
16295    dialog = $('#nsoccupancy2_edit_dialog').dialog(isyOptions);
16296	// Tooltip removed - Causing issues as the scope is too wide
16297//    $('[title]').tooltip();
16298
16299    $('#nsoccupancy2_geofenceadd2').button({ icons: {primary: 'ui-icon-plus'}, text: false}).tooltip().click(function(event) {
16300		$(this).blur(); // Remove focus from button
16301			nsOccupancy2GeofenceEditDialog.setFields({ uuid: uuid, editMode: false });
16302			nsOccupancy2GeofenceEditDialog.modal.dialog('open');
16303    });
16304
16305    $('#nsoccupancy2_deviceadd2').button({ icons: {primary: 'ui-icon-plus'}, text: false}).tooltip().click(function(event) {
16306		$(this).blur(); // Remove focus from button
16307			nsOccupancy2DeviceEditDialog.setFields({ uuid: uuid, editMode: false });
16308			nsOccupancy2DeviceEditDialog.modal.dialog('open');
16309	});
16310
16311    // Handles keyboad enter (submit event)
16312    form = dialog.find('form').submit(function(event) {
16313        event.preventDefault();
16314        submit();
16315    });
16316
16317    return { modal: dialog, setFields: setFields, updateGeofenceSelector: updateGeofenceSelector, updateDeviceSelector: updateDeviceSelector };
16318};
16319
16320// Occupancy2 Edit dialog - Geofence
16321var nsOccupancy2GeofenceEditDialogInit = function () {
16322    var dialog,
16323        form,
16324        pk,
16325        uuid,
16326        domain,
16327        editMode,
16328        name = $('#nsoccupancy2_geofence_name'),
16329        allFields = $([]).add(name),
16330        tips = $('#nsoccupancy2_geofence_edit_dialog .validateTips');
16331
16332    function setFields(obj) {
16333
16334        var label;
16335
16336        uuid = obj.uuid;
16337        domain = obj.domain;
16338        pk = obj.pk;
16339
16340        form[0].reset();
16341        name.val(obj.name);
16342
16343        editMode = obj.editMode;
16344
16345        tips.text('Please enter a Geofence name');
16346
16347        allFields.removeClass('fieldfocus');
16348    }
16349
16350    function submit() {
16351        var valid = true;
16352
16353        allFields.removeClass('fieldfocus');
16354
16355        valid = valid && checkLength(tips, name, 'Name', 2, 40);
16356        valid = valid && checkRegexp(tips, name, nodeNameRegex, 'Geofence name is not valid, please use only alphanumeric characters.' );
16357
16358        if (valid) {
16359            var data = form.find('input').serialize();
16360            data += '&domain=' + domain;
16361
16362            if (editMode) {
16363                data += '&id=' + pk;
16364            }
16365
16366            var url = '/api/nsmodule/' + uuid + '/occupancy2/geofence';
16367
16368            (editMode?ajaxJsonPost(url, data):ajaxJsonPut(url, data))
16369            .then(function(geofenceRecord, status, jqXHR) {
16370                tableNsOccupancy2Geofence.reload();
16371
16372                var geofencesPromise = nsOccupancy2Dialog.loadGeofences(uuid);
16373                tableNsOccupancy2Geofence.setGeofences(geofencesPromise); // Will be available when we edit!
16374
16375                geofencesPromise.done(function(data) {
16376                    var newGeofence=null;
16377
16378                    if (geofenceRecord.data && geofenceRecord.data.address) {
16379                        newGeofence = geofenceRecord.data.address;
16380                    }
16381
16382                    nsOccupancy2EditDialog.updateGeofenceSelector(data.data, newGeofence);
16383                    dialog.dialog('close');
16384                })
16385            })
16386            .fail(function(jqXHR, textStatus, errorThrown) {
16387                if (jqXHR.status==503) {
16388                    updateTips(tips, 'ISY is not online');
16389                }
16390                else if (jqXHR.status==404) { // Can happen if device was just deleted and recreated shortly with same address
16391                    updateTips(tips, 'Could not create geofence on ISY (HTTP 404 returned). Please try again later.');
16392                }
16393                else if (jqXHR.status==400) { // Duplicated Geofence Name
16394                    updateTips(tips, jqXHR.responseText);
16395                }
16396                else if (jqXHR.status==420) { // Dupkey error with Custom server error message
16397                    var data = JSON.parse(jqXHR.responseText);
16398
16399                    if (data.message) {
16400                        updateTips(tips, data.message);
16401                    }
16402                    else {
16403                        updateTips(tips, 'HTTP error ' + jqXHR.status);
16404                    }
16405                }
16406                else {
16407                    updateTips(tips, 'HTTP error ' + jqXHR.status);
16408                }
16409            })
16410            .done();
16411        }
16412    }
16413
16414    var isyOptions = {
16415        autoOpen: false,
16416        width: 400,
16417        modal: true,
16418        buttons: {
16419            'Save': function() {
16420                submit();
16421            }
16422        },
16423        open: function(event, ui) {
16424            dialog.dialog( "option", "position", { my: "center", at: "center+10", of: window });
16425        }
16426    };
16427
16428    // Initializes the dialog
16429    dialog = $('#nsoccupancy2_geofence_edit_dialog').dialog(isyOptions);
16430// Tooltip removed - Causing issues as the scope is too wide
16431//$('[title]').tooltip();
16432
16433    // Handles keyboad enter (submit event)
16434    form = dialog.find('form').submit(function(event) {
16435        event.preventDefault();
16436        submit();
16437    });
16438
16439    return { modal: dialog, setFields: setFields };
16440};
16441
16442// Occupancy2 Edit dialog - Mobile Device
16443var nsOccupancy2DeviceEditDialogInit = function () {
16444    var dialog,
16445        form,
16446        pk,
16447        uuid,
16448        domain,
16449        editMode,
16450        name = $('#nsoccupancy2_device_name'),
16451        allFields = $([]).add(name),
16452        tips = $('#nsoccupancy2_device_edit_dialog .validateTips');
16453
16454    function updateProfileSelector(profile) {
16455        if (typeof profile!='string') profile = 'User';
16456        if (profile==='' || !profile)  profile = 'User';
16457        var $select = $('<select><option value="User">User</option><option value="Editor">Editor</option></select>'); //<option value="Admin">Admin</option>
16458        $('#nsoccupancy2_profile_selector').empty().append($select.clone());
16459        $('#nsoccupancy2_profile_selector select').val(profile);
16460        $('#nsoccupancy2_profile_selector select').selectmenu({
16461            width: '40%'
16462        });
16463    }
16464
16465    function setFields(obj) {
16466
16467        var label;
16468
16469        uuid = obj.uuid;
16470        domain = obj.domain;
16471        pk = obj.pk;
16472
16473        form[0].reset();
16474        name.val(obj.name);
16475
16476        editMode = obj.editMode;
16477
16478        tips.text('Please enter required information:');
16479
16480        allFields.removeClass('fieldfocus');
16481
16482        updateProfileSelector(obj.profile);
16483
16484    }
16485
16486    function submit() {
16487        var valid = true;
16488
16489        allFields.removeClass('fieldfocus');
16490
16491        valid = valid && checkLength(tips, name, 'Name', 2, 40);
16492        valid = valid && checkRegexp(tips, name, nodeNameRegex, 'Mobile Device name is not valid, please use only alphanumeric characters.' );
16493
16494        if (valid) {
16495            var data = form.find('input').serialize();
16496            data += '&profile=' + $('#nsoccupancy2_profile_selector select').val();
16497            data += '&domain=' + domain;
16498
16499            if (editMode) {
16500                data += '&id=' + pk;
16501            }
16502
16503            var url = '/api/nsmodule/' + uuid + '/occupancy2/device';
16504
16505            (editMode?ajaxJsonPost(url, data):ajaxJsonPut(url, data))
16506            .then(function(deviceRecord, status, jqXHR) {
16507                tableNsOccupancy2Geofence.reload();
16508                tableNsOccupancy2Device.reload();
16509                var devicesPromise = nsOccupancy2Dialog.loadDevices(uuid);
16510                tableNsOccupancy2Geofence.setDevices(devicesPromise); // Will be available when we edit!
16511                tableNsOccupancy2Device.setDevices(devicesPromise); // Will be available when we edit!
16512
16513                devicesPromise.done(function(data) {
16514                    var newDevice=null;
16515
16516                    if (deviceRecord.data && deviceRecord.data.address) {
16517                        newDevice = deviceRecord.data.address;
16518                    }
16519
16520                    nsOccupancy2EditDialog.updateDeviceSelector(data.data, newDevice);
16521                    dialog.dialog('close');
16522                })
16523            })
16524            .fail(function(jqXHR, textStatus, errorThrown) {
16525                if (jqXHR.status==503) {
16526                    updateTips(tips, 'ISY is not online');
16527                }
16528                else if (jqXHR.status==404) { // Can happen if device was just deleted and recreated shortly with same address
16529                    updateTips(tips, 'Could not create geofence on ISY (HTTP 404 returned). Please try again later.');
16530                }
16531                else if (jqXHR.status==400) { // Duplicated Mobile Device Name
16532                    updateTips(tips, jqXHR.responseText);
16533                }
16534                else if (jqXHR.status==420) { // Dupkey error with Custom server error message
16535                    var data = JSON.parse(jqXHR.responseText);
16536
16537                    if (data.message) {
16538                        updateTips(tips, data.message);
16539                    }
16540                    else {
16541                        updateTips(tips, 'HTTP error ' + jqXHR.status);
16542                    }
16543                }
16544                else {
16545                    updateTips(tips, 'HTTP error ' + jqXHR.status);
16546                }
16547            })
16548            .done();
16549        }
16550    }
16551
16552    var isyOptions = {
16553        autoOpen: false,
16554        width: 400,
16555        modal: true,
16556        buttons: {
16557            'Save': function() {
16558                submit();
16559            }
16560        },
16561        open: function(event, ui) {
16562            dialog.dialog( "option", "position", { my: "center", at: "center+10", of: window });
16563        }
16564    };
16565
16566    // Initializes the dialog
16567    dialog = $('#nsoccupancy2_device_edit_dialog').dialog(isyOptions);
16568	// Tooltip removed - Causing issues as the scope is too wide
16569//    $('[title]').tooltip();
16570
16571    // Handles keyboad enter (submit event)
16572    form = dialog.find('form').submit(function(event) {
16573        event.preventDefault();
16574        submit();
16575    });
16576
16577    return { modal: dialog, setFields: setFields };
16578};
16579
16580// Occupancy2 dialog - Geofence report URL
16581var nsOccupancy2GeofenceUrlDialogInit = function() {
16582    var dialog;
16583
16584    function setFields(hostname, key) {
16585        var url = 'https://' + hostname + '/api/occupancy2/node/' + key + '/report/';
16586        $('#report_url_in2').text(url+'in');
16587        $('#report_url_out2').text(url+'out');
16588    }
16589
16590    var isyOptions = {
16591        autoOpen: false,
16592        modal: true,
16593        width: 875,
16594        buttons: {
16595            'Ok': function() {
16596                $(this).dialog('close');
16597            }
16598        },
16599        close: function() {
16600        }
16601    };
16602    dialog = $('#occupancy2_geofence_url_dialog').dialog(isyOptions);
16603
16604    $('#occupancy2_geofence_url_dialog .copy_clip').button().click(function() {
16605        $(this).blur();
16606    });
16607
16608    if (!Clipboard.isSupported()) {
16609        $('#occupancy2_geofence_url_dialog .copy_clip').hide();
16610    }
16611
16612    var clipboard = new Clipboard('#occupancy2_geofence_url_dialog .copy_clip');
16613
16614    clipboard.on('success', function(e) {
16615        var target = $(e.trigger).attr('data-clipboard-target');
16616        if($(target).tooltip('option', 'disabled')) {
16617            $(target).tooltip('option', {disabled: false, content: 'Copied'}).tooltip('open')
16618        }
16619        setTimeout(function(){
16620            e.clearSelection();
16621            $(target).tooltip('option', {disabled: true}).tooltip('close')
16622        }, 800);
16623    });
16624
16625    clipboard.on('error', function(e) {
16626        var target = $(e.trigger).attr('data-clipboard-target');
16627        if($(target).tooltip('option', 'disabled')) {
16628            $(target).tooltip('option', {disabled: false, content: 'COPY FAILED'}).tooltip('open')
16629        }
16630        setTimeout(function(){
16631            e.clearSelection();
16632            $(target).tooltip('option', {disabled: true}).tooltip('close')
16633        }, 2000);
16634    });
16635
16636    $(".copied").tooltip({
16637        disabled: true,
16638        position: { my: "center", at: "center", collision: "flipfit" }
16639    })
16640    .hover(function(){
16641        $(this).tooltip('option', {disabled: true}).tooltip('close');
16642    });
16643
16644    return { modal: dialog, setFields: setFields };
16645};
16646
16647// Occupancy2 dialog - Mobile Device 6-digit pin
16648var nsOccupancy2DevicePinDialogInit = function() {
16649
16650    var dialog,
16651    pk, name, uuid, pinCode, pinValid;
16652
16653    function setFields(obj) {
16654        uuid = obj.uuid;
16655        pk = obj.pk;
16656        name = obj.name;
16657        this.setPin(obj);
16658    }
16659
16660    function setPin(obj) {
16661        var validDate = new Date(obj.pinValid);
16662        var checkDate = new Date();
16663
16664        pinCode = obj.pinCode;
16665        pinValid = validDate.toLocaleString("en-US",{timeZone:"America/New_York"}); //toLocaleTimeString('en-US');
16666
16667        if(validDate<checkDate) pinCode='';
16668
16669        if(pinCode==='') $('#nsoccupancy2_pin_code').hide();
16670        else            $('#nsoccupancy2_pin_code').show();
16671        $('#device_pin_name').text(name);
16672        $('#device_pin_code').text(pinCode);
16673        $('#device_pin_valid').text(pinValid);
16674    }
16675
16676    var isyOptions = {
16677        autoOpen: false,
16678        modal: true,
16679        width: 875,
16680        buttons: {
16681            'Ok': function() {
16682                $(this).dialog('close');
16683            }
16684        },
16685        close: function() {
16686        }
16687    };
16688    dialog = $('#occupancy2_device_pin_dialog').dialog(isyOptions);
16689
16690    $('#nsoccupancy2_pin_generate').button().click(function () {
16691        $(this).blur(); // Remove focus from button
16692        confirmationDialog('Please confirm if you want to Generate new 6-digit pin.', 'Generate pin')
16693        .then(function () {
16694            ajaxJsonPost('/api/nsmodule/' + uuid + '/occupancy2/pin', $.param({id: pk}))
16695            .done(function (data) {
16696                nsOccupancy2DevicePinDialog.setPin(data);
16697                tableNsOccupancy2Device.reload();
16698            })
16699            .fail(function(jqXHR, textStatus, errorThrown) {
16700                if (jqXHR.status==503) {
16701                    errorMessageDialog('ISY is not online')
16702                }
16703                else if (jqXHR.status==400) { // Pin assigned to another Mobile Devices (after 100 iterations)
16704                    errorMessageDialog(jqXHR.responseText);
16705                }
16706                else if (jqXHR.status==504) {
16707                    errorMessageDialog('Timeout occured. Please try again.')
16708                }
16709                else if (jqXHR.status==550) {  // Custom error message
16710                    var data = JSON.parse(jqXHR.responseText);
16711
16712                    if (data.message) {
16713                        errorMessageDialog(data.message)
16714                    }
16715                    else {
16716                        errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
16717                    }
16718                }
16719                else {
16720                    errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350)
16721                }
16722            })
16723            .done();
16724        })
16725        .done();
16726    });
16727
16728    $('#occupancy2_device_pin_dialog .copy_clip').button().click(function() {
16729        $(this).blur();
16730    });
16731
16732    if (!Clipboard.isSupported()) {
16733        $('#occupancy2_device_pin_dialog .copy_clip').hide();
16734    }
16735
16736    var clipboard = new Clipboard('#occupancy2_device_pin_dialog .copy_clip');
16737
16738    clipboard.on('success', function(e) {
16739        var target = $(e.trigger).attr('data-clipboard-target');
16740        if($(target).tooltip('option', 'disabled')) {
16741            $(target).tooltip('option', {disabled: false, content: 'Copied'}).tooltip('open')
16742        }
16743        setTimeout(function(){
16744            e.clearSelection();
16745            $(target).tooltip('option', {disabled: true}).tooltip('close')
16746        }, 800);
16747    });
16748
16749    clipboard.on('error', function(e) {
16750        var target = $(e.trigger).attr('data-clipboard-target');
16751        if($(target).tooltip('option', 'disabled')) {
16752            $(target).tooltip('option', {disabled: false, content: 'COPY FAILED'}).tooltip('open')
16753        }
16754        setTimeout(function(){
16755            e.clearSelection();
16756            $(target).tooltip('option', {disabled: true}).tooltip('close')
16757        }, 2000);
16758    });
16759
16760    $(".copied").tooltip({
16761        disabled: true,
16762        position: { my: "center", at: "center", collision: "flipfit" }
16763    })
16764    .hover(function(){
16765        $(this).tooltip('option', {disabled: true}).tooltip('close');
16766    });
16767
16768    return { modal: dialog, setFields: setFields, setPin: setPin };
16769};
16770
16771// AMAZON ECHO Spoken Devices & Scenes mapping
16772var echoSpokenNodesAVEditDialogInit = function () {
16773	var dialog, form, pk, uuid, domain, currentRooms, editMode, allFields,
16774		tips = $('#echo_av_dialog .validateTips'),
16775		room = $('#echo_av_room'),
16776		spokens = [$('#echo_av_spoken'), $('#echo_av_spoken2'), $('#echo_av_spoken3'), $('#echo_av_spoken4'), $('#echo_av_spoken5') ],
16777
16778	allFields = $([]).add(room).add(spokens[0]).add(spokens[1]).add(spokens[2]).add(spokens[3]).add(spokens[4]).add(spokens[5]);
16779
16780	var fieldGroups = [ {
16781			target: '#echo_av_tab2',
16782			fields: [
16783				{ label: 'Turn On',			baseField: 'turnon', type: 1, help: 'Alexa, turn on <device>' },
16784				{ label: 'Turn Off',		baseField: 'turnoff', type: 1, help: 'Alexa, turn off <device>' },
16785				{ label: 'Set Channel To',	baseField: 'setchannel', type: 2, class_label:'vspacer', help: 'Alexa, set channel to <number> on <device>' },
16786				{ label: 'Channel Up',		baseField: 'channelup', type: 1, help: 'Alexa, channel up on <device>' },
16787				{ label: 'Channel Down',	baseField: 'channeldown', type: 1, help: 'Alexa, channel down on <device>' }
16788			]
16789		}, {
16790			target: '#echo_av_tab3',
16791			fields: [
16792				{ label: 'Set Volume To',	baseField: 'setvolume', type: 2, help: 'Alexa, set volume to <number> on <device>' },
16793				{ label: 'Volume Up',		baseField: 'volumeup', type: 1, help: 'Alexa, volume up on <device>' },
16794				{ label: 'Volume Down',		baseField: 'volumedown', type: 1, help: 'Alexa, volume down on <device>' },
16795				{ label: 'Mute',			baseField: 'mute', type: 1, class_label:'vspacer', help: 'Alexa, mute <device>' },
16796				{ label: 'Unmute',			baseField: 'unmute', type: 1, help: 'Alexa, unmute <device>' }
16797			]
16798		}, {
16799			target: '#echo_av_tab4',
16800			fields: [
16801				{ label: 'Play',			baseField: 'play', type: 1, help: 'Alexa, play on <device>' },
16802				{ label: 'Pause',			baseField: 'pause', type: 1, help: 'Alexa, pause on <device>' },
16803				{ label: 'Stop',			baseField: 'stop', type: 1, help: 'Alexa, stop on <device>' },
16804				{ label: 'Fast Forward',	baseField: 'fastforward', type: 1, help: 'Alexa, fast forward on <device>' },
16805				{ label: 'Rewind',			baseField: 'rewind', type: 1, help: 'Alexa, rewind on <device>' },
16806				{ label: 'Next',			baseField: 'next', type: 1, help: 'Alexa, next on <device>' },
16807				{ label: 'Previous',		baseField: 'previous', type: 1, help: 'Alexa, previous on <device>' },
16808				{ label: 'Start Over',		baseField: 'startover', type: 1, help: 'Alexa, start over on <device>' }
16809			]
16810		}, {
16811			target: '#echo_av_tab5',
16812			fields: [
16813				{ label: 'Input 1',		baseField: 'input1', type: 3, help: 'Alexa, change input to <input name> on <device>' },
16814				{ label: 'Input 2',		baseField: 'input2', type: 3, help: 'Alexa, change input to <input name> on <device>' },
16815				{ label: 'Input 3',		baseField: 'input3', type: 3, help: 'Alexa, change input to <input name> on <device>' },
16816				{ label: 'Input 4',		baseField: 'input4', type: 3, help: 'Alexa, change input to <input name> on <device>' },
16817				{ label: 'Input 5',		baseField: 'input5', type: 3, help: 'Alexa, change input to <input name> on <device>' },
16818				{ label: 'Input 6',		baseField: 'input6', type: 3, help: 'Alexa, change input to <input name> on <device>' },
16819				{ label: 'Input 7',		baseField: 'input7', type: 3, help: 'Alexa, change input to <input name> on <device>' },
16820				{ label: 'Input 8',		baseField: 'input8', type: 3, help: 'Alexa, change input to <input name> on <device>' }
16821			]
16822		}
16823	];
16824
16825	function displayField(item) {
16826		var $item = $(item);
16827		var option = $item.find('select').val();
16828
16829		$item.find('.av_opt').hide();
16830
16831		if (option!=='$disabled') {
16832			$item.find('.av_opt_'+option).show();
16833		}
16834	}
16835
16836	var fieldBuilder = {
16837		1: function (baseField) {
16838			return '<span><select class="av_entry_selector">' +
16839					'<option value="$disabled">Disabled</option>' +
16840					'<option value="svar">State Variable</option>' +
16841					'<option value="net">Network resource</option>' +
16842					'<option value="pgm">Program</option>' +
16843				'</select></span>' +
16844			 	'<span class="av_opt av_opt_svar"><div class="combobox-container av_selector_svar"></div><input id="' + baseField + '_svar_value" name="' + baseField + '_svar_value" class="text ui-widget-content ui-corner-all" placeholder="Set to" value=""></span>' +
16845				'<span class="av_opt av_opt_net"><div class="combobox-container av_selector_net"></div></span>'+
16846				'<span class="av_opt av_opt_pgm"><div class="combobox-container av_selector_pgm"></div></span>';
16847		},
16848
16849		2: function (baseField) {
16850			return '<span><select class="av_entry_selector">' +
16851					'<option value="$disabled">Disabled</option>' +
16852					'<option value="svar">State Variable</option>' +
16853				'</select></span>' +
16854			 	'<span class="av_opt av_opt_svar"><div class="combobox-container av_selector_svar"></div></span>'// +
16855		},
16856
16857		3: function (baseField) {
16858		return '<span><select class="av_entry_selector">' +
16859						'<option value="$disabled">Disabled</option>' +
16860						'<option value="svar">State Variable</option>' +
16861						'<option value="net">Network resource</option>' +
16862						'<option value="pgm">Program</option>' +
16863					'</select></span>' +
16864				 	'<span class="av_opt av_opt_svar"><div class="combobox-container av_selector_svar"></div></span>' + // <input id="' + baseField + '_input_name" name="' + baseField + '_input_name" class="text inputSelectName ui-widget-content ui-corner-all" placeholder="Input Name" value="">
16865					'<span class="av_opt av_opt_net"><div class="combobox-container av_selector_net"></div></span>'+
16866					'<span class="av_opt av_opt_pgm"><div class="combobox-container av_selector_pgm"></div></span>' +
16867					'<input id="' + baseField + '_input_name" name="' + baseField + '_input_name" class="text inputSelectName av_opt av_opt_svar av_opt_net av_opt_pgm ui-widget-content ui-corner-all" placeholder="Input Name" value="">';
16868		}
16869	};
16870
16871	function initUI() {
16872
16873		fieldGroups.forEach(function (fieldGroup) {
16874			var target = fieldGroup.target;
16875//			$(target).empty();
16876
16877			// Add all the fields to their tabs
16878			fieldGroup.fields.forEach(function(field) {
16879				var entry = '<div' + (field.class_label?' class="' + field.class_label + '"':'') + (field.help?' title="' + field.help + '"':'') + '><label>' + field.label + '</label></div>' +
16880							'<div class="av_opt_wrapper" basefield="' + field.baseField + '" fieldtype="' + field.type + '">' +
16881								fieldBuilder[field.type](field.baseField) +
16882							'</div>';
16883
16884				$(target).append(entry);
16885			});
16886
16887			// Initialize the selectors
16888			$(target + ' select.av_entry_selector').selectmenu({
16889				width: 200,
16890				change: function(event, ui) {
16891					var $wrapper = $(ui.item.element).closest('.av_opt_wrapper');
16892					displayField($wrapper); // When the selector changes, show/hide the relevant fields.
16893				}
16894			});
16895
16896			// Initialize all of the combobox!
16897			$(target + ' .combobox-container').combobox({inputClass: ''});
16898
16899			// Display/Hide fields based on the selector (actually hides all of them, bacause the selector is set to $disabled)
16900			$(target + ' .av_opt_wrapper').each(function() {
16901				displayField(this); // When initializing, hide detail fields. (The selector is disabled by default)
16902			});
16903
16904			// Auto-Capitalize input
16905			$(target + ' .av_opt_wrapper .inputSelectName').focusout(function() {
16906				$(this).val($(this).val().toUpperCase());
16907				$(this).val($(this).val().replace(/^HDMI /, 'HDMI'));
16908			});
16909
16910			// Adds all input fields (standard inputs, or combobox input field)
16911			allFields = allFields.add(target + ' input');
16912		})
16913	}
16914
16915	// Updates the room selector
16916	function updateRoomSelector(rooms, choice) {
16917		// label: shown in the selections
16918		// value: displayed in the combobox input field (this.input)
16919		// data: real data assigned to the input field (this.element)
16920
16921		rooms = rooms.map(function(item) {
16922			return {label: item.name, value: item.name, data: item._id}
16923		});
16924
16925		rooms.sort(function(a, b) {
16926			return a.label.localeCompare(b.label)
16927		});
16928
16929		rooms.unshift({ label: '(No room)', value: '', data: '$noroom'});
16930
16931		room.combobox("option", "source", rooms);
16932		room.combobox("option", "data", choice);
16933
16934		currentRooms = rooms;
16935	}
16936
16937	// Initialize form with new values
16938	function setFields(obj) {
16939		var label;
16940
16941		uuid = obj.uuid;
16942		domain = obj.domain;
16943		pk = obj.pk;
16944
16945		form[0].reset();
16946		spokens[0].focus();
16947
16948		editMode = obj.editMode;
16949
16950		// Set the source for all of the combobox
16951//		$('#echo_av_dialog .av_selector_node').combobox("option", "source", obj.lookupNodes.data.devices);
16952		$('#echo_av_dialog .av_selector_svar').combobox("option", "source", obj.lookupVars.data);
16953		$('#echo_av_dialog .av_selector_pgm').combobox("option", "source", obj.lookupPgms.data);
16954		$('#echo_av_dialog .av_selector_net').combobox("option", "source", obj.lookupNet.data);
16955
16956		// Load the fields with avData (or empty the fields)
16957		// This is Ok for editing or not. (obj.avParam will be undefined)
16958		$('#echo_av_dialog .av_opt_wrapper').each(function() {
16959			var $this = $(this);
16960			var baseField = $this.attr('basefield');
16961			var fieldType = parseInt($this.attr('fieldtype')); //$this.find('select.av_entry_selector').val()
16962
16963			var avParam = obj.avData && obj.avData[baseField];
16964
16965			if (!avParam) {
16966				avParam = {type: "$disabled"}; // If we don't have the configuration parameter, the default is disabled.
16967			}
16968
16969			var $selector = $this.find('select.av_entry_selector');
16970            var $svarCombo;
16971            var $svarValueField;
16972            var $inputName;
16973            var $pgmCombo;
16974            var $netCombo;
16975
16976			$selector.val(avParam.type);
16977			$selector.selectmenu('refresh');
16978			displayField($this);
16979
16980			switch (fieldType) {
16981				case 1:
16982					$svarCombo = $this.find('.av_opt_svar .av_selector_svar');
16983					$svarValueField = $this.find('#'+ baseField + '_svar_value');
16984					$pgmCombo = $this.find('.av_opt_pgm .av_selector_pgm');
16985					$netCombo = $this.find('.av_opt_net .av_selector_net');
16986
16987					switch (avParam.type) {
16988						case 'svar':
16989							$svarCombo.combobox("option", "data", avParam.svar);
16990							$pgmCombo.combobox("option", "data", '');
16991							$netCombo.combobox("option", "data", '');
16992							if (!isNaN(avParam.value)) {
16993								$svarValueField.val(avParam.value)
16994							}
16995							else if (!isNaN(avParam.valueDelta)) {
16996								$svarValueField.val(avParam.valueDelta>0?'+'+avParam.valueDelta:avParam.valueDelta)
16997							}
16998							else {
16999								$svarValueField.val('')
17000							}
17001							break;
17002
17003						case 'pgm':
17004							$svarCombo.combobox("option", "data", '');
17005							$pgmCombo.combobox("option", "data", avParam.pgm);
17006							$netCombo.combobox("option", "data", '');
17007							$svarValueField.val('');
17008							break;
17009
17010						case 'net':
17011							$svarCombo.combobox("option", "data", '');
17012							$pgmCombo.combobox("option", "data", '');
17013							$netCombo.combobox("option", "data", avParam.net);
17014							$svarValueField.val('');
17015							break;
17016
17017						case '$disabled':
17018						default:
17019							$svarCombo.combobox("option", "data", '');
17020							$pgmCombo.combobox("option", "data", '');
17021							$netCombo.combobox("option", "data", '');
17022							$svarValueField.val('');
17023							break;
17024					}
17025					break;
17026
17027				case 2:
17028					$svarCombo = $this.find('.av_opt_svar .av_selector_svar');
17029					switch (avParam.type) {
17030						case 'svar':
17031							$svarCombo.combobox("option", "data", avParam.svar);
17032							break;
17033
17034						case '$disabled':
17035						default:
17036							$svarCombo.combobox("option", "data", '');
17037							break;
17038					}
17039					break;
17040
17041				case 3:
17042					$svarCombo = $this.find('.av_opt_svar .av_selector_svar');
17043					$inputName = $this.find('#'+ baseField + '_input_name');
17044					$pgmCombo = $this.find('.av_opt_pgm .av_selector_pgm');
17045					$netCombo = $this.find('.av_opt_net .av_selector_net');
17046
17047					if (avParam.input_name && avParam.input_name.length) {
17048						$inputName.val(avParam.input_name)
17049					}
17050					else {
17051						$inputName.val('')
17052					}
17053
17054					switch (avParam.type) {
17055						case 'svar':
17056							$svarCombo.combobox("option", "data", avParam.svar);
17057							$pgmCombo.combobox("option", "data", '');
17058							$netCombo.combobox("option", "data", '');
17059							break;
17060
17061						case 'pgm':
17062							$svarCombo.combobox("option", "data", '');
17063							$pgmCombo.combobox("option", "data", avParam.pgm);
17064							$netCombo.combobox("option", "data", '');
17065							break;
17066
17067						case 'net':
17068							$svarCombo.combobox("option", "data", '');
17069							$pgmCombo.combobox("option", "data", '');
17070							$netCombo.combobox("option", "data", avParam.net);
17071							break;
17072
17073						case '$disabled':
17074						default:
17075							$svarCombo.combobox("option", "data", '');
17076							$pgmCombo.combobox("option", "data", '');
17077							$netCombo.combobox("option", "data", '');
17078							break;
17079					}
17080					break;
17081
17082			}
17083		});
17084
17085		if (editMode) {
17086			tips.text('Please update your A/V mapping.');
17087
17088			for (var i=0; i<5; i++) {
17089				if (obj.spokens && i<=obj.spokens.length) {
17090					spokens[i].val(obj.spokens[i]);
17091				}
17092				else {
17093					spokens[i].val('');
17094				}
17095			}
17096		}
17097		else {
17098			tips.text('Please enter your A/V mapping.');
17099			obj.room = '$noroom';
17100		}
17101
17102		updateRoomSelector(obj.rooms, obj.room);
17103		allFields.removeClass('fieldfocus');
17104	}
17105
17106	// Validate and save!
17107	function submit() {
17108		var valid = true;
17109
17110		if (!room.combobox("validate")) {
17111
17112			$("#echo_av_tabs").tabs("option", "active", 0);
17113			var name = room.combobox("getValue");
17114
17115			confirmationDialog('Room ' + name + ' does not exists.<br/><br/>Create it?', 'Create room?')
17116			.then(function () {
17117				ajaxJsonPut('/api/voice/amazon/spoken/room', $.param({domain: domain, uuid: uuid, name: name }))
17118				.done(function (record) {
17119					var roomsPromise = echoSpokenNodesDialog.loadRooms(uuid);
17120
17121					roomsPromise
17122					.then(function(data) {
17123						updateRoomSelector(data.data, record.data._id);
17124					});
17125
17126					tableEchoSpokenNodes.setRooms(roomsPromise);
17127				})
17128				.fail(function(jqXHR, textStatus, errorThrown) {
17129					errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350);
17130				})
17131			})
17132			.done();
17133
17134			valid=false;
17135		}
17136
17137		allFields.removeClass('fieldfocus');
17138		$("#echo_av_tabs").tabs("option", "active", 0); // We need to be on the first tab, otherwise the field won't have the focus.
17139
17140		valid = valid && checkLength(tips, spokens[0], 'Spoken', 2, 40);
17141		valid = valid && checkRegexp(tips, spokens[0], spokenRegex, 'Spoken must contain only letters with spaces');
17142		valid = valid && checkLength(tips, spokens[1], 'Alternate spoken 1', 0, 40);
17143		valid = valid && checkRegexp(tips, spokens[1], spokenRegex, 'Spoken must contain only letters with spaces');
17144		valid = valid && checkLength(tips, spokens[2], 'Alternate spoken 2', 0, 40);
17145		valid = valid && checkRegexp(tips, spokens[2], spokenRegex, 'Spoken must contain only letters with spaces');
17146		valid = valid && checkLength(tips, spokens[3], 'Alternate spoken 3', 0, 40);
17147		valid = valid && checkRegexp(tips, spokens[3], spokenRegex, 'Spoken must contain only letters with spaces');
17148		valid = valid && checkLength(tips, spokens[4], 'Alternate spoken 4', 0, 40);
17149		valid = valid && checkRegexp(tips, spokens[4], spokenRegex, 'Spoken must contain only letters with spaces');
17150
17151		var avParameters = {};
17152		var $fieldHasError=null; // We track which field has an error. We need to switch to the appropriate tab before putting the focus on the field.
17153		var avOptionsCount=0;
17154
17155		$('#echo_av_dialog .av_opt_wrapper').each(function() {
17156			var $this = $(this);
17157
17158			if (valid) {
17159				var baseField = $this.attr('basefield');
17160				var type = $this.find('select.av_entry_selector').val();
17161
17162				var fieldType = parseInt($this.attr('fieldtype'));
17163				var $combo;
17164
17165				switch (fieldType) {
17166					case 1:
17167						switch (type) {
17168							case 'svar':
17169								$combo = $this.find('.av_opt_svar .av_selector_svar');
17170
17171								if (!$combo.combobox("validate")) {
17172									updateTips(tips, 'State variable is not valid.');
17173									$fieldHasError = $this.find('.av_opt_svar .custom-combobox input');
17174									valid=false;
17175								}
17176
17177								var $valueField = $this.find('#'+ $this.attr('basefield') + '_svar_value');
17178
17179								valid = checkNumberOrDelta(tips, $valueField, 'State variable value');
17180
17181								if (valid) {
17182									avOptionsCount++;
17183									avParameters[baseField] = {
17184										type: type,
17185										svar: $combo.val(),
17186										svarname: $combo.combobox("getValue")
17187									};
17188
17189									if (/^[\+\-]/.test($valueField.val())) {
17190										avParameters[baseField].valueDelta = parseInt($valueField.val())
17191									}
17192									else {
17193										avParameters[baseField].value = parseInt($valueField.val())
17194									}
17195								}
17196								else {
17197									$fieldHasError = $valueField
17198								}
17199								break;
17200
17201							case 'pgm':
17202								$combo = $this.find('.av_opt_pgm .av_selector_pgm');
17203								if (!$combo.combobox('validate')) {
17204									updateTips(tips, 'Program is not valid.');
17205									$fieldHasError = $this.find('.av_opt_pgm .custom-combobox input');
17206									valid=false;
17207								}
17208								else {
17209									avOptionsCount++;
17210									avParameters[baseField] = {
17211										type: type,
17212										pgm: $combo.val(),
17213										pgmname: $combo.combobox("getValue")
17214									}
17215								}
17216								break;
17217
17218							case 'net':
17219								$combo = $this.find('.av_opt_net .av_selector_net');
17220								if (!$combo.combobox('validate')) {
17221									updateTips(tips, 'Network resource is not valid.');
17222									$fieldHasError = $this.find('.av_opt_net .custom-combobox input');
17223									valid=false;
17224								}
17225								else {
17226									avOptionsCount++;
17227									avParameters[baseField] = {
17228										type: type,
17229										net: $combo.val(),
17230										netname: $combo.combobox("getValue")
17231									}
17232								}
17233								break;
17234
17235							default:
17236								avParameters[baseField] = {
17237									type: '$disabled',
17238								}
17239						}
17240						break;
17241
17242					case 2:
17243						switch (type) {
17244							case 'svar':
17245								var $combo = $this.find('.av_opt_svar .av_selector_svar');
17246
17247								if (!$combo.combobox("validate")) {
17248									updateTips(tips, 'State variable is not valid.');
17249									$fieldHasError = $this.find('.av_opt_svar .custom-combobox input');
17250									valid=false;
17251								}
17252								else {
17253									avOptionsCount++;
17254									avParameters[baseField] = {
17255										type: type,
17256										svar: $combo.val(),
17257										svarname: $combo.combobox("getValue")
17258									}
17259								}
17260								break;
17261
17262							default:
17263								avParameters[baseField] = {
17264									type: '$disabled',
17265								}
17266						}
17267						break;
17268
17269					case 3:
17270						switch (type) {
17271							case 'svar':
17272								var $combo = $this.find('.av_opt_svar .av_selector_svar');
17273
17274								if (!$combo.combobox("validate")) {
17275									updateTips(tips, 'State variable is not valid.');
17276									$fieldHasError = $this.find('.av_opt_svar .custom-combobox input');
17277									valid=false;
17278								}
17279								else {
17280									avOptionsCount++;
17281									avParameters[baseField] = {
17282										type: type,
17283										svar: $combo.val(),
17284										svarname: $combo.combobox("getValue")
17285									}
17286								}
17287								break;
17288
17289							case 'pgm':
17290								var $combo = $this.find('.av_opt_pgm .av_selector_pgm');
17291								if (!$combo.combobox('validate')) {
17292									updateTips(tips, 'Program is not valid.');
17293									$fieldHasError = $this.find('.av_opt_pgm .custom-combobox input');
17294									valid=false;
17295								}
17296								else {
17297									avOptionsCount++;
17298									avParameters[baseField] = {
17299										type: type,
17300										pgm: $combo.val(),
17301										pgmname: $combo.combobox("getValue")
17302									}
17303								}
17304								break;
17305
17306							case 'net':
17307								var $combo = $this.find('.av_opt_net .av_selector_net');
17308								if (!$combo.combobox('validate')) {
17309									updateTips(tips, 'Network resource is not valid.');
17310									$fieldHasError = $this.find('.av_opt_net .custom-combobox input');
17311									valid=false;
17312								}
17313								else {
17314									avOptionsCount++;
17315									avParameters[baseField] = {
17316										type: type,
17317										net: $combo.val(),
17318										netname: $combo.combobox("getValue")
17319									}
17320								}
17321								break;
17322
17323							default:
17324								avParameters[baseField] = {
17325									type: '$disabled',
17326								}
17327						}
17328
17329						if (valid && type!=='$disabled') {
17330							var $inputNameField = $this.find('#'+ $this.attr('basefield') + '_input_name');
17331							valid = checkLength(tips, $inputNameField, 'Input name', 2, 10);
17332
17333							if (valid) {
17334								avParameters[baseField].input_name = $inputNameField.val();
17335							}
17336							else {
17337								$fieldHasError = $inputNameField;
17338							}
17339						}
17340						break;
17341
17342					default:
17343				}
17344			}
17345		});
17346
17347		// We need to have at least one option set
17348		if (valid && !avOptionsCount) {
17349			valid=false;
17350			tips.text('You need to configure at least one of the A/V options.');
17351		}
17352
17353		if (!valid && $fieldHasError) {
17354			var tabIndex = $fieldHasError.closest(".av_tab").attr('tabidx'); // It is set on purpose in index.htm so we can find the tab index
17355			$("#echo_av_tabs").tabs("option", "active", tabIndex);
17356
17357			$fieldHasError.addClass('fieldfocus').focus();
17358		}
17359
17360		if (valid) {
17361			var data = $('#echo_av_tab1 input').serialize();
17362			data += '&uuid=' + uuid + '&domain=' + domain + '&category=av&' + $.param({ avJson: JSON.stringify(avParameters) });
17363
17364			if (editMode) {
17365				data += '&id=' + pk;
17366			}
17367
17368			(editMode?ajaxJsonPost('/api/voice/amazon/spoken/node', data):ajaxJsonPut('/api/voice/amazon/spoken/node', data))
17369			.then(function(data, status, jqXHR) {
17370				dialog.dialog('close');
17371				tableEchoSpokenNodes.reload();
17372			})
17373			.fail(function(jqXHR, textStatus, errorThrown) {
17374				if (jqXHR.status==503) {
17375					updateTips(tips, 'ISY is not online');
17376				}
17377				else if (jqXHR.status==420) { // Dupkey error with Custom server error message
17378					var data = JSON.parse(jqXHR.responseText);
17379
17380					if (data.message) {
17381						updateTips(tips, data.message);
17382					}
17383					else {
17384						updateTips(tips, 'HTTP error ' + jqXHR.status);
17385					}
17386				}
17387				else {
17388					updateTips(tips, 'HTTP error ' + jqXHR.status);
17389				}
17390			})
17391			.done();
17392		}
17393	}
17394
17395	function onlineHelp() {
17396		informationMessageDialog(
17397			'<div class="online-help"><p>A/V Virtual Device is a feature available with the upcoming ISY Optimized for Smart Home V3. ' +
17398			'It allows to create virtual Alexa A/V devices that maps to a combination of ISY state variables, network resources and/or programs.<br><br> ' +
17399			'Configuration tips:<br><ul>' +
17400			'<li>Create as many virtual A/V devices as you have physical A/V devices. Example; one for the TV, one for the Home theater, etc.</li>' +
17401			'<li>All fields are optional.</li>' +
17402			'<li>Under Basic, use either Set Channel To, or Channel Up/Channel Down, not both.</li>' +
17403			'<li>Under Volume, you can use Set Volume To, Volume Up/Volume Down, or both. If you use both, use the same State Variable.</li>' +
17404			'<li>When entering a State Variable value, you can specify an absolute value, or a delta by entering + or - before the number. This can be especially useful for the Volume Up/Volume Down.</li>' +
17405			'<li>When you hover on a field label, you get an example of how to use it.</li>' +
17406			'</ul>',
17407			'A/V Virtual Device Online Help', 850)
17408	}
17409
17410	var isyOptions = {
17411		autoOpen: false,
17412		width: 825,
17413		modal: true,
17414		buttons: {
17415			'Help' : function() {
17416				$buttonHelp.blur(); // Remove focus from button
17417				onlineHelp();
17418			},
17419			'Save': function() {
17420				submit();
17421			}
17422		},
17423		open: function(event, ui) {
17424			dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
17425		}
17426	};
17427
17428	var $buttons = $('#echo_av_dialog').parent().find('.ui-dialog-buttonset button');
17429	var $buttonHelp = $buttons.first(); // First button
17430
17431	// Initializes the dialog
17432	dialog = $('#echo_av_dialog').dialog(isyOptions);
17433
17434	// Handles keyboad enter (submit event)
17435	form = dialog.find('form').submit(function(event) {
17436		document.activeElement.blur(); // Triggers the current field focus out; allows to capitalize & fix the input_name
17437		event.preventDefault();
17438		submit();
17439	});
17440
17441	$("#echo_av_tabs").tabs();
17442
17443	initUI();
17444// Tooltip removed - Causing issues as the scope is too wide
17445//	$('[title]').tooltip();
17446
17447	$('#echo_av_room_add').button({ icons: {primary: 'ui-icon-plus'}, text: false}).tooltip().click(function(event) {
17448		$(this).blur(); // Remove focus from button
17449		echoSpokenRoomEditDialog.setFields({ uuid: uuid, editMode: false, domain: domain });
17450		echoSpokenRoomEditDialog.modal.dialog('open');
17451	});
17452
17453	$('#echo_av_room_edit').button({ icons: {primary: 'ui-icon-pencil'}, text: false}).tooltip().click(function(event) {
17454		$(this).blur(); // Remove focus from button
17455
17456		room.combobox("refresh"); // Makes sure the real input field is up to date
17457		var pk = room.val();
17458
17459		var item = currentRooms.find(function(item) {
17460			return item.data === pk;
17461		});
17462
17463		if (pk!='$noroom' && item) {
17464			echoSpokenRoomEditDialog.setFields({ uuid: uuid, editMode: true, domain: domain, pk: pk, name: item.label });
17465			echoSpokenRoomEditDialog.modal.dialog('open');
17466		}
17467		else {
17468			informationMessageDialog("This can't be edited.")
17469		}
17470	});
17471
17472	$('#echo_av_room_delete').button({ icons: {primary: 'ui-icon-trash'}, text: false}).tooltip().click(function(event) {
17473		$(this).blur(); // Remove focus from button
17474
17475		room.combobox("refresh"); // Makes sure the real input field is up to date
17476		var pk = room.val();
17477
17478		var item = currentRooms.find(function(item) {
17479			return item.data === pk;
17480		});
17481
17482		if (pk!=='$noroom' && item) {
17483			confirmationDialog('Delete room ' + item.label + '.<br/><br/>Are you sure?', 'Delete room?')
17484			.then(function () {
17485				ajaxJsonDelete('/api/voice/amazon/spoken/room', $.param({id: pk, domain: domain, uuid: uuid }))
17486				.done(function () {
17487					var roomsPromise = echoSpokenNodesDialog.loadRooms(uuid);
17488
17489					roomsPromise
17490					.then(function(data) {
17491						echoSpokenNodesEditDialog.updateRoomSelector(data.data, '$noroom');
17492						echoSpokenNodesAVEditDialog.updateRoomSelector(data.data, '$noroom');
17493					});
17494
17495					tableEchoSpokenNodes.setRooms(roomsPromise);
17496					tableEchoSpokenNodes.reload();
17497				})
17498				.fail(function(jqXHR, textStatus, errorThrown) {
17499					errorMessageDialog('HTTP error ' + jqXHR.status, 'Error', 350);
17500				})
17501			})
17502			.done();
17503		}
17504		else {
17505			informationMessageDialog("This can't be removed.")
17506		}
17507	});
17508
17509	room.combobox({inputClass: ''});
17510
17511	return { modal: dialog, setFields: setFields, updateRoomSelector: updateRoomSelector };
17512};
17513
17514// Download user list
17515var downloadUsersDialogInit = function () {
17516	var dialog,
17517		form,
17518//		notificationmaint = $('#myprofile_notificationmaint'),
17519//		notificationnews = $('#myprofile_notificationnews'),
17520		allFields = $([]);//.add(isy).add(username).add(notificationmaint).add(notificationnews),
17521	var tips = $('#downloadUsers_dialog .validateTips');
17522	var selector = '#downloadUsers_accountType_selector';
17523
17524	function resetForm() {
17525		tips.text('Select the download options');
17526		allFields.removeClass('fieldfocus');
17527		form[0].reset();
17528		$(selector + ' select').selectmenu('refresh'); // Needs a refresh to show
17529	}
17530
17531	function submit() {
17532		var valid = true;
17533
17534		allFields.removeClass('fieldfocus');
17535
17536		if (valid) {
17537			var data = serializeFormWithCheckboxes(form.find('input'));
17538			data += '&accountType=' + $(selector + ' select').val();
17539
17540
17541			ajaxJsonPost('/api/download/userlist', data)
17542			.then(function(data, status, jqXHR) {
17543				dialog.dialog('close');
17544				try {
17545					var isFileSaverSupported = !!new Blob;
17546				} catch (e) {}
17547
17548				if (isFileSaverSupported) {
17549					var BOM = '\uFEFF';
17550					saveAs(new Blob([BOM + data.users], {type: "application/csv;charset=utf-8"}), 'users.csv');
17551				}
17552				else {
17553					errorMessageDialog('Feature not supported on this browser', 'Not supported');
17554				}
17555			})
17556			.fail(function(jqXHR, textStatus, errorThrown) {
17557				updateTips(tips, 'HTTP error ' + jqXHR.status);
17558			})
17559			.done();
17560		}
17561	}
17562
17563	var isyOptions = {
17564		autoOpen: false,
17565		width: 650,
17566//		height: 445,
17567		modal: true,
17568		buttons: {
17569			'Download': function() {
17570				submit();
17571			},
17572			Cancel: function() {
17573				$(this).dialog('close');
17574			}
17575		},
17576		open: function(event, ui) {
17577			resetForm();
17578			dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
17579			$('.custom-combobox-input').focus();
17580		}
17581	};
17582
17583	// Initializes the dialog
17584	dialog = $('#downloadUsers_dialog').dialog(isyOptions);
17585
17586	// Handles keyboad enter (submit event)
17587	form = dialog.find('form').submit(function(event) {
17588		event.preventDefault();
17589		submit();
17590	});
17591
17592	var selectOptions = [
17593		{ value: 'all', description: 'Include all accounts' },
17594		{ value: 'root', description: 'Include root account users only' },
17595		{ value: 'sub', description: 'Include sub-account users only' },
17596		{ value: 'account', description: 'Include license notification users specified on account' }
17597	];
17598
17599	buildSelect(selectOptions, selector, '35em');
17600
17601	return { modal: dialog };
17602};
17603
17604var downloadUsageDialogInit = function () {
17605	var dialog, form;
17606
17607	function submit() {
17608		var data = serializeFormWithCheckboxes(form.find('input'));
17609
17610		ajaxDownloadGet('/api/download/usage', data)
17611	}
17612
17613	var isyOptions = {
17614		autoOpen: false,
17615		width: 650,
17616		modal: true,
17617		buttons: {
17618			'Download': function() {
17619				submit();
17620			},
17621			Cancel: function() {
17622				$(this).dialog('close');
17623			}
17624		},
17625		open: function(event, ui) {
17626			dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
17627			$('.custom-combobox-input').focus();
17628		}
17629	};
17630
17631	// Initializes the dialog
17632	dialog = $('#downloadUsage_dialog').dialog(isyOptions);
17633
17634	// Handles keyboad enter (submit event)
17635	form = dialog.find('form').submit(function(event) {
17636		event.preventDefault();
17637		submit();
17638	});
17639
17640	return { modal: dialog };
17641};
17642
17643
17644var downloadStatsDialogInit = function () {
17645	var dialog, form;
17646
17647	function submit() {
17648		ajaxDownloadGet('/api/isy/stats/download')
17649	}
17650
17651	var isyOptions = {
17652		autoOpen: false,
17653		width: 450,
17654		modal: true,
17655		buttons: {
17656			'Download': function() {
17657				submit();
17658			},
17659			Cancel: function() {
17660				$(this).dialog('close');
17661			}
17662		},
17663		open: function(event, ui) {
17664			dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
17665			$('.custom-combobox-input').focus();
17666		}
17667	};
17668
17669	// Initializes the dialog
17670	dialog = $('#downloadStats_dialog').dialog(isyOptions);
17671
17672	// Handles keyboad enter (submit event)
17673	form = dialog.find('form').submit(function(event) {
17674		event.preventDefault();
17675		submit();
17676	});
17677
17678	return { modal: dialog };
17679};
17680
17681
17682
17683
17684// ISY Portal Plugin purchase dialog
17685var nsPurchaseDialogInit = function () {
17686	var paypalButtonTarget = '#nodeserver_purchase_paypal_button',
17687		dialog,
17688		pid, nsid,
17689		redirectUrl,
17690		nodeserverInfo,
17691		allOptions, // All the available purchase options from the API
17692		editionOptions, // Valid edition options (Free, Subscription, Perpetual...)
17693		purchaseOptions, // Valid purchase ptions (based on selected edition and current license)
17694		installed, // Current nodeServer installed. None if first.
17695		renewalStartDate,
17696		requestBeta,
17697		ppCurrentEnv=serverConfig.ppEnv;
17698
17699	function setFields(obj) {
17700		pid = obj.pid;
17701		nsid = obj.nsid;
17702		redirectUrl = obj.redirectUrl;
17703		nodeserverInfo = obj.nodeserverInfo;
17704		requestBeta = obj.requestBeta;
17705		installed = obj.installed;
17706
17707		// ppCurrentEnv = (typeof obj.isTest === 'boolean')?
17708		// 	(obj.isTest ? 'sandbox' : 'production')
17709		// 	: serverConfig.ppEnv; // If not specified, will take the paypal env from the server config
17710
17711		$('#nodeserver_purchase_pid').text(obj.pid);
17712		$('#nodeserver_purchase_alias').text(obj.alias);
17713
17714		var licenseExpiryHtml='';
17715		var licenseActiveHtml='';
17716		var editionHtml='';
17717		var versionHtml='';
17718
17719		// If we don't have a license
17720		if (!obj.installed) {
17721			editionHtml = versionHtml = licenseExpiryHtml = '<span title="Plugin has to be purchased.">[No License]</span>';
17722			licenseActiveHtml='<span title="Plugin has to be purchased.">No</span>'
17723		} else {
17724			if (obj.installed.expiry===null) {
17725				licenseExpiryHtml = '<span>No expiration</span>'
17726			} else if (obj.installed.expiry) {
17727				var expiryDate = new Date(obj.installed.expiry);
17728
17729				// Set access license expired flag
17730				var expired = expiryDate.valueOf() < new Date().valueOf();
17731
17732				// Set access license will be expired soon flag
17733				var soon = new Date();
17734				soon.setDate(soon.getDate() + accessLicenseExpirationSoonDays);
17735				var soonExpired = !expired && expiryDate.valueOf() < soon.valueOf();
17736
17737				licenseExpiryHtml = '<span>' + dateFormatNoError(expiryDate, "yyyy-mm-dd") +'</span>' +
17738					(expired?'<span class="red expired" title="Plugin license is expired.">expired</span>':'') +
17739					(soonExpired?'<span class="warning expired" title="Plugin License is expiring soon."><span class="ui-icon ui-icon-alert"></span></span>':'')
17740			}
17741
17742			var licenseActive = obj.installed.active && !expired;
17743
17744			licenseActiveHtml = '<span>' + (licenseActive?'Yes':'No') + '</span>'
17745			editionHtml = '<span>' + obj.installed.edition + '</span>'
17746			versionHtml = '<span>' + obj.installed.version + '</span>'
17747		}
17748
17749		$('#nodeserver_name').html(obj.nodeserverInfo.desc + (nodeserverInfo.isBeta ? ' (Beta)' : '')).tooltip();
17750		$('#nodeserver_purchase_edition').html(editionHtml).tooltip();
17751		$('#nodeserver_purchase_version').html(versionHtml).tooltip();
17752		$('#nodeserver_purchase_active').html(licenseActiveHtml).tooltip();
17753		$('#nodeserver_purchase_expiry').html(licenseExpiryHtml).tooltip();
17754
17755		renewalStartDate = (installed && installed.expiry && new Date(installed.expiry)) || new Date();
17756
17757		// Extend allOption.
17758		allOptions = obj.nodeserverInfo.purchaseOptions.map(function (option, index) {
17759			return Object.assign({}, option, { optionID: index })
17760		})
17761		.map(function (option) {
17762			if (!option.description) {
17763				// option.recurring is supposed to be an integer. If a string, the end date will be miscalculated.
17764				if (typeof option.recurring === 'string' && !option.isPerpetual) {
17765					option.recurring = parseInt(option.recurring);
17766				}
17767
17768				if ('price' in option && typeof option.price === 'string') {
17769					option.price = parseFloat(option.price);
17770				}
17771
17772
17773				// Adjust the descriptions
17774				if (option.isTrial) {
17775					option.description = 'Trial License (' + option.recurring + ' ' + option.recurringPeriod + (option.recurring>1?'s':'') + ')'
17776					option.descriptionForSku = option.description
17777				} else if (option.isPerpetual) {
17778					if (option.isUpgrade) {
17779						option.description =  'Perpetual License (Version upgrade to ' + option.version + ')'
17780						option.descriptionForSku =  'Perpetual-Upgrade'
17781					} else if (option.isTierUpgrade) {
17782						option.description =  'Perpetual License (Tier upgrade to ' + option.to + ')'
17783						option.descriptionForSku =  'Perpetual-TierUpgrade'
17784					} else {
17785						option.description =  'Perpetual License - Version ' + option.version
17786						option.descriptionForSku =  'Perpetual License'
17787					}
17788				} else if (option.isSubscription) {
17789					// Example 90 days
17790					var recurringStr = option.recurring + ' ' + option.recurringPeriod + (option.recurring>1?'s':'')
17791
17792					if (option.isUpgrade) {
17793						option.description = 'Subscription (' + recurringStr + ' - Version upgrade to ' + option.version + ')'
17794						option.descriptionForSku = 'Subscription-Upg (' + option.recurring + ' ' + option.recurringPeriod + (option.recurring>1?'s':'') + ')'
17795					} else if (option.isTierUpgrade) {
17796						option.description = 'Subscription (' + recurringStr + ' - Tier upgrade to ' + option.to + ')'
17797						option.descriptionForSku = 'Subscription-TierUpg (' + option.recurring + ' ' + option.recurringPeriod + (option.recurring>1?'s':'') + ')'
17798					} else {
17799						option.description = 'Subscription for version ' + option.version + '  (' + recurringStr + ')'
17800						option.descriptionForSku = 'Subscription (' + option.recurring + ' ' + option.recurringPeriod + (option.recurring>1?'s':'') + ')'
17801					}
17802				} else {
17803					option.description = '(Invalid option)'
17804				}
17805			}
17806
17807			if (option.isTrial) {
17808				option.price = 0;
17809			}
17810
17811			option.part = nsid;
17812
17813			// option.descriptionForSku is the description we were previously using before adding support for version & tier upgrades
17814			// We keep using this for the partDescription so it matches previous descriptions in QB Online
17815			option.partDescription = nodeserverInfo.name + ' - ' +  option.descriptionForSku;
17816
17817			return option
17818		})
17819
17820		// This should match all the possible edition options
17821		// We could rely on the content of the options only, but we want to sort in that order
17822		var allEditions = [ 'Free', 'Standard', 'Professional' ]
17823
17824		// If for some reasons we have more edition options, add them
17825		// allOptions.forEach(function (opt) {
17826		// 	if (!allEditions.includes(opt.edition)) {
17827		// 		editions.push(opt.edition)
17828		// 	}
17829		// })
17830
17831		editionOptions = allEditions
17832		// Keep only if we have an option with the edition
17833		.filter(function(edition) {
17834			return allOptions
17835			.filter(function(option) {
17836				// We control on the server side which options are available
17837				return option.isAvailable
17838			})
17839			.find(function (p) {
17840				return p.edition === edition
17841			})
17842		})
17843		.map(function(edition, index) {
17844			return {
17845				value: index,
17846				description: edition,
17847				edition: edition  // We use it to display the correct purchase options for this edition
17848			}
17849		});
17850
17851		var lenEdition = editionOptions.reduce(function(summary, element) {
17852			return Math.max(summary, element.description.length)
17853		}, 0);
17854		var emEdition = (lenEdition*0.55)+5; // Formula found by trial and error
17855
17856		buildSelect(editionOptions, '#nodeserver_edition_selector', ''+emEdition+'em', function(event, ui) {
17857			displayPurchaseOptions(ui.item.value)
17858		});
17859
17860		if (editionOptions.length) {
17861			// Always take the first edition to start with
17862			displayPurchaseOptions(editionOptions[0].value)
17863		} else {
17864			errorMessageDialog('There are no purchase options available.')
17865			throw Error('No purchase options available') // Prevent displaying the purchase dialog
17866		}
17867	}
17868
17869	// Called when the edition changes
17870	function displayPurchaseOptions(id) {
17871		var edition = editionOptions[id].edition; // id is the index in the array
17872
17873		purchaseOptions = allOptions
17874		.filter(function(option) {
17875			// We control on the server side which options are available
17876			return option.isAvailable
17877		})
17878		// Remove trial option if this nodeserver was already used for this pid
17879		.filter(function(opt) {
17880			return opt.edition === edition
17881		})
17882		.map(function(option) {
17883			return {
17884				value: option.optionID,
17885				description: option.description
17886			}
17887		})
17888		.sort(function (a, b) {
17889			return a.description.localeCompare(b.description)
17890		})
17891
17892		var lenPurchase = purchaseOptions.reduce(function(summary, element) {
17893			return Math.max(summary, element.description.length)
17894		}, 0);
17895
17896		var emPurchase = (lenPurchase*0.55)+5; // Formula found by trial and error
17897
17898		buildSelect(purchaseOptions, '#nodeserver_purchase_selector', ''+emPurchase+'em', function(event, ui) {
17899			displayPaymentInfo(ui.item.value)
17900		});
17901
17902		// Always take the first option
17903		displayPaymentInfo(purchaseOptions[0].value);
17904	}
17905
17906	// Called when the renewal option changes.
17907	function displayPaymentInfo(id) {
17908		var option = allOptions[id]; // id is the index in the array
17909
17910		if (option) {
17911			var newExpiry
17912			var newExpiryStr
17913			var startDateStr
17914
17915			if (option.isPerpetual) {
17916				startDateStr = dateFormatNoError(new Date(), "yyyy-mm-dd");
17917				newExpiryStr = '[No Expiry]'
17918			} else {
17919				// If trial or subcription
17920				newExpiry = new Date(renewalStartDate);
17921
17922				// If it's an upgrade or tier upgrade, we keep the same expiry date but sell at lower prorated price
17923				if (!option.isTierUpgrade && !option.isUpgrade) {
17924					startDateStr = dateFormatNoError(new Date(renewalStartDate), "yyyy-mm-dd");
17925
17926					switch (option.recurringPeriod) {
17927						case 'day':
17928							newExpiry.setDate(newExpiry.getDate() + option.recurring);
17929							break;
17930
17931						case 'month':
17932							newExpiry.setMonth(newExpiry.getMonth() + option.recurring);
17933							break;
17934
17935						case 'year':
17936							newExpiry.setFullYear(newExpiry.getFullYear() + option.recurring);
17937							break;
17938
17939						default:
17940							console.log('recurringPeriod is invalid: ' + option.recurringPeriod)
17941					}
17942				} else {
17943					startDateStr = dateFormatNoError(new Date(), "yyyy-mm-dd");
17944				}
17945
17946				newExpiryStr = dateFormatNoError(newExpiry, "yyyy-mm-dd")
17947			}
17948
17949			var startDate = dateFormatNoError(renewalStartDate, "yyyy-mm-dd");
17950			$('#nodeserver_purchase_start').html(startDateStr);
17951			$('#nodeserver_purchase_end').html(newExpiryStr); //.tooltip();
17952
17953			if (option.isSubscription && (option.isTierUpgrade || option.isUpgrade) && option.proratedPrice) {
17954				$('#nodeserver_purchase_price').text(option.proratedPrice.toFixed(2) + '$ (Prorated)');
17955			} else {
17956				$('#nodeserver_purchase_price').text(option.price.toFixed(2) + '$');
17957			}
17958
17959			$('#nodeserver_purchase_price_row').toggle(!option.isTrial)
17960			$('#nodeserver_purchase_sandbox_indicator').toggle(!option.isTrial)
17961
17962			if (option.isTrial) {
17963				createTrialButton(option);
17964			} else if (option.isPerpetual && option.price === 0) {
17965				createFreeButton(option);
17966			} else {
17967				createPaypalButton(option);
17968			}
17969		}
17970		else {
17971			$('#nodeserver_purchase_start').empty();
17972			$('#nodeserver_purchase_end').empty();
17973			$('#nodeserver_purchase_price').empty();
17974		}
17975	}
17976
17977	// We pass the selector option
17978	function createPaypalButton(option) {
17979		$('#nodeserver_purchase_sandbox_indicator').text(ppCurrentEnv === 'sandbox' ? '[SANDBOX ENVIRONMENT]' : '');
17980
17981		$(paypalButtonTarget).empty(); // Each time we change the option, we rebuild the button
17982
17983		paypal.Buttons({
17984
17985			// NODE SERVER LEGACY PAYPAL WEB UI
17986			// createOrder: function(data, actions) {
17987			// 	sendLog({
17988			// 		tag: 'nsPurchase',
17989			// 		subTag: 'ppcreate',
17990			// 		uuid: pid,
17991			// 		message: 'Paypal Create',
17992			// 		content: {
17993			// 			option: option,
17994			// 			sku: option.partDescription,
17995			// 			name: option.part
17996			// 		}
17997			// 	});
17998			//
17999			// 	// Sample option
18000			// 	// {
18001			// 	// 	description: "Perpetual License"
18002			// 	// 	index: 1
18003			// 	// 	isPerpetual: true
18004			// 	// 	part: "0bec5267-b1c0-44e3-aa60-e1f84d1c5291"
18005			// 	// 	partDescription: "Airscape - Perpetual License"
18006			// 	// 	price: 10
18007			// 	// }
18008			//
18009			// 	var orderRequest = {
18010			// 		intent: 'CAPTURE',
18011			// 		purchase_units: [{
18012			// 			// custom_id: option.part,
18013			// 			description: option.part, // Looks like this is not used
18014			// 			amount: {
18015			// 				currency_code: 'USD',
18016			// 				value: option.price,
18017			// 				breakdown: {
18018			// 					item_total: {
18019			// 						currency_code: 'USD',
18020			// 						value: option.price
18021			// 					}
18022			// 				}
18023			// 			},
18024			// 			items: [{
18025			// 				sku: option.partDescription, // Second line on the receipt: "numéro de l'object <sku>"
18026			// 				name: option.part + ' - ' + pid,  // First line on the receipt
18027			// 				description: (option.description + ' pid: ' + pid).substring(0,127), // Does not show on the receipt
18028			// 				unit_amount: {
18029			// 					currency_code: 'USD',
18030			// 					value: option.price
18031			// 				},
18032			// 				quantity: 1
18033			// 			}]
18034			// 		}],
18035			// 		application_context: {
18036			// 			shipping_preference: 'NO_SHIPPING'
18037			// 		}
18038			// 	}
18039			//
18040			// 	// console.log('order', orderRequest)
18041			//
18042			// 	return actions.order.create(orderRequest)
18043			// },
18044			//
18045			// // Finalize the transaction after payer approval
18046			// onApprove: function(data, actions) {
18047			// 	return actions.order.capture().then(function(orderData) {
18048			// 		// Successful capture!
18049			// 		// console.log('Capture result', orderData, JSON.stringify(orderData, null, 2));
18050			// 		var orderID = orderData.id
18051			// 		// var payerID = orderData.payer.payer_id;
18052			// 		var transaction = orderData.purchase_units[0].payments.captures[0];
18053			// 		var paymentID = transaction.id; // Need this in case of error
18054			//
18055			// 		if (registeredUser) {
18056			// 			sendLog({
18057			// 				tag: 'nsPurchase',
18058			// 				subTag: 'ppapprove',
18059			// 				uuid: pid,
18060			// 				message: 'Paypal Approve',
18061			// 				content: {
18062			// 					data: orderData,
18063			// 				}
18064			// 			});
18065			//
18066			// 			ajaxJsonPost('/api/nodeserver/payment/execute', $.param({
18067			// 				pid: pid,
18068			// 				nsid: nsid,
18069			// 				optionID: option.optionID, // We use it to verify the transaction amount has not been tampered with
18070			// 				orderID: orderID,
18071			// 				paymentID: paymentID,
18072			// 				beta: nodeserverInfo.isBeta,
18073			// 				env: ppCurrentEnv
18074			// 			}))
18075			// 			.then(function(data, status, jqXHR) {
18076			// 				var message = 'Node Server transaction has been completed successfully.<br/><br/>Press Ok to continue.'
18077			//
18078			// 				informationMessageDialog(message)
18079			// 				.always(function() {
18080			// 					// dialog.dialog('close');
18081			// 					redirectSuccess();
18082			// 				})
18083			// 			})
18084			// 			.fail(function(jqXHR, textStatus, errorThrown) {
18085			// 				sendLog({
18086			// 					tag: 'nsPurchase',
18087			// 					subTag: 'ppapproveFailure',
18088			// 					uuid: pid,
18089			// 					message: 'Paypal Approve Failure',
18090			// 					content: {
18091			// 						textStatus: textStatus,
18092			// 						status: jqXHR && jqXHR.status,
18093			// 						responseText: jqXHR && jqXHR.responseText,
18094			// 						error: errorThrown
18095			// 					}
18096			// 				});
18097			//
18098			// 				var message = 'Paypal transaction completed, but we could not update your Node Server purchase.<br><br>' +
18099			// 					(jqXHR.status==425?'Error: ' + jqXHR.responseText + '<br><br>':'') +
18100			// 					'Please contact [email protected] and provide your transaction ID:<br>' + paymentID;
18101			//
18102			// 				errorMessageDialog(message, 'Error', 800)
18103			// 				// .always(function() {
18104			// 				// 	redirectError('Authorization failure');
18105			// 				// })
18106			// 			})
18107			// 			.done();
18108			// 		}
18109			// 		else {
18110			// 			errorMessageDialog('You are logged out of ISY Portal.<br>Transaction is cancelled.')
18111			// 			.always(function() {
18112			// 				redirectError('Transaction is cancelled');
18113			// 			})
18114			// 		}
18115			// 	});
18116			// }
18117
18118			createOrder: function(data) {
18119				var optionID = option.optionID
18120				var edition = option.edition
18121				console.log('Creating order for edition ' + edition + '. Payment source selected: ' + data.paymentSource)
18122
18123				// New server side createOrder
18124				return ajaxJsonPost('/api/nodeserver/order/create', $.param({
18125					uuid: pid,
18126					nsid: nsid,
18127					edition: edition,
18128					optionID: optionID,
18129					// This is to facilitate the receipt only
18130					// See setfields for details (allOptions map)
18131					partDescription: option.partDescription, // ns name + sku short desc
18132					part: option.part, 							 // nsid
18133					description: option.description, // Longer description
18134
18135					// optionID: optionID,
18136					paymentSource: data.paymentSource,
18137					beta: nodeserverInfo.isBeta,
18138					env: ppCurrentEnv
18139				}))
18140				.then(function(data, status, jqXHR) {
18141					return data.id
18142				})
18143				.fail(function(jqXHR, textStatus, errorThrown) {
18144					console.log('Order creation failed: ' + jqXHR.responseText)
18145				})
18146			},
18147
18148			// New server side order capture
18149			onApprove: function(data) {
18150				var orderID = data.orderID
18151				var paymentID = data.paymentID
18152				// var payerID = data.payerID
18153				var paymentSource = data.paymentSource
18154
18155				if (registeredUser) {
18156					return ajaxJsonPost('/api/nodeserver/order/capture', $.param({
18157						uuid: pid,
18158						paymentSource: paymentSource,
18159						env: ppCurrentEnv,
18160						orderID: orderID,
18161						// payerID,
18162						paymentID: paymentID,
18163						paymentSource: paymentSource
18164					}))
18165					.then(function(data, status, jqXHR) {
18166						var message = 'Transaction has been completed successfully.<br/><br/>Press Ok to continue.'
18167
18168						informationMessageDialog(message)
18169						.always(function() {
18170							redirectSuccess();
18171						})
18172					})
18173					.fail(function(jqXHR, textStatus, errorThrown) {
18174						var message = 'Transaction failed.<br><br>' +
18175							(jqXHR.status==425?'Error: ' + jqXHR.responseText + '<br><br>':'')
18176
18177						errorMessageDialog(message, 'Error', 800)
18178						// .always(function() {
18179						// 	redirectError('Authorization failure');
18180						// })
18181					})
18182				} else {
18183					errorMessageDialog('You are logged out.<br>Transaction is cancelled.')
18184					.always(function() {
18185						redirectError('Transaction is cancelled');
18186					})
18187				}
18188			},
18189
18190			onError: function(err) {
18191				console.log('Order error', err)
18192
18193				sendLog({
18194					tag: 'nsPurchase',
18195					subTag: 'orderError',
18196					uuid: pid,
18197					message: 'Order error: ' + (err && err.message),
18198					content: {}
18199				});
18200			},
18201
18202			onCancel: function(data) {
18203				console.log('Order cancelled')
18204
18205				sendLog({
18206					tag: 'nsPurchase',
18207					subTag: 'orderCancelled',
18208					uuid: pid,
18209					message: 'Order cancelled: ' + data.orderID,
18210					content: {
18211						orderID: data.orderID
18212					}
18213				});
18214			}
18215		}).render(paypalButtonTarget);
18216	}
18217
18218	function redirectSuccess() {
18219		window.location.replace(redirectUrl + '?successful=true');
18220	}
18221
18222	function redirectError(errMessage) {
18223		var redirect = redirectUrl + '?successful=false' + (errMessage && '&error=' + errMessage)
18224		window.location.replace(redirect);
18225	}
18226
18227	function createTrialButton(option) {
18228		var $input = $('<input type="button" value="Start trial now" />');
18229		$input.appendTo($(paypalButtonTarget).empty());
18230		$(paypalButtonTarget + ' input').button().click(function() {
18231			ajaxJsonPost('/api/nodeserver/trial', $.param({
18232				pid: pid,
18233				nsid: nsid,
18234				// domain: isyDomain,
18235				beta: nodeserverInfo.isBeta,
18236				optionID: option.optionID // We use it to verify the transaction amount has not been tampered with
18237			}))
18238			.then(function(data, status, jqXHR) {
18239				informationMessageDialog('Plugin trial activated successfully.<br/><br/>Press Ok to continue.')
18240				.always(function() {
18241					dialog.dialog('close');
18242					redirectSuccess();
18243				})
18244			})
18245			.fail(function(jqXHR, textStatus, errorThrown) {
18246				var message = 'Trial activation failed.<br><br>' +
18247					(jqXHR.status==425?'Error: ' + jqXHR.responseText + '<br><br>':'') +
18248					'Please contact [email protected] for support.';
18249
18250				errorMessageDialog(message, 'Error', 800)
18251				// .always(function () {
18252				// 	redirectError('Error activating trial.');
18253				// })
18254			})
18255			.done();
18256		});
18257	}
18258
18259	function createFreeButton(option) {
18260		var $input = $('<input type="button" value="Activate free license" />');
18261		$input.appendTo($(paypalButtonTarget).empty());
18262		$(paypalButtonTarget + ' input').button().click(function() {
18263			ajaxJsonPost('/api/nodeserver/free', $.param({
18264				pid: pid,
18265				nsid: nsid,
18266				// domain: isyDomain,
18267				beta: nodeserverInfo.isBeta,
18268				optionID: option.optionID // We use it to verify the transaction amount has not been tampered with
18269			}))
18270			.then(function(data, status, jqXHR) {
18271				informationMessageDialog('Plugin free license activated successfully.<br/><br/>Press Ok to continue.')
18272				.always(function() {
18273					dialog.dialog('close');
18274					redirectSuccess();
18275				})
18276			})
18277			.fail(function(jqXHR, textStatus, errorThrown) {
18278				var message = 'Free license activation failed.<br><br>' +
18279					(jqXHR.status==425?'Error: ' + jqXHR.responseText + '<br><br>':'') +
18280					'Please contact [email protected] for support.';
18281
18282				errorMessageDialog(message, 'Error', 800)
18283				// .always(function () {
18284				// 	redirectError('Error activating trial.');
18285				// })
18286			})
18287			.done();
18288		});
18289	}
18290
18291	// Keypressed, active only when this dialog is open.
18292	// Switch between prod and sandbox
18293	// function keyEvent(e) {
18294	// 	// Ctrl-z
18295	// 	if (e.which === 90 && e.ctrlKey && !e.shiftKey){
18296	// 		var refreshPaypalButton=false;
18297	//
18298	// 		if (ppCurrentEnv === 'sandbox') {
18299	// 			if (serverConfig.ppClientIdProduction) {
18300	// 				ppCurrentEnv='production';
18301	// 				refreshPaypalButton=true;
18302	// 			}
18303	// 			else {
18304	// 				errorMessageDialog('Production environment is not configured');
18305	// 			}
18306	// 		}
18307	// 		else if (ppCurrentEnv === 'production') {
18308	// 			if (serverConfig.ppClientIdSandbox) {
18309	// 				ppCurrentEnv='sandbox';
18310	// 				refreshPaypalButton=true;
18311	// 			}
18312	// 			else {
18313	// 				errorMessageDialog('Sandbox environment is not configured');
18314	// 			}
18315	// 		}
18316	//
18317	// 		if (refreshPaypalButton) {
18318	// 			displayPaymentOptions($('#nodeserver_purchase_selector select').val())
18319	// 		}
18320	// 	}
18321	// }
18322
18323	var isyOptions = {
18324		autoOpen: false,
18325		width: 850,
18326		modal: true,
18327		buttons: {
18328			'Cancel': function() {
18329				$(this).dialog('close');
18330			}
18331		},
18332		open: function(event, ui) {
18333			dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
18334			// $(document).on('keydown.pp', keyEvent);
18335		},
18336		close: function( event, ui ) {
18337			// $(document).off('keydown.pp');
18338			redirectError('Cancelled');
18339		}
18340	};
18341
18342	// Initializes the dialog
18343	dialog = $('#nodeserver_purchase_dialog').dialog(isyOptions);
18344
18345	return { modal: dialog, setFields: setFields };
18346};
18347
18348var udCommsEditDialogInit = function () {
18349	var dialog,
18350		form,
18351		editMode,
18352		typeTarget = '#ud-comms-type',
18353		appLinkTargetId = '#ud-comms-content-appLink-target',
18354		// commsTabForm = '#tabs-rateplan-rates-form',
18355		code = $('#ud-comms-code'),
18356		message = $('#ud-comms-message'),
18357		type = $(typeTarget),
18358		filterLicense = $('#ud-comms-filterLicense'),
18359		filterLicenseExpiryDays = $('#ud-comms-filterLicenseExpiryDays'),
18360		filterModel = $('#ud-comms-filterModel'),
18361		filterVersion = $('#ud-comms-filterVersion'),
18362		filterOnline = $('#ud-comms-filterOnline'),
18363		filterOnlineHours = $('#ud-comms-filterOnlineHours'),
18364		separateUuid = $('#ud-comms-separateUuid'),
18365		accountWithIsy = $('#ud-comms-accountWithIsy'),
18366		contentPush = $('#ud-comms-content-push'),
18367		surveyHeader = $('#ud-comms-content-survey-header'),
18368		surveyContainer = $('#ud-comms-content-survey-items'),
18369		contentAppLinkTextHeader = $('#ud-comms-content-appLink-text-header'),
18370		contentAppLinkTextFooter = $('#ud-comms-content-appLink-text-footer'),
18371		contentAppLinkTargetText = $('#ud-comms-content-appLink-target-text'),
18372		contentAppLinkTarget = $(appLinkTargetId),
18373		allFields = $([]).add(code).add(message).add(type)
18374		.add(filterLicense).add(filterModel).add(filterVersion).add(filterLicenseExpiryDays)
18375		.add(contentPush).add(surveyHeader),
18376		tableRefreshFunction = null,
18377		tips = $('#comms_edit_dialog .validateTips'),
18378		sentCount,
18379		lastSentTime,
18380		saveUrl = '/api/ud/communication',
18381		getCountUrl = '/api/ud/communication/count',
18382		sendUrl = '/api/ud/communication/send',
18383		testUrl = '/api/ud/communication/test'
18384
18385	// Used for the selectmenu
18386	var typesOptions = [
18387		{ value: 'push', description: 'Push', tabs: ['push'] },
18388		{ value: 'survey', description: 'Push Survey', tabs: ['survey'] },
18389		{ value: 'appLink', description: 'Push with App Link', tabs: ['appLink'] },
18390	];
18391
18392	var appLinkOptions = [
18393		{ value: 'home', description: 'Home' },
18394		{ value: 'notificationSettings', description: 'Notification Settings' },
18395		{ value: 'settings', description: 'Settings' },
18396		{ value: 'account', description: 'Account' },
18397		{ value: 'license', description: 'License' }
18398	];
18399
18400	var typeTabIds = {
18401		push: '#tab-ud-comms-push',
18402		survey: '#tab-ud-comms-survey',
18403		appLink: '#tab-ud-comms-appLink'
18404	}
18405
18406	function setFields(obj) {
18407		loadTinymce()
18408
18409		form[0].reset();
18410		editMode = obj && obj.editMode;
18411
18412		code.prop('readonly', obj && obj.editMode===true)
18413
18414		clearSurveyItems();
18415
18416		sentCount = obj.sentCount;
18417		lastSentTime = obj.lastSentTime;
18418
18419		if (editMode) {
18420			code.val(obj.code);
18421			message.val(obj.message);
18422			contentPush.val(obj.contentPush);
18423			filterLicenseExpiryDays.val(obj.filterLicenseExpiryDays);
18424			filterOnlineHours.val(obj.filterOnlineHours);
18425			filterModel.val(obj.filterModel);
18426			filterVersion.val(obj.filterVersion);
18427
18428			$(typeTarget + ' select').val(obj.type);
18429			$(typeTarget + ' select').selectmenu('refresh');
18430
18431			displayExpiredDaysField(obj.filterLicense)
18432			displayOnlineHoursField(obj.filterOnline)
18433			displayIsyFilters(obj.accountWithIsy)
18434
18435			$('#ud-comms-filterLicense input[name=filterLicense]').val([obj.filterLicense])
18436			$('#ud-comms-filterOnline input[name=filterOnline]').val([obj.filterOnline])
18437
18438			separateUuid.prop('checked', obj.separateUuid);
18439			accountWithIsy.prop('checked', obj.accountWithIsy);
18440
18441			try {
18442				var survey = obj.contentSurvey && JSON.parse(obj.contentSurvey)
18443				if (survey) {
18444					surveyHeader.val(survey.header)
18445
18446					survey.questions.forEach(function (element) {
18447						addSurveyItem(element)
18448					})
18449				} else {
18450					surveyHeader.val('')
18451				}
18452			} catch(err) {
18453				console.log('Error parsing survey data', err)
18454			}
18455
18456			try {
18457				var appLink = obj.contentAppLink && JSON.parse(obj.contentAppLink)
18458				if (appLink) {
18459					contentAppLinkTextHeader.val(appLink.contentHeader)
18460					contentAppLinkTextFooter.val(appLink.contentFooter)
18461					contentAppLinkTargetText.val(appLink.appTargetText)
18462					$(appLinkTargetId + ' select').val(appLink.appTarget);
18463					$(appLinkTargetId + ' select').selectmenu('refresh');
18464				} else {
18465					contentAppLinkTextHeader.val('')
18466					contentAppLinkTextFooter.val('')
18467					contentAppLinkTargetText.val('')
18468					$(appLinkTargetId + ' select').val(appLinkOptions[0].value);
18469					$(appLinkTargetId + ' select').selectmenu('refresh');
18470				}
18471			} catch(err) {
18472				console.log('Error parsing appLink data', err)
18473			}
18474
18475			tips.text('');
18476		} else {
18477			$(typeTarget + ' select').val(typesOptions[0].value);
18478			$(typeTarget + ' select').selectmenu('refresh');
18479
18480			contentAppLinkTextHeader.val('')
18481			contentAppLinkTextFooter.val('')
18482			contentAppLinkTargetText.val('')
18483			$(appLinkTargetId + ' select').val(appLinkOptions[0].value);
18484			$(appLinkTargetId + ' select').selectmenu('refresh');
18485
18486			$('#ee-comms-filterLicense input[name=filterLicense]').val([''])
18487			$('#ee-comms-filterModel input[name=filterModel]').val([''])
18488			$('#ee-comms-filterVersion input[name=filterVersion]').val([''])
18489
18490			displayExpiredDaysField('')
18491			displayOnlineHoursField('')
18492			displayIsyFilters(true)
18493
18494			// When adding, always start with the first tab
18495			$('#ee-comms-tabs').tabs('option', 'active', 0);
18496
18497			tips.text('Please enter the communication details');
18498		}
18499
18500		refreshTabs($(typeTarget + ' select').val())
18501
18502		allFields.removeClass('fieldfocus');
18503	}
18504
18505	function validatePrepareSaveData() {
18506		var valid = true;
18507		var surveyElements = []
18508		var $field
18509
18510		allFields.removeClass('fieldfocus');
18511
18512		valid = valid && checkLength(tips, code, 'Code', 1, 50); // 0 length = error message!
18513		valid = valid && checkLength(tips, message, 'Message', 1, 178); // 0 length = error message!
18514
18515		if (valid) {
18516			surveyContainer.find('li.surveyElement').each(function () {
18517				var element = {}
18518
18519				$field = $(this).find('[data-id="question"]')
18520				element.question = $field.val();
18521				valid = valid && checkLength(tips, $field, 'Question', 1, 120);
18522
18523				element.type = $(this).find('[data-id="type"]').find(':selected').val();
18524
18525				switch (element.type) {
18526					case 'radio':
18527					case 'checkbox':
18528						$field = $(this).find('[data-id="options"]')
18529						element.options = $field.val().split(/[;]/).map(function(opt) { return opt.trim() }); // Splits using , or ;
18530						valid = valid && checkRegexp(tips, $field, /[;]/, 'Options must have semicolons as a separator.' );
18531						break;
18532
18533					case 'rating':
18534						element.min = $(this).find('[data-id="min"]').val();
18535						element.max = $(this).find('[data-id="max"]').val();
18536				}
18537
18538				surveyElements.push(element)
18539			});
18540		}
18541
18542		if (valid) {
18543			var contentSurvey = {
18544				header: surveyHeader.val(),
18545				questions: surveyElements
18546			}
18547			var contentAppLink = {
18548				contentHeader: contentAppLinkTextHeader.val(),
18549				contentFooter: contentAppLinkTextFooter.val(),
18550				appTarget: $(appLinkTargetId + ' select').val(),
18551				appTargetText: contentAppLinkTargetText.val(),
18552			}
18553			return serializeFormWithCheckboxes(form.find('input, textarea')) +
18554				'&type=' + $(typeTarget + ' select').val() +
18555				'&contentSurvey=' + encodeURIComponent(JSON.stringify(contentSurvey)) +
18556				'&contentAppLink=' + encodeURIComponent(JSON.stringify(contentAppLink))
18557		} else {
18558			return null
18559		}
18560	}
18561
18562	// Used for save and send
18563	function saveFailure (jqXHR, textStatus, errorThrown) {
18564		updateTips(tips, 'HTTP error ' + jqXHR.status);
18565	}
18566
18567	// Save as draft
18568	function submit() {
18569		var data = validatePrepareSaveData()
18570
18571		if (data) {
18572			(editMode?ajaxJsonPost(saveUrl, data):ajaxJsonPut(saveUrl, data))
18573			.then(function(result, status, jqXHR) {
18574				tableUdComms.reload()
18575				if (!result.successful) {
18576					errorMessageDialog(result.errorMessage, 'Error')
18577				} else {
18578					dialog.dialog('close')
18579				}
18580			})
18581			.fail(saveFailure)
18582			.done();
18583		}
18584	}
18585
18586	// Hide/Display the days field, based on filterLicense
18587	function displayExpiredDaysField(filterLicense) {
18588		$('#ud-comms-filterLicenseExpiryDaysUI').toggle(['expiryIn', 'expiredSince'].includes(filterLicense))
18589	}
18590
18591	function displayOnlineHoursField(filterOnline) {
18592		// Disable the online hours field if filterOnline is set to no filter
18593		$('#ud-comms-filterOnlineHoursUI').toggle(typeof filterOnline === 'string' && filterOnline!=='')
18594	}
18595
18596	function displayIsyFilters(accountWithIsy) {
18597		// Disable the online hours field if filterOnline is set to no filter
18598		$('#ud-comms-isyFilters').toggle(accountWithIsy)
18599	}
18600	function send() {
18601		var data = validatePrepareSaveData()
18602
18603		if (data) {
18604			// First save the record
18605			(editMode?ajaxJsonPost(saveUrl, data):ajaxJsonPut(saveUrl, data))
18606			.then(function(result, status, jqXHR) {
18607				tableUdComms.reload()
18608
18609				if (result.successful) {
18610					if (sentCount) {
18611						confirmationDialog('WARNING: This communication was already sent on ' + dtStr(new Date(lastSentTime)) + ' to ' + sentCount + ' accounts.<br/><br/>Do you want to continue?', 'Send this communication?', 600)
18612						.then(function () {
18613							sendContinue()
18614						})
18615						.done()
18616					} else {
18617						sendContinue()
18618					}
18619				} else {
18620					errorMessageDialog(result.errorMessage, 'Error')
18621					// Leave the dialog open.
18622				}
18623			})
18624			.fail(saveFailure)
18625			.done();
18626		}
18627	}
18628
18629	function sendContinue() {
18630		ajaxJsonGet(getCountUrl, $.param({ code: code.val() }))
18631		.then(function(result, status, jqXHR) {
18632			if (result.successful) {
18633				var accountCount = result.data.accountCount
18634
18635				if (accountCount>1) {
18636					confirmationDialog('This communication will be sent to ' + accountCount + (accountCount>1?' accounts':' account') +
18637						'.<br/><br/>Do you want to continue?', 'Send this communication?', 600)
18638					.then(function () {
18639						// If user confirms he wants to send, go ahead
18640						// TODO: When testing, no notifications are sent except to test devTestAccounts
18641						ajaxJsonPost(sendUrl, $.param({ code: code.val(), test: false }))
18642						.then(function(result, status, jqXHR) {
18643							if (result.successful) {
18644								var successMessage = 'Communication successfully sent to ' + result.data.accountCount + ' accounts.'
18645
18646								informationMessageDialog(successMessage, 'Success', 600)
18647								.then(function () {
18648									tableUdComms.reload() // Refresh the last Sent date
18649									dialog.dialog('close')
18650								})
18651							} else {
18652								errorMessageDialog(result.errorMessage, 'Error')
18653								// Leave the dialog open.
18654							}
18655						})
18656						.fail(saveFailure)
18657						.done();
18658					})
18659					.done();
18660				} else {
18661					errorMessageDialog('There are no accounts matching your criterias.', 'Error')
18662					// Leave the dialog open.
18663				}
18664			} else {
18665				errorMessageDialog(result.errorMessage, 'Error')
18666				// Leave the dialog open.
18667			}
18668		})
18669		.fail(saveFailure)
18670		.done();
18671	}
18672
18673	// Sends a notification to UDI admins only
18674	function test() {
18675		var data = validatePrepareSaveData()
18676
18677		if (data) {
18678			// First save the record
18679			(editMode?ajaxJsonPost(saveUrl, data):ajaxJsonPut(saveUrl, data))
18680			.then(function(result, status, jqXHR) {
18681				tableUdComms.reload()
18682
18683				if (result.successful) {
18684					ajaxJsonPost(testUrl, $.param({ code: code.val() }))
18685					.then(function(result, status, jqXHR) {
18686						if (result.successful) {
18687							var successMessage = 'Communication successfully sent to ' + result.data.accountCount + ' account.'
18688
18689							informationMessageDialog(successMessage, 'Success', 600)
18690							.then(function () {
18691								tableUdComms.reload() // Refresh the last Sent date
18692								dialog.dialog('close')
18693							})
18694						} else {
18695							errorMessageDialog(result.errorMessage, 'Error')
18696							// Leave the dialog open.
18697						}
18698					})
18699					.fail(saveFailure)
18700					.done();
18701				} else {
18702					errorMessageDialog(result.errorMessage, 'Error')
18703					// Leave the dialog open.
18704				}
18705			})
18706			.fail(saveFailure)
18707			.done();
18708		}
18709	}
18710
18711	function simulation() {
18712		var data = validatePrepareSaveData()
18713
18714		if (data) {
18715			// First save the record
18716			(editMode?ajaxJsonPost(saveUrl, data):ajaxJsonPut(saveUrl, data))
18717			.then(function(result, status, jqXHR) {
18718				tableUdComms.reload()
18719
18720				if (result.successful) {
18721					confirmationDialog('This will download the list of accounts matching your filter criterias.<br/><br/>Do you want to continue?', 'Download account list?', 600)
18722					.then(function () {
18723						ajaxJsonPost(sendUrl, $.param({ code: code.val(), simulation: 'true' }))
18724						.then(function (result, status, jqXHR) {
18725							if (result.successful) {
18726								try {
18727									var isFileSaverSupported = !!new Blob;
18728								} catch (e) {}
18729
18730								if (isFileSaverSupported) {
18731									var str = 'accountId,Account Short Name, Customer Contact, Notification Count, uuids\r\n';
18732
18733									result.data.list.forEach(function(f) {
18734										str += `${f.id},"${f.shortname}","${f.contactname}",${f.deviceCount},${f.uuids && f.uuids.join(',') || ''}\r\n`
18735									});
18736
18737									var BOM = '\uFEFF';
18738									var blob = new Blob([BOM + str], {type: "application/csv;charset=utf-8"});
18739									saveAs(blob, "AccountList.csv");
18740								}
18741								else {
18742									errorMessageDialog('Feature not supported on this browser', 'Not supported');
18743								}
18744							} else {
18745								errorMessageDialog(result.errorMessage, 'Error')
18746								// Leave the dialog open.
18747							}
18748						})
18749						.fail(saveFailure)
18750						.done();
18751					})
18752					.done();
18753				} else {
18754					errorMessageDialog(result.errorMessage, 'Error')
18755				}
18756			})
18757			.fail(saveFailure)
18758			.done();
18759		}
18760	}
18761
18762	function help () {
18763		var message = ['Message ID: Enter a unique ID of your choice. You can use letters and numbers.',
18764				'Message: This is the content of the push notification, alexa message and/or text message.',
18765				'Channel: Choose how you want the notification to be delivered:',
18766			].map(function (s) { return '<p class="littleMargin5">' + s + '</p>'} ).join('') +
18767			'<ul class="noMargin">' +
18768			[ 'Push: Send a push notification to accounts who have UD Communications enabled',
18769				'Push Survey: Sends a push notification asking for a survey',
18770				'Push with App link: Sends a push notification with a link inside the app',
18771			].map(function (s) { return '<li>' + s + '</li>'} ).join('') + '</ul>' +
18772			[ 'Filter License: You can send only to accounts who have an ISY with a license, or not.',
18773				'Filter Online: You can send only to accounts with ISYs online or not. Enter hours to include ISYs recently online.',
18774				'Filter Model: You can send only to accounts with a specific model. Separate them with comma. You can use ? and *.',
18775				'Include only accounts with ISYs: Deselect to include all portal accounts.',
18776				'Send separate notifications for each uuid: Deselect to prevent multiple notifications to the same users.',
18777				'Push content: This is what will be shown in the App when the user clicks on the push notification.',
18778				'Push Survey: Contains the survey header, along with a series of questions. If using a Radio Button or checkbox, please enter the options separated with a semicolon.',
18779				'&nbsp',
18780				'Send button: This will tell you the number of accounts applicable, and ask you for confirmation before sending.',
18781				'Simulation button: Allows you to download the list of accounts applicable.',
18782				'Save as draft: Save the communication for later use.',
18783			].map(function (s) { return '<p class="littleMargin5">' + s + '</p>'} ).join('')
18784
18785		informationMessageDialog(message, 'Help', 1100)
18786	}
18787
18788	function refreshTabs(selected) {
18789		// List of tabs to enable/disable based on chosen type
18790		var typeTabsToEnable = typesOptions.find(function (typeOption) {
18791			return typeOption.value === selected
18792		}).tabs
18793
18794		// For all possible tabs
18795		Object.keys(typeTabIds).forEach(function (key) {
18796			$(typeTabIds[key]).toggle(typeTabsToEnable.includes(key));
18797		})
18798
18799		// When switching type, show the main tab
18800		$('#ud-comms-tabs').tabs('option', 'active', 0);
18801	}
18802
18803	function clearSurveyItems() {
18804		surveyContainer.empty();
18805	}
18806
18807	function addSurveyItem(itemOptions) {
18808		var $content = $(`
18809        <li class="ui-state-default surveyElement">
18810            <div>
18811                <div>
18812                    <div class="floatLeft label label1"><label>Question:</label></div>
18813                    <div class="floatRight deleteButton"><div><span class="ui-icon ui-icon-closethick"></span></div></div>
18814                    <div class="remainingSpace"><input class="width100" type="text" data-id="question" value="${itemOptions && itemOptions.question || ''}"></div>
18815                </div>
18816                <div>
18817                    <div class="floatLeft label label1"><label>Type:</label></div>
18818                    <select class="floatLeft" data-id="type">
18819                        <option value="textbox" ${itemOptions && itemOptions.type==='textbox' && 'selected'}>Textbox</option>
18820                        <option value="radio" ${itemOptions && itemOptions.type==='radio' && 'selected'}>Radio Button</option>
18821                        <option value="checkbox" ${itemOptions && itemOptions.type==='checkbox' && 'selected'}>Checkbox</option>
18822                        <option value="rating" ${itemOptions && itemOptions.type==='rating' && 'selected'}>Rating</option>
18823                    </select>
18824                    <span class="typeDependent type-radio type-checkbox" style="display:none;">
18825                        <div class="floatLeft label"><label>Options:</label></div>
18826                        <div class="remainingSpace"><input class="width100" data-id="options" type="text" value="${itemOptions && itemOptions.options && itemOptions.options.join(';') || ''}"></div>
18827                    </span>
18828                    <span class="typeDependent type-rating" style="display:none;">
18829                        <div class="floatLeft label"><label>From:</label></div>
18830                        <div class="floatLeft"><input type="number" min="0" max="1" step="1" data-id="min" value="${itemOptions && itemOptions.min || '1'}"></div>
18831                        <div class="floatLeft label"><label>To:</label></div>
18832                        <div class="floatLeft"><input type="number" min="1" max="10" step="1" data-id="max" value="${itemOptions && itemOptions.max || '5'}"></div>
18833                    </span>
18834                </div>
18835            </div>
18836        </li>`);
18837
18838		$content.find('.deleteButton').click(function (event) {
18839			event.preventDefault(); // Prevent submitting the form
18840
18841			confirmationDialog('Do you want to remove this question?', 'Delete?')
18842			.then(function () {
18843				$(event.target).closest('li.surveyElement').remove()
18844			})
18845			.done();
18846		})
18847
18848		$content.find('select').change(function (event) {
18849			var option = this.value;
18850			var selector = '.type-' + option
18851			$(event.target).closest('.surveyElement').find('.typeDependent').not(selector).hide();
18852			$(event.target).closest('.surveyElement').find(selector).show();
18853		});
18854
18855		// Trigger a change event so that appropriate options are displayed
18856		$content.find('select').change()
18857
18858		surveyContainer.append($content);
18859	}
18860
18861	var dialogOptions = {
18862		autoOpen: false,
18863		width: 1000,
18864		modal: true,
18865		buttons: {
18866			'Help': help,
18867			'Send': send,
18868			'Test': test,
18869			'Simulation': simulation,
18870			'Save as draft': submit,
18871		},
18872		open: function(event, ui) {
18873			dialog.dialog( "option", "position", { my: "center", at: "center+10", of: window });
18874		}
18875	};
18876
18877	// Initializes the dialog
18878	dialog = $('#ud-comms-edit-dialog').dialog(dialogOptions);
18879
18880	buildSelect(typesOptions, typeTarget, '20em', function(event, ui) {
18881		refreshTabs(ui.item.value)
18882	});
18883
18884	buildSelect(appLinkOptions, appLinkTargetId, '20em');
18885
18886	$('#ud-comms-tabs').tabs()
18887
18888	// // Push notifications editor
18889	// $('textarea#ud-comms-content-push, textarea#ud-comms-content-appLink-text-header, textarea#ud-comms-content-appLink-text-footer, textarea#ipal_notification_option_pushContent').tinymce({
18890	// 	selector: 'textarea#ud-comms-content-push',
18891	// 	plugins: 'lists link',
18892	// 	toolbar: 'undo redo | styles | bold italic | bullist numlist | alignleft aligncenter alignright al
18892ignjustify | outdent indent | link',
18893	// 	// convert_newlines_to_brs : true,
18894	// 	// forced_root_block : false
18895	// 	// toolbar: 'bullist numlist',
18896	// 	// inline: true
18897	// 	// plugins: 'a11ychecker advcode casechange export formatpainter image editimage linkchecker autolink lists checklist media mediaembed pageembed permanentpen powerpaste table advtable tableofcontents tinycomments tinymcespellchecker',
18898	// 	// toolbar: 'a11ycheck addcomment showcomments casechange checklist code export formatpainter image editimage pageembed permanentpen table tableofcontents',
18899	// 	// toolbar_mode: 'floating',
18900	// 	// tinycomments_mode: 'embedded',
18901	// 	// tinycomments_author: 'Author name',
18902	// });
18903	//
18904	// // Editor for emails
18905	// $('textarea#ipal_notification_option_emailBody').tinymce({
18906	// 	selector: 'textarea#ud-comms-content-push',
18907	// 	plugins: 'lists',
18908	// 	toolbar: 'undo redo | styles | bold italic | bullist numlist | alignleft aligncenter alignright alignjustify | outdent indent | link',
18909	// 	plugins: 'link',
18910	// 	// br_in_pre: false
18911	// 	convert_newlines_to_brs : true,
18912	// 	// forced_root_block : false
18913	// 	// toolbar: 'bullist numlist',
18914	// 	// inline: true
18915	// 	// plugins: 'a11ychecker advcode casechange export formatpainter image editimage linkchecker autolink lists checklist media mediaembed pageembed permanentpen powerpaste table advtable tableofcontents tinycomments tinymcespellchecker',
18916	// 	// toolbar: 'a11ycheck addcomment showcomments casechange checklist code export formatpainter image editimage pageembed permanentpen table tableofcontents',
18917	// 	// toolbar_mode: 'floating',
18918	// 	// tinycomments_mode: 'embedded',
18919	// 	// tinycomments_author: 'Author name',
18920	// });
18921
18922
18923	// $('textarea#ud-comms-content-survey-header').tinymce({
18924	//   plugins: 'lists',
18925	//   toolbar: 'undo redo | styles | bold italic | bullist numlist | alignleft aligncenter alignright al
18925ignjustify | outdent indent',
18926	//   height : '200'
18927	// });
18928
18929	$('#ud-comms-content-survey-items').sortable();
18930
18931	// Add a question
18932	$('#ud-comms-content-survey-items-add').button().click(function (event) {
18933		event.preventDefault(); // Prevent submitting the form
18934		addSurveyItem()
18935	});
18936
18937	// Remove a question
18938	// $('#comms-content-survey-items .deleteButton').click(deleteHandler);
18939
18940	$('#ud-comms-filterLicense input[type=radio]').change(function(){
18941		displayExpiredDaysField($(this).val())
18942	})
18943
18944	$('#ud-comms-filterOnline input[type=radio]').change(function(){
18945		displayOnlineHoursField($(this).val())
18946	})
18947
18948	accountWithIsy.change(function(){
18949		displayIsyFilters($(this).is(':checked'))
18950	})
18951
18952	// Handles keyboad enter (submit event)
18953	form = dialog.find('form').submit(function(event) {
18954		event.preventDefault();
18955		submit();
18956	});
18957
18958	return { modal: dialog, setFields: setFields };
18959};
18960
18961var udCommsSurveyDialogInit = function () {
18962	var tabsId = '#ud-comms-surveys-tabs'
18963	var tabsContentId = '#ud-comms-surveys-tab'
18964	var dialog
18965
18966	function setFields(obj) {
18967		createTabs(tabsId, tabsContentId, [ 'Notifications' ])
18968		createTabCanvas(tabsContentId, 0, 'Survey notifications summary')
18969
18970		ajaxJsonGet('/api/ud/survey/stats', $.param({ code: obj.code, stats: true, data:false }))
18971		.done(function(apiData, status, jqXHR){
18972			var questions = apiData.data.stats.filter(function (q) { return q.type !== 'textbox'})
18973			addTabs(tabsId, tabsContentId, questions.map(function (q, index) { return 'Question#' + (index+1)}))
18974
18975			// Notifications tab
18976			createChart(tabsContentId, 0, {
18977				type: 'pie',
18978				data: {
18979					labels: [
18980						'Not acknowledged',
18981						'Acknowledged but not responded',
18982						'Survey received'
18983					],
18984					datasets: [{
18985						data: [apiData.data.stage.total - apiData.data.stage.acknowledged,
18986							apiData.data.stage.acknowledged - apiData.data.stage.responded,
18987							apiData.data.stage.responded],
18988						backgroundColor: [
18989							'rgb(187,19,43, 0.8)',  // Red - Not acknowledged
18990							'rgb(236,233,57, 0.8)',  // Yellow - Acknowledged but not responded
18991							'rgb(90,211,80, 0.8)'   // Green - Survey received
18992						],
18993						hoverOffset: 4
18994					}]
18995				},
18996			})
18997
18998			for (var i=0; i < questions.length; i++) {
18999				createTabCanvas(tabsContentId, i+1, questions[i].question)
19000
19001				// Question tabs
19002				createChart(tabsContentId, i+1, {
19003					type: 'bar',
19004					data: {
19005						labels: Object.keys(questions[i].answers),
19006						datasets: [{
19007							data: Object.values(questions[i].answers),
19008							backgroundColor: [
19009								'rgba(255, 99, 132, 0.4)',
19010								'rgba(255, 159, 64, 0.4)',
19011								'rgba(255, 205, 86, 0.4)',
19012								'rgba(75, 192, 192, 0.4)',
19013								'rgba(54, 162, 235, 0.4)',
19014								'rgba(153, 102, 255, 0.4)',
19015								'rgba(201, 203, 207, 0.4)',
19016								'rgba(255, 99, 132, 0.4)',
19017								'rgba(255, 159, 64, 0.4)',
19018								'rgba(255, 205, 86, 0.4)',
19019								'rgba(75, 192, 192, 0.4)',
19020								'rgba(54, 162, 235, 0.4)',
19021								'rgba(153, 102, 255, 0.4)',
19022								'rgba(201, 203, 207, 0.4)',
19023								'rgba(255, 99, 132, 0.4)',
19024								'rgba(255, 159, 64, 0.4)',
19025								'rgba(255, 205, 86, 0.4)',
19026								'rgba(75, 192, 192, 0.4)',
19027								'rgba(54, 162, 235, 0.4)',
19028								'rgba(153, 102, 255, 0.4)',
19029								'rgba(201, 203, 207, 0.4)'
19030							],
19031							borderColor: [
19032								'rgb(255, 99, 132)',
19033								'rgb(255, 159, 64)',
19034								'rgb(255, 205, 86)',
19035								'rgb(75, 192, 192)',
19036								'rgb(54, 162, 235)',
19037								'rgb(153, 102, 255)',
19038								'rgb(201, 203, 207)',
19039								'rgb(255, 99, 132)',
19040								'rgb(255, 159, 64)',
19041								'rgb(255, 205, 86)',
19042								'rgb(75, 192, 192)',
19043								'rgb(54, 162, 235)',
19044								'rgb(153, 102, 255)',
19045								'rgb(201, 203, 207)',
19046								'rgb(255, 99, 132)',
19047								'rgb(255, 159, 64)',
19048								'rgb(255, 205, 86)',
19049								'rgb(75, 192, 192)',
19050								'rgb(54, 162, 235)',
19051								'rgb(153, 102, 255)',
19052								'rgb(201, 203, 207)'
19053							],
19054							borderWidth: 1,
19055							hoverOffset: 4
19056						}]
19057					},
19058					options: {
19059						plugins: {
19060							legend: {
19061								display: false
19062							}
19063						},
19064						scales: {
19065							// yAxes: [{
19066							//   ticks: {
19067							//     // beginAtZero: true,
19068							//     callback: function(value) {if (value % 1 === 0) {return value;}}
19069							//   }
19070							// }]
19071							y: {
19072								beginAtZero: true,
19073								ticks: {
19074									// For a category axis, the val is the index so the lookup via getLabelForValue is needed
19075									// callback: function(val, index) {
19076									//   // Hide every 2nd tick label
19077									//   return index % 2 === 0 ? this.getLabelForValue(val) 
19077: '';
19078									// },
19079									callback: function(value) {if (value % 1 === 0) {return value;}}
19080									// color: 'red',
19081								}
19082							}
19083						}
19084					},
19085				})
19086			}
19087		});
19088	}
19089
19090	var dialogOptions = {
19091		autoOpen: false,
19092		width: 700,
19093		height: 850,
19094		modal: true,
19095		buttons: {
19096		},
19097		// Done after drawing the canvas
19098		open: function(event, ui) {
19099			dialog.dialog( 'option', 'position', { my: 'center', at: 'center', of: window, collision: 'fit' });
19100		},
19101		close: function(event, ui) {
19102			$(tabsId).tabs('destroy')
19103		}
19104	};
19105
19106	// Initializes the dialog
19107	dialog = $('#ud-comms-survey-dialog').dialog(dialogOptions);
19108
19109	return { modal: dialog, setFields: setFields };
19110};
19111
19112
19113var udCommsDeliveryDialogInit = function () {
19114	var tabsId = '#ud-comms-delivery-tabs'
19115	var tabsContentId = '#ud-comms-delivery-tab'
19116	var dialog
19117
19118	function setFields(obj) {
19119		createTabs(tabsId, tabsContentId, [ 'Delivery' ])
19120		createTabCanvas(tabsContentId, 0, 'Notifications delivery summary')
19121
19122		ajaxJsonGet('/api/ud/communication/delivery/stats', $.param({ code: obj.code, stats: true, data:false }))
19123		.done(function(apiData, status, jqXHR){
19124
19125			var push = apiData.data.push
19126			var account = apiData.data.account
19127
19128			function labelFormatter(value, ctx) {
19129				if (!value) { return null }
19130
19131				var sum = datasets.reduce(function (sum, dataset) { return sum+dataset.data[ctx.dataIndex] }, 0)
19132				return Math.round(value / sum * 100) + '%'
19133			}
19134
19135			var datasets = [
19136				{
19137					label: 'Not delivered',
19138					data: [
19139						push.total - push.sent,
19140						account.total - account.sent
19141					],
19142					backgroundColor: 'rgb(194,64,91)',
19143					datalabels: {
19144						color: '#ffffff',
19145						formatter: labelFormatter
19146					}
19147				},
19148				{
19149					label: 'Sent',
19150					data: [
19151						push.sent - push.ack,
19152						account.sent - account.ack
19153					],
19154					backgroundColor: 'rgb(236,233,57)',
19155					datalabels: {
19156						color: '#ffffff',
19157						formatter: labelFormatter
19158					}
19159				},
19160				{
19161					label: 'Sent & Ack',
19162					data: [
19163						push.ack - push.archived,
19164						account.ack - account.archived
19165					],
19166					backgroundColor: 'rgb(79,180,71)',
19167					datalabels: {
19168						color: '#020202',
19169						formatter: labelFormatter
19170					}
19171				},
19172				{
19173					label: 'Sent, Ack & Archived',
19174					data: [
19175						push.archived,
19176						account.archived
19177					],
19178					backgroundColor: 'rgb(2,2,2)',
19179					datalabels: {
19180						color: '#ffffff',
19181						formatter: labelFormatter
19182					}
19183				},
19184			]
19185
19186			// Notifications tab
19187			createChart(tabsContentId, 0, {
19188				type: 'bar',
19189				responsive: true,
19190				plugins: [ChartDataLabels],
19191				options: {
19192					scales: {
19193						x: {
19194							stacked: true,
19195						},
19196						y: {
19197							stacked: true
19198						}
19199					}
19200				},
19201				data: {
19202					labels: [
19203						'Individual push notifications',
19204						'Account level notifications'
19205					],
19206					datasets: datasets
19207				},
19208			})
19209		});
19210	}
19211
19212	var dialogOptions = {
19213		autoOpen: false,
19214		width: 700,
19215		height: 850,
19216		modal: true,
19217		buttons: {
19218		},
19219		// Done after drawing the canvas
19220		open: function(event, ui) {
19221			dialog.dialog( 'option', 'position', { my: 'center', at: 'center', of: window, collision: 'fit' });
19222		},
19223		close: function(event, ui) {
19224			$(tabsId).tabs('destroy')
19225		}
19226	};
19227
19228	// Initializes the dialog
19229	dialog = $('#ud-comms-delivery-dialog').dialog(dialogOptions);
19230
19231	return { modal: dialog, setFields: setFields };
19232};
19233
19234
19235// eisy Provisioning Dialog - AWS IoT connectivity
19236
19237// iotProvisioned: isydata.isy.iot.provisioned,
19238// iotProvisioningAvailable: isydata.isy.iot.provisioningAvailable,
19239// iotOnline: isydata.iotOnline,
19240// iotOnlineLastModified: isydata.iotOnlineLastModified
19241
19242// PG3 remote connection
19243var eisyProvisioningDialogInit = function () {
19244	var dialog, uuid, domain
19245
19246	var provisionButton = {
19247		text: 'Configure',
19248		click: provision
19249	}
19250
19251	var reprovisionButton = {
19252		text: 'Reconfigure',
19253		click: provision
19254	}
19255
19256	var deprovisionButton = {
19257		text: 'Remove configuration',
19258		click: deprovision
19259	}
19260
19261	var closeButton = {
19262		text: 'Close',
19263		click: function() {
19264			$(this).dialog('close');
19265		}
19266	}
19267
19268	var refreshButton = {
19269		text: 'Refresh',
19270		click: refresh
19271	}
19272
19273	var helpButton = {
19274		text: 'Help',
19275		click: help
19276	}
19277
19278	function help() {
19279		informationMessageDialog(
19280			'<div class="online-help"><p>PG3 remote connection establish a permanent MQTT connection with UDI. ' +
19281			'This is particularly useful if you want to use UD Mobile remotely, and for nodeservers using webhooks 
19281such as Ring.</p>' +
19282			'Connection status:<ul>' +
19283			'<li><b>Remote connection feature available:</b> Tells you if your device supports remote connections.</li>' +
19284			'<li><b>Remote connection configured:</b> Tells you if your device is configured to connect to UDI.</li>' +
19285			'<li><b>Remote connection active:</b> Tells you if your device is actually connected to UDI.</li>' +
19286			'</ul>',
19287
19288			'PG3 remote connection help', 820)
19289	}
19290
19291
19292	var buttonsProvisioned = [ reprovisionButton, deprovisionButton, refreshButton, helpButton, closeButton ]
19293	var buttonsNotProvisioned = [ provisionButton, refreshButton, helpButton, closeButton ]
19294	var buttonsProvisioningNotAvailable = [ refreshButton, helpButton, closeButton ] // Should not be displayed as the option is hidden
19295
19296	function setButtons(obj) {
19297		dialog.dialog('option', 'buttons', obj.iotProvisioningAvailable ?
19298			(obj.iotProvisioned ? buttonsProvisioned : buttonsNotProvisioned) :
19299			buttonsProvisioningNotAvailable);
19300	}
19301
19302
19303	function displayStatus(obj) {
19304		if (obj) {
19305			$('#eisy_provisioning_available').text(obj.iotProvisioningAvailable ? 'Yes' : 'No' ).hide().show('fade')
19306			$('#eisy_provisioning_provisioned').text(obj.iotProvisioned ? 'Yes' : 'No' ).hide().show('fade')
19307			$('#eisy_provisioning_online').text(obj.iotOnline ? 'Yes' : 'No' ).hide().show('fade')
19308		}
19309	}
19310
19311	function setFields(obj) {
19312		uuid=obj.uuid
19313		domain=obj.domain
19314
19315		$('#eisy_provisioning_uuid').text(obj.uuid);
19316		$('#eisy_provisioning_alias').text(obj.alias);
19317		displayStatus(obj)
19318		setButtons(obj)
19319		refresh()
19320	}
19321
19322	function refresh() {
19323		ajaxJsonGet('/api/eisy/iot/provisioning', $.param({ uuid: uuid, domain: domain }))
19324		.then(function(data, status, jqXHR) {
19325			displayStatus(data.data)
19326			setButtons(data.data) // It may change based on status!
19327		})
19328		.done()
19329	}
19330
19331	function provision() {
19332		ajaxJsonPost('/api/eisy/iot/provision', $.param({ uuid: uuid, domain: domain }))
19333		.then(function(data, status, jqXHR) {
19334			var message, title
19335			if (data.successful) {
19336				message = 'Request to configure was successful.<br/><br/>Please wait a few minutes while we reconfigure your system.'
19337				title = 'Successful'
19338			} else {
19339				message = 'Configuration failed: ' + data.errorMessage
19340				title = 'Configuration failed'
19341			}
19342
19343			informationMessageDialog(message, title)
19344			.then(function () {
19345				refresh()
19346			})
19347			.done()
19348		})
19349		.done();
19350	}
19351
19352	function deprovision() {
19353		ajaxJsonDelete('/api/eisy/iot/provision', $.param({ uuid: uuid, domain: domain }))
19354		.then(function(data, status, jqXHR) {
19355			var message, title
19356			if (data.successful) {
19357				message = 'Removal of configuration was successful.<br/><br/>Please wait a few minutes while we reconfigure your system.'
19358				title = 'Successful'
19359			} else {
19360				message = 'Removal of configuration failed: ' + data.errorMessage
19361				title = 'Configuration failed'
19362			}
19363
19364			informationMessageDialog(message, title)
19365			.then(function () {
19366				refresh()
19367			})
19368			.done()
19369		})
19370		.done();
19371	}
19372
19373	var isyOptions = {
19374		autoOpen: false,
19375		width: 700,
19376		modal: true,
19377		// Buttons are set during setFields
19378		open: function(event, ui) {
19379			dialog.dialog( "option", "position", { my: "center", at: "center", of: window, collision: "fit" });
19380		}
19381	};
19382
19383	// Initializes the dialog
19384	dialog = $('#eisy_provisioning_dialog').dialog(isyOptions);
19385
19386	return { modal: dialog, setFields: setFields };
19387};
19388
19389// ISY CSV Import
19390var isyImportDialogInit = function () {
19391	var dialog,
19392		uploader,
19393		domain,
19394		$uploadButton;
19395
19396	function resetUI() {
19397		$('#isy-import-upload_filename').text(' ');
19398		$uploadButton.hide();
19399	}
19400
19401	function setFields(obj) {
19402		domain = obj.domain;
19403		uploader.setData({ domain });
19404		uploader.setOptions({ customHeaders: { Authorization: 'Basic ' + registeredUser.basicAuth }});
19405	}
19406
19407	function help() {
19408		informationMessageDialog(
19409			'<div class="online-help"><p>The ISY import allows you to import a list of uuids in the account using a CSV file.<br/><br/> ' +
19410			'You can click on "Choose a file" to select a file, or drag a file to the input field. Once the file is selected, click on Import CSV.<br/><br/>' +
19411			'The CSV must have a header with the following columns: uuid, alias',
19412			'ISY Import Help', 820)
19413	}
19414
19415	var isyOptions = {
19416		autoOpen: false,
19417		width: 450,
19418		modal: true,
19419		buttons: {
19420			'Choose a file': function () {},
19421			'Import CSV': function () {
19422				$uploadButton.blur();
19423				uploader.submit();
19424			},
19425			'Help': function() {
19426				help();
19427			},
19428		},
19429		open: function(event, ui) {
19430			resetUI();
19431			dialog.dialog('option', 'position', { my: 'center', at: 'center', of: window, collision: 'fit'});
19432		}
19433	};
19434
19435	// Initializes the dialog
19436	dialog = $('#isy-import-dialog').dialog(isyOptions);
19437
19438	// Set id on first button to allow uploader to hook to it
19439	var $buttons = $('#isy-import-dialog').parent().find('.ui-dialog-buttonset button');
19440	var buttonId = 'isyimport_upload'
19441	$buttons.first().attr('id', buttonId);
19442
19443	$uploadButton = $buttons.eq(1); // Second button
19444
19445	uploader = new ss.SimpleUpload({
19446		button: buttonId, // file upload button
19447		url: '/api/isy/import', // server side handler
19448		name: 'file', // upload parameter name
19449		autoSubmit: false,
19450		maxSize: 80000,
19451		multipart: true,
19452//		responseType: 'json',
19453		dragClass: 'ui-state-hover',
19454		hoverClass: 'ui-state-hover',
19455		focusClass: 'ui-state-focus',
19456		disabledClass: 'ui-state-disabled',
19457		allowedExtensions: ['csv'],
19458		dropzone: 'isy-import-upload_filename',
19459		onChange: function(filename, extension) {
19460			$('#isy-import-upload_filename').text(filename);
19461			$uploadButton.show();
19462		},
19463		onExtError: function(filename, extension) {
19464			errorMessageDialog('File must be a CSV with extension .csv', 'Error')
19465		},
19466		onSizeError: function(filename, size) {
19467			errorMessageDialog('File is too large', 'Error')
19468		},
19469		onError: function(filename, errorType, status, statusText, response, uploadBtn) {
19470			var message
19471
19472			if (status === 426) {
19473				message = 'Error: ' + JSON.parse(response).message
19474			} else {
19475				message = 'Error: ' + response
19476			}
19477
19478			errorMessageDialog(message, 'Error')
19479			.then(function () {
19480				resetUI();
19481			})
19482		},
19483		onComplete: function(filename, response) {
19484			const { total, imported, licensed } = JSON.parse(response)
19485
19486			informationMessageDialog(`CSV processed successfully.<br/><br/>` +
19487				`${imported}/${total} ISYs were imported in the account.<br/><br/>` +
19488				`${licensed}/${total} ISYs were automatically licensed.`,
19489				'CSV processed', 600)
19490			.then(function () {
19491				tableIsys.reload();
19492				dialog.dialog('close');
19493			})
19494		}
19495	});
19496
19497	return { modal: dialog, setFields: setFields };
19498};
19499
19500

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.